@analyticscli/growth-engineer 0.1.0-preview.7 → 0.1.0-preview.9

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.
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { promises as fs } from 'node:fs';
2
+ import { existsSync, promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
5
  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 { fileURLToPath } from 'node:url';
9
10
  import { buildExtraSourceConfig, getDefaultSourceCommand, } from './openclaw-growth-shared.mjs';
10
11
  import { loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
11
12
  const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
@@ -13,6 +14,8 @@ const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
13
14
  const ENABLE_ISOLATED_SECRET_RUNNER_WIZARD = false;
14
15
  const DEFAULT_GROWTH_INTERVAL_MINUTES = 1440;
15
16
  const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
17
+ const GROWTH_ENGINEER_PACKAGE_SPEC = process.env.OPENCLAW_GROWTH_ENGINEER_PACKAGE || '@analyticscli/growth-engineer@preview';
18
+ const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
16
19
  const CONNECTOR_KEYS = ['analytics', 'github', 'revenuecat', 'sentry', 'asc'];
17
20
  class WizardAbortError extends Error {
18
21
  exitCode;
@@ -220,6 +223,39 @@ function quote(value) {
220
223
  }
221
224
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
222
225
  }
226
+ function resolveRuntimeScriptPath(scriptName) {
227
+ const candidates = [
228
+ path.join(RUNTIME_DIR, scriptName),
229
+ path.resolve('scripts', scriptName),
230
+ path.resolve('skills/openclaw-growth-engineer/scripts', scriptName),
231
+ ];
232
+ return candidates.find((candidate) => existsSync(candidate)) || path.join(RUNTIME_DIR, scriptName);
233
+ }
234
+ function nodeRuntimeScriptCommand(scriptName) {
235
+ return `node ${quote(resolveRuntimeScriptPath(scriptName))}`;
236
+ }
237
+ function growthEngineerPackageCommand(args) {
238
+ return `npx -y ${quote(GROWTH_ENGINEER_PACKAGE_SPEC)} ${args}`;
239
+ }
240
+ function getWizardDefaultSourceCommand(sourceName) {
241
+ const normalized = String(sourceName || '').trim().toLowerCase();
242
+ if (normalized === 'analytics' || normalized === 'analyticscli') {
243
+ return nodeRuntimeScriptCommand('export-analytics-summary.mjs');
244
+ }
245
+ if (normalized === 'revenuecat' || normalized === 'revenue-cat') {
246
+ return nodeRuntimeScriptCommand('export-revenuecat-summary.mjs');
247
+ }
248
+ if (normalized === 'sentry' || normalized === 'glitchtip') {
249
+ return nodeRuntimeScriptCommand('export-sentry-summary.mjs');
250
+ }
251
+ if (normalized === 'feedback') {
252
+ return getDefaultSourceCommand('feedback');
253
+ }
254
+ if (['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(normalized)) {
255
+ return nodeRuntimeScriptCommand('export-asc-summary.mjs');
256
+ }
257
+ return getDefaultSourceCommand(sourceName);
258
+ }
223
259
  function normalizeConnectorKey(value) {
224
260
  const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
225
261
  if (!normalized)
@@ -620,7 +656,7 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
620
656
  },
621
657
  ]));
622
658
  }
623
- const result = await runCommandCaptureWithProgress(`node scripts/openclaw-growth-status.mjs --config ${quote(configPath)} --json --progress-json`, onProgress);
659
+ const result = await runCommandCaptureWithProgress(`${nodeRuntimeScriptCommand('openclaw-growth-status.mjs')} --config ${quote(configPath)} --json --progress-json`, onProgress);
624
660
  const payload = parseJsonFromStdout(result.stdout);
625
661
  const connectors = payload?.connectors && typeof payload.connectors === 'object' ? payload.connectors : {};
626
662
  const healthByConnector = {
@@ -1366,7 +1402,7 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
1366
1402
  ...process.env,
1367
1403
  ...secrets,
1368
1404
  };
