@analyticscli/growth-engineer 0.1.0-preview.10 → 0.1.0-preview.12

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.
@@ -462,6 +462,170 @@ async function askMenuChoice(rl, { title, subtitle = 'Use Up/Down to move, Enter
462
462
  }
463
463
  }
464
464
  }
465
+ async function askMultiChoice(rl, { title, subtitle = 'Use Up/Down to move, Space to toggle, Enter to continue.', options, defaultValues, requiredValues = [], minSelections = 1, renderHeader, }) {
466
+ const required = new Set(requiredValues);
467
+ const normalizeSelection = (values) => {
468
+ const selected = new Set(values);
469
+ requiredValues.forEach((value) => selected.add(value));
470
+ return options.map((option) => option.value).filter((value) => selected.has(value));
471
+ };
472
+ if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
473
+ process.stdout.write(`\n${title}\n`);
474
+ options.forEach((option, index) => {
475
+ const checked = defaultValues.includes(option.value) || required.has(option.value) ? 'x' : ' ';
476
+ const requiredLabel = required.has(option.value) ? ' required' : '';
477
+ process.stdout.write(` ${index + 1}) [${checked}] ${option.label}${requiredLabel}: ${option.detail}\n`);
478
+ });
479
+ const answer = await ask(rl, `Select one or more (comma-separated 1-${options.length})`, normalizeSelection(defaultValues).map((value) => String(options.findIndex((option) => option.value === value) + 1)).join(','));
480
+ const selected = answer
481
+ .split(',')
482
+ .map((value) => Number.parseInt(value.trim(), 10) - 1)
483
+ .filter((index) => options[index])
484
+ .map((index) => options[index].value);
485
+ const normalized = normalizeSelection(selected);
486
+ return normalized.length >= minSelections ? normalized : normalizeSelection(defaultValues);
487
+ }
488
+ rl.pause();
489
+ let completed = false;
490
+ try {
491
+ const selected = await askMultiChoiceByKeys({
492
+ title,
493
+ subtitle,
494
+ options,
495
+ defaultValues: normalizeSelection(defaultValues),
496
+ requiredValues,
497
+ minSelections,
498
+ renderHeader,
499
+ });
500
+ completed = true;
501
+ return selected;
502
+ }
503
+ finally {
504
+ if (completed) {
505
+ rl.resume();
506
+ }
507
+ else {
508
+ process.stdin.pause();
509
+ }
510
+ }
511
+ }
512
+ async function askMultiChoiceByKeys({ title, subtitle, options, defaultValues, requiredValues, minSelections, renderHeader, }) {
513
+ emitKeypressEvents(process.stdin);
514
+ const wasRaw = process.stdin.isRaw;
515
+ const wasPaused = process.stdin.isPaused();
516
+ process.stdin.setRawMode(true);
517
+ process.stdin.resume();
518
+ const required = new Set(requiredValues);
519
+ const selected = new Set(defaultValues);
520
+ requiredValues.forEach((value) => selected.add(value));
521
+ let cursorIndex = 0;
522
+ let warning = '';
523
+ return await new Promise((resolve, reject) => {
524
+ const cleanup = () => {
525
+ process.stdin.off('keypress', onKeypress);
526
+ process.stdin.setRawMode(Boolean(wasRaw));
527
+ if (wasPaused) {
528
+ process.stdin.pause();
529
+ }
530
+ process.stdout.write(ANSI.showCursor);
531
+ };
532
+ const selectedValues = () => options.map((option) => option.value).filter((value) => selected.has(value));
533
+ const render = () => {
534
+ process.stdout.write('\x1b[2J\x1b[H');
535
+ renderHeader?.();
536
+ process.stdout.write(`\n${ANSI.bold}${title}${ANSI.reset}\n`);
537
+ process.stdout.write(`${ANSI.dim}${subtitle}${ANSI.reset}\n\n`);
538
+ if (warning) {
539
+ process.stdout.write(`${ANSI.cyan}${warning}${ANSI.reset}\n\n`);
540
+ }
541
+ for (let index = 0; index < options.length; index += 1) {
542
+ const option = options[index];
543
+ const pointer = index === cursorIndex ? `${ANSI.cyan}>${ANSI.reset}` : ' ';
544
+ const checkbox = selected.has(option.value) ? '[x]' : '[ ]';
545
+ const requiredLabel = required.has(option.value) ? ` ${ANSI.dim}(required)${ANSI.reset}` : '';
546
+ process.stdout.write(`${pointer} ${checkbox} ${index + 1}) ${ANSI.bold}${option.label}${ANSI.reset}${requiredLabel}\n`);
547
+ writeWrapped(option.detail, ' ', ANSI.dim);
548
+ }
549
+ process.stdout.write(`\n${ANSI.dim}Esc/Q cancels. Space toggles, A toggles all optional items, Enter continues. Number keys 1-${options.length} toggle items.${ANSI.reset}\n`);
550
+ };
551
+ const cancel = () => {
552
+ cleanup();
553
+ process.stdout.write('\n');
554
+ reject(new WizardAbortError('Setup cancelled.'));
555
+ };
556
+ const finish = () => {
557
+ const values = selectedValues();
558
+ if (values.length < minSelections) {
559
+ warning = `Select at least ${minSelections} item${minSelections === 1 ? '' : 's'} to continue.`;
560
+ render();
561
+ return;
562
+ }
563
+ cleanup();
564
+ process.stdout.write('\x1b[2J\x1b[H');
565
+ resolve(values);
566
+ };
567
+ const toggleIndex = (index) => {
568
+ const option = options[index];
569
+ if (!option)
570
+ return;
571
+ warning = '';
572
+ if (required.has(option.value)) {
573
+ selected.add(option.value);
574
+ warning = `${option.label} is required.`;
575
+ return;
576
+ }
577
+ if (selected.has(option.value))
578
+ selected.delete(option.value);
579
+ else
580
+ selected.add(option.value);
581
+ requiredValues.forEach((value) => selected.add(value));
582
+ };
583
+ const onKeypress = (_text, key) => {
584
+ if (key?.ctrl && key?.name === 'c') {
585
+ cancel();
586
+ return;
587
+ }
588
+ if (key?.name === 'escape' || key?.name === 'q') {
589
+ cancel();
590
+ return;
591
+ }
592
+ if (key?.name === 'up' || key?.name === 'k') {
593
+ cursorIndex = (cursorIndex - 1 + options.length) % options.length;
594
+ warning = '';
595
+ }
596
+ else if (key?.name === 'down' || key?.name === 'j') {
597
+ cursorIndex = (cursorIndex + 1) % options.length;
598
+ warning = '';
599
+ }
600
+ else if (key?.name === 'space') {
601
+ toggleIndex(cursorIndex);
602
+ }
603
+ else if (String(_text || '').toLowerCase() === 'a') {
604
+ const optional = options.filter((option) => !required.has(option.value));
605
+ const allSelected = optional.every((option) => selected.has(option.value));
606
+ optional.forEach((option) => {
607
+ if (allSelected)
608
+ selected.delete(option.value);
609
+ else
610
+ selected.add(option.value);
611
+ });
612
+ requiredValues.forEach((value) => selected.add(value));
613
+ warning = '';
614
+ }
615
+ else if (key?.name === 'return' || key?.name === 'enter') {
616
+ finish();
617
+ return;
618
+ }
619
+ else if (/^[1-9]$/.test(String(_text || ''))) {
620
+ toggleIndex(Number(_text) - 1);
621
+ }
622
+ render();
623
+ };
624
+ process.stdin.on('keypress', onKeypress);
625
+ process.stdout.write(ANSI.hideCursor);
626
+ render();
627
+ });
628
+ }
465
629
  async function askMenuChoiceByKeys({ title, subtitle, options, defaultValue, renderHeader, }) {
466
630
  emitKeypressEvents(process.stdin);
467
631
  const wasRaw = process.stdin.isRaw;
@@ -2885,12 +3049,32 @@ async function askYesNo(rl, label, defaultYes = true) {
2885
3049
  }
2886
3050
  }
