@analyticscli/growth-engineer 0.1.0-preview.1 → 0.1.0-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.
@@ -1,17 +1,30 @@
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 { buildExtraSourceConfig, getDefaultSourceCommand, getDefaultSourceHint, getDefaultSourcePath, } from './openclaw-growth-shared.mjs';
9
+ import { fileURLToPath } from 'node:url';
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';
12
13
  const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
13
14
  const ENABLE_ISOLATED_SECRET_RUNNER_WIZARD = false;
15
+ const DEFAULT_GROWTH_INTERVAL_MINUTES = 1440;
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));
14
19
  const CONNECTOR_KEYS = ['analytics', 'github', 'revenuecat', 'sentry', 'asc'];
20
+ class WizardAbortError extends Error {
21
+ exitCode;
22
+ constructor(message, exitCode = 130) {
23
+ super(message);
24
+ this.name = 'WizardAbortError';
25
+ this.exitCode = exitCode;
26
+ }
27
+ }
15
28
  const CONNECTOR_DEFINITIONS = [
16
29
  {
17
30
  key: 'analytics',
@@ -47,33 +60,33 @@ const CONNECTOR_DEFINITIONS = [
47
60
  const DEFAULT_CADENCE_PLAN = [
48
61
  {
49
62
  key: 'daily',
50
- title: 'Daily production guardrail',
63
+ title: 'Daily Sentry and production guardrail',
51
64
  intervalDays: 1,
52
65
  criticalOnly: true,
53
- focusAreas: ['crash', 'conversion', 'paywall'],
54
- sourcePriorities: ['sentry', 'glitchtip', 'analytics', 'asc_cli', 'revenuecat'],
55
- objective: 'Only investigate critical production blockers and business anomalies: Sentry/GlitchTip production errors, crashes, very low users, conversion, purchases, or other urgent drops.',
56
- instructions: 'Do exact root-cause analysis with connected production data, memory/state, release context, and recent code changes. Produce the fix or next debugging step; avoid generic growth ideas.',
66
+ focusAreas: ['sentry_errors', 'crash', 'onboarding', 'conversion', 'paywall', 'purchase'],
67
+ sourcePriorities: ['sentry', 'glitchtip', 'analytics', 'revenuecat', 'asc_cli', 'feedback', 'github'],
68
+ objective: 'Analyze every configured project for critical production blockers: Sentry/GlitchTip errors, crashes, onboarding or purchase drop-offs, zero-conversion days, missing buyers, very low users, and other silent business anomalies.',
69
+ instructions: 'Compare against recent baselines across connected sources and code changes. If the finding is critical, produce the exact fix or next debugging step and prefer a GitHub issue or draft PR when GitHub write access is configured; otherwise hand off via OpenClaw chat. Avoid generic growth ideas.',
57
70
  },
58
71
  {
59
72
  key: 'weekly',
60
- title: 'Weekly conversion, traffic, and RevenueCat review',
73
+ title: 'Weekly executive product and growth summary',
61
74
  intervalDays: 7,
62
75
  criticalOnly: false,
63
- focusAreas: ['conversion', 'paywall', 'onboarding', 'marketing', 'retention'],
64
- sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
65
- objective: 'Analyze total conversion, traffic quality, activation, retention, RevenueCat trials/subscriptions/revenue/churn, source mix, reviews, releases, and stability.',
66
- instructions: 'Pick one to three high-confidence growth bets with evidence, expected KPI movement, likely code/store surfaces, and verification plan.',
76
+ focusAreas: ['conversion', 'paywall', 'onboarding', 'marketing', 'retention', 'stability'],
77
+ sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry', 'github'],
78
+ objective: 'Create an executive summary across all configured projects, connectors, recent releases, code changes, revenue, activation, retention, reviews, and production stability.',
79
+ instructions: 'Pick one to three high-confidence improvements with evidence, expected KPI movement, likely code/store surfaces, owner-ready next steps, and a verification plan. Create GitHub issues or draft PR proposals only when the evidence is specific enough.',
67
80
  },
68
81
  {
69
82
  key: 'monthly',
70
- title: 'Monthly business and product review',
83
+ title: 'Monthly deep product, business, and code review',
71
84
  intervalDays: 30,
72
85
  criticalOnly: false,
73
- focusAreas: ['conversion', 'paywall', 'retention', 'marketing', 'onboarding'],
74
- sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
75
- objective: 'Compare MRR, trial conversion, churn, acquisition quality, store conversion, retention, review themes, feature usage, and crash totals month-over-month.',
76
- instructions: 'Decide what should be built, changed, or deleted next and explain why it should move revenue, activation, retention, or acquisition quality.',
86
+ focusAreas: ['conversion', 'paywall', 'retention', 'marketing', 'onboarding', 'codebase'],
87
+ sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry', 'github'],
88
+ objective: 'Compare all configured projects month-over-month: MRR, trial conversion, churn, acquisition quality, store conversion, retention, review themes, feature usage, crash totals, and codebase changes.',
89
+ instructions: 'Decide what should be built, changed, deleted, or instrumented next. Tie conclusions to connector data plus codebase evidence and explain why each recommendation should move revenue, activation, retention, stability, or acquisition quality.',
77
90
  },
78
91
  {
79
92
  key: 'quarterly',
@@ -81,8 +94,8 @@ const DEFAULT_CADENCE_PLAN = [
81
94
  intervalDays: 91,
82
95
  criticalOnly: false,
83
96
  focusAreas: ['marketing', 'paywall', 'retention', 'conversion', 'onboarding'],
84
- sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback'],
85
- objective: 'Revisit positioning, pricing/packaging, onboarding architecture, roadmap assumptions, tracking quality, and major funnel bets.',
97
+ sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'github', 'sentry'],
98
+ objective: 'Revisit positioning, pricing/packaging, onboarding architecture, roadmap assumptions, tracking quality, codebase constraints, and major funnel bets across every configured project.',
86
99
  instructions: 'Find structural constraints and durable opportunities. Tie recommendations to cohort behavior, monetization, reviews, channel quality, and shipped changes.',
87
100
  },
88
101
  {
@@ -92,7 +105,7 @@ const DEFAULT_CADENCE_PLAN = [
92
105
  criticalOnly: false,
93
106
  focusAreas: ['retention', 'conversion', 'paywall', 'marketing', 'general'],
94
107
  sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
95
- objective: 'Audit connector coverage, SDK instrumentation, event taxonomy, data reliability, memory, growth loops, and whether strategy still matches the best users.',
108
+ objective: 'Audit connector coverage, SDK instrumentation, event taxonomy, data reliability, memory, growth loops, and whether product/code strategy still matches the best users across configured projects.',
96
109
  instructions: 'Prioritize measurement fixes and system changes that make future analysis more trustworthy. Identify stale events, missing attribution, weak identity, and misleading dashboards.',
97
110
  },
98
111
  {
@@ -102,7 +115,7 @@ const DEFAULT_CADENCE_PLAN = [
102
115
  criticalOnly: false,
103
116
  focusAreas: ['marketing', 'retention', 'paywall', 'conversion', 'general'],
104
117
  sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
105
- objective: 'Reset strategy from evidence: market/channel fit, monetization model, retention ceiling, product scope, and whether to double down, reposition, rebuild, or sunset major surfaces/features.',
118
+ objective: 'Reset strategy from evidence across every configured project: market/channel fit, monetization model, retention ceiling, product scope, and whether to double down, reposition, rebuild, or sunset major surfaces/features.',
106
119
  instructions: 'Use the full year of memory, releases, revenue, acquisition, reviews, code changes, and cohort behavior. Produce strategic experiments and stop-doing decisions.',
107
120
  },
108
121
  ];
@@ -210,6 +223,91 @@ function quote(value) {
210
223
  }
211
224
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
212
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
+ }
259
+ function replaceLegacyRuntimeScriptCommand(command) {
260
+ const trimmed = String(command || '').trim();
261
+ if (!trimmed)
262
+ return trimmed;
263
+ return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-sentry-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-start\.mjs|openclaw-growth-status\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-preflight\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
264
+ }
265
+ function normalizeWizardSourceCommand(sourceName, source) {
266
+ const current = replaceLegacyRuntimeScriptCommand(source?.command || '');
267
+ return current || getWizardDefaultSourceCommand(sourceName);
268
+ }
269
+ function migrateRuntimeSourceCommands(config) {
270
+ if (!config || typeof config !== 'object')
271
+ return config;
272
+ const sources = config.sources && typeof config.sources === 'object' ? config.sources : {};
273
+ const nextSources = { ...sources };
274
+ for (const sourceName of ['analytics', 'revenuecat', 'sentry']) {
275
+ if (nextSources[sourceName]?.mode === 'command') {
276
+ nextSources[sourceName] = {
277
+ ...nextSources[sourceName],
278
+ command: normalizeWizardSourceCommand(sourceName, nextSources[sourceName]),
279
+ };
280
+ }
281
+ }
282
+ if (Array.isArray(nextSources.extra)) {
283
+ nextSources.extra = nextSources.extra.map((source) => {
284
+ if (!source || source.mode !== 'command')
285
+ return source;
286
+ const service = String(source.service || source.key || '').toLowerCase();
287
+ const sourceName = ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(service)
288
+ ? 'asc'
289
+ : service;
290
+ return {
291
+ ...source,
292
+ command: normalizeWizardSourceCommand(sourceName, source),
293
+ };
294
+ });
295
+ }
296
+ return {
297
+ ...config,
298
+ sources: nextSources,
299
+ };
300
+ }
301
+ async function migrateRuntimeSourceCommandsFile(configPath) {
302
+ const existing = await readJsonIfPresent(configPath).catch(() => null);
303
+ if (!existing || typeof existing !== 'object')
304
+ return null;
305
+ const migrated = migrateRuntimeSourceCommands(existing);
306
+ if (JSON.stringify(existing.sources || {}) !== JSON.stringify(migrated.sources || {})) {
307
+ await writeJsonFile(configPath, migrated);
308
+ }
309
+ return migrated;
310
+ }
213
311
  function normalizeConnectorKey(value) {
214
312
  const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
215
313
  if (!normalized)
@@ -268,23 +366,28 @@ function withMissingRequiredAnalyticsConnector(selected) {
268
366
  return orderConnectors(selected);
269
367
  return orderConnectors(['analytics', ...selected]);
270
368
  }
271
- async function askConnectorSelection(rl) {
272
- return askConnectorSelectionWithHealth(rl, {}, []);
273
- }
274
- async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = []) {
369
+ async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = [], copy = {}) {
275
370
  if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
276
- return await askConnectorSelectionByText(rl, healthByConnector);
371
+ return await askConnectorSelectionByText(rl, healthByConnector, copy);
277
372
  }
278
373
  rl.pause();
374
+ let completed = false;
279
375
  try {
280
- return await askConnectorSelectionByKeys(healthByConnector, initialSelected);
376
+ const selected = await askConnectorSelectionByKeys(healthByConnector, initialSelected, copy);
377
+ completed = true;
378
+ return selected;
281
379
  }
282
380
  finally {
283
- rl.resume();
381
+ if (completed) {
382
+ rl.resume();
383
+ }
384
+ else {
385
+ process.stdin.pause();
386
+ }
284
387
  }
285
388
  }
286
- async function askConnectorSelectionByText(rl, healthByConnector = {}) {
287
- printConnectorIntro();
389
+ async function askConnectorSelectionByText(rl, healthByConnector = {}, copy = {}) {
390
+ printConnectorIntro(copy);
288
391
  for (const group of connectorPickerGroups(healthByConnector)) {
289
392
  process.stdout.write(`${ANSI.bold}${group.title}${ANSI.reset}\n`);
290
393
  for (const connector of group.connectors) {
@@ -322,26 +425,117 @@ function orderConnectors(keys) {
322
425
  const selected = new Set(keys);
323
426
  return CONNECTOR_KEYS.filter((key) => selected.has(key));
324
427
  }
325
- function printConnectorIntro() {
326
- process.stdout.write(`\n${ANSI.bold}OpenClaw connector setup${ANSI.reset}\n`);
327
- process.stdout.write(`${ANSI.dim}Secrets stay local on this host. Do not paste them into any chat or social channel.${ANSI.reset}\n\n`);
428
+ function printConnectorIntro(copy = {}) {
429
+ process.stdout.write(`\n${ANSI.bold}${copy.introTitle || 'OpenClaw connector setup'}${ANSI.reset}\n`);
430
+ const detail = copy.introDetail === undefined
431
+ ? 'You can configure connector secrets here. API keys stay in this host\'s local secrets file, not in chat or config JSON.'
432
+ : copy.introDetail;
433
+ if (detail) {
434
+ process.stdout.write(`${ANSI.dim}${detail}${ANSI.reset}\n`);
435
+ }
436
+ process.stdout.write('\n');
328
437
  }
329
- async function withTerminalLoading(message, task) {
330
- const frames = ['-', '\\', '|', '/'];
331
- let index = 0;
332
- process.stdout.write(`${message} ${frames[index]}`);
333
- const timer = setInterval(() => {
334
- index = (index + 1) % frames.length;
335
- process.stdout.write(`\r${message} ${frames[index]}`);
336
- }, 120);
438
+ async function askMenuChoice(rl, { title, subtitle = 'Use Up/Down to move, Enter to continue.', options, defaultValue, renderHeader, }) {
439
+ if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
440
+ process.stdout.write(`\n${title}\n`);
441
+ options.forEach((option, index) => {
442
+ process.stdout.write(` ${index + 1}) ${option.label}: ${option.detail}\n`);
443
+ });
444
+ const defaultIndex = Math.max(0, options.findIndex((option) => option.value === defaultValue));
445
+ const answer = await ask(rl, `Setup area (1-${options.length})`, String(defaultIndex + 1));
446
+ const selected = options[Number(answer.trim()) - 1] || options[defaultIndex];
447
+ return selected.value;
448
+ }
449
+ rl.pause();
450
+ let completed = false;
337
451
  try {
338
- return await task;
452
+ const selected = await askMenuChoiceByKeys({ title, subtitle, options, defaultValue, renderHeader });
453
+ completed = true;
454
+ return selected;
339
455
  }
340
456
  finally {
341
- clearInterval(timer);
342
- process.stdout.write(`\r${message} done\n`);
457
+ if (completed) {
458
+ rl.resume();
459
+ }
460
+ else {
461
+ process.stdin.pause();
462
+ }
343
463
  }
344
464
  }
465
+ async function askMenuChoiceByKeys({ title, subtitle, options, defaultValue, renderHeader, }) {
466
+ emitKeypressEvents(process.stdin);
467
+ const wasRaw = process.stdin.isRaw;
468
+ const wasPaused = process.stdin.isPaused();
469
+ process.stdin.setRawMode(true);
470
+ process.stdin.resume();
471
+ let cursorIndex = Math.max(0, options.findIndex((option) => option.value === defaultValue));
472
+ return await new Promise((resolve, reject) => {
473
+ const cleanup = () => {
474
+ process.stdin.off('keypress', onKeypress);
475
+ process.stdin.setRawMode(Boolean(wasRaw));
476
+ if (wasPaused) {
477
+ process.stdin.pause();
478
+ }
479
+ process.stdout.write(ANSI.showCursor);
480
+ };
481
+ const render = () => {
482
+ process.stdout.write('\x1b[2J\x1b[H');
483
+ renderHeader?.();
484
+ process.stdout.write(`\n${ANSI.bold}${title}${ANSI.reset}\n`);
485
+ process.stdout.write(`${ANSI.dim}${subtitle}${ANSI.reset}\n\n`);
486
+ for (let index = 0; index < options.length; index += 1) {
487
+ const option = options[index];
488
+ const pointer = index === cursorIndex ? `${ANSI.cyan}>${ANSI.reset}` : ' ';
489
+ const number = `${index + 1})`;
490
+ process.stdout.write(`${pointer} ${number} ${ANSI.bold}${option.label}${ANSI.reset}\n`);
491
+ writeWrapped(option.detail, ' ', ANSI.dim);
492
+ }
493
+ process.stdout.write(`\n${ANSI.dim}Esc/Q cancels. Number keys 1-${options.length} select directly.${ANSI.reset}\n`);
494
+ };
495
+ const cancel = () => {
496
+ cleanup();
497
+ process.stdout.write('\n');
498
+ reject(new WizardAbortError('Setup cancelled.'));
499
+ };
500
+ const finish = () => {
501
+ cleanup();
502
+ process.stdout.write('\x1b[2J\x1b[H');
503
+ resolve(options[cursorIndex]?.value || defaultValue);
504
+ };
505
+ const onKeypress = (_text, key) => {
506
+ if (key?.ctrl && key?.name === 'c') {
507
+ cancel();
508
+ return;
509
+ }
510
+ if (key?.name === 'escape' || key?.name === 'q') {
511
+ cancel();
512
+ return;
513
+ }
514
+ if (key?.name === 'up' || key?.name === 'k') {
515
+ cursorIndex = (cursorIndex - 1 + options.length) % options.length;
516
+ }
517
+ else if (key?.name === 'down' || key?.name === 'j') {
518
+ cursorIndex = (cursorIndex + 1) % options.length;
519
+ }
520
+ else if (key?.name === 'return' || key?.name === 'enter') {
521
+ finish();
522
+ return;
523
+ }
524
+ else if (/^[1-9]$/.test(String(_text || ''))) {
525
+ const selectedIndex = Number(_text) - 1;
526
+ if (options[selectedIndex]) {
527
+ cursorIndex = selectedIndex;
528
+ finish();
529
+ return;
530
+ }
531
+ }
532
+ render();
533
+ };
534
+ process.stdin.on('keypress', onKeypress);
535
+ process.stdout.write(ANSI.hideCursor);
536
+ render();
537
+ });
538
+ }
345
539
  function normalizeConnectorProgressKey(key) {
346
540
  const normalized = String(key || '').trim().toLowerCase();
347
541
  if (normalized === 'analytics' || normalized === 'analyticscli')
@@ -439,9 +633,6 @@ function connectorStatusLabel(key, healthByConnector = {}) {
439
633
  return 'not configured';
440
634
  return `configured, ${connectorHealthLabel(health.status)}`;
441
635
  }
442
- function formatConnectorHealthLine(key, healthByConnector = {}) {
443
- return `${ANSI.dim}${formatConnectorHealthText(key, healthByConnector)}${ANSI.reset}`;
444
- }
445
636
  function formatConnectorHealthText(key, healthByConnector = {}) {
446
637
  const health = getConnectorHealth(key, healthByConnector);
447
638
  const label = connectorStatusLabel(key, healthByConnector);
@@ -517,7 +708,7 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
517
708
  },
518
709
  ]));
519
710
  }
520
- const result = await runCommandCaptureWithProgress(`node scripts/openclaw-growth-status.mjs --config ${quote(configPath)} --json --progress-json`, onProgress);
711
+ const result = await runCommandCaptureWithProgress(`${nodeRuntimeScriptCommand('openclaw-growth-status.mjs')} --config ${quote(configPath)} --json --progress-json`, onProgress);
521
712
  const payload = parseJsonFromStdout(result.stdout);
522
713
  const connectors = payload?.connectors && typeof payload.connectors === 'object' ? payload.connectors : {};
523
714
  const healthByConnector = {
@@ -529,11 +720,11 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
529
720
  };
530
721
  return Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, healthByConnector)]));
531
722
  }
