Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 0 additions & 2 deletions .prettierignore

This file was deleted.

54 changes: 2 additions & 52 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,40 +49,7 @@ if (!satisfies(process.version, NODE_VERSION_RANGE)) {
process.exit(1);
}

// Test mock server — only loaded when NODE_ENV is 'test'.
// In production builds, tsdown replaces process.env.NODE_ENV with 'production',
// making this block dead code.
if (process.env.NODE_ENV === 'test') {
void (async () => {
try {
const { server } = await import('./e2e-tests/mocks/server.js');
server.listen({
onUnhandledRequest: 'bypass',
});
} catch (error) {
// Mock server import failed - this can happen during non-E2E tests
}
})();
}

import { Wizard } from './src/cli/wizard';
import { basicIntegrationCommand } from './src/cli/commands/basic-integration';
import { mcpCommand } from './src/cli/commands/mcp';
import { mcpAnalyticsCommand } from './src/commands/mcp-analytics';
import { replayVisionCommand } from './src/commands/replay-vision';
import { aiObservabilityCommand } from './src/commands/ai-observability';
import { metricsCommand } from './src/commands/metrics';
import { auditCommand } from './src/cli/commands/audit';
import { doctorCommand } from './src/commands/doctor';
import { migrateCommand } from './src/commands/migrate';
import { revenueCommand } from './src/commands/revenue';
import { warehouseCommand } from './src/commands/warehouse';
import { selfDrivingCommand } from './src/cli/commands/self-driving';
import { slackCommand } from './src/cli/commands/slack';
import { uploadSourcemapsCommand } from './src/commands/upload-sourcemaps';
import { errorTrackingCommand } from './src/commands/error-tracking';
import { skillCommand } from './src/cli/commands/skill';
import { cliCommand } from './src/cli/commands/cli';
import { runCli } from '@cli';
import { recoverOrphanedSettingsBackups } from '@shared/claude-settings';

// Heal any .claude/settings backup a previous interrupted run left orphaned,
Expand All @@ -100,21 +67,4 @@ function resolveInstallDir(): string {
return process.env.POSTHOG_WIZARD_INSTALL_DIR ?? process.cwd();
}

Wizard.use(basicIntegrationCommand)
.use(mcpCommand)
.use(mcpAnalyticsCommand)
.use(replayVisionCommand)
.use(aiObservabilityCommand)
.use(metricsCommand)
.use(cliCommand)
.use(auditCommand)
.use(doctorCommand)
.use(migrateCommand)
.use(revenueCommand)
.use(warehouseCommand)
.use(selfDrivingCommand)
.use(slackCommand)
.use(uploadSourcemapsCommand)
.use(errorTrackingCommand)
.use(skillCommand)
.init();
runCli();
57 changes: 26 additions & 31 deletions docs/examples/run-agent-quack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,34 @@
// Needs local PostHog on :8010 (with its ai-gateway) and context-mill on :8765.
// POSTHOG_PERSONAL_API_KEY logs in. WIZARD_CI_GATEWAY_TOKEN_FILE holds the gateway token.
// QUACK_INSTALL_DIR sets the project the agent runs in (default: the current directory).
import {
configureGatewayFromCIEnvironment,
runAgent,
RunOutcome,
} from '@agent';
import { runAgent, RunOutcome } from '@agent';
import type { RunConfig, RunInput } from '@agent/types';
import {
Harness,
HAIKU_MODEL,
Sequence,
getSkillsBaseUrl,
} from '@shared/constants';
import { fetchProjectData, fetchUserData } from '@shared/api';
import { readCiGatewayCredential } from '@shared/ci-gateway';
import { HostResolution } from '@shared/host-resolution';
import { initLocalDev, POSTHOG_LOCAL_URL } from '@shared/local-dev';
import { getOrAskForProjectData } from '@utils/setup-utils';

// Point PostHog, skills and MCP at the local stack, like --local-posthog --local-context-mill --local-mcp.
initLocalDev({ localPosthog: true, localContextMill: true, localMcp: true });