1369
- const command = `node scripts/openclaw-growth-start.mjs --config ${quote(configPath)} --setup-only --connectors ${quote(connector)} --only-connectors ${quote(connector)}`;
1405
+ const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(configPath)} --setup-only --connectors ${quote(connector)} --only-connectors ${quote(connector)}`;
1370
1406
  let result = await runSetupCommandWithProgress(command, env, [connector], `Checking ${connectorLabel(connector)} immediately after setup...`);
1371
1407
  let payload = parseJsonFromStdout(result.stdout);
1372
1408
  if (connector === 'asc') {
@@ -1748,13 +1784,13 @@ function getGrowthRunCommand(config, displayConfigPath) {
1748
1784
  if (config?.security?.connectorSecrets?.mode === 'isolated-runner' && config.security.connectorSecrets.runCommand) {
1749
1785
  return config.security.connectorSecrets.runCommand;
1750
1786
  }
1751
- return `node scripts/openclaw-growth-runner.mjs --config ${displayConfigPath}`;
1787
+ return growthEngineerPackageCommand(`run --config ${quote(displayConfigPath)}`);
1752
1788
  }
1753
1789
  function getConnectorHealthCommand(config, displayConfigPath) {
1754
1790
  if (config?.security?.connectorSecrets?.mode === 'isolated-runner' && config.security.connectorSecrets.healthCommand) {
1755
1791
  return config.security.connectorSecrets.healthCommand;
1756
1792
  }
1757
- return `node scripts/openclaw-growth-runner.mjs --config ${displayConfigPath}`;
1793
+ return growthEngineerPackageCommand(`run --config ${quote(displayConfigPath)}`);
1758
1794
  }
1759
1795
  async function maybePromptSecret(rl, label, envName) {
1760
1796
  const existing = process.env[envName]?.trim();
@@ -2601,6 +2637,144 @@ async function maybeSelfUpdateFromClawHub(args) {
2601
2637
  const code = await rerunCurrentWizardWithoutSelfUpdate();
2602
2638
  process.exit(code ?? 0);
2603
2639
  }
2640
+ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, allowIsolationPrompt = true, }) {
2641
+ clearTerminal();
2642
+ printConnectorIntro();
2643
+ process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
2644
+ for (const key of selected) {
2645
+ process.stdout.write(` - ${connectorLabel(key)}\n`);
2646
+ }
2647
+ process.stdout.write('\n');
2648
+ const secrets = {};
2649
+ let sentryAccounts = [];
2650
+ if (selected.includes('analytics')) {
2651
+ let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
2652
+ while (true) {
2653
+ clearTerminal();
2654
+ await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
2655
+ const check = await runImmediateConnectorHealthCheck({
2656
+ rl,
2657
+ configPath: args.config,
2658
+ connector: 'analytics',
2659
+ secrets,
2660
+ });
2661
+ if (!check.retry)
2662
+ break;
2663
+ forceFreshAnalyticsToken = true;
2664
+ }
2665
+ }
2666
+ if (selected.includes('github')) {
2667
+ while (true) {
2668
+ clearTerminal();
2669
+ await guideGitHubConnector(rl, secrets);
2670
+ const check = await runImmediateConnectorHealthCheck({
2671
+ rl,
2672
+ configPath: args.config,
2673
+ connector: 'github',
2674
+ secrets,
2675
+ });
2676
+ if (!check.retry)
2677
+ break;
2678
+ }
2679
+ }
2680
+ if (selected.includes('revenuecat')) {
2681
+ while (true) {
2682
+ clearTerminal();
2683
+ await guideRevenueCatConnector(rl, secrets);
2684
+ const check = await runImmediateConnectorHealthCheck({
2685
+ rl,
2686
+ configPath: args.config,
2687
+ connector: 'revenuecat',
2688
+ secrets,
2689
+ });
2690
+ if (!check.retry)
2691
+ break;
2692
+ }
2693
+ }
2694
+ if (selected.includes('sentry')) {
2695
+ while (true) {
2696
+ clearTerminal();
2697
+ sentryAccounts = await guideSentryConnector(rl, secrets);
2698
+ const check = await runImmediateConnectorHealthCheck({
2699
+ rl,
2700
+ configPath: args.config,
2701
+ connector: 'sentry',
2702
+ secrets,
2703
+ sentryAccounts,
2704
+ });
2705
+ if (!check.retry)
2706
+ break;
2707
+ }
2708
+ }
2709
+ if (selected.includes('asc')) {
2710
+ while (true) {
2711
+ clearTerminal();
2712
+ await guideAscConnector(rl, secrets);
2713
+ const check = await runImmediateConnectorHealthCheck({
2714
+ rl,
2715
+ configPath: args.config,
2716
+ connector: 'asc',
2717
+ secrets,
2718
+ });
2719
+ if (!check.retry)
2720
+ break;
2721
+ }
2722
+ }
2723
+ const secretsFile = resolveSecretsFile();
2724
+ const wroteSecrets = Object.keys(secrets).length > 0;
2725
+ clearTerminal();
2726
+ if (wroteSecrets) {
2727
+ await writeSecretsFile(secretsFile, secrets);
2728
+ process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
2729
+ }
2730
+ else {
2731
+ process.stdout.write('\nNo new secrets were written.\n');
2732
+ }
2733
+ if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2734
+ process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
2735
+ }
2736
+ const env = {
2737
+ ...process.env,
2738
+ ...secrets,
2739
+ };
2740
+ const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
2741
+ let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
2742
+ let setupPayload = parseJsonFromStdout(setupResult.stdout);
2743
+ if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2744
+ process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
2745
+ }
2746
+ if (selected.includes('asc')) {
2747
+ try {
2748
+ const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
2749
+ if (ascWebAuthChanged) {
2750
+ setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
2751
+ setupPayload = parseJsonFromStdout(setupResult.stdout);
2752
+ }
2753
+ }
2754
+ catch (error) {
2755
+ process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
2756
+ }
2757
+ }
2758
+ if (setupResult.ok && setupPayload?.ok !== false) {
2759
+ printSetupSuccess(setupPayload);
2760
+ if (wroteSecrets) {
2761
+ process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
2762
+ }
2763
+ 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);
2764
+ if (configureIsolation) {
2765
+ const config = await loadEditableConfig(args.config);
2766
+ const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
2767
+ await writeJsonFile(path.resolve(args.config), config);
2768
+ const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
2769
+ process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
2770
+ printSecretRunnerKitInstructions(secretAccess.kit);
2771
+ }
2772
+ return true;
2773
+ }
2774
+ printSetupFailure({ result: setupResult, payload: setupPayload, command });
2775
+ process.exitCode = 1;
2776
+ return false;
2777
+ }
2604
2778
  async function runConnectorSetupWizard(args) {
2605
2779
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
2606
2780
  throw new Error('Connector wizard requires an interactive terminal.');
@@ -2619,141 +2793,7 @@ async function runConnectorSetupWizard(args) {
2619
2793
  if (selected.length === 0) {
2620
2794
  throw new Error('No supported connectors selected. Use analytics, github, revenuecat, sentry, asc, or all.');
2621
2795
  }
2622
- clearTerminal();
2623
- printConnectorIntro();
2624
- process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
2625
- for (const key of selected) {
2626
- process.stdout.write(` - ${connectorLabel(key)}\n`);
2627
- }
2628
- process.stdout.write('\n');
2629
- const secrets = {};
2630
- let sentryAccounts = [];
2631
- if (selected.includes('analytics')) {
2632
- let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
2633
- while (true) {
2634
- clearTerminal();
2635
- await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
2636
- const check = await runImmediateConnectorHealthCheck({
2637
- rl,
2638
- configPath: args.config,
2639
- connector: 'analytics',
2640
- secrets,
2641
- });
2642
- if (!check.retry)
2643
- break;
2644
- forceFreshAnalyticsToken = true;
2645
- }
2646
- }
2647
- if (selected.includes('github')) {
2648
- while (true) {
2649
- clearTerminal();
2650
- await guideGitHubConnector(rl, secrets);
2651
- const check = await runImmediateConnectorHealthCheck({
2652
- rl,
2653
- configPath: args.config,
2654
- connector: 'github',
2655
- secrets,
2656
- });
2657
- if (!check.retry)
2658
- break;
2659
- }
2660
- }
2661
- if (selected.includes('revenuecat')) {
2662
- while (true) {
2663
- clearTerminal();
2664
- await guideRevenueCatConnector(rl, secrets);
2665
- const check = await runImmediateConnectorHealthCheck({
2666
- rl,
2667
- configPath: args.config,
2668
- connector: 'revenuecat',
2669
- secrets,
2670
- });
2671
- if (!check.retry)
2672
- break;
2673
- }
2674
- }
2675
- if (selected.includes('sentry')) {
2676
- while (true) {
2677
- clearTerminal();
2678
- sentryAccounts = await guideSentryConnector(rl, secrets);
2679
- const check = await runImmediateConnectorHealthCheck({
2680
- rl,
2681
- configPath: args.config,
2682
- connector: 'sentry',
2683
- secrets,
2684
- sentryAccounts,
2685
- });
2686
- if (!check.retry)
2687
- break;
2688
- }
2689
- }
2690
- if (selected.includes('asc')) {
2691
- while (true) {
2692
- clearTerminal();
2693
- await guideAscConnector(rl, secrets);
2694
- const check = await runImmediateConnectorHealthCheck({
2695
- rl,
2696
- configPath: args.config,
2697
- connector: 'asc',
2698
- secrets,
2699
- });
2700
- if (!check.retry)
2701
- break;
2702
- }
2703
- }
2704
- const secretsFile = resolveSecretsFile();
2705
- const wroteSecrets = Object.keys(secrets).length > 0;
2706
- clearTerminal();
2707
- if (wroteSecrets) {
2708
- await writeSecretsFile(secretsFile, secrets);
2709
- process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
2710
- }
2711
- else {
2712
- process.stdout.write('\nNo new secrets were written.\n');
2713
- }
2714
- if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2715
- process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
2716
- }
2717
- const env = {
2718
- ...process.env,
2719
- ...secrets,
2720
- };
2721
- const command = `node scripts/openclaw-growth-start.mjs --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
2722
- let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
2723
- let setupPayload = parseJsonFromStdout(setupResult.stdout);
2724
- if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2725
- process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
2726
- }
2727
- if (selected.includes('asc')) {
2728
- try {
2729
- const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
2730
- if (ascWebAuthChanged) {
2731
- setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
2732
- setupPayload = parseJsonFromStdout(setupResult.stdout);
2733
- }
2734
- }
2735
- catch (error) {
2736
- process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
2737
- }
2738
- }
2739
- if (setupResult.ok && setupPayload?.ok !== false) {
2740
- printSetupSuccess(setupPayload);
2741
- if (wroteSecrets) {
2742
- process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
2743
- }
2744
- 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);
2745
- if (configureIsolation) {
2746
- const config = await loadEditableConfig(args.config);
2747
- const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
2748
- await writeJsonFile(path.resolve(args.config), config);
2749
- const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
2750
- process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
2751
- printSecretRunnerKitInstructions(secretAccess.kit);
2752
- }
2753
- return;
2754
- }
2755
- printSetupFailure({ result: setupResult, payload: setupPayload, command });
2756
- process.exitCode = 1;
2796
+ await runConnectorSetupSteps({ rl, args, selected, healthByConnector });
2757
2797
  }
