@analyticscli/growth-engineer 0.1.0-preview.6 → 0.1.0-preview.8

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.
@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process';
6
6
  import { createInterface } from 'node:readline/promises';
7
7
  import { emitKeypressEvents } from 'node:readline';
8
8
  import { createPrivateKey } from 'node:crypto';
9
- import { buildExtraSourceConfig, getDefaultSourceCommand, getDefaultSourceHint, getDefaultSourcePath, } from './openclaw-growth-shared.mjs';
9
+ import { buildExtraSourceConfig, getDefaultSourceCommand, } from './openclaw-growth-shared.mjs';
10
10
  import { loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
11
11
  const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
12
12
  const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -278,14 +278,14 @@ function withMissingRequiredAnalyticsConnector(selected) {
278
278
  return orderConnectors(selected);
279
279
  return orderConnectors(['analytics', ...selected]);
280
280
  }
281
- async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = []) {
281
+ async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = [], copy = {}) {
282
282
  if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
283
- return await askConnectorSelectionByText(rl, healthByConnector);
283
+ return await askConnectorSelectionByText(rl, healthByConnector, copy);
284
284
  }
285
285
  rl.pause();
286
286
  let completed = false;
287
287
  try {
288
- const selected = await askConnectorSelectionByKeys(healthByConnector, initialSelected);
288
+ const selected = await askConnectorSelectionByKeys(healthByConnector, initialSelected, copy);
289
289
  completed = true;
290
290
  return selected;
291
291
  }
@@ -298,8 +298,8 @@ async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initi
298
298
  }
299
299
  }
300
300
  }
301
- async function askConnectorSelectionByText(rl, healthByConnector = {}) {
302
- printConnectorIntro();
301
+ async function askConnectorSelectionByText(rl, healthByConnector = {}, copy = {}) {
302
+ printConnectorIntro(copy);
303
303
  for (const group of connectorPickerGroups(healthByConnector)) {
304
304
  process.stdout.write(`${ANSI.bold}${group.title}${ANSI.reset}\n`);
305
305
  for (const connector of group.connectors) {
@@ -337,9 +337,15 @@ function orderConnectors(keys) {
337
337
  const selected = new Set(keys);
338
338
  return CONNECTOR_KEYS.filter((key) => selected.has(key));
339
339
  }
340
- function printConnectorIntro() {
341
- process.stdout.write(`\n${ANSI.bold}OpenClaw connector setup${ANSI.reset}\n`);
342
- process.stdout.write(`${ANSI.dim}You can configure connector secrets here. API keys stay in this host's local secrets file, not in chat or config JSON.${ANSI.reset}\n\n`);
340
+ function printConnectorIntro(copy = {}) {
341
+ process.stdout.write(`\n${ANSI.bold}${copy.introTitle || 'OpenClaw connector setup'}${ANSI.reset}\n`);
342
+ const detail = copy.introDetail === undefined
343
+ ? 'You can configure connector secrets here. API keys stay in this host\'s local secrets file, not in chat or config JSON.'
344
+ : copy.introDetail;
345
+ if (detail) {
346
+ process.stdout.write(`${ANSI.dim}${detail}${ANSI.reset}\n`);
347
+ }
348
+ process.stdout.write('\n');
343
349
  }
344
350
  async function askMenuChoice(rl, { title, subtitle = 'Use Up/Down to move, Enter to continue.', options, defaultValue, renderHeader, }) {
345
351
  if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
@@ -626,11 +632,11 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
626
632
  };
627
633
  return Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, healthByConnector)]));
628
634
  }
