@@ -266,15 +266,59 @@ After setup, run 'claude' as usual. AXME tools are available automatically.`);
266266}
267267
268268async function main ( ) {
269+ // Send anonymous startup telemetry only for user-facing CLI commands.
270+ // Hook and audit-session subcommands run as short-lived subprocesses
271+ // (Claude Code hooks fire many times per session, audit-session is a
272+ // detached background worker), so firing telemetry per invocation would
273+ // spam the endpoint and skew startup counts. The `serve` command sends
274+ // its own startup event from server.ts after MCP server is up.
275+ // We AWAIT this so events flush before heavy work begins — under event
276+ // loop pressure (LLM scanners), fire-and-forget setImmediate may stall.
277+ const startupCommands = new Set ( [ "setup" , "status" , "stats" , "audit-kb" , "cleanup" , "help" ] ) ;
278+ if ( command && startupCommands . has ( command ) ) {
279+ const { sendStartupEvents } = await import ( "./telemetry.js" ) ;
280+ await sendStartupEvents ( ) ;
281+ }
282+
269283 switch ( command ) {
270284 case "setup" : {
285+ const setupStartMs = Date . now ( ) ;
271286 const forceSetup = args . includes ( "--force" ) ;
272287 const pluginMode = args . includes ( "--plugin" ) || ! ! process . env . CLAUDE_PLUGIN_ROOT ;
273288 const setupArgs = args . filter ( a => a !== "--force" && a !== "--plugin" ) ;
274289 const projectPath = resolve ( setupArgs [ 1 ] || "." ) ;
275290 const hasGitDir = existsSync ( join ( projectPath , ".git" ) ) ;
276291 const ws = detectWorkspace ( projectPath ) ;
277292 const isWorkspace = hasGitDir ? false : ws . type !== "single" ;
293+ const childRepos = isWorkspace
294+ ? ws . projects . filter ( p => existsSync ( join ( projectPath , p . path , ".git" ) ) ) . length
295+ : 0 ;
296+ // Telemetry-relevant fields, populated as setup progresses
297+ let setupOutcome : "success" | "fallback" | "failed" = "failed" ;
298+ let setupMethod : "llm" | "deterministic" = "deterministic" ;
299+ let setupPhaseFailed : string | null = null ;
300+ let setupPresetsApplied = 0 ;
301+ let setupScannersRun = 0 ;
302+ let setupScannersFailed = 0 ;
303+ // Use the blocking variant so the event lands BEFORE process.exit() runs.
304+ // The fire-and-forget sendTelemetry uses setImmediate, which is killed
305+ // by process.exit() before the network request is even started.
306+ const sendSetupTelemetry = async ( ) => {
307+ try {
308+ const { sendTelemetryBlocking } = await import ( "./telemetry.js" ) ;
309+ await sendTelemetryBlocking ( "setup_complete" , {
310+ outcome : setupOutcome ,
311+ duration_ms : Date . now ( ) - setupStartMs ,
312+ method : setupMethod ,
313+ scanners_run : setupScannersRun ,
314+ scanners_failed : setupScannersFailed ,
315+ phase_failed : setupPhaseFailed ,
316+ presets_applied : setupPresetsApplied ,
317+ is_workspace : isWorkspace ,
318+ child_repos : childRepos ,
319+ } ) ;
320+ } catch { /* swallow */ }
321+ } ;
278322
279323 if ( isWorkspace ) {
280324 console . log ( `Initializing AXME Code workspace in ${ projectPath } (${ ws . type } , ${ ws . projects . length } projects)...` ) ;
@@ -289,36 +333,60 @@ async function main() {
289333 console . error ( `To authenticate, run one of:` ) ;
290334 console . error ( ` claude login (Claude subscription)` ) ;
291335 console . error ( ` export ANTHROPIC_API_KEY=sk-ant-... (API key)\n` ) ;
336+ setupOutcome = "failed" ;
337+ setupPhaseFailed = "auth_check" ;
338+ await sendSetupTelemetry ( ) ;
292339 process . exit ( 1 ) ;
293340 }
294341
295342 // Init with LLM scanners (parallel)
296- if ( isWorkspace ) {
297- const { workspaceResult, projectResults } = await initWorkspaceWithLLM ( projectPath , { onProgress : console . log } ) ;
298- const totalCost = workspaceResult . cost . costUsd + projectResults . reduce ( ( s , r ) => s + r . cost . costUsd , 0 ) ;
299- console . log ( ` Workspace: ${ workspaceResult . decisions . count } decisions, ${ workspaceResult . memories . count } memories` ) ;
300- for ( const r of projectResults ) {
301- const name = r . projectPath . split ( "/" ) . pop ( ) ;
302- console . log ( ` ${ name } : ${ r . decisions . count } decisions (${ r . decisions . fromScan } LLM + ${ r . decisions . fromPresets } presets)` ) ;
303- }
304- if ( totalCost > 0 ) console . log ( ` Total cost: $${ totalCost . toFixed ( 2 ) } ` ) ;
305- for ( const e of [ ...workspaceResult . errors , ...projectResults . flatMap ( r => r . errors ) ] ) {
306- console . log ( ` Warning: ${ e } ` ) ;
307- }
308- generateWorkspaceYaml ( projectPath , ws ) ;
309- } else {
310- const result = await initProjectWithLLM ( projectPath , { onProgress : console . log , force : forceSetup } ) ;
311- if ( ! result . created && result . durationMs === 0 ) {
312- console . log ( ` Already initialized (skipped LLM scan). Use --force to re-scan.` ) ;
313- console . log ( ` Decisions: ${ result . decisions . count } , Memories: ${ result . memories . count } ` ) ;
343+ try {
344+ if ( isWorkspace ) {
345+ const { workspaceResult, projectResults } = await initWorkspaceWithLLM ( projectPath , { onProgress : console . log } ) ;
346+ const totalCost = workspaceResult . cost . costUsd + projectResults . reduce ( ( s , r ) => s + r . cost . costUsd , 0 ) ;
347+ console . log ( ` Workspace: ${ workspaceResult . decisions . count } decisions, ${ workspaceResult . memories . count } memories` ) ;
348+ for ( const r of projectResults ) {
349+ const name = r . projectPath . split ( "/" ) . pop ( ) ;
350+ console . log ( ` ${ name } : ${ r . decisions . count } decisions (${ r . decisions . fromScan } LLM + ${ r . decisions . fromPresets } presets)` ) ;
351+ }
352+ if ( totalCost > 0 ) console . log ( ` Total cost: $${ totalCost . toFixed ( 2 ) } ` ) ;
353+ for ( const e of [ ...workspaceResult . errors , ...projectResults . flatMap ( r => r . errors ) ] ) {
354+ console . log ( ` Warning: ${ e } ` ) ;
355+ }
356+ generateWorkspaceYaml ( projectPath , ws ) ;
357+ // Track telemetry: any LLM scan in any repo means LLM method
358+ const anyLlm = projectResults . some ( r => r . oracle . llm ) || workspaceResult . decisions . fromScan > 0 ;
359+ setupMethod = anyLlm ? "llm" : "deterministic" ;
360+ setupPresetsApplied = projectResults . reduce ( ( s , r ) => s + ( r . decisions . fromPresets || 0 ) , 0 ) ;
361+ // Sum scanner counts across workspace + all projects
362+ setupScannersRun = workspaceResult . scannersRun + projectResults . reduce ( ( s , r ) => s + r . scannersRun , 0 ) ;
363+ setupScannersFailed = workspaceResult . scannersFailed + projectResults . reduce ( ( s , r ) => s + r . scannersFailed , 0 ) ;
314364 } else {
315- console . log ( ` Oracle: ${ result . oracle . files } files (${ result . oracle . llm ? "LLM scan" : "deterministic fallback" } )` ) ;
316- console . log ( ` Decisions: ${ result . decisions . count } (${ result . decisions . fromScan } LLM + ${ result . decisions . fromPresets } presets)` ) ;
317- console . log ( ` Memories: ${ result . memories . count } (${ result . memories . fromPresets } from presets)` ) ;
318- console . log ( ` Safety: ${ result . safety . llm ? "LLM scan" : "defaults + presets" } ` ) ;
319- if ( result . cost . costUsd > 0 ) console . log ( ` Cost: $${ result . cost . costUsd . toFixed ( 2 ) } , ${ ( result . durationMs / 1000 ) . toFixed ( 1 ) } s` ) ;
320- for ( const e of result . errors ) console . log ( ` Warning: ${ e } ` ) ;
365+ const result = await initProjectWithLLM ( projectPath , { onProgress : console . log , force : forceSetup } ) ;
366+ if ( ! result . created && result . durationMs === 0 ) {
367+ console . log ( ` Already initialized (skipped LLM scan). Use --force to re-scan.` ) ;
368+ console . log ( ` Decisions: ${ result . decisions . count } , Memories: ${ result . memories . count } ` ) ;
369+ } else {
370+ console . log ( ` Oracle: ${ result . oracle . files } files (${ result . oracle . llm ? "LLM scan" : "deterministic fallback" } )` ) ;
371+ console . log ( ` Decisions: ${ result . decisions . count } (${ result . decisions . fromScan } LLM + ${ result . decisions . fromPresets } presets)` ) ;
372+ console . log ( ` Memories: ${ result . memories . count } (${ result . memories . fromPresets } from presets)` ) ;
373+ console . log ( ` Safety: ${ result . safety . llm ? "LLM scan" : "defaults + presets" } ` ) ;
374+ if ( result . cost . costUsd > 0 ) console . log ( ` Cost: $${ result . cost . costUsd . toFixed ( 2 ) } , ${ ( result . durationMs / 1000 ) . toFixed ( 1 ) } s` ) ;
375+ for ( const e of result . errors ) console . log ( ` Warning: ${ e } ` ) ;
376+ }
377+ // Track telemetry: oracle.llm tells us whether LLM path was used
378+ setupMethod = result . oracle . llm ? "llm" : "deterministic" ;
379+ setupPresetsApplied = ( result . decisions . fromPresets || 0 ) ;
380+ setupScannersRun = result . scannersRun ;
381+ setupScannersFailed = result . scannersFailed ;
321382 }
383+ } catch ( err ) {
384+ setupOutcome = "failed" ;
385+ setupPhaseFailed = "init_scan" ;
386+ const { classifyError, reportError } = await import ( "./telemetry.js" ) ;
387+ try { reportError ( "setup" , classifyError ( err ) , true ) ; } catch { /* swallow */ }
388+ await sendSetupTelemetry ( ) ;
389+ throw err ;
322390 }
323391
324392 // Detect plugin context — skip .mcp.json and hooks if running from plugin
@@ -378,6 +446,11 @@ async function main() {
378446 : 0 ;
379447 writeBootstrapToAxmeMemory ( projectPath , isWorkspace , repoCount ) ;
380448
449+ // Setup completed. setupMethod was set above by the init scan.
450+ // outcome=success when LLM ran end-to-end, fallback when deterministic was used.
451+ setupOutcome = setupMethod === "llm" ? "success" : "fallback" ;
452+ await sendSetupTelemetry ( ) ;
453+
381454 console . log ( "\nDone! Run 'claude' to start using AXME tools." ) ;
382455 break ;
383456 }
0 commit comments