2758
2798
  finally {
2759
2799
  rl.close();
@@ -2801,16 +2841,28 @@ function printCadencePlan(cadences) {
2801
2841
  process.stdout.write('\n');
2802
2842
  }
2803
2843
  async function askToolUsage(rl) {
2804
- process.stdout.write('\nHow should OpenClaw Growth Engineer use this tool?\n');
2805
- process.stdout.write(' 1) Production autopilot: notify, draft issues/PR handoffs, and analyze on schedule\n');
2806
- process.stdout.write(' 2) Advisory only: analyze and write OpenClaw chat summaries, no GitHub artifacts by default\n');
2807
- process.stdout.write(' 3) Manual reports: mostly one-off runs; keep scheduling conservative\n');
2808
- const answer = await ask(rl, 'Usage mode (1/2/3)', '1');
2809
- if (answer.trim() === '2')
2810
- return 'advisory';
2811
- if (answer.trim() === '3')
2812
- return 'manual_reports';
2813
- return 'production_autopilot';
2844
+ return await askMenuChoice(rl, {
2845
+ title: 'How should OpenClaw Growth Engineer run?',
2846
+ subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
2847
+ defaultValue: 'production_autopilot',
2848
+ options: [
2849
+ {
2850
+ value: 'production_autopilot',
2851
+ label: 'Production autopilot',
2852
+ detail: 'Notify, draft issues/PR handoffs, and analyze on schedule.',
2853
+ },
2854
+ {
2855
+ value: 'advisory',
2856
+ label: 'Advisory only',
2857
+ detail: 'Analyze and write OpenClaw chat summaries; no GitHub artifacts by default.',
2858
+ },
2859
+ {
2860
+ value: 'manual_reports',
2861
+ label: 'Manual reports',
2862
+ detail: 'Mostly one-off runs with conservative scheduling.',
2863
+ },
2864
+ ],
2865
+ });
2814
2866
  }
2815
2867
  async function askCadencePlan(rl) {
2816
2868
  const cadences = DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence }));