629
- function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '') {
635
+ function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '', copy = {}) {
630
636
  process.stdout.write('\x1b[2J\x1b[H');
631
- printConnectorIntro();
632
- process.stdout.write(`${ANSI.bold}Select connectors to set up or overwrite now${ANSI.reset}\n`);
633
- writeWrapped('Use Up/Down to move, Space to toggle optional connectors, A to toggle all optional connectors, Enter to continue.', '', ANSI.dim);
637
+ printConnectorIntro(copy);
638
+ process.stdout.write(`${ANSI.bold}${copy.actionTitle || 'Select connectors to set up or overwrite now'}${ANSI.reset}\n`);
639
+ writeWrapped(copy.helpText || 'Use Up/Down to move, Space to toggle optional connectors, A to toggle all optional connectors, Enter to continue.', '', ANSI.dim);
634
640
  process.stdout.write('\n');
635
641
  let index = 0;
636
642
  for (const group of connectorPickerGroups(healthByConnector)) {
@@ -656,16 +662,18 @@ function renderConnectorPicker(cursorIndex, selected, required, healthByConnecto
656
662
  }
657
663
  process.stdout.write(`${ANSI.dim}Esc/Q cancels. Number keys 1-${CONNECTOR_DEFINITIONS.length} also toggle connectors.${ANSI.reset}\n`);
658
664
  }
659
- async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelected = []) {
665
+ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelected = [], copy = {}) {
660
666
  emitKeypressEvents(process.stdin);
661
667
  const wasRaw = process.stdin.isRaw;
662
668
  const wasPaused = process.stdin.isPaused();
663
669
  process.stdin.setRawMode(true);
664
670
  process.stdin.resume();
665
671
  let cursorIndex = 0;
666
- const required = getRequiredConnectorKeys();
672
+ const required = copy.mode === 'input' ? new Set() : getRequiredConnectorKeys();
667
673
  const initial = new Set(initialSelected);
668
- const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) || initial.has(key) || !isConnectorLocallyConfigured(key)));
674
+ const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) ||
675
+ initial.has(key) ||
676
+ (copy.mode !== 'input' && !isConnectorLocallyConfigured(key))));
669
677
  let warning = '';
670
678
  return await new Promise((resolve, reject) => {
671
679
  const displayItems = () => connectorPickerDisplayItems(healthByConnector);
@@ -683,7 +691,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
683
691
  required.forEach((key) => selected.add(key));
684
692
  if (selected.size === 0) {
685
693
  warning = 'No connectors selected. Select a connector to update or press Esc to cancel.';
686
- renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
694
+ renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
687
695
  return;
688
696
  }
689
697
  cleanup();
@@ -768,11 +776,11 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
768
776
  }
769
777
  }
770
778
  }
771
- renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
779
+ renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
772
780
  };
773
781
  process.stdin.on('keypress', onKeypress);
774
782
  process.stdout.write(ANSI.hideCursor);
775
- renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
783
+ renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
776
784
  });
777
785
  }
