vidpipe 1.3.18 → 1.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -698,6 +698,387 @@ var init_configLogger = __esm({
698
698
  }
699
699
  });
700
700
 
701
+ // src/L0-pure/pipelineSpec/types.ts
702
+ function isPresetName(value) {
703
+ return value === "full" || value === "clean" || value === "minimal";
704
+ }
705
+ var init_types = __esm({
706
+ "src/L0-pure/pipelineSpec/types.ts"() {
707
+ "use strict";
708
+ }
709
+ });
710
+
711
+ // src/L0-pure/pipelineSpec/presets.ts
712
+ function getPreset(name) {
713
+ return PRESETS[name];
714
+ }
715
+ var PRESET_FULL, PRESET_CLEAN, PRESET_MINIMAL, PRESETS;
716
+ var init_presets = __esm({
717
+ "src/L0-pure/pipelineSpec/presets.ts"() {
718
+ "use strict";
719
+ PRESET_FULL = {
720
+ name: "full",
721
+ description: "Full pipeline with hook-first shorts, per-platform optimization",
722
+ processing: {
723
+ silenceRemoval: true,
724
+ visualEnhancement: true,
725
+ captions: true,
726
+ introOutro: true
727
+ },
728
+ clips: {
729
+ shorts: {
730
+ enabled: true,
731
+ strategy: "hook-first",
732
+ duration: { min: 15, max: 60 },
733
+ minViralScore: 8,
734
+ maxClips: 5
735
+ },
736
+ medium: {
737
+ enabled: true,
738
+ strategy: "chronological",
739
+ duration: { min: 60, max: 180 },
740
+ minViralScore: 10,
741
+ maxClips: 5
742
+ }
743
+ },
744
+ content: {
745
+ chapters: true,
746
+ summary: true,
747
+ blog: true
748
+ },
749
+ distribution: {
750
+ enabled: true,
751
+ publish: true,
752
+ platforms: {
753
+ targets: ["youtube", "linkedin", "instagram", "x", "tiktok"],
754
+ toneStrategy: "per-platform",
755
+ variants: true
756
+ }
757
+ }
758
+ };
759
+ PRESET_CLEAN = {
760
+ name: "clean",
761
+ description: "Simple cleanup with longer clips and unified platform handling",
762
+ processing: {
763
+ silenceRemoval: true,
764
+ visualEnhancement: false,
765
+ captions: true,
766
+ introOutro: true
767
+ },
768
+ clips: {
769
+ shorts: {
770
+ enabled: false
771
+ },
772
+ medium: {
773
+ enabled: true,
774
+ strategy: "chronological",
775
+ duration: { min: 60, max: 600 },
776
+ minViralScore: 6,
777
+ maxClips: 5
778
+ }
779
+ },
780
+ content: {
781
+ chapters: true,
782
+ summary: true,
783
+ blog: true
784
+ },
785
+ distribution: {
786
+ enabled: true,
787
+ publish: true,
788
+ platforms: {
789
+ targets: ["youtube", "linkedin", "instagram", "x", "tiktok"],
790
+ toneStrategy: "unified",
791
+ variants: false
792
+ }
793
+ }
794
+ };
795
+ PRESET_MINIMAL = {
796
+ name: "minimal",
797
+ description: "Cleanup only \u2014 no clips, no social, no blog",
798
+ processing: {
799
+ silenceRemoval: true,
800
+ visualEnhancement: false,
801
+ captions: true,
802
+ introOutro: false
803
+ },
804
+ clips: {
805
+ shorts: {
806
+ enabled: false
807
+ },
808
+ medium: {
809
+ enabled: false
810
+ }
811
+ },
812
+ content: {
813
+ chapters: true,
814
+ summary: true,
815
+ blog: false
816
+ },
817
+ distribution: {
818
+ enabled: false,
819
+ publish: false,
820
+ platforms: {
821
+ targets: [],
822
+ toneStrategy: "unified",
823
+ variants: false
824
+ }
825
+ }
826
+ };
827
+ PRESETS = {
828
+ full: PRESET_FULL,
829
+ clean: PRESET_CLEAN,
830
+ minimal: PRESET_MINIMAL
831
+ };
832
+ }
833
+ });
834
+
835
+ // src/L0-pure/pipelineSpec/validation.ts
836
+ function isObject(v) {
837
+ return v !== null && typeof v === "object" && !Array.isArray(v);
838
+ }
839
+ function validateClipConfig(clip, path, errors) {
840
+ if (!isObject(clip)) return;
841
+ if ("enabled" in clip && typeof clip.enabled !== "boolean") {
842
+ errors.push({ path: `${path}.enabled`, message: "must be a boolean" });
843
+ }
844
+ if ("strategy" in clip && clip.strategy !== void 0) {
845
+ if (!VALID_STRATEGIES.includes(clip.strategy)) {
846
+ errors.push({ path: `${path}.strategy`, message: `must be one of: ${VALID_STRATEGIES.join(", ")}` });
847
+ }
848
+ }
849
+ if ("duration" in clip && clip.duration !== void 0) {
850
+ const dur = clip.duration;
851
+ if (!isObject(dur)) {
852
+ errors.push({ path: `${path}.duration`, message: "must be an object with min and max" });
853
+ } else {
854
+ if ("min" in dur && (typeof dur.min !== "number" || dur.min < 0)) {
855
+ errors.push({ path: `${path}.duration.min`, message: "must be a non-negative number" });
856
+ }
857
+ if ("max" in dur && (typeof dur.max !== "number" || dur.max < 0)) {
858
+ errors.push({ path: `${path}.duration.max`, message: "must be a non-negative number" });
859
+ }
860
+ if (typeof dur.min === "number" && typeof dur.max === "number" && dur.min > dur.max) {
861
+ errors.push({ path: `${path}.duration`, message: "min must be less than or equal to max" });
862
+ }
863
+ }
864
+ }
865
+ if ("minViralScore" in clip && clip.minViralScore !== void 0) {
866
+ const score = clip.minViralScore;
867
+ if (typeof score !== "number" || score < 1 || score > 20) {
868
+ errors.push({ path: `${path}.minViralScore`, message: "must be a number between 1 and 20" });
869
+ }
870
+ }
871
+ if ("maxClips" in clip && clip.maxClips !== void 0) {
872
+ const max = clip.maxClips;
873
+ if (typeof max !== "number" || max < 1 || !Number.isInteger(max)) {
874
+ errors.push({ path: `${path}.maxClips`, message: "must be a positive integer" });
875
+ }
876
+ }
877
+ }
878
+ function validateProcessing(proc, errors) {
879
+ if (!isObject(proc)) return;
880
+ const boolFields = ["silenceRemoval", "visualEnhancement", "captions", "introOutro"];
881
+ for (const field of boolFields) {
882
+ if (field in proc && typeof proc[field] !== "boolean") {
883
+ errors.push({ path: `processing.${field}`, message: "must be a boolean" });
884
+ }
885
+ }
886
+ }
887
+ function validateContent(content, errors) {
888
+ if (!isObject(content)) return;
889
+ const boolFields = ["chapters", "summary", "blog"];
890
+ for (const field of boolFields) {
891
+ if (field in content && typeof content[field] !== "boolean") {
892
+ errors.push({ path: `content.${field}`, message: "must be a boolean" });
893
+ }
894
+ }
895
+ }
896
+ function validateDistribution(dist, errors) {
897
+ if (!isObject(dist)) return;
898
+ if ("enabled" in dist && typeof dist.enabled !== "boolean") {
899
+ errors.push({ path: "distribution.enabled", message: "must be a boolean" });
900
+ }
901
+ if ("publish" in dist && typeof dist.publish !== "boolean") {
902
+ errors.push({ path: "distribution.publish", message: "must be a boolean" });
903
+ }
904
+ if ("platforms" in dist && dist.platforms !== void 0) {
905
+ const plat = dist.platforms;
906
+ if (!isObject(plat)) {
907
+ errors.push({ path: "distribution.platforms", message: "must be an object" });
908
+ return;
909
+ }
910
+ if ("targets" in plat) {
911
+ if (!Array.isArray(plat.targets)) {
912
+ errors.push({ path: "distribution.platforms.targets", message: "must be an array" });
913
+ } else {
914
+ for (const target of plat.targets) {
915
+ if (!VALID_PLATFORMS.includes(target)) {
916
+ errors.push({
917
+ path: "distribution.platforms.targets",
918
+ message: `unknown platform '${String(target)}' \u2014 valid: ${VALID_PLATFORMS.join(", ")}`
919
+ });
920
+ }
921
+ }
922
+ }
923
+ }
924
+ if ("toneStrategy" in plat && plat.toneStrategy !== void 0) {
925
+ if (!VALID_TONE_STRATEGIES.includes(plat.toneStrategy)) {
926
+ errors.push({
927
+ path: "distribution.platforms.toneStrategy",
928
+ message: `must be one of: ${VALID_TONE_STRATEGIES.join(", ")}`
929
+ });
930
+ }
931
+ }
932
+ if ("variants" in plat && typeof plat.variants !== "boolean") {
933
+ errors.push({ path: "distribution.platforms.variants", message: "must be a boolean" });
934
+ }
935
+ }
936
+ }
937
+ function validateSpec(raw) {
938
+ const errors = [];
939
+ if (!isObject(raw)) {
940
+ errors.push({ path: "", message: "spec must be an object" });
941
+ return errors;
942
+ }
943
+ const spec = raw;
944
+ if ("name" in spec && spec.name !== void 0 && typeof spec.name !== "string") {
945
+ errors.push({ path: "name", message: "must be a string" });
946
+ }
947
+ if ("description" in spec && spec.description !== void 0 && typeof spec.description !== "string") {
948
+ errors.push({ path: "description", message: "must be a string" });
949
+ }
950
+ if ("processing" in spec) validateProcessing(spec.processing, errors);
951
+ if ("clips" in spec && spec.clips !== void 0) {
952
+ if (!isObject(spec.clips)) {
953
+ errors.push({ path: "clips", message: "must be an object" });
954
+ } else {
955
+ if ("shorts" in spec.clips) validateClipConfig(spec.clips.shorts, "clips.shorts", errors);
956
+ if ("medium" in spec.clips) validateClipConfig(spec.clips.medium, "clips.medium", errors);
957
+ }
958
+ }
959
+ if ("content" in spec) validateContent(spec.content, errors);
960
+ if ("distribution" in spec) validateDistribution(spec.distribution, errors);
961
+ return errors;
962
+ }
963
+ var VALID_STRATEGIES, VALID_TONE_STRATEGIES, VALID_PLATFORMS;
964
+ var init_validation = __esm({
965
+ "src/L0-pure/pipelineSpec/validation.ts"() {
966
+ "use strict";
967
+ VALID_STRATEGIES = ["hook-first", "chronological"];
968
+ VALID_TONE_STRATEGIES = ["unified", "per-platform"];
969
+ VALID_PLATFORMS = ["youtube", "linkedin", "instagram", "x", "tiktok"];
970
+ }
971
+ });
972
+
973
+ // src/L0-pure/pipelineSpec/merger.ts
974
+ function mergeDuration(base, override) {
975
+ if (!override) return base;
976
+ if (!base) return override;
977
+ return {
978
+ min: override.min ?? base.min,
979
+ max: override.max ?? base.max
980
+ };
981
+ }
982
+ function mergeClipConfig(base, override) {
983
+ if (!override) return base;
984
+ return {
985
+ enabled: override.enabled ?? base.enabled,
986
+ strategy: override.strategy ?? base.strategy,
987
+ duration: mergeDuration(base.duration, override.duration),
988
+ minViralScore: override.minViralScore ?? base.minViralScore,
989
+ maxClips: override.maxClips ?? base.maxClips
990
+ };
991
+ }
992
+ function mergeWithDefaults(partial, base = PRESET_FULL) {
993
+ return {
994
+ name: partial.name ?? base.name,
995
+ description: partial.description ?? base.description,
996
+ processing: {
997
+ silenceRemoval: partial.processing?.silenceRemoval ?? base.processing.silenceRemoval,
998
+ visualEnhancement: partial.processing?.visualEnhancement ?? base.processing.visualEnhancement,
999
+ captions: partial.processing?.captions ?? base.processing.captions,
1000
+ introOutro: partial.processing?.introOutro ?? base.processing.introOutro
1001
+ },
1002
+ clips: {
1003
+ shorts: mergeClipConfig(base.clips.shorts, partial.clips?.shorts),
1004
+ medium: mergeClipConfig(base.clips.medium, partial.clips?.medium)
1005
+ },
1006
+ content: {
1007
+ chapters: partial.content?.chapters ?? base.content.chapters,
1008
+ summary: partial.content?.summary ?? base.content.summary,
1009
+ blog: partial.content?.blog ?? base.content.blog
1010
+ },
1011
+ distribution: {
1012
+ enabled: partial.distribution?.enabled ?? base.distribution.enabled,
1013
+ publish: partial.distribution?.publish ?? base.distribution.publish,
1014
+ platforms: {
1015
+ targets: partial.distribution?.platforms?.targets ?? base.distribution.platforms.targets,
1016
+ toneStrategy: partial.distribution?.platforms?.toneStrategy ?? base.distribution.platforms.toneStrategy,
1017
+ variants: partial.distribution?.platforms?.variants ?? base.distribution.platforms.variants
1018
+ }
1019
+ }
1020
+ };
1021
+ }
1022
+ function applySkipFlags(spec, flags) {
1023
+ return {
1024
+ ...spec,
1025
+ processing: {
1026
+ silenceRemoval: spec.processing.silenceRemoval && !flags.SKIP_SILENCE_REMOVAL,
1027
+ visualEnhancement: spec.processing.visualEnhancement && !flags.SKIP_VISUAL_ENHANCEMENT,
1028
+ captions: spec.processing.captions && !flags.SKIP_CAPTIONS,
1029
+ introOutro: spec.processing.introOutro && !flags.SKIP_INTRO_OUTRO
1030
+ },
1031
+ clips: {
1032
+ shorts: {
1033
+ ...spec.clips.shorts,
1034
+ enabled: spec.clips.shorts.enabled && !flags.SKIP_SHORTS
1035
+ },
1036
+ medium: {
1037
+ ...spec.clips.medium,
1038
+ enabled: spec.clips.medium.enabled && !flags.SKIP_MEDIUM_CLIPS
1039
+ }
1040
+ },
1041
+ distribution: {
1042
+ ...spec.distribution,
1043
+ enabled: spec.distribution.enabled && !flags.SKIP_SOCIAL,
1044
+ publish: spec.distribution.publish && !flags.SKIP_SOCIAL_PUBLISH
1045
+ }
1046
+ };
1047
+ }
1048
+ function resolveFromFlags(flags) {
1049
+ return applySkipFlags(PRESET_FULL, flags);
1050
+ }
1051
+ var init_merger = __esm({
1052
+ "src/L0-pure/pipelineSpec/merger.ts"() {
1053
+ "use strict";
1054
+ init_presets();
1055
+ }
1056
+ });
1057
+
1058
+ // src/L0-pure/pipelineSpec/index.ts
1059
+ var pipelineSpec_exports = {};
1060
+ __export(pipelineSpec_exports, {
1061
+ PRESETS: () => PRESETS,
1062
+ PRESET_CLEAN: () => PRESET_CLEAN,
1063
+ PRESET_FULL: () => PRESET_FULL,
1064
+ PRESET_MINIMAL: () => PRESET_MINIMAL,
1065
+ applySkipFlags: () => applySkipFlags,
1066
+ getPreset: () => getPreset,
1067
+ isPresetName: () => isPresetName,
1068
+ mergeWithDefaults: () => mergeWithDefaults,
1069
+ resolveFromFlags: () => resolveFromFlags,
1070
+ validateSpec: () => validateSpec
1071
+ });
1072
+ var init_pipelineSpec = __esm({
1073
+ "src/L0-pure/pipelineSpec/index.ts"() {
1074
+ "use strict";
1075
+ init_types();
1076
+ init_presets();
1077
+ init_validation();
1078
+ init_merger();
1079
+ }
1080
+ });
1081
+
701
1082
  // src/L0-pure/types/index.ts
702
1083
  function getStageInfo(stage) {
703
1084
  const info = PIPELINE_STAGES.find((s) => s.stage === stage);
@@ -729,9 +1110,10 @@ function isSupportedVideoExtension(ext) {
729
1110
  return SUPPORTED_VIDEO_EXTENSIONS.includes(ext.toLowerCase());
730
1111
  }
731
1112
  var Platform, PIPELINE_STAGES, TOTAL_STAGES, PLATFORM_CHAR_LIMITS, SUPPORTED_VIDEO_EXTENSIONS;
732
- var init_types = __esm({
1113
+ var init_types2 = __esm({
733
1114
  "src/L0-pure/types/index.ts"() {
734
1115
  "use strict";
1116
+ init_pipelineSpec();
735
1117
  Platform = /* @__PURE__ */ ((Platform2) => {
736
1118
  Platform2["TikTok"] = "tiktok";
737
1119
  Platform2["YouTube"] = "youtube";
@@ -5837,23 +6219,63 @@ function createSessionRpc(connection, sessionId) {
5837
6219
  readFile: async (params) => connection.sendRequest("session.workspace.readFile", { sessionId, ...params }),
5838
6220
  createFile: async (params) => connection.sendRequest("session.workspace.createFile", { sessionId, ...params })
5839
6221
  },
6222
+ /** @experimental */
5840
6223
  fleet: {
5841
6224
  start: async (params) => connection.sendRequest("session.fleet.start", { sessionId, ...params })
5842
6225
  },
6226
+ /** @experimental */
5843
6227
  agent: {
5844
6228
  list: async () => connection.sendRequest("session.agent.list", { sessionId }),
5845
6229
  getCurrent: async () => connection.sendRequest("session.agent.getCurrent", { sessionId }),
5846
6230
  select: async (params) => connection.sendRequest("session.agent.select", { sessionId, ...params }),
5847
- deselect: async () => connection.sendRequest("session.agent.deselect", { sessionId })
6231
+ deselect: async () => connection.sendRequest("session.agent.deselect", { sessionId }),
6232
+ reload: async () => connection.sendRequest("session.agent.reload", { sessionId })
6233
+ },
6234
+ /** @experimental */
6235
+ skills: {
6236
+ list: async () => connection.sendRequest("session.skills.list", { sessionId }),
6237
+ enable: async (params) => connection.sendRequest("session.skills.enable", { sessionId, ...params }),
6238
+ disable: async (params) => connection.sendRequest("session.skills.disable", { sessionId, ...params }),
6239
+ reload: async () => connection.sendRequest("session.skills.reload", { sessionId })
6240
+ },
6241
+ /** @experimental */
6242
+ mcp: {
6243
+ list: async () => connection.sendRequest("session.mcp.list", { sessionId }),
6244
+ enable: async (params) => connection.sendRequest("session.mcp.enable", { sessionId, ...params }),
6245
+ disable: async (params) => connection.sendRequest("session.mcp.disable", { sessionId, ...params }),
6246
+ reload: async () => connection.sendRequest("session.mcp.reload", { sessionId })
6247
+ },
6248
+ /** @experimental */
6249
+ plugins: {
6250
+ list: async () => connection.sendRequest("session.plugins.list", { sessionId })
5848
6251
  },
6252
+ /** @experimental */
6253
+ extensions: {
6254
+ list: async () => connection.sendRequest("session.extensions.list", { sessionId }),
6255
+ enable: async (params) => connection.sendRequest("session.extensions.enable", { sessionId, ...params }),
6256
+ disable: async (params) => connection.sendRequest("session.extensions.disable", { sessionId, ...params }),
6257
+ reload: async () => connection.sendRequest("session.extensions.reload", { sessionId })
6258
+ },
6259
+ /** @experimental */
5849
6260
  compaction: {
5850
6261
  compact: async () => connection.sendRequest("session.compaction.compact", { sessionId })
5851
6262
  },
5852
6263
  tools: {
5853
6264
  handlePendingToolCall: async (params) => connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params })
5854
6265
  },
6266
+ commands: {
6267
+ handlePendingCommand: async (params) => connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params })
6268
+ },
6269
+ ui: {
6270
+ elicitation: async (params) => connection.sendRequest("session.ui.elicitation", { sessionId, ...params })
6271
+ },
5855
6272
  permissions: {
5856
6273
  handlePendingPermissionRequest: async (params) => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params })
6274
+ },
6275
+ log: async (params) => connection.sendRequest("session.log", { sessionId, ...params }),
6276
+ shell: {
6277
+ exec: async (params) => connection.sendRequest("session.shell.exec", { sessionId, ...params }),
6278
+ kill: async (params) => connection.sendRequest("session.shell.kill", { sessionId, ...params })
5857
6279
  }
5858
6280
  };
5859
6281
  }
