@analyticscli/growth-engineer 0.1.1-preview.0 → 0.1.1-preview.10

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.
@@ -11,6 +11,7 @@ import { buildOpenClawCronAddCommand, buildHermesCronCreateCommand, buildGrowthR
11
11
  import { loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
12
12
  const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
13
13
  const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
14
+ const SELF_UPDATE_SKILL_SLUG_CANDIDATES = ['growth-engineer', 'openclaw-growth-engineer'];
14
15
  const ENABLE_ISOLATED_SECRET_RUNNER_WIZARD = false;
15
16
  const DEFAULT_GROWTH_INTERVAL_MINUTES = 90;
16
17
  const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
@@ -83,7 +84,7 @@ const CONNECTOR_DEFINITIONS = [
83
84
  key: 'paddle',
84
85
  label: 'Paddle Billing metrics',
85
86
  summary: 'Read web checkout, revenue, MRR, refunds, chargebacks, and active subscriber metrics.',
86
- needs: 'A Paddle API key with metrics.read permission for the live account.',
87
+ needs: 'A scoped Paddle API key for the live account with metrics.read permission.',
87
88
  },
88
89
  {
89
90
  key: 'seo',
@@ -855,6 +856,7 @@ function resolveRuntimeScriptPath(scriptName) {
855
856
  const candidates = [
856
857
  path.join(RUNTIME_DIR, scriptName),
857
858
  path.resolve('scripts', scriptName),
859
+ path.resolve('skills/growth-engineer/scripts', scriptName),
858
860
  path.resolve('skills/openclaw-growth-engineer/scripts', scriptName),
859
861
  ];
860
862
  return candidates.find((candidate) => existsSync(candidate)) || path.join(RUNTIME_DIR, scriptName);
@@ -904,7 +906,9 @@ function sourceCommandNeedsActiveConfig(sourceName, command) {
904
906
  const value = String(command || '').toLowerCase();
905
907
  return (normalized === 'sentry' ||
906
908
  normalized === 'glitchtip' ||
909
+ normalized === 'paddle' ||
907
910
  normalized === 'coolify' ||
911
+ value.includes('export-paddle-summary') ||
908
912
  value.includes('export-sentry-summary') ||
909
913
  value.includes('export-coolify-summary') ||
910
914
  value.includes('exporters coolify-summary'));
@@ -1090,7 +1094,7 @@ function withMissingRequiredAnalyticsConnector(selected) {
1090
1094
  }
1091
1095
  async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = [], copy = {}) {
1092
1096
  if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
1093
- return await askConnectorSelectionByText(rl, healthByConnector, copy);
1097
+ return await askConnectorSelectionByText(rl, healthByConnector, initialSelected, copy);
1094
1098
  }
1095
1099
  rl.pause();
1096
1100
  let completed = false;
@@ -1108,20 +1112,23 @@ async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initi
1108
1112
  }
1109
1113
  }
1110
1114
  }
1111
- async function askConnectorSelectionByText(rl, healthByConnector = {}, copy = {}) {
1115
+ async function askConnectorSelectionByText(rl, healthByConnector = {}, initialSelected = [], copy = {}) {
1112
1116
  printConnectorIntro(copy);
1113
1117
  for (const group of connectorPickerGroups(healthByConnector)) {
1114
1118
  process.stdout.write(`${ANSI.bold}${group.title}${ANSI.reset}\n`);
1115
1119
  for (const connector of group.connectors) {
1116
1120
  const number = CONNECTOR_DEFINITIONS.findIndex((entry) => entry.key === connector.key) + 1;
1117
1121
  process.stdout.write(` ${number}) ${connector.label}\n`);
1118
- writeWrapped(formatConnectorHealthText(connector.key, healthByConnector), ' ', ANSI.dim);
1119
- writeWrapped(connector.summary, ' ');
1120
1122
  }
1121
1123
  process.stdout.write('\n');
1122
1124
  }
1125
+ const required = copy.mode === 'input' ? new Set() : getRequiredConnectorKeys();
1126
+ const defaultSelection = orderConnectors([...new Set([...initialSelected, ...required])]);
1127
+ const defaultAnswer = defaultSelection.length > 0
1128
+ ? defaultSelection.map((key) => String(CONNECTOR_DEFINITIONS.findIndex((entry) => entry.key === key) + 1)).join(',')
1129
+ : '';
1123
1130
  while (true) {
1124
- const answer = await ask(rl, 'Select connectors (comma-separated numbers/names, or all)', 'all');
1131
+ const answer = await ask(rl, 'Select connectors (comma-separated numbers/names, or all)', defaultAnswer);
1125
1132
  const selected = parseConnectorAnswer(answer);
1126
1133
  if (selected.length > 0)
1127
1134
  return selected;
@@ -1445,14 +1452,19 @@ function normalizeConnectorProgressKey(key) {
1445
1452
  return accountConnector;
1446
1453
  return null;
1447
1454
  }
1448
- async function withConnectorHealthLoading(taskFactory) {
1455
+ async function withConnectorHealthLoading(taskFactory, expectedConnectors = [...CONNECTOR_KEYS]) {
1456
+ const expected = orderConnectors(expectedConnectors);
1457
+ if (expected.length === 0) {
1458
+ return await taskFactory(() => { });
1459
+ }
1449
1460
  const frames = ['-', '\\', '|', '/'];
1450
1461
  const completed = new Set();
1462
+ const expectedSet = new Set(expected);
1451
1463
  let index = 0;
1452
1464
  let current = 'starting';
1453
1465
  const render = () => {
1454
- const count = Math.min(completed.size, CONNECTOR_KEYS.length);
1455
- process.stdout.write(`\rChecking connector health ${count}/${CONNECTOR_KEYS.length} (${current}) ${frames[index]}`);
1466
+ const count = Math.min(completed.size, expected.length);
1467
+ process.stdout.write(`\rChecking connector health ${count}/${expected.length} (${current}) ${frames[index]}`);
1456
1468
  };
1457
1469
  const timer = setInterval(() => {
1458
1470
  index = (index + 1) % frames.length;
@@ -1462,14 +1474,14 @@ async function withConnectorHealthLoading(taskFactory) {
1462
1474
  try {
1463
1475
  const result = await taskFactory((event) => {
1464
1476
  const key = normalizeConnectorProgressKey(event?.key);
1465
- if (!key)
1477
+ if (!key || !expectedSet.has(key))
1466
1478
  return;
1467
1479
  current = connectorLabel(key);
1468
1480
  if (event?.phase === 'finish')
1469
1481
  completed.add(key);
1470
1482
  render();
1471
1483
  });
1472
- CONNECTOR_KEYS.forEach((key) => completed.add(key));
1484
+ expected.forEach((key) => completed.add(key));
1473
1485
  current = 'done';
1474
1486
  render();
1475
1487
  process.stdout.write('\n');
@@ -1525,12 +1537,14 @@ function connectorStatusLabel(key, healthByConnector = {}) {
1525
1537
  if (health.status === 'connected')
1526
1538
  return configured ? 'configured, healthy' : 'healthy via local tool auth';
1527
1539
  if (!configured)
1528
- return 'not configured';
1540
+ return '';
1529
1541
  return `configured, ${connectorHealthLabel(health.status)}`;
1530
1542
  }
1531
1543
  function formatConnectorHealthText(key, healthByConnector = {}) {
1532
1544
  const health = getConnectorHealth(key, healthByConnector);
1533
1545
  const label = connectorStatusLabel(key, healthByConnector);
1546
+ if (!label)
1547
+ return '';
1534
1548
  const detail = health.detail ? ` - ${health.detail}` : '';
1535
1549
  return `Status: ${label}${detail}`;
1536
1550
  }
@@ -1589,11 +1603,133 @@ function connectorPickerDisplayItems(healthByConnector = {}) {
1589
1603
  return connectorPickerGroups(healthByConnector).flatMap((group) => group.connectors);
1590
1604
  }
1591
1605
  function connectorKeysNeedingAttention(healthByConnector = {}) {
1592
- return CONNECTOR_KEYS.filter((key) => ['blocked', 'partial', 'unknown', 'not_connected'].includes(String(getConnectorHealth(key, healthByConnector).status || '')));
1606
+ return CONNECTOR_KEYS.filter((key) => {
1607
+ const health = getConnectorHealth(key, healthByConnector);
1608
+ const status = String(health.status || '');
1609
+ if (!['blocked', 'partial', 'unknown', 'not_connected'].includes(status))
1610
+ return false;
1611
+ return isConnectorLocallyConfigured(key) || status !== 'not_connected';
1612
+ });
1613
+ }
1614
+ function isConfiguredSource(config, sourceName) {
1615
+ return Boolean(config?.sources?.[sourceName] && config.sources[sourceName].enabled !== false);
1616
+ }
1617
+ function configuredConnectorKeysFromConfig(config) {
1618
+ const configured = new Set();
1619
+ if (!config || typeof config !== 'object')
1620
+ return [];
1621
+ for (const key of ['analytics', 'revenuecat', 'paddle', 'seo', 'sentry', 'coolify']) {
1622
+ if (isConfiguredSource(config, key))
1623
+ configured.add(key);
1624
+ }
1625
+ if (isConfiguredGitHubRepo(config?.project?.githubRepo))
1626
+ configured.add('github');
1627
+ for (const source of Array.isArray(config?.sources?.extra) ? config.sources.extra : []) {
1628
+ if (!source || source.enabled === false)
1629
+ continue;
1630
+ if (source.service === 'asc-cli') {
1631
+ configured.add('asc');
1632
+ continue;
1633
+ }
1634
+ const connector = normalizeConnectorProgressKey(source.key || source.service || source.type);
1635
+ if (connector)
1636
+ configured.add(connector);
1637
+ }
1638
+ return [...configured];
1639
+ }
1640
+ async function connectorKeysForHealthCheck(configPath) {
1641
+ const configured = new Set();
1642
+ CONNECTOR_KEYS.forEach((key) => {
1643
+ if (isConnectorLocallyConfigured(key))
1644
+ configured.add(key);
1645
+ });
1646
+ const config = await readJsonIfPresent(configPath).catch(() => null);
1647
+ for (const key of configuredConnectorKeysFromConfig(config))
1648
+ configured.add(key);
1649
+ return orderConnectors([...configured]);
1650
+ }
1651
+ function connectorKeyFromRunnerHealthKey(key) {
1652
+ const normalized = String(key || '').trim();
1653
+ if (normalized === 'analyticscli')
1654
+ return 'analytics';
1655
+ if (normalized === 'appStoreConnect')
1656
+ return 'asc';
1657
+ const connector = normalizeConnectorProgressKey(normalized);
1658
+ return connector || null;
1659
+ }
1660
+ function activeIncidentStatusLabel(status) {
1661
+ const normalized = String(status || '').trim();
1662
+ return normalized || 'blocked';
1593
1663
  }
1594
- async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
1664
+ async function readActiveConnectorIncidents(configPath) {
1665
+ const statePath = deriveStatePathFromConfigPath(configPath);
1666
+ const state = await readJsonIfPresent(statePath).catch(() => null);
1667
+ const healthState = state?.connectorHealth;
1668
+ if (!healthState ||
1669
+ healthState.lastStatusOk !== false ||
1670
+ !healthState.activeIncidentFingerprint) {
1671
+ return {};
1672
+ }
1673
+ const alertJsonPath = healthState.lastAlertJsonPath
1674
+ ? path.resolve(String(healthState.lastAlertJsonPath))
1675
+ : path.resolve(path.dirname(statePath), 'runtime/connector-health/latest.json');
1676
+ const alertJson = await readJsonIfPresent(alertJsonPath).catch(() => null);
1677
+ const unhealthyConnectors = Array.isArray(alertJson?.unhealthyConnectors)
1678
+ ? alertJson.unhealthyConnectors
1679
+ : [];
1680
+ const incidents = {};
1681
+ for (const entry of unhealthyConnectors) {
1682
+ const key = connectorKeyFromRunnerHealthKey(entry?.key);
1683
+ if (!key)
1684
+ continue;
1685
+ incidents[key] = {
1686
+ ...entry,
1687
+ status: activeIncidentStatusLabel(entry?.status),
1688
+ detail: String(entry?.detail || 'Runner still has an active connector-health incident').trim(),
1689
+ activeRunnerIncident: true,
1690
+ activeIncidentFingerprint: healthState.activeIncidentFingerprint,
1691
+ lastCheckedAt: healthState.lastCheckedAt || null,
1692
+ };
1693
+ }
1694
+ return incidents;
1695
+ }
1696
+ function mergeActiveConnectorIncidents(healthByConnector, activeIncidents) {
1697
+ if (!activeIncidents || Object.keys(activeIncidents).length === 0) {
1698
+ return healthByConnector;
1699
+ }
1700
+ return Object.fromEntries(CONNECTOR_KEYS.map((key) => {
1701
+ const liveHealth = getConnectorHealth(key, healthByConnector);
1702
+ const incident = activeIncidents[key];
1703
+ if (!incident)
1704
+ return [key, liveHealth];
1705
+ if (liveHealth.status === 'connected') {
1706
+ return [
1707
+ key,
1708
+ {
1709
+ ...liveHealth,
1710
+ status: 'partial',
1711
+ detail: `Live wizard check passed, but the runner still has an active ${incident.status} incident; run the connector health job once to record recovery.`,
1712
+ activeRunnerIncident: true,
1713
+ activeIncidentFingerprint: incident.activeIncidentFingerprint,
1714
+ lastCheckedAt: incident.lastCheckedAt,
1715
+ },
1716
+ ];
1717
+ }
1718
+ return [
1719
+ key,
1720
+ {
1721
+ ...liveHealth,
1722
+ ...incident,
1723
+ status: incident.status || liveHealth.status || 'blocked',
1724
+ detail: incident.detail || liveHealth.detail,
1725
+ },
1726
+ ];
1727
+ }));
1728
+ }
1729
+ async function getConnectorPickerHealth(configPath, onProgress = () => { }, onlyConnectors = []) {
1730
+ const activeIncidents = await readActiveConnectorIncidents(configPath);
1595
1731
  if (!(await fileExists(configPath))) {
1596
- return Object.fromEntries(CONNECTOR_KEYS.map((key) => [
1732
+ const fallbackHealth = Object.fromEntries(CONNECTOR_KEYS.map((key) => [
1597
1733
  key,
1598
1734
  {
1599
1735
  status: isConnectorLocallyConfigured(key) ? 'unknown' : 'not_connected',
@@ -1602,8 +1738,14 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
1602
1738
  : '',
1603
1739
  },
1604
1740
  ]));
1741
+ return mergeActiveConnectorIncidents(fallbackHealth, activeIncidents);
1605
1742
  }
1606
- const result = await runCommandCaptureWithProgress(`${nodeRuntimeScriptCommand('openclaw-growth-status.mjs')} --config ${quote(configPath)} --json --progress-json`, onProgress);
1743
+ if (onlyConnectors.length === 0) {
1744
+ const fallbackHealth = Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, {})]));
1745
+ return mergeActiveConnectorIncidents(fallbackHealth, activeIncidents);
1746
+ }
1747
+ const onlyArg = ` --only-connectors ${quote(orderConnectors(onlyConnectors).join(','))}`;
1748
+ const result = await runCommandCaptureWithProgress(`${nodeRuntimeScriptCommand('openclaw-growth-status.mjs')} --config ${quote(configPath)} --json --progress-json${onlyArg}`, onProgress);
1607
1749
  const payload = parseJsonFromStdout(result.stdout);
1608
1750
  const connectors = payload?.connectors && typeof payload.connectors === 'object' ? payload.connectors : {};
1609
1751
  const healthByConnector = {
@@ -1616,7 +1758,8 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
1616
1758
  coolify: connectors.coolify,
1617
1759
  asc: connectors.appStoreConnect,
1618
1760
  };
1619
- return Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, healthByConnector)]));
1761
+ const liveHealth = Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, healthByConnector)]));
1762
+ return mergeActiveConnectorIncidents(liveHealth, activeIncidents);
1620
1763
  }
1621
1764
  function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '', copy = {}) {
1622
1765
  process.stdout.write('\x1b[2J\x1b[H');
@@ -1637,11 +1780,9 @@ function renderConnectorPicker(cursorIndex, selected, required, healthByConnecto
1637
1780
  const label = `${connector.label}${suffix}`;
1638
1781
  const title = active ? `${ANSI.bold}${label}${ANSI.reset}` : label;
1639
1782
  process.stdout.write(`${pointer} ${box} ${title}\n`);
1640
- writeWrapped(connector.summary, ' ');
1641
- writeWrapped(formatConnectorHealthText(connector.key, healthByConnector), ' ', ANSI.dim);
1642
- process.stdout.write('\n');
1643
1783
  index += 1;
1644
1784
  }
1785
+ process.stdout.write('\n');
1645
1786
  }
