@analyticscli/growth-engineer 0.1.0-preview.14 → 0.1.0-preview.18

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.
Files changed (40) hide show
  1. package/dist/config.d.ts +775 -22
  2. package/dist/config.js +39 -5
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.js +134 -21
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/export-asc-summary.mjs +1 -1
  7. package/dist/runtime/export-asc-summary.mjs.map +1 -1
  8. package/dist/runtime/export-coolify-summary.d.mts +2 -0
  9. package/dist/runtime/export-coolify-summary.mjs +230 -0
  10. package/dist/runtime/export-coolify-summary.mjs.map +1 -0
  11. package/dist/runtime/export-paddle-summary.d.mts +2 -0
  12. package/dist/runtime/export-paddle-summary.mjs +170 -0
  13. package/dist/runtime/export-paddle-summary.mjs.map +1 -0
  14. package/dist/runtime/export-sentry-summary.mjs +265 -38
  15. package/dist/runtime/export-sentry-summary.mjs.map +1 -1
  16. package/dist/runtime/export-seo-summary.d.mts +2 -0
  17. package/dist/runtime/export-seo-summary.mjs +503 -0
  18. package/dist/runtime/export-seo-summary.mjs.map +1 -0
  19. package/dist/runtime/openclaw-exporters-lib.d.mts +50 -0
  20. package/dist/runtime/openclaw-exporters-lib.mjs +761 -57
  21. package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
  22. package/dist/runtime/openclaw-growth-engineer.mjs +163 -4
  23. package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -1
  24. package/dist/runtime/openclaw-growth-env.mjs +5 -0
  25. package/dist/runtime/openclaw-growth-env.mjs.map +1 -1
  26. package/dist/runtime/openclaw-growth-preflight.mjs +399 -26
  27. package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
  28. package/dist/runtime/openclaw-growth-runner.mjs +564 -69
  29. package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
  30. package/dist/runtime/openclaw-growth-shared.d.mts +150 -2
  31. package/dist/runtime/openclaw-growth-shared.mjs +489 -7
  32. package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
  33. package/dist/runtime/openclaw-growth-start.mjs +584 -48
  34. package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
  35. package/dist/runtime/openclaw-growth-status.mjs +82 -6
  36. package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
  37. package/dist/runtime/openclaw-growth-wizard.mjs +1501 -105
  38. package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
  39. package/package.json +3 -1
  40. package/templates/config.example.json +120 -71
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import process from 'node:process';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { buildGrowthRunnerCommand, buildOpenClawGrowthSystemEvent, deriveSchedulerProofPathFromStatePath, deriveStatePathFromConfigPath, getActionMode, getAutomationConfig, getDefaultSourceCommand, } from './openclaw-growth-shared.mjs';
7
+ import { buildGrowthRunnerCommand, buildOpenClawCronAddCommand, deriveSchedulerProofPathFromStatePath, deriveStatePathFromConfigPath, getActionMode, getAutomationConfig, getDefaultSourceCommand, getOpenClawCronEditDeliveryCommandFromInspection, buildHermesCronCreateCommand, inspectHermesCronInstall, inspectOpenClawCronInstall, repairOpenClawCronDeliveryStore, } from './openclaw-growth-shared.mjs';
8
8
  import { applyOpenClawSecretRefs, loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
9
9
  const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
10
10
  const DEFAULT_TEMPLATE_PATH = 'data/openclaw-growth-engineer/config.example.json';
@@ -16,6 +16,55 @@ const ANALYTICSCLI_PACKAGE_SPEC = process.env.ANALYTICSCLI_CLI_PACKAGE || '@anal
16
16
  const ANALYTICSCLI_NPM_PREFIX = process.env.ANALYTICSCLI_NPM_PREFIX ||
17
17
  (process.env.HOME ? path.join(process.env.HOME, '.local') : path.join(process.cwd(), '.analyticscli-npm'));
18
18
  const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
19
+ const ACCOUNT_SIGNAL_CONNECTORS = [
20
+ 'stripe',
21
+ 'lemonsqueezy',
22
+ 'adapty',
23
+ 'superwall',
24
+ 'google-play',
25
+ 'datadog',
26
+ 'bugsnag',
27
+ 'intercom',
28
+ 'zendesk',
29
+ 'apple-search-ads',
30
+ 'google-ads',
31
+ 'meta-ads',
32
+ 'tiktok-ads',
33
+ 'vercel',
34
+ 'cloudflare',
35
+ 'resend',
36
+ 'customerio',
37
+ 'mailchimp',
38
+ 'appfollow',
39
+ 'apptweak',
40
+ 'linear',
41
+ 'postiz',
42
+ ];
43
+ const SUPPORTED_CONNECTORS = ['analytics', 'github', 'asc', 'revenuecat', 'paddle', 'seo', 'sentry', 'coolify', ...ACCOUNT_SIGNAL_CONNECTORS];
44
+ const ACCOUNT_SIGNAL_SECRET_ENVS = {
45
+ stripe: ['STRIPE_API_KEY'],
46
+ lemonsqueezy: ['LEMON_SQUEEZY_API_KEY'],
47
+ adapty: ['ADAPTY_API_KEY'],
48
+ superwall: ['SUPERWALL_API_KEY'],
49
+ 'google-play': ['GOOGLE_PLAY_SERVICE_ACCOUNT_JSON'],
50
+ datadog: ['DATADOG_API_KEY', 'DATADOG_APP_KEY'],
51
+ bugsnag: ['BUGSNAG_AUTH_TOKEN'],
52
+ intercom: ['INTERCOM_ACCESS_TOKEN'],
53
+ zendesk: ['ZENDESK_SUBDOMAIN', 'ZENDESK_EMAIL', 'ZENDESK_API_TOKEN'],
54
+ 'apple-search-ads': ['APPLE_SEARCH_ADS_REFRESH_TOKEN'],
55
+ 'google-ads': ['GOOGLE_ADS_DEVELOPER_TOKEN', 'GOOGLE_ADS_CLIENT_ID', 'GOOGLE_ADS_CLIENT_SECRET', 'GOOGLE_ADS_REFRESH_TOKEN'],
56
+ 'meta-ads': ['META_ADS_ACCESS_TOKEN'],
57
+ 'tiktok-ads': ['TIKTOK_ADS_ACCESS_TOKEN'],
58
+ vercel: ['VERCEL_ACCESS_TOKEN'],
59
+ cloudflare: ['CLOUDFLARE_API_TOKEN'],
60
+ resend: ['RESEND_API_KEY'],
61
+ customerio: ['CUSTOMERIO_APP_API_KEY'],
62
+ mailchimp: ['MAILCHIMP_API_KEY'],
63
+ appfollow: ['APPFOLLOW_API_TOKEN'],
64
+ apptweak: ['APPTWEAK_API_TOKEN'],
65
+ linear: ['LINEAR_API_KEY'],
66
+ postiz: ['POSTIZ_API_KEY'],
67
+ };
19
68
  function printHelpAndExit(exitCode, reason = null) {
20
69
  if (reason) {
21
70
  process.stderr.write(`${reason}\n\n`);
@@ -35,9 +84,9 @@ Options:
35
84
  --config <file> Config path (default: ${DEFAULT_CONFIG_PATH})
36
85
  --project <id> Optional AnalyticsCLI project ID pin for generated source commands
37
86
  --asc-app <id> Optional ASC app ID filter (defaults to all accessible apps)
38
- --connectors <list> Install/enable connector helpers (analytics,github,asc,revenuecat,sentry,all)
87
+ --connectors <list> Install/enable connector helpers (${SUPPORTED_CONNECTORS.join(',')},all)
39
88
  --only-connectors <list>
40
- Limit live preflight checks to analytics,github,asc,revenuecat,sentry
89
+ Limit live preflight checks to ${SUPPORTED_CONNECTORS.join(',')}
41
90
  --setup-only Run bootstrap + preflight only (skip first run)
42
91
  --no-test-connections Skip live API smoke checks in preflight
43
92
  --openclaw-cron <mode> Configure OpenClaw Gateway cron: auto, enable, require, disable (default: auto)
@@ -50,9 +99,21 @@ Options:
50
99
  `);
51
100
  process.exit(exitCode);
52
101
  }
102
+ function resolveDefaultConfigPath() {
103
+ const explicit = String(process.env.OPENCLAW_GROWTH_CONFIG_PATH || '').trim();
104
+ if (explicit)
105
+ return explicit;
106
+ const homeConfigPath = process.env.HOME ? path.join(process.env.HOME, 'data/openclaw-growth-engineer/config.json') : '';
107
+ const homeStatePath = process.env.HOME ? path.join(process.env.HOME, 'data/openclaw-growth-engineer/state.json') : '';
108
+ if (homeConfigPath && existsSync(homeConfigPath) && existsSync(homeStatePath))
109
+ return homeConfigPath;
110
+ if (!existsSync(DEFAULT_CONFIG_PATH) && homeConfigPath && existsSync(homeConfigPath))
111
+ return homeConfigPath;
112
+ return DEFAULT_CONFIG_PATH;
113
+ }
53
114
  function parseArgs(argv) {
54
115
  const args = {
55
- config: DEFAULT_CONFIG_PATH,
116
+ config: resolveDefaultConfigPath(),
56
117
  project: '',
57
118
  ascApp: '',
58
119
  run: true,
@@ -147,8 +208,58 @@ function normalizeConnectorKey(value) {
147
208
  return 'asc';
148
209
  if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
149
210
  return 'revenuecat';
211
+ if (['paddle', 'paddle-billing', 'billing-metrics', 'web-revenue'].includes(normalized))
212
+ return 'paddle';
213
+ if (['seo', 'gsc', 'google-search-console', 'search-console', 'dataforseo', 'organic-search'].includes(normalized))
214
+ return 'seo';
150
215
  if (['sentry', 'sentry-api', 'sentry-mcp', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
151
216
  return 'sentry';
217
+ if (['coolify', 'coolify-api', 'deployment', 'deployments', 'hosting', 'infra', 'infrastructure'].includes(normalized))
218
+ return 'coolify';
219
+ if (['stripe', 'stripe-billing', 'stripe-payments'].includes(normalized))
220
+ return 'stripe';
221
+ if (['lemonsqueezy', 'lemon-squeezy', 'lemon', 'ls'].includes(normalized))
222
+ return 'lemonsqueezy';
223
+ if (['adapty', 'adapty-paywalls', 'adapty-subscriptions'].includes(normalized))
224
+ return 'adapty';
225
+ if (['superwall', 'superwall-paywalls'].includes(normalized))
226
+ return 'superwall';
227
+ if (['google-play', 'google-play-console', 'play-console', 'play-store', 'android-store'].includes(normalized))
228
+ return 'google-play';
229
+ if (['datadog', 'datadog-rum', 'datadog-apm', 'datadog-logs'].includes(normalized))
230
+ return 'datadog';
231
+ if (['bugsnag', 'bugsnag-crashes'].includes(normalized))
232
+ return 'bugsnag';
233
+ if (['intercom', 'intercom-support'].includes(normalized))
234
+ return 'intercom';
235
+ if (['zendesk', 'zendesk-support'].includes(normalized))
236
+ return 'zendesk';
237
+ if (['apple-search-ads', 'apple-ads', 'asa', 'search-ads'].includes(normalized))
238
+ return 'apple-search-ads';
239
+ if (['google-ads', 'adwords'].includes(normalized))
240
+ return 'google-ads';
241
+ if (['meta-ads', 'facebook-ads', 'instagram-ads', 'fb-ads'].includes(normalized))
242
+ return 'meta-ads';
243
+ if (['tiktok-ads', 'tiktok-business', 'tiktok-business-api'].includes(normalized))
244
+ return 'tiktok-ads';
245
+ if (['vercel', 'vercel-deployments', 'vercel-hosting'].includes(normalized))
246
+ return 'vercel';
247
+ if (['cloudflare', 'cf', 'cloudflare-workers', 'cloudflare-pages'].includes(normalized))
248
+ return 'cloudflare';
249
+ if (['resend', 'resend-email'].includes(normalized))
250
+ return 'resend';
251
+ if (['customerio', 'customer-io', 'customer.io', 'cio'].includes(normalized))
252
+ return 'customerio';
253
+ if (['mailchimp', 'mailchimp-marketing'].includes(normalized))
254
+ return 'mailchimp';
255
+ if (['appfollow', 'app-follow'].includes(normalized))
256
+ return 'appfollow';
257
+ if (['apptweak', 'app-tweak'].includes(normalized))
258
+ return 'apptweak';
259
+ if (['linear', 'linear-issues', 'linear-planning'].includes(normalized))
260
+ return 'linear';
261
+ if (['postiz', 'postiz-api', 'social-publishing', 'social-scheduler'].includes(normalized))
262
+ return 'postiz';
152
263
  return null;
153
264
  }
154
265
  function parseConnectorList(value) {
@@ -158,14 +269,10 @@ function parseConnectorList(value) {
158
269
  for (const entry of String(value).split(',')) {
159
270
  const connector = normalizeConnectorKey(entry);
160
271
  if (!connector) {
161
- printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use analytics, github, asc, revenuecat, sentry, or all.`);
272
+ printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use ${SUPPORTED_CONNECTORS.join(', ')}, or all.`);
162
273
  }
163
274
  if (connector === 'all') {
164
- connectors.add('analytics');
165
- connectors.add('github');
166
- connectors.add('asc');
167
- connectors.add('revenuecat');
168
- connectors.add('sentry');
275
+ SUPPORTED_CONNECTORS.forEach((supported) => connectors.add(supported));
169
276
  }
170
277
  else {
171
278
  connectors.add(connector);
@@ -198,9 +305,18 @@ function getRuntimeSourceCommand(sourceName) {
198
305
  if (normalized === 'revenuecat' || normalized === 'revenue-cat') {
199
306
  return nodeRuntimeScriptCommand('export-revenuecat-summary.mjs');
200
307
  }
308
+ if (normalized === 'paddle') {
309
+ return nodeRuntimeScriptCommand('export-paddle-summary.mjs');
310
+ }
311
+ if (['seo', 'gsc', 'google-search-console', 'search-console', 'dataforseo'].includes(normalized)) {
312
+ return nodeRuntimeScriptCommand('export-seo-summary.mjs');
313
+ }
201
314
  if (normalized === 'sentry' || normalized === 'glitchtip') {
202
315
  return nodeRuntimeScriptCommand('export-sentry-summary.mjs');
203
316
  }
317
+ if (normalized === 'coolify') {
318
+ return nodeRuntimeScriptCommand('export-coolify-summary.mjs');
319
+ }
204
320
  if (['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(normalized)) {
205
321
  return nodeRuntimeScriptCommand('export-asc-summary.mjs');
206
322
  }
@@ -210,7 +326,7 @@ function replaceLegacyRuntimeScriptCommand(command) {
210
326
  const trimmed = String(command || '').trim();
211
327
  if (!trimmed)
212
328
  return trimmed;
213
- 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-preflight\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-engineer\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
329
+ return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-paddle-summary\.mjs|export-seo-summary\.mjs|export-sentry-summary\.mjs|export-coolify-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-start\.mjs|openclaw-growth-status\.mjs|openclaw-growth-preflight\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-engineer\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
214
330
  }
215
331
  function normalizeSourceCommand(sourceName, source) {
216
332
  return replaceLegacyRuntimeScriptCommand(source?.command || '') || getRuntimeSourceCommand(sourceName);
@@ -220,7 +336,7 @@ function migrateRuntimeSourceCommands(config) {
220
336
  return config;
221
337
  const sources = config.sources && typeof config.sources === 'object' ? config.sources : {};
222
338
  const nextSources = { ...sources };
223
- for (const sourceName of ['analytics', 'revenuecat', 'sentry']) {
339
+ for (const sourceName of ['analytics', 'revenuecat', 'paddle', 'seo', 'sentry', 'coolify']) {
224
340
  if (nextSources[sourceName]?.mode === 'command') {
225
341
  nextSources[sourceName] = {
226
342
  ...nextSources[sourceName],
@@ -275,10 +391,19 @@ function emitProgress(enabled, event) {
275
391
  return;
276
392
  process.stderr.write(`OPENCLAW_PROGRESS ${JSON.stringify(event)}\n`);
277
393
  }
394
+ function hardenUnattendedShellCommand(command) {
395
+ return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
396
+ }
278
397
  function runShellCommand(command, timeoutMs = 120_000, options = {}) {
279
398
  return new Promise((resolve) => {
280
- const child = spawn(resolveShellCommand(), ['-c', command], {
399
+ const child = spawn(resolveShellCommand(), ['-c', hardenUnattendedShellCommand(command)], {
281
400
  stdio: ['ignore', 'pipe', 'pipe'],
401
+ env: {
402
+ ...process.env,
403
+ DEBIAN_FRONTEND: 'noninteractive',
404
+ SUDO_ASKPASS: '/bin/false',
405
+ SUDO_PROMPT: '',
406
+ },
282
407
  });
283
408
  let stdout = '';
284
409
  let stderr = '';
@@ -561,12 +686,13 @@ function renderHeartbeatBlock(configPath, config) {
561
686
  const displayConfigPath = relativeWorkspacePath(configPath);
562
687
  const displayStatePath = deriveStatePathFromConfigPath(displayConfigPath);
563
688
  const runnerCommand = buildGrowthRunnerCommand(displayConfigPath, displayStatePath);
689
+ const wizardCommand = 'npx -y @analyticscli/growth-engineer@preview wizard --connectors';
564
690
  return `${HEARTBEAT_MARKER_START}
565
691
  tasks:
566
692
 
567
693
  - name: openclaw-growth-engineer-run
568
694
  interval: ${interval}
569
- prompt: "Run \`${runnerCommand}\` from the workspace if the config and runtime files exist. The runner owns schedule.cadences, connectorHealthCheckIntervalMinutes, skipIfNoDataChange, and skipIfIssueSetUnchanged. If it reports connector-health alerts, production crashes, generated issues, or actionable growth findings, summarize only the action and evidence. If setup files are missing, tell the user to run \`node scripts/openclaw-growth-wizard.mjs --connectors --config ${displayConfigPath}\`. If there is no actionable output, reply HEARTBEAT_OK."
695
+ prompt: "Run \`${runnerCommand}\` from the workspace if the config and runtime files exist. The runner owns schedule.cadences, connectorHealthCheckIntervalMinutes, skipIfNoDataChange, and skipIfIssueSetUnchanged. If it reports connector-health alerts, production crashes, generated issues, or actionable growth findings, summarize only the action and evidence. If setup files are missing, tell the user to run \`${wizardCommand}\`. If there is no actionable output, reply HEARTBEAT_OK."
570
696
 
571
697
  # Keep this section small. Do not put secrets in HEARTBEAT.md.
572
698
  ${HEARTBEAT_MARKER_END}`;
@@ -658,47 +784,162 @@ async function ensureOpenClawCronSchedule(configPath, config, mode = 'auto') {
658
784
  proof,
659
785
  };
660
786
  }
661
- const list = await runShellCommand('openclaw cron list', 30_000);
662
- if (list.ok && list.stdout.includes(automation.openclawCron.name)) {
787
+ const displayConfigPath = relativeWorkspacePath(configPath);
788
+ const addCommand = buildOpenClawCronAddCommand(displayConfigPath, config);
789
+ const inspection = await inspectOpenClawCronInstall({
790
+ configPath: displayConfigPath,
791
+ config,
792
+ runCommand: runShellCommand,
793
+ readFile: fs.readFile,
794
+ });
795
+ if (inspection.exists && inspection.verified) {
663
796
  return {
664
797
  ok: true,
665
798
  enabled: true,
666
799
  installed: true,
667
- status: 'already_configured',
668
- detail: `OpenClaw cron job already exists: ${automation.openclawCron.name}`,
800
+ status: 'already_configured_verified',
801
+ detail: `OpenClaw cron job already exists and matches the Growth Engineer runner contract: ${automation.openclawCron.name}`,
669
802
  schedule: automation.openclawCron.schedule,
670
803
  timezone: automation.openclawCron.timezone,
804
+ source: inspection.source,
671
805
  proof,
672
806
  };
673
807
  }
674
- const displayConfigPath = relativeWorkspacePath(configPath);
675
- const eventText = buildOpenClawGrowthSystemEvent(displayConfigPath, config);
676
- const addCommand = [
677
- 'openclaw cron add',
678
- '--name',
679
- quote(automation.openclawCron.name),
680
- '--cron',
681
- quote(automation.openclawCron.schedule),
682
- '--tz',
683
- quote(automation.openclawCron.timezone),
684
- '--session',
685
- automation.openclawCron.mode === 'isolated' ? 'isolated' : 'main',
686
- automation.openclawCron.mode === 'isolated' ? '--message' : '--system-event',
687
- quote(eventText),
688
- automation.openclawCron.mode === 'isolated' ? '--announce' : '--wake now',
689
- ].join(' ');
808
+ if (inspection.exists && inspection.reason === 'delivery_mismatch') {
809
+ const editCommand = getOpenClawCronEditDeliveryCommandFromInspection(inspection, config);
810
+ if (editCommand) {
811
+ const edit = await runShellCommand(editCommand, 60_000);
812
+ if (edit.ok) {
813
+ return {
814
+ ok: true,
815
+ enabled: true,
816
+ installed: true,
817
+ status: 'repaired_delivery_cli',
818
+ detail: `Repaired OpenClaw cron delivery with: ${editCommand}`,
819
+ schedule: automation.openclawCron.schedule,
820
+ timezone: automation.openclawCron.timezone,
821
+ command: editCommand,
822
+ source: inspection.source,
823
+ proof,
824
+ };
825
+ }
826
+ }
827
+ const repair = await repairOpenClawCronDeliveryStore({
828
+ configPath: displayConfigPath,
829
+ config,
830
+ readFile: fs.readFile,
831
+ writeFile: fs.writeFile,
832
+ });
833
+ if (repair.repaired) {
834
+ return {
835
+ ok: true,
836
+ enabled: true,
837
+ installed: true,
838
+ status: 'repaired_delivery',
839
+ detail: `Repaired OpenClaw cron delivery for "${automation.openclawCron.name}" in ${repair.path}`,
840
+ schedule: automation.openclawCron.schedule,
841
+ timezone: automation.openclawCron.timezone,
842
+ source: repair.path,
843
+ command: editCommand || undefined,
844
+ proof,
845
+ };
846
+ }
847
+ }
690
848
  const add = await runShellCommand(addCommand, 60_000);
849
+ const existingDetail = inspection.exists
850
+ ? `Existing OpenClaw cron job "${automation.openclawCron.name}" was not verifiably wired to the current runner contract (${inspection.reason} via ${inspection.source}). `
851
+ : '';
691
852
  return {
692
- ok: add.ok || normalizedMode === 'auto',
853
+ ok: add.ok || (normalizedMode === 'auto' && !inspection.exists),
693
854
  enabled: true,
694
855
  installed: add.ok,
695
- status: add.ok ? 'configured' : 'failed',
856
+ status: add.ok ? (inspection.exists ? 'reconfigured' : 'configured') : inspection.exists ? 'needs_repair' : 'failed',
696
857
  detail: add.ok
697
- ? `Configured OpenClaw cron job "${automation.openclawCron.name}" (${automation.openclawCron.schedule}, ${automation.openclawCron.timezone})`
698
- : add.stderr.trim() || add.stdout.trim() || `openclaw cron add exited ${add.code}`,
858
+ ? `${existingDetail}Configured OpenClaw cron job "${automation.openclawCron.name}" (${automation.openclawCron.schedule}, ${automation.openclawCron.timezone})`
859
+ : `${existingDetail}${add.stderr.trim() || add.stdout.trim() || `openclaw cron add exited ${add.code}`}`,
699
860
  schedule: automation.openclawCron.schedule,
700
861
  timezone: automation.openclawCron.timezone,
701
862
  command: addCommand,
863
+ remediation: inspection.exists && !add.ok
864
+ ? `Remove the stale OpenClaw cron job named "${automation.openclawCron.name}" with your installed OpenClaw CLI, then rerun: ${addCommand}`
865
+ : undefined,
866
+ proof,
867
+ };
868
+ }
869
+ async function ensureHermesCronSchedule(configPath, config, mode = 'auto') {
870
+ const normalizedMode = validateOpenClawCronMode(mode);
871
+ const automation = getAutomationConfig(config);
872
+ const displayConfigPath = relativeWorkspacePath(configPath);
873
+ const statePath = deriveStatePathFromConfigPath(displayConfigPath);
874
+ const proofPath = deriveSchedulerProofPathFromStatePath(statePath);
875
+ const workdir = path.resolve(automation.hermesCron.workdir || process.cwd());
876
+ const proof = {
877
+ listCommand: 'hermes cron list',
878
+ statusCommand: 'hermes cron status <job-id>',
879
+ stateCommand: `jq '.connectorHealth, .cadences, .lastRunAt, .skippedReason' ${quote(statePath)}`,
880
+ proofCommand: `tail -n 20 ${quote(proofPath)}`,
881
+ };
882
+ if (normalizedMode === 'disable' || automation.hermesCron.enabled === false) {
883
+ return {
884
+ ok: true,
885
+ enabled: false,
886
+ installed: false,
887
+ status: 'disabled',
888
+ detail: 'Hermes cron setup disabled',
889
+ proof,
890
+ };
891
+ }
892
+ const hermesPath = await resolveCommandPath('hermes');
893
+ if (!hermesPath) {
894
+ return {
895
+ ok: normalizedMode === 'auto',
896
+ enabled: true,
897
+ installed: false,
898
+ status: normalizedMode === 'auto' ? 'skipped' : 'failed',
899
+ detail: 'hermes CLI not found on PATH; skipping Hermes cron setup',
900
+ remediation: 'Run this setup inside the host shell where Hermes Gateway is installed, or install the hermes CLI.',
901
+ proof,
902
+ };
903
+ }
904
+ const createCommand = buildHermesCronCreateCommand(displayConfigPath, config, { workdir });
905
+ const inspection = await inspectHermesCronInstall({
906
+ configPath: displayConfigPath,
907
+ config,
908
+ runCommand: runShellCommand,
909
+ readFile: fs.readFile,
910
+ workdir,
911
+ });
912
+ if (inspection.exists && inspection.verified) {
913
+ return {
914
+ ok: true,
915
+ enabled: true,
916
+ installed: true,
917
+ status: 'already_configured_verified',
918
+ detail: `Hermes cron job already exists and matches the Growth Engineer runner contract: ${automation.hermesCron.name}`,
919
+ schedule: automation.hermesCron.schedule,
920
+ workdir,
921
+ source: inspection.source,
922
+ proof,
923
+ };
924
+ }
925
+ const create = await runShellCommand(createCommand, 60_000);
926
+ const existingDetail = inspection.exists
927
+ ? `Existing Hermes cron job "${automation.hermesCron.name}" was not verifiably wired to the current runner contract (${inspection.reason} via ${inspection.source}). `
928
+ : '';
929
+ return {
930
+ ok: create.ok || (normalizedMode === 'auto' && !inspection.exists),
931
+ enabled: true,
932
+ installed: create.ok,
933
+ status: create.ok ? (inspection.exists ? 'reconfigured' : 'configured') : inspection.exists ? 'needs_repair' : 'failed',
934
+ detail: create.ok
935
+ ? `${existingDetail}Configured Hermes cron job "${automation.hermesCron.name}" (${automation.hermesCron.schedule})`
936
+ : `${existingDetail}${create.stderr.trim() || create.stdout.trim() || `hermes cron create exited ${create.code}`}`,
937
+ schedule: automation.hermesCron.schedule,
938
+ workdir,
939
+ command: createCommand,
940
+ remediation: inspection.exists && !create.ok
941
+ ? `Remove the stale Hermes cron job named "${automation.hermesCron.name}" with your installed Hermes CLI, then rerun: ${createCommand}`
942
+ : undefined,
702
943
  proof,
703
944
  };
704
945
  }
@@ -960,6 +1201,13 @@ async function installSentryConnector() {
960
1201
  details.push('Sentry direct API exporter enabled via node scripts/export-sentry-summary.mjs');
961
1202
  return { connector: 'sentry', ok: true, detail: details.join('; ') };
962
1203
  }
1204
+ async function installCoolifyConnector() {
1205
+ return {
1206
+ connector: 'coolify',
1207
+ ok: true,
1208
+ detail: 'Coolify uses the built-in read-only API exporter; create a token in Coolify Keys & Tokens / API tokens and store COOLIFY_BASE_URL plus COOLIFY_API_TOKEN with the connector wizard',
1209
+ };
1210
+ }
963
1211
  async function installGitHubConnector() {
964
1212
  const details = [];
965
1213
  await installClawHubSkill('github', details);
@@ -1000,6 +1248,20 @@ async function installAnalyticsConnector() {
1000
1248
  : 'analyticscli binary missing after dependency setup',
1001
1249
  };
1002
1250
  }
1251
+ function isAccountSignalConnector(connector) {
1252
+ return ACCOUNT_SIGNAL_CONNECTORS.includes(connector);
1253
+ }
1254
+ async function installAccountSignalConnector(connector) {
1255
+ const envs = ACCOUNT_SIGNAL_SECRET_ENVS[connector] || [];
1256
+ const missing = envs.filter((envName) => !String(process.env[envName] || '').trim());
1257
+ return {
1258
+ connector,
1259
+ ok: missing.length === 0,
1260
+ detail: missing.length === 0
1261
+ ? 'account-wide credential is present; project/app/product scope is discovered later'
1262
+ : `missing account-wide credential(s): ${missing.join(', ')}`,
1263
+ };
1264
+ }
1003
1265
  async function enableConnectorConfig(configPath, connectors) {
1004
1266
  if (connectors.length === 0 || !(await fileExists(configPath)))
1005
1267
  return;
@@ -1015,12 +1277,30 @@ async function enableConnectorConfig(configPath, connectors) {
1015
1277
  revenuecat: connectors.includes('revenuecat')
1016
1278
  ? { ...(config.sources?.revenuecat || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('revenuecat', config.sources?.revenuecat) }
1017
1279
  : config.sources?.revenuecat,
1280
+ paddle: connectors.includes('paddle')
1281
+ ? { ...(config.sources?.paddle || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('paddle', config.sources?.paddle) }
1282
+ : config.sources?.paddle,
1283
+ seo: connectors.includes('seo')
1284
+ ? { ...(config.sources?.seo || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('seo', config.sources?.seo) }
1285
+ : config.sources?.seo,
1018
1286
  sentry: connectors.includes('sentry')
1019
1287
  ? { ...(config.sources?.sentry || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('sentry', config.sources?.sentry) }
1020
1288
  : config.sources?.sentry,
1289
+ coolify: connectors.includes('coolify')
1290
+ ? {
1291
+ ...(config.sources?.coolify || {}),
1292
+ enabled: true,
1293
+ mode: 'command',
1294
+ command: normalizeSourceCommand('coolify', config.sources?.coolify),
1295
+ baseUrl: config.sources?.coolify?.baseUrl || process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com',
1296
+ tokenEnv: config.sources?.coolify?.tokenEnv || 'COOLIFY_API_TOKEN',
1297
+ }
1298
+ : config.sources?.coolify,
1021
1299
  extra: extra.map((source) => connectors.includes('asc') && source?.service === 'asc-cli'
1022
1300
  ? { ...source, enabled: true, mode: 'command', command: normalizeSourceCommand('asc', source) }
1023
- : source),
1301
+ : connectors.includes(String(source?.key || source?.service || '')) && isAccountSignalConnector(String(source?.key || source?.service || ''))
1302
+ ? { ...source, enabled: true, mode: source.mode || 'file' }
1303
+ : source),
1024
1304
  },
1025
1305
  };
1026
1306
  await writeJson(configPath, next);
@@ -1037,8 +1317,16 @@ async function installConnectorHelpers(configPath, connectors) {
1037
1317
  results.push(await installAscConnector());
1038
1318
  if (connector === 'revenuecat')
1039
1319
  results.push(await installRevenueCatConnector());
1320
+ if (connector === 'paddle')
1321
+ results.push({ connector, ok: true, detail: 'Paddle uses the built-in metrics exporter; token is checked during preflight' });
1322
+ if (connector === 'seo')
1323
+ results.push({ connector, ok: true, detail: 'SEO/GSC uses the built-in exporter; credentials or CSV inputs are checked during preflight' });
1040
1324
  if (connector === 'sentry')
1041
1325
  results.push(await installSentryConnector());
1326
+ if (connector === 'coolify')
1327
+ results.push(await installCoolifyConnector());
1328
+ if (isAccountSignalConnector(connector))
1329
+ results.push(await installAccountSignalConnector(connector));
1042
1330
  }
1043
1331
  return results;
1044
1332
  }
@@ -1127,6 +1415,14 @@ async function ensureConfig(configPath) {
1127
1415
  mode: 'command',
1128
1416
  command: getRuntimeSourceCommand('sentry'),
1129
1417
  },
1418
+ coolify: {
1419
+ ...(template.sources?.coolify || {}),
1420
+ enabled: false,
1421
+ mode: 'command',
1422
+ command: getRuntimeSourceCommand('coolify'),
1423
+ baseUrl: template.sources?.coolify?.baseUrl || process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com',
1424
+ tokenEnv: template.sources?.coolify?.tokenEnv || 'COOLIFY_API_TOKEN',
1425
+ },
1130
1426
  feedback: {
1131
1427
  ...(template.sources?.feedback || {}),
1132
1428
  enabled: false,
@@ -1218,6 +1514,15 @@ function appendProjectFlag(command, projectId) {
1218
1514
  function commandHasAscAppFlag(command) {
1219
1515
  return /(^|\s)--app(\s|=|$)/.test(String(command || ''));
1220
1516
  }
1517
+ function removeAscAppFlag(command) {
1518
+ const raw = String(command || '').trim();
1519
+ if (!raw || !commandHasAscAppFlag(raw))
1520
+ return raw;
1521
+ return raw
1522
+ .replace(/(^|\s)--app(?:=(?:"[^"]*"|'[^']*'|\S+)|\s+(?:"[^"]*"|'[^']*'|\S+))/g, ' ')
1523
+ .replace(/\s+/g, ' ')
1524
+ .trim();
1525
+ }
1221
1526
  function appendAscAppFlag(command, appId) {
1222
1527
  const raw = String(command || '').trim();
1223
1528
  if (!raw || commandHasAscAppFlag(raw))
@@ -1287,6 +1592,34 @@ async function configureAscApp(configPath, appId) {
1287
1592
  process.env.ASC_APP_ID = normalizedAppId;
1288
1593
  return changed;
1289
1594
  }
1595
+ async function configureAscAllApps(configPath) {
1596
+ const config = await readJson(configPath);
1597
+ let changed = false;
1598
+ const extraSources = Array.isArray(config?.sources?.extra) ? config.sources.extra : [];
1599
+ for (const source of extraSources) {
1600
+ if (!source || typeof source !== 'object')
1601
+ continue;
1602
+ const service = String(source.service || source.key || '').trim().toLowerCase();
1603
+ if (!['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(service))
1604
+ continue;
1605
+ if (source.mode === 'command' && source.command) {
1606
+ const nextCommand = removeAscAppFlag(source.command);
1607
+ if (nextCommand !== source.command) {
1608
+ source.command = nextCommand;
1609
+ changed = true;
1610
+ }
1611
+ }
1612
+ }
1613
+ if (config.project && typeof config.project === 'object' && config.project.ascAppId) {
1614
+ delete config.project.ascAppId;
1615
+ changed = true;
1616
+ }
1617
+ if (changed) {
1618
+ await writeJson(configPath, config);
1619
+ }
1620
+ delete process.env.ASC_APP_ID;
1621
+ return changed;
1622
+ }
1290
1623
  function configHasEnabledAscSource(config) {
1291
1624
  const extraSources = Array.isArray(config?.sources?.extra) ? config.sources.extra : [];
1292
1625
  return extraSources.some((source) => {
@@ -1362,11 +1695,7 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
1362
1695
  if (!configHasEnabledAscSource(config)) {
1363
1696
  return { ok: true, configured: false, changed: false, appId: null, appScope: 'disabled', needsUserInput: false };
1364
1697
  }
1365
- const configuredAppId = normalizeString(config.project?.ascAppId) || normalizeString(process.env.ASC_APP_ID);
1366
- if (configuredAppId) {
1367
- const changed = await configureAscApp(configPath, configuredAppId);
1368
- return { ok: true, configured: true, changed, appId: configuredAppId, appScope: 'single_app', needsUserInput: false };
1369
- }
1698
+ const changed = await configureAscAllApps(configPath);
1370
1699
  const appList = await listAscApps();
1371
1700
  if (!appList.ok) {
1372
1701
  return {
@@ -1381,7 +1710,7 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
1381
1710
  return {
1382
1711
  ok: true,
1383
1712
  configured: true,
1384
- changed: false,
1713
+ changed,
1385
1714
  appId: null,
1386
1715
  appScope: 'all_accessible_apps',
1387
1716
  apps: appList.apps,
@@ -1389,6 +1718,131 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
1389
1718
  needsUserInput: false,
1390
1719
  };
1391
1720
  }
1721
+ function extractAscAnalyticsRequestIds(payload) {
1722
+ const candidates = (() => {
1723
+ if (Array.isArray(payload))
1724
+ return payload;
1725
+ if (payload && typeof payload === 'object') {
1726
+ if (Array.isArray(payload.requests))
1727
+ return payload.requests;
1728
+ if (Array.isArray(payload.analyticsReportRequests))
1729
+ return payload.analyticsReportRequests;
1730
+ if (Array.isArray(payload.items))
1731
+ return payload.items;
1732
+ if (Array.isArray(payload.data))
1733
+ return payload.data;
1734
+ }
1735
+ return [];
1736
+ })();
1737
+ const ids = [];
1738
+ for (const candidate of candidates) {
1739
+ if (!candidate || typeof candidate !== 'object')
1740
+ continue;
1741
+ const id = normalizeString(candidate.id) || normalizeString(candidate.requestId) || normalizeString(candidate.request_id);
1742
+ if (id)
1743
+ ids.push(id);
1744
+ }
1745
+ return [...new Set(ids)];
1746
+ }
1747
+ function extractAscAnalyticsRequestId(payload) {
1748
+ if (payload && typeof payload === 'object') {
1749
+ const id = normalizeString(payload.id) || normalizeString(payload.requestId) || normalizeString(payload.request_id);
1750
+ if (id)
1751
+ return id;
1752
+ }
1753
+ return extractAscAnalyticsRequestIds(payload)[0] || null;
1754
+ }
1755
+ async function listAscAnalyticsRequests(appId, state = '') {
1756
+ const stateArg = state ? ` --state ${quote(state)}` : '';
1757
+ const result = await runShellCommand(`asc analytics requests --app ${quote(appId)}${stateArg} --output json`, 60_000);
1758
+ if (!result.ok) {
1759
+ return { ok: false, ids: [], error: result.stderr || `exit ${result.code}` };
1760
+ }
1761
+ return { ok: true, ids: extractAscAnalyticsRequestIds(parseJsonFromStdout(result.stdout)), error: null };
1762
+ }
1763
+ async function ensureAscAnalyticsRequest(appId) {
1764
+ const normalizedAppId = normalizeString(appId);
1765
+ if (!normalizedAppId) {
1766
+ return { ok: true, status: 'skipped', requestId: null, detail: 'no single ASC app configured' };
1767
+ }
1768
+ const completedRequests = await listAscAnalyticsRequests(normalizedAppId, 'COMPLETED');
1769
+ if (!completedRequests.ok) {
1770
+ return { ok: false, status: 'query_failed', requestId: null, error: completedRequests.error };
1771
+ }
1772
+ if (completedRequests.ids.length > 0) {
1773
+ return { ok: true, status: 'completed', requestId: completedRequests.ids[0], detail: `completed request ${completedRequests.ids[0]}` };
1774
+ }
1775
+ const existingRequests = await listAscAnalyticsRequests(normalizedAppId);
1776
+ if (!existingRequests.ok) {
1777
+ return { ok: false, status: 'query_failed', requestId: null, error: existingRequests.error };
1778
+ }
1779
+ if (existingRequests.ids.length > 0) {
1780
+ return { ok: true, status: 'pending', requestId: existingRequests.ids[0], detail: `existing request ${existingRequests.ids[0]} is not completed yet` };
1781
+ }
1782
+ const created = await runShellCommand(`asc analytics request --app ${quote(normalizedAppId)} --access-type ONGOING --output json`, 60_000);
1783
+ if (!created.ok) {
1784
+ return { ok: false, status: 'create_failed', requestId: null, error: created.stderr || `exit ${created.code}` };
1785
+ }
1786
+ const requestId = extractAscAnalyticsRequestId(parseJsonFromStdout(created.stdout));
1787
+ return {
1788
+ ok: true,
1789
+ status: 'created',
1790
+ requestId,
1791
+ detail: requestId
1792
+ ? `created ongoing request ${requestId}; report instances will appear after Apple processing`
1793
+ : 'created ongoing request; report instances will appear after Apple processing',
1794
+ };
1795
+ }
1796
+ async function ensureAscAnalyticsRequestsForAppScope(ascAppSetup) {
1797
+ if (!ascAppSetup?.ok || ascAppSetup.appScope === 'disabled') {
1798
+ return { ok: true, status: 'skipped', requestId: null, requestIds: [], detail: 'ASC source is not enabled', results: [] };
1799
+ }
1800
+ const apps = ascAppSetup.appId
1801
+ ? [{ id: ascAppSetup.appId }]
1802
+ : Array.isArray(ascAppSetup.apps)
1803
+ ? ascAppSetup.apps.filter((app) => normalizeString(app?.id))
1804
+ : [];
1805
+ if (apps.length === 0) {
1806
+ return { ok: true, status: 'skipped', requestId: null, requestIds: [], detail: 'no accessible ASC apps found', results: [] };
1807
+ }
1808
+ const results = [];
1809
+ for (const app of apps) {
1810
+ const result = await ensureAscAnalyticsRequest(app.id);
1811
+ results.push({
1812
+ appId: app.id,
1813
+ appName: app.name || null,
1814
+ ...result,
1815
+ });
1816
+ }
1817
+ const failures = results.filter((result) => !result.ok);
1818
+ if (failures.length > 0) {
1819
+ return {
1820
+ ok: false,
1821
+ status: failures.length === results.length ? 'failed' : 'partial_failed',
1822
+ requestId: null,
1823
+ requestIds: results.map((result) => result.requestId).filter(Boolean),
1824
+ results,
1825
+ error: failures
1826
+ .map((failure) => `${failure.appName || failure.appId}: ${failure.error || failure.status || 'unknown error'}`)
1827
+ .join('; '),
1828
+ };
1829
+ }
1830
+ const counts = results.reduce((memo, result) => {
1831
+ memo[result.status] = (memo[result.status] || 0) + 1;
1832
+ return memo;
1833
+ }, {});
1834
+ const detail = Object.entries(counts)
1835
+ .map(([status, count]) => `${count} ${status}`)
1836
+ .join(', ');
1837
+ return {
1838
+ ok: true,
1839
+ status: 'ok',
1840
+ requestId: results[0]?.requestId || null,
1841
+ requestIds: results.map((result) => result.requestId).filter(Boolean),
1842
+ detail: `checked ${results.length} ASC app(s): ${detail}`,
1843
+ results,
1844
+ };
1845
+ }
1392
1846
  async function listAnalyticsProjects() {
1393
1847
  const result = await runShellCommand('analyticscli projects list --format json', 60_000);
1394
1848
  if (!result.ok) {
@@ -1482,7 +1936,7 @@ function remediationForCheck(checkName, configPath) {
1482
1936
  return 'Write `data/openclaw-growth-engineer/analytics_summary.json` via your analytics refresh step (API-key based source command/file generation).';
1483
1937
  }
1484
1938
  if (checkName === 'connection:analytics') {
1485
- return 'Run `node scripts/openclaw-growth-wizard.mjs --connectors analytics` and paste a fresh AnalyticsCLI readonly CLI token into the local terminal wizard.';
1939
+ return 'Run `npx -y @analyticscli/growth-engineer@preview wizard --connectors analytics` and paste a fresh AnalyticsCLI readonly CLI token into the local terminal wizard.';
1486
1940
  }
1487
1941
  if (checkName === 'connection:github') {
1488
1942
  return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>` + issues API.';
@@ -1491,7 +1945,7 @@ function remediationForCheck(checkName, configPath) {
1491
1945
  return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>/pulls`, plus `Pull requests: Read/Write` and `Contents: Read/Write` scopes.';
1492
1946
  }
1493
1947
  if (checkName === 'connection:asc_cli') {
1494
- return 'ASC setup should list App Store Connect apps and persist the selected app automatically. Rerun the connector wizard; if this repeats, update the skill/CLI rather than setting ASC_APP_ID by hand.';
1948
+ return 'ASC setup should list all App Store Connect apps the API key can access. Rerun the connector wizard with an API key scoped to the whole account; do not set ASC_APP_ID for normal Growth Engineer runs.';
1495
1949
  }
1496
1950
  return 'Fix this blocker and rerun start.';
1497
1951
  }
@@ -1556,6 +2010,7 @@ async function main() {
1556
2010
  await applyOpenClawSecretRefs(initialConfig);
1557
2011
  const heartbeat = await ensureGrowthHeartbeat(configPath, initialConfig);
1558
2012
  const openclawCron = await ensureOpenClawCronSchedule(configPath, initialConfig, args.openclawCron);
2013
+ const hermesCron = await ensureHermesCronSchedule(configPath, initialConfig, args.openclawCron);
1559
2014
  if (!openclawCron.ok) {
1560
2015
  process.stdout.write(`${JSON.stringify({
1561
2016
  ok: false,
@@ -1564,6 +2019,7 @@ async function main() {
1564
2019
  configPath,
1565
2020
  heartbeat,
1566
2021
  openclawCron,
2022
+ hermesCron,
1567
2023
  blockers: [
1568
2024
  {
1569
2025
  check: 'scheduler:openclaw-cron',
@@ -1575,6 +2031,26 @@ async function main() {
1575
2031
  process.exitCode = 1;
1576
2032
  return;
1577
2033
  }
2034
+ if (!hermesCron.ok) {
2035
+ process.stdout.write(`${JSON.stringify({
2036
+ ok: false,
2037
+ phase: 'hermes_cron_setup',
2038
+ configCreated: configResult.created,
2039
+ configPath,
2040
+ heartbeat,
2041
+ openclawCron,
2042
+ hermesCron,
2043
+ blockers: [
2044
+ {
2045
+ check: 'scheduler:hermes-cron',
2046
+ detail: hermesCron.detail,
2047
+ remediation: hermesCron.remediation || 'Fix Hermes cron setup and rerun start.',
2048
+ },
2049
+ ],
2050
+ }, null, 2)}\n`);
2051
+ process.exitCode = 1;
2052
+ return;
2053
+ }
1578
2054
  const projectConfigured = await configureAnalyticsProject(configPath, args.project);
1579
2055
  const ascAppConfiguredFromArg = await configureAscApp(configPath, args.ascApp);
1580
2056
  const analyticscliEnsure = await ensureAnalyticsCliInstalled();
@@ -1586,6 +2062,7 @@ async function main() {
1586
2062
  configPath,
1587
2063
  heartbeat,
1588
2064
  openclawCron,
2065
+ hermesCron,
1589
2066
  projectConfigured,
1590
2067
  ascAppConfigured: ascAppConfiguredFromArg,
1591
2068
  blockers: [
@@ -1628,6 +2105,7 @@ async function main() {
1628
2105
  configPath,
1629
2106
  heartbeat,
1630
2107
  openclawCron,
2108
+ hermesCron,
1631
2109
  projectConfigured,
1632
2110
  ascAppConfigured: ascAppConfiguredFromArg,
1633
2111
  connectorSetup,
@@ -1642,7 +2120,9 @@ async function main() {
1642
2120
  ? 'Install the ASC CLI and provide ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY_PATH or ASC_PRIVATE_KEY. Resolve the app after auth succeeds.'
1643
2121
  : entry.connector === 'sentry'
1644
2122
  ? 'Set SENTRY_AUTH_TOKEN plus SENTRY_ORG in the connector wizard. Defer project scope to app/repo context, or configure sources.sentry.accounts[].projects[] only when a fixed mapping is known.'
1645
- : 'Set REVENUECAT_API_KEY and rerun connector setup to write RevenueCat MCP config.',
2123
+ : entry.connector === 'coolify'
2124
+ ? 'Set COOLIFY_BASE_URL and COOLIFY_API_TOKEN from Coolify Keys & Tokens / API tokens in the connector wizard.'
2125
+ : 'Set REVENUECAT_API_KEY and rerun connector setup to write RevenueCat MCP config.',
1646
2126
  })),
1647
2127
  }, null, 2)}\n`);
1648
2128
  process.exitCode = 1;
@@ -1696,6 +2176,7 @@ async function main() {
1696
2176
  configPath,
1697
2177
  heartbeat,
1698
2178
  openclawCron,
2179
+ hermesCron,
1699
2180
  projectConfigured: projectConfigured || analyticsProjectSetup.configured,
1700
2181
  analyticsProjectId: analyticsProjectSetup.projectId || null,
1701
2182
  ascAppConfigured: false,
@@ -1715,6 +2196,51 @@ async function main() {
1715
2196
  process.exitCode = 1;
1716
2197
  return;
1717
2198
  }
2199
+ emitProgress(args.progressJson, {
2200
+ phase: 'start',
2201
+ key: 'ascAnalyticsRequest',
2202
+ label: 'ASC analytics reports',
2203
+ detail: 'checking ongoing Analytics Report Request',
2204
+ });
2205
+ const ascAnalyticsRequestSetup = await ensureAscAnalyticsRequestsForAppScope(ascAppSetup);
2206
+ emitProgress(args.progressJson, {
2207
+ phase: 'finish',
2208
+ key: 'ascAnalyticsRequest',
2209
+ label: 'ASC analytics reports',
2210
+ detail: ascAnalyticsRequestSetup.ok
2211
+ ? ascAnalyticsRequestSetup.detail
2212
+ : `could not ensure Analytics Report Request (${truncate(ascAnalyticsRequestSetup.error, 240)})`,
2213
+ status: ascAnalyticsRequestSetup.ok ? 'pass' : 'fail',
2214
+ });
2215
+ if (!ascAnalyticsRequestSetup.ok) {
2216
+ process.stdout.write(`${JSON.stringify({
2217
+ ok: false,
2218
+ phase: 'asc_analytics_request_setup',
2219
+ configCreated: configResult.created,
2220
+ configPath,
2221
+ heartbeat,
2222
+ openclawCron,
2223
+ hermesCron,
2224
+ projectConfigured: projectConfigured || analyticsProjectSetup.configured,
2225
+ analyticsProjectId: analyticsProjectSetup.projectId || null,
2226
+ ascAppConfigured: ascAppSetup.configured,
2227
+ ascAppId: ascAppSetup.appId || null,
2228
+ ascAppScope: ascAppSetup.appScope || null,
2229
+ ascAnalyticsRequestResults: ascAnalyticsRequestSetup.results || [],
2230
+ connectorSetup,
2231
+ needsUserInput: false,
2232
+ question: null,
2233
+ blockers: [
2234
+ {
2235
+ check: 'connection:asc_analytics_request',
2236
+ detail: `Could not ensure App Store Connect Analytics Report Request: ${truncate(ascAnalyticsRequestSetup.error, 800)}`,
2237
+ remediation: 'Use an ASC API key with Admin for first setup so Growth Engineer can create the ongoing Analytics Report Request. After the request exists, rotate to Sales and Reports for steady-state downloads.',
2238
+ },
2239
+ ],
2240
+ }, null, 2)}\n`);
2241
+ process.exitCode = 1;
2242
+ return;
2243
+ }
1718
2244
  const preflightResult = await runPreflight(configPath, args.testConnections, args.progressJson, args.onlyConnectors);
1719
2245
  const preflightPayload = preflightResult.payload;
1720
2246
  if (!preflightPayload) {
@@ -1736,11 +2262,15 @@ async function main() {
1736
2262
  configPath,
1737
2263
  heartbeat,
1738
2264
  openclawCron,
2265
+ hermesCron,
1739
2266
  projectConfigured: projectConfigured || analyticsProjectSetup.configured,
1740
2267
  analyticsProjectId: analyticsProjectSetup.projectId || null,
1741
2268
  ascAppConfigured: ascAppSetup.configured,
1742
2269
  ascAppId: ascAppSetup.appId || null,
1743
2270
  ascAppScope: ascAppSetup.appScope || null,
2271
+ ascAnalyticsRequestStatus: ascAnalyticsRequestSetup.status,
2272
+ ascAnalyticsRequestId: ascAnalyticsRequestSetup.requestId || null,
2273
+ ascAnalyticsRequestIds: ascAnalyticsRequestSetup.requestIds || [],
1744
2274
  githubRepo: configResult.githubRepo,
1745
2275
  connectorSetup,
1746
2276
  checks: preflightPayload.checks || [],
@@ -1757,11 +2287,15 @@ async function main() {
1757
2287
  configPath,
1758
2288
  heartbeat,
1759
2289
  openclawCron,
2290
+ hermesCron,
1760
2291
  projectConfigured: projectConfigured || analyticsProjectSetup.configured,
1761
2292
  analyticsProjectId: analyticsProjectSetup.projectId || null,
1762
2293
  ascAppConfigured: ascAppSetup.configured,
1763
2294
  ascAppId: ascAppSetup.appId || null,
1764
2295
  ascAppScope: ascAppSetup.appScope || null,
2296
+ ascAnalyticsRequestStatus: ascAnalyticsRequestSetup.status,
2297
+ ascAnalyticsRequestId: ascAnalyticsRequestSetup.requestId || null,
2298
+ ascAnalyticsRequestIds: ascAnalyticsRequestSetup.requestIds || [],
1765
2299
  connectorSetup,
1766
2300
  schedulerProofPath,
1767
2301
  message: 'Preflight passed. First run skipped due to --setup-only.',
@@ -1787,6 +2321,7 @@ async function main() {
1787
2321
  configPath,
1788
2322
  heartbeat,
1789
2323
  openclawCron,
2324
+ hermesCron,
1790
2325
  projectConfigured,
1791
2326
  error: rawError,
1792
2327
  }, null, 2)}\n`);
@@ -1801,6 +2336,7 @@ async function main() {
1801
2336
  configPath,
1802
2337
  heartbeat,
1803
2338
  openclawCron,
2339
+ hermesCron,
1804
2340
  projectConfigured,
1805
2341
  actionMode,
1806
2342
  runnerOutput: runResult.stdout.trim(),