778
786
  async function commandExists(commandName) {
@@ -2593,6 +2601,144 @@ async function maybeSelfUpdateFromClawHub(args) {
2593
2601
  const code = await rerunCurrentWizardWithoutSelfUpdate();
2594
2602
  process.exit(code ?? 0);
2595
2603
  }
2604
+ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, allowIsolationPrompt = true, }) {
2605
+ clearTerminal();
2606
+ printConnectorIntro();
2607
+ process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
2608
+ for (const key of selected) {
2609
+ process.stdout.write(` - ${connectorLabel(key)}\n`);
2610
+ }
2611
+ process.stdout.write('\n');
2612
+ const secrets = {};
2613
+ let sentryAccounts = [];
2614
+ if (selected.includes('analytics')) {
2615
+ let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
2616
+ while (true) {
2617
+ clearTerminal();
2618
+ await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
2619
+ const check = await runImmediateConnectorHealthCheck({
2620
+ rl,
2621
+ configPath: args.config,
2622
+ connector: 'analytics',
2623
+ secrets,
2624
+ });
2625
+ if (!check.retry)
2626
+ break;
2627
+ forceFreshAnalyticsToken = true;
2628
+ }
2629
+ }
2630
+ if (selected.includes('github')) {
2631
+ while (true) {
2632
+ clearTerminal();
2633
+ await guideGitHubConnector(rl, secrets);
2634
+ const check = await runImmediateConnectorHealthCheck({
2635
+ rl,
2636
+ configPath: args.config,
2637
+ connector: 'github',
2638
+ secrets,
2639
+ });
2640
+ if (!check.retry)
2641
+ break;
2642
+ }
2643
+ }
2644
+ if (selected.includes('revenuecat')) {
2645
+ while (true) {
2646
+ clearTerminal();
2647
+ await guideRevenueCatConnector(rl, secrets);
2648
+ const check = await runImmediateConnectorHealthCheck({
2649
+ rl,
2650
+ configPath: args.config,
2651
+ connector: 'revenuecat',
2652
+ secrets,
2653
+ });
2654
+ if (!check.retry)
2655
+ break;
2656
+ }
2657
+ }
2658
+ if (selected.includes('sentry')) {
2659
+ while (true) {
2660
+ clearTerminal();
2661
+ sentryAccounts = await guideSentryConnector(rl, secrets);
2662
+ const check = await runImmediateConnectorHealthCheck({
2663
+ rl,
2664
+ configPath: args.config,
2665
+ connector: 'sentry',
2666
+ secrets,
2667
+ sentryAccounts,
2668
+ });
2669
+ if (!check.retry)
2670
+ break;
2671
+ }
2672
+ }
2673
+ if (selected.includes('asc')) {
2674
+ while (true) {
2675
+ clearTerminal();
2676
+ await guideAscConnector(rl, secrets);
2677
+ const check = await runImmediateConnectorHealthCheck({
2678
+ rl,
2679
+ configPath: args.config,
2680
+ connector: 'asc',
2681
+ secrets,
2682
+ });
2683
+ if (!check.retry)
2684
+ break;
2685
+ }
2686
+ }
2687
+ const secretsFile = resolveSecretsFile();
2688
+ const wroteSecrets = Object.keys(secrets).length > 0;
2689
+ clearTerminal();
2690
+ if (wroteSecrets) {
2691
+ await writeSecretsFile(secretsFile, secrets);
2692
+ process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
2693
+ }
2694
+ else {
2695
+ process.stdout.write('\nNo new secrets were written.\n');
2696
+ }
2697
+ if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2698
+ process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
2699
+ }
2700
+ const env = {
2701
+ ...process.env,
2702
+ ...secrets,
2703
+ };
2704
+ const command = `node scripts/openclaw-growth-start.mjs --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
2705
+ let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
2706
+ let setupPayload = parseJsonFromStdout(setupResult.stdout);
2707
+ if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2708
+ process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
2709
+ }
2710
+ if (selected.includes('asc')) {
2711
+ try {
2712
+ const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
2713
+ if (ascWebAuthChanged) {
2714
+ setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
2715
+ setupPayload = parseJsonFromStdout(setupResult.stdout);
2716
+ }
2717
+ }
2718
+ catch (error) {
2719
+ process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
2720
+ }
2721
+ }
2722
+ if (setupResult.ok && setupPayload?.ok !== false) {
2723
+ printSetupSuccess(setupPayload);
2724
+ if (wroteSecrets) {
2725
+ process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
2726
+ }
2727
+ const configureIsolation = allowIsolationPrompt && ENABLE_ISOLATED_SECRET_RUNNER_WIZARD && await askYesNo(rl, 'Generate an isolated secret runner so OpenClaw can run health checks without reading API keys?', true);
2728
+ if (configureIsolation) {
2729
+ const config = await loadEditableConfig(args.config);
2730
+ const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
2731
+ await writeJsonFile(path.resolve(args.config), config);
2732
+ const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
2733
+ process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
2734
+ printSecretRunnerKitInstructions(secretAccess.kit);
2735
+ }
2736
+ return true;
2737
+ }
2738
+ printSetupFailure({ result: setupResult, payload: setupPayload, command });
2739
+ process.exitCode = 1;
2740
+ return false;
2741
+ }
2596
2742
  async function runConnectorSetupWizard(args) {
2597
2743
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
2598
2744
  throw new Error('Connector wizard requires an interactive terminal.');
@@ -2611,141 +2757,7 @@ async function runConnectorSetupWizard(args) {
2611
2757
  if (selected.length === 0) {
2612
2758
  throw new Error('No supported connectors selected. Use analytics, github, revenuecat, sentry, asc, or all.');
2613
2759
  }
2614
- clearTerminal();
2615
- printConnectorIntro();
2616
- process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
2617
- for (const key of selected) {
2618
- process.stdout.write(` - ${connectorLabel(key)}\n`);
2619
- }
2620
- process.stdout.write('\n');
2621
- const secrets = {};
2622
- let sentryAccounts = [];
2623
- if (selected.includes('analytics')) {
2624
- let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
2625
- while (true) {
2626
- clearTerminal();
2627
- await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
2628
- const check = await runImmediateConnectorHealthCheck({
2629
- rl,
2630
- configPath: args.config,
2631
- connector: 'analytics',
2632
- secrets,
2633
- });
2634
- if (!check.retry)
2635
- break;
2636
- forceFreshAnalyticsToken = true;
2637
- }
2638
- }
2639
- if (selected.includes('github')) {
2640
- while (true) {
2641
- clearTerminal();
2642
- await guideGitHubConnector(rl, secrets);
2643
- const check = await runImmediateConnectorHealthCheck({
2644
- rl,
2645
- configPath: args.config,
2646
- connector: 'github',
2647
- secrets,
2648
- });
2649
- if (!check.retry)
2650
- break;
2651
- }
2652
- }
2653
- if (selected.includes('revenuecat')) {
2654
- while (true) {
2655
- clearTerminal();
2656
- await guideRevenueCatConnector(rl, secrets);
2657
- const check = await runImmediateConnectorHealthCheck({
2658
- rl,
2659
- configPath: args.config,
2660
- connector: 'revenuecat',
2661
- secrets,
2662
- });
2663
- if (!check.retry)
2664
- break;
2665
- }
2666
- }
2667
- if (selected.includes('sentry')) {
2668
- while (true) {
2669
- clearTerminal();
2670
- sentryAccounts = await guideSentryConnector(rl, secrets);
2671
- const check = await runImmediateConnectorHealthCheck({
2672
- rl,
2673
- configPath: args.config,
2674
- connector: 'sentry',
2675
- secrets,
2676
- sentryAccounts,
2677
- });
2678
- if (!check.retry)
2679
- break;
2680
- }
2681
- }
2682
- if (selected.includes('asc')) {
2683
- while (true) {
2684
- clearTerminal();
2685
- await guideAscConnector(rl, secrets);
2686
- const check = await runImmediateConnectorHealthCheck({
2687
- rl,
2688
- configPath: args.config,
2689
- connector: 'asc',
2690
- secrets,
2691
- });
2692
- if (!check.retry)
2693
- break;
2694
- }
2695
- }
2696
- const secretsFile = resolveSecretsFile();
2697
- const wroteSecrets = Object.keys(secrets).length > 0;
2698
- clearTerminal();
2699
- if (wroteSecrets) {
2700
- await writeSecretsFile(secretsFile, secrets);
2701
- process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
2702
- }
2703
- else {
2704
- process.stdout.write('\nNo new secrets were written.\n');
2705
- }
2706
- if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2707
- process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
2708
- }
2709
- const env = {
2710
- ...process.env,
2711
- ...secrets,
2712
- };
2713
- const command = `node scripts/openclaw-growth-start.mjs --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
2714
- let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
2715
- let setupPayload = parseJsonFromStdout(setupResult.stdout);
2716
- if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2717
- process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
2718
- }
2719
- if (selected.includes('asc')) {
2720
- try {
2721
- const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
2722
- if (ascWebAuthChanged) {
2723
- setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
2724
- setupPayload = parseJsonFromStdout(setupResult.stdout);
2725
- }
2726
- }
2727
- catch (error) {
2728
- process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
2729
- }
2730
- }
2731
- if (setupResult.ok && setupPayload?.ok !== false) {
2732
- printSetupSuccess(setupPayload);
2733
- if (wroteSecrets) {
2734
- process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
2735
- }
2736
- const configureIsolation = ENABLE_ISOLATED_SECRET_RUNNER_WIZARD && await askYesNo(rl, 'Generate an isolated secret runner so OpenClaw can run health checks without reading API keys?', true);
2737
- if (configureIsolation) {
2738
- const config = await loadEditableConfig(args.config);
2739
- const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
2740
- await writeJsonFile(path.resolve(args.config), config);
2741
- const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
2742
- process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
2743
- printSecretRunnerKitInstructions(secretAccess.kit);
2744
- }
2745
- return;
2746
- }
2747
- printSetupFailure({ result: setupResult, payload: setupPayload, command });
2748
- process.exitCode = 1;
2760
+ await runConnectorSetupSteps({ rl, args, selected, healthByConnector });
2749
2761
  }