2887
3051
  }
3052
+ function truncateTableCell(value, width) {
3053
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
3054
+ if (text.length <= width)
3055
+ return text.padEnd(width, ' ');
3056
+ return `${text.slice(0, Math.max(0, width - 3))}...`.padEnd(width, ' ');
3057
+ }
3058
+ function printAsciiTable(headers, rows, widths) {
3059
+ const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
3060
+ const renderRow = (cells) => `| ${cells.map((cell, index) => truncateTableCell(cell, widths[index])).join(' | ')} |`;
3061
+ process.stdout.write(`${border}\n`);
3062
+ process.stdout.write(`${renderRow(headers)}\n`);
3063
+ process.stdout.write(`${border}\n`);
3064
+ for (const row of rows) {
3065
+ process.stdout.write(`${renderRow(row)}\n`);
3066
+ }
3067
+ process.stdout.write(`${border}\n`);
3068
+ }
2888
3069
  function printCadencePlan(cadences) {
2889
3070
  process.stdout.write('\nDefault growth cadence:\n');
2890
- for (const cadence of cadences) {
2891
- const critical = cadence.criticalOnly ? 'critical only' : 'full review';
2892
- process.stdout.write(`- ${cadence.title} (${critical}): ${cadence.objective}\n`);
2893
- }
3071
+ printAsciiTable(['Cadence', 'Every', 'Mode', 'Primary focus', 'What it decides'], cadences.map((cadence) => [
3072
+ cadence.key,
3073
+ `${cadence.intervalDays}d`,
3074
+ cadence.criticalOnly ? 'critical only' : 'full review',
3075
+ Array.isArray(cadence.focusAreas) ? cadence.focusAreas.slice(0, 4).join(', ') : '',
3076
+ cadence.objective,
3077
+ ]), [12, 7, 13, 30, 42]);
2894
3078
  process.stdout.write('\n');
2895
3079
  }