532
- function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '') {
723
+ function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '', copy = {}) {
533
724
  process.stdout.write('\x1b[2J\x1b[H');
534
- printConnectorIntro();
535
- process.stdout.write(`${ANSI.bold}Select connectors to set up or overwrite now${ANSI.reset}\n`);
536
- writeWrapped('Use Up/Down to move, Space to toggle optional connectors, A to toggle all optional connectors, Enter to continue.', '', ANSI.dim);
725
+ printConnectorIntro(copy);
726
+ process.stdout.write(`${ANSI.bold}${copy.actionTitle || 'Select connectors to set up or overwrite now'}${ANSI.reset}\n`);
727
+ writeWrapped(copy.helpText || 'Use Up/Down to move, Space to toggle optional connectors, A to toggle all optional connectors, Enter to continue.', '', ANSI.dim);
537
728
  process.stdout.write('\n');
538
729
  let index = 0;
539
730
  for (const group of connectorPickerGroups(healthByConnector)) {
@@ -559,16 +750,18 @@ function renderConnectorPicker(cursorIndex, selected, required, healthByConnecto
559
750
  }
560
751
  process.stdout.write(`${ANSI.dim}Esc/Q cancels. Number keys 1-${CONNECTOR_DEFINITIONS.length} also toggle connectors.${ANSI.reset}\n`);
561
752
  }
562
- async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelected = []) {
753
+ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelected = [], copy = {}) {
563
754
  emitKeypressEvents(process.stdin);
564
755
  const wasRaw = process.stdin.isRaw;
565
756
  const wasPaused = process.stdin.isPaused();
566
757
  process.stdin.setRawMode(true);
567
758
  process.stdin.resume();
568
759
  let cursorIndex = 0;
569
- const required = getRequiredConnectorKeys();
760
+ const required = copy.mode === 'input' ? new Set() : getRequiredConnectorKeys();
570
761
  const initial = new Set(initialSelected);
571
- const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) || initial.has(key) || !isConnectorLocallyConfigured(key)));
762
+ const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) ||
763
+ initial.has(key) ||
764
+ (copy.mode !== 'input' && !isConnectorLocallyConfigured(key))));
572
765
  let warning = '';
573
766
  return await new Promise((resolve, reject) => {
574
767
  const displayItems = () => connectorPickerDisplayItems(healthByConnector);
@@ -586,7 +779,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
586
779
  required.forEach((key) => selected.add(key));
587
780
  if (selected.size === 0) {
588
781
  warning = 'No connectors selected. Select a connector to update or press Esc to cancel.';
589
- renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
782
+ renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
590
783
  return;
591
784
  }
592
785
  cleanup();
@@ -596,7 +789,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
596
789
  const cancel = () => {
597
790
  cleanup();
598
791
  process.stdout.write('\n');
599
- reject(new Error('Connector setup cancelled.'));
792
+ reject(new WizardAbortError('Connector setup cancelled.'));
600
793
  };
601
794
  const toggleCurrent = () => {
602
795
  const connector = selectedDisplayConnector();
@@ -671,11 +864,11 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
671
864
  }
672
865
  }
673
866
  }
674
- renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
867
+ renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
675
868
  };
676
869
  process.stdin.on('keypress', onKeypress);
677
870
  process.stdout.write(ANSI.hideCursor);
678
- renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
871
+ renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
679
872
  });
680
873
  }
