-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathagent-runner.ts
More file actions
438 lines (371 loc) · 15 KB
/
agent-runner.ts
File metadata and controls
438 lines (371 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
import {
DEFAULT_PACKAGE_INSTALLATION,
getWelcomeMessage,
SPINNER_MESSAGE,
type FrameworkConfig,
} from './framework-config';
import type { WizardOptions } from '../utils/types';
import {
abort,
askForAIConsent,
confirmContinueIfNoOrDirtyGitRepo,
ensurePackageIsInstalled,
getOrAskForProjectData,
getPackageDotJson,
isUsingTypeScript,
printWelcome,
askForCloudRegion,
} from '../utils/clack-utils';
import type { PackageDotJson } from '../utils/package-json';
import { analytics } from '../utils/analytics';
import { WIZARD_INTERACTION_EVENT_NAME } from './constants';
import clack from '../utils/clack';
import {
initializeAgent,
runAgent,
AgentSignals,
AgentErrorType,
buildWizardMetadata,
} from './agent-interface';
import { getCloudUrlFromRegion } from '../utils/urls';
import chalk from 'chalk';
import * as semver from 'semver';
import {
addMCPServerToClientsStep,
uploadEnvironmentVariablesStep,
} from '../steps';
import { checkAnthropicStatusWithPrompt } from '../utils/anthropic-status';
import { enableDebugLogs } from '../utils/debug';
import { createBenchmarkPipeline } from './middleware/benchmark';
/**
* Universal agent-powered wizard runner.
* Handles the complete flow for any framework using PostHog MCP integration.
*/
export async function runAgentWizard(
config: FrameworkConfig,
options: WizardOptions,
): Promise<void> {
if (options.debug) {
enableDebugLogs();
}
// Version check
if (config.detection.minimumVersion && config.detection.getInstalledVersion) {
const version = await config.detection.getInstalledVersion(options);
if (version) {
const coerced = semver.coerce(version);
if (coerced && semver.lt(coerced, config.detection.minimumVersion)) {
const docsUrl =
config.metadata.unsupportedVersionDocsUrl ?? config.metadata.docsUrl;
clack.log.warn(
`Sorry: the wizard can't help you with ${config.metadata.name} ${version}. Upgrade to ${config.metadata.name} ${config.detection.minimumVersion} or later, or check out the manual setup guide.`,
);
clack.log.info(
`Setup ${config.metadata.name} manually: ${chalk.cyan(docsUrl)}`,
);
clack.outro('PostHog wizard will see you next time!');
return;
}
}
}
// Setup phase
printWelcome({ wizardName: getWelcomeMessage(config.metadata.name) });
if (config.metadata.beta) {
clack.log.info(
`${chalk.yellow('[BETA]')} The ${
config.metadata.name
} wizard is in beta. Questions or feedback? Email ${chalk.cyan(
'wizard@posthog.com',
)}`,
);
}
if (config.metadata.preRunNotice) {
clack.log.warn(config.metadata.preRunNotice);
}
clack.log.info(
`We're about to read your project using our LLM gateway.\n\n.env* file contents will not leave your machine.\n\nOther files will be read and edited to provide a fully-custom PostHog integration.`,
);
const aiConsent = await askForAIConsent(options);
if (!aiConsent) {
await abort(
`This wizard uses an LLM agent to intelligently modify your project. Please view the docs to set up ${config.metadata.name} manually instead: ${config.metadata.docsUrl}`,
0,
);
}
// Check Anthropic/Claude service status before proceeding
const statusOk = await checkAnthropicStatusWithPrompt({ ci: options.ci });
if (!statusOk) {
await abort(
`Please try again later, or set up ${config.metadata.name} manually: ${config.metadata.docsUrl}`,
0,
);
}
const cloudRegion = options.cloudRegion ?? (await askForCloudRegion());
const typeScriptDetected = isUsingTypeScript(options);
await confirmContinueIfNoOrDirtyGitRepo(options);
// Framework detection and version
// Only check package.json for Node.js/JavaScript frameworks
const usesPackageJson = config.detection.usesPackageJson !== false;
let packageJson: PackageDotJson | null = null;
let frameworkVersion: string | undefined;
if (usesPackageJson) {
packageJson = await getPackageDotJson(options);
await ensurePackageIsInstalled(
packageJson,
config.detection.packageName,
config.detection.packageDisplayName,
);
frameworkVersion = config.detection.getVersion(packageJson);
} else {
// For non-Node frameworks (e.g., Django), version is handled differently
frameworkVersion = config.detection.getVersion(null);
}
// Set analytics tags for framework version
if (frameworkVersion && config.detection.getVersionBucket) {
const versionBucket = config.detection.getVersionBucket(frameworkVersion);
analytics.setTag(`${config.metadata.integration}-version`, versionBucket);
}
analytics.capture(WIZARD_INTERACTION_EVENT_NAME, {
action: 'started agent integration',
integration: config.metadata.integration,
});
// Get PostHog credentials
const { projectApiKey, host, accessToken, projectId } =
await getOrAskForProjectData({
...options,
cloudRegion,
});
// Gather framework-specific context (e.g., Next.js router, React Native platform)
const frameworkContext = config.metadata.gatherContext
? await config.metadata.gatherContext(options)
: {};
// Set analytics tags from framework context
const contextTags = config.analytics.getTags(frameworkContext);
Object.entries(contextTags).forEach(([key, value]) => {
analytics.setTag(key, value);
});
const integrationPrompt = buildIntegrationPrompt(
config,
{
frameworkVersion: frameworkVersion || 'latest',
typescript: typeScriptDetected,
projectApiKey,
host,
projectId,
},
frameworkContext,
);
// Initialize and run agent
const spinner = clack.spinner();
// Evaluate all feature flags at the start of the run so they can be sent to the LLM gateway
const wizardFlags = await analytics.getAllFlagsForWizard();
const wizardMetadata = buildWizardMetadata(wizardFlags);
// Determine MCP URL: CLI flag > env var > production default
// Use EU subdomain for EU users to work around Claude Code's OAuth bug
// See: https://github.com/anthropics/claude-code/issues/2267
const mcpUrl = options.localMcp
? 'http://localhost:8787/mcp'
: process.env.MCP_URL ||
(cloudRegion === 'eu'
? 'https://mcp-eu.posthog.com/mcp'
: 'https://mcp.posthog.com/mcp');
const agent = await initializeAgent(
{
workingDirectory: options.installDir,
posthogMcpUrl: mcpUrl,
posthogApiKey: accessToken,
posthogApiHost: host,
additionalMcpServers: config.metadata.additionalMcpServers,
detectPackageManager: config.detection.detectPackageManager,
wizardFlags,
wizardMetadata,
},
options,
);
const middleware = options.benchmark
? createBenchmarkPipeline(spinner, options)
: undefined;
const agentResult = await runAgent(
agent,
integrationPrompt,
options,
spinner,
{
estimatedDurationMinutes: config.ui.estimatedDurationMinutes,
spinnerMessage: SPINNER_MESSAGE,
successMessage: config.ui.successMessage,
errorMessage: 'Integration failed',
},
middleware,
);
// Handle error cases detected in agent output
if (agentResult.error === AgentErrorType.MCP_MISSING) {
analytics.captureException(
new Error('Agent could not access PostHog MCP server'),
{
integration: config.metadata.integration,
error_type: AgentErrorType.MCP_MISSING,
signal: AgentSignals.ERROR_MCP_MISSING,
},
);
const errorMessage = `
${chalk.red('❌ Could not access the PostHog MCP server')}
The wizard was unable to connect to the PostHog MCP server.
This could be due to a network issue or a configuration problem.
Please try again, or set up ${
config.metadata.name
} manually by following our documentation:
${chalk.cyan(config.metadata.docsUrl)}`;
clack.outro(errorMessage);
await analytics.shutdown('error');
process.exit(1);
}
if (agentResult.error === AgentErrorType.RESOURCE_MISSING) {
analytics.captureException(
new Error('Agent could not access setup resource'),
{
integration: config.metadata.integration,
error_type: AgentErrorType.RESOURCE_MISSING,
signal: AgentSignals.ERROR_RESOURCE_MISSING,
},
);
const errorMessage = `
${chalk.red('❌ Could not access the setup resource')}
The wizard could not access the setup resource. This may indicate a version mismatch or a temporary service issue.
Please try again, or set up ${
config.metadata.name
} manually by following our documentation:
${chalk.cyan(config.metadata.docsUrl)}`;
clack.outro(errorMessage);
await analytics.shutdown('error');
process.exit(1);
}
if (
agentResult.error === AgentErrorType.RATE_LIMIT ||
agentResult.error === AgentErrorType.API_ERROR
) {
analytics.capture(WIZARD_INTERACTION_EVENT_NAME, {
action: 'api error',
integration: config.metadata.integration,
error_type: agentResult.error,
error_message: agentResult.message,
});
analytics.captureException(new Error(`API error: ${agentResult.message}`), {
integration: config.metadata.integration,
error_type: agentResult.error,
});
const errorMessage = `
${chalk.red('❌ API Error')}
${chalk.yellow(agentResult.message || 'Unknown error')}
Please report this error to: ${chalk.cyan('wizard@posthog.com')}`;
clack.outro(errorMessage);
await analytics.shutdown('error');
process.exit(1);
}
// Build environment variables from OAuth credentials
const envVars = config.environment.getEnvVars(projectApiKey, host);
// Upload environment variables to hosting providers (if configured)
let uploadedEnvVars: string[] = [];
if (config.environment.uploadToHosting) {
uploadedEnvVars = await uploadEnvironmentVariablesStep(envVars, {
integration: config.metadata.integration,
options,
});
}
// Add MCP server to clients
await addMCPServerToClientsStep({
cloudRegion,
integration: config.metadata.integration,
ci: options.ci,
});
// Build outro message
const continueUrl = options.signup
? `${getCloudUrlFromRegion(cloudRegion)}/products?source=wizard`
: undefined;
const changes = [
...config.ui.getOutroChanges(frameworkContext),
Object.keys(envVars).length > 0
? `Added environment variables to .env file`
: '',
uploadedEnvVars.length > 0
? `Uploaded environment variables to your hosting provider`
: '',
].filter(Boolean);
const nextSteps = [
...config.ui.getOutroNextSteps(frameworkContext),
uploadedEnvVars.length === 0 && config.environment.uploadToHosting
? `Upload your Project API key to your hosting provider`
: '',
].filter(Boolean);
const outroMessage = `
${chalk.green('Successfully installed PostHog!')}
${chalk.cyan('What the agent did:')}
${changes.map((change) => `• ${change}`).join('\n')}
${chalk.yellow('Next steps:')}
${nextSteps.map((step) => `• ${step}`).join('\n')}
Learn more: ${chalk.cyan(config.metadata.docsUrl)}
${continueUrl ? `\nContinue onboarding: ${chalk.cyan(continueUrl)}\n` : ``}
${chalk.dim(
'Note: This wizard uses an LLM agent to analyze and modify your project. Please review the changes made.',
)}
${chalk.dim(`How did this work for you? Drop us a line: wizard@posthog.com`)}`;
clack.outro(outroMessage);
await analytics.shutdown('success');
}
/**
* Build the integration prompt for the agent.
*/
function buildIntegrationPrompt(
config: FrameworkConfig,
context: {
frameworkVersion: string;
typescript: boolean;
projectApiKey: string;
host: string;
projectId: number;
},
frameworkContext: Record<string, unknown>,
): string {
const additionalLines = config.prompts.getAdditionalContextLines
? config.prompts.getAdditionalContextLines(frameworkContext)
: [];
const additionalContext =
additionalLines.length > 0
? '\n' + additionalLines.map((line) => `- ${line}`).join('\n')
: '';
return `You have access to the PostHog MCP server which provides skills to integrate PostHog into this ${
config.metadata.name
} project.
Project context:
- PostHog Project ID: ${context.projectId}
- Framework: ${config.metadata.name} ${context.frameworkVersion}
- TypeScript: ${context.typescript ? 'Yes' : 'No'}
- PostHog API Key: ${context.projectApiKey}
- PostHog Host: ${context.host}
- Project type: ${config.prompts.projectTypeDetection}
- Package installation: ${
config.prompts.packageInstallation ?? DEFAULT_PACKAGE_INSTALLATION
}${additionalContext}
Instructions (follow these steps IN ORDER - do not skip or reorder):
STEP 1: List available skills from the PostHog MCP server using ListMcpResourcesTool. If this tool is not available or you cannot access the MCP server, you must emit: ${
AgentSignals.ERROR_MCP_MISSING
} Could not access the PostHog MCP server and halt.
Review the skill descriptions and choose the one that best matches this project's framework and configuration.
If no suitable skill is found, or you cannot access the MCP server, you emit: ${
AgentSignals.ERROR_RESOURCE_MISSING
} Could not find a suitable skill for this project.
STEP 2: Fetch the chosen skill resource (e.g., posthog://skills/{skill-id}).
The resource returns a shell command to install the skill.
STEP 3: Run the installation command using Bash:
- Execute the EXACT command returned by the resource (do not modify it)
- This will download and extract the skill to .claude/skills/{skill-id}/
STEP 4: Load the installed skill's SKILL.md file to understand what references are available.
STEP 5: Follow the skill's workflow files in sequence. Look for numbered workflow files in the references (e.g., files with patterns like "1.0-", "1.1-", "1.2-"). Start with the first one and proceed through each step until completion. Each workflow file will tell you what to do and which file comes next. Never directly write PostHog keys directly to code files; always use environment variables.
STEP 6: Set up environment variables for PostHog using the wizard-tools MCP server (this runs locally — secret values never leave the machine):
- Use check_env_keys to see which keys already exist in the project's .env file (e.g. .env.local or .env).
- Use set_env_values to create or update the PostHog API key and host, using the appropriate naming convention for ${
config.metadata.name
}. The tool will also ensure .gitignore coverage. Don't assume the presence of keys means the value is up to date. Write the correct value each time.
- Reference these environment variables in the code files you create instead of hardcoding the API key and host.
Important: Use the detect_package_manager tool (from the wizard-tools MCP server) to determine which package manager the project uses. Do not manually search for lockfiles or config files. Always install packages as a background task. Don't await completion; proceed with other work immediately after starting the installation. You must read a file immediately before attempting to write it, even if you have previously read it; failure to do so will cause a tool failure.
`;
}