vidpipe 1.3.13 → 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -446,6 +446,12 @@ function saveGlobalConfig(config2) {
446
446
  chmodSync(configPath, 384);
447
447
  }
448
448
  }
449
+ function getGlobalConfigValue(section, key) {
450
+ const config2 = loadGlobalConfig();
451
+ const sectionValues = config2[section];
452
+ const value = sectionValues[key];
453
+ return typeof value === "string" ? value : void 0;
454
+ }
449
455
  function setGlobalConfigValue(section, key, value) {
450
456
  const config2 = loadGlobalConfig();
451
457
  const sectionValues = config2[section];
@@ -4661,6 +4667,150 @@ var require_node = __commonJS({
4661
4667
  }
4662
4668
  });
4663
4669
 
4670
+ // src/L3-services/postStore/postStore.ts
4671
+ function getQueueDir() {
4672
+ const { OUTPUT_DIR } = getConfig();
4673
+ return join(OUTPUT_DIR, "publish-queue");
4674
+ }
4675
+ function getPublishedDir() {
4676
+ const { OUTPUT_DIR } = getConfig();
4677
+ return join(OUTPUT_DIR, "published");
4678
+ }
4679
+ async function readQueueItem(folderPath, id) {
4680
+ const metadataPath = join(folderPath, "metadata.json");
4681
+ const postPath = join(folderPath, "post.md");
4682
+ try {
4683
+ const metadataRaw = await readTextFile(metadataPath);
4684
+ const metadata = JSON.parse(metadataRaw);
4685
+ let postContent = "";
4686
+ try {
4687
+ postContent = await readTextFile(postPath);
4688
+ } catch {
4689
+ logger_default.debug(`No post.md found for ${String(id).replace(/[\r\n]/g, "")}`);
4690
+ }
4691
+ const videoPath = join(folderPath, "media.mp4");
4692
+ const imagePath = join(folderPath, "media.png");
4693
+ let mediaPath = null;
4694
+ let hasMedia = false;
4695
+ if (await fileExists(videoPath)) {
4696
+ mediaPath = videoPath;
4697
+ hasMedia = true;
4698
+ } else if (await fileExists(imagePath)) {
4699
+ mediaPath = imagePath;
4700
+ hasMedia = true;
4701
+ }
4702
+ return {
4703
+ id,
4704
+ metadata,
4705
+ postContent,
4706
+ hasMedia,
4707
+ mediaPath,
4708
+ folderPath
4709
+ };
4710
+ } catch (err) {
4711
+ logger_default.debug(`Failed to read queue item ${String(id).replace(/[\r\n]/g, "")}: ${String(err).replace(/[\r\n]/g, "")}`);
4712
+ return null;
4713
+ }
4714
+ }
4715
+ async function getPendingItems() {
4716
+ const queueDir = getQueueDir();
4717
+ await ensureDirectory(queueDir);
4718
+ let entries;
4719
+ try {
4720
+ const dirents = await listDirectoryWithTypes(queueDir);
4721
+ entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
4722
+ } catch {
4723
+ return [];
4724
+ }
4725
+ const items = [];
4726
+ for (const name of entries) {
4727
+ const item = await readQueueItem(join(queueDir, name), name);
4728
+ if (item) items.push(item);
4729
+ }
4730
+ items.sort((a, b) => {
4731
+ if (a.hasMedia !== b.hasMedia) return a.hasMedia ? -1 : 1;
4732
+ return a.metadata.createdAt.localeCompare(b.metadata.createdAt);
4733
+ });
4734
+ return items;
4735
+ }
4736
+ async function createItem(id, metadata, postContent, mediaSourcePath) {
4737
+ if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
4738
+ throw new Error(`Invalid ID format: ${id}`);
4739
+ }
4740
+ const folderPath = join(getQueueDir(), basename(id));
4741
+ await ensureDirectory(folderPath);
4742
+ await writeJsonFile(join(folderPath, "metadata.json"), metadata);
4743
+ await writeTextFile(join(folderPath, "post.md"), postContent);
4744
+ let hasMedia = false;
4745
+ const ext = mediaSourcePath ? extname(mediaSourcePath) : ".mp4";
4746
+ const mediaFilename = `media${ext}`;
4747
+ const mediaPath = join(folderPath, mediaFilename);
4748
+ if (mediaSourcePath) {
4749
+ await copyFile(mediaSourcePath, mediaPath);
4750
+ hasMedia = true;
4751
+ }
4752
+ logger_default.debug(`Created queue item: ${String(id).replace(/[\r\n]/g, "")}`);
4753
+ return {
4754
+ id,
4755
+ metadata,
4756
+ postContent,
4757
+ hasMedia,
4758
+ mediaPath: hasMedia ? mediaPath : null,
4759
+ folderPath
4760
+ };
4761
+ }
4762
+ async function getPublishedItems() {
4763
+ const publishedDir = getPublishedDir();
4764
+ await ensureDirectory(publishedDir);
4765
+ let entries;
4766
+ try {
4767
+ const dirents = await listDirectoryWithTypes(publishedDir);
4768
+ entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
4769
+ } catch {
4770
+ return [];
4771
+ }
4772
+ const items = [];
4773
+ for (const name of entries) {
4774
+ const item = await readQueueItem(join(publishedDir, name), name);
4775
+ if (item) items.push(item);
4776
+ }
4777
+ items.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
4778
+ return items;
4779
+ }
4780
+ async function getScheduledItemsByIdeaIds(ideaIds) {
4781
+ if (ideaIds.length === 0) return [];
4782
+ const ideaIdSet = new Set(ideaIds);
4783
+ const [pendingItems, publishedItems] = await Promise.all([
4784
+ getPendingItems(),
4785
+ getPublishedItems()
4786
+ ]);
4787
+ return [...pendingItems, ...publishedItems].filter(
4788
+ (item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
4789
+ );
4790
+ }
4791
+ async function itemExists(id) {
4792
+ if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
4793
+ throw new Error(`Invalid ID format: ${id}`);
4794
+ }
4795
+ if (await fileExists(join(getQueueDir(), basename(id)))) {
4796
+ return "pending";
4797
+ }
4798
+ if (await fileExists(join(getPublishedDir(), basename(id)))) {
4799
+ return "published";
4800
+ }
4801
+ return null;
4802
+ }
4803
+ var init_postStore = __esm({
4804
+ "src/L3-services/postStore/postStore.ts"() {
4805
+ "use strict";
4806
+ init_types();
4807
+ init_environment();
4808
+ init_configLogger();
4809
+ init_fileSystem();
4810
+ init_paths();
4811
+ }
4812
+ });
4813
+
4664
4814
  // src/L7-app/sdk/VidPipeSDK.ts
4665
4815
  init_types();
4666
4816
  init_environment();
@@ -7403,16 +7553,10 @@ var LateApiClient = class {
7403
7553
  return data.accounts ?? [];
7404
7554
  }
7405
7555
  async getScheduledPosts(platform) {
7406
- const params = new URLSearchParams({ status: "scheduled" });
7407
- if (platform) params.set("platform", platform);
7408
- const data = await this.request(`/posts?${params}`);
7409
- return data.posts ?? [];
7556
+ return this.listPosts({ status: "scheduled", platform });
7410
7557
  }
7411
7558
  async getDraftPosts(platform) {
7412
- const params = new URLSearchParams({ status: "draft" });
7413
- if (platform) params.set("platform", platform);
7414
- const data = await this.request(`/posts?${params}`);
7415
- return data.posts ?? [];
7559
+ return this.listPosts({ status: "draft", platform });
7416
7560
  }