681
874
  async function commandExists(commandName) {
@@ -914,16 +1107,16 @@ function summarizeFailureFix(connector, blockers) {
914
1107
  if (/revoked|unauthorized|UNAUTHORIZED/i.test(combined)) {
915
1108
  return 'Paste a fresh AnalyticsCLI readonly CLI token in the wizard, then let setup retest.';
916
1109
  }
917
- return 'Verify the AnalyticsCLI token can list projects. Per-project query failures are reported as warnings and should not block connector setup.';
1110
+ return 'Verify the AnalyticsCLI token can list accessible projects. Per-project query failures are reported as warnings and should not block connector setup.';
918
1111
  }
919
1112
  if (connector === 'sentry') {
920
1113
  if (/404|Not Found/i.test(combined)) {
921
- return 'Rerun Sentry/GlitchTip setup and use the correct base URL + discovered org. If projects are discovered, accept/select those projects.';
1114
+ return 'Rerun Sentry/GlitchTip setup and use the correct base URL + visible org. Project scope stays unpinned and is resolved from app context later.';
922
1115
  }
923
- return 'Verify the Sentry/GlitchTip token, base URL, org, and project list, then rerun setup.';
1116
+ return 'Verify the Sentry/GlitchTip token, base URL, and org, then rerun setup.';
924
1117
  }
925
1118
  if (connector === 'github') {
926
- return 'Set project.githubRepo only if you want GitHub issue/PR delivery now; otherwise leave GitHub deferred.';
1119
+ return 'Verify the GitHub token. Repo scope is inferred from OPENCLAW_GITHUB_REPO, the local git remote, or runtime context.';
927
1120
  }
928
1121
  if (connector === 'revenuecat') {
929
1122
  return 'Paste a RevenueCat v2 secret API key with read-only project permissions, then rerun setup.';
@@ -1048,11 +1241,6 @@ function printSetupSuccess(payload) {
1048
1241
  process.stdout.write(`${payload.message}\n`);
1049
1242
  }
1050
1243
  }
1051
- function healthCheckFailures(payload) {
1052
- return Array.isArray(payload?.checks)
1053
- ? payload.checks.filter((check) => check?.status === 'fail')
1054
- : [];
1055
- }
1056
1244
  function connectorFromCheckName(name) {
1057
1245
  const value = String(name || '');
1058
1246
  if (value.includes('analytics') || value.includes('ANALYTICSCLI'))
@@ -1111,146 +1299,12 @@ function cleanHealthDetail(detail) {
1111
1299
  }
1112
1300
  return truncate(raw, 180);
1113
1301
  }
1114
- function actionForHealthFailure(failure, configPath) {
1115
- const name = String(failure?.name || '');
1116
- const detail = String(failure?.detail || '');
1117
- if (name === 'project:github-repo' || /project\.githubRepo/i.test(detail)) {
1118
- return `No action required for Sentry setup. Set project.githubRepo in ${configPath} only if you want GitHub issue/PR delivery now.`;
1119
- }
1120
- if (name.includes('analytics') || /ANALYTICSCLI|analytics/i.test(detail)) {
1121
- return 'Paste a fresh AnalyticsCLI readonly token, then let the wizard retest AnalyticsCLI.';
1122
- }
1123
- if (name.includes('sentry') || /Sentry|GlitchTip/i.test(detail)) {
1124
- return 'Only fix this if token, org, or base URL is missing or invalid.';
1125
- }
1126
- if (name.includes('github')) {
1127
- return 'Configure GitHub token/repo access, or leave GitHub delivery disabled.';
1128
- }
1129
- if (name.includes('revenuecat')) {
1130
- return 'Paste a RevenueCat v2 secret API key with read-only project permissions.';
1131
- }
1132
- if (name.includes('asc')) {
1133
- return 'Paste ASC API key details or rerun ASC setup when ready.';
1134
- }
1135
- return 'Use the connector setup flow below to refresh this configuration.';
1136
- }
1137
1302
  function isDeferredGitHubFailure(failure) {
1138
1303
  const name = String(failure?.name || '');
1139
1304
  const detail = String(failure?.detail || '');
1140
1305
  return (name === 'project:github-repo' ||
1141
1306
  (name === 'connection:github' && /project\.githubRepo|repo is missing|repo is not configured/i.test(detail)));
1142
1307
  }
1143
- function isDeferredSentryProjectFailure(failure) {
1144
- const name = String(failure?.name || '');
1145
- const detail = String(failure?.detail || '');
1146
- return name.includes('sentry') && /No Sentry projects configured/i.test(detail);
1147
- }
1148
- function summarizeHealthFailure(failure, configPath) {
1149
- const name = String(failure?.name || '');
1150
- const detail = String(failure?.detail || '');
1151
- const connector = connectorFromCheckName(`${name} ${detail}`) || 'setup';
1152
- if (connector === 'analytics' && /invalid token|unauthorized|token has been revoked/i.test(detail)) {
1153
- return {
1154
- connector,
1155
- status: 'token invalid or expired',
1156
- action: 'paste a fresh readonly token',
1157
- };
1158
- }
1159
- if (connector === 'sentry' && /No Sentry projects configured/i.test(detail)) {
1160
- return {
1161
- connector,
1162
- status: 'project scope deferred',
1163
- action: 'no user action; OpenClaw discovers visible projects from org + token',
1164
- };
1165
- }
1166
- if (connector === 'github' && isDeferredGitHubFailure(failure)) {
1167
- return {
1168
- connector,
1169
- status: 'repo not known yet',
1170
- action: `optional; set project.githubRepo in ${configPath} only for GitHub delivery`,
1171
- };
1172
- }
1173
- return {
1174
- connector,
1175
- status: cleanHealthDetail(detail),
1176
- action: actionForHealthFailure(failure, configPath),
1177
- };
1178
- }
1179
- function printHealthFailures(failures, configPath) {
1180
- const summarized = [];
1181
- const seen = new Set();
1182
- for (const failure of failures) {
1183
- if (isDeferredGitHubFailure(failure))
1184
- continue;
1185
- if (isDeferredSentryProjectFailure(failure))
1186
- continue;
1187
- const summary = summarizeHealthFailure(failure, configPath);
1188
- const key = `${summary.connector}:${summary.status}:${summary.action}`;
1189
- if (seen.has(key))
1190
- continue;
1191
- seen.add(key);
1192
- summarized.push(summary);
1193
- }
1194
- if (summarized.length === 0) {
1195
- process.stdout.write('\nOnly deferred optional checks remain.\n\n');
1196
- return;
1197
- }
1198
- process.stdout.write('\nNeeds attention\n');
1199
- process.stdout.write('---------------\n');
1200
- for (const summary of summarized) {
1201
- process.stdout.write(`- ${connectorTitle(summary.connector)}: ${summary.status}\n`);
1202
- process.stdout.write(` Next: ${summary.action}\n`);
1203
- }
1204
- process.stdout.write('\n');
1205
- }
1206
- function inferConnectorsFromHealthFailures(failures) {
1207
- const inferred = new Set();
1208
- for (const failure of failures) {
1209
- if (isDeferredGitHubFailure(failure))
1210
- continue;
1211
- if (isDeferredSentryProjectFailure(failure))
1212
- continue;
1213
- const connector = connectorFromCheckName(`${failure?.name || ''} ${failure?.detail || ''}`);
1214
- if (connector)
1215
- inferred.add(connector);
1216
- }
1217
- return orderConnectors([...inferred]);
1218
- }
1219
- async function getHealthCheckPlan(configPath, selected) {
1220
- const config = await readJsonIfPresent(configPath).catch(() => null);
1221
- const items = [
1222
- {
1223
- key: 'preflight',
1224
- label: 'Local preflight',
1225
- detail: 'config, dependencies, source wiring',
1226
- status: 'pending',
1227
- },
1228
- ];
1229
- const selectedSet = new Set(selected);
1230
- const hasAnalytics = selectedSet.has('analytics') ||
1231
- Boolean(process.env.ANALYTICSCLI_ACCESS_TOKEN?.trim() || process.env.ANALYTICSCLI_READONLY_TOKEN?.trim()) ||
1232
- (config?.sources?.analytics && config.sources.analytics.enabled !== false);
1233
- const sentryAccounts = Array.isArray(config?.sources?.sentry?.accounts) ? config.sources.sentry.accounts : [];
1234
- const hasSentry = selectedSet.has('sentry') ||
1235
- sentryAccounts.length > 0 ||
1236
- Boolean(process.env.SENTRY_AUTH_TOKEN?.trim() || process.env.GLITCHTIP_AUTH_TOKEN?.trim());
1237
- const hasRevenueCat = selectedSet.has('revenuecat') ||
1238
- Boolean(process.env.REVENUECAT_API_KEY?.trim()) ||
1239
- (config?.sources?.revenuecat && config.sources.revenuecat.enabled !== false);
1240
- const githubRepo = String(config?.project?.githubRepo || '').trim();
1241
- const hasGitHub = selectedSet.has('github') || Boolean(process.env.GITHUB_TOKEN?.trim()) || Boolean(githubRepo);
1242
- if (hasAnalytics)
1243
- items.push({ key: 'analytics', label: 'AnalyticsCLI', detail: 'token auth + readonly query', status: 'pending' });
1244
- if (hasSentry)
1245
- items.push({ key: 'sentry', label: 'Sentry / GlitchTip', detail: 'token/org API + project discovery', status: 'pending' });
1246
- if (hasRevenueCat)
1247
- items.push({ key: 'revenuecat', label: 'RevenueCat', detail: 'API key auth + project read', status: 'pending' });
1248
- if (hasGitHub && githubRepo)
1249
- items.push({ key: 'github', label: 'GitHub', detail: `repo access (${githubRepo})`, status: 'pending' });
1250
- if (hasGitHub && !githubRepo)
1251
- items.push({ key: 'github', label: 'GitHub', detail: 'skipped until repo is known', status: 'pending' });
1252
- return items;
1253
- }
1254
1308
  function healthStatusLabel(status) {
1255
1309
  if (status === 'running')
1256
1310
  return 'running';
@@ -1299,9 +1353,6 @@ function updateHealthProgress(items, event) {
1299
1353
  }
1300
1354
  return false;
1301
1355
  }
1302
- function allProgressItemsFinished(items) {
1303
- return items.length > 0 && items.every((item) => !['pending', 'running'].includes(String(item.status || '')));
1304
- }
1305
1356
  function buildSetupTestProgressPlan(selected) {
1306
1357
  const selectedSet = new Set(selected);
1307
1358
  const items = [
@@ -1403,7 +1454,7 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
1403
1454
  ...process.env,
1404
1455
  ...secrets,
1405
1456
  };
1406
- const command = `node scripts/openclaw-growth-start.mjs --config ${quote(configPath)} --setup-only --connectors ${quote(connector)} --only-connectors ${quote(connector)}`;
1457
+ const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(configPath)} --setup-only --connectors ${quote(connector)} --only-connectors ${quote(connector)}`;
1407
1458
  let result = await runSetupCommandWithProgress(command, env, [connector], `Checking ${connectorLabel(connector)} immediately after setup...`);
1408
1459
  let payload = parseJsonFromStdout(result.stdout);
1409
1460
  if (connector === 'asc') {
@@ -1430,39 +1481,6 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
1430
1481
  process.stdout.write(`\n${connectorLabel(connector)} immediate health check passed or is only waiting on optional/deferred context.\n`);
1431
1482
  return { ok: true, retry: false, result, payload };
1432
1483
  }
1433
- async function offerConfiguredConnectionFixes(rl, configPath, selected) {
1434
- if (!(await fileExists(configPath)))
1435
- return selected;
1436
- clearTerminal();
1437
- const plan = await getHealthCheckPlan(configPath, selected);
1438
- renderHealthProgress(plan, 'Starting live checks...');
1439
- const command = `node scripts/openclaw-growth-preflight.mjs --config ${quote(configPath)} --test-connections --progress-json`;
1440
- const result = await runCommandCaptureWithProgress(command, (event) => {
1441
- if (updateHealthProgress(plan, event)) {
1442
- renderHealthProgress(plan);
1443
- }
1444
- });
1445
- renderHealthProgress(plan, 'Checks finished.');
1446
- const payload = parseJsonFromStdout(result.stdout);
1447
- const failures = healthCheckFailures(payload).filter((failure) => !isDeferredGitHubFailure(failure) && !isDeferredSentryProjectFailure(failure));
1448
- if (payload?.ok === true || failures.length === 0) {
1449
- process.stdout.write('Configured connectors look healthy.\n\n');
1450
- return selected;
1451
- }
1452
- printHealthFailures(failures, configPath);
1453
- const inferred = inferConnectorsFromHealthFailures(failures);
1454
- if (inferred.length === 0) {
1455
- process.stdout.write('Continuing with the connector(s) you selected.\n\n');
1456
- return selected;
1457
- }
1458
- const fixNow = await askYesNo(rl, `Fix now (${inferred.join(', ')})?`, true);
1459
- clearTerminal();
1460
- if (!fixNow) {
1461
- process.stdout.write('Continuing with selected connector(s).\n\n');
1462
- return selected;
1463
- }
1464
- return orderConnectors([...new Set([...selected, ...inferred])]);
1465
- }
1466
1484
  function getUserLocalBinDir() {
1467
1485
  return process.env.HOME ? path.join(process.env.HOME, '.local', 'bin') : null;
1468
1486
  }
@@ -1818,13 +1836,13 @@ function getGrowthRunCommand(config, displayConfigPath) {
1818
1836
  if (config?.security?.connectorSecrets?.mode === 'isolated-runner' && config.security.connectorSecrets.runCommand) {
1819
1837
  return config.security.connectorSecrets.runCommand;
1820
1838
  }
1821
- return `node scripts/openclaw-growth-runner.mjs --config ${displayConfigPath}`;
1839
+ return growthEngineerPackageCommand(`run --config ${quote(displayConfigPath)}`);
1822
1840
  }
1823
1841
  function getConnectorHealthCommand(config, displayConfigPath) {
1824
1842
  if (config?.security?.connectorSecrets?.mode === 'isolated-runner' && config.security.connectorSecrets.healthCommand) {
1825
1843
  return config.security.connectorSecrets.healthCommand;
1826
1844
  }
1827
- return `node scripts/openclaw-growth-runner.mjs --config ${displayConfigPath}`;
1845
+ return growthEngineerPackageCommand(`run --config ${quote(displayConfigPath)}`);
1828
1846
  }
1829
1847
  async function maybePromptSecret(rl, label, envName) {
1830
1848
  const existing = process.env[envName]?.trim();
@@ -1874,12 +1892,6 @@ function printSentryTokenGuidance({ baseUrl, tokenEnv }) {
1874
1892
  'Optional for richer release context: `project:releases`.',
1875
1893
  ]);
1876
1894
  }
1877
- function parseCommaList(value) {
1878
- return String(value || '')
1879
- .split(',')
1880
- .map((entry) => entry.trim())
1881
- .filter(Boolean);
1882
- }
1883
1895
  function buildUrl(baseUrl, pathname, params = {}) {
1884
1896
  const url = new URL(pathname, `${String(baseUrl || 'https://sentry.io').replace(/\/$/, '')}/`);
1885
1897
  for (const [key, value] of Object.entries(params)) {
@@ -1904,7 +1916,7 @@ function apiListItems(payload) {
1904
1916
  return payload.teams;
1905
1917
  return [];
1906
1918
  }
1907
- async function fetchSentryJsonPage({ baseUrl, token, url }) {
1919
+ async function fetchSentryJsonPage({ token, url }) {
1908
1920
  const normalizedToken = String(token || '').trim();
1909
1921
  const response = await fetch(url, {
1910
1922
  method: 'GET',
@@ -1938,7 +1950,7 @@ async function fetchSentryJsonList({ baseUrl, token, url }) {
1938
1950
  const pages = [];
1939
1951
  let nextUrl = url;
1940
1952
  for (let page = 0; nextUrl && page < 10; page += 1) {
1941
- const result = await fetchSentryJsonPage({ baseUrl, token, url: nextUrl });
1953
+ const result = await fetchSentryJsonPage({ token, url: nextUrl });
1942
1954
  pages.push(result.detail);
1943
1955
  if (!result.ok)
1944
1956
  return { ...result, payload: items, detail: pages.join('; ') };
@@ -2069,7 +2081,7 @@ async function upsertSentryAccountsConfig(configPath, accounts) {
2069
2081
  ...(config.sources?.sentry || {}),
2070
2082
  enabled: true,
2071
2083
  mode: 'command',
2072
- command: getDefaultSourceCommand('sentry'),
2084
+ command: getWizardDefaultSourceCommand('sentry'),
2073
2085
  accounts: [...merged.values()],
2074
2086
  },
2075
2087
  };
@@ -2482,13 +2494,13 @@ async function guideSentryConnector(rl, secrets) {
2482
2494
  org = await ask(rl, `Sentry org slug for ${label} (leave empty to defer)`, index === 0 ? process.env.SENTRY_ORG || '' : '');
2483
2495
  }
2484
2496
  const environment = await ask(rl, `Sentry environment for ${label}`, index === 0 ? process.env.SENTRY_ENVIRONMENT || 'production' : 'production');
2485
- let projects = [];
2486
2497
  if (org.trim() && token) {
2487
- process.stdout.write(`Discovering Sentry projects for ${label}...\n`);
2498
+ process.stdout.write(`Checking visible Sentry projects for ${label} without pinning project scope...\n`);
2488
2499
  const discovery = await discoverSentryProjects({ baseUrl, token, org });
2500
+ let verifiedVisibleProjects = false;
2489
2501
  if (discovery.ok && discovery.projects.length > 0) {
2490
- projects = discovery.projects;
2491
- process.stdout.write(`Configured ${projects.length} project(s): ${projects.slice(0, 8).join(', ')}${projects.length > 8 ? ', ...' : ''}\n`);
2502
+ verifiedVisibleProjects = true;
2503
+ process.stdout.write(`Found ${discovery.projects.length} visible project(s). Project scope remains unpinned so OpenClaw/Hermes can decide per run.\n`);
2492
2504
  }
2493
2505
  else {
2494
2506
  const fallbackOrgs = discoveredOrganizations
@@ -2498,15 +2510,14 @@ async function guideSentryConnector(rl, secrets) {
2498
2510
  process.stdout.write(`Trying visible org ${fallbackOrg}...\n`);
2499
2511
  const fallbackDiscovery = await discoverSentryProjects({ baseUrl, token, org: fallbackOrg });
2500
2512
  if (fallbackDiscovery.ok && fallbackDiscovery.projects.length > 0) {
2501
- projects = fallbackDiscovery.projects;
2502
- process.stdout.write(`Using org ${fallbackOrg}; configured ${projects.length} project(s): ${projects.slice(0, 8).join(', ')}${projects.length > 8 ? ', ...' : ''}\n`);
2513
+ org = fallbackOrg;
2514
+ verifiedVisibleProjects = true;
2515
+ process.stdout.write(`Using org ${fallbackOrg}; found ${fallbackDiscovery.projects.length} visible project(s). Project scope remains unpinned.\n`);
2503
2516
  break;
2504
2517
  }
2505
2518
  }
2506
- if (projects.length === 0) {
2507
- process.stdout.write(`Could not discover projects automatically (${discovery.detail}).\n`);
2508
- const manualProjects = parseCommaList(await ask(rl, `Project slugs for ${label} (comma-separated, leave empty to let app context decide)`, ''));
2509
- projects = manualProjects;
2519
+ if (!verifiedVisibleProjects && !discovery.ok) {
2520
+ process.stdout.write(`Could not verify visible projects automatically (${discovery.detail}). Project scope will be resolved from app context later.\n`);
2510
2521
  }
2511
2522
  }
2512
2523
  }
@@ -2519,7 +2530,6 @@ async function guideSentryConnector(rl, secrets) {
2519
2530
  baseUrl,
2520
2531
  tokenEnv,
2521
2532
  ...(org.trim() ? { org: org.trim() } : {}),
2522
- ...(projects.length > 0 ? { projects } : {}),
2523
2533
  ...(environment.trim() ? { environment: environment.trim() } : {}),
2524
2534
  });
2525
2535
  if (index === 0) {
@@ -2679,6 +2689,144 @@ async function maybeSelfUpdateFromClawHub(args) {
2679
2689
  const code = await rerunCurrentWizardWithoutSelfUpdate();
2680
2690
  process.exit(code ?? 0);
2681
2691
  }
2692
+ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, allowIsolationPrompt = true, }) {
2693
+ clearTerminal();
2694
+ printConnectorIntro();
2695
+ process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
2696
+ for (const key of selected) {
2697
+ process.stdout.write(` - ${connectorLabel(key)}\n`);
2698
+ }
2699
+ process.stdout.write('\n');
2700
+ const secrets = {};
2701
+ let sentryAccounts = [];
2702
+ if (selected.includes('analytics')) {
2703
+ let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
2704
+ while (true) {
2705
+ clearTerminal();
2706
+ await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
2707
+ const check = await runImmediateConnectorHealthCheck({
2708
+ rl,
2709
+ configPath: args.config,
2710
+ connector: 'analytics',
2711
+ secrets,
2712
+ });
2713
+ if (!check.retry)
2714
+ break;
2715
+ forceFreshAnalyticsToken = true;
2716
+ }
2717
+ }
2718
+ if (selected.includes('github')) {
2719
+ while (true) {
2720
+ clearTerminal();
2721
+ await guideGitHubConnector(rl, secrets);
2722
+ const check = await runImmediateConnectorHealthCheck({
2723
+ rl,
2724
+ configPath: args.config,
2725
+ connector: 'github',
2726
+ secrets,
2727
+ });
2728
+ if (!check.retry)
2729
+ break;
2730
+ }
2731
+ }
2732
+ if (selected.includes('revenuecat')) {
2733
+ while (true) {
2734
+ clearTerminal();
2735
+ await guideRevenueCatConnector(rl, secrets);
2736
+ const check = await runImmediateConnectorHealthCheck({
2737
+ rl,
2738
+ configPath: args.config,
2739
+ connector: 'revenuecat',
2740
+ secrets,
2741
+ });
2742
+ if (!check.retry)
2743
+ break;
2744
+ }
2745
+ }
2746
+ if (selected.includes('sentry')) {
2747
+ while (true) {
2748
+ clearTerminal();
2749
+ sentryAccounts = await guideSentryConnector(rl, secrets);
2750
+ const check = await runImmediateConnectorHealthCheck({
2751
+ rl,
2752
+ configPath: args.config,
2753
+ connector: 'sentry',
2754
+ secrets,
2755
+ sentryAccounts,
2756
+ });
2757
+ if (!check.retry)
2758
+ break;
2759
+ }
2760
+ }
2761
+ if (selected.includes('asc')) {
2762
+ while (true) {
2763
+ clearTerminal();
2764
+ await guideAscConnector(rl, secrets);
2765
+ const check = await runImmediateConnectorHealthCheck({
2766
+ rl,
2767
+ configPath: args.config,
2768
+ connector: 'asc',
2769
+ secrets,
2770
+ });
2771
+ if (!check.retry)
2772
+ break;
2773
+ }
2774
+ }
2775
+ const secretsFile = resolveSecretsFile();
2776
+ const wroteSecrets = Object.keys(secrets).length > 0;
2777
+ clearTerminal();
2778
+ if (wroteSecrets) {
2779
+ await writeSecretsFile(secretsFile, secrets);
2780
+ process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
2781
+ }
2782
+ else {
2783
+ process.stdout.write('\nNo new secrets were written.\n');
2784
+ }
2785
+ if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2786
+ process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
2787
+ }
2788
+ const env = {
2789
+ ...process.env,
2790
+ ...secrets,
2791
+ };
2792
+ const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
2793
+ let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
2794
+ let setupPayload = parseJsonFromStdout(setupResult.stdout);
2795
+ if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2796
+ process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
2797
+ }
2798
+ if (selected.includes('asc')) {
2799
+ try {
2800
+ const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
2801
+ if (ascWebAuthChanged) {
2802
+ setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
2803
+ setupPayload = parseJsonFromStdout(setupResult.stdout);
2804
+ }
2805
+ }
2806
+ catch (error) {
2807
+ process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
2808
+ }
2809
+ }
2810
+ if (setupResult.ok && setupPayload?.ok !== false) {
2811
+ printSetupSuccess(setupPayload);
2812
+ if (wroteSecrets) {
2813
+ process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
2814
+ }
2815
+ 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);
2816
+ if (configureIsolation) {
2817
+ const config = await loadEditableConfig(args.config);
2818
+ const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
2819
+ await writeJsonFile(path.resolve(args.config), config);
2820
+ const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
2821
+ process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
2822
+ printSecretRunnerKitInstructions(secretAccess.kit);
2823
+ }
2824
+ return true;
2825
+ }
2826
+ printSetupFailure({ result: setupResult, payload: setupPayload, command });
2827
+ process.exitCode = 1;
2828
+ return false;
2829
+ }
2682
2830
  async function runConnectorSetupWizard(args) {
2683
2831
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
2684
2832
  throw new Error('Connector wizard requires an interactive terminal.');
@@ -2687,151 +2835,18 @@ async function runConnectorSetupWizard(args) {
2687
2835
  try {
2688
2836
  clearTerminal();
2689
2837
  printConnectorIntro();
2838
+ await migrateRuntimeSourceCommandsFile(args.config);
2690
2839
  const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(args.config, onProgress));
2691
2840
  const existingFixes = connectorKeysNeedingAttention(healthByConnector);
2692
2841
  const requestedConnectors = args.connectors ? parseConnectorList(args.connectors) : [];
2693
2842
  const chosenConnectors = requestedConnectors.length > 0
2694
2843
  ? orderConnectors([...new Set([...requestedConnectors, ...existingFixes])])
2695
2844
  : await askConnectorSelectionWithHealth(rl, healthByConnector, existingFixes);
2696
- let selected = withMissingRequiredAnalyticsConnector(chosenConnectors);
2845
+ const selected = withMissingRequiredAnalyticsConnector(chosenConnectors);
2697
2846
  if (selected.length === 0) {
2698
2847
  throw new Error('No supported connectors selected. Use analytics, github, revenuecat, sentry, asc, or all.');
2699
2848
  }
2700
- clearTerminal();
2701
- printConnectorIntro();
2702
- process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
2703
- for (const key of selected) {
2704
- process.stdout.write(` - ${connectorLabel(key)}\n`);
2705
- }
2706
- process.stdout.write('\n');
2707
- const secrets = {};
2708
- let sentryAccounts = [];
2709
- if (selected.includes('analytics')) {
2710
- let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
2711
- while (true) {
2712
- clearTerminal();
2713
- await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
2714
- const check = await runImmediateConnectorHealthCheck({
2715
- rl,
2716
- configPath: args.config,
2717
- connector: 'analytics',
2718
- secrets,
2719
- });
2720
- if (!check.retry)
2721
- break;
2722
- forceFreshAnalyticsToken = true;
2723
- }
2724
- }
2725
- if (selected.includes('github')) {
2726
- while (true) {
2727
- clearTerminal();
2728
- await guideGitHubConnector(rl, secrets);
2729
- const check = await runImmediateConnectorHealthCheck({
2730
- rl,
2731
- configPath: args.config,
2732
- connector: 'github',
2733
- secrets,
2734
- });
2735
- if (!check.retry)
2736
- break;
2737
- }
2738
- }
2739
- if (selected.includes('revenuecat')) {
2740
- while (true) {
2741
- clearTerminal();
2742
- await guideRevenueCatConnector(rl, secrets);
2743
- const check = await runImmediateConnectorHealthCheck({
2744
- rl,
2745
- configPath: args.config,
2746
- connector: 'revenuecat',
2747
- secrets,
2748
- });
2749
- if (!check.retry)
2750
- break;
2751
- }
2752
- }
2753
- if (selected.includes('sentry')) {
2754
- while (true) {
2755
- clearTerminal();
2756
- sentryAccounts = await guideSentryConnector(rl, secrets);
2757
- const check = await runImmediateConnectorHealthCheck({
2758
- rl,
2759
- configPath: args.config,
2760
- connector: 'sentry',
2761
- secrets,
2762
- sentryAccounts,
2763
- });
2764
- if (!check.retry)
2765
- break;
2766
- }
2767
- }
2768
- if (selected.includes('asc')) {
2769
- while (true) {
2770
- clearTerminal();
2771
- await guideAscConnector(rl, secrets);
2772
- const check = await runImmediateConnectorHealthCheck({
2773
- rl,
2774
- configPath: args.config,
2775
- connector: 'asc',
2776
- secrets,
2777
- });
2778
- if (!check.retry)
2779
- break;
2780
- }
2781
- }
2782
- const secretsFile = resolveSecretsFile();
2783
- const wroteSecrets = Object.keys(secrets).length > 0;
2784
- clearTerminal();
2785
- if (wroteSecrets) {
2786
- await writeSecretsFile(secretsFile, secrets);
2787
- process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
2788
- }
2789
- else {
2790
- process.stdout.write('\nNo new secrets were written.\n');
2791
- }
2792
- if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2793
- process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
2794
- }
2795
- const env = {
2796
- ...process.env,
2797
- ...secrets,
2798
- };
2799
- const command = `node scripts/openclaw-growth-start.mjs --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
2800
- let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
2801
- let setupPayload = parseJsonFromStdout(setupResult.stdout);
2802
- if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
2803
- process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
2804
- }
2805
- if (selected.includes('asc')) {
2806
- try {
2807
- const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
2808
- if (ascWebAuthChanged) {
2809
- setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
2810
- setupPayload = parseJsonFromStdout(setupResult.stdout);
2811
- }
2812
- }
2813
- catch (error) {
2814
- process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
2815
- }
2816
- }
2817
- if (setupResult.ok && setupPayload?.ok !== false) {
2818
- printSetupSuccess(setupPayload);
2819
- if (wroteSecrets) {
2820
- process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
2821
- }
2822
- 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);
2823
- if (configureIsolation) {
2824
- const config = await loadEditableConfig(args.config);
2825
- const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
2826
- await writeJsonFile(path.resolve(args.config), config);
2827
- const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
2828
- process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
2829
- printSecretRunnerKitInstructions(secretAccess.kit);
2830
- }
2831
- return;
2832
- }
2833
- printSetupFailure({ result: setupResult, payload: setupPayload, command });
2834
- process.exitCode = 1;
2849
+ await runConnectorSetupSteps({ rl, args, selected, healthByConnector });
2835
2850
  }
