@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/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.IDEON_OPENROUTER_API_KEY,
623
- replicateApiToken: env.IDEON_REPLICATE_API_TOKEN,
624
- disableKeytar: parseBoolean(env.IDEON_DISABLE_KEYTAR),
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 IDEON_DISABLE_KEYTAR=true to skip keychain access in this environment.`
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 [openRouterApiKey, replicateApiToken] = await Promise.all([
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 IDEON_DISABLE_KEYTAR=true. Use IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN instead."
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 IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN instead.`
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 IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN instead.`
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 = ["openRouterApiKey", "replicateApiToken"];
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.31",
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(input) {
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 = input.jobPath ? await loadJobInput(input.jobPath) : null;
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(input.contentTargets, "CLI contentTargets");
1548
- assertExactlyOnePrimary(input.contentTargets, "CLI contentTargets");
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
- ...input.style ? { style: input.style } : {},
1575
- ...input.intent ? { intent: input.intent } : {},
1576
- ...input.targetLength ? { targetLength: input.targetLength } : {},
1577
- ...input.contentTargets ? { contentTargets: input.contentTargets } : {}
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 = input.idea ?? job?.idea ?? job?.prompt;
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(input.audience) ?? normalizeOptionalText(job?.targetAudience);
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 and a sharp subtitle that promise a concrete benefit, mechanism, or outcome.",
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(input) {
3236
- if (typeof input.retryAfterMs === "number" && input.retryAfterMs > 0) {
3237
- return Math.min(input.maxBackoffMs, input.retryAfterMs);
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 = input.baseBackoffMs * 2 ** (input.attempt - 1);
3240
- const capped = Math.min(input.maxBackoffMs, exponential);
3241
- const jitter = input.jitterMs > 0 ? input.randomFraction() * input.jitterMs : 0;
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(input) {
4300
- const plan = input.plan;
4301
- const contentPlan = input.contentPlan;
4302
- const generationDir = input.generationDir;
4303
- const title = plan?.title ?? contentPlan?.title ?? input.idea;
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 = input.renderedImages.find((image) => image.kind === "cover") ?? null;
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 = input.renderedImages.map((image) => ({
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 = input.outputs.map((output) => ({
4329
- fileId: output.fileId,
4330
- contentType: output.contentType,
4331
- path: output.markdownPath,
4332
- relativePath: path7.relative(generationDir, output.markdownPath)
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: input.idea,
4497
+ idea: input2.idea,
4339
4498
  description,
4340
4499
  subtitle,
4341
4500
  keywords,
4342
4501
  contentType,
4343
- style: input.style,
4344
- intent: input.intent,
4345
- targetLength: input.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: input.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(input, options = {}) {
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(input.config.settings.contentTargets);
4595
- const secondaryTargets = getSecondaryTargets(input.config.settings.contentTargets);
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: input.idea,
4662
- targetAudienceHint: input.targetAudienceHint,
4663
- job: input.job,
4664
- settings: input.config.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(input.config.secrets.openRouterApiKey, "OpenRouter API key"));
4682
- const canRenderImagesLive = Boolean(input.config.secrets.replicateApiToken);
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: input.config.secrets.openRouterApiKey ?? void 0,
4686
- replicateApiKey: requireSecret(input.config.secrets.replicateApiToken, "Replicate API token"),
4687
- openrouterModel: input.config.settings.model
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: input.idea,
4708
- targetAudienceHint: input.targetAudienceHint,
4709
- settings: input.config.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: input.idea,
4918
+ idea: input2.idea,
4760
4919
  contentType: primaryTarget.contentType,
4761
4920
  contentPlan,
4762
- settings: input.config.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: input.config.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: input.config.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: input.idea,
5170
+ idea: input2.idea,
5012
5171
  contentType: primaryTarget.contentType,
5013
5172
  role: "primary",
5014
5173
  primaryContentType: primaryTarget.contentType,
5015
- style: input.config.settings.style,
5016
- intent: input.config.settings.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: input.config.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(input.idea, contentPlan?.title);
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: input.idea,
5080
- targetAudienceHint: input.targetAudienceHint,
5238
+ idea: input2.idea,
5239
+ targetAudienceHint: input2.targetAudienceHint,
5081
5240
  dryRun,
5082
5241
  runMode,
5083
- settings: input.config.settings,
5084
- sourceJob: input.job
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: input.config.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: input.config.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(input.idea));
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(input.idea)
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((output) => ({
5282
- id: toOutputItemId(output.filePrefix, output.index),
5283
- label: formatOutputItemLabel(output.contentType, output.index, output.outputCountForType),
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 output of requestedOutputs) {
5294
- const itemId = toOutputItemId(output.filePrefix, output.index);
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(output.contentType, output.index, output.outputCountForType)}.`,
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, `${output.filePrefix}-${output.index}.md`);
5475
+ const markdownPath = path9.join(generationDir, `${output2.filePrefix}-${output2.index}.md`);
5317
5476
  try {
5318
5477
  const content = await writeSingleShotContent({
5319
- idea: input.idea,
5320
- contentType: output.contentType,
5321
- style: input.config.settings.style,
5322
- intent: input.config.settings.intent,
5323
- outputIndex: output.index,
5324
- outputCountForType: output.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: input.config.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 ${output.contentType} output ${output.index}.`);
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: output.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: output.contentType,
5360
- filePrefix: output.filePrefix,
5361
- index: output.index,
5362
- outputCountForType: output.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(output.contentType, output.index, output.outputCountForType)}.`,
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((output) => output.contentType !== "x-post" && output.contentType !== "x-thread");
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((output) => ({
5422
- id: output.fileId,
5423
- label: output.fileId,
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 output of eligibleOutputsForLinks) {
5434
- const existingLinks = await readExistingLinks(resolveLinksPath(output.markdownPath));
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(output.markdownPath, {
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((output) => ({
5486
- fileId: output.fileId,
5487
- contentType: output.contentType,
5488
- markdownPath: output.markdownPath,
5489
- links: linksByFileId.get(output.fileId) ?? [],
5490
- customLinks: customLinksByFileId.get(output.fileId) ?? []
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(input.idea),
5677
+ articleTitle: plan?.title ?? contentPlan.title ?? deriveTitleFromIdea2(input2.idea),
5519
5678
  articleDescription: plan?.description ?? contentPlan.description,
5520
5679
  openRouter,
5521
- settings: input.config.settings,
5680
+ settings: input2.config.settings,
5522
5681
  dryRun,
5523
5682
  customLinks: parsePipelineCustomLinks(pipelineCustomLinkRaws, pipelineUnlinks),
5524
- maxLinks: pipelineMaxLinks ?? resolveDefaultMaxLinks(input.config.settings.targetLength),
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: input.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: input.config.settings.style,
5639
- intent: input.config.settings.intent,
5640
- targetLength: input.config.settings.targetLength ? resolveTargetLengthAlias(input.config.settings.targetLength) : null
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(input.idea),
5647
- slug: plan?.slug ?? resolveGenerationSlug(input.idea, contentPlan?.title),
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(input) {
6192
+ function buildRunJobDefinition(input2) {
6034
6193
  return {
6035
- idea: input.idea,
6036
- prompt: input.idea,
6037
- ...input.targetAudienceHint ? { targetAudience: input.targetAudienceHint } : {},
6038
- contentTargets: input.settings.contentTargets,
6039
- style: input.settings.style,
6040
- settings: input.settings,
6041
- sourceJob: input.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: input.dryRun,
6045
- runMode: input.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 IDEON_OPENROUTER_API_KEY."
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 output = {
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 = `${output.generationId}:${output.contentType}:${output.index}`;
6853
+ const outputKey = `${output2.generationId}:${output2.contentType}:${output2.index}`;
6695
6854
  const existing = outputMap.get(outputKey);
6696
- if (!existing || output.mtime > existing.mtime) {
6697
- outputMap.set(outputKey, output);
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 output of outputMap.values()) {
6704
- const existing = grouped.get(output.generationId);
6862
+ for (const output2 of outputMap.values()) {
6863
+ const existing = grouped.get(output2.generationId);
6705
6864
  if (existing) {
6706
- existing.push(output);
6865
+ existing.push(output2);
6707
6866
  } else {
6708
- grouped.set(output.generationId, [output]);
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((output) => output.contentType === primaryContentType) ?? outputs[0];
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, output) => Math.max(latest, output.mtime), 0);
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((output) => Boolean(output.coverImageUrl))?.coverImageUrl ?? null,
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((output) => output.contentType === "article")?.contentType ?? outputs[0]?.contentType ?? "article";
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((output) => output.contentType === generation.primaryContentType);
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((output) => output.index === targetIndex);
7008
+ const articleOutput = articleOutputs.find((output2) => output2.index === targetIndex);
6850
7009
  if (!articleOutput) {
6851
- const available = articleOutputs.map((output) => output.index).join(", ");
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((output) => output.slug === generationId)
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 (input) => {
7852
+ async (input2) => {
7013
7853
  try {
7014
7854
  const parsedTargets = parsePrimaryAndSecondarySpecs({
7015
- primarySpec: input.primary,
7016
- secondarySpecs: input.secondary
7855
+ primarySpec: input2.primary,
7856
+ secondarySpecs: input2.secondary
7017
7857
  });
7018
7858
  const resolved = await resolveRunInput({
7019
- idea: input.idea,
7020
- audience: input.audience,
7021
- jobPath: input.jobPath,
7022
- style: input.style,
7023
- intent: input.intent,
7024
- targetLength: input.length,
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: input.dryRun ?? false,
7031
- enrichLinks: input.enrichLinks ?? false,
7032
- customLinks: input.link,
7033
- unlinks: input.unlink,
7034
- maxLinks: input.maxLinks,
7035
- maxImages: input.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 (input) => {
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: input.dryRun ?? false,
7091
- enrichLinks: input.enrichLinks ?? false,
7092
- customLinks: input.link,
7093
- unlinks: input.unlink,
7094
- maxLinks: input.maxLinks,
7095
- maxImages: input.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 (input) => {
7965
+ async (input2) => {
7126
7966
  try {
7127
7967
  const messages = [];
7128
7968
  await runDeleteCommand(
7129
- { slug: input.slug, force: true },
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 ${input.slug}.`
7981
+ text: messages.length > 0 ? messages.join("\n") : `Deleted ${input2.slug}.`
7142
7982
  }
7143
7983
  ],
7144
7984
  structuredContent: {
7145
- slug: input.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 (input) => {
8001
+ async (input2) => {
7162
8002
  try {
7163
8003
  const messages = [];
7164
8004
  await runLinksCommand(
7165
8005
  {
7166
- slug: input.slug,
7167
- mode: input.mode ?? "fresh",
7168
- links: input.link,
7169
- unlinks: input.unlink,
7170
- maxLinks: input.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 ${input.slug}.`
8023
+ text: messages.length > 0 ? messages.join("\n") : `Enriched links for ${input2.slug}.`
7184
8024
  }
7185
8025
  ],
7186
8026
  structuredContent: {
7187
- slug: input.slug,
7188
- mode: input.mode ?? "fresh"
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 (input) => {
8043
+ async (input2) => {
7204
8044
  try {
7205
8045
  const messages = [];
7206
8046
  await runOutputCommand(
7207
8047
  {
7208
- generationId: input.generationId,
7209
- destinationPath: input.destinationPath,
7210
- index: input.index,
7211
- overwrite: input.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 ${input.generationId}.`
8064
+ text: messages.length > 0 ? messages.join("\n") : `Exported ${input2.generationId}.`
7225
8065
  }
7226
8066
  ],
7227
8067
  structuredContent: {
7228
- generationId: input.generationId,
7229
- destinationPath: input.destinationPath,
7230
- index: input.index ?? 1,
7231
- overwrite: input.overwrite ?? false,
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 (input) => {
8087
+ async (input2) => {
7248
8088
  try {
7249
- if (!isConfigKey(input.key)) {
7250
- throw new ReportedError(`Unsupported config key: ${input.key}`);
8089
+ if (!isConfigKey(input2.key)) {
8090
+ throw new ReportedError(`Unsupported config key: ${input2.key}`);
7251
8091
  }
7252
- const result = await configGet(input.key);
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 (input) => {
8118
+ async (input2) => {
7279
8119
  try {
7280
- if (!isConfigKey(input.key)) {
7281
- throw new ReportedError(`Unsupported config key: ${input.key}`);
8120
+ if (!isConfigKey(input2.key)) {
8121
+ throw new ReportedError(`Unsupported config key: ${input2.key}`);
7282
8122
  }
7283
- await configSet(input.key, input.value);
8123
+ await configSet(input2.key, input2.value);
7284
8124
  return {
7285
8125
  content: [
7286
8126
  {
7287
8127
  type: "text",
7288
- text: `Set ${input.key}.`
8128
+ text: `Set ${input2.key}.`
7289
8129
  }
7290
8130
  ],
7291
8131
  structuredContent: {
7292
- key: input.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 (input) => {
8172
+ async (input2) => {
7333
8173
  try {
7334
- if (!isConfigKey(input.key)) {
7335
- throw new ReportedError(`Unsupported config key: ${input.key}`);
8174
+ if (!isConfigKey(input2.key)) {
8175
+ throw new ReportedError(`Unsupported config key: ${input2.key}`);
7336
8176
  }
7337
- await configUnset(input.key);
8177
+ await configUnset(input2.key);
7338
8178
  return {
7339
8179
  content: [
7340
8180
  {
7341
8181
  type: "text",
7342
- text: `Unset ${input.key}.`
8182
+ text: `Unset ${input2.key}.`
7343
8183
  }
7344
8184
  ],
7345
8185
  structuredContent: {
7346
- key: input.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((input, key) => {
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 && input === "c") {
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 IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN in this environment.");
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 execFileAsync = promisify(execFile);
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((output) => output.contentType === generation.primaryContentType)?.slug ?? generation.outputs[0]?.slug ?? generation.id;
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 (output) => {
9407
+ generation.outputs.map(async (output2) => {
7964
9408
  let markdown = "";
7965
9409
  try {
7966
- markdown = await readFile11(output.sourcePath, "utf8");
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: output.id,
7975
- contentType: output.contentType,
7976
- contentTypeLabel: output.contentTypeLabel,
7977
- index: output.index,
9418
+ id: output2.id,
9419
+ contentType: output2.contentType,
9420
+ contentTypeLabel: output2.contentTypeLabel,
9421
+ index: output2.index,
7978
9422
  slug: canonicalSlug,
7979
- title: output.title,
7980
- htmlBody: await renderArticleHtml(markdown, generationId, output.sourcePath)
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((output) => output.contentType === generation.primaryContentType)?.sourcePath ?? generation.outputs[0]?.sourcePath ?? path13.join(markdownOutputDir, generation.id);
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 execFileAsync("open", [url]);
11079
+ await execFileAsync2("open", [url]);
9636
11080
  return;
9637
11081
  }
9638
11082
  if (process.platform === "win32") {
9639
- await execFileAsync("cmd", ["/c", "start", "", url]);
11083
+ await execFileAsync2("cmd", ["/c", "start", "", url]);
9640
11084
  return;
9641
11085
  }
9642
- await execFileAsync("xdg-open", [url]);
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(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages) {
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 = input.config.settings.notifications.enabled;
11616
+ const notificationsEnabled = input2.config.settings.notifications.enabled;
10173
11617
  try {
10174
11618
  await notifyWriteStarted({
10175
11619
  enabled: notificationsEnabled,
10176
- idea: input.idea,
11620
+ idea: input2.idea,
10177
11621
  runMode
10178
11622
  });
10179
- const result = await runPipelineShell(input, {
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((input, key) => {
10310
- if (key.ctrl && input === "c") {
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 (input === " ") {
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: input.config.settings.notifications.enabled,
10565
- idea: input.idea,
12008
+ enabled: input2.config.settings.notifications.enabled,
12009
+ idea: input2.idea,
10566
12010
  runMode
10567
12011
  });
10568
- const runResult = await runPipelineShell(input, {
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: input.config.settings.notifications.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: input.config.settings.notifications.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, input, links, unlinks, maxLinks, maxImages, onError, runMode]);
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: input.idea, stages, result, errorMessage: errorMessage2 });
12065
+ return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt: input2.idea, stages, result, errorMessage: errorMessage2 });
10622
12066
  }
10623
12067
  async function runWriteCommand(options) {
10624
- const input = await resolveInputWithInteractiveIdeaFallback(options);
10625
- await runWritePipeline(input, options.dryRun, options.enrichLinks, "fresh", options.noInteractive, options.links, options.unlinks, options.maxLinks, options.maxImages, options.exportPath);
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 input = {
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(input, session.dryRun, options.enrichLinks ?? false, "resume", options.noInteractive ?? false, options.links, options.unlinks, options.maxLinks, options.maxImages, options.exportPath);
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(input, dryRun, enrichLinks2, runMode, noInteractive, links, unlinks, maxLinks, maxImages, exportPath) {
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: input.config.settings.notifications.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(input, dryRun, enrichLinks2, runMode, links, unlinks, maxLinks, maxImages);
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 readline = createInterface({
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 readline.question("Enter primary content prompt: ")).trim();
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
- readline.close();
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 });