1646
1787
  if (warning) {
1647
1788
  process.stdout.write(`${ANSI.bold}${warning}${ANSI.reset}\n\n`);
@@ -1657,9 +1798,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
1657
1798
  let cursorIndex = 0;
1658
1799
  const required = copy.mode === 'input' ? new Set() : getRequiredConnectorKeys();
1659
1800
  const initial = new Set(initialSelected);
1660
- const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) ||
1661
- initial.has(key) ||
1662
- (copy.mode !== 'input' && !isConnectorLocallyConfigured(key))));
1801
+ const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) || initial.has(key)));
1663
1802
  let warning = '';
1664
1803
  return await new Promise((resolve, reject) => {
1665
1804
  const displayItems = () => connectorPickerDisplayItems(healthByConnector);
@@ -2020,7 +2159,7 @@ function summarizeFailureFix(connector, blockers) {
2020
2159
  return 'Paste a RevenueCat v2 secret API key with read-only project permissions, then rerun setup.';
2021
2160
  }
2022
2161
  if (connector === 'paddle') {
2023
- return 'Paste a Paddle API key with metrics.read permission for the live account, then rerun setup.';
2162
+ return 'Paste a live Paddle API key from Developer Tools > Authentication v2 with metrics.read permission, then rerun setup.';
2024
2163
  }
2025
2164
  if (connector === 'seo') {
2026
2165
  return 'Configure Search Console read access. Leave GSC_SITE_URL empty to scan all verified properties in the account, or set it only when you intentionally want one property.';
@@ -2099,12 +2238,20 @@ function payloadHasConnectorFailures(payload, connector) {
2099
2238
  const blockers = Array.isArray(payload?.blockers) ? payload.blockers : [];
2100
2239
  return blockers.some((blocker) => !isDeferredGitHubFailure(blocker) && connectorForBlocker(blocker) === connector);
2101
2240
  }
2241
+ function payloadOtherConnectorFailures(payload, connector) {
2242
+ const blockers = Array.isArray(payload?.blockers) ? payload.blockers : [];
2243
+ return blockers.filter((blocker) => {
2244
+ if (isDeferredGitHubFailure(blocker))
2245
+ return false;
2246
+ const blockerConnector = connectorForBlocker(blocker);
2247
+ return blockerConnector !== connector && blockerConnector !== 'setup';
2248
+ });
2249
+ }
2102
2250
  async function askListSelection(rl, label, entries, options = {}) {
2103
2251
  const includeManual = Boolean(options.includeManual);
2104
2252
  const includeDefer = Boolean(options.includeDefer);
2105
2253
  entries.forEach((entry, index) => {
2106
- const description = entry.description ? ` - ${entry.description}` : '';
2107
- process.stdout.write(` ${index + 1}) ${entry.label}${description}\n`);
2254
+ process.stdout.write(` ${index + 1}) ${entry.label}\n`);
2108
2255
  });
2109
2256
  const manualIndex = includeManual ? entries.length + 1 : null;
2110
2257
  const deferIndex = includeDefer ? entries.length + (includeManual ? 2 : 1) : null;
@@ -2154,6 +2301,8 @@ function printSetupSuccess(payload) {
2154
2301
  }
2155
2302
  function connectorFromCheckName(name) {
2156
2303
  const value = String(name || '');
2304
+ if (value.includes('asc') || value.includes('ASC_') || /App Store Connect|app-store-connect|app_store_connect|Analytics Report Request/i.test(value))
2305
+ return 'asc';
2157
2306
  if (value.includes('analytics') || value.includes('ANALYTICSCLI'))
2158
2307
  return 'analytics';
2159
2308
  if (value.includes('github') || value.includes('GITHUB'))
@@ -2168,8 +2317,6 @@ function connectorFromCheckName(name) {
2168
2317
  return 'sentry';
2169
2318
  if (value.includes('coolify') || value.includes('COOLIFY'))
2170
2319
  return 'coolify';
2171
- if (value.includes('asc') || value.includes('ASC_'))
2172
- return 'asc';
2173
2320
  for (const key of ACCOUNT_SIGNAL_CONNECTOR_KEYS) {
2174
2321
  const definition = getAccountSignalConnectorDefinition(key);
2175
2322
  const envMatch = definition?.credentials.some((credential) => value.includes(credential.env));
@@ -2388,10 +2535,13 @@ async function saveSecretsImmediately(secrets) {
2388
2535
  process.stdout.write(`Saved local secrets to ${secretsFile} with chmod 600.\n`);
2389
2536
  return true;
2390
2537
  }
2391
- async function runImmediateConnectorHealthCheck({ rl, configPath, connector, secrets, sentryAccounts = [], }) {
2538
+ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, secrets, sentryAccounts = [], paddleAccounts = [], }) {
2392
2539
  if (connector === 'sentry' && sentryAccounts.length > 0) {
2393
2540
  await upsertSentryAccountsConfig(configPath, sentryAccounts);
2394
2541
  }
2542
+ if (connector === 'paddle' && paddleAccounts.length > 0) {
2543
+ await upsertPaddleAccountsConfig(configPath, paddleAccounts);
2544
+ }
2395
2545
  await saveSecretsImmediately(secrets);
2396
2546
  const env = {
2397
2547
  ...process.env,
@@ -2409,6 +2559,17 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
2409
2559
  const retry = await askYesNo(rl, `Re-enter ${connectorLabel(connector)} configuration now?`, true);
2410
2560
  return { ok: false, retry, result, payload };
2411
2561
  }
2562
+ const otherConnectorBlockers = payloadOtherConnectorFailures(payload, connector);
2563
+ if (otherConnectorBlockers.length > 0) {
2564
+ process.stdout.write(`\n${connectorLabel(connector)} immediate health check passed, but another configured connector needs attention.\n`);
2565
+ printConciseSetupBlockers({
2566
+ ...payload,
2567
+ blockers: otherConnectorBlockers,
2568
+ }, command, {
2569
+ hideRerunWhenClean: true,
2570
+ });
2571
+ return { ok: false, retry: false, result, payload };
2572
+ }
2412
2573
  process.stdout.write(`\n${connectorLabel(connector)} immediate health check passed or is only waiting on optional/deferred context.\n`);
2413
2574
  return { ok: true, retry: false, result, payload };
2414
2575
  }
@@ -3059,6 +3220,71 @@ async function verifySentryAccountsConfig(configPath, expectedAccounts) {
3059
3220
  }
3060
3221
  return { ok: true, detail: `${realAccounts.length} active Sentry-compatible account(s) configured` };
3061
3222
  }
3223
+ async function upsertPaddleAccountsConfig(configPath, accounts) {
3224
+ if (!accounts.length || !(await fileExists(configPath)))
3225
+ return false;
3226
+ const config = await readJsonFile(configPath);
3227
+ const existingAccounts = Array.isArray(config?.sources?.paddle?.accounts)
3228
+ ? config.sources.paddle.accounts
3229
+ : [];
3230
+ const merged = new Map();
3231
+ for (const account of existingAccounts) {
3232
+ const id = String(account?.id || account?.key || account?.label || '').trim();
3233
+ if (id)
3234
+ merged.set(id, account);
3235
+ }
3236
+ for (const account of accounts) {
3237
+ merged.set(account.id, {
3238
+ ...(merged.get(account.id) || {}),
3239
+ ...account,
3240
+ });
3241
+ }
3242
+ const tokenEnv = accounts[0]?.tokenEnv || config?.sources?.paddle?.tokenEnv || config?.secrets?.paddleTokenEnv || 'PADDLE_API_KEY';
3243
+ config.sources = {
3244
+ ...(config.sources || {}),
3245
+ paddle: {
3246
+ ...(config.sources?.paddle || {}),
3247
+ enabled: true,
3248
+ mode: 'command',
3249
+ command: normalizeWizardSourceCommand('paddle', config.sources?.paddle || {}, configPath),
3250
+ environment: config.sources?.paddle?.environment || 'live',
3251
+ tokenEnv,
3252
+ accounts: [...merged.values()],
3253
+ },
3254
+ };
3255
+ config.secrets = {
3256
+ ...(config.secrets || {}),
3257
+ paddleTokenEnv: tokenEnv,
3258
+ paddleTokenRef: { source: 'env', provider: 'default', id: tokenEnv },
3259
+ };
3260
+ await writeJsonFile(configPath, config);
3261
+ return true;
3262
+ }
3263
+ async function verifyPaddleAccountsConfig(configPath, expectedAccounts) {
3264
+ if (!(await fileExists(configPath))) {
3265
+ return { ok: false, detail: `${configPath} does not exist` };
3266
+ }
3267
+ const config = await readJsonFile(configPath);
3268
+ const source = config?.sources?.paddle;
3269
+ if (!source || source.enabled !== true) {
3270
+ return { ok: false, detail: 'sources.paddle.enabled is not true' };
3271
+ }
3272
+ if (source.mode !== 'command') {
3273
+ return { ok: false, detail: 'sources.paddle.mode is not command' };
3274
+ }
3275
+ const configuredAccounts = Array.isArray(source.accounts) ? source.accounts : [];
3276
+ if (configuredAccounts.length === 0) {
3277
+ return { ok: false, detail: 'sources.paddle.accounts contains no account' };
3278
+ }
3279
+ const configuredIds = new Set(configuredAccounts.map((account) => String(account?.id || account?.key || '').trim()).filter(Boolean));
3280
+ const missingIds = expectedAccounts
3281
+ .map((account) => String(account?.id || '').trim())
3282
+ .filter((id) => id && !configuredIds.has(id));
3283
+ if (missingIds.length > 0) {
3284
+ return { ok: false, detail: `sources.paddle.accounts is missing configured account id(s): ${missingIds.join(', ')}` };
3285
+ }
3286
+ return { ok: true, detail: `${configuredAccounts.length} Paddle account(s) configured` };
3287
+ }
3062
3288
  async function upsertCoolifyConfig(configPath, { baseUrl, tokenEnv = 'COOLIFY_API_TOKEN' }) {
3063
3289
  if (!(await fileExists(configPath)))
3064
3290
  return false;
@@ -3356,21 +3582,57 @@ async function guideRevenueCatConnector(rl, secrets) {
3356
3582
  if (apiKey)
3357
3583
  secrets.REVENUECAT_API_KEY = apiKey;
3358
3584
  }
3585
+ function paddleAccountIdFromLabel(label, index) {
3586
+ const normalized = String(label || '')
3587
+ .trim()
3588
+ .toLowerCase()
3589
+ .replace(/[^a-z0-9]+/g, '_')
3590
+ .replace(/^_+|_+$/g, '');
3591
+ return normalized || `paddle_${index + 1}`;
3592
+ }
3593
+ function paddleTokenEnvForAccount(index, label) {
3594
+ if (index === 0)
3595
+ return 'PADDLE_API_KEY';
3596
+ const suffix = paddleAccountIdFromLabel(label, index).toUpperCase().replace(/[^A-Z0-9]+/g, '_');
3597
+ const base = suffix && suffix !== `PADDLE_${index + 1}` ? `PADDLE_API_KEY_${suffix}` : `PADDLE_API_KEY_${index + 1}`;
3598
+ return base.replace(/_+/g, '_');
3599
+ }
3359
3600
  async function guidePaddleConnector(rl, secrets) {
3360
3601
  printSection('Paddle Billing metrics', [
3361
3602
  'Use this when OpenClaw should read web checkout, revenue, MRR, refunds, chargebacks, and active subscriber metrics.',
3362
3603
  ]);
3363
- process.stdout.write('\nCreate or update a Paddle API key here:\n https://vendors.paddle.com/authentication\n\n');
3604
+ process.stdout.write('\nCreate or update a scoped Paddle API key here:\n https://vendors.paddle.com/authentication-v2\n\n');
3364
3605
  printBullets([
3365
3606
  'Open Paddle > Developer Tools > Authentication.',
3366
- 'Create a new API key for the live account when you want production revenue evidence.',
3367
- 'Grant `metrics.read`. Keep write permissions off unless another workflow explicitly needs them.',
3607
+ 'Use the API keys tab and create a new live API key.',
3608
+ 'Minimum: grant `metrics.read` so account-level revenue, MRR, refunds, chargebacks, subscribers, and checkout conversion work.',
3609
+ 'Recommended for better Growth Engineer analysis: grant all available read-only permissions (`*.read`), including products, prices, discounts, customers, transactions, subscriptions, adjustments, reports, and notifications.',
3610
+ 'Do not grant any write permissions (`*.write`) unless another workflow explicitly needs them.',
3368
3611
  'Do not select or hard-code a single product in the wizard; the Growth Engineer should keep account-level metrics context.',
3369
3612
  'Paste the key here so it is stored only in the local chmod 600 secrets file.',
3370
3613
  ]);
3371
- const apiKey = await maybePromptSecret(rl, 'Paste PADDLE_API_KEY into this local terminal', 'PADDLE_API_KEY');
3372
- if (apiKey)
3373
- secrets.PADDLE_API_KEY = apiKey;
3614
+ const accounts = [];
3615
+ let index = 0;
3616
+ while (true) {
3617
+ const label = await ask(rl, index === 0 ? 'Paddle account label' : 'Next Paddle account label (empty = done)', index === 0 ? 'Paddle' : '');
3618
+ if (!label.trim())
3619
+ break;
3620
+ const tokenEnv = paddleTokenEnvForAccount(index, label);
3621
+ const apiKey = await maybePromptSecret(rl, `Paste ${tokenEnv} into this local terminal`, tokenEnv);
3622
+ if (apiKey)
3623
+ secrets[tokenEnv] = apiKey;
3624
+ accounts.push({
3625
+ id: paddleAccountIdFromLabel(label, index),
3626
+ label: label.trim(),
3627
+ tokenEnv,
3628
+ environment: 'live',
3629
+ });
3630
+ index += 1;
3631
+ const addAnother = await askYesNo(rl, 'Add another Paddle account?', false);
3632
+ if (!addAnother)
3633
+ break;
3634
+ }
3635
+ return accounts;
3374
3636
  }
3375
3637
  async function guideSeoConnector(rl, secrets) {
3376
3638
  printSection('SEO / Google Search Console / DataForSEO', [
@@ -3402,10 +3664,15 @@ async function guideSeoConnector(rl, secrets) {
3402
3664
  secrets.DATAFORSEO_PASSWORD = password;
3403
3665
  }
3404
3666
  }
3405
- function buildAccountSignalExtraSourceConfig(key, existing = {}) {
3667
+ function buildAccountSignalExtraSourceConfig(key, existing = {}, accounts = []) {
3406
3668
  const definition = getAccountSignalConnectorDefinition(key);
3407
3669
  if (!definition)
3408
3670
  return existing;
3671
+ const accountConfig = accounts.length > 0
3672
+ ? {
3673
+ accounts: mergeConnectorAccounts(existing.accounts, accounts),
3674
+ }
3675
+ : {};
3409
3676
  return {
3410
3677
  ...buildExtraSourceConfig(definition.service, {
3411
3678
  key: definition.key,
@@ -3429,9 +3696,28 @@ function buildAccountSignalExtraSourceConfig(key, existing = {}) {
3429
3696
  signalKind: definition.sourceKind,
3430
3697
  experimental: Boolean(definition.experimental),
3431
3698
  hint: existing.hint || definition.signalHint,
3699
+ ...accountConfig,
3432
3700
  };
3433
3701
  }
3434
- async function upsertAccountSignalConnectorConfig(configPath, key) {
3702
+ function mergeConnectorAccounts(existingAccounts, nextAccounts) {
3703
+ const merged = new Map();
3704
+ for (const account of Array.isArray(existingAccounts) ? existingAccounts : []) {
3705
+ const id = String(account?.id || account?.key || account?.label || '').trim();
3706
+ if (id)
3707
+ merged.set(id, account);
3708
+ }
3709
+ for (const account of nextAccounts) {
3710
+ const id = String(account?.id || account?.key || account?.label || '').trim();
3711
+ if (!id)
3712
+ continue;
3713
+ merged.set(id, {
3714
+ ...(merged.get(id) || {}),
3715
+ ...account,
3716
+ });
3717
+ }
3718
+ return [...merged.values()];
3719
+ }
3720
+ async function upsertAccountSignalConnectorConfig(configPath, key, accounts = []) {
3435
3721
  const definition = getAccountSignalConnectorDefinition(key);
3436
3722
  if (!definition)
3437
3723
  return false;
@@ -3440,7 +3726,7 @@ async function upsertAccountSignalConnectorConfig(configPath, key) {
3440
3726
  const extra = Array.isArray(sources.extra) ? sources.extra : [];
3441
3727
  const nextExtra = extra.filter((source) => String(source?.key || source?.service || '') !== definition.key);
3442
3728
  const existing = extra.find((source) => String(source?.key || source?.service || '') === definition.key) || {};
3443
- nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing));
3729
+ nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing, accounts));
3444
3730
  config.sources = {
3445
3731
  ...sources,
3446
3732
  extra: nextExtra,
@@ -3448,28 +3734,58 @@ async function upsertAccountSignalConnectorConfig(configPath, key) {
3448
3734
  await writeJsonFile(configPath, config);
3449
3735
  return true;
3450
3736
  }
3737
+ function accountSignalTokenEnvForAccount(baseEnv, key, index, label) {
3738
+ if (index === 0)
3739
+ return baseEnv;
3740
+ const suffix = toConfigId(label || key, `${key}_${index + 1}`).toUpperCase().replace(/[^A-Z0-9]+/g, '_');
3741
+ return `${baseEnv}_${suffix}`.replace(/_+/g, '_');
3742
+ }
3451
3743
  async function guideAccountSignalConnector(rl, secrets, key) {
3452
3744
  const definition = getAccountSignalConnectorDefinition(key);
3453
3745
  if (!definition)
3454
- return;
3746
+ return [];
3455
3747
  printSection(definition.label, [
3456
3748
  definition.summary,
3457
3749
  'Setup is account-wide. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here.',
3458
3750
  ]);
3459
3751
  process.stdout.write(`Docs: ${definition.docsUrl}\n\n`);
3460
3752
  printBullets(definition.steps);
3461
- for (const credential of definition.credentials) {
3462
- const defaultValue = credential.defaultValue ?? process.env[credential.env] ?? '';
3463
- const value = credential.optional
3464
- ? await maybePromptSecret(rl, credential.prompt, credential.env)
3465
- : await maybePromptSecret(rl, credential.prompt, credential.env);
3466
- const finalValue = value || defaultValue;
3467
- if (finalValue)
3468
- secrets[credential.env] = finalValue;
3469
- else if (!credential.optional) {
3470
- process.stdout.write(`${credential.env} was not saved. ${definition.label} setup remains pending; rerun this wizard when ready.\n`);
3753
+ const accounts = [];
3754
+ let index = 0;
3755
+ while (true) {
3756
+ const label = await ask(rl, index === 0 ? `${definition.label} account label` : `Next ${definition.label} account label (empty = done)`, index === 0 ? definition.label.replace(/\s+\(experimental\)$/i, '') : '');
3757
+ if (!label.trim())
3758
+ break;
3759
+ const credentialEnvs = {};
3760
+ for (const credential of definition.credentials) {
3761
+ const envName = accountSignalTokenEnvForAccount(credential.env, key, index, label);
3762
+ const defaultValue = index === 0 ? credential.defaultValue ?? process.env[credential.env] ?? '' : '';
3763
+ const prompt = envName === credential.env ? credential.prompt : `${credential.prompt.replace(credential.env, envName)}`;
3764
+ const value = credential.optional
3765
+ ? await maybePromptSecret(rl, prompt, envName)
3766
+ : await maybePromptSecret(rl, prompt, envName);
3767
+ const finalValue = value || defaultValue;
3768
+ credentialEnvs[credential.env] = envName;
3769
+ if (finalValue)
3770
+ secrets[envName] = finalValue;
3771
+ else if (!credential.optional) {
3772
+ process.stdout.write(`${envName} was not saved. ${definition.label} setup remains pending for ${label}; rerun this wizard when ready.\n`);
3773
+ }
3471
3774
  }
3775
+ accounts.push({
3776
+ id: toConfigId(label, `${key}_${index + 1}`),
3777
+ label: label.trim(),
3778
+ credentialEnvs,
3779
+ tokenEnv: credentialEnvs[definition.credentials[0]?.env] || definition.credentials[0]?.env || null,
3780
+ accountWide: true,
3781
+ projectScope: 'discover_from_account',
3782
+ });
3783
+ index += 1;
3784
+ const addAnother = await askYesNo(rl, `Add another ${definition.label} account?`, false);
3785
+ if (!addAnother)
3786
+ break;
3472
3787
  }
3788
+ return accounts;
3473
3789
  }
3474
3790
  async function guideSentryConnector(rl, secrets) {
3475
3791
  printSection('Sentry / GlitchTip', [
@@ -3694,6 +4010,23 @@ async function filesHaveSameContent(leftPath, rightPath) {
3694
4010
  return false;
3695
4011
  }
3696
4012
  }
4013
+ function getSelfUpdateSkillCandidates(workspaceRoot) {
4014
+ const explicit = String(process.env.OPENCLAW_GROWTH_SKILL_SLUG || '').trim();
4015
+ const uniqueSlugs = [...new Set([explicit, ...SELF_UPDATE_SKILL_SLUG_CANDIDATES].filter(Boolean))];
4016
+ return uniqueSlugs.map((slug) => {
4017
+ const skillRoot = path.join(workspaceRoot, 'skills', slug);
4018
+ return {
4019
+ slug,
4020
+ skillRoot,
4021
+ originPath: path.join(skillRoot, '.clawhub/origin.json'),
4022
+ wizardPath: path.join(skillRoot, 'scripts/openclaw-growth-wizard.mjs'),
4023
+ bootstrapPath: path.join(skillRoot, 'scripts/bootstrap-openclaw-workspace.sh'),
4024
+ };
4025
+ });
4026
+ }
4027
+ function resolveInstalledSelfUpdateSkill(workspaceRoot) {
4028
+ return getSelfUpdateSkillCandidates(workspaceRoot).find((candidate) => existsSync(candidate.originPath)) || null;
4029
+ }
3697
4030
  async function maybeSelfUpdateFromClawHub(args) {
3698
4031
  if (args.noSelfUpdate)
3699
4032
  return false;
@@ -3704,26 +4037,27 @@ async function maybeSelfUpdateFromClawHub(args) {
3704
4037
  if (isFalseyEnv(process.env.OPENCLAW_GROWTH_SELF_UPDATE))
3705
4038
  return false;
3706
4039
  const workspaceRoot = process.cwd();
3707
- const skillOriginPath = path.join(workspaceRoot, 'skills/openclaw-growth-engineer/.clawhub/origin.json');
3708
- if (!(await fileExists(skillOriginPath)))
4040
+ const installedSkill = resolveInstalledSelfUpdateSkill(workspaceRoot);
4041
+ if (!installedSkill)
3709
4042
  return false;
3710
4043
  if (!(await commandExists('npx')))
3711
4044
  return false;
3712
- const force = String(process.env.OPENCLAW_GROWTH_SELF_UPDATE || '').trim().toLowerCase() === 'always';
4045
+ const force = args.connectorWizard || String(process.env.OPENCLAW_GROWTH_SELF_UPDATE || '').trim().toLowerCase() === 'always';
3713
4046
  if (!(await shouldRunSelfUpdate(workspaceRoot, force)))
3714
4047
  return false;
3715
- const beforeOrigin = await readJsonIfPresent(skillOriginPath).catch(() => null);
4048
+ const beforeOrigin = await readJsonIfPresent(installedSkill.originPath).catch(() => null);
3716
4049
  const beforeVersion = String(beforeOrigin?.installedVersion || '');
3717
- process.stdout.write('Checking for OpenClaw Growth Engineer skill updates...\n');
3718
- const updateResult = await runCommandCaptureWithTimeout('npx -y clawhub --no-input --dir skills update openclaw-growth-engineer --force', { timeoutMs: 120_000 });
3719
- const afterOrigin = await readJsonIfPresent(skillOriginPath).catch(() => null);
4050
+ process.stdout.write(`Checking for Growth Engineer skill updates (${installedSkill.slug})...\n`);
4051
+ const updateResult = await runCommandCaptureWithTimeout(`npx -y clawhub --no-input --dir skills update ${quote(installedSkill.slug)} --force`, { timeoutMs: 120_000 });
4052
+ const afterOrigin = await readJsonIfPresent(installedSkill.originPath).catch(() => null);
3720
4053
  const afterVersion = String(afterOrigin?.installedVersion || beforeVersion || '');
3721
4054
  const workspaceWizardPath = path.resolve(process.argv[1] || 'scripts/openclaw-growth-wizard.mjs');
3722
- const skillWizardPath = path.join(workspaceRoot, 'skills/openclaw-growth-engineer/scripts/openclaw-growth-wizard.mjs');
3723
- const runtimeOutdated = !(await filesHaveSameContent(workspaceWizardPath, skillWizardPath));
4055
+ const runtimeOutdated = !(await filesHaveSameContent(workspaceWizardPath, installedSkill.wizardPath));
3724
4056
  await writeSelfUpdateState(workspaceRoot, {
3725
4057
  lastCheckedAt: new Date().toISOString(),
3726
4058
  ok: updateResult.ok,
4059
+ skillSlug: installedSkill.slug,
4060
+ skillRoot: installedSkill.skillRoot,
3727
4061
  previousVersion: beforeVersion || null,
3728
4062
  installedVersion: afterVersion || null,
3729
4063
  }).catch(() => { });
@@ -3741,7 +4075,7 @@ async function maybeSelfUpdateFromClawHub(args) {
3741
4075
  else {
3742
4076
  process.stdout.write('Refreshing workspace runtime from the installed OpenClaw Growth Engineer skill...\n');
3743
4077
  }
3744
- const bootstrapResult = await runCommandCaptureWithTimeout('bash skills/openclaw-growth-engineer/scripts/bootstrap-openclaw-workspace.sh', { timeoutMs: 60_000 });
4078
+ const bootstrapResult = await runCommandCaptureWithTimeout(`bash ${quote(installedSkill.bootstrapPath)}`, { timeoutMs: 60_000 });
3745
4079
  if (!bootstrapResult.ok) {
3746
4080
  process.stdout.write(`${ANSI.dim}Workspace runtime refresh failed; continuing with current process.${ANSI.reset}\n`);
3747
4081
  return false;
@@ -3760,6 +4094,7 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
3760
4094
  process.stdout.write('\n');
3761
4095
  const secrets = {};
3762
4096
  let sentryAccounts = [];
4097
+ let paddleAccounts = [];
3763
4098
  let coolifyConfig = null;
3764
4099
  if (selected.includes('analytics')) {
3765
4100
  let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
@@ -3808,12 +4143,13 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
3808
4143
  if (selected.includes('paddle')) {
3809
4144
  while (true) {
3810
4145
  clearTerminal();
3811
- await guidePaddleConnector(rl, secrets);
4146
+ paddleAccounts = await guidePaddleConnector(rl, secrets);
3812
4147
  const check = await runImmediateConnectorHealthCheck({
3813
4148
  rl,
3814
4149
  configPath: args.config,
3815
4150
  connector: 'paddle',
3816
4151
  secrets,
4152
+ paddleAccounts,
3817
4153
  });
3818
4154
  if (!check.retry)
3819
4155
  break;
@@ -3882,8 +4218,8 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
3882
4218
  for (const connector of selected.filter(isAccountSignalConnector)) {
3883
4219
  while (true) {
3884
4220
  clearTerminal();
3885
- await guideAccountSignalConnector(rl, secrets, connector);
3886
- await upsertAccountSignalConnectorConfig(args.config, connector);
4221
+ const accountSignalAccounts = await guideAccountSignalConnector(rl, secrets, connector);
4222
+ await upsertAccountSignalConnectorConfig(args.config, connector, accountSignalAccounts);
3887
4223
  const check = await runImmediateConnectorHealthCheck({
3888
4224
  rl,
3889
4225
  configPath: args.config,
@@ -3910,6 +4246,12 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
3910
4246
  process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
3911
4247
  }
3912
4248
  }
4249
+ if (paddleAccounts.length > 0 && await upsertPaddleAccountsConfig(args.config, paddleAccounts)) {
4250
+ const readiness = await verifyPaddleAccountsConfig(args.config, paddleAccounts);
4251
+ if (readiness.ok) {
4252
+ process.stdout.write(`Configured ${paddleAccounts.length} Paddle account(s) in ${args.config}.\n`);
4253
+ }
4254
+ }
3913
4255
  if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
3914
4256
  process.stdout.write(`Configured Coolify monitoring for ${coolifyConfig.baseUrl} in ${args.config}.\n`);
3915
4257
  }
@@ -3934,6 +4276,19 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
3934
4276
  });
3935
4277
  }
3936
4278
  }
4279
+ if (paddleAccounts.length > 0 && await upsertPaddleAccountsConfig(args.config, paddleAccounts)) {
4280
+ const readiness = await verifyPaddleAccountsConfig(args.config, paddleAccounts);
4281
+ if (readiness.ok) {
4282
+ process.stdout.write(`Paddle account config is up to date in ${args.config}.\n`);
4283
+ }
4284
+ else {
4285
+ postSetupBlockers.push({
4286
+ check: 'connection:paddle',
4287
+ detail: readiness.detail,
4288
+ remediation: 'Rerun Paddle setup so the active config persists sources.paddle.enabled=true and sources.paddle.accounts[].',
4289
+ });
4290
+ }
4291
+ }
3937
4292
  if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
3938
4293
  process.stdout.write(`Coolify config is up to date in ${args.config}.\n`);
3939
4294
  }
@@ -3976,7 +4331,8 @@ async function runConnectorSetupWizard(args) {
3976
4331
  clearTerminal();
3977
4332
  printConnectorIntro();
3978
4333
  await migrateRuntimeSourceCommandsFile(args.config);
3979
- const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(args.config, onProgress));
4334
+ const healthCheckConnectors = await connectorKeysForHealthCheck(args.config);
4335
+ const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(args.config, onProgress, healthCheckConnectors), healthCheckConnectors);
3980
4336
  const existingFixes = connectorKeysNeedingAttention(healthByConnector);
3981
4337
  const requestedConnectors = args.connectors ? parseConnectorList(args.connectors) : [];
3982
4338
  const chosenConnectors = requestedConnectors.length > 0
@@ -4900,7 +5256,8 @@ async function askInputSourceConfig(rl, config, configPath) {
4900
5256
  config = migrateRuntimeSourceCommands(config, configPath);
4901
5257
  await ensureDirForFile(configPath);
4902
5258
  await writeJsonFile(configPath, config);
4903
- const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(configPath, onProgress));
5259
+ const healthCheckConnectors = await connectorKeysForHealthCheck(configPath);
5260
+ const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(configPath, onProgress, healthCheckConnectors), healthCheckConnectors);
4904
5261
  const selected = await askConnectorSelectionWithHealth(rl, healthByConnector, getInputChannelInitialSelection(config), {
4905
5262
  introTitle: 'Input channels',
4906
5263
  introDetail: null,