2836
2851
  finally {
2837
2852
  rl.close();
@@ -2870,57 +2885,6 @@ async function askYesNo(rl, label, defaultYes = true) {
2870
2885
  }
2871
2886
  }
2872
2887
  }
2873
- async function askChoice(rl, label, options, defaultValue) {
2874
- const normalizedDefault = options.includes(defaultValue) ? defaultValue : options[0];
2875
- while (true) {
2876
- const answer = (await rl.question(`${label} (${options.join('/')}) [${normalizedDefault}]: `))
2877
- .trim()
2878
- .toLowerCase();
2879
- if (!answer) {
2880
- return normalizedDefault;
2881
- }
2882
- if (options.includes(answer)) {
2883
- return answer;
2884
- }
2885
- }
2886
- }
2887
- async function askSourceConfig(rl, sourceName, defaultPath, hint, options = {}) {
2888
- const forceEnabled = Boolean(options.forceEnabled);
2889
- const defaultCommand = String(options.defaultCommand || getDefaultSourceCommand(sourceName) || '').trim();
2890
- const defaultMode = defaultCommand ? 'command' : 'file';
2891
- const defaultEnabled = options.defaultEnabled ?? sourceName === 'analytics';
2892
- const enabled = forceEnabled
2893
- ? true
2894
- : await askYesNo(rl, `Enable source "${sourceName}"?`, defaultEnabled);
2895
- if (!enabled) {
2896
- return {
2897
- enabled: false,
2898
- mode: 'file',
2899
- path: defaultPath,
2900
- hint,
2901
- };
2902
- }
2903
- process.stdout.write(`Where to get ${sourceName} data:\n${hint}\n`);
2904
- const modeInput = await ask(rl, 'Mode (file/command)', defaultMode);
2905
- const mode = modeInput.toLowerCase() === 'command' ? 'command' : 'file';
2906
- const value = await ask(rl, mode === 'file' ? `${sourceName} JSON file path` : `${sourceName} command`, mode === 'file' ? defaultPath : defaultCommand);
2907
- if (mode === 'file') {
2908
- return {
2909
- enabled: true,
2910
- mode,
2911
- path: value,
2912
- hint,
2913
- };
2914
- }
2915
- return {
2916
- enabled: true,
2917
- mode,
2918
- command: value,
2919
- hint,
2920
- ...(options.cursorMode ? { cursorMode: options.cursorMode } : {}),
2921
- ...(options.initialLookback ? { initialLookback: options.initialLookback } : {}),
2922
- };
2923
- }
2924
2888
  function printCadencePlan(cadences) {
2925
2889
  process.stdout.write('\nDefault growth cadence:\n');
2926
2890
  for (const cadence of cadences) {
@@ -2930,16 +2894,28 @@ function printCadencePlan(cadences) {
2930
2894
  process.stdout.write('\n');
2931
2895
  }
2932
2896
  async function askToolUsage(rl) {
2933
- process.stdout.write('\nHow should OpenClaw Growth Engineer use this tool?\n');
2934
- process.stdout.write(' 1) Production autopilot: notify, draft issues/PR handoffs, and analyze on schedule\n');
2935
- process.stdout.write(' 2) Advisory only: analyze and write OpenClaw chat summaries, no GitHub artifacts by default\n');
2936
- process.stdout.write(' 3) Manual reports: mostly one-off runs; keep scheduling conservative\n');
2937
- const answer = await ask(rl, 'Usage mode (1/2/3)', '1');
2938
- if (answer.trim() === '2')
2939
- return 'advisory';
2940
- if (answer.trim() === '3')
2941
- return 'manual_reports';
2942
- return 'production_autopilot';
2897
+ return await askMenuChoice(rl, {
2898
+ title: 'How should OpenClaw Growth Engineer run?',
2899
+ subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
2900
+ defaultValue: 'production_autopilot',
2901
+ options: [
2902
+ {
2903
+ value: 'production_autopilot',
2904
+ label: 'Production autopilot',
2905
+ detail: 'Notify, draft issues/PR handoffs, and analyze on schedule.',
2906
+ },
2907
+ {
2908
+ value: 'advisory',
2909
+ label: 'Advisory only',
2910
+ detail: 'Analyze and write OpenClaw chat summaries; no GitHub artifacts by default.',
2911
+ },
2912
+ {
2913
+ value: 'manual_reports',
2914
+ label: 'Manual reports',
2915
+ detail: 'Mostly one-off runs with conservative scheduling.',
2916
+ },
2917
+ ],
2918
+ });
2943
2919
  }
2944
2920
  async function askCadencePlan(rl) {
2945
2921
  const cadences = DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence }));