@@ -2884,17 +2936,17 @@ async function buildDefaultWizardConfig() {
2884
2936
  analytics: {
2885
2937
  enabled: true,
2886
2938
  mode: 'command',
2887
- command: getDefaultSourceCommand('analytics'),
2939
+ command: getWizardDefaultSourceCommand('analytics'),
2888
2940
  },
2889
2941
  revenuecat: {
2890
2942
  enabled: false,
2891
2943
  mode: 'command',
2892
- command: getDefaultSourceCommand('revenuecat'),
2944
+ command: getWizardDefaultSourceCommand('revenuecat'),
2893
2945
  },
2894
2946
  sentry: {
2895
2947
  enabled: true,
2896
2948
  mode: 'command',
2897
- command: getDefaultSourceCommand('sentry'),
2949
+ command: getWizardDefaultSourceCommand('sentry'),
2898
2950
  },
2899
2951
  feedback: {
2900
2952
  enabled: true,
@@ -2904,7 +2956,7 @@ async function buildDefaultWizardConfig() {
2904
2956
  initialLookback: '30d',
2905
2957
  },
2906
2958
  extra: [
2907
- buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getDefaultSourceCommand('asc') }),
2959
+ buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
2908
2960
  ],
2909
2961
  },
2910
2962
  schedule: {
@@ -2997,17 +3049,17 @@ function buildRecommendedSourceConfig() {
2997
3049
  analytics: {
2998
3050
  enabled: true,
2999
3051
  mode: 'command',
3000
- command: getDefaultSourceCommand('analytics'),
3052
+ command: getWizardDefaultSourceCommand('analytics'),
3001
3053
  },
3002
3054
  revenuecat: {
3003
3055
  enabled: false,
3004
3056
  mode: 'command',
3005
- command: getDefaultSourceCommand('revenuecat'),
3057
+ command: getWizardDefaultSourceCommand('revenuecat'),
3006
3058
  },
3007
3059
  sentry: {
3008
3060
  enabled: true,
3009
3061
  mode: 'command',
3010
- command: getDefaultSourceCommand('sentry'),
3062
+ command: getWizardDefaultSourceCommand('sentry'),
3011
3063
  },
3012
3064
  feedback: {
3013
3065
  enabled: true,
@@ -3017,7 +3069,7 @@ function buildRecommendedSourceConfig() {
3017
3069
  initialLookback: '30d',
3018
3070
  },
3019
3071
  extra: [
3020
- buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getDefaultSourceCommand('asc') }),
3072
+ buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
3021
3073
  ],
3022
3074
  };
