@analyticscli/growth-engineer 0.1.0-preview.15 → 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.
- package/dist/config.d.ts +775 -22
- package/dist/config.js +39 -5
- package/dist/config.js.map +1 -1
- package/dist/index.js +131 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/export-asc-summary.mjs +1 -1
- package/dist/runtime/export-asc-summary.mjs.map +1 -1
- package/dist/runtime/export-coolify-summary.d.mts +2 -0
- package/dist/runtime/export-coolify-summary.mjs +230 -0
- package/dist/runtime/export-coolify-summary.mjs.map +1 -0
- package/dist/runtime/export-paddle-summary.d.mts +2 -0
- package/dist/runtime/export-paddle-summary.mjs +170 -0
- package/dist/runtime/export-paddle-summary.mjs.map +1 -0
- package/dist/runtime/export-sentry-summary.mjs +265 -38
- package/dist/runtime/export-sentry-summary.mjs.map +1 -1
- package/dist/runtime/export-seo-summary.d.mts +2 -0
- package/dist/runtime/export-seo-summary.mjs +503 -0
- package/dist/runtime/export-seo-summary.mjs.map +1 -0
- package/dist/runtime/openclaw-exporters-lib.d.mts +50 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +761 -57
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-engineer.mjs +163 -4
- package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-env.mjs +5 -0
- package/dist/runtime/openclaw-growth-env.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +399 -26
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +564 -69
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.d.mts +150 -2
- package/dist/runtime/openclaw-growth-shared.mjs +489 -7
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +584 -48
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +82 -6
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +1501 -105
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +1 -1
- 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,
|
|
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 (
|
|
87
|
+
--connectors <list> Install/enable connector helpers (${SUPPORTED_CONNECTORS.join(',')},all)
|
|
39
88
|
--only-connectors <list>
|
|
40
|
-
Limit live preflight checks to
|
|
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:
|
|
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
|
|
272
|
+
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use ${SUPPORTED_CONNECTORS.join(', ')}, or all.`);
|
|
162
273
|
}
|
|
163
274
|
if (connector === 'all') {
|
|
164
|
-
connectors.add(
|
|
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
|
|
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
|
|
662
|
-
|
|
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: '
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
:
|
|
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(),
|