@telepat/ideon 0.1.32 → 0.1.33
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.de.md +121 -0
- package/README.md +7 -3
- package/README.zh-CN.md +3 -2
- package/dist/ideon.js +1701 -259
- package/package.json +1 -1
package/dist/ideon.js
CHANGED
|
@@ -586,7 +586,13 @@ var envSettingsSchema = z2.object({
|
|
|
586
586
|
notificationsEnabled: z2.boolean().optional(),
|
|
587
587
|
style: z2.enum(writingStyleValues).optional(),
|
|
588
588
|
intent: z2.enum(contentIntentValues).optional(),
|
|
589
|
-
targetLength: targetLengthWordsSchema.optional()
|
|
589
|
+
targetLength: targetLengthWordsSchema.optional(),
|
|
590
|
+
googleAdsDeveloperToken: z2.string().optional(),
|
|
591
|
+
googleAdsClientId: z2.string().optional(),
|
|
592
|
+
googleAdsClientSecret: z2.string().optional(),
|
|
593
|
+
googleAdsRefreshToken: z2.string().optional(),
|
|
594
|
+
googleAdsCustomerId: z2.string().optional(),
|
|
595
|
+
googleAdsLoginCustomerId: z2.string().optional()
|
|
590
596
|
});
|
|
591
597
|
var jobInputSchema = z2.object({
|
|
592
598
|
idea: z2.string().min(1).optional(),
|
|
@@ -619,9 +625,9 @@ function parseBoolean(value2) {
|
|
|
619
625
|
}
|
|
620
626
|
function readEnvSettings(env = process.env) {
|
|
621
627
|
return envSettingsSchema.parse({
|
|
622
|
-
openRouterApiKey: env.
|
|
623
|
-
replicateApiToken: env.
|
|
624
|
-
disableKeytar: parseBoolean(env.
|
|
628
|
+
openRouterApiKey: env.TELEPAT_OPENROUTER_KEY,
|
|
629
|
+
replicateApiToken: env.TELEPAT_REPLICATE_TOKEN,
|
|
630
|
+
disableKeytar: parseBoolean(env.TELEPAT_DISABLE_KEYTAR),
|
|
625
631
|
model: env.IDEON_MODEL,
|
|
626
632
|
temperature: parseNumber(env.IDEON_TEMPERATURE),
|
|
627
633
|
maxTokens: parseNumber(env.IDEON_MAX_TOKENS),
|
|
@@ -631,7 +637,13 @@ function readEnvSettings(env = process.env) {
|
|
|
631
637
|
notificationsEnabled: parseBoolean(env.IDEON_NOTIFICATIONS_ENABLED),
|
|
632
638
|
style: env.IDEON_STYLE,
|
|
633
639
|
intent: env.IDEON_INTENT,
|
|
634
|
-
targetLength: env.IDEON_TARGET_LENGTH
|
|
640
|
+
targetLength: env.IDEON_TARGET_LENGTH,
|
|
641
|
+
googleAdsDeveloperToken: env.TELEPAT_GOOGLE_ADS_DEVELOPER_TOKEN,
|
|
642
|
+
googleAdsClientId: env.TELEPAT_GOOGLE_ADS_CLIENT_ID,
|
|
643
|
+
googleAdsClientSecret: env.TELEPAT_GOOGLE_ADS_CLIENT_SECRET,
|
|
644
|
+
googleAdsRefreshToken: env.TELEPAT_GOOGLE_ADS_REFRESH_TOKEN,
|
|
645
|
+
googleAdsCustomerId: env.TELEPAT_GOOGLE_ADS_CUSTOMER_ID,
|
|
646
|
+
googleAdsLoginCustomerId: env.TELEPAT_GOOGLE_ADS_LOGIN_CUSTOMER_ID
|
|
635
647
|
});
|
|
636
648
|
}
|
|
637
649
|
|
|
@@ -666,6 +678,12 @@ async function saveSettings(settings) {
|
|
|
666
678
|
var SERVICE_NAME = "ideon";
|
|
667
679
|
var OPENROUTER_ACCOUNT = "openrouter-api-key";
|
|
668
680
|
var REPLICATE_ACCOUNT = "replicate-api-token";
|
|
681
|
+
var GOOGLE_ADS_DEVELOPER_TOKEN_ACCOUNT = "google-ads-developer-token";
|
|
682
|
+
var GOOGLE_ADS_CLIENT_ID_ACCOUNT = "google-ads-client-id";
|
|
683
|
+
var GOOGLE_ADS_CLIENT_SECRET_ACCOUNT = "google-ads-client-secret";
|
|
684
|
+
var GOOGLE_ADS_REFRESH_TOKEN_ACCOUNT = "google-ads-refresh-token";
|
|
685
|
+
var GOOGLE_ADS_CUSTOMER_ID_ACCOUNT = "google-ads-customer-id";
|
|
686
|
+
var GOOGLE_ADS_LOGIN_CUSTOMER_ID_ACCOUNT = "google-ads-login-customer-id";
|
|
669
687
|
var KEYTAR_UNAVAILABLE_ERROR_NAME = "KeytarUnavailableError";
|
|
670
688
|
var hasWarnedAboutUnavailableKeytar = false;
|
|
671
689
|
var keytarClientPromise = null;
|
|
@@ -679,7 +697,13 @@ var KeytarUnavailableError = class extends Error {
|
|
|
679
697
|
function nullSecrets() {
|
|
680
698
|
return {
|
|
681
699
|
openRouterApiKey: null,
|
|
682
|
-
replicateApiToken: null
|
|
700
|
+
replicateApiToken: null,
|
|
701
|
+
googleAdsDeveloperToken: null,
|
|
702
|
+
googleAdsClientId: null,
|
|
703
|
+
googleAdsClientSecret: null,
|
|
704
|
+
googleAdsRefreshToken: null,
|
|
705
|
+
googleAdsCustomerId: null,
|
|
706
|
+
googleAdsLoginCustomerId: null
|
|
683
707
|
};
|
|
684
708
|
}
|
|
685
709
|
function shouldDisableKeytar(options) {
|
|
@@ -725,7 +749,7 @@ function warnKeytarUnavailable(details) {
|
|
|
725
749
|
}
|
|
726
750
|
hasWarnedAboutUnavailableKeytar = true;
|
|
727
751
|
console.warn(
|
|
728
|
-
`System keychain unavailable (${details}). Falling back to environment variables for secrets. Set
|
|
752
|
+
`System keychain unavailable (${details}). Falling back to environment variables for secrets. Set TELEPAT_DISABLE_KEYTAR=true to skip keychain access in this environment.`
|
|
729
753
|
);
|
|
730
754
|
}
|
|
731
755
|
async function getKeytarClient() {
|
|
@@ -751,13 +775,34 @@ async function loadSecrets(options = {}) {
|
|
|
751
775
|
return nullSecrets();
|
|
752
776
|
}
|
|
753
777
|
try {
|
|
754
|
-
const [
|
|
778
|
+
const [
|
|
779
|
+
openRouterApiKey,
|
|
780
|
+
replicateApiToken,
|
|
781
|
+
googleAdsDeveloperToken,
|
|
782
|
+
googleAdsClientId,
|
|
783
|
+
googleAdsClientSecret,
|
|
784
|
+
googleAdsRefreshToken,
|
|
785
|
+
googleAdsCustomerId,
|
|
786
|
+
googleAdsLoginCustomerId
|
|
787
|
+
] = await Promise.all([
|
|
755
788
|
keytarClient.getPassword(SERVICE_NAME, OPENROUTER_ACCOUNT),
|
|
756
|
-
keytarClient.getPassword(SERVICE_NAME, REPLICATE_ACCOUNT)
|
|
789
|
+
keytarClient.getPassword(SERVICE_NAME, REPLICATE_ACCOUNT),
|
|
790
|
+
keytarClient.getPassword(SERVICE_NAME, GOOGLE_ADS_DEVELOPER_TOKEN_ACCOUNT),
|
|
791
|
+
keytarClient.getPassword(SERVICE_NAME, GOOGLE_ADS_CLIENT_ID_ACCOUNT),
|
|
792
|
+
keytarClient.getPassword(SERVICE_NAME, GOOGLE_ADS_CLIENT_SECRET_ACCOUNT),
|
|
793
|
+
keytarClient.getPassword(SERVICE_NAME, GOOGLE_ADS_REFRESH_TOKEN_ACCOUNT),
|
|
794
|
+
keytarClient.getPassword(SERVICE_NAME, GOOGLE_ADS_CUSTOMER_ID_ACCOUNT),
|
|
795
|
+
keytarClient.getPassword(SERVICE_NAME, GOOGLE_ADS_LOGIN_CUSTOMER_ID_ACCOUNT)
|
|
757
796
|
]);
|
|
758
797
|
return {
|
|
759
798
|
openRouterApiKey,
|
|
760
|
-
replicateApiToken
|
|
799
|
+
replicateApiToken,
|
|
800
|
+
googleAdsDeveloperToken,
|
|
801
|
+
googleAdsClientId,
|
|
802
|
+
googleAdsClientSecret,
|
|
803
|
+
googleAdsRefreshToken,
|
|
804
|
+
googleAdsCustomerId,
|
|
805
|
+
googleAdsLoginCustomerId
|
|
761
806
|
};
|
|
762
807
|
} catch (error) {
|
|
763
808
|
if (isKeytarAvailabilityError(error)) {
|
|
@@ -771,13 +816,13 @@ async function loadSecrets(options = {}) {
|
|
|
771
816
|
async function saveSecrets(secrets, options = {}) {
|
|
772
817
|
if (shouldDisableKeytar(options)) {
|
|
773
818
|
throw new KeytarUnavailableError(
|
|
774
|
-
"System keychain access is disabled by
|
|
819
|
+
"System keychain access is disabled by TELEPAT_DISABLE_KEYTAR=true. Use TELEPAT_OPENROUTER_KEY and TELEPAT_REPLICATE_TOKEN instead."
|
|
775
820
|
);
|
|
776
821
|
}
|
|
777
822
|
const keytarClient = await getKeytarClient();
|
|
778
823
|
if (!keytarClient) {
|
|
779
824
|
throw new KeytarUnavailableError(
|
|
780
|
-
`System keychain unavailable while saving credentials (${keytarUnavailableReason ?? "keytar module failed to load"}). Use
|
|
825
|
+
`System keychain unavailable while saving credentials (${keytarUnavailableReason ?? "keytar module failed to load"}). Use TELEPAT_OPENROUTER_KEY and TELEPAT_REPLICATE_TOKEN instead.`
|
|
781
826
|
);
|
|
782
827
|
}
|
|
783
828
|
const tasks = [];
|
|
@@ -787,6 +832,24 @@ async function saveSecrets(secrets, options = {}) {
|
|
|
787
832
|
if (secrets.replicateApiToken !== void 0) {
|
|
788
833
|
tasks.push(saveSecretValue(keytarClient, REPLICATE_ACCOUNT, secrets.replicateApiToken));
|
|
789
834
|
}
|
|
835
|
+
if (secrets.googleAdsDeveloperToken !== void 0) {
|
|
836
|
+
tasks.push(saveSecretValue(keytarClient, GOOGLE_ADS_DEVELOPER_TOKEN_ACCOUNT, secrets.googleAdsDeveloperToken));
|
|
837
|
+
}
|
|
838
|
+
if (secrets.googleAdsClientId !== void 0) {
|
|
839
|
+
tasks.push(saveSecretValue(keytarClient, GOOGLE_ADS_CLIENT_ID_ACCOUNT, secrets.googleAdsClientId));
|
|
840
|
+
}
|
|
841
|
+
if (secrets.googleAdsClientSecret !== void 0) {
|
|
842
|
+
tasks.push(saveSecretValue(keytarClient, GOOGLE_ADS_CLIENT_SECRET_ACCOUNT, secrets.googleAdsClientSecret));
|
|
843
|
+
}
|
|
844
|
+
if (secrets.googleAdsRefreshToken !== void 0) {
|
|
845
|
+
tasks.push(saveSecretValue(keytarClient, GOOGLE_ADS_REFRESH_TOKEN_ACCOUNT, secrets.googleAdsRefreshToken));
|
|
846
|
+
}
|
|
847
|
+
if (secrets.googleAdsCustomerId !== void 0) {
|
|
848
|
+
tasks.push(saveSecretValue(keytarClient, GOOGLE_ADS_CUSTOMER_ID_ACCOUNT, secrets.googleAdsCustomerId));
|
|
849
|
+
}
|
|
850
|
+
if (secrets.googleAdsLoginCustomerId !== void 0) {
|
|
851
|
+
tasks.push(saveSecretValue(keytarClient, GOOGLE_ADS_LOGIN_CUSTOMER_ID_ACCOUNT, secrets.googleAdsLoginCustomerId));
|
|
852
|
+
}
|
|
790
853
|
await Promise.all(tasks);
|
|
791
854
|
}
|
|
792
855
|
async function saveSecretValue(keytarClient, account, value2) {
|
|
@@ -800,7 +863,7 @@ async function saveSecretValue(keytarClient, account, value2) {
|
|
|
800
863
|
if (isKeytarAvailabilityError(error)) {
|
|
801
864
|
const message = readErrorMessage(error);
|
|
802
865
|
throw new KeytarUnavailableError(
|
|
803
|
-
`System keychain unavailable while saving credentials (${message}). Use
|
|
866
|
+
`System keychain unavailable while saving credentials (${message}). Use TELEPAT_OPENROUTER_KEY and TELEPAT_REPLICATE_TOKEN instead.`
|
|
804
867
|
);
|
|
805
868
|
}
|
|
806
869
|
throw error;
|
|
@@ -820,7 +883,16 @@ var configSettingKeys = [
|
|
|
820
883
|
"intent",
|
|
821
884
|
"targetLength"
|
|
822
885
|
];
|
|
823
|
-
var configSecretKeys = [
|
|
886
|
+
var configSecretKeys = [
|
|
887
|
+
"openRouterApiKey",
|
|
888
|
+
"replicateApiToken",
|
|
889
|
+
"googleAdsDeveloperToken",
|
|
890
|
+
"googleAdsClientId",
|
|
891
|
+
"googleAdsClientSecret",
|
|
892
|
+
"googleAdsRefreshToken",
|
|
893
|
+
"googleAdsCustomerId",
|
|
894
|
+
"googleAdsLoginCustomerId"
|
|
895
|
+
];
|
|
824
896
|
function isConfigSettingKey(key) {
|
|
825
897
|
return configSettingKeys.includes(key);
|
|
826
898
|
}
|
|
@@ -850,7 +922,13 @@ async function configList() {
|
|
|
850
922
|
},
|
|
851
923
|
secrets: {
|
|
852
924
|
openRouterApiKey: Boolean(secrets.openRouterApiKey),
|
|
853
|
-
replicateApiToken: Boolean(secrets.replicateApiToken)
|
|
925
|
+
replicateApiToken: Boolean(secrets.replicateApiToken),
|
|
926
|
+
googleAdsDeveloperToken: Boolean(secrets.googleAdsDeveloperToken),
|
|
927
|
+
googleAdsClientId: Boolean(secrets.googleAdsClientId),
|
|
928
|
+
googleAdsClientSecret: Boolean(secrets.googleAdsClientSecret),
|
|
929
|
+
googleAdsRefreshToken: Boolean(secrets.googleAdsRefreshToken),
|
|
930
|
+
googleAdsCustomerId: Boolean(secrets.googleAdsCustomerId),
|
|
931
|
+
googleAdsLoginCustomerId: Boolean(secrets.googleAdsLoginCustomerId)
|
|
854
932
|
}
|
|
855
933
|
};
|
|
856
934
|
}
|
|
@@ -1088,6 +1166,32 @@ var configUnsetToolInputSchema = {
|
|
|
1088
1166
|
key: z3.enum(configKeys)
|
|
1089
1167
|
};
|
|
1090
1168
|
var configUnsetToolInputZodSchema = z3.object(configUnsetToolInputSchema);
|
|
1169
|
+
var gkpGenerateIdeasToolInputSchema = {
|
|
1170
|
+
seedKeywords: z3.array(z3.string().min(1)).optional(),
|
|
1171
|
+
url: z3.string().min(1).optional(),
|
|
1172
|
+
site: z3.string().min(1).optional(),
|
|
1173
|
+
countryCodes: z3.array(z3.string().min(1)).optional(),
|
|
1174
|
+
language: z3.string().min(1).optional(),
|
|
1175
|
+
pageSize: z3.coerce.number().int().positive().optional()
|
|
1176
|
+
};
|
|
1177
|
+
var gkpGenerateIdeasToolInputZodSchema = z3.object(gkpGenerateIdeasToolInputSchema);
|
|
1178
|
+
var gkpGetHistoricalDataToolInputSchema = {
|
|
1179
|
+
keywords: z3.array(z3.string().min(1)).min(1),
|
|
1180
|
+
countryCodes: z3.array(z3.string().min(1)).optional(),
|
|
1181
|
+
language: z3.string().min(1).optional(),
|
|
1182
|
+
includeAverageCpc: z3.boolean().optional()
|
|
1183
|
+
};
|
|
1184
|
+
var gkpGetHistoricalDataToolInputZodSchema = z3.object(gkpGetHistoricalDataToolInputSchema);
|
|
1185
|
+
var gkpGetForecastDataToolInputSchema = {
|
|
1186
|
+
keywords: z3.array(z3.string().min(1)).min(1),
|
|
1187
|
+
keywordMatchType: z3.enum(["BROAD", "EXACT", "PHRASE"]).optional(),
|
|
1188
|
+
maxCpcBidMicros: z3.coerce.number().int().positive().optional(),
|
|
1189
|
+
countryCodes: z3.array(z3.string().min(1)).optional(),
|
|
1190
|
+
language: z3.string().min(1).optional(),
|
|
1191
|
+
startDate: z3.string().min(1).optional(),
|
|
1192
|
+
endDate: z3.string().min(1).optional()
|
|
1193
|
+
};
|
|
1194
|
+
var gkpGetForecastDataToolInputZodSchema = z3.object(gkpGetForecastDataToolInputSchema);
|
|
1091
1195
|
var ideonToolContracts = [
|
|
1092
1196
|
{
|
|
1093
1197
|
name: "ideon_write",
|
|
@@ -1145,6 +1249,23 @@ var ideonToolContracts = [
|
|
|
1145
1249
|
enums: {
|
|
1146
1250
|
key: [...configKeys]
|
|
1147
1251
|
}
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
name: "gkp_generate_ideas",
|
|
1255
|
+
required: [],
|
|
1256
|
+
enums: {}
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
name: "gkp_get_historical_data",
|
|
1260
|
+
required: ["keywords"],
|
|
1261
|
+
enums: {}
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
name: "gkp_get_forecast_data",
|
|
1265
|
+
required: ["keywords"],
|
|
1266
|
+
enums: {
|
|
1267
|
+
keywordMatchType: ["BROAD", "EXACT", "PHRASE"]
|
|
1268
|
+
}
|
|
1148
1269
|
}
|
|
1149
1270
|
];
|
|
1150
1271
|
|
|
@@ -1428,7 +1549,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1428
1549
|
// package.json
|
|
1429
1550
|
var package_default = {
|
|
1430
1551
|
name: "@telepat/ideon",
|
|
1431
|
-
version: "0.1.
|
|
1552
|
+
version: "0.1.33",
|
|
1432
1553
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1433
1554
|
type: "module",
|
|
1434
1555
|
repository: {
|
|
@@ -1535,17 +1656,17 @@ var package_default = {
|
|
|
1535
1656
|
|
|
1536
1657
|
// src/config/resolver.ts
|
|
1537
1658
|
import { readFile as readFile3 } from "fs/promises";
|
|
1538
|
-
async function resolveRunInput(
|
|
1659
|
+
async function resolveRunInput(input2) {
|
|
1539
1660
|
const envSettings = readEnvSettings();
|
|
1540
1661
|
const [savedSettings, secrets] = await Promise.all([
|
|
1541
1662
|
loadSavedSettings(),
|
|
1542
1663
|
loadSecrets({ disableKeytar: envSettings.disableKeytar })
|
|
1543
1664
|
]);
|
|
1544
|
-
const job =
|
|
1665
|
+
const job = input2.jobPath ? await loadJobInput(input2.jobPath) : null;
|
|
1545
1666
|
assertNoLegacyXMode(savedSettings.contentTargets, "saved settings contentTargets");
|
|
1546
1667
|
assertNoLegacyXMode(job?.settings?.contentTargets, "job settings contentTargets");
|
|
1547
|
-
assertNoLegacyXMode(
|
|
1548
|
-
assertExactlyOnePrimary(
|
|
1668
|
+
assertNoLegacyXMode(input2.contentTargets, "CLI contentTargets");
|
|
1669
|
+
assertExactlyOnePrimary(input2.contentTargets, "CLI contentTargets");
|
|
1549
1670
|
const mergedSettings = appSettingsSchema.parse({
|
|
1550
1671
|
...savedSettings,
|
|
1551
1672
|
...job?.settings ?? {},
|
|
@@ -1571,22 +1692,28 @@ async function resolveRunInput(input) {
|
|
|
1571
1692
|
...envSettings.style ? { style: envSettings.style } : {},
|
|
1572
1693
|
...envSettings.intent ? { intent: envSettings.intent } : {},
|
|
1573
1694
|
...envSettings.targetLength ? { targetLength: envSettings.targetLength } : {},
|
|
1574
|
-
...
|
|
1575
|
-
...
|
|
1576
|
-
...
|
|
1577
|
-
...
|
|
1695
|
+
...input2.style ? { style: input2.style } : {},
|
|
1696
|
+
...input2.intent ? { intent: input2.intent } : {},
|
|
1697
|
+
...input2.targetLength ? { targetLength: input2.targetLength } : {},
|
|
1698
|
+
...input2.contentTargets ? { contentTargets: input2.contentTargets } : {}
|
|
1578
1699
|
});
|
|
1579
|
-
const idea =
|
|
1700
|
+
const idea = input2.idea ?? job?.idea ?? job?.prompt;
|
|
1580
1701
|
if (!idea) {
|
|
1581
1702
|
throw new Error("No idea provided. Pass an argument to `ideon write` or use --job with an idea in the JSON file.");
|
|
1582
1703
|
}
|
|
1583
|
-
const targetAudienceHint = normalizeOptionalText(
|
|
1704
|
+
const targetAudienceHint = normalizeOptionalText(input2.audience) ?? normalizeOptionalText(job?.targetAudience);
|
|
1584
1705
|
return {
|
|
1585
1706
|
config: {
|
|
1586
1707
|
settings: mergedSettings,
|
|
1587
1708
|
secrets: {
|
|
1588
1709
|
openRouterApiKey: envSettings.openRouterApiKey ?? secrets.openRouterApiKey,
|
|
1589
|
-
replicateApiToken: envSettings.replicateApiToken ?? secrets.replicateApiToken
|
|
1710
|
+
replicateApiToken: envSettings.replicateApiToken ?? secrets.replicateApiToken,
|
|
1711
|
+
googleAdsDeveloperToken: envSettings.googleAdsDeveloperToken ?? secrets.googleAdsDeveloperToken,
|
|
1712
|
+
googleAdsClientId: envSettings.googleAdsClientId ?? secrets.googleAdsClientId,
|
|
1713
|
+
googleAdsClientSecret: envSettings.googleAdsClientSecret ?? secrets.googleAdsClientSecret,
|
|
1714
|
+
googleAdsRefreshToken: envSettings.googleAdsRefreshToken ?? secrets.googleAdsRefreshToken,
|
|
1715
|
+
googleAdsCustomerId: envSettings.googleAdsCustomerId ?? secrets.googleAdsCustomerId,
|
|
1716
|
+
googleAdsLoginCustomerId: envSettings.googleAdsLoginCustomerId ?? secrets.googleAdsLoginCustomerId
|
|
1590
1717
|
}
|
|
1591
1718
|
},
|
|
1592
1719
|
idea,
|
|
@@ -3264,13 +3391,13 @@ async function withRetry(op, opts) {
|
|
|
3264
3391
|
}
|
|
3265
3392
|
throw buildFinalError(opts.operationLabel, attemptsMade, lastError, lastClassification);
|
|
3266
3393
|
}
|
|
3267
|
-
function computeDelayMs(
|
|
3268
|
-
if (typeof
|
|
3269
|
-
return Math.min(
|
|
3394
|
+
function computeDelayMs(input2) {
|
|
3395
|
+
if (typeof input2.retryAfterMs === "number" && input2.retryAfterMs > 0) {
|
|
3396
|
+
return Math.min(input2.maxBackoffMs, input2.retryAfterMs);
|
|
3270
3397
|
}
|
|
3271
|
-
const exponential =
|
|
3272
|
-
const capped = Math.min(
|
|
3273
|
-
const jitter =
|
|
3398
|
+
const exponential = input2.baseBackoffMs * 2 ** (input2.attempt - 1);
|
|
3399
|
+
const capped = Math.min(input2.maxBackoffMs, exponential);
|
|
3400
|
+
const jitter = input2.jitterMs > 0 ? input2.randomFraction() * input2.jitterMs : 0;
|
|
3274
3401
|
return Math.floor(capped + jitter);
|
|
3275
3402
|
}
|
|
3276
3403
|
function classifyHttpError(error) {
|
|
@@ -4328,18 +4455,18 @@ ${body.join("\n").trim()}
|
|
|
4328
4455
|
|
|
4329
4456
|
// src/output/meta.ts
|
|
4330
4457
|
import path7 from "path";
|
|
4331
|
-
function buildMetaJson(
|
|
4332
|
-
const plan =
|
|
4333
|
-
const contentPlan =
|
|
4334
|
-
const generationDir =
|
|
4335
|
-
const title = plan?.title ?? contentPlan?.title ??
|
|
4458
|
+
function buildMetaJson(input2) {
|
|
4459
|
+
const plan = input2.plan;
|
|
4460
|
+
const contentPlan = input2.contentPlan;
|
|
4461
|
+
const generationDir = input2.generationDir;
|
|
4462
|
+
const title = plan?.title ?? contentPlan?.title ?? input2.idea;
|
|
4336
4463
|
const slug = plan?.slug ?? "";
|
|
4337
4464
|
const description = plan?.description ?? contentPlan?.description ?? "";
|
|
4338
4465
|
const subtitle = (plan && "subtitle" in plan ? plan.subtitle : null) ?? null;
|
|
4339
4466
|
const keywords = (plan && "keywords" in plan ? plan.keywords : null) ?? [];
|
|
4340
4467
|
const contentType = plan?.contentType ?? contentPlan?.primaryContentType ?? "article";
|
|
4341
4468
|
const angle = plan?.angle ?? null;
|
|
4342
|
-
const coverImage =
|
|
4469
|
+
const coverImage = input2.renderedImages.find((image) => image.kind === "cover") ?? null;
|
|
4343
4470
|
const cover = coverImage ? {
|
|
4344
4471
|
path: coverImage.outputPath,
|
|
4345
4472
|
relativePath: coverImage.relativePath,
|
|
@@ -4349,7 +4476,7 @@ function buildMetaJson(input) {
|
|
|
4349
4476
|
title: section.title,
|
|
4350
4477
|
description: section.description
|
|
4351
4478
|
})) ?? [];
|
|
4352
|
-
const images =
|
|
4479
|
+
const images = input2.renderedImages.map((image) => ({
|
|
4353
4480
|
id: image.id,
|
|
4354
4481
|
kind: image.kind,
|
|
4355
4482
|
path: image.outputPath,
|
|
@@ -4357,30 +4484,30 @@ function buildMetaJson(input) {
|
|
|
4357
4484
|
description: image.description,
|
|
4358
4485
|
anchorAfterSection: image.anchorAfterSection
|
|
4359
4486
|
}));
|
|
4360
|
-
const outputs =
|
|
4361
|
-
fileId:
|
|
4362
|
-
contentType:
|
|
4363
|
-
path:
|
|
4364
|
-
relativePath: path7.relative(generationDir,
|
|
4487
|
+
const outputs = input2.outputs.map((output2) => ({
|
|
4488
|
+
fileId: output2.fileId,
|
|
4489
|
+
contentType: output2.contentType,
|
|
4490
|
+
path: output2.markdownPath,
|
|
4491
|
+
relativePath: path7.relative(generationDir, output2.markdownPath)
|
|
4365
4492
|
}));
|
|
4366
4493
|
return {
|
|
4367
4494
|
version: 1,
|
|
4368
4495
|
title,
|
|
4369
4496
|
slug,
|
|
4370
|
-
idea:
|
|
4497
|
+
idea: input2.idea,
|
|
4371
4498
|
description,
|
|
4372
4499
|
subtitle,
|
|
4373
4500
|
keywords,
|
|
4374
4501
|
contentType,
|
|
4375
|
-
style:
|
|
4376
|
-
intent:
|
|
4377
|
-
targetLength:
|
|
4502
|
+
style: input2.style,
|
|
4503
|
+
intent: input2.intent,
|
|
4504
|
+
targetLength: input2.targetLength,
|
|
4378
4505
|
angle,
|
|
4379
4506
|
cover,
|
|
4380
4507
|
sections,
|
|
4381
4508
|
images,
|
|
4382
4509
|
outputs,
|
|
4383
|
-
generatedAt:
|
|
4510
|
+
generatedAt: input2.generatedAt,
|
|
4384
4511
|
generationDir
|
|
4385
4512
|
};
|
|
4386
4513
|
}
|
|
@@ -4619,12 +4746,12 @@ function createInitialStages() {
|
|
|
4619
4746
|
}
|
|
4620
4747
|
];
|
|
4621
4748
|
}
|
|
4622
|
-
async function runPipelineShell(
|
|
4749
|
+
async function runPipelineShell(input2, options = {}) {
|
|
4623
4750
|
const runStartedAtMs = Date.now();
|
|
4624
4751
|
const runStartedAt = new Date(runStartedAtMs).toISOString();
|
|
4625
4752
|
const runId = randomUUID();
|
|
4626
|
-
const primaryTarget = getPrimaryTarget(
|
|
4627
|
-
const secondaryTargets = getSecondaryTargets(
|
|
4753
|
+
const primaryTarget = getPrimaryTarget(input2.config.settings.contentTargets);
|
|
4754
|
+
const secondaryTargets = getSecondaryTargets(input2.config.settings.contentTargets);
|
|
4628
4755
|
const stages = createInitialStages();
|
|
4629
4756
|
options.onUpdate?.(cloneStages(stages));
|
|
4630
4757
|
const dryRun = options.dryRun ?? false;
|
|
@@ -4690,10 +4817,10 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4690
4817
|
if (runMode === "fresh") {
|
|
4691
4818
|
writeSession = await startFreshWriteSession(
|
|
4692
4819
|
{
|
|
4693
|
-
idea:
|
|
4694
|
-
targetAudienceHint:
|
|
4695
|
-
job:
|
|
4696
|
-
settings:
|
|
4820
|
+
idea: input2.idea,
|
|
4821
|
+
targetAudienceHint: input2.targetAudienceHint,
|
|
4822
|
+
job: input2.job,
|
|
4823
|
+
settings: input2.config.settings,
|
|
4697
4824
|
dryRun,
|
|
4698
4825
|
outputPaths
|
|
4699
4826
|
},
|
|
@@ -4710,13 +4837,13 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4710
4837
|
}
|
|
4711
4838
|
try {
|
|
4712
4839
|
await ensureOutputDirectories(writeSession.outputPaths);
|
|
4713
|
-
const openRouter = dryRun ? null : new OpenRouterClient(requireSecret(
|
|
4714
|
-
const canRenderImagesLive = Boolean(
|
|
4840
|
+
const openRouter = dryRun ? null : new OpenRouterClient(requireSecret(input2.config.secrets.openRouterApiKey, "OpenRouter API key"));
|
|
4841
|
+
const canRenderImagesLive = Boolean(input2.config.secrets.replicateApiToken);
|
|
4715
4842
|
const imageDryRun = dryRun || !canRenderImagesLive;
|
|
4716
4843
|
const limn = imageDryRun ? null : new Limn({
|
|
4717
|
-
openrouterApiKey:
|
|
4718
|
-
replicateApiKey: requireSecret(
|
|
4719
|
-
openrouterModel:
|
|
4844
|
+
openrouterApiKey: input2.config.secrets.openRouterApiKey ?? void 0,
|
|
4845
|
+
replicateApiKey: requireSecret(input2.config.secrets.replicateApiToken, "Replicate API token"),
|
|
4846
|
+
openrouterModel: input2.config.settings.model
|
|
4720
4847
|
});
|
|
4721
4848
|
let contentPlan = writeSession.contentPlan;
|
|
4722
4849
|
let plan = writeSession.plan;
|
|
@@ -4736,9 +4863,9 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4736
4863
|
};
|
|
4737
4864
|
} else {
|
|
4738
4865
|
contentPlan = await planContentPlan({
|
|
4739
|
-
idea:
|
|
4740
|
-
targetAudienceHint:
|
|
4741
|
-
settings:
|
|
4866
|
+
idea: input2.idea,
|
|
4867
|
+
targetAudienceHint: input2.targetAudienceHint,
|
|
4868
|
+
settings: input2.config.settings,
|
|
4742
4869
|
openRouter,
|
|
4743
4870
|
dryRun,
|
|
4744
4871
|
onInteraction(interaction) {
|
|
@@ -4788,10 +4915,10 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4788
4915
|
throw new Error("Shared content plan is missing for primary planning stage.");
|
|
4789
4916
|
}
|
|
4790
4917
|
plan = await planPrimaryContent({
|
|
4791
|
-
idea:
|
|
4918
|
+
idea: input2.idea,
|
|
4792
4919
|
contentType: primaryTarget.contentType,
|
|
4793
4920
|
contentPlan,
|
|
4794
|
-
settings:
|
|
4921
|
+
settings: input2.config.settings,
|
|
4795
4922
|
markdownOutputDir: writeSession.outputPaths.markdownOutputDir,
|
|
4796
4923
|
openRouter,
|
|
4797
4924
|
dryRun,
|
|
@@ -4858,7 +4985,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4858
4985
|
const sectionItemTracking = /* @__PURE__ */ new Map();
|
|
4859
4986
|
text = await writeArticleSections({
|
|
4860
4987
|
plan: longPlan,
|
|
4861
|
-
settings:
|
|
4988
|
+
settings: input2.config.settings,
|
|
4862
4989
|
openRouter,
|
|
4863
4990
|
dryRun,
|
|
4864
4991
|
onInteraction(interaction) {
|
|
@@ -4973,7 +5100,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4973
5100
|
slots: buildImageSlots(longPlan, text.sections, { maxImages: options.maxImages }),
|
|
4974
5101
|
planContext: longPlan,
|
|
4975
5102
|
sections: text.sections,
|
|
4976
|
-
settings:
|
|
5103
|
+
settings: input2.config.settings,
|
|
4977
5104
|
openRouter,
|
|
4978
5105
|
dryRun,
|
|
4979
5106
|
onInteraction(interaction) {
|
|
@@ -5040,18 +5167,18 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5040
5167
|
markStageStarted(stageTracking, "sections");
|
|
5041
5168
|
options.onUpdate?.(cloneStages(stages));
|
|
5042
5169
|
primaryMarkdownTemplate = await writeSingleShotContent({
|
|
5043
|
-
idea:
|
|
5170
|
+
idea: input2.idea,
|
|
5044
5171
|
contentType: primaryTarget.contentType,
|
|
5045
5172
|
role: "primary",
|
|
5046
5173
|
primaryContentType: primaryTarget.contentType,
|
|
5047
|
-
style:
|
|
5048
|
-
intent:
|
|
5174
|
+
style: input2.config.settings.style,
|
|
5175
|
+
intent: input2.config.settings.intent,
|
|
5049
5176
|
outputIndex: 1,
|
|
5050
5177
|
outputCountForType: 1,
|
|
5051
5178
|
articleReferenceMarkdown: void 0,
|
|
5052
5179
|
contentPlan,
|
|
5053
5180
|
plan,
|
|
5054
|
-
settings:
|
|
5181
|
+
settings: input2.config.settings,
|
|
5055
5182
|
openRouter,
|
|
5056
5183
|
dryRun,
|
|
5057
5184
|
onInteraction(interaction) {
|
|
@@ -5098,7 +5225,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5098
5225
|
};
|
|
5099
5226
|
options.onUpdate?.(cloneStages(stages));
|
|
5100
5227
|
}
|
|
5101
|
-
const baseSlug = plan?.slug ?? resolveGenerationSlug(
|
|
5228
|
+
const baseSlug = plan?.slug ?? resolveGenerationSlug(input2.idea, contentPlan?.title);
|
|
5102
5229
|
const generationDir = path9.join(
|
|
5103
5230
|
writeSession.outputPaths.markdownOutputDir,
|
|
5104
5231
|
buildGenerationDirectoryName(baseSlug)
|
|
@@ -5108,12 +5235,12 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5108
5235
|
await writeJsonFile(
|
|
5109
5236
|
jobDefinitionPath,
|
|
5110
5237
|
buildRunJobDefinition({
|
|
5111
|
-
idea:
|
|
5112
|
-
targetAudienceHint:
|
|
5238
|
+
idea: input2.idea,
|
|
5239
|
+
targetAudienceHint: input2.targetAudienceHint,
|
|
5113
5240
|
dryRun,
|
|
5114
5241
|
runMode,
|
|
5115
|
-
settings:
|
|
5116
|
-
sourceJob:
|
|
5242
|
+
settings: input2.config.settings,
|
|
5243
|
+
sourceJob: input2.job
|
|
5117
5244
|
})
|
|
5118
5245
|
);
|
|
5119
5246
|
const planPath = plan ? path9.join(generationDir, "plan.md") : null;
|
|
@@ -5140,7 +5267,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5140
5267
|
}
|
|
5141
5268
|
const renderedImages = await renderExpandedImages({
|
|
5142
5269
|
prompts: imagePrompts,
|
|
5143
|
-
settings:
|
|
5270
|
+
settings: input2.config.settings,
|
|
5144
5271
|
limn,
|
|
5145
5272
|
markdownPath: primaryMarkdownPath,
|
|
5146
5273
|
assetDir: sharedAssetDir,
|
|
@@ -5224,7 +5351,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5224
5351
|
} else {
|
|
5225
5352
|
const renderedImages = await renderExpandedImages({
|
|
5226
5353
|
prompts: imagePrompts,
|
|
5227
|
-
settings:
|
|
5354
|
+
settings: input2.config.settings,
|
|
5228
5355
|
limn,
|
|
5229
5356
|
markdownPath: primaryMarkdownPath,
|
|
5230
5357
|
assetDir: sharedAssetDir,
|
|
@@ -5285,11 +5412,11 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5285
5412
|
}
|
|
5286
5413
|
const coverImage = imageArtifacts?.renderedImages.find((image) => image.kind === "cover") ?? null;
|
|
5287
5414
|
if (coverImage) {
|
|
5288
|
-
primaryMarkdownTemplate = withCoverImage(primaryMarkdownTemplate, coverImage.relativePath, plan.title || deriveTitleFromIdea2(
|
|
5415
|
+
primaryMarkdownTemplate = withCoverImage(primaryMarkdownTemplate, coverImage.relativePath, plan.title || deriveTitleFromIdea2(input2.idea));
|
|
5289
5416
|
}
|
|
5290
5417
|
primaryMarkdownTemplate = applyPrimaryTitleHeading(
|
|
5291
5418
|
primaryMarkdownTemplate,
|
|
5292
|
-
plan.title || contentPlan.title || deriveTitleFromIdea2(
|
|
5419
|
+
plan.title || contentPlan.title || deriveTitleFromIdea2(input2.idea)
|
|
5293
5420
|
);
|
|
5294
5421
|
}
|
|
5295
5422
|
const markdownPaths = [];
|
|
@@ -5310,9 +5437,9 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5310
5437
|
...stages[5],
|
|
5311
5438
|
status: "running",
|
|
5312
5439
|
detail: requestedOutputs.length > 0 ? "Generating secondary channel outputs from primary anchor content." : "No secondary content requested.",
|
|
5313
|
-
items: requestedOutputs.map((
|
|
5314
|
-
id: toOutputItemId(
|
|
5315
|
-
label: formatOutputItemLabel(
|
|
5440
|
+
items: requestedOutputs.map((output2) => ({
|
|
5441
|
+
id: toOutputItemId(output2.filePrefix, output2.index),
|
|
5442
|
+
label: formatOutputItemLabel(output2.contentType, output2.index, output2.outputCountForType),
|
|
5316
5443
|
status: "pending",
|
|
5317
5444
|
detail: "Waiting to start."
|
|
5318
5445
|
}))
|
|
@@ -5322,8 +5449,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5322
5449
|
if (!contentPlan) {
|
|
5323
5450
|
throw new Error("Shared content plan is missing for output generation stage.");
|
|
5324
5451
|
}
|
|
5325
|
-
for (const
|
|
5326
|
-
const itemId = toOutputItemId(
|
|
5452
|
+
for (const output2 of requestedOutputs) {
|
|
5453
|
+
const itemId = toOutputItemId(output2.filePrefix, output2.index);
|
|
5327
5454
|
const itemStartedAtMs = Date.now();
|
|
5328
5455
|
const itemTracking = {
|
|
5329
5456
|
retries: 0,
|
|
@@ -5332,7 +5459,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5332
5459
|
};
|
|
5333
5460
|
stages[5] = {
|
|
5334
5461
|
...stages[5],
|
|
5335
|
-
detail: `Generating ${formatOutputItemLabel(
|
|
5462
|
+
detail: `Generating ${formatOutputItemLabel(output2.contentType, output2.index, output2.outputCountForType)}.`,
|
|
5336
5463
|
items: (stages[5].items ?? []).map((item) => {
|
|
5337
5464
|
if (item.id !== itemId) {
|
|
5338
5465
|
return item;
|
|
@@ -5345,19 +5472,19 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5345
5472
|
})
|
|
5346
5473
|
};
|
|
5347
5474
|
options.onUpdate?.(cloneStages(stages));
|
|
5348
|
-
const markdownPath = path9.join(generationDir, `${
|
|
5475
|
+
const markdownPath = path9.join(generationDir, `${output2.filePrefix}-${output2.index}.md`);
|
|
5349
5476
|
try {
|
|
5350
5477
|
const content = await writeSingleShotContent({
|
|
5351
|
-
idea:
|
|
5352
|
-
contentType:
|
|
5353
|
-
style:
|
|
5354
|
-
intent:
|
|
5355
|
-
outputIndex:
|
|
5356
|
-
outputCountForType:
|
|
5478
|
+
idea: input2.idea,
|
|
5479
|
+
contentType: output2.contentType,
|
|
5480
|
+
style: input2.config.settings.style,
|
|
5481
|
+
intent: input2.config.settings.intent,
|
|
5482
|
+
outputIndex: output2.index,
|
|
5483
|
+
outputCountForType: output2.outputCountForType,
|
|
5357
5484
|
articleReferenceMarkdown: primaryMarkdownTemplate ?? void 0,
|
|
5358
5485
|
contentPlan,
|
|
5359
5486
|
plan,
|
|
5360
|
-
settings:
|
|
5487
|
+
settings: input2.config.settings,
|
|
5361
5488
|
openRouter,
|
|
5362
5489
|
dryRun,
|
|
5363
5490
|
role: "secondary",
|
|
@@ -5374,13 +5501,13 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5374
5501
|
}
|
|
5375
5502
|
});
|
|
5376
5503
|
if (!content) {
|
|
5377
|
-
throw new Error(`Generated empty content for ${
|
|
5504
|
+
throw new Error(`Generated empty content for ${output2.contentType} output ${output2.index}.`);
|
|
5378
5505
|
}
|
|
5379
5506
|
markdownPaths.push(markdownPath);
|
|
5380
5507
|
await writeUtf8File(markdownPath, content);
|
|
5381
5508
|
generatedOutputs.push({
|
|
5382
5509
|
fileId: itemId,
|
|
5383
|
-
contentType:
|
|
5510
|
+
contentType: output2.contentType,
|
|
5384
5511
|
markdownPath
|
|
5385
5512
|
});
|
|
5386
5513
|
const itemDurationMs = Date.now() - itemStartedAtMs;
|
|
@@ -5388,10 +5515,10 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5388
5515
|
const itemCostSource = chooseStageCostSource(itemTracking.costSources, knownItemCost.source);
|
|
5389
5516
|
outputItemCalls.push({
|
|
5390
5517
|
itemId,
|
|
5391
|
-
contentType:
|
|
5392
|
-
filePrefix:
|
|
5393
|
-
index:
|
|
5394
|
-
outputCountForType:
|
|
5518
|
+
contentType: output2.contentType,
|
|
5519
|
+
filePrefix: output2.filePrefix,
|
|
5520
|
+
index: output2.index,
|
|
5521
|
+
outputCountForType: output2.outputCountForType,
|
|
5395
5522
|
durationMs: itemDurationMs,
|
|
5396
5523
|
retries: itemTracking.retries,
|
|
5397
5524
|
costUsd: knownItemCost.usd,
|
|
@@ -5399,7 +5526,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5399
5526
|
});
|
|
5400
5527
|
stages[5] = {
|
|
5401
5528
|
...stages[5],
|
|
5402
|
-
detail: `Completed ${formatOutputItemLabel(
|
|
5529
|
+
detail: `Completed ${formatOutputItemLabel(output2.contentType, output2.index, output2.outputCountForType)}.`,
|
|
5403
5530
|
items: (stages[5].items ?? []).map((item) => {
|
|
5404
5531
|
if (item.id !== itemId) {
|
|
5405
5532
|
return item;
|
|
@@ -5445,14 +5572,14 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5445
5572
|
stageAnalytics: snapshotStageAnalytics(stageTracking, "output")
|
|
5446
5573
|
};
|
|
5447
5574
|
options.onUpdate?.(cloneStages(stages));
|
|
5448
|
-
const eligibleOutputsForLinks = generatedOutputs.filter((
|
|
5575
|
+
const eligibleOutputsForLinks = generatedOutputs.filter((output2) => output2.contentType !== "x-post" && output2.contentType !== "x-thread");
|
|
5449
5576
|
stages[6] = {
|
|
5450
5577
|
...stages[6],
|
|
5451
5578
|
status: "running",
|
|
5452
5579
|
detail: "Selecting expressions and resolving source URLs.",
|
|
5453
|
-
items: eligibleOutputsForLinks.map((
|
|
5454
|
-
id:
|
|
5455
|
-
label:
|
|
5580
|
+
items: eligibleOutputsForLinks.map((output2) => ({
|
|
5581
|
+
id: output2.fileId,
|
|
5582
|
+
label: output2.fileId,
|
|
5456
5583
|
status: "pending",
|
|
5457
5584
|
detail: "Waiting to start."
|
|
5458
5585
|
}))
|
|
@@ -5462,15 +5589,15 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5462
5589
|
if (!shouldEnrichLinks) {
|
|
5463
5590
|
const customLinkActions = pipelineCustomLinkRaws.length > 0 || pipelineUnlinks.length > 0;
|
|
5464
5591
|
if (customLinkActions && eligibleOutputsForLinks.length > 0) {
|
|
5465
|
-
for (const
|
|
5466
|
-
const existingLinks = await readExistingLinks(resolveLinksPath(
|
|
5592
|
+
for (const output2 of eligibleOutputsForLinks) {
|
|
5593
|
+
const existingLinks = await readExistingLinks(resolveLinksPath(output2.markdownPath));
|
|
5467
5594
|
const mergedCustomLinks = resolvePipelineCustomLinks(
|
|
5468
5595
|
existingLinks?.customLinks ?? [],
|
|
5469
5596
|
pipelineCustomLinkRaws,
|
|
5470
5597
|
pipelineUnlinks
|
|
5471
5598
|
);
|
|
5472
5599
|
const generatedLinks = existingLinks?.links ?? [];
|
|
5473
|
-
await writeLinksFile(
|
|
5600
|
+
await writeLinksFile(output2.markdownPath, {
|
|
5474
5601
|
version: 2,
|
|
5475
5602
|
customLinks: mergedCustomLinks,
|
|
5476
5603
|
links: generatedLinks
|
|
@@ -5514,12 +5641,12 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5514
5641
|
} else if (linksResult) {
|
|
5515
5642
|
const linksByFileId = new Map(linksResult.map((item) => [item.fileId, item.links]));
|
|
5516
5643
|
const customLinksByFileId = new Map(linksResult.map((item) => [item.fileId, item.customLinks]));
|
|
5517
|
-
const resumedLinks = eligibleOutputsForLinks.map((
|
|
5518
|
-
fileId:
|
|
5519
|
-
contentType:
|
|
5520
|
-
markdownPath:
|
|
5521
|
-
links: linksByFileId.get(
|
|
5522
|
-
customLinks: customLinksByFileId.get(
|
|
5644
|
+
const resumedLinks = eligibleOutputsForLinks.map((output2) => ({
|
|
5645
|
+
fileId: output2.fileId,
|
|
5646
|
+
contentType: output2.contentType,
|
|
5647
|
+
markdownPath: output2.markdownPath,
|
|
5648
|
+
links: linksByFileId.get(output2.fileId) ?? [],
|
|
5649
|
+
customLinks: customLinksByFileId.get(output2.fileId) ?? []
|
|
5523
5650
|
}));
|
|
5524
5651
|
linksResult = resumedLinks;
|
|
5525
5652
|
for (const item of resumedLinks) {
|
|
@@ -5547,13 +5674,13 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5547
5674
|
const itemTracking = /* @__PURE__ */ new Map();
|
|
5548
5675
|
linksResult = await enrichLinks({
|
|
5549
5676
|
markdownFiles: eligibleOutputsForLinks,
|
|
5550
|
-
articleTitle: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(
|
|
5677
|
+
articleTitle: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input2.idea),
|
|
5551
5678
|
articleDescription: plan?.description ?? contentPlan.description,
|
|
5552
5679
|
openRouter,
|
|
5553
|
-
settings:
|
|
5680
|
+
settings: input2.config.settings,
|
|
5554
5681
|
dryRun,
|
|
5555
5682
|
customLinks: parsePipelineCustomLinks(pipelineCustomLinkRaws, pipelineUnlinks),
|
|
5556
|
-
maxLinks: pipelineMaxLinks ?? resolveDefaultMaxLinks(
|
|
5683
|
+
maxLinks: pipelineMaxLinks ?? resolveDefaultMaxLinks(input2.config.settings.targetLength),
|
|
5557
5684
|
onInteraction(interaction) {
|
|
5558
5685
|
onLlmInteraction(interaction);
|
|
5559
5686
|
},
|
|
@@ -5660,23 +5787,23 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5660
5787
|
await writeJsonFile(analyticsPath, analytics);
|
|
5661
5788
|
await writeJsonFile(interactionsPath, interactions);
|
|
5662
5789
|
const metaJson = buildMetaJson({
|
|
5663
|
-
idea:
|
|
5790
|
+
idea: input2.idea,
|
|
5664
5791
|
generationDir,
|
|
5665
5792
|
contentPlan,
|
|
5666
5793
|
plan,
|
|
5667
5794
|
renderedImages: imageArtifacts?.renderedImages ?? [],
|
|
5668
5795
|
outputs: generatedOutputs,
|
|
5669
5796
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5670
|
-
style:
|
|
5671
|
-
intent:
|
|
5672
|
-
targetLength:
|
|
5797
|
+
style: input2.config.settings.style,
|
|
5798
|
+
intent: input2.config.settings.intent,
|
|
5799
|
+
targetLength: input2.config.settings.targetLength ? resolveTargetLengthAlias(input2.config.settings.targetLength) : null
|
|
5673
5800
|
});
|
|
5674
5801
|
const metaJsonPath = path9.join(generationDir, "meta.json");
|
|
5675
5802
|
await writeJsonFile(metaJsonPath, metaJson);
|
|
5676
5803
|
const primaryMarkdownPathForArtifact = markdownPaths[0] ?? primaryMarkdownPath;
|
|
5677
5804
|
const artifact = {
|
|
5678
|
-
title: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(
|
|
5679
|
-
slug: plan?.slug ?? resolveGenerationSlug(
|
|
5805
|
+
title: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input2.idea),
|
|
5806
|
+
slug: plan?.slug ?? resolveGenerationSlug(input2.idea, contentPlan?.title),
|
|
5680
5807
|
sectionCount: text?.sections.length ?? 0,
|
|
5681
5808
|
imageCount: imageArtifacts?.renderedImages.length ?? 0,
|
|
5682
5809
|
outputCount: markdownPaths.length,
|
|
@@ -6062,19 +6189,19 @@ function resolveGenerationSlug(idea, planTitle) {
|
|
|
6062
6189
|
}
|
|
6063
6190
|
return slugifyIdea(idea, 80);
|
|
6064
6191
|
}
|
|
6065
|
-
function buildRunJobDefinition(
|
|
6192
|
+
function buildRunJobDefinition(input2) {
|
|
6066
6193
|
return {
|
|
6067
|
-
idea:
|
|
6068
|
-
prompt:
|
|
6069
|
-
...
|
|
6070
|
-
contentTargets:
|
|
6071
|
-
style:
|
|
6072
|
-
settings:
|
|
6073
|
-
sourceJob:
|
|
6194
|
+
idea: input2.idea,
|
|
6195
|
+
prompt: input2.idea,
|
|
6196
|
+
...input2.targetAudienceHint ? { targetAudience: input2.targetAudienceHint } : {},
|
|
6197
|
+
contentTargets: input2.settings.contentTargets,
|
|
6198
|
+
style: input2.settings.style,
|
|
6199
|
+
settings: input2.settings,
|
|
6200
|
+
sourceJob: input2.sourceJob,
|
|
6074
6201
|
runMetadata: {
|
|
6075
6202
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6076
|
-
dryRun:
|
|
6077
|
-
runMode:
|
|
6203
|
+
dryRun: input2.dryRun,
|
|
6204
|
+
runMode: input2.runMode
|
|
6078
6205
|
}
|
|
6079
6206
|
};
|
|
6080
6207
|
}
|
|
@@ -6230,7 +6357,7 @@ async function runLinksCommand(options, dependencies = {}) {
|
|
|
6230
6357
|
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6231
6358
|
if (!openRouterApiKey) {
|
|
6232
6359
|
throw new ReportedError(
|
|
6233
|
-
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set
|
|
6360
|
+
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set TELEPAT_OPENROUTER_KEY."
|
|
6234
6361
|
);
|
|
6235
6362
|
}
|
|
6236
6363
|
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
@@ -6710,7 +6837,7 @@ async function listAllGenerations(markdownOutputDir) {
|
|
|
6710
6837
|
try {
|
|
6711
6838
|
const metadata = await extractArticleMetadata(filePath);
|
|
6712
6839
|
const identity = deriveOutputIdentity(filePath, markdownOutputDir);
|
|
6713
|
-
const
|
|
6840
|
+
const output2 = {
|
|
6714
6841
|
id: `${identity.generationId}:${identity.contentType}:${identity.index}`,
|
|
6715
6842
|
generationId: identity.generationId,
|
|
6716
6843
|
sourcePath: filePath,
|
|
@@ -6723,38 +6850,38 @@ async function listAllGenerations(markdownOutputDir) {
|
|
|
6723
6850
|
contentTypeLabel: toContentTypeLabel(identity.contentType),
|
|
6724
6851
|
index: identity.index
|
|
6725
6852
|
};
|
|
6726
|
-
const outputKey = `${
|
|
6853
|
+
const outputKey = `${output2.generationId}:${output2.contentType}:${output2.index}`;
|
|
6727
6854
|
const existing = outputMap.get(outputKey);
|
|
6728
|
-
if (!existing ||
|
|
6729
|
-
outputMap.set(outputKey,
|
|
6855
|
+
if (!existing || output2.mtime > existing.mtime) {
|
|
6856
|
+
outputMap.set(outputKey, output2);
|
|
6730
6857
|
}
|
|
6731
6858
|
} catch {
|
|
6732
6859
|
}
|
|
6733
6860
|
}
|
|
6734
6861
|
const grouped = /* @__PURE__ */ new Map();
|
|
6735
|
-
for (const
|
|
6736
|
-
const existing = grouped.get(
|
|
6862
|
+
for (const output2 of outputMap.values()) {
|
|
6863
|
+
const existing = grouped.get(output2.generationId);
|
|
6737
6864
|
if (existing) {
|
|
6738
|
-
existing.push(
|
|
6865
|
+
existing.push(output2);
|
|
6739
6866
|
} else {
|
|
6740
|
-
grouped.set(
|
|
6867
|
+
grouped.set(output2.generationId, [output2]);
|
|
6741
6868
|
}
|
|
6742
6869
|
}
|
|
6743
6870
|
const generations = [];
|
|
6744
6871
|
for (const [id, outputs] of grouped.entries()) {
|
|
6745
6872
|
outputs.sort((a, b) => compareContentTypes(a.contentType, b.contentType) || a.index - b.index || b.mtime - a.mtime);
|
|
6746
6873
|
const primaryContentType = await resolvePrimaryContentType(outputs);
|
|
6747
|
-
const primary = outputs.find((
|
|
6874
|
+
const primary = outputs.find((output2) => output2.contentType === primaryContentType) ?? outputs[0];
|
|
6748
6875
|
if (!primary) {
|
|
6749
6876
|
continue;
|
|
6750
6877
|
}
|
|
6751
|
-
const newestMtime = outputs.reduce((latest,
|
|
6878
|
+
const newestMtime = outputs.reduce((latest, output2) => Math.max(latest, output2.mtime), 0);
|
|
6752
6879
|
generations.push({
|
|
6753
6880
|
id,
|
|
6754
6881
|
title: primary.title,
|
|
6755
6882
|
mtime: newestMtime,
|
|
6756
6883
|
previewSnippet: primary.previewSnippet,
|
|
6757
|
-
coverImageUrl: primary.coverImageUrl ?? outputs.find((
|
|
6884
|
+
coverImageUrl: primary.coverImageUrl ?? outputs.find((output2) => Boolean(output2.coverImageUrl))?.coverImageUrl ?? null,
|
|
6758
6885
|
primaryContentType,
|
|
6759
6886
|
outputs
|
|
6760
6887
|
});
|
|
@@ -6838,7 +6965,7 @@ function toContentTypeLabel(contentType) {
|
|
|
6838
6965
|
return contentType.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
6839
6966
|
}
|
|
6840
6967
|
async function resolvePrimaryContentType(outputs) {
|
|
6841
|
-
const fallback = outputs.find((
|
|
6968
|
+
const fallback = outputs.find((output2) => output2.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
|
|
6842
6969
|
const generationDir = path11.dirname(outputs[0]?.sourcePath ?? "");
|
|
6843
6970
|
if (!generationDir) {
|
|
6844
6971
|
return fallback;
|
|
@@ -6872,15 +6999,15 @@ async function runOutputCommand(options, dependencies = {}) {
|
|
|
6872
6999
|
const outputPaths = resolveOutputPaths();
|
|
6873
7000
|
const generations = await listAllGenerations(outputPaths.markdownOutputDir);
|
|
6874
7001
|
const generation = resolveGeneration(generations, options.generationId);
|
|
6875
|
-
const articleOutputs = generation.outputs.filter((
|
|
7002
|
+
const articleOutputs = generation.outputs.filter((output2) => output2.contentType === generation.primaryContentType);
|
|
6876
7003
|
if (articleOutputs.length === 0) {
|
|
6877
7004
|
throw new ReportedError(
|
|
6878
7005
|
`Generation "${generation.id}" has no primary content outputs (type: ${generation.primaryContentType}).`
|
|
6879
7006
|
);
|
|
6880
7007
|
}
|
|
6881
|
-
const articleOutput = articleOutputs.find((
|
|
7008
|
+
const articleOutput = articleOutputs.find((output2) => output2.index === targetIndex);
|
|
6882
7009
|
if (!articleOutput) {
|
|
6883
|
-
const available = articleOutputs.map((
|
|
7010
|
+
const available = articleOutputs.map((output2) => output2.index).join(", ");
|
|
6884
7011
|
throw new ReportedError(
|
|
6885
7012
|
`Generation "${generation.id}" has no primary output at index ${targetIndex}. Available: ${available}.`
|
|
6886
7013
|
);
|
|
@@ -6928,7 +7055,7 @@ function resolveGeneration(generations, generationId) {
|
|
|
6928
7055
|
return exact;
|
|
6929
7056
|
}
|
|
6930
7057
|
const bySlug = generations.find(
|
|
6931
|
-
(g) => g.outputs.some((
|
|
7058
|
+
(g) => g.outputs.some((output2) => output2.slug === generationId)
|
|
6932
7059
|
);
|
|
6933
7060
|
if (bySlug) {
|
|
6934
7061
|
return bySlug;
|
|
@@ -7028,7 +7155,688 @@ function parsePrimaryAndSecondarySpecs(options) {
|
|
|
7028
7155
|
];
|
|
7029
7156
|
}
|
|
7030
7157
|
|
|
7158
|
+
// src/integrations/keywordplanner/models.ts
|
|
7159
|
+
var countryCodeToGeoTargetId = {
|
|
7160
|
+
AD: 2020,
|
|
7161
|
+
AE: 2784,
|
|
7162
|
+
AF: 2004,
|
|
7163
|
+
AG: 2028,
|
|
7164
|
+
AI: 2660,
|
|
7165
|
+
AL: 2008,
|
|
7166
|
+
AM: 2051,
|
|
7167
|
+
AO: 2024,
|
|
7168
|
+
AQ: 2010,
|
|
7169
|
+
AR: 2032,
|
|
7170
|
+
AS: 2016,
|
|
7171
|
+
AT: 2040,
|
|
7172
|
+
AU: 2036,
|
|
7173
|
+
AW: 2533,
|
|
7174
|
+
AX: 2248,
|
|
7175
|
+
AZ: 2031,
|
|
7176
|
+
BA: 2070,
|
|
7177
|
+
BB: 2052,
|
|
7178
|
+
BD: 2050,
|
|
7179
|
+
BE: 2056,
|
|
7180
|
+
BF: 2854,
|
|
7181
|
+
BG: 2100,
|
|
7182
|
+
BH: 2048,
|
|
7183
|
+
BI: 2108,
|
|
7184
|
+
BJ: 2204,
|
|
7185
|
+
BL: 2652,
|
|
7186
|
+
BM: 2060,
|
|
7187
|
+
BN: 2096,
|
|
7188
|
+
BO: 2068,
|
|
7189
|
+
BQ: 2535,
|
|
7190
|
+
BR: 2076,
|
|
7191
|
+
BS: 2044,
|
|
7192
|
+
BT: 2064,
|
|
7193
|
+
BV: 2074,
|
|
7194
|
+
BW: 2072,
|
|
7195
|
+
BY: 2112,
|
|
7196
|
+
BZ: 2084,
|
|
7197
|
+
CA: 2124,
|
|
7198
|
+
CC: 2166,
|
|
7199
|
+
CD: 2180,
|
|
7200
|
+
CF: 2140,
|
|
7201
|
+
CG: 2178,
|
|
7202
|
+
CH: 2756,
|
|
7203
|
+
CI: 2384,
|
|
7204
|
+
CK: 2184,
|
|
7205
|
+
CL: 2152,
|
|
7206
|
+
CM: 2120,
|
|
7207
|
+
CN: 2156,
|
|
7208
|
+
CO: 2170,
|
|
7209
|
+
CR: 2188,
|
|
7210
|
+
CU: 2192,
|
|
7211
|
+
CV: 2132,
|
|
7212
|
+
CW: 2531,
|
|
7213
|
+
CX: 2162,
|
|
7214
|
+
CY: 2196,
|
|
7215
|
+
CZ: 2203,
|
|
7216
|
+
DE: 2276,
|
|
7217
|
+
DJ: 2262,
|
|
7218
|
+
DK: 2208,
|
|
7219
|
+
DM: 2212,
|
|
7220
|
+
DO: 2214,
|
|
7221
|
+
DZ: 2012,
|
|
7222
|
+
EC: 2218,
|
|
7223
|
+
EE: 2233,
|
|
7224
|
+
EG: 2818,
|
|
7225
|
+
EH: 2732,
|
|
7226
|
+
ER: 2232,
|
|
7227
|
+
ES: 2724,
|
|
7228
|
+
ET: 2231,
|
|
7229
|
+
FI: 2246,
|
|
7230
|
+
FJ: 2242,
|
|
7231
|
+
FK: 2238,
|
|
7232
|
+
FM: 2583,
|
|
7233
|
+
FO: 2234,
|
|
7234
|
+
FR: 2250,
|
|
7235
|
+
GA: 2266,
|
|
7236
|
+
GB: 2826,
|
|
7237
|
+
GD: 2308,
|
|
7238
|
+
GE: 2268,
|
|
7239
|
+
GF: 2254,
|
|
7240
|
+
GG: 2831,
|
|
7241
|
+
GH: 2288,
|
|
7242
|
+
GI: 2292,
|
|
7243
|
+
GL: 2304,
|
|
7244
|
+
GM: 2270,
|
|
7245
|
+
GN: 2324,
|
|
7246
|
+
GP: 2312,
|
|
7247
|
+
GQ: 2226,
|
|
7248
|
+
GR: 2300,
|
|
7249
|
+
GS: 2239,
|
|
7250
|
+
GT: 2320,
|
|
7251
|
+
GU: 2316,
|
|
7252
|
+
GW: 2624,
|
|
7253
|
+
GY: 2328,
|
|
7254
|
+
HK: 2344,
|
|
7255
|
+
HM: 2334,
|
|
7256
|
+
HN: 2340,
|
|
7257
|
+
HR: 2191,
|
|
7258
|
+
HT: 2332,
|
|
7259
|
+
HU: 2348,
|
|
7260
|
+
ID: 2360,
|
|
7261
|
+
IE: 2372,
|
|
7262
|
+
IL: 2376,
|
|
7263
|
+
IM: 2833,
|
|
7264
|
+
IN: 2356,
|
|
7265
|
+
IO: 2086,
|
|
7266
|
+
IQ: 2368,
|
|
7267
|
+
IR: 2364,
|
|
7268
|
+
IS: 2352,
|
|
7269
|
+
IT: 2380,
|
|
7270
|
+
JE: 2832,
|
|
7271
|
+
JM: 2388,
|
|
7272
|
+
JO: 2400,
|
|
7273
|
+
JP: 2392,
|
|
7274
|
+
KE: 2404,
|
|
7275
|
+
KG: 2417,
|
|
7276
|
+
KH: 2116,
|
|
7277
|
+
KI: 2296,
|
|
7278
|
+
KM: 2174,
|
|
7279
|
+
KN: 2659,
|
|
7280
|
+
KP: 2408,
|
|
7281
|
+
KR: 2410,
|
|
7282
|
+
KW: 2414,
|
|
7283
|
+
KY: 2136,
|
|
7284
|
+
KZ: 2398,
|
|
7285
|
+
LA: 2418,
|
|
7286
|
+
LB: 2422,
|
|
7287
|
+
LC: 2662,
|
|
7288
|
+
LI: 2438,
|
|
7289
|
+
LK: 2144,
|
|
7290
|
+
LR: 2430,
|
|
7291
|
+
LS: 2426,
|
|
7292
|
+
LT: 2440,
|
|
7293
|
+
LU: 2442,
|
|
7294
|
+
LV: 2428,
|
|
7295
|
+
LY: 2434,
|
|
7296
|
+
MA: 2504,
|
|
7297
|
+
MC: 2492,
|
|
7298
|
+
MD: 2498,
|
|
7299
|
+
ME: 2499,
|
|
7300
|
+
MF: 2663,
|
|
7301
|
+
MG: 2450,
|
|
7302
|
+
MH: 2584,
|
|
7303
|
+
MK: 2807,
|
|
7304
|
+
ML: 2466,
|
|
7305
|
+
MM: 2104,
|
|
7306
|
+
MN: 2496,
|
|
7307
|
+
MO: 2446,
|
|
7308
|
+
MP: 2580,
|
|
7309
|
+
MQ: 2474,
|
|
7310
|
+
MR: 2478,
|
|
7311
|
+
MS: 2500,
|
|
7312
|
+
MT: 2470,
|
|
7313
|
+
MU: 2480,
|
|
7314
|
+
MV: 2462,
|
|
7315
|
+
MW: 2454,
|
|
7316
|
+
MX: 2484,
|
|
7317
|
+
MY: 2458,
|
|
7318
|
+
MZ: 2508,
|
|
7319
|
+
NA: 2516,
|
|
7320
|
+
NC: 2540,
|
|
7321
|
+
NE: 2562,
|
|
7322
|
+
NF: 2574,
|
|
7323
|
+
NG: 2566,
|
|
7324
|
+
NI: 2558,
|
|
7325
|
+
NL: 2528,
|
|
7326
|
+
NO: 2578,
|
|
7327
|
+
NP: 2524,
|
|
7328
|
+
NR: 2520,
|
|
7329
|
+
NU: 2570,
|
|
7330
|
+
NZ: 2554,
|
|
7331
|
+
OM: 2512,
|
|
7332
|
+
PA: 2591,
|
|
7333
|
+
PE: 2604,
|
|
7334
|
+
PF: 2258,
|
|
7335
|
+
PG: 2598,
|
|
7336
|
+
PH: 2608,
|
|
7337
|
+
PK: 2586,
|
|
7338
|
+
PL: 2616,
|
|
7339
|
+
PM: 2666,
|
|
7340
|
+
PN: 2612,
|
|
7341
|
+
PR: 2630,
|
|
7342
|
+
PS: 2275,
|
|
7343
|
+
PT: 2620,
|
|
7344
|
+
PW: 2585,
|
|
7345
|
+
PY: 2600,
|
|
7346
|
+
QA: 2634,
|
|
7347
|
+
RE: 2638,
|
|
7348
|
+
RO: 2642,
|
|
7349
|
+
RS: 2688,
|
|
7350
|
+
RU: 2643,
|
|
7351
|
+
RW: 2646,
|
|
7352
|
+
SA: 2682,
|
|
7353
|
+
SB: 2090,
|
|
7354
|
+
SC: 2690,
|
|
7355
|
+
SD: 2729,
|
|
7356
|
+
SE: 2752,
|
|
7357
|
+
SG: 2702,
|
|
7358
|
+
SH: 2654,
|
|
7359
|
+
SI: 2705,
|
|
7360
|
+
SJ: 2744,
|
|
7361
|
+
SK: 2703,
|
|
7362
|
+
SL: 2694,
|
|
7363
|
+
SM: 2674,
|
|
7364
|
+
SN: 2686,
|
|
7365
|
+
SO: 2706,
|
|
7366
|
+
SR: 2740,
|
|
7367
|
+
SS: 2728,
|
|
7368
|
+
ST: 2678,
|
|
7369
|
+
SV: 2222,
|
|
7370
|
+
SX: 2534,
|
|
7371
|
+
SY: 2760,
|
|
7372
|
+
SZ: 2748,
|
|
7373
|
+
TC: 2796,
|
|
7374
|
+
TD: 2148,
|
|
7375
|
+
TF: 2260,
|
|
7376
|
+
TG: 2768,
|
|
7377
|
+
TH: 2764,
|
|
7378
|
+
TJ: 2762,
|
|
7379
|
+
TK: 2772,
|
|
7380
|
+
TL: 2626,
|
|
7381
|
+
TM: 2795,
|
|
7382
|
+
TN: 2788,
|
|
7383
|
+
TO: 2776,
|
|
7384
|
+
TR: 2792,
|
|
7385
|
+
TT: 2780,
|
|
7386
|
+
TV: 2798,
|
|
7387
|
+
TW: 2158,
|
|
7388
|
+
TZ: 2834,
|
|
7389
|
+
UA: 2804,
|
|
7390
|
+
UG: 2800,
|
|
7391
|
+
UM: 2581,
|
|
7392
|
+
US: 2840,
|
|
7393
|
+
UY: 2858,
|
|
7394
|
+
UZ: 2860,
|
|
7395
|
+
VA: 2336,
|
|
7396
|
+
VC: 2670,
|
|
7397
|
+
VE: 2862,
|
|
7398
|
+
VG: 2092,
|
|
7399
|
+
VI: 2850,
|
|
7400
|
+
VN: 2704,
|
|
7401
|
+
VU: 2548,
|
|
7402
|
+
WF: 2876,
|
|
7403
|
+
WS: 2882,
|
|
7404
|
+
YE: 2887,
|
|
7405
|
+
YT: 2175,
|
|
7406
|
+
ZA: 2710,
|
|
7407
|
+
ZM: 2894,
|
|
7408
|
+
ZW: 2716
|
|
7409
|
+
};
|
|
7410
|
+
var languageCodeToConstantId = {
|
|
7411
|
+
af: 1064,
|
|
7412
|
+
sq: 1066,
|
|
7413
|
+
am: 1067,
|
|
7414
|
+
ar: 1001,
|
|
7415
|
+
hy: 1068,
|
|
7416
|
+
az: 1069,
|
|
7417
|
+
eu: 1070,
|
|
7418
|
+
be: 1071,
|
|
7419
|
+
bn: 1072,
|
|
7420
|
+
bs: 1073,
|
|
7421
|
+
bg: 1074,
|
|
7422
|
+
my: 1075,
|
|
7423
|
+
ca: 1076,
|
|
7424
|
+
zh: 1020,
|
|
7425
|
+
hr: 1077,
|
|
7426
|
+
cs: 1078,
|
|
7427
|
+
da: 1079,
|
|
7428
|
+
nl: 1080,
|
|
7429
|
+
en: 1e3,
|
|
7430
|
+
et: 1081,
|
|
7431
|
+
fi: 1082,
|
|
7432
|
+
fr: 1002,
|
|
7433
|
+
gl: 1083,
|
|
7434
|
+
ka: 1084,
|
|
7435
|
+
de: 1003,
|
|
7436
|
+
el: 1008,
|
|
7437
|
+
gu: 1085,
|
|
7438
|
+
iw: 1009,
|
|
7439
|
+
hi: 1086,
|
|
7440
|
+
hu: 1087,
|
|
7441
|
+
is: 1088,
|
|
7442
|
+
id: 1089,
|
|
7443
|
+
it: 1005,
|
|
7444
|
+
ja: 1006,
|
|
7445
|
+
kn: 1090,
|
|
7446
|
+
kk: 1091,
|
|
7447
|
+
km: 1092,
|
|
7448
|
+
ko: 1007,
|
|
7449
|
+
ky: 1093,
|
|
7450
|
+
lo: 1094,
|
|
7451
|
+
lv: 1095,
|
|
7452
|
+
lt: 1096,
|
|
7453
|
+
mk: 1097,
|
|
7454
|
+
ms: 1098,
|
|
7455
|
+
ml: 1099,
|
|
7456
|
+
mr: 1100,
|
|
7457
|
+
mn: 1101,
|
|
7458
|
+
ne: 1102,
|
|
7459
|
+
no: 1103,
|
|
7460
|
+
fa: 1104,
|
|
7461
|
+
pl: 1105,
|
|
7462
|
+
pt: 1009,
|
|
7463
|
+
pa: 1106,
|
|
7464
|
+
ro: 1107,
|
|
7465
|
+
ru: 1010,
|
|
7466
|
+
sr: 1108,
|
|
7467
|
+
si: 1109,
|
|
7468
|
+
sk: 1110,
|
|
7469
|
+
sl: 1111,
|
|
7470
|
+
es: 1004,
|
|
7471
|
+
sw: 1112,
|
|
7472
|
+
sv: 1011,
|
|
7473
|
+
tl: 1113,
|
|
7474
|
+
ta: 1114,
|
|
7475
|
+
te: 1115,
|
|
7476
|
+
th: 1012,
|
|
7477
|
+
tr: 1013,
|
|
7478
|
+
uk: 1115,
|
|
7479
|
+
ur: 1116,
|
|
7480
|
+
uz: 1117,
|
|
7481
|
+
vi: 1118,
|
|
7482
|
+
zu: 1119
|
|
7483
|
+
};
|
|
7484
|
+
var DEFAULT_LANGUAGE = "en";
|
|
7485
|
+
var DEFAULT_FORECAST_COUNTRY = "US";
|
|
7486
|
+
var FORECAST_DEFAULT_DAYS = 30;
|
|
7487
|
+
var MONTH_MAP = {
|
|
7488
|
+
JANUARY: 1,
|
|
7489
|
+
FEBRUARY: 2,
|
|
7490
|
+
MARCH: 3,
|
|
7491
|
+
APRIL: 4,
|
|
7492
|
+
MAY: 5,
|
|
7493
|
+
JUNE: 6,
|
|
7494
|
+
JULY: 7,
|
|
7495
|
+
AUGUST: 8,
|
|
7496
|
+
SEPTEMBER: 9,
|
|
7497
|
+
OCTOBER: 10,
|
|
7498
|
+
NOVEMBER: 11,
|
|
7499
|
+
DECEMBER: 12
|
|
7500
|
+
};
|
|
7501
|
+
function parseStrInt(s) {
|
|
7502
|
+
if (!s) return 0;
|
|
7503
|
+
const n = Number.parseInt(s, 10);
|
|
7504
|
+
return Number.isFinite(n) ? n : 0;
|
|
7505
|
+
}
|
|
7506
|
+
function normalizeCompetition(raw) {
|
|
7507
|
+
if (!raw || raw === "COMPETITION_UNSPECIFIED") return "UNKNOWN";
|
|
7508
|
+
return raw;
|
|
7509
|
+
}
|
|
7510
|
+
function resolveGeoTargetConstant(code) {
|
|
7511
|
+
if (code.startsWith("geoTargetConstants/")) return code;
|
|
7512
|
+
const id = countryCodeToGeoTargetId[code.toUpperCase()];
|
|
7513
|
+
if (id === void 0) throw new Error(`Unsupported country code: ${code}`);
|
|
7514
|
+
return `geoTargetConstants/${id}`;
|
|
7515
|
+
}
|
|
7516
|
+
function resolveLanguageConstant(code) {
|
|
7517
|
+
if (code.startsWith("languageConstants/")) return code;
|
|
7518
|
+
const id = languageCodeToConstantId[code.toLowerCase()];
|
|
7519
|
+
if (id === void 0) throw new Error(`Unsupported language code: ${code}`);
|
|
7520
|
+
return `languageConstants/${id}`;
|
|
7521
|
+
}
|
|
7522
|
+
function resolveGeoTargets(countryCodes, forForecast) {
|
|
7523
|
+
if (!countryCodes || countryCodes.length === 0) {
|
|
7524
|
+
if (forForecast) return [resolveGeoTargetConstant(DEFAULT_FORECAST_COUNTRY)];
|
|
7525
|
+
return void 0;
|
|
7526
|
+
}
|
|
7527
|
+
return countryCodes.map(resolveGeoTargetConstant);
|
|
7528
|
+
}
|
|
7529
|
+
function resolveLanguage(lang) {
|
|
7530
|
+
if (!lang) return resolveLanguageConstant(DEFAULT_LANGUAGE);
|
|
7531
|
+
return resolveLanguageConstant(lang);
|
|
7532
|
+
}
|
|
7533
|
+
function buildGenerateIdeasBody(input2) {
|
|
7534
|
+
const hasKeywords = input2.seedKeywords && input2.seedKeywords.length > 0;
|
|
7535
|
+
const hasUrl = input2.url && input2.url.length > 0;
|
|
7536
|
+
const hasSite = input2.site && input2.site.length > 0;
|
|
7537
|
+
if (!hasKeywords && !hasUrl && !hasSite) {
|
|
7538
|
+
throw new Error("At least one of seedKeywords, url, or site is required.");
|
|
7539
|
+
}
|
|
7540
|
+
if (hasSite && (hasKeywords || hasUrl)) {
|
|
7541
|
+
throw new Error("site cannot be combined with seedKeywords or url.");
|
|
7542
|
+
}
|
|
7543
|
+
const body = {
|
|
7544
|
+
language: resolveLanguage(input2.language)
|
|
7545
|
+
};
|
|
7546
|
+
const geoTargets = resolveGeoTargets(input2.countryCodes, false);
|
|
7547
|
+
if (geoTargets) body.geoTargetConstants = geoTargets;
|
|
7548
|
+
if (hasKeywords && hasUrl) {
|
|
7549
|
+
body.keywordAndUrlSeed = { keywords: input2.seedKeywords, url: input2.url };
|
|
7550
|
+
} else if (hasKeywords) {
|
|
7551
|
+
body.keywordSeed = { keywords: input2.seedKeywords };
|
|
7552
|
+
} else if (hasUrl) {
|
|
7553
|
+
body.urlSeed = { url: input2.url };
|
|
7554
|
+
} else if (hasSite) {
|
|
7555
|
+
body.siteSeed = { site: input2.site };
|
|
7556
|
+
}
|
|
7557
|
+
if (input2.pageSize && input2.pageSize > 0) {
|
|
7558
|
+
body.pageSize = input2.pageSize;
|
|
7559
|
+
}
|
|
7560
|
+
return body;
|
|
7561
|
+
}
|
|
7562
|
+
function buildGetHistoricalDataBody(input2) {
|
|
7563
|
+
const body = {
|
|
7564
|
+
keywords: input2.keywords
|
|
7565
|
+
};
|
|
7566
|
+
body.language = resolveLanguage(input2.language);
|
|
7567
|
+
const geoTargets = resolveGeoTargets(input2.countryCodes, false);
|
|
7568
|
+
if (geoTargets) body.geoTargetConstants = geoTargets;
|
|
7569
|
+
body.historicalMetricsOptions = {
|
|
7570
|
+
includeAverageCpc: input2.includeAverageCpc !== false,
|
|
7571
|
+
monthlySearchVolume: true
|
|
7572
|
+
};
|
|
7573
|
+
return body;
|
|
7574
|
+
}
|
|
7575
|
+
function buildForecastBody(input2) {
|
|
7576
|
+
const now = /* @__PURE__ */ new Date();
|
|
7577
|
+
const startDate = input2.startDate || now.toISOString().split("T")[0];
|
|
7578
|
+
const endDate = input2.endDate || new Date(now.getTime() + FORECAST_DEFAULT_DAYS * 864e5).toISOString().split("T")[0];
|
|
7579
|
+
const geoTargets = resolveGeoTargets(input2.countryCodes, true);
|
|
7580
|
+
const language = resolveLanguage(input2.language);
|
|
7581
|
+
const biddableKeywords = input2.keywords.map((kw) => ({
|
|
7582
|
+
keyword: {
|
|
7583
|
+
text: kw,
|
|
7584
|
+
matchType: input2.keywordMatchType || "BROAD"
|
|
7585
|
+
}
|
|
7586
|
+
}));
|
|
7587
|
+
const campaign = {
|
|
7588
|
+
languageConstants: [language],
|
|
7589
|
+
geoTargetConstants: geoTargets,
|
|
7590
|
+
forecastPeriod: { startDate, endDate },
|
|
7591
|
+
adGroups: [{ biddableKeywords }]
|
|
7592
|
+
};
|
|
7593
|
+
if (input2.maxCpcBidMicros !== void 0) {
|
|
7594
|
+
campaign.biddingStrategy = {
|
|
7595
|
+
manualCpcBiddingStrategy: {
|
|
7596
|
+
maxCpcBidMicros: String(input2.maxCpcBidMicros)
|
|
7597
|
+
}
|
|
7598
|
+
};
|
|
7599
|
+
}
|
|
7600
|
+
return { campaign };
|
|
7601
|
+
}
|
|
7602
|
+
function parseGenerateIdeasResponse(raw) {
|
|
7603
|
+
const results = raw.results || [];
|
|
7604
|
+
const ideas = results.map((r) => {
|
|
7605
|
+
const metrics = r.keywordIdeaMetrics || {};
|
|
7606
|
+
const closeVariantsRaw = r.closeVariants || [];
|
|
7607
|
+
return {
|
|
7608
|
+
text: r.text || "",
|
|
7609
|
+
avgMonthlySearches: parseStrInt(metrics.avgMonthlySearches),
|
|
7610
|
+
competition: normalizeCompetition(metrics.competition),
|
|
7611
|
+
competitionIndex: parseStrInt(metrics.competitionIndex),
|
|
7612
|
+
lowTopOfPageBidMicros: parseStrInt(metrics.lowTopOfPageBidMicros),
|
|
7613
|
+
highTopOfPageBidMicros: parseStrInt(metrics.highTopOfPageBidMicros),
|
|
7614
|
+
closeVariants: closeVariantsRaw.map((cv) => cv.text || "")
|
|
7615
|
+
};
|
|
7616
|
+
});
|
|
7617
|
+
return { ideas, count: ideas.length };
|
|
7618
|
+
}
|
|
7619
|
+
function parseGetHistoricalDataResponse(raw) {
|
|
7620
|
+
const metrics = raw.metrics || [];
|
|
7621
|
+
const keywords = metrics.map((m) => {
|
|
7622
|
+
const km = m.keywordMetrics || {};
|
|
7623
|
+
const monthlyRaw = km.monthlySearchVolumes || [];
|
|
7624
|
+
const monthlySearchVolumes = monthlyRaw.map((v) => ({
|
|
7625
|
+
year: v.year || 0,
|
|
7626
|
+
month: MONTH_MAP[v.month || ""] || 0,
|
|
7627
|
+
monthlySearches: parseStrInt(v.monthlySearches)
|
|
7628
|
+
}));
|
|
7629
|
+
return {
|
|
7630
|
+
text: m.text || "",
|
|
7631
|
+
avgMonthlySearches: parseStrInt(km.avgMonthlySearches),
|
|
7632
|
+
competition: normalizeCompetition(km.competition),
|
|
7633
|
+
competitionIndex: parseStrInt(String(km.competitionIndex ?? "")),
|
|
7634
|
+
lowTopOfPageBidMicros: parseStrInt(km.lowTopOfPageBidMicros),
|
|
7635
|
+
highTopOfPageBidMicros: parseStrInt(km.highTopOfPageBidMicros),
|
|
7636
|
+
monthlySearchVolumes
|
|
7637
|
+
};
|
|
7638
|
+
});
|
|
7639
|
+
return { keywords, count: keywords.length };
|
|
7640
|
+
}
|
|
7641
|
+
function parseGetForecastDataResponse(raw) {
|
|
7642
|
+
const adGroupMetrics = raw.adGroupForecastMetrics || [];
|
|
7643
|
+
const keywords = [];
|
|
7644
|
+
for (const ag of adGroupMetrics) {
|
|
7645
|
+
const kfMetrics = ag.keywordForecastMetrics || [];
|
|
7646
|
+
for (const kf of kfMetrics) {
|
|
7647
|
+
const kw = kf.keyword || {};
|
|
7648
|
+
const m = kf.metrics || {};
|
|
7649
|
+
keywords.push({
|
|
7650
|
+
text: kw.text || "",
|
|
7651
|
+
matchType: kw.matchType || "BROAD",
|
|
7652
|
+
impressions: m.impressions || 0,
|
|
7653
|
+
clicks: m.clicks || 0,
|
|
7654
|
+
costMicros: m.costMicros || 0,
|
|
7655
|
+
ctr: m.ctr || 0
|
|
7656
|
+
});
|
|
7657
|
+
}
|
|
7658
|
+
}
|
|
7659
|
+
return { keywords, count: keywords.length };
|
|
7660
|
+
}
|
|
7661
|
+
|
|
7662
|
+
// src/integrations/keywordplanner/client.ts
|
|
7663
|
+
var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
7664
|
+
var API_BASE = "https://googleads.googleapis.com/v24";
|
|
7665
|
+
var EXPIRY_BUFFER_MS = 6e4;
|
|
7666
|
+
var HTTP_TIMEOUT_MS = 3e4;
|
|
7667
|
+
var AUTH_ERROR_MESSAGES = {
|
|
7668
|
+
DEVELOPER_TOKEN_INVALID: "Invalid developer token. Set it via: ideon config set googleAdsDeveloperToken <token>. Get one at https://ads.google.com/aw/apicenter.",
|
|
7669
|
+
DEVELOPER_TOKEN_NOT_APPROVED: "Developer token is in test mode and cannot access real accounts. Apply for Basic access at https://ads.google.com/aw/apicenter and wait for Google approval (a few days).",
|
|
7670
|
+
DEVELOPER_TOKEN_PROHIBITED: "Developer token is not associated with this Google Cloud project. Ensure the token and OAuth client belong to the same GCP project.",
|
|
7671
|
+
OAUTH_TOKEN_INVALID: "OAuth access token is invalid. Re-configure your refresh token via: ideon config set googleAdsRefreshToken <token>.",
|
|
7672
|
+
GOOGLE_ACCOUNT_COOKIE_INVALID: "Google account cookie is invalid. Re-configure your refresh token via: ideon config set googleAdsRefreshToken <token>.",
|
|
7673
|
+
CLIENT_CUSTOMER_ID_INVALID: "Customer ID is invalid. Set it via: ideon config set googleAdsCustomerId <10-digit-id>. Use the account number from the top-right of the Google Ads UI.",
|
|
7674
|
+
CLIENT_CUSTOMER_ID_IS_REQUIRED: "Customer ID is required. Set it via: ideon config set googleAdsCustomerId <10-digit-id>.",
|
|
7675
|
+
CUSTOMER_NOT_FOUND: "Google Ads account not found. Verify googleAdsCustomerId is correct and the account is provisioned. Set via: ideon config set googleAdsCustomerId <id>.",
|
|
7676
|
+
NOT_ADS_USER: "Google account is not associated with any Google Ads account. Sign in to ads.google.com and create or link an account first.",
|
|
7677
|
+
USER_PERMISSION_DENIED: "Permission denied. If accessing through a manager account, set: ideon config set googleAdsLoginCustomerId <manager-account-id>.",
|
|
7678
|
+
CUSTOMER_NOT_ENABLED: "Google Ads account is not enabled. Complete account setup at ads.google.com.",
|
|
7679
|
+
ACCESS_TOKEN_SCOPE_INSUFFICIENT: 'OAuth token is missing required "adwords" scope. Re-authorize with scope https://www.googleapis.com/auth/adwords and set the new refresh token via: ideon config set googleAdsRefreshToken <token>.'
|
|
7680
|
+
};
|
|
7681
|
+
function stripDashes(customerId) {
|
|
7682
|
+
return customerId.replace(/-/g, "");
|
|
7683
|
+
}
|
|
7684
|
+
function extractErrorMessage(statusCode, body) {
|
|
7685
|
+
try {
|
|
7686
|
+
const parsed = JSON.parse(body);
|
|
7687
|
+
const errors = parsed.error;
|
|
7688
|
+
if (errors && errors.length > 0) {
|
|
7689
|
+
const firstError = errors[0];
|
|
7690
|
+
const message = firstError.message;
|
|
7691
|
+
const errorCode = firstError.errorCode;
|
|
7692
|
+
if (errorCode) {
|
|
7693
|
+
for (const [key, value2] of Object.entries(errorCode)) {
|
|
7694
|
+
if (value2 === true && AUTH_ERROR_MESSAGES[key]) {
|
|
7695
|
+
return AUTH_ERROR_MESSAGES[key];
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
}
|
|
7699
|
+
if (message) return message;
|
|
7700
|
+
}
|
|
7701
|
+
} catch {
|
|
7702
|
+
}
|
|
7703
|
+
return `Google Ads API returned HTTP ${statusCode}: ${body}`;
|
|
7704
|
+
}
|
|
7705
|
+
var GkpClient = class {
|
|
7706
|
+
developerToken;
|
|
7707
|
+
clientId;
|
|
7708
|
+
clientSecret;
|
|
7709
|
+
refreshToken;
|
|
7710
|
+
customerId;
|
|
7711
|
+
loginCustomerId;
|
|
7712
|
+
baseUrl;
|
|
7713
|
+
accessToken = null;
|
|
7714
|
+
tokenExpiresAt = 0;
|
|
7715
|
+
constructor(options) {
|
|
7716
|
+
this.developerToken = options.developerToken;
|
|
7717
|
+
this.clientId = options.clientId;
|
|
7718
|
+
this.clientSecret = options.clientSecret;
|
|
7719
|
+
this.refreshToken = options.refreshToken;
|
|
7720
|
+
this.customerId = stripDashes(options.customerId);
|
|
7721
|
+
this.loginCustomerId = options.loginCustomerId ? stripDashes(options.loginCustomerId) : void 0;
|
|
7722
|
+
this.baseUrl = `${API_BASE}/customers/${this.customerId}`;
|
|
7723
|
+
}
|
|
7724
|
+
async refreshAccessToken() {
|
|
7725
|
+
const controller = new AbortController();
|
|
7726
|
+
const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
7727
|
+
try {
|
|
7728
|
+
const body = new URLSearchParams({
|
|
7729
|
+
client_id: this.clientId,
|
|
7730
|
+
client_secret: this.clientSecret,
|
|
7731
|
+
refresh_token: this.refreshToken,
|
|
7732
|
+
grant_type: "refresh_token"
|
|
7733
|
+
});
|
|
7734
|
+
const response = await fetch(TOKEN_ENDPOINT, {
|
|
7735
|
+
method: "POST",
|
|
7736
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
7737
|
+
body: body.toString(),
|
|
7738
|
+
signal: controller.signal
|
|
7739
|
+
});
|
|
7740
|
+
if (!response.ok) {
|
|
7741
|
+
const errorBody = await response.text();
|
|
7742
|
+
throw new Error(`OAuth2 token exchange failed (${response.status}): ${errorBody}`);
|
|
7743
|
+
}
|
|
7744
|
+
const data = await response.json();
|
|
7745
|
+
if (!data.access_token) {
|
|
7746
|
+
throw new Error("OAuth2 response did not include an access_token.");
|
|
7747
|
+
}
|
|
7748
|
+
this.accessToken = data.access_token;
|
|
7749
|
+
this.tokenExpiresAt = Date.now() + data.expires_in * 1e3 - EXPIRY_BUFFER_MS;
|
|
7750
|
+
} finally {
|
|
7751
|
+
clearTimeout(timeout);
|
|
7752
|
+
}
|
|
7753
|
+
}
|
|
7754
|
+
async getAccessToken() {
|
|
7755
|
+
if (this.accessToken && Date.now() < this.tokenExpiresAt) {
|
|
7756
|
+
return this.accessToken;
|
|
7757
|
+
}
|
|
7758
|
+
await this.refreshAccessToken();
|
|
7759
|
+
if (!this.accessToken) {
|
|
7760
|
+
throw new Error("Failed to obtain access token.");
|
|
7761
|
+
}
|
|
7762
|
+
return this.accessToken;
|
|
7763
|
+
}
|
|
7764
|
+
buildHeaders() {
|
|
7765
|
+
const headers = {
|
|
7766
|
+
"developer-token": this.developerToken,
|
|
7767
|
+
"Authorization": `Bearer ${this.accessToken}`,
|
|
7768
|
+
"Content-Type": "application/json"
|
|
7769
|
+
};
|
|
7770
|
+
if (this.loginCustomerId) {
|
|
7771
|
+
headers["login-customer-id"] = this.loginCustomerId;
|
|
7772
|
+
}
|
|
7773
|
+
return headers;
|
|
7774
|
+
}
|
|
7775
|
+
async request(method, path15, body) {
|
|
7776
|
+
const token = await this.getAccessToken();
|
|
7777
|
+
const headers = this.buildHeaders();
|
|
7778
|
+
const controller = new AbortController();
|
|
7779
|
+
const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
7780
|
+
try {
|
|
7781
|
+
const response = await fetch(`${this.baseUrl}${path15}`, {
|
|
7782
|
+
method,
|
|
7783
|
+
headers,
|
|
7784
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
7785
|
+
signal: controller.signal
|
|
7786
|
+
});
|
|
7787
|
+
const responseText = await response.text();
|
|
7788
|
+
if (!response.ok) {
|
|
7789
|
+
throw new Error(extractErrorMessage(response.status, responseText));
|
|
7790
|
+
}
|
|
7791
|
+
return JSON.parse(responseText);
|
|
7792
|
+
} finally {
|
|
7793
|
+
clearTimeout(timeout);
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
async generateKeywordIdeas(input2) {
|
|
7797
|
+
const body = buildGenerateIdeasBody(input2);
|
|
7798
|
+
const raw = await this.request("POST", ":generateKeywordIdeas", body);
|
|
7799
|
+
return parseGenerateIdeasResponse(raw);
|
|
7800
|
+
}
|
|
7801
|
+
async getHistoricalMetrics(input2) {
|
|
7802
|
+
const body = buildGetHistoricalDataBody(input2);
|
|
7803
|
+
const raw = await this.request("POST", ":generateKeywordHistoricalMetrics", body);
|
|
7804
|
+
return parseGetHistoricalDataResponse(raw);
|
|
7805
|
+
}
|
|
7806
|
+
async getForecastData(input2) {
|
|
7807
|
+
const body = buildForecastBody(input2);
|
|
7808
|
+
const raw = await this.request("POST", ":generateKeywordForecastMetrics", body);
|
|
7809
|
+
return parseGetForecastDataResponse(raw);
|
|
7810
|
+
}
|
|
7811
|
+
};
|
|
7812
|
+
|
|
7031
7813
|
// src/integrations/mcp/server.ts
|
|
7814
|
+
var gkpClient = null;
|
|
7815
|
+
async function getOrCreateGkpClient() {
|
|
7816
|
+
if (gkpClient) return gkpClient;
|
|
7817
|
+
const envSettings = readEnvSettings();
|
|
7818
|
+
const secrets = await loadSecrets({ disableKeytar: envSettings.disableKeytar });
|
|
7819
|
+
const devToken = envSettings.googleAdsDeveloperToken ?? secrets.googleAdsDeveloperToken;
|
|
7820
|
+
const clientId = envSettings.googleAdsClientId ?? secrets.googleAdsClientId;
|
|
7821
|
+
const clientSecret = envSettings.googleAdsClientSecret ?? secrets.googleAdsClientSecret;
|
|
7822
|
+
const refreshToken = envSettings.googleAdsRefreshToken ?? secrets.googleAdsRefreshToken;
|
|
7823
|
+
const customerId = envSettings.googleAdsCustomerId ?? secrets.googleAdsCustomerId;
|
|
7824
|
+
const loginCustomerId = envSettings.googleAdsLoginCustomerId ?? secrets.googleAdsLoginCustomerId;
|
|
7825
|
+
if (!devToken || !clientId || !clientSecret || !refreshToken || !customerId) {
|
|
7826
|
+
throw new ReportedError(
|
|
7827
|
+
"Google Ads credentials are not configured. Set googleAdsDeveloperToken, googleAdsClientId, googleAdsClientSecret, googleAdsRefreshToken, and googleAdsCustomerId via ideon_config_set or environment variables."
|
|
7828
|
+
);
|
|
7829
|
+
}
|
|
7830
|
+
gkpClient = new GkpClient({
|
|
7831
|
+
developerToken: devToken,
|
|
7832
|
+
clientId,
|
|
7833
|
+
clientSecret,
|
|
7834
|
+
refreshToken,
|
|
7835
|
+
customerId,
|
|
7836
|
+
loginCustomerId: loginCustomerId || void 0
|
|
7837
|
+
});
|
|
7838
|
+
return gkpClient;
|
|
7839
|
+
}
|
|
7032
7840
|
async function startIdeonMcpServer() {
|
|
7033
7841
|
const server = new McpServer({
|
|
7034
7842
|
name: "ideon",
|
|
@@ -7041,30 +7849,30 @@ async function startIdeonMcpServer() {
|
|
|
7041
7849
|
description: "Generate content from an idea using the Ideon pipeline.",
|
|
7042
7850
|
inputSchema: writeToolInputSchema
|
|
7043
7851
|
},
|
|
7044
|
-
async (
|
|
7852
|
+
async (input2) => {
|
|
7045
7853
|
try {
|
|
7046
7854
|
const parsedTargets = parsePrimaryAndSecondarySpecs({
|
|
7047
|
-
primarySpec:
|
|
7048
|
-
secondarySpecs:
|
|
7855
|
+
primarySpec: input2.primary,
|
|
7856
|
+
secondarySpecs: input2.secondary
|
|
7049
7857
|
});
|
|
7050
7858
|
const resolved = await resolveRunInput({
|
|
7051
|
-
idea:
|
|
7052
|
-
audience:
|
|
7053
|
-
jobPath:
|
|
7054
|
-
style:
|
|
7055
|
-
intent:
|
|
7056
|
-
targetLength:
|
|
7859
|
+
idea: input2.idea,
|
|
7860
|
+
audience: input2.audience,
|
|
7861
|
+
jobPath: input2.jobPath,
|
|
7862
|
+
style: input2.style,
|
|
7863
|
+
intent: input2.intent,
|
|
7864
|
+
targetLength: input2.length,
|
|
7057
7865
|
contentTargets: parsedTargets
|
|
7058
7866
|
});
|
|
7059
7867
|
const run = await runPipelineShell(resolved, {
|
|
7060
7868
|
workingDir: cwd(),
|
|
7061
7869
|
runMode: "fresh",
|
|
7062
|
-
dryRun:
|
|
7063
|
-
enrichLinks:
|
|
7064
|
-
customLinks:
|
|
7065
|
-
unlinks:
|
|
7066
|
-
maxLinks:
|
|
7067
|
-
maxImages:
|
|
7870
|
+
dryRun: input2.dryRun ?? false,
|
|
7871
|
+
enrichLinks: input2.enrichLinks ?? false,
|
|
7872
|
+
customLinks: input2.link,
|
|
7873
|
+
unlinks: input2.unlink,
|
|
7874
|
+
maxLinks: input2.maxLinks,
|
|
7875
|
+
maxImages: input2.maxImages
|
|
7068
7876
|
});
|
|
7069
7877
|
return {
|
|
7070
7878
|
content: [
|
|
@@ -7095,7 +7903,7 @@ async function startIdeonMcpServer() {
|
|
|
7095
7903
|
description: "Resume the last failed or interrupted Ideon write session.",
|
|
7096
7904
|
inputSchema: writeResumeToolInputSchema
|
|
7097
7905
|
},
|
|
7098
|
-
async (
|
|
7906
|
+
async (input2) => {
|
|
7099
7907
|
try {
|
|
7100
7908
|
const session = await loadWriteSession(cwd());
|
|
7101
7909
|
if (!session) {
|
|
@@ -7119,12 +7927,12 @@ async function startIdeonMcpServer() {
|
|
|
7119
7927
|
const run = await runPipelineShell(resumeInput, {
|
|
7120
7928
|
workingDir: cwd(),
|
|
7121
7929
|
runMode: "resume",
|
|
7122
|
-
dryRun:
|
|
7123
|
-
enrichLinks:
|
|
7124
|
-
customLinks:
|
|
7125
|
-
unlinks:
|
|
7126
|
-
maxLinks:
|
|
7127
|
-
maxImages:
|
|
7930
|
+
dryRun: input2.dryRun ?? false,
|
|
7931
|
+
enrichLinks: input2.enrichLinks ?? false,
|
|
7932
|
+
customLinks: input2.link,
|
|
7933
|
+
unlinks: input2.unlink,
|
|
7934
|
+
maxLinks: input2.maxLinks,
|
|
7935
|
+
maxImages: input2.maxImages
|
|
7128
7936
|
});
|
|
7129
7937
|
return {
|
|
7130
7938
|
content: [
|
|
@@ -7154,11 +7962,11 @@ async function startIdeonMcpServer() {
|
|
|
7154
7962
|
description: "Delete generated output and assets by slug.",
|
|
7155
7963
|
inputSchema: deleteToolInputSchema
|
|
7156
7964
|
},
|
|
7157
|
-
async (
|
|
7965
|
+
async (input2) => {
|
|
7158
7966
|
try {
|
|
7159
7967
|
const messages = [];
|
|
7160
7968
|
await runDeleteCommand(
|
|
7161
|
-
{ slug:
|
|
7969
|
+
{ slug: input2.slug, force: true },
|
|
7162
7970
|
{
|
|
7163
7971
|
cwd: cwd(),
|
|
7164
7972
|
log: (message) => {
|
|
@@ -7170,11 +7978,11 @@ async function startIdeonMcpServer() {
|
|
|
7170
7978
|
content: [
|
|
7171
7979
|
{
|
|
7172
7980
|
type: "text",
|
|
7173
|
-
text: messages.length > 0 ? messages.join("\n") : `Deleted ${
|
|
7981
|
+
text: messages.length > 0 ? messages.join("\n") : `Deleted ${input2.slug}.`
|
|
7174
7982
|
}
|
|
7175
7983
|
],
|
|
7176
7984
|
structuredContent: {
|
|
7177
|
-
slug:
|
|
7985
|
+
slug: input2.slug,
|
|
7178
7986
|
deleted: true
|
|
7179
7987
|
}
|
|
7180
7988
|
};
|
|
@@ -7190,16 +7998,16 @@ async function startIdeonMcpServer() {
|
|
|
7190
7998
|
description: "Run link enrichment for a previously generated article by slug.",
|
|
7191
7999
|
inputSchema: linksToolInputSchema
|
|
7192
8000
|
},
|
|
7193
|
-
async (
|
|
8001
|
+
async (input2) => {
|
|
7194
8002
|
try {
|
|
7195
8003
|
const messages = [];
|
|
7196
8004
|
await runLinksCommand(
|
|
7197
8005
|
{
|
|
7198
|
-
slug:
|
|
7199
|
-
mode:
|
|
7200
|
-
links:
|
|
7201
|
-
unlinks:
|
|
7202
|
-
maxLinks:
|
|
8006
|
+
slug: input2.slug,
|
|
8007
|
+
mode: input2.mode ?? "fresh",
|
|
8008
|
+
links: input2.link,
|
|
8009
|
+
unlinks: input2.unlink,
|
|
8010
|
+
maxLinks: input2.maxLinks
|
|
7203
8011
|
},
|
|
7204
8012
|
{
|
|
7205
8013
|
cwd: cwd(),
|
|
@@ -7212,12 +8020,12 @@ async function startIdeonMcpServer() {
|
|
|
7212
8020
|
content: [
|
|
7213
8021
|
{
|
|
7214
8022
|
type: "text",
|
|
7215
|
-
text: messages.length > 0 ? messages.join("\n") : `Enriched links for ${
|
|
8023
|
+
text: messages.length > 0 ? messages.join("\n") : `Enriched links for ${input2.slug}.`
|
|
7216
8024
|
}
|
|
7217
8025
|
],
|
|
7218
8026
|
structuredContent: {
|
|
7219
|
-
slug:
|
|
7220
|
-
mode:
|
|
8027
|
+
slug: input2.slug,
|
|
8028
|
+
mode: input2.mode ?? "fresh"
|
|
7221
8029
|
}
|
|
7222
8030
|
};
|
|
7223
8031
|
} catch (error) {
|
|
@@ -7232,15 +8040,15 @@ async function startIdeonMcpServer() {
|
|
|
7232
8040
|
description: "Export a generated article as a standalone markdown file with inline links and copied images.",
|
|
7233
8041
|
inputSchema: exportToolInputZodSchema
|
|
7234
8042
|
},
|
|
7235
|
-
async (
|
|
8043
|
+
async (input2) => {
|
|
7236
8044
|
try {
|
|
7237
8045
|
const messages = [];
|
|
7238
8046
|
await runOutputCommand(
|
|
7239
8047
|
{
|
|
7240
|
-
generationId:
|
|
7241
|
-
destinationPath:
|
|
7242
|
-
index:
|
|
7243
|
-
overwrite:
|
|
8048
|
+
generationId: input2.generationId,
|
|
8049
|
+
destinationPath: input2.destinationPath,
|
|
8050
|
+
index: input2.index,
|
|
8051
|
+
overwrite: input2.overwrite
|
|
7244
8052
|
},
|
|
7245
8053
|
{
|
|
7246
8054
|
cwd: cwd(),
|
|
@@ -7253,14 +8061,14 @@ async function startIdeonMcpServer() {
|
|
|
7253
8061
|
content: [
|
|
7254
8062
|
{
|
|
7255
8063
|
type: "text",
|
|
7256
|
-
text: messages.length > 0 ? messages.join("\n") : `Exported ${
|
|
8064
|
+
text: messages.length > 0 ? messages.join("\n") : `Exported ${input2.generationId}.`
|
|
7257
8065
|
}
|
|
7258
8066
|
],
|
|
7259
8067
|
structuredContent: {
|
|
7260
|
-
generationId:
|
|
7261
|
-
destinationPath:
|
|
7262
|
-
index:
|
|
7263
|
-
overwrite:
|
|
8068
|
+
generationId: input2.generationId,
|
|
8069
|
+
destinationPath: input2.destinationPath,
|
|
8070
|
+
index: input2.index ?? 1,
|
|
8071
|
+
overwrite: input2.overwrite ?? false,
|
|
7264
8072
|
messages
|
|
7265
8073
|
}
|
|
7266
8074
|
};
|
|
@@ -7276,12 +8084,12 @@ async function startIdeonMcpServer() {
|
|
|
7276
8084
|
description: "Read a configuration value or secret availability flag.",
|
|
7277
8085
|
inputSchema: configGetToolInputSchema
|
|
7278
8086
|
},
|
|
7279
|
-
async (
|
|
8087
|
+
async (input2) => {
|
|
7280
8088
|
try {
|
|
7281
|
-
if (!isConfigKey(
|
|
7282
|
-
throw new ReportedError(`Unsupported config key: ${
|
|
8089
|
+
if (!isConfigKey(input2.key)) {
|
|
8090
|
+
throw new ReportedError(`Unsupported config key: ${input2.key}`);
|
|
7283
8091
|
}
|
|
7284
|
-
const result = await configGet(
|
|
8092
|
+
const result = await configGet(input2.key);
|
|
7285
8093
|
return {
|
|
7286
8094
|
content: [
|
|
7287
8095
|
{
|
|
@@ -7307,21 +8115,21 @@ async function startIdeonMcpServer() {
|
|
|
7307
8115
|
description: "Set a configuration value or secret token.",
|
|
7308
8116
|
inputSchema: configSetToolInputSchema
|
|
7309
8117
|
},
|
|
7310
|
-
async (
|
|
8118
|
+
async (input2) => {
|
|
7311
8119
|
try {
|
|
7312
|
-
if (!isConfigKey(
|
|
7313
|
-
throw new ReportedError(`Unsupported config key: ${
|
|
8120
|
+
if (!isConfigKey(input2.key)) {
|
|
8121
|
+
throw new ReportedError(`Unsupported config key: ${input2.key}`);
|
|
7314
8122
|
}
|
|
7315
|
-
await configSet(
|
|
8123
|
+
await configSet(input2.key, input2.value);
|
|
7316
8124
|
return {
|
|
7317
8125
|
content: [
|
|
7318
8126
|
{
|
|
7319
8127
|
type: "text",
|
|
7320
|
-
text: `Set ${
|
|
8128
|
+
text: `Set ${input2.key}.`
|
|
7321
8129
|
}
|
|
7322
8130
|
],
|
|
7323
8131
|
structuredContent: {
|
|
7324
|
-
key:
|
|
8132
|
+
key: input2.key,
|
|
7325
8133
|
updated: true
|
|
7326
8134
|
}
|
|
7327
8135
|
};
|
|
@@ -7361,21 +8169,21 @@ async function startIdeonMcpServer() {
|
|
|
7361
8169
|
description: "Reset a setting to its default or delete a stored secret.",
|
|
7362
8170
|
inputSchema: configUnsetToolInputSchema
|
|
7363
8171
|
},
|
|
7364
|
-
async (
|
|
8172
|
+
async (input2) => {
|
|
7365
8173
|
try {
|
|
7366
|
-
if (!isConfigKey(
|
|
7367
|
-
throw new ReportedError(`Unsupported config key: ${
|
|
8174
|
+
if (!isConfigKey(input2.key)) {
|
|
8175
|
+
throw new ReportedError(`Unsupported config key: ${input2.key}`);
|
|
7368
8176
|
}
|
|
7369
|
-
await configUnset(
|
|
8177
|
+
await configUnset(input2.key);
|
|
7370
8178
|
return {
|
|
7371
8179
|
content: [
|
|
7372
8180
|
{
|
|
7373
8181
|
type: "text",
|
|
7374
|
-
text: `Unset ${
|
|
8182
|
+
text: `Unset ${input2.key}.`
|
|
7375
8183
|
}
|
|
7376
8184
|
],
|
|
7377
8185
|
structuredContent: {
|
|
7378
|
-
key:
|
|
8186
|
+
key: input2.key,
|
|
7379
8187
|
updated: true
|
|
7380
8188
|
}
|
|
7381
8189
|
};
|
|
@@ -7384,6 +8192,86 @@ async function startIdeonMcpServer() {
|
|
|
7384
8192
|
}
|
|
7385
8193
|
}
|
|
7386
8194
|
);
|
|
8195
|
+
server.registerTool(
|
|
8196
|
+
"gkp_generate_ideas",
|
|
8197
|
+
{
|
|
8198
|
+
title: "Google Keyword Planner - Generate Ideas",
|
|
8199
|
+
description: "Generate keyword ideas from seed keywords, a URL, or a site using Google Ads Keyword Planner.",
|
|
8200
|
+
inputSchema: gkpGenerateIdeasToolInputZodSchema
|
|
8201
|
+
},
|
|
8202
|
+
async (input2) => {
|
|
8203
|
+
try {
|
|
8204
|
+
const client = await getOrCreateGkpClient();
|
|
8205
|
+
const result = await client.generateKeywordIdeas({
|
|
8206
|
+
seedKeywords: input2.seedKeywords,
|
|
8207
|
+
url: input2.url,
|
|
8208
|
+
site: input2.site,
|
|
8209
|
+
countryCodes: input2.countryCodes,
|
|
8210
|
+
language: input2.language,
|
|
8211
|
+
pageSize: input2.pageSize
|
|
8212
|
+
});
|
|
8213
|
+
return {
|
|
8214
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
8215
|
+
structuredContent: result
|
|
8216
|
+
};
|
|
8217
|
+
} catch (error) {
|
|
8218
|
+
return formatToolError(error);
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
8221
|
+
);
|
|
8222
|
+
server.registerTool(
|
|
8223
|
+
"gkp_get_historical_data",
|
|
8224
|
+
{
|
|
8225
|
+
title: "Google Keyword Planner - Historical Data",
|
|
8226
|
+
description: "Get historical search volume and competition metrics for a list of keywords.",
|
|
8227
|
+
inputSchema: gkpGetHistoricalDataToolInputZodSchema
|
|
8228
|
+
},
|
|
8229
|
+
async (input2) => {
|
|
8230
|
+
try {
|
|
8231
|
+
const client = await getOrCreateGkpClient();
|
|
8232
|
+
const result = await client.getHistoricalMetrics({
|
|
8233
|
+
keywords: input2.keywords,
|
|
8234
|
+
countryCodes: input2.countryCodes,
|
|
8235
|
+
language: input2.language,
|
|
8236
|
+
includeAverageCpc: input2.includeAverageCpc
|
|
8237
|
+
});
|
|
8238
|
+
return {
|
|
8239
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
8240
|
+
structuredContent: result
|
|
8241
|
+
};
|
|
8242
|
+
} catch (error) {
|
|
8243
|
+
return formatToolError(error);
|
|
8244
|
+
}
|
|
8245
|
+
}
|
|
8246
|
+
);
|
|
8247
|
+
server.registerTool(
|
|
8248
|
+
"gkp_get_forecast_data",
|
|
8249
|
+
{
|
|
8250
|
+
title: "Google Keyword Planner - Forecast Data",
|
|
8251
|
+
description: "Get projected impressions, clicks, and cost for a set of keywords.",
|
|
8252
|
+
inputSchema: gkpGetForecastDataToolInputZodSchema
|
|
8253
|
+
},
|
|
8254
|
+
async (input2) => {
|
|
8255
|
+
try {
|
|
8256
|
+
const client = await getOrCreateGkpClient();
|
|
8257
|
+
const result = await client.getForecastData({
|
|
8258
|
+
keywords: input2.keywords,
|
|
8259
|
+
keywordMatchType: input2.keywordMatchType,
|
|
8260
|
+
maxCpcBidMicros: input2.maxCpcBidMicros,
|
|
8261
|
+
countryCodes: input2.countryCodes,
|
|
8262
|
+
language: input2.language,
|
|
8263
|
+
startDate: input2.startDate,
|
|
8264
|
+
endDate: input2.endDate
|
|
8265
|
+
});
|
|
8266
|
+
return {
|
|
8267
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
8268
|
+
structuredContent: result
|
|
8269
|
+
};
|
|
8270
|
+
} catch (error) {
|
|
8271
|
+
return formatToolError(error);
|
|
8272
|
+
}
|
|
8273
|
+
}
|
|
8274
|
+
);
|
|
7387
8275
|
const transport = new StdioServerTransport();
|
|
7388
8276
|
await server.connect(transport);
|
|
7389
8277
|
}
|
|
@@ -7400,6 +8288,530 @@ async function runMcpServeCommand() {
|
|
|
7400
8288
|
await startIdeonMcpServer();
|
|
7401
8289
|
}
|
|
7402
8290
|
|
|
8291
|
+
// src/cli/commands/gads.ts
|
|
8292
|
+
import * as readline from "readline/promises";
|
|
8293
|
+
import { stdin as input, stdout as output } from "process";
|
|
8294
|
+
|
|
8295
|
+
// src/integrations/keywordplanner/oauth.ts
|
|
8296
|
+
import { createServer } from "http";
|
|
8297
|
+
import { execFile } from "child_process";
|
|
8298
|
+
import { promisify } from "util";
|
|
8299
|
+
import { URL as URL2 } from "url";
|
|
8300
|
+
var execFileAsync = promisify(execFile);
|
|
8301
|
+
var AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8302
|
+
var TOKEN_ENDPOINT2 = "https://oauth2.googleapis.com/token";
|
|
8303
|
+
var OAUTH_SCOPE = "https://www.googleapis.com/auth/adwords";
|
|
8304
|
+
var DEFAULT_REDIRECT_PORT = 9876;
|
|
8305
|
+
var MAX_PORT_ATTEMPTS = 4;
|
|
8306
|
+
var HTTP_TIMEOUT_MS2 = 12e4;
|
|
8307
|
+
var defaultDependencies2 = {
|
|
8308
|
+
createHttpServer: createServer,
|
|
8309
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
8310
|
+
setTimeout: globalThis.setTimeout.bind(globalThis),
|
|
8311
|
+
clearTimeout: globalThis.clearTimeout.bind(globalThis),
|
|
8312
|
+
openBrowser: async (url) => {
|
|
8313
|
+
if (process.platform === "darwin") {
|
|
8314
|
+
await execFileAsync("open", [url]);
|
|
8315
|
+
} else if (process.platform === "win32") {
|
|
8316
|
+
await execFileAsync("cmd", ["/c", "start", "", url]);
|
|
8317
|
+
} else {
|
|
8318
|
+
await execFileAsync("xdg-open", [url]);
|
|
8319
|
+
}
|
|
8320
|
+
},
|
|
8321
|
+
log: (message) => console.log(message)
|
|
8322
|
+
};
|
|
8323
|
+
function buildAuthUrl(clientId, redirectUri) {
|
|
8324
|
+
const params = new URLSearchParams({
|
|
8325
|
+
client_id: clientId,
|
|
8326
|
+
redirect_uri: redirectUri,
|
|
8327
|
+
response_type: "code",
|
|
8328
|
+
scope: OAUTH_SCOPE,
|
|
8329
|
+
access_type: "offline",
|
|
8330
|
+
prompt: "consent"
|
|
8331
|
+
});
|
|
8332
|
+
return `${AUTH_ENDPOINT}?${params.toString()}`;
|
|
8333
|
+
}
|
|
8334
|
+
async function exchangeCode(code, clientId, clientSecret, redirectUri, deps) {
|
|
8335
|
+
const body = new URLSearchParams({
|
|
8336
|
+
code,
|
|
8337
|
+
client_id: clientId,
|
|
8338
|
+
client_secret: clientSecret,
|
|
8339
|
+
redirect_uri: redirectUri,
|
|
8340
|
+
grant_type: "authorization_code"
|
|
8341
|
+
});
|
|
8342
|
+
const response = await deps.fetch(TOKEN_ENDPOINT2, {
|
|
8343
|
+
method: "POST",
|
|
8344
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
8345
|
+
body: body.toString()
|
|
8346
|
+
});
|
|
8347
|
+
if (!response.ok) {
|
|
8348
|
+
const errorBody = await response.text();
|
|
8349
|
+
throw new Error(`OAuth2 token exchange failed (${response.status}): ${errorBody}`);
|
|
8350
|
+
}
|
|
8351
|
+
const data = await response.json();
|
|
8352
|
+
if (!data.refresh_token) {
|
|
8353
|
+
throw new Error("OAuth2 response did not include a refresh_token. Ensure prompt=consent was used.");
|
|
8354
|
+
}
|
|
8355
|
+
return data.refresh_token;
|
|
8356
|
+
}
|
|
8357
|
+
function waitForCode(server, redirectPath, redirectUri, deps) {
|
|
8358
|
+
return new Promise((resolve, reject) => {
|
|
8359
|
+
const timeout = deps.setTimeout(() => {
|
|
8360
|
+
server.close();
|
|
8361
|
+
reject(new Error("OAuth flow timed out after 120 seconds."));
|
|
8362
|
+
}, HTTP_TIMEOUT_MS2);
|
|
8363
|
+
function onRequest(req, res) {
|
|
8364
|
+
const url = new URL2(req.url ?? "/", `http://localhost`);
|
|
8365
|
+
if (url.pathname !== redirectPath) {
|
|
8366
|
+
res.writeHead(404);
|
|
8367
|
+
res.end("Not found.");
|
|
8368
|
+
return;
|
|
8369
|
+
}
|
|
8370
|
+
const code = url.searchParams.get("code");
|
|
8371
|
+
const error = url.searchParams.get("error");
|
|
8372
|
+
if (error) {
|
|
8373
|
+
deps.clearTimeout(timeout);
|
|
8374
|
+
server.close();
|
|
8375
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
8376
|
+
res.end(`<h1>Authorization failed</h1><p>${error}</p><p>Close this window and try again.</p>`);
|
|
8377
|
+
reject(new Error(`OAuth authorization error: ${error}`));
|
|
8378
|
+
return;
|
|
8379
|
+
}
|
|
8380
|
+
if (!code) {
|
|
8381
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
8382
|
+
res.end("<h1>Missing authorization code</h1><p>Close this window and try again.</p>");
|
|
8383
|
+
return;
|
|
8384
|
+
}
|
|
8385
|
+
deps.clearTimeout(timeout);
|
|
8386
|
+
server.close();
|
|
8387
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
8388
|
+
res.end("<h1>Authorization successful</h1><p>You can close this window and return to the terminal.</p>");
|
|
8389
|
+
resolve(code);
|
|
8390
|
+
}
|
|
8391
|
+
server.on("request", onRequest);
|
|
8392
|
+
server.on("error", (err) => {
|
|
8393
|
+
deps.clearTimeout(timeout);
|
|
8394
|
+
reject(err);
|
|
8395
|
+
});
|
|
8396
|
+
});
|
|
8397
|
+
}
|
|
8398
|
+
async function startServerOnPort(port, deps) {
|
|
8399
|
+
const redirectPath = "/callback";
|
|
8400
|
+
const redirectUri = `http://localhost:${port}${redirectPath}`;
|
|
8401
|
+
const server = deps.createHttpServer();
|
|
8402
|
+
return new Promise((resolve, reject) => {
|
|
8403
|
+
server.listen(port, () => {
|
|
8404
|
+
resolve({ server, redirectPath, redirectUri });
|
|
8405
|
+
});
|
|
8406
|
+
server.on("error", (err) => {
|
|
8407
|
+
if (err.code === "EADDRINUSE") {
|
|
8408
|
+
reject(new Error(`Port ${port} is in use.`));
|
|
8409
|
+
} else {
|
|
8410
|
+
reject(err);
|
|
8411
|
+
}
|
|
8412
|
+
});
|
|
8413
|
+
});
|
|
8414
|
+
}
|
|
8415
|
+
async function startOAuthFlow(options, dependencies = {}) {
|
|
8416
|
+
const deps = { ...defaultDependencies2, ...dependencies };
|
|
8417
|
+
let server = null;
|
|
8418
|
+
let port = DEFAULT_REDIRECT_PORT;
|
|
8419
|
+
let redirectPath = "/callback";
|
|
8420
|
+
let redirectUri = "";
|
|
8421
|
+
for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
|
|
8422
|
+
try {
|
|
8423
|
+
const result = await startServerOnPort(port, deps);
|
|
8424
|
+
server = result.server;
|
|
8425
|
+
redirectPath = result.redirectPath;
|
|
8426
|
+
redirectUri = result.redirectUri;
|
|
8427
|
+
break;
|
|
8428
|
+
} catch (err) {
|
|
8429
|
+
if (err instanceof Error && err.message.startsWith("Port") && err.message.endsWith("is in use.")) {
|
|
8430
|
+
port++;
|
|
8431
|
+
continue;
|
|
8432
|
+
}
|
|
8433
|
+
throw err;
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
if (!server) {
|
|
8437
|
+
throw new Error(`All ports ${DEFAULT_REDIRECT_PORT}\u2013${DEFAULT_REDIRECT_PORT + MAX_PORT_ATTEMPTS - 1} are in use. Close another process and try again.`);
|
|
8438
|
+
}
|
|
8439
|
+
const authUrl = buildAuthUrl(options.clientId, redirectUri);
|
|
8440
|
+
deps.log("Opening browser for Google Ads authorization...");
|
|
8441
|
+
deps.log(`If the browser does not open, visit:
|
|
8442
|
+
${authUrl}
|
|
8443
|
+
`);
|
|
8444
|
+
try {
|
|
8445
|
+
await deps.openBrowser(authUrl);
|
|
8446
|
+
} catch {
|
|
8447
|
+
deps.log("Could not open browser automatically. Please open the URL above manually.");
|
|
8448
|
+
}
|
|
8449
|
+
try {
|
|
8450
|
+
const code = await waitForCode(server, redirectPath, redirectUri, deps);
|
|
8451
|
+
const refreshToken = await exchangeCode(code, options.clientId, options.clientSecret, redirectUri, deps);
|
|
8452
|
+
return { refreshToken };
|
|
8453
|
+
} catch (err) {
|
|
8454
|
+
if (server && server.listening) {
|
|
8455
|
+
server.close();
|
|
8456
|
+
}
|
|
8457
|
+
throw err;
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8460
|
+
|
|
8461
|
+
// src/cli/commands/gads.ts
|
|
8462
|
+
var GOOGLE_ADS_SECRET_KEYS = [
|
|
8463
|
+
"googleAdsDeveloperToken",
|
|
8464
|
+
"googleAdsClientId",
|
|
8465
|
+
"googleAdsClientSecret",
|
|
8466
|
+
"googleAdsRefreshToken",
|
|
8467
|
+
"googleAdsCustomerId",
|
|
8468
|
+
"googleAdsLoginCustomerId"
|
|
8469
|
+
];
|
|
8470
|
+
function createDefaultDependencies() {
|
|
8471
|
+
const rl = readline.createInterface({ input, output });
|
|
8472
|
+
return {
|
|
8473
|
+
log: (message) => console.log(message),
|
|
8474
|
+
prompt: async (question) => rl.question(question),
|
|
8475
|
+
configSet,
|
|
8476
|
+
configGet,
|
|
8477
|
+
configUnset,
|
|
8478
|
+
startOAuthFlow,
|
|
8479
|
+
readEnvSettings,
|
|
8480
|
+
loadSecrets,
|
|
8481
|
+
isTTY: Boolean(input?.isTTY)
|
|
8482
|
+
};
|
|
8483
|
+
}
|
|
8484
|
+
async function runGadsLoginCommand(options, dependencies = {}) {
|
|
8485
|
+
const deps = { ...createDefaultDependencies(), ...dependencies };
|
|
8486
|
+
if (!deps.isTTY) {
|
|
8487
|
+
throw new ReportedError(
|
|
8488
|
+
"OAuth login requires an interactive terminal with browser access.\n\nFor CI/CD or headless environments, set credentials via environment variables:\n TELEPAT_GOOGLE_ADS_DEVELOPER_TOKEN\n TELEPAT_GOOGLE_ADS_CLIENT_ID\n TELEPAT_GOOGLE_ADS_CLIENT_SECRET\n TELEPAT_GOOGLE_ADS_REFRESH_TOKEN\n TELEPAT_GOOGLE_ADS_CUSTOMER_ID\n TELEPAT_GOOGLE_ADS_LOGIN_CUSTOMER_ID (optional)\n\nEnvironment variables bypass keychain storage entirely."
|
|
8489
|
+
);
|
|
8490
|
+
}
|
|
8491
|
+
const envSettings = deps.readEnvSettings();
|
|
8492
|
+
const secrets = await deps.loadSecrets({ disableKeytar: envSettings.disableKeytar });
|
|
8493
|
+
const hasRefreshToken = Boolean(envSettings.googleAdsRefreshToken ?? secrets.googleAdsRefreshToken);
|
|
8494
|
+
if (hasRefreshToken && !options.force) {
|
|
8495
|
+
deps.log("Already authenticated with Google Ads. Use --force to re-authorize.");
|
|
8496
|
+
return;
|
|
8497
|
+
}
|
|
8498
|
+
const developerToken = options.developerToken ?? await deps.prompt("Google Ads developer token: ");
|
|
8499
|
+
if (!developerToken.trim()) {
|
|
8500
|
+
throw new ReportedError("Developer token cannot be empty.");
|
|
8501
|
+
}
|
|
8502
|
+
const clientId = options.clientId ?? await deps.prompt("OAuth2 client ID: ");
|
|
8503
|
+
if (!clientId.trim()) {
|
|
8504
|
+
throw new ReportedError("Client ID cannot be empty.");
|
|
8505
|
+
}
|
|
8506
|
+
const clientSecret = options.clientSecret ?? await deps.prompt("OAuth2 client secret: ");
|
|
8507
|
+
if (!clientSecret.trim()) {
|
|
8508
|
+
throw new ReportedError("Client secret cannot be empty.");
|
|
8509
|
+
}
|
|
8510
|
+
const customerId = options.customerId ?? await deps.prompt("Google Ads customer ID (10 digits, dashes optional): ");
|
|
8511
|
+
if (!customerId.trim()) {
|
|
8512
|
+
throw new ReportedError("Customer ID cannot be empty.");
|
|
8513
|
+
}
|
|
8514
|
+
if (options.loginCustomerId) {
|
|
8515
|
+
await deps.configSet("googleAdsLoginCustomerId", options.loginCustomerId);
|
|
8516
|
+
}
|
|
8517
|
+
await deps.configSet("googleAdsDeveloperToken", developerToken);
|
|
8518
|
+
await deps.configSet("googleAdsClientId", clientId);
|
|
8519
|
+
await deps.configSet("googleAdsClientSecret", clientSecret);
|
|
8520
|
+
await deps.configSet("googleAdsCustomerId", customerId);
|
|
8521
|
+
deps.log("Starting OAuth2 authorization flow...");
|
|
8522
|
+
deps.log("A browser window will open for Google Ads authorization.");
|
|
8523
|
+
const result = await deps.startOAuthFlow({ clientId, clientSecret });
|
|
8524
|
+
await deps.configSet("googleAdsRefreshToken", result.refreshToken);
|
|
8525
|
+
deps.log("Google Ads credentials saved successfully.");
|
|
8526
|
+
deps.log("Run `ideon gads test` to verify your credentials work.");
|
|
8527
|
+
}
|
|
8528
|
+
async function runGadsLogoutCommand(options, dependencies = {}) {
|
|
8529
|
+
const deps = { ...createDefaultDependencies(), ...dependencies };
|
|
8530
|
+
const keysToClear = options.all ? [...GOOGLE_ADS_SECRET_KEYS] : ["googleAdsRefreshToken"];
|
|
8531
|
+
for (const key of keysToClear) {
|
|
8532
|
+
await deps.configUnset(key);
|
|
8533
|
+
}
|
|
8534
|
+
const label2 = options.all ? "All Google Ads credentials" : "Google Ads refresh token";
|
|
8535
|
+
deps.log(`${label2} cleared.`);
|
|
8536
|
+
}
|
|
8537
|
+
function detectCredentialSource(envValue, keyValue) {
|
|
8538
|
+
if (envValue) return { set: true, source: "env" };
|
|
8539
|
+
if (keyValue) return { set: true, source: "keychain" };
|
|
8540
|
+
return { set: false, source: null };
|
|
8541
|
+
}
|
|
8542
|
+
function buildStatusResult(envSettings, secrets) {
|
|
8543
|
+
return {
|
|
8544
|
+
googleAdsDeveloperToken: detectCredentialSource(envSettings.googleAdsDeveloperToken, secrets.googleAdsDeveloperToken),
|
|
8545
|
+
googleAdsClientId: detectCredentialSource(envSettings.googleAdsClientId, secrets.googleAdsClientId),
|
|
8546
|
+
googleAdsClientSecret: detectCredentialSource(envSettings.googleAdsClientSecret, secrets.googleAdsClientSecret),
|
|
8547
|
+
googleAdsRefreshToken: detectCredentialSource(envSettings.googleAdsRefreshToken, secrets.googleAdsRefreshToken),
|
|
8548
|
+
googleAdsCustomerId: detectCredentialSource(envSettings.googleAdsCustomerId, secrets.googleAdsCustomerId),
|
|
8549
|
+
googleAdsLoginCustomerId: detectCredentialSource(envSettings.googleAdsLoginCustomerId, secrets.googleAdsLoginCustomerId)
|
|
8550
|
+
};
|
|
8551
|
+
}
|
|
8552
|
+
function formatStatusTTY(result) {
|
|
8553
|
+
const lines = ["", "Google Ads Credential Status", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"];
|
|
8554
|
+
for (const [key, status] of Object.entries(result)) {
|
|
8555
|
+
const label2 = key.replace("googleAds", "").replace(/([A-Z])/g, " $1").trim();
|
|
8556
|
+
const displayLabel = label2.charAt(0).toLowerCase() + label2.slice(1);
|
|
8557
|
+
if (status.set) {
|
|
8558
|
+
lines.push(` ${displayLabel.padEnd(20)} \u2713 ${status.source}`);
|
|
8559
|
+
} else {
|
|
8560
|
+
const suffix = key === "googleAdsLoginCustomerId" ? " (optional)" : "";
|
|
8561
|
+
lines.push(` ${displayLabel.padEnd(20)} \u2014 not set${suffix}`);
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
lines.push("");
|
|
8565
|
+
lines.push("Run `ideon gads test` to verify credentials work.");
|
|
8566
|
+
const allSet = Object.entries(result).filter(([k]) => k !== "googleAdsLoginCustomerId").every(([, s]) => s.set);
|
|
8567
|
+
if (!allSet) {
|
|
8568
|
+
lines.push("Run `ideon gads login` to set up missing credentials.");
|
|
8569
|
+
}
|
|
8570
|
+
lines.push("");
|
|
8571
|
+
return lines.join("\n");
|
|
8572
|
+
}
|
|
8573
|
+
async function runGadsStatusCommand(options, dependencies = {}) {
|
|
8574
|
+
const deps = { ...createDefaultDependencies(), ...dependencies };
|
|
8575
|
+
const envSettings = deps.readEnvSettings();
|
|
8576
|
+
const secrets = await deps.loadSecrets({ disableKeytar: envSettings.disableKeytar });
|
|
8577
|
+
const result = buildStatusResult(envSettings, secrets);
|
|
8578
|
+
if (options.json) {
|
|
8579
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
8580
|
+
return;
|
|
8581
|
+
}
|
|
8582
|
+
deps.log(formatStatusTTY(result));
|
|
8583
|
+
}
|
|
8584
|
+
async function runGadsTestCommand(_options, dependencies = {}) {
|
|
8585
|
+
const deps = { ...createDefaultDependencies(), ...dependencies };
|
|
8586
|
+
const envSettings = deps.readEnvSettings();
|
|
8587
|
+
const secrets = await deps.loadSecrets({ disableKeytar: envSettings.disableKeytar });
|
|
8588
|
+
const devToken = envSettings.googleAdsDeveloperToken ?? secrets.googleAdsDeveloperToken;
|
|
8589
|
+
const clientId = envSettings.googleAdsClientId ?? secrets.googleAdsClientId;
|
|
8590
|
+
const clientSecret = envSettings.googleAdsClientSecret ?? secrets.googleAdsClientSecret;
|
|
8591
|
+
const refreshToken = envSettings.googleAdsRefreshToken ?? secrets.googleAdsRefreshToken;
|
|
8592
|
+
const customerId = envSettings.googleAdsCustomerId ?? secrets.googleAdsCustomerId;
|
|
8593
|
+
const loginCustomerId = envSettings.googleAdsLoginCustomerId ?? secrets.googleAdsLoginCustomerId;
|
|
8594
|
+
const missing = [];
|
|
8595
|
+
if (!devToken) missing.push("googleAdsDeveloperToken");
|
|
8596
|
+
if (!clientId) missing.push("googleAdsClientId");
|
|
8597
|
+
if (!clientSecret) missing.push("googleAdsClientSecret");
|
|
8598
|
+
if (!refreshToken) missing.push("googleAdsRefreshToken");
|
|
8599
|
+
if (!customerId) missing.push("googleAdsCustomerId");
|
|
8600
|
+
if (missing.length > 0) {
|
|
8601
|
+
const setCommands = missing.map((k) => `ideon config set ${k} <value>`).join("\n ");
|
|
8602
|
+
throw new ReportedError(
|
|
8603
|
+
`Missing required Google Ads credentials:
|
|
8604
|
+
${missing.join(", ")}
|
|
8605
|
+
|
|
8606
|
+
Set them via:
|
|
8607
|
+
${setCommands}
|
|
8608
|
+
|
|
8609
|
+
Or run \`ideon gads login\` for guided setup.`
|
|
8610
|
+
);
|
|
8611
|
+
}
|
|
8612
|
+
const client = new GkpClient({
|
|
8613
|
+
developerToken: devToken,
|
|
8614
|
+
clientId,
|
|
8615
|
+
clientSecret,
|
|
8616
|
+
refreshToken,
|
|
8617
|
+
customerId,
|
|
8618
|
+
loginCustomerId: loginCustomerId || void 0
|
|
8619
|
+
});
|
|
8620
|
+
try {
|
|
8621
|
+
const result = await client.generateKeywordIdeas({
|
|
8622
|
+
seedKeywords: ["test"],
|
|
8623
|
+
pageSize: 1
|
|
8624
|
+
});
|
|
8625
|
+
deps.log(`\u2713 Google Ads credentials verified.`);
|
|
8626
|
+
deps.log(` Customer ID: ${customerId}`);
|
|
8627
|
+
deps.log(` API response received successfully (${result.count} keyword${result.count === 1 ? "" : "s"} returned).`);
|
|
8628
|
+
} catch (error) {
|
|
8629
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
8630
|
+
throw new ReportedError(
|
|
8631
|
+
`Google Ads credentials test failed:
|
|
8632
|
+
${message}
|
|
8633
|
+
|
|
8634
|
+
Run \`ideon gads status\` to check configuration, or \`ideon gads login\` to re-authorize.`
|
|
8635
|
+
);
|
|
8636
|
+
}
|
|
8637
|
+
}
|
|
8638
|
+
|
|
8639
|
+
// src/cli/commands/gkp.ts
|
|
8640
|
+
function createDefaultDependencies2() {
|
|
8641
|
+
return {
|
|
8642
|
+
log: (message) => console.log(message),
|
|
8643
|
+
readEnvSettings,
|
|
8644
|
+
loadSecrets,
|
|
8645
|
+
GkpClientFactory: (options) => new GkpClient(options)
|
|
8646
|
+
};
|
|
8647
|
+
}
|
|
8648
|
+
function parseCommaSeparated(value2) {
|
|
8649
|
+
if (!value2) return void 0;
|
|
8650
|
+
const items = value2.split(",").map((s) => s.trim()).filter(Boolean);
|
|
8651
|
+
return items.length > 0 ? items : void 0;
|
|
8652
|
+
}
|
|
8653
|
+
async function createClient(deps) {
|
|
8654
|
+
const envSettings = deps.readEnvSettings();
|
|
8655
|
+
const secrets = await deps.loadSecrets({ disableKeytar: envSettings.disableKeytar });
|
|
8656
|
+
const devToken = envSettings.googleAdsDeveloperToken ?? secrets.googleAdsDeveloperToken;
|
|
8657
|
+
const clientId = envSettings.googleAdsClientId ?? secrets.googleAdsClientId;
|
|
8658
|
+
const clientSecret = envSettings.googleAdsClientSecret ?? secrets.googleAdsClientSecret;
|
|
8659
|
+
const refreshToken = envSettings.googleAdsRefreshToken ?? secrets.googleAdsRefreshToken;
|
|
8660
|
+
const customerId = envSettings.googleAdsCustomerId ?? secrets.googleAdsCustomerId;
|
|
8661
|
+
const loginCustomerId = envSettings.googleAdsLoginCustomerId ?? secrets.googleAdsLoginCustomerId;
|
|
8662
|
+
const missing = [];
|
|
8663
|
+
if (!devToken) missing.push("googleAdsDeveloperToken");
|
|
8664
|
+
if (!clientId) missing.push("googleAdsClientId");
|
|
8665
|
+
if (!clientSecret) missing.push("googleAdsClientSecret");
|
|
8666
|
+
if (!refreshToken) missing.push("googleAdsRefreshToken");
|
|
8667
|
+
if (!customerId) missing.push("googleAdsCustomerId");
|
|
8668
|
+
if (missing.length > 0) {
|
|
8669
|
+
const setCommands = missing.map((k) => `ideon config set ${k} <value>`).join("\n ");
|
|
8670
|
+
throw new ReportedError(
|
|
8671
|
+
`Missing required Google Ads credentials:
|
|
8672
|
+
${missing.join(", ")}
|
|
8673
|
+
|
|
8674
|
+
Set them via:
|
|
8675
|
+
${setCommands}
|
|
8676
|
+
|
|
8677
|
+
Or run \`ideon gads login\` for guided setup.`
|
|
8678
|
+
);
|
|
8679
|
+
}
|
|
8680
|
+
return deps.GkpClientFactory({
|
|
8681
|
+
developerToken: devToken,
|
|
8682
|
+
clientId,
|
|
8683
|
+
clientSecret,
|
|
8684
|
+
refreshToken,
|
|
8685
|
+
customerId,
|
|
8686
|
+
loginCustomerId: loginCustomerId || void 0
|
|
8687
|
+
});
|
|
8688
|
+
}
|
|
8689
|
+
function microsToDollars(micros) {
|
|
8690
|
+
const dollars = micros / 1e6;
|
|
8691
|
+
return `$${dollars.toFixed(2)}`;
|
|
8692
|
+
}
|
|
8693
|
+
function formatIdeasTTY(result) {
|
|
8694
|
+
if (result.ideas.length === 0) {
|
|
8695
|
+
return "No keyword ideas found.";
|
|
8696
|
+
}
|
|
8697
|
+
const header = "Keyword".padEnd(40) + "Searches".padStart(14) + "Competition".padStart(14) + "Low Bid".padStart(12) + "High Bid".padStart(12);
|
|
8698
|
+
const divider = "\u2500".repeat(header.length);
|
|
8699
|
+
const rows = result.ideas.map((idea) => {
|
|
8700
|
+
const text = idea.text.length > 38 ? idea.text.slice(0, 35) + "..." : idea.text;
|
|
8701
|
+
return text.padEnd(40) + idea.avgMonthlySearches.toLocaleString().padStart(14) + idea.competition.padStart(14) + microsToDollars(idea.lowTopOfPageBidMicros).padStart(12) + microsToDollars(idea.highTopOfPageBidMicros).padStart(12);
|
|
8702
|
+
});
|
|
8703
|
+
return ["", "Keyword Ideas", divider, header, divider, ...rows, divider, `Total: ${result.count} keyword${result.count === 1 ? "" : "s"}`, ""].join("\n");
|
|
8704
|
+
}
|
|
8705
|
+
function formatHistoricalTTY(result) {
|
|
8706
|
+
if (result.keywords.length === 0) {
|
|
8707
|
+
return "No historical data found.";
|
|
8708
|
+
}
|
|
8709
|
+
const header = "Keyword".padEnd(40) + "Avg Monthly".padStart(14) + "Competition".padStart(14) + "Low Bid".padStart(12) + "High Bid".padStart(12);
|
|
8710
|
+
const divider = "\u2500".repeat(header.length);
|
|
8711
|
+
const rows = result.keywords.map((kw) => {
|
|
8712
|
+
const text = kw.text.length > 38 ? kw.text.slice(0, 35) + "..." : kw.text;
|
|
8713
|
+
return text.padEnd(40) + kw.avgMonthlySearches.toLocaleString().padStart(14) + kw.competition.padStart(14) + microsToDollars(kw.lowTopOfPageBidMicros).padStart(12) + microsToDollars(kw.highTopOfPageBidMicros).padStart(12);
|
|
8714
|
+
});
|
|
8715
|
+
return ["", "Historical Metrics", divider, header, divider, ...rows, divider, `Total: ${result.count} keyword${result.count === 1 ? "" : "s"}`, ""].join("\n");
|
|
8716
|
+
}
|
|
8717
|
+
function formatForecastTTY(result) {
|
|
8718
|
+
if (result.keywords.length === 0) {
|
|
8719
|
+
return "No forecast data found.";
|
|
8720
|
+
}
|
|
8721
|
+
const header = "Keyword".padEnd(32) + "Match".padStart(8) + "Impr.".padStart(10) + "Clicks".padStart(10) + "Cost".padStart(12) + "CTR".padStart(8);
|
|
8722
|
+
const divider = "\u2500".repeat(header.length);
|
|
8723
|
+
const rows = result.keywords.map((kw) => {
|
|
8724
|
+
const text = kw.text.length > 30 ? kw.text.slice(0, 27) + "..." : kw.text;
|
|
8725
|
+
return text.padEnd(32) + kw.matchType.padStart(8) + kw.impressions.toLocaleString().padStart(10) + kw.clicks.toLocaleString().padStart(10) + microsToDollars(kw.costMicros).padStart(12) + `${(kw.ctr * 100).toFixed(1)}%`.padStart(8);
|
|
8726
|
+
});
|
|
8727
|
+
return ["", "Forecast", divider, header, divider, ...rows, divider, `Total: ${result.count} keyword${result.count === 1 ? "" : "s"}`, ""].join("\n");
|
|
8728
|
+
}
|
|
8729
|
+
async function runGkpIdeasCommand(options, dependencies = {}) {
|
|
8730
|
+
const deps = { ...createDefaultDependencies2(), ...dependencies };
|
|
8731
|
+
const seedKeywords = parseCommaSeparated(options.keywords);
|
|
8732
|
+
const url = options.url || void 0;
|
|
8733
|
+
const site = options.site || void 0;
|
|
8734
|
+
const countryCodes = parseCommaSeparated(options.country);
|
|
8735
|
+
if (!seedKeywords && !url) {
|
|
8736
|
+
throw new ReportedError("At least one of --keywords or --url is required.");
|
|
8737
|
+
}
|
|
8738
|
+
const client = await createClient(deps);
|
|
8739
|
+
try {
|
|
8740
|
+
const result = await client.generateKeywordIdeas({
|
|
8741
|
+
seedKeywords: seedKeywords || void 0,
|
|
8742
|
+
url,
|
|
8743
|
+
site,
|
|
8744
|
+
countryCodes,
|
|
8745
|
+
language: options.language,
|
|
8746
|
+
pageSize: options.pageSize
|
|
8747
|
+
});
|
|
8748
|
+
if (options.json) {
|
|
8749
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
8750
|
+
} else {
|
|
8751
|
+
deps.log(formatIdeasTTY(result));
|
|
8752
|
+
}
|
|
8753
|
+
} catch (error) {
|
|
8754
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
8755
|
+
throw new ReportedError(`Failed to generate keyword ideas:
|
|
8756
|
+
${message}`);
|
|
8757
|
+
}
|
|
8758
|
+
}
|
|
8759
|
+
async function runGkpHistoricalCommand(options, dependencies = {}) {
|
|
8760
|
+
const deps = { ...createDefaultDependencies2(), ...dependencies };
|
|
8761
|
+
const keywords = parseCommaSeparated(options.keywords);
|
|
8762
|
+
if (!keywords || keywords.length === 0) {
|
|
8763
|
+
throw new ReportedError("--keywords is required.");
|
|
8764
|
+
}
|
|
8765
|
+
const countryCodes = parseCommaSeparated(options.country);
|
|
8766
|
+
const client = await createClient(deps);
|
|
8767
|
+
try {
|
|
8768
|
+
const result = await client.getHistoricalMetrics({
|
|
8769
|
+
keywords,
|
|
8770
|
+
countryCodes,
|
|
8771
|
+
language: options.language,
|
|
8772
|
+
includeAverageCpc: options.includeCpc
|
|
8773
|
+
});
|
|
8774
|
+
if (options.json) {
|
|
8775
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
8776
|
+
} else {
|
|
8777
|
+
deps.log(formatHistoricalTTY(result));
|
|
8778
|
+
}
|
|
8779
|
+
} catch (error) {
|
|
8780
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
8781
|
+
throw new ReportedError(`Failed to get historical data:
|
|
8782
|
+
${message}`);
|
|
8783
|
+
}
|
|
8784
|
+
}
|
|
8785
|
+
async function runGkpForecastCommand(options, dependencies = {}) {
|
|
8786
|
+
const deps = { ...createDefaultDependencies2(), ...dependencies };
|
|
8787
|
+
const keywords = parseCommaSeparated(options.keywords);
|
|
8788
|
+
if (!keywords || keywords.length === 0) {
|
|
8789
|
+
throw new ReportedError("--keywords is required.");
|
|
8790
|
+
}
|
|
8791
|
+
const countryCodes = parseCommaSeparated(options.country);
|
|
8792
|
+
const client = await createClient(deps);
|
|
8793
|
+
try {
|
|
8794
|
+
const result = await client.getForecastData({
|
|
8795
|
+
keywords,
|
|
8796
|
+
keywordMatchType: options.matchType,
|
|
8797
|
+
maxCpcBidMicros: options.maxCpcBid,
|
|
8798
|
+
countryCodes,
|
|
8799
|
+
language: options.language,
|
|
8800
|
+
startDate: options.startDate,
|
|
8801
|
+
endDate: options.endDate
|
|
8802
|
+
});
|
|
8803
|
+
if (options.json) {
|
|
8804
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
8805
|
+
} else {
|
|
8806
|
+
deps.log(formatForecastTTY(result));
|
|
8807
|
+
}
|
|
8808
|
+
} catch (error) {
|
|
8809
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
8810
|
+
throw new ReportedError(`Failed to get forecast data:
|
|
8811
|
+
${message}`);
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
|
|
7403
8815
|
// src/cli/commands/settings.tsx
|
|
7404
8816
|
import { render } from "ink";
|
|
7405
8817
|
|
|
@@ -7593,7 +9005,7 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
|
7593
9005
|
const [showModelSelect, setShowModelSelect] = useState(false);
|
|
7594
9006
|
const [menuMode, setMenuMode] = useState("main");
|
|
7595
9007
|
const currentModelEntry = getLimnGenerationModels().find((m) => m.family === settings.t2i.modelId) ?? getLimnGenerationModels()[0];
|
|
7596
|
-
useInput((
|
|
9008
|
+
useInput((input2, key) => {
|
|
7597
9009
|
if (key.escape) {
|
|
7598
9010
|
if (editing) {
|
|
7599
9011
|
setEditing(null);
|
|
@@ -7608,7 +9020,7 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
|
7608
9020
|
return;
|
|
7609
9021
|
}
|
|
7610
9022
|
}
|
|
7611
|
-
if (key.ctrl &&
|
|
9023
|
+
if (key.ctrl && input2 === "c") {
|
|
7612
9024
|
onDone(null);
|
|
7613
9025
|
exit();
|
|
7614
9026
|
}
|
|
@@ -7816,7 +9228,7 @@ async function openSettings() {
|
|
|
7816
9228
|
} catch (error) {
|
|
7817
9229
|
if (error instanceof KeytarUnavailableError) {
|
|
7818
9230
|
console.log("Settings saved, but secrets were not stored in the system keychain.");
|
|
7819
|
-
console.log("Use
|
|
9231
|
+
console.log("Use TELEPAT_OPENROUTER_KEY and TELEPAT_REPLICATE_TOKEN in this environment.");
|
|
7820
9232
|
return;
|
|
7821
9233
|
}
|
|
7822
9234
|
throw error;
|
|
@@ -7829,15 +9241,15 @@ import path14 from "path";
|
|
|
7829
9241
|
import { spawn } from "child_process";
|
|
7830
9242
|
|
|
7831
9243
|
// src/server/previewServer.ts
|
|
7832
|
-
import { execFile } from "child_process";
|
|
7833
|
-
import { promisify } from "util";
|
|
9244
|
+
import { execFile as execFile2 } from "child_process";
|
|
9245
|
+
import { promisify as promisify2 } from "util";
|
|
7834
9246
|
import { readFile as readFile11, stat as stat6 } from "fs/promises";
|
|
7835
9247
|
import { watch as fsWatch } from "fs";
|
|
7836
9248
|
import path13 from "path";
|
|
7837
9249
|
import { fileURLToPath } from "url";
|
|
7838
9250
|
import express from "express";
|
|
7839
9251
|
import { marked } from "marked";
|
|
7840
|
-
var
|
|
9252
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
7841
9253
|
var MissingArticleError = class extends Error {
|
|
7842
9254
|
constructor(message) {
|
|
7843
9255
|
super(message);
|
|
@@ -7990,12 +9402,12 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7990
9402
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
7991
9403
|
}
|
|
7992
9404
|
const sourcePath = resolveGenerationSourcePath(generation, markdownOutputDir);
|
|
7993
|
-
const canonicalSlug = generation.outputs.find((
|
|
9405
|
+
const canonicalSlug = generation.outputs.find((output2) => output2.contentType === generation.primaryContentType)?.slug ?? generation.outputs[0]?.slug ?? generation.id;
|
|
7994
9406
|
const outputs = await Promise.all(
|
|
7995
|
-
generation.outputs.map(async (
|
|
9407
|
+
generation.outputs.map(async (output2) => {
|
|
7996
9408
|
let markdown = "";
|
|
7997
9409
|
try {
|
|
7998
|
-
markdown = await readFile11(
|
|
9410
|
+
markdown = await readFile11(output2.sourcePath, "utf8");
|
|
7999
9411
|
} catch (error) {
|
|
8000
9412
|
if (isMissingFileError(error)) {
|
|
8001
9413
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
@@ -8003,13 +9415,13 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
8003
9415
|
throw error;
|
|
8004
9416
|
}
|
|
8005
9417
|
return {
|
|
8006
|
-
id:
|
|
8007
|
-
contentType:
|
|
8008
|
-
contentTypeLabel:
|
|
8009
|
-
index:
|
|
9418
|
+
id: output2.id,
|
|
9419
|
+
contentType: output2.contentType,
|
|
9420
|
+
contentTypeLabel: output2.contentTypeLabel,
|
|
9421
|
+
index: output2.index,
|
|
8010
9422
|
slug: canonicalSlug,
|
|
8011
|
-
title:
|
|
8012
|
-
htmlBody: await renderArticleHtml(markdown, generationId,
|
|
9423
|
+
title: output2.title,
|
|
9424
|
+
htmlBody: await renderArticleHtml(markdown, generationId, output2.sourcePath)
|
|
8013
9425
|
};
|
|
8014
9426
|
})
|
|
8015
9427
|
);
|
|
@@ -8044,7 +9456,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
|
|
|
8044
9456
|
};
|
|
8045
9457
|
}
|
|
8046
9458
|
function resolveGenerationSourcePath(generation, markdownOutputDir) {
|
|
8047
|
-
return generation.outputs.find((
|
|
9459
|
+
return generation.outputs.find((output2) => output2.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path13.join(markdownOutputDir, generation.id);
|
|
8048
9460
|
}
|
|
8049
9461
|
function isMissingFileError(error) {
|
|
8050
9462
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
@@ -9664,14 +11076,14 @@ async function tryOpenBrowser(url) {
|
|
|
9664
11076
|
}
|
|
9665
11077
|
try {
|
|
9666
11078
|
if (process.platform === "darwin") {
|
|
9667
|
-
await
|
|
11079
|
+
await execFileAsync2("open", [url]);
|
|
9668
11080
|
return;
|
|
9669
11081
|
}
|
|
9670
11082
|
if (process.platform === "win32") {
|
|
9671
|
-
await
|
|
11083
|
+
await execFileAsync2("cmd", ["/c", "start", "", url]);
|
|
9672
11084
|
return;
|
|
9673
11085
|
}
|
|
9674
|
-
await
|
|
11086
|
+
await execFileAsync2("xdg-open", [url]);
|
|
9675
11087
|
} catch {
|
|
9676
11088
|
}
|
|
9677
11089
|
}
|
|
@@ -9725,7 +11137,7 @@ async function runServeCommand(options) {
|
|
|
9725
11137
|
// src/cli/commands/write.tsx
|
|
9726
11138
|
import React4, { useEffect as useEffect3, useState as useState4 } from "react";
|
|
9727
11139
|
import { render as render2, useApp as useApp3 } from "ink";
|
|
9728
|
-
import { createInterface } from "readline/promises";
|
|
11140
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
9729
11141
|
|
|
9730
11142
|
// src/cli/ui/pipelinePresenter.tsx
|
|
9731
11143
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
@@ -10198,17 +11610,17 @@ function formatPipelineStageCost(stage) {
|
|
|
10198
11610
|
}
|
|
10199
11611
|
return stage.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
10200
11612
|
}
|
|
10201
|
-
async function renderPlainPipeline(
|
|
11613
|
+
async function renderPlainPipeline(input2, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages) {
|
|
10202
11614
|
let previousStages = /* @__PURE__ */ new Map();
|
|
10203
11615
|
let previousItemStatuses = /* @__PURE__ */ new Map();
|
|
10204
|
-
const notificationsEnabled =
|
|
11616
|
+
const notificationsEnabled = input2.config.settings.notifications.enabled;
|
|
10205
11617
|
try {
|
|
10206
11618
|
await notifyWriteStarted({
|
|
10207
11619
|
enabled: notificationsEnabled,
|
|
10208
|
-
idea:
|
|
11620
|
+
idea: input2.idea,
|
|
10209
11621
|
runMode
|
|
10210
11622
|
});
|
|
10211
|
-
const result = await runPipelineShell(
|
|
11623
|
+
const result = await runPipelineShell(input2, {
|
|
10212
11624
|
dryRun,
|
|
10213
11625
|
enrichLinks: enrichLinks2,
|
|
10214
11626
|
runMode,
|
|
@@ -10338,8 +11750,8 @@ function WriteOptionsFlow({
|
|
|
10338
11750
|
};
|
|
10339
11751
|
});
|
|
10340
11752
|
};
|
|
10341
|
-
useInput2((
|
|
10342
|
-
if (key.ctrl &&
|
|
11753
|
+
useInput2((input2, key) => {
|
|
11754
|
+
if (key.ctrl && input2 === "c") {
|
|
10343
11755
|
onDone(null);
|
|
10344
11756
|
exit();
|
|
10345
11757
|
return;
|
|
@@ -10355,7 +11767,7 @@ function WriteOptionsFlow({
|
|
|
10355
11767
|
setCursor((current) => (current + 1) % secondarySelections.length);
|
|
10356
11768
|
return;
|
|
10357
11769
|
}
|
|
10358
|
-
if (
|
|
11770
|
+
if (input2 === " ") {
|
|
10359
11771
|
setSecondarySelections(
|
|
10360
11772
|
(current) => current.map(
|
|
10361
11773
|
(item, index) => index === cursor && item.contentType !== primaryType ? {
|
|
@@ -10571,7 +11983,7 @@ function WriteOptionsFlow({
|
|
|
10571
11983
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
10572
11984
|
var USER_INTERRUPTED_MESSAGE = "Write interrupted by user. Run `ideon write resume` to continue from the last checkpoint.";
|
|
10573
11985
|
function WriteApp({
|
|
10574
|
-
input,
|
|
11986
|
+
input: input2,
|
|
10575
11987
|
dryRun,
|
|
10576
11988
|
enrichLinks: enrichLinks2,
|
|
10577
11989
|
runMode,
|
|
@@ -10593,11 +12005,11 @@ function WriteApp({
|
|
|
10593
12005
|
void (async () => {
|
|
10594
12006
|
try {
|
|
10595
12007
|
await notifyWriteStarted({
|
|
10596
|
-
enabled:
|
|
10597
|
-
idea:
|
|
12008
|
+
enabled: input2.config.settings.notifications.enabled,
|
|
12009
|
+
idea: input2.idea,
|
|
10598
12010
|
runMode
|
|
10599
12011
|
});
|
|
10600
|
-
const runResult = await runPipelineShell(
|
|
12012
|
+
const runResult = await runPipelineShell(input2, {
|
|
10601
12013
|
dryRun,
|
|
10602
12014
|
enrichLinks: enrichLinks2,
|
|
10603
12015
|
runMode,
|
|
@@ -10617,7 +12029,7 @@ function WriteApp({
|
|
|
10617
12029
|
setResult(runResult);
|
|
10618
12030
|
onSuccess?.(runResult);
|
|
10619
12031
|
await notifyWriteSucceeded({
|
|
10620
|
-
enabled:
|
|
12032
|
+
enabled: input2.config.settings.notifications.enabled,
|
|
10621
12033
|
title: runResult.artifact.title,
|
|
10622
12034
|
slug: runResult.artifact.slug
|
|
10623
12035
|
});
|
|
@@ -10630,7 +12042,7 @@ function WriteApp({
|
|
|
10630
12042
|
setErrorMessage(messageWithResumeHint);
|
|
10631
12043
|
onError(new Error(messageWithResumeHint));
|
|
10632
12044
|
await notifyWriteFailed({
|
|
10633
|
-
enabled:
|
|
12045
|
+
enabled: input2.config.settings.notifications.enabled,
|
|
10634
12046
|
message: messageWithResumeHint
|
|
10635
12047
|
});
|
|
10636
12048
|
}
|
|
@@ -10638,7 +12050,7 @@ function WriteApp({
|
|
|
10638
12050
|
return () => {
|
|
10639
12051
|
mounted = false;
|
|
10640
12052
|
};
|
|
10641
|
-
}, [dryRun, enrichLinks2,
|
|
12053
|
+
}, [dryRun, enrichLinks2, input2, links, unlinks, maxLinks, maxImages, onError, runMode]);
|
|
10642
12054
|
useEffect3(() => {
|
|
10643
12055
|
if (!result && !errorMessage2) {
|
|
10644
12056
|
return;
|
|
@@ -10650,11 +12062,11 @@ function WriteApp({
|
|
|
10650
12062
|
clearTimeout(exitTimer);
|
|
10651
12063
|
};
|
|
10652
12064
|
}, [errorMessage2, exit, result]);
|
|
10653
|
-
return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt:
|
|
12065
|
+
return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt: input2.idea, stages, result, errorMessage: errorMessage2 });
|
|
10654
12066
|
}
|
|
10655
12067
|
async function runWriteCommand(options) {
|
|
10656
|
-
const
|
|
10657
|
-
await runWritePipeline(
|
|
12068
|
+
const input2 = await resolveInputWithInteractiveIdeaFallback(options);
|
|
12069
|
+
await runWritePipeline(input2, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks, options.maxImages, options.exportPath);
|
|
10658
12070
|
}
|
|
10659
12071
|
async function runWriteResumeCommand(options = {}) {
|
|
10660
12072
|
const session = await loadWriteSession();
|
|
@@ -10668,7 +12080,7 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
10668
12080
|
idea: session.idea,
|
|
10669
12081
|
audience: session.targetAudienceHint ?? void 0
|
|
10670
12082
|
});
|
|
10671
|
-
const
|
|
12083
|
+
const input2 = {
|
|
10672
12084
|
...resolved,
|
|
10673
12085
|
job: session.job,
|
|
10674
12086
|
config: {
|
|
@@ -10676,9 +12088,9 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
10676
12088
|
secrets: resolved.config.secrets
|
|
10677
12089
|
}
|
|
10678
12090
|
};
|
|
10679
|
-
await runWritePipeline(
|
|
12091
|
+
await runWritePipeline(input2, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks, options.maxImages, options.exportPath);
|
|
10680
12092
|
}
|
|
10681
|
-
async function runWritePipeline(
|
|
12093
|
+
async function runWritePipeline(input2, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages, exportPath) {
|
|
10682
12094
|
let interruptHandled = false;
|
|
10683
12095
|
const handleSignal = (signal) => {
|
|
10684
12096
|
if (interruptHandled) {
|
|
@@ -10688,7 +12100,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10688
12100
|
void (async () => {
|
|
10689
12101
|
try {
|
|
10690
12102
|
await notifyWriteCanceled({
|
|
10691
|
-
enabled:
|
|
12103
|
+
enabled: input2.config.settings.notifications.enabled,
|
|
10692
12104
|
signal
|
|
10693
12105
|
});
|
|
10694
12106
|
await recordInterruptedWrite(signal);
|
|
@@ -10712,7 +12124,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10712
12124
|
process.on("SIGTERM", onSigterm);
|
|
10713
12125
|
try {
|
|
10714
12126
|
if (noInteractive || !process.stdout.isTTY) {
|
|
10715
|
-
const result = await renderPlainPipeline(
|
|
12127
|
+
const result = await renderPlainPipeline(input2, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
|
|
10716
12128
|
if (exportPath) {
|
|
10717
12129
|
await runOutputCommand({
|
|
10718
12130
|
generationId: result.artifact.slug,
|
|
@@ -10727,7 +12139,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10727
12139
|
/* @__PURE__ */ jsx7(
|
|
10728
12140
|
WriteApp,
|
|
10729
12141
|
{
|
|
10730
|
-
input,
|
|
12142
|
+
input: input2,
|
|
10731
12143
|
dryRun,
|
|
10732
12144
|
enrichLinks: enrichLinks2,
|
|
10733
12145
|
runMode,
|
|
@@ -10884,20 +12296,20 @@ function isMissingIdeaError(error) {
|
|
|
10884
12296
|
return error.message.startsWith("No idea provided.");
|
|
10885
12297
|
}
|
|
10886
12298
|
async function promptForIdea() {
|
|
10887
|
-
const
|
|
12299
|
+
const readline2 = createInterface2({
|
|
10888
12300
|
input: process.stdin,
|
|
10889
12301
|
output: process.stdout
|
|
10890
12302
|
});
|
|
10891
12303
|
try {
|
|
10892
12304
|
while (true) {
|
|
10893
|
-
const idea = (await
|
|
12305
|
+
const idea = (await readline2.question("Enter primary content prompt: ")).trim();
|
|
10894
12306
|
if (idea.length > 0) {
|
|
10895
12307
|
return idea;
|
|
10896
12308
|
}
|
|
10897
12309
|
console.error("Prompt cannot be empty.");
|
|
10898
12310
|
}
|
|
10899
12311
|
} finally {
|
|
10900
|
-
|
|
12312
|
+
readline2.close();
|
|
10901
12313
|
}
|
|
10902
12314
|
}
|
|
10903
12315
|
async function autoExport(exportPath, result) {
|
|
@@ -10934,6 +12346,36 @@ async function runCli(argv) {
|
|
|
10934
12346
|
program.command("mcp").description("Model Context Protocol server operations.").command("serve").description("Start the Ideon MCP server over stdio transport.").action(async () => {
|
|
10935
12347
|
await runMcpServeCommand();
|
|
10936
12348
|
});
|
|
12349
|
+
const gadsCommand = program.command("gads").description("Manage Google Ads integration credentials and verification.");
|
|
12350
|
+
gadsCommand.command("login").description("Start OAuth flow to obtain Google Ads tokens.").option("--force", "Re-authorize even if a refresh token already exists", false).option("--developer-token <token>", "Google Ads developer token").option("--client-id <id>", "OAuth2 client ID").option("--client-secret <secret>", "OAuth2 client secret").option("--customer-id <id>", "Google Ads customer ID (10 digits)").option("--login-customer-id <id>", "Manager account customer ID (MCC only)").action(async (options) => {
|
|
12351
|
+
await runGadsLoginCommand({
|
|
12352
|
+
force: options.force,
|
|
12353
|
+
developerToken: options.developerToken,
|
|
12354
|
+
clientId: options.clientId,
|
|
12355
|
+
clientSecret: options.clientSecret,
|
|
12356
|
+
customerId: options.customerId,
|
|
12357
|
+
loginCustomerId: options.loginCustomerId
|
|
12358
|
+
});
|
|
12359
|
+
});
|
|
12360
|
+
gadsCommand.command("logout").description("Clear stored Google Ads credentials.").option("--all", "Clear all Google Ads credentials instead of just the refresh token", false).action(async (options) => {
|
|
12361
|
+
await runGadsLogoutCommand({ all: options.all });
|
|
12362
|
+
});
|
|
12363
|
+
gadsCommand.command("status").description("Show which Google Ads credentials are configured.").option("--json", "Print machine-readable JSON output", false).action(async (options) => {
|
|
12364
|
+
await runGadsStatusCommand({ json: options.json });
|
|
12365
|
+
});
|
|
12366
|
+
gadsCommand.command("test").description("Verify Google Ads credentials by making a test API call.").action(async () => {
|
|
12367
|
+
await runGadsTestCommand({});
|
|
12368
|
+
});
|
|
12369
|
+
const gkpCommand = program.command("gkp").description("Query Google Ads Keyword Planner data.");
|
|
12370
|
+
gkpCommand.command("ideas").description("Generate keyword ideas from seed keywords, a URL, or a site.").option("--keywords <keywords>", "Comma-separated seed keywords").option("--url <url>", "Seed URL for keyword ideas").option("--site <site>", "Seed site domain (exclusive with keywords/url)").option("--country <codes>", "Comma-separated ISO country codes (omit for all countries)").option("--language <code>", "ISO 639-1 language code (default: en)").option("--page-size <n>", "Number of results per page", (v) => Number.parseInt(v, 10)).option("--json", "Print machine-readable JSON output", false).action(async (options) => {
|
|
12371
|
+
await runGkpIdeasCommand(options);
|
|
12372
|
+
});
|
|
12373
|
+
gkpCommand.command("historical").description("Get historical search volume and competition metrics for keywords.").requiredOption("--keywords <keywords>", "Comma-separated keywords to look up").option("--country <codes>", "Comma-separated ISO country codes (omit for all countries)").option("--language <code>", "ISO 639-1 language code (default: en)").option("--no-include-cpc", "Exclude average CPC from results").option("--json", "Print machine-readable JSON output", false).action(async (options) => {
|
|
12374
|
+
await runGkpHistoricalCommand({ ...options, includeCpc: options.includeCpc });
|
|
12375
|
+
});
|
|
12376
|
+
gkpCommand.command("forecast").description("Get projected impressions, clicks, and cost for keywords.").requiredOption("--keywords <keywords>", "Comma-separated keywords to forecast").option("--match-type <type>", "Keyword match type: BROAD, EXACT, or PHRASE", "BROAD").option("--max-cpc-bid <micros>", "Max CPC bid in micros", (v) => Number.parseInt(v, 10)).option("--country <codes>", "Comma-separated ISO country codes (default: US)").option("--language <code>", "ISO 639-1 language code (default: en)").option("--start-date <date>", "Forecast start date (YYYY-MM-DD)").option("--end-date <date>", "Forecast end date (YYYY-MM-DD)").option("--json", "Print machine-readable JSON output", false).action(async (options) => {
|
|
12377
|
+
await runGkpForecastCommand(options);
|
|
12378
|
+
});
|
|
10937
12379
|
const agentCommand = program.command("agent").description("Manage local agent integration runtime registrations.");
|
|
10938
12380
|
agentCommand.command("install").description("Install a runtime integration profile (CLI/MCP only).").argument("<runtime>", "Runtime id (claude, chatgpt, gemini, generic-mcp)").option("--dry-run", "Preview actions without writing state", false).action(async (runtime, options) => {
|
|
10939
12381
|
await runAgentInstallCommand({ runtime, dryRun: options.dryRun });
|