@telepat/ideon 0.1.31 → 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 +8 -3
- package/README.zh-CN.md +4 -2
- package/dist/ideon.js +1737 -263
- 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,
|
|
@@ -2064,6 +2191,9 @@ function intentToGuidePath(intent) {
|
|
|
2064
2191
|
function styleToGuidePath(style) {
|
|
2065
2192
|
return `writing-guide/styles/${style}.md`;
|
|
2066
2193
|
}
|
|
2194
|
+
function seoGuidePath(name) {
|
|
2195
|
+
return `writing-guide/seo/${name}.md`;
|
|
2196
|
+
}
|
|
2067
2197
|
function dedupe(items) {
|
|
2068
2198
|
return Array.from(new Set(items));
|
|
2069
2199
|
}
|
|
@@ -2079,6 +2209,7 @@ function buildPrimaryPlanGuideInstruction(intent, contentType) {
|
|
|
2079
2209
|
"writing-guide/references/headline-writing-systems.md",
|
|
2080
2210
|
"writing-guide/references/ideation-and-credibility-systems.md",
|
|
2081
2211
|
"writing-guide/references/content-frameworks.md",
|
|
2212
|
+
seoGuidePath("on-page-essentials"),
|
|
2082
2213
|
intentToGuidePath(intent),
|
|
2083
2214
|
formatToGuidePath(contentType)
|
|
2084
2215
|
];
|
|
@@ -2098,6 +2229,9 @@ function buildArticleSectionGuideInstruction(style, intent, contentType) {
|
|
|
2098
2229
|
"writing-guide/references/prose-quality-checks.md",
|
|
2099
2230
|
"writing-guide/references/readability-and-pace.md",
|
|
2100
2231
|
"writing-guide/references/skimmability-patterns.md",
|
|
2232
|
+
seoGuidePath("on-page-essentials"),
|
|
2233
|
+
seoGuidePath("eeat-signals"),
|
|
2234
|
+
seoGuidePath("fact-density"),
|
|
2101
2235
|
styleToGuidePath(style),
|
|
2102
2236
|
intentToGuidePath(intent),
|
|
2103
2237
|
formatToGuidePath(contentType)
|
|
@@ -2445,9 +2579,9 @@ function buildLongFormPlanMessages(idea, options) {
|
|
|
2445
2579
|
"",
|
|
2446
2580
|
"Requirements:",
|
|
2447
2581
|
"- The content should feel authoritative, practical, and clearly structured for scanning and deep reading.",
|
|
2448
|
-
"- Generate a memorable title
|
|
2582
|
+
"- Generate a memorable title (under 60 characters) that leads with the primary entity. Include a sharp subtitle that promises a concrete benefit, mechanism, or outcome.",
|
|
2449
2583
|
"- The slug must be lowercase kebab-case and publication-ready.",
|
|
2450
|
-
"- The description should work as a concise meta description and align with the shared content plan.",
|
|
2584
|
+
"- The description should work as a concise meta description (120-160 characters), include the primary entity, and align with the shared content plan.",
|
|
2451
2585
|
`- Plan ${sectionCounts.label} strong sections with distinct focus areas and logical progression (no repetitive section intent).`,
|
|
2452
2586
|
"- Frame section titles to reflect likely search intent or practical reader questions when appropriate.",
|
|
2453
2587
|
"- Each section description should name the mechanism, evidence type, or practical action that makes the section useful.",
|
|
@@ -2468,7 +2602,7 @@ function buildLongFormPlanMessages(idea, options) {
|
|
|
2468
2602
|
`- contentType: set to "${options.contentType}" exactly`,
|
|
2469
2603
|
"- title: string",
|
|
2470
2604
|
"- subtitle: string",
|
|
2471
|
-
"- keywords: array of 3 to 8 strings",
|
|
2605
|
+
"- keywords: array of 3 to 8 specific, non-generic strings representing primary entities and search topics (not exact-match duplicates of heading text)",
|
|
2472
2606
|
"- slug: string in lowercase kebab-case",
|
|
2473
2607
|
"- description: string",
|
|
2474
2608
|
"- introBrief: string",
|
|
@@ -2615,6 +2749,30 @@ async function planPrimaryContent({
|
|
|
2615
2749
|
return shortFormPlanSchema.parse(data);
|
|
2616
2750
|
}
|
|
2617
2751
|
});
|
|
2752
|
+
if (!dryRun) {
|
|
2753
|
+
const seoWarnings = [];
|
|
2754
|
+
if (basePlan.title && basePlan.title.length > 60) {
|
|
2755
|
+
seoWarnings.push(`Title is ${basePlan.title.length} chars (recommended: under 60 for search display safety)`);
|
|
2756
|
+
}
|
|
2757
|
+
if (basePlan.description) {
|
|
2758
|
+
if (basePlan.description.length < 120) {
|
|
2759
|
+
seoWarnings.push(`Description is ${basePlan.description.length} chars (recommended: 120-160 for meta description effectiveness)`);
|
|
2760
|
+
} else if (basePlan.description.length > 160) {
|
|
2761
|
+
seoWarnings.push(`Description is ${basePlan.description.length} chars (recommended: 120-160 for meta description effectiveness)`);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
if (isLongForm) {
|
|
2765
|
+
const longPlan = basePlan;
|
|
2766
|
+
const headings = longPlan.sections.map((s) => s.title.toLowerCase());
|
|
2767
|
+
const duplicateKeywords = longPlan.keywords.filter((kw) => headings.includes(kw.toLowerCase()));
|
|
2768
|
+
if (duplicateKeywords.length > 0) {
|
|
2769
|
+
seoWarnings.push(`Keywords duplicate heading text: ${duplicateKeywords.join(", ")} (consider using semantic variants)`);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
for (const warning of seoWarnings) {
|
|
2773
|
+
console.warn(`[seo] ${warning}`);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2618
2776
|
const normalizedSlug = slugify(basePlan.slug || basePlan.title);
|
|
2619
2777
|
const uniqueSlug = await resolveUniqueSlug(markdownOutputDir, normalizedSlug);
|
|
2620
2778
|
if (isLongForm) {
|
|
@@ -2949,7 +3107,8 @@ function buildSectionMessages(plan, section, articleSoFar, style, intent, conten
|
|
|
2949
3107
|
"Requirements:",
|
|
2950
3108
|
`- ${paragraphCount} paragraphs.`,
|
|
2951
3109
|
`- Target length: about ${sectionTargetWords} words.`,
|
|
2952
|
-
"- Be concrete and specific.",
|
|
3110
|
+
"- Be concrete and specific. Support key claims with statistics, data points, or authoritative citations.",
|
|
3111
|
+
"- Include at least one practical insight that sounds like first-hand practitioner experience.",
|
|
2953
3112
|
"- Continue naturally from the article draft so far without rehashing prior sections.",
|
|
2954
3113
|
"- Use short Markdown lists only if they materially improve clarity."
|
|
2955
3114
|
].join("\n")
|
|
@@ -3232,13 +3391,13 @@ async function withRetry(op, opts) {
|
|
|
3232
3391
|
}
|
|
3233
3392
|
throw buildFinalError(opts.operationLabel, attemptsMade, lastError, lastClassification);
|
|
3234
3393
|
}
|
|
3235
|
-
function computeDelayMs(
|
|
3236
|
-
if (typeof
|
|
3237
|
-
return Math.min(
|
|
3394
|
+
function computeDelayMs(input2) {
|
|
3395
|
+
if (typeof input2.retryAfterMs === "number" && input2.retryAfterMs > 0) {
|
|
3396
|
+
return Math.min(input2.maxBackoffMs, input2.retryAfterMs);
|
|
3238
3397
|
}
|
|
3239
|
-
const exponential =
|
|
3240
|
-
const capped = Math.min(
|
|
3241
|
-
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;
|
|
3242
3401
|
return Math.floor(capped + jitter);
|
|
3243
3402
|
}
|
|
3244
3403
|
function classifyHttpError(error) {
|
|
@@ -4296,18 +4455,18 @@ ${body.join("\n").trim()}
|
|
|
4296
4455
|
|
|
4297
4456
|
// src/output/meta.ts
|
|
4298
4457
|
import path7 from "path";
|
|
4299
|
-
function buildMetaJson(
|
|
4300
|
-
const plan =
|
|
4301
|
-
const contentPlan =
|
|
4302
|
-
const generationDir =
|
|
4303
|
-
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;
|
|
4304
4463
|
const slug = plan?.slug ?? "";
|
|
4305
4464
|
const description = plan?.description ?? contentPlan?.description ?? "";
|
|
4306
4465
|
const subtitle = (plan && "subtitle" in plan ? plan.subtitle : null) ?? null;
|
|
4307
4466
|
const keywords = (plan && "keywords" in plan ? plan.keywords : null) ?? [];
|
|
4308
4467
|
const contentType = plan?.contentType ?? contentPlan?.primaryContentType ?? "article";
|
|
4309
4468
|
const angle = plan?.angle ?? null;
|
|
4310
|
-
const coverImage =
|
|
4469
|
+
const coverImage = input2.renderedImages.find((image) => image.kind === "cover") ?? null;
|
|
4311
4470
|
const cover = coverImage ? {
|
|
4312
4471
|
path: coverImage.outputPath,
|
|
4313
4472
|
relativePath: coverImage.relativePath,
|
|
@@ -4317,7 +4476,7 @@ function buildMetaJson(input) {
|
|
|
4317
4476
|
title: section.title,
|
|
4318
4477
|
description: section.description
|
|
4319
4478
|
})) ?? [];
|
|
4320
|
-
const images =
|
|
4479
|
+
const images = input2.renderedImages.map((image) => ({
|
|
4321
4480
|
id: image.id,
|
|
4322
4481
|
kind: image.kind,
|
|
4323
4482
|
path: image.outputPath,
|
|
@@ -4325,30 +4484,30 @@ function buildMetaJson(input) {
|
|
|
4325
4484
|
description: image.description,
|
|
4326
4485
|
anchorAfterSection: image.anchorAfterSection
|
|
4327
4486
|
}));
|
|
4328
|
-
const outputs =
|
|
4329
|
-
fileId:
|
|
4330
|
-
contentType:
|
|
4331
|
-
path:
|
|
4332
|
-
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)
|
|
4333
4492
|
}));
|
|
4334
4493
|
return {
|
|
4335
4494
|
version: 1,
|
|
4336
4495
|
title,
|
|
4337
4496
|
slug,
|
|
4338
|
-
idea:
|
|
4497
|
+
idea: input2.idea,
|
|
4339
4498
|
description,
|
|
4340
4499
|
subtitle,
|
|
4341
4500
|
keywords,
|
|
4342
4501
|
contentType,
|
|
4343
|
-
style:
|
|
4344
|
-
intent:
|
|
4345
|
-
targetLength:
|
|
4502
|
+
style: input2.style,
|
|
4503
|
+
intent: input2.intent,
|
|
4504
|
+
targetLength: input2.targetLength,
|
|
4346
4505
|
angle,
|
|
4347
4506
|
cover,
|
|
4348
4507
|
sections,
|
|
4349
4508
|
images,
|
|
4350
4509
|
outputs,
|
|
4351
|
-
generatedAt:
|
|
4510
|
+
generatedAt: input2.generatedAt,
|
|
4352
4511
|
generationDir
|
|
4353
4512
|
};
|
|
4354
4513
|
}
|
|
@@ -4587,12 +4746,12 @@ function createInitialStages() {
|
|
|
4587
4746
|
}
|
|
4588
4747
|
];
|
|
4589
4748
|
}
|
|
4590
|
-
async function runPipelineShell(
|
|
4749
|
+
async function runPipelineShell(input2, options = {}) {
|
|
4591
4750
|
const runStartedAtMs = Date.now();
|
|
4592
4751
|
const runStartedAt = new Date(runStartedAtMs).toISOString();
|
|
4593
4752
|
const runId = randomUUID();
|
|
4594
|
-
const primaryTarget = getPrimaryTarget(
|
|
4595
|
-
const secondaryTargets = getSecondaryTargets(
|
|
4753
|
+
const primaryTarget = getPrimaryTarget(input2.config.settings.contentTargets);
|
|
4754
|
+
const secondaryTargets = getSecondaryTargets(input2.config.settings.contentTargets);
|
|
4596
4755
|
const stages = createInitialStages();
|
|
4597
4756
|
options.onUpdate?.(cloneStages(stages));
|
|
4598
4757
|
const dryRun = options.dryRun ?? false;
|
|
@@ -4658,10 +4817,10 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4658
4817
|
if (runMode === "fresh") {
|
|
4659
4818
|
writeSession = await startFreshWriteSession(
|
|
4660
4819
|
{
|
|
4661
|
-
idea:
|
|
4662
|
-
targetAudienceHint:
|
|
4663
|
-
job:
|
|
4664
|
-
settings:
|
|
4820
|
+
idea: input2.idea,
|
|
4821
|
+
targetAudienceHint: input2.targetAudienceHint,
|
|
4822
|
+
job: input2.job,
|
|
4823
|
+
settings: input2.config.settings,
|
|
4665
4824
|
dryRun,
|
|
4666
4825
|
outputPaths
|
|
4667
4826
|
},
|
|
@@ -4678,13 +4837,13 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4678
4837
|
}
|
|
4679
4838
|
try {
|
|
4680
4839
|
await ensureOutputDirectories(writeSession.outputPaths);
|
|
4681
|
-
const openRouter = dryRun ? null : new OpenRouterClient(requireSecret(
|
|
4682
|
-
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);
|
|
4683
4842
|
const imageDryRun = dryRun || !canRenderImagesLive;
|
|
4684
4843
|
const limn = imageDryRun ? null : new Limn({
|
|
4685
|
-
openrouterApiKey:
|
|
4686
|
-
replicateApiKey: requireSecret(
|
|
4687
|
-
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
|
|
4688
4847
|
});
|
|
4689
4848
|
let contentPlan = writeSession.contentPlan;
|
|
4690
4849
|
let plan = writeSession.plan;
|
|
@@ -4704,9 +4863,9 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4704
4863
|
};
|
|
4705
4864
|
} else {
|
|
4706
4865
|
contentPlan = await planContentPlan({
|
|
4707
|
-
idea:
|
|
4708
|
-
targetAudienceHint:
|
|
4709
|
-
settings:
|
|
4866
|
+
idea: input2.idea,
|
|
4867
|
+
targetAudienceHint: input2.targetAudienceHint,
|
|
4868
|
+
settings: input2.config.settings,
|
|
4710
4869
|
openRouter,
|
|
4711
4870
|
dryRun,
|
|
4712
4871
|
onInteraction(interaction) {
|
|
@@ -4756,10 +4915,10 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4756
4915
|
throw new Error("Shared content plan is missing for primary planning stage.");
|
|
4757
4916
|
}
|
|
4758
4917
|
plan = await planPrimaryContent({
|
|
4759
|
-
idea:
|
|
4918
|
+
idea: input2.idea,
|
|
4760
4919
|
contentType: primaryTarget.contentType,
|
|
4761
4920
|
contentPlan,
|
|
4762
|
-
settings:
|
|
4921
|
+
settings: input2.config.settings,
|
|
4763
4922
|
markdownOutputDir: writeSession.outputPaths.markdownOutputDir,
|
|
4764
4923
|
openRouter,
|
|
4765
4924
|
dryRun,
|
|
@@ -4826,7 +4985,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4826
4985
|
const sectionItemTracking = /* @__PURE__ */ new Map();
|
|
4827
4986
|
text = await writeArticleSections({
|
|
4828
4987
|
plan: longPlan,
|
|
4829
|
-
settings:
|
|
4988
|
+
settings: input2.config.settings,
|
|
4830
4989
|
openRouter,
|
|
4831
4990
|
dryRun,
|
|
4832
4991
|
onInteraction(interaction) {
|
|
@@ -4941,7 +5100,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4941
5100
|
slots: buildImageSlots(longPlan, text.sections, { maxImages: options.maxImages }),
|
|
4942
5101
|
planContext: longPlan,
|
|
4943
5102
|
sections: text.sections,
|
|
4944
|
-
settings:
|
|
5103
|
+
settings: input2.config.settings,
|
|
4945
5104
|
openRouter,
|
|
4946
5105
|
dryRun,
|
|
4947
5106
|
onInteraction(interaction) {
|
|
@@ -5008,18 +5167,18 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5008
5167
|
markStageStarted(stageTracking, "sections");
|
|
5009
5168
|
options.onUpdate?.(cloneStages(stages));
|
|
5010
5169
|
primaryMarkdownTemplate = await writeSingleShotContent({
|
|
5011
|
-
idea:
|
|
5170
|
+
idea: input2.idea,
|
|
5012
5171
|
contentType: primaryTarget.contentType,
|
|
5013
5172
|
role: "primary",
|
|
5014
5173
|
primaryContentType: primaryTarget.contentType,
|
|
5015
|
-
style:
|
|
5016
|
-
intent:
|
|
5174
|
+
style: input2.config.settings.style,
|
|
5175
|
+
intent: input2.config.settings.intent,
|
|
5017
5176
|
outputIndex: 1,
|
|
5018
5177
|
outputCountForType: 1,
|
|
5019
5178
|
articleReferenceMarkdown: void 0,
|
|
5020
5179
|
contentPlan,
|
|
5021
5180
|
plan,
|
|
5022
|
-
settings:
|
|
5181
|
+
settings: input2.config.settings,
|
|
5023
5182
|
openRouter,
|
|
5024
5183
|
dryRun,
|
|
5025
5184
|
onInteraction(interaction) {
|
|
@@ -5066,7 +5225,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5066
5225
|
};
|
|
5067
5226
|
options.onUpdate?.(cloneStages(stages));
|
|
5068
5227
|
}
|
|
5069
|
-
const baseSlug = plan?.slug ?? resolveGenerationSlug(
|
|
5228
|
+
const baseSlug = plan?.slug ?? resolveGenerationSlug(input2.idea, contentPlan?.title);
|
|
5070
5229
|
const generationDir = path9.join(
|
|
5071
5230
|
writeSession.outputPaths.markdownOutputDir,
|
|
5072
5231
|
buildGenerationDirectoryName(baseSlug)
|
|
@@ -5076,12 +5235,12 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5076
5235
|
await writeJsonFile(
|
|
5077
5236
|
jobDefinitionPath,
|
|
5078
5237
|
buildRunJobDefinition({
|
|
5079
|
-
idea:
|
|
5080
|
-
targetAudienceHint:
|
|
5238
|
+
idea: input2.idea,
|
|
5239
|
+
targetAudienceHint: input2.targetAudienceHint,
|
|
5081
5240
|
dryRun,
|
|
5082
5241
|
runMode,
|
|
5083
|
-
settings:
|
|
5084
|
-
sourceJob:
|
|
5242
|
+
settings: input2.config.settings,
|
|
5243
|
+
sourceJob: input2.job
|
|
5085
5244
|
})
|
|
5086
5245
|
);
|
|
5087
5246
|
const planPath = plan ? path9.join(generationDir, "plan.md") : null;
|
|
@@ -5108,7 +5267,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5108
5267
|
}
|
|
5109
5268
|
const renderedImages = await renderExpandedImages({
|
|
5110
5269
|
prompts: imagePrompts,
|
|
5111
|
-
settings:
|
|
5270
|
+
settings: input2.config.settings,
|
|
5112
5271
|
limn,
|
|
5113
5272
|
markdownPath: primaryMarkdownPath,
|
|
5114
5273
|
assetDir: sharedAssetDir,
|
|
@@ -5192,7 +5351,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5192
5351
|
} else {
|
|
5193
5352
|
const renderedImages = await renderExpandedImages({
|
|
5194
5353
|
prompts: imagePrompts,
|
|
5195
|
-
settings:
|
|
5354
|
+
settings: input2.config.settings,
|
|
5196
5355
|
limn,
|
|
5197
5356
|
markdownPath: primaryMarkdownPath,
|
|
5198
5357
|
assetDir: sharedAssetDir,
|
|
@@ -5253,11 +5412,11 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5253
5412
|
}
|
|
5254
5413
|
const coverImage = imageArtifacts?.renderedImages.find((image) => image.kind === "cover") ?? null;
|
|
5255
5414
|
if (coverImage) {
|
|
5256
|
-
primaryMarkdownTemplate = withCoverImage(primaryMarkdownTemplate, coverImage.relativePath, plan.title || deriveTitleFromIdea2(
|
|
5415
|
+
primaryMarkdownTemplate = withCoverImage(primaryMarkdownTemplate, coverImage.relativePath, plan.title || deriveTitleFromIdea2(input2.idea));
|
|
5257
5416
|
}
|
|
5258
5417
|
primaryMarkdownTemplate = applyPrimaryTitleHeading(
|
|
5259
5418
|
primaryMarkdownTemplate,
|
|
5260
|
-
plan.title || contentPlan.title || deriveTitleFromIdea2(
|
|
5419
|
+
plan.title || contentPlan.title || deriveTitleFromIdea2(input2.idea)
|
|
5261
5420
|
);
|
|
5262
5421
|
}
|
|
5263
5422
|
const markdownPaths = [];
|
|
@@ -5278,9 +5437,9 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5278
5437
|
...stages[5],
|
|
5279
5438
|
status: "running",
|
|
5280
5439
|
detail: requestedOutputs.length > 0 ? "Generating secondary channel outputs from primary anchor content." : "No secondary content requested.",
|
|
5281
|
-
items: requestedOutputs.map((
|
|
5282
|
-
id: toOutputItemId(
|
|
5283
|
-
label: formatOutputItemLabel(
|
|
5440
|
+
items: requestedOutputs.map((output2) => ({
|
|
5441
|
+
id: toOutputItemId(output2.filePrefix, output2.index),
|
|
5442
|
+
label: formatOutputItemLabel(output2.contentType, output2.index, output2.outputCountForType),
|
|
5284
5443
|
status: "pending",
|
|
5285
5444
|
detail: "Waiting to start."
|
|
5286
5445
|
}))
|
|
@@ -5290,8 +5449,8 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5290
5449
|
if (!contentPlan) {
|
|
5291
5450
|
throw new Error("Shared content plan is missing for output generation stage.");
|
|
5292
5451
|
}
|
|
5293
|
-
for (const
|
|
5294
|
-
const itemId = toOutputItemId(
|
|
5452
|
+
for (const output2 of requestedOutputs) {
|
|
5453
|
+
const itemId = toOutputItemId(output2.filePrefix, output2.index);
|
|
5295
5454
|
const itemStartedAtMs = Date.now();
|
|
5296
5455
|
const itemTracking = {
|
|
5297
5456
|
retries: 0,
|
|
@@ -5300,7 +5459,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5300
5459
|
};
|
|
5301
5460
|
stages[5] = {
|
|
5302
5461
|
...stages[5],
|
|
5303
|
-
detail: `Generating ${formatOutputItemLabel(
|
|
5462
|
+
detail: `Generating ${formatOutputItemLabel(output2.contentType, output2.index, output2.outputCountForType)}.`,
|
|
5304
5463
|
items: (stages[5].items ?? []).map((item) => {
|
|
5305
5464
|
if (item.id !== itemId) {
|
|
5306
5465
|
return item;
|
|
@@ -5313,19 +5472,19 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5313
5472
|
})
|
|
5314
5473
|
};
|
|
5315
5474
|
options.onUpdate?.(cloneStages(stages));
|
|
5316
|
-
const markdownPath = path9.join(generationDir, `${
|
|
5475
|
+
const markdownPath = path9.join(generationDir, `${output2.filePrefix}-${output2.index}.md`);
|
|
5317
5476
|
try {
|
|
5318
5477
|
const content = await writeSingleShotContent({
|
|
5319
|
-
idea:
|
|
5320
|
-
contentType:
|
|
5321
|
-
style:
|
|
5322
|
-
intent:
|
|
5323
|
-
outputIndex:
|
|
5324
|
-
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,
|
|
5325
5484
|
articleReferenceMarkdown: primaryMarkdownTemplate ?? void 0,
|
|
5326
5485
|
contentPlan,
|
|
5327
5486
|
plan,
|
|
5328
|
-
settings:
|
|
5487
|
+
settings: input2.config.settings,
|
|
5329
5488
|
openRouter,
|
|
5330
5489
|
dryRun,
|
|
5331
5490
|
role: "secondary",
|
|
@@ -5342,13 +5501,13 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5342
5501
|
}
|
|
5343
5502
|
});
|
|
5344
5503
|
if (!content) {
|
|
5345
|
-
throw new Error(`Generated empty content for ${
|
|
5504
|
+
throw new Error(`Generated empty content for ${output2.contentType} output ${output2.index}.`);
|
|
5346
5505
|
}
|
|
5347
5506
|
markdownPaths.push(markdownPath);
|
|
5348
5507
|
await writeUtf8File(markdownPath, content);
|
|
5349
5508
|
generatedOutputs.push({
|
|
5350
5509
|
fileId: itemId,
|
|
5351
|
-
contentType:
|
|
5510
|
+
contentType: output2.contentType,
|
|
5352
5511
|
markdownPath
|
|
5353
5512
|
});
|
|
5354
5513
|
const itemDurationMs = Date.now() - itemStartedAtMs;
|
|
@@ -5356,10 +5515,10 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5356
5515
|
const itemCostSource = chooseStageCostSource(itemTracking.costSources, knownItemCost.source);
|
|
5357
5516
|
outputItemCalls.push({
|
|
5358
5517
|
itemId,
|
|
5359
|
-
contentType:
|
|
5360
|
-
filePrefix:
|
|
5361
|
-
index:
|
|
5362
|
-
outputCountForType:
|
|
5518
|
+
contentType: output2.contentType,
|
|
5519
|
+
filePrefix: output2.filePrefix,
|
|
5520
|
+
index: output2.index,
|
|
5521
|
+
outputCountForType: output2.outputCountForType,
|
|
5363
5522
|
durationMs: itemDurationMs,
|
|
5364
5523
|
retries: itemTracking.retries,
|
|
5365
5524
|
costUsd: knownItemCost.usd,
|
|
@@ -5367,7 +5526,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5367
5526
|
});
|
|
5368
5527
|
stages[5] = {
|
|
5369
5528
|
...stages[5],
|
|
5370
|
-
detail: `Completed ${formatOutputItemLabel(
|
|
5529
|
+
detail: `Completed ${formatOutputItemLabel(output2.contentType, output2.index, output2.outputCountForType)}.`,
|
|
5371
5530
|
items: (stages[5].items ?? []).map((item) => {
|
|
5372
5531
|
if (item.id !== itemId) {
|
|
5373
5532
|
return item;
|
|
@@ -5413,14 +5572,14 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5413
5572
|
stageAnalytics: snapshotStageAnalytics(stageTracking, "output")
|
|
5414
5573
|
};
|
|
5415
5574
|
options.onUpdate?.(cloneStages(stages));
|
|
5416
|
-
const eligibleOutputsForLinks = generatedOutputs.filter((
|
|
5575
|
+
const eligibleOutputsForLinks = generatedOutputs.filter((output2) => output2.contentType !== "x-post" && output2.contentType !== "x-thread");
|
|
5417
5576
|
stages[6] = {
|
|
5418
5577
|
...stages[6],
|
|
5419
5578
|
status: "running",
|
|
5420
5579
|
detail: "Selecting expressions and resolving source URLs.",
|
|
5421
|
-
items: eligibleOutputsForLinks.map((
|
|
5422
|
-
id:
|
|
5423
|
-
label:
|
|
5580
|
+
items: eligibleOutputsForLinks.map((output2) => ({
|
|
5581
|
+
id: output2.fileId,
|
|
5582
|
+
label: output2.fileId,
|
|
5424
5583
|
status: "pending",
|
|
5425
5584
|
detail: "Waiting to start."
|
|
5426
5585
|
}))
|
|
@@ -5430,15 +5589,15 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5430
5589
|
if (!shouldEnrichLinks) {
|
|
5431
5590
|
const customLinkActions = pipelineCustomLinkRaws.length > 0 || pipelineUnlinks.length > 0;
|
|
5432
5591
|
if (customLinkActions && eligibleOutputsForLinks.length > 0) {
|
|
5433
|
-
for (const
|
|
5434
|
-
const existingLinks = await readExistingLinks(resolveLinksPath(
|
|
5592
|
+
for (const output2 of eligibleOutputsForLinks) {
|
|
5593
|
+
const existingLinks = await readExistingLinks(resolveLinksPath(output2.markdownPath));
|
|
5435
5594
|
const mergedCustomLinks = resolvePipelineCustomLinks(
|
|
5436
5595
|
existingLinks?.customLinks ?? [],
|
|
5437
5596
|
pipelineCustomLinkRaws,
|
|
5438
5597
|
pipelineUnlinks
|
|
5439
5598
|
);
|
|
5440
5599
|
const generatedLinks = existingLinks?.links ?? [];
|
|
5441
|
-
await writeLinksFile(
|
|
5600
|
+
await writeLinksFile(output2.markdownPath, {
|
|
5442
5601
|
version: 2,
|
|
5443
5602
|
customLinks: mergedCustomLinks,
|
|
5444
5603
|
links: generatedLinks
|
|
@@ -5482,12 +5641,12 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5482
5641
|
} else if (linksResult) {
|
|
5483
5642
|
const linksByFileId = new Map(linksResult.map((item) => [item.fileId, item.links]));
|
|
5484
5643
|
const customLinksByFileId = new Map(linksResult.map((item) => [item.fileId, item.customLinks]));
|
|
5485
|
-
const resumedLinks = eligibleOutputsForLinks.map((
|
|
5486
|
-
fileId:
|
|
5487
|
-
contentType:
|
|
5488
|
-
markdownPath:
|
|
5489
|
-
links: linksByFileId.get(
|
|
5490
|
-
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) ?? []
|
|
5491
5650
|
}));
|
|
5492
5651
|
linksResult = resumedLinks;
|
|
5493
5652
|
for (const item of resumedLinks) {
|
|
@@ -5515,13 +5674,13 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5515
5674
|
const itemTracking = /* @__PURE__ */ new Map();
|
|
5516
5675
|
linksResult = await enrichLinks({
|
|
5517
5676
|
markdownFiles: eligibleOutputsForLinks,
|
|
5518
|
-
articleTitle: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(
|
|
5677
|
+
articleTitle: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input2.idea),
|
|
5519
5678
|
articleDescription: plan?.description ?? contentPlan.description,
|
|
5520
5679
|
openRouter,
|
|
5521
|
-
settings:
|
|
5680
|
+
settings: input2.config.settings,
|
|
5522
5681
|
dryRun,
|
|
5523
5682
|
customLinks: parsePipelineCustomLinks(pipelineCustomLinkRaws, pipelineUnlinks),
|
|
5524
|
-
maxLinks: pipelineMaxLinks ?? resolveDefaultMaxLinks(
|
|
5683
|
+
maxLinks: pipelineMaxLinks ?? resolveDefaultMaxLinks(input2.config.settings.targetLength),
|
|
5525
5684
|
onInteraction(interaction) {
|
|
5526
5685
|
onLlmInteraction(interaction);
|
|
5527
5686
|
},
|
|
@@ -5628,23 +5787,23 @@ async function runPipelineShell(input, options = {}) {
|
|
|
5628
5787
|
await writeJsonFile(analyticsPath, analytics);
|
|
5629
5788
|
await writeJsonFile(interactionsPath, interactions);
|
|
5630
5789
|
const metaJson = buildMetaJson({
|
|
5631
|
-
idea:
|
|
5790
|
+
idea: input2.idea,
|
|
5632
5791
|
generationDir,
|
|
5633
5792
|
contentPlan,
|
|
5634
5793
|
plan,
|
|
5635
5794
|
renderedImages: imageArtifacts?.renderedImages ?? [],
|
|
5636
5795
|
outputs: generatedOutputs,
|
|
5637
5796
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5638
|
-
style:
|
|
5639
|
-
intent:
|
|
5640
|
-
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
|
|
5641
5800
|
});
|
|
5642
5801
|
const metaJsonPath = path9.join(generationDir, "meta.json");
|
|
5643
5802
|
await writeJsonFile(metaJsonPath, metaJson);
|
|
5644
5803
|
const primaryMarkdownPathForArtifact = markdownPaths[0] ?? primaryMarkdownPath;
|
|
5645
5804
|
const artifact = {
|
|
5646
|
-
title: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(
|
|
5647
|
-
slug: plan?.slug ?? resolveGenerationSlug(
|
|
5805
|
+
title: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input2.idea),
|
|
5806
|
+
slug: plan?.slug ?? resolveGenerationSlug(input2.idea, contentPlan?.title),
|
|
5648
5807
|
sectionCount: text?.sections.length ?? 0,
|
|
5649
5808
|
imageCount: imageArtifacts?.renderedImages.length ?? 0,
|
|
5650
5809
|
outputCount: markdownPaths.length,
|
|
@@ -6030,19 +6189,19 @@ function resolveGenerationSlug(idea, planTitle) {
|
|
|
6030
6189
|
}
|
|
6031
6190
|
return slugifyIdea(idea, 80);
|
|
6032
6191
|
}
|
|
6033
|
-
function buildRunJobDefinition(
|
|
6192
|
+
function buildRunJobDefinition(input2) {
|
|
6034
6193
|
return {
|
|
6035
|
-
idea:
|
|
6036
|
-
prompt:
|
|
6037
|
-
...
|
|
6038
|
-
contentTargets:
|
|
6039
|
-
style:
|
|
6040
|
-
settings:
|
|
6041
|
-
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,
|
|
6042
6201
|
runMetadata: {
|
|
6043
6202
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6044
|
-
dryRun:
|
|
6045
|
-
runMode:
|
|
6203
|
+
dryRun: input2.dryRun,
|
|
6204
|
+
runMode: input2.runMode
|
|
6046
6205
|
}
|
|
6047
6206
|
};
|
|
6048
6207
|
}
|
|
@@ -6198,7 +6357,7 @@ async function runLinksCommand(options, dependencies = {}) {
|
|
|
6198
6357
|
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6199
6358
|
if (!openRouterApiKey) {
|
|
6200
6359
|
throw new ReportedError(
|
|
6201
|
-
"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."
|
|
6202
6361
|
);
|
|
6203
6362
|
}
|
|
6204
6363
|
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
@@ -6678,7 +6837,7 @@ async function listAllGenerations(markdownOutputDir) {
|
|
|
6678
6837
|
try {
|
|
6679
6838
|
const metadata = await extractArticleMetadata(filePath);
|
|
6680
6839
|
const identity = deriveOutputIdentity(filePath, markdownOutputDir);
|
|
6681
|
-
const
|
|
6840
|
+
const output2 = {
|
|
6682
6841
|
id: `${identity.generationId}:${identity.contentType}:${identity.index}`,
|
|
6683
6842
|
generationId: identity.generationId,
|
|
6684
6843
|
sourcePath: filePath,
|
|
@@ -6691,38 +6850,38 @@ async function listAllGenerations(markdownOutputDir) {
|
|
|
6691
6850
|
contentTypeLabel: toContentTypeLabel(identity.contentType),
|
|
6692
6851
|
index: identity.index
|
|
6693
6852
|
};
|
|
6694
|
-
const outputKey = `${
|
|
6853
|
+
const outputKey = `${output2.generationId}:${output2.contentType}:${output2.index}`;
|
|
6695
6854
|
const existing = outputMap.get(outputKey);
|
|
6696
|
-
if (!existing ||
|
|
6697
|
-
outputMap.set(outputKey,
|
|
6855
|
+
if (!existing || output2.mtime > existing.mtime) {
|
|
6856
|
+
outputMap.set(outputKey, output2);
|
|
6698
6857
|
}
|
|
6699
6858
|
} catch {
|
|
6700
6859
|
}
|
|
6701
6860
|
}
|
|
6702
6861
|
const grouped = /* @__PURE__ */ new Map();
|
|
6703
|
-
for (const
|
|
6704
|
-
const existing = grouped.get(
|
|
6862
|
+
for (const output2 of outputMap.values()) {
|
|
6863
|
+
const existing = grouped.get(output2.generationId);
|
|
6705
6864
|
if (existing) {
|
|
6706
|
-
existing.push(
|
|
6865
|
+
existing.push(output2);
|
|
6707
6866
|
} else {
|
|
6708
|
-
grouped.set(
|
|
6867
|
+
grouped.set(output2.generationId, [output2]);
|
|
6709
6868
|
}
|
|
6710
6869
|
}
|
|
6711
6870
|
const generations = [];
|
|
6712
6871
|
for (const [id, outputs] of grouped.entries()) {
|
|
6713
6872
|
outputs.sort((a, b) => compareContentTypes(a.contentType, b.contentType) || a.index - b.index || b.mtime - a.mtime);
|
|
6714
6873
|
const primaryContentType = await resolvePrimaryContentType(outputs);
|
|
6715
|
-
const primary = outputs.find((
|
|
6874
|
+
const primary = outputs.find((output2) => output2.contentType === primaryContentType) ?? outputs[0];
|
|
6716
6875
|
if (!primary) {
|
|
6717
6876
|
continue;
|
|
6718
6877
|
}
|
|
6719
|
-
const newestMtime = outputs.reduce((latest,
|
|
6878
|
+
const newestMtime = outputs.reduce((latest, output2) => Math.max(latest, output2.mtime), 0);
|
|
6720
6879
|
generations.push({
|
|
6721
6880
|
id,
|
|
6722
6881
|
title: primary.title,
|
|
6723
6882
|
mtime: newestMtime,
|
|
6724
6883
|
previewSnippet: primary.previewSnippet,
|
|
6725
|
-
coverImageUrl: primary.coverImageUrl ?? outputs.find((
|
|
6884
|
+
coverImageUrl: primary.coverImageUrl ?? outputs.find((output2) => Boolean(output2.coverImageUrl))?.coverImageUrl ?? null,
|
|
6726
6885
|
primaryContentType,
|
|
6727
6886
|
outputs
|
|
6728
6887
|
});
|
|
@@ -6806,7 +6965,7 @@ function toContentTypeLabel(contentType) {
|
|
|
6806
6965
|
return contentType.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
6807
6966
|
}
|
|
6808
6967
|
async function resolvePrimaryContentType(outputs) {
|
|
6809
|
-
const fallback = outputs.find((
|
|
6968
|
+
const fallback = outputs.find((output2) => output2.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
|
|
6810
6969
|
const generationDir = path11.dirname(outputs[0]?.sourcePath ?? "");
|
|
6811
6970
|
if (!generationDir) {
|
|
6812
6971
|
return fallback;
|
|
@@ -6840,15 +6999,15 @@ async function runOutputCommand(options, dependencies = {}) {
|
|
|
6840
6999
|
const outputPaths = resolveOutputPaths();
|
|
6841
7000
|
const generations = await listAllGenerations(outputPaths.markdownOutputDir);
|
|
6842
7001
|
const generation = resolveGeneration(generations, options.generationId);
|
|
6843
|
-
const articleOutputs = generation.outputs.filter((
|
|
7002
|
+
const articleOutputs = generation.outputs.filter((output2) => output2.contentType === generation.primaryContentType);
|
|
6844
7003
|
if (articleOutputs.length === 0) {
|
|
6845
7004
|
throw new ReportedError(
|
|
6846
7005
|
`Generation "${generation.id}" has no primary content outputs (type: ${generation.primaryContentType}).`
|
|
6847
7006
|
);
|
|
6848
7007
|
}
|
|
6849
|
-
const articleOutput = articleOutputs.find((
|
|
7008
|
+
const articleOutput = articleOutputs.find((output2) => output2.index === targetIndex);
|
|
6850
7009
|
if (!articleOutput) {
|
|
6851
|
-
const available = articleOutputs.map((
|
|
7010
|
+
const available = articleOutputs.map((output2) => output2.index).join(", ");
|
|
6852
7011
|
throw new ReportedError(
|
|
6853
7012
|
`Generation "${generation.id}" has no primary output at index ${targetIndex}. Available: ${available}.`
|
|
6854
7013
|
);
|
|
@@ -6896,7 +7055,7 @@ function resolveGeneration(generations, generationId) {
|
|
|
6896
7055
|
return exact;
|
|
6897
7056
|
}
|
|
6898
7057
|
const bySlug = generations.find(
|
|
6899
|
-
(g) => g.outputs.some((
|
|
7058
|
+
(g) => g.outputs.some((output2) => output2.slug === generationId)
|
|
6900
7059
|
);
|
|
6901
7060
|
if (bySlug) {
|
|
6902
7061
|
return bySlug;
|
|
@@ -6996,7 +7155,688 @@ function parsePrimaryAndSecondarySpecs(options) {
|
|
|
6996
7155
|
];
|
|
6997
7156
|
}
|
|
6998
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
|
+
|
|
6999
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
|
+
}
|
|
7000
7840
|
async function startIdeonMcpServer() {
|
|
7001
7841
|
const server = new McpServer({
|
|
7002
7842
|
name: "ideon",
|
|
@@ -7009,30 +7849,30 @@ async function startIdeonMcpServer() {
|
|
|
7009
7849
|
description: "Generate content from an idea using the Ideon pipeline.",
|
|
7010
7850
|
inputSchema: writeToolInputSchema
|
|
7011
7851
|
},
|
|
7012
|
-
async (
|
|
7852
|
+
async (input2) => {
|
|
7013
7853
|
try {
|
|
7014
7854
|
const parsedTargets = parsePrimaryAndSecondarySpecs({
|
|
7015
|
-
primarySpec:
|
|
7016
|
-
secondarySpecs:
|
|
7855
|
+
primarySpec: input2.primary,
|
|
7856
|
+
secondarySpecs: input2.secondary
|
|
7017
7857
|
});
|
|
7018
7858
|
const resolved = await resolveRunInput({
|
|
7019
|
-
idea:
|
|
7020
|
-
audience:
|
|
7021
|
-
jobPath:
|
|
7022
|
-
style:
|
|
7023
|
-
intent:
|
|
7024
|
-
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,
|
|
7025
7865
|
contentTargets: parsedTargets
|
|
7026
7866
|
});
|
|
7027
7867
|
const run = await runPipelineShell(resolved, {
|
|
7028
7868
|
workingDir: cwd(),
|
|
7029
7869
|
runMode: "fresh",
|
|
7030
|
-
dryRun:
|
|
7031
|
-
enrichLinks:
|
|
7032
|
-
customLinks:
|
|
7033
|
-
unlinks:
|
|
7034
|
-
maxLinks:
|
|
7035
|
-
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
|
|
7036
7876
|
});
|
|
7037
7877
|
return {
|
|
7038
7878
|
content: [
|
|
@@ -7063,7 +7903,7 @@ async function startIdeonMcpServer() {
|
|
|
7063
7903
|
description: "Resume the last failed or interrupted Ideon write session.",
|
|
7064
7904
|
inputSchema: writeResumeToolInputSchema
|
|
7065
7905
|
},
|
|
7066
|
-
async (
|
|
7906
|
+
async (input2) => {
|
|
7067
7907
|
try {
|
|
7068
7908
|
const session = await loadWriteSession(cwd());
|
|
7069
7909
|
if (!session) {
|
|
@@ -7087,12 +7927,12 @@ async function startIdeonMcpServer() {
|
|
|
7087
7927
|
const run = await runPipelineShell(resumeInput, {
|
|
7088
7928
|
workingDir: cwd(),
|
|
7089
7929
|
runMode: "resume",
|
|
7090
|
-
dryRun:
|
|
7091
|
-
enrichLinks:
|
|
7092
|
-
customLinks:
|
|
7093
|
-
unlinks:
|
|
7094
|
-
maxLinks:
|
|
7095
|
-
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
|
|
7096
7936
|
});
|
|
7097
7937
|
return {
|
|
7098
7938
|
content: [
|
|
@@ -7122,11 +7962,11 @@ async function startIdeonMcpServer() {
|
|
|
7122
7962
|
description: "Delete generated output and assets by slug.",
|
|
7123
7963
|
inputSchema: deleteToolInputSchema
|
|
7124
7964
|
},
|
|
7125
|
-
async (
|
|
7965
|
+
async (input2) => {
|
|
7126
7966
|
try {
|
|
7127
7967
|
const messages = [];
|
|
7128
7968
|
await runDeleteCommand(
|
|
7129
|
-
{ slug:
|
|
7969
|
+
{ slug: input2.slug, force: true },
|
|
7130
7970
|
{
|
|
7131
7971
|
cwd: cwd(),
|
|
7132
7972
|
log: (message) => {
|
|
@@ -7138,11 +7978,11 @@ async function startIdeonMcpServer() {
|
|
|
7138
7978
|
content: [
|
|
7139
7979
|
{
|
|
7140
7980
|
type: "text",
|
|
7141
|
-
text: messages.length > 0 ? messages.join("\n") : `Deleted ${
|
|
7981
|
+
text: messages.length > 0 ? messages.join("\n") : `Deleted ${input2.slug}.`
|
|
7142
7982
|
}
|
|
7143
7983
|
],
|
|
7144
7984
|
structuredContent: {
|
|
7145
|
-
slug:
|
|
7985
|
+
slug: input2.slug,
|
|
7146
7986
|
deleted: true
|
|
7147
7987
|
}
|
|
7148
7988
|
};
|
|
@@ -7158,16 +7998,16 @@ async function startIdeonMcpServer() {
|
|
|
7158
7998
|
description: "Run link enrichment for a previously generated article by slug.",
|
|
7159
7999
|
inputSchema: linksToolInputSchema
|
|
7160
8000
|
},
|
|
7161
|
-
async (
|
|
8001
|
+
async (input2) => {
|
|
7162
8002
|
try {
|
|
7163
8003
|
const messages = [];
|
|
7164
8004
|
await runLinksCommand(
|
|
7165
8005
|
{
|
|
7166
|
-
slug:
|
|
7167
|
-
mode:
|
|
7168
|
-
links:
|
|
7169
|
-
unlinks:
|
|
7170
|
-
maxLinks:
|
|
8006
|
+
slug: input2.slug,
|
|
8007
|
+
mode: input2.mode ?? "fresh",
|
|
8008
|
+
links: input2.link,
|
|
8009
|
+
unlinks: input2.unlink,
|
|
8010
|
+
maxLinks: input2.maxLinks
|
|
7171
8011
|
},
|
|
7172
8012
|
{
|
|
7173
8013
|
cwd: cwd(),
|
|
@@ -7180,12 +8020,12 @@ async function startIdeonMcpServer() {
|
|
|
7180
8020
|
content: [
|
|
7181
8021
|
{
|
|
7182
8022
|
type: "text",
|
|
7183
|
-
text: messages.length > 0 ? messages.join("\n") : `Enriched links for ${
|
|
8023
|
+
text: messages.length > 0 ? messages.join("\n") : `Enriched links for ${input2.slug}.`
|
|
7184
8024
|
}
|
|
7185
8025
|
],
|
|
7186
8026
|
structuredContent: {
|
|
7187
|
-
slug:
|
|
7188
|
-
mode:
|
|
8027
|
+
slug: input2.slug,
|
|
8028
|
+
mode: input2.mode ?? "fresh"
|
|
7189
8029
|
}
|
|
7190
8030
|
};
|
|
7191
8031
|
} catch (error) {
|
|
@@ -7200,15 +8040,15 @@ async function startIdeonMcpServer() {
|
|
|
7200
8040
|
description: "Export a generated article as a standalone markdown file with inline links and copied images.",
|
|
7201
8041
|
inputSchema: exportToolInputZodSchema
|
|
7202
8042
|
},
|
|
7203
|
-
async (
|
|
8043
|
+
async (input2) => {
|
|
7204
8044
|
try {
|
|
7205
8045
|
const messages = [];
|
|
7206
8046
|
await runOutputCommand(
|
|
7207
8047
|
{
|
|
7208
|
-
generationId:
|
|
7209
|
-
destinationPath:
|
|
7210
|
-
index:
|
|
7211
|
-
overwrite:
|
|
8048
|
+
generationId: input2.generationId,
|
|
8049
|
+
destinationPath: input2.destinationPath,
|
|
8050
|
+
index: input2.index,
|
|
8051
|
+
overwrite: input2.overwrite
|
|
7212
8052
|
},
|
|
7213
8053
|
{
|
|
7214
8054
|
cwd: cwd(),
|
|
@@ -7221,14 +8061,14 @@ async function startIdeonMcpServer() {
|
|
|
7221
8061
|
content: [
|
|
7222
8062
|
{
|
|
7223
8063
|
type: "text",
|
|
7224
|
-
text: messages.length > 0 ? messages.join("\n") : `Exported ${
|
|
8064
|
+
text: messages.length > 0 ? messages.join("\n") : `Exported ${input2.generationId}.`
|
|
7225
8065
|
}
|
|
7226
8066
|
],
|
|
7227
8067
|
structuredContent: {
|
|
7228
|
-
generationId:
|
|
7229
|
-
destinationPath:
|
|
7230
|
-
index:
|
|
7231
|
-
overwrite:
|
|
8068
|
+
generationId: input2.generationId,
|
|
8069
|
+
destinationPath: input2.destinationPath,
|
|
8070
|
+
index: input2.index ?? 1,
|
|
8071
|
+
overwrite: input2.overwrite ?? false,
|
|
7232
8072
|
messages
|
|
7233
8073
|
}
|
|
7234
8074
|
};
|
|
@@ -7244,12 +8084,12 @@ async function startIdeonMcpServer() {
|
|
|
7244
8084
|
description: "Read a configuration value or secret availability flag.",
|
|
7245
8085
|
inputSchema: configGetToolInputSchema
|
|
7246
8086
|
},
|
|
7247
|
-
async (
|
|
8087
|
+
async (input2) => {
|
|
7248
8088
|
try {
|
|
7249
|
-
if (!isConfigKey(
|
|
7250
|
-
throw new ReportedError(`Unsupported config key: ${
|
|
8089
|
+
if (!isConfigKey(input2.key)) {
|
|
8090
|
+
throw new ReportedError(`Unsupported config key: ${input2.key}`);
|
|
7251
8091
|
}
|
|
7252
|
-
const result = await configGet(
|
|
8092
|
+
const result = await configGet(input2.key);
|
|
7253
8093
|
return {
|
|
7254
8094
|
content: [
|
|
7255
8095
|
{
|
|
@@ -7275,21 +8115,21 @@ async function startIdeonMcpServer() {
|
|
|
7275
8115
|
description: "Set a configuration value or secret token.",
|
|
7276
8116
|
inputSchema: configSetToolInputSchema
|
|
7277
8117
|
},
|
|
7278
|
-
async (
|
|
8118
|
+
async (input2) => {
|
|
7279
8119
|
try {
|
|
7280
|
-
if (!isConfigKey(
|
|
7281
|
-
throw new ReportedError(`Unsupported config key: ${
|
|
8120
|
+
if (!isConfigKey(input2.key)) {
|
|
8121
|
+
throw new ReportedError(`Unsupported config key: ${input2.key}`);
|
|
7282
8122
|
}
|
|
7283
|
-
await configSet(
|
|
8123
|
+
await configSet(input2.key, input2.value);
|
|
7284
8124
|
return {
|
|
7285
8125
|
content: [
|
|
7286
8126
|
{
|
|
7287
8127
|
type: "text",
|
|
7288
|
-
text: `Set ${
|
|
8128
|
+
text: `Set ${input2.key}.`
|
|
7289
8129
|
}
|
|
7290
8130
|
],
|
|
7291
8131
|
structuredContent: {
|
|
7292
|
-
key:
|
|
8132
|
+
key: input2.key,
|
|
7293
8133
|
updated: true
|
|
7294
8134
|
}
|
|
7295
8135
|
};
|
|
@@ -7329,21 +8169,21 @@ async function startIdeonMcpServer() {
|
|
|
7329
8169
|
description: "Reset a setting to its default or delete a stored secret.",
|
|
7330
8170
|
inputSchema: configUnsetToolInputSchema
|
|
7331
8171
|
},
|
|
7332
|
-
async (
|
|
8172
|
+
async (input2) => {
|
|
7333
8173
|
try {
|
|
7334
|
-
if (!isConfigKey(
|
|
7335
|
-
throw new ReportedError(`Unsupported config key: ${
|
|
8174
|
+
if (!isConfigKey(input2.key)) {
|
|
8175
|
+
throw new ReportedError(`Unsupported config key: ${input2.key}`);
|
|
7336
8176
|
}
|
|
7337
|
-
await configUnset(
|
|
8177
|
+
await configUnset(input2.key);
|
|
7338
8178
|
return {
|
|
7339
8179
|
content: [
|
|
7340
8180
|
{
|
|
7341
8181
|
type: "text",
|
|
7342
|
-
text: `Unset ${
|
|
8182
|
+
text: `Unset ${input2.key}.`
|
|
7343
8183
|
}
|
|
7344
8184
|
],
|
|
7345
8185
|
structuredContent: {
|
|
7346
|
-
key:
|
|
8186
|
+
key: input2.key,
|
|
7347
8187
|
updated: true
|
|
7348
8188
|
}
|
|
7349
8189
|
};
|
|
@@ -7352,6 +8192,86 @@ async function startIdeonMcpServer() {
|
|
|
7352
8192
|
}
|
|
7353
8193
|
}
|
|
7354
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
|
+
);
|
|
7355
8275
|
const transport = new StdioServerTransport();
|
|
7356
8276
|
await server.connect(transport);
|
|
7357
8277
|
}
|
|
@@ -7368,6 +8288,530 @@ async function runMcpServeCommand() {
|
|
|
7368
8288
|
await startIdeonMcpServer();
|
|
7369
8289
|
}
|
|
7370
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
|
+
|
|
7371
8815
|
// src/cli/commands/settings.tsx
|
|
7372
8816
|
import { render } from "ink";
|
|
7373
8817
|
|
|
@@ -7561,7 +9005,7 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
|
7561
9005
|
const [showModelSelect, setShowModelSelect] = useState(false);
|
|
7562
9006
|
const [menuMode, setMenuMode] = useState("main");
|
|
7563
9007
|
const currentModelEntry = getLimnGenerationModels().find((m) => m.family === settings.t2i.modelId) ?? getLimnGenerationModels()[0];
|
|
7564
|
-
useInput((
|
|
9008
|
+
useInput((input2, key) => {
|
|
7565
9009
|
if (key.escape) {
|
|
7566
9010
|
if (editing) {
|
|
7567
9011
|
setEditing(null);
|
|
@@ -7576,7 +9020,7 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
|
7576
9020
|
return;
|
|
7577
9021
|
}
|
|
7578
9022
|
}
|
|
7579
|
-
if (key.ctrl &&
|
|
9023
|
+
if (key.ctrl && input2 === "c") {
|
|
7580
9024
|
onDone(null);
|
|
7581
9025
|
exit();
|
|
7582
9026
|
}
|
|
@@ -7784,7 +9228,7 @@ async function openSettings() {
|
|
|
7784
9228
|
} catch (error) {
|
|
7785
9229
|
if (error instanceof KeytarUnavailableError) {
|
|
7786
9230
|
console.log("Settings saved, but secrets were not stored in the system keychain.");
|
|
7787
|
-
console.log("Use
|
|
9231
|
+
console.log("Use TELEPAT_OPENROUTER_KEY and TELEPAT_REPLICATE_TOKEN in this environment.");
|
|
7788
9232
|
return;
|
|
7789
9233
|
}
|
|
7790
9234
|
throw error;
|
|
@@ -7797,15 +9241,15 @@ import path14 from "path";
|
|
|
7797
9241
|
import { spawn } from "child_process";
|
|
7798
9242
|
|
|
7799
9243
|
// src/server/previewServer.ts
|
|
7800
|
-
import { execFile } from "child_process";
|
|
7801
|
-
import { promisify } from "util";
|
|
9244
|
+
import { execFile as execFile2 } from "child_process";
|
|
9245
|
+
import { promisify as promisify2 } from "util";
|
|
7802
9246
|
import { readFile as readFile11, stat as stat6 } from "fs/promises";
|
|
7803
9247
|
import { watch as fsWatch } from "fs";
|
|
7804
9248
|
import path13 from "path";
|
|
7805
9249
|
import { fileURLToPath } from "url";
|
|
7806
9250
|
import express from "express";
|
|
7807
9251
|
import { marked } from "marked";
|
|
7808
|
-
var
|
|
9252
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
7809
9253
|
var MissingArticleError = class extends Error {
|
|
7810
9254
|
constructor(message) {
|
|
7811
9255
|
super(message);
|
|
@@ -7958,12 +9402,12 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7958
9402
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
7959
9403
|
}
|
|
7960
9404
|
const sourcePath = resolveGenerationSourcePath(generation, markdownOutputDir);
|
|
7961
|
-
const canonicalSlug = generation.outputs.find((
|
|
9405
|
+
const canonicalSlug = generation.outputs.find((output2) => output2.contentType === generation.primaryContentType)?.slug ?? generation.outputs[0]?.slug ?? generation.id;
|
|
7962
9406
|
const outputs = await Promise.all(
|
|
7963
|
-
generation.outputs.map(async (
|
|
9407
|
+
generation.outputs.map(async (output2) => {
|
|
7964
9408
|
let markdown = "";
|
|
7965
9409
|
try {
|
|
7966
|
-
markdown = await readFile11(
|
|
9410
|
+
markdown = await readFile11(output2.sourcePath, "utf8");
|
|
7967
9411
|
} catch (error) {
|
|
7968
9412
|
if (isMissingFileError(error)) {
|
|
7969
9413
|
throw new MissingArticleError(`Generation "${generationId}" no longer exists.`);
|
|
@@ -7971,13 +9415,13 @@ async function getArticleContent(generationId, markdownOutputDir) {
|
|
|
7971
9415
|
throw error;
|
|
7972
9416
|
}
|
|
7973
9417
|
return {
|
|
7974
|
-
id:
|
|
7975
|
-
contentType:
|
|
7976
|
-
contentTypeLabel:
|
|
7977
|
-
index:
|
|
9418
|
+
id: output2.id,
|
|
9419
|
+
contentType: output2.contentType,
|
|
9420
|
+
contentTypeLabel: output2.contentTypeLabel,
|
|
9421
|
+
index: output2.index,
|
|
7978
9422
|
slug: canonicalSlug,
|
|
7979
|
-
title:
|
|
7980
|
-
htmlBody: await renderArticleHtml(markdown, generationId,
|
|
9423
|
+
title: output2.title,
|
|
9424
|
+
htmlBody: await renderArticleHtml(markdown, generationId, output2.sourcePath)
|
|
7981
9425
|
};
|
|
7982
9426
|
})
|
|
7983
9427
|
);
|
|
@@ -8012,7 +9456,7 @@ async function resolveActivePreviewArticle(preferredMarkdownPath, markdownOutput
|
|
|
8012
9456
|
};
|
|
8013
9457
|
}
|
|
8014
9458
|
function resolveGenerationSourcePath(generation, markdownOutputDir) {
|
|
8015
|
-
return generation.outputs.find((
|
|
9459
|
+
return generation.outputs.find((output2) => output2.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path13.join(markdownOutputDir, generation.id);
|
|
8016
9460
|
}
|
|
8017
9461
|
function isMissingFileError(error) {
|
|
8018
9462
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
@@ -9632,14 +11076,14 @@ async function tryOpenBrowser(url) {
|
|
|
9632
11076
|
}
|
|
9633
11077
|
try {
|
|
9634
11078
|
if (process.platform === "darwin") {
|
|
9635
|
-
await
|
|
11079
|
+
await execFileAsync2("open", [url]);
|
|
9636
11080
|
return;
|
|
9637
11081
|
}
|
|
9638
11082
|
if (process.platform === "win32") {
|
|
9639
|
-
await
|
|
11083
|
+
await execFileAsync2("cmd", ["/c", "start", "", url]);
|
|
9640
11084
|
return;
|
|
9641
11085
|
}
|
|
9642
|
-
await
|
|
11086
|
+
await execFileAsync2("xdg-open", [url]);
|
|
9643
11087
|
} catch {
|
|
9644
11088
|
}
|
|
9645
11089
|
}
|
|
@@ -9693,7 +11137,7 @@ async function runServeCommand(options) {
|
|
|
9693
11137
|
// src/cli/commands/write.tsx
|
|
9694
11138
|
import React4, { useEffect as useEffect3, useState as useState4 } from "react";
|
|
9695
11139
|
import { render as render2, useApp as useApp3 } from "ink";
|
|
9696
|
-
import { createInterface } from "readline/promises";
|
|
11140
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
9697
11141
|
|
|
9698
11142
|
// src/cli/ui/pipelinePresenter.tsx
|
|
9699
11143
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
@@ -10166,17 +11610,17 @@ function formatPipelineStageCost(stage) {
|
|
|
10166
11610
|
}
|
|
10167
11611
|
return stage.costSource === "estimated" ? `~${formatted}` : formatted;
|
|
10168
11612
|
}
|
|
10169
|
-
async function renderPlainPipeline(
|
|
11613
|
+
async function renderPlainPipeline(input2, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages) {
|
|
10170
11614
|
let previousStages = /* @__PURE__ */ new Map();
|
|
10171
11615
|
let previousItemStatuses = /* @__PURE__ */ new Map();
|
|
10172
|
-
const notificationsEnabled =
|
|
11616
|
+
const notificationsEnabled = input2.config.settings.notifications.enabled;
|
|
10173
11617
|
try {
|
|
10174
11618
|
await notifyWriteStarted({
|
|
10175
11619
|
enabled: notificationsEnabled,
|
|
10176
|
-
idea:
|
|
11620
|
+
idea: input2.idea,
|
|
10177
11621
|
runMode
|
|
10178
11622
|
});
|
|
10179
|
-
const result = await runPipelineShell(
|
|
11623
|
+
const result = await runPipelineShell(input2, {
|
|
10180
11624
|
dryRun,
|
|
10181
11625
|
enrichLinks: enrichLinks2,
|
|
10182
11626
|
runMode,
|
|
@@ -10306,8 +11750,8 @@ function WriteOptionsFlow({
|
|
|
10306
11750
|
};
|
|
10307
11751
|
});
|
|
10308
11752
|
};
|
|
10309
|
-
useInput2((
|
|
10310
|
-
if (key.ctrl &&
|
|
11753
|
+
useInput2((input2, key) => {
|
|
11754
|
+
if (key.ctrl && input2 === "c") {
|
|
10311
11755
|
onDone(null);
|
|
10312
11756
|
exit();
|
|
10313
11757
|
return;
|
|
@@ -10323,7 +11767,7 @@ function WriteOptionsFlow({
|
|
|
10323
11767
|
setCursor((current) => (current + 1) % secondarySelections.length);
|
|
10324
11768
|
return;
|
|
10325
11769
|
}
|
|
10326
|
-
if (
|
|
11770
|
+
if (input2 === " ") {
|
|
10327
11771
|
setSecondarySelections(
|
|
10328
11772
|
(current) => current.map(
|
|
10329
11773
|
(item, index) => index === cursor && item.contentType !== primaryType ? {
|
|
@@ -10539,7 +11983,7 @@ function WriteOptionsFlow({
|
|
|
10539
11983
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
10540
11984
|
var USER_INTERRUPTED_MESSAGE = "Write interrupted by user. Run `ideon write resume` to continue from the last checkpoint.";
|
|
10541
11985
|
function WriteApp({
|
|
10542
|
-
input,
|
|
11986
|
+
input: input2,
|
|
10543
11987
|
dryRun,
|
|
10544
11988
|
enrichLinks: enrichLinks2,
|
|
10545
11989
|
runMode,
|
|
@@ -10561,11 +12005,11 @@ function WriteApp({
|
|
|
10561
12005
|
void (async () => {
|
|
10562
12006
|
try {
|
|
10563
12007
|
await notifyWriteStarted({
|
|
10564
|
-
enabled:
|
|
10565
|
-
idea:
|
|
12008
|
+
enabled: input2.config.settings.notifications.enabled,
|
|
12009
|
+
idea: input2.idea,
|
|
10566
12010
|
runMode
|
|
10567
12011
|
});
|
|
10568
|
-
const runResult = await runPipelineShell(
|
|
12012
|
+
const runResult = await runPipelineShell(input2, {
|
|
10569
12013
|
dryRun,
|
|
10570
12014
|
enrichLinks: enrichLinks2,
|
|
10571
12015
|
runMode,
|
|
@@ -10585,7 +12029,7 @@ function WriteApp({
|
|
|
10585
12029
|
setResult(runResult);
|
|
10586
12030
|
onSuccess?.(runResult);
|
|
10587
12031
|
await notifyWriteSucceeded({
|
|
10588
|
-
enabled:
|
|
12032
|
+
enabled: input2.config.settings.notifications.enabled,
|
|
10589
12033
|
title: runResult.artifact.title,
|
|
10590
12034
|
slug: runResult.artifact.slug
|
|
10591
12035
|
});
|
|
@@ -10598,7 +12042,7 @@ function WriteApp({
|
|
|
10598
12042
|
setErrorMessage(messageWithResumeHint);
|
|
10599
12043
|
onError(new Error(messageWithResumeHint));
|
|
10600
12044
|
await notifyWriteFailed({
|
|
10601
|
-
enabled:
|
|
12045
|
+
enabled: input2.config.settings.notifications.enabled,
|
|
10602
12046
|
message: messageWithResumeHint
|
|
10603
12047
|
});
|
|
10604
12048
|
}
|
|
@@ -10606,7 +12050,7 @@ function WriteApp({
|
|
|
10606
12050
|
return () => {
|
|
10607
12051
|
mounted = false;
|
|
10608
12052
|
};
|
|
10609
|
-
}, [dryRun, enrichLinks2,
|
|
12053
|
+
}, [dryRun, enrichLinks2, input2, links, unlinks, maxLinks, maxImages, onError, runMode]);
|
|
10610
12054
|
useEffect3(() => {
|
|
10611
12055
|
if (!result && !errorMessage2) {
|
|
10612
12056
|
return;
|
|
@@ -10618,11 +12062,11 @@ function WriteApp({
|
|
|
10618
12062
|
clearTimeout(exitTimer);
|
|
10619
12063
|
};
|
|
10620
12064
|
}, [errorMessage2, exit, result]);
|
|
10621
|
-
return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt:
|
|
12065
|
+
return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt: input2.idea, stages, result, errorMessage: errorMessage2 });
|
|
10622
12066
|
}
|
|
10623
12067
|
async function runWriteCommand(options) {
|
|
10624
|
-
const
|
|
10625
|
-
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);
|
|
10626
12070
|
}
|
|
10627
12071
|
async function runWriteResumeCommand(options = {}) {
|
|
10628
12072
|
const session = await loadWriteSession();
|
|
@@ -10636,7 +12080,7 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
10636
12080
|
idea: session.idea,
|
|
10637
12081
|
audience: session.targetAudienceHint ?? void 0
|
|
10638
12082
|
});
|
|
10639
|
-
const
|
|
12083
|
+
const input2 = {
|
|
10640
12084
|
...resolved,
|
|
10641
12085
|
job: session.job,
|
|
10642
12086
|
config: {
|
|
@@ -10644,9 +12088,9 @@ async function runWriteResumeCommand(options = {}) {
|
|
|
10644
12088
|
secrets: resolved.config.secrets
|
|
10645
12089
|
}
|
|
10646
12090
|
};
|
|
10647
|
-
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);
|
|
10648
12092
|
}
|
|
10649
|
-
async function runWritePipeline(
|
|
12093
|
+
async function runWritePipeline(input2, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages, exportPath) {
|
|
10650
12094
|
let interruptHandled = false;
|
|
10651
12095
|
const handleSignal = (signal) => {
|
|
10652
12096
|
if (interruptHandled) {
|
|
@@ -10656,7 +12100,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10656
12100
|
void (async () => {
|
|
10657
12101
|
try {
|
|
10658
12102
|
await notifyWriteCanceled({
|
|
10659
|
-
enabled:
|
|
12103
|
+
enabled: input2.config.settings.notifications.enabled,
|
|
10660
12104
|
signal
|
|
10661
12105
|
});
|
|
10662
12106
|
await recordInterruptedWrite(signal);
|
|
@@ -10680,7 +12124,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10680
12124
|
process.on("SIGTERM", onSigterm);
|
|
10681
12125
|
try {
|
|
10682
12126
|
if (noInteractive || !process.stdout.isTTY) {
|
|
10683
|
-
const result = await renderPlainPipeline(
|
|
12127
|
+
const result = await renderPlainPipeline(input2, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
|
|
10684
12128
|
if (exportPath) {
|
|
10685
12129
|
await runOutputCommand({
|
|
10686
12130
|
generationId: result.artifact.slug,
|
|
@@ -10695,7 +12139,7 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode, noInteract
|
|
|
10695
12139
|
/* @__PURE__ */ jsx7(
|
|
10696
12140
|
WriteApp,
|
|
10697
12141
|
{
|
|
10698
|
-
input,
|
|
12142
|
+
input: input2,
|
|
10699
12143
|
dryRun,
|
|
10700
12144
|
enrichLinks: enrichLinks2,
|
|
10701
12145
|
runMode,
|
|
@@ -10852,20 +12296,20 @@ function isMissingIdeaError(error) {
|
|
|
10852
12296
|
return error.message.startsWith("No idea provided.");
|
|
10853
12297
|
}
|
|
10854
12298
|
async function promptForIdea() {
|
|
10855
|
-
const
|
|
12299
|
+
const readline2 = createInterface2({
|
|
10856
12300
|
input: process.stdin,
|
|
10857
12301
|
output: process.stdout
|
|
10858
12302
|
});
|
|
10859
12303
|
try {
|
|
10860
12304
|
while (true) {
|
|
10861
|
-
const idea = (await
|
|
12305
|
+
const idea = (await readline2.question("Enter primary content prompt: ")).trim();
|
|
10862
12306
|
if (idea.length > 0) {
|
|
10863
12307
|
return idea;
|
|
10864
12308
|
}
|
|
10865
12309
|
console.error("Prompt cannot be empty.");
|
|
10866
12310
|
}
|
|
10867
12311
|
} finally {
|
|
10868
|
-
|
|
12312
|
+
readline2.close();
|
|
10869
12313
|
}
|
|
10870
12314
|
}
|
|
10871
12315
|
async function autoExport(exportPath, result) {
|
|
@@ -10902,6 +12346,36 @@ async function runCli(argv) {
|
|
|
10902
12346
|
program.command("mcp").description("Model Context Protocol server operations.").command("serve").description("Start the Ideon MCP server over stdio transport.").action(async () => {
|
|
10903
12347
|
await runMcpServeCommand();
|
|
10904
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
|
+
});
|
|
10905
12379
|
const agentCommand = program.command("agent").description("Manage local agent integration runtime registrations.");
|
|
10906
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) => {
|
|
10907
12381
|
await runAgentInstallCommand({ runtime, dryRun: options.dryRun });
|