2896
3080
  async function askToolUsage(rl) {
@@ -2917,18 +3101,37 @@ async function askToolUsage(rl) {
2917
3101
  ],
2918
3102
  });
2919
3103
  }
2920
- async function askCadencePlan(rl) {
2921
- const cadences = DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence }));
3104
+ async function askCadencePlan(rl, existingCadences = []) {
3105
+ const existingByKey = new Map((Array.isArray(existingCadences) ? existingCadences : [])
3106
+ .filter((cadence) => cadence?.key)
3107
+ .map((cadence) => [String(cadence.key), cadence]));
3108
+ const cadences = DEFAULT_CADENCE_PLAN.map((cadence) => ({
3109
+ ...cadence,
3110
+ ...(existingByKey.get(cadence.key) || {}),
3111
+ }));
2922
3112
  printCadencePlan(cadences);
2923
- const customize = await askYesNo(rl, 'Use this default cadence plan? Answer no to edit daily/weekly/monthly/3-month/6-month/1-year instructions.', true);
2924
- if (customize)
3113
+ const selectedCadences = await askMultiChoice(rl, {
3114
+ title: 'Scheduled review cadences',
3115
+ subtitle: 'Use Up/Down to move, Space to toggle cadences, A to toggle all, Enter to continue.',
3116
+ defaultValues: cadences.filter((cadence) => cadence.enabled !== false).map((cadence) => cadence.key),
3117
+ minSelections: 1,
3118
+ options: cadences.map((cadence) => ({
3119
+ value: cadence.key,
3120
+ label: cadence.title,
3121
+ detail: `${cadence.intervalDays}d, ${cadence.criticalOnly ? 'critical only' : 'full review'} - ${cadence.objective}`,
3122
+ })),
3123
+ });
3124
+ const selected = new Set(selectedCadences);
3125
+ cadences.forEach((cadence) => {
3126
+ cadence.enabled = selected.has(cadence.key);
3127
+ });
3128
+ const customize = await askYesNo(rl, 'Customize objectives, instructions, focus areas, or source priorities for enabled cadences?', false);
3129
+ if (!customize)
2925
3130
  return cadences;
2926
3131
  for (const cadence of cadences) {
2927
- process.stdout.write(`\n${cadence.title}\n`);
2928
- const enabled = await askYesNo(rl, `Enable ${cadence.key}?`, true);
2929
- cadence.enabled = enabled;
2930
- if (!enabled)
3132
+ if (cadence.enabled === false)
2931
3133
  continue;
3134
+ process.stdout.write(`\n${cadence.title}\n`);
2932
3135
  cadence.objective = await ask(rl, `${cadence.key} objective`, cadence.objective);
2933
3136
  cadence.instructions = await ask(rl, `${cadence.key} instructions`, cadence.instructions);
2934
3137
  const focusAreas = await ask(rl, `${cadence.key} focus areas (comma-separated)`, cadence.focusAreas.join(','));
@@ -2992,7 +3195,7 @@ async function buildDefaultWizardConfig() {
2992
3195
  command: getWizardDefaultSourceCommand('analytics'),
2993
3196
  },
2994
3197
  revenuecat: {
2995
- enabled: false,
3198
+ enabled: true,
2996
3199
  mode: 'command',
2997
3200
  command: getWizardDefaultSourceCommand('revenuecat'),
2998
3201
  },
@@ -3009,7 +3212,7 @@ async function buildDefaultWizardConfig() {
3009
3212
  initialLookback: '30d',
3010
3213
  },
3011
3214
  extra: [
3012
- buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
3215
+ buildExtraSourceConfig('asc-cli', { enabled: true, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
3013
3216
  ],
3014
3217
  },
3015
3218
  schedule: {
@@ -3025,6 +3228,7 @@ async function buildDefaultWizardConfig() {
3025
3228
  autoCreateWhenGitHubWriteAccess: true,
3026
3229
  disableAutoCreateGitHubArtifacts: false,
3027
3230
  mode: 'issue',
3231
+ outputDestinations: ['openclaw_chat', 'github_issue', 'github_pull_request'],
3028
3232
  usageMode: 'production_autopilot',
3029
3233
  draftPullRequests: true,
3030
3234
  proposalBranchPrefix: 'openclaw/proposals',
@@ -3058,7 +3262,7 @@ async function buildDefaultWizardConfig() {
3058
3262
  },
3059
3263
  },
3060
3264
  charting: {
3061
- enabled: false,
3265
+ enabled: true,
3062
3266
  command: null,
3063
3267
  },
3064
3268
  notifications: {
@@ -3105,7 +3309,7 @@ function buildRecommendedSourceConfig() {
3105
3309
  command: getWizardDefaultSourceCommand('analytics'),
3106
3310
  },
3107
3311
  revenuecat: {
3108
- enabled: false,
3312
+ enabled: true,
3109
3313
  mode: 'command',
3110
3314
  command: getWizardDefaultSourceCommand('revenuecat'),
3111
3315
  },
@@ -3122,7 +3326,7 @@ function buildRecommendedSourceConfig() {
3122
3326
  initialLookback: '30d',
3123
3327
  },
3124
3328
  extra: [
3125
- buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
3329
+ buildExtraSourceConfig('asc-cli', { enabled: true, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
3126
3330
  ],
3127
3331
  };
3128
3332
  }
@@ -3131,6 +3335,8 @@ function getInputChannelInitialSelection(config) {
3131
3335
  const extraSources = Array.isArray(sources.extra) ? sources.extra : [];
3132
3336
  const selected = new Set();
3133
3337
  const hasExplicitSources = Boolean(config?.sources);
3338
+ if (!hasExplicitSources)
3339
+ return orderConnectors([...CONNECTOR_KEYS]);
3134
3340
  if (!hasExplicitSources || sources.analytics?.enabled !== false)
3135
3341
  selected.add('analytics');
3136
3342
  if (sources.revenuecat?.enabled === true || isConnectorLocallyConfigured('revenuecat'))
@@ -3142,12 +3348,9 @@ function getInputChannelInitialSelection(config) {
3142
3348
  isConnectorLocallyConfigured('asc')) {
3143
3349
  selected.add('asc');
3144
3350
  }
3145
- if (config?.deliveries?.github?.enabled ||
3146
- config?.actions?.autoCreateIssues ||
3147
- config?.actions?.autoCreatePullRequests ||
3148
- isConnectorLocallyConfigured('github')) {
3149
- selected.add('github');
3150
- }
3351
+ selected.add('github');
3352
+ if (selected.size === 0)
3353
+ return orderConnectors([...CONNECTOR_KEYS]);
3151
3354
  return orderConnectors([...selected]);
3152
3355
  }
3153
3356
  function buildSourceConfigFromInputChannels(selectedConnectors, existingSources = {}) {
@@ -3266,11 +3469,25 @@ async function askOutputConfig(rl, config) {
3266
3469
  'GitHub issues or draft PRs are optional and only run when a token plus an inferred repo are available.',
3267
3470
  ]);
3268
3471
  const currentMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
3269
- const currentAutoCreate = Boolean(config?.actions?.autoCreateIssues || config?.actions?.autoCreatePullRequests || config?.deliveries?.github?.autoCreate);
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',
3472
+ const configuredDestinations = Array.isArray(config?.actions?.outputDestinations)
3473
+ ? config.actions.outputDestinations
3474
+ : [];
3475
+ const currentAutoCreateIssue = Boolean(config?.actions?.autoCreateIssues ||
3476
+ configuredDestinations.includes('github_issue') ||
3477
+ (config?.deliveries?.github?.autoCreate && currentMode !== 'pull_request'));
3478
+ const currentAutoCreatePullRequest = Boolean(config?.actions?.autoCreatePullRequests ||
3479
+ configuredDestinations.includes('github_pull_request') ||
3480
+ (config?.deliveries?.github?.autoCreate && currentMode === 'pull_request'));
3481
+ const outputChoices = await askMultiChoice(rl, {
3482
+ title: 'Output destinations',
3483
+ subtitle: 'Use Up/Down to move, Space to toggle outputs, A to toggle all optional outputs, Enter to continue.',
3484
+ defaultValues: [
3485
+ 'chat',
3486
+ ...(currentAutoCreateIssue ? ['issue'] : []),
3487
+ ...(currentAutoCreatePullRequest ? ['pull_request'] : []),
3488
+ ],
3489
+ requiredValues: ['chat'],
3490
+ minSelections: 1,
3274
3491
  options: [
3275
3492
  {
3276
3493
  value: 'chat',
@@ -3289,9 +3506,11 @@ async function askOutputConfig(rl, config) {
3289
3506
  },
3290
3507
  ],
3291
3508
  });
3292
- const summaryOnly = outputChoice === 'chat';
3293
- const mode = outputChoice === 'pull_request' ? 'pull_request' : 'issue';
3294
- const autoCreate = !summaryOnly;
3509
+ const wantsIssue = outputChoices.includes('issue');
3510
+ const wantsPullRequest = outputChoices.includes('pull_request');
3511
+ const summaryOnly = !wantsIssue && !wantsPullRequest;
3512
+ const mode = wantsPullRequest ? 'pull_request' : 'issue';
3513
+ const autoCreate = wantsIssue || wantsPullRequest;
3295
3514
  if (!summaryOnly) {
3296
3515
  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');
3297
3516
  }
@@ -3308,8 +3527,13 @@ async function askOutputConfig(rl, config) {
3308
3527
  config.actions = {
3309
3528
  ...(config.actions || {}),
3310
3529
  mode,
3311
- autoCreateIssues: mode === 'issue' && autoCreate,
3312
- autoCreatePullRequests: mode === 'pull_request' && autoCreate,
3530
+ outputDestinations: [
3531
+ 'openclaw_chat',
3532
+ ...(wantsIssue ? ['github_issue'] : []),
3533
+ ...(wantsPullRequest ? ['github_pull_request'] : []),
3534
+ ],
3535
+ autoCreateIssues: wantsIssue,
3536
+ autoCreatePullRequests: wantsPullRequest,
3313
3537
  autoCreateWhenGitHubWriteAccess: config.actions?.autoCreateWhenGitHubWriteAccess !== false,
3314
3538
  disableAutoCreateGitHubArtifacts: config.actions?.disableAutoCreateGitHubArtifacts === true,
3315
3539
  draftPullRequests: true,
@@ -3327,6 +3551,10 @@ async function askOutputConfig(rl, config) {
3327
3551
  ...(config.deliveries?.github || {}),
3328
3552
  enabled: !summaryOnly,
3329
3553
  mode,
3554
+ modes: [
3555
+ ...(wantsIssue ? ['issue'] : []),
3556
+ ...(wantsPullRequest ? ['pull_request'] : []),
3557
+ ],
3330
3558
  autoCreate,
3331
3559
  draftPullRequests: true,
3332
3560
  proposalBranchPrefix: config?.actions?.proposalBranchPrefix || 'openclaw/proposals',
@@ -3388,7 +3616,7 @@ async function askGitHubArtifactDetails(rl, config) {
3388
3616
  if (!customize) {
3389
3617
  config.charting = {
3390
3618
  ...(config.charting || {}),
3391
- enabled: config.charting?.enabled === true,
3619
+ enabled: config.charting?.enabled !== false,
3392
3620
  command: config.charting?.command || null,
3393
3621
  };
3394
3622
  return config;
@@ -3419,7 +3647,7 @@ async function askIntervalConfig(rl, config) {
3419
3647
  const usageMode = await askToolUsage(rl);
3420
3648
  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
3649
  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;
3422
- const cadences = await askCadencePlan(rl);
3650
+ const cadences = await askCadencePlan(rl, currentSchedule.cadences);
3423
3651
  config.schedule = {
3424
3652
  ...currentSchedule,
3425
3653
  intervalMinutes,