7417
7561
  async createPost(params) {
7418
7562
  const data = await this.request("/posts", {
@@ -7518,6 +7662,7 @@ function createLateApiClient(...args) {
7518
7662
  // src/L2-clients/scheduleStore/scheduleStore.ts
7519
7663
  init_fileSystem();
7520
7664
  init_paths();
7665
+ init_globalConfig();
7521
7666
  async function readScheduleFile(filePath) {
7522
7667
  return readTextFile(filePath);
7523
7668
  }
@@ -7529,7 +7674,10 @@ async function writeScheduleFile(filePath, content) {
7529
7674
  });
7530
7675
  }
7531
7676
  function resolveSchedulePath(configPath) {
7532
- return configPath ?? join(process.cwd(), "schedule.json");
7677
+ if (configPath) return configPath;
7678
+ const globalPath = getGlobalConfigValue("defaults", "scheduleConfig");
7679
+ if (globalPath) return globalPath;
7680
+ return join(process.cwd(), "schedule.json");
7533
7681
  }
7534
7682
 
7535
7683
  // src/L3-services/scheduler/scheduleConfig.ts
@@ -7794,7 +7942,7 @@ function getPlatformSchedule(platform, clipType) {
7794
7942
  avoidDays: sub.avoidDays
7795
7943
  };
7796
7944
  }
7797
- if (clipType && schedule.slots.length === 0 && schedule.byClipType) {
7945
+ if (schedule.slots.length === 0 && schedule.byClipType) {
7798
7946
  const allSlots = [];
7799
7947
  const allAvoidDays = /* @__PURE__ */ new Set();
7800
7948
  for (const sub of Object.values(schedule.byClipType)) {
@@ -7815,194 +7963,342 @@ function getDisplacementConfig() {
7815
7963
  return cachedConfig?.displacement ?? { ...defaultDisplacement };
7816
7964
  }
7817
7965
 
7818
- // src/L3-services/postStore/postStore.ts
7819
- init_types();
7820
- init_environment();
7966
+ // src/L3-services/scheduler/realign.ts
7967
+ init_postStore();
7821
7968
  init_configLogger();
7822
- init_fileSystem();
7823
- init_paths();
7824
- function getQueueDir() {
7825
- const { OUTPUT_DIR } = getConfig();
7826
- return join(OUTPUT_DIR, "publish-queue");
7969
+
7970
+ // src/L3-services/scheduler/scheduler.ts
7971
+ init_configLogger();
7972
+ init_postStore();
7973
+ var MAX_LOOKAHEAD_DAYS = 730;
7974
+ var DAY_MS = 24 * 60 * 60 * 1e3;
7975
+ var HOUR_MS = 60 * 60 * 1e3;
7976
+ function normalizeDateTime(isoString) {
7977
+ return new Date(isoString).getTime();
7827
7978
  }
7828
- function getPublishedDir() {
7829
- const { OUTPUT_DIR } = getConfig();
7830
- return join(OUTPUT_DIR, "published");
7979
+ function sanitizeLogValue(value) {
7980
+ return value.replace(/[\r\n]/g, "");
7831
7981
  }
7832
- async function readQueueItem(folderPath, id) {
7833
- const metadataPath = join(folderPath, "metadata.json");
7834
- const postPath = join(folderPath, "post.md");
7982
+ function getTimezoneOffset(timezone, date) {
7983
+ const formatter = new Intl.DateTimeFormat("en-US", {
7984
+ timeZone: timezone,
7985
+ timeZoneName: "longOffset"
7986
+ });
7987
+ const parts = formatter.formatToParts(date);
7988
+ const tzPart = parts.find((part) => part.type === "timeZoneName");
7989
+ const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
7990
+ if (match) return match[1];
7991
+ if (tzPart?.value === "GMT") return "+00:00";
7992
+ logger_default.warn(
7993
+ `Could not parse timezone offset for timezone "${timezone}" on date "${date.toISOString()}". Raw timeZoneName part: "${tzPart?.value ?? "undefined"}". Falling back to UTC (+00:00).`
7994
+ );
7995
+ return "+00:00";
7996
+ }
7997
+ function buildSlotDatetime(date, time, timezone) {
7998
+ const formatter = new Intl.DateTimeFormat("en-US", {
7999
+ timeZone: timezone,
8000
+ year: "numeric",
8001
+ month: "2-digit",
8002
+ day: "2-digit"
8003
+ });
8004
+ const parts = formatter.formatToParts(date);
8005
+ const yearPart = parts.find((part) => part.type === "year")?.value;
8006
+ const monthPart = parts.find((part) => part.type === "month")?.value;
8007
+ const dayPart = parts.find((part) => part.type === "day")?.value;
8008
+ const year = yearPart ?? String(date.getFullYear());
8009
+ const month = (monthPart ?? String(date.getMonth() + 1)).padStart(2, "0");
8010
+ const day = (dayPart ?? String(date.getDate())).padStart(2, "0");
8011
+ const offset = getTimezoneOffset(timezone, date);
8012
+ return `${year}-${month}-${day}T${time}:00${offset}`;
8013
+ }
8014
+ function getDayOfWeekInTimezone(date, timezone) {
8015
+ const formatter = new Intl.DateTimeFormat("en-US", {
8016
+ timeZone: timezone,
8017
+ weekday: "short"
8018
+ });
8019
+ const short = formatter.format(date).toLowerCase().slice(0, 3);
8020
+ const map = {
8021
+ sun: "sun",
8022
+ mon: "mon",
8023
+ tue: "tue",
8024
+ wed: "wed",
8025
+ thu: "thu",
8026
+ fri: "fri",
8027
+ sat: "sat"
8028
+ };
8029
+ return map[short] ?? "mon";
8030
+ }
8031
+ async function fetchScheduledPostsSafe(platform) {
7835
8032
  try {
7836
- const metadataRaw = await readTextFile(metadataPath);
7837
- const metadata = JSON.parse(metadataRaw);
7838
- let postContent = "";
7839
- try {
7840
- postContent = await readTextFile(postPath);
7841
- } catch {
7842
- logger_default.debug(`No post.md found for ${String(id).replace(/[\r\n]/g, "")}`);
7843
- }
7844
- const videoPath = join(folderPath, "media.mp4");
7845
- const imagePath = join(folderPath, "media.png");
7846
- let mediaPath = null;
7847
- let hasMedia = false;
7848
- if (await fileExists(videoPath)) {
7849
- mediaPath = videoPath;
7850
- hasMedia = true;
7851
- } else if (await fileExists(imagePath)) {
7852
- mediaPath = imagePath;
7853
- hasMedia = true;
7854
- }
7855
- return {
7856
- id,
7857
- metadata,
7858
- postContent,
7859
- hasMedia,
7860
- mediaPath,
7861
- folderPath
7862
- };
8033
+ const client = new LateApiClient();
8034
+ return await client.getScheduledPosts(platform);
7863
8035
  } catch (err) {
7864
- logger_default.debug(`Failed to read queue item ${String(id).replace(/[\r\n]/g, "")}: ${String(err).replace(/[\r\n]/g, "")}`);
7865
- return null;
8036
+ const msg = err instanceof Error ? err.message : String(err);
8037
+ logger_default.warn(`Late API unreachable, using local data only: ${msg}`);
8038
+ return [];
7866
8039
  }
7867
8040
  }
7868
- async function getPendingItems() {
7869
- const queueDir = getQueueDir();
7870
- await ensureDirectory(queueDir);
7871
- let entries;
7872
- try {
7873
- const dirents = await listDirectoryWithTypes(queueDir);
7874
- entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
7875
- } catch {
7876
- return [];
7877
- }
7878
- const items = [];
7879
- for (const name of entries) {
7880
- const item = await readQueueItem(join(queueDir, name), name);
7881
- if (item) items.push(item);
8041
+ async function buildBookedMap(platform) {
8042
+ const [latePosts, publishedItems] = await Promise.all([
8043
+ fetchScheduledPostsSafe(platform),
8044
+ getPublishedItems()
8045
+ ]);
8046
+ const ideaLinkedPostIds = /* @__PURE__ */ new Set();
8047
+ for (const item of publishedItems) {
8048
+ if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
8049
+ ideaLinkedPostIds.add(item.metadata.latePostId);
8050
+ }
7882
8051
  }
7883
- items.sort((a, b) => {
7884
- if (a.hasMedia !== b.hasMedia) return a.hasMedia ? -1 : 1;
7885
- return a.metadata.createdAt.localeCompare(b.metadata.createdAt);
7886
- });
7887
- return items;
7888
- }
7889
- async function createItem(id, metadata, postContent, mediaSourcePath) {
7890
- if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
7891
- throw new Error(`Invalid ID format: ${id}`);
8052
+ const map = /* @__PURE__ */ new Map();
8053
+ for (const post of latePosts) {
8054
+ if (!post.scheduledFor) continue;
8055
+ for (const scheduledPlatform of post.platforms) {
8056
+ if (!platform || scheduledPlatform.platform === platform) {
8057
+ const ms = normalizeDateTime(post.scheduledFor);
8058
+ map.set(ms, {
8059
+ scheduledFor: post.scheduledFor,
8060
+ source: "late",
8061
+ postId: post._id,
8062
+ platform: scheduledPlatform.platform,
8063
+ status: post.status,
8064
+ ideaLinked: ideaLinkedPostIds.has(post._id)
8065
+ });
8066
+ }
8067
+ }
7892
8068
  }
7893
- const folderPath = join(getQueueDir(), basename(id));
7894
- await ensureDirectory(folderPath);
7895
- await writeJsonFile(join(folderPath, "metadata.json"), metadata);
7896
- await writeTextFile(join(folderPath, "post.md"), postContent);
7897
- let hasMedia = false;
7898
- const ext = mediaSourcePath ? extname(mediaSourcePath) : ".mp4";
7899
- const mediaFilename = `media${ext}`;
7900
- const mediaPath = join(folderPath, mediaFilename);
7901
- if (mediaSourcePath) {
7902
- await copyFile(mediaSourcePath, mediaPath);
7903
- hasMedia = true;
8069
+ for (const item of publishedItems) {
8070
+ if (platform && item.metadata.platform !== platform) continue;
8071
+ if (!item.metadata.scheduledFor) continue;
8072
+ const ms = normalizeDateTime(item.metadata.scheduledFor);
8073
+ if (!map.has(ms)) {
8074
+ map.set(ms, {
8075
+ scheduledFor: item.metadata.scheduledFor,
8076
+ source: "local",
8077
+ itemId: item.id,
8078
+ platform: item.metadata.platform,
8079
+ ideaLinked: Boolean(item.metadata.ideaIds?.length)
8080
+ });
8081
+ }
7904
8082
  }
7905
- logger_default.debug(`Created queue item: ${String(id).replace(/[\r\n]/g, "")}`);
7906
- return {
7907
- id,
7908
- metadata,
7909
- postContent,
7910
- hasMedia,
7911
- mediaPath: hasMedia ? mediaPath : null,
7912
- folderPath
7913
- };
8083
+ return map;
7914
8084
  }
7915
- async function getPublishedItems() {
7916
- const publishedDir = getPublishedDir();
7917
- await ensureDirectory(publishedDir);
7918
- let entries;
7919
- try {
7920
- const dirents = await listDirectoryWithTypes(publishedDir);
7921
- entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
7922
- } catch {
7923
- return [];
8085
+ async function getIdeaLinkedLatePostIds() {
8086
+ const publishedItems = await getPublishedItems();
8087
+ const ids = /* @__PURE__ */ new Set();
8088
+ for (const item of publishedItems) {
8089
+ if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
8090
+ ids.add(item.metadata.latePostId);
8091
+ }
7924
8092
  }
7925
- const items = [];
7926
- for (const name of entries) {
7927
- const item = await readQueueItem(join(publishedDir, name), name);
7928
- if (item) items.push(item);
8093
+ return ids;
8094
+ }
8095
+ function* generateTimeslots(platformConfig, timezone, fromMs, maxMs) {
8096
+ const baseDate = new Date(fromMs);
8097
+ const upperMs = maxMs ?? fromMs + MAX_LOOKAHEAD_DAYS * DAY_MS;
8098
+ for (let dayOffset = 0; dayOffset <= MAX_LOOKAHEAD_DAYS; dayOffset++) {
8099
+ const day = new Date(baseDate);
8100
+ day.setDate(day.getDate() + dayOffset);
8101
+ const dayOfWeek = getDayOfWeekInTimezone(day, timezone);
8102
+ if (platformConfig.avoidDays.includes(dayOfWeek)) continue;
8103
+ const dayCandidates = [];
8104
+ for (const slot of platformConfig.slots) {
8105
+ if (!slot.days.includes(dayOfWeek)) continue;
8106
+ const datetime = buildSlotDatetime(day, slot.time, timezone);
8107
+ const ms = normalizeDateTime(datetime);
8108
+ if (ms <= fromMs) continue;
8109
+ if (ms > upperMs) continue;
8110
+ dayCandidates.push({ datetime, ms });
8111
+ }
8112
+ dayCandidates.sort((a, b) => a.ms - b.ms);
8113
+ for (const candidate of dayCandidates) yield candidate;
8114
+ if (dayCandidates.length === 0) {
8115
+ const dayStartMs = normalizeDateTime(buildSlotDatetime(day, "00:00", timezone));
8116
+ if (dayStartMs > upperMs) break;
8117
+ }
7929
8118
  }
7930
- items.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
7931
- return items;
7932
8119
  }
7933
- async function getScheduledItemsByIdeaIds(ideaIds) {
7934
- if (ideaIds.length === 0) return [];
7935
- const ideaIdSet = new Set(ideaIds);
7936
- const [pendingItems, publishedItems] = await Promise.all([
7937
- getPendingItems(),
7938
- getPublishedItems()
7939
- ]);
7940
- return [...pendingItems, ...publishedItems].filter(
7941
- (item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
7942
- );
8120
+ function passesIdeaSpacing(candidateMs, candidatePlatform, ideaRefs, samePlatformMs, crossPlatformMs) {
8121
+ for (const ref of ideaRefs) {
8122
+ const diff = Math.abs(candidateMs - ref.scheduledForMs);
8123
+ if (ref.platform === candidatePlatform && diff < samePlatformMs) return false;
8124
+ if (diff < crossPlatformMs) return false;
8125
+ }
8126
+ return true;
7943
8127
  }
7944
- async function getPublishedItemByLatePostId(latePostId) {
7945
- const publishedItems = await getPublishedItems();
7946
- return publishedItems.find((item) => item.metadata.latePostId === latePostId) ?? null;
8128
+ async function getIdeaReferences(ideaIds, bookedMap) {
8129
+ const sameIdeaPosts = await getScheduledItemsByIdeaIds(ideaIds);
8130
+ const lateSlotsByPostId = /* @__PURE__ */ new Map();
8131
+ const localSlotsByItemId = /* @__PURE__ */ new Map();
8132
+ for (const slot of bookedMap.values()) {
8133
+ if (slot.postId) {
8134
+ const arr = lateSlotsByPostId.get(slot.postId) ?? [];
8135
+ arr.push(slot);
8136
+ lateSlotsByPostId.set(slot.postId, arr);
8137
+ }
8138
+ if (slot.itemId) {
8139
+ const arr = localSlotsByItemId.get(slot.itemId) ?? [];
8140
+ arr.push(slot);
8141
+ localSlotsByItemId.set(slot.itemId, arr);
8142
+ }
8143
+ }
8144
+ const refs = [];
8145
+ const seen = /* @__PURE__ */ new Set();
8146
+ const addRef = (platform, scheduledFor) => {
8147
+ if (!scheduledFor) return;
8148
+ const key = `${platform}@${scheduledFor}`;
8149
+ if (seen.has(key)) return;
8150
+ seen.add(key);
8151
+ refs.push({ platform, scheduledForMs: normalizeDateTime(scheduledFor) });
8152
+ };
8153
+ for (const item of sameIdeaPosts) {
8154
+ addRef(item.metadata.platform, item.metadata.scheduledFor);
8155
+ if (item.metadata.latePostId) {
8156
+ for (const slot of lateSlotsByPostId.get(item.metadata.latePostId) ?? []) {
8157
+ addRef(slot.platform, slot.scheduledFor);
8158
+ }
8159
+ }
8160
+ for (const slot of localSlotsByItemId.get(item.id) ?? []) {
8161
+ addRef(slot.platform, slot.scheduledFor);
8162
+ }
8163
+ }
8164
+ return refs;
8165
+ }
8166
+ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
8167
+ const indent = " ".repeat(ctx.depth);
8168
+ let checked = 0;
8169
+ let skippedBooked = 0;
8170
+ let skippedSpacing = 0;
8171
+ logger_default.debug(`${indent}[schedulePost] Looking for slot for ${label} (idea=${isIdeaPost}) from ${new Date(fromMs).toISOString()}`);
8172
+ for (const { datetime, ms } of generateTimeslots(platformConfig, ctx.timezone, fromMs)) {
8173
+ checked++;
8174
+ const booked = ctx.bookedMap.get(ms);
8175
+ if (!booked) {
8176
+ if (isIdeaPost && ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
8177
+ skippedSpacing++;
8178
+ if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
8179
+ logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping`);
8180
+ }
8181
+ continue;
8182
+ }
8183
+ logger_default.debug(`${indent}[schedulePost] \u2705 Found empty slot: ${datetime} (checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing)`);
8184
+ return datetime;
8185
+ }
8186
+ if (isIdeaPost && ctx.displacementEnabled && !booked.ideaLinked && booked.source === "late" && booked.postId) {
8187
+ if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
8188
+ skippedSpacing++;
8189
+ if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
8190
+ logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping (even though displaceable)`);
8191
+ }
8192
+ continue;
8193
+ }
8194
+ logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by non-idea post ${booked.postId} \u2014 displacing`);
8195
+ const newHome = await schedulePost(
8196
+ platformConfig,
8197
+ ms,
8198
+ false,
8199
+ `displaced:${booked.postId}`,
8200
+ { ...ctx, depth: ctx.depth + 1 }
8201
+ );
8202
+ if (newHome) {
8203
+ if (!ctx.dryRun) {
8204
+ try {
8205
+ await ctx.lateClient.schedulePost(booked.postId, newHome);
8206
+ } catch (err) {
8207
+ const msg = err instanceof Error ? err.message : String(err);
8208
+ logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Failed to displace ${booked.postId} via Late API: ${msg} \u2014 skipping slot`);
8209
+ continue;
8210
+ }
8211
+ }
8212
+ logger_default.info(`${indent}[schedulePost] \u{1F4E6} Displaced ${booked.postId}: ${datetime} \u2192 ${newHome}`);
8213
+ ctx.bookedMap.delete(ms);
8214
+ const newMs = normalizeDateTime(newHome);
8215
+ ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
8216
+ logger_default.debug(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked} candidates)`);
8217
+ return datetime;
8218
+ }
8219
+ logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace ${booked.postId} \u2014 no empty slot found after ${datetime}`);
8220
+ }
8221
+ if (booked.ideaLinked) {
8222
+ skippedBooked++;
8223
+ if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
8224
+ logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken by idea post ${booked.postId ?? booked.itemId} \u2014 skipping`);
8225
+ }
8226
+ continue;
8227
+ }
8228
+ skippedBooked++;
8229
+ if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
8230
+ logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken (${booked.source}/${booked.postId ?? booked.itemId}) \u2014 skipping`);
8231
+ }
8232
+ }
8233
+ logger_default.warn(`[schedulePost] \u274C No slot found for ${label} \u2014 checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing`);
8234
+ return null;
7947
8235
  }
7948
- async function itemExists(id) {
7949
- if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
7950
- throw new Error(`Invalid ID format: ${id}`);
8236
+ async function findNextSlot(platform, clipType, options) {
8237
+ const config2 = await loadScheduleConfig();
8238
+ const platformConfig = getPlatformSchedule(platform, clipType);
8239
+ if (!platformConfig) {
8240
+ logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
8241
+ return null;
7951
8242
  }
7952
- if (await fileExists(join(getQueueDir(), basename(id)))) {
7953
- return "pending";
8243
+ const { timezone } = config2;
8244
+ const nowMs = Date.now();
8245
+ const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
8246
+ const isIdeaAware = ideaIds.length > 0;
8247
+ const bookedMap = await buildBookedMap(platform);
8248
+ const ideaLinkedPostIds = await getIdeaLinkedLatePostIds();
8249
+ const label = `${platform}/${clipType ?? "default"}`;
8250
+ let ideaRefs = [];
8251
+ let samePlatformMs = 0;
8252
+ let crossPlatformMs = 0;
8253
+ if (isIdeaAware) {
8254
+ const allBookedMap = await buildBookedMap();
8255
+ ideaRefs = await getIdeaReferences(ideaIds, allBookedMap);
8256
+ const spacingConfig = getIdeaSpacingConfig();
8257
+ samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
8258
+ crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
8259
+ }
8260
+ logger_default.info(`[findNextSlot] Scheduling ${label} (idea=${isIdeaAware}, booked=${bookedMap.size} slots, spacingRefs=${ideaRefs.length})`);
8261
+ const ctx = {
8262
+ timezone,
8263
+ bookedMap,
8264
+ ideaLinkedPostIds,
8265
+ lateClient: new LateApiClient(),
8266
+ displacementEnabled: getDisplacementConfig().enabled,
8267
+ dryRun: false,
8268
+ depth: 0,
8269
+ ideaRefs,
8270
+ samePlatformMs,
8271
+ crossPlatformMs,
8272
+ platform
8273
+ };
8274
+ const result = await schedulePost(platformConfig, nowMs, isIdeaAware, label, ctx);
8275
+ if (!result) {
8276
+ logger_default.warn(`[findNextSlot] No available slot for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
7954
8277
  }
7955
- if (await fileExists(join(getPublishedDir(), basename(id)))) {
7956
- return "published";
8278
+ return result;
8279
+ }
8280
+ async function getScheduleCalendar(startDate, endDate) {
8281
+ const bookedMap = await buildBookedMap();
8282
+ let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
8283
+ platform: slot.platform,
8284
+ scheduledFor: slot.scheduledFor,
8285
+ source: slot.source,
8286
+ postId: slot.postId,
8287
+ itemId: slot.itemId
8288
+ }));
8289
+ if (startDate) {
8290
+ const startMs = startDate.getTime();
8291
+ filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) >= startMs);
7957
8292
  }
7958
- return null;
8293
+ if (endDate) {
8294
+ const endMs = endDate.getTime();
8295
+ filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) <= endMs);
8296
+ }
8297
+ filtered.sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
8298
+ return filtered;
7959
8299
  }
7960
8300
 
7961
8301
  // src/L3-services/scheduler/realign.ts
7962
- init_configLogger();
7963
- function getTimezoneOffset(timezone, date) {
7964
- const formatter = new Intl.DateTimeFormat("en-US", {
7965
- timeZone: timezone,
7966
- timeZoneName: "longOffset"
7967
- });
7968
- const parts = formatter.formatToParts(date);
7969
- const tzPart = parts.find((p) => p.type === "timeZoneName");
7970
- const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
7971
- if (match) return match[1];
7972
- if (tzPart?.value === "GMT") return "+00:00";
7973
- return "+00:00";
7974
- }
7975
- function buildSlotDatetime(date, time, timezone) {
7976
- const formatter = new Intl.DateTimeFormat("en-US", {
7977
- timeZone: timezone,
7978
- year: "numeric",
7979
- month: "2-digit",
7980
- day: "2-digit"
7981
- });
7982
- const parts = formatter.formatToParts(date);
7983
- const year = parts.find((p) => p.type === "year")?.value ?? String(date.getFullYear());
7984
- const month = (parts.find((p) => p.type === "month")?.value ?? String(date.getMonth() + 1)).padStart(2, "0");
7985
- const day = (parts.find((p) => p.type === "day")?.value ?? String(date.getDate())).padStart(2, "0");
7986
- const offset = getTimezoneOffset(timezone, date);
7987
- return `${year}-${month}-${day}T${time}:00${offset}`;
7988
- }
7989
- function getDayOfWeekInTimezone(date, timezone) {
7990
- const formatter = new Intl.DateTimeFormat("en-US", {
7991
- timeZone: timezone,
7992
- weekday: "short"
7993
- });
7994
- const short = formatter.format(date).toLowerCase().slice(0, 3);
7995
- const map = {
7996
- sun: "sun",
7997
- mon: "mon",
7998
- tue: "tue",
7999
- wed: "wed",
8000
- thu: "thu",
8001
- fri: "fri",
8002
- sat: "sat"
8003
- };
8004
- return map[short] ?? "mon";
8005
- }
8006
8302
  var PLATFORM_ALIASES2 = { twitter: "x" };
8007
8303
  function normalizeSchedulePlatform(platform) {
8008
8304
  return PLATFORM_ALIASES2[platform] ?? platform;
@@ -8035,33 +8331,22 @@ async function fetchAllPosts(client, statuses, platform) {
8035
8331
  }
8036
8332
  return allPosts;
8037
8333
  }
8038
- function generateSlots(platform, clipType, count, bookedMs, timezone) {
8039
- const schedule = getPlatformSchedule(platform, clipType);
8040
- if (!schedule || schedule.slots.length === 0) {
8041
- logger_default.warn(`No schedule slots for ${platform}/${clipType}`);
8042
- return [];
8043
- }
8044
- const available = [];
8045
- const now = /* @__PURE__ */ new Date();
8046
- const nowMs = now.getTime();
8047
- for (let dayOffset = 0; dayOffset < 730 && available.length < count; dayOffset++) {
8048
- const day = new Date(now);
8049
- day.setDate(day.getDate() + dayOffset);
8050
- const dayOfWeek = getDayOfWeekInTimezone(day, timezone);
8051
- if (schedule.avoidDays.includes(dayOfWeek)) continue;
8052
- for (const slot of schedule.slots) {
8053
- if (available.length >= count) break;
8054
- if (!slot.days.includes(dayOfWeek)) continue;
8055
- const iso = buildSlotDatetime(day, slot.time, timezone);
8056
- const ms = new Date(iso).getTime();
8057
- if (ms <= nowMs) continue;
8058
- if (!bookedMs.has(ms)) {
8059
- available.push(iso);
8060
- bookedMs.add(ms);
8061
- }
8062
- }
8063
- }
8064
- return available;
8334
+ function isOnValidSlot(iso, schedule, timezone) {
8335
+ if (schedule.slots.length === 0) return false;
8336
+ const date = new Date(iso);
8337
+ const dayOfWeek = getDayOfWeekInTimezone(date, timezone);
8338
+ if (schedule.avoidDays.includes(dayOfWeek)) return false;
8339
+ const timeFormatter = new Intl.DateTimeFormat("en-US", {
8340
+ timeZone: timezone,
8341
+ hour: "2-digit",
8342
+ minute: "2-digit",
8343
+ hour12: false
8344
+ });
8345
+ const timeParts = timeFormatter.formatToParts(date);
8346
+ const hour = timeParts.find((p) => p.type === "hour")?.value ?? "00";
8347
+ const minute = timeParts.find((p) => p.type === "minute")?.value ?? "00";
8348
+ const timeKey = `${hour}:${minute}`;
8349
+ return schedule.slots.some((slot) => slot.time === timeKey && slot.days.includes(dayOfWeek));
8065
8350
  }
8066
8351
  async function buildRealignPlan(options = {}) {
8067
8352
  const config2 = await loadScheduleConfig();
@@ -8073,9 +8358,8 @@ async function buildRealignPlan(options = {}) {
8073
8358
  return { posts: [], toCancel: [], skipped: 0, unmatched: 0, totalFetched: 0 };
8074
8359
  }
8075
8360
  const { byLatePostId, byContent } = options.clipTypeMaps ?? await buildClipTypeMaps();
8076
- const grouped = /* @__PURE__ */ new Map();
8077
8361
  let unmatched = 0;
8078
- let contentMatched = 0;
8362
+ const tagged = [];
8079
8363
  for (const post of allPosts) {
8080
8364
  const platform = post.platforms[0]?.platform;
8081
8365
  if (!platform) continue;
@@ -8083,74 +8367,90 @@ async function buildRealignPlan(options = {}) {
8083
8367
  if (!clipType && post.content) {
8084
8368
  const contentKey = `${platform}::${normalizeContent(post.content)}`;
8085
8369
  clipType = byContent.get(contentKey) ?? null;
8086
- if (clipType) contentMatched++;
8087
8370
  }
8088
8371
  if (!clipType) {
8089
8372
  clipType = "short";
8090
8373
  unmatched++;
8091
8374
  }
8092
- const key = `${platform}::${clipType}`;
8093
- if (!grouped.has(key)) grouped.set(key, []);
8094
- grouped.get(key).push({ post, platform, clipType });
8375
+ tagged.push({ post, platform, clipType });
8095
8376
  }
8096
- const bookedMs = /* @__PURE__ */ new Set();
8097
- if (contentMatched > 0) {
8098
- logger_default.info(`${contentMatched} post(s) matched by content fallback (no latePostId)`);
8377
+ const bookedMap = await buildBookedMap();
8378
+ const ctx = {
8379
+ timezone,
8380
+ bookedMap,
8381
+ ideaLinkedPostIds: /* @__PURE__ */ new Set(),
8382
+ lateClient: client,
8383
+ displacementEnabled: getDisplacementConfig().enabled,
8384
+ dryRun: true,
8385
+ depth: 0,
8386
+ ideaRefs: [],
8387
+ samePlatformMs: 0,
8388
+ crossPlatformMs: 0,
8389
+ platform: ""
8390
+ };
8391
+ for (const [, slot] of bookedMap) {
8392
+ if (slot.ideaLinked && slot.postId) {
8393
+ ctx.ideaLinkedPostIds.add(slot.postId);
8394
+ }
8099
8395
  }
8100
8396
  const result = [];
8101
8397
  const toCancel = [];
8102
8398
  let skipped = 0;
8103
- for (const [key, posts] of grouped) {
8104
- const [platform, clipType] = key.split("::");
8399
+ tagged.sort((a, b) => {
8400
+ const aIdea = ctx.ideaLinkedPostIds.has(a.post._id) ? 0 : 1;
8401
+ const bIdea = ctx.ideaLinkedPostIds.has(b.post._id) ? 0 : 1;
8402
+ return aIdea - bIdea;
8403
+ });
8404
+ const nowMs = Date.now();
8405
+ for (const { post, platform, clipType } of tagged) {
8105
8406
  const schedulePlatform = normalizeSchedulePlatform(platform);
8106
- const schedule = getPlatformSchedule(schedulePlatform, clipType);
8107
- const hasSlots = schedule && schedule.slots.length > 0;
8108
- if (!hasSlots) {
8109
- for (const { post, clipType: ct } of posts) {
8110
- if (post.status === "cancelled") continue;
8111
- toCancel.push({
8112
- post,
8113
- platform,
8114
- clipType: ct,
8115
- reason: `No schedule slots for ${schedulePlatform}/${clipType}`
8116
- });
8407
+ const platformConfig = getPlatformSchedule(schedulePlatform, clipType);
8408
+ if (!platformConfig || platformConfig.slots.length === 0) {
8409
+ if (post.status !== "cancelled") {
8410
+ toCancel.push({ post, platform, clipType, reason: `No schedule slots for ${schedulePlatform}/${clipType}` });
8117
8411
  }
8118
8412
  continue;
8119
8413
  }
8120
- posts.sort((a, b) => {
8121
- const aTime = a.post.scheduledFor ? new Date(a.post.scheduledFor).getTime() : Infinity;
8122
- const bTime = b.post.scheduledFor ? new Date(b.post.scheduledFor).getTime() : Infinity;
8123
- return aTime - bTime;
8124
- });
8125
- const slots = generateSlots(schedulePlatform, clipType, posts.length, bookedMs, timezone);
8126
- for (let i = 0; i < posts.length; i++) {
8127
- const { post } = posts[i];
8128
- const newSlot = slots[i];
8129
- if (!newSlot) {
8130
- if (post.status !== "cancelled") {
8131
- toCancel.push({
8132
- post,
8133
- platform,
8134
- clipType: posts[i].clipType,
8135
- reason: `No more available slots for ${schedulePlatform}/${clipType}`
8136
- });
8137
- }
8138
- continue;
8414
+ if (post.scheduledFor && post.status === "scheduled" && isOnValidSlot(post.scheduledFor, platformConfig, timezone)) {
8415
+ skipped++;
8416
+ continue;
8417
+ }
8418
+ if (post.scheduledFor) {
8419
+ const currentMs2 = new Date(post.scheduledFor).getTime();
8420
+ const currentBooked = bookedMap.get(currentMs2);
8421
+ if (currentBooked?.postId === post._id) {
8422
+ bookedMap.delete(currentMs2);
8139
8423
  }
8140
- const currentMs = post.scheduledFor ? new Date(post.scheduledFor).getTime() : 0;
8141
- const newMs = new Date(newSlot).getTime();
8142
- if (currentMs === newMs && post.status === "scheduled") {
8143
- skipped++;
8144
- continue;
8424
+ }
8425
+ const isIdea = ctx.ideaLinkedPostIds.has(post._id);
8426
+ const label = `${schedulePlatform}/${clipType}:${post._id.slice(-6)}`;
8427
+ const newSlot = await schedulePost(platformConfig, nowMs, isIdea, label, ctx);
8428
+ if (!newSlot) {
8429
+ if (post.status !== "cancelled") {
8430
+ toCancel.push({ post, platform, clipType, reason: `No available slot for ${schedulePlatform}/${clipType}` });
8145
8431
  }
8146
- result.push({
8147
- post,
8148
- platform,
8149
- clipType: posts[i].clipType,
8150
- oldScheduledFor: post.scheduledFor ?? null,
8151
- newScheduledFor: newSlot
8152
- });
8432
+ continue;
8433
+ }
8434
+ const newMs = new Date(newSlot).getTime();
8435
+ ctx.bookedMap.set(newMs, {
8436
+ scheduledFor: newSlot,
8437
+ source: "late",
8438
+ postId: post._id,
8439
+ platform: schedulePlatform,
8440
+ ideaLinked: isIdea
8441
+ });
8442
+ const currentMs = post.scheduledFor ? new Date(post.scheduledFor).getTime() : 0;
8443
+ if (currentMs === newMs && post.status === "scheduled") {
8444
+ skipped++;
8445
+ continue;
8153
8446
  }
8447
+ result.push({
8448
+ post,
8449
+ platform,
8450
+ clipType,
8451
+ oldScheduledFor: post.scheduledFor ?? null,
8452
+ newScheduledFor: newSlot
8453
+ });
8154
8454
  }
8155
8455
  result.sort((a, b) => new Date(a.newScheduledFor).getTime() - new Date(b.newScheduledFor).getTime());
8156
8456
  return { posts: result, toCancel, skipped, unmatched, totalFetched: allPosts.length };
@@ -8198,381 +8498,6 @@ async function executeRealignPlan(plan, onProgress) {
8198
8498
  return { updated, cancelled, failed, errors };
8199
8499
  }
8200
8500
 
8201
- // src/L3-services/scheduler/scheduler.ts
8202
- init_configLogger();
8203
- function normalizeDateTime(isoString) {
8204
- return new Date(isoString).getTime();
8205
- }
8206
- var CHUNK_DAYS = 14;
8207
- var MAX_LOOKAHEAD_DAYS = 730;
8208
- var DEFAULT_IDEA_WINDOW_DAYS = 14;
8209
- var DAY_MS = 24 * 60 * 60 * 1e3;
8210
- var HOUR_MS = 60 * 60 * 1e3;
8211
- function sanitizeLogValue(value) {
8212
- return value.replace(/[\r\n]/g, "");
8213
- }
8214
- function getTimezoneOffset2(timezone, date) {
8215
- const formatter = new Intl.DateTimeFormat("en-US", {
8216
- timeZone: timezone,
8217
- timeZoneName: "longOffset"
8218
- });
8219
- const parts = formatter.formatToParts(date);
8220
- const tzPart = parts.find((part) => part.type === "timeZoneName");
8221
- const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
8222
- if (match) return match[1];
8223
- if (tzPart?.value === "GMT") return "+00:00";
8224
- logger_default.warn(
8225
- `Could not parse timezone offset for timezone "${timezone}" on date "${date.toISOString()}". Raw timeZoneName part: "${tzPart?.value ?? "undefined"}". Falling back to UTC (+00:00).`
8226
- );
8227
- return "+00:00";
8228
- }
8229
- function buildSlotDatetime2(date, time, timezone) {
8230
- const formatter = new Intl.DateTimeFormat("en-US", {
8231
- timeZone: timezone,
8232
- year: "numeric",
8233
- month: "2-digit",
8234
- day: "2-digit"
8235
- });
8236
- const parts = formatter.formatToParts(date);
8237
- const yearPart = parts.find((part) => part.type === "year")?.value;
8238
- const monthPart = parts.find((part) => part.type === "month")?.value;
8239
- const dayPart = parts.find((part) => part.type === "day")?.value;
8240
- const year = yearPart ?? String(date.getFullYear());
8241
- const month = (monthPart ?? String(date.getMonth() + 1)).padStart(2, "0");
8242
- const day = (dayPart ?? String(date.getDate())).padStart(2, "0");
8243
- const offset = getTimezoneOffset2(timezone, date);
8244
- return `${year}-${month}-${day}T${time}:00${offset}`;
8245
- }
8246
- function getDayOfWeekInTimezone2(date, timezone) {
8247
- const formatter = new Intl.DateTimeFormat("en-US", {
8248
- timeZone: timezone,
8249
- weekday: "short"
8250
- });
8251
- const short = formatter.format(date).toLowerCase().slice(0, 3);
8252
- const map = {
8253
- sun: "sun",
8254
- mon: "mon",
8255
- tue: "tue",
8256
- wed: "wed",
8257
- thu: "thu",
8258
- fri: "fri",
8259
- sat: "sat"
8260
- };
8261
- return map[short] ?? "mon";
8262
- }
8263
- async function fetchScheduledPostsSafe(platform) {
8264
- try {
8265
- const client = new LateApiClient();
8266
- return await client.getScheduledPosts(platform);
8267
- } catch (err) {
8268
- const msg = err instanceof Error ? err.message : String(err);
8269
- logger_default.warn(`Late API unreachable, using local data only: ${msg}`);
8270
- return [];
8271
- }
8272
- }
8273
- async function buildBookedSlots(platform) {
8274
- const [latePosts, publishedItems] = await Promise.all([
8275
- fetchScheduledPostsSafe(platform),
8276
- getPublishedItems()
8277
- ]);
8278
- const slots = [];
8279
- for (const post of latePosts) {
8280
- if (!post.scheduledFor) continue;
8281
- for (const scheduledPlatform of post.platforms) {
8282
- if (!platform || scheduledPlatform.platform === platform) {
8283
- slots.push({
8284
- scheduledFor: post.scheduledFor,
8285
- source: "late",
8286
- postId: post._id,
8287
- platform: scheduledPlatform.platform,
8288
- status: post.status
8289
- });
8290
- }
8291
- }
8292
- }
8293
- for (const item of publishedItems) {
8294
- if (platform && item.metadata.platform !== platform) continue;
8295
- if (!item.metadata.scheduledFor) continue;
8296
- slots.push({
8297
- scheduledFor: item.metadata.scheduledFor,
8298
- source: "local",
8299
- itemId: item.id,
8300
- platform: item.metadata.platform
8301
- });
8302
- }
8303
- return slots;
8304
- }
8305
- function buildIdeaReferences(sameIdeaPosts, allBookedSlots) {
8306
- const lateSlotsByPostId = /* @__PURE__ */ new Map();
8307
- const localSlotsByItemId = /* @__PURE__ */ new Map();
8308
- for (const slot of allBookedSlots) {
8309
- if (slot.postId) {
8310
- const slots = lateSlotsByPostId.get(slot.postId) ?? [];
8311
- slots.push(slot);
8312
- lateSlotsByPostId.set(slot.postId, slots);
8313
- }
8314
- if (slot.itemId) {
8315
- const slots = localSlotsByItemId.get(slot.itemId) ?? [];
8316
- slots.push(slot);
8317
- localSlotsByItemId.set(slot.itemId, slots);
8318
- }
8319
- }
8320
- const references = [];
8321
- const seen = /* @__PURE__ */ new Set();
8322
- const addReference = (platformName, scheduledFor) => {
8323
- if (!scheduledFor) return;
8324
- const key = `${platformName}@${scheduledFor}`;
8325
- if (seen.has(key)) return;
8326
- seen.add(key);
8327
- references.push({ platform: platformName, scheduledFor });
8328
- };
8329
- for (const item of sameIdeaPosts) {
8330
- addReference(item.metadata.platform, item.metadata.scheduledFor);
8331
- if (item.metadata.latePostId) {
8332
- for (const slot of lateSlotsByPostId.get(item.metadata.latePostId) ?? []) {
8333
- addReference(slot.platform, slot.scheduledFor);
8334
- }
8335
- }
8336
- for (const slot of localSlotsByItemId.get(item.id) ?? []) {
8337
- addReference(slot.platform, slot.scheduledFor);
8338
- }
8339
- }
8340
- return references;
8341
- }
8342
- function createSpacingGuard(ideaReferences, samePlatformHours, crossPlatformHours) {
8343
- const samePlatformWindowMs = samePlatformHours * HOUR_MS;
8344
- const crossPlatformWindowMs = crossPlatformHours * HOUR_MS;
8345
- return (candidateMs, candidatePlatform) => {
8346
- for (const reference of ideaReferences) {
8347
- const referenceMs = normalizeDateTime(reference.scheduledFor);
8348
- const diff = Math.abs(candidateMs - referenceMs);
8349
- if (reference.platform === candidatePlatform && diff < samePlatformWindowMs) {
8350
- return false;
8351
- }
8352
- if (diff < crossPlatformWindowMs) {
8353
- return false;
8354
- }
8355
- }
8356
- return true;
8357
- };
8358
- }
8359
- function resolveSearchWindow(nowMs, options) {
8360
- const defaultWindowEndMs = nowMs + DEFAULT_IDEA_WINDOW_DAYS * DAY_MS;
8361
- const publishBy = options?.publishBy;
8362
- if (!publishBy) {
8363
- return {
8364
- emptyWindowEndMs: defaultWindowEndMs,
8365
- displacementWindowEndMs: defaultWindowEndMs
8366
- };
8367
- }
8368
- const publishByMs = normalizeDateTime(publishBy);
8369
- if (Number.isNaN(publishByMs)) {
8370
- logger_default.warn(`Invalid publishBy "${sanitizeLogValue(publishBy)}" provided; scheduling normally without urgency bias`);
8371
- return {};
8372
- }
8373
- const daysUntilPublishBy = (publishByMs - nowMs) / DAY_MS;
8374
- if (daysUntilPublishBy <= 0) {
8375
- logger_default.warn(`publishBy "${sanitizeLogValue(publishBy)}" has already passed; scheduling normally without urgency bias`);
8376
- return {};
8377
- }
8378
- if (daysUntilPublishBy < 3) {
8379
- logger_default.debug(`Urgent publishBy "${sanitizeLogValue(publishBy)}"; prioritizing earliest displaceable slot`);
8380
- }
8381
- return {
8382
- emptyWindowEndMs: publishByMs,
8383
- displacementWindowEndMs: daysUntilPublishBy < 7 ? Math.min(publishByMs, nowMs + 3 * DAY_MS) : publishByMs
8384
- };
8385
- }
8386
- function findEmptySlot({
8387
- platformConfig,
8388
- timezone,
8389
- bookedDatetimes,
8390
- platform,
8391
- searchFromMs,
8392
- includeSearchDay = false,
8393
- maxCandidateMs,
8394
- passesCandidate
8395
- }) {
8396
- if (maxCandidateMs !== void 0 && maxCandidateMs < searchFromMs) {
8397
- return null;
8398
- }
8399
- const baseDate = new Date(searchFromMs);
8400
- const initialOffset = includeSearchDay ? 0 : 1;
8401
- let maxDayOffset = MAX_LOOKAHEAD_DAYS;
8402
- if (maxCandidateMs !== void 0) {
8403
- maxDayOffset = Math.min(
8404
- MAX_LOOKAHEAD_DAYS,
8405
- Math.max(initialOffset, Math.ceil((maxCandidateMs - searchFromMs) / DAY_MS))
8406
- );
8407
- }
8408
- let startOffset = initialOffset;
8409
- while (startOffset <= maxDayOffset) {
8410
- const endOffset = Math.min(startOffset + CHUNK_DAYS - 1, maxDayOffset);
8411
- const candidates = [];
8412
- for (let dayOffset = startOffset; dayOffset <= endOffset; dayOffset++) {
8413
- const candidateDate = new Date(baseDate);
8414
- candidateDate.setDate(candidateDate.getDate() + dayOffset);
8415
- const dayOfWeek = getDayOfWeekInTimezone2(candidateDate, timezone);
8416
- if (platformConfig.avoidDays.includes(dayOfWeek)) continue;
8417
- for (const slot of platformConfig.slots) {
8418
- if (!slot.days.includes(dayOfWeek)) continue;
8419
- const candidate = buildSlotDatetime2(candidateDate, slot.time, timezone);
8420
- const candidateMs = normalizeDateTime(candidate);
8421
- if (candidateMs <= searchFromMs) continue;
8422
- if (maxCandidateMs !== void 0 && candidateMs > maxCandidateMs) continue;
8423
- if (bookedDatetimes.has(candidateMs)) continue;
8424
- if (passesCandidate && !passesCandidate(candidateMs, platform)) continue;
8425
- candidates.push(candidate);
8426
- }
8427
- }
8428
- candidates.sort((left, right) => normalizeDateTime(left) - normalizeDateTime(right));
8429
- if (candidates.length > 0) {
8430
- return candidates[0];
8431
- }
8432
- startOffset = endOffset + 1;
8433
- }
8434
- return null;
8435
- }
8436
- async function tryDisplacement({
8437
- bookedSlots,
8438
- platform,
8439
- platformConfig,
8440
- timezone,
8441
- bookedDatetimes,
8442
- options,
8443
- nowMs,
8444
- maxCandidateMs,
8445
- passesSpacing
8446
- }) {
8447
- const displacementConfig = getDisplacementConfig();
8448
- if (!displacementConfig.enabled || !options.ideaIds?.length) {
8449
- return null;
8450
- }
8451
- const candidateSlots = bookedSlots.filter((slot) => {
8452
- const slotMs = normalizeDateTime(slot.scheduledFor);
8453
- if (slotMs <= nowMs) return false;
8454
- if (maxCandidateMs !== void 0 && slotMs > maxCandidateMs) return false;
8455
- return true;
8456
- }).sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
8457
- const lateClient = new LateApiClient();
8458
- const publishedItemCache = /* @__PURE__ */ new Map();
8459
- for (const slot of candidateSlots) {
8460
- if (slot.source !== "late" || !slot.postId) continue;
8461
- const candidateMs = normalizeDateTime(slot.scheduledFor);
8462
- if (passesSpacing && !passesSpacing(candidateMs, platform)) continue;
8463
- let publishedItem = publishedItemCache.get(slot.postId);
8464
- if (publishedItem === void 0) {
8465
- publishedItem = await getPublishedItemByLatePostId(slot.postId);
8466
- publishedItemCache.set(slot.postId, publishedItem);
8467
- }
8468
- if (!publishedItem) {
8469
- continue;
8470
- }
8471
- if (publishedItem.metadata.ideaIds?.length) {
8472
- continue;
8473
- }
8474
- const displacedPlatformConfig = publishedItem?.metadata.clipType ? getPlatformSchedule(platform, publishedItem.metadata.clipType) ?? platformConfig : platformConfig;
8475
- const newSlot = findEmptySlot({
8476
- platformConfig: displacedPlatformConfig,
8477
- timezone,
8478
- bookedDatetimes,
8479
- platform,
8480
- searchFromMs: candidateMs,
8481
- includeSearchDay: true
8482
- });
8483
- if (!newSlot) continue;
8484
- await lateClient.schedulePost(slot.postId, newSlot);
8485
- logger_default.info(
8486
- `Displaced post ${sanitizeLogValue(slot.postId)} from ${sanitizeLogValue(slot.scheduledFor)} to ${sanitizeLogValue(newSlot)} for idea-linked content`
8487
- );
8488
- return {
8489
- slot: slot.scheduledFor,
8490
- displaced: {
8491
- postId: slot.postId,
8492
- originalSlot: slot.scheduledFor,
8493
- newSlot
8494
- }
8495
- };
8496
- }
8497
- return null;
8498
- }
8499
- async function findNextSlot(platform, clipType, options) {
8500
- const config2 = await loadScheduleConfig();
8501
- const platformConfig = getPlatformSchedule(platform, clipType);
8502
- if (!platformConfig) {
8503
- logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
8504
- return null;
8505
- }
8506
- const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
8507
- const isIdeaAware = ideaIds.length > 0;
8508
- const nowMs = Date.now();
8509
- const { timezone } = config2;
8510
- const [allBookedSlots, sameIdeaPosts] = await Promise.all([
8511
- isIdeaAware ? buildBookedSlots() : Promise.resolve([]),
8512
- isIdeaAware ? getScheduledItemsByIdeaIds(ideaIds) : Promise.resolve([])
8513
- ]);
8514
- const bookedSlots = isIdeaAware ? allBookedSlots.filter((slot) => slot.platform === platform) : await buildBookedSlots(platform);
8515
- const bookedDatetimes = new Set(bookedSlots.map((slot) => normalizeDateTime(slot.scheduledFor)));
8516
- const spacingConfig = isIdeaAware ? getIdeaSpacingConfig() : null;
8517
- const spacingGuard = spacingConfig ? createSpacingGuard(
8518
- buildIdeaReferences(sameIdeaPosts, allBookedSlots),
8519
- spacingConfig.samePlatformHours,
8520
- spacingConfig.crossPlatformHours
8521
- ) : void 0;
8522
- const searchWindow = isIdeaAware ? resolveSearchWindow(nowMs, options) : {};
8523
- const emptySlot = findEmptySlot({
8524
- platformConfig,
8525
- timezone,
8526
- bookedDatetimes,
8527
- platform,
8528
- searchFromMs: nowMs,
8529
- maxCandidateMs: searchWindow.emptyWindowEndMs,
8530
- passesCandidate: spacingGuard
8531
- });
8532
- if (emptySlot) {
8533
- logger_default.debug(`Found available slot for ${sanitizeLogValue(platform)}: ${sanitizeLogValue(emptySlot)}`);
8534
- return emptySlot;
8535
- }
8536
- if (isIdeaAware) {
8537
- const displaced = await tryDisplacement({
8538
- bookedSlots,
8539
- platform,
8540
- platformConfig,
8541
- timezone,
8542
- bookedDatetimes,
8543
- options: { ...options, ideaIds },
8544
- nowMs,
8545
- maxCandidateMs: searchWindow.displacementWindowEndMs,
8546
- passesSpacing: spacingGuard
8547
- });
8548
- if (displaced) {
8549
- return displaced.slot;
8550
- }
8551
- }
8552
- logger_default.warn(`No available slot found for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
8553
- return null;
8554
- }
8555
- async function getScheduleCalendar(startDate, endDate) {
8556
- const slots = await buildBookedSlots();
8557
- let filtered = slots.filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
8558
- platform: slot.platform,
8559
- scheduledFor: slot.scheduledFor,
8560
- source: slot.source,
8561
- postId: slot.postId,
8562
- itemId: slot.itemId
8563
- }));
8564
- if (startDate) {
8565
- const startMs = startDate.getTime();
8566
- filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) >= startMs);
8567
- }
8568
- if (endDate) {
8569
- const endMs = endDate.getTime();
8570
- filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) <= endMs);
8571
- }
8572
- filtered.sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
8573
- return filtered;
8574
- }
8575
-
8576
8501
  // src/L2-clients/ffmpeg/audioExtraction.ts
8577
8502
  init_fileSystem();
8578
8503
  init_paths();
@@ -10079,6 +10004,9 @@ function platformAcceptsMedia(platform, clipType) {
10079
10004
  return getMediaRule(platform, clipType) !== null;
10080
10005
  }
10081
10006
 
10007
+ // src/L3-services/queueBuilder/queueBuilder.ts
10008
+ init_postStore();
10009
+
10082
10010
  // src/L1-infra/image/image.ts
10083
10011
  import { default as default7 } from "sharp";
10084
10012
 
@@ -10766,7 +10694,7 @@ function buildSystemPrompt(brand, existingIdeas, seedTopics, count, ideaRepo) {
10766
10694
  }
10767
10695
  return promptSections.join("\n");
10768
10696
  }
10769
- function buildUserMessage(count, seedTopics, hasMcpServers) {
10697
+ function buildUserMessage(count, seedTopics, hasMcpServers, userPrompt) {
10770
10698
  const focusText = seedTopics.length > 0 ? `Focus areas: ${seedTopics.join(", ")}` : "Focus areas: choose the strongest timely opportunities from the creator context and current trends.";
10771
10699
  const steps = [
10772
10700
  "1. Call get_brand_context to load the creator profile.",
@@ -10790,13 +10718,15 @@ function buildUserMessage(count, seedTopics, hasMcpServers) {
10790
10718
  "5. Call finalize_ideas when done."
10791
10719
  );
10792
10720
  }
10793
- return [
10721
+ const sections = [
10794
10722
  `Generate ${count} new content ideas.`,
10795
- focusText,
10796
- "",
10797
- "Follow this exact workflow:",
10798
- ...steps
10799
- ].join("\n");
10723
+ focusText
10724
+ ];
10725
+ if (userPrompt) {
10726
+ sections.push("", `## User Prompt`, userPrompt);
10727
+ }
10728
+ sections.push("", "Follow this exact workflow:", ...steps);
10729
+ return sections.join("\n");
10800
10730
  }
10801
10731
  async function loadBrandContext(brandPath) {
10802
10732
  if (!brandPath) {
@@ -11313,7 +11243,7 @@ async function generateIdeas(options = {}) {
11313
11243
  });
11314
11244
  try {
11315
11245
  const hasMcpServers = !!(config2.EXA_API_KEY || config2.YOUTUBE_API_KEY || config2.PERPLEXITY_API_KEY);
11316
- const userMessage = buildUserMessage(count, seedTopics, hasMcpServers);
11246
+ const userMessage = buildUserMessage(count, seedTopics, hasMcpServers, options.prompt);
11317
11247
  await agent.run(userMessage);
11318
11248
  const ideas = agent.getGeneratedIdeas();
11319
11249
  if (!agent.isFinalized()) {
@@ -16599,7 +16529,8 @@ var defaultKeys = [
16599
16529
  "brandPath",
16600
16530
  "ideasRepo",
16601
16531
  "lateProfileId",
16602
- "geminiModel"
16532
+ "geminiModel",
16533
+ "scheduleConfig"
16603
16534
  ];
16604
16535
  var configKeyMap = {
16605
16536
  "openai-key": { section: "credentials", key: "openaiApiKey" },
@@ -16617,7 +16548,8 @@ var configKeyMap = {
16617
16548
  "brand-path": { section: "defaults", key: "brandPath" },
16618
16549
  "ideas-repo": { section: "defaults", key: "ideasRepo" },
16619
16550
  "late-profile-id": { section: "defaults", key: "lateProfileId" },
16620
- "gemini-model": { section: "defaults", key: "geminiModel" }
16551
+ "gemini-model": { section: "defaults", key: "geminiModel" },
16552
+ "schedule-config": { section: "defaults", key: "scheduleConfig" }
16621
16553
  };
16622
16554
  var providerDefaults = {
16623
16555
  copilot: "Claude Opus 4.6",