@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
|
@@ -7,17 +7,51 @@ import { createInterface } from 'node:readline/promises';
|
|
|
7
7
|
import { emitKeypressEvents } from 'node:readline';
|
|
8
8
|
import { createPrivateKey } from 'node:crypto';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import {
|
|
10
|
+
import { buildOpenClawCronAddCommand, buildHermesCronCreateCommand, buildGrowthRunnerCommand, deriveSchedulerProofPathFromStatePath, deriveStatePathFromConfigPath, buildExtraSourceConfig, getAutomationConfig, getDefaultSourceCommand, getDefaultSourcePath, inspectHermesCronInstall, inspectOpenClawCronInstall, } from './openclaw-growth-shared.mjs';
|
|
11
11
|
import { loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
12
12
|
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
13
13
|
const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
14
14
|
const ENABLE_ISOLATED_SECRET_RUNNER_WIZARD = false;
|
|
15
|
-
const DEFAULT_GROWTH_INTERVAL_MINUTES =
|
|
15
|
+
const DEFAULT_GROWTH_INTERVAL_MINUTES = 90;
|
|
16
16
|
const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
|
|
17
17
|
const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
|
|
18
18
|
const GROWTH_ENGINEER_PACKAGE_SPEC = process.env.OPENCLAW_GROWTH_ENGINEER_PACKAGE || '@analyticscli/growth-engineer@preview';
|
|
19
19
|
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const
|
|
20
|
+
const ACCOUNT_SIGNAL_CONNECTOR_KEYS = [
|
|
21
|
+
'stripe',
|
|
22
|
+
'lemonsqueezy',
|
|
23
|
+
'adapty',
|
|
24
|
+
'superwall',
|
|
25
|
+
'google-play',
|
|
26
|
+
'datadog',
|
|
27
|
+
'bugsnag',
|
|
28
|
+
'intercom',
|
|
29
|
+
'zendesk',
|
|
30
|
+
'apple-search-ads',
|
|
31
|
+
'google-ads',
|
|
32
|
+
'meta-ads',
|
|
33
|
+
'tiktok-ads',
|
|
34
|
+
'vercel',
|
|
35
|
+
'cloudflare',
|
|
36
|
+
'resend',
|
|
37
|
+
'customerio',
|
|
38
|
+
'mailchimp',
|
|
39
|
+
'appfollow',
|
|
40
|
+
'apptweak',
|
|
41
|
+
'linear',
|
|
42
|
+
'postiz',
|
|
43
|
+
];
|
|
44
|
+
const CONNECTOR_KEYS = [
|
|
45
|
+
'analytics',
|
|
46
|
+
'github',
|
|
47
|
+
'revenuecat',
|
|
48
|
+
'paddle',
|
|
49
|
+
'seo',
|
|
50
|
+
'sentry',
|
|
51
|
+
'coolify',
|
|
52
|
+
'asc',
|
|
53
|
+
...ACCOUNT_SIGNAL_CONNECTOR_KEYS,
|
|
54
|
+
];
|
|
21
55
|
class WizardAbortError extends Error {
|
|
22
56
|
exitCode;
|
|
23
57
|
constructor(message, exitCode = 130) {
|
|
@@ -45,69 +79,640 @@ const CONNECTOR_DEFINITIONS = [
|
|
|
45
79
|
summary: 'Read subscription, product, entitlement, and revenue context.',
|
|
46
80
|
needs: 'A RevenueCat v2 secret API key with read-only project permissions.',
|
|
47
81
|
},
|
|
82
|
+
{
|
|
83
|
+
key: 'paddle',
|
|
84
|
+
label: 'Paddle Billing metrics',
|
|
85
|
+
summary: 'Read web checkout, revenue, MRR, refunds, chargebacks, and active subscriber metrics.',
|
|
86
|
+
needs: 'A Paddle API key with metrics.read permission for the live account.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'seo',
|
|
90
|
+
label: 'SEO / GSC / DataForSEO',
|
|
91
|
+
summary: 'Read organic search demand, GSC clicks/impressions/CTR/position, and optional capped DataForSEO keyword ideas.',
|
|
92
|
+
needs: 'A GSC property plus an access token/service-account credential. DataForSEO credentials are optional and paid.',
|
|
93
|
+
},
|
|
48
94
|
{
|
|
49
95
|
key: 'sentry',
|
|
50
96
|
label: 'Sentry-compatible crash monitoring',
|
|
51
97
|
summary: 'Read unresolved crashes, regressions, affected users, releases, and production stability signals.',
|
|
52
98
|
needs: 'A Sentry or GlitchTip-compatible auth token plus the org slug. Project scope is inferred later from app context or config.',
|
|
53
99
|
},
|
|
100
|
+
{
|
|
101
|
+
key: 'coolify',
|
|
102
|
+
label: 'Coolify deployment monitoring',
|
|
103
|
+
summary: 'Read applications, deployments, servers, resources, and production health-check gaps.',
|
|
104
|
+
needs: 'A Coolify API token with read-only permissions from Keys & Tokens / API tokens.',
|
|
105
|
+
},
|
|
54
106
|
{
|
|
55
107
|
key: 'asc',
|
|
56
108
|
label: 'ASC / App Store Connect CLI',
|
|
57
109
|
summary: 'Read App Store analytics, reviews/ratings, builds/TestFlight/release context, subscriptions, purchases, and crash totals.',
|
|
58
110
|
needs: 'ASC_KEY_ID, ASC_ISSUER_ID, and the AuthKey_XXXX.p8 content or path.',
|
|
59
111
|
},
|
|
112
|
+
{
|
|
113
|
+
key: 'stripe',
|
|
114
|
+
label: 'Stripe billing and checkout',
|
|
115
|
+
summary: 'Read web payments, subscriptions, trials, invoices, refunds, disputes, coupons, and checkout conversion context.',
|
|
116
|
+
needs: 'An account-level Stripe restricted key or secret key with read access to customers, subscriptions, invoices, balance, charges, disputes, prices, products, coupons, and checkout sessions.',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: 'lemonsqueezy',
|
|
120
|
+
label: 'Lemon Squeezy sales and licensing',
|
|
121
|
+
summary: 'Read stores, products, variants, orders, subscriptions, discounts, license keys, and churn/revenue context.',
|
|
122
|
+
needs: 'A live-mode Lemon Squeezy API key from account settings.',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: 'adapty',
|
|
126
|
+
label: 'Adapty subscriptions and paywalls',
|
|
127
|
+
summary: 'Read mobile subscription, paywall, product, profile, attribution, and revenue signals across Adapty apps.',
|
|
128
|
+
needs: 'An Adapty server-side API key from dashboard app settings. App/project scope is left unpinned.',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: 'superwall',
|
|
132
|
+
label: 'Superwall paywall experiments',
|
|
133
|
+
summary: 'Read paywalls, products, placements/campaigns, experiments, subscription outcomes, and conversion evidence.',
|
|
134
|
+
needs: 'A Superwall organization API key with read scopes.',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: 'google-play',
|
|
138
|
+
label: 'Google Play Console',
|
|
139
|
+
summary: 'Read Android store, release, review, subscription, in-app purchase, and order signals across accessible apps.',
|
|
140
|
+
needs: 'A Play Console service account JSON credential with account-level read/reporting access.',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: 'datadog',
|
|
144
|
+
label: 'Datadog observability',
|
|
145
|
+
summary: 'Read RUM, logs, errors, APM, monitors, incidents, deployment, and reliability signals.',
|
|
146
|
+
needs: 'Datadog API and application keys plus the Datadog site.',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: 'bugsnag',
|
|
150
|
+
label: 'Bugsnag crash monitoring',
|
|
151
|
+
summary: 'Read error, release, session, stability, and affected-user signals across visible projects.',
|
|
152
|
+
needs: 'A Bugsnag data-access auth token with read access.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: 'intercom',
|
|
156
|
+
label: 'Intercom support and feedback',
|
|
157
|
+
summary: 'Read conversations, tickets, contacts, companies, support themes, and onboarding friction signals.',
|
|
158
|
+
needs: 'An Intercom private app access token for the workspace.',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
key: 'zendesk',
|
|
162
|
+
label: 'Zendesk support and feedback',
|
|
163
|
+
summary: 'Read support tickets, tags, CSAT, customer friction, cancellation themes, and help-center signals.',
|
|
164
|
+
needs: 'Zendesk subdomain, agent/admin email, and API token or OAuth token.',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
key: 'apple-search-ads',
|
|
168
|
+
label: 'Apple Search Ads (experimental)',
|
|
169
|
+
summary: 'Read iOS paid search campaigns, spend, installs, taps, CPT/CPA, keywords, and campaign quality signals.',
|
|
170
|
+
needs: 'Apple Ads OAuth client credentials or a current access/refresh token with account-level reporting access.',
|
|
171
|
+
experimental: true,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: 'google-ads',
|
|
175
|
+
label: 'Google Ads (experimental)',
|
|
176
|
+
summary: 'Read paid search/app campaign spend, clicks, conversions, CAC, ROAS, and landing-page/ad-group signals.',
|
|
177
|
+
needs: 'Google Ads developer token plus OAuth client/refresh token credentials with account-wide read access.',
|
|
178
|
+
experimental: true,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: 'meta-ads',
|
|
182
|
+
label: 'Meta Ads (experimental)',
|
|
183
|
+
summary: 'Read Facebook/Instagram campaign, ad set, creative, spend, conversion, CAC, and ROAS signals.',
|
|
184
|
+
needs: 'A Meta access token with Marketing API read permissions for the ad accounts you want analyzed.',
|
|
185
|
+
experimental: true,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
key: 'tiktok-ads',
|
|
189
|
+
label: 'TikTok Ads (experimental)',
|
|
190
|
+
summary: 'Read TikTok campaign, ad group, creative, spend, conversion, CAC, and ROAS signals.',
|
|
191
|
+
needs: 'TikTok Business API app credentials and access token with advertiser reporting access.',
|
|
192
|
+
experimental: true,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
key: 'vercel',
|
|
196
|
+
label: 'Vercel deployments (experimental)',
|
|
197
|
+
summary: 'Read projects, deployments, build failures, domains, environment health, and frontend reliability signals.',
|
|
198
|
+
needs: 'A Vercel access token with read access across the team/account.',
|
|
199
|
+
experimental: true,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: 'cloudflare',
|
|
203
|
+
label: 'Cloudflare traffic and edge (experimental)',
|
|
204
|
+
summary: 'Read zones/accounts, traffic, cache, Workers/Pages, WAF/security, DNS, and edge reliability signals.',
|
|
205
|
+
needs: 'A Cloudflare API token with account/zone read scopes for analytics, Workers/Pages, DNS, and security events as needed.',
|
|
206
|
+
experimental: true,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
key: 'resend',
|
|
210
|
+
label: 'Resend lifecycle email (experimental)',
|
|
211
|
+
summary: 'Read domains, broadcasts, transactional email volume, bounces, complaints, and deliverability signals.',
|
|
212
|
+
needs: 'A Resend API key with account-wide read access where available.',
|
|
213
|
+
experimental: true,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
key: 'customerio',
|
|
217
|
+
label: 'Customer.io lifecycle messaging (experimental)',
|
|
218
|
+
summary: 'Read campaigns, broadcasts, journeys, segments, deliveries, conversions, and lifecycle engagement signals.',
|
|
219
|
+
needs: 'Customer.io App API credentials for the workspace.',
|
|
220
|
+
experimental: true,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
key: 'mailchimp',
|
|
224
|
+
label: 'Mailchimp lifecycle email (experimental)',
|
|
225
|
+
summary: 'Read audiences, campaigns, automations, ecommerce, unsubscribes, bounces, and lifecycle performance signals.',
|
|
226
|
+
needs: 'A Mailchimp Marketing API key for the account.',
|
|
227
|
+
experimental: true,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: 'appfollow',
|
|
231
|
+
label: 'AppFollow reviews and ASO (experimental)',
|
|
232
|
+
summary: 'Read app reviews, ratings, semantic review themes, ASO positions, and competitor/app collection signals.',
|
|
233
|
+
needs: 'An AppFollow API token from the API Dashboard with account/app collection read access.',
|
|
234
|
+
experimental: true,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
key: 'apptweak',
|
|
238
|
+
label: 'AppTweak ASO intelligence (experimental)',
|
|
239
|
+
summary: 'Read keyword rankings, ASO metadata, competitor movement, category ranks, and store visibility signals.',
|
|
240
|
+
needs: 'An AppTweak API token from an account with API access.',
|
|
241
|
+
experimental: true,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
key: 'linear',
|
|
245
|
+
label: 'Linear planning context (experimental)',
|
|
246
|
+
summary: 'Read teams, projects, issues, cycles, labels, roadmap context, and delivery bottleneck signals.',
|
|
247
|
+
needs: 'A Linear personal API key or OAuth access token with read access across the workspace.',
|
|
248
|
+
experimental: true,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
key: 'postiz',
|
|
252
|
+
label: 'Postiz social publishing (experimental)',
|
|
253
|
+
summary: 'Read social integrations, scheduled/published posts, platform analytics, posting cadence, and content distribution signals.',
|
|
254
|
+
needs: 'A Postiz Public API key from Settings -> Developers -> Public API. Self-hosted installs can also set a custom API base URL.',
|
|
255
|
+
experimental: true,
|
|
256
|
+
},
|
|
60
257
|
];
|
|
258
|
+
const ACCOUNT_SIGNAL_CONNECTOR_DEFINITIONS = [
|
|
259
|
+
{
|
|
260
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'stripe'),
|
|
261
|
+
key: 'stripe',
|
|
262
|
+
service: 'stripe',
|
|
263
|
+
docsUrl: 'https://docs.stripe.com/keys',
|
|
264
|
+
sourceKind: 'revenue',
|
|
265
|
+
signalHint: 'Stripe account summary with payments, subscriptions, trials, invoices, refunds, disputes, coupons, checkout sessions, product/price changes, and churn/revenue deltas. Do not pin a single product or price unless the agent explicitly narrows a later run.',
|
|
266
|
+
steps: [
|
|
267
|
+
'Open Stripe Dashboard -> Developers -> API keys.',
|
|
268
|
+
'Create a restricted key for OpenClaw/Growth Engineer, or use a standard secret key only when restricted keys are not practical.',
|
|
269
|
+
'Prefer live-mode read permissions for Customers, Subscriptions, Invoices, Charges, Balance, Disputes, Prices, Products, Coupons, Promotion Codes, and Checkout Sessions.',
|
|
270
|
+
'Do not select a single product, price, or connected account in the wizard. Account-wide access lets the agent discover the relevant products later.',
|
|
271
|
+
'Copy the key once and paste it into this local terminal.',
|
|
272
|
+
],
|
|
273
|
+
credentials: [{ env: 'STRIPE_API_KEY', prompt: 'Paste STRIPE_API_KEY into this local terminal' }],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'lemonsqueezy'),
|
|
277
|
+
key: 'lemonsqueezy',
|
|
278
|
+
service: 'lemonsqueezy',
|
|
279
|
+
docsUrl: 'https://docs.lemonsqueezy.com/guides/developer-guide/getting-started',
|
|
280
|
+
sourceKind: 'revenue',
|
|
281
|
+
signalHint: 'Lemon Squeezy account summary with stores, products, variants, orders, subscriptions, discounts, license keys, refunds, and revenue/churn movement. Keep store/product filtering out of setup so the agent can inspect all accessible stores.',
|
|
282
|
+
steps: [
|
|
283
|
+
'Open Lemon Squeezy Dashboard -> Settings -> API.',
|
|
284
|
+
'Create a new API key in live mode for production revenue evidence.',
|
|
285
|
+
'Keep the key private and store it only in this local wizard.',
|
|
286
|
+
'Do not enter a store ID or product ID here; the agent should discover accessible stores from the account.',
|
|
287
|
+
'Copy the key and paste it below.',
|
|
288
|
+
],
|
|
289
|
+
credentials: [{ env: 'LEMON_SQUEEZY_API_KEY', prompt: 'Paste LEMON_SQUEEZY_API_KEY into this local terminal' }],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'adapty'),
|
|
293
|
+
key: 'adapty',
|
|
294
|
+
service: 'adapty',
|
|
295
|
+
docsUrl: 'https://adapty.io/docs/api-adapty',
|
|
296
|
+
sourceKind: 'revenue',
|
|
297
|
+
signalHint: 'Adapty summary with apps, paywalls, placements, products, profiles, access levels, subscriptions, attribution, conversion, renewals, cancellations, and revenue signals. Leave app scope unpinned.',
|
|
298
|
+
steps: [
|
|
299
|
+
'Open Adapty Dashboard.',
|
|
300
|
+
'Go to App Settings -> General -> API keys.',
|
|
301
|
+
'Copy the server-side API key for read access.',
|
|
302
|
+
'Do not paste an app ID, product ID, or paywall ID here; the agent can discover visible apps/paywalls later.',
|
|
303
|
+
'Paste the API key into this local terminal.',
|
|
304
|
+
],
|
|
305
|
+
credentials: [{ env: 'ADAPTY_API_KEY', prompt: 'Paste ADAPTY_API_KEY into this local terminal' }],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'superwall'),
|
|
309
|
+
key: 'superwall',
|
|
310
|
+
service: 'superwall',
|
|
311
|
+
docsUrl: 'https://api.superwall.com/docs',
|
|
312
|
+
sourceKind: 'revenue',
|
|
313
|
+
signalHint: 'Superwall organization summary with paywalls, placements, campaigns, products, experiments, subscription outcomes, conversion movement, and pricing/package signals. Keep project/paywall scope discoverable.',
|
|
314
|
+
steps: [
|
|
315
|
+
'Open Superwall dashboard.',
|
|
316
|
+
'Create or copy an organization API key with read scopes.',
|
|
317
|
+
'Use organization-wide access so OpenClaw/Hermes can inspect all relevant paywalls and experiments.',
|
|
318
|
+
'Do not enter a paywall, campaign, placement, or product ID in setup.',
|
|
319
|
+
'Paste the organization API key below.',
|
|
320
|
+
],
|
|
321
|
+
credentials: [{ env: 'SUPERWALL_API_KEY', prompt: 'Paste SUPERWALL_API_KEY into this local terminal' }],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'google-play'),
|
|
325
|
+
key: 'google-play',
|
|
326
|
+
service: 'google-play',
|
|
327
|
+
docsUrl: 'https://developer.android.com/google/play/developer-api',
|
|
328
|
+
sourceKind: 'store',
|
|
329
|
+
signalHint: 'Google Play account summary with accessible apps, releases, reviews, ratings, Android vitals, subscriptions, in-app products, orders, cancellation reasons, and store/acquisition signals. Do not pin package names during setup.',
|
|
330
|
+
steps: [
|
|
331
|
+
'Open Play Console -> Setup -> API access.',
|
|
332
|
+
'Link or create a Google Cloud project and service account if needed.',
|
|
333
|
+
'Grant read/reporting permissions that cover the apps you want analyzed, including financial/order data only when revenue analysis is desired.',
|
|
334
|
+
'Save the service-account JSON on this host or paste the JSON into a secret env outside chat.',
|
|
335
|
+
'Do not enter a package name in this wizard; accessible apps are discovered from the account.',
|
|
336
|
+
],
|
|
337
|
+
credentials: [
|
|
338
|
+
{
|
|
339
|
+
env: 'GOOGLE_PLAY_SERVICE_ACCOUNT_JSON',
|
|
340
|
+
prompt: 'Paste GOOGLE_PLAY_SERVICE_ACCOUNT_JSON path or JSON content into this local terminal',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'datadog'),
|
|
346
|
+
key: 'datadog',
|
|
347
|
+
service: 'datadog',
|
|
348
|
+
docsUrl: 'https://docs.datadoghq.com/account_management/api-app-keys/',
|
|
349
|
+
sourceKind: 'crash',
|
|
350
|
+
signalHint: 'Datadog account summary with RUM, logs, errors, APM, monitors, incidents, deploy markers, performance regressions, and affected-user reliability signals. Do not pin services during setup.',
|
|
351
|
+
steps: [
|
|
352
|
+
'Open Datadog -> Organization Settings -> API Keys and Application Keys.',
|
|
353
|
+
'Create an API key and an application key with read scopes for RUM/logs/APM/monitors/incidents as needed.',
|
|
354
|
+
'Choose the Datadog site for your account, for example datadoghq.com or datadoghq.eu.',
|
|
355
|
+
'Do not enter service names, env names, or monitor IDs here; the agent can discover them later.',
|
|
356
|
+
'Paste the keys below.',
|
|
357
|
+
],
|
|
358
|
+
credentials: [
|
|
359
|
+
{ env: 'DATADOG_API_KEY', prompt: 'Paste DATADOG_API_KEY into this local terminal' },
|
|
360
|
+
{ env: 'DATADOG_APP_KEY', prompt: 'Paste DATADOG_APP_KEY into this local terminal' },
|
|
361
|
+
{ env: 'DATADOG_SITE', prompt: 'Datadog site', optional: true, defaultValue: process.env.DATADOG_SITE || 'datadoghq.com' },
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'bugsnag'),
|
|
366
|
+
key: 'bugsnag',
|
|
367
|
+
service: 'bugsnag',
|
|
368
|
+
docsUrl: 'https://docs.bugsnag.com/api/',
|
|
369
|
+
sourceKind: 'crash',
|
|
370
|
+
signalHint: 'Bugsnag account summary with organizations, projects, errors, releases, stability score, sessions, affected users, and crash/regression signals. Project scope stays discoverable.',
|
|
371
|
+
steps: [
|
|
372
|
+
'Open Bugsnag settings and create or copy a data-access auth token.',
|
|
373
|
+
'Grant read access for organizations/projects you want analyzed.',
|
|
374
|
+
'Do not enter a project ID in this wizard.',
|
|
375
|
+
'Paste the token below.',
|
|
376
|
+
],
|
|
377
|
+
credentials: [{ env: 'BUGSNAG_AUTH_TOKEN', prompt: 'Paste BUGSNAG_AUTH_TOKEN into this local terminal' }],
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'intercom'),
|
|
381
|
+
key: 'intercom',
|
|
382
|
+
service: 'intercom',
|
|
383
|
+
docsUrl: 'https://developers.intercom.com/building-apps/docs/authentication',
|
|
384
|
+
sourceKind: 'feedback',
|
|
385
|
+
signalHint: 'Intercom workspace summary with conversations, tickets, contacts, companies, tags, support themes, onboarding friction, cancellation language, and customer feedback loops.',
|
|
386
|
+
steps: [
|
|
387
|
+
'Open Intercom Developer Hub and create or open a private app for your own workspace.',
|
|
388
|
+
'Go to Configure -> Authentication.',
|
|
389
|
+
'Copy the access token for that workspace.',
|
|
390
|
+
'Do not enter workspace-specific filters, tags, or inbox IDs here; the agent can discover relevant support surfaces later.',
|
|
391
|
+
'Paste the token below.',
|
|
392
|
+
],
|
|
393
|
+
credentials: [{ env: 'INTERCOM_ACCESS_TOKEN', prompt: 'Paste INTERCOM_ACCESS_TOKEN into this local terminal' }],
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'zendesk'),
|
|
397
|
+
key: 'zendesk',
|
|
398
|
+
service: 'zendesk',
|
|
399
|
+
docsUrl: 'https://developer.zendesk.com/api-reference/introduction/security-and-auth/',
|
|
400
|
+
sourceKind: 'feedback',
|
|
401
|
+
signalHint: 'Zendesk account summary with tickets, tags, views, CSAT, help-center signals, support themes, cancellation/friction language, and customer feedback loops.',
|
|
402
|
+
steps: [
|
|
403
|
+
'Open Zendesk Admin Center -> Apps and integrations -> APIs -> Zendesk API.',
|
|
404
|
+
'Create or copy an API token, or use an OAuth token if that is your workspace standard.',
|
|
405
|
+
'Use the account subdomain and an agent/admin email that can read support data.',
|
|
406
|
+
'Do not enter view IDs, brand IDs, product IDs, or ticket tags in setup.',
|
|
407
|
+
'Paste the account credentials below.',
|
|
408
|
+
],
|
|
409
|
+
credentials: [
|
|
410
|
+
{ env: 'ZENDESK_SUBDOMAIN', prompt: 'Zendesk subdomain, for example mycompany', defaultValue: process.env.ZENDESK_SUBDOMAIN || '' },
|
|
411
|
+
{ env: 'ZENDESK_EMAIL', prompt: 'Zendesk agent/admin email', defaultValue: process.env.ZENDESK_EMAIL || '' },
|
|
412
|
+
{ env: 'ZENDESK_API_TOKEN', prompt: 'Paste ZENDESK_API_TOKEN into this local terminal' },
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'apple-search-ads'),
|
|
417
|
+
key: 'apple-search-ads',
|
|
418
|
+
service: 'apple-search-ads',
|
|
419
|
+
docsUrl: 'https://developer.apple.com/documentation/apple_ads/implementing-oauth-for-the-apple-search-ads-api',
|
|
420
|
+
sourceKind: 'acquisition',
|
|
421
|
+
signalHint: 'Experimental Apple Search Ads account summary with organizations, campaigns, ad groups, keywords, spend, taps, installs, CPT/CPA, ROAS, and iOS paid-search acquisition quality. Keep campaign/app IDs out of setup so the agent can discover accessible accounts later.',
|
|
422
|
+
steps: [
|
|
423
|
+
'Open Apple Search Ads / Apple Ads API access for the account.',
|
|
424
|
+
'Create OAuth credentials or copy an existing refresh/access token from your server-side integration.',
|
|
425
|
+
'Use account-level reporting access for every organization you want analyzed.',
|
|
426
|
+
'Do not enter campaign IDs, ad group IDs, keyword IDs, or app IDs here.',
|
|
427
|
+
'Paste the account credentials below.',
|
|
428
|
+
],
|
|
429
|
+
credentials: [
|
|
430
|
+
{ env: 'APPLE_SEARCH_ADS_CLIENT_ID', prompt: 'Paste APPLE_SEARCH_ADS_CLIENT_ID, or leave empty if using a token only', optional: true },
|
|
431
|
+
{ env: 'APPLE_SEARCH_ADS_CLIENT_SECRET', prompt: 'Paste APPLE_SEARCH_ADS_CLIENT_SECRET, or leave empty if using a token only', optional: true },
|
|
432
|
+
{ env: 'APPLE_SEARCH_ADS_REFRESH_TOKEN', prompt: 'Paste APPLE_SEARCH_ADS_REFRESH_TOKEN or current access token into this local terminal' },
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'google-ads'),
|
|
437
|
+
key: 'google-ads',
|
|
438
|
+
service: 'google-ads',
|
|
439
|
+
docsUrl: 'https://developers.google.com/google-ads/api/docs/oauth/overview',
|
|
440
|
+
sourceKind: 'acquisition',
|
|
441
|
+
signalHint: 'Experimental Google Ads account summary with accessible customer accounts, campaigns, ad groups, keywords, spend, clicks, conversions, ROAS, CAC, search terms, and app/web acquisition quality. Keep customer IDs and campaign IDs discoverable.',
|
|
442
|
+
steps: [
|
|
443
|
+
'Open Google Ads API Center and Google Cloud OAuth credentials for the account.',
|
|
444
|
+
'Create or copy a developer token plus OAuth client credentials and refresh token.',
|
|
445
|
+
'Use a manager/account credential that can read every customer account you want analyzed.',
|
|
446
|
+
'Do not enter customer IDs, campaign IDs, ad group IDs, or conversion action IDs in setup.',
|
|
447
|
+
'Paste the account-wide credentials below.',
|
|
448
|
+
],
|
|
449
|
+
credentials: [
|
|
450
|
+
{ env: 'GOOGLE_ADS_DEVELOPER_TOKEN', prompt: 'Paste GOOGLE_ADS_DEVELOPER_TOKEN into this local terminal' },
|
|
451
|
+
{ env: 'GOOGLE_ADS_CLIENT_ID', prompt: 'Paste GOOGLE_ADS_CLIENT_ID into this local terminal' },
|
|
452
|
+
{ env: 'GOOGLE_ADS_CLIENT_SECRET', prompt: 'Paste GOOGLE_ADS_CLIENT_SECRET into this local terminal' },
|
|
453
|
+
{ env: 'GOOGLE_ADS_REFRESH_TOKEN', prompt: 'Paste GOOGLE_ADS_REFRESH_TOKEN into this local terminal' },
|
|
454
|
+
{ env: 'GOOGLE_ADS_LOGIN_CUSTOMER_ID', prompt: 'Optional manager login customer ID (empty = discover accessible accounts)', optional: true },
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'meta-ads'),
|
|
459
|
+
key: 'meta-ads',
|
|
460
|
+
service: 'meta-ads',
|
|
461
|
+
docsUrl: 'https://developers.facebook.com/docs/marketing-apis/',
|
|
462
|
+
sourceKind: 'acquisition',
|
|
463
|
+
signalHint: 'Experimental Meta Ads account summary with accessible ad accounts, campaigns, ad sets, creatives, spend, impressions, clicks, conversion values, CAC, ROAS, and paid social acquisition quality. Keep ad account IDs and campaign IDs discoverable.',
|
|
464
|
+
steps: [
|
|
465
|
+
'Open Meta for Developers / Business Manager for the app or system user that owns Marketing API access.',
|
|
466
|
+
'Create or copy a long-lived access token with ads_read and related read permissions approved for your business.',
|
|
467
|
+
'Use business/account-level access for all ad accounts you want analyzed.',
|
|
468
|
+
'Do not enter ad account IDs, campaign IDs, pixel IDs, or page IDs here.',
|
|
469
|
+
'Paste the access token below.',
|
|
470
|
+
],
|
|
471
|
+
credentials: [{ env: 'META_ADS_ACCESS_TOKEN', prompt: 'Paste META_ADS_ACCESS_TOKEN into this local terminal' }],
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'tiktok-ads'),
|
|
475
|
+
key: 'tiktok-ads',
|
|
476
|
+
service: 'tiktok-ads',
|
|
477
|
+
docsUrl: 'https://business-api.tiktok.com/portal/docs',
|
|
478
|
+
sourceKind: 'acquisition',
|
|
479
|
+
signalHint: 'Experimental TikTok Ads account summary with advertisers, campaigns, ad groups, creatives, spend, impressions, clicks, conversions, CAC, ROAS, and paid social acquisition quality. Keep advertiser/campaign IDs out of setup.',
|
|
480
|
+
steps: [
|
|
481
|
+
'Open TikTok Business API / Marketing API portal.',
|
|
482
|
+
'Create or copy app credentials and an access token with advertiser reporting permissions.',
|
|
483
|
+
'Use a credential that can list the advertisers you want analyzed.',
|
|
484
|
+
'Do not enter advertiser IDs, campaign IDs, ad group IDs, or creative IDs in setup.',
|
|
485
|
+
'Paste the credentials below.',
|
|
486
|
+
],
|
|
487
|
+
credentials: [
|
|
488
|
+
{ env: 'TIKTOK_ADS_ACCESS_TOKEN', prompt: 'Paste TIKTOK_ADS_ACCESS_TOKEN into this local terminal' },
|
|
489
|
+
{ env: 'TIKTOK_ADS_APP_ID', prompt: 'Paste TIKTOK_ADS_APP_ID, or leave empty if token-only', optional: true },
|
|
490
|
+
{ env: 'TIKTOK_ADS_SECRET', prompt: 'Paste TIKTOK_ADS_SECRET, or leave empty if token-only', optional: true },
|
|
491
|
+
],
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'vercel'),
|
|
495
|
+
key: 'vercel',
|
|
496
|
+
service: 'vercel',
|
|
497
|
+
docsUrl: 'https://vercel.com/docs/rest-api/reference/welcome',
|
|
498
|
+
sourceKind: 'infrastructure',
|
|
499
|
+
signalHint: 'Experimental Vercel account summary with projects, deployments, failed builds, domains, edge/runtime errors, environment health, web vitals where available, and release reliability. Keep project IDs/team IDs discoverable.',
|
|
500
|
+
steps: [
|
|
501
|
+
'Open Vercel Account Settings -> Tokens.',
|
|
502
|
+
'Create an access token with read access for the team/account you want analyzed.',
|
|
503
|
+
'Use team/account-level access so the agent can discover projects and deployments.',
|
|
504
|
+
'Do not enter project IDs, deployment IDs, domain names, or team IDs in setup.',
|
|
505
|
+
'Paste the token below.',
|
|
506
|
+
],
|
|
507
|
+
credentials: [{ env: 'VERCEL_ACCESS_TOKEN', prompt: 'Paste VERCEL_ACCESS_TOKEN into this local terminal' }],
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'cloudflare'),
|
|
511
|
+
key: 'cloudflare',
|
|
512
|
+
service: 'cloudflare',
|
|
513
|
+
docsUrl: 'https://developers.cloudflare.com/fundamentals/api/get-started/create-token/',
|
|
514
|
+
sourceKind: 'infrastructure',
|
|
515
|
+
signalHint: 'Experimental Cloudflare account summary with accounts, zones, Workers, Pages, DNS, traffic, cache, WAF/security events, outages, and edge reliability. Keep account/zone IDs discoverable.',
|
|
516
|
+
steps: [
|
|
517
|
+
'Open Cloudflare dashboard -> My Profile -> API Tokens.',
|
|
518
|
+
'Create a custom token with read scopes for accounts/zones, analytics, Workers/Pages, DNS, and security events as needed.',
|
|
519
|
+
'Prefer account-level read access for every zone/app the agent may analyze.',
|
|
520
|
+
'Do not enter account IDs, zone IDs, Worker names, or domain names here.',
|
|
521
|
+
'Paste the API token below.',
|
|
522
|
+
],
|
|
523
|
+
credentials: [{ env: 'CLOUDFLARE_API_TOKEN', prompt: 'Paste CLOUDFLARE_API_TOKEN into this local terminal' }],
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'resend'),
|
|
527
|
+
key: 'resend',
|
|
528
|
+
service: 'resend',
|
|
529
|
+
docsUrl: 'https://resend.com/docs/dashboard/api-keys/introduction',
|
|
530
|
+
sourceKind: 'lifecycle',
|
|
531
|
+
signalHint: 'Experimental Resend account summary with domains, broadcasts, transactional volume, bounces, complaints, delivery health, and lifecycle/email deliverability signals. Keep domain filters discoverable.',
|
|
532
|
+
steps: [
|
|
533
|
+
'Open Resend Dashboard -> API Keys.',
|
|
534
|
+
'Create an API key with the narrowest account-wide access available for reporting.',
|
|
535
|
+
'Use account-level access so the agent can inspect all relevant domains and sending streams.',
|
|
536
|
+
'Do not enter domain IDs, broadcast IDs, or audience/list IDs in setup.',
|
|
537
|
+
'Paste the key below.',
|
|
538
|
+
],
|
|
539
|
+
credentials: [{ env: 'RESEND_API_KEY', prompt: 'Paste RESEND_API_KEY into this local terminal' }],
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'customerio'),
|
|
543
|
+
key: 'customerio',
|
|
544
|
+
service: 'customerio',
|
|
545
|
+
docsUrl: 'https://docs.customer.io/accounts-and-workspaces/managing-credentials/',
|
|
546
|
+
sourceKind: 'lifecycle',
|
|
547
|
+
signalHint: 'Experimental Customer.io account summary with campaigns, broadcasts, journeys, segments, deliveries, conversions, unsubscribes, and lifecycle engagement quality. Keep workspace object IDs discoverable.',
|
|
548
|
+
steps: [
|
|
549
|
+
'Open Customer.io -> Settings -> Workspace Settings -> API and Webhook Credentials.',
|
|
550
|
+
'Create or copy App API credentials for the workspace.',
|
|
551
|
+
'Use workspace-level credentials so the agent can inspect campaigns, broadcasts, journeys, and segments.',
|
|
552
|
+
'Do not enter campaign IDs, segment IDs, newsletter IDs, or workspace-specific filters here.',
|
|
553
|
+
'Paste the credentials below.',
|
|
554
|
+
],
|
|
555
|
+
credentials: [
|
|
556
|
+
{ env: 'CUSTOMERIO_APP_API_KEY', prompt: 'Paste CUSTOMERIO_APP_API_KEY into this local terminal' },
|
|
557
|
+
{ env: 'CUSTOMERIO_SITE_ID', prompt: 'Paste CUSTOMERIO_SITE_ID, or leave empty when App API key is enough', optional: true },
|
|
558
|
+
],
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'mailchimp'),
|
|
562
|
+
key: 'mailchimp',
|
|
563
|
+
service: 'mailchimp',
|
|
564
|
+
docsUrl: 'https://mailchimp.com/developer/marketing/guides/quick-start/',
|
|
565
|
+
sourceKind: 'lifecycle',
|
|
566
|
+
signalHint: 'Experimental Mailchimp account summary with audiences, campaigns, automations, ecommerce, unsubscribes, bounces, clicks, opens, and lifecycle/email performance. Keep audience and campaign IDs discoverable.',
|
|
567
|
+
steps: [
|
|
568
|
+
'Open Mailchimp -> Account & billing -> Extras -> API keys.',
|
|
569
|
+
'Create or copy a Marketing API key for the account.',
|
|
570
|
+
'Use account-level access so the agent can inspect all relevant audiences and campaigns.',
|
|
571
|
+
'Do not enter audience IDs, campaign IDs, list IDs, or store IDs in setup.',
|
|
572
|
+
'Paste the API key below.',
|
|
573
|
+
],
|
|
574
|
+
credentials: [{ env: 'MAILCHIMP_API_KEY', prompt: 'Paste MAILCHIMP_API_KEY into this local terminal' }],
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'appfollow'),
|
|
578
|
+
key: 'appfollow',
|
|
579
|
+
service: 'appfollow',
|
|
580
|
+
docsUrl: 'https://support.appfollow.io/hc/en-us/articles/4403679243409-API-Access-Methods',
|
|
581
|
+
sourceKind: 'aso',
|
|
582
|
+
signalHint: 'Experimental AppFollow account summary with app collections, reviews, ratings, semantic themes, ASO/rank signals, competitor context, and store feedback quality. Keep collection/app IDs discoverable.',
|
|
583
|
+
steps: [
|
|
584
|
+
'Open AppFollow -> Integrations -> API Dashboard.',
|
|
585
|
+
'Create or copy the API token from an Owner/Admin account.',
|
|
586
|
+
'Use account-level access for every app collection you want analyzed.',
|
|
587
|
+
'Do not enter app IDs, collection IDs, country codes, or store IDs in setup.',
|
|
588
|
+
'Paste the API token below.',
|
|
589
|
+
],
|
|
590
|
+
credentials: [{ env: 'APPFOLLOW_API_TOKEN', prompt: 'Paste APPFOLLOW_API_TOKEN into this local terminal' }],
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'apptweak'),
|
|
594
|
+
key: 'apptweak',
|
|
595
|
+
service: 'apptweak',
|
|
596
|
+
docsUrl: 'https://help.apptweak.com/en/articles/4901806-learn-more-about-apptweak-takeout-api',
|
|
597
|
+
sourceKind: 'aso',
|
|
598
|
+
signalHint: 'Experimental AppTweak account summary with keyword rankings, category ranks, metadata, ASO visibility, competitors, and store-market opportunity signals. Keep app IDs and markets discoverable.',
|
|
599
|
+
steps: [
|
|
600
|
+
'Open AppTweak account/API settings or API documentation area.',
|
|
601
|
+
'Create or copy an API token from an account with API access.',
|
|
602
|
+
'Use account-level API access for every app/market you want analyzed.',
|
|
603
|
+
'Do not enter app IDs, competitor IDs, country codes, keyword IDs, or store IDs here.',
|
|
604
|
+
'Paste the API token below.',
|
|
605
|
+
],
|
|
606
|
+
credentials: [{ env: 'APPTWEAK_API_TOKEN', prompt: 'Paste APPTWEAK_API_TOKEN into this local terminal' }],
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'linear'),
|
|
610
|
+
key: 'linear',
|
|
611
|
+
service: 'linear',
|
|
612
|
+
docsUrl: 'https://linear.app/developers',
|
|
613
|
+
sourceKind: 'planning',
|
|
614
|
+
signalHint: 'Experimental Linear workspace summary with teams, projects, cycles, issues, labels, stale work, roadmap commitments, delivery bottlenecks, and product execution context. Keep team/project IDs discoverable.',
|
|
615
|
+
steps: [
|
|
616
|
+
'Open Linear -> Settings -> API.',
|
|
617
|
+
'Create a personal API key or use an OAuth token with read access.',
|
|
618
|
+
'Use workspace-level read access so the agent can inspect all relevant teams/projects.',
|
|
619
|
+
'Do not enter team IDs, project IDs, issue IDs, labels, or cycle IDs in setup.',
|
|
620
|
+
'Paste the token below.',
|
|
621
|
+
],
|
|
622
|
+
credentials: [{ env: 'LINEAR_API_KEY', prompt: 'Paste LINEAR_API_KEY or LINEAR_ACCESS_TOKEN into this local terminal' }],
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
...CONNECTOR_DEFINITIONS.find((connector) => connector.key === 'postiz'),
|
|
626
|
+
key: 'postiz',
|
|
627
|
+
service: 'postiz',
|
|
628
|
+
docsUrl: 'https://docs.postiz.com/public-api',
|
|
629
|
+
sourceKind: 'acquisition',
|
|
630
|
+
signalHint: 'Experimental Postiz account summary with connected social integrations, scheduled/published posts, platform analytics, content cadence, failed/pending posts, and organic distribution signals. Keep integration IDs and channel filters discoverable.',
|
|
631
|
+
steps: [
|
|
632
|
+
'Open Postiz -> Settings -> Developers -> Public API.',
|
|
633
|
+
'Create or copy a Public API key. OAuth2 tokens are also usable for app-user flows.',
|
|
634
|
+
'For Postiz Cloud, keep the default API base URL. For self-hosted Postiz, use your backend public URL ending in /public/v1.',
|
|
635
|
+
'Do not enter integration IDs, channel IDs, platform names, post IDs, or tag filters in setup.',
|
|
636
|
+
'Paste the API credentials below.',
|
|
637
|
+
],
|
|
638
|
+
credentials: [
|
|
639
|
+
{ env: 'POSTIZ_API_KEY', prompt: 'Paste POSTIZ_API_KEY into this local terminal' },
|
|
640
|
+
{
|
|
641
|
+
env: 'POSTIZ_API_BASE_URL',
|
|
642
|
+
prompt: 'Postiz API base URL',
|
|
643
|
+
optional: true,
|
|
644
|
+
defaultValue: process.env.POSTIZ_API_BASE_URL || 'https://api.postiz.com/public/v1',
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
},
|
|
648
|
+
];
|
|
649
|
+
const ACCOUNT_SIGNAL_CONNECTORS = new Map(ACCOUNT_SIGNAL_CONNECTOR_DEFINITIONS.map((definition) => [definition.key, definition]));
|
|
650
|
+
function getAccountSignalConnectorDefinition(key) {
|
|
651
|
+
return ACCOUNT_SIGNAL_CONNECTORS.get(key) || null;
|
|
652
|
+
}
|
|
653
|
+
function isAccountSignalConnector(key) {
|
|
654
|
+
return ACCOUNT_SIGNAL_CONNECTORS.has(key);
|
|
655
|
+
}
|
|
61
656
|
const DEFAULT_CADENCE_PLAN = [
|
|
657
|
+
{
|
|
658
|
+
key: 'healthcheck',
|
|
659
|
+
title: '90-minute production error healthcheck',
|
|
660
|
+
intervalMinutes: 90,
|
|
661
|
+
criticalOnly: true,
|
|
662
|
+
focusAreas: ['crash', 'deployment', 'availability'],
|
|
663
|
+
sourcePriorities: ['sentry', 'glitchtip', 'coolify', 'asc_cli'],
|
|
664
|
+
objective: 'Check Sentry/GlitchTip and Coolify for production errors, failed deploys, unhealthy resources, and availability blockers across every configured app.',
|
|
665
|
+
instructions: 'For Sentry/GlitchTip app errors, compare the issue release or app version with ASC production versions first. Ignore errors that only affect TestFlight, debug, staging, unreleased, or non-production app versions. Keep the social output short and action-oriented.',
|
|
666
|
+
},
|
|
62
667
|
{
|
|
63
668
|
key: 'daily',
|
|
64
|
-
title: 'Daily
|
|
669
|
+
title: 'Daily behavioral anomaly guardrail',
|
|
65
670
|
intervalDays: 1,
|
|
66
671
|
criticalOnly: true,
|
|
67
|
-
focusAreas: ['
|
|
68
|
-
sourcePriorities: ['
|
|
69
|
-
objective: '
|
|
70
|
-
instructions: 'Compare
|
|
672
|
+
focusAreas: ['analytics_anomaly', 'onboarding', 'conversion', 'paywall', 'purchase', 'retention', 'revenue'],
|
|
673
|
+
sourcePriorities: ['analytics', 'revenuecat', 'paddle', 'asc_cli', 'feedback', 'github', 'sentry', 'glitchtip', 'coolify'],
|
|
674
|
+
objective: 'Detect non-Sentry product and payment anomalies that affect real users: broken login or account flows inferred from behavior, onboarding or purchase drop-offs, zero-conversion days, missing buyers, very low active users, retention cliffs, and revenue anomalies.',
|
|
675
|
+
instructions: 'Compare AnalyticsCLI, RevenueCat, Paddle, ASC, feedback, memory/state, and recent code changes against recent baselines. Use Sentry/GlitchTip/Coolify only as corroborating context; do not repeat pure crash or deployment alerts that belong to the 90-minute healthcheck.',
|
|
71
676
|
},
|
|
72
677
|
{
|
|
73
678
|
key: 'weekly',
|
|
74
679
|
title: 'Weekly executive product and growth summary',
|
|
75
680
|
intervalDays: 7,
|
|
76
681
|
criticalOnly: false,
|
|
77
|
-
focusAreas: ['conversion', 'paywall', 'onboarding', 'marketing', 'retention', 'stability'],
|
|
78
|
-
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry', 'github'],
|
|
79
|
-
objective: 'Create
|
|
80
|
-
instructions: '
|
|
682
|
+
focusAreas: ['conversion', 'paywall', 'onboarding', 'marketing', 'retention', 'stability', 'seo'],
|
|
683
|
+
sourcePriorities: ['analytics', 'revenuecat', 'paddle', 'seo', 'asc_cli', 'feedback', 'sentry', 'coolify', 'github'],
|
|
684
|
+
objective: 'Create a deep app-by-app executive summary across all configured projects, connectors, recent releases, code changes, traffic, SEO/acquisition, revenue, activation, conversion, retention, reviews, and production stability.',
|
|
685
|
+
instructions: 'Be detailed. Group findings per app, explain why each recommendation should improve app usage, revenue, conversion, retention, or traffic, include expected KPI movement, likely code/store surfaces, owner-ready next steps, and verification plans. Generate charts when they clarify the evidence.',
|
|
81
686
|
},
|
|
82
687
|
{
|
|
83
688
|
key: 'monthly',
|
|
84
689
|
title: 'Monthly deep product, business, and code review',
|
|
85
690
|
intervalDays: 30,
|
|
86
691
|
criticalOnly: false,
|
|
87
|
-
focusAreas: ['conversion', 'paywall', 'retention', 'marketing', 'onboarding', 'codebase'],
|
|
88
|
-
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry', 'github'],
|
|
89
|
-
objective: 'Compare all configured projects month-over-month: MRR, trial conversion, churn, acquisition quality, store conversion, retention, review themes, feature usage, crash totals, and codebase changes.',
|
|
90
|
-
instructions: 'Decide what should be built, changed, deleted, or instrumented next. Tie conclusions to connector data plus codebase evidence and explain why each recommendation should move revenue,
|
|
692
|
+
focusAreas: ['conversion', 'paywall', 'retention', 'marketing', 'onboarding', 'codebase', 'seo'],
|
|
693
|
+
sourcePriorities: ['analytics', 'revenuecat', 'paddle', 'seo', 'asc_cli', 'feedback', 'sentry', 'coolify', 'github'],
|
|
694
|
+
objective: 'Compare all configured projects month-over-month: MRR, trial conversion, churn, Paddle revenue/subscriber movement, SEO demand/clicks, acquisition quality, store conversion, retention, review themes, feature usage, crash totals, and codebase changes.',
|
|
695
|
+
instructions: 'Be very detailed and app-grouped. Decide what should be built, changed, deleted, priced differently, marketed differently, or instrumented next. Tie conclusions to connector data plus codebase evidence and explain why each recommendation should move revenue, conversion, retention, traffic, or acquisition quality. Generate charts when useful.',
|
|
91
696
|
},
|
|
92
697
|
{
|
|
93
698
|
key: 'quarterly',
|
|
94
|
-
title: '
|
|
699
|
+
title: '3-month positioning, pricing, and roadmap review',
|
|
95
700
|
intervalDays: 91,
|
|
96
701
|
criticalOnly: false,
|
|
97
702
|
focusAreas: ['marketing', 'paywall', 'retention', 'conversion', 'onboarding'],
|
|
98
|
-
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'github', 'sentry'],
|
|
99
|
-
objective: 'Revisit positioning, pricing/packaging, onboarding architecture, roadmap assumptions, tracking quality, codebase constraints, and major funnel bets across every configured
|
|
100
|
-
instructions: 'Find structural constraints and durable opportunities.
|
|
703
|
+
sourcePriorities: ['analytics', 'revenuecat', 'paddle', 'seo', 'asc_cli', 'feedback', 'github', 'sentry'],
|
|
704
|
+
objective: 'Revisit positioning, pricing/packaging, onboarding architecture, roadmap assumptions, tracking quality, codebase constraints, and major funnel bets across every configured app.',
|
|
705
|
+
instructions: 'Find structural constraints and durable opportunities, not small UI tweaks. Group the analysis by app and tie recommendations to cohort behavior, monetization, SEO demand, reviews, channel quality, and shipped changes. Include concrete roadmap, pricing, conversion, and traffic recommendations.',
|
|
101
706
|
},
|
|
102
707
|
{
|
|
103
708
|
key: 'six_months',
|
|
104
709
|
title: 'Six-month instrumentation and growth-system audit',
|
|
105
710
|
intervalDays: 182,
|
|
106
711
|
criticalOnly: false,
|
|
107
|
-
focusAreas: ['retention', 'conversion', 'paywall', 'marketing', 'general'],
|
|
108
|
-
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
|
|
109
|
-
objective: 'Audit connector coverage, SDK instrumentation, event taxonomy, data reliability, memory, growth loops, and whether product/code strategy still matches the best users across configured
|
|
110
|
-
instructions: 'Prioritize measurement fixes and system changes that make future analysis more trustworthy. Identify stale events, missing attribution, weak identity, and misleading dashboards.',
|
|
712
|
+
focusAreas: ['retention', 'conversion', 'paywall', 'marketing', 'general', 'seo'],
|
|
713
|
+
sourcePriorities: ['analytics', 'revenuecat', 'paddle', 'seo', 'asc_cli', 'feedback', 'sentry'],
|
|
714
|
+
objective: 'Audit connector coverage, SDK instrumentation, event taxonomy, data reliability, memory, growth loops, and whether product/code strategy still matches the best users across configured apps.',
|
|
715
|
+
instructions: 'Group by app. Prioritize measurement fixes and system changes that make future analysis more trustworthy, then identify the highest-leverage app/revenue/conversion/SEO/traffic improvements. Identify stale events, missing attribution, weak identity, broken feedback loops, and misleading dashboards.',
|
|
111
716
|
},
|
|
112
717
|
{
|
|
113
718
|
key: 'yearly',
|
|
@@ -115,7 +720,7 @@ const DEFAULT_CADENCE_PLAN = [
|
|
|
115
720
|
intervalDays: 365,
|
|
116
721
|
criticalOnly: false,
|
|
117
722
|
focusAreas: ['marketing', 'retention', 'paywall', 'conversion', 'general'],
|
|
118
|
-
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
|
|
723
|
+
sourcePriorities: ['analytics', 'revenuecat', 'paddle', 'seo', 'asc_cli', 'feedback', 'sentry'],
|
|
119
724
|
objective: 'Reset strategy from evidence across every configured project: market/channel fit, monetization model, retention ceiling, product scope, and whether to double down, reposition, rebuild, or sunset major surfaces/features.',
|
|
120
725
|
instructions: 'Use the full year of memory, releases, revenue, acquisition, reviews, code changes, and cohort behavior. Produce strategic experiments and stop-doing decisions.',
|
|
121
726
|
},
|
|
@@ -159,13 +764,27 @@ function isTruthyEnv(value) {
|
|
|
159
764
|
function isFalseyEnv(value) {
|
|
160
765
|
return ['0', 'false', 'no', 'n', 'off'].includes(String(value || '').trim().toLowerCase());
|
|
161
766
|
}
|
|
767
|
+
function resolveDefaultConfigPath() {
|
|
768
|
+
const explicit = String(process.env.OPENCLAW_GROWTH_CONFIG_PATH || '').trim();
|
|
769
|
+
if (explicit)
|
|
770
|
+
return explicit;
|
|
771
|
+
const homeConfigPath = process.env.HOME ? path.join(process.env.HOME, 'data/openclaw-growth-engineer/config.json') : '';
|
|
772
|
+
const homeStatePath = process.env.HOME ? path.join(process.env.HOME, 'data/openclaw-growth-engineer/state.json') : '';
|
|
773
|
+
if (homeConfigPath && existsSync(homeConfigPath) && existsSync(homeStatePath))
|
|
774
|
+
return homeConfigPath;
|
|
775
|
+
if (!existsSync(DEFAULT_CONFIG_PATH) && homeConfigPath && existsSync(homeConfigPath))
|
|
776
|
+
return homeConfigPath;
|
|
777
|
+
return DEFAULT_CONFIG_PATH;
|
|
778
|
+
}
|
|
162
779
|
function parseArgs(argv) {
|
|
780
|
+
const defaultConfigPath = resolveDefaultConfigPath();
|
|
163
781
|
const args = {
|
|
164
|
-
config:
|
|
782
|
+
config: defaultConfigPath,
|
|
165
783
|
connectorWizard: false,
|
|
166
784
|
connectors: '',
|
|
167
785
|
noSelfUpdate: false,
|
|
168
|
-
out:
|
|
786
|
+
out: defaultConfigPath,
|
|
787
|
+
sandboxSmoke: false,
|
|
169
788
|
};
|
|
170
789
|
for (let i = 0; i < argv.length; i += 1) {
|
|
171
790
|
const token = argv[i];
|
|
@@ -193,6 +812,10 @@ function parseArgs(argv) {
|
|
|
193
812
|
else if (token === '--no-self-update') {
|
|
194
813
|
args.noSelfUpdate = true;
|
|
195
814
|
}
|
|
815
|
+
else if (token === '--sandbox-smoke') {
|
|
816
|
+
args.sandboxSmoke = true;
|
|
817
|
+
args.noSelfUpdate = true;
|
|
818
|
+
}
|
|
196
819
|
else if (token === '--help' || token === '-h') {
|
|
197
820
|
printHelpAndExit(0);
|
|
198
821
|
}
|
|
@@ -210,10 +833,14 @@ function printHelpAndExit(exitCode, reason = null) {
|
|
|
210
833
|
OpenClaw Growth Setup Wizard
|
|
211
834
|
|
|
212
835
|
Usage:
|
|
213
|
-
|
|
214
|
-
|
|
836
|
+
npx -y @analyticscli/growth-engineer@preview wizard [--out <config-path>]
|
|
837
|
+
npx -y @analyticscli/growth-engineer@preview wizard --connectors [${CONNECTOR_KEYS.join(',')}]
|
|
838
|
+
|
|
839
|
+
Compatibility note:
|
|
840
|
+
Existing cron/heartbeat runners may still execute generated runtime scripts, but user-facing setup and connector repair should use the npx command above.
|
|
215
841
|
|
|
216
842
|
Options:
|
|
843
|
+
--config <file> Override auto-discovered config path
|
|
217
844
|
--no-self-update Skip the ClawHub skill update check for this run
|
|
218
845
|
`);
|
|
219
846
|
process.exit(exitCode);
|
|
@@ -246,9 +873,18 @@ function getWizardDefaultSourceCommand(sourceName) {
|
|
|
246
873
|
if (normalized === 'revenuecat' || normalized === 'revenue-cat') {
|
|
247
874
|
return nodeRuntimeScriptCommand('export-revenuecat-summary.mjs');
|
|
248
875
|
}
|
|
876
|
+
if (normalized === 'paddle') {
|
|
877
|
+
return nodeRuntimeScriptCommand('export-paddle-summary.mjs');
|
|
878
|
+
}
|
|
879
|
+
if (['seo', 'gsc', 'google-search-console', 'search-console', 'dataforseo'].includes(normalized)) {
|
|
880
|
+
return nodeRuntimeScriptCommand('export-seo-summary.mjs');
|
|
881
|
+
}
|
|
249
882
|
if (normalized === 'sentry' || normalized === 'glitchtip') {
|
|
250
883
|
return nodeRuntimeScriptCommand('export-sentry-summary.mjs');
|
|
251
884
|
}
|
|
885
|
+
if (normalized === 'coolify') {
|
|
886
|
+
return growthEngineerPackageCommand('exporters coolify-summary');
|
|
887
|
+
}
|
|
252
888
|
if (normalized === 'feedback') {
|
|
253
889
|
return getDefaultSourceCommand('feedback');
|
|
254
890
|
}
|
|
@@ -261,22 +897,43 @@ function replaceLegacyRuntimeScriptCommand(command) {
|
|
|
261
897
|
const trimmed = String(command || '').trim();
|
|
262
898
|
if (!trimmed)
|
|
263
899
|
return trimmed;
|
|
264
|
-
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-runner\.mjs|openclaw-growth-preflight\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
|
|
900
|
+
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-runner\.mjs|openclaw-growth-preflight\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
|
|
265
901
|
}
|
|
266
|
-
function
|
|
902
|
+
function sourceCommandNeedsActiveConfig(sourceName, command) {
|
|
903
|
+
const normalized = String(sourceName || '').trim().toLowerCase();
|
|
904
|
+
const value = String(command || '').toLowerCase();
|
|
905
|
+
return (normalized === 'sentry' ||
|
|
906
|
+
normalized === 'glitchtip' ||
|
|
907
|
+
normalized === 'coolify' ||
|
|
908
|
+
value.includes('export-sentry-summary') ||
|
|
909
|
+
value.includes('export-coolify-summary') ||
|
|
910
|
+
value.includes('exporters coolify-summary'));
|
|
911
|
+
}
|
|
912
|
+
function withWizardConfigArg(sourceName, command, configPath) {
|
|
913
|
+
const trimmed = String(command || '').trim();
|
|
914
|
+
if (!trimmed || !configPath || !sourceCommandNeedsActiveConfig(sourceName, trimmed))
|
|
915
|
+
return trimmed;
|
|
916
|
+
return trimmed
|
|
917
|
+
.replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${quote(configPath)}`)
|
|
918
|
+
.replace(/(^|\s)--config\s+(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${quote(configPath)}`)
|
|
919
|
+
.replace(new RegExp(`^(?!.*(?:^|\\s)--config(?:=|\\s|$))(.+)$`), `$1 --config ${quote(configPath)}`)
|
|
920
|
+
.trim();
|
|
921
|
+
}
|
|
922
|
+
function normalizeWizardSourceCommand(sourceName, source, configPath = null) {
|
|
267
923
|
const current = replaceLegacyRuntimeScriptCommand(source?.command || '');
|
|
268
|
-
|
|
924
|
+
const command = current || getWizardDefaultSourceCommand(sourceName);
|
|
925
|
+
return withWizardConfigArg(sourceName, command, configPath);
|
|
269
926
|
}
|
|
270
|
-
function migrateRuntimeSourceCommands(config) {
|
|
927
|
+
function migrateRuntimeSourceCommands(config, configPath = null) {
|
|
271
928
|
if (!config || typeof config !== 'object')
|
|
272
929
|
return config;
|
|
273
930
|
const sources = config.sources && typeof config.sources === 'object' ? config.sources : {};
|
|
274
931
|
const nextSources = { ...sources };
|
|
275
|
-
for (const sourceName of ['analytics', 'revenuecat', 'sentry']) {
|
|
932
|
+
for (const sourceName of ['analytics', 'revenuecat', 'paddle', 'seo', 'sentry', 'coolify']) {
|
|
276
933
|
if (nextSources[sourceName]?.mode === 'command') {
|
|
277
934
|
nextSources[sourceName] = {
|
|
278
935
|
...nextSources[sourceName],
|
|
279
|
-
command: normalizeWizardSourceCommand(sourceName, nextSources[sourceName]),
|
|
936
|
+
command: normalizeWizardSourceCommand(sourceName, nextSources[sourceName], configPath),
|
|
280
937
|
};
|
|
281
938
|
}
|
|
282
939
|
}
|
|
@@ -290,7 +947,7 @@ function migrateRuntimeSourceCommands(config) {
|
|
|
290
947
|
: service;
|
|
291
948
|
return {
|
|
292
949
|
...source,
|
|
293
|
-
command: normalizeWizardSourceCommand(sourceName, source),
|
|
950
|
+
command: normalizeWizardSourceCommand(sourceName, source, configPath),
|
|
294
951
|
};
|
|
295
952
|
});
|
|
296
953
|
}
|
|
@@ -303,7 +960,7 @@ async function migrateRuntimeSourceCommandsFile(configPath) {
|
|
|
303
960
|
const existing = await readJsonIfPresent(configPath).catch(() => null);
|
|
304
961
|
if (!existing || typeof existing !== 'object')
|
|
305
962
|
return null;
|
|
306
|
-
const migrated = migrateRuntimeSourceCommands(existing);
|
|
963
|
+
const migrated = migrateRuntimeSourceCommands(existing, configPath);
|
|
307
964
|
if (JSON.stringify(existing.sources || {}) !== JSON.stringify(migrated.sources || {})) {
|
|
308
965
|
await writeJsonFile(configPath, migrated);
|
|
309
966
|
}
|
|
@@ -321,10 +978,60 @@ function normalizeConnectorKey(value) {
|
|
|
321
978
|
return 'github';
|
|
322
979
|
if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
|
|
323
980
|
return 'revenuecat';
|
|
981
|
+
if (['paddle', 'paddle-billing', 'billing-metrics', 'web-revenue'].includes(normalized))
|
|
982
|
+
return 'paddle';
|
|
983
|
+
if (['seo', 'gsc', 'google-search-console', 'search-console', 'dataforseo', 'organic-search'].includes(normalized))
|
|
984
|
+
return 'seo';
|
|
324
985
|
if (['sentry', 'sentry-api', 'sentry-mcp', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
|
|
325
986
|
return 'sentry';
|
|
987
|
+
if (['coolify', 'coolify-api', 'deployment', 'deployments', 'hosting', 'infra', 'infrastructure'].includes(normalized))
|
|
988
|
+
return 'coolify';
|
|
326
989
|
if (['asc', 'asc-cli', 'app-store-connect', 'appstoreconnect', 'app-store'].includes(normalized))
|
|
327
990
|
return 'asc';
|
|
991
|
+
if (['stripe', 'stripe-billing', 'stripe-payments'].includes(normalized))
|
|
992
|
+
return 'stripe';
|
|
993
|
+
if (['lemonsqueezy', 'lemon-squeezy', 'lemon', 'ls'].includes(normalized))
|
|
994
|
+
return 'lemonsqueezy';
|
|
995
|
+
if (['adapty', 'adapty-paywalls', 'adapty-subscriptions'].includes(normalized))
|
|
996
|
+
return 'adapty';
|
|
997
|
+
if (['superwall', 'superwall-paywalls'].includes(normalized))
|
|
998
|
+
return 'superwall';
|
|
999
|
+
if (['google-play', 'google-play-console', 'play-console', 'play-store', 'android-store'].includes(normalized))
|
|
1000
|
+
return 'google-play';
|
|
1001
|
+
if (['datadog', 'datadog-rum', 'datadog-apm', 'datadog-logs'].includes(normalized))
|
|
1002
|
+
return 'datadog';
|
|
1003
|
+
if (['bugsnag', 'bugsnag-crashes'].includes(normalized))
|
|
1004
|
+
return 'bugsnag';
|
|
1005
|
+
if (['intercom', 'intercom-support'].includes(normalized))
|
|
1006
|
+
return 'intercom';
|
|
1007
|
+
if (['zendesk', 'zendesk-support'].includes(normalized))
|
|
1008
|
+
return 'zendesk';
|
|
1009
|
+
if (['apple-search-ads', 'apple-ads', 'asa', 'search-ads'].includes(normalized))
|
|
1010
|
+
return 'apple-search-ads';
|
|
1011
|
+
if (['google-ads', 'adwords'].includes(normalized))
|
|
1012
|
+
return 'google-ads';
|
|
1013
|
+
if (['meta-ads', 'facebook-ads', 'instagram-ads', 'fb-ads'].includes(normalized))
|
|
1014
|
+
return 'meta-ads';
|
|
1015
|
+
if (['tiktok-ads', 'tiktok-business', 'tiktok-business-api'].includes(normalized))
|
|
1016
|
+
return 'tiktok-ads';
|
|
1017
|
+
if (['vercel', 'vercel-deployments', 'vercel-hosting'].includes(normalized))
|
|
1018
|
+
return 'vercel';
|
|
1019
|
+
if (['cloudflare', 'cf', 'cloudflare-workers', 'cloudflare-pages'].includes(normalized))
|
|
1020
|
+
return 'cloudflare';
|
|
1021
|
+
if (['resend', 'resend-email'].includes(normalized))
|
|
1022
|
+
return 'resend';
|
|
1023
|
+
if (['customerio', 'customer-io', 'customer.io', 'cio'].includes(normalized))
|
|
1024
|
+
return 'customerio';
|
|
1025
|
+
if (['mailchimp', 'mailchimp-marketing'].includes(normalized))
|
|
1026
|
+
return 'mailchimp';
|
|
1027
|
+
if (['appfollow', 'app-follow'].includes(normalized))
|
|
1028
|
+
return 'appfollow';
|
|
1029
|
+
if (['apptweak', 'app-tweak'].includes(normalized))
|
|
1030
|
+
return 'apptweak';
|
|
1031
|
+
if (['linear', 'linear-issues', 'linear-planning'].includes(normalized))
|
|
1032
|
+
return 'linear';
|
|
1033
|
+
if (['postiz', 'postiz-api', 'social-publishing', 'social-scheduler'].includes(normalized))
|
|
1034
|
+
return 'postiz';
|
|
328
1035
|
return null;
|
|
329
1036
|
}
|
|
330
1037
|
function parseConnectorList(value) {
|
|
@@ -350,13 +1057,27 @@ function isConnectorLocallyConfigured(key) {
|
|
|
350
1057
|
return Boolean(process.env.GITHUB_TOKEN?.trim());
|
|
351
1058
|
if (key === 'revenuecat')
|
|
352
1059
|
return Boolean(process.env.REVENUECAT_API_KEY?.trim());
|
|
1060
|
+
if (key === 'paddle')
|
|
1061
|
+
return Boolean(process.env.PADDLE_API_KEY?.trim());
|
|
1062
|
+
if (key === 'seo') {
|
|
1063
|
+
return Boolean(process.env.GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN?.trim() ||
|
|
1064
|
+
process.env.GSC_ACCESS_TOKEN?.trim() ||
|
|
1065
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS?.trim() ||
|
|
1066
|
+
process.env.GSC_SERVICE_ACCOUNT_JSON?.trim());
|
|
1067
|
+
}
|
|
353
1068
|
if (key === 'sentry')
|
|
354
1069
|
return Boolean(process.env.SENTRY_AUTH_TOKEN?.trim());
|
|
1070
|
+
if (key === 'coolify')
|
|
1071
|
+
return Boolean(process.env.COOLIFY_API_TOKEN?.trim() && process.env.COOLIFY_BASE_URL?.trim());
|
|
355
1072
|
if (key === 'asc') {
|
|
356
1073
|
return Boolean(process.env.ASC_KEY_ID?.trim() &&
|
|
357
1074
|
process.env.ASC_ISSUER_ID?.trim() &&
|
|
358
1075
|
(process.env.ASC_PRIVATE_KEY_PATH?.trim() || process.env.ASC_PRIVATE_KEY?.trim()));
|
|
359
1076
|
}
|
|
1077
|
+
const accountConnector = getAccountSignalConnectorDefinition(key);
|
|
1078
|
+
if (accountConnector) {
|
|
1079
|
+
return accountConnector.credentials.some((credential) => Boolean(process.env[credential.env]?.trim()));
|
|
1080
|
+
}
|
|
360
1081
|
return false;
|
|
361
1082
|
}
|
|
362
1083
|
function getRequiredConnectorKeys() {
|
|
@@ -709,10 +1430,19 @@ function normalizeConnectorProgressKey(key) {
|
|
|
709
1430
|
return 'github';
|
|
710
1431
|
if (normalized === 'revenuecat')
|
|
711
1432
|
return 'revenuecat';
|
|
1433
|
+
if (normalized === 'paddle')
|
|
1434
|
+
return 'paddle';
|
|
1435
|
+
if (normalized === 'seo' || normalized === 'gsc' || normalized === 'google-search-console')
|
|
1436
|
+
return 'seo';
|
|
712
1437
|
if (normalized === 'sentry')
|
|
713
1438
|
return 'sentry';
|
|
1439
|
+
if (normalized === 'coolify')
|
|
1440
|
+
return 'coolify';
|
|
714
1441
|
if (normalized === 'asc' || normalized === 'appstoreconnect' || normalized === 'app-store-connect')
|
|
715
1442
|
return 'asc';
|
|
1443
|
+
const accountConnector = normalizeConnectorKey(normalized);
|
|
1444
|
+
if (accountConnector && accountConnector !== 'all')
|
|
1445
|
+
return accountConnector;
|
|
716
1446
|
return null;
|
|
717
1447
|
}
|
|
718
1448
|
async function withConnectorHealthLoading(taskFactory) {
|
|
@@ -880,7 +1610,10 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
|
|
|
880
1610
|
analytics: connectors.analyticscli,
|
|
881
1611
|
github: connectors.github,
|
|
882
1612
|
revenuecat: connectors.revenuecat,
|
|
1613
|
+
paddle: connectors.paddle,
|
|
1614
|
+
seo: connectors.seo,
|
|
883
1615
|
sentry: connectors.sentry,
|
|
1616
|
+
coolify: connectors.coolify,
|
|
884
1617
|
asc: connectors.appStoreConnect,
|
|
885
1618
|
};
|
|
886
1619
|
return Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, healthByConnector)]));
|
|
@@ -1286,9 +2019,22 @@ function summarizeFailureFix(connector, blockers) {
|
|
|
1286
2019
|
if (connector === 'revenuecat') {
|
|
1287
2020
|
return 'Paste a RevenueCat v2 secret API key with read-only project permissions, then rerun setup.';
|
|
1288
2021
|
}
|
|
2022
|
+
if (connector === 'paddle') {
|
|
2023
|
+
return 'Paste a Paddle API key with metrics.read permission for the live account, then rerun setup.';
|
|
2024
|
+
}
|
|
2025
|
+
if (connector === 'seo') {
|
|
2026
|
+
return 'Configure Search Console read access. Leave GSC_SITE_URL empty to scan all verified properties in the account, or set it only when you intentionally want one property.';
|
|
2027
|
+
}
|
|
2028
|
+
if (connector === 'coolify') {
|
|
2029
|
+
return 'Paste a Coolify base URL and read-only API token from Keys & Tokens / API tokens, then rerun setup.';
|
|
2030
|
+
}
|
|
1289
2031
|
if (connector === 'asc') {
|
|
1290
2032
|
return 'Rerun ASC setup and verify ASC credentials, key role access, and `asc apps list --output json`.';
|
|
1291
2033
|
}
|
|
2034
|
+
if (isAccountSignalConnector(connector)) {
|
|
2035
|
+
const definition = getAccountSignalConnectorDefinition(connector);
|
|
2036
|
+
return `Paste ${definition?.credentials.map((credential) => credential.env).join(' / ') || connector} in the connector wizard. Keep setup account-wide; do not add project, app, product, paywall, or service IDs unless a later run explicitly narrows scope.`;
|
|
2037
|
+
}
|
|
1292
2038
|
return blockers.find((blocker) => blocker.remediation)?.remediation || 'Fix the failing configuration and rerun setup.';
|
|
1293
2039
|
}
|
|
1294
2040
|
function connectorForBlocker(blocker) {
|
|
@@ -1414,10 +2160,22 @@ function connectorFromCheckName(name) {
|
|
|
1414
2160
|
return 'github';
|
|
1415
2161
|
if (value.includes('revenuecat') || value.includes('REVENUECAT'))
|
|
1416
2162
|
return 'revenuecat';
|
|
2163
|
+
if (value.includes('paddle') || value.includes('PADDLE'))
|
|
2164
|
+
return 'paddle';
|
|
2165
|
+
if (value.includes('seo') || value.includes('GSC') || value.includes('GOOGLE_SEARCH_CONSOLE'))
|
|
2166
|
+
return 'seo';
|
|
1417
2167
|
if (value.includes('sentry') || value.includes('SENTRY') || value.includes('GLITCHTIP'))
|
|
1418
2168
|
return 'sentry';
|
|
2169
|
+
if (value.includes('coolify') || value.includes('COOLIFY'))
|
|
2170
|
+
return 'coolify';
|
|
1419
2171
|
if (value.includes('asc') || value.includes('ASC_'))
|
|
1420
2172
|
return 'asc';
|
|
2173
|
+
for (const key of ACCOUNT_SIGNAL_CONNECTOR_KEYS) {
|
|
2174
|
+
const definition = getAccountSignalConnectorDefinition(key);
|
|
2175
|
+
const envMatch = definition?.credentials.some((credential) => value.includes(credential.env));
|
|
2176
|
+
if (value.includes(key) || value.includes(key.toUpperCase()) || envMatch)
|
|
2177
|
+
return key;
|
|
2178
|
+
}
|
|
1421
2179
|
return null;
|
|
1422
2180
|
}
|
|
1423
2181
|
function connectorTitle(key) {
|
|
@@ -1557,9 +2315,29 @@ function buildSetupTestProgressPlan(selected) {
|
|
|
1557
2315
|
if (selectedSet.has('revenuecat')) {
|
|
1558
2316
|
items.push({ key: 'revenuecat', label: 'RevenueCat', detail: 'waiting for API key auth + project read', status: 'pending' });
|
|
1559
2317
|
}
|
|
2318
|
+
if (selectedSet.has('paddle')) {
|
|
2319
|
+
items.push({ key: 'paddle', label: 'Paddle', detail: 'waiting for metrics API auth + revenue read', status: 'pending' });
|
|
2320
|
+
}
|
|
2321
|
+
if (selectedSet.has('seo')) {
|
|
2322
|
+
items.push({ key: 'seo', label: 'SEO / GSC', detail: 'waiting for Search Console auth or CSV/DataForSEO config', status: 'pending' });
|
|
2323
|
+
}
|
|
2324
|
+
if (selectedSet.has('coolify')) {
|
|
2325
|
+
items.push({ key: 'coolify', label: 'Coolify', detail: 'waiting for API key auth + deployment/resource read', status: 'pending' });
|
|
2326
|
+
}
|
|
1560
2327
|
if (selectedSet.has('github')) {
|
|
1561
2328
|
items.push({ key: 'github', label: 'GitHub', detail: 'waiting for repo/token access check', status: 'pending' });
|
|
1562
2329
|
}
|
|
2330
|
+
for (const key of ACCOUNT_SIGNAL_CONNECTOR_KEYS) {
|
|
2331
|
+
if (!selectedSet.has(key))
|
|
2332
|
+
continue;
|
|
2333
|
+
const definition = getAccountSignalConnectorDefinition(key);
|
|
2334
|
+
items.push({
|
|
2335
|
+
key,
|
|
2336
|
+
label: definition?.label || key,
|
|
2337
|
+
detail: 'waiting for account-wide credential presence and source wiring',
|
|
2338
|
+
status: 'pending',
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
1563
2341
|
items.push({
|
|
1564
2342
|
key: 'finalize',
|
|
1565
2343
|
label: 'Finalizing result',
|
|
@@ -2218,6 +2996,8 @@ async function upsertSentryAccountsConfig(configPath, accounts) {
|
|
|
2218
2996
|
: [];
|
|
2219
2997
|
const merged = new Map();
|
|
2220
2998
|
for (const account of existingAccounts) {
|
|
2999
|
+
if (isPlaceholderSentryAccount(account))
|
|
3000
|
+
continue;
|
|
2221
3001
|
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
2222
3002
|
if (id)
|
|
2223
3003
|
merged.set(id, account);
|
|
@@ -2234,13 +3014,75 @@ async function upsertSentryAccountsConfig(configPath, accounts) {
|
|
|
2234
3014
|
...(config.sources?.sentry || {}),
|
|
2235
3015
|
enabled: true,
|
|
2236
3016
|
mode: 'command',
|
|
2237
|
-
command:
|
|
3017
|
+
command: normalizeWizardSourceCommand('sentry', config.sources?.sentry || {}, configPath),
|
|
2238
3018
|
accounts: [...merged.values()],
|
|
2239
3019
|
},
|
|
2240
3020
|
};
|
|
2241
3021
|
await writeJsonFile(configPath, config);
|
|
2242
3022
|
return true;
|
|
2243
3023
|
}
|
|
3024
|
+
function isPlaceholderSentryAccount(account) {
|
|
3025
|
+
const baseUrl = String(account?.baseUrl || account?.base_url || account?.url || '').trim().toLowerCase();
|
|
3026
|
+
const org = String(account?.org || account?.organization || '').trim().toLowerCase();
|
|
3027
|
+
const projects = Array.isArray(account?.projects)
|
|
3028
|
+
? account.projects.map((project) => String(project || '').trim().toLowerCase())
|
|
3029
|
+
: [];
|
|
3030
|
+
return (org === 'owner-org' ||
|
|
3031
|
+
baseUrl.includes('example.com') ||
|
|
3032
|
+
projects.includes('ios-app') ||
|
|
3033
|
+
projects.includes('backend-api') ||
|
|
3034
|
+
projects.includes('web-app'));
|
|
3035
|
+
}
|
|
3036
|
+
async function verifySentryAccountsConfig(configPath, expectedAccounts) {
|
|
3037
|
+
if (!(await fileExists(configPath))) {
|
|
3038
|
+
return { ok: false, detail: `${configPath} does not exist` };
|
|
3039
|
+
}
|
|
3040
|
+
const config = await readJsonFile(configPath);
|
|
3041
|
+
const source = config?.sources?.sentry;
|
|
3042
|
+
if (!source || source.enabled !== true) {
|
|
3043
|
+
return { ok: false, detail: 'sources.sentry.enabled is not true' };
|
|
3044
|
+
}
|
|
3045
|
+
if (source.mode !== 'command') {
|
|
3046
|
+
return { ok: false, detail: 'sources.sentry.mode is not command' };
|
|
3047
|
+
}
|
|
3048
|
+
const configuredAccounts = Array.isArray(source.accounts) ? source.accounts : [];
|
|
3049
|
+
const realAccounts = configuredAccounts.filter((account) => !isPlaceholderSentryAccount(account));
|
|
3050
|
+
if (realAccounts.length === 0) {
|
|
3051
|
+
return { ok: false, detail: 'sources.sentry.accounts contains no non-placeholder account' };
|
|
3052
|
+
}
|
|
3053
|
+
const configuredIds = new Set(realAccounts.map((account) => String(account?.id || account?.key || '').trim()).filter(Boolean));
|
|
3054
|
+
const missingIds = expectedAccounts
|
|
3055
|
+
.map((account) => String(account?.id || '').trim())
|
|
3056
|
+
.filter((id) => id && !configuredIds.has(id));
|
|
3057
|
+
if (missingIds.length > 0) {
|
|
3058
|
+
return { ok: false, detail: `sources.sentry.accounts is missing configured account id(s): ${missingIds.join(', ')}` };
|
|
3059
|
+
}
|
|
3060
|
+
return { ok: true, detail: `${realAccounts.length} active Sentry-compatible account(s) configured` };
|
|
3061
|
+
}
|
|
3062
|
+
async function upsertCoolifyConfig(configPath, { baseUrl, tokenEnv = 'COOLIFY_API_TOKEN' }) {
|
|
3063
|
+
if (!(await fileExists(configPath)))
|
|
3064
|
+
return false;
|
|
3065
|
+
const coolifyCommand = `${getWizardDefaultSourceCommand('coolify')} --config ${quote(configPath)}`;
|
|
3066
|
+
const config = await readJsonFile(configPath);
|
|
3067
|
+
config.sources = {
|
|
3068
|
+
...(config.sources || {}),
|
|
3069
|
+
coolify: {
|
|
3070
|
+
...(config.sources?.coolify || {}),
|
|
3071
|
+
enabled: true,
|
|
3072
|
+
mode: 'command',
|
|
3073
|
+
command: coolifyCommand,
|
|
3074
|
+
baseUrl,
|
|
3075
|
+
tokenEnv,
|
|
3076
|
+
},
|
|
3077
|
+
};
|
|
3078
|
+
config.secrets = {
|
|
3079
|
+
...(config.secrets || {}),
|
|
3080
|
+
coolifyTokenEnv: tokenEnv,
|
|
3081
|
+
coolifyTokenRef: { source: 'env', provider: 'default', id: tokenEnv },
|
|
3082
|
+
};
|
|
3083
|
+
await writeJsonFile(configPath, config);
|
|
3084
|
+
return true;
|
|
3085
|
+
}
|
|
2244
3086
|
const ASC_PRIVATE_KEY_BEGIN = '-----BEGIN PRIVATE KEY-----';
|
|
2245
3087
|
const ASC_PRIVATE_KEY_END = '-----END PRIVATE KEY-----';
|
|
2246
3088
|
const BRACKETED_PASTE_START = new RegExp(`${String.fromCharCode(27)}\\[200~`, 'g');
|
|
@@ -2514,6 +3356,121 @@ async function guideRevenueCatConnector(rl, secrets) {
|
|
|
2514
3356
|
if (apiKey)
|
|
2515
3357
|
secrets.REVENUECAT_API_KEY = apiKey;
|
|
2516
3358
|
}
|
|
3359
|
+
async function guidePaddleConnector(rl, secrets) {
|
|
3360
|
+
printSection('Paddle Billing metrics', [
|
|
3361
|
+
'Use this when OpenClaw should read web checkout, revenue, MRR, refunds, chargebacks, and active subscriber metrics.',
|
|
3362
|
+
]);
|
|
3363
|
+
process.stdout.write('\nCreate or update a Paddle API key here:\n https://vendors.paddle.com/authentication\n\n');
|
|
3364
|
+
printBullets([
|
|
3365
|
+
'Open Paddle > Developer Tools > Authentication.',
|
|
3366
|
+
'Create a new API key for the live account when you want production revenue evidence.',
|
|
3367
|
+
'Grant `metrics.read`. Keep write permissions off unless another workflow explicitly needs them.',
|
|
3368
|
+
'Do not select or hard-code a single product in the wizard; the Growth Engineer should keep account-level metrics context.',
|
|
3369
|
+
'Paste the key here so it is stored only in the local chmod 600 secrets file.',
|
|
3370
|
+
]);
|
|
3371
|
+
const apiKey = await maybePromptSecret(rl, 'Paste PADDLE_API_KEY into this local terminal', 'PADDLE_API_KEY');
|
|
3372
|
+
if (apiKey)
|
|
3373
|
+
secrets.PADDLE_API_KEY = apiKey;
|
|
3374
|
+
}
|
|
3375
|
+
async function guideSeoConnector(rl, secrets) {
|
|
3376
|
+
printSection('SEO / Google Search Console / DataForSEO', [
|
|
3377
|
+
'Use this when OpenClaw should read organic search demand, GSC clicks/impressions/CTR/position, and optional paid keyword ideas.',
|
|
3378
|
+
]);
|
|
3379
|
+
process.stdout.write('\nGoogle Search Console:\n https://search.google.com/search-console\nGoogle Cloud service accounts:\n https://console.cloud.google.com/iam-admin/serviceaccounts\nDataForSEO API dashboard:\n https://app.dataforseo.com/api-dashboard\n\n');
|
|
3380
|
+
printBullets([
|
|
3381
|
+
'Preferred: give the token/service account access to all Search Console properties you want analyzed.',
|
|
3382
|
+
'Leave the property URL empty to let the exporter list and query all verified GSC properties in the account.',
|
|
3383
|
+
'Enter a property URL only when you intentionally want to restrict analysis to one site.',
|
|
3384
|
+
'For OAuth token mode, paste a read-only Search Console token with `webmasters.readonly` scope.',
|
|
3385
|
+
'For service-account mode, add the service account email as a restricted/full user in Search Console, then set GOOGLE_APPLICATION_CREDENTIALS or GSC_SERVICE_ACCOUNT_JSON outside this wizard.',
|
|
3386
|
+
'DataForSEO is optional and paid. The exporter refuses paid calls unless the source command includes --confirm-paid and a small --max-paid-requests cap.',
|
|
3387
|
+
'CSV-only mode is also supported with --gsc-csv or --csv in sources.seo.command.',
|
|
3388
|
+
]);
|
|
3389
|
+
const siteUrl = await ask(rl, 'Optional GSC property URL (empty = all verified properties)', process.env.GSC_SITE_URL || '');
|
|
3390
|
+
if (siteUrl.trim())
|
|
3391
|
+
secrets.GSC_SITE_URL = siteUrl.trim();
|
|
3392
|
+
const gscToken = await maybePromptSecret(rl, 'Paste GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN, or leave empty for service-account/all-sites/CSV mode', 'GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN');
|
|
3393
|
+
if (gscToken)
|
|
3394
|
+
secrets.GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN = gscToken;
|
|
3395
|
+
const useDataForSeo = await askYesNo(rl, 'Also store DataForSEO credentials for optional paid keyword research?', false);
|
|
3396
|
+
if (useDataForSeo) {
|
|
3397
|
+
const login = await maybePromptSecret(rl, 'Paste DATAFORSEO_LOGIN into this local terminal', 'DATAFORSEO_LOGIN');
|
|
3398
|
+
const password = await maybePromptSecret(rl, 'Paste DATAFORSEO_PASSWORD into this local terminal', 'DATAFORSEO_PASSWORD');
|
|
3399
|
+
if (login)
|
|
3400
|
+
secrets.DATAFORSEO_LOGIN = login;
|
|
3401
|
+
if (password)
|
|
3402
|
+
secrets.DATAFORSEO_PASSWORD = password;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
function buildAccountSignalExtraSourceConfig(key, existing = {}) {
|
|
3406
|
+
const definition = getAccountSignalConnectorDefinition(key);
|
|
3407
|
+
if (!definition)
|
|
3408
|
+
return existing;
|
|
3409
|
+
return {
|
|
3410
|
+
...buildExtraSourceConfig(definition.service, {
|
|
3411
|
+
key: definition.key,
|
|
3412
|
+
label: definition.label,
|
|
3413
|
+
enabled: true,
|
|
3414
|
+
mode: 'file',
|
|
3415
|
+
secretEnv: definition.credentials[0]?.env || null,
|
|
3416
|
+
hint: definition.signalHint,
|
|
3417
|
+
}),
|
|
3418
|
+
...existing,
|
|
3419
|
+
key: definition.key,
|
|
3420
|
+
label: definition.label,
|
|
3421
|
+
service: definition.service,
|
|
3422
|
+
enabled: true,
|
|
3423
|
+
mode: existing.mode || 'file',
|
|
3424
|
+
path: existing.path || getDefaultSourcePath(definition.key),
|
|
3425
|
+
secretEnv: existing.secretEnv || definition.credentials[0]?.env || null,
|
|
3426
|
+
accountWide: true,
|
|
3427
|
+
projectScope: 'discover_from_account',
|
|
3428
|
+
docsUrl: definition.docsUrl,
|
|
3429
|
+
signalKind: definition.sourceKind,
|
|
3430
|
+
experimental: Boolean(definition.experimental),
|
|
3431
|
+
hint: existing.hint || definition.signalHint,
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
async function upsertAccountSignalConnectorConfig(configPath, key) {
|
|
3435
|
+
const definition = getAccountSignalConnectorDefinition(key);
|
|
3436
|
+
if (!definition)
|
|
3437
|
+
return false;
|
|
3438
|
+
const config = await loadEditableConfig(configPath);
|
|
3439
|
+
const sources = config.sources && typeof config.sources === 'object' ? config.sources : {};
|
|
3440
|
+
const extra = Array.isArray(sources.extra) ? sources.extra : [];
|
|
3441
|
+
const nextExtra = extra.filter((source) => String(source?.key || source?.service || '') !== definition.key);
|
|
3442
|
+
const existing = extra.find((source) => String(source?.key || source?.service || '') === definition.key) || {};
|
|
3443
|
+
nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing));
|
|
3444
|
+
config.sources = {
|
|
3445
|
+
...sources,
|
|
3446
|
+
extra: nextExtra,
|
|
3447
|
+
};
|
|
3448
|
+
await writeJsonFile(configPath, config);
|
|
3449
|
+
return true;
|
|
3450
|
+
}
|
|
3451
|
+
async function guideAccountSignalConnector(rl, secrets, key) {
|
|
3452
|
+
const definition = getAccountSignalConnectorDefinition(key);
|
|
3453
|
+
if (!definition)
|
|
3454
|
+
return;
|
|
3455
|
+
printSection(definition.label, [
|
|
3456
|
+
definition.summary,
|
|
3457
|
+
'Setup is account-wide. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here.',
|
|
3458
|
+
]);
|
|
3459
|
+
process.stdout.write(`Docs: ${definition.docsUrl}\n\n`);
|
|
3460
|
+
printBullets(definition.steps);
|
|
3461
|
+
for (const credential of definition.credentials) {
|
|
3462
|
+
const defaultValue = credential.defaultValue ?? process.env[credential.env] ?? '';
|
|
3463
|
+
const value = credential.optional
|
|
3464
|
+
? await maybePromptSecret(rl, credential.prompt, credential.env)
|
|
3465
|
+
: await maybePromptSecret(rl, credential.prompt, credential.env);
|
|
3466
|
+
const finalValue = value || defaultValue;
|
|
3467
|
+
if (finalValue)
|
|
3468
|
+
secrets[credential.env] = finalValue;
|
|
3469
|
+
else if (!credential.optional) {
|
|
3470
|
+
process.stdout.write(`${credential.env} was not saved. ${definition.label} setup remains pending; rerun this wizard when ready.\n`);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
2517
3474
|
async function guideSentryConnector(rl, secrets) {
|
|
2518
3475
|
printSection('Sentry / GlitchTip', [
|
|
2519
3476
|
'Paste token, org, and base URL. Projects are discovered automatically.',
|
|
@@ -2618,6 +3575,34 @@ async function guideSentryConnector(rl, secrets) {
|
|
|
2618
3575
|
}
|
|
2619
3576
|
return accounts;
|
|
2620
3577
|
}
|
|
3578
|
+
function normalizeCoolifyBaseUrl(value) {
|
|
3579
|
+
const raw = String(value || '').trim().replace(/\/+$/, '');
|
|
3580
|
+
if (!raw)
|
|
3581
|
+
return '';
|
|
3582
|
+
return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
3583
|
+
}
|
|
3584
|
+
async function guideCoolifyConnector(rl, secrets) {
|
|
3585
|
+
printSection('Coolify deployment monitoring', [
|
|
3586
|
+
'Use this when OpenClaw should read deployment, resource, server, and health-check signals from Coolify.',
|
|
3587
|
+
'The token should be read-only. Do not use "*" or sensitive-token permissions for normal monitoring.',
|
|
3588
|
+
]);
|
|
3589
|
+
const baseUrl = normalizeCoolifyBaseUrl(await ask(rl, 'Coolify base URL', process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com'));
|
|
3590
|
+
const tokenUrl = baseUrl ? `${baseUrl}/security/api-tokens` : 'https://<your-coolify-host>/security/api-tokens';
|
|
3591
|
+
process.stdout.write(`\nToken page: ${tokenUrl}\n\n`);
|
|
3592
|
+
printBullets([
|
|
3593
|
+
'Open the Coolify dashboard.',
|
|
3594
|
+
'In the sidebar, go to "Keys & Tokens".',
|
|
3595
|
+
'Open "API tokens".',
|
|
3596
|
+
'Create a new API key/token with read-only permissions.',
|
|
3597
|
+
'Copy the token once and paste it into this local terminal.',
|
|
3598
|
+
]);
|
|
3599
|
+
const token = await maybePromptSecret(rl, 'Paste COOLIFY_API_TOKEN into this local terminal', 'COOLIFY_API_TOKEN');
|
|
3600
|
+
if (baseUrl)
|
|
3601
|
+
secrets.COOLIFY_BASE_URL = baseUrl;
|
|
3602
|
+
if (token)
|
|
3603
|
+
secrets.COOLIFY_API_TOKEN = token;
|
|
3604
|
+
return { baseUrl, tokenEnv: 'COOLIFY_API_TOKEN' };
|
|
3605
|
+
}
|
|
2621
3606
|
async function guideAscConnector(rl, secrets) {
|
|
2622
3607
|
printSection('App Store Connect CLI', [
|
|
2623
3608
|
'Use this mainly for App Store analytics batch reports, plus builds, TestFlight, reviews, ratings, and store context.',
|
|
@@ -2626,11 +3611,18 @@ async function guideAscConnector(rl, secrets) {
|
|
|
2626
3611
|
process.stdout.write('Create an App Store Connect API key here:\n https://appstoreconnect.apple.com/access/integrations/api\n\n');
|
|
2627
3612
|
process.stdout.write('Roles to choose for this key:\n');
|
|
2628
3613
|
printBullets([
|
|
2629
|
-
'Required:
|
|
3614
|
+
'Required for first setup: Admin, because Apple only allows Admin keys to create the initial Analytics Report Request.',
|
|
3615
|
+
'Required for steady-state report downloads after the request exists: Sales and Reports, Finance, or Admin.',
|
|
2630
3616
|
'Recommended: Customer Support, for App Store ratings and review text.',
|
|
2631
3617
|
'Recommended: Developer, for builds, TestFlight, and delivery status.',
|
|
2632
3618
|
'Optional: App Manager, only if OpenClaw should also read or manage app metadata, pricing, or release settings.',
|
|
2633
|
-
'
|
|
3619
|
+
'Least privilege option: run setup once with Admin, then rotate Growth Engineer to a Sales and Reports key for ongoing analytics downloads.',
|
|
3620
|
+
]);
|
|
3621
|
+
process.stdout.write('\nWhy Admin is requested during setup:\n');
|
|
3622
|
+
printBullets([
|
|
3623
|
+
'Growth Engineer automatically creates an ongoing App Analytics report request when none exists.',
|
|
3624
|
+
'Without that request, Apple will not generate Impressions, Product Page Views, App Units, Conversion Rate, and related report instances.',
|
|
3625
|
+
'A non-Admin key can read existing reports, but creation fails with a forbidden response.',
|
|
2634
3626
|
]);
|
|
2635
3627
|
process.stdout.write('\nAfter creating the key, copy these values into this wizard:\n');
|
|
2636
3628
|
printBullets([
|
|
@@ -2638,6 +3630,7 @@ async function guideAscConnector(rl, secrets) {
|
|
|
2638
3630
|
'Key ID from the API key row or from the downloaded file name: AuthKey_<KEY_ID>.p8.',
|
|
2639
3631
|
'Download the .p8 file, open it, then paste the full file content into this terminal.',
|
|
2640
3632
|
'If the .p8 is already on this host, leave the content prompt empty and paste the file path instead.',
|
|
3633
|
+
'Vendor Number from App Store Connect Sales and Trends > Reports, needed for Sales and Trends/App Units reports.',
|
|
2641
3634
|
]);
|
|
2642
3635
|
const keyId = await ask(rl, 'ASC_KEY_ID (leave empty to skip)', process.env.ASC_KEY_ID || '');
|
|
2643
3636
|
const issuerId = await ask(rl, 'ASC_ISSUER_ID (leave empty to skip)', process.env.ASC_ISSUER_ID || '');
|
|
@@ -2659,6 +3652,9 @@ async function guideAscConnector(rl, secrets) {
|
|
|
2659
3652
|
if (privateKeyPath.trim())
|
|
2660
3653
|
secrets.ASC_PRIVATE_KEY_PATH = privateKeyPath.trim();
|
|
2661
3654
|
}
|
|
3655
|
+
const vendorNumber = await ask(rl, 'ASC_VENDOR_NUMBER for Sales and Trends/App Units (leave empty to skip)', process.env.ASC_VENDOR_NUMBER || process.env.ASC_ANALYTICS_VENDOR_NUMBER || '');
|
|
3656
|
+
if (vendorNumber.trim())
|
|
3657
|
+
secrets.ASC_VENDOR_NUMBER = vendorNumber.trim();
|
|
2662
3658
|
}
|
|
2663
3659
|
async function shouldRunSelfUpdate(workspaceRoot, force) {
|
|
2664
3660
|
if (force)
|
|
@@ -2764,6 +3760,7 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
2764
3760
|
process.stdout.write('\n');
|
|
2765
3761
|
const secrets = {};
|
|
2766
3762
|
let sentryAccounts = [];
|
|
3763
|
+
let coolifyConfig = null;
|
|
2767
3764
|
if (selected.includes('analytics')) {
|
|
2768
3765
|
let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
|
|
2769
3766
|
while (true) {
|
|
@@ -2808,6 +3805,34 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
2808
3805
|
break;
|
|
2809
3806
|
}
|
|
2810
3807
|
}
|
|
3808
|
+
if (selected.includes('paddle')) {
|
|
3809
|
+
while (true) {
|
|
3810
|
+
clearTerminal();
|
|
3811
|
+
await guidePaddleConnector(rl, secrets);
|
|
3812
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
3813
|
+
rl,
|
|
3814
|
+
configPath: args.config,
|
|
3815
|
+
connector: 'paddle',
|
|
3816
|
+
secrets,
|
|
3817
|
+
});
|
|
3818
|
+
if (!check.retry)
|
|
3819
|
+
break;
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
if (selected.includes('seo')) {
|
|
3823
|
+
while (true) {
|
|
3824
|
+
clearTerminal();
|
|
3825
|
+
await guideSeoConnector(rl, secrets);
|
|
3826
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
3827
|
+
rl,
|
|
3828
|
+
configPath: args.config,
|
|
3829
|
+
connector: 'seo',
|
|
3830
|
+
secrets,
|
|
3831
|
+
});
|
|
3832
|
+
if (!check.retry)
|
|
3833
|
+
break;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
2811
3836
|
if (selected.includes('sentry')) {
|
|
2812
3837
|
while (true) {
|
|
2813
3838
|
clearTerminal();
|
|
@@ -2823,6 +3848,23 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
2823
3848
|
break;
|
|
2824
3849
|
}
|
|
2825
3850
|
}
|
|
3851
|
+
if (selected.includes('coolify')) {
|
|
3852
|
+
while (true) {
|
|
3853
|
+
clearTerminal();
|
|
3854
|
+
coolifyConfig = await guideCoolifyConnector(rl, secrets);
|
|
3855
|
+
if (coolifyConfig?.baseUrl) {
|
|
3856
|
+
await upsertCoolifyConfig(args.config, coolifyConfig);
|
|
3857
|
+
}
|
|
3858
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
3859
|
+
rl,
|
|
3860
|
+
configPath: args.config,
|
|
3861
|
+
connector: 'coolify',
|
|
3862
|
+
secrets,
|
|
3863
|
+
});
|
|
3864
|
+
if (!check.retry)
|
|
3865
|
+
break;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
2826
3868
|
if (selected.includes('asc')) {
|
|
2827
3869
|
while (true) {
|
|
2828
3870
|
clearTerminal();
|
|
@@ -2837,6 +3879,21 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
2837
3879
|
break;
|
|
2838
3880
|
}
|
|
2839
3881
|
}
|
|
3882
|
+
for (const connector of selected.filter(isAccountSignalConnector)) {
|
|
3883
|
+
while (true) {
|
|
3884
|
+
clearTerminal();
|
|
3885
|
+
await guideAccountSignalConnector(rl, secrets, connector);
|
|
3886
|
+
await upsertAccountSignalConnectorConfig(args.config, connector);
|
|
3887
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
3888
|
+
rl,
|
|
3889
|
+
configPath: args.config,
|
|
3890
|
+
connector,
|
|
3891
|
+
secrets,
|
|
3892
|
+
});
|
|
3893
|
+
if (!check.retry)
|
|
3894
|
+
break;
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
2840
3897
|
const secretsFile = resolveSecretsFile();
|
|
2841
3898
|
const wroteSecrets = Object.keys(secrets).length > 0;
|
|
2842
3899
|
clearTerminal();
|
|
@@ -2848,17 +3905,47 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
2848
3905
|
process.stdout.write('\nNo new secrets were written.\n');
|
|
2849
3906
|
}
|
|
2850
3907
|
if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
|
|
2851
|
-
|
|
3908
|
+
const readiness = await verifySentryAccountsConfig(args.config, sentryAccounts);
|
|
3909
|
+
if (readiness.ok) {
|
|
3910
|
+
process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
|
|
3914
|
+
process.stdout.write(`Configured Coolify monitoring for ${coolifyConfig.baseUrl} in ${args.config}.\n`);
|
|
2852
3915
|
}
|
|
2853
3916
|
const env = {
|
|
2854
3917
|
...process.env,
|
|
2855
3918
|
...secrets,
|
|
2856
3919
|
};
|
|
2857
|
-
const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
|
|
3920
|
+
const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))} --only-connectors ${quote(selected.join(','))}`;
|
|
2858
3921
|
let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
|
|
2859
3922
|
let setupPayload = parseJsonFromStdout(setupResult.stdout);
|
|
3923
|
+
const postSetupBlockers = [];
|
|
2860
3924
|
if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
|
|
2861
|
-
|
|
3925
|
+
const readiness = await verifySentryAccountsConfig(args.config, sentryAccounts);
|
|
3926
|
+
if (readiness.ok) {
|
|
3927
|
+
process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
|
|
3928
|
+
}
|
|
3929
|
+
else {
|
|
3930
|
+
postSetupBlockers.push({
|
|
3931
|
+
check: 'connection:sentry',
|
|
3932
|
+
detail: readiness.detail,
|
|
3933
|
+
remediation: 'Rerun Sentry/GlitchTip setup so the active config persists sources.sentry.enabled=true and sources.sentry.accounts[].',
|
|
3934
|
+
});
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
|
|
3938
|
+
process.stdout.write(`Coolify config is up to date in ${args.config}.\n`);
|
|
3939
|
+
}
|
|
3940
|
+
if (postSetupBlockers.length > 0) {
|
|
3941
|
+
setupPayload = {
|
|
3942
|
+
...(setupPayload || {}),
|
|
3943
|
+
ok: false,
|
|
3944
|
+
blockers: [...(Array.isArray(setupPayload?.blockers) ? setupPayload.blockers : []), ...postSetupBlockers],
|
|
3945
|
+
};
|
|
3946
|
+
printSetupFailure({ result: { ...setupResult, ok: false, code: setupResult.code ?? 1 }, payload: setupPayload, command });
|
|
3947
|
+
process.exitCode = 1;
|
|
3948
|
+
return false;
|
|
2862
3949
|
}
|
|
2863
3950
|
if (setupResult.ok && setupPayload?.ok !== false) {
|
|
2864
3951
|
printSetupSuccess(setupPayload);
|
|
@@ -2893,11 +3980,13 @@ async function runConnectorSetupWizard(args) {
|
|
|
2893
3980
|
const existingFixes = connectorKeysNeedingAttention(healthByConnector);
|
|
2894
3981
|
const requestedConnectors = args.connectors ? parseConnectorList(args.connectors) : [];
|
|
2895
3982
|
const chosenConnectors = requestedConnectors.length > 0
|
|
2896
|
-
? orderConnectors(
|
|
3983
|
+
? orderConnectors(requestedConnectors)
|
|
2897
3984
|
: await askConnectorSelectionWithHealth(rl, healthByConnector, existingFixes);
|
|
2898
|
-
const selected =
|
|
3985
|
+
const selected = requestedConnectors.length > 0
|
|
3986
|
+
? orderConnectors(chosenConnectors)
|
|
3987
|
+
: withMissingRequiredAnalyticsConnector(chosenConnectors);
|
|
2899
3988
|
if (selected.length === 0) {
|
|
2900
|
-
throw new Error(
|
|
3989
|
+
throw new Error(`No supported connectors selected. Use ${CONNECTOR_KEYS.join(', ')}, or all.`);
|
|
2901
3990
|
}
|
|
2902
3991
|
await runConnectorSetupSteps({ rl, args, selected, healthByConnector });
|
|
2903
3992
|
}
|
|
@@ -2959,7 +4048,7 @@ function printCadencePlan(cadences) {
|
|
|
2959
4048
|
process.stdout.write('\nDefault growth cadence:\n');
|
|
2960
4049
|
printAsciiTable(['Cadence', 'Every', 'Mode', 'Primary focus', 'What it decides'], cadences.map((cadence) => [
|
|
2961
4050
|
cadence.key,
|
|
2962
|
-
`${cadence.intervalDays}d`,
|
|
4051
|
+
cadence.intervalMinutes ? `${cadence.intervalMinutes}m` : `${cadence.intervalDays}d`,
|
|
2963
4052
|
cadence.criticalOnly ? 'critical only' : 'full review',
|
|
2964
4053
|
Array.isArray(cadence.focusAreas) ? cadence.focusAreas.slice(0, 4).join(', ') : '',
|
|
2965
4054
|
cadence.objective,
|
|
@@ -2990,6 +4079,30 @@ async function askToolUsage(rl) {
|
|
|
2990
4079
|
],
|
|
2991
4080
|
});
|
|
2992
4081
|
}
|
|
4082
|
+
async function askSchedulePreset(rl) {
|
|
4083
|
+
return await askMenuChoice(rl, {
|
|
4084
|
+
title: 'Schedule preset',
|
|
4085
|
+
subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
|
|
4086
|
+
defaultValue: 'recommended',
|
|
4087
|
+
options: [
|
|
4088
|
+
{
|
|
4089
|
+
value: 'recommended',
|
|
4090
|
+
label: 'Recommended',
|
|
4091
|
+
detail: 'OpenClaw/Hermes wake every 30m; Sentry/GlitchTip/Coolify healthcheck runs every 90m; daily and larger reviews stay on cadence.',
|
|
4092
|
+
},
|
|
4093
|
+
{
|
|
4094
|
+
value: 'quiet',
|
|
4095
|
+
label: 'Quiet',
|
|
4096
|
+
detail: 'OpenClaw/Hermes wake hourly; healthcheck runs every 6h; deep reviews unchanged.',
|
|
4097
|
+
},
|
|
4098
|
+
{
|
|
4099
|
+
value: 'manual',
|
|
4100
|
+
label: 'Manual',
|
|
4101
|
+
detail: 'Enter runner, connector-health, cron, and cadence intervals yourself.',
|
|
4102
|
+
},
|
|
4103
|
+
],
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
2993
4106
|
async function askCadencePlan(rl, existingCadences = []) {
|
|
2994
4107
|
const existingByKey = new Map((Array.isArray(existingCadences) ? existingCadences : [])
|
|
2995
4108
|
.filter((cadence) => cadence?.key)
|
|
@@ -3007,7 +4120,7 @@ async function askCadencePlan(rl, existingCadences = []) {
|
|
|
3007
4120
|
options: cadences.map((cadence) => ({
|
|
3008
4121
|
value: cadence.key,
|
|
3009
4122
|
label: cadence.title,
|
|
3010
|
-
detail: `${cadence.intervalDays}d, ${cadence.criticalOnly ? 'critical only' : 'full review'} - ${cadence.objective}`,
|
|
4123
|
+
detail: `${cadence.intervalMinutes ? `${cadence.intervalMinutes}m` : `${cadence.intervalDays}d`}, ${cadence.criticalOnly ? 'critical only' : 'full review'} - ${cadence.objective}`,
|
|
3011
4124
|
})),
|
|
3012
4125
|
});
|
|
3013
4126
|
const selected = new Set(selectedCadences);
|
|
@@ -3021,6 +4134,17 @@ async function askCadencePlan(rl, existingCadences = []) {
|
|
|
3021
4134
|
if (cadence.enabled === false)
|
|
3022
4135
|
continue;
|
|
3023
4136
|
process.stdout.write(`\n${cadence.title}\n`);
|
|
4137
|
+
const intervalDefault = cadence.intervalMinutes ? `${cadence.intervalMinutes}m` : `${cadence.intervalDays || 1}d`;
|
|
4138
|
+
const intervalRaw = await ask(rl, `${cadence.key} interval (for example 90m, 1d, 7d)`, intervalDefault);
|
|
4139
|
+
const intervalMatch = String(intervalRaw || intervalDefault).trim().match(/^(\d+)\s*([md])$/i);
|
|
4140
|
+
if (intervalMatch?.[2]?.toLowerCase() === 'm') {
|
|
4141
|
+
cadence.intervalMinutes = Number.parseInt(intervalMatch[1], 10) || cadence.intervalMinutes || 90;
|
|
4142
|
+
delete cadence.intervalDays;
|
|
4143
|
+
}
|
|
4144
|
+
else if (intervalMatch?.[2]?.toLowerCase() === 'd') {
|
|
4145
|
+
cadence.intervalDays = Number.parseInt(intervalMatch[1], 10) || cadence.intervalDays || 1;
|
|
4146
|
+
delete cadence.intervalMinutes;
|
|
4147
|
+
}
|
|
3024
4148
|
cadence.objective = await ask(rl, `${cadence.key} objective`, cadence.objective);
|
|
3025
4149
|
cadence.instructions = await ask(rl, `${cadence.key} instructions`, cadence.instructions);
|
|
3026
4150
|
const focusAreas = await ask(rl, `${cadence.key} focus areas (comma-separated)`, cadence.focusAreas.join(','));
|
|
@@ -3065,7 +4189,7 @@ function printWizardHeader() {
|
|
|
3065
4189
|
process.stdout.write('OpenClaw Growth Engineer - Setup Wizard\n');
|
|
3066
4190
|
process.stdout.write('This wizard can configure connector secrets. Normal config is written to config JSON; API keys stay in the local chmod 600 secrets file.\n\n');
|
|
3067
4191
|
}
|
|
3068
|
-
async function buildDefaultWizardConfig() {
|
|
4192
|
+
async function buildDefaultWizardConfig(configPath = null) {
|
|
3069
4193
|
return {
|
|
3070
4194
|
version: 7,
|
|
3071
4195
|
generatedAt: new Date().toISOString(),
|
|
@@ -3088,10 +4212,36 @@ async function buildDefaultWizardConfig() {
|
|
|
3088
4212
|
mode: 'command',
|
|
3089
4213
|
command: getWizardDefaultSourceCommand('revenuecat'),
|
|
3090
4214
|
},
|
|
4215
|
+
paddle: {
|
|
4216
|
+
enabled: true,
|
|
4217
|
+
mode: 'command',
|
|
4218
|
+
command: getWizardDefaultSourceCommand('paddle'),
|
|
4219
|
+
environment: 'live',
|
|
4220
|
+
},
|
|
4221
|
+
seo: {
|
|
4222
|
+
enabled: true,
|
|
4223
|
+
mode: 'command',
|
|
4224
|
+
command: getWizardDefaultSourceCommand('seo'),
|
|
4225
|
+
siteUrl: process.env.GSC_SITE_URL || '',
|
|
4226
|
+
paidProvider: {
|
|
4227
|
+
dataforseo: {
|
|
4228
|
+
enabled: false,
|
|
4229
|
+
confirmPaid: false,
|
|
4230
|
+
maxPaidRequests: 1,
|
|
4231
|
+
},
|
|
4232
|
+
},
|
|
4233
|
+
},
|
|
3091
4234
|
sentry: {
|
|
3092
4235
|
enabled: true,
|
|
3093
4236
|
mode: 'command',
|
|
3094
|
-
command:
|
|
4237
|
+
command: normalizeWizardSourceCommand('sentry', {}, configPath),
|
|
4238
|
+
},
|
|
4239
|
+
coolify: {
|
|
4240
|
+
enabled: true,
|
|
4241
|
+
mode: 'command',
|
|
4242
|
+
command: normalizeWizardSourceCommand('coolify', {}, configPath),
|
|
4243
|
+
baseUrl: process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com',
|
|
4244
|
+
tokenEnv: 'COOLIFY_API_TOKEN',
|
|
3095
4245
|
},
|
|
3096
4246
|
feedback: {
|
|
3097
4247
|
enabled: true,
|
|
@@ -3117,7 +4267,8 @@ async function buildDefaultWizardConfig() {
|
|
|
3117
4267
|
autoCreateWhenGitHubWriteAccess: true,
|
|
3118
4268
|
disableAutoCreateGitHubArtifacts: false,
|
|
3119
4269
|
mode: 'issue',
|
|
3120
|
-
outputDestinations: ['openclaw_chat', 'github_issue'
|
|
4270
|
+
outputDestinations: ['openclaw_chat', 'github_issue'],
|
|
4271
|
+
productionErrorMode: 'issue',
|
|
3121
4272
|
usageMode: 'production_autopilot',
|
|
3122
4273
|
draftPullRequests: true,
|
|
3123
4274
|
proposalBranchPrefix: 'openclaw/proposals',
|
|
@@ -3191,6 +4342,12 @@ async function buildDefaultWizardConfig() {
|
|
|
3191
4342
|
schedule: '*/30 * * * *',
|
|
3192
4343
|
timezone: process.env.TZ || 'UTC',
|
|
3193
4344
|
name: 'OpenClaw Growth Engineer scheduler',
|
|
4345
|
+
delivery: {
|
|
4346
|
+
enabled: true,
|
|
4347
|
+
mode: 'announce',
|
|
4348
|
+
channel: 'last',
|
|
4349
|
+
to: '',
|
|
4350
|
+
},
|
|
3194
4351
|
},
|
|
3195
4352
|
},
|
|
3196
4353
|
secrets: {
|
|
@@ -3200,12 +4357,22 @@ async function buildDefaultWizardConfig() {
|
|
|
3200
4357
|
analyticsTokenRef: { source: 'env', provider: 'default', id: 'ANALYTICSCLI_ACCESS_TOKEN' },
|
|
3201
4358
|
revenuecatTokenEnv: 'REVENUECAT_API_KEY',
|
|
3202
4359
|
revenuecatTokenRef: { source: 'env', provider: 'default', id: 'REVENUECAT_API_KEY' },
|
|
4360
|
+
paddleTokenEnv: 'PADDLE_API_KEY',
|
|
4361
|
+
paddleTokenRef: { source: 'env', provider: 'default', id: 'PADDLE_API_KEY' },
|
|
4362
|
+
gscTokenEnv: 'GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN',
|
|
4363
|
+
gscTokenRef: { source: 'env', provider: 'default', id: 'GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN' },
|
|
4364
|
+
dataforseoLoginEnv: 'DATAFORSEO_LOGIN',
|
|
4365
|
+
dataforseoLoginRef: { source: 'env', provider: 'default', id: 'DATAFORSEO_LOGIN' },
|
|
4366
|
+
dataforseoPasswordEnv: 'DATAFORSEO_PASSWORD',
|
|
4367
|
+
dataforseoPasswordRef: { source: 'env', provider: 'default', id: 'DATAFORSEO_PASSWORD' },
|
|
3203
4368
|
sentryTokenEnv: 'SENTRY_AUTH_TOKEN',
|
|
3204
4369
|
sentryTokenRef: { source: 'env', provider: 'default', id: 'SENTRY_AUTH_TOKEN' },
|
|
4370
|
+
coolifyTokenEnv: 'COOLIFY_API_TOKEN',
|
|
4371
|
+
coolifyTokenRef: { source: 'env', provider: 'default', id: 'COOLIFY_API_TOKEN' },
|
|
3205
4372
|
},
|
|
3206
4373
|
};
|
|
3207
4374
|
}
|
|
3208
|
-
function buildRecommendedSourceConfig() {
|
|
4375
|
+
function buildRecommendedSourceConfig(configPath = null) {
|
|
3209
4376
|
return {
|
|
3210
4377
|
analytics: {
|
|
3211
4378
|
enabled: true,
|
|
@@ -3217,10 +4384,36 @@ function buildRecommendedSourceConfig() {
|
|
|
3217
4384
|
mode: 'command',
|
|
3218
4385
|
command: getWizardDefaultSourceCommand('revenuecat'),
|
|
3219
4386
|
},
|
|
4387
|
+
paddle: {
|
|
4388
|
+
enabled: true,
|
|
4389
|
+
mode: 'command',
|
|
4390
|
+
command: getWizardDefaultSourceCommand('paddle'),
|
|
4391
|
+
environment: 'live',
|
|
4392
|
+
},
|
|
4393
|
+
seo: {
|
|
4394
|
+
enabled: true,
|
|
4395
|
+
mode: 'command',
|
|
4396
|
+
command: getWizardDefaultSourceCommand('seo'),
|
|
4397
|
+
siteUrl: process.env.GSC_SITE_URL || '',
|
|
4398
|
+
paidProvider: {
|
|
4399
|
+
dataforseo: {
|
|
4400
|
+
enabled: false,
|
|
4401
|
+
confirmPaid: false,
|
|
4402
|
+
maxPaidRequests: 1,
|
|
4403
|
+
},
|
|
4404
|
+
},
|
|
4405
|
+
},
|
|
3220
4406
|
sentry: {
|
|
3221
4407
|
enabled: true,
|
|
3222
4408
|
mode: 'command',
|
|
3223
|
-
command:
|
|
4409
|
+
command: normalizeWizardSourceCommand('sentry', {}, configPath),
|
|
4410
|
+
},
|
|
4411
|
+
coolify: {
|
|
4412
|
+
enabled: true,
|
|
4413
|
+
mode: 'command',
|
|
4414
|
+
command: normalizeWizardSourceCommand('coolify', {}, configPath),
|
|
4415
|
+
baseUrl: process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com',
|
|
4416
|
+
tokenEnv: 'COOLIFY_API_TOKEN',
|
|
3224
4417
|
},
|
|
3225
4418
|
feedback: {
|
|
3226
4419
|
enabled: true,
|
|
@@ -3245,25 +4438,46 @@ function getInputChannelInitialSelection(config) {
|
|
|
3245
4438
|
selected.add('analytics');
|
|
3246
4439
|
if (sources.revenuecat?.enabled === true || isConnectorLocallyConfigured('revenuecat'))
|
|
3247
4440
|
selected.add('revenuecat');
|
|
4441
|
+
if (sources.paddle?.enabled === true || isConnectorLocallyConfigured('paddle'))
|
|
4442
|
+
selected.add('paddle');
|
|
4443
|
+
if (sources.seo?.enabled === true || isConnectorLocallyConfigured('seo'))
|
|
4444
|
+
selected.add('seo');
|
|
3248
4445
|
if (!hasExplicitSources || sources.sentry?.enabled !== false)
|
|
3249
4446
|
selected.add('sentry');
|
|
4447
|
+
if (sources.coolify?.enabled === true || isConnectorLocallyConfigured('coolify'))
|
|
4448
|
+
selected.add('coolify');
|
|
3250
4449
|
if (extraSources.some((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()) &&
|
|
3251
4450
|
source?.enabled !== false) ||
|
|
3252
4451
|
isConnectorLocallyConfigured('asc')) {
|
|
3253
4452
|
selected.add('asc');
|
|
3254
4453
|
}
|
|
4454
|
+
for (const key of ACCOUNT_SIGNAL_CONNECTOR_KEYS) {
|
|
4455
|
+
if (extraSources.some((source) => String(source?.key || source?.service || '').toLowerCase() === key && source?.enabled !== false) ||
|
|
4456
|
+
isConnectorLocallyConfigured(key)) {
|
|
4457
|
+
selected.add(key);
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
3255
4460
|
selected.add('github');
|
|
3256
4461
|
if (selected.size === 0)
|
|
3257
4462
|
return orderConnectors([...CONNECTOR_KEYS]);
|
|
3258
4463
|
return orderConnectors([...selected]);
|
|
3259
4464
|
}
|
|
3260
|
-
function buildSourceConfigFromInputChannels(selectedConnectors, existingSources = {}) {
|
|
4465
|
+
function buildSourceConfigFromInputChannels(selectedConnectors, existingSources = {}, configPath = null) {
|
|
3261
4466
|
const selected = new Set(selectedConnectors);
|
|
3262
|
-
const recommended = buildRecommendedSourceConfig();
|
|
3263
|
-
const migratedSources = migrateRuntimeSourceCommands({ sources: existingSources }).sources || {};
|
|
4467
|
+
const recommended = buildRecommendedSourceConfig(configPath);
|
|
4468
|
+
const migratedSources = migrateRuntimeSourceCommands({ sources: existingSources }, configPath).sources || {};
|
|
3264
4469
|
const existingExtra = Array.isArray(migratedSources.extra) ? migratedSources.extra : [];
|
|
3265
4470
|
const ascSource = existingExtra.find((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()));
|
|
3266
|
-
const
|
|
4471
|
+
const managedAccountKeys = new Set(ACCOUNT_SIGNAL_CONNECTOR_KEYS);
|
|
4472
|
+
const accountSourceByKey = new Map(existingExtra
|
|
4473
|
+
.filter((source) => managedAccountKeys.has(String(source?.key || source?.service || '').toLowerCase()))
|
|
4474
|
+
.map((source) => [String(source?.key || source?.service || '').toLowerCase(), source]));
|
|
4475
|
+
const nonAscExtra = existingExtra.filter((source) => {
|
|
4476
|
+
if (source === ascSource)
|
|
4477
|
+
return false;
|
|
4478
|
+
return !managedAccountKeys.has(String(source?.key || source?.service || '').toLowerCase());
|
|
4479
|
+
});
|
|
4480
|
+
const accountExtra = ACCOUNT_SIGNAL_CONNECTOR_KEYS.map((key) => buildAccountSignalExtraSourceConfig(key, accountSourceByKey.get(key) || { enabled: selected.has(key) })).map((source) => ({ ...source, enabled: selected.has(source.key) }));
|
|
3267
4481
|
return {
|
|
3268
4482
|
...recommended,
|
|
3269
4483
|
...migratedSources,
|
|
@@ -3273,7 +4487,7 @@ function buildSourceConfigFromInputChannels(selectedConnectors, existingSources
|
|
|
3273
4487
|
command: normalizeWizardSourceCommand('analytics', {
|
|
3274
4488
|
...recommended.analytics,
|
|
3275
4489
|
...(migratedSources.analytics || {}),
|
|
3276
|
-
}),
|
|
4490
|
+
}, configPath),
|
|
3277
4491
|
enabled: selected.has('analytics'),
|
|
3278
4492
|
},
|
|
3279
4493
|
revenuecat: {
|
|
@@ -3282,18 +4496,45 @@ function buildSourceConfigFromInputChannels(selectedConnectors, existingSources
|
|
|
3282
4496
|
command: normalizeWizardSourceCommand('revenuecat', {
|
|
3283
4497
|
...recommended.revenuecat,
|
|
3284
4498
|
...(migratedSources.revenuecat || {}),
|
|
3285
|
-
}),
|
|
4499
|
+
}, configPath),
|
|
3286
4500
|
enabled: selected.has('revenuecat'),
|
|
3287
4501
|
},
|
|
4502
|
+
paddle: {
|
|
4503
|
+
...recommended.paddle,
|
|
4504
|
+
...(migratedSources.paddle || {}),
|
|
4505
|
+
command: normalizeWizardSourceCommand('paddle', {
|
|
4506
|
+
...recommended.paddle,
|
|
4507
|
+
...(migratedSources.paddle || {}),
|
|
4508
|
+
}, configPath),
|
|
4509
|
+
enabled: selected.has('paddle'),
|
|
4510
|
+
},
|
|
4511
|
+
seo: {
|
|
4512
|
+
...recommended.seo,
|
|
4513
|
+
...(migratedSources.seo || {}),
|
|
4514
|
+
command: normalizeWizardSourceCommand('seo', {
|
|
4515
|
+
...recommended.seo,
|
|
4516
|
+
...(migratedSources.seo || {}),
|
|
4517
|
+
}, configPath),
|
|
4518
|
+
enabled: selected.has('seo'),
|
|
4519
|
+
},
|
|
3288
4520
|
sentry: {
|
|
3289
4521
|
...recommended.sentry,
|
|
3290
4522
|
...(migratedSources.sentry || {}),
|
|
3291
4523
|
command: normalizeWizardSourceCommand('sentry', {
|
|
3292
4524
|
...recommended.sentry,
|
|
3293
4525
|
...(migratedSources.sentry || {}),
|
|
3294
|
-
}),
|
|
4526
|
+
}, configPath),
|
|
3295
4527
|
enabled: selected.has('sentry'),
|
|
3296
4528
|
},
|
|
4529
|
+
coolify: {
|
|
4530
|
+
...recommended.coolify,
|
|
4531
|
+
...(migratedSources.coolify || {}),
|
|
4532
|
+
command: normalizeWizardSourceCommand('coolify', {
|
|
4533
|
+
...recommended.coolify,
|
|
4534
|
+
...(migratedSources.coolify || {}),
|
|
4535
|
+
}, configPath),
|
|
4536
|
+
enabled: selected.has('coolify'),
|
|
4537
|
+
},
|
|
3297
4538
|
feedback: {
|
|
3298
4539
|
...recommended.feedback,
|
|
3299
4540
|
...(migratedSources.feedback || {}),
|
|
@@ -3301,6 +4542,7 @@ function buildSourceConfigFromInputChannels(selectedConnectors, existingSources
|
|
|
3301
4542
|
},
|
|
3302
4543
|
extra: [
|
|
3303
4544
|
...nonAscExtra,
|
|
4545
|
+
...accountExtra,
|
|
3304
4546
|
{
|
|
3305
4547
|
...buildExtraSourceConfig('asc-cli', {
|
|
3306
4548
|
enabled: selected.has('asc'),
|
|
@@ -3315,7 +4557,7 @@ function buildSourceConfigFromInputChannels(selectedConnectors, existingSources
|
|
|
3315
4557
|
command: getWizardDefaultSourceCommand('asc'),
|
|
3316
4558
|
}),
|
|
3317
4559
|
...(ascSource || {}),
|
|
3318
|
-
}),
|
|
4560
|
+
}, configPath),
|
|
3319
4561
|
enabled: selected.has('asc'),
|
|
3320
4562
|
},
|
|
3321
4563
|
],
|
|
@@ -3324,8 +4566,8 @@ function buildSourceConfigFromInputChannels(selectedConnectors, existingSources
|
|
|
3324
4566
|
async function loadEditableConfig(configPath) {
|
|
3325
4567
|
const existing = await readJsonIfPresent(configPath).catch(() => null);
|
|
3326
4568
|
if (existing && typeof existing === 'object')
|
|
3327
|
-
return migrateRuntimeSourceCommands(existing);
|
|
3328
|
-
return await buildDefaultWizardConfig();
|
|
4569
|
+
return migrateRuntimeSourceCommands(existing, configPath);
|
|
4570
|
+
return await buildDefaultWizardConfig(configPath);
|
|
3329
4571
|
}
|
|
3330
4572
|
function mergeNotificationChannels(baseChannels, extraChannels) {
|
|
3331
4573
|
const channels = [];
|
|
@@ -3412,9 +4654,33 @@ async function askOutputConfig(rl, config) {
|
|
|
3412
4654
|
});
|
|
3413
4655
|
const wantsIssue = outputChoices.includes('issue');
|
|
3414
4656
|
const wantsPullRequest = outputChoices.includes('pull_request');
|
|
3415
|
-
const
|
|
3416
|
-
|
|
3417
|
-
|
|
4657
|
+
const productionErrorMode = await askMenuChoice(rl, {
|
|
4658
|
+
title: 'Production error handling',
|
|
4659
|
+
subtitle: 'What should happen when the 90-minute healthcheck confirms a production Sentry/GlitchTip/Coolify issue?',
|
|
4660
|
+
defaultValue: config?.actions?.productionErrorMode || (wantsPullRequest ? 'pull_request' : wantsIssue ? 'issue' : 'alert'),
|
|
4661
|
+
options: [
|
|
4662
|
+
{
|
|
4663
|
+
value: 'alert',
|
|
4664
|
+
label: 'Alert only',
|
|
4665
|
+
detail: 'Send the short alert/handoff; do not auto-create GitHub artifacts for production errors.',
|
|
4666
|
+
},
|
|
4667
|
+
{
|
|
4668
|
+
value: 'issue',
|
|
4669
|
+
label: 'GitHub issue',
|
|
4670
|
+
detail: 'Create a GitHub issue with the production evidence and suggested investigation when access allows it.',
|
|
4671
|
+
},
|
|
4672
|
+
{
|
|
4673
|
+
value: 'pull_request',
|
|
4674
|
+
label: 'Draft PR',
|
|
4675
|
+
detail: 'Create a draft PR proposal for implementation-ready production fixes when access allows it.',
|
|
4676
|
+
},
|
|
4677
|
+
],
|
|
4678
|
+
});
|
|
4679
|
+
const effectiveWantsIssue = wantsIssue || productionErrorMode === 'issue';
|
|
4680
|
+
const effectiveWantsPullRequest = wantsPullRequest || productionErrorMode === 'pull_request';
|
|
4681
|
+
const summaryOnly = !effectiveWantsIssue && !effectiveWantsPullRequest;
|
|
4682
|
+
const mode = effectiveWantsPullRequest ? 'pull_request' : 'issue';
|
|
4683
|
+
const autoCreate = effectiveWantsIssue || effectiveWantsPullRequest;
|
|
3418
4684
|
if (!summaryOnly) {
|
|
3419
4685
|
process.stdout.write('GitHub repo scope is not pinned by the wizard; OpenClaw/Hermes will infer it from OPENCLAW_GITHUB_REPO, the local git remote, or runtime context when creating issues/PRs.\n');
|
|
3420
4686
|
}
|
|
@@ -3433,11 +4699,12 @@ async function askOutputConfig(rl, config) {
|
|
|
3433
4699
|
mode,
|
|
3434
4700
|
outputDestinations: [
|
|
3435
4701
|
'openclaw_chat',
|
|
3436
|
-
...(
|
|
3437
|
-
...(
|
|
4702
|
+
...(effectiveWantsIssue ? ['github_issue'] : []),
|
|
4703
|
+
...(effectiveWantsPullRequest ? ['github_pull_request'] : []),
|
|
3438
4704
|
],
|
|
3439
|
-
|
|
3440
|
-
|
|
4705
|
+
productionErrorMode,
|
|
4706
|
+
autoCreateIssues: effectiveWantsIssue,
|
|
4707
|
+
autoCreatePullRequests: effectiveWantsPullRequest,
|
|
3441
4708
|
autoCreateWhenGitHubWriteAccess: config.actions?.autoCreateWhenGitHubWriteAccess !== false,
|
|
3442
4709
|
disableAutoCreateGitHubArtifacts: config.actions?.disableAutoCreateGitHubArtifacts === true,
|
|
3443
4710
|
draftPullRequests: true,
|
|
@@ -3456,8 +4723,8 @@ async function askOutputConfig(rl, config) {
|
|
|
3456
4723
|
enabled: !summaryOnly,
|
|
3457
4724
|
mode,
|
|
3458
4725
|
modes: [
|
|
3459
|
-
...(
|
|
3460
|
-
...(
|
|
4726
|
+
...(effectiveWantsIssue ? ['issue'] : []),
|
|
4727
|
+
...(effectiveWantsPullRequest ? ['pull_request'] : []),
|
|
3461
4728
|
],
|
|
3462
4729
|
autoCreate,
|
|
3463
4730
|
draftPullRequests: true,
|
|
@@ -3553,21 +4820,37 @@ async function askIntervalConfig(rl, config) {
|
|
|
3553
4820
|
printSection('Schedule and analysis depth', [
|
|
3554
4821
|
'The runner wakes up often, but larger reviews only run on their daily/weekly/monthly cadence.',
|
|
3555
4822
|
'Connector health checks are separate and default to every 6 hours.',
|
|
3556
|
-
'On OpenClaw VPS installs,
|
|
4823
|
+
'On OpenClaw or Hermes VPS installs, the agent scheduler should wake Growth Engineer; heartbeat stays as a fallback checklist.',
|
|
3557
4824
|
]);
|
|
3558
4825
|
const currentSchedule = config?.schedule || {};
|
|
3559
4826
|
const currentAutomation = getAutomationConfig(config);
|
|
3560
4827
|
const usageMode = await askToolUsage(rl);
|
|
3561
|
-
const
|
|
3562
|
-
const
|
|
4828
|
+
const schedulePreset = await askSchedulePreset(rl);
|
|
4829
|
+
const recommendedRunnerInterval = schedulePreset === 'quiet' ? 360 : 90;
|
|
4830
|
+
const recommendedConnectorHealthInterval = schedulePreset === 'quiet' ? 360 : DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES;
|
|
4831
|
+
const recommendedOpenClawCronSchedule = schedulePreset === 'quiet' ? '0 * * * *' : '*/30 * * * *';
|
|
4832
|
+
const intervalMinutes = schedulePreset === 'manual'
|
|
4833
|
+
? Number.parseInt(await ask(rl, 'Growth runner fallback loop interval in minutes', String(currentSchedule.intervalMinutes || recommendedRunnerInterval)), 10) || recommendedRunnerInterval
|
|
4834
|
+
: recommendedRunnerInterval;
|
|
4835
|
+
const connectorHealthCheckIntervalMinutes = schedulePreset === 'manual'
|
|
4836
|
+
? Number.parseInt(await ask(rl, 'Connector credentials health check interval in minutes', String(currentSchedule.connectorHealthCheckIntervalMinutes || recommendedConnectorHealthInterval)), 10) || recommendedConnectorHealthInterval
|
|
4837
|
+
: recommendedConnectorHealthInterval;
|
|
3563
4838
|
const cadences = await askCadencePlan(rl, currentSchedule.cadences);
|
|
3564
4839
|
const enableOpenClawCron = await askYesNo(rl, 'Install an OpenClaw Gateway cron job to wake Growth Engineer on this VPS?', currentAutomation.openclawCron.enabled !== false);
|
|
3565
4840
|
const openclawCronSchedule = enableOpenClawCron
|
|
3566
|
-
?
|
|
4841
|
+
? schedulePreset === 'manual'
|
|
4842
|
+
? await ask(rl, 'OpenClaw cron expression for scheduler wakeups', currentAutomation.openclawCron.schedule || recommendedOpenClawCronSchedule)
|
|
4843
|
+
: recommendedOpenClawCronSchedule
|
|
3567
4844
|
: currentAutomation.openclawCron.schedule;
|
|
3568
4845
|
const openclawCronTimezone = enableOpenClawCron
|
|
3569
4846
|
? await ask(rl, 'OpenClaw cron timezone', currentAutomation.openclawCron.timezone || process.env.TZ || 'UTC')
|
|
3570
4847
|
: currentAutomation.openclawCron.timezone;
|
|
4848
|
+
const enableHermesCron = await askYesNo(rl, 'Install a Hermes cron job when Hermes is available on this host?', currentAutomation.hermesCron.enabled !== false);
|
|
4849
|
+
const hermesCronSchedule = enableHermesCron
|
|
4850
|
+
? schedulePreset === 'manual'
|
|
4851
|
+
? await ask(rl, 'Hermes cron expression for scheduler wakeups', currentAutomation.hermesCron.schedule || openclawCronSchedule || recommendedOpenClawCronSchedule)
|
|
4852
|
+
: openclawCronSchedule || recommendedOpenClawCronSchedule
|
|
4853
|
+
: currentAutomation.hermesCron.schedule;
|
|
3571
4854
|
config.schedule = {
|
|
3572
4855
|
...currentSchedule,
|
|
3573
4856
|
intervalMinutes,
|
|
@@ -3589,6 +4872,21 @@ async function askIntervalConfig(rl, config) {
|
|
|
3589
4872
|
schedule: openclawCronSchedule || '*/30 * * * *',
|
|
3590
4873
|
timezone: openclawCronTimezone || 'UTC',
|
|
3591
4874
|
name: currentAutomation.openclawCron.name || 'OpenClaw Growth Engineer scheduler',
|
|
4875
|
+
delivery: {
|
|
4876
|
+
...(currentAutomation.openclawCron.delivery || {}),
|
|
4877
|
+
enabled: currentAutomation.openclawCron.delivery?.enabled !== false,
|
|
4878
|
+
mode: currentAutomation.openclawCron.delivery?.mode || 'announce',
|
|
4879
|
+
channel: currentAutomation.openclawCron.delivery?.channel || 'last',
|
|
4880
|
+
to: currentAutomation.openclawCron.delivery?.to || '',
|
|
4881
|
+
},
|
|
4882
|
+
},
|
|
4883
|
+
hermesCron: {
|
|
4884
|
+
...(currentAutomation.hermesCron || {}),
|
|
4885
|
+
enabled: enableHermesCron,
|
|
4886
|
+
schedule: hermesCronSchedule || '*/30 * * * *',
|
|
4887
|
+
name: currentAutomation.hermesCron.name || 'Hermes Growth Engineer scheduler',
|
|
4888
|
+
skill: currentAutomation.hermesCron.skill || 'growth-engineer',
|
|
4889
|
+
deliver: currentAutomation.hermesCron.deliver || 'local',
|
|
3592
4890
|
},
|
|
3593
4891
|
};
|
|
3594
4892
|
return config;
|
|
@@ -3599,7 +4897,7 @@ async function askOutputsAndIntervalsConfig(rl, config) {
|
|
|
3599
4897
|
return await askGitHubArtifactDetails(rl, withOutput);
|
|
3600
4898
|
}
|
|
3601
4899
|
async function askInputSourceConfig(rl, config, configPath) {
|
|
3602
|
-
config = migrateRuntimeSourceCommands(config);
|
|
4900
|
+
config = migrateRuntimeSourceCommands(config, configPath);
|
|
3603
4901
|
await ensureDirForFile(configPath);
|
|
3604
4902
|
await writeJsonFile(configPath, config);
|
|
3605
4903
|
const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(configPath, onProgress));
|
|
@@ -3610,7 +4908,7 @@ async function askInputSourceConfig(rl, config, configPath) {
|
|
|
3610
4908
|
helpText: 'Use Up/Down to move, Space to toggle channels, A to toggle all channels, Enter to continue.',
|
|
3611
4909
|
mode: 'input',
|
|
3612
4910
|
});
|
|
3613
|
-
config.sources = buildSourceConfigFromInputChannels(selected, config.sources || {});
|
|
4911
|
+
config.sources = buildSourceConfigFromInputChannels(selected, config.sources || {}, configPath);
|
|
3614
4912
|
return { config, selected, healthByConnector };
|
|
3615
4913
|
}
|
|
3616
4914
|
async function writeOpenClawJobManifest(configPath, config) {
|
|
@@ -3634,7 +4932,7 @@ async function writeOpenClawJobManifest(configPath, config) {
|
|
|
3634
4932
|
openClawCanEditOutputDelivery: true,
|
|
3635
4933
|
openClawCanEditConnectors: true,
|
|
3636
4934
|
openClawCanEditConnectorSecrets: false,
|
|
3637
|
-
connectorChanges: 'OpenClaw may read and modify non-secret connector config such as enabled flags, source commands, project/app mappings, and source priorities. Use `
|
|
4935
|
+
connectorChanges: 'OpenClaw may read and modify non-secret connector config such as enabled flags, source commands, project/app mappings, and source priorities. Use `npx -y @analyticscli/growth-engineer@preview wizard --connectors` for API keys or other connector secrets; never write secret values into config files, manifests, issues, PRs, or chat output.',
|
|
3638
4936
|
secretAccessMode: config?.security?.connectorSecrets?.mode || 'local-user-file',
|
|
3639
4937
|
secretPolicy: config?.security?.connectorSecrets?.mode === 'isolated-runner'
|
|
3640
4938
|
? 'OpenClaw must use the allowlisted sudo wrapper commands and must not read the persisted secret file.'
|
|
@@ -3643,12 +4941,14 @@ async function writeOpenClawJobManifest(configPath, config) {
|
|
|
3643
4941
|
scheduler: {
|
|
3644
4942
|
recommended: 'openclaw-cron',
|
|
3645
4943
|
openclawCron: automation.openclawCron,
|
|
4944
|
+
hermesCron: automation.hermesCron,
|
|
3646
4945
|
statePath,
|
|
3647
4946
|
proofPath,
|
|
3648
4947
|
verifyCommands: [
|
|
3649
4948
|
'openclaw cron list',
|
|
3650
4949
|
'openclaw tasks list',
|
|
3651
4950
|
'openclaw tasks audit',
|
|
4951
|
+
'hermes cron list',
|
|
3652
4952
|
`tail -n 20 ${proofPath}`,
|
|
3653
4953
|
`jq '.connectorHealth, .cadences, .lastRunAt, .skippedReason' ${statePath}`,
|
|
3654
4954
|
],
|
|
@@ -3674,25 +4974,6 @@ async function writeOpenClawJobManifest(configPath, config) {
|
|
|
3674
4974
|
await writeJsonFile(manifestPath, manifest);
|
|
3675
4975
|
return manifestPath;
|
|
3676
4976
|
}
|
|
3677
|
-
function buildOpenClawCronAddCommand(configPath, config) {
|
|
3678
|
-
const automation = getAutomationConfig(config).openclawCron;
|
|
3679
|
-
const displayConfigPath = path.relative(process.cwd(), configPath) || configPath;
|
|
3680
|
-
const eventText = buildOpenClawGrowthSystemEvent(displayConfigPath, config);
|
|
3681
|
-
return [
|
|
3682
|
-
'openclaw cron add',
|
|
3683
|
-
'--name',
|
|
3684
|
-
quote(automation.name),
|
|
3685
|
-
'--cron',
|
|
3686
|
-
quote(automation.schedule),
|
|
3687
|
-
'--tz',
|
|
3688
|
-
quote(automation.timezone),
|
|
3689
|
-
'--session',
|
|
3690
|
-
automation.mode === 'isolated' ? 'isolated' : 'main',
|
|
3691
|
-
automation.mode === 'isolated' ? '--message' : '--system-event',
|
|
3692
|
-
quote(eventText),
|
|
3693
|
-
automation.mode === 'isolated' ? '--announce' : '--wake now',
|
|
3694
|
-
].join(' ');
|
|
3695
|
-
}
|
|
3696
4977
|
async function ensureOpenClawCronFromWizard(configPath, config) {
|
|
3697
4978
|
const automation = getAutomationConfig(config).openclawCron;
|
|
3698
4979
|
const displayConfigPath = path.relative(process.cwd(), configPath) || configPath;
|
|
@@ -3708,7 +4989,7 @@ async function ensureOpenClawCronFromWizard(configPath, config) {
|
|
|
3708
4989
|
proofPath,
|
|
3709
4990
|
};
|
|
3710
4991
|
}
|
|
3711
|
-
const addCommand = buildOpenClawCronAddCommand(
|
|
4992
|
+
const addCommand = buildOpenClawCronAddCommand(displayConfigPath, config);
|
|
3712
4993
|
if (!(await commandExists('openclaw'))) {
|
|
3713
4994
|
return {
|
|
3714
4995
|
ok: true,
|
|
@@ -3720,26 +5001,108 @@ async function ensureOpenClawCronFromWizard(configPath, config) {
|
|
|
3720
5001
|
proofPath,
|
|
3721
5002
|
};
|
|
3722
5003
|
}
|
|
3723
|
-
const
|
|
3724
|
-
|
|
5004
|
+
const inspection = await inspectOpenClawCronInstall({
|
|
5005
|
+
configPath: displayConfigPath,
|
|
5006
|
+
config,
|
|
5007
|
+
runCommand: runCommandCapture,
|
|
5008
|
+
readFile: fs.readFile,
|
|
5009
|
+
});
|
|
5010
|
+
if (inspection.exists && inspection.verified) {
|
|
3725
5011
|
return {
|
|
3726
5012
|
ok: true,
|
|
3727
5013
|
installed: true,
|
|
3728
|
-
status: '
|
|
3729
|
-
detail: `OpenClaw cron job already exists: ${automation.name}`,
|
|
5014
|
+
status: 'already_configured_verified',
|
|
5015
|
+
detail: `OpenClaw cron job already exists and matches the Growth Engineer runner contract: ${automation.name}`,
|
|
5016
|
+
source: inspection.source,
|
|
3730
5017
|
statePath,
|
|
3731
5018
|
proofPath,
|
|
3732
5019
|
};
|
|
3733
5020
|
}
|
|
3734
5021
|
const add = await runCommandCapture(addCommand);
|
|
5022
|
+
const existingDetail = inspection.exists
|
|
5023
|
+
? `Existing OpenClaw cron job "${automation.name}" was not verifiably wired to the current runner contract (${inspection.reason} via ${inspection.source}). `
|
|
5024
|
+
: '';
|
|
3735
5025
|
return {
|
|
3736
5026
|
ok: add.ok,
|
|
3737
5027
|
installed: add.ok,
|
|
3738
|
-
status: add.ok ? 'configured' : 'failed',
|
|
3739
|
-
detail: add.ok
|
|
5028
|
+
status: add.ok ? (inspection.exists ? 'reconfigured' : 'configured') : inspection.exists ? 'needs_repair' : 'failed',
|
|
5029
|
+
detail: add.ok
|
|
5030
|
+
? `${existingDetail}Configured OpenClaw cron job: ${automation.name}`
|
|
5031
|
+
: `${existingDetail}${add.stderr || add.stdout || `exit ${add.code}`}`,
|
|
3740
5032
|
command: addCommand,
|
|
5033
|
+
remediation: inspection.exists && !add.ok
|
|
5034
|
+
? `Remove the stale OpenClaw cron job named "${automation.name}" with your installed OpenClaw CLI, then rerun: ${addCommand}`
|
|
5035
|
+
: undefined,
|
|
5036
|
+
statePath,
|
|
5037
|
+
proofPath,
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
5040
|
+
async function ensureHermesCronFromWizard(configPath, config) {
|
|
5041
|
+
const automation = getAutomationConfig(config).hermesCron;
|
|
5042
|
+
const displayConfigPath = path.relative(process.cwd(), configPath) || configPath;
|
|
5043
|
+
const statePath = deriveStatePathFromConfigPath(displayConfigPath);
|
|
5044
|
+
const proofPath = path.resolve(deriveSchedulerProofPathFromStatePath(statePath));
|
|
5045
|
+
const workdir = path.resolve(automation.workdir || process.cwd());
|
|
5046
|
+
if (automation.enabled === false) {
|
|
5047
|
+
return {
|
|
5048
|
+
ok: true,
|
|
5049
|
+
installed: false,
|
|
5050
|
+
status: 'disabled',
|
|
5051
|
+
detail: 'Hermes cron disabled by user choice.',
|
|
5052
|
+
statePath,
|
|
5053
|
+
proofPath,
|
|
5054
|
+
};
|
|
5055
|
+
}
|
|
5056
|
+
const createCommand = buildHermesCronCreateCommand(displayConfigPath, config, { workdir });
|
|
5057
|
+
if (!(await commandExists('hermes'))) {
|
|
5058
|
+
return {
|
|
5059
|
+
ok: true,
|
|
5060
|
+
installed: false,
|
|
5061
|
+
status: 'hermes_cli_missing',
|
|
5062
|
+
detail: 'hermes CLI was not found on PATH. Run the shown command on the host where Hermes Gateway is installed.',
|
|
5063
|
+
command: createCommand,
|
|
5064
|
+
statePath,
|
|
5065
|
+
proofPath,
|
|
5066
|
+
workdir,
|
|
5067
|
+
};
|
|
5068
|
+
}
|
|
5069
|
+
const inspection = await inspectHermesCronInstall({
|
|
5070
|
+
configPath: displayConfigPath,
|
|
5071
|
+
config,
|
|
5072
|
+
runCommand: runCommandCapture,
|
|
5073
|
+
readFile: fs.readFile,
|
|
5074
|
+
workdir,
|
|
5075
|
+
});
|
|
5076
|
+
if (inspection.exists && inspection.verified) {
|
|
5077
|
+
return {
|
|
5078
|
+
ok: true,
|
|
5079
|
+
installed: true,
|
|
5080
|
+
status: 'already_configured_verified',
|
|
5081
|
+
detail: `Hermes cron job already exists and matches the Growth Engineer runner contract: ${automation.name}`,
|
|
5082
|
+
source: inspection.source,
|
|
5083
|
+
statePath,
|
|
5084
|
+
proofPath,
|
|
5085
|
+
workdir,
|
|
5086
|
+
};
|
|
5087
|
+
}
|
|
5088
|
+
const create = await runCommandCapture(createCommand);
|
|
5089
|
+
const existingDetail = inspection.exists
|
|
5090
|
+
? `Existing Hermes cron job "${automation.name}" was not verifiably wired to the current runner contract (${inspection.reason} via ${inspection.source}). `
|
|
5091
|
+
: '';
|
|
5092
|
+
return {
|
|
5093
|
+
ok: create.ok,
|
|
5094
|
+
installed: create.ok,
|
|
5095
|
+
status: create.ok ? (inspection.exists ? 'reconfigured' : 'configured') : inspection.exists ? 'needs_repair' : 'failed',
|
|
5096
|
+
detail: create.ok
|
|
5097
|
+
? `${existingDetail}Configured Hermes cron job: ${automation.name}`
|
|
5098
|
+
: `${existingDetail}${create.stderr || create.stdout || `exit ${create.code}`}`,
|
|
5099
|
+
command: createCommand,
|
|
5100
|
+
remediation: inspection.exists && !create.ok
|
|
5101
|
+
? `Remove the stale Hermes cron job named "${automation.name}" with your installed Hermes CLI, then rerun: ${createCommand}`
|
|
5102
|
+
: undefined,
|
|
3741
5103
|
statePath,
|
|
3742
5104
|
proofPath,
|
|
5105
|
+
workdir,
|
|
3743
5106
|
};
|
|
3744
5107
|
}
|
|
3745
5108
|
function printOpenClawCronResult(result) {
|
|
@@ -3748,6 +5111,10 @@ function printOpenClawCronResult(result) {
|
|
|
3748
5111
|
process.stdout.write('\nRun this on the VPS where OpenClaw Gateway is installed:\n');
|
|
3749
5112
|
process.stdout.write(`${result.command}\n`);
|
|
3750
5113
|
}
|
|
5114
|
+
if (result.remediation) {
|
|
5115
|
+
process.stdout.write('\nOpenClaw cron repair:\n');
|
|
5116
|
+
process.stdout.write(`${result.remediation}\n`);
|
|
5117
|
+
}
|
|
3751
5118
|
process.stdout.write('\nVPS verification commands:\n');
|
|
3752
5119
|
process.stdout.write(' openclaw cron list\n');
|
|
3753
5120
|
process.stdout.write(' openclaw tasks list\n');
|
|
@@ -3755,10 +5122,33 @@ function printOpenClawCronResult(result) {
|
|
|
3755
5122
|
process.stdout.write(` tail -n 20 ${result.proofPath || path.resolve(DEFAULT_SCHEDULER_PROOF_PATH)}\n`);
|
|
3756
5123
|
process.stdout.write(` jq '.connectorHealth, .cadences, .lastRunAt, .skippedReason' ${result.statePath || 'data/openclaw-growth-engineer/state.json'}\n`);
|
|
3757
5124
|
}
|
|
5125
|
+
function printHermesCronResult(result) {
|
|
5126
|
+
process.stdout.write(`Hermes cron: ${result.status} - ${result.detail}\n`);
|
|
5127
|
+
if (result.command && result.status === 'hermes_cli_missing') {
|
|
5128
|
+
process.stdout.write('\nRun this on the host where Hermes Gateway is installed:\n');
|
|
5129
|
+
process.stdout.write(`${result.command}\n`);
|
|
5130
|
+
}
|
|
5131
|
+
if (result.remediation) {
|
|
5132
|
+
process.stdout.write('\nHermes cron repair:\n');
|
|
5133
|
+
process.stdout.write(`${result.remediation}\n`);
|
|
5134
|
+
}
|
|
5135
|
+
process.stdout.write('\nHermes verification commands:\n');
|
|
5136
|
+
process.stdout.write(' hermes cron list\n');
|
|
5137
|
+
process.stdout.write(' hermes gateway status\n');
|
|
5138
|
+
process.stdout.write(` tail -n 20 ${result.proofPath || path.resolve(DEFAULT_SCHEDULER_PROOF_PATH)}\n`);
|
|
5139
|
+
process.stdout.write(` jq '.connectorHealth, .cadences, .lastRunAt, .skippedReason' ${result.statePath || 'data/openclaw-growth-engineer/state.json'}\n`);
|
|
5140
|
+
}
|
|
3758
5141
|
async function main() {
|
|
3759
5142
|
await loadOpenClawGrowthSecrets();
|
|
3760
5143
|
const args = parseArgs(process.argv.slice(2));
|
|
3761
5144
|
await maybeSelfUpdateFromClawHub(args);
|
|
5145
|
+
if (args.sandboxSmoke) {
|
|
5146
|
+
const configPath = path.resolve(args.out);
|
|
5147
|
+
const config = await loadEditableConfig(configPath);
|
|
5148
|
+
await writeJsonFile(configPath, config);
|
|
5149
|
+
process.stdout.write(`${JSON.stringify({ ok: true, configPath, sources: config.sources || {} })}\n`);
|
|
5150
|
+
return;
|
|
5151
|
+
}
|
|
3762
5152
|
if (args.connectorWizard) {
|
|
3763
5153
|
await runConnectorSetupWizard(args);
|
|
3764
5154
|
return;
|
|
@@ -3782,9 +5172,11 @@ async function main() {
|
|
|
3782
5172
|
await writeJsonFile(configPath, config);
|
|
3783
5173
|
const manifestPath = await writeOpenClawJobManifest(configPath, config);
|
|
3784
5174
|
const cronResult = await ensureOpenClawCronFromWizard(configPath, config);
|
|
5175
|
+
const hermesCronResult = await ensureHermesCronFromWizard(configPath, config);
|
|
3785
5176
|
process.stdout.write(`\nSaved schedule config: ${configPath}\n`);
|
|
3786
5177
|
process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
|
|
3787
5178
|
printOpenClawCronResult(cronResult);
|
|
5179
|
+
printHermesCronResult(hermesCronResult);
|
|
3788
5180
|
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
3789
5181
|
process.stdout.write('OpenClaw can run and update growth jobs plus non-secret connector config from the manifest; connector API keys stay behind the connector wizard.\n');
|
|
3790
5182
|
return;
|
|
@@ -3795,9 +5187,11 @@ async function main() {
|
|
|
3795
5187
|
await writeJsonFile(configPath, config);
|
|
3796
5188
|
const manifestPath = await writeOpenClawJobManifest(configPath, config);
|
|
3797
5189
|
const cronResult = await ensureOpenClawCronFromWizard(configPath, config);
|
|
5190
|
+
const hermesCronResult = await ensureHermesCronFromWizard(configPath, config);
|
|
3798
5191
|
process.stdout.write(`\nSaved output and interval config: ${configPath}\n`);
|
|
3799
5192
|
process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
|
|
3800
5193
|
printOpenClawCronResult(cronResult);
|
|
5194
|
+
printHermesCronResult(hermesCronResult);
|
|
3801
5195
|
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
3802
5196
|
process.stdout.write('Daily checks prioritize Sentry and production anomalies; larger cadences analyze all configured projects and connectors.\n');
|
|
3803
5197
|
return;
|
|
@@ -3830,9 +5224,11 @@ async function main() {
|
|
|
3830
5224
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
3831
5225
|
const manifestPath = await writeOpenClawJobManifest(configPath, config);
|
|
3832
5226
|
const cronResult = await ensureOpenClawCronFromWizard(configPath, config);
|
|
5227
|
+
const hermesCronResult = await ensureHermesCronFromWizard(configPath, config);
|
|
3833
5228
|
process.stdout.write(`\nSaved config: ${configPath}\n`);
|
|
3834
5229
|
process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
|
|
3835
5230
|
printOpenClawCronResult(cronResult);
|
|
5231
|
+
printHermesCronResult(hermesCronResult);
|
|
3836
5232
|
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
3837
5233
|
process.stdout.write('\nNext steps:\n');
|
|
3838
5234
|
process.stdout.write(`1) Set secrets in OpenClaw secret store (env var names in config.secrets)\n`);
|