@@ -2964,27 +2940,45 @@ async function askCadencePlan(rl) {
2964
2940
  return cadences;
2965
2941
  }
2966
2942
  async function askWizardGoal(rl) {
2967
- process.stdout.write('\nWhat do you want to configure?\n');
2968
- process.stdout.write(' 1) Full setup: project, schedule, outputs, and sources\n');
2969
- process.stdout.write(' 2) Connectors: credentials and provider health checks\n');
2970
- process.stdout.write(' 3) Intervals: growth cadence and connector health check interval\n');
2971
- process.stdout.write(' 4) Output: summary, GitHub issues, draft PRs, and notifications\n');
2972
- const answer = await ask(rl, 'Setup area (1/2/3/4)', '1');
2973
- if (answer.trim() === '2')
2974
- return 'connectors';
2975
- if (answer.trim() === '3')
2976
- return 'intervals';
2977
- if (answer.trim() === '4')
2978
- return 'output';
2979
- return 'full';
2943
+ return await askMenuChoice(rl, {
2944
+ title: 'What do you want to configure?',
2945
+ subtitle: 'Use Up/Down to move, Enter to continue, or press 1-4.',
2946
+ defaultValue: 'full',
2947
+ renderHeader: printWizardHeader,
2948
+ options: [
2949
+ {
2950
+ value: 'connectors',
2951
+ label: 'Connectors',
2952
+ detail: 'Credentials, provider setup, and health checks.',
2953
+ },
2954
+ {
2955
+ value: 'outputs_intervals',
2956
+ label: 'Outputs and intervals',
2957
+ detail: 'Daily/weekly/monthly jobs, GitHub issue/PR delivery, and OpenClaw chat notifications.',
2958
+ },
2959
+ {
2960
+ value: 'full',
2961
+ label: 'Full setup',
2962
+ detail: 'Project, connectors, outputs, intervals, and sources.',
2963
+ },
2964
+ {
2965
+ value: 'intervals',
2966
+ label: 'Advanced intervals only',
2967
+ detail: 'Runner wake-up interval and connector health check cadence.',
2968
+ },
2969
+ ],
2970
+ });
2971
+ }
2972
+ function printWizardHeader() {
2973
+ process.stdout.write('OpenClaw Growth Engineer - Setup Wizard\n');
2974
+ process.stdout.write('This wizard can configure connector secrets. Normal config is written to config JSON; API keys stay in the local chmod 600 secrets file.\n\n');
2980
2975
  }
