diff --git a/bin.ts b/bin.ts index cd1c45f6..fb891aeb 100644 --- a/bin.ts +++ b/bin.ts @@ -496,6 +496,8 @@ function runWizard( session.workflowLabel = config.flowKey; if (options.skillId) { session.skillId = options.skillId as string; + } else if (config.skillId) { + session.skillId = config.skillId; } tui.store.session = session; @@ -635,6 +637,9 @@ function runWizardCI( ...env, }); session.workflowLabel = config.flowKey; + if (config.skillId) { + session.skillId = config.skillId; + } const runDef = typeof config.run === 'object' ? config.run : null; getUI().intro('Welcome to the PostHog setup wizard'); diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index c6ef9c89..15244d7e 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -83,6 +83,12 @@ export const AgentSignals = { WIZARD_REMARK: '[WIZARD-REMARK]', /** Signal prefix for benchmark logging */ BENCHMARK: '[BENCHMARK]', + /** + * Signal emitted when the agent has created a PostHog dashboard for the + * user. Format: `[DASHBOARD_URL] `. The URL is captured + * onto `session.dashboardUrl` and surfaced by workflows in their outro. + */ + DASHBOARD_URL: '[DASHBOARD_URL]', } as const; export type AgentSignal = (typeof AgentSignals)[keyof typeof AgentSignals]; @@ -1212,6 +1218,19 @@ function handleSDKMessage( getUI().pushStatus(statusText); spinner.message(statusText); } + + // Check for [DASHBOARD_URL] markers + const dashboardRegex = new RegExp( + `${AgentSignals.DASHBOARD_URL.replace( + /[.*+?^${}()|[\]\\]/g, + '\\$&', + )}\\s*(\\S+)`, + 'm', + ); + const dashboardMatch = block.text.match(dashboardRegex); + if (dashboardMatch) { + getUI().setDashboardUrl(dashboardMatch[1].trim()); + } } // Intercept TodoWrite tool_use blocks for task progression diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 9544e9b0..6accae80 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -90,8 +90,18 @@ export interface OutroData { continueUrl?: string; /** Report file the agent wrote (e.g. "posthog-setup-report.md") */ reportFile?: string; + /** PostHog dashboard URL the workflow created on the user's behalf. */ + dashboardUrl?: string; } +/** + * PostHog dashboard URL emitted by the agent during a workflow run. + * Populated via the `[DASHBOARD_URL]` text marker in agent assistant messages + * — see `handleSDKMessage` in `agent/agent-interface.ts`. Read by workflows + * (e.g. events-audit) inside `buildOutroData` to surface a dashboard link + * the agent actually created. + */ + export interface WizardSession { // From CLI args debug: boolean; @@ -158,6 +168,7 @@ export interface WizardSession { user: string; } | null; outroData: OutroData | null; + dashboardUrl: string | null; // Additional features queue (drained via stop hook after main integration) additionalFeatureQueue: AdditionalFeature[]; @@ -230,6 +241,7 @@ export function buildSession(args: { settingsConflicts: null, portConflictProcess: null, outroData: null, + dashboardUrl: null, additionalFeatureQueue: [], workflowLabel: null, skillId: null, diff --git a/src/lib/workflows/agent-skill/index.ts b/src/lib/workflows/agent-skill/index.ts index 2ad4a2ac..8598cbfa 100644 --- a/src/lib/workflows/agent-skill/index.ts +++ b/src/lib/workflows/agent-skill/index.ts @@ -56,6 +56,7 @@ export function createSkillWorkflow( command: opts.command, description: opts.description, flowKey: opts.flowKey, + skillId: opts.skillId, steps: AGENT_SKILL_STEPS, run: { skillId: opts.skillId, diff --git a/src/lib/workflows/events-audit/index.ts b/src/lib/workflows/events-audit/index.ts new file mode 100644 index 00000000..78db03d2 --- /dev/null +++ b/src/lib/workflows/events-audit/index.ts @@ -0,0 +1,77 @@ +import type { WorkflowConfig } from '../workflow-step.js'; +import type { WorkflowRun } from '../../agent/agent-runner.js'; +import type { WizardSession } from '../../wizard-session.js'; +import { OutroKind } from '../../wizard-session.js'; +import { SPINNER_MESSAGE } from '../../framework-config.js'; +import { isUsingTypeScript } from '../../../utils/setup-utils.js'; +import { getCloudUrlFromRegion } from '../../../utils/urls.js'; +import { EVENTS_AUDIT_WORKFLOW } from './steps.js'; + +export const SETUP_REPORT_FILE = 'posthog-events-audit-report.md'; + +const DOCS_URL = 'https://posthog.com/docs/product-analytics/best-practices'; + +export const eventsAuditConfig: WorkflowConfig = { + command: 'events-audit', + description: 'Audit PostHog event tracking in this project', + flowKey: 'events-audit', + skillId: 'events-audit', + steps: EVENTS_AUDIT_WORKFLOW, + + run: (session: WizardSession): Promise => { + const typeScriptDetected = isUsingTypeScript({ + installDir: session.installDir, + }); + session.typescript = typeScriptDetected; + + return Promise.resolve({ + skillId: 'events-audit', + integrationLabel: 'events-audit', + spinnerMessage: SPINNER_MESSAGE, + successMessage: + 'Events audit complete! You can view the report at ./posthog-events-audit-report.md', + estimatedDurationMinutes: 5, + reportFile: SETUP_REPORT_FILE, + docsUrl: DOCS_URL, + errorMessage: 'Events audit failed', + additionalFeatureQueue: session.additionalFeatureQueue, + + customPrompt: (ctx) => + `Audit PostHog event capture in this project. Do not modify any project files — produce a read-only report only. + +Project context: +- PostHog Project ID: ${ctx.projectId} +- TypeScript: ${typeScriptDetected ? 'Yes' : 'No'} +- PostHog public token: ${ctx.projectApiKey} +- PostHog Host: ${ctx.host} +`, + + buildOutroData: (sess, _credentials, cloudRegion) => { + const cloudUrl = cloudRegion + ? getCloudUrlFromRegion(cloudRegion) + : undefined; + const continueUrl = + sess.signup && cloudUrl + ? `${cloudUrl}/products?source=wizard` + : undefined; + // The agent emits `[DASHBOARD_URL] ` once it creates the + // dashboard; the SDK-message interceptor stores it on the session. + // Fall back to the dashboards index if nothing was emitted. + const dashboardUrl = + sess.dashboardUrl ?? (cloudUrl ? `${cloudUrl}/dashboard` : undefined); + + return { + kind: OutroKind.Success as const, + message: 'Your events audit was successful', + reportFile: SETUP_REPORT_FILE, + changes: [], + docsUrl: DOCS_URL, + continueUrl, + dashboardUrl, + }; + }, + }); + }, +}; + +export { EVENTS_AUDIT_WORKFLOW } from './steps.js'; diff --git a/src/lib/workflows/events-audit/steps.ts b/src/lib/workflows/events-audit/steps.ts new file mode 100644 index 00000000..fadc1108 --- /dev/null +++ b/src/lib/workflows/events-audit/steps.ts @@ -0,0 +1,115 @@ +/** + * Events-audit workflow. + * + * Mirrors the posthog-integration step list, except: + * - The initial framework detection step is omitted — the events-audit + * skill handles detection at agent run time. + * - The intro step uses the audit intro screen (no framework selection + * logic) instead of the integration intro. + */ + +import type { Workflow } from '../workflow-step.js'; +import type { WizardSession } from '../../wizard-session.js'; +import { RunPhase } from '../../wizard-session.js'; +import { + evaluateWizardReadiness, + WizardReadiness, + SIGNUP_WIZARD_READINESS_CONFIG, + getBlockingServiceKeys, +} from '../../health-checks/readiness.js'; + +function needsSetup(session: WizardSession): boolean { + const config = session.frameworkConfig; + if (!config?.metadata.setup?.questions) return false; + + return config.metadata.setup.questions.some( + (q: { key: string }) => !(q.key in session.frameworkContext), + ); +} + +function healthCheckReady(session: WizardSession): boolean { + if (!session.readinessResult) return false; + + if (session.signup) { + const hardBlocking = getBlockingServiceKeys( + session.readinessResult.health, + SIGNUP_WIZARD_READINESS_CONFIG, + ); + const defaultBlocking = getBlockingServiceKeys( + session.readinessResult.health, + ); + if (hardBlocking.length === 0 && defaultBlocking.length === 0) return true; + return session.outageDismissed; + } + + if (session.readinessResult.decision === WizardReadiness.No) { + return session.outageDismissed; + } + return true; +} + +export const EVENTS_AUDIT_WORKFLOW: Workflow = [ + { + id: 'intro', + label: 'Welcome', + screen: 'audit-intro', + gate: (session) => session.setupConfirmed, + }, + { + id: 'health-check', + label: 'Health check', + screen: 'health-check', + gate: healthCheckReady, + onInit: (ctx) => { + evaluateWizardReadiness() + .then((readiness) => { + ctx.setReadinessResult(readiness); + }) + .catch(() => { + ctx.setReadinessResult({ + decision: WizardReadiness.Yes, + health: {} as never, + reasons: [], + }); + }); + }, + }, + { + id: 'setup', + label: 'Setup', + screen: 'setup', + show: needsSetup, + isComplete: (session) => !needsSetup(session), + }, + { + id: 'auth', + label: 'Authentication', + screen: 'auth', + isComplete: (session) => session.credentials !== null, + }, + { + id: 'run', + label: 'Events audit', + screen: 'run', + isComplete: (session) => + session.runPhase === RunPhase.Completed || + session.runPhase === RunPhase.Error, + }, + { + id: 'mcp', + label: 'MCP servers', + screen: 'mcp', + isComplete: (session) => session.mcpComplete, + }, + { + id: 'outro', + label: 'Done', + screen: 'outro', + isComplete: (session) => session.outroDismissed, + }, + { + id: 'keep-skills', + label: 'Keep Skills', + screen: 'keep-skills', + }, +]; diff --git a/src/lib/workflows/workflow-registry.ts b/src/lib/workflows/workflow-registry.ts index 3a729dad..ea31cc6c 100644 --- a/src/lib/workflows/workflow-registry.ts +++ b/src/lib/workflows/workflow-registry.ts @@ -15,12 +15,14 @@ import type { WorkflowConfig } from './workflow-step.js'; import { posthogIntegrationConfig } from './posthog-integration/index.js'; import { revenueAnalyticsConfig } from './revenue-analytics/index.js'; import { auditConfig } from './audit/index.js'; +import { eventsAuditConfig } from './events-audit/index.js'; import { posthogDoctorConfig } from './posthog-doctor/index.js'; export const WORKFLOW_REGISTRY: WorkflowConfig[] = [ posthogIntegrationConfig, revenueAnalyticsConfig, auditConfig, + eventsAuditConfig, posthogDoctorConfig, ]; diff --git a/src/lib/workflows/workflow-step.ts b/src/lib/workflows/workflow-step.ts index cc7bd0f6..4c5790b4 100644 --- a/src/lib/workflows/workflow-step.ts +++ b/src/lib/workflows/workflow-step.ts @@ -117,6 +117,13 @@ export interface WorkflowConfig { description: string; /** Unique flow key — matches the Flow enum value */ flowKey: string; + /** + * Context-mill skill ID this workflow installs and runs. When present, + * bin.ts seeds `session.skillId` with this value before the TUI renders + * so intro screens can resolve skill metadata without waiting for the + * agent run. + */ + skillId?: string; /** The ordered step list */ steps: Workflow; /** Agent run config. Static object or async function for dynamic config. */ diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts index c67f0b6b..0e54d910 100644 --- a/src/ui/logging-ui.ts +++ b/src/ui/logging-ui.ts @@ -179,6 +179,10 @@ export class LoggingUI implements WizardUI { // No-op in CI mode } + setDashboardUrl(_url: string): void { + // No-op in CI mode + } + setFrameworkContext(_key: string, _value: unknown): void { // No-op in CI mode } diff --git a/src/ui/tui/flows.ts b/src/ui/tui/flows.ts index 79a9e0ab..5f3ee7fc 100644 --- a/src/ui/tui/flows.ts +++ b/src/ui/tui/flows.ts @@ -46,6 +46,7 @@ export enum Flow { PostHogIntegration = 'posthog-integration', RevenueAnalyticsSetup = 'revenue-analytics-setup', Audit = 'audit', + EventsAudit = 'events-audit', PosthogDoctor = 'posthog-doctor', AgentSkill = 'agent-skill', McpAdd = 'mcp-add', diff --git a/src/ui/tui/ink-ui.ts b/src/ui/tui/ink-ui.ts index a7b6aae8..2b1ebbaa 100644 --- a/src/ui/tui/ink-ui.ts +++ b/src/ui/tui/ink-ui.ts @@ -184,6 +184,10 @@ export class InkUI implements WizardUI { this.store.setEventPlan(events); } + setDashboardUrl(url: string): void { + this.store.setDashboardUrl(url); + } + setFrameworkContext(key: string, value: unknown): void { this.store.setFrameworkContext(key, value); } diff --git a/src/ui/tui/screens/OutroScreen.tsx b/src/ui/tui/screens/OutroScreen.tsx index bc6784c6..2f288929 100644 --- a/src/ui/tui/screens/OutroScreen.tsx +++ b/src/ui/tui/screens/OutroScreen.tsx @@ -83,6 +83,15 @@ export const OutroScreen = ({ store }: OutroScreenProps) => { )} + {outroData.dashboardUrl && ( + + + We've also made you a dashboard:{' '} + {outroData.dashboardUrl} + + + )} + {outroData.docsUrl && ( diff --git a/src/ui/tui/screens/audit/AuditIntroScreen.tsx b/src/ui/tui/screens/audit/AuditIntroScreen.tsx index fec818f5..8f2da3a4 100644 --- a/src/ui/tui/screens/audit/AuditIntroScreen.tsx +++ b/src/ui/tui/screens/audit/AuditIntroScreen.tsx @@ -4,8 +4,6 @@ import type { WizardStore } from '../../store.js'; import { IntroScreenLayout } from '../IntroScreenLayout.js'; import { SkillSourceInfo, useSkillEntry } from '../SkillSourceInfo.js'; -const AUDIT_SKILL_ID = 'audit'; - interface AuditIntroScreenProps { store: WizardStore; } @@ -18,10 +16,10 @@ export const AuditIntroScreen = ({ store }: AuditIntroScreenProps) => { const [showingMoreInfo, setShowingMoreInfo] = useState(false); const { session } = store; - const { skillEntry, fetchFailed } = useSkillEntry( - AUDIT_SKILL_ID, - session.localMcp, - ); + // bin.ts seeds session.skillId from WorkflowConfig.skillId before render, + // so audit and events-audit pick up their respective skill metadata here. + const skillId = session.skillId ?? 'audit'; + const { skillEntry, fetchFailed } = useSkillEntry(skillId, session.localMcp); const body = showingMoreInfo ? ( @@ -35,7 +33,7 @@ export const AuditIntroScreen = ({ store }: AuditIntroScreenProps) => { The{' '} - {AUDIT_SKILL_ID} + {skillId} {' '} workflow reviews your project's PostHog integration against best practices to help you capture high-quality events and writes a report @@ -43,7 +41,7 @@ export const AuditIntroScreen = ({ store }: AuditIntroScreenProps) => { diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index cf5b0550..b174e89f 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -439,6 +439,11 @@ export class WizardStore { this.emitChange(); } + setDashboardUrl(url: string): void { + this.$session.setKey('dashboardUrl', url); + this.emitChange(); + } + setFrameworkContext(key: string, value: unknown): void { const ctx = { ...this.$session.get().frameworkContext, [key]: value }; this.$session.setKey('frameworkContext', ctx); diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index ba1d1e3d..f7577eeb 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -112,6 +112,9 @@ export interface WizardUI { // ── Event plan from .posthog-events.json ──────────────────── setEventPlan(events: Array<{ name: string; description: string }>): void; + // ── Dashboard URL emitted by the agent via [DASHBOARD_URL] marker ── + setDashboardUrl(url: string): void; + // ── Generic frameworkContext setter for workflow file watchers ───── setFrameworkContext(key: string, value: unknown): void; }