vidpipe 1.3.12 → 1.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -4
- package/dist/cli.js +8854 -8423
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +67 -6
- package/dist/index.js +1239 -927
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -93,7 +93,6 @@ var init_types = __esm({
|
|
|
93
93
|
PipelineStage2["MediumClipPosts"] = "medium-clip-posts";
|
|
94
94
|
PipelineStage2["Blog"] = "blog";
|
|
95
95
|
PipelineStage2["QueueBuild"] = "queue-build";
|
|
96
|
-
PipelineStage2["GitPush"] = "git-push";
|
|
97
96
|
return PipelineStage2;
|
|
98
97
|
})(PipelineStage || {});
|
|
99
98
|
PIPELINE_STAGES = [
|
|
@@ -111,8 +110,7 @@ var init_types = __esm({
|
|
|
111
110
|
{ stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 12 },
|
|
112
111
|
{ stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 13 },
|
|
113
112
|
{ stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 14 },
|
|
114
|
-
{ stage: "blog" /* Blog */, name: "Blog", stageNumber: 15 }
|
|
115
|
-
{ stage: "git-push" /* GitPush */, name: "Git Push", stageNumber: 16 }
|
|
113
|
+
{ stage: "blog" /* Blog */, name: "Blog", stageNumber: 15 }
|
|
116
114
|
];
|
|
117
115
|
TOTAL_STAGES = PIPELINE_STAGES.length;
|
|
118
116
|
PLATFORM_CHAR_LIMITS = {
|
|
@@ -143,7 +141,7 @@ function findRoot(startDir) {
|
|
|
143
141
|
}
|
|
144
142
|
}
|
|
145
143
|
function projectRoot() {
|
|
146
|
-
if (!_cachedRoot) _cachedRoot = findRoot(
|
|
144
|
+
if (!_cachedRoot) _cachedRoot = findRoot(__dirname2);
|
|
147
145
|
return _cachedRoot;
|
|
148
146
|
}
|
|
149
147
|
function assetsDir(...segments) {
|
|
@@ -157,11 +155,11 @@ function modelsDir() {
|
|
|
157
155
|
const bundled = resolve2(projectRoot(), "dist", "models");
|
|
158
156
|
return existsSync(bundled) ? bundled : assetsDir("models");
|
|
159
157
|
}
|
|
160
|
-
var
|
|
158
|
+
var __dirname2, _cachedRoot;
|
|
161
159
|
var init_paths = __esm({
|
|
162
160
|
"src/L1-infra/paths/paths.ts"() {
|
|
163
161
|
"use strict";
|
|
164
|
-
|
|
162
|
+
__dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
165
163
|
}
|
|
166
164
|
});
|
|
167
165
|
|
|
@@ -448,6 +446,12 @@ function saveGlobalConfig(config2) {
|
|
|
448
446
|
chmodSync(configPath, 384);
|
|
449
447
|
}
|
|
450
448
|
}
|
|
449
|
+
function getGlobalConfigValue(section, key) {
|
|
450
|
+
const config2 = loadGlobalConfig();
|
|
451
|
+
const sectionValues = config2[section];
|
|
452
|
+
const value = sectionValues[key];
|
|
453
|
+
return typeof value === "string" ? value : void 0;
|
|
454
|
+
}
|
|
451
455
|
function setGlobalConfigValue(section, key, value) {
|
|
452
456
|
const config2 = loadGlobalConfig();
|
|
453
457
|
const sectionValues = config2[section];
|
|
@@ -471,8 +475,9 @@ var init_globalConfig = __esm({
|
|
|
471
475
|
|
|
472
476
|
// src/L1-infra/config/configResolver.ts
|
|
473
477
|
import { join as join4 } from "path";
|
|
474
|
-
function resolveString(...
|
|
475
|
-
|
|
478
|
+
function resolveString(cliValue, ...fallbacks) {
|
|
479
|
+
if (cliValue !== void 0) return cliValue;
|
|
480
|
+
for (const source of fallbacks) {
|
|
476
481
|
if (source !== void 0 && source !== "") {
|
|
477
482
|
return source;
|
|
478
483
|
}
|
|
@@ -490,7 +495,11 @@ function resolveBoolean(cliValue, envValue, defaultValue) {
|
|
|
490
495
|
}
|
|
491
496
|
function resolveConfig(cliOptions = {}) {
|
|
492
497
|
const globalConfig = loadGlobalConfig();
|
|
493
|
-
const repoRoot =
|
|
498
|
+
const repoRoot = resolveString(
|
|
499
|
+
cliOptions.repoRoot,
|
|
500
|
+
process.env.REPO_ROOT,
|
|
501
|
+
process.cwd()
|
|
502
|
+
);
|
|
494
503
|
return {
|
|
495
504
|
OPENAI_API_KEY: resolveString(
|
|
496
505
|
cliOptions.openaiKey,
|
|
@@ -504,14 +513,22 @@ function resolveConfig(cliOptions = {}) {
|
|
|
504
513
|
join4(repoRoot, "watch")
|
|
505
514
|
),
|
|
506
515
|
REPO_ROOT: repoRoot,
|
|
507
|
-
FFMPEG_PATH: resolveString(
|
|
508
|
-
|
|
516
|
+
FFMPEG_PATH: resolveString(
|
|
517
|
+
cliOptions.ffmpegPath,
|
|
518
|
+
process.env.FFMPEG_PATH,
|
|
519
|
+
"ffmpeg"
|
|
520
|
+
),
|
|
521
|
+
FFPROBE_PATH: resolveString(
|
|
522
|
+
cliOptions.ffprobePath,
|
|
523
|
+
process.env.FFPROBE_PATH,
|
|
524
|
+
"ffprobe"
|
|
525
|
+
),
|
|
509
526
|
EXA_API_KEY: resolveString(
|
|
510
527
|
cliOptions.exaKey,
|
|
511
528
|
process.env.EXA_API_KEY,
|
|
512
529
|
globalConfig.credentials.exaApiKey
|
|
513
530
|
),
|
|
514
|
-
EXA_MCP_URL: resolveString(process.env.EXA_MCP_URL, "https://mcp.exa.ai/mcp"),
|
|
531
|
+
EXA_MCP_URL: resolveString(void 0, process.env.EXA_MCP_URL, "https://mcp.exa.ai/mcp"),
|
|
515
532
|
YOUTUBE_API_KEY: resolveString(
|
|
516
533
|
cliOptions.youtubeKey,
|
|
517
534
|
process.env.YOUTUBE_API_KEY,
|
|
@@ -523,12 +540,18 @@ function resolveConfig(cliOptions = {}) {
|
|
|
523
540
|
globalConfig.credentials.perplexityApiKey
|
|
524
541
|
),
|
|
525
542
|
LLM_PROVIDER: resolveString(
|
|
543
|
+
cliOptions.llmProvider,
|
|
526
544
|
process.env.LLM_PROVIDER,
|
|
527
545
|
globalConfig.defaults.llmProvider,
|
|
528
546
|
"copilot"
|
|
529
547
|
),
|
|
530
|
-
LLM_MODEL: resolveString(
|
|
548
|
+
LLM_MODEL: resolveString(
|
|
549
|
+
cliOptions.llmModel,
|
|
550
|
+
process.env.LLM_MODEL,
|
|
551
|
+
globalConfig.defaults.llmModel
|
|
552
|
+
),
|
|
531
553
|
ANTHROPIC_API_KEY: resolveString(
|
|
554
|
+
cliOptions.anthropicKey,
|
|
532
555
|
process.env.ANTHROPIC_API_KEY,
|
|
533
556
|
globalConfig.credentials.anthropicApiKey
|
|
534
557
|
),
|
|
@@ -545,7 +568,6 @@ function resolveConfig(cliOptions = {}) {
|
|
|
545
568
|
join4(repoRoot, "brand.json")
|
|
546
569
|
),
|
|
547
570
|
VERBOSE: cliOptions.verbose ?? false,
|
|
548
|
-
SKIP_GIT: resolveBoolean(cliOptions.git === void 0 ? void 0 : !cliOptions.git, process.env.SKIP_GIT, false),
|
|
549
571
|
SKIP_SILENCE_REMOVAL: resolveBoolean(
|
|
550
572
|
cliOptions.silenceRemoval === void 0 ? void 0 : !cliOptions.silenceRemoval,
|
|
551
573
|
process.env.SKIP_SILENCE_REMOVAL,
|
|
@@ -592,10 +614,12 @@ function resolveConfig(cliOptions = {}) {
|
|
|
592
614
|
false
|
|
593
615
|
),
|
|
594
616
|
GEMINI_API_KEY: resolveString(
|
|
617
|
+
cliOptions.geminiKey,
|
|
595
618
|
process.env.GEMINI_API_KEY,
|
|
596
619
|
globalConfig.credentials.geminiApiKey
|
|
597
620
|
),
|
|
598
621
|
GEMINI_MODEL: resolveString(
|
|
622
|
+
cliOptions.geminiModel,
|
|
599
623
|
process.env.GEMINI_MODEL,
|
|
600
624
|
globalConfig.defaults.geminiModel,
|
|
601
625
|
"gemini-2.5-pro"
|
|
@@ -610,9 +634,19 @@ function resolveConfig(cliOptions = {}) {
|
|
|
610
634
|
cliOptions.githubToken,
|
|
611
635
|
process.env.GITHUB_TOKEN,
|
|
612
636
|
globalConfig.credentials.githubToken
|
|
613
|
-
)
|
|
637
|
+
),
|
|
638
|
+
MODEL_OVERRIDES: resolveModelOverrides()
|
|
614
639
|
};
|
|
615
640
|
}
|
|
641
|
+
function resolveModelOverrides() {
|
|
642
|
+
const overrides = {};
|
|
643
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
644
|
+
if (key.startsWith("MODEL_") && value) {
|
|
645
|
+
overrides[key] = value;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return overrides;
|
|
649
|
+
}
|
|
616
650
|
var init_configResolver = __esm({
|
|
617
651
|
"src/L1-infra/config/configResolver.ts"() {
|
|
618
652
|
"use strict";
|
|
@@ -628,7 +662,7 @@ __export(environment_exports, {
|
|
|
628
662
|
validateRequiredKeys: () => validateRequiredKeys
|
|
629
663
|
});
|
|
630
664
|
function validateRequiredKeys() {
|
|
631
|
-
if (!config?.OPENAI_API_KEY
|
|
665
|
+
if (!config?.OPENAI_API_KEY) {
|
|
632
666
|
throw new Error("Missing required: OPENAI_API_KEY (set via --openai-key, env var, or vidpipe configure)");
|
|
633
667
|
}
|
|
634
668
|
}
|
|
@@ -1439,7 +1473,7 @@ var require_messages = __commonJS({
|
|
|
1439
1473
|
ErrorCodes2.jsonrpcReservedErrorRangeEnd = -32e3;
|
|
1440
1474
|
ErrorCodes2.serverErrorEnd = -32e3;
|
|
1441
1475
|
})(ErrorCodes || (exports.ErrorCodes = ErrorCodes = {}));
|
|
1442
|
-
var
|
|
1476
|
+
var ResponseError2 = class _ResponseError extends Error {
|
|
1443
1477
|
constructor(code, message, data) {
|
|
1444
1478
|
super(message);
|
|
1445
1479
|
this.code = is.number(code) ? code : ErrorCodes.UnknownErrorCode;
|
|
@@ -1457,7 +1491,7 @@ var require_messages = __commonJS({
|
|
|
1457
1491
|
return result;
|
|
1458
1492
|
}
|
|
1459
1493
|
};
|
|
1460
|
-
exports.ResponseError =
|
|
1494
|
+
exports.ResponseError = ResponseError2;
|
|
1461
1495
|
var ParameterStructures = class _ParameterStructures {
|
|
1462
1496
|
constructor(kind) {
|
|
1463
1497
|
this.kind = kind;
|
|
@@ -3010,14 +3044,14 @@ var require_connection = __commonJS({
|
|
|
3010
3044
|
ConnectionErrors2[ConnectionErrors2["Disposed"] = 2] = "Disposed";
|
|
3011
3045
|
ConnectionErrors2[ConnectionErrors2["AlreadyListening"] = 3] = "AlreadyListening";
|
|
3012
3046
|
})(ConnectionErrors || (exports.ConnectionErrors = ConnectionErrors = {}));
|
|
3013
|
-
var
|
|
3047
|
+
var ConnectionError2 = class _ConnectionError extends Error {
|
|
3014
3048
|
constructor(code, message) {
|
|
3015
3049
|
super(message);
|
|
3016
3050
|
this.code = code;
|
|
3017
3051
|
Object.setPrototypeOf(this, _ConnectionError.prototype);
|
|
3018
3052
|
}
|
|
3019
3053
|
};
|
|
3020
|
-
exports.ConnectionError =
|
|
3054
|
+
exports.ConnectionError = ConnectionError2;
|
|
3021
3055
|
var ConnectionStrategy;
|
|
3022
3056
|
(function(ConnectionStrategy2) {
|
|
3023
3057
|
function is(value) {
|
|
@@ -3640,15 +3674,15 @@ ${JSON.stringify(message, null, 4)}`);
|
|
|
3640
3674
|
}
|
|
3641
3675
|
function throwIfClosedOrDisposed() {
|
|
3642
3676
|
if (isClosed()) {
|
|
3643
|
-
throw new
|
|
3677
|
+
throw new ConnectionError2(ConnectionErrors.Closed, "Connection is closed.");
|
|
3644
3678
|
}
|
|
3645
3679
|
if (isDisposed()) {
|
|
3646
|
-
throw new
|
|
3680
|
+
throw new ConnectionError2(ConnectionErrors.Disposed, "Connection is disposed.");
|
|
3647
3681
|
}
|
|
3648
3682
|
}
|
|
3649
3683
|
function throwIfListening() {
|
|
3650
3684
|
if (isListening()) {
|
|
3651
|
-
throw new
|
|
3685
|
+
throw new ConnectionError2(ConnectionErrors.AlreadyListening, "Connection is already listening");
|
|
3652
3686
|
}
|
|
3653
3687
|
}
|
|
3654
3688
|
function throwIfNotListening() {
|
|
@@ -4633,6 +4667,150 @@ var require_node = __commonJS({
|
|
|
4633
4667
|
}
|
|
4634
4668
|
});
|
|
4635
4669
|
|
|
4670
|
+
// src/L3-services/postStore/postStore.ts
|
|
4671
|
+
function getQueueDir() {
|
|
4672
|
+
const { OUTPUT_DIR } = getConfig();
|
|
4673
|
+
return join(OUTPUT_DIR, "publish-queue");
|
|
4674
|
+
}
|
|
4675
|
+
function getPublishedDir() {
|
|
4676
|
+
const { OUTPUT_DIR } = getConfig();
|
|
4677
|
+
return join(OUTPUT_DIR, "published");
|
|
4678
|
+
}
|
|
4679
|
+
async function readQueueItem(folderPath, id) {
|
|
4680
|
+
const metadataPath = join(folderPath, "metadata.json");
|
|
4681
|
+
const postPath = join(folderPath, "post.md");
|
|
4682
|
+
try {
|
|
4683
|
+
const metadataRaw = await readTextFile(metadataPath);
|
|
4684
|
+
const metadata = JSON.parse(metadataRaw);
|
|
4685
|
+
let postContent = "";
|
|
4686
|
+
try {
|
|
4687
|
+
postContent = await readTextFile(postPath);
|
|
4688
|
+
} catch {
|
|
4689
|
+
logger_default.debug(`No post.md found for ${String(id).replace(/[\r\n]/g, "")}`);
|
|
4690
|
+
}
|
|
4691
|
+
const videoPath = join(folderPath, "media.mp4");
|
|
4692
|
+
const imagePath = join(folderPath, "media.png");
|
|
4693
|
+
let mediaPath = null;
|
|
4694
|
+
let hasMedia = false;
|
|
4695
|
+
if (await fileExists(videoPath)) {
|
|
4696
|
+
mediaPath = videoPath;
|
|
4697
|
+
hasMedia = true;
|
|
4698
|
+
} else if (await fileExists(imagePath)) {
|
|
4699
|
+
mediaPath = imagePath;
|
|
4700
|
+
hasMedia = true;
|
|
4701
|
+
}
|
|
4702
|
+
return {
|
|
4703
|
+
id,
|
|
4704
|
+
metadata,
|
|
4705
|
+
postContent,
|
|
4706
|
+
hasMedia,
|
|
4707
|
+
mediaPath,
|
|
4708
|
+
folderPath
|
|
4709
|
+
};
|
|
4710
|
+
} catch (err) {
|
|
4711
|
+
logger_default.debug(`Failed to read queue item ${String(id).replace(/[\r\n]/g, "")}: ${String(err).replace(/[\r\n]/g, "")}`);
|
|
4712
|
+
return null;
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
async function getPendingItems() {
|
|
4716
|
+
const queueDir = getQueueDir();
|
|
4717
|
+
await ensureDirectory(queueDir);
|
|
4718
|
+
let entries;
|
|
4719
|
+
try {
|
|
4720
|
+
const dirents = await listDirectoryWithTypes(queueDir);
|
|
4721
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4722
|
+
} catch {
|
|
4723
|
+
return [];
|
|
4724
|
+
}
|
|
4725
|
+
const items = [];
|
|
4726
|
+
for (const name of entries) {
|
|
4727
|
+
const item = await readQueueItem(join(queueDir, name), name);
|
|
4728
|
+
if (item) items.push(item);
|
|
4729
|
+
}
|
|
4730
|
+
items.sort((a, b) => {
|
|
4731
|
+
if (a.hasMedia !== b.hasMedia) return a.hasMedia ? -1 : 1;
|
|
4732
|
+
return a.metadata.createdAt.localeCompare(b.metadata.createdAt);
|
|
4733
|
+
});
|
|
4734
|
+
return items;
|
|
4735
|
+
}
|
|
4736
|
+
async function createItem(id, metadata, postContent, mediaSourcePath) {
|
|
4737
|
+
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
4738
|
+
throw new Error(`Invalid ID format: ${id}`);
|
|
4739
|
+
}
|
|
4740
|
+
const folderPath = join(getQueueDir(), basename(id));
|
|
4741
|
+
await ensureDirectory(folderPath);
|
|
4742
|
+
await writeJsonFile(join(folderPath, "metadata.json"), metadata);
|
|
4743
|
+
await writeTextFile(join(folderPath, "post.md"), postContent);
|
|
4744
|
+
let hasMedia = false;
|
|
4745
|
+
const ext = mediaSourcePath ? extname(mediaSourcePath) : ".mp4";
|
|
4746
|
+
const mediaFilename = `media${ext}`;
|
|
4747
|
+
const mediaPath = join(folderPath, mediaFilename);
|
|
4748
|
+
if (mediaSourcePath) {
|
|
4749
|
+
await copyFile(mediaSourcePath, mediaPath);
|
|
4750
|
+
hasMedia = true;
|
|
4751
|
+
}
|
|
4752
|
+
logger_default.debug(`Created queue item: ${String(id).replace(/[\r\n]/g, "")}`);
|
|
4753
|
+
return {
|
|
4754
|
+
id,
|
|
4755
|
+
metadata,
|
|
4756
|
+
postContent,
|
|
4757
|
+
hasMedia,
|
|
4758
|
+
mediaPath: hasMedia ? mediaPath : null,
|
|
4759
|
+
folderPath
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4762
|
+
async function getPublishedItems() {
|
|
4763
|
+
const publishedDir = getPublishedDir();
|
|
4764
|
+
await ensureDirectory(publishedDir);
|
|
4765
|
+
let entries;
|
|
4766
|
+
try {
|
|
4767
|
+
const dirents = await listDirectoryWithTypes(publishedDir);
|
|
4768
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4769
|
+
} catch {
|
|
4770
|
+
return [];
|
|
4771
|
+
}
|
|
4772
|
+
const items = [];
|
|
4773
|
+
for (const name of entries) {
|
|
4774
|
+
const item = await readQueueItem(join(publishedDir, name), name);
|
|
4775
|
+
if (item) items.push(item);
|
|
4776
|
+
}
|
|
4777
|
+
items.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
|
|
4778
|
+
return items;
|
|
4779
|
+
}
|
|
4780
|
+
async function getScheduledItemsByIdeaIds(ideaIds) {
|
|
4781
|
+
if (ideaIds.length === 0) return [];
|
|
4782
|
+
const ideaIdSet = new Set(ideaIds);
|
|
4783
|
+
const [pendingItems, publishedItems] = await Promise.all([
|
|
4784
|
+
getPendingItems(),
|
|
4785
|
+
getPublishedItems()
|
|
4786
|
+
]);
|
|
4787
|
+
return [...pendingItems, ...publishedItems].filter(
|
|
4788
|
+
(item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
|
|
4789
|
+
);
|
|
4790
|
+
}
|
|
4791
|
+
async function itemExists(id) {
|
|
4792
|
+
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
4793
|
+
throw new Error(`Invalid ID format: ${id}`);
|
|
4794
|
+
}
|
|
4795
|
+
if (await fileExists(join(getQueueDir(), basename(id)))) {
|
|
4796
|
+
return "pending";
|
|
4797
|
+
}
|
|
4798
|
+
if (await fileExists(join(getPublishedDir(), basename(id)))) {
|
|
4799
|
+
return "published";
|
|
4800
|
+
}
|
|
4801
|
+
return null;
|
|
4802
|
+
}
|
|
4803
|
+
var init_postStore = __esm({
|
|
4804
|
+
"src/L3-services/postStore/postStore.ts"() {
|
|
4805
|
+
"use strict";
|
|
4806
|
+
init_types();
|
|
4807
|
+
init_environment();
|
|
4808
|
+
init_configLogger();
|
|
4809
|
+
init_fileSystem();
|
|
4810
|
+
init_paths();
|
|
4811
|
+
}
|
|
4812
|
+
});
|
|
4813
|
+
|
|
4636
4814
|
// src/L7-app/sdk/VidPipeSDK.ts
|
|
4637
4815
|
init_types();
|
|
4638
4816
|
init_environment();
|
|
@@ -4640,6 +4818,47 @@ init_globalConfig();
|
|
|
4640
4818
|
init_fileSystem();
|
|
4641
4819
|
init_paths();
|
|
4642
4820
|
|
|
4821
|
+
// src/L1-infra/progress/progressEmitter.ts
|
|
4822
|
+
var ProgressEmitter = class {
|
|
4823
|
+
enabled = false;
|
|
4824
|
+
listeners = /* @__PURE__ */ new Set();
|
|
4825
|
+
/** Turn on progress event output to stderr. */
|
|
4826
|
+
enable() {
|
|
4827
|
+
this.enabled = true;
|
|
4828
|
+
}
|
|
4829
|
+
/** Turn off progress event output. */
|
|
4830
|
+
disable() {
|
|
4831
|
+
this.enabled = false;
|
|
4832
|
+
}
|
|
4833
|
+
/** Whether the emitter is currently active (stderr or listeners). */
|
|
4834
|
+
isEnabled() {
|
|
4835
|
+
return this.enabled || this.listeners.size > 0;
|
|
4836
|
+
}
|
|
4837
|
+
/** Register a programmatic listener for progress events. */
|
|
4838
|
+
addListener(fn) {
|
|
4839
|
+
this.listeners.add(fn);
|
|
4840
|
+
}
|
|
4841
|
+
/** Remove a previously registered listener. */
|
|
4842
|
+
removeListener(fn) {
|
|
4843
|
+
this.listeners.delete(fn);
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4846
|
+
* Write a progress event as a single JSON line to stderr (if enabled)
|
|
4847
|
+
* and dispatch to all registered listeners.
|
|
4848
|
+
* No-op when neither stderr output nor listeners are active.
|
|
4849
|
+
*/
|
|
4850
|
+
emit(event) {
|
|
4851
|
+
if (!this.enabled && this.listeners.size === 0) return;
|
|
4852
|
+
if (this.enabled) {
|
|
4853
|
+
process.stderr.write(JSON.stringify(event) + "\n");
|
|
4854
|
+
}
|
|
4855
|
+
for (const listener of this.listeners) {
|
|
4856
|
+
listener(event);
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
};
|
|
4860
|
+
var progressEmitter = new ProgressEmitter();
|
|
4861
|
+
|
|
4643
4862
|
// src/L1-infra/process/process.ts
|
|
4644
4863
|
import { execFile as nodeExecFile, execSync as nodeExecSync, spawnSync as nodeSpawnSync } from "child_process";
|
|
4645
4864
|
import { createRequire } from "module";
|
|
@@ -4648,9 +4867,6 @@ function execFileRaw(cmd, args, opts, callback) {
|
|
|
4648
4867
|
callback(error, String(stdout ?? ""), String(stderr ?? ""));
|
|
4649
4868
|
});
|
|
4650
4869
|
}
|
|
4651
|
-
function execCommandSync(cmd, opts) {
|
|
4652
|
-
return nodeExecSync(cmd, { encoding: "utf-8", ...opts }).toString().trim();
|
|
4653
|
-
}
|
|
4654
4870
|
function spawnCommand(cmd, args, opts) {
|
|
4655
4871
|
return nodeSpawnSync(cmd, args, { encoding: "utf-8", ...opts });
|
|
4656
4872
|
}
|
|
@@ -4744,12 +4960,13 @@ var AGENT_MODEL_MAP = {
|
|
|
4744
4960
|
ProducerAgent: PREMIUM_MODEL
|
|
4745
4961
|
};
|
|
4746
4962
|
function getModelForAgent(agentName) {
|
|
4963
|
+
const config2 = getConfig();
|
|
4747
4964
|
const envKey = `MODEL_${agentName.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase()}`;
|
|
4748
|
-
const envOverride =
|
|
4965
|
+
const envOverride = config2.MODEL_OVERRIDES[envKey];
|
|
4749
4966
|
if (envOverride) return envOverride;
|
|
4750
4967
|
const mapped = AGENT_MODEL_MAP[agentName];
|
|
4751
4968
|
if (mapped) return mapped;
|
|
4752
|
-
const global =
|
|
4969
|
+
const global = config2.LLM_MODEL;
|
|
4753
4970
|
if (global) return global;
|
|
4754
4971
|
return void 0;
|
|
4755
4972
|
}
|
|
@@ -4765,20 +4982,77 @@ import { default as default4 } from "openai";
|
|
|
4765
4982
|
import { default as default5 } from "@anthropic-ai/sdk";
|
|
4766
4983
|
|
|
4767
4984
|
// node_modules/@github/copilot-sdk/dist/client.js
|
|
4768
|
-
var
|
|
4985
|
+
var import_node2 = __toESM(require_node(), 1);
|
|
4769
4986
|
import { spawn } from "child_process";
|
|
4770
4987
|
import { existsSync as existsSync4 } from "fs";
|
|
4771
4988
|
import { Socket } from "net";
|
|
4772
4989
|
import { dirname as dirname3, join as join5 } from "path";
|
|
4773
4990
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4774
4991
|
|
|
4992
|
+
// node_modules/@github/copilot-sdk/dist/generated/rpc.js
|
|
4993
|
+
function createServerRpc(connection) {
|
|
4994
|
+
return {
|
|
4995
|
+
ping: async (params) => connection.sendRequest("ping", params),
|
|
4996
|
+
models: {
|
|
4997
|
+
list: async () => connection.sendRequest("models.list", {})
|
|
4998
|
+
},
|
|
4999
|
+
tools: {
|
|
5000
|
+
list: async (params) => connection.sendRequest("tools.list", params)
|
|
5001
|
+
},
|
|
5002
|
+
account: {
|
|
5003
|
+
getQuota: async () => connection.sendRequest("account.getQuota", {})
|
|
5004
|
+
}
|
|
5005
|
+
};
|
|
5006
|
+
}
|
|
5007
|
+
function createSessionRpc(connection, sessionId) {
|
|
5008
|
+
return {
|
|
5009
|
+
model: {
|
|
5010
|
+
getCurrent: async () => connection.sendRequest("session.model.getCurrent", { sessionId }),
|
|
5011
|
+
switchTo: async (params) => connection.sendRequest("session.model.switchTo", { sessionId, ...params })
|
|
5012
|
+
},
|
|
5013
|
+
mode: {
|
|
5014
|
+
get: async () => connection.sendRequest("session.mode.get", { sessionId }),
|
|
5015
|
+
set: async (params) => connection.sendRequest("session.mode.set", { sessionId, ...params })
|
|
5016
|
+
},
|
|
5017
|
+
plan: {
|
|
5018
|
+
read: async () => connection.sendRequest("session.plan.read", { sessionId }),
|
|
5019
|
+
update: async (params) => connection.sendRequest("session.plan.update", { sessionId, ...params }),
|
|
5020
|
+
delete: async () => connection.sendRequest("session.plan.delete", { sessionId })
|
|
5021
|
+
},
|
|
5022
|
+
workspace: {
|
|
5023
|
+
listFiles: async () => connection.sendRequest("session.workspace.listFiles", { sessionId }),
|
|
5024
|
+
readFile: async (params) => connection.sendRequest("session.workspace.readFile", { sessionId, ...params }),
|
|
5025
|
+
createFile: async (params) => connection.sendRequest("session.workspace.createFile", { sessionId, ...params })
|
|
5026
|
+
},
|
|
5027
|
+
fleet: {
|
|
5028
|
+
start: async (params) => connection.sendRequest("session.fleet.start", { sessionId, ...params })
|
|
5029
|
+
},
|
|
5030
|
+
agent: {
|
|
5031
|
+
list: async () => connection.sendRequest("session.agent.list", { sessionId }),
|
|
5032
|
+
getCurrent: async () => connection.sendRequest("session.agent.getCurrent", { sessionId }),
|
|
5033
|
+
select: async (params) => connection.sendRequest("session.agent.select", { sessionId, ...params }),
|
|
5034
|
+
deselect: async () => connection.sendRequest("session.agent.deselect", { sessionId })
|
|
5035
|
+
},
|
|
5036
|
+
compaction: {
|
|
5037
|
+
compact: async () => connection.sendRequest("session.compaction.compact", { sessionId })
|
|
5038
|
+
},
|
|
5039
|
+
tools: {
|
|
5040
|
+
handlePendingToolCall: async (params) => connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params })
|
|
5041
|
+
},
|
|
5042
|
+
permissions: {
|
|
5043
|
+
handlePendingPermissionRequest: async (params) => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params })
|
|
5044
|
+
}
|
|
5045
|
+
};
|
|
5046
|
+
}
|
|
5047
|
+
|
|
4775
5048
|
// node_modules/@github/copilot-sdk/dist/sdkProtocolVersion.js
|
|
4776
|
-
var SDK_PROTOCOL_VERSION =
|
|
5049
|
+
var SDK_PROTOCOL_VERSION = 3;
|
|
4777
5050
|
function getSdkProtocolVersion() {
|
|
4778
5051
|
return SDK_PROTOCOL_VERSION;
|
|
4779
5052
|
}
|
|
4780
5053
|
|
|
4781
5054
|
// node_modules/@github/copilot-sdk/dist/session.js
|
|
5055
|
+
var import_node = __toESM(require_node(), 1);
|
|
4782
5056
|
var CopilotSession = class {
|
|
4783
5057
|
/**
|
|
4784
5058
|
* Creates a new CopilotSession instance.
|
|
@@ -4799,6 +5073,16 @@ var CopilotSession = class {
|
|
|
4799
5073
|
permissionHandler;
|
|
4800
5074
|
userInputHandler;
|
|
4801
5075
|
hooks;
|
|
5076
|
+
_rpc = null;
|
|
5077
|
+
/**
|
|
5078
|
+
* Typed session-scoped RPC methods.
|
|
5079
|
+
*/
|
|
5080
|
+
get rpc() {
|
|
5081
|
+
if (!this._rpc) {
|
|
5082
|
+
this._rpc = createSessionRpc(this.connection, this.sessionId);
|
|
5083
|
+
}
|
|
5084
|
+
return this._rpc;
|
|
5085
|
+
}
|
|
4802
5086
|
/**
|
|
4803
5087
|
* Path to the session workspace directory when infinite sessions are enabled.
|
|
4804
5088
|
* Contains checkpoints/, plan.md, and files/ subdirectories.
|
|
@@ -4815,7 +5099,7 @@ var CopilotSession = class {
|
|
|
4815
5099
|
*
|
|
4816
5100
|
* @param options - The message options including the prompt and optional attachments
|
|
4817
5101
|
* @returns A promise that resolves with the message ID of the response
|
|
4818
|
-
* @throws Error if the session has been
|
|
5102
|
+
* @throws Error if the session has been disconnected or the connection fails
|
|
4819
5103
|
*
|
|
4820
5104
|
* @example
|
|
4821
5105
|
* ```typescript
|
|
@@ -4848,7 +5132,7 @@ var CopilotSession = class {
|
|
|
4848
5132
|
* @returns A promise that resolves with the final assistant message when the session becomes idle,
|
|
4849
5133
|
* or undefined if no assistant message was received
|
|
4850
5134
|
* @throws Error if the timeout is reached before the session becomes idle
|
|
4851
|
-
* @throws Error if the session has been
|
|
5135
|
+
* @throws Error if the session has been disconnected or the connection fails
|
|
4852
5136
|
*
|
|
4853
5137
|
* @example
|
|
4854
5138
|
* ```typescript
|
|
@@ -4922,11 +5206,13 @@ var CopilotSession = class {
|
|
|
4922
5206
|
}
|
|
4923
5207
|
/**
|
|
4924
5208
|
* Dispatches an event to all registered handlers.
|
|
5209
|
+
* Also handles broadcast request events internally (external tool calls, permissions).
|
|
4925
5210
|
*
|
|
4926
5211
|
* @param event - The session event to dispatch
|
|
4927
5212
|
* @internal This method is for internal use by the SDK.
|
|
4928
5213
|
*/
|
|
4929
5214
|
_dispatchEvent(event) {
|
|
5215
|
+
this._handleBroadcastEvent(event);
|
|
4930
5216
|
const typedHandlers = this.typedEventHandlers.get(event.type);
|
|
4931
5217
|
if (typedHandlers) {
|
|
4932
5218
|
for (const handler of typedHandlers) {
|
|
@@ -4943,6 +5229,85 @@ var CopilotSession = class {
|
|
|
4943
5229
|
}
|
|
4944
5230
|
}
|
|
4945
5231
|
}
|
|
5232
|
+
/**
|
|
5233
|
+
* Handles broadcast request events by executing local handlers and responding via RPC.
|
|
5234
|
+
* Handlers are dispatched as fire-and-forget — rejections propagate as unhandled promise
|
|
5235
|
+
* rejections, consistent with standard EventEmitter / event handler semantics.
|
|
5236
|
+
* @internal
|
|
5237
|
+
*/
|
|
5238
|
+
_handleBroadcastEvent(event) {
|
|
5239
|
+
if (event.type === "external_tool.requested") {
|
|
5240
|
+
const { requestId, toolName } = event.data;
|
|
5241
|
+
const args = event.data.arguments;
|
|
5242
|
+
const toolCallId = event.data.toolCallId;
|
|
5243
|
+
const handler = this.toolHandlers.get(toolName);
|
|
5244
|
+
if (handler) {
|
|
5245
|
+
void this._executeToolAndRespond(requestId, toolName, toolCallId, args, handler);
|
|
5246
|
+
}
|
|
5247
|
+
} else if (event.type === "permission.requested") {
|
|
5248
|
+
const { requestId, permissionRequest } = event.data;
|
|
5249
|
+
if (this.permissionHandler) {
|
|
5250
|
+
void this._executePermissionAndRespond(requestId, permissionRequest);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
/**
|
|
5255
|
+
* Executes a tool handler and sends the result back via RPC.
|
|
5256
|
+
* @internal
|
|
5257
|
+
*/
|
|
5258
|
+
async _executeToolAndRespond(requestId, toolName, toolCallId, args, handler) {
|
|
5259
|
+
try {
|
|
5260
|
+
const rawResult = await handler(args, {
|
|
5261
|
+
sessionId: this.sessionId,
|
|
5262
|
+
toolCallId,
|
|
5263
|
+
toolName,
|
|
5264
|
+
arguments: args
|
|
5265
|
+
});
|
|
5266
|
+
let result;
|
|
5267
|
+
if (rawResult == null) {
|
|
5268
|
+
result = "";
|
|
5269
|
+
} else if (typeof rawResult === "string") {
|
|
5270
|
+
result = rawResult;
|
|
5271
|
+
} else {
|
|
5272
|
+
result = JSON.stringify(rawResult);
|
|
5273
|
+
}
|
|
5274
|
+
await this.rpc.tools.handlePendingToolCall({ requestId, result });
|
|
5275
|
+
} catch (error) {
|
|
5276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5277
|
+
try {
|
|
5278
|
+
await this.rpc.tools.handlePendingToolCall({ requestId, error: message });
|
|
5279
|
+
} catch (rpcError) {
|
|
5280
|
+
if (!(rpcError instanceof import_node.ConnectionError || rpcError instanceof import_node.ResponseError)) {
|
|
5281
|
+
throw rpcError;
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
/**
|
|
5287
|
+
* Executes a permission handler and sends the result back via RPC.
|
|
5288
|
+
* @internal
|
|
5289
|
+
*/
|
|
5290
|
+
async _executePermissionAndRespond(requestId, permissionRequest) {
|
|
5291
|
+
try {
|
|
5292
|
+
const result = await this.permissionHandler(permissionRequest, {
|
|
5293
|
+
sessionId: this.sessionId
|
|
5294
|
+
});
|
|
5295
|
+
await this.rpc.permissions.handlePendingPermissionRequest({ requestId, result });
|
|
5296
|
+
} catch (_error) {
|
|
5297
|
+
try {
|
|
5298
|
+
await this.rpc.permissions.handlePendingPermissionRequest({
|
|
5299
|
+
requestId,
|
|
5300
|
+
result: {
|
|
5301
|
+
kind: "denied-no-approval-rule-and-could-not-request-from-user"
|
|
5302
|
+
}
|
|
5303
|
+
});
|
|
5304
|
+
} catch (rpcError) {
|
|
5305
|
+
if (!(rpcError instanceof import_node.ConnectionError || rpcError instanceof import_node.ResponseError)) {
|
|
5306
|
+
throw rpcError;
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
4946
5311
|
/**
|
|
4947
5312
|
* Registers custom tool handlers for this session.
|
|
4948
5313
|
*
|
|
@@ -5008,13 +5373,14 @@ var CopilotSession = class {
|
|
|
5008
5373
|
this.hooks = hooks;
|
|
5009
5374
|
}
|
|
5010
5375
|
/**
|
|
5011
|
-
* Handles a permission request
|
|
5376
|
+
* Handles a permission request in the v2 protocol format (synchronous RPC).
|
|
5377
|
+
* Used as a back-compat adapter when connected to a v2 server.
|
|
5012
5378
|
*
|
|
5013
5379
|
* @param request - The permission request data from the CLI
|
|
5014
5380
|
* @returns A promise that resolves with the permission decision
|
|
5015
5381
|
* @internal This method is for internal use by the SDK.
|
|
5016
5382
|
*/
|
|
5017
|
-
async
|
|
5383
|
+
async _handlePermissionRequestV2(request) {
|
|
5018
5384
|
if (!this.permissionHandler) {
|
|
5019
5385
|
return { kind: "denied-no-approval-rule-and-could-not-request-from-user" };
|
|
5020
5386
|
}
|
|
@@ -5085,7 +5451,7 @@ var CopilotSession = class {
|
|
|
5085
5451
|
* assistant responses, tool executions, and other session events.
|
|
5086
5452
|
*
|
|
5087
5453
|
* @returns A promise that resolves with an array of all session events
|
|
5088
|
-
* @throws Error if the session has been
|
|
5454
|
+
* @throws Error if the session has been disconnected or the connection fails
|
|
5089
5455
|
*
|
|
5090
5456
|
* @example
|
|
5091
5457
|
* ```typescript
|
|
@@ -5104,22 +5470,27 @@ var CopilotSession = class {
|
|
|
5104
5470
|
return response.events;
|
|
5105
5471
|
}
|
|
5106
5472
|
/**
|
|
5107
|
-
*
|
|
5473
|
+
* Disconnects this session and releases all in-memory resources (event handlers,
|
|
5474
|
+
* tool handlers, permission handlers).
|
|
5108
5475
|
*
|
|
5109
|
-
*
|
|
5110
|
-
*
|
|
5111
|
-
*
|
|
5476
|
+
* Session state on disk (conversation history, planning state, artifacts) is
|
|
5477
|
+
* preserved, so the conversation can be resumed later by calling
|
|
5478
|
+
* {@link CopilotClient.resumeSession} with the session ID. To permanently
|
|
5479
|
+
* remove all session data including files on disk, use
|
|
5480
|
+
* {@link CopilotClient.deleteSession} instead.
|
|
5112
5481
|
*
|
|
5113
|
-
*
|
|
5482
|
+
* After calling this method, the session object can no longer be used.
|
|
5483
|
+
*
|
|
5484
|
+
* @returns A promise that resolves when the session is disconnected
|
|
5114
5485
|
* @throws Error if the connection fails
|
|
5115
5486
|
*
|
|
5116
5487
|
* @example
|
|
5117
5488
|
* ```typescript
|
|
5118
|
-
* // Clean up when done
|
|
5119
|
-
* await session.
|
|
5489
|
+
* // Clean up when done — session can still be resumed later
|
|
5490
|
+
* await session.disconnect();
|
|
5120
5491
|
* ```
|
|
5121
5492
|
*/
|
|
5122
|
-
async
|
|
5493
|
+
async disconnect() {
|
|
5123
5494
|
await this.connection.sendRequest("session.destroy", {
|
|
5124
5495
|
sessionId: this.sessionId
|
|
5125
5496
|
});
|
|
@@ -5128,6 +5499,22 @@ var CopilotSession = class {
|
|
|
5128
5499
|
this.toolHandlers.clear();
|
|
5129
5500
|
this.permissionHandler = void 0;
|
|
5130
5501
|
}
|
|
5502
|
+
/**
|
|
5503
|
+
* @deprecated Use {@link disconnect} instead. This method will be removed in a future release.
|
|
5504
|
+
*
|
|
5505
|
+
* Disconnects this session and releases all in-memory resources.
|
|
5506
|
+
* Session data on disk is preserved for later resumption.
|
|
5507
|
+
*
|
|
5508
|
+
* @returns A promise that resolves when the session is disconnected
|
|
5509
|
+
* @throws Error if the connection fails
|
|
5510
|
+
*/
|
|
5511
|
+
async destroy() {
|
|
5512
|
+
return this.disconnect();
|
|
5513
|
+
}
|
|
5514
|
+
/** Enables `await using session = ...` syntax for automatic cleanup. */
|
|
5515
|
+
async [Symbol.asyncDispose]() {
|
|
5516
|
+
return this.disconnect();
|
|
5517
|
+
}
|
|
5131
5518
|
/**
|
|
5132
5519
|
* Aborts the currently processing message in this session.
|
|
5133
5520
|
*
|
|
@@ -5135,7 +5522,7 @@ var CopilotSession = class {
|
|
|
5135
5522
|
* and can continue to be used for new messages.
|
|
5136
5523
|
*
|
|
5137
5524
|
* @returns A promise that resolves when the abort request is acknowledged
|
|
5138
|
-
* @throws Error if the session has been
|
|
5525
|
+
* @throws Error if the session has been disconnected or the connection fails
|
|
5139
5526
|
*
|
|
5140
5527
|
* @example
|
|
5141
5528
|
* ```typescript
|
|
@@ -5153,12 +5540,27 @@ var CopilotSession = class {
|
|
|
5153
5540
|
sessionId: this.sessionId
|
|
5154
5541
|
});
|
|
5155
5542
|
}
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5543
|
+
/**
|
|
5544
|
+
* Change the model for this session.
|
|
5545
|
+
* The new model takes effect for the next message. Conversation history is preserved.
|
|
5546
|
+
*
|
|
5547
|
+
* @param model - Model ID to switch to
|
|
5548
|
+
*
|
|
5549
|
+
* @example
|
|
5550
|
+
* ```typescript
|
|
5551
|
+
* await session.setModel("gpt-4.1");
|
|
5552
|
+
* ```
|
|
5553
|
+
*/
|
|
5554
|
+
async setModel(model) {
|
|
5555
|
+
await this.rpc.model.switchTo({ modelId: model });
|
|
5556
|
+
}
|
|
5557
|
+
};
|
|
5558
|
+
|
|
5559
|
+
// node_modules/@github/copilot-sdk/dist/client.js
|
|
5560
|
+
var MIN_PROTOCOL_VERSION = 2;
|
|
5561
|
+
function isZodSchema(value) {
|
|
5562
|
+
return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
|
|
5563
|
+
}
|
|
5162
5564
|
function toJsonSchema(parameters) {
|
|
5163
5565
|
if (!parameters) return void 0;
|
|
5164
5566
|
if (isZodSchema(parameters)) {
|
|
@@ -5166,6 +5568,12 @@ function toJsonSchema(parameters) {
|
|
|
5166
5568
|
}
|
|
5167
5569
|
return parameters;
|
|
5168
5570
|
}
|
|
5571
|
+
function getNodeExecPath() {
|
|
5572
|
+
if (process.versions.bun) {
|
|
5573
|
+
return "node";
|
|
5574
|
+
}
|
|
5575
|
+
return process.execPath;
|
|
5576
|
+
}
|
|
5169
5577
|
function getBundledCliPath() {
|
|
5170
5578
|
const sdkUrl = import.meta.resolve("@github/copilot/sdk");
|
|
5171
5579
|
const sdkPath = fileURLToPath3(sdkUrl);
|
|
@@ -5179,6 +5587,8 @@ var CopilotClient = class {
|
|
|
5179
5587
|
actualHost = "localhost";
|
|
5180
5588
|
state = "disconnected";
|
|
5181
5589
|
sessions = /* @__PURE__ */ new Map();
|
|
5590
|
+
stderrBuffer = "";
|
|
5591
|
+
// Captures CLI stderr for error messages
|
|
5182
5592
|
options;
|
|
5183
5593
|
isExternalServer = false;
|
|
5184
5594
|
forceStopping = false;
|
|
@@ -5186,6 +5596,23 @@ var CopilotClient = class {
|
|
|
5186
5596
|
modelsCacheLock = Promise.resolve();
|
|
5187
5597
|
sessionLifecycleHandlers = /* @__PURE__ */ new Set();
|
|
5188
5598
|
typedLifecycleHandlers = /* @__PURE__ */ new Map();
|
|
5599
|
+
_rpc = null;
|
|
5600
|
+
processExitPromise = null;
|
|
5601
|
+
// Rejects when CLI process exits
|
|
5602
|
+
negotiatedProtocolVersion = null;
|
|
5603
|
+
/**
|
|
5604
|
+
* Typed server-scoped RPC methods.
|
|
5605
|
+
* @throws Error if the client is not connected
|
|
5606
|
+
*/
|
|
5607
|
+
get rpc() {
|
|
5608
|
+
if (!this.connection) {
|
|
5609
|
+
throw new Error("Client is not connected. Call start() first.");
|
|
5610
|
+
}
|
|
5611
|
+
if (!this._rpc) {
|
|
5612
|
+
this._rpc = createServerRpc(this.connection);
|
|
5613
|
+
}
|
|
5614
|
+
return this._rpc;
|
|
5615
|
+
}
|
|
5189
5616
|
/**
|
|
5190
5617
|
* Creates a new CopilotClient instance.
|
|
5191
5618
|
*
|
|
@@ -5211,6 +5638,11 @@ var CopilotClient = class {
|
|
|
5211
5638
|
if (options.cliUrl && (options.useStdio === true || options.cliPath)) {
|
|
5212
5639
|
throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
|
|
5213
5640
|
}
|
|
5641
|
+
if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) {
|
|
5642
|
+
throw new Error(
|
|
5643
|
+
"isChildProcess must be used in conjunction with useStdio and not with cliUrl"
|
|
5644
|
+
);
|
|
5645
|
+
}
|
|
5214
5646
|
if (options.cliUrl && (options.githubToken || options.useLoggedInUser !== void 0)) {
|
|
5215
5647
|
throw new Error(
|
|
5216
5648
|
"githubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
|
|
@@ -5222,6 +5654,9 @@ var CopilotClient = class {
|
|
|
5222
5654
|
this.actualPort = port;
|
|
5223
5655
|
this.isExternalServer = true;
|
|
5224
5656
|
}
|
|
5657
|
+
if (options.isChildProcess) {
|
|
5658
|
+
this.isExternalServer = true;
|
|
5659
|
+
}
|
|
5225
5660
|
this.options = {
|
|
5226
5661
|
cliPath: options.cliPath || getBundledCliPath(),
|
|
5227
5662
|
cliArgs: options.cliArgs ?? [],
|
|
@@ -5229,6 +5664,7 @@ var CopilotClient = class {
|
|
|
5229
5664
|
port: options.port || 0,
|
|
5230
5665
|
useStdio: options.cliUrl ? false : options.useStdio ?? true,
|
|
5231
5666
|
// Default to stdio unless cliUrl is provided
|
|
5667
|
+
isChildProcess: options.isChildProcess ?? false,
|
|
5232
5668
|
cliUrl: options.cliUrl,
|
|
5233
5669
|
logLevel: options.logLevel || "debug",
|
|
5234
5670
|
autoStart: options.autoStart ?? true,
|
|
@@ -5300,10 +5736,14 @@ var CopilotClient = class {
|
|
|
5300
5736
|
* Stops the CLI server and closes all active sessions.
|
|
5301
5737
|
*
|
|
5302
5738
|
* This method performs graceful cleanup:
|
|
5303
|
-
* 1.
|
|
5739
|
+
* 1. Closes all active sessions (releases in-memory resources)
|
|
5304
5740
|
* 2. Closes the JSON-RPC connection
|
|
5305
5741
|
* 3. Terminates the CLI server process (if spawned by this client)
|
|
5306
5742
|
*
|
|
5743
|
+
* Note: session data on disk is preserved, so sessions can be resumed later.
|
|
5744
|
+
* To permanently remove session data before stopping, call
|
|
5745
|
+
* {@link deleteSession} for each session first.
|
|
5746
|
+
*
|
|
5307
5747
|
* @returns A promise that resolves with an array of errors encountered during cleanup.
|
|
5308
5748
|
* An empty array indicates all cleanup succeeded.
|
|
5309
5749
|
*
|
|
@@ -5322,7 +5762,7 @@ var CopilotClient = class {
|
|
|
5322
5762
|
let lastError = null;
|
|
5323
5763
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
5324
5764
|
try {
|
|
5325
|
-
await session.
|
|
5765
|
+
await session.disconnect();
|
|
5326
5766
|
lastError = null;
|
|
5327
5767
|
break;
|
|
5328
5768
|
} catch (error) {
|
|
@@ -5336,7 +5776,7 @@ var CopilotClient = class {
|
|
|
5336
5776
|
if (lastError) {
|
|
5337
5777
|
errors.push(
|
|
5338
5778
|
new Error(
|
|
5339
|
-
`Failed to
|
|
5779
|
+
`Failed to disconnect session ${sessionId} after 3 attempts: ${lastError.message}`
|
|
5340
5780
|
)
|
|
5341
5781
|
);
|
|
5342
5782
|
}
|
|
@@ -5353,6 +5793,7 @@ var CopilotClient = class {
|
|
|
5353
5793
|
);
|
|
5354
5794
|
}
|
|
5355
5795
|
this.connection = null;
|
|
5796
|
+
this._rpc = null;
|
|
5356
5797
|
}
|
|
5357
5798
|
this.modelsCache = null;
|
|
5358
5799
|
if (this.socket) {
|
|
@@ -5381,6 +5822,8 @@ var CopilotClient = class {
|
|
|
5381
5822
|
}
|
|
5382
5823
|
this.state = "disconnected";
|
|
5383
5824
|
this.actualPort = null;
|
|
5825
|
+
this.stderrBuffer = "";
|
|
5826
|
+
this.processExitPromise = null;
|
|
5384
5827
|
return errors;
|
|
5385
5828
|
}
|
|
5386
5829
|
/**
|
|
@@ -5417,6 +5860,7 @@ var CopilotClient = class {
|
|
|
5417
5860
|
} catch {
|
|
5418
5861
|
}
|
|
5419
5862
|
this.connection = null;
|
|
5863
|
+
this._rpc = null;
|
|
5420
5864
|
}
|
|
5421
5865
|
this.modelsCache = null;
|
|
5422
5866
|
if (this.socket) {
|
|
@@ -5435,6 +5879,8 @@ var CopilotClient = class {
|
|
|
5435
5879
|
}
|
|
5436
5880
|
this.state = "disconnected";
|
|
5437
5881
|
this.actualPort = null;
|
|
5882
|
+
this.stderrBuffer = "";
|
|
5883
|
+
this.processExitPromise = null;
|
|
5438
5884
|
}
|
|
5439
5885
|
/**
|
|
5440
5886
|
* Creates a new conversation session with the Copilot CLI.
|
|
@@ -5450,10 +5896,11 @@ var CopilotClient = class {
|
|
|
5450
5896
|
* @example
|
|
5451
5897
|
* ```typescript
|
|
5452
5898
|
* // Basic session
|
|
5453
|
-
* const session = await client.createSession();
|
|
5899
|
+
* const session = await client.createSession({ onPermissionRequest: approveAll });
|
|
5454
5900
|
*
|
|
5455
5901
|
* // Session with model and tools
|
|
5456
5902
|
* const session = await client.createSession({
|
|
5903
|
+
* onPermissionRequest: approveAll,
|
|
5457
5904
|
* model: "gpt-4",
|
|
5458
5905
|
* tools: [{
|
|
5459
5906
|
* name: "get_weather",
|
|
@@ -5464,7 +5911,12 @@ var CopilotClient = class {
|
|
|
5464
5911
|
* });
|
|
5465
5912
|
* ```
|
|
5466
5913
|
*/
|
|
5467
|
-
async createSession(config2
|
|
5914
|
+
async createSession(config2) {
|
|
5915
|
+
if (!config2?.onPermissionRequest) {
|
|
5916
|
+
throw new Error(
|
|
5917
|
+
"An onPermissionRequest handler is required when creating a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
|
|
5918
|
+
);
|
|
5919
|
+
}
|
|
5468
5920
|
if (!this.connection) {
|
|
5469
5921
|
if (this.options.autoStart) {
|
|
5470
5922
|
await this.start();
|
|
@@ -5475,22 +5927,25 @@ var CopilotClient = class {
|
|
|
5475
5927
|
const response = await this.connection.sendRequest("session.create", {
|
|
5476
5928
|
model: config2.model,
|
|
5477
5929
|
sessionId: config2.sessionId,
|
|
5930
|
+
clientName: config2.clientName,
|
|
5478
5931
|
reasoningEffort: config2.reasoningEffort,
|
|
5479
5932
|
tools: config2.tools?.map((tool) => ({
|
|
5480
5933
|
name: tool.name,
|
|
5481
5934
|
description: tool.description,
|
|
5482
|
-
parameters: toJsonSchema(tool.parameters)
|
|
5935
|
+
parameters: toJsonSchema(tool.parameters),
|
|
5936
|
+
overridesBuiltInTool: tool.overridesBuiltInTool
|
|
5483
5937
|
})),
|
|
5484
5938
|
systemMessage: config2.systemMessage,
|
|
5485
5939
|
availableTools: config2.availableTools,
|
|
5486
5940
|
excludedTools: config2.excludedTools,
|
|
5487
5941
|
provider: config2.provider,
|
|
5488
|
-
requestPermission:
|
|
5942
|
+
requestPermission: true,
|
|
5489
5943
|
requestUserInput: !!config2.onUserInputRequest,
|
|
5490
5944
|
hooks: !!(config2.hooks && Object.values(config2.hooks).some(Boolean)),
|
|
5491
5945
|
workingDirectory: config2.workingDirectory,
|
|
5492
5946
|
streaming: config2.streaming,
|
|
5493
5947
|
mcpServers: config2.mcpServers,
|
|
5948
|
+
envValueMode: "direct",
|
|
5494
5949
|
customAgents: config2.customAgents,
|
|
5495
5950
|
configDir: config2.configDir,
|
|
5496
5951
|
skillDirectories: config2.skillDirectories,
|
|
@@ -5500,9 +5955,7 @@ var CopilotClient = class {
|
|
|
5500
5955
|
const { sessionId, workspacePath } = response;
|
|
5501
5956
|
const session = new CopilotSession(sessionId, this.connection, workspacePath);
|
|
5502
5957
|
session.registerTools(config2.tools);
|
|
5503
|
-
|
|
5504
|
-
session.registerPermissionHandler(config2.onPermissionRequest);
|
|
5505
|
-
}
|
|
5958
|
+
session.registerPermissionHandler(config2.onPermissionRequest);
|
|
5506
5959
|
if (config2.onUserInputRequest) {
|
|
5507
5960
|
session.registerUserInputHandler(config2.onUserInputRequest);
|
|
5508
5961
|
}
|
|
@@ -5527,15 +5980,21 @@ var CopilotClient = class {
|
|
|
5527
5980
|
* @example
|
|
5528
5981
|
* ```typescript
|
|
5529
5982
|
* // Resume a previous session
|
|
5530
|
-
* const session = await client.resumeSession("session-123");
|
|
5983
|
+
* const session = await client.resumeSession("session-123", { onPermissionRequest: approveAll });
|
|
5531
5984
|
*
|
|
5532
5985
|
* // Resume with new tools
|
|
5533
5986
|
* const session = await client.resumeSession("session-123", {
|
|
5987
|
+
* onPermissionRequest: approveAll,
|
|
5534
5988
|
* tools: [myNewTool]
|
|
5535
5989
|
* });
|
|
5536
5990
|
* ```
|
|
5537
5991
|
*/
|
|
5538
|
-
async resumeSession(sessionId, config2
|
|
5992
|
+
async resumeSession(sessionId, config2) {
|
|
5993
|
+
if (!config2?.onPermissionRequest) {
|
|
5994
|
+
throw new Error(
|
|
5995
|
+
"An onPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
|
|
5996
|
+
);
|
|
5997
|
+
}
|
|
5539
5998
|
if (!this.connection) {
|
|
5540
5999
|
if (this.options.autoStart) {
|
|
5541
6000
|
await this.start();
|
|
@@ -5545,6 +6004,7 @@ var CopilotClient = class {
|
|
|
5545
6004
|
}
|
|
5546
6005
|
const response = await this.connection.sendRequest("session.resume", {
|
|
5547
6006
|
sessionId,
|
|
6007
|
+
clientName: config2.clientName,
|
|
5548
6008
|
model: config2.model,
|
|
5549
6009
|
reasoningEffort: config2.reasoningEffort,
|
|
5550
6010
|
systemMessage: config2.systemMessage,
|
|
@@ -5553,16 +6013,18 @@ var CopilotClient = class {
|
|
|
5553
6013
|
tools: config2.tools?.map((tool) => ({
|
|
5554
6014
|
name: tool.name,
|
|
5555
6015
|
description: tool.description,
|
|
5556
|
-
parameters: toJsonSchema(tool.parameters)
|
|
6016
|
+
parameters: toJsonSchema(tool.parameters),
|
|
6017
|
+
overridesBuiltInTool: tool.overridesBuiltInTool
|
|
5557
6018
|
})),
|
|
5558
6019
|
provider: config2.provider,
|
|
5559
|
-
requestPermission:
|
|
6020
|
+
requestPermission: true,
|
|
5560
6021
|
requestUserInput: !!config2.onUserInputRequest,
|
|
5561
6022
|
hooks: !!(config2.hooks && Object.values(config2.hooks).some(Boolean)),
|
|
5562
6023
|
workingDirectory: config2.workingDirectory,
|
|
5563
6024
|
configDir: config2.configDir,
|
|
5564
6025
|
streaming: config2.streaming,
|
|
5565
6026
|
mcpServers: config2.mcpServers,
|
|
6027
|
+
envValueMode: "direct",
|
|
5566
6028
|
customAgents: config2.customAgents,
|
|
5567
6029
|
skillDirectories: config2.skillDirectories,
|
|
5568
6030
|
disabledSkills: config2.disabledSkills,
|
|
@@ -5572,9 +6034,7 @@ var CopilotClient = class {
|
|
|
5572
6034
|
const { sessionId: resumedSessionId, workspacePath } = response;
|
|
5573
6035
|
const session = new CopilotSession(resumedSessionId, this.connection, workspacePath);
|
|
5574
6036
|
session.registerTools(config2.tools);
|
|
5575
|
-
|
|
5576
|
-
session.registerPermissionHandler(config2.onPermissionRequest);
|
|
5577
|
-
}
|
|
6037
|
+
session.registerPermissionHandler(config2.onPermissionRequest);
|
|
5578
6038
|
if (config2.onUserInputRequest) {
|
|
5579
6039
|
session.registerUserInputHandler(config2.onUserInputRequest);
|
|
5580
6040
|
}
|
|
@@ -5592,7 +6052,7 @@ var CopilotClient = class {
|
|
|
5592
6052
|
* @example
|
|
5593
6053
|
* ```typescript
|
|
5594
6054
|
* if (client.getState() === "connected") {
|
|
5595
|
-
* const session = await client.createSession();
|
|
6055
|
+
* const session = await client.createSession({ onPermissionRequest: approveAll });
|
|
5596
6056
|
* }
|
|
5597
6057
|
* ```
|
|
5598
6058
|
*/
|
|
@@ -5670,22 +6130,29 @@ var CopilotClient = class {
|
|
|
5670
6130
|
}
|
|
5671
6131
|
}
|
|
5672
6132
|
/**
|
|
5673
|
-
* Verify that the server's protocol version
|
|
6133
|
+
* Verify that the server's protocol version is within the supported range
|
|
6134
|
+
* and store the negotiated version.
|
|
5674
6135
|
*/
|
|
5675
6136
|
async verifyProtocolVersion() {
|
|
5676
|
-
const
|
|
5677
|
-
|
|
6137
|
+
const maxVersion = getSdkProtocolVersion();
|
|
6138
|
+
let pingResult;
|
|
6139
|
+
if (this.processExitPromise) {
|
|
6140
|
+
pingResult = await Promise.race([this.ping(), this.processExitPromise]);
|
|
6141
|
+
} else {
|
|
6142
|
+
pingResult = await this.ping();
|
|
6143
|
+
}
|
|
5678
6144
|
const serverVersion = pingResult.protocolVersion;
|
|
5679
6145
|
if (serverVersion === void 0) {
|
|
5680
6146
|
throw new Error(
|
|
5681
|
-
`SDK protocol version mismatch: SDK
|
|
6147
|
+
`SDK protocol version mismatch: SDK supports versions ${MIN_PROTOCOL_VERSION}-${maxVersion}, but server does not report a protocol version. Please update your server to ensure compatibility.`
|
|
5682
6148
|
);
|
|
5683
6149
|
}
|
|
5684
|
-
if (serverVersion
|
|
6150
|
+
if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > maxVersion) {
|
|
5685
6151
|
throw new Error(
|
|
5686
|
-
`SDK protocol version mismatch: SDK
|
|
6152
|
+
`SDK protocol version mismatch: SDK supports versions ${MIN_PROTOCOL_VERSION}-${maxVersion}, but server reports version ${serverVersion}. Please update your SDK or server to ensure compatibility.`
|
|
5687
6153
|
);
|
|
5688
6154
|
}
|
|
6155
|
+
this.negotiatedProtocolVersion = serverVersion;
|
|
5689
6156
|
}
|
|
5690
6157
|
/**
|
|
5691
6158
|
* Gets the ID of the most recently updated session.
|
|
@@ -5700,7 +6167,7 @@ var CopilotClient = class {
|
|
|
5700
6167
|
* ```typescript
|
|
5701
6168
|
* const lastId = await client.getLastSessionId();
|
|
5702
6169
|
* if (lastId) {
|
|
5703
|
-
* const session = await client.resumeSession(lastId);
|
|
6170
|
+
* const session = await client.resumeSession(lastId, { onPermissionRequest: approveAll });
|
|
5704
6171
|
* }
|
|
5705
6172
|
* ```
|
|
5706
6173
|
*/
|
|
@@ -5712,10 +6179,12 @@ var CopilotClient = class {
|
|
|
5712
6179
|
return response.sessionId;
|
|
5713
6180
|
}
|
|
5714
6181
|
/**
|
|
5715
|
-
*
|
|
6182
|
+
* Permanently deletes a session and all its data from disk, including
|
|
6183
|
+
* conversation history, planning state, and artifacts.
|
|
5716
6184
|
*
|
|
5717
|
-
*
|
|
5718
|
-
*
|
|
6185
|
+
* Unlike {@link CopilotSession.disconnect}, which only releases in-memory
|
|
6186
|
+
* resources and preserves session data for later resumption, this method
|
|
6187
|
+
* is irreversible. The session cannot be resumed after deletion.
|
|
5719
6188
|
*
|
|
5720
6189
|
* @param sessionId - The ID of the session to delete
|
|
5721
6190
|
* @returns A promise that resolves when the session is deleted
|
|
@@ -5740,33 +6209,31 @@ var CopilotClient = class {
|
|
|
5740
6209
|
this.sessions.delete(sessionId);
|
|
5741
6210
|
}
|
|
5742
6211
|
/**
|
|
5743
|
-
*
|
|
6212
|
+
* List all available sessions.
|
|
5744
6213
|
*
|
|
5745
|
-
*
|
|
5746
|
-
*
|
|
5747
|
-
* @returns A promise that resolves with an array of session metadata
|
|
5748
|
-
* @throws Error if the client is not connected
|
|
6214
|
+
* @param filter - Optional filter to limit returned sessions by context fields
|
|
5749
6215
|
*
|
|
5750
6216
|
* @example
|
|
5751
|
-
*
|
|
6217
|
+
* // List all sessions
|
|
5752
6218
|
* const sessions = await client.listSessions();
|
|
5753
|
-
*
|
|
5754
|
-
*
|
|
5755
|
-
*
|
|
5756
|
-
*
|
|
6219
|
+
*
|
|
6220
|
+
* @example
|
|
6221
|
+
* // List sessions for a specific repository
|
|
6222
|
+
* const sessions = await client.listSessions({ repository: "owner/repo" });
|
|
5757
6223
|
*/
|
|
5758
|
-
async listSessions() {
|
|
6224
|
+
async listSessions(filter) {
|
|
5759
6225
|
if (!this.connection) {
|
|
5760
6226
|
throw new Error("Client not connected");
|
|
5761
6227
|
}
|
|
5762
|
-
const response = await this.connection.sendRequest("session.list", {});
|
|
6228
|
+
const response = await this.connection.sendRequest("session.list", { filter });
|
|
5763
6229
|
const { sessions } = response;
|
|
5764
6230
|
return sessions.map((s) => ({
|
|
5765
6231
|
sessionId: s.sessionId,
|
|
5766
6232
|
startTime: new Date(s.startTime),
|
|
5767
6233
|
modifiedTime: new Date(s.modifiedTime),
|
|
5768
6234
|
summary: s.summary,
|
|
5769
|
-
isRemote: s.isRemote
|
|
6235
|
+
isRemote: s.isRemote,
|
|
6236
|
+
context: s.context
|
|
5770
6237
|
}));
|
|
5771
6238
|
}
|
|
5772
6239
|
/**
|
|
@@ -5845,6 +6312,7 @@ var CopilotClient = class {
|
|
|
5845
6312
|
*/
|
|
5846
6313
|
async startCLIServer() {
|
|
5847
6314
|
return new Promise((resolve3, reject) => {
|
|
6315
|
+
this.stderrBuffer = "";
|
|
5848
6316
|
const args = [
|
|
5849
6317
|
...this.options.cliArgs,
|
|
5850
6318
|
"--headless",
|
|
@@ -5876,16 +6344,18 @@ var CopilotClient = class {
|
|
|
5876
6344
|
const stdioConfig = this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
|
|
5877
6345
|
const isJsFile = this.options.cliPath.endsWith(".js");
|
|
5878
6346
|
if (isJsFile) {
|
|
5879
|
-
this.cliProcess = spawn(
|
|
6347
|
+
this.cliProcess = spawn(getNodeExecPath(), [this.options.cliPath, ...args], {
|
|
5880
6348
|
stdio: stdioConfig,
|
|
5881
6349
|
cwd: this.options.cwd,
|
|
5882
|
-
env: envWithoutNodeDebug
|
|
6350
|
+
env: envWithoutNodeDebug,
|
|
6351
|
+
windowsHide: true
|
|
5883
6352
|
});
|
|
5884
6353
|
} else {
|
|
5885
6354
|
this.cliProcess = spawn(this.options.cliPath, args, {
|
|
5886
6355
|
stdio: stdioConfig,
|
|
5887
6356
|
cwd: this.options.cwd,
|
|
5888
|
-
env: envWithoutNodeDebug
|
|
6357
|
+
env: envWithoutNodeDebug,
|
|
6358
|
+
windowsHide: true
|
|
5889
6359
|
});
|
|
5890
6360
|
}
|
|
5891
6361
|
let stdout = "";
|
|
@@ -5905,6 +6375,7 @@ var CopilotClient = class {
|
|
|
5905
6375
|
});
|
|
5906
6376
|
}
|
|
5907
6377
|
this.cliProcess.stderr?.on("data", (data) => {
|
|
6378
|
+
this.stderrBuffer += data.toString();
|
|
5908
6379
|
const lines = data.toString().split("\n");
|
|
5909
6380
|
for (const line of lines) {
|
|
5910
6381
|
if (line.trim()) {
|
|
@@ -5916,13 +6387,54 @@ var CopilotClient = class {
|
|
|
5916
6387
|
this.cliProcess.on("error", (error) => {
|
|
5917
6388
|
if (!resolved) {
|
|
5918
6389
|
resolved = true;
|
|
5919
|
-
|
|
6390
|
+
const stderrOutput = this.stderrBuffer.trim();
|
|
6391
|
+
if (stderrOutput) {
|
|
6392
|
+
reject(
|
|
6393
|
+
new Error(
|
|
6394
|
+
`Failed to start CLI server: ${error.message}
|
|
6395
|
+
stderr: ${stderrOutput}`
|
|
6396
|
+
)
|
|
6397
|
+
);
|
|
6398
|
+
} else {
|
|
6399
|
+
reject(new Error(`Failed to start CLI server: ${error.message}`));
|
|
6400
|
+
}
|
|
5920
6401
|
}
|
|
5921
6402
|
});
|
|
6403
|
+
this.processExitPromise = new Promise((_, rejectProcessExit) => {
|
|
6404
|
+
this.cliProcess.on("exit", (code) => {
|
|
6405
|
+
setTimeout(() => {
|
|
6406
|
+
const stderrOutput = this.stderrBuffer.trim();
|
|
6407
|
+
if (stderrOutput) {
|
|
6408
|
+
rejectProcessExit(
|
|
6409
|
+
new Error(
|
|
6410
|
+
`CLI server exited with code ${code}
|
|
6411
|
+
stderr: ${stderrOutput}`
|
|
6412
|
+
)
|
|
6413
|
+
);
|
|
6414
|
+
} else {
|
|
6415
|
+
rejectProcessExit(
|
|
6416
|
+
new Error(`CLI server exited unexpectedly with code ${code}`)
|
|
6417
|
+
);
|
|
6418
|
+
}
|
|
6419
|
+
}, 50);
|
|
6420
|
+
});
|
|
6421
|
+
});
|
|
6422
|
+
this.processExitPromise.catch(() => {
|
|
6423
|
+
});
|
|
5922
6424
|
this.cliProcess.on("exit", (code) => {
|
|
5923
6425
|
if (!resolved) {
|
|
5924
6426
|
resolved = true;
|
|
5925
|
-
|
|
6427
|
+
const stderrOutput = this.stderrBuffer.trim();
|
|
6428
|
+
if (stderrOutput) {
|
|
6429
|
+
reject(
|
|
6430
|
+
new Error(
|
|
6431
|
+
`CLI server exited with code ${code}
|
|
6432
|
+
stderr: ${stderrOutput}`
|
|
6433
|
+
)
|
|
6434
|
+
);
|
|
6435
|
+
} else {
|
|
6436
|
+
reject(new Error(`CLI server exited with code ${code}`));
|
|
6437
|
+
}
|
|
5926
6438
|
} else if (this.options.autoRestart && this.state === "connected") {
|
|
5927
6439
|
void this.reconnect();
|
|
5928
6440
|
}
|
|
@@ -5939,16 +6451,18 @@ var CopilotClient = class {
|
|
|
5939
6451
|
* Connect to the CLI server (via socket or stdio)
|
|
5940
6452
|
*/
|
|
5941
6453
|
async connectToServer() {
|
|
5942
|
-
if (this.options.
|
|
5943
|
-
return this.
|
|
6454
|
+
if (this.options.isChildProcess) {
|
|
6455
|
+
return this.connectToParentProcessViaStdio();
|
|
6456
|
+
} else if (this.options.useStdio) {
|
|
6457
|
+
return this.connectToChildProcessViaStdio();
|
|
5944
6458
|
} else {
|
|
5945
6459
|
return this.connectViaTcp();
|
|
5946
6460
|
}
|
|
5947
6461
|
}
|
|
5948
6462
|
/**
|
|
5949
|
-
* Connect via stdio pipes
|
|
6463
|
+
* Connect to child via stdio pipes
|
|
5950
6464
|
*/
|
|
5951
|
-
async
|
|
6465
|
+
async connectToChildProcessViaStdio() {
|
|
5952
6466
|
if (!this.cliProcess) {
|
|
5953
6467
|
throw new Error("CLI process not started");
|
|
5954
6468
|
}
|
|
@@ -5957,9 +6471,23 @@ var CopilotClient = class {
|
|
|
5957
6471
|
throw err;
|
|
5958
6472
|
}
|
|
5959
6473
|
});
|
|
5960
|
-
this.connection = (0,
|
|
5961
|
-
new
|
|
5962
|
-
new
|
|
6474
|
+
this.connection = (0, import_node2.createMessageConnection)(
|
|
6475
|
+
new import_node2.StreamMessageReader(this.cliProcess.stdout),
|
|
6476
|
+
new import_node2.StreamMessageWriter(this.cliProcess.stdin)
|
|
6477
|
+
);
|
|
6478
|
+
this.attachConnectionHandlers();
|
|
6479
|
+
this.connection.listen();
|
|
6480
|
+
}
|
|
6481
|
+
/**
|
|
6482
|
+
* Connect to parent via stdio pipes
|
|
6483
|
+
*/
|
|
6484
|
+
async connectToParentProcessViaStdio() {
|
|
6485
|
+
if (this.cliProcess) {
|
|
6486
|
+
throw new Error("CLI child process was unexpectedly started in parent process mode");
|
|
6487
|
+
}
|
|
6488
|
+
this.connection = (0, import_node2.createMessageConnection)(
|
|
6489
|
+
new import_node2.StreamMessageReader(process.stdin),
|
|
6490
|
+
new import_node2.StreamMessageWriter(process.stdout)
|
|
5963
6491
|
);
|
|
5964
6492
|
this.attachConnectionHandlers();
|
|
5965
6493
|
this.connection.listen();
|
|
@@ -5974,9 +6502,9 @@ var CopilotClient = class {
|
|
|
5974
6502
|
return new Promise((resolve3, reject) => {
|
|
5975
6503
|
this.socket = new Socket();
|
|
5976
6504
|
this.socket.connect(this.actualPort, this.actualHost, () => {
|
|
5977
|
-
this.connection = (0,
|
|
5978
|
-
new
|
|
5979
|
-
new
|
|
6505
|
+
this.connection = (0, import_node2.createMessageConnection)(
|
|
6506
|
+
new import_node2.StreamMessageReader(this.socket),
|
|
6507
|
+
new import_node2.StreamMessageWriter(this.socket)
|
|
5980
6508
|
);
|
|
5981
6509
|
this.attachConnectionHandlers();
|
|
5982
6510
|
this.connection.listen();
|
|
@@ -5999,11 +6527,11 @@ var CopilotClient = class {
|
|
|
5999
6527
|
});
|
|
6000
6528
|
this.connection.onRequest(
|
|
6001
6529
|
"tool.call",
|
|
6002
|
-
async (params) => await this.
|
|
6530
|
+
async (params) => await this.handleToolCallRequestV2(params)
|
|
6003
6531
|
);
|
|
6004
6532
|
this.connection.onRequest(
|
|
6005
6533
|
"permission.request",
|
|
6006
|
-
async (params) => await this.
|
|
6534
|
+
async (params) => await this.handlePermissionRequestV2(params)
|
|
6007
6535
|
);
|
|
6008
6536
|
this.connection.onRequest(
|
|
6009
6537
|
"userInput.request",
|
|
@@ -6051,7 +6579,41 @@ var CopilotClient = class {
|
|
|
6051
6579
|
}
|
|
6052
6580
|
}
|
|
6053
6581
|
}
|
|
6054
|
-
async
|
|
6582
|
+
async handleUserInputRequest(params) {
|
|
6583
|
+
if (!params || typeof params.sessionId !== "string" || typeof params.question !== "string") {
|
|
6584
|
+
throw new Error("Invalid user input request payload");
|
|
6585
|
+
}
|
|
6586
|
+
const session = this.sessions.get(params.sessionId);
|
|
6587
|
+
if (!session) {
|
|
6588
|
+
throw new Error(`Session not found: ${params.sessionId}`);
|
|
6589
|
+
}
|
|
6590
|
+
const result = await session._handleUserInputRequest({
|
|
6591
|
+
question: params.question,
|
|
6592
|
+
choices: params.choices,
|
|
6593
|
+
allowFreeform: params.allowFreeform
|
|
6594
|
+
});
|
|
6595
|
+
return result;
|
|
6596
|
+
}
|
|
6597
|
+
async handleHooksInvoke(params) {
|
|
6598
|
+
if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
|
|
6599
|
+
throw new Error("Invalid hooks invoke payload");
|
|
6600
|
+
}
|
|
6601
|
+
const session = this.sessions.get(params.sessionId);
|
|
6602
|
+
if (!session) {
|
|
6603
|
+
throw new Error(`Session not found: ${params.sessionId}`);
|
|
6604
|
+
}
|
|
6605
|
+
const output = await session._handleHooksInvoke(params.hookType, params.input);
|
|
6606
|
+
return { output };
|
|
6607
|
+
}
|
|
6608
|
+
// ========================================================================
|
|
6609
|
+
// Protocol v2 backward-compatibility adapters
|
|
6610
|
+
// ========================================================================
|
|
6611
|
+
/**
|
|
6612
|
+
* Handles a v2-style tool.call RPC request from the server.
|
|
6613
|
+
* Looks up the session and tool handler, executes it, and returns the result
|
|
6614
|
+
* in the v2 response format.
|
|
6615
|
+
*/
|
|
6616
|
+
async handleToolCallRequestV2(params) {
|
|
6055
6617
|
if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
|
|
6056
6618
|
throw new Error("Invalid tool call payload");
|
|
6057
6619
|
}
|
|
@@ -6061,25 +6623,28 @@ var CopilotClient = class {
|
|
|
6061
6623
|
}
|
|
6062
6624
|
const handler = session.getToolHandler(params.toolName);
|
|
6063
6625
|
if (!handler) {
|
|
6064
|
-
return {
|
|
6626
|
+
return {
|
|
6627
|
+
result: {
|
|
6628
|
+
textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`,
|
|
6629
|
+
resultType: "failure",
|
|
6630
|
+
error: `tool '${params.toolName}' not supported`,
|
|
6631
|
+
toolTelemetry: {}
|
|
6632
|
+
}
|
|
6633
|
+
};
|
|
6065
6634
|
}
|
|
6066
|
-
return await this.executeToolCall(handler, params);
|
|
6067
|
-
}
|
|
6068
|
-
async executeToolCall(handler, request) {
|
|
6069
6635
|
try {
|
|
6070
6636
|
const invocation = {
|
|
6071
|
-
sessionId:
|
|
6072
|
-
toolCallId:
|
|
6073
|
-
toolName:
|
|
6074
|
-
arguments:
|
|
6637
|
+
sessionId: params.sessionId,
|
|
6638
|
+
toolCallId: params.toolCallId,
|
|
6639
|
+
toolName: params.toolName,
|
|
6640
|
+
arguments: params.arguments
|
|
6075
6641
|
};
|
|
6076
|
-
const result = await handler(
|
|
6077
|
-
return { result: this.
|
|
6642
|
+
const result = await handler(params.arguments, invocation);
|
|
6643
|
+
return { result: this.normalizeToolResultV2(result) };
|
|
6078
6644
|
} catch (error) {
|
|
6079
6645
|
const message = error instanceof Error ? error.message : String(error);
|
|
6080
6646
|
return {
|
|
6081
6647
|
result: {
|
|
6082
|
-
// Don't expose detailed error information to the LLM for security reasons
|
|
6083
6648
|
textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
|
|
6084
6649
|
resultType: "failure",
|
|
6085
6650
|
error: message,
|
|
@@ -6088,7 +6653,10 @@ var CopilotClient = class {
|
|
|
6088
6653
|
};
|
|
6089
6654
|
}
|
|
6090
6655
|
}
|
|
6091
|
-
|
|
6656
|
+
/**
|
|
6657
|
+
* Handles a v2-style permission.request RPC request from the server.
|
|
6658
|
+
*/
|
|
6659
|
+
async handlePermissionRequestV2(params) {
|
|
6092
6660
|
if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) {
|
|
6093
6661
|
throw new Error("Invalid permission request payload");
|
|
6094
6662
|
}
|
|
@@ -6097,7 +6665,7 @@ var CopilotClient = class {
|
|
|
6097
6665
|
throw new Error(`Session not found: ${params.sessionId}`);
|
|
6098
6666
|
}
|
|
6099
6667
|
try {
|
|
6100
|
-
const result = await session.
|
|
6668
|
+
const result = await session._handlePermissionRequestV2(params.permissionRequest);
|
|
6101
6669
|
return { result };
|
|
6102
6670
|
} catch (_error) {
|
|
6103
6671
|
return {
|
|
@@ -6107,33 +6675,7 @@ var CopilotClient = class {
|
|
|
6107
6675
|
};
|
|
6108
6676
|
}
|
|
6109
6677
|
}
|
|
6110
|
-
|
|
6111
|
-
if (!params || typeof params.sessionId !== "string" || typeof params.question !== "string") {
|
|
6112
|
-
throw new Error("Invalid user input request payload");
|
|
6113
|
-
}
|
|
6114
|
-
const session = this.sessions.get(params.sessionId);
|
|
6115
|
-
if (!session) {
|
|
6116
|
-
throw new Error(`Session not found: ${params.sessionId}`);
|
|
6117
|
-
}
|
|
6118
|
-
const result = await session._handleUserInputRequest({
|
|
6119
|
-
question: params.question,
|
|
6120
|
-
choices: params.choices,
|
|
6121
|
-
allowFreeform: params.allowFreeform
|
|
6122
|
-
});
|
|
6123
|
-
return result;
|
|
6124
|
-
}
|
|
6125
|
-
async handleHooksInvoke(params) {
|
|
6126
|
-
if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
|
|
6127
|
-
throw new Error("Invalid hooks invoke payload");
|
|
6128
|
-
}
|
|
6129
|
-
const session = this.sessions.get(params.sessionId);
|
|
6130
|
-
if (!session) {
|
|
6131
|
-
throw new Error(`Session not found: ${params.sessionId}`);
|
|
6132
|
-
}
|
|
6133
|
-
const output = await session._handleHooksInvoke(params.hookType, params.input);
|
|
6134
|
-
return { output };
|
|
6135
|
-
}
|
|
6136
|
-
normalizeToolResult(result) {
|
|
6678
|
+
normalizeToolResultV2(result) {
|
|
6137
6679
|
if (result === void 0 || result === null) {
|
|
6138
6680
|
return {
|
|
6139
6681
|
textResultForLlm: "Tool returned no result",
|
|
@@ -6155,14 +6697,6 @@ var CopilotClient = class {
|
|
|
6155
6697
|
isToolResultObject(value) {
|
|
6156
6698
|
return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
|
|
6157
6699
|
}
|
|
6158
|
-
buildUnsupportedToolResult(toolName) {
|
|
6159
|
-
return {
|
|
6160
|
-
textResultForLlm: `Tool '${toolName}' is not supported by this client instance.`,
|
|
6161
|
-
resultType: "failure",
|
|
6162
|
-
error: `tool '${toolName}' not supported`,
|
|
6163
|
-
toolTelemetry: {}
|
|
6164
|
-
};
|
|
6165
|
-
}
|
|
6166
6700
|
/**
|
|
6167
6701
|
* Attempt to reconnect to the server
|
|
6168
6702
|
*/
|
|
@@ -6176,6 +6710,38 @@ var CopilotClient = class {
|
|
|
6176
6710
|
}
|
|
6177
6711
|
};
|
|
6178
6712
|
|
|
6713
|
+
// node_modules/@github/copilot-sdk/dist/types.js
|
|
6714
|
+
var approveAll = () => ({ kind: "approved" });
|
|
6715
|
+
|
|
6716
|
+
// src/L1-infra/ai/copilot.ts
|
|
6717
|
+
import { existsSync as existsSync5 } from "fs";
|
|
6718
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
6719
|
+
import { createRequire as createRequire2 } from "module";
|
|
6720
|
+
function resolveCopilotCliPath() {
|
|
6721
|
+
const platform = process.platform;
|
|
6722
|
+
const arch = process.arch;
|
|
6723
|
+
const binaryName = platform === "win32" ? "copilot.exe" : "copilot";
|
|
6724
|
+
const platformPkg = `@github/copilot-${platform}-${arch}`;
|
|
6725
|
+
try {
|
|
6726
|
+
const require_ = createRequire2(import.meta.url);
|
|
6727
|
+
const searchPaths = require_.resolve.paths(platformPkg) ?? [];
|
|
6728
|
+
for (const base of searchPaths) {
|
|
6729
|
+
const candidate = join6(base, platformPkg, binaryName);
|
|
6730
|
+
if (existsSync5(candidate)) return candidate;
|
|
6731
|
+
}
|
|
6732
|
+
} catch {
|
|
6733
|
+
}
|
|
6734
|
+
let dir = dirname4(import.meta.dirname ?? __dirname);
|
|
6735
|
+
for (let i = 0; i < 10; i++) {
|
|
6736
|
+
const candidate = join6(dir, "node_modules", platformPkg, binaryName);
|
|
6737
|
+
if (existsSync5(candidate)) return candidate;
|
|
6738
|
+
const parent = dirname4(dir);
|
|
6739
|
+
if (parent === dir) break;
|
|
6740
|
+
dir = parent;
|
|
6741
|
+
}
|
|
6742
|
+
return void 0;
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6179
6745
|
// src/L2-clients/llm/ai.ts
|
|
6180
6746
|
function createOpenAI(...args) {
|
|
6181
6747
|
return new default4(...args);
|
|
@@ -6191,6 +6757,7 @@ function createCopilotClient(...args) {
|
|
|
6191
6757
|
init_configLogger();
|
|
6192
6758
|
var DEFAULT_MODEL = "claude-opus-4.5";
|
|
6193
6759
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
6760
|
+
var SESSION_CREATE_TIMEOUT_MS = 3e4;
|
|
6194
6761
|
var CopilotProvider = class {
|
|
6195
6762
|
name = "copilot";
|
|
6196
6763
|
client = null;
|
|
@@ -6202,21 +6769,57 @@ var CopilotProvider = class {
|
|
|
6202
6769
|
}
|
|
6203
6770
|
async createSession(config2) {
|
|
6204
6771
|
if (!this.client) {
|
|
6205
|
-
|
|
6772
|
+
const cliPath = resolveCopilotCliPath();
|
|
6773
|
+
if (cliPath) {
|
|
6774
|
+
logger_default.info(`[CopilotProvider] Using native CLI binary: ${cliPath}`);
|
|
6775
|
+
}
|
|
6776
|
+
this.client = createCopilotClient({
|
|
6777
|
+
autoStart: true,
|
|
6778
|
+
autoRestart: true,
|
|
6779
|
+
logLevel: "error",
|
|
6780
|
+
env: buildChildEnv(),
|
|
6781
|
+
...cliPath ? { cliPath } : {}
|
|
6782
|
+
});
|
|
6206
6783
|
}
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6784
|
+
logger_default.info("[CopilotProvider] Creating session\u2026");
|
|
6785
|
+
let copilotSession;
|
|
6786
|
+
try {
|
|
6787
|
+
copilotSession = await new Promise((resolve3, reject) => {
|
|
6788
|
+
const timeoutId = setTimeout(
|
|
6789
|
+
() => reject(new Error(
|
|
6790
|
+
`[CopilotProvider] createSession timed out after ${SESSION_CREATE_TIMEOUT_MS / 1e3}s \u2014 the Copilot SDK language server may not be reachable. Check GitHub authentication and network connectivity.`
|
|
6791
|
+
)),
|
|
6792
|
+
SESSION_CREATE_TIMEOUT_MS
|
|
6793
|
+
);
|
|
6794
|
+
this.client.createSession({
|
|
6795
|
+
model: config2.model,
|
|
6796
|
+
mcpServers: config2.mcpServers,
|
|
6797
|
+
systemMessage: { mode: "replace", content: config2.systemPrompt },
|
|
6798
|
+
tools: config2.tools.map((t) => ({
|
|
6799
|
+
name: t.name,
|
|
6800
|
+
description: t.description,
|
|
6801
|
+
parameters: t.parameters,
|
|
6802
|
+
handler: t.handler
|
|
6803
|
+
})),
|
|
6804
|
+
streaming: config2.streaming ?? true,
|
|
6805
|
+
onPermissionRequest: approveAll,
|
|
6806
|
+
onUserInputRequest: config2.onUserInputRequest ? (request) => config2.onUserInputRequest(request) : void 0
|
|
6807
|
+
}).then(
|
|
6808
|
+
(session) => {
|
|
6809
|
+
clearTimeout(timeoutId);
|
|
6810
|
+
resolve3(session);
|
|
6811
|
+
},
|
|
6812
|
+
(err) => {
|
|
6813
|
+
clearTimeout(timeoutId);
|
|
6814
|
+
reject(err);
|
|
6815
|
+
}
|
|
6816
|
+
);
|
|
6817
|
+
});
|
|
6818
|
+
} catch (err) {
|
|
6819
|
+
this.client = null;
|
|
6820
|
+
throw err;
|
|
6821
|
+
}
|
|
6822
|
+
logger_default.info("[CopilotProvider] Session created successfully");
|
|
6220
6823
|
return new CopilotSessionWrapper(
|
|
6221
6824
|
copilotSession,
|
|
6222
6825
|
config2.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
@@ -6361,6 +6964,15 @@ var CopilotSessionWrapper = class {
|
|
|
6361
6964
|
}
|
|
6362
6965
|
}
|
|
6363
6966
|
};
|
|
6967
|
+
function buildChildEnv() {
|
|
6968
|
+
const env = { ...process.env };
|
|
6969
|
+
const flag = "--disable-warning=ExperimentalWarning";
|
|
6970
|
+
const current = env.NODE_OPTIONS ?? "";
|
|
6971
|
+
if (!current.includes(flag)) {
|
|
6972
|
+
env.NODE_OPTIONS = current ? `${current} ${flag}` : flag;
|
|
6973
|
+
}
|
|
6974
|
+
return env;
|
|
6975
|
+
}
|
|
6364
6976
|
|
|
6365
6977
|
// src/L0-pure/pricing/pricing.ts
|
|
6366
6978
|
var COPILOT_PRU_OVERAGE_RATE = 0.04;
|
|
@@ -6611,7 +7223,8 @@ var OpenAIProvider = class {
|
|
|
6611
7223
|
return "gpt-4o";
|
|
6612
7224
|
}
|
|
6613
7225
|
async createSession(config2) {
|
|
6614
|
-
const
|
|
7226
|
+
const appConfig = getConfig();
|
|
7227
|
+
const client = createOpenAI({ apiKey: appConfig.OPENAI_API_KEY });
|
|
6615
7228
|
const model = config2.model ?? this.getDefaultModel();
|
|
6616
7229
|
logger_default.info(`OpenAI session created (model=${model}, tools=${config2.tools.length})`);
|
|
6617
7230
|
return new OpenAISession(client, config2, model);
|
|
@@ -6940,16 +7553,10 @@ var LateApiClient = class {
|
|
|
6940
7553
|
return data.accounts ?? [];
|
|
6941
7554
|
}
|
|
6942
7555
|
async getScheduledPosts(platform) {
|
|
6943
|
-
|
|
6944
|
-
if (platform) params.set("platform", platform);
|
|
6945
|
-
const data = await this.request(`/posts?${params}`);
|
|
6946
|
-
return data.posts ?? [];
|
|
7556
|
+
return this.listPosts({ status: "scheduled", platform });
|
|
6947
7557
|
}
|
|
6948
7558
|
async getDraftPosts(platform) {
|
|
6949
|
-
|
|
6950
|
-
if (platform) params.set("platform", platform);
|
|
6951
|
-
const data = await this.request(`/posts?${params}`);
|
|
6952
|
-
return data.posts ?? [];
|
|
7559
|
+
return this.listPosts({ status: "draft", platform });
|
|
6953
7560
|
}
|
|
6954
7561
|
async createPost(params) {
|
|
6955
7562
|
const data = await this.request("/posts", {
|
|
@@ -7055,6 +7662,7 @@ function createLateApiClient(...args) {
|
|
|
7055
7662
|
// src/L2-clients/scheduleStore/scheduleStore.ts
|
|
7056
7663
|
init_fileSystem();
|
|
7057
7664
|
init_paths();
|
|
7665
|
+
init_globalConfig();
|
|
7058
7666
|
async function readScheduleFile(filePath) {
|
|
7059
7667
|
return readTextFile(filePath);
|
|
7060
7668
|
}
|
|
@@ -7066,7 +7674,10 @@ async function writeScheduleFile(filePath, content) {
|
|
|
7066
7674
|
});
|
|
7067
7675
|
}
|
|
7068
7676
|
function resolveSchedulePath(configPath) {
|
|
7069
|
-
|
|
7677
|
+
if (configPath) return configPath;
|
|
7678
|
+
const globalPath = getGlobalConfigValue("defaults", "scheduleConfig");
|
|
7679
|
+
if (globalPath) return globalPath;
|
|
7680
|
+
return join(process.cwd(), "schedule.json");
|
|
7070
7681
|
}
|
|
7071
7682
|
|
|
7072
7683
|
// src/L3-services/scheduler/scheduleConfig.ts
|
|
@@ -7331,7 +7942,7 @@ function getPlatformSchedule(platform, clipType) {
|
|
|
7331
7942
|
avoidDays: sub.avoidDays
|
|
7332
7943
|
};
|
|
7333
7944
|
}
|
|
7334
|
-
if (
|
|
7945
|
+
if (schedule.slots.length === 0 && schedule.byClipType) {
|
|
7335
7946
|
const allSlots = [];
|
|
7336
7947
|
const allAvoidDays = /* @__PURE__ */ new Set();
|
|
7337
7948
|
for (const sub of Object.values(schedule.byClipType)) {
|
|
@@ -7352,403 +7963,23 @@ function getDisplacementConfig() {
|
|
|
7352
7963
|
return cachedConfig?.displacement ?? { ...defaultDisplacement };
|
|
7353
7964
|
}
|
|
7354
7965
|
|
|
7355
|
-
// src/L3-services/postStore/postStore.ts
|
|
7356
|
-
init_types();
|
|
7357
|
-
init_environment();
|
|
7358
|
-
init_configLogger();
|
|
7359
|
-
init_fileSystem();
|
|
7360
|
-
init_paths();
|
|
7361
|
-
function getQueueDir() {
|
|
7362
|
-
const { OUTPUT_DIR } = getConfig();
|
|
7363
|
-
return join(OUTPUT_DIR, "publish-queue");
|
|
7364
|
-
}
|
|
7365
|
-
function getPublishedDir() {
|
|
7366
|
-
const { OUTPUT_DIR } = getConfig();
|
|
7367
|
-
return join(OUTPUT_DIR, "published");
|
|
7368
|
-
}
|
|
7369
|
-
async function readQueueItem(folderPath, id) {
|
|
7370
|
-
const metadataPath = join(folderPath, "metadata.json");
|
|
7371
|
-
const postPath = join(folderPath, "post.md");
|
|
7372
|
-
try {
|
|
7373
|
-
const metadataRaw = await readTextFile(metadataPath);
|
|
7374
|
-
const metadata = JSON.parse(metadataRaw);
|
|
7375
|
-
let postContent = "";
|
|
7376
|
-
try {
|
|
7377
|
-
postContent = await readTextFile(postPath);
|
|
7378
|
-
} catch {
|
|
7379
|
-
logger_default.debug(`No post.md found for ${String(id).replace(/[\r\n]/g, "")}`);
|
|
7380
|
-
}
|
|
7381
|
-
const videoPath = join(folderPath, "media.mp4");
|
|
7382
|
-
const imagePath = join(folderPath, "media.png");
|
|
7383
|
-
let mediaPath = null;
|
|
7384
|
-
let hasMedia = false;
|
|
7385
|
-
if (await fileExists(videoPath)) {
|
|
7386
|
-
mediaPath = videoPath;
|
|
7387
|
-
hasMedia = true;
|
|
7388
|
-
} else if (await fileExists(imagePath)) {
|
|
7389
|
-
mediaPath = imagePath;
|
|
7390
|
-
hasMedia = true;
|
|
7391
|
-
}
|
|
7392
|
-
return {
|
|
7393
|
-
id,
|
|
7394
|
-
metadata,
|
|
7395
|
-
postContent,
|
|
7396
|
-
hasMedia,
|
|
7397
|
-
mediaPath,
|
|
7398
|
-
folderPath
|
|
7399
|
-
};
|
|
7400
|
-
} catch (err) {
|
|
7401
|
-
logger_default.debug(`Failed to read queue item ${String(id).replace(/[\r\n]/g, "")}: ${String(err).replace(/[\r\n]/g, "")}`);
|
|
7402
|
-
return null;
|
|
7403
|
-
}
|
|
7404
|
-
}
|
|
7405
|
-
async function getPendingItems() {
|
|
7406
|
-
const queueDir = getQueueDir();
|
|
7407
|
-
await ensureDirectory(queueDir);
|
|
7408
|
-
let entries;
|
|
7409
|
-
try {
|
|
7410
|
-
const dirents = await listDirectoryWithTypes(queueDir);
|
|
7411
|
-
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
7412
|
-
} catch {
|
|
7413
|
-
return [];
|
|
7414
|
-
}
|
|
7415
|
-
const items = [];
|
|
7416
|
-
for (const name of entries) {
|
|
7417
|
-
const item = await readQueueItem(join(queueDir, name), name);
|
|
7418
|
-
if (item) items.push(item);
|
|
7419
|
-
}
|
|
7420
|
-
items.sort((a, b) => {
|
|
7421
|
-
if (a.hasMedia !== b.hasMedia) return a.hasMedia ? -1 : 1;
|
|
7422
|
-
return a.metadata.createdAt.localeCompare(b.metadata.createdAt);
|
|
7423
|
-
});
|
|
7424
|
-
return items;
|
|
7425
|
-
}
|
|
7426
|
-
async function createItem(id, metadata, postContent, mediaSourcePath) {
|
|
7427
|
-
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
7428
|
-
throw new Error(`Invalid ID format: ${id}`);
|
|
7429
|
-
}
|
|
7430
|
-
const folderPath = join(getQueueDir(), basename(id));
|
|
7431
|
-
await ensureDirectory(folderPath);
|
|
7432
|
-
await writeJsonFile(join(folderPath, "metadata.json"), metadata);
|
|
7433
|
-
await writeTextFile(join(folderPath, "post.md"), postContent);
|
|
7434
|
-
let hasMedia = false;
|
|
7435
|
-
const ext = mediaSourcePath ? extname(mediaSourcePath) : ".mp4";
|
|
7436
|
-
const mediaFilename = `media${ext}`;
|
|
7437
|
-
const mediaPath = join(folderPath, mediaFilename);
|
|
7438
|
-
if (mediaSourcePath) {
|
|
7439
|
-
await copyFile(mediaSourcePath, mediaPath);
|
|
7440
|
-
hasMedia = true;
|
|
7441
|
-
}
|
|
7442
|
-
logger_default.debug(`Created queue item: ${String(id).replace(/[\r\n]/g, "")}`);
|
|
7443
|
-
return {
|
|
7444
|
-
id,
|
|
7445
|
-
metadata,
|
|
7446
|
-
postContent,
|
|
7447
|
-
hasMedia,
|
|
7448
|
-
mediaPath: hasMedia ? mediaPath : null,
|
|
7449
|
-
folderPath
|
|
7450
|
-
};
|
|
7451
|
-
}
|
|
7452
|
-
async function getPublishedItems() {
|
|
7453
|
-
const publishedDir = getPublishedDir();
|
|
7454
|
-
await ensureDirectory(publishedDir);
|
|
7455
|
-
let entries;
|
|
7456
|
-
try {
|
|
7457
|
-
const dirents = await listDirectoryWithTypes(publishedDir);
|
|
7458
|
-
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
7459
|
-
} catch {
|
|
7460
|
-
return [];
|
|
7461
|
-
}
|
|
7462
|
-
const items = [];
|
|
7463
|
-
for (const name of entries) {
|
|
7464
|
-
const item = await readQueueItem(join(publishedDir, name), name);
|
|
7465
|
-
if (item) items.push(item);
|
|
7466
|
-
}
|
|
7467
|
-
items.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
|
|
7468
|
-
return items;
|
|
7469
|
-
}
|
|
7470
|
-
async function getScheduledItemsByIdeaIds(ideaIds) {
|
|
7471
|
-
if (ideaIds.length === 0) return [];
|
|
7472
|
-
const ideaIdSet = new Set(ideaIds);
|
|
7473
|
-
const [pendingItems, publishedItems] = await Promise.all([
|
|
7474
|
-
getPendingItems(),
|
|
7475
|
-
getPublishedItems()
|
|
7476
|
-
]);
|
|
7477
|
-
return [...pendingItems, ...publishedItems].filter(
|
|
7478
|
-
(item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
|
|
7479
|
-
);
|
|
7480
|
-
}
|
|
7481
|
-
async function getPublishedItemByLatePostId(latePostId) {
|
|
7482
|
-
const publishedItems = await getPublishedItems();
|
|
7483
|
-
return publishedItems.find((item) => item.metadata.latePostId === latePostId) ?? null;
|
|
7484
|
-
}
|
|
7485
|
-
async function itemExists(id) {
|
|
7486
|
-
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
7487
|
-
throw new Error(`Invalid ID format: ${id}`);
|
|
7488
|
-
}
|
|
7489
|
-
if (await fileExists(join(getQueueDir(), basename(id)))) {
|
|
7490
|
-
return "pending";
|
|
7491
|
-
}
|
|
7492
|
-
if (await fileExists(join(getPublishedDir(), basename(id)))) {
|
|
7493
|
-
return "published";
|
|
7494
|
-
}
|
|
7495
|
-
return null;
|
|
7496
|
-
}
|
|
7497
|
-
|
|
7498
7966
|
// src/L3-services/scheduler/realign.ts
|
|
7967
|
+
init_postStore();
|
|
7499
7968
|
init_configLogger();
|
|
7500
|
-
function getTimezoneOffset(timezone, date) {
|
|
7501
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7502
|
-
timeZone: timezone,
|
|
7503
|
-
timeZoneName: "longOffset"
|
|
7504
|
-
});
|
|
7505
|
-
const parts = formatter.formatToParts(date);
|
|
7506
|
-
const tzPart = parts.find((p) => p.type === "timeZoneName");
|
|
7507
|
-
const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
|
|
7508
|
-
if (match) return match[1];
|
|
7509
|
-
if (tzPart?.value === "GMT") return "+00:00";
|
|
7510
|
-
return "+00:00";
|
|
7511
|
-
}
|
|
7512
|
-
function buildSlotDatetime(date, time, timezone) {
|
|
7513
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7514
|
-
timeZone: timezone,
|
|
7515
|
-
year: "numeric",
|
|
7516
|
-
month: "2-digit",
|
|
7517
|
-
day: "2-digit"
|
|
7518
|
-
});
|
|
7519
|
-
const parts = formatter.formatToParts(date);
|
|
7520
|
-
const year = parts.find((p) => p.type === "year")?.value ?? String(date.getFullYear());
|
|
7521
|
-
const month = (parts.find((p) => p.type === "month")?.value ?? String(date.getMonth() + 1)).padStart(2, "0");
|
|
7522
|
-
const day = (parts.find((p) => p.type === "day")?.value ?? String(date.getDate())).padStart(2, "0");
|
|
7523
|
-
const offset = getTimezoneOffset(timezone, date);
|
|
7524
|
-
return `${year}-${month}-${day}T${time}:00${offset}`;
|
|
7525
|
-
}
|
|
7526
|
-
function getDayOfWeekInTimezone(date, timezone) {
|
|
7527
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7528
|
-
timeZone: timezone,
|
|
7529
|
-
weekday: "short"
|
|
7530
|
-
});
|
|
7531
|
-
const short = formatter.format(date).toLowerCase().slice(0, 3);
|
|
7532
|
-
const map = {
|
|
7533
|
-
sun: "sun",
|
|
7534
|
-
mon: "mon",
|
|
7535
|
-
tue: "tue",
|
|
7536
|
-
wed: "wed",
|
|
7537
|
-
thu: "thu",
|
|
7538
|
-
fri: "fri",
|
|
7539
|
-
sat: "sat"
|
|
7540
|
-
};
|
|
7541
|
-
return map[short] ?? "mon";
|
|
7542
|
-
}
|
|
7543
|
-
var PLATFORM_ALIASES2 = { twitter: "x" };
|
|
7544
|
-
function normalizeSchedulePlatform(platform) {
|
|
7545
|
-
return PLATFORM_ALIASES2[platform] ?? platform;
|
|
7546
|
-
}
|
|
7547
|
-
function normalizeContent(content) {
|
|
7548
|
-
return content.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 200);
|
|
7549
|
-
}
|
|
7550
|
-
async function buildClipTypeMaps() {
|
|
7551
|
-
const published = await getPublishedItems();
|
|
7552
|
-
const byLatePostId = /* @__PURE__ */ new Map();
|
|
7553
|
-
const byContent = /* @__PURE__ */ new Map();
|
|
7554
|
-
for (const item of published) {
|
|
7555
|
-
if (item.metadata.latePostId) {
|
|
7556
|
-
byLatePostId.set(item.metadata.latePostId, item.metadata.clipType);
|
|
7557
|
-
}
|
|
7558
|
-
if (item.postContent) {
|
|
7559
|
-
const contentKey = `${item.metadata.platform}::${normalizeContent(item.postContent)}`;
|
|
7560
|
-
byContent.set(contentKey, item.metadata.clipType);
|
|
7561
|
-
}
|
|
7562
|
-
}
|
|
7563
|
-
logger_default.debug(`Built clipType maps: ${byLatePostId.size} by latePostId, ${byContent.size} by content`);
|
|
7564
|
-
return { byLatePostId, byContent };
|
|
7565
|
-
}
|
|
7566
|
-
async function fetchAllPosts(client, statuses, platform) {
|
|
7567
|
-
const allPosts = [];
|
|
7568
|
-
for (const status of statuses) {
|
|
7569
|
-
const posts = await client.listPosts({ status, platform });
|
|
7570
|
-
allPosts.push(...posts);
|
|
7571
|
-
logger_default.info(`Fetched ${posts.length} ${status} post(s)${platform ? ` for ${platform}` : ""}`);
|
|
7572
|
-
}
|
|
7573
|
-
return allPosts;
|
|
7574
|
-
}
|
|
7575
|
-
function generateSlots(platform, clipType, count, bookedMs, timezone) {
|
|
7576
|
-
const schedule = getPlatformSchedule(platform, clipType);
|
|
7577
|
-
if (!schedule || schedule.slots.length === 0) {
|
|
7578
|
-
logger_default.warn(`No schedule slots for ${platform}/${clipType}`);
|
|
7579
|
-
return [];
|
|
7580
|
-
}
|
|
7581
|
-
const available = [];
|
|
7582
|
-
const now = /* @__PURE__ */ new Date();
|
|
7583
|
-
const nowMs = now.getTime();
|
|
7584
|
-
for (let dayOffset = 0; dayOffset < 730 && available.length < count; dayOffset++) {
|
|
7585
|
-
const day = new Date(now);
|
|
7586
|
-
day.setDate(day.getDate() + dayOffset);
|
|
7587
|
-
const dayOfWeek = getDayOfWeekInTimezone(day, timezone);
|
|
7588
|
-
if (schedule.avoidDays.includes(dayOfWeek)) continue;
|
|
7589
|
-
for (const slot of schedule.slots) {
|
|
7590
|
-
if (available.length >= count) break;
|
|
7591
|
-
if (!slot.days.includes(dayOfWeek)) continue;
|
|
7592
|
-
const iso = buildSlotDatetime(day, slot.time, timezone);
|
|
7593
|
-
const ms = new Date(iso).getTime();
|
|
7594
|
-
if (ms <= nowMs) continue;
|
|
7595
|
-
if (!bookedMs.has(ms)) {
|
|
7596
|
-
available.push(iso);
|
|
7597
|
-
bookedMs.add(ms);
|
|
7598
|
-
}
|
|
7599
|
-
}
|
|
7600
|
-
}
|
|
7601
|
-
return available;
|
|
7602
|
-
}
|
|
7603
|
-
async function buildRealignPlan(options = {}) {
|
|
7604
|
-
const config2 = await loadScheduleConfig();
|
|
7605
|
-
const { timezone } = config2;
|
|
7606
|
-
const client = new LateApiClient();
|
|
7607
|
-
const statuses = ["scheduled", "draft", "cancelled", "failed"];
|
|
7608
|
-
const allPosts = await fetchAllPosts(client, statuses, options.platform);
|
|
7609
|
-
if (allPosts.length === 0) {
|
|
7610
|
-
return { posts: [], toCancel: [], skipped: 0, unmatched: 0, totalFetched: 0 };
|
|
7611
|
-
}
|
|
7612
|
-
const { byLatePostId, byContent } = options.clipTypeMaps ?? await buildClipTypeMaps();
|
|
7613
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
7614
|
-
let unmatched = 0;
|
|
7615
|
-
let contentMatched = 0;
|
|
7616
|
-
for (const post of allPosts) {
|
|
7617
|
-
const platform = post.platforms[0]?.platform;
|
|
7618
|
-
if (!platform) continue;
|
|
7619
|
-
let clipType = byLatePostId.get(post._id) ?? null;
|
|
7620
|
-
if (!clipType && post.content) {
|
|
7621
|
-
const contentKey = `${platform}::${normalizeContent(post.content)}`;
|
|
7622
|
-
clipType = byContent.get(contentKey) ?? null;
|
|
7623
|
-
if (clipType) contentMatched++;
|
|
7624
|
-
}
|
|
7625
|
-
if (!clipType) {
|
|
7626
|
-
clipType = "short";
|
|
7627
|
-
unmatched++;
|
|
7628
|
-
}
|
|
7629
|
-
const key = `${platform}::${clipType}`;
|
|
7630
|
-
if (!grouped.has(key)) grouped.set(key, []);
|
|
7631
|
-
grouped.get(key).push({ post, platform, clipType });
|
|
7632
|
-
}
|
|
7633
|
-
const bookedMs = /* @__PURE__ */ new Set();
|
|
7634
|
-
if (contentMatched > 0) {
|
|
7635
|
-
logger_default.info(`${contentMatched} post(s) matched by content fallback (no latePostId)`);
|
|
7636
|
-
}
|
|
7637
|
-
const result = [];
|
|
7638
|
-
const toCancel = [];
|
|
7639
|
-
let skipped = 0;
|
|
7640
|
-
for (const [key, posts] of grouped) {
|
|
7641
|
-
const [platform, clipType] = key.split("::");
|
|
7642
|
-
const schedulePlatform = normalizeSchedulePlatform(platform);
|
|
7643
|
-
const schedule = getPlatformSchedule(schedulePlatform, clipType);
|
|
7644
|
-
const hasSlots = schedule && schedule.slots.length > 0;
|
|
7645
|
-
if (!hasSlots) {
|
|
7646
|
-
for (const { post, clipType: ct } of posts) {
|
|
7647
|
-
if (post.status === "cancelled") continue;
|
|
7648
|
-
toCancel.push({
|
|
7649
|
-
post,
|
|
7650
|
-
platform,
|
|
7651
|
-
clipType: ct,
|
|
7652
|
-
reason: `No schedule slots for ${schedulePlatform}/${clipType}`
|
|
7653
|
-
});
|
|
7654
|
-
}
|
|
7655
|
-
continue;
|
|
7656
|
-
}
|
|
7657
|
-
posts.sort((a, b) => {
|
|
7658
|
-
const aTime = a.post.scheduledFor ? new Date(a.post.scheduledFor).getTime() : Infinity;
|
|
7659
|
-
const bTime = b.post.scheduledFor ? new Date(b.post.scheduledFor).getTime() : Infinity;
|
|
7660
|
-
return aTime - bTime;
|
|
7661
|
-
});
|
|
7662
|
-
const slots = generateSlots(schedulePlatform, clipType, posts.length, bookedMs, timezone);
|
|
7663
|
-
for (let i = 0; i < posts.length; i++) {
|
|
7664
|
-
const { post } = posts[i];
|
|
7665
|
-
const newSlot = slots[i];
|
|
7666
|
-
if (!newSlot) {
|
|
7667
|
-
if (post.status !== "cancelled") {
|
|
7668
|
-
toCancel.push({
|
|
7669
|
-
post,
|
|
7670
|
-
platform,
|
|
7671
|
-
clipType: posts[i].clipType,
|
|
7672
|
-
reason: `No more available slots for ${schedulePlatform}/${clipType}`
|
|
7673
|
-
});
|
|
7674
|
-
}
|
|
7675
|
-
continue;
|
|
7676
|
-
}
|
|
7677
|
-
const currentMs = post.scheduledFor ? new Date(post.scheduledFor).getTime() : 0;
|
|
7678
|
-
const newMs = new Date(newSlot).getTime();
|
|
7679
|
-
if (currentMs === newMs && post.status === "scheduled") {
|
|
7680
|
-
skipped++;
|
|
7681
|
-
continue;
|
|
7682
|
-
}
|
|
7683
|
-
result.push({
|
|
7684
|
-
post,
|
|
7685
|
-
platform,
|
|
7686
|
-
clipType: posts[i].clipType,
|
|
7687
|
-
oldScheduledFor: post.scheduledFor ?? null,
|
|
7688
|
-
newScheduledFor: newSlot
|
|
7689
|
-
});
|
|
7690
|
-
}
|
|
7691
|
-
}
|
|
7692
|
-
result.sort((a, b) => new Date(a.newScheduledFor).getTime() - new Date(b.newScheduledFor).getTime());
|
|
7693
|
-
return { posts: result, toCancel, skipped, unmatched, totalFetched: allPosts.length };
|
|
7694
|
-
}
|
|
7695
|
-
async function executeRealignPlan(plan, onProgress) {
|
|
7696
|
-
const client = new LateApiClient();
|
|
7697
|
-
let updated = 0;
|
|
7698
|
-
let cancelled = 0;
|
|
7699
|
-
let failed = 0;
|
|
7700
|
-
const errors = [];
|
|
7701
|
-
const totalOps = plan.toCancel.length + plan.posts.length;
|
|
7702
|
-
let completed = 0;
|
|
7703
|
-
for (const entry of plan.toCancel) {
|
|
7704
|
-
completed++;
|
|
7705
|
-
try {
|
|
7706
|
-
await client.updatePost(entry.post._id, { status: "cancelled" });
|
|
7707
|
-
cancelled++;
|
|
7708
|
-
const preview = entry.post.content.slice(0, 40).replace(/\n/g, " ");
|
|
7709
|
-
logger_default.info(`[${completed}/${totalOps}] \u{1F6AB} Cancelled ${entry.platform}/${entry.clipType}: "${preview}..."`);
|
|
7710
|
-
onProgress?.(completed, totalOps, "cancelling");
|
|
7711
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
7712
|
-
} catch (err) {
|
|
7713
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
7714
|
-
errors.push({ postId: entry.post._id, error: msg });
|
|
7715
|
-
failed++;
|
|
7716
|
-
logger_default.error(`[${completed}/${totalOps}] \u274C Failed to cancel ${entry.post._id}: ${msg}`);
|
|
7717
|
-
}
|
|
7718
|
-
}
|
|
7719
|
-
for (const entry of plan.posts) {
|
|
7720
|
-
completed++;
|
|
7721
|
-
try {
|
|
7722
|
-
await client.schedulePost(entry.post._id, entry.newScheduledFor);
|
|
7723
|
-
updated++;
|
|
7724
|
-
const preview = entry.post.content.slice(0, 40).replace(/\n/g, " ");
|
|
7725
|
-
logger_default.info(`[${completed}/${totalOps}] \u2705 ${entry.platform}/${entry.clipType}: "${preview}..." \u2192 ${entry.newScheduledFor}`);
|
|
7726
|
-
onProgress?.(completed, totalOps, "updating");
|
|
7727
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
7728
|
-
} catch (err) {
|
|
7729
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
7730
|
-
errors.push({ postId: entry.post._id, error: msg });
|
|
7731
|
-
failed++;
|
|
7732
|
-
logger_default.error(`[${completed}/${totalOps}] \u274C Failed to update ${entry.post._id}: ${msg}`);
|
|
7733
|
-
}
|
|
7734
|
-
}
|
|
7735
|
-
return { updated, cancelled, failed, errors };
|
|
7736
|
-
}
|
|
7737
7969
|
|
|
7738
7970
|
// src/L3-services/scheduler/scheduler.ts
|
|
7739
7971
|
init_configLogger();
|
|
7740
|
-
|
|
7741
|
-
return new Date(isoString).getTime();
|
|
7742
|
-
}
|
|
7743
|
-
var CHUNK_DAYS = 14;
|
|
7972
|
+
init_postStore();
|
|
7744
7973
|
var MAX_LOOKAHEAD_DAYS = 730;
|
|
7745
|
-
var DEFAULT_IDEA_WINDOW_DAYS = 14;
|
|
7746
7974
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
7747
7975
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
7976
|
+
function normalizeDateTime(isoString) {
|
|
7977
|
+
return new Date(isoString).getTime();
|
|
7978
|
+
}
|
|
7748
7979
|
function sanitizeLogValue(value) {
|
|
7749
7980
|
return value.replace(/[\r\n]/g, "");
|
|
7750
7981
|
}
|
|
7751
|
-
function
|
|
7982
|
+
function getTimezoneOffset(timezone, date) {
|
|
7752
7983
|
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7753
7984
|
timeZone: timezone,
|
|
7754
7985
|
timeZoneName: "longOffset"
|
|
@@ -7763,7 +7994,7 @@ function getTimezoneOffset2(timezone, date) {
|
|
|
7763
7994
|
);
|
|
7764
7995
|
return "+00:00";
|
|
7765
7996
|
}
|
|
7766
|
-
function
|
|
7997
|
+
function buildSlotDatetime(date, time, timezone) {
|
|
7767
7998
|
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7768
7999
|
timeZone: timezone,
|
|
7769
8000
|
year: "numeric",
|
|
@@ -7777,10 +8008,10 @@ function buildSlotDatetime2(date, time, timezone) {
|
|
|
7777
8008
|
const year = yearPart ?? String(date.getFullYear());
|
|
7778
8009
|
const month = (monthPart ?? String(date.getMonth() + 1)).padStart(2, "0");
|
|
7779
8010
|
const day = (dayPart ?? String(date.getDate())).padStart(2, "0");
|
|
7780
|
-
const offset =
|
|
8011
|
+
const offset = getTimezoneOffset(timezone, date);
|
|
7781
8012
|
return `${year}-${month}-${day}T${time}:00${offset}`;
|
|
7782
8013
|
}
|
|
7783
|
-
function
|
|
8014
|
+
function getDayOfWeekInTimezone(date, timezone) {
|
|
7784
8015
|
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7785
8016
|
timeZone: timezone,
|
|
7786
8017
|
weekday: "short"
|
|
@@ -7807,22 +8038,30 @@ async function fetchScheduledPostsSafe(platform) {
|
|
|
7807
8038
|
return [];
|
|
7808
8039
|
}
|
|
7809
8040
|
}
|
|
7810
|
-
async function
|
|
8041
|
+
async function buildBookedMap(platform) {
|
|
7811
8042
|
const [latePosts, publishedItems] = await Promise.all([
|
|
7812
8043
|
fetchScheduledPostsSafe(platform),
|
|
7813
8044
|
getPublishedItems()
|
|
7814
8045
|
]);
|
|
7815
|
-
const
|
|
8046
|
+
const ideaLinkedPostIds = /* @__PURE__ */ new Set();
|
|
8047
|
+
for (const item of publishedItems) {
|
|
8048
|
+
if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
|
|
8049
|
+
ideaLinkedPostIds.add(item.metadata.latePostId);
|
|
8050
|
+
}
|
|
8051
|
+
}
|
|
8052
|
+
const map = /* @__PURE__ */ new Map();
|
|
7816
8053
|
for (const post of latePosts) {
|
|
7817
8054
|
if (!post.scheduledFor) continue;
|
|
7818
8055
|
for (const scheduledPlatform of post.platforms) {
|
|
7819
8056
|
if (!platform || scheduledPlatform.platform === platform) {
|
|
7820
|
-
|
|
8057
|
+
const ms = normalizeDateTime(post.scheduledFor);
|
|
8058
|
+
map.set(ms, {
|
|
7821
8059
|
scheduledFor: post.scheduledFor,
|
|
7822
8060
|
source: "late",
|
|
7823
8061
|
postId: post._id,
|
|
7824
8062
|
platform: scheduledPlatform.platform,
|
|
7825
|
-
status: post.status
|
|
8063
|
+
status: post.status,
|
|
8064
|
+
ideaLinked: ideaLinkedPostIds.has(post._id)
|
|
7826
8065
|
});
|
|
7827
8066
|
}
|
|
7828
8067
|
}
|
|
@@ -7830,207 +8069,168 @@ async function buildBookedSlots(platform) {
|
|
|
7830
8069
|
for (const item of publishedItems) {
|
|
7831
8070
|
if (platform && item.metadata.platform !== platform) continue;
|
|
7832
8071
|
if (!item.metadata.scheduledFor) continue;
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
8072
|
+
const ms = normalizeDateTime(item.metadata.scheduledFor);
|
|
8073
|
+
if (!map.has(ms)) {
|
|
8074
|
+
map.set(ms, {
|
|
8075
|
+
scheduledFor: item.metadata.scheduledFor,
|
|
8076
|
+
source: "local",
|
|
8077
|
+
itemId: item.id,
|
|
8078
|
+
platform: item.metadata.platform,
|
|
8079
|
+
ideaLinked: Boolean(item.metadata.ideaIds?.length)
|
|
8080
|
+
});
|
|
8081
|
+
}
|
|
7839
8082
|
}
|
|
7840
|
-
return
|
|
8083
|
+
return map;
|
|
7841
8084
|
}
|
|
7842
|
-
function
|
|
8085
|
+
async function getIdeaLinkedLatePostIds() {
|
|
8086
|
+
const publishedItems = await getPublishedItems();
|
|
8087
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8088
|
+
for (const item of publishedItems) {
|
|
8089
|
+
if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
|
|
8090
|
+
ids.add(item.metadata.latePostId);
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8093
|
+
return ids;
|
|
8094
|
+
}
|
|
8095
|
+
function* generateTimeslots(platformConfig, timezone, fromMs, maxMs) {
|
|
8096
|
+
const baseDate = new Date(fromMs);
|
|
8097
|
+
const upperMs = maxMs ?? fromMs + MAX_LOOKAHEAD_DAYS * DAY_MS;
|
|
8098
|
+
for (let dayOffset = 0; dayOffset <= MAX_LOOKAHEAD_DAYS; dayOffset++) {
|
|
8099
|
+
const day = new Date(baseDate);
|
|
8100
|
+
day.setDate(day.getDate() + dayOffset);
|
|
8101
|
+
const dayOfWeek = getDayOfWeekInTimezone(day, timezone);
|
|
8102
|
+
if (platformConfig.avoidDays.includes(dayOfWeek)) continue;
|
|
8103
|
+
const dayCandidates = [];
|
|
8104
|
+
for (const slot of platformConfig.slots) {
|
|
8105
|
+
if (!slot.days.includes(dayOfWeek)) continue;
|
|
8106
|
+
const datetime = buildSlotDatetime(day, slot.time, timezone);
|
|
8107
|
+
const ms = normalizeDateTime(datetime);
|
|
8108
|
+
if (ms <= fromMs) continue;
|
|
8109
|
+
if (ms > upperMs) continue;
|
|
8110
|
+
dayCandidates.push({ datetime, ms });
|
|
8111
|
+
}
|
|
8112
|
+
dayCandidates.sort((a, b) => a.ms - b.ms);
|
|
8113
|
+
for (const candidate of dayCandidates) yield candidate;
|
|
8114
|
+
if (dayCandidates.length === 0) {
|
|
8115
|
+
const dayStartMs = normalizeDateTime(buildSlotDatetime(day, "00:00", timezone));
|
|
8116
|
+
if (dayStartMs > upperMs) break;
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
8119
|
+
}
|
|
8120
|
+
function passesIdeaSpacing(candidateMs, candidatePlatform, ideaRefs, samePlatformMs, crossPlatformMs) {
|
|
8121
|
+
for (const ref of ideaRefs) {
|
|
8122
|
+
const diff = Math.abs(candidateMs - ref.scheduledForMs);
|
|
8123
|
+
if (ref.platform === candidatePlatform && diff < samePlatformMs) return false;
|
|
8124
|
+
if (diff < crossPlatformMs) return false;
|
|
8125
|
+
}
|
|
8126
|
+
return true;
|
|
8127
|
+
}
|
|
8128
|
+
async function getIdeaReferences(ideaIds, bookedMap) {
|
|
8129
|
+
const sameIdeaPosts = await getScheduledItemsByIdeaIds(ideaIds);
|
|
7843
8130
|
const lateSlotsByPostId = /* @__PURE__ */ new Map();
|
|
7844
8131
|
const localSlotsByItemId = /* @__PURE__ */ new Map();
|
|
7845
|
-
for (const slot of
|
|
8132
|
+
for (const slot of bookedMap.values()) {
|
|
7846
8133
|
if (slot.postId) {
|
|
7847
|
-
const
|
|
7848
|
-
|
|
7849
|
-
lateSlotsByPostId.set(slot.postId,
|
|
8134
|
+
const arr = lateSlotsByPostId.get(slot.postId) ?? [];
|
|
8135
|
+
arr.push(slot);
|
|
8136
|
+
lateSlotsByPostId.set(slot.postId, arr);
|
|
7850
8137
|
}
|
|
7851
8138
|
if (slot.itemId) {
|
|
7852
|
-
const
|
|
7853
|
-
|
|
7854
|
-
localSlotsByItemId.set(slot.itemId,
|
|
8139
|
+
const arr = localSlotsByItemId.get(slot.itemId) ?? [];
|
|
8140
|
+
arr.push(slot);
|
|
8141
|
+
localSlotsByItemId.set(slot.itemId, arr);
|
|
7855
8142
|
}
|
|
7856
8143
|
}
|
|
7857
|
-
const
|
|
8144
|
+
const refs = [];
|
|
7858
8145
|
const seen = /* @__PURE__ */ new Set();
|
|
7859
|
-
const
|
|
8146
|
+
const addRef = (platform, scheduledFor) => {
|
|
7860
8147
|
if (!scheduledFor) return;
|
|
7861
|
-
const key = `${
|
|
8148
|
+
const key = `${platform}@${scheduledFor}`;
|
|
7862
8149
|
if (seen.has(key)) return;
|
|
7863
8150
|
seen.add(key);
|
|
7864
|
-
|
|
8151
|
+
refs.push({ platform, scheduledForMs: normalizeDateTime(scheduledFor) });
|
|
7865
8152
|
};
|
|
7866
8153
|
for (const item of sameIdeaPosts) {
|
|
7867
|
-
|
|
8154
|
+
addRef(item.metadata.platform, item.metadata.scheduledFor);
|
|
7868
8155
|
if (item.metadata.latePostId) {
|
|
7869
8156
|
for (const slot of lateSlotsByPostId.get(item.metadata.latePostId) ?? []) {
|
|
7870
|
-
|
|
8157
|
+
addRef(slot.platform, slot.scheduledFor);
|
|
7871
8158
|
}
|
|
7872
8159
|
}
|
|
7873
8160
|
for (const slot of localSlotsByItemId.get(item.id) ?? []) {
|
|
7874
|
-
|
|
8161
|
+
addRef(slot.platform, slot.scheduledFor);
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
return refs;
|
|
8165
|
+
}
|
|
8166
|
+
async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
|
|
8167
|
+
const indent = " ".repeat(ctx.depth);
|
|
8168
|
+
let checked = 0;
|
|
8169
|
+
let skippedBooked = 0;
|
|
8170
|
+
let skippedSpacing = 0;
|
|
8171
|
+
logger_default.debug(`${indent}[schedulePost] Looking for slot for ${label} (idea=${isIdeaPost}) from ${new Date(fromMs).toISOString()}`);
|
|
8172
|
+
for (const { datetime, ms } of generateTimeslots(platformConfig, ctx.timezone, fromMs)) {
|
|
8173
|
+
checked++;
|
|
8174
|
+
const booked = ctx.bookedMap.get(ms);
|
|
8175
|
+
if (!booked) {
|
|
8176
|
+
if (isIdeaPost && ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
|
|
8177
|
+
skippedSpacing++;
|
|
8178
|
+
if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
|
|
8179
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping`);
|
|
8180
|
+
}
|
|
8181
|
+
continue;
|
|
8182
|
+
}
|
|
8183
|
+
logger_default.debug(`${indent}[schedulePost] \u2705 Found empty slot: ${datetime} (checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing)`);
|
|
8184
|
+
return datetime;
|
|
7875
8185
|
}
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
for (const reference of ideaReferences) {
|
|
7884
|
-
const referenceMs = normalizeDateTime(reference.scheduledFor);
|
|
7885
|
-
const diff = Math.abs(candidateMs - referenceMs);
|
|
7886
|
-
if (reference.platform === candidatePlatform && diff < samePlatformWindowMs) {
|
|
7887
|
-
return false;
|
|
8186
|
+
if (isIdeaPost && ctx.displacementEnabled && !booked.ideaLinked && booked.source === "late" && booked.postId) {
|
|
8187
|
+
if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
|
|
8188
|
+
skippedSpacing++;
|
|
8189
|
+
if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
|
|
8190
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping (even though displaceable)`);
|
|
8191
|
+
}
|
|
8192
|
+
continue;
|
|
7888
8193
|
}
|
|
7889
|
-
|
|
7890
|
-
|
|
8194
|
+
logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by non-idea post ${booked.postId} \u2014 displacing`);
|
|
8195
|
+
const newHome = await schedulePost(
|
|
8196
|
+
platformConfig,
|
|
8197
|
+
ms,
|
|
8198
|
+
false,
|
|
8199
|
+
`displaced:${booked.postId}`,
|
|
8200
|
+
{ ...ctx, depth: ctx.depth + 1 }
|
|
8201
|
+
);
|
|
8202
|
+
if (newHome) {
|
|
8203
|
+
if (!ctx.dryRun) {
|
|
8204
|
+
try {
|
|
8205
|
+
await ctx.lateClient.schedulePost(booked.postId, newHome);
|
|
8206
|
+
} catch (err) {
|
|
8207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8208
|
+
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Failed to displace ${booked.postId} via Late API: ${msg} \u2014 skipping slot`);
|
|
8209
|
+
continue;
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
logger_default.info(`${indent}[schedulePost] \u{1F4E6} Displaced ${booked.postId}: ${datetime} \u2192 ${newHome}`);
|
|
8213
|
+
ctx.bookedMap.delete(ms);
|
|
8214
|
+
const newMs = normalizeDateTime(newHome);
|
|
8215
|
+
ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
|
|
8216
|
+
logger_default.debug(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked} candidates)`);
|
|
8217
|
+
return datetime;
|
|
7891
8218
|
}
|
|
8219
|
+
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace ${booked.postId} \u2014 no empty slot found after ${datetime}`);
|
|
7892
8220
|
}
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
const publishBy = options?.publishBy;
|
|
7899
|
-
if (!publishBy) {
|
|
7900
|
-
return {
|
|
7901
|
-
emptyWindowEndMs: defaultWindowEndMs,
|
|
7902
|
-
displacementWindowEndMs: defaultWindowEndMs
|
|
7903
|
-
};
|
|
7904
|
-
}
|
|
7905
|
-
const publishByMs = normalizeDateTime(publishBy);
|
|
7906
|
-
if (Number.isNaN(publishByMs)) {
|
|
7907
|
-
logger_default.warn(`Invalid publishBy "${sanitizeLogValue(publishBy)}" provided; scheduling normally without urgency bias`);
|
|
7908
|
-
return {};
|
|
7909
|
-
}
|
|
7910
|
-
const daysUntilPublishBy = (publishByMs - nowMs) / DAY_MS;
|
|
7911
|
-
if (daysUntilPublishBy <= 0) {
|
|
7912
|
-
logger_default.warn(`publishBy "${sanitizeLogValue(publishBy)}" has already passed; scheduling normally without urgency bias`);
|
|
7913
|
-
return {};
|
|
7914
|
-
}
|
|
7915
|
-
if (daysUntilPublishBy < 3) {
|
|
7916
|
-
logger_default.debug(`Urgent publishBy "${sanitizeLogValue(publishBy)}"; prioritizing earliest displaceable slot`);
|
|
7917
|
-
}
|
|
7918
|
-
return {
|
|
7919
|
-
emptyWindowEndMs: publishByMs,
|
|
7920
|
-
displacementWindowEndMs: daysUntilPublishBy < 7 ? Math.min(publishByMs, nowMs + 3 * DAY_MS) : publishByMs
|
|
7921
|
-
};
|
|
7922
|
-
}
|
|
7923
|
-
function findEmptySlot({
|
|
7924
|
-
platformConfig,
|
|
7925
|
-
timezone,
|
|
7926
|
-
bookedDatetimes,
|
|
7927
|
-
platform,
|
|
7928
|
-
searchFromMs,
|
|
7929
|
-
includeSearchDay = false,
|
|
7930
|
-
maxCandidateMs,
|
|
7931
|
-
passesCandidate
|
|
7932
|
-
}) {
|
|
7933
|
-
if (maxCandidateMs !== void 0 && maxCandidateMs < searchFromMs) {
|
|
7934
|
-
return null;
|
|
7935
|
-
}
|
|
7936
|
-
const baseDate = new Date(searchFromMs);
|
|
7937
|
-
const initialOffset = includeSearchDay ? 0 : 1;
|
|
7938
|
-
let maxDayOffset = MAX_LOOKAHEAD_DAYS;
|
|
7939
|
-
if (maxCandidateMs !== void 0) {
|
|
7940
|
-
maxDayOffset = Math.min(
|
|
7941
|
-
MAX_LOOKAHEAD_DAYS,
|
|
7942
|
-
Math.max(initialOffset, Math.ceil((maxCandidateMs - searchFromMs) / DAY_MS))
|
|
7943
|
-
);
|
|
7944
|
-
}
|
|
7945
|
-
let startOffset = initialOffset;
|
|
7946
|
-
while (startOffset <= maxDayOffset) {
|
|
7947
|
-
const endOffset = Math.min(startOffset + CHUNK_DAYS - 1, maxDayOffset);
|
|
7948
|
-
const candidates = [];
|
|
7949
|
-
for (let dayOffset = startOffset; dayOffset <= endOffset; dayOffset++) {
|
|
7950
|
-
const candidateDate = new Date(baseDate);
|
|
7951
|
-
candidateDate.setDate(candidateDate.getDate() + dayOffset);
|
|
7952
|
-
const dayOfWeek = getDayOfWeekInTimezone2(candidateDate, timezone);
|
|
7953
|
-
if (platformConfig.avoidDays.includes(dayOfWeek)) continue;
|
|
7954
|
-
for (const slot of platformConfig.slots) {
|
|
7955
|
-
if (!slot.days.includes(dayOfWeek)) continue;
|
|
7956
|
-
const candidate = buildSlotDatetime2(candidateDate, slot.time, timezone);
|
|
7957
|
-
const candidateMs = normalizeDateTime(candidate);
|
|
7958
|
-
if (candidateMs <= searchFromMs) continue;
|
|
7959
|
-
if (maxCandidateMs !== void 0 && candidateMs > maxCandidateMs) continue;
|
|
7960
|
-
if (bookedDatetimes.has(candidateMs)) continue;
|
|
7961
|
-
if (passesCandidate && !passesCandidate(candidateMs, platform)) continue;
|
|
7962
|
-
candidates.push(candidate);
|
|
7963
|
-
}
|
|
7964
|
-
}
|
|
7965
|
-
candidates.sort((left, right) => normalizeDateTime(left) - normalizeDateTime(right));
|
|
7966
|
-
if (candidates.length > 0) {
|
|
7967
|
-
return candidates[0];
|
|
7968
|
-
}
|
|
7969
|
-
startOffset = endOffset + 1;
|
|
7970
|
-
}
|
|
7971
|
-
return null;
|
|
7972
|
-
}
|
|
7973
|
-
async function tryDisplacement({
|
|
7974
|
-
bookedSlots,
|
|
7975
|
-
platform,
|
|
7976
|
-
platformConfig,
|
|
7977
|
-
timezone,
|
|
7978
|
-
bookedDatetimes,
|
|
7979
|
-
options,
|
|
7980
|
-
nowMs,
|
|
7981
|
-
maxCandidateMs,
|
|
7982
|
-
passesSpacing
|
|
7983
|
-
}) {
|
|
7984
|
-
const displacementConfig = getDisplacementConfig();
|
|
7985
|
-
if (!displacementConfig.enabled || !options.ideaIds?.length) {
|
|
7986
|
-
return null;
|
|
7987
|
-
}
|
|
7988
|
-
const candidateSlots = bookedSlots.filter((slot) => {
|
|
7989
|
-
const slotMs = normalizeDateTime(slot.scheduledFor);
|
|
7990
|
-
if (slotMs <= nowMs) return false;
|
|
7991
|
-
if (maxCandidateMs !== void 0 && slotMs > maxCandidateMs) return false;
|
|
7992
|
-
return true;
|
|
7993
|
-
}).sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
|
|
7994
|
-
const lateClient = new LateApiClient();
|
|
7995
|
-
const publishedItemCache = /* @__PURE__ */ new Map();
|
|
7996
|
-
for (const slot of candidateSlots) {
|
|
7997
|
-
if (slot.source !== "late" || !slot.postId) continue;
|
|
7998
|
-
const candidateMs = normalizeDateTime(slot.scheduledFor);
|
|
7999
|
-
if (passesSpacing && !passesSpacing(candidateMs, platform)) continue;
|
|
8000
|
-
let publishedItem = publishedItemCache.get(slot.postId);
|
|
8001
|
-
if (publishedItem === void 0) {
|
|
8002
|
-
publishedItem = await getPublishedItemByLatePostId(slot.postId);
|
|
8003
|
-
publishedItemCache.set(slot.postId, publishedItem);
|
|
8004
|
-
}
|
|
8005
|
-
if (!publishedItem) {
|
|
8221
|
+
if (booked.ideaLinked) {
|
|
8222
|
+
skippedBooked++;
|
|
8223
|
+
if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
|
|
8224
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken by idea post ${booked.postId ?? booked.itemId} \u2014 skipping`);
|
|
8225
|
+
}
|
|
8006
8226
|
continue;
|
|
8007
8227
|
}
|
|
8008
|
-
|
|
8009
|
-
|
|
8228
|
+
skippedBooked++;
|
|
8229
|
+
if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
|
|
8230
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken (${booked.source}/${booked.postId ?? booked.itemId}) \u2014 skipping`);
|
|
8010
8231
|
}
|
|
8011
|
-
const displacedPlatformConfig = publishedItem?.metadata.clipType ? getPlatformSchedule(platform, publishedItem.metadata.clipType) ?? platformConfig : platformConfig;
|
|
8012
|
-
const newSlot = findEmptySlot({
|
|
8013
|
-
platformConfig: displacedPlatformConfig,
|
|
8014
|
-
timezone,
|
|
8015
|
-
bookedDatetimes,
|
|
8016
|
-
platform,
|
|
8017
|
-
searchFromMs: candidateMs,
|
|
8018
|
-
includeSearchDay: true
|
|
8019
|
-
});
|
|
8020
|
-
if (!newSlot) continue;
|
|
8021
|
-
await lateClient.schedulePost(slot.postId, newSlot);
|
|
8022
|
-
logger_default.info(
|
|
8023
|
-
`Displaced post ${sanitizeLogValue(slot.postId)} from ${sanitizeLogValue(slot.scheduledFor)} to ${sanitizeLogValue(newSlot)} for idea-linked content`
|
|
8024
|
-
);
|
|
8025
|
-
return {
|
|
8026
|
-
slot: slot.scheduledFor,
|
|
8027
|
-
displaced: {
|
|
8028
|
-
postId: slot.postId,
|
|
8029
|
-
originalSlot: slot.scheduledFor,
|
|
8030
|
-
newSlot
|
|
8031
|
-
}
|
|
8032
|
-
};
|
|
8033
8232
|
}
|
|
8233
|
+
logger_default.warn(`[schedulePost] \u274C No slot found for ${label} \u2014 checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing`);
|
|
8034
8234
|
return null;
|
|
8035
8235
|
}
|
|
8036
8236
|
async function findNextSlot(platform, clipType, options) {
|
|
@@ -8040,58 +8240,46 @@ async function findNextSlot(platform, clipType, options) {
|
|
|
8040
8240
|
logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
|
|
8041
8241
|
return null;
|
|
8042
8242
|
}
|
|
8243
|
+
const { timezone } = config2;
|
|
8244
|
+
const nowMs = Date.now();
|
|
8043
8245
|
const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
|
|
8044
8246
|
const isIdeaAware = ideaIds.length > 0;
|
|
8045
|
-
const
|
|
8046
|
-
const
|
|
8047
|
-
const
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
const bookedSlots = isIdeaAware ? allBookedSlots.filter((slot) => slot.platform === platform) : await buildBookedSlots(platform);
|
|
8052
|
-
const bookedDatetimes = new Set(bookedSlots.map((slot) => normalizeDateTime(slot.scheduledFor)));
|
|
8053
|
-
const spacingConfig = isIdeaAware ? getIdeaSpacingConfig() : null;
|
|
8054
|
-
const spacingGuard = spacingConfig ? createSpacingGuard(
|
|
8055
|
-
buildIdeaReferences(sameIdeaPosts, allBookedSlots),
|
|
8056
|
-
spacingConfig.samePlatformHours,
|
|
8057
|
-
spacingConfig.crossPlatformHours
|
|
8058
|
-
) : void 0;
|
|
8059
|
-
const searchWindow = isIdeaAware ? resolveSearchWindow(nowMs, options) : {};
|
|
8060
|
-
const emptySlot = findEmptySlot({
|
|
8061
|
-
platformConfig,
|
|
8062
|
-
timezone,
|
|
8063
|
-
bookedDatetimes,
|
|
8064
|
-
platform,
|
|
8065
|
-
searchFromMs: nowMs,
|
|
8066
|
-
maxCandidateMs: searchWindow.emptyWindowEndMs,
|
|
8067
|
-
passesCandidate: spacingGuard
|
|
8068
|
-
});
|
|
8069
|
-
if (emptySlot) {
|
|
8070
|
-
logger_default.debug(`Found available slot for ${sanitizeLogValue(platform)}: ${sanitizeLogValue(emptySlot)}`);
|
|
8071
|
-
return emptySlot;
|
|
8072
|
-
}
|
|
8247
|
+
const bookedMap = await buildBookedMap(platform);
|
|
8248
|
+
const ideaLinkedPostIds = await getIdeaLinkedLatePostIds();
|
|
8249
|
+
const label = `${platform}/${clipType ?? "default"}`;
|
|
8250
|
+
let ideaRefs = [];
|
|
8251
|
+
let samePlatformMs = 0;
|
|
8252
|
+
let crossPlatformMs = 0;
|
|
8073
8253
|
if (isIdeaAware) {
|
|
8074
|
-
const
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8254
|
+
const allBookedMap = await buildBookedMap();
|
|
8255
|
+
ideaRefs = await getIdeaReferences(ideaIds, allBookedMap);
|
|
8256
|
+
const spacingConfig = getIdeaSpacingConfig();
|
|
8257
|
+
samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
|
|
8258
|
+
crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
|
|
8259
|
+
}
|
|
8260
|
+
logger_default.info(`[findNextSlot] Scheduling ${label} (idea=${isIdeaAware}, booked=${bookedMap.size} slots, spacingRefs=${ideaRefs.length})`);
|
|
8261
|
+
const ctx = {
|
|
8262
|
+
timezone,
|
|
8263
|
+
bookedMap,
|
|
8264
|
+
ideaLinkedPostIds,
|
|
8265
|
+
lateClient: new LateApiClient(),
|
|
8266
|
+
displacementEnabled: getDisplacementConfig().enabled,
|
|
8267
|
+
dryRun: false,
|
|
8268
|
+
depth: 0,
|
|
8269
|
+
ideaRefs,
|
|
8270
|
+
samePlatformMs,
|
|
8271
|
+
crossPlatformMs,
|
|
8272
|
+
platform
|
|
8273
|
+
};
|
|
8274
|
+
const result = await schedulePost(platformConfig, nowMs, isIdeaAware, label, ctx);
|
|
8275
|
+
if (!result) {
|
|
8276
|
+
logger_default.warn(`[findNextSlot] No available slot for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
|
|
8088
8277
|
}
|
|
8089
|
-
|
|
8090
|
-
return null;
|
|
8278
|
+
return result;
|
|
8091
8279
|
}
|
|
8092
8280
|
async function getScheduleCalendar(startDate, endDate) {
|
|
8093
|
-
const
|
|
8094
|
-
let filtered =
|
|
8281
|
+
const bookedMap = await buildBookedMap();
|
|
8282
|
+
let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
|
|
8095
8283
|
platform: slot.platform,
|
|
8096
8284
|
scheduledFor: slot.scheduledFor,
|
|
8097
8285
|
source: slot.source,
|
|
@@ -8110,6 +8298,206 @@ async function getScheduleCalendar(startDate, endDate) {
|
|
|
8110
8298
|
return filtered;
|
|
8111
8299
|
}
|
|
8112
8300
|
|
|
8301
|
+
// src/L3-services/scheduler/realign.ts
|
|
8302
|
+
var PLATFORM_ALIASES2 = { twitter: "x" };
|
|
8303
|
+
function normalizeSchedulePlatform(platform) {
|
|
8304
|
+
return PLATFORM_ALIASES2[platform] ?? platform;
|
|
8305
|
+
}
|
|
8306
|
+
function normalizeContent(content) {
|
|
8307
|
+
return content.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 200);
|
|
8308
|
+
}
|
|
8309
|
+
async function buildClipTypeMaps() {
|
|
8310
|
+
const published = await getPublishedItems();
|
|
8311
|
+
const byLatePostId = /* @__PURE__ */ new Map();
|
|
8312
|
+
const byContent = /* @__PURE__ */ new Map();
|
|
8313
|
+
for (const item of published) {
|
|
8314
|
+
if (item.metadata.latePostId) {
|
|
8315
|
+
byLatePostId.set(item.metadata.latePostId, item.metadata.clipType);
|
|
8316
|
+
}
|
|
8317
|
+
if (item.postContent) {
|
|
8318
|
+
const contentKey = `${item.metadata.platform}::${normalizeContent(item.postContent)}`;
|
|
8319
|
+
byContent.set(contentKey, item.metadata.clipType);
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
8322
|
+
logger_default.debug(`Built clipType maps: ${byLatePostId.size} by latePostId, ${byContent.size} by content`);
|
|
8323
|
+
return { byLatePostId, byContent };
|
|
8324
|
+
}
|
|
8325
|
+
async function fetchAllPosts(client, statuses, platform) {
|
|
8326
|
+
const allPosts = [];
|
|
8327
|
+
for (const status of statuses) {
|
|
8328
|
+
const posts = await client.listPosts({ status, platform });
|
|
8329
|
+
allPosts.push(...posts);
|
|
8330
|
+
logger_default.info(`Fetched ${posts.length} ${status} post(s)${platform ? ` for ${platform}` : ""}`);
|
|
8331
|
+
}
|
|
8332
|
+
return allPosts;
|
|
8333
|
+
}
|
|
8334
|
+
function isOnValidSlot(iso, schedule, timezone) {
|
|
8335
|
+
if (schedule.slots.length === 0) return false;
|
|
8336
|
+
const date = new Date(iso);
|
|
8337
|
+
const dayOfWeek = getDayOfWeekInTimezone(date, timezone);
|
|
8338
|
+
if (schedule.avoidDays.includes(dayOfWeek)) return false;
|
|
8339
|
+
const timeFormatter = new Intl.DateTimeFormat("en-US", {
|
|
8340
|
+
timeZone: timezone,
|
|
8341
|
+
hour: "2-digit",
|
|
8342
|
+
minute: "2-digit",
|
|
8343
|
+
hour12: false
|
|
8344
|
+
});
|
|
8345
|
+
const timeParts = timeFormatter.formatToParts(date);
|
|
8346
|
+
const hour = timeParts.find((p) => p.type === "hour")?.value ?? "00";
|
|
8347
|
+
const minute = timeParts.find((p) => p.type === "minute")?.value ?? "00";
|
|
8348
|
+
const timeKey = `${hour}:${minute}`;
|
|
8349
|
+
return schedule.slots.some((slot) => slot.time === timeKey && slot.days.includes(dayOfWeek));
|
|
8350
|
+
}
|
|
8351
|
+
async function buildRealignPlan(options = {}) {
|
|
8352
|
+
const config2 = await loadScheduleConfig();
|
|
8353
|
+
const { timezone } = config2;
|
|
8354
|
+
const client = new LateApiClient();
|
|
8355
|
+
const statuses = ["scheduled", "draft", "cancelled", "failed"];
|
|
8356
|
+
const allPosts = await fetchAllPosts(client, statuses, options.platform);
|
|
8357
|
+
if (allPosts.length === 0) {
|
|
8358
|
+
return { posts: [], toCancel: [], skipped: 0, unmatched: 0, totalFetched: 0 };
|
|
8359
|
+
}
|
|
8360
|
+
const { byLatePostId, byContent } = options.clipTypeMaps ?? await buildClipTypeMaps();
|
|
8361
|
+
let unmatched = 0;
|
|
8362
|
+
const tagged = [];
|
|
8363
|
+
for (const post of allPosts) {
|
|
8364
|
+
const platform = post.platforms[0]?.platform;
|
|
8365
|
+
if (!platform) continue;
|
|
8366
|
+
let clipType = byLatePostId.get(post._id) ?? null;
|
|
8367
|
+
if (!clipType && post.content) {
|
|
8368
|
+
const contentKey = `${platform}::${normalizeContent(post.content)}`;
|
|
8369
|
+
clipType = byContent.get(contentKey) ?? null;
|
|
8370
|
+
}
|
|
8371
|
+
if (!clipType) {
|
|
8372
|
+
clipType = "short";
|
|
8373
|
+
unmatched++;
|
|
8374
|
+
}
|
|
8375
|
+
tagged.push({ post, platform, clipType });
|
|
8376
|
+
}
|
|
8377
|
+
const bookedMap = await buildBookedMap();
|
|
8378
|
+
const ctx = {
|
|
8379
|
+
timezone,
|
|
8380
|
+
bookedMap,
|
|
8381
|
+
ideaLinkedPostIds: /* @__PURE__ */ new Set(),
|
|
8382
|
+
lateClient: client,
|
|
8383
|
+
displacementEnabled: getDisplacementConfig().enabled,
|
|
8384
|
+
dryRun: true,
|
|
8385
|
+
depth: 0,
|
|
8386
|
+
ideaRefs: [],
|
|
8387
|
+
samePlatformMs: 0,
|
|
8388
|
+
crossPlatformMs: 0,
|
|
8389
|
+
platform: ""
|
|
8390
|
+
};
|
|
8391
|
+
for (const [, slot] of bookedMap) {
|
|
8392
|
+
if (slot.ideaLinked && slot.postId) {
|
|
8393
|
+
ctx.ideaLinkedPostIds.add(slot.postId);
|
|
8394
|
+
}
|
|
8395
|
+
}
|
|
8396
|
+
const result = [];
|
|
8397
|
+
const toCancel = [];
|
|
8398
|
+
let skipped = 0;
|
|
8399
|
+
tagged.sort((a, b) => {
|
|
8400
|
+
const aIdea = ctx.ideaLinkedPostIds.has(a.post._id) ? 0 : 1;
|
|
8401
|
+
const bIdea = ctx.ideaLinkedPostIds.has(b.post._id) ? 0 : 1;
|
|
8402
|
+
return aIdea - bIdea;
|
|
8403
|
+
});
|
|
8404
|
+
const nowMs = Date.now();
|
|
8405
|
+
for (const { post, platform, clipType } of tagged) {
|
|
8406
|
+
const schedulePlatform = normalizeSchedulePlatform(platform);
|
|
8407
|
+
const platformConfig = getPlatformSchedule(schedulePlatform, clipType);
|
|
8408
|
+
if (!platformConfig || platformConfig.slots.length === 0) {
|
|
8409
|
+
if (post.status !== "cancelled") {
|
|
8410
|
+
toCancel.push({ post, platform, clipType, reason: `No schedule slots for ${schedulePlatform}/${clipType}` });
|
|
8411
|
+
}
|
|
8412
|
+
continue;
|
|
8413
|
+
}
|
|
8414
|
+
if (post.scheduledFor && post.status === "scheduled" && isOnValidSlot(post.scheduledFor, platformConfig, timezone)) {
|
|
8415
|
+
skipped++;
|
|
8416
|
+
continue;
|
|
8417
|
+
}
|
|
8418
|
+
if (post.scheduledFor) {
|
|
8419
|
+
const currentMs2 = new Date(post.scheduledFor).getTime();
|
|
8420
|
+
const currentBooked = bookedMap.get(currentMs2);
|
|
8421
|
+
if (currentBooked?.postId === post._id) {
|
|
8422
|
+
bookedMap.delete(currentMs2);
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8425
|
+
const isIdea = ctx.ideaLinkedPostIds.has(post._id);
|
|
8426
|
+
const label = `${schedulePlatform}/${clipType}:${post._id.slice(-6)}`;
|
|
8427
|
+
const newSlot = await schedulePost(platformConfig, nowMs, isIdea, label, ctx);
|
|
8428
|
+
if (!newSlot) {
|
|
8429
|
+
if (post.status !== "cancelled") {
|
|
8430
|
+
toCancel.push({ post, platform, clipType, reason: `No available slot for ${schedulePlatform}/${clipType}` });
|
|
8431
|
+
}
|
|
8432
|
+
continue;
|
|
8433
|
+
}
|
|
8434
|
+
const newMs = new Date(newSlot).getTime();
|
|
8435
|
+
ctx.bookedMap.set(newMs, {
|
|
8436
|
+
scheduledFor: newSlot,
|
|
8437
|
+
source: "late",
|
|
8438
|
+
postId: post._id,
|
|
8439
|
+
platform: schedulePlatform,
|
|
8440
|
+
ideaLinked: isIdea
|
|
8441
|
+
});
|
|
8442
|
+
const currentMs = post.scheduledFor ? new Date(post.scheduledFor).getTime() : 0;
|
|
8443
|
+
if (currentMs === newMs && post.status === "scheduled") {
|
|
8444
|
+
skipped++;
|
|
8445
|
+
continue;
|
|
8446
|
+
}
|
|
8447
|
+
result.push({
|
|
8448
|
+
post,
|
|
8449
|
+
platform,
|
|
8450
|
+
clipType,
|
|
8451
|
+
oldScheduledFor: post.scheduledFor ?? null,
|
|
8452
|
+
newScheduledFor: newSlot
|
|
8453
|
+
});
|
|
8454
|
+
}
|
|
8455
|
+
result.sort((a, b) => new Date(a.newScheduledFor).getTime() - new Date(b.newScheduledFor).getTime());
|
|
8456
|
+
return { posts: result, toCancel, skipped, unmatched, totalFetched: allPosts.length };
|
|
8457
|
+
}
|
|
8458
|
+
async function executeRealignPlan(plan, onProgress) {
|
|
8459
|
+
const client = new LateApiClient();
|
|
8460
|
+
let updated = 0;
|
|
8461
|
+
let cancelled = 0;
|
|
8462
|
+
let failed = 0;
|
|
8463
|
+
const errors = [];
|
|
8464
|
+
const totalOps = plan.toCancel.length + plan.posts.length;
|
|
8465
|
+
let completed = 0;
|
|
8466
|
+
for (const entry of plan.toCancel) {
|
|
8467
|
+
completed++;
|
|
8468
|
+
try {
|
|
8469
|
+
await client.updatePost(entry.post._id, { status: "cancelled" });
|
|
8470
|
+
cancelled++;
|
|
8471
|
+
const preview = entry.post.content.slice(0, 40).replace(/\n/g, " ");
|
|
8472
|
+
logger_default.info(`[${completed}/${totalOps}] \u{1F6AB} Cancelled ${entry.platform}/${entry.clipType}: "${preview}..."`);
|
|
8473
|
+
onProgress?.(completed, totalOps, "cancelling");
|
|
8474
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
8475
|
+
} catch (err) {
|
|
8476
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8477
|
+
errors.push({ postId: entry.post._id, error: msg });
|
|
8478
|
+
failed++;
|
|
8479
|
+
logger_default.error(`[${completed}/${totalOps}] \u274C Failed to cancel ${entry.post._id}: ${msg}`);
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
for (const entry of plan.posts) {
|
|
8483
|
+
completed++;
|
|
8484
|
+
try {
|
|
8485
|
+
await client.schedulePost(entry.post._id, entry.newScheduledFor);
|
|
8486
|
+
updated++;
|
|
8487
|
+
const preview = entry.post.content.slice(0, 40).replace(/\n/g, " ");
|
|
8488
|
+
logger_default.info(`[${completed}/${totalOps}] \u2705 ${entry.platform}/${entry.clipType}: "${preview}..." \u2192 ${entry.newScheduledFor}`);
|
|
8489
|
+
onProgress?.(completed, totalOps, "updating");
|
|
8490
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
8491
|
+
} catch (err) {
|
|
8492
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8493
|
+
errors.push({ postId: entry.post._id, error: msg });
|
|
8494
|
+
failed++;
|
|
8495
|
+
logger_default.error(`[${completed}/${totalOps}] \u274C Failed to update ${entry.post._id}: ${msg}`);
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
return { updated, cancelled, failed, errors };
|
|
8499
|
+
}
|
|
8500
|
+
|
|
8113
8501
|
// src/L2-clients/ffmpeg/audioExtraction.ts
|
|
8114
8502
|
init_fileSystem();
|
|
8115
8503
|
init_paths();
|
|
@@ -9271,6 +9659,8 @@ function transcodeToMp4(inputPath, outputPath) {
|
|
|
9271
9659
|
createFFmpeg(inputPath).outputOptions([
|
|
9272
9660
|
"-c:v",
|
|
9273
9661
|
"libx264",
|
|
9662
|
+
"-pix_fmt",
|
|
9663
|
+
"yuv420p",
|
|
9274
9664
|
"-preset",
|
|
9275
9665
|
"ultrafast",
|
|
9276
9666
|
"-crf",
|
|
@@ -9574,32 +9964,6 @@ async function markFailed(slug, error) {
|
|
|
9574
9964
|
logger_default.info(`[ProcessingState] Marked failed: ${slug} \u2014 ${error}`);
|
|
9575
9965
|
}
|
|
9576
9966
|
|
|
9577
|
-
// src/L3-services/gitOperations/gitOperations.ts
|
|
9578
|
-
init_environment();
|
|
9579
|
-
init_configLogger();
|
|
9580
|
-
async function commitAndPush(videoSlug, message) {
|
|
9581
|
-
const { REPO_ROOT } = getConfig();
|
|
9582
|
-
const commitMessage = message || `Auto-processed video: ${videoSlug}`;
|
|
9583
|
-
try {
|
|
9584
|
-
logger_default.info(`Staging all changes in ${REPO_ROOT}`);
|
|
9585
|
-
execCommandSync("git add -A", { cwd: REPO_ROOT, stdio: "pipe" });
|
|
9586
|
-
logger_default.info(`Committing: ${commitMessage}`);
|
|
9587
|
-
execCommandSync(`git commit -m "${commitMessage}"`, { cwd: REPO_ROOT, stdio: "pipe" });
|
|
9588
|
-
const branch = execCommandSync("git rev-parse --abbrev-ref HEAD", { cwd: REPO_ROOT, stdio: "pipe" });
|
|
9589
|
-
logger_default.info(`Pushing to origin ${branch}`);
|
|
9590
|
-
execCommandSync(`git push origin ${branch}`, { cwd: REPO_ROOT, stdio: "pipe" });
|
|
9591
|
-
logger_default.info("Git commit and push completed successfully");
|
|
9592
|
-
} catch (error) {
|
|
9593
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
9594
|
-
if (msg.includes("nothing to commit")) {
|
|
9595
|
-
logger_default.info("Nothing to commit, working tree clean");
|
|
9596
|
-
return;
|
|
9597
|
-
}
|
|
9598
|
-
logger_default.error(`Git operation failed: ${msg}`);
|
|
9599
|
-
throw error;
|
|
9600
|
-
}
|
|
9601
|
-
}
|
|
9602
|
-
|
|
9603
9967
|
// src/L3-services/queueBuilder/queueBuilder.ts
|
|
9604
9968
|
init_fileSystem();
|
|
9605
9969
|
init_paths();
|
|
@@ -9640,6 +10004,9 @@ function platformAcceptsMedia(platform, clipType) {
|
|
|
9640
10004
|
return getMediaRule(platform, clipType) !== null;
|
|
9641
10005
|
}
|
|
9642
10006
|
|
|
10007
|
+
// src/L3-services/queueBuilder/queueBuilder.ts
|
|
10008
|
+
init_postStore();
|
|
10009
|
+
|
|
9643
10010
|
// src/L1-infra/image/image.ts
|
|
9644
10011
|
import { default as default7 } from "sharp";
|
|
9645
10012
|
|
|
@@ -9904,9 +10271,6 @@ function markCompleted2(...args) {
|
|
|
9904
10271
|
function markFailed2(...args) {
|
|
9905
10272
|
return markFailed(...args);
|
|
9906
10273
|
}
|
|
9907
|
-
function commitAndPush2(...args) {
|
|
9908
|
-
return commitAndPush(...args);
|
|
9909
|
-
}
|
|
9910
10274
|
function buildPublishQueue2(...args) {
|
|
9911
10275
|
return buildPublishQueue(...args);
|
|
9912
10276
|
}
|
|
@@ -9960,6 +10324,7 @@ var BaseAgent = class _BaseAgent {
|
|
|
9960
10324
|
for (let attempt = 1; attempt <= _BaseAgent.MAX_RETRIES; attempt++) {
|
|
9961
10325
|
try {
|
|
9962
10326
|
if (!this.session) {
|
|
10327
|
+
logger_default.info(`[${this.agentName}] Creating LLM session (provider=${this.provider.name})\u2026`);
|
|
9963
10328
|
this.session = await this.provider.createSession({
|
|
9964
10329
|
systemPrompt: this.systemPrompt,
|
|
9965
10330
|
tools: this.getTools(),
|
|
@@ -9970,6 +10335,7 @@ var BaseAgent = class _BaseAgent {
|
|
|
9970
10335
|
onUserInputRequest: this.getUserInputHandler()
|
|
9971
10336
|
});
|
|
9972
10337
|
this.setupEventHandlers(this.session);
|
|
10338
|
+
logger_default.info(`[${this.agentName}] LLM session ready`);
|
|
9973
10339
|
}
|
|
9974
10340
|
logger_default.info(`[${this.agentName}] Sending message (attempt ${attempt}/${_BaseAgent.MAX_RETRIES}): ${userMessage.substring(0, 80)}\u2026`);
|
|
9975
10341
|
costTracker.setAgent(this.agentName);
|
|
@@ -10007,11 +10373,12 @@ var BaseAgent = class _BaseAgent {
|
|
|
10007
10373
|
}
|
|
10008
10374
|
/** Check if an error message indicates a transient/retryable failure. */
|
|
10009
10375
|
static isRetryableError(message) {
|
|
10010
|
-
const
|
|
10376
|
+
const lower = message.toLowerCase();
|
|
10377
|
+
return [
|
|
10011
10378
|
"missing finish_reason",
|
|
10012
|
-
"
|
|
10013
|
-
"
|
|
10014
|
-
"
|
|
10379
|
+
"econnreset",
|
|
10380
|
+
"etimedout",
|
|
10381
|
+
"econnrefused",
|
|
10015
10382
|
"socket hang up",
|
|
10016
10383
|
"network error",
|
|
10017
10384
|
"rate limit",
|
|
@@ -10021,10 +10388,9 @@ var BaseAgent = class _BaseAgent {
|
|
|
10021
10388
|
"503",
|
|
10022
10389
|
"504",
|
|
10023
10390
|
"stream ended",
|
|
10024
|
-
"aborted"
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
return retryablePatterns.some((p) => lower.includes(p.toLowerCase()));
|
|
10391
|
+
"aborted",
|
|
10392
|
+
"cli server exited"
|
|
10393
|
+
].some((p) => lower.includes(p));
|
|
10028
10394
|
}
|
|
10029
10395
|
/** Wire up session event listeners for logging. Override for custom display. */
|
|
10030
10396
|
setupEventHandlers(session) {
|
|
@@ -10328,7 +10694,7 @@ function buildSystemPrompt(brand, existingIdeas, seedTopics, count, ideaRepo) {
|
|
|
10328
10694
|
}
|
|
10329
10695
|
return promptSections.join("\n");
|
|
10330
10696
|
}
|
|
10331
|
-
function buildUserMessage(count, seedTopics, hasMcpServers) {
|
|
10697
|
+
function buildUserMessage(count, seedTopics, hasMcpServers, userPrompt) {
|
|
10332
10698
|
const focusText = seedTopics.length > 0 ? `Focus areas: ${seedTopics.join(", ")}` : "Focus areas: choose the strongest timely opportunities from the creator context and current trends.";
|
|
10333
10699
|
const steps = [
|
|
10334
10700
|
"1. Call get_brand_context to load the creator profile.",
|
|
@@ -10352,13 +10718,15 @@ function buildUserMessage(count, seedTopics, hasMcpServers) {
|
|
|
10352
10718
|
"5. Call finalize_ideas when done."
|
|
10353
10719
|
);
|
|
10354
10720
|
}
|
|
10355
|
-
|
|
10721
|
+
const sections = [
|
|
10356
10722
|
`Generate ${count} new content ideas.`,
|
|
10357
|
-
focusText
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10723
|
+
focusText
|
|
10724
|
+
];
|
|
10725
|
+
if (userPrompt) {
|
|
10726
|
+
sections.push("", `## User Prompt`, userPrompt);
|
|
10727
|
+
}
|
|
10728
|
+
sections.push("", "Follow this exact workflow:", ...steps);
|
|
10729
|
+
return sections.join("\n");
|
|
10362
10730
|
}
|
|
10363
10731
|
async function loadBrandContext(brandPath) {
|
|
10364
10732
|
if (!brandPath) {
|
|
@@ -10875,7 +11243,7 @@ async function generateIdeas(options = {}) {
|
|
|
10875
11243
|
});
|
|
10876
11244
|
try {
|
|
10877
11245
|
const hasMcpServers = !!(config2.EXA_API_KEY || config2.YOUTUBE_API_KEY || config2.PERPLEXITY_API_KEY);
|
|
10878
|
-
const userMessage = buildUserMessage(count, seedTopics, hasMcpServers);
|
|
11246
|
+
const userMessage = buildUserMessage(count, seedTopics, hasMcpServers, options.prompt);
|
|
10879
11247
|
await agent.run(userMessage);
|
|
10880
11248
|
const ideas = agent.getGeneratedIdeas();
|
|
10881
11249
|
if (!agent.isFinalized()) {
|
|
@@ -10923,32 +11291,6 @@ init_fileSystem();
|
|
|
10923
11291
|
init_configLogger();
|
|
10924
11292
|
init_environment();
|
|
10925
11293
|
|
|
10926
|
-
// src/L1-infra/progress/progressEmitter.ts
|
|
10927
|
-
var ProgressEmitter = class {
|
|
10928
|
-
enabled = false;
|
|
10929
|
-
/** Turn on progress event output to stderr. */
|
|
10930
|
-
enable() {
|
|
10931
|
-
this.enabled = true;
|
|
10932
|
-
}
|
|
10933
|
-
/** Turn off progress event output. */
|
|
10934
|
-
disable() {
|
|
10935
|
-
this.enabled = false;
|
|
10936
|
-
}
|
|
10937
|
-
/** Whether the emitter is currently active. */
|
|
10938
|
-
isEnabled() {
|
|
10939
|
-
return this.enabled;
|
|
10940
|
-
}
|
|
10941
|
-
/**
|
|
10942
|
-
* Write a progress event as a single JSON line to stderr.
|
|
10943
|
-
* No-op when the emitter is disabled.
|
|
10944
|
-
*/
|
|
10945
|
-
emit(event) {
|
|
10946
|
-
if (!this.enabled) return;
|
|
10947
|
-
process.stderr.write(JSON.stringify(event) + "\n");
|
|
10948
|
-
}
|
|
10949
|
-
};
|
|
10950
|
-
var progressEmitter = new ProgressEmitter();
|
|
10951
|
-
|
|
10952
11294
|
// src/L5-assets/Asset.ts
|
|
10953
11295
|
init_fileSystem();
|
|
10954
11296
|
var Asset = class {
|
|
@@ -15860,12 +16202,6 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
15860
16202
|
async buildQueue(shorts, mediumClips, socialPosts, captionedVideoPath) {
|
|
15861
16203
|
await this.buildPublishQueueData(shorts, mediumClips, socialPosts, captionedVideoPath);
|
|
15862
16204
|
}
|
|
15863
|
-
/**
|
|
15864
|
-
* Commit and push all generated assets via git.
|
|
15865
|
-
*/
|
|
15866
|
-
async commitAndPushChanges(message) {
|
|
15867
|
-
return commitAndPush2(this.slug, message);
|
|
15868
|
-
}
|
|
15869
16205
|
};
|
|
15870
16206
|
|
|
15871
16207
|
// src/L6-pipeline/pipeline.ts
|
|
@@ -16071,11 +16407,6 @@ async function processVideo(videoPath, ideas) {
|
|
|
16071
16407
|
skipStage("queue-build" /* QueueBuild */, "NO_SOCIAL_POSTS");
|
|
16072
16408
|
}
|
|
16073
16409
|
const blogPost = await trackStage("blog" /* Blog */, () => asset.getBlog());
|
|
16074
|
-
if (!cfg.SKIP_GIT) {
|
|
16075
|
-
await trackStage("git-push" /* GitPush */, () => asset.commitAndPushChanges());
|
|
16076
|
-
} else {
|
|
16077
|
-
skipStage("git-push" /* GitPush */, "SKIP_GIT");
|
|
16078
|
-
}
|
|
16079
16410
|
const totalDuration = Date.now() - pipelineStart;
|
|
16080
16411
|
const report = costTracker3.getReport();
|
|
16081
16412
|
if (report.records.length > 0) {
|
|
@@ -16198,7 +16529,8 @@ var defaultKeys = [
|
|
|
16198
16529
|
"brandPath",
|
|
16199
16530
|
"ideasRepo",
|
|
16200
16531
|
"lateProfileId",
|
|
16201
|
-
"geminiModel"
|
|
16532
|
+
"geminiModel",
|
|
16533
|
+
"scheduleConfig"
|
|
16202
16534
|
];
|
|
16203
16535
|
var configKeyMap = {
|
|
16204
16536
|
"openai-key": { section: "credentials", key: "openaiApiKey" },
|
|
@@ -16216,7 +16548,8 @@ var configKeyMap = {
|
|
|
16216
16548
|
"brand-path": { section: "defaults", key: "brandPath" },
|
|
16217
16549
|
"ideas-repo": { section: "defaults", key: "ideasRepo" },
|
|
16218
16550
|
"late-profile-id": { section: "defaults", key: "lateProfileId" },
|
|
16219
|
-
"gemini-model": { section: "defaults", key: "geminiModel" }
|
|
16551
|
+
"gemini-model": { section: "defaults", key: "geminiModel" },
|
|
16552
|
+
"schedule-config": { section: "defaults", key: "scheduleConfig" }
|
|
16220
16553
|
};
|
|
16221
16554
|
var providerDefaults = {
|
|
16222
16555
|
copilot: "Claude Opus 4.6",
|
|
@@ -16252,20 +16585,11 @@ var platformVariantMap = {
|
|
|
16252
16585
|
youtube: "youtube",
|
|
16253
16586
|
"youtube-shorts": "youtube-shorts"
|
|
16254
16587
|
};
|
|
16255
|
-
function applySdkEnvironment(sdkConfig) {
|
|
16256
|
-
if (!sdkConfig) {
|
|
16257
|
-
return;
|
|
16258
|
-
}
|
|
16259
|
-
if (sdkConfig.anthropicApiKey) process.env.ANTHROPIC_API_KEY = sdkConfig.anthropicApiKey;
|
|
16260
|
-
if (sdkConfig.geminiApiKey) process.env.GEMINI_API_KEY = sdkConfig.geminiApiKey;
|
|
16261
|
-
if (sdkConfig.llmProvider) process.env.LLM_PROVIDER = sdkConfig.llmProvider;
|
|
16262
|
-
if (sdkConfig.llmModel) process.env.LLM_MODEL = sdkConfig.llmModel;
|
|
16263
|
-
if (sdkConfig.geminiModel) process.env.GEMINI_MODEL = sdkConfig.geminiModel;
|
|
16264
|
-
if (sdkConfig.repoRoot) process.env.REPO_ROOT = sdkConfig.repoRoot;
|
|
16265
|
-
}
|
|
16266
16588
|
function mapSdkConfigToCliOptions(sdkConfig) {
|
|
16267
16589
|
return {
|
|
16268
16590
|
openaiKey: sdkConfig?.openaiApiKey,
|
|
16591
|
+
anthropicKey: sdkConfig?.anthropicApiKey,
|
|
16592
|
+
geminiKey: sdkConfig?.geminiApiKey,
|
|
16269
16593
|
exaKey: sdkConfig?.exaApiKey,
|
|
16270
16594
|
youtubeKey: sdkConfig?.youtubeApiKey,
|
|
16271
16595
|
perplexityKey: sdkConfig?.perplexityApiKey,
|
|
@@ -16276,12 +16600,15 @@ function mapSdkConfigToCliOptions(sdkConfig) {
|
|
|
16276
16600
|
lateApiKey: sdkConfig?.lateApiKey,
|
|
16277
16601
|
lateProfileId: sdkConfig?.lateProfileId,
|
|
16278
16602
|
ideasRepo: sdkConfig?.ideasRepo,
|
|
16279
|
-
githubToken: sdkConfig?.githubToken
|
|
16603
|
+
githubToken: sdkConfig?.githubToken,
|
|
16604
|
+
llmProvider: sdkConfig?.llmProvider,
|
|
16605
|
+
llmModel: sdkConfig?.llmModel,
|
|
16606
|
+
geminiModel: sdkConfig?.geminiModel,
|
|
16607
|
+
repoRoot: sdkConfig?.repoRoot
|
|
16280
16608
|
};
|
|
16281
16609
|
}
|
|
16282
16610
|
function mapProcessOptionsToCliOverrides(options) {
|
|
16283
16611
|
const overrides = {};
|
|
16284
|
-
if (options?.skipGit !== void 0) overrides.git = !options.skipGit;
|
|
16285
16612
|
if (options?.skipSilenceRemoval !== void 0) overrides.silenceRemoval = !options.skipSilenceRemoval;
|
|
16286
16613
|
if (options?.skipShorts !== void 0) overrides.shorts = !options.skipShorts;
|
|
16287
16614
|
if (options?.skipMediumClips !== void 0) overrides.mediumClips = !options.skipMediumClips;
|
|
@@ -16346,10 +16673,10 @@ function applyPersistedConfigToRuntime(target, value, currentCliOptions) {
|
|
|
16346
16673
|
currentCliOptions.githubToken = value;
|
|
16347
16674
|
break;
|
|
16348
16675
|
case "credentials.anthropicApiKey":
|
|
16349
|
-
|
|
16676
|
+
currentCliOptions.anthropicKey = value;
|
|
16350
16677
|
break;
|
|
16351
16678
|
case "credentials.geminiApiKey":
|
|
16352
|
-
|
|
16679
|
+
currentCliOptions.geminiKey = value;
|
|
16353
16680
|
break;
|
|
16354
16681
|
case "defaults.outputDir":
|
|
16355
16682
|
currentCliOptions.outputDir = value;
|
|
@@ -16367,13 +16694,13 @@ function applyPersistedConfigToRuntime(target, value, currentCliOptions) {
|
|
|
16367
16694
|
currentCliOptions.lateProfileId = value;
|
|
16368
16695
|
break;
|
|
16369
16696
|
case "defaults.llmProvider":
|
|
16370
|
-
|
|
16697
|
+
currentCliOptions.llmProvider = value;
|
|
16371
16698
|
break;
|
|
16372
16699
|
case "defaults.llmModel":
|
|
16373
|
-
|
|
16700
|
+
currentCliOptions.llmModel = value;
|
|
16374
16701
|
break;
|
|
16375
16702
|
case "defaults.geminiModel":
|
|
16376
|
-
|
|
16703
|
+
currentCliOptions.geminiModel = value;
|
|
16377
16704
|
break;
|
|
16378
16705
|
default:
|
|
16379
16706
|
break;
|
|
@@ -16386,10 +16713,6 @@ function applyRuntimeOnlyOverride(rawKey, value, currentCliOptions) {
|
|
|
16386
16713
|
case "VERBOSE":
|
|
16387
16714
|
currentCliOptions.verbose = Boolean(value);
|
|
16388
16715
|
return true;
|
|
16389
|
-
case "skipGit":
|
|
16390
|
-
case "SKIP_GIT":
|
|
16391
|
-
currentCliOptions.git = !Boolean(value);
|
|
16392
|
-
return true;
|
|
16393
16716
|
case "skipSilenceRemoval":
|
|
16394
16717
|
case "SKIP_SILENCE_REMOVAL":
|
|
16395
16718
|
currentCliOptions.silenceRemoval = !Boolean(value);
|
|
@@ -16420,15 +16743,15 @@ function applyRuntimeOnlyOverride(rawKey, value, currentCliOptions) {
|
|
|
16420
16743
|
return true;
|
|
16421
16744
|
case "repoRoot":
|
|
16422
16745
|
case "REPO_ROOT":
|
|
16423
|
-
|
|
16746
|
+
currentCliOptions.repoRoot = String(value);
|
|
16424
16747
|
return true;
|
|
16425
16748
|
case "ffmpegPath":
|
|
16426
16749
|
case "FFMPEG_PATH":
|
|
16427
|
-
|
|
16750
|
+
currentCliOptions.ffmpegPath = String(value);
|
|
16428
16751
|
return true;
|
|
16429
16752
|
case "ffprobePath":
|
|
16430
16753
|
case "FFPROBE_PATH":
|
|
16431
|
-
|
|
16754
|
+
currentCliOptions.ffprobePath = String(value);
|
|
16432
16755
|
return true;
|
|
16433
16756
|
default:
|
|
16434
16757
|
return false;
|
|
@@ -16587,7 +16910,6 @@ async function buildSocialPosts(context, platforms) {
|
|
|
16587
16910
|
}));
|
|
16588
16911
|
}
|
|
16589
16912
|
function createVidPipe(sdkConfig) {
|
|
16590
|
-
applySdkEnvironment(sdkConfig);
|
|
16591
16913
|
let currentCliOptions = mapSdkConfigToCliOptions(sdkConfig);
|
|
16592
16914
|
initConfig(currentCliOptions);
|
|
16593
16915
|
function refreshConfig() {
|
|
@@ -16658,22 +16980,6 @@ function createVidPipe(sdkConfig) {
|
|
|
16658
16980
|
status: buildDiagnosticStatus(false, Boolean(config2.EXA_API_KEY)),
|
|
16659
16981
|
message: config2.EXA_API_KEY ? "EXA_API_KEY is configured" : "EXA_API_KEY is not configured (optional)"
|
|
16660
16982
|
});
|
|
16661
|
-
try {
|
|
16662
|
-
const gitResult = spawnCommand("git", ["--version"], { timeout: 1e4 });
|
|
16663
|
-
const passed = gitResult.status === 0 && typeof gitResult.stdout === "string" && gitResult.stdout.length > 0;
|
|
16664
|
-
checks.push({
|
|
16665
|
-
name: "git",
|
|
16666
|
-
status: buildDiagnosticStatus(false, passed),
|
|
16667
|
-
message: passed ? `Git ${parseVersionFromOutput(gitResult.stdout)} detected` : "Git is not available (optional for git-push stage)"
|
|
16668
|
-
});
|
|
16669
|
-
} catch (error) {
|
|
16670
|
-
checks.push({
|
|
16671
|
-
name: "git",
|
|
16672
|
-
status: "warn",
|
|
16673
|
-
message: "Git is not available (optional for git-push stage)",
|
|
16674
|
-
details: error instanceof Error ? error.message : String(error)
|
|
16675
|
-
});
|
|
16676
|
-
}
|
|
16677
16983
|
const watchFolder = config2.WATCH_FOLDER || join(process.cwd(), "watch");
|
|
16678
16984
|
checks.push({
|
|
16679
16985
|
name: "watch-folder",
|
|
@@ -16758,13 +17064,19 @@ function createVidPipe(sdkConfig) {
|
|
|
16758
17064
|
const cliOverrides = mapProcessOptionsToCliOverrides(options);
|
|
16759
17065
|
const ideaIds = options?.ideas?.map((ideaId) => String(ideaId));
|
|
16760
17066
|
const ideas = ideaIds && ideaIds.length > 0 ? await getIdeasByIds(ideaIds) : void 0;
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
17067
|
+
const listener = options?.onProgress;
|
|
17068
|
+
if (listener) progressEmitter.addListener(listener);
|
|
17069
|
+
try {
|
|
17070
|
+
return await withTemporaryCliOverrides(cliOverrides, async () => {
|
|
17071
|
+
const result = await processVideoSafe(videoPath, ideas);
|
|
17072
|
+
if (!result) {
|
|
17073
|
+
throw new Error(`VidPipe pipeline failed for "${videoPath}" with an uncaught error`);
|
|
17074
|
+
}
|
|
17075
|
+
return result;
|
|
17076
|
+
});
|
|
17077
|
+
} finally {
|
|
17078
|
+
if (listener) progressEmitter.removeListener(listener);
|
|
17079
|
+
}
|
|
16768
17080
|
},
|
|
16769
17081
|
async ideate(options) {
|
|
16770
17082
|
return await generateIdeas3({
|