@analyticscli/growth-engineer 0.1.0-preview.8 → 0.1.0

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 +925 -45
  2. package/dist/config.js +58 -6
  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 +295 -4
  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 +51 -0
  20. package/dist/runtime/openclaw-exporters-lib.mjs +769 -63
  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 +446 -30
  27. package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
  28. package/dist/runtime/openclaw-growth-runner.mjs +847 -150
  29. package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
  30. package/dist/runtime/openclaw-growth-shared.d.mts +158 -3
  31. package/dist/runtime/openclaw-growth-shared.mjs +574 -8
  32. package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
  33. package/dist/runtime/openclaw-growth-start.mjs +816 -41
  34. package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
  35. package/dist/runtime/openclaw-growth-status.mjs +100 -34
  36. package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
  37. package/dist/runtime/openclaw-growth-wizard.mjs +1997 -226
  38. package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
  39. package/package.json +3 -1
  40. package/templates/config.example.json +128 -65
@@ -3,16 +3,68 @@ import { existsSync, promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
5
  import { spawn } from 'node:child_process';
6
- import { getActionMode, getDefaultSourceCommand } from './openclaw-growth-shared.mjs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { buildGrowthRunnerCommand, buildOpenClawCronAddCommand, deriveSchedulerProofPathFromStatePath, deriveStatePathFromConfigPath, getActionMode, getAutomationConfig, getDefaultSourceCommand, getOpenClawCronEditDeliveryCommandFromInspection, buildHermesCronCreateCommand, inspectHermesCronInstall, inspectOpenClawCronInstall, repairOpenClawCronDeliveryStore, } from './openclaw-growth-shared.mjs';
7
8
  import { applyOpenClawSecretRefs, loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
8
9
  const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
9
10
  const DEFAULT_TEMPLATE_PATH = 'data/openclaw-growth-engineer/config.example.json';
10
11
  const DEFAULT_HEARTBEAT_PATH = 'HEARTBEAT.md';
12
+ const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
11
13
  const HEARTBEAT_MARKER_START = '<!-- openclaw-growth-engineer:start -->';
12
14
  const HEARTBEAT_MARKER_END = '<!-- openclaw-growth-engineer:end -->';
13
15
  const ANALYTICSCLI_PACKAGE_SPEC = process.env.ANALYTICSCLI_CLI_PACKAGE || '@analyticscli/cli@preview';
14
16
  const ANALYTICSCLI_NPM_PREFIX = process.env.ANALYTICSCLI_NPM_PREFIX ||
15
17
  (process.env.HOME ? path.join(process.env.HOME, '.local') : path.join(process.cwd(), '.analyticscli-npm'));
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
+ };
16
68
  function printHelpAndExit(exitCode, reason = null) {
17
69
  if (reason) {
18
70
  process.stderr.write(`${reason}\n\n`);
@@ -32,25 +84,45 @@ Options:
32
84
  --config <file> Config path (default: ${DEFAULT_CONFIG_PATH})
33
85
  --project <id> Optional AnalyticsCLI project ID pin for generated source commands
34
86
  --asc-app <id> Optional ASC app ID filter (defaults to all accessible apps)
35
- --connectors <list> Install/enable connector helpers (analytics,github,asc,revenuecat,sentry,all)
87
+ --connectors <list> Install/enable connector helpers (${SUPPORTED_CONNECTORS.join(',')},all)
36
88
  --only-connectors <list>
37
- Limit live preflight checks to analytics,github,asc,revenuecat,sentry
89
+ Limit live preflight checks to ${SUPPORTED_CONNECTORS.join(',')}
38
90
  --setup-only Run bootstrap + preflight only (skip first run)
39
91
  --no-test-connections Skip live API smoke checks in preflight
92
+ --openclaw-cron <mode> Configure OpenClaw Gateway cron: auto, enable, require, disable (default: auto)
93
+ --openclaw-cron-schedule <expr>
94
+ Cron expression for OpenClaw Gateway cron (default: */30 * * * *)
95
+ --openclaw-cron-tz <tz>
96
+ Timezone for OpenClaw Gateway cron (default: TZ or UTC)
40
97
  --progress-json Emit machine-readable setup progress to stderr
41
98
  --help, -h Show help
42
99
  `);
43
100
  process.exit(exitCode);
44
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
+ }
45
114
  function parseArgs(argv) {
46
115
  const args = {
47
- config: DEFAULT_CONFIG_PATH,
116
+ config: resolveDefaultConfigPath(),
48
117
  project: '',
49
118
  ascApp: '',
50
119
  run: true,
51
120
  testConnections: true,
52
121
  connectors: [],
53
122
  onlyConnectors: [],
123
+ openclawCron: String(process.env.OPENCLAW_GROWTH_OPENCLAW_CRON || 'auto').trim().toLowerCase(),
124
+ openclawCronSchedule: '',
125
+ openclawCronTimezone: '',
54
126
  progressJson: false,
55
127
  };
56
128
  for (let i = 0; i < argv.length; i += 1) {
@@ -85,6 +157,24 @@ function parseArgs(argv) {
85
157
  else if (token === '--no-test-connections') {
86
158
  args.testConnections = false;
87
159
  }
160
+ else if (token === '--openclaw-cron') {
161
+ args.openclawCron = String(next || '').trim().toLowerCase() || 'auto';
162
+ i += 1;
163
+ }
164
+ else if (token === '--enable-openclaw-cron') {
165
+ args.openclawCron = 'enable';
166
+ }
167
+ else if (token === '--no-openclaw-cron') {
168
+ args.openclawCron = 'disable';
169
+ }
170
+ else if (token === '--openclaw-cron-schedule') {
171
+ args.openclawCronSchedule = String(next || '').trim();
172
+ i += 1;
173
+ }
174
+ else if (token === '--openclaw-cron-tz') {
175
+ args.openclawCronTimezone = String(next || '').trim();
176
+ i += 1;
177
+ }
88
178
  else if (token === '--progress-json') {
89
179
  args.progressJson = true;
90
180
  }
@@ -95,8 +185,15 @@ function parseArgs(argv) {
95
185
  printHelpAndExit(1, `Unknown argument: ${token}`);
96
186
  }
97
187
  }
188
+ args.openclawCron = validateOpenClawCronMode(args.openclawCron);
98
189
  return args;
99
190
  }
191
+ function validateOpenClawCronMode(value) {
192
+ const mode = String(value || 'auto').trim().toLowerCase();
193
+ if (['auto', 'enable', 'require', 'disable'].includes(mode))
194
+ return mode;
195
+ printHelpAndExit(1, `Invalid --openclaw-cron mode: ${value}. Use auto, enable, require, or disable.`);
196
+ }
100
197
  function normalizeConnectorKey(value) {
101
198
  const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
102
199
  if (!normalized)
@@ -111,8 +208,58 @@ function normalizeConnectorKey(value) {
111
208
  return 'asc';
112
209
  if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
113
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';
114
215
  if (['sentry', 'sentry-api', 'sentry-mcp', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
115
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';
116
263
  return null;
117
264
  }
118
265
  function parseConnectorList(value) {
@@ -122,14 +269,10 @@ function parseConnectorList(value) {
122
269
  for (const entry of String(value).split(',')) {
123
270
  const connector = normalizeConnectorKey(entry);
124
271
  if (!connector) {
125
- 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.`);
126
273
  }
127
274
  if (connector === 'all') {
128
- connectors.add('analytics');
129
- connectors.add('github');
130
- connectors.add('asc');
131
- connectors.add('revenuecat');
132
- connectors.add('sentry');
275
+ SUPPORTED_CONNECTORS.forEach((supported) => connectors.add(supported));
133
276
  }
134
277
  else {
135
278
  connectors.add(connector);
@@ -143,6 +286,83 @@ function quote(value) {
143
286
  }
144
287
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
145
288
  }
289
+ function resolveRuntimeScriptPath(scriptName) {
290
+ const candidates = [
291
+ path.join(RUNTIME_DIR, scriptName),
292
+ path.resolve('scripts', scriptName),
293
+ path.resolve('skills/openclaw-growth-engineer/scripts', scriptName),
294
+ ];
295
+ return candidates.find((candidate) => existsSync(candidate)) || path.join(RUNTIME_DIR, scriptName);
296
+ }
297
+ function nodeRuntimeScriptCommand(scriptName) {
298
+ return `node ${quote(resolveRuntimeScriptPath(scriptName))}`;
299
+ }
300
+ function getRuntimeSourceCommand(sourceName) {
301
+ const normalized = String(sourceName || '').trim().toLowerCase();
302
+ if (normalized === 'analytics' || normalized === 'analyticscli') {
303
+ return nodeRuntimeScriptCommand('export-analytics-summary.mjs');
304
+ }
305
+ if (normalized === 'revenuecat' || normalized === 'revenue-cat') {
306
+ return nodeRuntimeScriptCommand('export-revenuecat-summary.mjs');
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
+ }
314
+ if (normalized === 'sentry' || normalized === 'glitchtip') {
315
+ return nodeRuntimeScriptCommand('export-sentry-summary.mjs');
316
+ }
317
+ if (normalized === 'coolify') {
318
+ return nodeRuntimeScriptCommand('export-coolify-summary.mjs');
319
+ }
320
+ if (['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(normalized)) {
321
+ return nodeRuntimeScriptCommand('export-asc-summary.mjs');
322
+ }
323
+ return getDefaultSourceCommand(sourceName);
324
+ }
325
+ function replaceLegacyRuntimeScriptCommand(command) {
326
+ const trimmed = String(command || '').trim();
327
+ if (!trimmed)
328
+ return trimmed;
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));
330
+ }
331
+ function normalizeSourceCommand(sourceName, source) {
332
+ return replaceLegacyRuntimeScriptCommand(source?.command || '') || getRuntimeSourceCommand(sourceName);
333
+ }
334
+ function migrateRuntimeSourceCommands(config) {
335
+ if (!config || typeof config !== 'object')
336
+ return config;
337
+ const sources = config.sources && typeof config.sources === 'object' ? config.sources : {};
338
+ const nextSources = { ...sources };
339
+ for (const sourceName of ['analytics', 'revenuecat', 'paddle', 'seo', 'sentry', 'coolify']) {
340
+ if (nextSources[sourceName]?.mode === 'command') {
341
+ nextSources[sourceName] = {
342
+ ...nextSources[sourceName],
343
+ command: normalizeSourceCommand(sourceName, nextSources[sourceName]),
344
+ };
345
+ }
346
+ }
347
+ if (Array.isArray(nextSources.extra)) {
348
+ nextSources.extra = nextSources.extra.map((source) => {
349
+ if (!source || source.mode !== 'command')
350
+ return source;
351
+ const service = String(source.service || source.key || '').toLowerCase();
352
+ const sourceName = ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(service)
353
+ ? 'asc'
354
+ : service;
355
+ return {
356
+ ...source,
357
+ command: normalizeSourceCommand(sourceName, source),
358
+ };
359
+ });
360
+ }
361
+ return {
362
+ ...config,
363
+ sources: nextSources,
364
+ };
365
+ }
146
366
  function truncate(value, max = 240) {
147
367
  const text = String(value || '');
148
368
  if (text.length <= max)
@@ -171,10 +391,19 @@ function emitProgress(enabled, event) {
171
391
  return;
172
392
  process.stderr.write(`OPENCLAW_PROGRESS ${JSON.stringify(event)}\n`);
173
393
  }
394
+ function hardenUnattendedShellCommand(command) {
395
+ return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
396
+ }
174
397
  function runShellCommand(command, timeoutMs = 120_000, options = {}) {
175
398
  return new Promise((resolve) => {
176
- const child = spawn(resolveShellCommand(), ['-c', command], {
399
+ const child = spawn(resolveShellCommand(), ['-c', hardenUnattendedShellCommand(command)], {
177
400
  stdio: ['ignore', 'pipe', 'pipe'],
401
+ env: {
402
+ ...process.env,
403
+ DEBIAN_FRONTEND: 'noninteractive',
404
+ SUDO_ASKPASS: '/bin/false',
405
+ SUDO_PROMPT: '',
406
+ },
178
407
  });
179
408
  let stdout = '';
180
409
  let stderr = '';
@@ -455,12 +684,15 @@ function isEffectivelyEmptyHeartbeat(value) {
455
684
  function renderHeartbeatBlock(configPath, config) {
456
685
  const interval = formatHeartbeatInterval(getHeartbeatInterval(config));
457
686
  const displayConfigPath = relativeWorkspacePath(configPath);
687
+ const displayStatePath = deriveStatePathFromConfigPath(displayConfigPath);
688
+ const runnerCommand = buildGrowthRunnerCommand(displayConfigPath, displayStatePath);
689
+ const wizardCommand = 'npx -y @analyticscli/growth-engineer@preview wizard --connectors';
458
690
  return `${HEARTBEAT_MARKER_START}
459
691
  tasks:
460
692
 
461
693
  - name: openclaw-growth-engineer-run
462
694
  interval: ${interval}
463
- prompt: "Run \`node scripts/openclaw-growth-runner.mjs --config ${displayConfigPath}\` 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\`. 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."
464
696
 
465
697
  # Keep this section small. Do not put secrets in HEARTBEAT.md.
466
698
  ${HEARTBEAT_MARKER_END}`;
@@ -498,6 +730,219 @@ async function ensureGrowthHeartbeat(configPath, config) {
498
730
  updated: false,
499
731
  };
500
732
  }
733
+ function applyOpenClawCronOverrides(config, args) {
734
+ const automation = getAutomationConfig(config);
735
+ const cron = {
736
+ ...automation.openclawCron,
737
+ ...(args.openclawCronSchedule ? { schedule: args.openclawCronSchedule } : {}),
738
+ ...(args.openclawCronTimezone ? { timezone: args.openclawCronTimezone } : {}),
739
+ };
740
+ return {
741
+ ...config,
742
+ automation: {
743
+ ...(config?.automation || {}),
744
+ openclawCron: cron,
745
+ },
746
+ };
747
+ }
748
+ function getOpenClawCronProofCommands(configPath) {
749
+ const displayConfigPath = relativeWorkspacePath(configPath);
750
+ const statePath = deriveStatePathFromConfigPath(displayConfigPath);
751
+ const proofPath = deriveSchedulerProofPathFromStatePath(statePath);
752
+ return {
753
+ listCommand: 'openclaw cron list',
754
+ runsCommand: 'openclaw cron runs --id <job-id>',
755
+ stateCommand: `jq '.connectorHealth, .cadences, .lastRunAt, .skippedReason' ${quote(statePath)}`,
756
+ proofCommand: `tail -n 20 ${quote(proofPath)}`,
757
+ manualWakeCommand: `openclaw system event --text ${quote(`Run OpenClaw Growth Engineer now using config ${relativeWorkspacePath(configPath)} and inspect scheduler proof.`)} --mode now`,
758
+ };
759
+ }
760
+ async function ensureOpenClawCronSchedule(configPath, config, mode = 'auto') {
761
+ const normalizedMode = validateOpenClawCronMode(mode);
762
+ const automation = getAutomationConfig(config);
763
+ const proof = getOpenClawCronProofCommands(configPath);
764
+ if (normalizedMode === 'disable' || automation.openclawCron.enabled === false) {
765
+ return {
766
+ ok: true,
767
+ enabled: false,
768
+ installed: false,
769
+ status: 'disabled',
770
+ detail: 'OpenClaw Gateway cron setup disabled',
771
+ proof,
772
+ };
773
+ }
774
+ const openclawPath = await resolveCommandPath('openclaw');
775
+ if (!openclawPath) {
776
+ const detail = 'openclaw CLI not found on PATH; skipping OpenClaw Gateway cron setup';
777
+ return {
778
+ ok: normalizedMode === 'auto',
779
+ enabled: true,
780
+ installed: false,
781
+ status: normalizedMode === 'auto' ? 'skipped' : 'failed',
782
+ detail,
783
+ remediation: 'Run this setup inside the VPS shell where OpenClaw Gateway is installed, or install the openclaw CLI.',
784
+ proof,
785
+ };
786
+ }
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) {
796
+ return {
797
+ ok: true,
798
+ enabled: true,
799
+ installed: true,
800
+ status: 'already_configured_verified',
801
+ detail: `OpenClaw cron job already exists and matches the Growth Engineer runner contract: ${automation.openclawCron.name}`,
802
+ schedule: automation.openclawCron.schedule,
803
+ timezone: automation.openclawCron.timezone,
804
+ source: inspection.source,
805
+ proof,
806
+ };
807
+ }
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
+ }
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
+ : '';
852
+ return {
853
+ ok: add.ok || (normalizedMode === 'auto' && !inspection.exists),
854
+ enabled: true,
855
+ installed: add.ok,
856
+ status: add.ok ? (inspection.exists ? 'reconfigured' : 'configured') : inspection.exists ? 'needs_repair' : 'failed',
857
+ detail: add.ok
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}`}`,
860
+ schedule: automation.openclawCron.schedule,
861
+ timezone: automation.openclawCron.timezone,
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,
943
+ proof,
944
+ };
945
+ }
501
946
  async function appendHelperDetail(details, label, result) {
502
947
  if (result.ok) {
503
948
  details.push(`${label}: ok`);
@@ -756,6 +1201,13 @@ async function installSentryConnector() {
756
1201
  details.push('Sentry direct API exporter enabled via node scripts/export-sentry-summary.mjs');
757
1202
  return { connector: 'sentry', ok: true, detail: details.join('; ') };
758
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
+ }
759
1211
  async function installGitHubConnector() {
760
1212
  const details = [];
761
1213
  await installClawHubSkill('github', details);
@@ -796,6 +1248,20 @@ async function installAnalyticsConnector() {
796
1248
  : 'analyticscli binary missing after dependency setup',
797
1249
  };
798
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
+ }
799
1265
  async function enableConnectorConfig(configPath, connectors) {
800
1266
  if (connectors.length === 0 || !(await fileExists(configPath)))
801
1267
  return;
@@ -806,17 +1272,35 @@ async function enableConnectorConfig(configPath, connectors) {
806
1272
  sources: {
807
1273
  ...(config.sources || {}),
808
1274
  analytics: connectors.includes('analytics')
809
- ? { ...(config.sources?.analytics || {}), enabled: true, mode: 'command', command: config.sources?.analytics?.command || getDefaultSourceCommand('analytics') }
1275
+ ? { ...(config.sources?.analytics || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('analytics', config.sources?.analytics) }
810
1276
  : config.sources?.analytics,
811
1277
  revenuecat: connectors.includes('revenuecat')
812
- ? { ...(config.sources?.revenuecat || {}), enabled: true, mode: 'command', command: getDefaultSourceCommand('revenuecat') }
1278
+ ? { ...(config.sources?.revenuecat || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('revenuecat', config.sources?.revenuecat) }
813
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,
814
1286
  sentry: connectors.includes('sentry')
815
- ? { ...(config.sources?.sentry || {}), enabled: true, mode: 'command', command: getDefaultSourceCommand('sentry') }
1287
+ ? { ...(config.sources?.sentry || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('sentry', config.sources?.sentry) }
816
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,
817
1299
  extra: extra.map((source) => connectors.includes('asc') && source?.service === 'asc-cli'
818
- ? { ...source, enabled: true, mode: 'command', command: source.command || getDefaultSourceCommand('asc') }
819
- : source),
1300
+ ? { ...source, enabled: true, mode: 'command', command: normalizeSourceCommand('asc', 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),
820
1304
  },
821
1305
  };
822
1306
  await writeJson(configPath, next);
@@ -833,8 +1317,16 @@ async function installConnectorHelpers(configPath, connectors) {
833
1317
  results.push(await installAscConnector());
834
1318
  if (connector === 'revenuecat')
835
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' });
836
1324
  if (connector === 'sentry')
837
1325
  results.push(await installSentryConnector());
1326
+ if (connector === 'coolify')
1327
+ results.push(await installCoolifyConnector());
1328
+ if (isAccountSignalConnector(connector))
1329
+ results.push(await installAccountSignalConnector(connector));
838
1330
  }
839
1331
  return results;
840
1332
  }
@@ -865,7 +1357,9 @@ async function detectGitHubRepo() {
865
1357
  }
866
1358
  async function ensureConfig(configPath) {
867
1359
  if (await fileExists(configPath)) {
868
- const config = await readJson(configPath);
1360
+ const originalConfig = await readJson(configPath);
1361
+ const config = migrateRuntimeSourceCommands(originalConfig);
1362
+ let changed = JSON.stringify(originalConfig.sources || {}) !== JSON.stringify(config.sources || {});
869
1363
  if (!isConfiguredGitHubRepo(config?.project?.githubRepo)) {
870
1364
  const detectedRepo = await detectGitHubRepo();
871
1365
  if (detectedRepo) {
@@ -873,14 +1367,17 @@ async function ensureConfig(configPath) {
873
1367
  ...(config.project || {}),
874
1368
  githubRepo: detectedRepo,
875
1369
  };
876
- await writeJson(configPath, config);
877
- return {
878
- created: false,
879
- configPath,
880
- githubRepo: detectedRepo,
881
- };
1370
+ changed = true;
882
1371
  }
883
1372
  }
1373
+ if (changed) {
1374
+ await writeJson(configPath, config);
1375
+ return {
1376
+ created: false,
1377
+ configPath,
1378
+ githubRepo: config.project?.githubRepo || null,
1379
+ };
1380
+ }
884
1381
  return {
885
1382
  created: false,
886
1383
  configPath,
@@ -904,19 +1401,27 @@ async function ensureConfig(configPath) {
904
1401
  analytics: {
905
1402
  enabled: true,
906
1403
  mode: 'command',
907
- command: getDefaultSourceCommand('analytics'),
1404
+ command: getRuntimeSourceCommand('analytics'),
908
1405
  },
909
1406
  revenuecat: {
910
1407
  ...(template.sources?.revenuecat || {}),
911
1408
  enabled: false,
912
1409
  mode: 'command',
913
- command: getDefaultSourceCommand('revenuecat'),
1410
+ command: getRuntimeSourceCommand('revenuecat'),
914
1411
  },
915
1412
  sentry: {
916
1413
  ...(template.sources?.sentry || {}),
917
1414
  enabled: false,
918
1415
  mode: 'command',
919
- command: getDefaultSourceCommand('sentry'),
1416
+ command: getRuntimeSourceCommand('sentry'),
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',
920
1425
  },
921
1426
  feedback: {
922
1427
  ...(template.sources?.feedback || {}),
@@ -927,7 +1432,7 @@ async function ensureConfig(configPath) {
927
1432
  actions: {
928
1433
  ...template.actions,
929
1434
  mode: 'issue',
930
- autoCreateIssues: false,
1435
+ autoCreateIssues: true,
931
1436
  autoCreatePullRequests: false,
932
1437
  draftPullRequests: true,
933
1438
  proposalBranchPrefix: 'openclaw/proposals',
@@ -1009,6 +1514,15 @@ function appendProjectFlag(command, projectId) {
1009
1514
  function commandHasAscAppFlag(command) {
1010
1515
  return /(^|\s)--app(\s|=|$)/.test(String(command || ''));
1011
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
+ }
1012
1526
  function appendAscAppFlag(command, appId) {
1013
1527
  const raw = String(command || '').trim();
1014
1528
  if (!raw || commandHasAscAppFlag(raw))
@@ -1078,6 +1592,34 @@ async function configureAscApp(configPath, appId) {
1078
1592
  process.env.ASC_APP_ID = normalizedAppId;
1079
1593
  return changed;
1080
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
+ }
1081
1623
  function configHasEnabledAscSource(config) {
1082
1624
  const extraSources = Array.isArray(config?.sources?.extra) ? config.sources.extra : [];
1083
1625
  return extraSources.some((source) => {
@@ -1153,11 +1695,7 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
1153
1695
  if (!configHasEnabledAscSource(config)) {
1154
1696
  return { ok: true, configured: false, changed: false, appId: null, appScope: 'disabled', needsUserInput: false };
1155
1697
  }
1156
- const configuredAppId = normalizeString(config.project?.ascAppId) || normalizeString(process.env.ASC_APP_ID);
1157
- if (configuredAppId) {
1158
- const changed = await configureAscApp(configPath, configuredAppId);
1159
- return { ok: true, configured: true, changed, appId: configuredAppId, appScope: 'single_app', needsUserInput: false };
1160
- }
1698
+ const changed = await configureAscAllApps(configPath);
1161
1699
  const appList = await listAscApps();
1162
1700
  if (!appList.ok) {
1163
1701
  return {
@@ -1172,7 +1710,7 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
1172
1710
  return {
1173
1711
  ok: true,
1174
1712
  configured: true,
1175
- changed: false,
1713
+ changed,
1176
1714
  appId: null,
1177
1715
  appScope: 'all_accessible_apps',
1178
1716
  apps: appList.apps,
@@ -1180,6 +1718,131 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
1180
1718
  needsUserInput: false,
1181
1719
  };
1182
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
+ }
1183
1846
  async function listAnalyticsProjects() {
1184
1847
  const result = await runShellCommand('analyticscli projects list --format json', 60_000);
1185
1848
  if (!result.ok) {
@@ -1273,7 +1936,7 @@ function remediationForCheck(checkName, configPath) {
1273
1936
  return 'Write `data/openclaw-growth-engineer/analytics_summary.json` via your analytics refresh step (API-key based source command/file generation).';
1274
1937
  }
1275
1938
  if (checkName === 'connection:analytics') {
1276
- 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.';
1277
1940
  }
1278
1941
  if (checkName === 'connection:github') {
1279
1942
  return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>` + issues API.';
@@ -1282,7 +1945,7 @@ function remediationForCheck(checkName, configPath) {
1282
1945
  return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>/pulls`, plus `Pull requests: Read/Write` and `Contents: Read/Write` scopes.';
1283
1946
  }
1284
1947
  if (checkName === 'connection:asc_cli') {
1285
- 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.';
1286
1949
  }
1287
1950
  return 'Fix this blocker and rerun start.';
1288
1951
  }
@@ -1303,8 +1966,7 @@ function remediateAscAppSetupFailure(error) {
1303
1966
  }
1304
1967
  async function runPreflight(configPath, testConnections, progressJson = false, onlyConnectors = []) {
1305
1968
  const commandParts = [
1306
- 'node',
1307
- 'scripts/openclaw-growth-preflight.mjs',
1969
+ nodeRuntimeScriptCommand('openclaw-growth-preflight.mjs'),
1308
1970
  '--config',
1309
1971
  quote(configPath),
1310
1972
  ];
@@ -1334,17 +1996,61 @@ async function runPreflight(configPath, testConnections, progressJson = false, o
1334
1996
  };
1335
1997
  }
1336
1998
  async function runFirstPass(configPath) {
1337
- const command = `node scripts/openclaw-growth-runner.mjs --config ${quote(configPath)}`;
1999
+ const statePath = path.resolve(deriveStatePathFromConfigPath(configPath));
2000
+ const command = `${nodeRuntimeScriptCommand('openclaw-growth-runner.mjs')} --config ${quote(configPath)} --state ${quote(statePath)}`;
1338
2001
  return runShellCommand(command, 300_000);
1339
2002
  }
1340
2003
  async function main() {
1341
2004
  await loadOpenClawGrowthSecrets();
1342
2005
  const args = parseArgs(process.argv.slice(2));
1343
2006
  const configPath = path.resolve(args.config);
2007
+ const schedulerProofPath = path.resolve(deriveSchedulerProofPathFromStatePath(deriveStatePathFromConfigPath(configPath)));
1344
2008
  const configResult = await ensureConfig(configPath);
1345
- const initialConfig = await readJson(configPath);
2009
+ const initialConfig = applyOpenClawCronOverrides(await readJson(configPath), args);
1346
2010
  await applyOpenClawSecretRefs(initialConfig);
1347
2011
  const heartbeat = await ensureGrowthHeartbeat(configPath, initialConfig);
2012
+ const openclawCron = await ensureOpenClawCronSchedule(configPath, initialConfig, args.openclawCron);
2013
+ const hermesCron = await ensureHermesCronSchedule(configPath, initialConfig, args.openclawCron);
2014
+ if (!openclawCron.ok) {
2015
+ process.stdout.write(`${JSON.stringify({
2016
+ ok: false,
2017
+ phase: 'openclaw_cron_setup',
2018
+ configCreated: configResult.created,
2019
+ configPath,
2020
+ heartbeat,
2021
+ openclawCron,
2022
+ hermesCron,
2023
+ blockers: [
2024
+ {
2025
+ check: 'scheduler:openclaw-cron',
2026
+ detail: openclawCron.detail,
2027
+ remediation: openclawCron.remediation || 'Fix OpenClaw cron setup and rerun start.',
2028
+ },
2029
+ ],
2030
+ }, null, 2)}\n`);
2031
+ process.exitCode = 1;
2032
+ return;
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
+ }
1348
2054
  const projectConfigured = await configureAnalyticsProject(configPath, args.project);
1349
2055
  const ascAppConfiguredFromArg = await configureAscApp(configPath, args.ascApp);
1350
2056
  const analyticscliEnsure = await ensureAnalyticsCliInstalled();
@@ -1355,6 +2061,8 @@ async function main() {
1355
2061
  configCreated: configResult.created,
1356
2062
  configPath,
1357
2063
  heartbeat,
2064
+ openclawCron,
2065
+ hermesCron,
1358
2066
  projectConfigured,
1359
2067
  ascAppConfigured: ascAppConfiguredFromArg,
1360
2068
  blockers: [
@@ -1396,6 +2104,8 @@ async function main() {
1396
2104
  configCreated: configResult.created,
1397
2105
  configPath,
1398
2106
  heartbeat,
2107
+ openclawCron,
2108
+ hermesCron,
1399
2109
  projectConfigured,
1400
2110
  ascAppConfigured: ascAppConfiguredFromArg,
1401
2111
  connectorSetup,
@@ -1410,7 +2120,9 @@ async function main() {
1410
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.'
1411
2121
  : entry.connector === 'sentry'
1412
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.'
1413
- : '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.',
1414
2126
  })),
1415
2127
  }, null, 2)}\n`);
1416
2128
  process.exitCode = 1;
@@ -1463,6 +2175,8 @@ async function main() {
1463
2175
  configCreated: configResult.created,
1464
2176
  configPath,
1465
2177
  heartbeat,
2178
+ openclawCron,
2179
+ hermesCron,
1466
2180
  projectConfigured: projectConfigured || analyticsProjectSetup.configured,
1467
2181
  analyticsProjectId: analyticsProjectSetup.projectId || null,
1468
2182
  ascAppConfigured: false,
@@ -1482,6 +2196,51 @@ async function main() {
1482
2196
  process.exitCode = 1;
1483
2197
  return;
1484
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
+ }
1485
2244
  const preflightResult = await runPreflight(configPath, args.testConnections, args.progressJson, args.onlyConnectors);
1486
2245
  const preflightPayload = preflightResult.payload;
1487
2246
  if (!preflightPayload) {
@@ -1502,11 +2261,16 @@ async function main() {
1502
2261
  configCreated: configResult.created,
1503
2262
  configPath,
1504
2263
  heartbeat,
2264
+ openclawCron,
2265
+ hermesCron,
1505
2266
  projectConfigured: projectConfigured || analyticsProjectSetup.configured,
1506
2267
  analyticsProjectId: analyticsProjectSetup.projectId || null,
1507
2268
  ascAppConfigured: ascAppSetup.configured,
1508
2269
  ascAppId: ascAppSetup.appId || null,
1509
2270
  ascAppScope: ascAppSetup.appScope || null,
2271
+ ascAnalyticsRequestStatus: ascAnalyticsRequestSetup.status,
2272
+ ascAnalyticsRequestId: ascAnalyticsRequestSetup.requestId || null,
2273
+ ascAnalyticsRequestIds: ascAnalyticsRequestSetup.requestIds || [],
1510
2274
  githubRepo: configResult.githubRepo,
1511
2275
  connectorSetup,
1512
2276
  checks: preflightPayload.checks || [],
@@ -1522,12 +2286,18 @@ async function main() {
1522
2286
  configCreated: configResult.created,
1523
2287
  configPath,
1524
2288
  heartbeat,
2289
+ openclawCron,
2290
+ hermesCron,
1525
2291
  projectConfigured: projectConfigured || analyticsProjectSetup.configured,
1526
2292
  analyticsProjectId: analyticsProjectSetup.projectId || null,
1527
2293
  ascAppConfigured: ascAppSetup.configured,
1528
2294
  ascAppId: ascAppSetup.appId || null,
1529
2295
  ascAppScope: ascAppSetup.appScope || null,
2296
+ ascAnalyticsRequestStatus: ascAnalyticsRequestSetup.status,
2297
+ ascAnalyticsRequestId: ascAnalyticsRequestSetup.requestId || null,
2298
+ ascAnalyticsRequestIds: ascAnalyticsRequestSetup.requestIds || [],
1530
2299
  connectorSetup,
2300
+ schedulerProofPath,
1531
2301
  message: 'Preflight passed. First run skipped due to --setup-only.',
1532
2302
  }, null, 2)}\n`);
1533
2303
  return;
@@ -1550,6 +2320,8 @@ async function main() {
1550
2320
  configCreated: configResult.created,
1551
2321
  configPath,
1552
2322
  heartbeat,
2323
+ openclawCron,
2324
+ hermesCron,
1553
2325
  projectConfigured,
1554
2326
  error: rawError,
1555
2327
  }, null, 2)}\n`);
@@ -1563,9 +2335,12 @@ async function main() {
1563
2335
  configCreated: configResult.created,
1564
2336
  configPath,
1565
2337
  heartbeat,
2338
+ openclawCron,
2339
+ hermesCron,
1566
2340
  projectConfigured,
1567
2341
  actionMode,
1568
2342
  runnerOutput: runResult.stdout.trim(),
2343
+ schedulerProofPath,
1569
2344
  }, null, 2)}\n`);
1570
2345
  }
1571
2346
  main().catch((error) => {