Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ wizard alongside all of our other PostHog product data, and this is very
powerful. For example: we could show in-product surveys to people who have used
the wizard to improve the experience.

When the user authenticates, the wizard also streams live run state — current
phase, task list, planned events — to `POST /api/projects/{id}/wizard_sessions/`
so the PostHog web app can render real-time progress. Updates are debounced
(250ms) with phase changes flushed immediately; failures fall back silently to
the wizard's debug log without disturbing the TUI. Pass `--no-telemetry` (or
set `POSTHOG_WIZARD_NO_TELEMETRY=1`) to disable.

## Leave rules behind

Supporting agent sessions after we leave is important. There are plenty of ways
Expand Down
65 changes: 60 additions & 5 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { VERSION } from './src/lib/version.js';

Check warning on line 8 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
const WIZARD_VERSION = VERSION;

const NODE_VERSION_RANGE = '>=18.17.0';
Expand Down Expand Up @@ -126,6 +126,12 @@
'Email address for signup (used with --signup)\nenv: POSTHOG_WIZARD_EMAIL',
type: 'string',
},
'no-telemetry': {
default: false,
describe:
'Disable pushing wizard run state to PostHog\nenv: POSTHOG_WIZARD_NO_TELEMETRY',
type: 'boolean',
},
})
.command(
['$0'],
Expand Down Expand Up @@ -361,7 +367,7 @@
const { startPlayground } = await import(
'./src/ui/tui/playground/start-playground.js'
);
(startPlayground as (version: string) => void)(WIZARD_VERSION);

Check warning on line 370 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
})();
} else if (options.skill) {
// Run a specific skill by ID
Expand Down Expand Up @@ -441,7 +447,7 @@
);

const { Flow } = await import('./src/ui/tui/router.js');
const tui = startTUI(WIZARD_VERSION, Flow.McpAdd);