@@ -5875,13 +6297,30 @@ var init_sdkProtocolVersion = __esm({
5875
6297
  }
5876
6298
  });
5877
6299
 
6300
+ // node_modules/@github/copilot-sdk/dist/telemetry.js
6301
+ async function getTraceContext(provider) {
6302
+ if (!provider) return {};
6303
+ try {
6304
+ return await provider() ?? {};
6305
+ } catch {
6306
+ return {};
6307
+ }
6308
+ }
6309
+ var init_telemetry = __esm({
6310
+ "node_modules/@github/copilot-sdk/dist/telemetry.js"() {
6311
+ "use strict";
6312
+ }
6313
+ });
6314
+
5878
6315
  // node_modules/@github/copilot-sdk/dist/session.js
5879
- var import_node, CopilotSession;
6316
+ var import_node, NO_RESULT_PERMISSION_V2_ERROR, CopilotSession;
5880
6317
  var init_session = __esm({
5881
6318
  "node_modules/@github/copilot-sdk/dist/session.js"() {
5882
6319
  "use strict";
5883
6320
  import_node = __toESM(require_node(), 1);
5884
6321
  init_rpc();
6322
+ init_telemetry();
6323
+ NO_RESULT_PERMISSION_V2_ERROR = "Permission handlers cannot return 'no-result' when connected to a protocol v2 server.";
5885
6324
  CopilotSession = class {
5886
6325
  /**
5887
6326
  * Creates a new CopilotSession instance.
@@ -5889,12 +6328,14 @@ var init_session = __esm({
5889
6328
  * @param sessionId - The unique identifier for this session
5890
6329
  * @param connection - The JSON-RPC message connection to the Copilot CLI
5891
6330
  * @param workspacePath - Path to the session workspace directory (when infinite sessions enabled)
6331
+ * @param traceContextProvider - Optional callback to get W3C Trace Context for outbound RPCs
5892
6332
  * @internal This constructor is internal. Use {@link CopilotClient.createSession} to create sessions.
5893
6333
  */
5894
- constructor(sessionId, connection, _workspacePath) {
6334
+ constructor(sessionId, connection, _workspacePath, traceContextProvider) {
5895
6335
  this.sessionId = sessionId;
5896
6336
  this.connection = connection;
5897
6337
  this._workspacePath = _workspacePath;
6338
+ this.traceContextProvider = traceContextProvider;
5898
6339
  }
5899
6340
  eventHandlers = /* @__PURE__ */ new Set();
5900
6341
  typedEventHandlers = /* @__PURE__ */ new Map();
@@ -5902,7 +6343,9 @@ var init_session = __esm({
5902
6343
  permissionHandler;
5903
6344
  userInputHandler;
5904
6345
  hooks;
6346
+ transformCallbacks;
5905
6347
  _rpc = null;
6348
+ traceContextProvider;
5906
6349
  /**
5907
6350
  * Typed session-scoped RPC methods.
5908
6351
  */
@@ -5940,6 +6383,7 @@ var init_session = __esm({
5940
6383
  */
5941
6384
  async send(options) {
5942
6385
  const response = await this.connection.sendRequest("session.send", {
6386
+ ...await getTraceContext(this.traceContextProvider),
5943
6387
  sessionId: this.sessionId,
5944
6388
  prompt: options.prompt,
5945
6389
  attachments: options.attachments,
@@ -6069,9 +6513,19 @@ var init_session = __esm({
6069
6513
  const { requestId, toolName } = event.data;
6070
6514
  const args = event.data.arguments;
6071
6515
  const toolCallId = event.data.toolCallId;
6516
+ const traceparent = event.data.traceparent;
6517
+ const tracestate = event.data.tracestate;
6072
6518
  const handler = this.toolHandlers.get(toolName);
6073
6519
  if (handler) {
6074
- void this._executeToolAndRespond(requestId, toolName, toolCallId, args, handler);
6520
+ void this._executeToolAndRespond(
6521
+ requestId,
6522
+ toolName,
6523
+ toolCallId,
6524
+ args,
6525
+ handler,
6526
+ traceparent,
6527
+ tracestate
6528
+ );
6075
6529
  }
6076
6530
  } else if (event.type === "permission.requested") {
6077
6531
  const { requestId, permissionRequest } = event.data;
@@ -6084,13 +6538,15 @@ var init_session = __esm({
6084
6538
  * Executes a tool handler and sends the result back via RPC.
6085
6539
  * @internal
6086
6540
  */
6087
- async _executeToolAndRespond(requestId, toolName, toolCallId, args, handler) {
6541
+ async _executeToolAndRespond(requestId, toolName, toolCallId, args, handler, traceparent, tracestate) {
6088
6542
  try {
6089
6543
  const rawResult = await handler(args, {
6090
6544
  sessionId: this.sessionId,
6091
6545
  toolCallId,
6092
6546
  toolName,
6093
- arguments: args
6547
+ arguments: args,
6548
+ traceparent,
6549
+ tracestate
6094
6550
  });
6095
6551
  let result;
6096
6552
  if (rawResult == null) {
@@ -6121,6 +6577,9 @@ var init_session = __esm({
6121
6577
  const result = await this.permissionHandler(permissionRequest, {
6122
6578
  sessionId: this.sessionId
6123
6579
  });
6580
+ if (result.kind === "no-result") {
6581
+ return;
6582
+ }
6124
6583
  await this.rpc.permissions.handlePendingPermissionRequest({ requestId, result });
6125
6584
  } catch (_error) {
6126
6585
  try {
@@ -6201,6 +6660,40 @@ var init_session = __esm({
6201
6660
  registerHooks(hooks) {
6202
6661
  this.hooks = hooks;
6203
6662
  }
6663
+ /**
6664
+ * Registers transform callbacks for system message sections.
6665
+ *
6666
+ * @param callbacks - Map of section ID to transform callback, or undefined to clear
6667
+ * @internal This method is typically called internally when creating a session.
6668
+ */
6669
+ registerTransformCallbacks(callbacks) {
6670
+ this.transformCallbacks = callbacks;
6671
+ }
6672
+ /**
6673
+ * Handles a systemMessage.transform request from the runtime.
6674
+ * Dispatches each section to its registered transform callback.
6675
+ *
6676
+ * @param sections - Map of section IDs to their current rendered content
6677
+ * @returns A promise that resolves with the transformed sections
6678
+ * @internal This method is for internal use by the SDK.
6679
+ */
6680
+ async _handleSystemMessageTransform(sections) {
6681
+ const result = {};
6682
+ for (const [sectionId, { content }] of Object.entries(sections)) {
6683
+ const callback = this.transformCallbacks?.get(sectionId);
6684
+ if (callback) {
6685
+ try {
6686
+ const transformed = await callback(content);
6687
+ result[sectionId] = { content: transformed };
6688
+ } catch (_error) {
6689
+ result[sectionId] = { content };
6690
+ }
6691
+ } else {
6692
+ result[sectionId] = { content };
6693
+ }
6694
+ }
6695
+ return { sections: result };
6696
+ }
6204
6697
  /**
6205
6698
  * Handles a permission request in the v2 protocol format (synchronous RPC).
6206
6699
  * Used as a back-compat adapter when connected to a v2 server.
@@ -6217,8 +6710,14 @@ var init_session = __esm({
6217
6710
  const result = await this.permissionHandler(request, {
6218
6711
  sessionId: this.sessionId
6219
6712
  });
6713
+ if (result.kind === "no-result") {
6714
+ throw new Error(NO_RESULT_PERMISSION_V2_ERROR);
6715
+ }
6220
6716
  return result;
6221
- } catch (_error) {
6717
+ } catch (error) {
6718
+ if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
6719
+ throw error;
6720
+ }
6222
6721
  return { kind: "denied-no-approval-rule-and-could-not-request-from-user" };
6223
6722
  }
6224
6723
  }
@@ -6374,14 +6873,35 @@ var init_session = __esm({
6374
6873
  * The new model takes effect for the next message. Conversation history is preserved.
6375
6874
  *
6376
6875
  * @param model - Model ID to switch to
6876
+ * @param options - Optional settings for the new model
6377
6877
  *
6378
6878
  * @example
6379
6879
  * ```typescript
6380
6880
  * await session.setModel("gpt-4.1");
6881
+ * await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" });
6882
+ * ```
6883
+ */
6884
+ async setModel(model, options) {
6885
+ await this.rpc.model.switchTo({ modelId: model, ...options });
6886
+ }
6887
+ /**
6888
+ * Log a message to the session timeline.
6889
+ * The message appears in the session event stream and is visible to SDK consumers
6890
+ * and (for non-ephemeral messages) persisted to the session event log on disk.
6891
+ *
6892
+ * @param message - Human-readable message text
6893
+ * @param options - Optional log level and ephemeral flag
6894
+ *
6895
+ * @example
6896
+ * ```typescript
6897
+ * await session.log("Processing started");
6898
+ * await session.log("Disk usage high", { level: "warning" });
6899
+ * await session.log("Connection failed", { level: "error" });
6900
+ * await session.log("Debug info", { ephemeral: true });
6381
6901
  * ```
6382
6902
  */
6383
- async setModel(model) {
6384
- await this.rpc.model.switchTo({ modelId: model });
6903
+ async log(message, options) {
6904
+ await this.rpc.log({ message, ...options });
6385
6905
  }
6386
6906
  };
6387
6907
  }
@@ -6389,7 +6909,9 @@ var init_session = __esm({
6389
6909
 
6390
6910
  // node_modules/@github/copilot-sdk/dist/client.js
6391
6911
  import { spawn } from "child_process";
6912
+ import { randomUUID } from "crypto";
6392
6913
  import { existsSync as existsSync4 } from "fs";
6914
+ import { createRequire as createRequire2 } from "module";
6393
6915
  import { Socket } from "net";
6394
6916
  import { dirname as dirname3, join as join6 } from "path";
6395
6917
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -6403,6 +6925,30 @@ function toJsonSchema(parameters) {
6403
6925
  }
6404
6926
  return parameters;
6405
6927
  }
6928
+ function extractTransformCallbacks(systemMessage) {
6929
+ if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
6930
+ return { wirePayload: systemMessage, transformCallbacks: void 0 };
6931
+ }
6932
+ const transformCallbacks = /* @__PURE__ */ new Map();
6933
+ const wireSections = {};
6934
+ for (const [sectionId, override] of Object.entries(systemMessage.sections)) {
6935
+ if (!override) continue;
6936
+ if (typeof override.action === "function") {
6937
+ transformCallbacks.set(sectionId, override.action);
6938
+ wireSections[sectionId] = { action: "transform" };
6939
+ } else {
6940
+ wireSections[sectionId] = { action: override.action, content: override.content };
6941
+ }
6942
+ }
6943
+ if (transformCallbacks.size === 0) {
6944
+ return { wirePayload: systemMessage, transformCallbacks: void 0 };
6945
+ }
6946
+ const wirePayload = {
6947
+ ...systemMessage,
6948
+ sections: wireSections
6949
+ };
6950
+ return { wirePayload, transformCallbacks };
6951
+ }
6406
6952
  function getNodeExecPath() {
6407
6953
  if (process.versions.bun) {
6408
6954
  return "node";
@@ -6410,9 +6956,22 @@ function getNodeExecPath() {
6410
6956
  return process.execPath;
6411
6957
  }
6412
6958
  function getBundledCliPath() {
6413
- const sdkUrl = import.meta.resolve("@github/copilot/sdk");
6414
- const sdkPath = fileURLToPath3(sdkUrl);
6415
- return join6(dirname3(dirname3(sdkPath)), "index.js");
6959
+ if (typeof import.meta.resolve === "function") {
6960
+ const sdkUrl = import.meta.resolve("@github/copilot/sdk");
6961
+ const sdkPath = fileURLToPath3(sdkUrl);
6962
+ return join6(dirname3(dirname3(sdkPath)), "index.js");
6963
+ }
6964
+ const req = createRequire2(__filename);
6965
+ const searchPaths = req.resolve.paths("@github/copilot") ?? [];
6966
+ for (const base of searchPaths) {
6967
+ const candidate = join6(base, "@github", "copilot", "index.js");
6968
+ if (existsSync4(candidate)) {
6969
+ return candidate;
6970
+ }
6971
+ }
6972
+ throw new Error(
6973
+ `Could not find @github/copilot package. Searched ${searchPaths.length} paths. Ensure it is installed, or pass cliPath/cliUrl to CopilotClient.`
6974
+ );
6416
6975
  }
6417
6976
  var import_node2, MIN_PROTOCOL_VERSION, CopilotClient;
6418
6977
  var init_client = __esm({
@@ -6422,6 +6981,7 @@ var init_client = __esm({
6422
6981
  init_rpc();
6423
6982
  init_sdkProtocolVersion();
6424
6983
  init_session();
6984
+ init_telemetry();
6425
6985
  MIN_PROTOCOL_VERSION = 2;
6426
6986
  CopilotClient = class {
6427
6987
  cliProcess = null;
@@ -6436,6 +6996,8 @@ var init_client = __esm({
6436
6996
  options;
6437
6997
  isExternalServer = false;
6438
6998
  forceStopping = false;
6999
+ onListModels;
7000
+ onGetTraceContext;
6439
7001
  modelsCache = null;
6440
7002
  modelsCacheLock = Promise.resolve();
6441
7003
  sessionLifecycleHandlers = /* @__PURE__ */ new Set();
@@ -6501,8 +7063,10 @@ var init_client = __esm({
6501
7063
  if (options.isChildProcess) {
6502
7064
  this.isExternalServer = true;
6503
7065
  }
7066
+ this.onListModels = options.onListModels;
7067
+ this.onGetTraceContext = options.onGetTraceContext;
6504
7068
  this.options = {
6505
- cliPath: options.cliPath || getBundledCliPath(),
7069
+ cliPath: options.cliUrl ? void 0 : options.cliPath || getBundledCliPath(),
6506
7070
  cliArgs: options.cliArgs ?? [],
6507
7071
  cwd: options.cwd ?? process.cwd(),
6508
7072
  port: options.port || 0,
@@ -6512,11 +7076,12 @@ var init_client = __esm({
6512
7076
  cliUrl: options.cliUrl,
6513
7077
  logLevel: options.logLevel || "debug",
6514
7078
  autoStart: options.autoStart ?? true,
6515
- autoRestart: options.autoRestart ?? true,
7079
+ autoRestart: false,
6516
7080
  env: options.env ?? process.env,
6517
7081
  githubToken: options.githubToken,
6518
7082
  // Default useLoggedInUser to false when githubToken is provided, otherwise true
6519
- useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true)
7083
+ useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true),
7084
+ telemetry: options.telemetry
6520
7085
  };
6521
7086
  }
6522
7087
  /**
@@ -6612,8 +7177,8 @@ var init_client = __esm({
6612
7177
  } catch (error) {
6613
7178
  lastError = error instanceof Error ? error : new Error(String(error));
6614
7179
  if (attempt < 3) {
6615
- const delay = 100 * Math.pow(2, attempt - 1);
6616
- await new Promise((resolve4) => setTimeout(resolve4, delay));
7180
+ const delay2 = 100 * Math.pow(2, attempt - 1);
7181
+ await new Promise((resolve4) => setTimeout(resolve4, delay2));
6617
7182
  }
6618
7183
  }
6619
7184
  }
@@ -6768,36 +7333,13 @@ var init_client = __esm({
6768
7333
  throw new Error("Client not connected. Call start() first.");
6769
7334
  }
6770
7335
  }
6771
- const response = await this.connection.sendRequest("session.create", {
6772
- model: config2.model,
6773
- sessionId: config2.sessionId,
6774
- clientName: config2.clientName,
6775
- reasoningEffort: config2.reasoningEffort,
6776
- tools: config2.tools?.map((tool) => ({
6777
- name: tool.name,
6778
- description: tool.description,
6779
- parameters: toJsonSchema(tool.parameters),
6780
- overridesBuiltInTool: tool.overridesBuiltInTool
6781
- })),
6782
- systemMessage: config2.systemMessage,
6783
- availableTools: config2.availableTools,
6784
- excludedTools: config2.excludedTools,
6785
- provider: config2.provider,
6786
- requestPermission: true,
6787
- requestUserInput: !!config2.onUserInputRequest,
6788
- hooks: !!(config2.hooks && Object.values(config2.hooks).some(Boolean)),
6789
- workingDirectory: config2.workingDirectory,
6790
- streaming: config2.streaming,
6791
- mcpServers: config2.mcpServers,
6792
- envValueMode: "direct",
6793
- customAgents: config2.customAgents,
6794
- configDir: config2.configDir,
6795
- skillDirectories: config2.skillDirectories,
6796
- disabledSkills: config2.disabledSkills,
6797
- infiniteSessions: config2.infiniteSessions
6798
- });
6799
- const { sessionId, workspacePath } = response;
6800
- const session = new CopilotSession(sessionId, this.connection, workspacePath);
7336
+ const sessionId = config2.sessionId ?? randomUUID();
7337
+ const session = new CopilotSession(
7338
+ sessionId,
7339
+ this.connection,
7340
+ void 0,
7341
+ this.onGetTraceContext
7342
+ );
6801
7343
  session.registerTools(config2.tools);
6802
7344
  session.registerPermissionHandler(config2.onPermissionRequest);
6803
7345
  if (config2.onUserInputRequest) {
@@ -6806,7 +7348,54 @@ var init_client = __esm({
6806
7348
  if (config2.hooks) {
6807
7349
  session.registerHooks(config2.hooks);
6808
7350
  }
7351
+ const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
7352
+ config2.systemMessage
7353
+ );
7354
+ if (transformCallbacks) {
7355
+ session.registerTransformCallbacks(transformCallbacks);
7356
+ }
7357
+ if (config2.onEvent) {
7358
+ session.on(config2.onEvent);
7359
+ }
6809
7360
  this.sessions.set(sessionId, session);
7361
+ try {
7362
+ const response = await this.connection.sendRequest("session.create", {
7363
+ ...await getTraceContext(this.onGetTraceContext),
7364
+ model: config2.model,
7365
+ sessionId,
7366
+ clientName: config2.clientName,
7367
+ reasoningEffort: config2.reasoningEffort,
7368
+ tools: config2.tools?.map((tool) => ({
7369
+ name: tool.name,
7370
+ description: tool.description,
7371
+ parameters: toJsonSchema(tool.parameters),
7372
+ overridesBuiltInTool: tool.overridesBuiltInTool,
7373
+ skipPermission: tool.skipPermission
7374
+ })),
7375
+ systemMessage: wireSystemMessage,
7376
+ availableTools: config2.availableTools,
7377
+ excludedTools: config2.excludedTools,
7378
+ provider: config2.provider,
7379
+ requestPermission: true,
7380
+ requestUserInput: !!config2.onUserInputRequest,
7381
+ hooks: !!(config2.hooks && Object.values(config2.hooks).some(Boolean)),
7382
+ workingDirectory: config2.workingDirectory,
7383
+ streaming: config2.streaming,
7384
+ mcpServers: config2.mcpServers,
7385
+ envValueMode: "direct",
7386
+ customAgents: config2.customAgents,
7387
+ agent: config2.agent,
7388
+ configDir: config2.configDir,
7389
+ skillDirectories: config2.skillDirectories,
7390
+ disabledSkills: config2.disabledSkills,
7391
+ infiniteSessions: config2.infiniteSessions
7392
+ });
7393
+ const { workspacePath } = response;
7394
+ session["_workspacePath"] = workspacePath;
7395
+ } catch (e) {
7396
+ this.sessions.delete(sessionId);
7397
+ throw e;
7398
+ }
6810
7399
  return session;
6811
7400
  }
6812
7401
  /**
@@ -6846,37 +7435,12 @@ var init_client = __esm({
6846
7435
  throw new Error("Client not connected. Call start() first.");
6847
7436
  }
6848
7437
  }
6849
- const response = await this.connection.sendRequest("session.resume", {
7438
+ const session = new CopilotSession(
6850
7439
  sessionId,
6851
- clientName: config2.clientName,
6852
- model: config2.model,
6853
- reasoningEffort: config2.reasoningEffort,
6854
- systemMessage: config2.systemMessage,
6855
- availableTools: config2.availableTools,
6856
- excludedTools: config2.excludedTools,
6857
- tools: config2.tools?.map((tool) => ({
6858
- name: tool.name,
6859
- description: tool.description,
6860
- parameters: toJsonSchema(tool.parameters),
6861
- overridesBuiltInTool: tool.overridesBuiltInTool
6862
- })),
6863
- provider: config2.provider,
6864
- requestPermission: true,
6865
- requestUserInput: !!config2.onUserInputRequest,
6866
- hooks: !!(config2.hooks && Object.values(config2.hooks).some(Boolean)),
6867
- workingDirectory: config2.workingDirectory,
6868
- configDir: config2.configDir,
6869
- streaming: config2.streaming,
6870
- mcpServers: config2.mcpServers,
6871
- envValueMode: "direct",
6872
- customAgents: config2.customAgents,
6873
- skillDirectories: config2.skillDirectories,
6874
- disabledSkills: config2.disabledSkills,
6875
- infiniteSessions: config2.infiniteSessions,
6876
- disableResume: config2.disableResume
6877
- });
6878
- const { sessionId: resumedSessionId, workspacePath } = response;
6879
- const session = new CopilotSession(resumedSessionId, this.connection, workspacePath);
7440
+ this.connection,
7441
+ void 0,
7442
+ this.onGetTraceContext
7443
+ );
6880
7444
  session.registerTools(config2.tools);
6881
7445
  session.registerPermissionHandler(config2.onPermissionRequest);
6882
7446
  if (config2.onUserInputRequest) {
@@ -6885,7 +7449,55 @@ var init_client = __esm({
6885
7449
  if (config2.hooks) {
6886
7450
  session.registerHooks(config2.hooks);
6887
7451
  }
6888
- this.sessions.set(resumedSessionId, session);
7452
+ const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
7453
+ config2.systemMessage
7454
+ );
7455
+ if (transformCallbacks) {
7456
+ session.registerTransformCallbacks(transformCallbacks);
7457
+ }
7458
+ if (config2.onEvent) {
7459
+ session.on(config2.onEvent);
7460
+ }
7461
+ this.sessions.set(sessionId, session);
7462
+ try {
7463
+ const response = await this.connection.sendRequest("session.resume", {
7464
+ ...await getTraceContext(this.onGetTraceContext),
7465
+ sessionId,
7466
+ clientName: config2.clientName,
7467
+ model: config2.model,
7468
+ reasoningEffort: config2.reasoningEffort,
7469
+ systemMessage: wireSystemMessage,
7470
+ availableTools: config2.availableTools,
7471
+ excludedTools: config2.excludedTools,
7472
+ tools: config2.tools?.map((tool) => ({
7473
+ name: tool.name,
7474
+ description: tool.description,
7475
+ parameters: toJsonSchema(tool.parameters),
7476
+ overridesBuiltInTool: tool.overridesBuiltInTool,
7477
+ skipPermission: tool.skipPermission
7478
+ })),
7479
+ provider: config2.provider,
7480
+ requestPermission: true,
7481
+ requestUserInput: !!config2.onUserInputRequest,
7482
+ hooks: !!(config2.hooks && Object.values(config2.hooks).some(Boolean)),
7483
+ workingDirectory: config2.workingDirectory,
7484
+ configDir: config2.configDir,
7485
+ streaming: config2.streaming,
7486
+ mcpServers: config2.mcpServers,
7487
+ envValueMode: "direct",
7488
+ customAgents: config2.customAgents,
7489
+ agent: config2.agent,
7490
+ skillDirectories: config2.skillDirectories,
7491
+ disabledSkills: config2.disabledSkills,
7492
+ infiniteSessions: config2.infiniteSessions,
7493
+ disableResume: config2.disableResume
7494
+ });
7495
+ const { workspacePath } = response;
7496
+ session["_workspacePath"] = workspacePath;
7497
+ } catch (e) {
7498
+ this.sessions.delete(sessionId);
7499
+ throw e;
7500
+ }
6889
7501
  return session;
6890
7502
  }
6891
7503
  /**
@@ -6946,15 +7558,15 @@ var init_client = __esm({
6946
7558
  /**
6947
7559
  * List available models with their metadata.
6948
7560
  *
7561
+ * If an `onListModels` handler was provided in the client options,
7562
+ * it is called instead of querying the CLI server.
7563
+ *
6949
7564
  * Results are cached after the first successful call to avoid rate limiting.
6950
7565
  * The cache is cleared when the client disconnects.
6951
7566
  *
6952
- * @throws Error if not authenticated
7567
+ * @throws Error if not connected (when no custom handler is set)
6953
7568
  */
6954
7569
  async listModels() {
6955
- if (!this.connection) {
6956
- throw new Error("Client not connected");
6957
- }
6958
7570
  await this.modelsCacheLock;
6959
7571
  let resolveLock;
6960
7572
  this.modelsCacheLock = new Promise((resolve4) => {
@@ -6964,10 +7576,18 @@ var init_client = __esm({
6964
7576
  if (this.modelsCache !== null) {
6965
7577
  return [...this.modelsCache];
6966
7578
  }
6967
- const result = await this.connection.sendRequest("models.list", {});
6968
- const response = result;
6969
- const models = response.models;
6970
- this.modelsCache = models;
7579
+ let models;
7580
+ if (this.onListModels) {
7581
+ models = await this.onListModels();
7582
+ } else {
7583
+ if (!this.connection) {
7584
+ throw new Error("Client not connected");
7585
+ }
7586
+ const result = await this.connection.sendRequest("models.list", {});
7587
+ const response = result;
7588
+ models = response.models;
7589
+ }
7590
+ this.modelsCache = [...models];
6971
7591
  return [...models];
6972
7592
  } finally {
6973
7593
  resolveLock();
@@ -7180,6 +7800,27 @@ var init_client = __esm({
7180
7800
  if (this.options.githubToken) {
7181
7801
  envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.githubToken;
7182
7802
  }
7803
+ if (!this.options.cliPath) {
7804
+ throw new Error(
7805
+ "Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI."
7806
+ );
7807
+ }
7808
+ if (this.options.telemetry) {
7809
+ const t = this.options.telemetry;
7810
+ envWithoutNodeDebug.COPILOT_OTEL_ENABLED = "true";
7811
+ if (t.otlpEndpoint !== void 0)
7812
+ envWithoutNodeDebug.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint;
7813
+ if (t.filePath !== void 0)
7814
+ envWithoutNodeDebug.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath;
7815
+ if (t.exporterType !== void 0)
7816
+ envWithoutNodeDebug.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType;
7817
+ if (t.sourceName !== void 0)
7818
+ envWithoutNodeDebug.COPILOT_OTEL_SOURCE_NAME = t.sourceName;
7819
+ if (t.captureContent !== void 0)
7820
+ envWithoutNodeDebug.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String(
7821
+ t.captureContent
7822
+ );
7823
+ }
7183
7824
  if (!existsSync4(this.options.cliPath)) {
7184
7825
  throw new Error(
7185
7826
  `Copilot CLI not found at ${this.options.cliPath}. Ensure @github/copilot is installed.`
@@ -7279,8 +7920,6 @@ stderr: ${stderrOutput}`
7279
7920
  } else {
7280
7921
  reject(new Error(`CLI server exited with code ${code}`));
7281
7922
  }
7282
- } else if (this.options.autoRestart && this.state === "connected") {
7283
- void this.reconnect();
7284
7923
  }
7285
7924
  });
7286
7925
  setTimeout(() => {
@@ -7385,12 +8024,15 @@ stderr: ${stderrOutput}`
7385
8024
  "hooks.invoke",
7386
8025
  async (params) => await this.handleHooksInvoke(params)
7387
8026
  );
8027
+ this.connection.onRequest(
8028
+ "systemMessage.transform",
8029
+ async (params) => await this.handleSystemMessageTransform(params)
8030
+ );
7388
8031
  this.connection.onClose(() => {
7389
- if (this.state === "connected" && this.options.autoRestart) {
7390
- void this.reconnect();
7391
- }
8032
+ this.state = "disconnected";
7392
8033
  });
7393
8034
  this.connection.onError((_error) => {
8035
+ this.state = "disconnected";
7394
8036
  });
7395
8037
  }
7396
8038
  handleSessionEventNotification(notification) {
@@ -7449,6 +8091,16 @@ stderr: ${stderrOutput}`
7449
8091
  const output = await session._handleHooksInvoke(params.hookType, params.input);
7450
8092
  return { output };
7451
8093
  }
8094
+ async handleSystemMessageTransform(params) {
8095
+ if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
8096
+ throw new Error("Invalid systemMessage.transform payload");
8097
+ }
8098
+ const session = this.sessions.get(params.sessionId);
8099
+ if (!session) {
8100
+ throw new Error(`Session not found: ${params.sessionId}`);
8101
+ }
8102
+ return await session._handleSystemMessageTransform(params.sections);
8103
+ }
7452
8104
  // ========================================================================
7453
8105
  // Protocol v2 backward-compatibility adapters
7454
8106
  // ========================================================================
@@ -7477,11 +8129,15 @@ stderr: ${stderrOutput}`
7477
8129
  };
7478
8130
  }
7479
8131
  try {
8132
+ const traceparent = params.traceparent;
8133
+ const tracestate = params.tracestate;
7480
8134
  const invocation = {
7481
8135
  sessionId: params.sessionId,
7482
8136
  toolCallId: params.toolCallId,
7483
8137
  toolName: params.toolName,
7484
- arguments: params.arguments
8138
+ arguments: params.arguments,
8139
+ traceparent,
8140
+ tracestate
7485
8141
  };
7486
8142
  const result = await handler(params.arguments, invocation);
7487
8143
  return { result: this.normalizeToolResultV2(result) };
@@ -7511,7 +8167,10 @@ stderr: ${stderrOutput}`
7511
8167
  try {
7512
8168
  const result = await session._handlePermissionRequestV2(params.permissionRequest);
7513
8169
  return { result };
7514
- } catch (_error) {
8170
+ } catch (error) {
8171
+ if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
8172
+ throw error;
8173
+ }
7515
8174
  return {
7516
8175
  result: {
7517
8176
  kind: "denied-no-approval-rule-and-could-not-request-from-user"
@@ -7541,24 +8200,13 @@ stderr: ${stderrOutput}`
7541
8200
  isToolResultObject(value) {
7542
8201
  return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
7543
8202
  }
7544
- /**
7545
- * Attempt to reconnect to the server
7546
- */
7547
- async reconnect() {
7548
- this.state = "disconnected";
7549
- try {
7550
- await this.stop();
7551
- await this.start();
7552
- } catch (_error) {
7553
- }
7554
- }
7555
8203
  };
7556
8204
  }
7557
8205
  });
7558
8206
 
7559
8207
  // node_modules/@github/copilot-sdk/dist/types.js
7560
8208
  var approveAll;
7561
- var init_types2 = __esm({
8209
+ var init_types3 = __esm({
7562
8210
  "node_modules/@github/copilot-sdk/dist/types.js"() {
7563
8211
  "use strict";
7564
8212
  approveAll = () => ({ kind: "approved" });
@@ -7571,21 +8219,21 @@ var init_dist = __esm({
7571
8219
  "use strict";
7572
8220
  init_client();
7573
8221
  init_session();
7574
- init_types2();
8222
+ init_types3();
7575
8223
  }
7576
8224
  });
7577
8225
 
7578
8226
  // src/L1-infra/ai/copilot.ts
7579
8227
  import { existsSync as existsSync5 } from "fs";
7580
8228
  import { join as join7, dirname as dirname4 } from "path";
7581
- import { createRequire as createRequire2 } from "module";
8229
+ import { createRequire as createRequire3 } from "module";
7582
8230
  function resolveCopilotCliPath() {
7583
8231
  const platform = process.platform;
7584
8232
  const arch = process.arch;
7585
8233
  const binaryName = platform === "win32" ? "copilot.exe" : "copilot";
7586
8234
  const platformPkg = `@github/copilot-${platform}-${arch}`;
7587
8235
  try {
7588
- const require_ = createRequire2(import.meta.url);
8236
+ const require_ = createRequire3(import.meta.url);
7589
8237
  const searchPaths = require_.resolve.paths(platformPkg) ?? [];
7590
8238
  for (const base of searchPaths) {
7591
8239
  const candidate = join7(base, platformPkg, binaryName);
@@ -9804,7 +10452,7 @@ var STATUS_LABEL_PREFIX, PLATFORM_LABEL_PREFIX, PRIORITY_LABEL_PREFIX, COMMENT_M
9804
10452
  var init_ideaService = __esm({
9805
10453
  "src/L3-services/ideaService/ideaService.ts"() {
9806
10454
  "use strict";
9807
- init_types();
10455
+ init_types2();
9808
10456
  init_githubClient();
9809
10457
  init_environment();
9810
10458
  init_configLogger();
@@ -10229,7 +10877,7 @@ async function itemExists(id) {
10229
10877
  var init_postStore = __esm({
10230
10878
  "src/L3-services/postStore/postStore.ts"() {
10231
10879
  "use strict";
10232
- init_types();
10880
+ init_types2();
10233
10881
  init_environment();
10234
10882
  init_configLogger();
10235
10883
  init_fileSystem();
@@ -10396,6 +11044,31 @@ var init_lateApi = __esm({
10396
11044
  }
10397
11045
  return allPosts;
10398
11046
  }
11047
+ // ── Queue management ─────────────────────────────────────────────────
11048
+ async listQueues(profileId, all = false) {
11049
+ const params = new URLSearchParams({ profileId });
11050
+ if (all) params.set("all", "true");
11051
+ return this.request(`/queue/slots?${params}`);
11052
+ }
11053
+ async createQueue(params) {
11054
+ return this.request("/queue/slots", { method: "POST", body: JSON.stringify(params) });
11055
+ }
11056
+ async updateQueue(params) {
11057
+ return this.request("/queue/slots", { method: "PUT", body: JSON.stringify(params) });
11058
+ }
11059
+ async deleteQueue(profileId, queueId) {
11060
+ return this.request(`/queue/slots?profileId=${profileId}&queueId=${queueId}`, { method: "DELETE" });
11061
+ }
11062
+ async previewQueue(profileId, queueId, count = 20) {
11063
+ const params = new URLSearchParams({ profileId, count: String(count) });
11064
+ if (queueId) params.set("queueId", queueId);
11065
+ return this.request(`/queue/preview?${params}`);
11066
+ }
11067
+ async getNextQueueSlot(profileId, queueId) {
11068
+ const params = new URLSearchParams({ profileId });
11069
+ if (queueId) params.set("queueId", queueId);
11070
+ return this.request(`/queue/next-slot?${params}`);
11071
+ }
10399
11072
  // ── Helper ───────────────────────────────────────────────────────────
10400
11073
  async validateConnection() {
10401
11074
  try {
@@ -11254,81 +11927,396 @@ async function rescheduleAllPosts(options) {
11254
11927
  }
11255
11928
  if (!dryRun) {
11256
11929
  try {
11257
- await lateClient.schedulePost(latePostId, newSlotDatetime);
11258
- } catch (scheduleErr) {
11259
- const errMsg = scheduleErr instanceof Error ? scheduleErr.message : String(scheduleErr);
11260
- if (errMsg.includes("Published posts can only have their recycling config updated")) {
11261
- logger_default.info(`Skipping ${label}: post already published on platform`);
11262
- result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: "Already published \u2014 skipped" });
11263
- result.unchanged++;
11264
- continue;
11265
- }
11266
- throw scheduleErr;
11930
+ await lateClient.schedulePost(latePostId, newSlotDatetime);
11931
+ } catch (scheduleErr) {
11932
+ const errMsg = scheduleErr instanceof Error ? scheduleErr.message : String(scheduleErr);
11933
+ if (errMsg.includes("Published posts can only have their recycling config updated")) {
11934
+ logger_default.info(`Skipping ${label}: post already published on platform`);
11935
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: "Already published \u2014 skipped" });
11936
+ result.unchanged++;
11937
+ continue;
11938
+ }
11939
+ throw scheduleErr;
11940
+ }
11941
+ await updatePublishedItemSchedule(item.id, newSlotDatetime);
11942
+ }
11943
+ if (oldSlot) {
11944
+ const oldMs = normalizeDateTime(oldSlot);
11945
+ const oldBooked = bookedMap.get(oldMs);
11946
+ if (oldBooked?.postId === latePostId) {
11947
+ bookedMap.delete(oldMs);
11948
+ }
11949
+ }
11950
+ bookedMap.set(newSlotMs, {
11951
+ scheduledFor: newSlotDatetime,
11952
+ source: "late",
11953
+ postId: latePostId,
11954
+ platform: itemPlatform,
11955
+ ideaLinked: isIdea,
11956
+ ideaIds: item.metadata.ideaIds
11957
+ });
11958
+ logger_default.info(`Rescheduled ${label}: ${oldSlot ?? "unscheduled"} \u2192 ${newSlotDatetime}`);
11959
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: newSlotDatetime });
11960
+ result.rescheduled++;
11961
+ } catch (err) {
11962
+ const msg = err instanceof Error ? err.message : String(err);
11963
+ logger_default.error(`Failed to reschedule ${label}: ${msg}`);
11964
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: msg });
11965
+ result.failed++;
11966
+ }
11967
+ }
11968
+ logger_default.info(`Reschedule complete: ${result.rescheduled} moved, ${result.unchanged} unchanged, ${result.failed} failed`);
11969
+ return result;
11970
+ }
11971
+ async function rescheduleIdeaPosts(options) {
11972
+ return rescheduleAllPosts(options);
11973
+ }
11974
+ async function getScheduleCalendar(startDate, endDate) {
11975
+ const bookedMap = await buildBookedMap();
11976
+ let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
11977
+ platform: slot.platform,
11978
+ scheduledFor: slot.scheduledFor,
11979
+ source: slot.source,
11980
+ postId: slot.postId,
11981
+ itemId: slot.itemId
11982
+ }));
11983
+ if (startDate) {
11984
+ const startMs = startDate.getTime();
11985
+ filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) >= startMs);
11986
+ }
11987
+ if (endDate) {
11988
+ const endMs = endDate.getTime();
11989
+ filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) <= endMs);
11990
+ }
11991
+ filtered.sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
11992
+ return filtered;
11993
+ }
11994
+ var MAX_LOOKAHEAD_DAYS, DAY_MS, HOUR_MS;
11995
+ var init_scheduler = __esm({
11996
+ "src/L3-services/scheduler/scheduler.ts"() {
11997
+ "use strict";
11998
+ init_lateApi();
11999
+ init_configLogger();
12000
+ init_postStore();
12001
+ init_scheduleConfig();
12002
+ MAX_LOOKAHEAD_DAYS = 730;
12003
+ DAY_MS = 24 * 60 * 60 * 1e3;
12004
+ HOUR_MS = 60 * 60 * 1e3;
12005
+ }
12006
+ });
12007
+
12008
+ // src/L3-services/queueMapping/queueMapping.ts
12009
+ function cachePath() {
12010
+ return join(process.cwd(), CACHE_FILE);
12011
+ }
12012
+ function isCacheValid(cache2) {
12013
+ const fetchedAtTime = new Date(cache2.fetchedAt).getTime();
12014
+ if (Number.isNaN(fetchedAtTime)) {
12015
+ logger_default.warn("Invalid fetchedAt in queue cache; treating as stale", {
12016
+ fetchedAt: cache2.fetchedAt
12017
+ });
12018
+ return false;
12019
+ }
12020
+ const age = Date.now() - fetchedAtTime;
12021
+ return age < CACHE_TTL_MS;
12022
+ }
12023
+ async function readFileCache() {
12024
+ try {
12025
+ const raw = await readTextFile(cachePath());
12026
+ const cache2 = JSON.parse(raw);
12027
+ if (cache2.mappings && cache2.profileId && cache2.fetchedAt && isCacheValid(cache2)) {
12028
+ return cache2;
12029
+ }
12030
+ return null;
12031
+ } catch {
12032
+ return null;
12033
+ }
12034
+ }
12035
+ async function writeFileCache(cache2) {
12036
+ try {
12037
+ if (!cache2 || typeof cache2 !== "object" || !cache2.mappings || !cache2.profileId || !cache2.fetchedAt) {
12038
+ logger_default.warn("Invalid queue cache structure, skipping write");
12039
+ return;
12040
+ }
12041
+ const sanitized = {
12042
+ mappings: typeof cache2.mappings === "object" ? { ...cache2.mappings } : {},
12043
+ profileId: String(cache2.profileId),
12044
+ fetchedAt: String(cache2.fetchedAt)
12045
+ };
12046
+ for (const [name, id] of Object.entries(sanitized.mappings)) {
12047
+ if (typeof name !== "string" || typeof id !== "string" || /[\x00-\x1f]/.test(name) || /[\x00-\x1f]/.test(id)) {
12048
+ logger_default.warn("Invalid queue mapping data from API, skipping cache write");
12049
+ return;
12050
+ }
12051
+ }
12052
+ const resolvedCachePath = resolve(cachePath());
12053
+ if (!resolvedCachePath.startsWith(resolve(process.cwd()) + sep)) {
12054
+ throw new Error("Cache path outside working directory");
12055
+ }
12056
+ await writeTextFile(resolvedCachePath, JSON.stringify(sanitized, null, 2));
12057
+ } catch (err) {
12058
+ logger_default.warn("Failed to write queue cache file", { error: err });
12059
+ }
12060
+ }
12061
+ async function fetchAndCache() {
12062
+ const client = new LateApiClient();
12063
+ const profiles = await client.listProfiles();
12064
+ if (profiles.length === 0) {
12065
+ logger_default.warn("No Late API profiles found \u2014 queue mappings will be empty");
12066
+ const emptyCache = {
12067
+ mappings: {},
12068
+ profileId: "",
12069
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
12070
+ };
12071
+ memoryCache = emptyCache;
12072
+ return emptyCache;
12073
+ }
12074
+ const profileId = profiles[0]._id;
12075
+ const { queues } = await client.listQueues(profileId, true);
12076
+ if (queues.length === 0) {
12077
+ logger_default.warn(
12078
+ "No queues found in Late API \u2014 run `vidpipe sync-queues` to create platform queues"
12079
+ );
12080
+ }
12081
+ const mappings = {};
12082
+ for (const queue2 of queues) {
12083
+ mappings[queue2.name] = queue2._id;
12084
+ }
12085
+ const cache2 = {
12086
+ mappings,
12087
+ profileId,
12088
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
12089
+ };
12090
+ memoryCache = cache2;
12091
+ await writeFileCache(cache2);
12092
+ logger_default.info("Refreshed Late queue mappings", {
12093
+ queueCount: queues.length,
12094
+ queues: Object.keys(mappings)
12095
+ });
12096
+ return cache2;
12097
+ }
12098
+ async function ensureMappings() {
12099
+ if (memoryCache && isCacheValid(memoryCache)) {
12100
+ return memoryCache;
12101
+ }
12102
+ const fileCache = await readFileCache();
12103
+ if (fileCache) {
12104
+ memoryCache = fileCache;
12105
+ return fileCache;
12106
+ }
12107
+ try {
12108
+ return await fetchAndCache();
12109
+ } catch (err) {
12110
+ logger_default.error("Failed to fetch Late queue mappings", { error: err });
12111
+ return { mappings: {}, profileId: "", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
12112
+ }
12113
+ }
12114
+ async function getQueueId(platform, clipType) {
12115
+ const cache2 = await ensureMappings();
12116
+ const normalizedPlatform = platform === "twitter" ? "x" : platform;
12117
+ const queueName = `${normalizedPlatform}-${clipType}`;
12118
+ return cache2.mappings[queueName] ?? null;
12119
+ }
12120
+ async function getProfileId() {
12121
+ const cache2 = await ensureMappings();
12122
+ return cache2.profileId;
12123
+ }
12124
+ async function refreshQueueMappings() {
12125
+ memoryCache = null;
12126
+ const cache2 = await fetchAndCache();
12127
+ return { ...cache2.mappings };
12128
+ }
12129
+ var CACHE_FILE, CACHE_TTL_MS, memoryCache;
12130
+ var init_queueMapping = __esm({
12131
+ "src/L3-services/queueMapping/queueMapping.ts"() {
12132
+ "use strict";
12133
+ init_lateApi();
12134
+ init_configLogger();
12135
+ init_fileSystem();
12136
+ init_paths();
12137
+ CACHE_FILE = ".vidpipe-queue-cache.json";
12138
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
12139
+ memoryCache = null;
12140
+ }
12141
+ });
12142
+
12143
+ // src/L3-services/queueSync/queueSync.ts
12144
+ var queueSync_exports = {};
12145
+ __export(queueSync_exports, {
12146
+ syncQueuesToLate: () => syncQueuesToLate
12147
+ });
12148
+ function normalizePlatformName(platform) {
12149
+ return PLATFORM_NAME_MAP[platform] ?? platform;
12150
+ }
12151
+ function slotsToSortedKey(slots) {
12152
+ const sorted = [...slots].sort(
12153
+ (a, b) => a.dayOfWeek !== b.dayOfWeek ? a.dayOfWeek - b.dayOfWeek : a.time.localeCompare(b.time)
12154
+ );
12155
+ return JSON.stringify(sorted);
12156
+ }
12157
+ function delay(ms) {
12158
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
12159
+ }
12160
+ function buildDesiredQueues(config2) {
12161
+ const queues = [];
12162
+ for (const [platformKey, platformSchedule] of Object.entries(config2.platforms)) {
12163
+ const normalizedPlatform = normalizePlatformName(platformKey);
12164
+ if (!platformSchedule.byClipType) continue;
12165
+ for (const [clipType, clipTypeSchedule] of Object.entries(platformSchedule.byClipType)) {
12166
+ const queueName = `${normalizedPlatform}-${clipType}`;
12167
+ const slots = [];
12168
+ for (const timeSlot of clipTypeSchedule.slots) {
12169
+ for (const day of timeSlot.days) {
12170
+ slots.push({ dayOfWeek: DAY_MAP[day], time: timeSlot.time });
12171
+ }
12172
+ }
12173
+ if (slots.length > 0) {
12174
+ queues.push({ name: queueName, slots });
12175
+ }
12176
+ }
12177
+ }
12178
+ return queues;
12179
+ }
12180
+ async function syncQueuesToLate(options) {
12181
+ const reshuffle = options?.reshuffle ?? false;
12182
+ const dryRun = options?.dryRun ?? false;
12183
+ const deleteOrphans = options?.deleteOrphans ?? false;
12184
+ const result = {
12185
+ created: [],
12186
+ updated: [],
12187
+ deleted: [],
12188
+ unchanged: [],
12189
+ errors: []
12190
+ };
12191
+ const config2 = await loadScheduleConfig();
12192
+ const timezone = config2.timezone;
12193
+ const client = new LateApiClient();
12194
+ const profiles = await client.listProfiles();
12195
+ if (profiles.length === 0) {
12196
+ throw new Error("No Late API profiles found \u2014 cannot sync queues");
12197
+ }
12198
+ const profileId = profiles[0]._id;
12199
+ logger_default.info(`Using Late profile "${profiles[0].name}" (${profileId})`);
12200
+ const desiredQueues = buildDesiredQueues(config2);
12201
+ logger_default.info(`Built ${desiredQueues.length} queue definitions from schedule.json`);
12202
+ const existingData = await client.listQueues(profileId, true);
12203
+ const existingQueues = existingData.queues.map((q) => ({
12204
+ _id: q._id,
12205
+ name: q.name,
12206
+ slots: q.slots,
12207
+ active: q.active
12208
+ }));
12209
+ logger_default.info(`Found ${existingQueues.length} existing queues in Late`);
12210
+ const existingByName = /* @__PURE__ */ new Map();
12211
+ for (const eq of existingQueues) {
12212
+ existingByName.set(eq.name, eq);
12213
+ }
12214
+ const desiredNames = /* @__PURE__ */ new Set();
12215
+ for (const desired of desiredQueues) {
12216
+ desiredNames.add(desired.name);
12217
+ const existing = existingByName.get(desired.name);
12218
+ if (existing) {
12219
+ const existingKey = slotsToSortedKey(existing.slots);
12220
+ const desiredKey = slotsToSortedKey(desired.slots);
12221
+ if (existingKey === desiredKey) {
12222
+ logger_default.debug(`Queue "${desired.name}" unchanged`);
12223
+ result.unchanged.push(desired.name);
12224
+ } else {
12225
+ logger_default.info(`Queue "${desired.name}" slots differ \u2014 updating`);
12226
+ if (!dryRun) {
12227
+ try {
12228
+ await client.updateQueue({
12229
+ profileId,
12230
+ queueId: existing._id,
12231
+ name: desired.name,
12232
+ timezone,
12233
+ slots: desired.slots,
12234
+ reshuffleExisting: reshuffle
12235
+ });
12236
+ result.updated.push(desired.name);
12237
+ } catch (err) {
12238
+ const message = err instanceof Error ? err.message : String(err);
12239
+ logger_default.error(`Failed to update queue "${desired.name}": ${message}`);
12240
+ result.errors.push({ queueName: desired.name, error: message });
12241
+ }
12242
+ } else {
12243
+ logger_default.info(`[DRY RUN] Would update queue "${desired.name}"`);
12244
+ result.updated.push(desired.name);
12245
+ }
12246
+ await delay(RATE_LIMIT_DELAY_MS);
12247
+ }
12248
+ } else {
12249
+ logger_default.info(`Queue "${desired.name}" does not exist \u2014 creating`);
12250
+ if (!dryRun) {
12251
+ try {
12252
+ await client.createQueue({
12253
+ profileId,
12254
+ name: desired.name,
12255
+ timezone,
12256
+ slots: desired.slots,
12257
+ active: true
12258
+ });
12259
+ result.created.push(desired.name);
12260
+ } catch (err) {
12261
+ const message = err instanceof Error ? err.message : String(err);
12262
+ logger_default.error(`Failed to create queue "${desired.name}": ${message}`);
12263
+ result.errors.push({ queueName: desired.name, error: message });
11267
12264
  }
11268
- await updatePublishedItemSchedule(item.id, newSlotDatetime);
12265
+ } else {
12266
+ logger_default.info(`[DRY RUN] Would create queue "${desired.name}"`);
12267
+ result.created.push(desired.name);
11269
12268
  }
11270
- if (oldSlot) {
11271
- const oldMs = normalizeDateTime(oldSlot);
11272
- const oldBooked = bookedMap.get(oldMs);
11273
- if (oldBooked?.postId === latePostId) {
11274
- bookedMap.delete(oldMs);
12269
+ await delay(RATE_LIMIT_DELAY_MS);
12270
+ }
12271
+ }
12272
+ if (deleteOrphans) {
12273
+ for (const existing of existingQueues) {
12274
+ if (!desiredNames.has(existing.name)) {
12275
+ logger_default.info(`Orphan queue "${existing.name}" \u2014 deleting`);
12276
+ if (!dryRun) {
12277
+ try {
12278
+ await client.deleteQueue(profileId, existing._id);
12279
+ result.deleted.push(existing.name);
12280
+ } catch (err) {
12281
+ const message = err instanceof Error ? err.message : String(err);
12282
+ logger_default.error(`Failed to delete orphan queue "${existing.name}": ${message}`);
12283
+ result.errors.push({ queueName: existing.name, error: message });
12284
+ }
12285
+ } else {
12286
+ logger_default.info(`[DRY RUN] Would delete orphan queue "${existing.name}"`);
12287
+ result.deleted.push(existing.name);
11275
12288
  }
12289
+ await delay(RATE_LIMIT_DELAY_MS);
11276
12290
  }
11277
- bookedMap.set(newSlotMs, {
11278
- scheduledFor: newSlotDatetime,
11279
- source: "late",
11280
- postId: latePostId,
11281
- platform: itemPlatform,
11282
- ideaLinked: isIdea,
11283
- ideaIds: item.metadata.ideaIds
11284
- });
11285
- logger_default.info(`Rescheduled ${label}: ${oldSlot ?? "unscheduled"} \u2192 ${newSlotDatetime}`);
11286
- result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: newSlotDatetime });
11287
- result.rescheduled++;
11288
- } catch (err) {
11289
- const msg = err instanceof Error ? err.message : String(err);
11290
- logger_default.error(`Failed to reschedule ${label}: ${msg}`);
11291
- result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: msg });
11292
- result.failed++;
11293
12291
  }
11294
12292
  }
11295
- logger_default.info(`Reschedule complete: ${result.rescheduled} moved, ${result.unchanged} unchanged, ${result.failed} failed`);
11296
- return result;
11297
- }
11298
- async function rescheduleIdeaPosts(options) {
11299
- return rescheduleAllPosts(options);
11300
- }
11301
- async function getScheduleCalendar(startDate, endDate) {
11302
- const bookedMap = await buildBookedMap();
11303
- let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
11304
- platform: slot.platform,
11305
- scheduledFor: slot.scheduledFor,
11306
- source: slot.source,
11307
- postId: slot.postId,
11308
- itemId: slot.itemId
11309
- }));
11310
- if (startDate) {
11311
- const startMs = startDate.getTime();
11312
- filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) >= startMs);
11313
- }
11314
- if (endDate) {
11315
- const endMs = endDate.getTime();
11316
- filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) <= endMs);
12293
+ if (!dryRun) {
12294
+ await refreshQueueMappings();
11317
12295
  }
11318
- filtered.sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
11319
- return filtered;
12296
+ logger_default.info(
12297
+ `Queue sync complete: ${result.created.length} created, ${result.updated.length} updated, ${result.deleted.length} deleted, ${result.unchanged.length} unchanged, ${result.errors.length} errors`
12298
+ );
12299
+ return result;
11320
12300
  }
11321
- var MAX_LOOKAHEAD_DAYS, DAY_MS, HOUR_MS;
11322
- var init_scheduler = __esm({
11323
- "src/L3-services/scheduler/scheduler.ts"() {
12301
+ var DAY_MAP, PLATFORM_NAME_MAP, RATE_LIMIT_DELAY_MS;
12302
+ var init_queueSync = __esm({
12303
+ "src/L3-services/queueSync/queueSync.ts"() {
11324
12304
  "use strict";
11325
12305
  init_lateApi();
11326
- init_configLogger();
11327
- init_postStore();
11328
12306
  init_scheduleConfig();
11329
- MAX_LOOKAHEAD_DAYS = 730;
11330
- DAY_MS = 24 * 60 * 60 * 1e3;
11331
- HOUR_MS = 60 * 60 * 1e3;
12307
+ init_queueMapping();
12308
+ init_configLogger();
12309
+ DAY_MAP = {
12310
+ sun: 0,
12311
+ mon: 1,
12312
+ tue: 2,
12313
+ wed: 3,
12314
+ thu: 4,
12315
+ fri: 5,
12316
+ sat: 6
12317
+ };
12318
+ PLATFORM_NAME_MAP = { twitter: "x" };
12319
+ RATE_LIMIT_DELAY_MS = 200;
11332
12320
  }
11333
12321
  });
11334
12322
 
@@ -11502,6 +12490,13 @@ function formatDate2(iso, timezone) {
11502
12490
  }
11503
12491
  async function runReschedule(options = {}) {
11504
12492
  initConfig();
12493
+ if (options.queue) {
12494
+ const { syncQueuesToLate: syncQueuesToLate2 } = await Promise.resolve().then(() => (init_queueSync(), queueSync_exports));
12495
+ logger_default.info("Using Late API queue reshuffle for rescheduling...");
12496
+ const result2 = await syncQueuesToLate2({ reshuffle: true, dryRun: options.dryRun });
12497
+ logger_default.info(`Queue reshuffle complete: ${result2.updated.length} queues reshuffled`);
12498
+ return;
12499
+ }
11505
12500
  const scheduleConfig = await loadScheduleConfig();
11506
12501
  const { timezone } = scheduleConfig;
11507
12502
  if (options.dryRun) {
@@ -11543,6 +12538,7 @@ var init_reschedule = __esm({
11543
12538
  init_environment();
11544
12539
  init_scheduler();
11545
12540
  init_scheduleConfig();
12541
+ init_configLogger();
11546
12542
  PLATFORM_ICON2 = {
11547
12543
  tiktok: "\u{1F3B5}",
11548
12544
  youtube: "\u25B6\uFE0F",
@@ -11554,6 +12550,132 @@ var init_reschedule = __esm({
11554
12550
  }
11555
12551
  });
11556
12552
 
12553
+ // src/L7-app/commands/syncQueues.ts
12554
+ var syncQueues_exports = {};
12555
+ __export(syncQueues_exports, {
12556
+ runSyncQueues: () => runSyncQueues
12557
+ });
12558
+ async function runSyncQueues(options = {}) {
12559
+ initConfig();
12560
+ logger_default.info("Syncing schedule.json to Late API queues...");
12561
+ if (options.dryRun) logger_default.info("[DRY RUN] No changes will be made");
12562
+ const result = await syncQueuesToLate({
12563
+ reshuffle: options.reshuffle,
12564
+ dryRun: options.dryRun,
12565
+ deleteOrphans: options.deleteOrphans
12566
+ });
12567
+ if (result.created.length > 0) {
12568
+ logger_default.info(`Created ${result.created.length} queues: ${result.created.join(", ")}`);
12569
+ }
12570
+ if (result.updated.length > 0) {
12571
+ logger_default.info(`Updated ${result.updated.length} queues: ${result.updated.join(", ")}`);
12572
+ }
12573
+ if (result.deleted.length > 0) {
12574
+ logger_default.info(`Deleted ${result.deleted.length} queues: ${result.deleted.join(", ")}`);
12575
+ }
12576
+ if (result.unchanged.length > 0) {
12577
+ logger_default.info(`Unchanged: ${result.unchanged.length} queues`);
12578
+ }
12579
+ if (result.errors.length > 0) {
12580
+ for (const err of result.errors) {
12581
+ logger_default.error(`Error syncing ${err.queueName}: ${err.error}`);
12582
+ }
12583
+ }
12584
+ const total = result.created.length + result.updated.length + result.deleted.length + result.unchanged.length;
12585
+ logger_default.info(`Sync complete: ${total} queues processed`);
12586
+ }
12587
+ var init_syncQueues = __esm({
12588
+ "src/L7-app/commands/syncQueues.ts"() {
12589
+ "use strict";
12590
+ init_environment();
12591
+ init_configLogger();
12592
+ init_queueSync();
12593
+ }
12594
+ });
12595
+
12596
+ // src/L1-infra/spec/specLoader.ts
12597
+ var specLoader_exports = {};
12598
+ __export(specLoader_exports, {
12599
+ loadSpec: () => loadSpec
12600
+ });
12601
+ import { readFile, readdir } from "fs/promises";
12602
+ import { join as join8, extname as extname2 } from "path";
12603
+ import { parse as parseYaml } from "yaml";
12604
+ function isFilePath(nameOrPath) {
12605
+ return nameOrPath.includes("/") || nameOrPath.includes("\\") || nameOrPath.endsWith(".yaml") || nameOrPath.endsWith(".yml") || nameOrPath.endsWith(".json");
12606
+ }
12607
+ function parseFileContent(raw, filePath) {
12608
+ const ext = extname2(filePath).toLowerCase();
12609
+ if (ext === ".json") {
12610
+ return JSON.parse(raw);
12611
+ }
12612
+ return parseYaml(raw);
12613
+ }
12614
+ async function fileExists4(filePath) {
12615
+ try {
12616
+ await readFile(filePath);
12617
+ return true;
12618
+ } catch {
12619
+ return false;
12620
+ }
12621
+ }
12622
+ async function listSpecFiles(specsDir) {
12623
+ try {
12624
+ const entries = await readdir(specsDir);
12625
+ return entries.filter((e) => e.endsWith(".yaml") || e.endsWith(".yml") || e.endsWith(".json"));
12626
+ } catch {
12627
+ return [];
12628
+ }
12629
+ }
12630
+ function validateAndMerge(raw, source) {
12631
+ const errors = validateSpec(raw);
12632
+ if (errors.length > 0) {
12633
+ const details = errors.map((e) => ` - ${e.path ? e.path + ": " : ""}${e.message}`).join("\n");
12634
+ throw new Error(`Invalid pipeline spec from ${source}:
12635
+ ${details}`);
12636
+ }
12637
+ return mergeWithDefaults(raw);
12638
+ }
12639
+ async function loadSpec(nameOrPath, repoRoot) {
12640
+ if (isFilePath(nameOrPath)) {
12641
+ let raw;
12642
+ try {
12643
+ raw = await readFile(nameOrPath, "utf-8");
12644
+ } catch (err) {
12645
+ throw new Error(`Failed to read spec file '${nameOrPath}': ${err.message}`);
12646
+ }
12647
+ const parsed = parseFileContent(raw, nameOrPath);
12648
+ return validateAndMerge(parsed, nameOrPath);
12649
+ }
12650
+ if (isPresetName(nameOrPath)) {
12651
+ return getPreset(nameOrPath);
12652
+ }
12653
+ const specsDir = join8(repoRoot, "pipeline-specs");
12654
+ const conventionPath = join8(specsDir, `${nameOrPath}.yaml`);
12655
+ if (await fileExists4(conventionPath)) {
12656
+ const raw = await readFile(conventionPath, "utf-8");
12657
+ const parsed = parseFileContent(raw, conventionPath);
12658
+ return validateAndMerge(parsed, conventionPath);
12659
+ }
12660
+ const availableFiles = await listSpecFiles(specsDir);
12661
+ const presetList = ["full", "clean", "minimal"];
12662
+ const parts = [`Unknown spec '${nameOrPath}'.`];
12663
+ parts.push(` Built-in presets: ${presetList.join(", ")}`);
12664
+ if (availableFiles.length > 0) {
12665
+ const names = availableFiles.map((f) => f.replace(/\.(yaml|yml|json)$/, ""));
12666
+ parts.push(` Custom specs in pipeline-specs/: ${names.join(", ")}`);
12667
+ } else {
12668
+ parts.push(` No custom specs found in pipeline-specs/`);
12669
+ }
12670
+ throw new Error(parts.join("\n"));
12671
+ }
12672
+ var init_specLoader = __esm({
12673
+ "src/L1-infra/spec/specLoader.ts"() {
12674
+ "use strict";
12675
+ init_pipelineSpec();
12676
+ }
12677
+ });
12678
+
11557
12679
  // src/L1-infra/cli/cli.ts
11558
12680
  import { Command } from "commander";
11559
12681
  import readline from "readline";
@@ -11577,7 +12699,7 @@ init_environment();
11577
12699
  init_paths();
11578
12700
  init_fileSystem();
11579
12701
  init_configLogger();
11580
- init_types();
12702
+ init_types2();
11581
12703
  var FileWatcher = class _FileWatcher extends EventEmitter {
11582
12704
  watchFolder;
11583
12705
  watcher = null;
@@ -13260,7 +14382,7 @@ var SocialPostAsset = class extends TextAsset {
13260
14382
  // src/L5-assets/ShortVideoAsset.ts
13261
14383
  init_paths();
13262
14384
  init_fileSystem();
13263
- init_types();
14385
+ init_types2();
13264
14386
 
13265
14387
  // src/L5-assets/thumbnailGeneration.ts
13266
14388
  init_paths();
@@ -13520,7 +14642,7 @@ var ShortVideoAsset = class extends VideoAsset {
13520
14642
  // src/L5-assets/MediumClipAsset.ts
13521
14643
  init_paths();
13522
14644
  init_fileSystem();
13523
- init_types();
14645
+ init_types2();
13524
14646
  var MediumClipAsset = class extends VideoAsset {
13525
14647
  /** Parent video this clip was extracted from */
13526
14648
  parent;
@@ -13930,7 +15052,39 @@ init_videoOperations();
13930
15052
  init_fileSystem();
13931
15053
  init_paths();
13932
15054
  init_configLogger();
13933
- var SYSTEM_PROMPT2 = `You are a viral short-form video strategist. Your job is to analyze a video transcript with word-level timestamps and extract the **most compelling, shareable moments** as shorts (15\u201360 seconds each).
15055
+ function buildShortsSystemPrompt(clipConfig) {
15056
+ const minDuration = clipConfig?.duration?.min ?? 15;
15057
+ const maxDuration = clipConfig?.duration?.max ?? 60;
15058
+ const minViralScore = clipConfig?.minViralScore ?? 8;
15059
+ const maxClips = clipConfig?.maxClips ?? 5;
15060
+ const strategy = clipConfig?.strategy ?? "hook-first";
15061
+ const isHookFirst = strategy === "hook-first";
15062
+ const hookFirstSection = isHookFirst ? `
15063
+ ### Hook-First Video Ordering
15064
+
15065
+ If a short's natural content flows A\u2192B\u2192C\u2192D, the final short should play as D\u2192A\u2192B\u2192C \u2014 the **payoff moment (D) is moved to the front as the hook**, then the content plays from the beginning up to that point. The hook does NOT repeat.
15066
+
15067
+ **How to implement:**
15068
+ 1. Plan the content as normal (full story A\u2192D)
15069
+ 2. Identify the single most arresting 2-5 second moment \u2014 usually the payoff, punchline, or emotional peak
15070
+ 3. That moment becomes the FIRST segment in the segments array
15071
+ 4. The remaining content plays chronologically from start to just before the hook
15072
+ 5. Example: content [120s\u2013150s], best moment [145s\u2013150s] \u2192 segments: [{start: 145, end: 150}, {start: 120, end: 145}]
15073
+
15074
+ **Hook quality rules (NEVER violate):**
15075
+ - The hook segment MUST start and end on a **complete sentence or clause boundary**
15076
+ - The hook MUST be a **self-contained, complete thought** \u2014 understandable without prior context
15077
+ - If no moment qualifies as a clean hook, **keep segments chronological** and use hook text only` : `
15078
+ ### Chronological Video Ordering
15079
+
15080
+ Segments MUST be ordered chronologically \u2014 preserve the original video timeline. Do NOT reorder segments for hook-first ordering. Instead, rely on a strong verbal hook (the \`hook\` text field) as a text overlay to capture attention in the first 3 seconds while the content plays in its natural order.
15081
+
15082
+ **How to implement:**
15083
+ 1. Plan the content as normal \u2014 segments play in order from the video
15084
+ 2. Identify the most compelling hook text that teases the payoff
15085
+ 3. All segments in the segments array must be in ascending time order
15086
+ 4. The hook text overlay provides the attention-grab, not segment reordering`;
15087
+ return `You are a viral short-form video strategist. Your job is to analyze a video transcript with word-level timestamps and extract the **most compelling, shareable moments** as shorts (${minDuration}\u2013${maxDuration} seconds each).
13934
15088
 
13935
15089
  ## Core Philosophy: Quality Over Quantity
13936
15090
 
@@ -13947,7 +15101,7 @@ Design every clip to maximize rewatches and shares, not passive likes.
13947
15101
  ## Your workflow
13948
15102
  1. Read the transcript and note the total duration.
13949
15103
  2. Work through the transcript **section by section**. For each chunk, identify moments with genuine viral potential.
13950
- 3. For each potential short, score it using the Viral Score Framework (see below). **Only extract clips scoring 8 or higher.**
15104
+ 3. For each potential short, score it using the Viral Score Framework (see below). **Only extract clips scoring ${minViralScore} or higher.**
13951
15105
  4. Call **add_shorts** for each batch of qualifying shorts. You can call it as many times as needed.
13952
15106
  5. After your first pass, call **review_shorts** to see everything you've planned so far.
13953
15107
  6. Review critically: Would YOU share each of these? Could any be combined into stronger composites? Are there moments you underscored?
@@ -13962,7 +15116,7 @@ Viral Score = (Hook Strength \xD7 3) + (Emotional Intensity \xD7 2) +
13962
15116
  (Replay Potential \xD7 2)
13963
15117
 
13964
15118
  Maximum score: 60 \u2192 Normalized to 1-20 scale (divide by 3)
13965
- Minimum to extract: 8/20
15119
+ Minimum to extract: ${minViralScore}/20
13966
15120
  \`\`\`
13967
15121
 
13968
15122
  | Factor | 1 (Weak) | 3 (Moderate) | 5 (Strong) |
@@ -13998,22 +15152,7 @@ Minimum to extract: 8/20
13998
15152
  | **result-first** | Show the outcome immediately, then explain how | Tutorials, before/after |
13999
15153
  | **bold-claim** | Make a specific, surprising statement of fact | Data-driven, authority content |
14000
15154
  | **question** | "Want to know why X?" \u2014 engage curiosity directly | Engagement-focused, relatable |
14001
-
14002
- ### Hook-First Video Ordering
14003
-
14004
- If a short's natural content flows A\u2192B\u2192C\u2192D, the final short should play as D\u2192A\u2192B\u2192C \u2014 the **payoff moment (D) is moved to the front as the hook**, then the content plays from the beginning up to that point. The hook does NOT repeat.
14005
-
14006
- **How to implement:**
14007
- 1. Plan the content as normal (full story A\u2192D)
14008
- 2. Identify the single most arresting 2-5 second moment \u2014 usually the payoff, punchline, or emotional peak
14009
- 3. That moment becomes the FIRST segment in the segments array
14010
- 4. The remaining content plays chronologically from start to just before the hook
14011
- 5. Example: content [120s\u2013150s], best moment [145s\u2013150s] \u2192 segments: [{start: 145, end: 150}, {start: 120, end: 145}]
14012
-
14013
- **Hook quality rules (NEVER violate):**
14014
- - The hook segment MUST start and end on a **complete sentence or clause boundary**
14015
- - The hook MUST be a **self-contained, complete thought** \u2014 understandable without prior context
14016
- - If no moment qualifies as a clean hook, **keep segments chronological** and use hook text only
15155
+ ${hookFirstSection}
14017
15156
 
14018
15157
  ## Narrative structures (classify every short)
14019
15158
 
@@ -14064,14 +15203,14 @@ Composites (multi-segment shorts) often make the **best** shorts:
14064
15203
 
14065
15204
  ## Rules
14066
15205
 
14067
- 1. Each short must be 15-60 seconds total duration.
15206
+ 1. Each short must be ${minDuration}-${maxDuration} seconds total duration.
14068
15207
  2. Timestamps must align to word boundaries from the transcript.
14069
15208
  3. Prefer natural sentence boundaries for clean cuts.
14070
15209
  4. Every short needs a catchy, descriptive title (5-10 words).
14071
15210
  5. Tags should be lowercase, no hashes, 3-6 per short.
14072
15211
  6. A 1-second buffer is automatically added around each segment boundary.
14073
15212
  7. Avoid significant timestamp overlap between shorts.
14074
- 8. **Minimum viral score of 8/20 to extract.** Be ruthless about quality.
15213
+ 8. **Minimum viral score of ${minViralScore}/20 to extract.** Be ruthless about quality.
14075
15214
  9. Every short MUST have a hook, hookType, emotionalTrigger, viralScore, narrativeStructure, and shareReason.
14076
15215
 
14077
15216
  ## Using Clip Direction
@@ -14084,8 +15223,10 @@ You may receive AI-generated clip direction with suggested shorts. Use these as
14084
15223
 
14085
15224
  Before adding a short, ask yourself: **"Would I interrupt someone to show them this?"**
14086
15225
  - If YES \u2192 strong clip, add it
14087
- - If "maybe, it's interesting" \u2192 score it honestly and only keep if \u22658
15226
+ - If "maybe, it's interesting" \u2192 score it honestly and only keep if \u2265${minViralScore}
14088
15227
  - If NO \u2192 drop it, no matter how "complete" the topic coverage feels`;
15228
+ }
15229
+ var SYSTEM_PROMPT2 = buildShortsSystemPrompt();
14089
15230
  var ADD_SHORTS_SCHEMA = {
14090
15231
  type: "object",
14091
15232
  properties: {
@@ -14235,20 +15376,22 @@ Review critically:
14235
15376
  return this.isFinalized;
14236
15377
  }
14237
15378
  };
14238
- async function generateShorts(video, transcript, model, clipDirection, webcamOverride, ideas) {
14239
- const systemPrompt = SYSTEM_PROMPT2 + (ideas?.length ? buildIdeaContext(ideas) : "");
15379
+ async function generateShorts(video, transcript, model, clipDirection, webcamOverride, ideas, clipConfig, generateVariants) {
15380
+ const basePrompt = clipConfig ? buildShortsSystemPrompt(clipConfig) : SYSTEM_PROMPT2;
15381
+ const systemPrompt = basePrompt + (ideas?.length ? buildIdeaContext(ideas) : "");
14240
15382
  const agent = new ShortsAgent(systemPrompt, model);
14241
15383
  const transcriptLines = transcript.segments.map((seg) => {
14242
15384
  const words = seg.words.map((w) => `[${w.start.toFixed(2)}-${w.end.toFixed(2)}] ${w.word}`).join(" ");
14243
15385
  return `[${seg.start.toFixed(2)}s \u2013 ${seg.end.toFixed(2)}s] ${seg.text}
14244
15386
  Words: ${words}`;
14245
15387
  });
15388
+ const minViralScore = clipConfig?.minViralScore ?? 8;
14246
15389
  const promptParts = [
14247
15390
  `Analyze the following transcript (${transcript.duration.toFixed(0)}s total) and find the most viral-worthy moments for shorts.
14248
15391
  `,
14249
15392
  `Video: ${video.filename}`,
14250
15393
  `Duration: ${transcript.duration.toFixed(1)}s`,
14251
- `Focus on quality over quantity \u2014 only extract clips scoring 8+ on the viral score framework. Every clip must have a hook, hookType, emotionalTrigger, viralScore, narrativeStructure, and shareReason.
15394
+ `Focus on quality over quantity \u2014 only extract clips scoring ${minViralScore}+ on the viral score framework. Every clip must have a hook, hookType, emotionalTrigger, viralScore, narrativeStructure, and shareReason.
14252
15395
  `,
14253
15396
  "--- TRANSCRIPT ---\n",
14254
15397
  transcriptLines.join("\n\n"),
@@ -14300,23 +15443,27 @@ Words: ${words}`;
14300
15443
  } else {
14301
15444
  await extractCompositeClip2(video.repoPath, segments, outputPath);
14302
15445
  }
14303
- let variants;
14304
- try {
14305
- const defaultPlatforms = ["tiktok", "youtube-shorts", "instagram-reels", "instagram-feed", "linkedin"];
14306
- const results = await generatePlatformVariants2(outputPath, shortsDir, shortSlug, defaultPlatforms, { webcamOverride });
14307
- if (results.length > 0) {
14308
- variants = results.map((v) => ({
14309
- path: v.path,
14310
- aspectRatio: v.aspectRatio,
14311
- platform: v.platform,
14312
- width: v.width,
14313
- height: v.height
14314
- }));
14315
- logger_default.info(`[ShortsAgent] Generated ${variants.length} platform variants for: ${plan.title}`);
15446
+ let clipVariants;
15447
+ if (generateVariants !== false) {
15448
+ try {
15449
+ const defaultPlatforms = ["tiktok", "youtube-shorts", "instagram-reels", "instagram-feed", "linkedin"];
15450
+ const results = await generatePlatformVariants2(outputPath, shortsDir, shortSlug, defaultPlatforms, { webcamOverride });
15451
+ if (results.length > 0) {
15452
+ clipVariants = results.map((v) => ({
15453
+ path: v.path,
15454
+ aspectRatio: v.aspectRatio,
15455
+ platform: v.platform,
15456
+ width: v.width,
15457
+ height: v.height
15458
+ }));
15459
+ logger_default.info(`[ShortsAgent] Generated ${clipVariants.length} platform variants for: ${plan.title}`);
15460
+ }
15461
+ } catch (err) {
15462
+ const message = err instanceof Error ? err.message : String(err);
15463
+ logger_default.warn(`[ShortsAgent] Platform variant generation failed for ${plan.title}: ${message}`);
14316
15464
  }
14317
- } catch (err) {
14318
- const message = err instanceof Error ? err.message : String(err);
14319
- logger_default.warn(`[ShortsAgent] Platform variant generation failed for ${plan.title}: ${message}`);
15465
+ } else {
15466
+ logger_default.info(`[ShortsAgent] Skipping variant generation for: ${plan.title} (disabled by spec)`);
14320
15467
  }
14321
15468
  let captionedPath;
14322
15469
  try {
@@ -14331,8 +15478,8 @@ Words: ${words}`;
14331
15478
  logger_default.warn(`[ShortsAgent] Caption burning failed for ${plan.title}: ${message}`);
14332
15479
  captionedPath = void 0;
14333
15480
  }
14334
- if (variants) {
14335
- const portraitVariants = variants.filter((v) => v.aspectRatio === "9:16");
15481
+ if (clipVariants) {
15482
+ const portraitVariants = clipVariants.filter((v) => v.aspectRatio === "9:16");
14336
15483
  if (portraitVariants.length > 0) {
14337
15484
  try {
14338
15485
  const hookText = plan.hook ?? plan.title;
@@ -14350,7 +15497,7 @@ Words: ${words}`;
14350
15497
  logger_default.warn(`[ShortsAgent] Portrait caption burning failed for ${plan.title}: ${message}`);
14351
15498
  }
14352
15499
  }
14353
- const nonPortraitVariants = variants.filter((v) => v.aspectRatio !== "9:16");
15500
+ const nonPortraitVariants = clipVariants.filter((v) => v.aspectRatio !== "9:16");
14354
15501
  for (const variant of nonPortraitVariants) {
14355
15502
  try {
14356
15503
  const variantAssContent = segments.length === 1 ? generateStyledASSForSegment(transcript, segments[0].start, segments[0].end) : generateStyledASSForComposite(transcript, segments);
@@ -14401,7 +15548,7 @@ Words: ${words}`;
14401
15548
  description: plan.description,
14402
15549
  tags: plan.tags,
14403
15550
  hook: plan.hook,
14404
- variants,
15551
+ variants: clipVariants,
14405
15552
  hookType: plan.hookType,
14406
15553
  emotionalTrigger: plan.emotionalTrigger,
14407
15554
  viralScore: plan.viralScore,
@@ -14424,7 +15571,15 @@ init_videoOperations();
14424
15571
  init_fileSystem();
14425
15572
  init_paths();
14426
15573
  init_configLogger();
14427
- var SYSTEM_PROMPT3 = `You are a medium-form video content strategist. Your job is to analyze a video transcript with word-level timestamps and extract the **most valuable, engaging 1-3 minute segments** as standalone medium-form clips.
15574
+ function buildMediumSystemPrompt(clipConfig) {
15575
+ const minDuration = clipConfig?.duration?.min ?? 60;
15576
+ const maxDuration = clipConfig?.duration?.max ?? 180;
15577
+ const minViralScore = clipConfig?.minViralScore ?? 10;
15578
+ const maxClips = clipConfig?.maxClips ?? 5;
15579
+ const minMinutes = Math.floor(minDuration / 60);
15580
+ const maxMinutes = Math.ceil(maxDuration / 60);
15581
+ const durationLabel = `${minMinutes}-${maxMinutes} minute`;
15582
+ return `You are a medium-form video content strategist. Your job is to analyze a video transcript with word-level timestamps and extract the **most valuable, engaging ${minMinutes}-${maxMinutes} minute segments** as standalone medium-form clips.
14428
15583
 
14429
15584
  ## Core Philosophy: Value Density Over Coverage
14430
15585
 
@@ -14441,7 +15596,7 @@ Design every clip to maximize saves and shares.
14441
15596
  ## Your workflow
14442
15597
  1. Read the transcript and note the total duration.
14443
15598
  2. Work through the transcript **section by section** (roughly 5-8 minute chunks). For each chunk, identify segments with genuine standalone value.
14444
- 3. For each potential clip, score it using the Viral Score Framework (see below). **Only extract clips scoring 10 or higher.**
15599
+ 3. For each potential clip, score it using the Viral Score Framework (see below). **Only extract clips scoring ${minViralScore} or higher.**
14445
15600
  4. Call **add_medium_clips** for each batch of clips you find. You can call it as many times as needed.
14446
15601
  5. After your first pass, call **review_medium_clips** to see everything you've planned so far.
14447
15602
  6. Review critically: Does each clip deliver clear, standalone value? Would someone save it? Could segments be combined into something stronger?
@@ -14456,7 +15611,7 @@ Viral Score = (Hook Strength \xD7 3) + (Emotional Intensity \xD7 2) +
14456
15611
  (Replay Potential \xD7 2)
14457
15612
 
14458
15613
  Maximum score: 60 \u2192 Normalized to 1-20 scale (divide by 3)
14459
- Minimum to extract: 10/20 (higher bar than shorts \u2014 medium clips cost more to produce)
15614
+ Minimum to extract: ${minViralScore}/20 (higher bar than shorts \u2014 medium clips cost more to produce)
14460
15615
  \`\`\`
14461
15616
 
14462
15617
  | Factor | 1 (Weak) | 3 (Moderate) | 5 (Strong) |
@@ -14535,10 +15690,10 @@ Identify the PRIMARY emotion driving engagement:
14535
15690
 
14536
15691
  ## Duration optimization
14537
15692
 
14538
- - **Sweet spot**: 60-120 seconds (1-2 minutes)
14539
- - **Maximum**: 180 seconds \u2014 only if retention quality is exceptional
14540
- - **Under 60 seconds**: Too short for medium format \u2014 should be a short instead
14541
- - **Over 120 seconds**: Requires multiple micro-hooks and exceptional pacing
15693
+ - **Sweet spot**: ${minDuration}-${Math.min(maxDuration, 120)} seconds (${minMinutes}-${Math.min(maxMinutes, 2)} minutes)
15694
+ - **Maximum**: ${maxDuration} seconds \u2014 only if retention quality is exceptional
15695
+ - **Under ${minDuration} seconds**: Too short for medium format \u2014 should be a short instead
15696
+ - **Over ${Math.min(maxDuration, 120)} seconds**: Requires multiple micro-hooks and exceptional pacing
14542
15697
 
14543
15698
  ## Differences from shorts
14544
15699
 
@@ -14560,7 +15715,7 @@ For compilations, segments must be in chronological order.
14560
15715
 
14561
15716
  ## Rules
14562
15717
 
14563
- 1. Each clip must be 60-180 seconds total duration.
15718
+ 1. Each clip must be ${minDuration}-${maxDuration} seconds total duration.
14564
15719
  2. Timestamps must align to word boundaries from the transcript.
14565
15720
  3. Prefer natural sentence and paragraph boundaries for clean entry/exit points.
14566
15721
  4. Each clip must be self-contained \u2014 a viewer with no other context should get value.
@@ -14568,7 +15723,7 @@ For compilations, segments must be in chronological order.
14568
15723
  6. For compilations, specify segments in **chronological order**.
14569
15724
  7. Tags should be lowercase, no hashes, 3-6 per clip.
14570
15725
  8. A 1-second buffer is automatically added around each segment boundary.
14571
- 9. **Minimum viral score of 10/20 to extract.** Medium clips cost more to produce \u2014 quality bar is higher.
15726
+ 9. **Minimum viral score of ${minViralScore}/20 to extract.** Medium clips cost more to produce \u2014 quality bar is higher.
14572
15727
  10. Every clip MUST have hook, hookType, emotionalTrigger, viralScore, narrativeStructure, clipType, saveReason, and microHooks.
14573
15728
  11. Avoid significant overlap with content that would work better as a short.
14574
15729
 
@@ -14576,7 +15731,7 @@ For compilations, segments must be in chronological order.
14576
15731
 
14577
15732
  Before adding a clip, ask yourself: **"Would I bookmark this to come back to later?"**
14578
15733
  - If YES \u2192 strong clip, add it
14579
- - If "it's informative but not reference-worthy" \u2192 score it honestly and only keep if \u226510
15734
+ - If "it's informative but not reference-worthy" \u2192 score it honestly and only keep if \u2265${minViralScore}
14580
15735
  - If NO \u2192 drop it or consider if it works better as a short
14581
15736
 
14582
15737
  ## Using Clip Direction
@@ -14585,6 +15740,8 @@ You may receive AI-generated clip direction with suggested medium clips. Use the
14585
15740
  - Feel free to adjust timestamps, combine suggestions, or ignore ones that don't work
14586
15741
  - You may also find good clips NOT in the suggestions \u2014 always analyze the full transcript
14587
15742
  - Pay special attention to suggested hooks and topic arcs \u2014 they come from multimodal analysis`;
15743
+ }
15744
+ var SYSTEM_PROMPT3 = buildMediumSystemPrompt();
14588
15745
  var ADD_MEDIUM_CLIPS_SCHEMA = {
14589
15746
  type: "object",
14590
15747
  properties: {
@@ -14742,20 +15899,26 @@ Review critically:
14742
15899
  return this.isFinalized;
14743
15900
  }
14744
15901
  };
14745
- async function generateMediumClips(video, transcript, model, clipDirection, ideas) {
14746
- const systemPrompt = SYSTEM_PROMPT3 + (ideas?.length ? buildIdeaContext(ideas) : "");
15902
+ async function generateMediumClips(video, transcript, model, clipDirection, ideas, clipConfig) {
15903
+ const basePrompt = clipConfig ? buildMediumSystemPrompt(clipConfig) : SYSTEM_PROMPT3;
15904
+ const systemPrompt = basePrompt + (ideas?.length ? buildIdeaContext(ideas) : "");
14747
15905
  const agent = new MediumVideoAgent(systemPrompt, model);
14748
15906
  const transcriptLines = transcript.segments.map((seg) => {
14749
15907
  const words = seg.words.map((w) => `[${w.start.toFixed(2)}-${w.end.toFixed(2)}] ${w.word}`).join(" ");
14750
15908
  return `[${seg.start.toFixed(2)}s \u2013 ${seg.end.toFixed(2)}s] ${seg.text}
14751
15909
  Words: ${words}`;
14752
15910
  });
15911
+ const minDuration = clipConfig?.duration?.min ?? 60;
15912
+ const maxDuration = clipConfig?.duration?.max ?? 180;
15913
+ const minViralScore = clipConfig?.minViralScore ?? 10;
15914
+ const minMinutes = Math.floor(minDuration / 60);
15915
+ const maxMinutes = Math.ceil(maxDuration / 60);
14753
15916
  const promptParts = [
14754
- `Analyze the following transcript (${transcript.duration.toFixed(0)}s total) and find the most valuable segments for medium-length clips (1-3 minutes each).
15917
+ `Analyze the following transcript (${transcript.duration.toFixed(0)}s total) and find the most valuable segments for medium-length clips (${minMinutes}-${maxMinutes} minutes each).
14755
15918
  `,
14756
15919
  `Video: ${video.filename}`,
14757
15920
  `Duration: ${transcript.duration.toFixed(1)}s`,
14758
- `Focus on value density over coverage \u2014 only extract clips scoring 10+ on the viral score framework. Every clip must have hook, hookType, emotionalTrigger, viralScore, narrativeStructure, clipType, saveReason, and microHooks.
15921
+ `Focus on value density over coverage \u2014 only extract clips scoring ${minViralScore}+ on the viral score framework. Every clip must have hook, hookType, emotionalTrigger, viralScore, narrativeStructure, clipType, saveReason, and microHooks.
14759
15922
  `,
14760
15923
  "--- TRANSCRIPT ---\n",
14761
15924
  transcriptLines.join("\n\n"),
@@ -15640,17 +16803,27 @@ init_paths();
15640
16803
  init_BaseAgent();
15641
16804
  init_configLogger();
15642
16805
  init_environment();
15643
- init_types();
15644
- var SYSTEM_PROMPT5 = `You are a viral social-media content strategist.
15645
- Given a video transcript and summary you MUST generate one post for each of the 5 platforms listed below.
15646
- Each post must match the platform's tone, format, and constraints exactly.
15647
-
15648
- Platform guidelines:
16806
+ init_types2();
16807
+ var PER_PLATFORM_GUIDELINES = `Platform guidelines:
15649
16808
  1. **TikTok** \u2013 Casual, hook-driven, trending hashtags, 150 chars max, emoji-heavy.
15650
16809
  2. **YouTube** \u2013 Descriptive, SEO-optimized title + description, relevant tags.
15651
16810
  3. **Instagram** \u2013 Visual storytelling, emoji-rich, 30 hashtags max, engaging caption.
15652
16811
  4. **LinkedIn** \u2013 Professional, thought-leadership, industry insights, 1-3 hashtags.
15653
- 5. **X (Twitter)** \u2013 Concise, punchy, 280 chars max, 2-5 hashtags, thread-ready.
16812
+ 5. **X (Twitter)** \u2013 Concise, punchy, 280 chars max, 2-5 hashtags, thread-ready.`;
16813
+ var UNIFIED_TONE_GUIDELINES = `Platform guidelines:
16814
+ For all platforms, use the same professional but approachable tone. Keep posts concise, informative, and authentic. Use the same core message \u2014 adapt only for platform-specific formatting constraints (character limits, hashtag placement).
16815
+ 1. **TikTok** \u2013 150 chars max, include relevant hashtags.
16816
+ 2. **YouTube** \u2013 SEO-optimized title + description, relevant tags.
16817
+ 3. **Instagram** \u2013 30 hashtags max, engaging caption.
16818
+ 4. **LinkedIn** \u2013 1-3 hashtags, professional framing.
16819
+ 5. **X (Twitter)** \u2013 280 chars max, 2-5 hashtags.`;
16820
+ function buildSocialMediaSystemPrompt(toneStrategy) {
16821
+ const platformGuidelines = toneStrategy === "unified" ? UNIFIED_TONE_GUIDELINES : PER_PLATFORM_GUIDELINES;
16822
+ return `You are a viral social-media content strategist.
16823
+ Given a video transcript and summary you MUST generate one post for each of the 5 platforms listed below.
16824
+ Each post must match the platform's format and constraints exactly.
16825
+
16826
+ ${platformGuidelines}
15654
16827
 
15655
16828
  IMPORTANT \u2013 Content format:
15656
16829
  The "content" field you provide must be the FINAL, ready-to-post text that can be directly copied and pasted onto the platform. Do NOT use markdown headers, bullet points, or any formatting inside the content. Include hashtags inline at the end of the post text where appropriate. The content is saved as-is for direct posting.
@@ -15662,6 +16835,8 @@ Workflow:
15662
16835
 
15663
16836
  Include relevant links in posts when search results provide them.
15664
16837
  Always call "create_posts" exactly once with all 5 platform posts.`;
16838
+ }
16839
+ var SYSTEM_PROMPT5 = buildSocialMediaSystemPrompt();
15665
16840
  var SocialMediaAgent = class extends BaseAgent {
15666
16841
  collectedPosts = [];
15667
16842
  constructor(systemPrompt = SYSTEM_PROMPT5, model) {
@@ -15842,8 +17017,9 @@ async function generateShortPosts(video, short, transcript, model, summary) {
15842
17017
  await agent.destroy();
15843
17018
  }
15844
17019
  }
15845
- async function generateSocialPosts(video, transcript, summary, outputDir, model, ideas) {
15846
- const systemPrompt = SYSTEM_PROMPT5 + (ideas?.length ? buildIdeaContextForPosts(ideas) : "");
17020
+ async function generateSocialPosts(video, transcript, summary, outputDir, model, ideas, toneStrategy) {
17021
+ const basePrompt = toneStrategy ? buildSocialMediaSystemPrompt(toneStrategy) : SYSTEM_PROMPT5;
17022
+ const systemPrompt = basePrompt + (ideas?.length ? buildIdeaContextForPosts(ideas) : "");
15847
17023
  const agent = new SocialMediaAgent(systemPrompt, model);
15848
17024
  try {
15849
17025
  const userMessage = [
@@ -16131,11 +17307,11 @@ async function markFailed(slug, error) {
16131
17307
  init_fileSystem();
16132
17308
  init_paths();
16133
17309
  init_configLogger();
16134
- init_types();
16135
- init_types();
17310
+ init_types2();
17311
+ init_types2();
16136
17312
 
16137
17313
  // src/L3-services/socialPosting/platformContentStrategy.ts
16138
- init_types();
17314
+ init_types2();
16139
17315
  var CONTENT_MATRIX = {
16140
17316
  ["youtube" /* YouTube */]: {
16141
17317
  video: { captions: true, variantKey: null },
@@ -16169,10 +17345,10 @@ function platformAcceptsMedia(platform, clipType) {
16169
17345
 
16170
17346
  // src/L3-services/queueBuilder/queueBuilder.ts
16171
17347
  init_postStore();
16172
- function resolveShortMedia(clip, platform) {
17348
+ function resolveShortMedia(clip, platform, variantsEnabled) {
16173
17349
  const rule = getMediaRule(platform, "short");
16174
17350
  if (!rule) return null;
16175
- if (rule.variantKey && clip.variants?.length) {
17351
+ if (variantsEnabled !== false && rule.variantKey && clip.variants?.length) {
16176
17352
  const match = clip.variants.find((v) => v.platform === rule.variantKey);
16177
17353
  if (match) return match.path;
16178
17354
  if (platform === "instagram" /* Instagram */) {
@@ -16219,7 +17395,7 @@ async function parsePostFrontmatter(postPath) {
16219
17395
  function stripFrontmatter(content) {
16220
17396
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
16221
17397
  }
16222
- async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captionedVideoPath, ideaIds) {
17398
+ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captionedVideoPath, ideaIds, variantsEnabled) {
16223
17399
  const result = { itemsCreated: 0, itemsSkipped: 0, errors: [] };
16224
17400
  for (const post of socialPosts) {
16225
17401
  try {
@@ -16238,7 +17414,7 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
16238
17414
  clipSlug = short.slug;
16239
17415
  clipType = "short";
16240
17416
  sourceClip = dirname(short.outputPath);
16241
- mediaPath = resolveShortMedia(short, post.platform);
17417
+ mediaPath = resolveShortMedia(short, post.platform, variantsEnabled);
16242
17418
  thumbnailPath = short.thumbnailPath ?? null;
16243
17419
  clipIdeaIssueNumber = short.ideaIssueNumber;
16244
17420
  } else if (medium) {
@@ -16991,7 +18167,7 @@ async function enhanceVideo(videoPath, transcript, video) {
16991
18167
  // src/L5-assets/MainVideoAsset.ts
16992
18168
  init_environment();
16993
18169
  init_configLogger();
16994
- init_types();
18170
+ init_types2();
16995
18171
  var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
16996
18172
  sourcePath;
16997
18173
  videoDir;
@@ -17000,6 +18176,16 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
17000
18176
  _ideas = [];
17001
18177
  /** Per-clip idea assignments from idea discovery (clipId → ideaIssueNumber) */
17002
18178
  _clipIdeaMap = /* @__PURE__ */ new Map();
18179
+ /** Pipeline spec controlling clip and platform configuration */
18180
+ _spec;
18181
+ /** Set the pipeline spec for configuring agent behavior */
18182
+ setSpec(spec) {
18183
+ this._spec = spec;
18184
+ }
18185
+ /** Get the pipeline spec */
18186
+ get spec() {
18187
+ return this._spec;
18188
+ }
17003
18189
  /** Set ideas for editorial direction */
17004
18190
  setIdeas(ideas) {
17005
18191
  this._ideas = ideas;
@@ -17549,7 +18735,18 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
17549
18735
  async generateShortsInternal() {
17550
18736
  const transcript = await this.getTranscript();
17551
18737
  const videoFile = await this.toVideoFile();
17552
- const shorts = await generateShorts(videoFile, transcript);
18738
+ const shortsConfig = this._spec?.clips.shorts;
18739
+ const shouldGenerateVariants = this._spec?.distribution.platforms.variants;
18740
+ const shorts = await generateShorts(
18741
+ videoFile,
18742
+ transcript,
18743
+ void 0,
18744
+ void 0,
18745
+ void 0,
18746
+ void 0,
18747
+ shortsConfig,
18748
+ shouldGenerateVariants
18749
+ );
17553
18750
  logger_default.info(`Generated ${shorts.length} short clips`);
17554
18751
  return shorts;
17555
18752
  }
@@ -17581,7 +18778,15 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
17581
18778
  async generateMediumClipsInternal() {
17582
18779
  const transcript = await this.getTranscript();
17583
18780
  const videoFile = await this.toVideoFile();
17584
- const clips = await generateMediumClips(videoFile, transcript);
18781
+ const mediumConfig = this._spec?.clips.medium;
18782
+ const clips = await generateMediumClips(
18783
+ videoFile,
18784
+ transcript,
18785
+ void 0,
18786
+ void 0,
18787
+ void 0,
18788
+ mediumConfig
18789
+ );
17585
18790
  logger_default.info(`Generated ${clips.length} medium clips for ${this.slug}`);
17586
18791
  return clips;
17587
18792
  }
@@ -17604,7 +18809,16 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
17604
18809
  const summary = await this.getSummary();
17605
18810
  const video = await this.toVideoFile();
17606
18811
  await ensureDirectory(this.socialPostsDir);
17607
- const posts = await generateSocialPosts(video, transcript, summary, this.socialPostsDir);
18812
+ const toneStrategy = this._spec?.distribution.platforms.toneStrategy;
18813
+ const posts = await generateSocialPosts(
18814
+ video,
18815
+ transcript,
18816
+ summary,
18817
+ this.socialPostsDir,
18818
+ void 0,
18819
+ void 0,
18820
+ toneStrategy
18821
+ );
17608
18822
  await this.markSocialPostsComplete();
17609
18823
  return posts;
17610
18824
  }
@@ -17978,7 +19192,8 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
17978
19192
  async buildPublishQueueData(shorts, mediumClips, socialPosts, captionedVideoPath) {
17979
19193
  const video = await this.toVideoFile();
17980
19194
  const ideaIds = this._ideas.length > 0 ? this._ideas.map((idea) => String(idea.issueNumber)) : void 0;
17981
- return buildPublishQueue2(video, shorts, mediumClips, socialPosts, captionedVideoPath, ideaIds);
19195
+ const variantsEnabled = this._spec?.distribution.platforms.variants;
19196
+ return buildPublishQueue2(video, shorts, mediumClips, socialPosts, captionedVideoPath, ideaIds, variantsEnabled);
17982
19197
  }
17983
19198
  /**
17984
19199
  * Build the publish queue (simplified wrapper for pipeline).
@@ -18199,6 +19414,7 @@ async function executeRealignPlan(plan, onProgress) {
18199
19414
  }
18200
19415
 
18201
19416
  // src/L4-agents/ScheduleAgent.ts
19417
+ init_queueMapping();
18202
19418
  init_configLogger();
18203
19419
  var TOOL_LABELS = {
18204
19420
  list_posts: "\u{1F4CB} Listing posts",
@@ -18314,14 +19530,16 @@ var ScheduleAgent = class extends BaseAgent {
18314
19530
  },
18315
19531
  {
18316
19532
  name: "reschedule_post",
18317
- description: "Move a post to a new scheduled time.",
19533
+ description: "Move a post to a new scheduled time, or re-queue it to get the next available queue slot. If scheduledFor is provided, the post is moved to that exact time. If omitted, the post is re-queued using the Late API queue for its platform/clipType.",
18318
19534
  parameters: {
18319
19535
  type: "object",
18320
19536
  properties: {
18321
19537
  postId: { type: "string", description: "The Late post ID" },
18322
- scheduledFor: { type: "string", description: "New scheduled datetime (ISO 8601)" }
19538
+ scheduledFor: { type: "string", description: "New scheduled datetime (ISO 8601). Omit to re-queue using the platform queue." },
19539
+ platform: { type: "string", description: "Platform for queue lookup when re-queuing without scheduledFor" },
19540
+ clipType: { type: "string", description: "Clip type for queue lookup: short, medium-clip, video" }
18323
19541
  },
18324
- required: ["postId", "scheduledFor"]
19542
+ required: ["postId"]
18325
19543
  },
18326
19544
  handler: async (args) => this.handleToolCall("reschedule_post", args)
18327
19545
  },
@@ -18511,9 +19729,23 @@ var ScheduleAgent = class extends BaseAgent {
18511
19729
  try {
18512
19730
  const postId = args.postId;
18513
19731
  const scheduledFor = args.scheduledFor;
19732
+ const platform = args.platform;
19733
+ const clipType = args.clipType;
18514
19734
  const client = createLateApiClient();
18515
- const updated = await client.schedulePost(postId, scheduledFor);
18516
- return { success: true, postId, scheduledFor: updated.scheduledFor };
19735
+ if (scheduledFor) {
19736
+ const updated = await client.schedulePost(postId, scheduledFor);
19737
+ return { success: true, postId, scheduledFor: updated.scheduledFor, source: "manual" };
19738
+ }
19739
+ if (platform) {
19740
+ const normalized = platform === "twitter" ? "x" : platform;
19741
+ const queueId = await getQueueId(normalized, clipType ?? "short");
19742
+ if (queueId) {
19743
+ const profileId = await getProfileId();
19744
+ const updated = await client.updatePost(postId, { queuedFromProfile: profileId, queueId });
19745
+ return { success: true, postId, scheduledFor: updated.scheduledFor, source: "queue", queueId };
19746
+ }
19747
+ }
19748
+ return { error: "Either scheduledFor or platform must be provided. Provide scheduledFor for a specific time, or platform to re-queue." };
18517
19749
  } catch (err) {
18518
19750
  logger_default.error("reschedule_post failed", { error: err });
18519
19751
  return { error: `Failed to reschedule post: ${err.message}` };
@@ -18535,9 +19767,22 @@ var ScheduleAgent = class extends BaseAgent {
18535
19767
  const platform = args.platform;
18536
19768
  const clipType = args.clipType;
18537
19769
  const normalized = platform === "twitter" ? "x" : platform;
19770
+ const queueId = await getQueueId(normalized, clipType ?? "short");
19771
+ if (queueId) {
19772
+ try {
19773
+ const profileId = await getProfileId();
19774
+ const client = createLateApiClient();
19775
+ const preview = await client.previewQueue(profileId, queueId, 1);
19776
+ if (preview.slots?.length > 0) {
19777
+ return { platform: normalized, clipType: clipType ?? "any", nextSlot: preview.slots[0], source: "queue", queueId };
19778
+ }
19779
+ } catch (err) {
19780
+ logger_default.warn("Queue preview failed, falling back to local calculation", { queueId, error: err });
19781
+ }
19782
+ }
18538
19783
  const slot = await findNextSlot(normalized, clipType);
18539
19784
  if (!slot) return { error: `No available slot found for ${normalized}` };
18540
- return { platform: normalized, clipType: clipType ?? "any", nextSlot: slot };
19785
+ return { platform: normalized, clipType: clipType ?? "any", nextSlot: slot, source: "local" };
18541
19786
  } catch (err) {
18542
19787
  logger_default.error("find_next_slot failed", { error: err });
18543
19788
  return { error: `Failed to find next slot: ${err.message}` };
@@ -18610,6 +19855,7 @@ var ScheduleAgent = class extends BaseAgent {
18610
19855
  skipped: plan.skipped,
18611
19856
  unmatched: plan.unmatched
18612
19857
  };
19858
+ logger_default.info("Queue-based reshuffle is also available via `vidpipe sync-queues --reshuffle`");
18613
19859
  if (dryRun) {
18614
19860
  job.status = "completed";
18615
19861
  job.completedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -18651,7 +19897,7 @@ init_modelConfig();
18651
19897
  init_configLogger();
18652
19898
  init_ideaService();
18653
19899
  init_providerFactory();
18654
- init_types();
19900
+ init_types2();
18655
19901
  init_BaseAgent();
18656
19902
  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.
18657
19903
 
@@ -20071,7 +21317,7 @@ function createIdeaDiscoveryAgent(...args) {
20071
21317
  }
20072
21318
 
20073
21319
  // src/L6-pipeline/pipeline.ts
20074
- init_types();
21320
+ init_types2();
20075
21321
  async function runStage(stageName, fn, stageResults) {
20076
21322
  const start = Date.now();
20077
21323
  if (progressEmitter.isEnabled()) {
@@ -20125,11 +21371,22 @@ async function runStage(stageName, fn, stageResults) {
20125
21371
  return void 0;
20126
21372
  }
20127
21373
  }
20128
- async function processVideo(videoPath, ideas, publishBy) {
21374
+ async function processVideo(videoPath, ideas, publishBy, spec) {
20129
21375
  const pipelineStart = Date.now();
20130
21376
  const stageResults = [];
20131
21377
  const cfg = getConfig();
20132
21378
  let stagesSkipped = 0;
21379
+ const skipFlags = {
21380
+ SKIP_SILENCE_REMOVAL: cfg.SKIP_SILENCE_REMOVAL,
21381
+ SKIP_VISUAL_ENHANCEMENT: cfg.SKIP_VISUAL_ENHANCEMENT,
21382
+ SKIP_CAPTIONS: cfg.SKIP_CAPTIONS,
21383
+ SKIP_INTRO_OUTRO: cfg.SKIP_INTRO_OUTRO,
21384
+ SKIP_SHORTS: cfg.SKIP_SHORTS,
21385
+ SKIP_MEDIUM_CLIPS: cfg.SKIP_MEDIUM_CLIPS,
21386
+ SKIP_SOCIAL: cfg.SKIP_SOCIAL,
21387
+ SKIP_SOCIAL_PUBLISH: cfg.SKIP_SOCIAL_PUBLISH
21388
+ };
21389
+ const effectiveSpec = spec ? applySkipFlags(spec, skipFlags) : resolveFromFlags(skipFlags);
20133
21390
  costTracker3.reset();
20134
21391
  function trackStage(stage, fn) {
20135
21392
  costTracker3.setStage(stage);
@@ -20194,43 +21451,44 @@ async function processVideo(videoPath, ideas, publishBy) {
20194
21451
  asset.setIdeas(ideas);
20195
21452
  logger_default.info(`Pipeline using ${ideas.length} idea(s) for editorial direction`);
20196
21453
  }
21454
+ asset.setSpec(effectiveSpec);
20197
21455
  try {
20198
21456
  const transcript = await trackStage("transcription" /* Transcription */, () => asset.getTranscript());
20199
21457
  let editedVideoPath;
20200
- if (!cfg.SKIP_SILENCE_REMOVAL) {
21458
+ if (effectiveSpec.processing.silenceRemoval) {
20201
21459
  editedVideoPath = await trackStage("silence-removal" /* SilenceRemoval */, () => asset.getEditedVideo());
20202
21460
  } else {
20203
- skipStage("silence-removal" /* SilenceRemoval */, "SKIP_SILENCE_REMOVAL");
21461
+ skipStage("silence-removal" /* SilenceRemoval */, "SPEC_DISABLED");
20204
21462
  }
20205
21463
  let enhancedVideoPath;
20206
- if (!cfg.SKIP_VISUAL_ENHANCEMENT) {
21464
+ if (effectiveSpec.processing.visualEnhancement) {
20207
21465
  enhancedVideoPath = await trackStage("visual-enhancement" /* VisualEnhancement */, () => asset.getEnhancedVideo());
20208
21466
  } else {
20209
- skipStage("visual-enhancement" /* VisualEnhancement */, "SKIP_VISUAL_ENHANCEMENT");
21467
+ skipStage("visual-enhancement" /* VisualEnhancement */, "SPEC_DISABLED");
20210
21468
  }
20211
21469
  let captions;
20212
- if (!cfg.SKIP_CAPTIONS) {
21470
+ if (effectiveSpec.processing.captions) {
20213
21471
  captions = await trackStage("captions" /* Captions */, () => asset.getCaptions());
20214
21472
  } else {
20215
- skipStage("captions" /* Captions */, "SKIP_CAPTIONS");
21473
+ skipStage("captions" /* Captions */, "SPEC_DISABLED");
20216
21474
  }
20217
21475
  let captionedVideoPath;
20218
- if (!cfg.SKIP_CAPTIONS) {
21476
+ if (effectiveSpec.processing.captions) {
20219
21477
  captionedVideoPath = await trackStage("caption-burn" /* CaptionBurn */, () => asset.getCaptionedVideo());
20220
21478
  } else {
20221
- skipStage("caption-burn" /* CaptionBurn */, "SKIP_CAPTIONS");
21479
+ skipStage("caption-burn" /* CaptionBurn */, "SPEC_DISABLED");
20222
21480
  }
20223
21481
  let introOutroVideoPath;
20224
- if (!cfg.SKIP_INTRO_OUTRO) {
21482
+ if (effectiveSpec.processing.introOutro) {
20225
21483
  introOutroVideoPath = await trackStage("intro-outro" /* IntroOutro */, () => asset.getIntroOutroVideo());
20226
21484
  } else {
20227
- skipStage("intro-outro" /* IntroOutro */, "SKIP_INTRO_OUTRO");
21485
+ skipStage("intro-outro" /* IntroOutro */, "SPEC_DISABLED");
20228
21486
  }
20229
21487
  let shorts = [];
20230
- if (!cfg.SKIP_SHORTS) {
21488
+ if (effectiveSpec.clips.shorts.enabled) {
20231
21489
  const shortAssets = await trackStage("shorts" /* Shorts */, async () => {
20232
21490
  const assets = await asset.getShorts();
20233
- if (!cfg.SKIP_INTRO_OUTRO) {
21491
+ if (effectiveSpec.processing.introOutro) {
20234
21492
  for (const shortAsset of assets) {
20235
21493
  const introOutroPath = await shortAsset.getIntroOutroVideo();
20236
21494
  if (introOutroPath !== shortAsset.clip.outputPath) {
@@ -20257,13 +21515,13 @@ async function processVideo(videoPath, ideas, publishBy) {
20257
21515
  }
20258
21516
  }
20259
21517
  } else {
20260
- skipStage("shorts" /* Shorts */, "SKIP_SHORTS");
21518
+ skipStage("shorts" /* Shorts */, "SPEC_DISABLED");
20261
21519
  }
20262
21520
  let mediumClips = [];
20263
- if (!cfg.SKIP_MEDIUM_CLIPS) {
21521
+ if (effectiveSpec.clips.medium.enabled) {
20264
21522
  const mediumAssets = await trackStage("medium-clips" /* MediumClips */, async () => {
20265
21523
  const assets = await asset.getMediumClips();
20266
- if (!cfg.SKIP_INTRO_OUTRO) {
21524
+ if (effectiveSpec.processing.introOutro) {
20267
21525
  for (const clipAsset of assets) {
20268
21526
  const introOutroPath = await clipAsset.getIntroOutroVideo();
20269
21527
  if (introOutroPath !== clipAsset.clip.outputPath) {
@@ -20283,7 +21541,7 @@ async function processVideo(videoPath, ideas, publishBy) {
20283
21541
  }
20284
21542
  }
20285
21543
  } else {
20286
- skipStage("medium-clips" /* MediumClips */, "SKIP_MEDIUM_CLIPS");
21544
+ skipStage("medium-clips" /* MediumClips */, "SPEC_DISABLED");
20287
21545
  }
20288
21546
  const chapters = await trackStage("chapters" /* Chapters */, () => asset.getChapters());
20289
21547
  const summary = await trackStage("summary" /* Summary */, () => asset.getSummary());
@@ -20302,7 +21560,7 @@ async function processVideo(videoPath, ideas, publishBy) {
20302
21560
  skipStage("idea-discovery" /* IdeaDiscovery */, "NO_CLIPS");
20303
21561
  }
20304
21562
  let socialPosts = [];
20305
- if (!cfg.SKIP_SOCIAL) {
21563
+ if (effectiveSpec.distribution.enabled) {
20306
21564
  const mainPosts = await trackStage("social-media" /* SocialMedia */, () => asset.getSocialPosts()) ?? [];
20307
21565
  socialPosts.push(...mainPosts);
20308
21566
  if (shorts.length > 0) {
@@ -20326,14 +21584,14 @@ async function processVideo(videoPath, ideas, publishBy) {
20326
21584
  skipStage("medium-clip-posts" /* MediumClipPosts */, "NO_MEDIUM_CLIPS");
20327
21585
  }
20328
21586
  } else {
20329
- skipStage("social-media" /* SocialMedia */, "SKIP_SOCIAL");
20330
- skipStage("short-posts" /* ShortPosts */, "SKIP_SOCIAL");
20331
- skipStage("medium-clip-posts" /* MediumClipPosts */, "SKIP_SOCIAL");
21587
+ skipStage("social-media" /* SocialMedia */, "SPEC_DISABLED");
21588
+ skipStage("short-posts" /* ShortPosts */, "SPEC_DISABLED");
21589
+ skipStage("medium-clip-posts" /* MediumClipPosts */, "SPEC_DISABLED");
20332
21590
  }
20333
- if (!cfg.SKIP_SOCIAL_PUBLISH && socialPosts.length > 0) {
21591
+ if (effectiveSpec.distribution.publish && socialPosts.length > 0) {
20334
21592
  await trackStage("queue-build" /* QueueBuild */, () => asset.buildQueue(shorts, mediumClips, socialPosts, introOutroVideoPath ?? captionedVideoPath));
20335
- } else if (cfg.SKIP_SOCIAL_PUBLISH) {
20336
- skipStage("queue-build" /* QueueBuild */, "SKIP_SOCIAL_PUBLISH");
21593
+ } else if (!effectiveSpec.distribution.publish) {
21594
+ skipStage("queue-build" /* QueueBuild */, "SPEC_DISABLED");
20337
21595
  } else {
20338
21596
  skipStage("queue-build" /* QueueBuild */, "NO_SOCIAL_POSTS");
20339
21597
  }
@@ -20425,13 +21683,13 @@ function generateCostMarkdown(report) {
20425
21683
  }
20426
21684
  return md;
20427
21685
  }
20428
- async function processVideoSafe(videoPath, ideas, publishBy) {
21686
+ async function processVideoSafe(videoPath, ideas, publishBy, spec) {
20429
21687
  const filename = basename(videoPath);
20430
21688
  const slug = filename.replace(/\.(mp4|mov|webm|avi|mkv)$/i, "");
20431
21689
  await markPending3(slug, videoPath);
20432
21690
  await markProcessing3(slug);
20433
21691
  try {
20434
- const result = await processVideo(videoPath, ideas, publishBy);
21692
+ const result = await processVideo(videoPath, ideas, publishBy, spec);
20435
21693
  await markCompleted3(slug);
20436
21694
  return result;
20437
21695
  } catch (err) {
@@ -20909,6 +22167,7 @@ function getPlatformIcon(platform) {
20909
22167
 
20910
22168
  // src/L7-app/commands/realign.ts
20911
22169
  init_environment();
22170
+ init_configLogger();
20912
22171
  function formatDate(iso) {
20913
22172
  const d = new Date(iso);
20914
22173
  return d.toLocaleDateString("en-US", {
@@ -20938,6 +22197,23 @@ var PLATFORM_ICON = {
20938
22197
  async function runRealign(options = {}) {
20939
22198
  initConfig();
20940
22199
  console.log("\n\u{1F504} Realign Late Posts\n");
22200
+ if (options.queue) {
22201
+ console.log(" Using Late API queue reshuffle mode");
22202
+ if (options.dryRun) {
22203
+ console.log(" Mode: DRY RUN (no changes will be made)\n");
22204
+ }
22205
+ const { syncQueuesToLate: syncQueuesToLate2 } = await Promise.resolve().then(() => (init_queueSync(), queueSync_exports));
22206
+ const result2 = await syncQueuesToLate2({ reshuffle: true, dryRun: options.dryRun });
22207
+ logger_default.info(`Queue reshuffle complete: ${result2.updated.length} queues reshuffled`);
22208
+ console.log(` \u2705 Queue reshuffle complete: ${result2.updated.length} queues reshuffled`);
22209
+ if (result2.errors.length > 0) {
22210
+ for (const err of result2.errors) {
22211
+ console.log(` \u274C ${err.queueName}: ${err.error}`);
22212
+ }
22213
+ }
22214
+ console.log();
22215
+ return;
22216
+ }
20941
22217
  if (options.platform) {
20942
22218
  console.log(` Platform filter: ${options.platform}`);
20943
22219
  }
@@ -21356,8 +22632,8 @@ async function discoverIdeas(input) {
21356
22632
  }
21357
22633
 
21358
22634
  // src/L7-app/commands/ideate.ts
21359
- init_types();
21360
- var VALID_PLATFORMS = new Set(Object.values(Platform));
22635
+ init_types2();
22636
+ var VALID_PLATFORMS2 = new Set(Object.values(Platform));
21361
22637
  async function runIdeate(options = {}) {
21362
22638
  initConfig();
21363
22639
  if (options.add) {
@@ -21466,8 +22742,8 @@ function parsePlatforms(value) {
21466
22742
  const platforms = [];
21467
22743
  for (const name of names) {
21468
22744
  const lower = name.toLowerCase();
21469
- if (!VALID_PLATFORMS.has(lower)) {
21470
- throw new Error(`Invalid platform "${name}". Valid platforms: ${[...VALID_PLATFORMS].join(", ")}`);
22745
+ if (!VALID_PLATFORMS2.has(lower)) {
22746
+ throw new Error(`Invalid platform "${name}". Valid platforms: ${[...VALID_PLATFORMS2].join(", ")}`);
21471
22747
  }
21472
22748
  platforms.push(lower);
21473
22749
  }
@@ -21760,7 +23036,7 @@ init_postStore();
21760
23036
  init_fileSystem();
21761
23037
  init_brand();
21762
23038
  init_paths();
21763
- init_types();
23039
+ init_types2();
21764
23040
  init_configLogger();
21765
23041
  async function runDiscoverIdeas(options) {
21766
23042
  const pendingItems = await getPendingItems();
@@ -22702,8 +23978,8 @@ function buildPromptFromContext(ctx, contentType) {
22702
23978
  // src/L7-app/commands/ideaUpdate.ts
22703
23979
  init_environment();
22704
23980
  init_ideaService();
22705
- init_types();
22706
- var VALID_PLATFORMS2 = new Set(Object.values(Platform));
23981
+ init_types2();
23982
+ var VALID_PLATFORMS3 = new Set(Object.values(Platform));
22707
23983
  var VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "ready", "recorded", "published"]);
22708
23984
  var VALID_URGENCIES = /* @__PURE__ */ new Map([
22709
23985
  ["hot", 3],
@@ -22717,7 +23993,7 @@ var VALID_URGENCIES = /* @__PURE__ */ new Map([
22717
23993
  ]);
22718
23994
  function parsePlatforms2(raw) {
22719
23995
  if (!raw) return void 0;
22720
- return raw.split(",").map((p) => p.trim().toLowerCase()).filter((p) => VALID_PLATFORMS2.has(p));
23996
+ return raw.split(",").map((p) => p.trim().toLowerCase()).filter((p) => VALID_PLATFORMS3.has(p));
22721
23997
  }
22722
23998
  function parseCommaSeparated2(raw) {
22723
23999
  if (!raw) return void 0;
@@ -22887,8 +24163,9 @@ init_paths();
22887
24163
  init_postStore();
22888
24164
  init_ideaService2();
22889
24165
  init_scheduler();
22890
- init_types();
24166
+ init_types2();
22891
24167
  init_configLogger();
24168
+ init_queueMapping();
22892
24169
 
22893
24170
  // src/L7-app/review/approvalQueue.ts
22894
24171
  init_fileSystem();
@@ -22898,21 +24175,21 @@ init_scheduler();
22898
24175
  init_scheduleConfig();
22899
24176
 
22900
24177
  // src/L3-services/socialPosting/accountMapping.ts
22901
- init_types();
24178
+ init_types2();
22902
24179
  init_lateApi();
22903
24180
  init_configLogger();
22904
24181
  init_fileSystem();
22905
24182
  init_paths();
22906
- var CACHE_FILE = ".vidpipe-cache.json";
22907
- var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
22908
- var memoryCache = null;
24183
+ var CACHE_FILE2 = ".vidpipe-cache.json";
24184
+ var CACHE_TTL_MS2 = 24 * 60 * 60 * 1e3;
24185
+ var memoryCache2 = null;
22909
24186
  function toLatePlatform2(platform) {
22910
24187
  return platform === "x" /* X */ ? "twitter" : platform;
22911
24188
  }
22912
- function cachePath() {
22913
- return join(process.cwd(), CACHE_FILE);
24189
+ function cachePath2() {
24190
+ return join(process.cwd(), CACHE_FILE2);
22914
24191
  }
22915
- function isCacheValid(cache2) {
24192
+ function isCacheValid2(cache2) {
22916
24193
  const fetchedAtTime = new Date(cache2.fetchedAt).getTime();
22917
24194
  if (Number.isNaN(fetchedAtTime)) {
22918
24195
  logger_default.warn("Invalid fetchedAt in account cache; treating as stale", {
@@ -22921,13 +24198,13 @@ function isCacheValid(cache2) {
22921
24198
  return false;
22922
24199
  }
22923
24200
  const age = Date.now() - fetchedAtTime;
22924
- return age < CACHE_TTL_MS;
24201
+ return age < CACHE_TTL_MS2;
22925
24202
  }
22926
- async function readFileCache() {
24203
+ async function readFileCache2() {
22927
24204
  try {
22928
- const raw = await readTextFile(cachePath());
24205
+ const raw = await readTextFile(cachePath2());
22929
24206
  const cache2 = JSON.parse(raw);
22930
- if (cache2.accounts && cache2.fetchedAt && isCacheValid(cache2)) {
24207
+ if (cache2.accounts && cache2.fetchedAt && isCacheValid2(cache2)) {
22931
24208
  return cache2;
22932
24209
  }
22933
24210
  return null;
@@ -22935,7 +24212,7 @@ async function readFileCache() {
22935
24212
  return null;
22936
24213
  }
22937
24214
  }
22938
- async function writeFileCache(cache2) {
24215
+ async function writeFileCache2(cache2) {
22939
24216
  try {
22940
24217
  if (!cache2 || typeof cache2 !== "object" || !cache2.accounts || !cache2.fetchedAt) {
22941
24218
  logger_default.warn("Invalid cache structure, skipping write");
@@ -22951,7 +24228,7 @@ async function writeFileCache(cache2) {
22951
24228
  return;
22952
24229
  }
22953
24230
  }
22954
- const resolvedCachePath = resolve(cachePath());
24231
+ const resolvedCachePath = resolve(cachePath2());
22955
24232
  if (!resolvedCachePath.startsWith(resolve(process.cwd()) + sep)) {
22956
24233
  throw new Error("Cache path outside working directory");
22957
24234
  }
@@ -22960,7 +24237,7 @@ async function writeFileCache(cache2) {
22960
24237
  logger_default.warn("Failed to write account cache file", { error: err });
22961
24238
  }
22962
24239
  }
22963
- async function fetchAndCache() {
24240
+ async function fetchAndCache2() {
22964
24241
  const client = new LateApiClient();
22965
24242
  const accounts = await client.listAccounts();
22966
24243
  const mapping = {};
@@ -22973,37 +24250,38 @@ async function fetchAndCache() {
22973
24250
  accounts: mapping,
22974
24251
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
22975
24252
  };
22976
- memoryCache = cache2;
22977
- await writeFileCache(cache2);
24253
+ memoryCache2 = cache2;
24254
+ await writeFileCache2(cache2);
22978
24255
  logger_default.info("Refreshed Late account mappings", {
22979
24256
  platforms: Object.keys(mapping)
22980
24257
  });
22981
24258
  return mapping;
22982
24259
  }
22983
- async function ensureMappings() {
22984
- if (memoryCache && isCacheValid(memoryCache)) {
22985
- return memoryCache.accounts;
24260
+ async function ensureMappings2() {
24261
+ if (memoryCache2 && isCacheValid2(memoryCache2)) {
24262
+ return memoryCache2.accounts;
22986
24263
  }
22987
- const fileCache = await readFileCache();
24264
+ const fileCache = await readFileCache2();
22988
24265
  if (fileCache) {
22989
- memoryCache = fileCache;
24266
+ memoryCache2 = fileCache;
22990
24267
  return fileCache.accounts;
22991
24268
  }
22992
24269
  try {
22993
- return await fetchAndCache();
24270
+ return await fetchAndCache2();
22994
24271
  } catch (err) {
22995
24272
  logger_default.error("Failed to fetch Late account mappings", { error: err });
22996
24273
  return {};
22997
24274
  }
22998
24275
  }
22999
24276
  async function getAccountId(platform) {
23000
- const mappings = await ensureMappings();
24277
+ const mappings = await ensureMappings2();
23001
24278
  const latePlatform = toLatePlatform2(platform);
23002
24279
  return mappings[latePlatform] ?? null;
23003
24280
  }
23004
24281
 
23005
24282
  // src/L7-app/review/approvalQueue.ts
23006
- init_types();
24283
+ init_queueMapping();
24284
+ init_types2();
23007
24285
  init_configLogger();
23008
24286
  var queue = [];
23009
24287
  var processing = false;
@@ -23103,10 +24381,21 @@ async function processApprovalBatch(itemIds) {
23103
24381
  }
23104
24382
  const ideaIds = item.metadata.ideaIds;
23105
24383
  const publishBy = publishByMap.get(itemId);
23106
- const slot = ideaIds?.length ? await findNextSlot(latePlatform, item.metadata.clipType, { ideaIds, publishBy }) : await findNextSlot(latePlatform, item.metadata.clipType);
23107
- if (!slot) {
23108
- results.push({ itemId, success: false, error: `No available slot for ${latePlatform}` });
23109
- continue;
24384
+ const clipType = item.metadata.clipType || "short";
24385
+ const queueId = await getQueueId(latePlatform, clipType);
24386
+ let slot;
24387
+ let useQueue = false;
24388
+ if (queueId) {
24389
+ useQueue = true;
24390
+ logger_default.debug(`Using Late queue ${queueId} for ${latePlatform}/${clipType} (idea priority via batch order)`);
24391
+ } else {
24392
+ logger_default.debug(`No queue for ${latePlatform}/${clipType}, using local slot calculation`);
24393
+ const foundSlot = ideaIds?.length ? await findNextSlot(latePlatform, clipType, { ideaIds, publishBy }) : await findNextSlot(latePlatform, clipType);
24394
+ slot = foundSlot ?? void 0;
24395
+ if (!slot) {
24396
+ results.push({ itemId, success: false, error: `No available slot for ${latePlatform}` });
24397
+ continue;
24398
+ }
23110
24399
  }
23111
24400
  const platform = fromLatePlatform(latePlatform);
23112
24401
  const accountId = item.metadata.accountId || await getAccountId(platform);
@@ -23151,23 +24440,30 @@ async function processApprovalBatch(itemIds) {
23151
24440
  content_preview_confirmed: true,
23152
24441
  express_consent_given: true
23153
24442
  } : void 0;
23154
- const latePost = await client.createPost({
24443
+ const profileId = useQueue ? await getProfileId() : void 0;
24444
+ const createParams = {
23155
24445
  content: item.postContent,
23156
24446
  platforms: [{ platform: latePlatform, accountId }],
23157
- scheduledFor: slot,
23158
24447
  timezone: schedConfig.timezone,
23159
24448
  isDraft: false,
23160
24449
  mediaItems,
23161
24450
  platformSpecificData,
23162
24451
  tiktokSettings
23163
- });
24452
+ };
24453
+ if (useQueue) {
24454
+ createParams.queuedFromProfile = profileId;
24455
+ createParams.queueId = queueId ?? void 0;
24456
+ } else {
24457
+ createParams.scheduledFor = slot;
24458
+ }
24459
+ const latePost = await client.createPost(createParams);
23164
24460
  publishDataMap.set(itemId, {
23165
24461
  latePostId: latePost._id,
23166
- scheduledFor: slot,
24462
+ scheduledFor: latePost.scheduledFor ?? slot ?? "",
23167
24463
  publishedUrl: void 0,
23168
24464
  accountId
23169
24465
  });
23170
- results.push({ itemId, success: true, scheduledFor: slot, latePostId: latePost._id });
24466
+ results.push({ itemId, success: true, scheduledFor: latePost.scheduledFor ?? slot, latePostId: latePost._id });
23171
24467
  } catch (itemErr) {
23172
24468
  const itemMsg = itemErr instanceof Error ? itemErr.message : String(itemErr);
23173
24469
  if (itemMsg.includes("429") || itemMsg.includes("Daily post limit")) {
@@ -23197,7 +24493,7 @@ async function processApprovalBatch(itemIds) {
23197
24493
  }
23198
24494
 
23199
24495
  // src/L7-app/review/routes.ts
23200
- var CACHE_TTL_MS2 = 5 * 60 * 1e3;
24496
+ var CACHE_TTL_MS3 = 5 * 60 * 1e3;
23201
24497
  var cache = /* @__PURE__ */ new Map();
23202
24498
  function getCached(key) {
23203
24499
  const entry = cache.get(key);
@@ -23205,7 +24501,7 @@ function getCached(key) {
23205
24501
  cache.delete(key);
23206
24502
  return void 0;
23207
24503
  }
23208
- function setCache(key, data, ttl = CACHE_TTL_MS2) {
24504
+ function setCache(key, data, ttl = CACHE_TTL_MS3) {
23209
24505
  cache.set(key, { data, expiry: Date.now() + ttl });
23210
24506
  }
23211
24507
  async function getEarliestPublishBy(ideaIds) {
@@ -23374,6 +24670,18 @@ function createRouter() {
23374
24670
  try {
23375
24671
  const normalized = normalizePlatformString(req.params.platform);
23376
24672
  const clipType = typeof req.query.clipType === "string" ? req.query.clipType : void 0;
24673
+ const queueId = await getQueueId(normalized, clipType ?? "short");
24674
+ if (queueId) {
24675
+ try {
24676
+ const profileId = await getProfileId();
24677
+ const client = createLateApiClient();
24678
+ const preview = await client.previewQueue(profileId, queueId, 1);
24679
+ if (preview.slots?.length > 0) {
24680
+ return res.json({ platform: normalized, nextSlot: preview.slots[0], source: "queue" });
24681
+ }
24682
+ } catch {
24683
+ }
24684
+ }
23377
24685
  const slot = await findNextSlot(normalized, clipType);
23378
24686
  res.json({ platform: normalized, nextSlot: slot });
23379
24687
  } catch (err) {
@@ -23545,13 +24853,22 @@ program.command("schedule").description("View the current posting schedule acros
23545
24853
  await runSchedule({ platform: opts.platform });
23546
24854
  process.exit(0);
23547
24855
  });
23548
- program.command("realign").description("Realign all Late scheduled, cancelled, and failed posts to match schedule.json slots").option("--platform <name>", "Filter by platform (tiktok, youtube, instagram, linkedin, twitter)").option("--dry-run", "Preview changes without updating posts").action(async (opts) => {
23549
- await runRealign({ platform: opts.platform, dryRun: opts.dryRun });
24856
+ program.command("realign").description("Realign all Late scheduled, cancelled, and failed posts to match schedule.json slots").option("--platform <name>", "Filter by platform (tiktok, youtube, instagram, linkedin, twitter)").option("--dry-run", "Preview changes without updating posts").option("--queue", "Use Late API queue reshuffle instead of per-post reschedule").action(async (opts) => {
24857
+ await runRealign({ platform: opts.platform, dryRun: opts.dryRun, queue: opts.queue });
23550
24858
  process.exit(0);
23551
24859
  });
23552
- program.command("reschedule").description("Reschedule idea-linked posts for optimal slot placement, displacing non-idea content").option("--dry-run", "Preview changes without updating posts").action(async (opts) => {
24860
+ program.command("reschedule").description("Reschedule idea-linked posts for optimal slot placement, displacing non-idea content").option("--dry-run", "Preview changes without updating posts").option("--queue", "Use Late API queue reshuffle instead of per-post reschedule").action(async (opts) => {
23553
24861
  const { runReschedule: runReschedule2 } = await Promise.resolve().then(() => (init_reschedule(), reschedule_exports));
23554
- await runReschedule2({ dryRun: opts.dryRun });
24862
+ await runReschedule2({ dryRun: opts.dryRun, queue: opts.queue });
24863
+ process.exit(0);
24864
+ });
24865
+ program.command("sync-queues").description("Sync schedule.json queue definitions to Late API queues").option("--reshuffle", "Reschedule existing queued posts to match new slot times").option("--dry-run", "Preview changes without making API calls").option("--delete-orphans", "Delete Late queues not in schedule.json").action(async (opts) => {
24866
+ const { runSyncQueues: runSyncQueues2 } = await Promise.resolve().then(() => (init_syncQueues(), syncQueues_exports));
24867
+ await runSyncQueues2({
24868
+ reshuffle: opts.reshuffle,
24869
+ dryRun: opts.dryRun,
24870
+ deleteOrphans: opts.deleteOrphans
24871
+ });
23555
24872
  process.exit(0);
23556
24873
  });
23557
24874
  program.command("chat").description("Interactive chat session with the schedule management agent").action(async () => {
@@ -23627,7 +24944,29 @@ program.command("thumbnail").description("Generate a thumbnail for a recording f
23627
24944
  });
23628
24945
  process.exit(0);
23629
24946
  });
23630
- var defaultCmd = program.command("process", { isDefault: true }).argument("[video-path]", "Path to a video file to process (implies --once)").option("--watch-dir <path>", "Folder to watch for new recordings (default: env WATCH_FOLDER)").option("--output-dir <path>", "Output directory for processed videos (default: ./recordings)").option("--openai-key <key>", "OpenAI API key (default: env OPENAI_API_KEY)").option("--exa-key <key>", "Exa AI API key for web search (default: env EXA_API_KEY)").option("--youtube-key <key>", "YouTube API key (default: env YOUTUBE_API_KEY)").option("--perplexity-key <key>", "Perplexity API key (default: env PERPLEXITY_API_KEY)").option("--once", "Process a single video and exit (no watching)").option("--brand <path>", "Path to brand.json config (default: ./brand.json)").option("--no-silence-removal", "Skip silence removal stage").option("--no-shorts", "Skip shorts generation").option("--no-medium-clips", "Skip medium clip generation").option("--no-social", "Skip social media post generation").option("--no-captions", "Skip caption generation/burning").option("--no-visual-enhancement", "Skip visual enhancement (AI image overlays)").option("--no-intro-outro", "Skip intro/outro concatenation").option("--no-social-publish", "Skip social media publishing/queue-build stage").option("--late-api-key <key>", "Late API key (default: env LATE_API_KEY)").option("--late-profile-id <id>", "Late profile ID (default: env LATE_PROFILE_ID)").option("--ideas <ids>", "Comma-separated idea IDs to link to this video").option("--publish-by <date>", "Publish-by deadline for auto-created ideas (ISO date or +Nd for relative, default: +7d)").option("-v, --verbose", "Verbose logging").option("--progress", "Emit structured JSON progress events to stderr").option("--doctor", "Check all prerequisites and exit").action(async (videoPath) => {
24947
+ program.command("specs").description("List available pipeline spec presets and custom specs").action(async () => {
24948
+ const { PRESETS: PRESETS2 } = await Promise.resolve().then(() => (init_pipelineSpec(), pipelineSpec_exports));
24949
+ console.log("\nBuilt-in presets:\n");
24950
+ for (const [name, preset] of Object.entries(PRESETS2)) {
24951
+ console.log(` ${name.padEnd(12)} ${preset.description}`);
24952
+ }
24953
+ try {
24954
+ const specsDir = join(projectRoot(), "pipeline-specs");
24955
+ const files = listDirectorySync(specsDir);
24956
+ const specFiles = files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml") || f.endsWith(".json"));
24957
+ if (specFiles.length > 0) {
24958
+ console.log("\nCustom specs (pipeline-specs/):\n");
24959
+ for (const file of specFiles) {
24960
+ const name = file.replace(/\.(yaml|yml|json)$/, "");
24961
+ console.log(` ${name}`);
24962
+ }
24963
+ }
24964
+ } catch {
24965
+ }
24966
+ console.log("\nUsage: vidpipe process --spec <name-or-path> video.mp4\n");
24967
+ process.exit(0);
24968
+ });
24969
+ var defaultCmd = program.command("process", { isDefault: true }).argument("[video-path]", "Path to a video file to process (implies --once)").option("--watch-dir <path>", "Folder to watch for new recordings (default: env WATCH_FOLDER)").option("--output-dir <path>", "Output directory for processed videos (default: ./recordings)").option("--openai-key <key>", "OpenAI API key (default: env OPENAI_API_KEY)").option("--exa-key <key>", "Exa AI API key for web search (default: env EXA_API_KEY)").option("--youtube-key <key>", "YouTube API key (default: env YOUTUBE_API_KEY)").option("--perplexity-key <key>", "Perplexity API key (default: env PERPLEXITY_API_KEY)").option("--once", "Process a single video and exit (no watching)").option("--brand <path>", "Path to brand.json config (default: ./brand.json)").option("--no-silence-removal", "Skip silence removal stage").option("--no-shorts", "Skip shorts generation").option("--no-medium-clips", "Skip medium clip generation").option("--no-social", "Skip social media post generation").option("--no-captions", "Skip caption generation/burning").option("--no-visual-enhancement", "Skip visual enhancement (AI image overlays)").option("--no-intro-outro", "Skip intro/outro concatenation").option("--no-social-publish", "Skip social media publishing/queue-build stage").option("--spec <nameOrPath>", "Pipeline spec preset name or YAML file path").option("--late-api-key <key>", "Late API key (default: env LATE_API_KEY)").option("--late-profile-id <id>", "Late profile ID (default: env LATE_PROFILE_ID)").option("--ideas <ids>", "Comma-separated idea IDs to link to this video").option("--publish-by <date>", "Publish-by deadline for auto-created ideas (ISO date or +Nd for relative, default: +7d)").option("-v, --verbose", "Verbose logging").option("--progress", "Emit structured JSON progress events to stderr").option("--doctor", "Check all prerequisites and exit").action(async (videoPath) => {
23631
24970
  const opts = defaultCmd.opts();
23632
24971
  if (opts.doctor) {
23633
24972
  await runDoctor();
@@ -23662,6 +25001,18 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
23662
25001
  const config2 = getConfig();
23663
25002
  logger_default.info(`Watch folder: ${config2.WATCH_FOLDER}`);
23664
25003
  logger_default.info(`Output dir: ${config2.OUTPUT_DIR}`);
25004
+ let pipelineSpec;
25005
+ if (opts.spec) {
25006
+ try {
25007
+ const { loadSpec: loadSpec2 } = await Promise.resolve().then(() => (init_specLoader(), specLoader_exports));
25008
+ pipelineSpec = await loadSpec2(opts.spec, config2.REPO_ROOT);
25009
+ logger_default.info(`Using pipeline spec: ${pipelineSpec.name}`);
25010
+ } catch (err) {
25011
+ const msg = err instanceof Error ? err.message : String(err);
25012
+ logger_default.error(`Failed to load pipeline spec: ${msg}`);
25013
+ process.exit(1);
25014
+ }
25015
+ }
23665
25016
  let ideas;
23666
25017
  if (opts.ideas) {
23667
25018
  const { getIdeasByIds: getIdeasByIds2 } = await Promise.resolve().then(() => (init_ideaService2(), ideaService_exports2));
@@ -23692,7 +25043,7 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
23692
25043
  process.exit(1);
23693
25044
  }
23694
25045
  }
23695
- await processVideoSafe(resolvedPath, ideas, publishBy);
25046
+ await processVideoSafe(resolvedPath, ideas, publishBy, pipelineSpec);
23696
25047
  if (ideas && ideas.length > 0) {
23697
25048
  try {
23698
25049
  const { markRecorded: markRecorded3 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
@@ -23720,7 +25071,7 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
23720
25071
  while (queue2.length > 0) {
23721
25072
  const vp = queue2.shift();
23722
25073
  logger_default.info(`Processing video: ${vp}`);
23723
- await processVideoSafe(vp, ideas);
25074
+ await processVideoSafe(vp, ideas, void 0, pipelineSpec);
23724
25075
  if (onceMode) {
23725
25076
  logger_default.info("--once flag set, exiting after first video.");
23726
25077
  await shutdown();