diff --git a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts index ac549020..c5085faf 100644 --- a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts +++ b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts @@ -17,6 +17,7 @@ import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; import { setUI } from '@ui/index'; import { buildSession, RunPhase } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; import { Integration } from '@lib/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { WizardReadiness } from '@lib/health-checks/readiness'; @@ -66,7 +67,7 @@ function traceFlow( store.setCredentials({ accessToken: 'phx_x', projectApiKey: 'phc_x', - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), projectId: 1, }); } else if (screen === ScreenId.Run) { diff --git a/e2e-harness/__tests__/wizard-ci-driver.test.ts b/e2e-harness/__tests__/wizard-ci-driver.test.ts index 230923b5..0cdb7e98 100644 --- a/e2e-harness/__tests__/wizard-ci-driver.test.ts +++ b/e2e-harness/__tests__/wizard-ci-driver.test.ts @@ -13,6 +13,7 @@ import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; import { setUI } from '@ui/index'; import { buildSession, RunPhase, McpOutcome } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; import { Integration } from '@lib/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { WizardReadiness } from '@lib/health-checks/readiness'; @@ -72,7 +73,7 @@ describe('WizardCiDriver — full integration flow', () => { store.setCredentials({ accessToken: 'phx_secret_should_not_leak', projectApiKey: 'phc_public', - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), projectId: 42, }); @@ -112,7 +113,7 @@ describe('WizardCiDriver — full integration flow', () => { store.setCredentials({ accessToken: 'phx_secret_should_not_leak', projectApiKey: 'phc_public', - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), projectId: 7, }); const state = driver.readState(); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index f3088ff9..79234949 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -58,7 +58,11 @@ async function runDoctorCI(options: Record): Promise { baseUrl: options.baseUrl as string | undefined, }); - const issues = await fetchHealthIssues(accessToken, host, projectId); + const issues = await fetchHealthIssues( + accessToken, + host.apiHost, + projectId, + ); if (issues.length === 0) { getUI().log.success('No active issues — your project looks healthy.'); process.exit(0); diff --git a/src/lib/agent/__tests__/agent-prompt-loader.test.ts b/src/lib/agent/__tests__/agent-prompt-loader.test.ts index 9446cea4..636b5393 100644 --- a/src/lib/agent/__tests__/agent-prompt-loader.test.ts +++ b/src/lib/agent/__tests__/agent-prompt-loader.test.ts @@ -13,6 +13,7 @@ import { type OrchestratorPromptContext, } from '../agent-prompt-loader'; import { QueueStore } from '@lib/agent/runner/orchestrator/queue'; +import { HostResolution } from '@lib/host-resolution'; function tmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'agent-loader-test-')); @@ -280,7 +281,7 @@ describe('assembleTaskPrompt', () => { const ctx: OrchestratorPromptContext = { projectId: 1, projectApiKey: 'phc_x', - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), }; it('points the agent at its installed task instructions', () => { diff --git a/src/lib/agent/__tests__/agent-prompt.test.ts b/src/lib/agent/__tests__/agent-prompt.test.ts index 18609e29..2ee6bfdb 100644 --- a/src/lib/agent/__tests__/agent-prompt.test.ts +++ b/src/lib/agent/__tests__/agent-prompt.test.ts @@ -1,5 +1,6 @@ import { assemblePrompt, type PromptContext } from '@lib/agent/agent-prompt'; import type { ProgramRun } from '@lib/agent/agent-runner'; +import { HostResolution } from '@lib/host-resolution'; function makeRunDef(overrides: Partial = {}): ProgramRun { return { @@ -16,7 +17,7 @@ function makeRunDef(overrides: Partial = {}): ProgramRun { const baseCtx: PromptContext = { projectId: 42, projectApiKey: 'phc_test123', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), }; describe('assemblePrompt', () => { diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index 40fe979a..ce58dfb0 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -25,7 +25,7 @@ import { } from '@lib/wizard-session'; import { wizardAbort, WizardError } from '@utils/wizard-abort'; import { createCustomHeaders } from '@utils/custom-headers'; -import { HostResolution } from '@lib/host-resolution'; +import type { HostResolution } from '@lib/host-resolution'; import { LINTING_TOOLS } from '@lib/safe-tools'; import { createWizardToolsServer, WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; import { @@ -176,7 +176,7 @@ export type AgentConfig = { workingDirectory: string; posthogMcpUrl: string; posthogApiKey: string; - posthogApiHost: string; + host: HostResolution; additionalMcpServers?: Record; detectPackageManager: PackageManagerDetector; /** Base URL for the skills server (context-mill dev or GitHub releases) */ @@ -630,16 +630,12 @@ export async function initializeAgent( logToFile('Install directory:', options.installDir); try { - // TODO: clean up in #755 - const gatewayUrl = HostResolution.fromApiHost( - config.posthogApiHost, - ).gatewayUrl; - // Configure model routing (inherited by the SDK subprocess). All model // calls route through the PostHog LLM gateway, authed with the user's // OAuth token. // Disable experimental betas (like input_examples) the gateway doesn't support. process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = 'true'; + const gatewayUrl = config.host.gatewayUrl; process.env.ANTHROPIC_BASE_URL = gatewayUrl; process.env.ANTHROPIC_AUTH_TOKEN = config.posthogApiKey; diff --git a/src/lib/agent/agent-prompt-loader.ts b/src/lib/agent/agent-prompt-loader.ts index f40276ea..9dbe17b2 100644 --- a/src/lib/agent/agent-prompt-loader.ts +++ b/src/lib/agent/agent-prompt-loader.ts @@ -17,6 +17,7 @@ */ import type { QueueStore, QueuedTask } from './runner/orchestrator/queue'; import type { ResolvedTask } from './runner/orchestrator/executor'; +import type { HostResolution } from '@lib/host-resolution'; /** * The basics the client injects around every agent-prompt body. The `/agents/` @@ -27,7 +28,7 @@ import type { ResolvedTask } from './runner/orchestrator/executor'; export interface OrchestratorPromptContext { projectId: number; projectApiKey: string; - host: string; + host: HostResolution; /** Path to the framework's reference implementation (EXAMPLE.md), if available. */ examplePath?: string; /** Path to the framework's rules (COMMANDMENTS.md), if available. */ @@ -40,7 +41,7 @@ function projectContext(ctx: OrchestratorPromptContext): string { Project context: - PostHog Project ID: ${ctx.projectId} - PostHog public token: ${ctx.projectApiKey} -- PostHog Host: ${ctx.host}`; +- PostHog Host: ${ctx.host.apiHost}`; } /** Points the agent at the framework's reference integration to learn patterns from. */ diff --git a/src/lib/agent/agent-prompt.ts b/src/lib/agent/agent-prompt.ts index 0cff0d7e..0427fc4f 100644 --- a/src/lib/agent/agent-prompt.ts +++ b/src/lib/agent/agent-prompt.ts @@ -8,6 +8,7 @@ */ import type { ProgramRun } from './agent-runner.js'; +import type { HostResolution } from '@lib/host-resolution'; /** * Values available to prompt builders after OAuth completes. @@ -15,7 +16,7 @@ import type { ProgramRun } from './agent-runner.js'; export interface PromptContext { projectId: number; projectApiKey: string; - host: string; + host: HostResolution; /** Set when skillId was provided and the skill was installed successfully. */ skillPath?: string; /** @@ -44,7 +45,7 @@ function defaultProjectPrompt(ctx: PromptContext): string { Project context: - PostHog Project ID: ${ctx.projectId} - PostHog public token: ${ctx.projectApiKey} -- PostHog Host: ${ctx.host}`; +- PostHog Host: ${ctx.host.apiHost}`; } function skillPrompt(skillPath: string, reportFile: string): string { diff --git a/src/lib/agent/mcp-prompt-streaming.ts b/src/lib/agent/mcp-prompt-streaming.ts index a01667ff..b7438e76 100644 --- a/src/lib/agent/mcp-prompt-streaming.ts +++ b/src/lib/agent/mcp-prompt-streaming.ts @@ -15,8 +15,6 @@ import type { AgentChunk } from '@ui/tui/services/mcp-suggested-prompts-services'; import type { Credentials } from '@lib/wizard-session'; import { WIZARD_USER_AGENT } from '@lib/constants'; -import { HostResolution } from '@lib/host-resolution'; -import { runtimeEnv } from '@env'; import { logToFile } from '@utils/debug'; import { buildAgentEnv } from '@lib/agent/agent-interface'; import { sanitizeAgentSubprocessEnv } from '@lib/agent/agent-env-isolation'; @@ -43,13 +41,6 @@ const MODEL = 'claude-sonnet-4-6'; // telemetry on average turn counts per prompt. const MAX_TURNS = 30; -// One MCP url for every region: the server resolves the user's region from -// the bearer token, so the EU subdomain (a Claude Code OAuth workaround) is -// not needed here. -function resolveMcpUrl(): string { - return runtimeEnv('MCP_URL') || 'https://mcp.posthog.com/mcp'; -} - /** * Extract a short, single-line summary from an arbitrary value. Used * for tool-call args and tool-result bodies so the screen has something @@ -198,8 +189,7 @@ export async function* runMcpPromptViaSdk(args: { process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = 'true'; // Route through the PostHog LLM gateway, authed with the user's OAuth token. - // TODO: clean up in #755 - const gatewayUrl = HostResolution.fromApiHost(credentials.host).gatewayUrl; + const gatewayUrl = credentials.host.gatewayUrl; process.env.ANTHROPIC_BASE_URL = gatewayUrl; process.env.ANTHROPIC_AUTH_TOKEN = credentials.accessToken; process.env.CLAUDE_CODE_OAUTH_TOKEN = credentials.accessToken; @@ -223,7 +213,7 @@ export async function* runMcpPromptViaSdk(args: { once: true, }); - const mcpUrl = resolveMcpUrl(); + const mcpUrl = credentials.host.mcpUrl; logToFile( `[runMcpPromptViaSdk] mcpUrl=${mcpUrl} model=${MODEL} resume=${ resumeSessionId ?? '(none)' diff --git a/src/lib/agent/runner/linear.ts b/src/lib/agent/runner/linear.ts index 8da01a2d..bdd8646a 100644 --- a/src/lib/agent/runner/linear.ts +++ b/src/lib/agent/runner/linear.ts @@ -15,7 +15,6 @@ import { AgentSignals, } from '../agent-interface'; import { restoreClaudeSettings } from '../claude-settings'; -import { HostResolution } from '@lib/host-resolution'; import { logToFile, getLogFilePath } from '../../../utils/debug'; import { createBenchmarkPipeline } from '../../middleware/benchmark'; import { @@ -45,18 +44,10 @@ export async function runLinearProgram( boot: BootstrapResult, composed = false, ): Promise { - const { - skillsBaseUrl, - projectApiKey, - host, - accessToken, - projectId, - cloudRegion, - mcpUrl, - wizardFlags, - wizardMetadata, - project, - } = boot; + const { skillsBaseUrl, wizardFlags, wizardMetadata, project } = boot; + // Set by bootstrapProgram before the fork — guaranteed non-null here. + const credentials = session.credentials!; + const { projectApiKey, host, accessToken, projectId } = credentials; // 5. Skill install (if skillId provided) let skillPath: string | undefined; @@ -110,9 +101,9 @@ export async function runLinearProgram( const agent = await initializeAgent( { workingDirectory: session.installDir, - posthogMcpUrl: mcpUrl, + posthogMcpUrl: host.mcpUrl, posthogApiKey: accessToken, - posthogApiHost: host, + host, additionalMcpServers: config.additionalMcpServers, detectPackageManager: config.detectPackageManager ?? detectNodePackageManagers, @@ -267,12 +258,7 @@ export async function runLinearProgram( // 10. Post-run hooks if (config.postRun) { - await config.postRun(session, { - accessToken, - projectApiKey, - host, - projectId, - }); + await config.postRun(session, credentials); } // A composed sub-run (integration inside self-driving) skips the terminal @@ -288,23 +274,14 @@ export async function runLinearProgram( // that the screen never reads. UI.setOutroData() goes through the store // and also merges in any post-snapshot URLs from the live session. const outroData = config.buildOutroData - ? config.buildOutroData( - session, - { accessToken, projectApiKey, host, projectId }, - cloudRegion, - ) + ? config.buildOutroData(session, credentials) : { kind: OutroKind.Success, message: config.successMessage, reportFile: config.reportFile, docsUrl: config.docsUrl, - // TODO: clean up in #755 continueUrl: session.signup - ? `${ - HostResolution.fromRegion(cloudRegion, { - baseUrl: session.baseUrl, - }).appHost - }/products?source=wizard` + ? `${host.appHost}/products?source=wizard` : undefined, }; if (outroData) { diff --git a/src/lib/agent/runner/orchestrator/orchestrator-runner.ts b/src/lib/agent/runner/orchestrator/orchestrator-runner.ts index 7785a3a2..341abef2 100644 --- a/src/lib/agent/runner/orchestrator/orchestrator-runner.ts +++ b/src/lib/agent/runner/orchestrator/orchestrator-runner.ts @@ -105,6 +105,9 @@ export async function runOrchestrator( const options = sessionRunOptions(session); + // Set by bootstrapProgram before the fork — guaranteed non-null here. + const credentials = session.credentials!; + // The WHAT (agent prompts) is served from context-mill. Fetch the registry // once up front: its types drive enqueue validation, and resolving a task to // its run config is then synchronous, with no mid-drain network latency. @@ -219,9 +222,9 @@ export async function runOrchestrator( // The client injects the basics (project context + the I/O contract) around // every authored agent-prompt body. const promptContext: OrchestratorPromptContext = { - projectId: boot.projectId, - projectApiKey: boot.projectApiKey, - host: boot.host, + projectId: credentials.projectId, + projectApiKey: credentials.projectApiKey, + host: credentials.host, examplePath, commandmentsPath, }; @@ -253,9 +256,9 @@ export async function runOrchestrator( // so its context has no task id. const agentConfigFor = (currentTaskId?: string): AgentConfig => ({ workingDirectory: session.installDir, - posthogMcpUrl: boot.mcpUrl, - posthogApiKey: boot.accessToken, - posthogApiHost: boot.host, + posthogMcpUrl: credentials.host.mcpUrl, + posthogApiKey: credentials.accessToken, + host: credentials.host, detectPackageManager: detectNodePackageManagers, skillsBaseUrl: boot.skillsBaseUrl, wizardFlags: boot.wizardFlags, diff --git a/src/lib/agent/runner/shared/authenticate.ts b/src/lib/agent/runner/shared/authenticate.ts index c67f5c9d..3bc8aed9 100644 --- a/src/lib/agent/runner/shared/authenticate.ts +++ b/src/lib/agent/runner/shared/authenticate.ts @@ -29,7 +29,6 @@ export async function authenticate( host, accessToken, projectId, - cloudRegion, roleAtOrganization, user, project, @@ -41,11 +40,11 @@ export async function authenticate( email: session.email, region: session.region, baseUrl: session.baseUrl, + localMcp: session.localMcp, programId, }); session.credentials = { accessToken, projectApiKey, host, projectId }; - session.cloudRegion = cloudRegion; session.apiProject = project; session.roleAtOrganization = roleAtOrganization; session.apiUser = user; @@ -57,5 +56,5 @@ export async function authenticate( // Identify the user (email, name) before flags are evaluated, so flags can // target the individual user and not just $app_name. if (user) analytics.identifyUser(user); - analytics.setGroups(groupsFromUser(user, host)); + analytics.setGroups(groupsFromUser(user, host.apiHost)); } diff --git a/src/lib/agent/runner/shared/bootstrap.ts b/src/lib/agent/runner/shared/bootstrap.ts index cd39b0a0..a941c0e7 100644 --- a/src/lib/agent/runner/shared/bootstrap.ts +++ b/src/lib/agent/runner/shared/bootstrap.ts @@ -28,7 +28,6 @@ import { enableDebugLogs, logToFile, initLogFile } from '@utils/debug'; import { wizardAbort } from '@utils/wizard-abort'; import { isNonInteractiveEnvironment } from '@utils/environment'; import { getSkillsBaseUrl } from '@lib/constants'; -import { runtimeEnv } from '@env'; import type { WizardRunOptions } from '@utils/types'; import type { ProgramConfig } from '@lib/programs/program-step'; import type { ProgramRun, BootstrapResult } from './types'; @@ -215,8 +214,6 @@ export async function bootstrapProgram( // the first login; it does not launch another OAuth. authenticate() also // identifies the user and sets analytics groups. await authenticate(session, programConfig.id); - const { projectApiKey, host, accessToken, projectId } = session.credentials!; - const cloudRegion = session.cloudRegion!; const project = session.apiProject; // 4.5. AI opt-in enforcement. Parks here while AiOptInRequiredScreen is @@ -245,8 +242,7 @@ export async function bootstrapProgram( } } - // Feature flags and MCP url. Both arms need these, and the fork decision reads - // the flags. + // Feature flags. Both arms need these, and the fork decision reads the flags. const wizardFlags = await analytics.getAllFlagsForWizard(); // Gateway trace tags for this run. The runner stamps its variant onto this // after the fork (see runProgram), so the value reflects which arm ran. @@ -258,21 +254,11 @@ export async function bootstrapProgram( skillId: config.skillId, }); - // One MCP url for every region: the server resolves the user's region from - // the bearer token, so the EU subdomain (a Claude Code OAuth workaround) is - // not needed here. - const mcpUrl = session.localMcp - ? 'http://localhost:8787/mcp' - : runtimeEnv('MCP_URL') || 'https://mcp.posthog.com/mcp'; - + // Credentials (incl. the resolved host family and its MCP url) live on + // `session.credentials`; the arms read them from there. This carries only the + // run-scoped extras that aren't on the session. return { skillsBaseUrl, - projectApiKey, - host, - accessToken, - projectId, - cloudRegion, - mcpUrl, wizardFlags, wizardMetadata, project, diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index 9fa35e02..6dae3bfa 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -9,7 +9,6 @@ import type { } from '@lib/wizard-session'; import type { PromptContext } from '@lib/agent/agent-prompt'; import type { PackageManagerDetector } from '@lib/detection/package-manager'; -import type { CloudRegion } from '@utils/types'; import type { ApiProject } from '@lib/api'; export type { PromptContext, Credentials }; @@ -69,7 +68,6 @@ export interface ProgramRun { buildOutroData?: ( session: WizardSession, credentials: Credentials, - cloudRegion: CloudRegion | undefined, ) => WizardSession['outroData']; /** * Per-run cap on `wizard_ask` invocations. Defaults to 10. The 4th call @@ -102,17 +100,13 @@ export interface ProgramRun { /** * Result of the shared bootstrap, consumed by both the linear and the - * orchestrator arm. Credentials, role, and user are already applied to the - * session by `bootstrapProgram`; this carries the values both arms still need. + * orchestrator arm. Credentials (including the resolved host family and its MCP + * url), role, and user are already applied to the session by `bootstrapProgram` + * — the arms read those from `session.credentials`. This carries only the + * run-scoped extras that don't live on the session. */ export interface BootstrapResult { skillsBaseUrl: string; - projectApiKey: Credentials['projectApiKey']; - host: Credentials['host']; - accessToken: Credentials['accessToken']; - projectId: Credentials['projectId']; - cloudRegion: CloudRegion; - mcpUrl: string; wizardFlags: Record; wizardMetadata: Record; /** Full project payload, for project-level prompt context (opt-ins). */ diff --git a/src/lib/detection/agentic.ts b/src/lib/detection/agentic.ts index 6eb0890d..deb7a848 100644 --- a/src/lib/detection/agentic.ts +++ b/src/lib/detection/agentic.ts @@ -227,16 +227,12 @@ export async function detectProjectsWithAgent( const cwd = session.installDir; const runOptions = sessionToWizardOptions(session); - const mcpUrl = session.localMcp - ? 'http://localhost:8787/mcp' - : 'https://mcp.posthog.com/mcp'; - const agent = await initializeAgent( { workingDirectory: cwd, - posthogMcpUrl: mcpUrl, + posthogMcpUrl: host.mcpUrl, posthogApiKey: accessToken, - posthogApiHost: host, + host, detectPackageManager: detectNodePackageManagers, skillsBaseUrl: getSkillsBaseUrl(session.localMcp), integrationLabel: 'agentic-detect', diff --git a/src/lib/programs/__tests__/agent-skill.test.ts b/src/lib/programs/__tests__/agent-skill.test.ts index c6c31f0c..132586e1 100644 --- a/src/lib/programs/__tests__/agent-skill.test.ts +++ b/src/lib/programs/__tests__/agent-skill.test.ts @@ -5,6 +5,7 @@ import { } from '@lib/programs/agent-skill/index'; import type { ProgramRun } from '@lib/agent/agent-runner'; import { buildSession, RunPhase } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; const baseOpts: SkillProgramOptions = { skillId: 'error-tracking-setup', @@ -78,7 +79,7 @@ describe('AGENT_SKILL_STEPS', () => { session.credentials = { accessToken: 't', projectApiKey: 'k', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 1, }; expect(auth.isComplete!(session)).toBe(true); diff --git a/src/lib/programs/__tests__/self-driving-prompt.test.ts b/src/lib/programs/__tests__/self-driving-prompt.test.ts index c6975b8f..d239a51e 100644 --- a/src/lib/programs/__tests__/self-driving-prompt.test.ts +++ b/src/lib/programs/__tests__/self-driving-prompt.test.ts @@ -1,10 +1,11 @@ import { buildSelfDrivingPrompt } from '@lib/programs/self-driving/prompt'; import type { PromptContext } from '@lib/agent/agent-runner'; +import { HostResolution } from '@lib/host-resolution'; const ctx: PromptContext = { projectId: 123, projectApiKey: 'phc_test', - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), }; describe('buildSelfDrivingPrompt', () => { diff --git a/src/lib/programs/audit/index.ts b/src/lib/programs/audit/index.ts index 2964e0c0..b326b905 100644 --- a/src/lib/programs/audit/index.ts +++ b/src/lib/programs/audit/index.ts @@ -7,7 +7,6 @@ import type { ProgramRun } from '@lib/agent/agent-runner'; import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; -import { HostResolution } from '@lib/host-resolution'; import { AUDIT_ABORT_CASES } from './detect.js'; import { AUDIT_CHECKS_KEY, AUDIT_REPORT_FILE } from './types.js'; import { AUDIT_SEED_CHECKS, seedAuditLedger } from './seed.js'; @@ -68,16 +67,11 @@ const auditRun = async (session: WizardSession): Promise => { // Override the default outro so the dashboard + notebook URLs the // agent emits via `[DASHBOARD_URL]` / `[NOTEBOOK_URL]` are surfaced // on the post-run screen. - buildOutroData: (session, _credentials, cloudRegion) => { - // TODO: clean up in #755 - const cloudUrl = cloudRegion - ? HostResolution.fromRegion(cloudRegion, { baseUrl: session.baseUrl }) - .appHost + buildOutroData: (sess, credentials) => { + const cloudUrl = credentials.host.appHost; + const continueUrl = sess.signup + ? `${cloudUrl}/products?source=wizard` : undefined; - const continueUrl = - session.signup && cloudUrl - ? `${cloudUrl}/products?source=wizard` - : undefined; // Note: `sess` here is the agent-runner's snapshot of session at // runAgent() invocation time. Any URL emissions during the run land diff --git a/src/lib/programs/error-tracking-upload-source-maps/index.ts b/src/lib/programs/error-tracking-upload-source-maps/index.ts index 57c657bc..ad009d25 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/index.ts +++ b/src/lib/programs/error-tracking-upload-source-maps/index.ts @@ -13,7 +13,6 @@ import { type SkillVariant, } from './detect.js'; import { getContentBlocks } from './content/index.js'; -import { getUiHostFromHost } from '@utils/urls'; import { getUI } from '@ui'; const REPORT_FILE = 'posthog-source-maps-report.md'; @@ -75,7 +74,7 @@ export const errorTrackingUploadSourceMapsConfig: ProgramConfig = { return SOURCE_MAPS_DETECTION_FAILED_PROMPT; } - const uiHost = getUiHostFromHost(ctx.host).replace(/\/$/, ''); + const uiHost = ctx.host.appHost.replace(/\/$/, ''); return buildSourceMapsUploadPrompt({ displayName, @@ -83,7 +82,7 @@ export const errorTrackingUploadSourceMapsConfig: ProgramConfig = { skillId, projectPath, projectId: ctx.projectId, - host: ctx.host, + host: ctx.host.apiHost, settingsUrl: `${uiHost}/project/${ctx.projectId}/settings/user-api-keys`, uiHost, }); diff --git a/src/lib/programs/events-audit/index.ts b/src/lib/programs/events-audit/index.ts index ae704453..f6937b2b 100644 --- a/src/lib/programs/events-audit/index.ts +++ b/src/lib/programs/events-audit/index.ts @@ -4,7 +4,6 @@ import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; import { SPINNER_MESSAGE } from '@lib/framework-config'; import { isUsingTypeScript } from '@utils/setup-utils'; -import { HostResolution } from '@lib/host-resolution'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; import { EVENTS_AUDIT_PROGRAM } from './steps.js'; import { AUDIT_CHECKS_KEY } from '@lib/programs/audit/types'; @@ -63,19 +62,14 @@ Project context: - PostHog Project ID: ${ctx.projectId} - TypeScript: ${typeScriptDetected ? 'Yes' : 'No'} - PostHog public token: ${ctx.projectApiKey} -- PostHog Host: ${ctx.host} +- PostHog Host: ${ctx.host.apiHost} `, - buildOutroData: (sess, _credentials, cloudRegion) => { - // TODO: clean up in #755 - const cloudUrl = cloudRegion - ? HostResolution.fromRegion(cloudRegion, { baseUrl: sess.baseUrl }) - .appHost + buildOutroData: (sess, credentials) => { + const cloudUrl = credentials.host.appHost; + const continueUrl = sess.signup + ? `${cloudUrl}/products?source=wizard` : 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. diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index 88f2395d..635d51a3 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -15,10 +15,9 @@ import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { wizardAbort } from '@utils/wizard-abort'; import { WIZARD_INTERACTION_EVENT_NAME } from '@lib/constants'; import { getUI } from '@ui/index'; -import { HostResolution } from '@lib/host-resolution'; import { requestDeepLink } from '@utils/provisioning'; import { openTrackedLink, withUtm } from '@utils/links'; -import type { CloudRegion } from '@utils/types'; +import type { HostResolution } from '@lib/host-resolution'; import { POSTHOG_INTEGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from './content/index.js'; import { buildCodingAgentPrompt } from './handoff.js'; @@ -26,22 +25,13 @@ import { buildCodingAgentPrompt } from './handoff.js'; const DASHBOARD_DEEP_LINK_KEY = 'dashboardDeepLink'; function resolveContinueUrl( - session: WizardSession, - cloudRegion: CloudRegion | undefined, + sess: WizardSession, + host: HostResolution, deepLink: unknown, ): string | undefined { - if (!session.signup) return undefined; + if (!sess.signup) return undefined; if (typeof deepLink === 'string' && deepLink) return deepLink; - if (cloudRegion) - // TODO: clean up in #755 - return withUtm( - `${ - HostResolution.fromRegion(cloudRegion, { baseUrl: session.baseUrl }) - .appHost - }/products?source=wizard`, - 'outro-continue', - ); - return undefined; + return withUtm(`${host.appHost}/products?source=wizard`, 'outro-continue'); } export const SETUP_REPORT_FILE = 'posthog-setup-report.md'; @@ -168,7 +158,7 @@ Project context: - Framework: ${config.metadata.name} ${frameworkVersion || 'latest'} - TypeScript: ${typeScriptDetected ? 'Yes' : 'No'} - PostHog public token: ${ctx.projectApiKey} -- PostHog Host: ${ctx.host} +- PostHog Host: ${ctx.host.apiHost} - Project type: ${config.prompts.projectTypeDetection} - Package installation: ${ config.prompts.packageInstallation ?? DEFAULT_PACKAGE_INSTALLATION @@ -209,7 +199,7 @@ Important: Use the detect_package_manager tool (from the wizard-tools MCP server postRun: async (sess, credentials) => { const envVars = config.environment.getEnvVars( credentials.projectApiKey, - credentials.host, + credentials.host.apiHost, ); if (config.environment.uploadToHosting) { const { uploadEnvironmentVariablesStep } = await import( @@ -247,13 +237,17 @@ Important: Use the detect_package_manager tool (from the wizard-tools MCP server } }, - buildOutroData: (sess, credentials, cloudRegion) => { + buildOutroData: (sess, credentials) => { const envVars = config.environment.getEnvVars( credentials.projectApiKey, - credentials.host, + credentials.host.apiHost, ); const deepLink = sess.frameworkContext[DASHBOARD_DEEP_LINK_KEY]; - const continueUrl = resolveContinueUrl(sess, cloudRegion, deepLink); + const continueUrl = resolveContinueUrl( + sess, + credentials.host, + deepLink, + ); const changes = [ ...config.ui.getOutroChanges(frameworkContext), diff --git a/src/lib/programs/self-driving/index.ts b/src/lib/programs/self-driving/index.ts index 8d4ef165..87ab1587 100644 --- a/src/lib/programs/self-driving/index.ts +++ b/src/lib/programs/self-driving/index.ts @@ -3,7 +3,6 @@ import { access, rm } from 'node:fs/promises'; import type { ProgramConfig } from '@lib/programs/program-step'; import type { ProgramRun } from '@lib/agent/agent-runner'; import { OutroKind } from '@lib/wizard-session'; -import { getUiHostFromHost } from '@utils/urls'; import { createSkillProgram } from '../agent-skill/index.js'; import { getContentBlocks as agentSkillContentBlocks } from '../agent-skill/content/index.js'; import { SELF_DRIVING_PROGRAM } from './steps.js'; @@ -72,7 +71,7 @@ const run: ProgramRun = { }, buildOutroData: (_session, credentials) => { - const uiHost = getUiHostFromHost(credentials.host).replace(/\/$/, ''); + const uiHost = credentials.host.appHost.replace(/\/$/, ''); const inboxUrl = `${uiHost}/project/${credentials.projectId}/inbox`; return { kind: OutroKind.Success as const, diff --git a/src/lib/programs/self-driving/prompt.ts b/src/lib/programs/self-driving/prompt.ts index fd6e1b29..9a01ae7c 100644 --- a/src/lib/programs/self-driving/prompt.ts +++ b/src/lib/programs/self-driving/prompt.ts @@ -1,6 +1,5 @@ import { AgentSignals } from '@lib/agent/agent-interface'; import type { PromptContext } from '@lib/agent/agent-runner'; -import { getUiHostFromHost } from '@utils/urls'; /** * Build the self-driving run prompt. The installed @@ -14,7 +13,7 @@ import { getUiHostFromHost } from '@utils/urls'; * list — so this prompt only covers the Self-driving steps. */ export function buildSelfDrivingPrompt(ctx: PromptContext): string { - const uiHost = getUiHostFromHost(ctx.host).replace(/\/$/, ''); + const uiHost = ctx.host.appHost.replace(/\/$/, ''); const projectBase = `${uiHost}/project/${ctx.projectId}`; const integrationsSettingsUrl = `${projectBase}/settings/environment-integrations`; const orgAiSettingsUrl = `${uiHost}/settings/organization#organization-ai-consent`; diff --git a/src/lib/task-stream/__tests__/posthog-destination.test.ts b/src/lib/task-stream/__tests__/posthog-destination.test.ts index 266b18b3..3f594545 100644 --- a/src/lib/task-stream/__tests__/posthog-destination.test.ts +++ b/src/lib/task-stream/__tests__/posthog-destination.test.ts @@ -1,9 +1,10 @@ import { PostHogDestination } from '../destinations/posthog'; import { StreamEvent, type TaskStreamUpdate } from '../types'; import { RunPhase, type Credentials } from '../../wizard-session'; +import { HostResolution } from '../../host-resolution'; const SAMPLE_CREDS: Credentials = { - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), projectId: 42, accessToken: 'pha_abc', projectApiKey: 'phc_test', @@ -277,7 +278,7 @@ describe('PostHogDestination', () => { const dest = new PostHogDestination({ getCredentials: () => ({ ...SAMPLE_CREDS, - host: 'https://us.posthog.com/', + host: HostResolution.fromApiHost('https://us.posthog.com/'), }), fetchImpl, }); diff --git a/src/lib/task-stream/destinations/posthog.ts b/src/lib/task-stream/destinations/posthog.ts index affcf39e..f40e6c92 100644 --- a/src/lib/task-stream/destinations/posthog.ts +++ b/src/lib/task-stream/destinations/posthog.ts @@ -106,7 +106,7 @@ export class PostHogDestination implements TaskStreamDestination { creds: Credentials, body: object, ): { url: string; init: Parameters[1] } { - const url = `${creds.host.replace(/\/$/, '')}/api/projects/${ + const url = `${creds.host.apiHost.replace(/\/$/, '')}/api/projects/${ creds.projectId }/wizard/sessions/`; return { diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index ae9c277c..b19fc455 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -15,11 +15,13 @@ import type { FrameworkConfig } from './framework-config'; import type { WizardReadinessResult } from './health-checks/readiness'; import type { SettingsConflict } from './agent/claude-settings'; import type { ApiUser, ApiProject } from './api'; +import type { HostResolution } from './host-resolution'; export interface Credentials { accessToken: string; projectApiKey: string; - host: string; + /** Resolved at auth time and immutable thereafter — see {@link HostResolution}. */ + host: HostResolution; projectId: number; } @@ -234,11 +236,11 @@ export interface WizardSession { apiUser: ApiUser | null; /** - * Cloud region and project payload resolved at authentication, kept so a - * second agent run in the same invocation (e.g. self-driving's integration - * phase) reuses the first login wholesale instead of re-authenticating. + * Project payload resolved at authentication, kept so a second agent run in + * the same invocation (e.g. self-driving's integration phase) reuses the + * first login wholesale instead of re-authenticating. The resolved region + * lives on `credentials.host.region`. */ - cloudRegion: CloudRegion | null; apiProject: ApiProject | null; // Lifecycle @@ -394,7 +396,6 @@ export function buildSession(args: { credentials: null, roleAtOrganization: null, apiUser: null, - cloudRegion: null, apiProject: null, readinessResult: null, outageDismissed: false, diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts index 1d2230fc..b898a1d0 100644 --- a/src/ui/logging-ui.ts +++ b/src/ui/logging-ui.ts @@ -20,6 +20,7 @@ import { } from '@lib/health-checks/readiness'; import type { AskAnswers, + Credentials, OutroData, PendingQuestion, } from '@lib/wizard-session'; @@ -216,12 +217,7 @@ export class LoggingUI implements WizardUI { // No-op in CI mode } - setCredentials(_credentials: { - accessToken: string; - projectApiKey: string; - host: string; - projectId: number; - }): void { + setCredentials(_credentials: Credentials): void { // No-op in CI mode — credentials are handled directly } diff --git a/src/ui/tui/__tests__/router.test.ts b/src/ui/tui/__tests__/router.test.ts index a5c38bf3..cad6ef35 100644 --- a/src/ui/tui/__tests__/router.test.ts +++ b/src/ui/tui/__tests__/router.test.ts @@ -1,4 +1,5 @@ import { buildSession, McpOutcome, RunPhase } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; import { WizardReadiness } from '@lib/health-checks/readiness'; import { WizardRouter, ScreenId, Overlay, Program } from '@ui/tui/router'; import { Integration } from '@lib/constants'; @@ -25,7 +26,7 @@ describe('WizardRouter', () => { session.credentials = { accessToken: 'tok', projectApiKey: 'pk', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), projectId: 1, }; @@ -67,7 +68,7 @@ describe('WizardRouter', () => { session.credentials = { accessToken: 'tok', projectApiKey: 'pk', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), projectId: 1, }; session.runPhase = RunPhase.Completed; @@ -227,7 +228,7 @@ describe('WizardRouter', () => { session.credentials = { accessToken: 'tok', projectApiKey: 'pk', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), projectId: 1, }; return session; diff --git a/src/ui/tui/__tests__/store.test.ts b/src/ui/tui/__tests__/store.test.ts index 740f123c..2d750bf2 100644 --- a/src/ui/tui/__tests__/store.test.ts +++ b/src/ui/tui/__tests__/store.test.ts @@ -15,6 +15,7 @@ import { evaluateWizardReadiness, } from '@lib/health-checks/readiness'; import { buildSession } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; import { Integration } from '@lib/constants'; import { analytics } from '@utils/analytics'; @@ -187,7 +188,7 @@ describe('WizardStore', () => { const creds = { accessToken: 'tok', projectApiKey: 'pk', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), projectId: 42, }; store.setCredentials(creds); @@ -316,7 +317,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 42, }); expect(wizardCaptureMock).toHaveBeenCalledWith('auth complete', { @@ -412,7 +413,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 1, }); expect(store.currentScreen).toBe(ScreenId.Run); @@ -429,7 +430,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 1, }); store.setRunPhase(RunPhase.Completed); @@ -447,7 +448,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 1, }); store.setRunPhase(RunPhase.Completed); @@ -466,7 +467,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 1, }); store.setRunPhase(RunPhase.Completed); @@ -879,7 +880,7 @@ describe('WizardStore', () => { // -> settings-override (overlay still on top) accessToken: 'tok', projectApiKey: 'pk', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 1, }); store.popOverlay(); // -> health-check (readinessResult still null) @@ -1024,7 +1025,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 1, }); store.setRunPhase(RunPhase.Error); @@ -1079,7 +1080,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), projectId: 1, }); expect(store.currentScreen).toBe(ScreenId.Run); @@ -1128,7 +1129,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), projectId: 1, }); expect(store.currentScreen).toBe(ScreenId.Run); @@ -1166,7 +1167,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), projectId: 1, }); expect(store.currentScreen).toBe(ScreenId.Run); @@ -1280,7 +1281,7 @@ describe('WizardStore', () => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', - host: 'h', + host: HostResolution.fromApiHost('h'), projectId: 1, }); wizardCaptureMock.mockClear(); diff --git a/src/ui/tui/ink-ui.ts b/src/ui/tui/ink-ui.ts index 8ad61eb3..c3945e24 100644 --- a/src/ui/tui/ink-ui.ts +++ b/src/ui/tui/ink-ui.ts @@ -13,6 +13,7 @@ import type { WizardReadinessResult } from '@lib/health-checks/readiness'; import type { ApiUser } from '@lib/api'; import type { AskAnswers, + Credentials, OutroData, PendingQuestion, } from '@lib/wizard-session'; @@ -76,12 +77,7 @@ export class InkUI implements WizardUI { }); } - setCredentials(credentials: { - accessToken: string; - projectApiKey: string; - host: string; - projectId: number; - }): void { + setCredentials(credentials: Credentials): void { this.store.setCredentials(credentials); } diff --git a/src/ui/tui/playground/demos/AiOptInDemo.tsx b/src/ui/tui/playground/demos/AiOptInDemo.tsx index 6e899d46..97897e21 100644 --- a/src/ui/tui/playground/demos/AiOptInDemo.tsx +++ b/src/ui/tui/playground/demos/AiOptInDemo.tsx @@ -16,6 +16,7 @@ import { useEffect, useState } from 'react'; import { Box, Text } from 'ink'; import { WizardStore } from '@ui/tui/store'; import { AiOptInRequiredScreen } from '@ui/tui/screens/AiOptInRequiredScreen'; +import { HostResolution } from '@lib/host-resolution'; type Variant = 'admin' | 'non-admin'; @@ -29,7 +30,7 @@ export const AiOptInDemo = ({ variant }: AiOptInDemoProps) => { s.setCredentials({ accessToken: 'demo-fake-token', projectApiKey: 'demo-fake-project-key', - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), projectId: 0, }); return s; diff --git a/src/ui/tui/playground/demos/EndScreensDemo.tsx b/src/ui/tui/playground/demos/EndScreensDemo.tsx index 025b3850..7fc165ba 100644 --- a/src/ui/tui/playground/demos/EndScreensDemo.tsx +++ b/src/ui/tui/playground/demos/EndScreensDemo.tsx @@ -27,6 +27,7 @@ import { SlackConnectScreen } from '@ui/tui/screens/SlackConnectScreen'; import { OutroScreen } from '@ui/tui/screens/OutroScreen'; import { Colors } from '@ui/tui/styles'; import { OutroKind, type OutroData } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; const VIEWS = ['slack-connect', 'outro'] as const; type View = (typeof VIEWS)[number]; @@ -87,7 +88,7 @@ export const EndScreensDemo = ({ store }: EndScreensDemoProps) => { store.setCredentials({ accessToken: 'playground', projectApiKey: 'playground', - host: 'http://127.0.0.1:9', + host: HostResolution.fromApiHost('http://127.0.0.1:9'), projectId: 0, }); return () => { diff --git a/src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx b/src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx index a5336576..badf7ac2 100644 --- a/src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx +++ b/src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx @@ -30,6 +30,7 @@ import { McpSuggestedPromptsScreen } from '@ui/tui/screens/McpSuggestedPromptsSc import { Colors } from '@ui/tui/styles'; import { Integration } from '@lib/constants'; import { McpOutcome } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; import { TAILORED_ROLES } from '@lib/mcp-role-prompts'; import type { AgentChunk, @@ -156,7 +157,7 @@ function createMockServices( credentials: { accessToken: 'phx_mock', projectApiKey: 'phc_mock', - host: 'http://127.0.0.1:1', + host: HostResolution.fromApiHost('http://127.0.0.1:1'), projectId: 1, }, roleAtOrganization: cfg.role, diff --git a/src/ui/tui/playground/start-playground.ts b/src/ui/tui/playground/start-playground.ts index 051b9b86..1d59c495 100644 --- a/src/ui/tui/playground/start-playground.ts +++ b/src/ui/tui/playground/start-playground.ts @@ -6,6 +6,7 @@ import { render } from 'ink'; import { createElement } from 'react'; import { WizardStore } from '@ui/tui/store'; import { PlaygroundApp } from './PlaygroundApp.js'; +import { HostResolution } from '@lib/host-resolution'; import { WizardReadiness } from '@lib/health-checks/readiness'; import { enterDarkTerminal, releaseTerminal } from '../terminal.js'; @@ -28,7 +29,7 @@ export function startPlayground(version: string): void { store.setCredentials({ accessToken: 'fake', projectApiKey: 'fake', - host: 'https://app.posthog.com', + host: HostResolution.fromApiHost('https://app.posthog.com'), projectId: 0, }); diff --git a/src/ui/tui/screens/AiOptInRequiredScreen.tsx b/src/ui/tui/screens/AiOptInRequiredScreen.tsx index c1053f0f..d86063ee 100644 --- a/src/ui/tui/screens/AiOptInRequiredScreen.tsx +++ b/src/ui/tui/screens/AiOptInRequiredScreen.tsx @@ -98,8 +98,10 @@ export const AiOptInRequiredScreen = ({ } setRetrying(true); setRetryError(null); - // TODO: clean up in #755 - void fetchUserData(accessToken, HostResolution.fromRegion(region, { baseUrl: session.baseUrl }).appHost) + const cloudUrl = + session.credentials?.host.appHost ?? + HostResolution.fromRegion(region, { baseUrl: session.baseUrl }).appHost; + void fetchUserData(accessToken, cloudUrl) .then((user) => { store.setApiUser(user); }) diff --git a/src/ui/tui/screens/SlackConnectScreen.tsx b/src/ui/tui/screens/SlackConnectScreen.tsx index 9db8f74d..f0f4d99c 100644 --- a/src/ui/tui/screens/SlackConnectScreen.tsx +++ b/src/ui/tui/screens/SlackConnectScreen.tsx @@ -133,7 +133,7 @@ export const SlackConnectScreen = ({ store }: SlackConnectScreenProps) => { fetchSlackConnected( credentials.accessToken, credentials.projectId, - credentials.host, + credentials.host.apiHost, controller.signal, ) .then((isConnected) => { diff --git a/src/ui/tui/screens/doctor/DoctorReportScreen.tsx b/src/ui/tui/screens/doctor/DoctorReportScreen.tsx index 4c5d5fe1..eb4a982d 100644 --- a/src/ui/tui/screens/doctor/DoctorReportScreen.tsx +++ b/src/ui/tui/screens/doctor/DoctorReportScreen.tsx @@ -7,7 +7,6 @@ import { fetchHealthIssues, type HealthIssue, } from '@lib/programs/posthog-doctor/index'; -import { getUiHostFromHost } from '@utils/urls'; import { OutroKind } from '@lib/wizard-session'; import { ApiError } from '@lib/api'; import { POSTHOG_DOCS_URL } from '@lib/constants'; @@ -30,17 +29,17 @@ export const DoctorReportScreen = ({ store }: DoctorReportScreenProps) => { const { credentials } = store.session; const accessToken = credentials?.accessToken; - const host = credentials?.host; + const apiHost = credentials?.host.apiHost; const projectId = credentials?.projectId; const [state, setState] = useState({ kind: 'loading' }); useEffect(() => { - if (!accessToken || !host || projectId == null) return; + if (!accessToken || !apiHost || projectId == null) return; let cancelled = false; void (async () => { try { - const issues = await fetchHealthIssues(accessToken, host, projectId); + const issues = await fetchHealthIssues(accessToken, apiHost, projectId); if (!cancelled) { setState({ kind: 'ready', issues }); } @@ -59,7 +58,7 @@ export const DoctorReportScreen = ({ store }: DoctorReportScreenProps) => { return () => { cancelled = true; }; - }, [accessToken, host, projectId]); + }, [accessToken, apiHost, projectId]); if (!credentials) { return ; @@ -68,20 +67,24 @@ export const DoctorReportScreen = ({ store }: DoctorReportScreenProps) => { if (state.kind === 'loading') { return ( -
+
); } - const healthUrl = `${getUiHostFromHost(credentials.host)}/project/${ - credentials.projectId - }/health`; + const healthUrl = `${credentials.host.appHost}/project/${credentials.projectId}/health`; if (state.kind === 'error') { return ( -
+
{Icons.squareFilled} Failed to fetch health issues @@ -108,7 +111,10 @@ export const DoctorReportScreen = ({ store }: DoctorReportScreenProps) => { if (issues.length === 0) { return ( -
+
{Icons.check} No active issues — you're all set! @@ -131,7 +137,10 @@ export const DoctorReportScreen = ({ store }: DoctorReportScreenProps) => { return ( -
+
{formatSummaryLine(issues)} diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index a71d6f59..73ad24c3 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -11,6 +11,7 @@ import type { SettingsConflict } from '@lib/agent/claude-settings'; import type { WizardReadinessResult } from '@lib/health-checks/readiness'; import type { ApiUser } from '@lib/api'; +import type { Credentials } from '@lib/wizard-session'; import type { AskAnswers, OutroData, @@ -96,12 +97,7 @@ export interface WizardUI { startRun(): void; /** Store OAuth/API credentials. Resolves past AuthScreen in TUI. */ - setCredentials(credentials: { - accessToken: string; - projectApiKey: string; - host: string; - projectId: number; - }): void; + setCredentials(credentials: Credentials): void; /** * Persist the user's `role_at_organization` once it's been fetched from diff --git a/src/utils/__tests__/ci-region.test.ts b/src/utils/__tests__/ci-region.test.ts index 2769b215..3de124ad 100644 --- a/src/utils/__tests__/ci-region.test.ts +++ b/src/utils/__tests__/ci-region.test.ts @@ -62,7 +62,7 @@ describe('getOrAskForProjectData CI region', () => { 123, 'https://eu.posthog.com', ); - expect(result.cloudRegion).toBe('eu'); + expect(result.host.region).toBe('eu'); }); it('falls back to detection only when no region is provided', async () => { diff --git a/src/utils/provisioning.ts b/src/utils/provisioning.ts index 779b030a..e5c5b8c8 100644 --- a/src/utils/provisioning.ts +++ b/src/utils/provisioning.ts @@ -19,6 +19,7 @@ import { import { resolveBaseUrl } from './urls'; import { logToFile } from './debug'; import { analytics } from './analytics'; +import type { HostResolution } from '@lib/host-resolution'; const API_VERSION = '0.1d'; @@ -245,13 +246,11 @@ export async function provisionNewAccount( */ export async function requestDeepLink( accessToken: string, - host: string, + host: HostResolution, opts?: { purpose?: string; path?: string }, ): Promise { try { - const baseUrl = host - .replace('us.i.posthog.com', 'us.posthog.com') - .replace('eu.i.posthog.com', 'eu.posthog.com'); + const baseUrl = host.appHost; const res = await axios.post( `${baseUrl}/api/agentic/provisioning/deep_links`, diff --git a/src/utils/setup-utils.ts b/src/utils/setup-utils.ts index d6234800..69eb163a 100644 --- a/src/utils/setup-utils.ts +++ b/src/utils/setup-utils.ts @@ -14,11 +14,7 @@ import { } from './package-manager'; import type { CloudRegion, WizardRunOptions } from './types'; import { getDeclaredVersion } from './package-json'; -import { - DEFAULT_HOST_URL, - DUMMY_PROJECT_API_KEY, - ISSUES_URL, -} from '@lib/constants'; +import { DUMMY_PROJECT_API_KEY, ISSUES_URL } from '@lib/constants'; import { getOAuthScopesForProgram } from '@lib/oauth/program-scopes'; import type { ProgramId } from '@lib/programs/program-registry'; import { analytics } from './analytics'; @@ -39,7 +35,7 @@ import { wizardAbort } from './wizard-abort'; interface ProjectData { projectApiKey: string; accessToken: string; - host: string; + host: HostResolution; distinctId: string; projectId: number; /** @@ -400,17 +396,18 @@ export async function getOrAskForProjectData( /** Explicit base URL override (`--base-url`, from `session.baseUrl`). When * set, pins every PostHog origin and bypasses region resolution. */ baseUrl?: string; + /** `--local-mcp`: forwarded into the resolved host so `host.mcpUrl` is local. */ + localMcp?: boolean; /** Optional — picks the OAuth scope set via * `getOAuthScopesForProgram`. Omitted → default * `WIZARD_OAUTH_SCOPES`. Threaded into `askForWizardLogin`. */ programId?: ProgramId | null; }, ): Promise<{ - host: string; + host: HostResolution; projectApiKey: string; accessToken: string; projectId: number; - cloudRegion: CloudRegion; roleAtOrganization: string | null; user: ApiUser | null; project: ApiProject | null; @@ -419,14 +416,12 @@ export async function getOrAskForProjectData( if (_options.ci && _options.apiKey) { getUI().log.info('Using provided API key (CI mode - OAuth bypassed)'); - const hosts = await HostResolution.fromAccessToken(_options.apiKey, { + const host = await HostResolution.fromAccessToken(_options.apiKey, { region: _options.region, + localMcp: _options.localMcp, baseUrl: _options.baseUrl, }); - - const cloudRegion = hosts.region; - const host = hosts.apiHost; - const cloudUrl = hosts.appHost; + const cloudUrl = host.appHost; const projectData = _options.projectId != null @@ -455,7 +450,6 @@ export async function getOrAskForProjectData( projectApiKey: projectData.api_token, accessToken: _options.apiKey, projectId: projectData.id, - cloudRegion, roleAtOrganization, user, project: projectData.project, @@ -467,7 +461,6 @@ export async function getOrAskForProjectData( projectApiKey, accessToken, projectId, - cloudRegion, roleAtOrganization, user, project, @@ -479,14 +472,12 @@ export async function getOrAskForProjectData( baseUrl: _options.baseUrl, programId: _options.programId, projectId: _options.projectId, + localMcp: _options.localMcp, }), ); if (!projectApiKey) { - // TODO: clean up in #755 - const cloudUrl = HostResolution.fromRegion(cloudRegion, { - baseUrl: _options.baseUrl, - }).appHost; + const cloudUrl = host.appHost; getUI().log.error(`Didn't receive a project token. This shouldn't happen :( Please let us know if you think this is a bug in the wizard: @@ -500,10 +491,9 @@ ${cloudUrl}/settings/project#variables`); return { accessToken, - host: host || DEFAULT_HOST_URL, + host, projectApiKey: projectApiKey || DUMMY_PROJECT_API_KEY, projectId, - cloudRegion, roleAtOrganization: roleAtOrganization ?? null, user: user ?? null, project: project ?? null, @@ -556,12 +546,15 @@ async function askForWizardLogin(options: { /** `--project-id`, if passed. When the user granted access to it on the consent * screen we use it directly; otherwise we fall back to the first granted team. */ projectId?: number; -}): Promise { + /** `--local-mcp`: forwarded into the resolved host so `host.mcpUrl` is local. */ + localMcp?: boolean; +}): Promise { if (options.signup) { return askForProvisioningSignup( options.email, options.region, options.baseUrl, + options.localMcp, ); } @@ -607,13 +600,14 @@ async function askForWizardLogin(options: { await abort(); } - const hosts = await HostResolution.fromAccessToken( + const host = await HostResolution.fromAccessToken( tokenResponse.access_token, - { baseUrl: options.baseUrl }, + { + localMcp: options.localMcp, + baseUrl: options.baseUrl, + }, ); - const cloudRegion = hosts.region; - const cloudUrl = hosts.appHost; - const host = hosts.apiHost; + const cloudUrl = host.appHost; const projectData = await fetchProjectData( tokenResponse.access_token, @@ -628,7 +622,6 @@ async function askForWizardLogin(options: { host, distinctId: userData.distinct_id, projectId: projectId!, - cloudRegion, roleAtOrganization: userData.role_at_organization ?? null, user: userData, project: projectData, @@ -645,7 +638,8 @@ async function askForProvisioningSignup( email?: string, region?: CloudRegion, baseUrl?: string, -): Promise { + localMcp?: boolean, +): Promise { if (!email || !email.includes('@')) { getUI().log.error( 'Email is required for signup. Use --email your@email.com with --signup.', @@ -669,8 +663,7 @@ async function askForProvisioningSignup( spinner.stop('Account created!'); getUI().log.success('Welcome to PostHog!'); - const host = result.host; - const cloudRegion: CloudRegion = host.includes('eu.') ? 'eu' : 'us'; + const host = HostResolution.fromApiHost(result.host, { localMcp }); analytics.setTag('provisioning-signup', true); @@ -680,7 +673,6 @@ async function askForProvisioningSignup( host, distinctId: email, projectId: parseInt(result.projectId, 10) || 0, - cloudRegion, }; } catch (error) { spinner.stop('Account creation failed.'); @@ -691,7 +683,7 @@ async function askForProvisioningSignup( 'This email already has a PostHog account. Switching to login flow...', ); - return askForWizardLogin({ signup: false, baseUrl }); + return askForWizardLogin({ signup: false, baseUrl, localMcp }); } getUI().log.error(`Failed to create account: ${message}`);