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