2750
2762
  finally {
2751
2763
  rl.close();
@@ -2784,43 +2796,6 @@ async function askYesNo(rl, label, defaultYes = true) {
2784
2796
  }
2785
2797
  }
2786
2798
  }
2787
- async function askSourceConfig(rl, sourceName, defaultPath, hint, options = {}) {
2788
- const forceEnabled = Boolean(options.forceEnabled);
2789
- const defaultCommand = String(options.defaultCommand || getDefaultSourceCommand(sourceName) || '').trim();
2790
- const defaultMode = defaultCommand ? 'command' : 'file';
2791
- const defaultEnabled = options.defaultEnabled ?? sourceName === 'analytics';
2792
- const enabled = forceEnabled
2793
- ? true
2794
- : await askYesNo(rl, `Enable source "${sourceName}"?`, defaultEnabled);
2795
- if (!enabled) {
2796
- return {
2797
- enabled: false,
2798
- mode: 'file',
2799
- path: defaultPath,
2800
- hint,
2801
- };
2802
- }
2803
- process.stdout.write(`Where to get ${sourceName} data:\n${hint}\n`);
2804
- const modeInput = await ask(rl, 'Mode (file/command)', defaultMode);
2805
- const mode = modeInput.toLowerCase() === 'command' ? 'command' : 'file';
2806
- const value = await ask(rl, mode === 'file' ? `${sourceName} JSON file path` : `${sourceName} command`, mode === 'file' ? defaultPath : defaultCommand);
2807
- if (mode === 'file') {
2808
- return {
2809
- enabled: true,
2810
- mode,
2811
- path: value,
2812
- hint,
2813
- };
2814
- }
2815
- return {
2816
- enabled: true,
2817
- mode,
2818
- command: value,
2819
- hint,
2820
- ...(options.cursorMode ? { cursorMode: options.cursorMode } : {}),
2821
- ...(options.initialLookback ? { initialLookback: options.initialLookback } : {}),
2822
- };
2823
- }
2824
2799
  function printCadencePlan(cadences) {
2825
2800
  process.stdout.write('\nDefault growth cadence:\n');
2826
2801
  for (const cadence of cadences) {
@@ -2830,16 +2805,28 @@ function printCadencePlan(cadences) {
2830
2805
  process.stdout.write('\n');
2831
2806
  }
2832
2807
  async function askToolUsage(rl) {
2833
- process.stdout.write('\nHow should OpenClaw Growth Engineer use this tool?\n');
2834
- process.stdout.write(' 1) Production autopilot: notify, draft issues/PR handoffs, and analyze on schedule\n');
2835
- process.stdout.write(' 2) Advisory only: analyze and write OpenClaw chat summaries, no GitHub artifacts by default\n');
2836
- process.stdout.write(' 3) Manual reports: mostly one-off runs; keep scheduling conservative\n');
2837
- const answer = await ask(rl, 'Usage mode (1/2/3)', '1');
2838
- if (answer.trim() === '2')
2839
- return 'advisory';
2840
- if (answer.trim() === '3')
2841
- return 'manual_reports';
2842
- return 'production_autopilot';
2808
+ return await askMenuChoice(rl, {
2809
+ title: 'How should OpenClaw Growth Engineer run?',
2810
+ subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
2811
+ defaultValue: 'production_autopilot',
2812
+ options: [
2813
+ {
2814
+ value: 'production_autopilot',
2815
+ label: 'Production autopilot',
2816
+ detail: 'Notify, draft issues/PR handoffs, and analyze on schedule.',
2817
+ },
2818
+ {
2819
+ value: 'advisory',
2820
+ label: 'Advisory only',
2821
+ detail: 'Analyze and write OpenClaw chat summaries; no GitHub artifacts by default.',
2822
+ },
2823
+ {
2824
+ value: 'manual_reports',
2825
+ label: 'Manual reports',
2826
+ detail: 'Mostly one-off runs with conservative scheduling.',
2827
+ },
2828
+ ],
2829
+ });
2843
2830
  }
2844
2831
  async function askCadencePlan(rl) {
2845
2832
  const cadences = DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence }));
