@analyticscli/growth-engineer 0.1.1-preview.18 → 0.1.1-preview.20

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.
@@ -3524,13 +3524,14 @@ function printBullets(lines) {
3524
3524
  }
3525
3525
  process.stdout.write('\n');
3526
3526
  }
3527
+ function bold(text) {
3528
+ return `${ANSI.bold}${text}${ANSI.reset}`;
3529
+ }
3527
3530
  async function guideGitHubConnector(rl, secrets) {
3528
3531
  printSection('GitHub code access', [
3529
- 'Use this when OpenClaw should read repo context or create GitHub delivery artifacts.',
3530
- ]);
3531
- printBullets([
3532
- 'Open the token page, select the scopes you want, then paste the token here.',
3533
- 'You can rerun this wizard later to change GitHub permissions.',
3532
+ `${bold('Create token')} here: https://github.com/settings/tokens/new`,
3533
+ `${bold('Scopes')}: public repos = public_repo, private repos/issues/PRs = repo.`,
3534
+ `${bold('Only add workflow')} if OpenClaw should edit GitHub Actions files.`,
3534
3535
  ]);
3535
3536
  let hasGh = await commandExists('gh');
3536
3537
  if (!hasGh) {
@@ -3539,15 +3540,6 @@ async function guideGitHubConnector(rl, secrets) {
3539
3540
  if (hasGh) {
3540
3541
  process.stdout.write('GitHub CLI is available for helper commands.\n\n');
3541
3542
  }
3542
- process.stdout.write('Token URL: https://github.com/settings/tokens/new\n\n');
3543
- process.stdout.write(`${ANSI.bold}Suggested scopes${ANSI.reset}\n`);
3544
- printBullets([
3545
- 'Public repo only: select `public_repo`.',
3546
- 'Private repo access: select `repo` (classic GitHub tokens make private repo access broad).',
3547
- 'Create issues / draft PRs in private repos: `repo` is the relevant classic-token scope.',
3548
- 'Edit GitHub Actions workflow files: add `workflow` only if you explicitly want this.',
3549
- 'Usually do not select: packages, admin:org, hooks, gist, user, delete_repo, enterprise, codespace, copilot.',
3550
- ]);
3551
3543
  const token = await maybePromptSecret(rl, 'Paste GITHUB_TOKEN into this local terminal', 'GITHUB_TOKEN');
3552
3544
  if (token)
3553
3545
  secrets.GITHUB_TOKEN = token;
@@ -3568,12 +3560,10 @@ function shouldForceFreshAnalyticsToken(healthByConnector = {}) {
3568
3560
  return ['blocked', 'partial'].includes(String(health?.status || '')) || /revoked|unauthorized|invalid token/i.test(detail);
3569
3561
  }
3570
3562
  async function guideAnalyticsConnector(rl, secrets, options = {}) {
3571
- printSection('AnalyticsCLI');
3572
- process.stdout.write('Create a readonly CLI token:\n');
3573
- process.stdout.write('1. Open https://dash.analyticscli.com/\n');
3574
- process.stdout.write('2. Account -> API Keys\n');
3575
- process.stdout.write('3. Create Access Token\n');
3576
- process.stdout.write('4. Copy the Readonly CLI Token and paste it below\n\n');
3563
+ printSection('AnalyticsCLI', [
3564
+ `${bold('Create readonly CLI token')}: https://dash.analyticscli.com/`,
3565
+ `${bold('Path')}: Account -> API Keys -> Create Access Token.`,
3566
+ ]);
3577
3567
  const forceFresh = Boolean(options.forceFresh);
3578
3568
  if (forceFresh && process.env.ANALYTICSCLI_ACCESS_TOKEN) {
3579
3569
  process.stdout.write('Stored token failed. Paste a new token.\n\n');
@@ -3590,17 +3580,9 @@ async function guideAnalyticsConnector(rl, secrets, options = {}) {
3590
3580
  }
3591
3581
  async function guideRevenueCatConnector(rl, secrets) {
3592
3582
  printSection('RevenueCat monetization data', [
3593
- 'Use this when OpenClaw should read subscription, product, entitlement, and revenue context.',
3594
- ]);
3595
- process.stdout.write('\nCreate a RevenueCat secret API key here:\n https://app.revenuecat.com/\n\n');
3596
- printBullets([
3597
- 'Select your app.',
3598
- 'In the sidebar, choose "Apps & providers".',
3599
- 'Click "API keys" and generate a new secret API key.',
3600
- 'Name it "analyticscli" and choose API version 2.',
3601
- 'Set Charts metrics permissions to read.',
3602
- 'Set Customer information permissions to read.',
3603
- 'Set Project configuration permissions to read.',
3583
+ `${bold('Create secret API key')}: https://app.revenuecat.com/`,
3584
+ `${bold('Path')}: Apps & providers -> API keys -> New secret key.`,
3585
+ `${bold('Permissions')}: API v2, read for Charts metrics, Customer information, Project configuration.`,
3604
3586
  ]);
3605
3587
  const apiKey = await maybePromptSecret(rl, 'Paste REVENUECAT_API_KEY into this local terminal', 'REVENUECAT_API_KEY');
3606
3588
  if (apiKey)
@@ -3623,17 +3605,9 @@ function paddleTokenEnvForAccount(index, label) {
3623
3605
  }
3624
3606
  async function guidePaddleConnector(rl, secrets) {
3625
3607
  printSection('Paddle Billing metrics', [
3626
- 'Use this when OpenClaw should read web checkout, revenue, MRR, refunds, chargebacks, and active subscriber metrics.',
3627
- ]);
3628
- process.stdout.write('\nCreate or update a scoped Paddle API key here:\n https://vendors.paddle.com/authentication-v2\n\n');
3629
- printBullets([
3630
- 'Open Paddle > Developer Tools > Authentication.',
3631
- 'Use the API keys tab and create a new live API key.',
3632
- 'Minimum: grant `metrics.read` so account-level revenue, MRR, refunds, chargebacks, subscribers, and checkout conversion work.',
3633
- 'Recommended for better Growth Engineer analysis: grant all available read-only permissions (`*.read`), including products, prices, discounts, customers, transactions, subscriptions, adjustments, reports, and notifications.',
3634
- 'Do not grant any write permissions (`*.write`) unless another workflow explicitly needs them.',
3635
- 'Do not select or hard-code a single product in the wizard; the Growth Engineer should keep account-level metrics context.',
3636
- 'Paste the key here so it is stored only in the local chmod 600 secrets file.',
3608
+ `${bold('Create live API key')}: https://vendors.paddle.com/authentication-v2`,
3609
+ `${bold('Minimum')}: metrics.read. Better: all read-only *.read scopes.`,
3610
+ `${bold('Do not grant write scopes')} unless you explicitly need them elsewhere.`,
3637
3611
  ]);
3638
3612
  const accounts = [];
3639
3613
  let index = 0;
@@ -3660,17 +3634,10 @@ async function guidePaddleConnector(rl, secrets) {
3660
3634
  }
3661
3635
  async function guideSeoConnector(rl, secrets) {
3662
3636
  printSection('SEO / Google Search Console / DataForSEO', [
3663
- 'Use this when OpenClaw should read organic search demand, GSC clicks/impressions/CTR/position, and optional paid keyword ideas.',
3664
- ]);
3665
- process.stdout.write('\nGoogle Search Console:\n https://search.google.com/search-console\nGoogle Cloud service accounts:\n https://console.cloud.google.com/iam-admin/serviceaccounts\nDataForSEO API dashboard:\n https://app.dataforseo.com/api-dashboard\n\n');
3666
- printBullets([
3667
- 'Preferred: give the token/service account access to all Search Console properties you want analyzed.',
3668
- 'Leave the property URL empty to let the exporter list and query all verified GSC properties in the account.',
3669
- 'Enter a property URL only when you intentionally want to restrict analysis to one site.',
3670
- 'For OAuth token mode, paste a read-only Search Console token with `webmasters.readonly` scope.',
3671
- 'For service-account mode, add the service account email as a restricted/full user in Search Console, then set GOOGLE_APPLICATION_CREDENTIALS or GSC_SERVICE_ACCOUNT_JSON outside this wizard.',
3672
- 'DataForSEO is optional and paid. The exporter refuses paid calls unless the source command includes --confirm-paid and a small --max-paid-requests cap.',
3673
- 'CSV-only mode is also supported with --gsc-csv or --csv in sources.seo.command.',
3637
+ `${bold('GSC')}: https://search.google.com/search-console`,
3638
+ `${bold('Service account')}: https://console.cloud.google.com/iam-admin/serviceaccounts`,
3639
+ `${bold('Optional paid keyword data')}: https://app.dataforseo.com/api-dashboard`,
3640
+ `${bold('Default')}: leave property URL empty to use all verified GSC properties.`,
3674
3641
  ]);
3675
3642
  const siteUrl = await ask(rl, 'Optional GSC property URL (empty = all verified properties)', process.env.GSC_SITE_URL || '');
3676
3643
  if (siteUrl.trim())
@@ -3769,11 +3736,10 @@ async function guideAccountSignalConnector(rl, secrets, key) {
3769
3736
  if (!definition)
3770
3737
  return [];
3771
3738
  printSection(definition.label, [
3772
- definition.summary,
3773
- 'Setup is account-wide. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here.',
3739
+ `${bold('Docs')}: ${definition.docsUrl}`,
3740
+ `${bold('Setup is account-wide')}. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here.`,
3741
+ `${bold('Paste only credentials')} below. The agent discovers accounts/apps/projects later.`,
3774
3742
  ]);
3775
- process.stdout.write(`Docs: ${definition.docsUrl}\n\n`);
3776
- printBullets(definition.steps);
3777
3743
  const accounts = [];
3778
3744
  let index = 0;
3779
3745
  while (true) {
@@ -3813,8 +3779,8 @@ async function guideAccountSignalConnector(rl, secrets, key) {
3813
3779
  }
3814
3780
  async function guideSentryConnector(rl, secrets) {
3815
3781
  printSection('Sentry / GlitchTip', [
3816
- 'Paste token, org, and base URL. Projects are discovered automatically.',
3817
- 'Use `https://sentry.io` for Sentry Cloud or your GlitchTip/self-hosted base URL.',
3782
+ `${bold('Base URL')}: https://sentry.io for Sentry Cloud, otherwise your GlitchTip/self-hosted URL.`,
3783
+ `${bold('Token + org')} are needed. Project scope remains unpinned.`,
3818
3784
  ]);
3819
3785
  const accounts = [];
3820
3786
  let index = 0;
@@ -3866,7 +3832,7 @@ async function guideSentryConnector(rl, secrets) {
3866
3832
  let verifiedVisibleProjects = false;
3867
3833
  if (discovery.ok && discovery.projects.length > 0) {
3868
3834
  verifiedVisibleProjects = true;
3869
- process.stdout.write(`Found ${discovery.projects.length} visible project(s). Project scope remains unpinned so OpenClaw/Hermes can decide per run.\n`);
3835
+ process.stdout.write(`Found ${discovery.projects.length} visible project(s). Project scope remains unpinned.\n`);
3870
3836
  }
3871
3837
  else {
3872
3838
  const fallbackOrgs = discoveredOrganizations
@@ -3923,19 +3889,12 @@ function normalizeCoolifyBaseUrl(value) {
3923
3889
  }
3924
3890
  async function guideCoolifyConnector(rl, secrets) {
3925
3891
  printSection('Coolify deployment monitoring', [
3926
- 'Use this when OpenClaw should read deployment, resource, server, and health-check signals from Coolify.',
3927
- 'The token should be read-only. Do not use "*" or sensitive-token permissions for normal monitoring.',
3892
+ `${bold('Create read-only API token')} in Coolify.`,
3893
+ `${bold('Do not use * or sensitive-token permissions')} for normal monitoring.`,
3928
3894
  ]);
3929
3895
  const baseUrl = normalizeCoolifyBaseUrl(await ask(rl, 'Coolify base URL', process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com'));
3930
3896
  const tokenUrl = baseUrl ? `${baseUrl}/security/api-tokens` : 'https://<your-coolify-host>/security/api-tokens';
3931
- process.stdout.write(`\nToken page: ${tokenUrl}\n\n`);
3932
- printBullets([
3933
- 'Open the Coolify dashboard.',
3934
- 'In the sidebar, go to "Keys & Tokens".',
3935
- 'Open "API tokens".',
3936
- 'Create a new API key/token with read-only permissions.',
3937
- 'Copy the token once and paste it into this local terminal.',
3938
- ]);
3897
+ process.stdout.write(`${bold('Token page')}: ${tokenUrl}\n\n`);
3939
3898
  const token = await maybePromptSecret(rl, 'Paste COOLIFY_API_TOKEN into this local terminal', 'COOLIFY_API_TOKEN');
3940
3899
  if (baseUrl)
3941
3900
  secrets.COOLIFY_BASE_URL = baseUrl;
@@ -3945,37 +3904,18 @@ async function guideCoolifyConnector(rl, secrets) {
3945
3904
  }
3946
3905
  async function guideAscConnector(rl, secrets) {
3947
3906
  printSection('App Store Connect CLI', [
3948
- 'You need two App Store Connect API keys: one normal reporting key and one temporary Admin key.',
3949
- 'Create both here: https://appstoreconnect.apple.com/access/integrations/api',
3950
- ]);
3951
- process.stdout.write('\nStep 1 - normal key, saved for Growth Engineer\n');
3952
- printBullets([
3953
- 'Create an API key named "Growth Engineer Reports".',
3954
- 'Role: Sales and Reports. Finance or Admin also works.',
3955
- 'Optional extra roles: Customer Support for reviews, Developer for builds/TestFlight.',
3956
- 'Copy this key into the ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY prompts below.',
3957
- ]);
3958
- process.stdout.write('\nStep 2 - temporary Admin key, used once\n');
3959
- printBullets([
3960
- 'Create a second API key named "Growth Engineer Setup Admin".',
3961
- 'Role: Admin.',
3962
- 'Use it only when the wizard asks for ASC_BOOTSTRAP_* later.',
3963
- 'The wizard does not save this key to secrets.env. Revoke it in App Store Connect after setup.',
3964
- ]);
3965
- process.stdout.write('\nWhy two keys?\n');
3966
- printBullets([
3967
- 'Apple requires Admin once to create the initial App Analytics report request.',
3968
- 'After that, Growth Engineer only needs the normal reporting key for downloads.',
3907
+ `${bold('Create 2 API keys')} here: https://appstoreconnect.apple.com/access/integrations/api`,
3908
+ `1. ${bold('Reports key')} - role ${bold('Sales and Reports')} - saved for Growth Engineer.`,
3909
+ `2. ${bold('Setup key')} - role ${bold('Admin')} - used once, then revoke.`,
3969
3910
  ]);
3970
- process.stdout.write('\nNeeded values for the normal key now\n');
3911
+ process.stdout.write(`${bold('Enter the Reports key now:')}\n`);
3971
3912
  printBullets([
3972
- 'Issuer ID: shown at the top of the API keys page.',
3973
- '.p8 file path: use Apple\'s original downloaded file name AuthKey_<KEY_ID>.p8.',
3974
- 'Do not rename the .p8 file; the wizard reads ASC_KEY_ID from the file name.',
3975
- 'Vendor Number: App Store Connect > Sales and Trends > Reports.',
3913
+ `${bold('.p8 path')} to Apple\'s original ${bold('AuthKey_<KEY_ID>.p8')} file. ${bold('Do not rename it')}; KEY_ID is read from the filename.`,
3914
+ `${bold('Issuer ID')} from the API keys page.`,
3915
+ `${bold('Vendor Number')} from Sales and Trends > Reports.`,
3976
3916
  ]);
3977
3917
  const normalKeyPath = await askAscPrivateKeyPathWithKeyId(rl, {
3978
- label: 'ASC_PRIVATE_KEY_PATH for normal reporting key (AuthKey_<KEY_ID>.p8 path, empty = paste content instead)',
3918
+ label: 'Reports .p8 path (AuthKey_<KEY_ID>.p8, empty = paste)',
3979
3919
  defaultValue: process.env.ASC_PRIVATE_KEY_PATH || '',
3980
3920
  keyLabel: 'the normal reporting key',
3981
3921
  });
@@ -3985,11 +3925,11 @@ async function guideAscConnector(rl, secrets) {
3985
3925
  secrets.ASC_KEY_ID = keyId;
3986
3926
  process.stdout.write(`Inferred ASC_KEY_ID=${keyId} from ${path.basename(normalKeyPath.privateKeyPath)}.\n`);
3987
3927
  }
3988
- const issuerId = await ask(rl, 'ASC_ISSUER_ID for normal reporting key (leave empty to skip)', process.env.ASC_ISSUER_ID || '');
3928
+ const issuerId = await ask(rl, 'ASC_ISSUER_ID (reports key, empty = skip)', process.env.ASC_ISSUER_ID || '');
3989
3929
  if (issuerId.trim())
3990
3930
  secrets.ASC_ISSUER_ID = issuerId.trim();
3991
3931
  if (!normalKeyPath.privateKeyPath) {
3992
- keyId = await ask(rl, 'ASC_KEY_ID for normal reporting key (from AuthKey_<KEY_ID>.p8, leave empty to skip)', process.env.ASC_KEY_ID || '');
3932
+ keyId = await ask(rl, 'ASC_KEY_ID (from AuthKey_<KEY_ID>.p8, empty = skip)', process.env.ASC_KEY_ID || '');
3993
3933
  if (keyId.trim())
3994
3934
  secrets.ASC_KEY_ID = keyId.trim();
3995
3935
  const privateKeyContent = await askAscPrivateKeyContent(rl, {
@@ -4004,30 +3944,26 @@ async function guideAscConnector(rl, secrets) {
4004
3944
  secrets.ASC_PRIVATE_KEY_PATH = privateKeyPath;
4005
3945
  process.stdout.write(`Saved ASC private key to ${privateKeyPath} with chmod 600.\n`);
4006
3946
  }
4007
- const vendorNumber = await ask(rl, 'ASC_VENDOR_NUMBER for Sales and Trends/App Units (required for healthy ASC status)', process.env.ASC_VENDOR_NUMBER || '');
3947
+ const vendorNumber = await ask(rl, 'ASC_VENDOR_NUMBER (Sales and Trends > Reports)', process.env.ASC_VENDOR_NUMBER || '');
4008
3948
  if (vendorNumber.trim())
4009
3949
  secrets.ASC_VENDOR_NUMBER = vendorNumber.trim();
4010
3950
  return await guideAscBootstrapAdminKey(rl, issuerId.trim());
4011
3951
  }
4012
3952
  async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
4013
3953
  const bootstrapEnv = {};
4014
- process.stdout.write('\nStep 3 - temporary Admin key for first ASC setup\n');
3954
+ process.stdout.write(`\n${bold('Enter the Setup Admin key:')}\n`);
4015
3955
  printBullets([
4016
- 'Use the second key you created with the Admin role.',
4017
- 'This is only needed to create the first App Analytics report request.',
4018
- 'The wizard does not save ASC_BOOTSTRAP_* to secrets.env.',
4019
- 'Use Apple\'s original AuthKey_<KEY_ID>.p8 file name so the wizard can infer ASC_BOOTSTRAP_KEY_ID.',
4020
- 'Do not rename the .p8 file.',
4021
- 'If you paste the .p8 content, the local temporary file is deleted after analytics initialization.',
4022
- 'Revoke this Admin key in App Store Connect after setup.',
3956
+ `${bold('Role must be Admin')} so Apple can create the first App Analytics report request.`,
3957
+ `${bold('Use original AuthKey_<KEY_ID>.p8 filename')} so KEY_ID is read automatically.`,
3958
+ `${bold('Not saved')} to secrets.env. Revoke this key after setup.`,
4023
3959
  ]);
4024
3960
  const bootstrapKeyPath = await askAscPrivateKeyPathWithKeyId(rl, {
4025
- label: 'ASC_BOOTSTRAP_PRIVATE_KEY_PATH for temporary Admin key (AuthKey_<KEY_ID>.p8 path, empty = paste content instead)',
3961
+ label: 'Setup Admin .p8 path (AuthKey_<KEY_ID>.p8, empty = paste)',
4026
3962
  defaultValue: '',
4027
3963
  keyLabel: 'the temporary Admin key',
4028
3964
  });
4029
3965
  let bootstrapKeyId = bootstrapKeyPath.keyId;
4030
- const bootstrapIssuerId = await ask(rl, 'ASC_BOOTSTRAP_ISSUER_ID for temporary Admin key', issuerIdDefault);
3966
+ const bootstrapIssuerId = await ask(rl, 'ASC_BOOTSTRAP_ISSUER_ID', issuerIdDefault);
4031
3967
  if (bootstrapKeyPath.privateKeyPath) {
4032
3968
  bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_PATH = bootstrapKeyPath.privateKeyPath;
4033
3969
  process.stdout.write(`Inferred ASC_BOOTSTRAP_KEY_ID=${bootstrapKeyId} from ${path.basename(bootstrapKeyPath.privateKeyPath)}.\n`);
@@ -4036,7 +3972,7 @@ async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
4036
3972
  bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_DELETE_AFTER_USE = '1';
4037
3973
  }
4038
3974
  else {
4039
- bootstrapKeyId = await ask(rl, 'ASC_BOOTSTRAP_KEY_ID for temporary Admin key (from AuthKey_<KEY_ID>.p8, required)', '');
3975
+ bootstrapKeyId = await ask(rl, 'ASC_BOOTSTRAP_KEY_ID (from AuthKey_<KEY_ID>.p8)', '');
4040
3976
  }
4041
3977
  if (!bootstrapKeyId.trim() || !bootstrapIssuerId.trim())
4042
3978
  return { bootstrapEnv };