3023
3075
  }
@@ -3080,7 +3132,7 @@ function buildSourceConfigFromInputChannels(selectedConnectors, existingSources
3080
3132
  ...buildExtraSourceConfig('asc-cli', {
3081
3133
  enabled: selected.has('asc'),
3082
3134
  mode: 'command',
3083
- command: getDefaultSourceCommand('asc'),
3135
+ command: getWizardDefaultSourceCommand('asc'),
3084
3136
  }),
3085
3137
  ...(ascSource || {}),
3086
3138
  enabled: selected.has('asc'),
@@ -3139,20 +3191,33 @@ async function askOutputConfig(rl, config) {
3139
3191
  'OpenClaw chat is always enabled so the agent has a readable handoff.',
3140
3192
  'GitHub issues or draft PRs are optional and only run when a token plus an inferred repo are available.',
3141
3193
  ]);
3142
- process.stdout.write(' 1) OpenClaw chat only, with GitHub left as runtime fallback\n');
3143
- process.stdout.write(' 2) Auto-create GitHub issues for concrete findings\n');
3144
- process.stdout.write(' 3) Auto-create draft PR proposals for implementation-ready fixes\n');
3145
3194
  const currentMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
3146
3195
  const currentAutoCreate = Boolean(config?.actions?.autoCreateIssues || config?.actions?.autoCreatePullRequests || config?.deliveries?.github?.autoCreate);