@@ -3050,6 +3037,73 @@ function buildRecommendedSourceConfig() {
3050
3037
  ],
3051
3038
  };
3052
3039
  }
3040
+ function getInputChannelInitialSelection(config) {
3041
+ const sources = config?.sources || {};
3042
+ const extraSources = Array.isArray(sources.extra) ? sources.extra : [];
3043
+ const selected = new Set();
3044
+ const hasExplicitSources = Boolean(config?.sources);
3045
+ if (!hasExplicitSources || sources.analytics?.enabled !== false)
3046
+ selected.add('analytics');
3047
+ if (sources.revenuecat?.enabled === true || isConnectorLocallyConfigured('revenuecat'))
3048
+ selected.add('revenuecat');
3049
+ if (!hasExplicitSources || sources.sentry?.enabled !== false)
3050
+ selected.add('sentry');
3051
+ if (extraSources.some((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()) &&
3052
+ source?.enabled !== false) ||
3053
+ isConnectorLocallyConfigured('asc')) {
3054
+ selected.add('asc');
3055
+ }
3056
+ if (config?.deliveries?.github?.enabled ||
3057
+ config?.actions?.autoCreateIssues ||
3058
+ config?.actions?.autoCreatePullRequests ||
3059
+ isConnectorLocallyConfigured('github')) {
3060
+ selected.add('github');
3061
+ }
3062
+ return orderConnectors([...selected]);
3063
+ }
3064
+ function buildSourceConfigFromInputChannels(selectedConnectors, existingSources = {}) {
3065
+ const selected = new Set(selectedConnectors);
3066
+ const recommended = buildRecommendedSourceConfig();
3067
+ const existingExtra = Array.isArray(existingSources.extra) ? existingSources.extra : [];
3068
+ const ascSource = existingExtra.find((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()));
3069
+ const nonAscExtra = existingExtra.filter((source) => source !== ascSource);
3070
+ return {
3071
+ ...recommended,
3072
+ ...existingSources,
3073
+ analytics: {
3074
+ ...recommended.analytics,
3075
+ ...(existingSources.analytics || {}),
3076
+ enabled: selected.has('analytics'),
3077
+ },
3078
+ revenuecat: {
3079
+ ...recommended.revenuecat,
3080
+ ...(existingSources.revenuecat || {}),
3081
+ enabled: selected.has('revenuecat'),
3082
+ },
3083
+ sentry: {
3084
+ ...recommended.sentry,
3085
+ ...(existingSources.sentry || {}),
3086
+ enabled: selected.has('sentry'),
3087
+ },
3088
+ feedback: {
3089
+ ...recommended.feedback,
3090
+ ...(existingSources.feedback || {}),
3091
+ enabled: selected.has('analytics'),
3092
+ },
3093
+ extra: [
3094
+ ...nonAscExtra,
3095
+ {
3096
+ ...buildExtraSourceConfig('asc-cli', {
3097
+ enabled: selected.has('asc'),
3098
+ mode: 'command',
3099
+ command: getDefaultSourceCommand('asc'),
3100
+ }),
3101
+ ...(ascSource || {}),
3102
+ enabled: selected.has('asc'),
3103
+ },
3104
+ ],
3105
+ };
3106
+ }
3053
3107
  async function loadEditableConfig(configPath) {
3054
3108
  const existing = await readJsonIfPresent(configPath).catch(() => null);
3055
3109
  if (existing && typeof existing === 'object')
@@ -3101,20 +3155,33 @@ async function askOutputConfig(rl, config) {
3101
3155
  'OpenClaw chat is always enabled so the agent has a readable handoff.',
3102
3156
  'GitHub issues or draft PRs are optional and only run when a token plus an inferred repo are available.',
3103
3157
  ]);