2981
2976
  async function buildDefaultWizardConfig() {
2982
- const detectedRepo = await detectGitHubRepo();
2983
2977
  return {
2984
2978
  version: 7,
2985
2979
  generatedAt: new Date().toISOString(),
2986
2980
  project: {
2987
- githubRepo: detectedRepo || '',
2981
+ githubRepo: '',
2988
2982
  repoRoot: '.',
2989
2983
  outFile: 'data/openclaw-growth-engineer/issues.generated.json',
2990
2984
  maxIssues: 4,
@@ -2995,17 +2989,17 @@ async function buildDefaultWizardConfig() {
2995
2989
  analytics: {
2996
2990
  enabled: true,
2997
2991
  mode: 'command',
2998
- command: getDefaultSourceCommand('analytics'),
2992
+ command: getWizardDefaultSourceCommand('analytics'),
2999
2993
  },
3000
2994
  revenuecat: {
3001
2995
  enabled: false,
3002
2996
  mode: 'command',
3003
- command: getDefaultSourceCommand('revenuecat'),
2997
+ command: getWizardDefaultSourceCommand('revenuecat'),
3004
2998
  },
3005
2999
  sentry: {
3006
- enabled: false,
3000
+ enabled: true,
3007
3001
  mode: 'command',
3008
- command: getDefaultSourceCommand('sentry'),
3002
+ command: getWizardDefaultSourceCommand('sentry'),
3009
3003
  },
3010
3004
  feedback: {
3011
3005
  enabled: true,
@@ -3015,12 +3009,12 @@ async function buildDefaultWizardConfig() {
3015
3009
  initialLookback: '30d',
3016
3010
  },
3017
3011
  extra: [
3018
- buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getDefaultSourceCommand('asc') }),
3012
+ buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
3019
3013
  ],
3020
3014
  },
3021
3015
  schedule: {
3022
- intervalMinutes: 1440,
3023
- connectorHealthCheckIntervalMinutes: 720,
3016
+ intervalMinutes: DEFAULT_GROWTH_INTERVAL_MINUTES,
3017
+ connectorHealthCheckIntervalMinutes: DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES,
3024
3018
  skipIfNoDataChange: true,
3025
3019
  skipIfIssueSetUnchanged: true,
3026
3020
  cadences: DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence })),
@@ -3028,6 +3022,8 @@ async function buildDefaultWizardConfig() {
3028
3022
  actions: {
3029
3023
  autoCreateIssues: false,
3030
3024
  autoCreatePullRequests: false,
3025
+ autoCreateWhenGitHubWriteAccess: true,
3026
+ disableAutoCreateGitHubArtifacts: false,
3031
3027
  mode: 'issue',
3032
3028
  usageMode: 'production_autopilot',
3033
3029
  draftPullRequests: true,
@@ -3101,10 +3097,127 @@ async function buildDefaultWizardConfig() {
3101
3097
  },
3102
3098
  };
3103
3099
  }
3100
+ function buildRecommendedSourceConfig() {
3101
+ return {
3102
+ analytics: {
3103
+ enabled: true,
3104
+ mode: 'command',
3105
+ command: getWizardDefaultSourceCommand('analytics'),
3106
+ },
3107
+ revenuecat: {
3108
+ enabled: false,
3109
+ mode: 'command',
3110
+ command: getWizardDefaultSourceCommand('revenuecat'),
3111
+ },
3112
+ sentry: {
3113
+ enabled: true,
3114
+ mode: 'command',
3115
+ command: getWizardDefaultSourceCommand('sentry'),
3116
+ },
3117
+ feedback: {
3118
+ enabled: true,
3119
+ mode: 'command',
3120
+ command: getDefaultSourceCommand('feedback'),
3121
+ cursorMode: 'auto_since_last_fetch',
3122
+ initialLookback: '30d',
3123
+ },
3124
+ extra: [
3125
+ buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
3126
+ ],
3127
+ };
3128
+ }
3129
+ function getInputChannelInitialSelection(config) {
3130
+ const sources = config?.sources || {};
3131
+ const extraSources = Array.isArray(sources.extra) ? sources.extra : [];
3132
+ const selected = new Set();
3133
+ const hasExplicitSources = Boolean(config?.sources);
3134
+ if (!hasExplicitSources || sources.analytics?.enabled !== false)
3135
+ selected.add('analytics');
3136
+ if (sources.revenuecat?.enabled === true || isConnectorLocallyConfigured('revenuecat'))
3137
+ selected.add('revenuecat');
3138
+ if (!hasExplicitSources || sources.sentry?.enabled !== false)
3139
+ selected.add('sentry');
3140
+ if (extraSources.some((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()) &&
3141
+ source?.enabled !== false) ||
3142
+ isConnectorLocallyConfigured('asc')) {
3143
+ selected.add('asc');
3144
+ }
3145
+ if (config?.deliveries?.github?.enabled ||
3146
+ config?.actions?.autoCreateIssues ||
3147
+ config?.actions?.autoCreatePullRequests ||
3148
+ isConnectorLocallyConfigured('github')) {
3149
+ selected.add('github');
3150
+ }
3151
+ return orderConnectors([...selected]);
3152
+ }
3153
+ function buildSourceConfigFromInputChannels(selectedConnectors, existingSources = {}) {
3154
+ const selected = new Set(selectedConnectors);
3155
+ const recommended = buildRecommendedSourceConfig();
3156
+ const migratedSources = migrateRuntimeSourceCommands({ sources: existingSources }).sources || {};
3157
+ const existingExtra = Array.isArray(migratedSources.extra) ? migratedSources.extra : [];
3158
+ const ascSource = existingExtra.find((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()));
3159
+ const nonAscExtra = existingExtra.filter((source) => source !== ascSource);
3160
+ return {
3161
+ ...recommended,
3162
+ ...migratedSources,
3163
+ analytics: {
3164
+ ...recommended.analytics,
3165
+ ...(migratedSources.analytics || {}),
3166
+ command: normalizeWizardSourceCommand('analytics', {
3167
+ ...recommended.analytics,
3168
+ ...(migratedSources.analytics || {}),
3169
+ }),
3170
+ enabled: selected.has('analytics'),
3171
+ },
3172
+ revenuecat: {
3173
+ ...recommended.revenuecat,
3174
+ ...(migratedSources.revenuecat || {}),
3175
+ command: normalizeWizardSourceCommand('revenuecat', {
3176
+ ...recommended.revenuecat,
3177
+ ...(migratedSources.revenuecat || {}),
3178
+ }),
3179
+ enabled: selected.has('revenuecat'),
3180
+ },
3181
+ sentry: {
3182
+ ...recommended.sentry,
3183
+ ...(migratedSources.sentry || {}),
3184
+ command: normalizeWizardSourceCommand('sentry', {
3185
+ ...recommended.sentry,
3186
+ ...(migratedSources.sentry || {}),
3187
+ }),
3188
+ enabled: selected.has('sentry'),
3189
+ },
3190
+ feedback: {
3191
+ ...recommended.feedback,
3192
+ ...(migratedSources.feedback || {}),
3193
+ enabled: selected.has('analytics'),
3194
+ },
3195
+ extra: [
3196
+ ...nonAscExtra,
3197
+ {
3198
+ ...buildExtraSourceConfig('asc-cli', {
3199
+ enabled: selected.has('asc'),
3200
+ mode: 'command',
3201
+ command: getWizardDefaultSourceCommand('asc'),
3202
+ }),
3203
+ ...(ascSource || {}),
3204
+ command: normalizeWizardSourceCommand('asc', {
3205
+ ...buildExtraSourceConfig('asc-cli', {
3206
+ enabled: selected.has('asc'),
3207
+ mode: 'command',
3208
+ command: getWizardDefaultSourceCommand('asc'),
3209
+ }),
3210
+ ...(ascSource || {}),
3211
+ }),
3212
+ enabled: selected.has('asc'),
3213
+ },
3214
+ ],
3215
+ };
3216
+ }
3104
3217
  async function loadEditableConfig(configPath) {
3105
3218
  const existing = await readJsonIfPresent(configPath).catch(() => null);
3106
3219
  if (existing && typeof existing === 'object')
3107
- return existing;
3220
+ return migrateRuntimeSourceCommands(existing);
3108
3221
  return await buildDefaultWizardConfig();
3109
3222
  }