// Log in with keys instead of the browser, the same way --ci does.
// Log in with a personal API key instead of the browser: the host, the user, then the key's current project.
const apiKey = process.env.POSTHOG_PERSONAL_API_KEY;
if (!apiKey) throw new Error('Set POSTHOG_PERSONAL_API_KEY');
const programId = 'posthog-integration'; // a program the local gateway admits
const login = await getOrAskForProjectData({
signup: false,
ci: true, // with apiKey, this skips OAuth
apiKey,
const host = await HostResolution.fromAccessToken(apiKey, {
baseUrl: POSTHOG_LOCAL_URL,
localMcp: true,
programId,
});
// Use the token in WIZARD_CI_GATEWAY_TOKEN_FILE at WIZARD_CI_GATEWAY_URL instead of minting one.
configureGatewayFromCIEnvironment(login.projectId, 'us');
const apiUser = await fetchUserData(apiKey, host.appHost);
const projectId = apiUser.team?.id;
if (!projectId) throw new Error('The API key has no current project');
const project = await fetchProjectData(apiKey, projectId, host.appHost);

// What the agent runs: one prompt, a small model, no Write, Edit or Bash.
const config: RunConfig = {
Expand All @@ -53,35 +49,34 @@ const config: RunConfig = {
reportFile: '',
docsUrl: 'https://posthog.com/docs',
},
composed: true, // a sub-run: no terminal outro
// runAgent doesn't resolve a route. Linear on the Anthropic harness keeps the transcript.
binding: {
sequence: Sequence.linear,
harness: Harness.anthropic,
model: HAIKU_MODEL,
composed: false, // a top-level run: the agent writes its own outro
// Linear on the Anthropic harness keeps the transcript. With no flags, the agent runs this binding as is.
routing: {
binding: {
sequence: Sequence.linear,
harness: Harness.anthropic,
model: HAIKU_MODEL,
},
},
switchboard: { program: programId, composed: true, flags: {} },
skillsBaseUrl: getSkillsBaseUrl(),
wizardFlags: {},
wizardFlagPayloads: {},
wizardMetadata: {},
disallowedTools: ['Write', 'Edit', 'Bash'],
};

// Where and as whom: the project, the login and the flags.
const input: RunInput = {
installDir: process.env.QUACK_INSTALL_DIR ?? process.cwd(),
// Use the token in WIZARD_CI_GATEWAY_TOKEN_FILE at WIZARD_CI_GATEWAY_URL instead of minting one.
credentials: {
accessToken: login.accessToken,
refreshToken: login.refreshToken,
expiresAt: login.expiresAt,
projectApiKey: login.projectApiKey,
host: login.host,
projectId: login.projectId,
missingScopes: login.missingScopes,
accessToken: apiKey,
projectApiKey: project.api_token,
host,
projectId: project.id,
gateway: readCiGatewayCredential('us'),
},
project: login.project,
apiUser: login.user,
project,
apiUser,
flags: {
ci: false,
signup: false,
Expand Down
103 changes: 52 additions & 51 deletions docs/examples/run-program-quack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
// Needs local PostHog on :8010 (with its ai-gateway) and context-mill on :8765.
// POSTHOG_PERSONAL_API_KEY logs in. WIZARD_CI_GATEWAY_TOKEN_FILE holds the gateway token.
// QUACK_INSTALL_DIR sets the project the agent runs in (default: the current directory).
import { configureGatewayFromCIEnvironment, RunOutcome } from '@agent';
import { runProgram } from '@programs';
import {
buildSession,
resolveApiKeyLogin,
RunOutcome,
runProgram,
SessionStore,
} from '@programs';
import type { ProgramProgress } from '@programs/types';
import { Harness, HAIKU_MODEL, Sequence } from '@shared/constants';
import { readCiGatewayCredential } from '@shared/ci-gateway';
import { initLocalDev, POSTHOG_LOCAL_URL } from '@shared/local-dev';
import { getOrAskForProjectData } from '@utils/setup-utils';

// Point PostHog, skills and MCP at the local stack, like --local-posthog --local-context-mill --local-mcp.
initLocalDev({ localPosthog: true, localContextMill: true, localMcp: true });
Expand All @@ -20,77 +25,73 @@ initLocalDev({ localPosthog: true, localContextMill: true, localMcp: true });
const apiKey = process.env.POSTHOG_PERSONAL_API_KEY;
if (!apiKey) throw new Error('Set POSTHOG_PERSONAL_API_KEY');
const programId = 'posthog-integration'; // a program the local gateway admits
const login = await getOrAskForProjectData({
signup: false,
ci: true, // with apiKey, this skips OAuth
apiKey,
const login = await resolveApiKeyLogin(apiKey, {
baseUrl: POSTHOG_LOCAL_URL,
localMcp: true,
programId,
onWarning: (message) => console.warn(message),
});
// Use the token in WIZARD_CI_GATEWAY_TOKEN_FILE at WIZARD_CI_GATEWAY_URL instead of minting one.
configureGatewayFromCIEnvironment(login.projectId, 'us');
login.posthog.gateway = readCiGatewayCredential('us');

// Log status lines as the program reports them. runProgram never waits for this.
function logProgress(progress: ProgramProgress): void {
if (progress.kind === 'program') return; // a data snapshot; it holds tokens, don't log it
const { event } = progress;
function logProgress({ event }: ProgramProgress): void {
if (event.kind === 'status') console.log(`status: ${event.message}`);
if (event.kind === 'lifecycle') console.log(`lifecycle: ${event.phase}`);
}

// You own the session store: runProgram reads the launch values from it and writes the run into it.
const store = new SessionStore(
buildSession({
installDir: process.env.QUACK_INSTALL_DIR ?? process.cwd(),
baseUrl: POSTHOG_LOCAL_URL,
localMcp: true,
// A small model on the linear Anthropic route, where the transcript is kept.
sequence: Sequence.linear,
harness: Harness.anthropic,
model: HAIKU_MODEL,
}),
);
// The quack prompt reads nothing from the project, so skip the program's detection.
store.setDetectionComplete();

const result = await runProgram(
programId,
{
installDir: process.env.QUACK_INSTALL_DIR ?? process.cwd(),
// A caller-built run in place of the program's own: one prompt, and keep the reply.
run: {
integrationLabel: 'quack',
prompt: () => 'Reply with the single word quack. Use no tools.',
collectTranscript: true, // keep the agent's output for the reply below
requestRemark: false, // no closing remark
spinnerMessage: 'Quacking...',
successMessage: 'Quacked',
estimatedDurationMinutes: 1,
reportFile: '',
docsUrl: 'https://posthog.com/docs',
},
program: { disallowedTools: ['Write', 'Edit', 'Bash'] },
// The login from above, so runProgram skips its own login step.
credentials: {
posthog: {
accessToken: login.accessToken,
refreshToken: login.refreshToken,
expiresAt: login.expiresAt,
projectApiKey: login.projectApiKey,
host: login.host,
projectId: login.projectId,
missingScopes: login.missingScopes,
store,
// Laid over the program's own config: one prompt, keep the reply, no health check.
config: {
run: {
integrationLabel: 'quack',
prompt: () => 'Reply with the single word quack. Use no tools.',
collectTranscript: true, // keep the agent's output for the reply below
requestRemark: false, // no closing remark
spinnerMessage: 'Quacking...',
successMessage: 'Quacked',
estimatedDurationMinutes: 1,
reportFile: '',
docsUrl: 'https://posthog.com/docs',
},
project: login.project,
apiUser: login.user,
disallowedTools: ['Write', 'Edit', 'Bash'],
healthCheck: false,
},
composed: true, // a sub-run: no terminal outro
// A small model on the linear Anthropic route, where the transcript is kept.
overrides: {
sequence: Sequence.linear,
harness: Harness.anthropic,
model: HAIKU_MODEL,
},
flags: { localMcp: true },
host: { baseUrl: POSTHOG_LOCAL_URL },
credentials: login, // the login from above, so runProgram skips its own login step
wizardFlags: {}, // no flag snapshot to load
},
{
// You approved AI data processing for this local test user.
awaitAiApproval: () => Promise.resolve(true),
// You approved AI data processing for this local test user, and the run
// goes ahead; an outage or an unfixable settings conflict stops it.
workflow: {
confirmStep: (step) =>
Promise.resolve(step.kind === 'ai-approval' || step.kind === 'run'),
},
onProgress: logProgress,
},
);

// Endings resolve to an outcome. The agent's reply is in its settled run's transcript.
const reply = result.settledRuns[0]?.result.snapshot.transcriptTail ?? '';
// Endings resolve to an outcome and settle the store. The agent's reply is in its run's transcript.
const reply = result.runResults[0]?.snapshot.transcriptTail ?? '';
console.log(`reply: ${reply}`);
console.log(`phase: ${store.session.runPhase}`);
console.log(`outcome: ${result.outcome}`);
if (result.failure) console.log(`failure: ${result.failure.message}`);
process.exit(result.outcome === RunOutcome.Success ? 0 : 1);
Loading
Loading