3104
- process.stdout.write(' 1) OpenClaw chat only, with GitHub left as runtime fallback\n');
3105
- process.stdout.write(' 2) Auto-create GitHub issues for concrete findings\n');
3106
- process.stdout.write(' 3) Auto-create draft PR proposals for implementation-ready fixes\n');
3107
3158
  const currentMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
3108
3159
  const currentAutoCreate = Boolean(config?.actions?.autoCreateIssues || config?.actions?.autoCreatePullRequests || config?.deliveries?.github?.autoCreate);
3109
- const defaultChoice = currentAutoCreate ? (currentMode === 'pull_request' ? '3' : '2') : '1';
3110
- const outputChoice = await ask(rl, 'Output type (1/2/3)', defaultChoice);
3111
- const summaryOnly = outputChoice.trim() === '1';
3112
- const mode = outputChoice.trim() === '3' ? 'pull_request' : 'issue';
3113
- const autoCreate = summaryOnly
3114
- ? false
3115
- : await askYesNo(rl, mode === 'pull_request'
3116
- ? 'Automatically create draft pull requests when new findings are found?'
3117
- : 'Automatically create GitHub issues when new findings are found?', currentAutoCreate);
3160
+ const outputChoice = await askMenuChoice(rl, {
3161
+ title: 'Output mode',
3162
+ subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
3163
+ defaultValue: currentAutoCreate ? (currentMode === 'pull_request' ? 'pull_request' : 'issue') : 'chat',
3164
+ options: [
3165
+ {
3166
+ value: 'chat',
3167
+ label: 'OpenClaw chat',
3168
+ detail: 'Write readable summaries and leave GitHub as runtime fallback.',
3169
+ },
3170
+ {
3171
+ value: 'issue',
3172
+ label: 'GitHub issues',
3173
+ detail: 'Auto-create issues for concrete findings when GitHub access allows it.',
3174
+ },
3175
+ {
3176
+ value: 'pull_request',
3177
+ label: 'Draft PR proposals',
3178
+ detail: 'Auto-create draft PR-oriented proposal branches for implementation-ready fixes.',
3179
+ },
3180
+ ],
3181
+ });
3182
+ const summaryOnly = outputChoice === 'chat';
3183
+ const mode = outputChoice === 'pull_request' ? 'pull_request' : 'issue';
3184
+ const autoCreate = !summaryOnly;
3118
3185
  if (!summaryOnly) {
3119
3186
  process.stdout.write('GitHub repo scope is not pinned by the wizard; OpenClaw/Hermes will infer it from OPENCLAW_GITHUB_REPO, the local git remote, or runtime context when creating issues/PRs.\n');
3120
3187
  }