3147
- const defaultChoice = currentAutoCreate ? (currentMode === 'pull_request' ? '3' : '2') : '1';
3148
- const outputChoice = await ask(rl, 'Output type (1/2/3)', defaultChoice);
3149
- const summaryOnly = outputChoice.trim() === '1';
3150
- const mode = outputChoice.trim() === '3' ? 'pull_request' : 'issue';
3151
- const autoCreate = summaryOnly
3152
- ? false
3153
- : await askYesNo(rl, mode === 'pull_request'
3154
- ? 'Automatically create draft pull requests when new findings are found?'
3155
- : 'Automatically create GitHub issues when new findings are found?', currentAutoCreate);
3196
+ const outputChoice = await askMenuChoice(rl, {
3197
+ title: 'Output mode',
3198
+ subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
3199
+ defaultValue: currentAutoCreate ? (currentMode === 'pull_request' ? 'pull_request' : 'issue') : 'chat',
3200
+ options: [
3201
+ {
3202
+ value: 'chat',
3203
+ label: 'OpenClaw chat',
3204
+ detail: 'Write readable summaries and leave GitHub as runtime fallback.',
3205
+ },
3206
+ {
3207
+ value: 'issue',
3208
+ label: 'GitHub issues',
3209
+ detail: 'Auto-create issues for concrete findings when GitHub access allows it.',
3210
+ },
3211
+ {
3212
+ value: 'pull_request',
3213
+ label: 'Draft PR proposals',
3214
+ detail: 'Auto-create draft PR-oriented proposal branches for implementation-ready fixes.',
3215
+ },
3216
+ ],
3217
+ });
3218
+ const summaryOnly = outputChoice === 'chat';
3219
+ const mode = outputChoice === 'pull_request' ? 'pull_request' : 'issue';
3220
+ const autoCreate = !summaryOnly;
3156
3221
  if (!summaryOnly) {
3157
3222
  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');
3158
3223
  }
@@ -3310,7 +3375,7 @@ async function askInputSourceConfig(rl, config, configPath) {
3310
3375
  mode: 'input',
3311
3376
  });
3312
3377
  config.sources = buildSourceConfigFromInputChannels(selected, config.sources || {});
3313
- return config;
3378
+ return { config, selected, healthByConnector };
3314
3379
  }
3315
3380
  async function writeOpenClawJobManifest(configPath, config) {
3316
3381
  const manifestPath = path.resolve('.openclaw/jobs/openclaw-growth-engineer.json');
@@ -3403,7 +3468,23 @@ async function main() {
3403
3468
  let config = await loadEditableConfig(configPath);
3404
3469
  config.version = Number(config.version || 7);
3405
3470
  config.generatedAt = new Date().toISOString();
3406
- config = await askInputSourceConfig(rl, config, configPath);
3471
+ const inputSetup = await askInputSourceConfig(rl, config, configPath);
3472
+ config = inputSetup.config;
3473
+ await ensureDirForFile(configPath);
3474
+ await writeJsonFile(configPath, config);
3475
+ const connectorsOk = await runConnectorSetupSteps({
3476
+ rl,
3477
+ args: { ...args, config: configPath },
3478
+ selected: inputSetup.selected,
3479
+ healthByConnector: inputSetup.healthByConnector,
3480
+ allowIsolationPrompt: false,
3481
+ });
3482
+ if (!connectorsOk) {
3483
+ return;
3484
+ }
3485
+ config = await loadEditableConfig(configPath);
3486
+ config.version = Number(config.version || 7);
3487
+ config.generatedAt = new Date().toISOString();
3407
3488
  config = await askIntervalConfig(rl, config);
3408
3489
  config = await askOutputConfig(rl, config);
3409
3490
  config = await askGitHubArtifactDetails(rl, config);
@@ -3416,8 +3497,8 @@ async function main() {
3416
3497
  printSecretRunnerKitInstructions(secretAccess.kit);
3417
3498
  process.stdout.write('\nNext steps:\n');
3418
3499
  process.stdout.write(`1) Set secrets in OpenClaw secret store (env var names in config.secrets)\n`);
3419
- process.stdout.write(`2) Run once: node scripts/openclaw-growth-runner.mjs --config ${configPath}\n`);
3420
- process.stdout.write(`3) Run interval loop: node scripts/openclaw-growth-runner.mjs --config ${configPath} --loop\n`);
3500
+ process.stdout.write(`2) Run once: ${growthEngineerPackageCommand(`run --config ${quote(configPath)}`)}\n`);
3501
+ process.stdout.write(`3) Run interval loop: ${growthEngineerPackageCommand(`run --config ${quote(configPath)} --loop`)}\n`);
3421
3502
  }
3422
3503
  finally {
3423
3504
  rl.close();