Check warning on line 450 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
const session = buildSession({
debug: options.debug,
localMcp: options.local,
Expand Down Expand Up @@ -487,7 +493,7 @@
);

const { Flow } = await import('./src/ui/tui/router.js');
const tui = startTUI(WIZARD_VERSION, Flow.McpRemove);

Check warning on line 496 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
const session = buildSession({
debug: options.debug,
localMcp: options.local,
Expand Down Expand Up @@ -597,7 +603,7 @@
// ── Skill-based workflow subcommands (derived from registry) ─────────
for (const wfConfig of getSubcommandWorkflows()) {
cli.command(
wfConfig.command!,

Check warning on line 606 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
wfConfig.description,
(y) => y.options(skillSubcommandOptions),
(argv) => {
Expand Down Expand Up @@ -632,7 +638,9 @@
const installDir = (options.installDir as string) || process.cwd();

const { startTUI } = await import('./src/ui/tui/start-tui.js');
const { buildSession } = await import('./src/lib/wizard-session.js');
const { buildSession, RunPhase } = await import(
'./src/lib/wizard-session.js'
);
const { TaskStreamPush } = await import('./src/lib/task-stream/index.js');
const { FileDestination } = await import(
'./src/lib/task-stream/destinations/file.js'
Expand All @@ -641,8 +649,11 @@
'./src/lib/task-stream/destinations/posthog.js'
);
const { analytics } = await import('./src/utils/analytics.js');
const { logToFile } = await import('./src/utils/debug.js');
type AnyDestination =
import('./src/lib/task-stream/types.js').TaskStreamDestination;

const tui = startTUI(WIZARD_VERSION, config.flowKey as any);

Check warning on line 656 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

Check warning on line 656 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `Flow`

Check warning on line 656 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`

const session = buildSession({
debug: options.debug as boolean | undefined,
Expand All @@ -655,9 +666,10 @@
projectId: options.projectId as string | undefined,
email: options.email as string | undefined,
menu: options.menu as boolean | undefined,
integration: options.integration as any,

Check warning on line 669 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

Check warning on line 669 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
benchmark: options.benchmark as boolean | undefined,
yaraReport: options.yaraReport as boolean | undefined,
noTelemetry: options.noTelemetry as boolean | undefined,
});
session.workflowLabel = config.flowKey;
if (options.skillId) {
Expand All @@ -666,13 +678,53 @@

tui.store.session = session;

// Task stream — pushes state to external consumers on task changes
// Task stream — subscribes to store changes and pushes run state
// to external consumers (file log + PostHog backend). The PostHog
// destination is omitted when --no-telemetry is set so no HTTP
// request is ever issued.
const taskStreamEnabled = !session.noTelemetry;
const destinations: AnyDestination[] = [new FileDestination()];
if (taskStreamEnabled) {
destinations.push(
new PostHogDestination({
getCredentials: () => {
const creds = tui.store.session.credentials;
if (!creds) return null;
return {
host: creds.host,
projectId: creds.projectId,
auth: { kind: 'oauth_session', token: creds.accessToken },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the our Wizard session is a bit of a mess rn (sorry we need spring cleaning :kek:), but this should be getting credentials from the Wizard session (in wizard-session.ts). This is where it's stored already I'm pretty sure.

We also don't need to care if it's oauth_session or another type. The bearer token shape we send through the API is identical.

};
},
onError: (err) => logToFile('[task-stream-push]', err.message),
}),
);
}
const taskStream = new TaskStreamPush({
store: tui.store,
workflowId: config.flowKey,
destinations: [new FileDestination(), new PostHogDestination()],
destinations,
enabled: taskStreamEnabled,
});
tui.store.onTasksChanged = () => void taskStream.push();
taskStream.attach();

// Flush a terminal-phase push on Ctrl-C so the web app sees the
// run ended in error rather than hanging on the last "running"
// snapshot. A second signal during shutdown still exits normally
// because process.exit is the last line.
let signalled = false;
const onSignal = (): void => {
if (signalled) return;
signalled = true;
if (tui.store.session.runPhase === RunPhase.Running) {
tui.store.setRunPhase(RunPhase.Error);
}
void taskStream.shutdown(2000).finally(() => {
process.exit(130);
});
};
process.on('SIGINT', onSignal);
process.on('SIGTERM', onSignal);

await tui.store.runReadyHooks();
await tui.store.getGate('intro');
Expand Down Expand Up @@ -721,10 +773,12 @@
});

try {
await taskStream.dispose();
await taskStream.shutdown(2000);
} catch (error) {
analytics.captureException(error as Error);
}
process.off('SIGINT', onSignal);
process.off('SIGTERM', onSignal);
tui.unmount();
process.exit(0);
} catch (err) {
Expand Down Expand Up @@ -798,6 +852,7 @@
projectId: options.projectId as string | undefined,
benchmark: options.benchmark as boolean | undefined,
yaraReport: options.yaraReport as boolean | undefined,
noTelemetry: options.noTelemetry as boolean | undefined,
...env,
});
session.workflowLabel = config.flowKey;
Expand Down
8 changes: 5 additions & 3 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,17 @@ export const WIZARD_PROVISIONING_SCOPES = [

/**
* Scopes the wizard requests during the OAuth login flow. Superset of
* `WIZARD_PROVISIONING_SCOPES` with two scopes that only apply to the login
* `WIZARD_PROVISIONING_SCOPES` with scopes that only apply to the login
* path and are not in the provisioning allowlist:
* - introspection lets the wizard introspect its own token
* - health_issue:read used by `wizard doctor`
* - introspection lets the wizard introspect its own token
* - health_issue:read used by `wizard doctor`
* - wizard_session:write stream run state to /api/projects/{id}/wizard_sessions/
*/
export const WIZARD_OAUTH_SCOPES = [
...WIZARD_PROVISIONING_SCOPES,
'introspection',
'health_issue:read',
'wizard_session:write',
] as const;

// ── Wizard run / variants ───────────────────────────────────────────
Expand Down
Loading
Loading