@@ -3262,76 +3329,17 @@ async function askOutputsAndIntervalsConfig(rl, config) {
3262
3329
  const withOutput = await askOutputConfig(rl, withIntervals);
3263
3330
  return await askGitHubArtifactDetails(rl, withOutput);
3264
3331
  }
3265
- async function askInputSourceConfig(rl, config) {
3266
- printSection('Input channels', [
3267
- 'These are the data streams Growth Engineer will read during scheduled runs.',
3268
- 'Connector credentials are configured through the connector setup; this section only chooses which inputs are enabled and how the runner fetches them.',
3269
- ]);
3270
- process.stdout.write('Recommended defaults: AnalyticsCLI product analytics, Sentry-compatible production stability, and feedback are enabled. RevenueCat and App Store Connect are ready to enable once their connectors are configured.\n\n');
3271
- const useRecommended = await askYesNo(rl, 'Use recommended input channels and default fetch commands?', true);
3272
- if (useRecommended) {
3273
- config.sources = {
3274
- ...buildRecommendedSourceConfig(),
3275
- ...(config.sources || {}),
3276
- analytics: {
3277
- ...buildRecommendedSourceConfig().analytics,
3278
- ...(config.sources?.analytics || {}),
3279
- enabled: config.sources?.analytics?.enabled !== false,
3280
- },
3281
- sentry: {
3282
- ...buildRecommendedSourceConfig().sentry,
3283
- ...(config.sources?.sentry || {}),
3284
- enabled: config.sources?.sentry?.enabled !== false,
3285
- },
3286
- feedback: {
3287
- ...buildRecommendedSourceConfig().feedback,
3288
- ...(config.sources?.feedback || {}),
3289
- enabled: config.sources?.feedback?.enabled !== false,
3290
- },
3291
- revenuecat: {
3292
- ...buildRecommendedSourceConfig().revenuecat,
3293
- ...(config.sources?.revenuecat || {}),
3294
- },
3295
- extra: Array.isArray(config.sources?.extra)
3296
- ? config.sources.extra
3297
- : buildRecommendedSourceConfig().extra,
3298
- };
3299
- return config;
3300
- }
3301
- process.stdout.write('\nAdvanced input setup\n');
3302
- process.stdout.write('Only change these when the default CLI exporters do not match this host.\n');
3303
- const analytics = await askSourceConfig(rl, 'analytics', 'data/openclaw-growth-engineer/analytics_summary.example.json', getDefaultSourceHint('analytics'), {
3304
- forceEnabled: true,
3305
- defaultCommand: getDefaultSourceCommand('analytics'),
3306
- });
3307
- const revenuecat = await askSourceConfig(rl, 'revenuecat', 'data/openclaw-growth-engineer/revenuecat_summary.example.json', getDefaultSourceHint('revenuecat'));
3308
- const sentry = await askSourceConfig(rl, 'sentry', 'data/openclaw-growth-engineer/sentry_summary.example.json', getDefaultSourceHint('sentry'), {
3309
- defaultEnabled: true,
3310
- defaultCommand: getDefaultSourceCommand('sentry'),
3311
- });
3312
- const feedback = await askSourceConfig(rl, 'feedback', 'data/openclaw-growth-engineer/feedback_summary.example.json', getDefaultSourceHint('feedback'), {
3313
- defaultEnabled: true,
3314
- defaultCommand: getDefaultSourceCommand('feedback'),
3315
- cursorMode: 'auto_since_last_fetch',
3316
- initialLookback: '30d',
3332
+ async function askInputSourceConfig(rl, config, configPath) {
3333
+ const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(configPath, onProgress));
3334
+ const selected = await askConnectorSelectionWithHealth(rl, healthByConnector, getInputChannelInitialSelection(config), {
3335
+ introTitle: 'Input channels',
3336
+ introDetail: null,
3337
+ actionTitle: 'Select input channels',
3338
+ helpText: 'Use Up/Down to move, Space to toggle channels, A to toggle all channels, Enter to continue.',
3339
+ mode: 'input',
3317
3340
  });
3318
- const extraSourcesRaw = await ask(rl, 'Extra input connectors to define now', '');
3319
- const extraSources = extraSourcesRaw
3320
- .split(',')
3321
- .map((value) => value.trim())
3322
- .filter(Boolean)
3323
- .map((service) => {
3324
- const defaultCommand = getDefaultSourceCommand(service);
3325
- return buildExtraSourceConfig(service, defaultCommand ? {} : { mode: 'file', path: getDefaultSourcePath(service) });
3326
- });
3327
- config.sources = {
3328
- analytics,
3329
- revenuecat,
3330
- sentry,
3331
- feedback,
3332
- extra: extraSources,
3333
- };
3334
- return config;
3341
+ config.sources = buildSourceConfigFromInputChannels(selected, config.sources || {});
3342
+ return { config, selected, healthByConnector };
3335
3343
  }
3336
3344
  async function writeOpenClawJobManifest(configPath, config) {
3337
3345
  const manifestPath = path.resolve('.openclaw/jobs/openclaw-growth-engineer.json');
@@ -3424,7 +3432,23 @@ async function main() {
3424
3432
  let config = await loadEditableConfig(configPath);
3425
3433
  config.version = Number(config.version || 7);
3426
3434
  config.generatedAt = new Date().toISOString();
3427
- config = await askInputSourceConfig(rl, config);
3435
+ const inputSetup = await askInputSourceConfig(rl, config, configPath);
3436
+ config = inputSetup.config;
3437
+ await ensureDirForFile(configPath);
3438
+ await writeJsonFile(configPath, config);
3439
+ const connectorsOk = await runConnectorSetupSteps({
3440
+ rl,
3441
+ args: { ...args, config: configPath },
3442
+ selected: inputSetup.selected,
3443
+ healthByConnector: inputSetup.healthByConnector,
3444
+ allowIsolationPrompt: false,
3445
+ });
3446
+ if (!connectorsOk) {
3447
+ return;
3448
+ }
3449
+ config = await loadEditableConfig(configPath);
3450
+ config.version = Number(config.version || 7);
3451
+ config.generatedAt = new Date().toISOString();
3428
3452
  config = await askIntervalConfig(rl, config);
3429
3453
  config = await askOutputConfig(rl, config);
3430
3454
  config = await askGitHubArtifactDetails(rl, config);