3110
3223
  function mergeNotificationChannels(baseChannels, extraChannels) {
@@ -3148,28 +3261,39 @@ async function askNotificationChannels(rl, config) {
3148
3261
  return channels;
3149
3262
  }
3150
3263
  async function askOutputConfig(rl, config) {
3151
- process.stdout.write('\nOutput type\n');
3152
- process.stdout.write(' 1) Summary only: OpenClaw chat handoff and notifications\n');
3153
- process.stdout.write(' 2) GitHub issue drafts: generate issue-ready handoffs, no auto-create by default\n');
3154
- process.stdout.write(' 3) GitHub pull request drafts: generate PR-oriented proposal branches when enabled\n');
3264
+ printSection('Outputs and notifications', [
3265
+ 'OpenClaw chat is always enabled so the agent has a readable handoff.',
3266
+ 'GitHub issues or draft PRs are optional and only run when a token plus an inferred repo are available.',
3267
+ ]);
3155
3268
  const currentMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
3156
3269
  const currentAutoCreate = Boolean(config?.actions?.autoCreateIssues || config?.actions?.autoCreatePullRequests || config?.deliveries?.github?.autoCreate);
3157
- const defaultChoice = currentAutoCreate ? (currentMode === 'pull_request' ? '3' : '2') : '1';
3158
- const outputChoice = await ask(rl, 'Output type (1/2/3)', defaultChoice);
3159
- const summaryOnly = outputChoice.trim() === '1';
3160
- const mode = outputChoice.trim() === '3' ? 'pull_request' : 'issue';
3161
- const autoCreate = summaryOnly
3162
- ? false
3163
- : await askYesNo(rl, mode === 'pull_request'
3164
- ? 'Automatically create draft pull requests when new findings are found?'
3165
- : 'Automatically create GitHub issues when new findings are found?', currentAutoCreate);
3270
+ const outputChoice = await askMenuChoice(rl, {
3271
+ title: 'Output mode',
3272
+ subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
3273
+ defaultValue: currentAutoCreate ? (currentMode === 'pull_request' ? 'pull_request' : 'issue') : 'chat',
3274
+ options: [
3275
+ {
3276
+ value: 'chat',
3277
+ label: 'OpenClaw chat',
3278
+ detail: 'Write readable summaries and leave GitHub as runtime fallback.',
3279
+ },
3280
+ {
3281
+ value: 'issue',
3282
+ label: 'GitHub issues',
3283
+ detail: 'Auto-create issues for concrete findings when GitHub access allows it.',
3284
+ },
3285
+ {
3286
+ value: 'pull_request',
3287
+ label: 'Draft PR proposals',
3288
+ detail: 'Auto-create draft PR-oriented proposal branches for implementation-ready fixes.',
3289
+ },
3290
+ ],
3291
+ });
3292
+ const summaryOnly = outputChoice === 'chat';
3293
+ const mode = outputChoice === 'pull_request' ? 'pull_request' : 'issue';
3294
+ const autoCreate = !summaryOnly;
3166
3295
  if (!summaryOnly) {
3167
- const detectedRepo = await detectGitHubRepo();
3168
- const currentRepo = config?.project?.githubRepo || detectedRepo || '';
3169
- config.project = {
3170
- ...(config.project || {}),
3171
- githubRepo: await ask(rl, 'GitHub repo for issue/PR delivery (owner/name)', currentRepo),
3172
- };
3296
+ 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');
3173
3297
  }
3174
3298
  const channels = await askNotificationChannels(rl, config);
3175
3299
  const connectorHealthChannels = channels.map((channel) => {
@@ -3186,6 +3310,8 @@ async function askOutputConfig(rl, config) {
3186
3310
  mode,
3187
3311
  autoCreateIssues: mode === 'issue' && autoCreate,
3188
3312
  autoCreatePullRequests: mode === 'pull_request' && autoCreate,
3313
+ autoCreateWhenGitHubWriteAccess: config.actions?.autoCreateWhenGitHubWriteAccess !== false,
3314
+ disableAutoCreateGitHubArtifacts: config.actions?.disableAutoCreateGitHubArtifacts === true,
3189
3315
  draftPullRequests: true,
3190
3316
  proposalBranchPrefix: config?.actions?.proposalBranchPrefix || 'openclaw/proposals',
3191
3317
  };
@@ -3238,11 +3364,61 @@ async function askOutputConfig(rl, config) {
3238
3364
  };
3239
3365
  return config;
3240
3366
  }
3367
+ async function askGitHubArtifactDetails(rl, config) {
3368
+ const githubEnabled = Boolean(config?.actions?.autoCreateIssues ||
3369
+ config?.actions?.autoCreatePullRequests ||
3370
+ config?.deliveries?.github?.enabled ||
3371
+ config?.deliveries?.github?.autoCreate);
3372
+ config.project = {
3373
+ ...(config.project || {}),
3374
+ githubRepo: '',
3375
+ repoRoot: config.project?.repoRoot || '.',
3376
+ outFile: config.project?.outFile || 'data/openclaw-growth-engineer/issues.generated.json',
3377
+ maxIssues: Number(config.project?.maxIssues || 4),
3378
+ titlePrefix: config.project?.titlePrefix || '[Growth]',
3379
+ labels: Array.isArray(config.project?.labels) && config.project.labels.length > 0
3380
+ ? config.project.labels
3381
+ : ['ai-growth', 'autogenerated', 'product'],
3382
+ };
3383
+ if (!githubEnabled) {
3384
+ return config;
3385
+ }
3386
+ process.stdout.write('\nGitHub repo scope is not pinned by the wizard. OpenClaw/Hermes infers it from OPENCLAW_GITHUB_REPO, the local git remote, or runtime context.\n');
3387
+ const customize = await askYesNo(rl, 'Customize GitHub issue/PR limits, labels, or chart attachment settings?', false);
3388
+ if (!customize) {
3389
+ config.charting = {
3390
+ ...(config.charting || {}),
3391
+ enabled: config.charting?.enabled === true,
3392
+ command: config.charting?.command || null,
3393
+ };
3394
+ return config;
3395
+ }
3396
+ const labelsRaw = await ask(rl, 'GitHub labels for created issues/PRs', config.project.labels.join(','));
3397
+ config.project.labels = labelsRaw
3398
+ .split(',')
3399
+ .map((value) => value.trim())
3400
+ .filter(Boolean);
3401
+ config.project.maxIssues = Number.parseInt(await ask(rl, 'Maximum GitHub artifacts per run', String(config.project.maxIssues || 4)), 10) || 4;
3402
+ config.project.titlePrefix = await ask(rl, 'GitHub artifact title prefix', config.project.titlePrefix || '[Growth]');
3403
+ const enableCharting = await askYesNo(rl, 'Attach generated charts to GitHub artifacts when useful?', config.charting?.enabled === true);
3404
+ config.charting = {
3405
+ ...(config.charting || {}),
3406
+ enabled: enableCharting,
3407
+ command: enableCharting
3408
+ ? await ask(rl, 'Optional chart command override', config.charting?.command || '')
3409
+ : null,
3410
+ };
3411
+ return config;
3412
+ }
3241
3413
  async function askIntervalConfig(rl, config) {
3414
+ printSection('Schedule and analysis depth', [
3415
+ 'The runner wakes up often, but larger reviews only run on their daily/weekly/monthly cadence.',
3416
+ 'Connector health checks are separate and default to every 6 hours.',
3417
+ ]);
3242
3418
  const currentSchedule = config?.schedule || {};
3243
- const intervalMinutes = Number.parseInt(await ask(rl, 'Growth runner wake-up interval in minutes', String(currentSchedule.intervalMinutes || 1440)), 10) || 1440;
3244
- const connectorHealthCheckIntervalMinutes = Number.parseInt(await ask(rl, 'Connector health check interval in minutes', String(currentSchedule.connectorHealthCheckIntervalMinutes || 720)), 10) || 720;
3245
3419
  const usageMode = await askToolUsage(rl);
3420
+ const intervalMinutes = Number.parseInt(await ask(rl, 'Growth runner wake-up interval in minutes', String(currentSchedule.intervalMinutes || DEFAULT_GROWTH_INTERVAL_MINUTES)), 10) || DEFAULT_GROWTH_INTERVAL_MINUTES;
3421
+ const connectorHealthCheckIntervalMinutes = Number.parseInt(await ask(rl, 'Connector health check interval in minutes', String(currentSchedule.connectorHealthCheckIntervalMinutes || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES)), 10) || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES;
3246
3422
  const cadences = await askCadencePlan(rl);
3247
3423
  config.schedule = {
3248
3424
  ...currentSchedule,
@@ -3258,11 +3434,31 @@ async function askIntervalConfig(rl, config) {
3258
3434
  };
3259
3435
  return config;
3260
3436
  }
3437
+ async function askOutputsAndIntervalsConfig(rl, config) {
3438
+ const withIntervals = await askIntervalConfig(rl, config);
3439
+ const withOutput = await askOutputConfig(rl, withIntervals);
3440
+ return await askGitHubArtifactDetails(rl, withOutput);
3441
+ }
3442
+ async function askInputSourceConfig(rl, config, configPath) {
3443
+ config = migrateRuntimeSourceCommands(config);
3444
+ await ensureDirForFile(configPath);
3445
+ await writeJsonFile(configPath, config);
3446
+ const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(configPath, onProgress));
3447
+ const selected = await askConnectorSelectionWithHealth(rl, healthByConnector, getInputChannelInitialSelection(config), {
3448
+ introTitle: 'Input channels',
3449
+ introDetail: null,
3450
+ actionTitle: 'Select input channels',
3451
+ helpText: 'Use Up/Down to move, Space to toggle channels, A to toggle all channels, Enter to continue.',
3452
+ mode: 'input',
3453
+ });
3454
+ config.sources = buildSourceConfigFromInputChannels(selected, config.sources || {});
3455
+ return { config, selected, healthByConnector };
3456
+ }
3261
3457
  async function writeOpenClawJobManifest(configPath, config) {
3262
3458
  const manifestPath = path.resolve('.openclaw/jobs/openclaw-growth-engineer.json');
3263
3459
  const displayConfigPath = path.relative(process.cwd(), configPath) || configPath;
3264
- const intervalMinutes = Math.max(1, Number(config?.schedule?.intervalMinutes || 1440));
3265
- const connectorHealthCheckIntervalMinutes = Math.max(1, Number(config?.schedule?.connectorHealthCheckIntervalMinutes || 720));
3460
+ const intervalMinutes = Math.max(1, Number(config?.schedule?.intervalMinutes || DEFAULT_GROWTH_INTERVAL_MINUTES));
3461
+ const connectorHealthCheckIntervalMinutes = Math.max(1, Number(config?.schedule?.connectorHealthCheckIntervalMinutes || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES));
3266
3462
  const actionMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
3267
3463
  const growthRunCommand = getGrowthRunCommand(config, displayConfigPath);
3268
3464
  const connectorHealthCommand = getConnectorHealthCommand(config, displayConfigPath);
@@ -3317,8 +3513,7 @@ async function main() {
3317
3513
  }
3318
3514
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3319
3515
  try {
3320
- process.stdout.write('OpenClaw Growth Engineer - Setup Wizard\n');
3321
- process.stdout.write('This wizard writes non-secret config only. Connector secrets stay in the local secrets file.\n\n');
3516
+ printWizardHeader();
3322
3517
  const goal = await askWizardGoal(rl);
3323
3518
  if (goal === 'connectors') {
3324
3519
  rl.close();
@@ -3336,160 +3531,40 @@ async function main() {
3336
3531
  process.stdout.write('OpenClaw can run and update growth jobs plus non-secret connector config from the manifest; connector API keys stay behind the connector wizard.\n');
3337
3532
  return;
3338
3533
  }
3339
- if (goal === 'output') {
3340
- const config = await askOutputConfig(rl, await loadEditableConfig(configPath));
3534
+ if (goal === 'outputs_intervals') {
3535
+ const config = await askOutputsAndIntervalsConfig(rl, await loadEditableConfig(configPath));
3341
3536
  const secretAccess = await askSecretAccessModel(rl, configPath, config);
3342
3537
  await writeJsonFile(configPath, config);
3343
3538
  const manifestPath = await writeOpenClawJobManifest(configPath, config);
3344
- process.stdout.write(`\nSaved output config: ${configPath}\n`);
3539
+ process.stdout.write(`\nSaved output and interval config: ${configPath}\n`);
3345
3540
  process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
3346
3541
  printSecretRunnerKitInstructions(secretAccess.kit);
3347
- process.stdout.write('Connector-health alerts are deduped per unhealthy incident and sent through configured channels.\n');
3542
+ process.stdout.write('Daily checks prioritize Sentry and production anomalies; larger cadences analyze all configured projects and connectors.\n');
3348
3543
  return;
3349
3544
  }
3350
- const detectedRepo = await detectGitHubRepo();
3351
- const githubRepo = await ask(rl, 'GitHub repo (owner/name, optional; leave empty to infer later)', detectedRepo || '');
3352
- const labelsRaw = await ask(rl, 'Issue labels (comma-separated)', 'ai-growth,autogenerated,product');
3353
- const labels = labelsRaw
3354
- .split(',')
3355
- .map((value) => value.trim())
3356
- .filter(Boolean);
3357
- const maxIssues = Number.parseInt(await ask(rl, 'Max issues per run', '4'), 10) || 4;
3358
- const intervalMinutes = Number.parseInt(await ask(rl, 'Check interval in minutes', '1440'), 10) || 1440;
3359
- const usageMode = await askToolUsage(rl);
3360
- const cadences = await askCadencePlan(rl);
3361
- const actionMode = await askChoice(rl, 'Preferred GitHub artifact mode', ['issue', 'pull_request'], 'issue');
3362
- const analytics = await askSourceConfig(rl, 'analytics', 'data/openclaw-growth-engineer/analytics_summary.example.json', getDefaultSourceHint('analytics'), {
3363
- forceEnabled: true,
3364
- defaultCommand: getDefaultSourceCommand('analytics'),
3365
- });
3366
- const revenuecat = await askSourceConfig(rl, 'revenuecat', 'data/openclaw-growth-engineer/revenuecat_summary.example.json', getDefaultSourceHint('revenuecat'));
3367
- const sentry = await askSourceConfig(rl, 'sentry', 'data/openclaw-growth-engineer/sentry_summary.example.json', getDefaultSourceHint('sentry'));
3368
- const feedback = await askSourceConfig(rl, 'feedback', 'data/openclaw-growth-engineer/feedback_summary.example.json', getDefaultSourceHint('feedback'), {
3369
- defaultEnabled: true,
3370
- defaultCommand: getDefaultSourceCommand('feedback'),
3371
- cursorMode: 'auto_since_last_fetch',
3372
- initialLookback: '30d',
3373
- });
3374
- const extraSourcesRaw = await ask(rl, 'Extra connectors (comma-separated, e.g. firebase-crashlytics,app-store-reviews,play-console)', '');
3375
- const extraSources = extraSourcesRaw
3376
- .split(',')
3377
- .map((value) => value.trim())
3378
- .filter(Boolean)
3379
- .map((service) => {
3380
- const defaultCommand = getDefaultSourceCommand(service);
3381
- return buildExtraSourceConfig(service, defaultCommand ? {} : { mode: 'file', path: getDefaultSourcePath(service) });
3545
+ let config = await loadEditableConfig(configPath);
3546
+ config.version = Number(config.version || 7);
3547
+ config.generatedAt = new Date().toISOString();
3548
+ const inputSetup = await askInputSourceConfig(rl, config, configPath);
3549
+ config = inputSetup.config;
3550
+ await ensureDirForFile(configPath);
3551
+ await writeJsonFile(configPath, config);
3552
+ const connectorsOk = await runConnectorSetupSteps({
3553
+ rl,
3554
+ args: { ...args, config: configPath },
3555
+ selected: inputSetup.selected,
3556
+ healthByConnector: inputSetup.healthByConnector,
3557
+ allowIsolationPrompt: false,
3382
3558
  });
3383
- const autoCreateIssues = actionMode === 'issue'
3384
- ? await askYesNo(rl, 'Create GitHub issues automatically when new ideas are found?', false)
3385
- : false;
3386
- const autoCreatePullRequests = actionMode === 'pull_request'
3387
- ? await askYesNo(rl, 'Create draft pull requests with implementation proposal files automatically?', false)
3388
- : false;
3389
- const enableCharting = await askYesNo(rl, 'Generate matplotlib charts from analytics signals and include them in generated GitHub artifacts?', false);
3390
- const chartCommand = enableCharting
3391
- ? await ask(rl, 'Optional chart command override (leave empty for default python script)', '')
3392
- : '';
3393
- const config = {
3394
- version: 1,
3395
- generatedAt: new Date().toISOString(),
3396
- project: {
3397
- githubRepo,
3398
- repoRoot: '.',
3399
- outFile: 'data/openclaw-growth-engineer/issues.generated.json',
3400
- maxIssues,
3401
- titlePrefix: '[Growth]',
3402
- labels,
3403
- },
3404
- sources: {
3405
- analytics,
3406
- revenuecat,
3407
- sentry,
3408
- feedback,
3409
- extra: extraSources,
3410
- },
3411
- schedule: {
3412
- intervalMinutes,
3413
- connectorHealthCheckIntervalMinutes: 720,
3414
- skipIfNoDataChange: true,
3415
- skipIfIssueSetUnchanged: true,
3416
- cadences,
3417
- },
3418
- actions: {
3419
- autoCreateIssues,
3420
- autoCreatePullRequests,
3421
- mode: actionMode,
3422
- usageMode,
3423
- draftPullRequests: true,
3424
- proposalBranchPrefix: 'openclaw/proposals',
3425
- },
3426
- deliveries: {
3427
- openclawChat: {
3428
- enabled: true,
3429
- markdownPath: '.openclaw/chat/latest.md',
3430
- jsonPath: '.openclaw/chat/latest.json',
3431
- },
3432
- github: {
3433
- enabled: autoCreateIssues || autoCreatePullRequests,
3434
- mode: actionMode,
3435
- autoCreate: autoCreateIssues || autoCreatePullRequests,
3436
- draftPullRequests: true,
3437
- proposalBranchPrefix: 'openclaw/proposals',
3438
- },
3439
- slack: {
3440
- enabled: false,
3441
- webhookEnv: 'SLACK_WEBHOOK_URL',
3442
- },
3443
- webhook: {
3444
- enabled: false,
3445
- urlEnv: 'OPENCLAW_WEBHOOK_URL',
3446
- method: 'POST',
3447
- headers: {},
3448
- },
3449
- discord: {
3450
- enabled: false,
3451
- command: 'node scripts/discord-openclaw-bridge.mjs send --stdin',
3452
- },
3453
- },
3454
- charting: {
3455
- enabled: enableCharting,
3456
- command: chartCommand || null,
3457
- },
3458
- notifications: {
3459
- connectorHealth: {
3460
- enabled: true,
3461
- channels: [
3462
- {
3463
- type: 'openclaw-chat',
3464
- enabled: true,
3465
- markdownPath: '.openclaw/chat/connector-health.md',
3466
- jsonPath: '.openclaw/chat/connector-health.json',
3467
- },
3468
- ],
3469
- },
3470
- growthRun: {
3471
- enabled: true,
3472
- channels: [
3473
- {
3474
- type: 'openclaw-chat',
3475
- enabled: true,
3476
- markdownPath: '.openclaw/chat/growth-summary.md',
3477
- jsonPath: '.openclaw/chat/growth-summary.json',
3478
- },
3479
- ],
3480
- },
3481
- },
3482
- secrets: {
3483
- githubTokenEnv: 'GITHUB_TOKEN',
3484
- githubTokenRef: { source: 'env', provider: 'default', id: 'GITHUB_TOKEN' },
3485
- analyticsTokenEnv: 'ANALYTICSCLI_ACCESS_TOKEN',
3486
- analyticsTokenRef: { source: 'env', provider: 'default', id: 'ANALYTICSCLI_ACCESS_TOKEN' },
3487
- revenuecatTokenEnv: 'REVENUECAT_API_KEY',
3488
- revenuecatTokenRef: { source: 'env', provider: 'default', id: 'REVENUECAT_API_KEY' },
3489
- sentryTokenEnv: 'SENTRY_AUTH_TOKEN',
3490
- sentryTokenRef: { source: 'env', provider: 'default', id: 'SENTRY_AUTH_TOKEN' },
3491
- },
3492
- };
3559
+ if (!connectorsOk) {
3560
+ return;
3561
+ }
3562
+ config = await loadEditableConfig(configPath);
3563
+ config.version = Number(config.version || 7);
3564
+ config.generatedAt = new Date().toISOString();
3565
+ config = await askIntervalConfig(rl, config);
3566
+ config = await askOutputConfig(rl, config);
3567
+ config = await askGitHubArtifactDetails(rl, config);
3493
3568
  const secretAccess = await askSecretAccessModel(rl, configPath, config);
3494
3569
  await ensureDirForFile(configPath);
3495
3570
  await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
@@ -3499,14 +3574,8 @@ async function main() {
3499
3574
  printSecretRunnerKitInstructions(secretAccess.kit);
3500
3575
  process.stdout.write('\nNext steps:\n');
3501
3576
  process.stdout.write(`1) Set secrets in OpenClaw secret store (env var names in config.secrets)\n`);
3502
- if (extraSources.length > 0) {
3503
- process.stdout.write(`2) Fill each extra connector under \`sources.extra[]\` with the final file path or command and optional \`secretEnv\`\n`);
3504
- process.stdout.write(`3) Run once: node scripts/openclaw-growth-runner.mjs --config ${configPath}\n`);
3505
- process.stdout.write(`4) Run interval loop: node scripts/openclaw-growth-runner.mjs --config ${configPath} --loop\n`);
3506
- return;
3507
- }
3508
- process.stdout.write(`2) Run once: node scripts/openclaw-growth-runner.mjs --config ${configPath}\n`);
3509
- process.stdout.write(`3) Run interval loop: node scripts/openclaw-growth-runner.mjs --config ${configPath} --loop\n`);
3577
+ process.stdout.write(`2) Run once: ${growthEngineerPackageCommand(`run --config ${quote(configPath)}`)}\n`);
3578
+ process.stdout.write(`3) Run interval loop: ${growthEngineerPackageCommand(`run --config ${quote(configPath)} --loop`)}\n`);
3510
3579
  }
3511
3580
  finally {
3512
3581
  rl.close();
@@ -3514,6 +3583,6 @@ async function main() {
3514
3583
  }
3515
3584
  main().catch((error) => {
3516
3585
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
3517
- process.exitCode = 1;
3586
+ process.exitCode = error instanceof WizardAbortError ? error.exitCode : 1;
3518
3587
  });
3519
3588
  //# sourceMappingURL=openclaw-growth-wizard.mjs.map