@telepat/ideon 0.1.32 → 0.1.33

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