@orquentra/codebuddy 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/{chunk-6DONJW3W.js → chunk-2WH2Q4TA.js} +22 -5
  2. package/dist/chunk-2WH2Q4TA.js.map +1 -0
  3. package/dist/{chunk-EJQVGFH2.js → chunk-ENKDFLOO.js} +2 -2
  4. package/dist/{chunk-ZNLSCV3J.js → chunk-F4BJ3DXW.js} +5 -5
  5. package/dist/{chunk-NY2YXP3R.js → chunk-QN76UJP7.js} +2 -2
  6. package/dist/{chunk-6KGYAFG7.js → chunk-SRA4MDVG.js} +2 -2
  7. package/dist/{chunk-6KGYAFG7.js.map → chunk-SRA4MDVG.js.map} +1 -1
  8. package/dist/{chunk-XL2HLACE.js → chunk-YN7TEIUH.js} +1 -1
  9. package/dist/chunk-YN7TEIUH.js.map +1 -0
  10. package/dist/cli/index.js +3 -3
  11. package/dist/index.js +5 -5
  12. package/dist/{main-QUASSFP7.js → main-YJKSYFPT.js} +638 -204
  13. package/dist/main-YJKSYFPT.js.map +1 -0
  14. package/dist/{operations-WJR7AZZF.js → operations-OG4ISWIM.js} +3 -3
  15. package/dist/{pretool-command-JTKX6G3M.js → pretool-command-PRFOIHMW.js} +3 -3
  16. package/dist/{runtime-OQQ22VJF.js → runtime-GCSEIAVD.js} +4 -4
  17. package/package.json +1 -1
  18. package/dist/chunk-6DONJW3W.js.map +0 -1
  19. package/dist/chunk-XL2HLACE.js.map +0 -1
  20. package/dist/main-QUASSFP7.js.map +0 -1
  21. /package/dist/{chunk-EJQVGFH2.js.map → chunk-ENKDFLOO.js.map} +0 -0
  22. /package/dist/{chunk-ZNLSCV3J.js.map → chunk-F4BJ3DXW.js.map} +0 -0
  23. /package/dist/{chunk-NY2YXP3R.js.map → chunk-QN76UJP7.js.map} +0 -0
  24. /package/dist/{operations-WJR7AZZF.js.map → operations-OG4ISWIM.js.map} +0 -0
  25. /package/dist/{pretool-command-JTKX6G3M.js.map → pretool-command-PRFOIHMW.js.map} +0 -0
  26. /package/dist/{runtime-OQQ22VJF.js.map → runtime-GCSEIAVD.js.map} +0 -0
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-UK6XTS2Y.js";
5
5
  import {
6
6
  runPreToolHookCommand
7
- } from "./chunk-EJQVGFH2.js";
7
+ } from "./chunk-ENKDFLOO.js";
8
8
  import {
9
9
  failSoft,
10
10
  readHookInput,
@@ -65,9 +65,10 @@ import {
65
65
  telemetryBlocked,
66
66
  telemetryStatePath,
67
67
  telemetryUrl,
68
+ traceCliCommand,
68
69
  watchRepository,
69
70
  writeTelemetryState
70
- } from "./chunk-6DONJW3W.js";
71
+ } from "./chunk-2WH2Q4TA.js";
71
72
  import {
72
73
  DEFAULT_ZVEC_MODEL,
73
74
  buildZvecIndex,
@@ -90,7 +91,7 @@ import {
90
91
  runResetHook,
91
92
  runStageHook,
92
93
  runSyncHook
93
- } from "./chunk-ZNLSCV3J.js";
94
+ } from "./chunk-F4BJ3DXW.js";
94
95
  import {
95
96
  SessionCapsuleLedger,
96
97
  detectTestCommand,
@@ -100,7 +101,7 @@ import {
100
101
  parseTraceLines,
101
102
  readTraces,
102
103
  tracePath
103
- } from "./chunk-XL2HLACE.js";
104
+ } from "./chunk-YN7TEIUH.js";
104
105
  import {
105
106
  hookRoot
106
107
  } from "./chunk-TGFTKWOR.js";
@@ -116,11 +117,11 @@ import {
116
117
  pingDatabase,
117
118
  runDoctor,
118
119
  runMigrations
119
- } from "./chunk-NY2YXP3R.js";
120
+ } from "./chunk-QN76UJP7.js";
120
121
  import {
121
122
  MemoryFileStore,
122
123
  VERSION
123
- } from "./chunk-6KGYAFG7.js";
124
+ } from "./chunk-SRA4MDVG.js";
124
125
  import {
125
126
  computeFactHash,
126
127
  generateEntityId
@@ -1199,7 +1200,7 @@ th.n { text-align: right; }
1199
1200
  .chip { display: inline-block; font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; }
1200
1201
  .chip.recall { background: var(--recall-soft); color: var(--recall); }
1201
1202
  .chip.tool { background: var(--tool-soft); color: var(--tool); }
1202
- .chip.capture, .chip.sync, .chip.guard, .chip.kw { background: var(--line); color: var(--capture); }
1203
+ .chip.capture, .chip.sync, .chip.guard, .chip.cli, .chip.kw { background: var(--line); color: var(--capture); }
1203
1204
  .chip.err { background: var(--bad-soft); color: var(--bad); }
1204
1205
  .feed { display: grid; gap: 10px; max-height: 520px; overflow: auto; }
1205
1206
  .prompt { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
@@ -3431,101 +3432,174 @@ var ADMIN_HTML = `<!doctype html>
3431
3432
  <title>CodeBuddy Usage</title>
3432
3433
  <style>
3433
3434
  :root {
3434
- --bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --line: #e2e1dc;
3435
- --bar: #2a78d6; --bar-soft: #e3edfa; --bad: #b42318;
3435
+ --bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --faint: #8a8984; --line: #e4e3de;
3436
+ --bar: #2a78d6; --bar-soft: #e3edfa; --hook: #eb6834; --cli: #1baf7a;
3437
+ --good: #0a7d0a; --bad: #c23030; --bad-soft: #fbeaea;
3436
3438
  --sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
3437
3439
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
3438
3440
  }
3439
3441
  @media (prefers-color-scheme: dark) { :root {
3440
- --bg: #121211; --panel: #1a1a19; --fg: #f0efec; --muted: #c3c2b7; --line: #2e2e2b;
3441
- --bar: #3987e5; --bar-soft: #1d2b3d; --bad: #f97066; color-scheme: dark } }
3442
+ --bg: #111110; --panel: #1a1a19; --fg: #f0efec; --muted: #c3c2b7; --faint: #8f8e86; --line: #2e2e2b;
3443
+ --bar: #3987e5; --bar-soft: #1d2b3d; --hook: #d95926; --cli: #199e70;
3444
+ --good: #3fbf3f; --bad: #f07070; --bad-soft: #3a1a1a; color-scheme: dark } }
3442
3445
  * { box-sizing: border-box; }
3443
3446
  body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 var(--sans); }
3444
- .wrap { max-width: 1200px; margin: 0 auto; padding: 20px 16px 48px; display: grid; gap: 16px; }
3447
+ a { color: inherit; }
3448
+ .wrap { max-width: 1240px; margin: 0 auto; padding: 0 16px 56px; display: grid; gap: 16px; }
3445
3449
  .wrap > * { min-width: 0; }
3446
- header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; }
3447
- h1 { font-size: 20px; margin: 0; letter-spacing: -.01em; }
3448
- h1 small { font: 500 13px var(--mono); color: var(--muted); margin-left: 8px; }
3449
- h2 { font-size: 13px; margin: 0 0 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
3450
+ .top { position: sticky; top: 0; z-index: 5; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(8px); border-bottom: 1px solid var(--line); }
3451
+ .bar-inner { max-width: 1240px; margin: 0 auto; padding: 12px 16px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; }
3452
+ .brand { display: flex; align-items: center; gap: 10px; font-weight: 650; font-size: 16px; letter-spacing: -.01em; }
3453
+ .mark { width: 22px; height: 22px; border-radius: 6px; background: var(--bar); display: grid; place-items: center; color: #fff; font: 700 12px var(--mono); }
3454
+ .updated { font: 12px var(--mono); color: var(--faint); }
3455
+ .tabs { display: flex; gap: 4px; }
3456
+ .tabs a { text-decoration: none; color: var(--muted); font-weight: 500; font-size: 13px; padding: 6px 10px; border-radius: 7px; }
3457
+ .tabs a[aria-current="page"] { background: var(--panel); color: var(--fg); box-shadow: inset 0 0 0 1px var(--line); }
3458
+ .tabs a:focus-visible, .seg button:focus-visible, .btn:focus-visible, input:focus-visible, th button:focus-visible, tr.link:focus-visible { outline: 2px solid var(--bar); outline-offset: 1px; }
3450
3459
  .spacer { flex: 1; }
3451
3460
  .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel); }
3452
- .seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px 12px; cursor: pointer; }
3461
+ .seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px 11px; cursor: pointer; }
3453
3462
  .seg button + button { border-left: 1px solid var(--line); }
3454
3463
  .seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
3455
- .seg button:focus-visible { outline: 2px solid var(--bar); outline-offset: -2px; }
3456
- .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
3457
- .kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
3464
+ h2 { font-size: 12px; margin: 0; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
3465
+ .head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 12px; }
3466
+ .head .note { font-size: 12px; color: var(--faint); }
3467
+ .hero { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 12px; padding-top: 16px; }
3468
+ .kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; }
3458
3469
  .kpi .label { font-size: 12px; color: var(--muted); }
3459
- .kpi .value { font: 600 28px/1.2 var(--mono); font-variant-numeric: tabular-nums; margin-top: 4px; }
3460
- .kpi .sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
3461
- .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; min-width: 0; position: relative; }
3462
- .cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
3463
- .chart svg { display: block; width: 100%; height: 180px; }
3464
- .chart rect.hit { fill: transparent; }
3465
- .chart rect.hit:hover + rect.mark, .chart rect.mark:hover { opacity: .75; }
3466
- svg text { fill: var(--muted); font: 11px var(--mono); }
3467
- .tip { position: absolute; pointer-events: none; background: var(--fg); color: var(--bg); font: 12px var(--mono); padding: 4px 8px; border-radius: 6px; white-space: nowrap; display: none; z-index: 2; }
3468
- .rows { display: grid; gap: 6px; }
3469
- .row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; font-size: 13px; }
3470
- .row .track { grid-column: 1 / -1; height: 6px; background: var(--bar-soft); border-radius: 3px; }
3471
- .row .fill { height: 6px; background: var(--bar); border-radius: 3px; }
3470
+ .kpi .value { font: 650 32px/1.15 var(--mono); font-variant-numeric: tabular-nums; margin-top: 6px; letter-spacing: -.02em; }
3471
+ .kpi .sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
3472
+ .up { color: var(--good); font-weight: 600; }
3473
+ .down { color: var(--bad); font-weight: 600; }
3474
+ .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; min-width: 0; }
3475
+ .grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: 16px; }
3476
+ .grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 16px; }
3477
+ .chart svg { display: block; width: 100%; height: 190px; overflow: visible; }
3478
+ svg text { fill: var(--faint); font: 11px var(--mono); }
3479
+ .hit { fill: transparent; cursor: crosshair; }
3480
+ .hit:hover { fill: color-mix(in srgb, var(--fg) 6%, transparent); }
3481
+ .tip { position: absolute; pointer-events: none; background: var(--fg); color: var(--bg); font: 12px var(--mono); padding: 5px 8px; border-radius: 6px; white-space: nowrap; display: none; z-index: 10; }
3482
+ .rows { display: grid; gap: 8px; }
3483
+ .row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 12px; font-size: 13px; }
3484
+ .row .track { grid-column: 1 / -1; height: 8px; background: var(--bar-soft); border-radius: 4px; overflow: hidden; }
3485
+ .row .fill { height: 8px; background: var(--bar); border-radius: 4px; }
3472
3486
  .row .n { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--muted); }
3487
+ .funnel .row { font-size: 14px; }
3488
+ .funnel .track { height: 22px; border-radius: 6px; }
3489
+ .funnel .fill { height: 22px; border-radius: 6px; }
3473
3490
  .scroll { overflow-x: auto; }
3474
3491
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
3475
3492
  th { text-align: left; font: 600 11px var(--sans); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
3476
- td { padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
3493
+ th button { all: unset; cursor: pointer; }
3494
+ th button::after { content: attr(data-dir); margin-left: 4px; color: var(--faint); }
3495
+ td { padding: 7px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
3477
3496
  tr:last-child td { border-bottom: 0; }
3478
3497
  td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
3498
+ tr.link { cursor: pointer; }
3499
+ tr.link:hover td { background: var(--bar-soft); }
3500
+ .inline { display: inline-block; height: 6px; background: var(--bar); border-radius: 3px; vertical-align: middle; margin-left: 8px; }
3501
+ .heat td { text-align: center; font: 12px var(--mono); padding: 6px 4px; border: 2px solid var(--panel); border-radius: 6px; min-width: 44px; }
3502
+ .heat td.lab { text-align: left; color: var(--muted); font-family: var(--sans); white-space: nowrap; }
3503
+ .heat td.empty { background: transparent; color: var(--faint); }
3479
3504
  .bad { color: var(--bad); }
3480
3505
  .muted { color: var(--muted); }
3481
3506
  .mono { font-family: var(--mono); }
3482
- .btn { border: 1px solid var(--line); background: var(--panel); color: var(--fg); font: 500 12px var(--sans); padding: 6px 12px; border-radius: 8px; cursor: pointer; }
3483
- .btn:focus-visible { outline: 2px solid var(--bar); outline-offset: 2px; }
3484
- .users { max-height: 560px; overflow: auto; }
3485
- .users thead th { position: sticky; top: 0; background: var(--panel); }
3486
- .empty { padding: 32px 16px; text-align: center; color: var(--muted); }
3507
+ .empty-state { padding: 36px 16px; text-align: center; color: var(--muted); }
3508
+ .toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
3509
+ input[type="search"] { font: 13px var(--sans); padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); min-width: 0; flex: 1 1 200px; }
3510
+ .btn { border: 1px solid var(--line); background: var(--panel); color: var(--fg); font: 500 12px var(--sans); padding: 7px 12px; border-radius: 8px; cursor: pointer; text-decoration: none; }
3511
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; }
3512
+ .chip { display: inline-flex; align-items: center; gap: 6px; font: 600 11px var(--mono); padding: 2px 8px; border-radius: 999px; background: var(--bar-soft); color: var(--fg); }
3513
+ .sw { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
3514
+ .sessions { display: grid; gap: 6px; max-height: 520px; overflow: auto; }
3515
+ .session { all: unset; display: grid; gap: 2px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; font-size: 12px; }
3516
+ .session[aria-pressed="true"] { border-color: var(--bar); box-shadow: inset 3px 0 0 var(--bar); }
3517
+ .session:focus-visible { outline: 2px solid var(--bar); }
3518
+ .split { display: grid; grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); gap: 16px; }
3519
+ @media (max-width: 820px) { .split { grid-template-columns: 1fr; } .wf .step { grid-template-columns: 120px 52px minmax(0, 1fr) 56px; } }
3520
+ .wf { display: grid; gap: 3px; font-size: 12px; }
3521
+ .wf .step { display: grid; grid-template-columns: 170px 64px minmax(0, 1fr) 70px; gap: 10px; align-items: center; }
3522
+ .wf .lane { position: relative; height: 14px; background: var(--bg); border-radius: 3px; }
3523
+ .wf .seg-bar { position: absolute; top: 2px; height: 10px; min-width: 3px; border-radius: 3px; }
3524
+ .wf .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
3525
+ .wf .at { font-family: var(--mono); color: var(--faint); text-align: right; }
3526
+ .wf .t { text-align: right; font-family: var(--mono); color: var(--muted); }
3527
+ .legend { display: flex; gap: 14px; font-size: 12px; color: var(--muted); margin-bottom: 10px; flex-wrap: wrap; }
3528
+ .legend span { display: inline-flex; align-items: center; gap: 6px; }
3529
+ footer { font-size: 12px; color: var(--faint); }
3530
+ [hidden] { display: none !important; }
3487
3531
  </style>
3488
3532
  </head>
3489
3533
  <body>
3534
+ <div class="top"><div class="bar-inner">
3535
+ <div class="brand"><span class="mark">cb</span>CodeBuddy usage</div>
3536
+ <nav class="tabs" aria-label="Views">
3537
+ <a href="#overview" data-view="overview">Overview</a>
3538
+ <a href="#users" data-view="users">Users</a>
3539
+ <a href="#events" data-view="events">Events</a>
3540
+ </nav>
3541
+ <span class="spacer"></span>
3542
+ <span class="updated" id="updated">loading\u2026</span>
3543
+ <div class="seg" role="group" aria-label="Time range" id="range">
3544
+ <button data-days="7">7d</button><button data-days="30" aria-pressed="true">30d</button><button data-days="90">90d</button><button data-days="365">1y</button>
3545
+ </div>
3546
+ </div></div>
3547
+
3490
3548
  <div class="wrap">
3491
- <header>
3492
- <h1>CodeBuddy usage<small id="updated"></small></h1>
3493
- <span class="spacer"></span>
3494
- <div class="seg" role="group" aria-label="Time range" id="range">
3495
- <button data-days="7">7 days</button>
3496
- <button data-days="30" aria-pressed="true">30 days</button>
3497
- <button data-days="90">90 days</button>
3498
- <button data-days="365">1 year</button>
3549
+ <section id="view-overview">
3550
+ <div class="hero" id="hero" aria-label="Headline numbers"></div>
3551
+ <div class="grid2" style="margin-top:16px">
3552
+ <section class="panel"><div class="head"><h2>Total installs</h2><span class="note">all-time, by first-seen day</span></div><div class="chart" id="cumulative"></div></section>
3553
+ <section class="panel"><div class="head"><h2>Daily active installs</h2><span class="note">installs that sent any event</span></div><div class="chart" id="active"></div></section>
3499
3554
  </div>
3500
- </header>
3501
- <section class="kpis" id="kpis" aria-label="Summary"></section>
3502
- <div class="cols">
3503
- <section class="panel"><h2>Daily active installs</h2><div class="chart" id="active"></div></section>
3504
- <section class="panel"><h2>npm downloads per day</h2><div class="chart" id="npm"></div></section>
3505
- </div>
3506
- <section class="panel"><h2>Feature usage</h2><div class="scroll" id="features"></div></section>
3507
- <div class="cols">
3508
- <section class="panel"><h2>Agent clients</h2><div class="rows" id="clients"></div></section>
3509
- <section class="panel"><h2>Versions</h2><div class="rows" id="versions"></div></section>
3510
- <section class="panel"><h2>Platforms</h2><div class="rows" id="platforms"></div></section>
3511
- </div>
3512
- <section class="panel">
3513
- <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px"><h2 style="margin:0">Users</h2><span class="muted" id="userCount" style="font-size:12px"></span><span class="spacer"></span><button class="btn" id="csv">Download CSV</button></div>
3514
- <div class="scroll users" id="users"></div>
3555
+ <div class="grid2" style="margin-top:16px">
3556
+ <section class="panel"><div class="head"><h2>Activation</h2><span class="note">installs active in range reaching each step</span></div><div class="rows funnel" id="funnel"></div></section>
3557
+ <section class="panel"><div class="head"><h2>Retention by weekly cohort</h2><span class="note">share of each week's new installs active in later weeks</span></div><div class="scroll" id="cohorts"></div></section>
3558
+ </div>
3559
+ <section class="panel" style="margin-top:16px"><div class="head"><h2>Feature usage</h2><span class="note">MCP tools, hook steps (recall, capture\u2026) and CLI commands</span></div><div class="scroll" id="features"></div></section>
3560
+ <div class="grid3" style="margin-top:16px">
3561
+ <section class="panel"><div class="head"><h2>Agent clients</h2></div><div class="rows" id="clients"></div></section>
3562
+ <section class="panel"><div class="head"><h2>Versions</h2></div><div class="rows" id="versions"></div></section>
3563
+ <section class="panel"><div class="head"><h2>Platforms</h2></div><div class="rows" id="platforms"></div></section>
3564
+ </div>
3565
+ <section class="panel" style="margin-top:16px"><div class="head"><h2>npm downloads per day</h2><span class="note" id="npmNote"></span></div><div class="chart" id="npm"></div></section>
3566
+ </section>
3567
+
3568
+ <section id="view-users" class="panel" style="margin-top:16px" hidden>
3569
+ <div class="toolbar">
3570
+ <h2>Users</h2><span class="muted" id="userCount" style="font-size:12px"></span><span class="spacer"></span>
3571
+ <input type="search" id="userSearch" placeholder="Filter by id, client, version, platform\u2026" aria-label="Filter users">
3572
+ <button class="btn" id="csv">Download CSV</button>
3573
+ </div>
3574
+ <div class="scroll" id="users"></div>
3515
3575
  </section>
3516
- <section class="panel"><h2>Latest events</h2><div class="scroll" id="recent"></div></section>
3517
- <p class="muted" style="margin:0;font-size:12px">Anonymous telemetry from installs that did not opt out: random install ids, tool names, timings and versions. No prompts, code, paths or repository names are collected. Shares count each install once, from its latest batch.</p>
3576
+
3577
+ <section id="view-install" style="margin-top:16px" hidden></section>
3578
+
3579
+ <section id="view-events" class="panel" style="margin-top:16px" hidden>
3580
+ <div class="toolbar"><h2>Latest events</h2><span class="spacer"></span><div class="chips" id="kindFilter"></div></div>
3581
+ <div class="scroll" id="recent"></div>
3582
+ </section>
3583
+
3584
+ <footer>Anonymous telemetry from installs that did not opt out: random install ids, tool and command names, timings, versions. No prompts, code, paths or repository names are collected. Shares count each install once, from its latest batch.</footer>
3518
3585
  </div>
3519
- <div class="tip" id="tip"></div>
3586
+ <div class="tip" id="tip" role="status"></div>
3520
3587
  <script>
3521
3588
  (function () {
3522
3589
  var days = 30;
3523
- // Works at "/" and under a proxy mount such as "/admin" (with or without a trailing slash).
3524
- var base = location.pathname.replace(/\\/?$/, "/");
3525
- var summary = null;
3526
- var npm = null;
3590
+ var summary = null, npm = null, install = null;
3591
+ var userSort = { key: "lastSeen", dir: -1 };
3592
+ var kindShown = "all";
3593
+ var selectedSession = null;
3594
+ var path = location.pathname;
3595
+ var base = path.charAt(path.length - 1) === "/" ? path : path + "/";
3527
3596
  var tip = document.getElementById("tip");
3597
+ var NL = String.fromCharCode(10);
3598
+ var KIND_COLOR = { tool: "var(--bar)", cli: "var(--cli)" };
3599
+ function kindColor(kind) { return KIND_COLOR[kind] || "var(--hook)"; }
3600
+ function kindGroup(kind) { return kind === "tool" ? "MCP tool" : kind === "cli" ? "CLI command" : "hook step"; }
3528
3601
 
3602
+ function $(id) { return document.getElementById(id); }
3529
3603
  function esc(value) {
3530
3604
  return String(value == null ? "" : value).replace(/[&<>"']/g, function (c) {
3531
3605
  return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
@@ -3533,36 +3607,57 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
3533
3607
  }
3534
3608
  function num(n) { return Math.round(n).toLocaleString(); }
3535
3609
  function compact(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e4 ? (n / 1e3).toFixed(1) + "k" : num(n); }
3536
- function pct(n) { return (n * 100).toFixed(n < 0.1 ? 1 : 0) + "%"; }
3537
- function ms(n) { return n >= 1000 ? (n / 1000).toFixed(1) + " s" : Math.round(n) + " ms"; }
3610
+ function pct(n) { return (n * 100).toFixed(n > 0 && n < 0.1 ? 1 : 0) + "%"; }
3611
+ function plural(n, word) { return num(n) + " " + word + (n === 1 ? "" : "s"); }
3612
+ function offset(n) { var s = Math.round(n / 1000); return "+" + (s >= 60 ? Math.floor(s / 60) + "m " + (s % 60) + "s" : s + "s"); }
3613
+ function ms(n) { return n >= 60000 ? (n / 60000).toFixed(1) + " min" : n >= 1000 ? (n / 1000).toFixed(1) + " s" : Math.round(n) + " ms"; }
3614
+ function date(iso) { return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" }); }
3615
+ function ago(iso) {
3616
+ var s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
3617
+ return s < 60 ? "just now" : s < 3600 ? Math.floor(s / 60) + "m ago" : s < 86400 ? Math.floor(s / 3600) + "h ago" : Math.floor(s / 86400) + "d ago";
3618
+ }
3619
+ function get(url) { return fetch(base + url).then(function (r) { return r.ok ? r.json() : null; }); }
3538
3620
 
3539
3621
  function kpi(label, value, sub) {
3540
- return '<div class="kpi"><div class="label">' + esc(label) + '</div><div class="value">' + esc(value) +
3541
- '</div><div class="sub">' + esc(sub || "") + "</div></div>";
3622
+ return '<div class="kpi"><div class="label">' + esc(label) + '</div><div class="value">' + esc(value) + '</div><div class="sub">' + (sub || "") + "</div></div>";
3623
+ }
3624
+ function delta(now, before) {
3625
+ if (!before) return now ? '<span class="up">\u25B2 new</span> vs previous 7 days' : "no activity yet";
3626
+ var change = (now - before) / before;
3627
+ return '<span class="' + (change >= 0 ? "up" : "down") + '">' + (change >= 0 ? "\u25B2 " : "\u25BC ") + pct(Math.abs(change)) + "</span> vs previous 7 days";
3542
3628
  }
3543
3629
 
3544
- /** Bars of one measure over days; hover shows the exact value. */
3545
- function bars(points, label) {
3546
- if (!points.length) return '<div class="empty">No data yet.</div>';
3547
- var w = 600, h = 180, top = 10, bottom = 22, left = 36;
3630
+ /** Bars or a filled line over days, one measure; every point has a hover target. */
3631
+ /** Draw into a container at its own pixel width, so labels never stretch. */
3632
+ function draw(id, points, label, line) {
3633
+ var el = $(id);
3634
+ if (el) el.innerHTML = chart(points, label, line, el.clientWidth);
3635
+ }
3636
+ function chart(points, label, line, width) {
3637
+ if (!points.length) return '<div class="empty-state">No data yet.</div>';
3638
+ var w = Math.max(280, width || 640), h = 190, top = 10, bottom = 22, left = 40, plot = h - top - bottom;
3548
3639
  var max = Math.max.apply(null, points.map(function (p) { return p.value; }).concat([1]));
3549
3640
  var step = (w - left) / points.length;
3550
- var bw = Math.max(1, step - 2);
3641
+ var y = function (v) { return top + plot * (1 - v / max); };
3551
3642
  var out = '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" role="img" aria-label="' + esc(label) + '">';
3552
3643
  [0, 0.5, 1].forEach(function (f) {
3553
- var y = top + (h - top - bottom) * (1 - f);
3554
- out += '<line x1="' + left + '" x2="' + w + '" y1="' + y + '" y2="' + y + '" stroke="var(--line)" stroke-width="1"/>';
3555
- out += '<text x="' + (left - 6) + '" y="' + (y + 4) + '" text-anchor="end">' + esc(compact(max * f)) + "</text>";
3644
+ out += '<line x1="' + left + '" x2="' + w + '" y1="' + y(max * f) + '" y2="' + y(max * f) + '" stroke="var(--line)"/>';
3645
+ out += '<text x="' + (left - 6) + '" y="' + (y(max * f) + 4) + '" text-anchor="end">' + esc(compact(max * f)) + "</text>";
3556
3646
  });
3647
+ if (line) {
3648
+ var pts = points.map(function (p, i) { return (left + i * step + step / 2).toFixed(1) + "," + y(p.value).toFixed(1); });
3649
+ out += '<path d="M' + (left + step / 2) + "," + (top + plot) + " L" + pts.join(" L") + " L" + (left + (points.length - 0.5) * step) + "," + (top + plot) + ' Z" fill="var(--bar-soft)"/>';
3650
+ out += '<polyline points="' + pts.join(" ") + '" fill="none" stroke="var(--bar)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round"/>';
3651
+ }
3557
3652
  points.forEach(function (p, i) {
3558
- var bh = (h - top - bottom) * (p.value / max);
3559
- var x = left + i * step + 1;
3560
- var tipText = esc(p.date + " \xB7 " + num(p.value) + " " + label);
3561
- out += '<rect class="hit" x="' + (left + i * step) + '" y="' + top + '" width="' + step + '" height="' + (h - top - bottom) + '" data-tip="' + tipText + '"/>';
3562
- out += '<rect class="mark" x="' + x + '" y="' + (h - bottom - bh) + '" width="' + bw + '" height="' + bh + '" rx="' + Math.min(2, bw / 2) + '" fill="var(--bar)" data-tip="' + tipText + '"/>';
3653
+ var x = left + i * step;
3654
+ if (!line) {
3655
+ var bh = plot * (p.value / max), bw = Math.max(1, step - 2);
3656
+ out += '<rect x="' + (x + 1) + '" y="' + (top + plot - bh) + '" width="' + bw + '" height="' + bh + '" rx="' + Math.min(2, bw / 2) + '" fill="var(--bar)"/>';
3657
+ }
3658
+ out += '<rect class="hit" x="' + x + '" y="' + top + '" width="' + step + '" height="' + plot + '" data-tip="' + esc(p.date + " \xB7 " + num(p.value) + " " + label) + '"/>';
3563
3659
  });
3564
- var first = points[0].date, last = points[points.length - 1].date;
3565
- out += '<text x="' + left + '" y="' + (h - 6) + '">' + esc(first) + '</text><text x="' + w + '" y="' + (h - 6) + '" text-anchor="end">' + esc(last) + "</text>";
3660
+ out += '<text x="' + left + '" y="' + (h - 5) + '">' + esc(points[0].date) + '</text><text x="' + w + '" y="' + (h - 5) + '" text-anchor="end">' + esc(points[points.length - 1].date) + "</text>";
3566
3661
  return out + "</svg>";
3567
3662
  }
3568
3663
 
@@ -3570,85 +3665,230 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
3570
3665
  if (!items.length) return '<div class="muted">No data yet.</div>';
3571
3666
  return items.slice(0, 8).map(function (item) {
3572
3667
  var share = total ? item.installs / total : 0;
3573
- return '<div class="row"><span>' + esc(item.label) + '</span><span class="n">' + num(item.installs) + " \xB7 " + pct(share) +
3574
- '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
3668
+ return '<div class="row"><span>' + esc(item.label) + '</span><span class="n">' + num(item.installs) + " \xB7 " + pct(share) + '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
3575
3669
  }).join("");
3576
3670
  }
3577
3671
 
3578
- function npmPoints() {
3579
- if (!npm || !npm.downloads) return [];
3580
- return npm.downloads.slice(-Math.min(days, 365)).map(function (d) { return { date: d.day, value: d.downloads }; });
3672
+ function renderOverview() {
3673
+ var t = summary.totals;
3674
+ var npm30 = npmTotal(30);
3675
+ $("hero").innerHTML =
3676
+ kpi("Weekly active installs", num(t.wau), delta(t.wau, t.wauPrevious)) +
3677
+ kpi("Total installs", num(t.allTimeInstalls), "+" + num(t.newInRange) + " in the last " + summary.days + " days \xB7 " + num(t.newToday) + " today") +
3678
+ kpi("Week-1 retention", t.week1Retention == null ? "\u2013" : pct(t.week1Retention), t.week1Retention == null ? "needs a cohort older than 7 days" : "new installs active again the next week") +
3679
+ kpi("Agent sessions", compact(t.sessions), num(t.dau) + " installs active today \xB7 " + num(t.mau) + " this month") +
3680
+ kpi("npm downloads \xB7 30 days", npm30 == null ? "\u2013" : num(npm30), npm && npm.error ? esc(npm.error) : num(npmTotal(365) || 0) + " in the last year");
3681
+ draw("cumulative", summary.daily.map(function (d) { return { date: d.date, value: d.totalInstalls }; }), "total installs", true);
3682
+ draw("active", summary.daily.map(function (d) { return { date: d.date, value: d.installs }; }), "active installs", false);
3683
+
3684
+ var active = summary.funnel[0] ? summary.funnel[0].installs : 0;
3685
+ $("funnel").innerHTML = active ? summary.funnel.map(function (step) {
3686
+ var share = active ? step.installs / active : 0;
3687
+ return '<div class="row"><span>' + esc(step.label) + '</span><span class="n">' + num(step.installs) + " \xB7 " + pct(share) + '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
3688
+ }).join("") : '<div class="empty-state">No active installs in this range.</div>';
3689
+
3690
+ var cohorts = summary.cohorts.filter(function (c) { return c.size > 0; });
3691
+ var weeks = summary.cohorts.length;
3692
+ $("cohorts").innerHTML = cohorts.length
3693
+ ? '<table class="heat"><thead><tr><th>Cohort</th><th class="n">Installs</th>' + Array.from({ length: weeks }, function (_, i) { return '<th class="n">W' + i + "</th>"; }).join("") + "</tr></thead><tbody>" +
3694
+ cohorts.map(function (c) {
3695
+ var cells = "";
3696
+ for (var i = 0; i < weeks; i += 1) {
3697
+ var v = c.weeks[i];
3698
+ if (v == null) { cells += '<td class="empty">\xB7</td>'; continue; }
3699
+ var strength = Math.round(12 + v * 88);
3700
+ cells += '<td style="background:color-mix(in oklab, var(--bar) ' + strength + '%, var(--panel));color:' + (v > 0.5 ? "#fff" : "var(--fg)") + '" title="' + esc(c.start + " cohort, week " + i + ": " + pct(v)) + '">' + pct(v) + "</td>";
3701
+ }
3702
+ return '<tr><td class="lab">' + esc(date(c.start)) + '</td><td class="n">' + num(c.size) + "</td>" + cells + "</tr>";
3703
+ }).join("") + "</tbody></table>"
3704
+ : '<div class="empty-state">Cohorts appear once new installs report in.</div>';
3705
+
3706
+ var maxCalls = Math.max.apply(null, summary.features.map(function (f) { return f.calls; }).concat([1]));
3707
+ $("features").innerHTML = summary.features.length
3708
+ ? '<table><thead><tr><th>Feature</th><th class="n">Calls</th><th class="n">Installs</th><th class="n">Median</th><th class="n">p95</th><th class="n">Errors</th></tr></thead><tbody>' +
3709
+ summary.features.map(function (f) {
3710
+ return '<tr><td class="mono">' + esc(f.label) + '<span class="inline" style="width:' + Math.max(2, (f.calls / maxCalls) * 120).toFixed(0) + 'px"></span></td><td class="n">' + num(f.calls) + '</td><td class="n">' + num(f.installs) +
3711
+ '</td><td class="n">' + ms(f.p50ms) + '</td><td class="n">' + ms(f.p95ms) + '</td><td class="n' + (f.errorRate > 0.05 ? " bad" : "") + '">' + pct(f.errorRate) + "</td></tr>";
3712
+ }).join("") + "</tbody></table>"
3713
+ : '<div class="empty-state">No events yet. Installs send a batch at most every 10 minutes.</div>';
3714
+ var installs = summary.versions.reduce(function (sum, v) { return sum + v.installs; }, 0);
3715
+ $("clients").innerHTML = rows(summary.clients, installs);
3716
+ $("versions").innerHTML = rows(summary.versions, installs);
3717
+ $("platforms").innerHTML = rows(summary.platforms, installs);
3718
+ renderNpm();
3581
3719
  }
3720
+
3582
3721
  function npmTotal(n) {
3583
3722
  if (!npm || !npm.downloads) return null;
3584
3723
  return npm.downloads.slice(-n).reduce(function (sum, d) { return sum + d.downloads; }, 0);
3585
3724
  }
3725
+ function renderNpm() {
3726
+ if (!npm) return;
3727
+ if (npm.error) { $("npm").innerHTML = '<div class="empty-state">' + esc(npm.error) + "</div>"; return; }
3728
+ $("npmNote").textContent = npm.package + " \xB7 public npm registry";
3729
+ draw("npm", npm.downloads.slice(-Math.min(days, 365)).map(function (d) { return { date: d.day, value: d.downloads }; }), "downloads", false);
3730
+ }
3586
3731
 
3587
- function render() {
3588
- if (!summary) return;
3589
- var t = summary.totals;
3590
- var npm30 = npmTotal(30), npm365 = npmTotal(365);
3591
- document.getElementById("updated").textContent = "updated " + new Date(summary.generatedAt).toLocaleTimeString();
3592
- document.getElementById("kpis").innerHTML =
3593
- kpi("Active installs \xB7 7 days", num(t.active7d), num(t.active1d) + " today") +
3594
- kpi("Active installs \xB7 30 days", num(t.active30d), num(t.newToday) + " new today") +
3595
- kpi("Installs in range", num(t.installs), num(t.projects) + " projects") +
3596
- kpi("Agent sessions", num(t.sessions), "in " + summary.days + " days") +
3597
- kpi("Events", compact(t.events), pct(1 - t.errorRate) + " succeeded") +
3598
- kpi("Tokens delivered", compact(t.tokensOut), "context sent to agents") +
3599
- kpi("npm downloads \xB7 30 days", npm30 == null ? "\u2013" : num(npm30), npm365 == null ? (npm && npm.error ? npm.error : "") : num(npm365) + " in the last year");
3600
- document.getElementById("active").innerHTML = bars(summary.daily.map(function (d) { return { date: d.date, value: d.installs }; }), "active installs");
3601
- document.getElementById("npm").innerHTML = npm && npm.error ? '<div class="empty">' + esc(npm.error) + "</div>" : bars(npmPoints(), "downloads");
3602
- document.getElementById("features").innerHTML = summary.features.length
3603
- ? "<table><thead><tr><th>Feature</th><th class=\\"n\\">Calls</th><th class=\\"n\\">Installs</th><th class=\\"n\\">Median</th><th class=\\"n\\">Errors</th></tr></thead><tbody>" +
3604
- summary.features.map(function (f) {
3605
- return "<tr><td class=\\"mono\\">" + esc(f.label) + '</td><td class="n">' + num(f.calls) + '</td><td class="n">' + num(f.installs) +
3606
- '</td><td class="n">' + ms(f.p50ms) + '</td><td class="n' + (f.errorRate > 0.05 ? " bad" : "") + '">' + pct(f.errorRate) + "</td></tr>";
3607
- }).join("") + "</tbody></table>"
3608
- : '<div class="empty">No events yet. Installs send a batch at most every 10 minutes.</div>';
3609
- var installs = summary.versions.reduce(function (sum, v) { return sum + v.installs; }, 0);
3610
- document.getElementById("clients").innerHTML = rows(summary.clients, installs);
3611
- document.getElementById("versions").innerHTML = rows(summary.versions, installs);
3612
- document.getElementById("platforms").innerHTML = rows(summary.platforms, installs);
3613
- document.getElementById("userCount").textContent = num(summary.users.length) + " installs seen in " + summary.days + " days";
3614
- document.getElementById("users").innerHTML = summary.users.length
3615
- ? "<table><thead><tr><th>Install</th><th>First seen</th><th>Last active</th><th class=\\"n\\">Events</th><th class=\\"n\\">Sessions</th><th class=\\"n\\">Projects</th><th>Client</th><th>Version</th><th>Platform</th><th>Top feature</th></tr></thead><tbody>" +
3616
- summary.users.map(function (u) {
3617
- return '<tr><td class="mono">' + esc(u.install) + "</td><td>" + esc(new Date(u.firstSeen).toLocaleDateString()) + "</td><td>" + esc(new Date(u.lastSeen).toLocaleString()) +
3618
- '</td><td class="n">' + num(u.events) + '</td><td class="n">' + num(u.sessions) + '</td><td class="n">' + num(u.projects) + "</td><td>" + esc(u.client) +
3619
- '</td><td class="mono">' + esc(u.version) + "</td><td>" + esc(u.platform) + '</td><td class="mono">' + esc(u.topFeature) + "</td></tr>";
3732
+ var USER_COLS = [
3733
+ ["install", "Install"], ["firstSeen", "First seen"], ["lastSeen", "Last active"], ["activeDays", "Active days", 1],
3734
+ ["sessions", "Sessions", 1], ["events", "Events", 1], ["errors", "Errors", 1], ["client", "Client"], ["version", "Version"], ["platform", "Platform"], ["topFeature", "Top feature"]
3735
+ ];
3736
+ function filteredUsers() {
3737
+ var q = $("userSearch").value.trim().toLowerCase();
3738
+ return summary.users.filter(function (u) {
3739
+ return !q || [u.install, u.client, u.version, u.platform, u.topFeature].join(" ").toLowerCase().indexOf(q) !== -1;
3740
+ }).sort(function (a, b) {
3741
+ var x = a[userSort.key], y = b[userSort.key];
3742
+ return (x < y ? -1 : x > y ? 1 : 0) * userSort.dir;
3743
+ });
3744
+ }
3745
+ function renderUsers() {
3746
+ var list = filteredUsers();
3747
+ $("userCount").textContent = num(summary.users.length) + " installs active in the last " + summary.days + " days";
3748
+ $("users").innerHTML = list.length
3749
+ ? "<table><thead><tr>" + USER_COLS.map(function (c) {
3750
+ return "<th" + (c[2] ? ' class="n"' : "") + '><button data-sort="' + c[0] + '" data-dir="' + (userSort.key === c[0] ? (userSort.dir > 0 ? "\u2191" : "\u2193") : "") + '">' + c[1] + "</button></th>";
3751
+ }).join("") + "</tr></thead><tbody>" +
3752
+ list.map(function (u) {
3753
+ return '<tr class="link" tabindex="0" data-install="' + esc(u.install) + '"><td class="mono">' + esc(u.install) + "</td><td>" + esc(date(u.firstSeen)) + "</td><td>" + esc(ago(u.lastSeen)) +
3754
+ '</td><td class="n">' + num(u.activeDays) + '</td><td class="n">' + num(u.sessions) + '</td><td class="n">' + num(u.events) + '</td><td class="n' + (u.errors ? " bad" : "") + '">' + num(u.errors) +
3755
+ "</td><td>" + esc(u.client) + '</td><td class="mono">' + esc(u.version) + "</td><td>" + esc(u.platform) + '</td><td class="mono">' + esc(u.topFeature) + "</td></tr>";
3620
3756
  }).join("") + "</tbody></table>"
3621
- : '<div class="empty">No users yet.</div>';
3622
- document.getElementById("recent").innerHTML = summary.recent.length
3623
- ? "<table><thead><tr><th>Received</th><th>Install</th><th>Kind</th><th>Name</th><th>Client</th><th>Version</th><th class=\\"n\\">Time</th><th>Result</th></tr></thead><tbody>" +
3624
- summary.recent.map(function (e) {
3625
- return "<tr><td>" + esc(new Date(e.receivedAt).toLocaleString()) + '</td><td class="mono">' + esc(e.install) + "</td><td>" + esc(e.kind) +
3626
- '</td><td class="mono">' + esc(e.name) + "</td><td>" + esc(e.client) + '</td><td class="mono">' + esc(e.version) +
3627
- '</td><td class="n">' + ms(e.ms) + '</td><td class="' + (e.ok ? "" : "bad") + '">' + (e.ok ? "ok" : "error") + "</td></tr>";
3757
+ : '<div class="empty-state">' + (summary.users.length ? "No installs match this filter." : "No users yet.") + "</div>";
3758
+ }
3759
+
3760
+ function renderInstall() {
3761
+ var el = $("view-install");
3762
+ if (!install) { el.innerHTML = '<div class="panel empty-state">Loading\u2026</div>'; return; }
3763
+ if (install.missing) { el.innerHTML = '<div class="panel empty-state">No events for this install in the last 90 days. <a href="#users">Back to users</a></div>'; return; }
3764
+ var errors = install.events.filter(function (e) { return !e.ok; }).length;
3765
+ var perDay = {};
3766
+ install.perDay.forEach(function (d) { perDay[d.date] = d.events; });
3767
+ var points = [];
3768
+ for (var i = 89; i >= 0; i -= 1) {
3769
+ var d = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
3770
+ points.push({ date: d, value: perDay[d] || 0 });
3771
+ }
3772
+ if (!selectedSession || !install.sessions.some(function (s) { return s.id === selectedSession; })) selectedSession = install.sessions[0] ? install.sessions[0].id : null;
3773
+ el.innerHTML =
3774
+ '<div class="toolbar"><a class="btn" href="#users">\u2190 Users</a><h2 style="font-size:14px;color:var(--fg);text-transform:none;letter-spacing:0">Install <span class="mono">' + esc(install.install) + "</span></h2>" +
3775
+ '<span class="chips"><span class="chip">' + esc(install.client) + '</span><span class="chip">v' + esc(install.version) + '</span><span class="chip">' + esc(install.platform) + '</span><span class="chip">node ' + esc(install.node) + "</span></span></div>" +
3776
+ '<div class="hero" style="padding-top:0">' + kpi("Events \xB7 90 days", num(install.events.length), errors ? '<span class="bad">' + plural(errors, "error") + "</span>" : "no errors") +
3777
+ kpi("Sessions", num(install.sessions.length), "agent sessions with CodeBuddy") + kpi("Active days", num(install.perDay.length), "in the last 90 days") + kpi("Projects", num(install.projects), "hashed, never named") + "</div>" +
3778
+ '<section class="panel" style="margin-top:16px"><div class="head"><h2>Activity</h2><span class="note">events per day, last 90 days</span></div><div class="chart" id="installActivity"></div></section>' +
3779
+ '<section class="panel" style="margin-top:16px"><div class="head"><h2>Session traces</h2><span class="note">each step ends at its recorded time; bar length is its duration</span></div>' +
3780
+ '<div class="split"><div class="sessions" id="sessionList">' + install.sessions.map(function (s) {
3781
+ return '<button class="session" data-session="' + esc(s.id) + '" aria-pressed="' + (s.id === selectedSession) + '"><strong class="mono">' + esc(s.id === "none" ? "no session id" : s.id.slice(0, 8)) + "</strong>" +
3782
+ '<span class="muted">' + esc(new Date(s.start).toLocaleString()) + " \xB7 " + num(s.events) + " steps" + (s.errors ? ' \xB7 <span class="bad">' + plural(s.errors, "error") + "</span>" : "") + "</span></button>";
3783
+ }).join("") + '</div><div id="waterfall"></div></div></section>';
3784
+ draw("installActivity", points, "events", false);
3785
+ renderWaterfall();
3786
+ }
3787
+
3788
+ function renderWaterfall() {
3789
+ var target = $("waterfall");
3790
+ if (!target || !install) return;
3791
+ var steps = install.events.filter(function (e) { return e.session === selectedSession; })
3792
+ .sort(function (a, b) { return a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0; }).slice(-200);
3793
+ if (!steps.length) { target.innerHTML = '<div class="empty-state">Pick a session.</div>'; return; }
3794
+ var start = Math.min.apply(null, steps.map(function (e) { return Date.parse(e.ts) - e.ms; }));
3795
+ var end = Math.max.apply(null, steps.map(function (e) { return Date.parse(e.ts); }));
3796
+ var span = Math.max(1, end - start);
3797
+ target.innerHTML = '<div class="legend"><span><i class="sw" style="background:var(--bar)"></i>MCP tool</span><span><i class="sw" style="background:var(--hook)"></i>hook step</span><span><i class="sw" style="background:var(--cli)"></i>CLI command</span><span class="spacer"></span><span>' + esc(ms(span)) + " total</span></div>" +
3798
+ '<div class="wf">' + steps.map(function (e) {
3799
+ var s = Date.parse(e.ts) - e.ms;
3800
+ var left = ((s - start) / span) * 100, width = Math.max(0.8, (e.ms / span) * 100);
3801
+ return '<div class="step"><span class="name' + (e.ok ? "" : " bad") + '" title="' + esc(kindGroup(e.kind) + ": " + e.name) + '">' + (e.ok ? "" : "\u2715 ") + esc(e.feature) + '</span><span class="at">' + esc(offset(s - start)) + '</span><span class="lane"><span class="seg-bar" style="left:' + left.toFixed(2) + "%;width:" + width.toFixed(2) + "%;background:" + kindColor(e.kind) + '" data-tip="' + esc(e.feature + " \xB7 " + ms(e.ms) + (e.ok ? "" : " \xB7 error") + (e.tokensOut ? " \xB7 " + num(e.tokensOut) + " tokens out" : "")) + '"></span></span><span class="t">' + esc(ms(e.ms)) + "</span></div>";
3802
+ }).join("") + "</div>";
3803
+ }
3804
+
3805
+ function renderEvents() {
3806
+ var kinds = ["all"];
3807
+ summary.recent.forEach(function (e) { if (kinds.indexOf(e.kind) === -1) kinds.push(e.kind); });
3808
+ $("kindFilter").innerHTML = '<div class="seg" role="group" aria-label="Event kind">' + kinds.map(function (k) {
3809
+ return '<button data-kind="' + esc(k) + '" aria-pressed="' + (k === kindShown) + '">' + esc(k) + "</button>";
3810
+ }).join("") + "</div>";
3811
+ var list = summary.recent.filter(function (e) { return kindShown === "all" || e.kind === kindShown; });
3812
+ $("recent").innerHTML = list.length
3813
+ ? '<table><thead><tr><th>Received</th><th>Install</th><th>Kind</th><th>Name</th><th>Client</th><th>Version</th><th class="n">Time</th><th>Result</th></tr></thead><tbody>' +
3814
+ list.map(function (e) {
3815
+ return '<tr class="link" tabindex="0" data-install="' + esc(e.install) + '"><td>' + esc(new Date(e.receivedAt).toLocaleString()) + '</td><td class="mono">' + esc(e.install) + '</td><td><span class="sw" style="background:' + kindColor(e.kind) + '"></span> ' + esc(e.kind) +
3816
+ '</td><td class="mono">' + esc(e.name) + "</td><td>" + esc(e.client) + '</td><td class="mono">' + esc(e.version) + '</td><td class="n">' + ms(e.ms) + '</td><td class="' + (e.ok ? "" : "bad") + '">' + (e.ok ? "ok" : "\u2715 error") + "</td></tr>";
3628
3817
  }).join("") + "</tbody></table>"
3629
- : '<div class="empty">No events yet.</div>';
3818
+ : '<div class="empty-state">No events yet.</div>';
3630
3819
  }
3631
3820
 
3632
- function load() {
3633
- fetch(base + "api/summary?days=" + days).then(function (r) { return r.json(); }).then(function (s) { summary = s; render(); });
3821
+ function route() {
3822
+ var hash = location.hash.replace("#", "") || "overview";
3823
+ var view = hash.indexOf("install/") === 0 ? "install" : ["overview", "users", "events"].indexOf(hash) !== -1 ? hash : "overview";
3824
+ ["overview", "users", "install", "events"].forEach(function (v) { $("view-" + v).hidden = v !== view; });
3825
+ document.querySelectorAll(".tabs a").forEach(function (a) {
3826
+ var current = a.dataset.view === view || (view === "install" && a.dataset.view === "users");
3827
+ if (current) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current");
3828
+ });
3829
+ $("range").hidden = view === "install";
3830
+ if (view === "install") {
3831
+ var id = hash.slice(8);
3832
+ if (!install || install.install !== id) {
3833
+ install = null;
3834
+ renderInstall();
3835
+ get("api/install?id=" + encodeURIComponent(id)).then(function (data) { install = data || { install: id, missing: true }; renderInstall(); });
3836
+ } else renderInstall();
3837
+ } else render();
3634
3838
  }
3635
- fetch(base + "api/npm").then(function (r) { return r.json(); }).then(function (n) { npm = n; render(); });
3636
3839
 
3637
- document.getElementById("range").addEventListener("click", function (event) {
3840
+ function render() {
3841
+ if (!summary) return;
3842
+ $("updated").textContent = "updated " + new Date(summary.generatedAt).toLocaleTimeString();
3843
+ var hash = location.hash.replace("#", "") || "overview";
3844
+ if (hash === "users") renderUsers();
3845
+ else if (hash === "events") renderEvents();
3846
+ else if (hash.indexOf("install/") !== 0) renderOverview();
3847
+ }
3848
+
3849
+ function load() { get("api/summary?days=" + days).then(function (s) { if (s) { summary = s; render(); } }); }
3850
+ get("api/npm").then(function (n) { npm = n; if (summary) render(); });
3851
+
3852
+ $("range").addEventListener("click", function (event) {
3638
3853
  var button = event.target.closest("button");
3639
3854
  if (!button) return;
3640
3855
  days = Number(button.dataset.days);
3641
3856
  document.querySelectorAll("#range button").forEach(function (b) { b.setAttribute("aria-pressed", String(b === button)); });
3642
3857
  load();
3643
3858
  });
3644
- document.getElementById("csv").addEventListener("click", function () {
3859
+ $("userSearch").addEventListener("input", function () { if (summary) renderUsers(); });
3860
+ document.addEventListener("click", function (event) {
3861
+ var sort = event.target.closest("[data-sort]");
3862
+ if (sort) {
3863
+ var key = sort.dataset.sort;
3864
+ userSort = { key: key, dir: userSort.key === key ? -userSort.dir : -1 };
3865
+ renderUsers();
3866
+ return;
3867
+ }
3868
+ var row = event.target.closest("tr[data-install]");
3869
+ if (row) { location.hash = "install/" + row.dataset.install; return; }
3870
+ var session = event.target.closest("[data-session]");
3871
+ if (session) {
3872
+ selectedSession = session.dataset.session;
3873
+ document.querySelectorAll("#sessionList .session").forEach(function (b) { b.setAttribute("aria-pressed", String(b === session)); });
3874
+ renderWaterfall();
3875
+ return;
3876
+ }
3877
+ var kind = event.target.closest("[data-kind]");
3878
+ if (kind) { kindShown = kind.dataset.kind; renderEvents(); }
3879
+ });
3880
+ document.addEventListener("keydown", function (event) {
3881
+ var row = event.target.closest && event.target.closest("tr[data-install]");
3882
+ if (row && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); location.hash = "install/" + row.dataset.install; }
3883
+ });
3884
+ $("csv").addEventListener("click", function () {
3645
3885
  if (!summary) return;
3646
- var cols = ["install", "firstSeen", "lastSeen", "events", "sessions", "projects", "client", "version", "platform", "topFeature"];
3647
- var lines = [cols.join(",")].concat(summary.users.map(function (u) {
3886
+ var cols = USER_COLS.map(function (c) { return c[0]; });
3887
+ var lines = [cols.join(",")].concat(filteredUsers().map(function (u) {
3648
3888
  return cols.map(function (c) { return '"' + String(u[c]).replace(/"/g, '""') + '"'; }).join(",");
3649
3889
  }));
3650
3890
  var link = document.createElement("a");
3651
- link.href = URL.createObjectURL(new Blob([lines.join("\\n")], { type: "text/csv" }));
3891
+ link.href = URL.createObjectURL(new Blob([lines.join(NL)], { type: "text/csv" }));
3652
3892
  link.download = "codebuddy-users-" + new Date().toISOString().slice(0, 10) + ".csv";
3653
3893
  link.click();
3654
3894
  });
@@ -3657,10 +3897,16 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
3657
3897
  if (!text) { tip.style.display = "none"; return; }
3658
3898
  tip.textContent = text;
3659
3899
  tip.style.display = "block";
3660
- var x = Math.min(event.pageX + 12, document.documentElement.scrollWidth - tip.offsetWidth - 8);
3661
- tip.style.left = x + "px";
3662
- tip.style.top = (event.pageY - 32) + "px";
3900
+ tip.style.left = Math.min(event.pageX + 12, document.documentElement.scrollWidth - tip.offsetWidth - 8) + "px";
3901
+ tip.style.top = (event.pageY - 34) + "px";
3902
+ });
3903
+ var resizeTimer = 0;
3904
+ window.addEventListener("resize", function () {
3905
+ clearTimeout(resizeTimer);
3906
+ resizeTimer = setTimeout(function () { if (location.hash.indexOf("#install/") === 0) renderInstall(); else render(); }, 150);
3663
3907
  });
3908
+ window.addEventListener("hashchange", route);
3909
+ route();
3664
3910
  load();
3665
3911
  setInterval(load, 60000);
3666
3912
  })();
@@ -3700,6 +3946,7 @@ var batchSchema = z2.object({
3700
3946
  async function startCollector(options) {
3701
3947
  const { dataDir } = options;
3702
3948
  await mkdir2(dataDir, { recursive: true });
3949
+ const registry = await loadRegistry(dataDir);
3703
3950
  let summaryCache;
3704
3951
  let npmCache;
3705
3952
  const perInstall = /* @__PURE__ */ new Map();
@@ -3743,6 +3990,14 @@ async function startCollector(options) {
3743
3990
  return;
3744
3991
  }
3745
3992
  const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
3993
+ if (!registry.has(batch.installId)) {
3994
+ registry.set(batch.installId, receivedAt);
3995
+ await appendFile(
3996
+ join6(dataDir, "installs.jsonl"),
3997
+ `${JSON.stringify({ installId: batch.installId, firstSeen: receivedAt })}
3998
+ `
3999
+ );
4000
+ }
3746
4001
  const { events, ...meta } = batch;
3747
4002
  const lines = events.map((event) => JSON.stringify({ ...meta, ...event, receivedAt }));
3748
4003
  await appendFile(
@@ -3790,12 +4045,29 @@ async function startCollector(options) {
3790
4045
  summaryCache = {
3791
4046
  at: Date.now(),
3792
4047
  days,
3793
- value: summarize(await readEvents(dataDir, days), days)
4048
+ value: summarize(
4049
+ await readEvents(dataDir, Math.max(days, COHORT_WEEKS * 7)),
4050
+ days,
4051
+ Date.now(),
4052
+ registry
4053
+ )
3794
4054
  };
3795
4055
  }
3796
4056
  json2(summaryCache.value);
3797
4057
  return;
3798
4058
  }
4059
+ if (url.pathname === "/api/install") {
4060
+ const trace = installTrace(
4061
+ await readEvents(dataDir, 90),
4062
+ (url.searchParams.get("id") ?? "").toLowerCase()
4063
+ );
4064
+ if (!trace) {
4065
+ response.writeHead(404, { "content-type": "application/json" }).end("null");
4066
+ return;
4067
+ }
4068
+ json2(trace);
4069
+ return;
4070
+ }
3799
4071
  if (url.pathname === "/api/npm") {
3800
4072
  if (!npmCache || Date.now() - npmCache.at > 36e5) {
3801
4073
  npmCache = { at: Date.now(), value: await npmDownloads(options.fetchImpl ?? fetch) };
@@ -3863,64 +4135,125 @@ async function readEvents(dataDir, days, now = Date.now()) {
3863
4135
  }
3864
4136
  return events;
3865
4137
  }
3866
- function summarize(events, days, now = Date.now()) {
3867
- const day = (iso) => iso.slice(0, 10);
3868
- const today = day(new Date(now).toISOString());
3869
- const activeWithin = (window) => {
3870
- const since = day(new Date(now - (window - 1) * 864e5).toISOString());
3871
- return new Set(events.filter((e) => day(e.receivedAt) >= since).map((e) => e.installId)).size;
4138
+ var DAY_MS = 864e5;
4139
+ var dayOf = (iso) => iso.slice(0, 10);
4140
+ var dayNumber = (iso) => Math.floor(Date.parse(iso) / DAY_MS);
4141
+ var COHORT_WEEKS = 8;
4142
+ function featureOf(event) {
4143
+ if (event.kind === "tool") return event.name;
4144
+ if (event.kind === "cli") return `cli:${event.name}`;
4145
+ return event.kind;
4146
+ }
4147
+ var GOT_CONTEXT = (e) => e.kind === "recall" || e.kind === "tool" && /^(context_(pack|bootstrap|before_edit)|recall)$/.test(e.name);
4148
+ var SAVED_MEMORY = (e) => e.kind === "capture" || e.kind === "tool" && /^(context_after_turn|remember)$/.test(e.name);
4149
+ function summarize(events, days, now = Date.now(), registry = /* @__PURE__ */ new Map()) {
4150
+ const today = Math.floor(now / DAY_MS);
4151
+ const rangeStart = today - days + 1;
4152
+ const firstSeenOf = (install, fallback) => registry.get(install) ?? fallback;
4153
+ const activeDays = /* @__PURE__ */ new Map();
4154
+ for (const event of events) {
4155
+ const set = activeDays.get(event.installId) ?? /* @__PURE__ */ new Set();
4156
+ set.add(dayNumber(event.receivedAt));
4157
+ activeDays.set(event.installId, set);
4158
+ }
4159
+ const activeBetween = (from, to) => {
4160
+ let count = 0;
4161
+ for (const set of activeDays.values()) {
4162
+ for (const d of set) {
4163
+ if (d >= from && d <= to) {
4164
+ count += 1;
4165
+ break;
4166
+ }
4167
+ }
4168
+ }
4169
+ return count;
3872
4170
  };
4171
+ const inRange = events.filter((e) => dayNumber(e.receivedAt) >= rangeStart);
3873
4172
  const daily = /* @__PURE__ */ new Map();
3874
- for (let index = days - 1; index >= 0; index -= 1) {
3875
- daily.set(day(new Date(now - index * 864e5).toISOString()), {
3876
- installs: /* @__PURE__ */ new Set(),
3877
- events: 0
3878
- });
3879
- }
3880
- const firstSeen = /* @__PURE__ */ new Map();
3881
- const tools = /* @__PURE__ */ new Map();
3882
- const kinds = /* @__PURE__ */ new Map();
4173
+ for (let d = rangeStart; d <= today; d += 1) daily.set(d, { installs: /* @__PURE__ */ new Set(), events: 0 });
4174
+ const features = /* @__PURE__ */ new Map();
3883
4175
  const latest = /* @__PURE__ */ new Map();
3884
4176
  const users = /* @__PURE__ */ new Map();
4177
+ const funnel = {
4178
+ active: /* @__PURE__ */ new Set(),
4179
+ context: /* @__PURE__ */ new Set(),
4180
+ memory: /* @__PURE__ */ new Set()
4181
+ };
3885
4182
  let errors = 0;
3886
4183
  let tokensOut = 0;
3887
- for (const event of events) {
3888
- const bucket = daily.get(day(event.receivedAt));
4184
+ for (const event of inRange) {
4185
+ const d = dayNumber(event.receivedAt);
4186
+ const bucket = daily.get(d);
3889
4187
  if (bucket) {
3890
4188
  bucket.installs.add(event.installId);
3891
4189
  bucket.events += 1;
3892
4190
  }
3893
- const seen = firstSeen.get(event.installId);
3894
- if (!seen || event.receivedAt < seen) firstSeen.set(event.installId, event.receivedAt);
3895
4191
  latest.set(event.installId, event);
4192
+ const feature = featureOf(event);
3896
4193
  const user = users.get(event.installId) ?? {
4194
+ firstEvent: event.receivedAt,
3897
4195
  lastSeen: event.receivedAt,
3898
4196
  events: 0,
4197
+ errors: 0,
3899
4198
  sessions: /* @__PURE__ */ new Set(),
3900
4199
  projects: /* @__PURE__ */ new Set(),
3901
- features: /* @__PURE__ */ new Map()
4200
+ features: /* @__PURE__ */ new Map(),
4201
+ days: /* @__PURE__ */ new Set()
3902
4202
  };
3903
4203
  if (event.receivedAt > user.lastSeen) user.lastSeen = event.receivedAt;
3904
4204
  user.events += 1;
4205
+ if (!event.ok) user.errors += 1;
3905
4206
  if (event.session) user.sessions.add(event.session);
3906
4207
  user.projects.add(event.project);
3907
- const feature = event.kind === "tool" ? event.name : event.kind;
3908
4208
  user.features.set(feature, (user.features.get(feature) ?? 0) + 1);
4209
+ user.days.add(d);
3909
4210
  users.set(event.installId, user);
3910
- kinds.set(event.kind, (kinds.get(event.kind) ?? 0) + 1);
4211
+ funnel.active.add(event.installId);
4212
+ if (GOT_CONTEXT(event)) funnel.context.add(event.installId);
4213
+ if (SAVED_MEMORY(event)) funnel.memory.add(event.installId);
3911
4214
  if (!event.ok) errors += 1;
3912
4215
  tokensOut += event.tokensOut ?? 0;
3913
- const key = event.kind === "tool" ? event.name : `${event.kind}`;
3914
- const tool = tools.get(key) ?? { calls: 0, errors: 0, installs: /* @__PURE__ */ new Set(), ms: [] };
3915
- tool.calls += 1;
3916
- if (!event.ok) tool.errors += 1;
3917
- tool.installs.add(event.installId);
3918
- tool.ms.push(event.ms);
3919
- tools.set(key, tool);
4216
+ const stats = features.get(feature) ?? { calls: 0, errors: 0, installs: /* @__PURE__ */ new Set(), ms: [] };
4217
+ stats.calls += 1;
4218
+ if (!event.ok) stats.errors += 1;
4219
+ stats.installs.add(event.installId);
4220
+ stats.ms.push(event.ms);
4221
+ features.set(feature, stats);
4222
+ }
4223
+ const firstSeenDay = /* @__PURE__ */ new Map();
4224
+ for (const [install, seen] of registry) firstSeenDay.set(install, dayNumber(seen));
4225
+ for (const [install, user] of users) {
4226
+ if (!firstSeenDay.has(install)) firstSeenDay.set(install, dayNumber(user.firstEvent));
4227
+ }
4228
+ for (const [install, set] of activeDays) {
4229
+ if (!firstSeenDay.has(install)) firstSeenDay.set(install, Math.min(...set));
3920
4230
  }
3921
4231
  const newPerDay = /* @__PURE__ */ new Map();
3922
- for (const seen of firstSeen.values())
3923
- newPerDay.set(day(seen), (newPerDay.get(day(seen)) ?? 0) + 1);
4232
+ for (const d of firstSeenDay.values()) newPerDay.set(d, (newPerDay.get(d) ?? 0) + 1);
4233
+ let cumulative = [...firstSeenDay.values()].filter((d) => d < rangeStart).length;
4234
+ const cohorts = [];
4235
+ for (let age = COHORT_WEEKS - 1; age >= 0; age -= 1) {
4236
+ const from = today - (age + 1) * 7 + 1;
4237
+ const members = [...firstSeenDay].filter(([, d]) => d >= from && d < from + 7).map(([install]) => install);
4238
+ const weeks = [];
4239
+ for (let week = 0; week <= age; week += 1) {
4240
+ const wFrom = from + week * 7;
4241
+ const active = members.filter((install) => {
4242
+ const set = activeDays.get(install);
4243
+ if (!set) return week === 0;
4244
+ for (const d of set) if (d >= wFrom && d < wFrom + 7) return true;
4245
+ return false;
4246
+ }).length;
4247
+ weeks.push(members.length ? active / members.length : 0);
4248
+ }
4249
+ cohorts.push({
4250
+ start: new Date(from * DAY_MS).toISOString().slice(0, 10),
4251
+ size: members.length,
4252
+ weeks
4253
+ });
4254
+ }
4255
+ const week1 = cohorts.filter((c) => c.size > 0 && c.weeks.length > 1);
4256
+ const week1Retention = week1.length ? week1.reduce((sum, c) => sum + (c.weeks[1] ?? 0) * c.size, 0) / week1.reduce((sum, c) => sum + c.size, 0) : null;
3924
4257
  const share = (pick) => {
3925
4258
  const counts = /* @__PURE__ */ new Map();
3926
4259
  for (const event of latest.values()) {
@@ -3932,47 +4265,72 @@ function summarize(events, days, now = Date.now()) {
3932
4265
  const sorted = [...values].sort((a, b) => a - b);
3933
4266
  return sorted[Math.floor(sorted.length / 2)] ?? 0;
3934
4267
  };
4268
+ const p95 = (values) => {
4269
+ const sorted = [...values].sort((a, b) => a - b);
4270
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
4271
+ };
4272
+ const wau = activeBetween(today - 6, today);
4273
+ const wauPrevious = activeBetween(today - 13, today - 7);
4274
+ const returned = [...users.values()].filter((user) => user.days.size >= 2).length;
3935
4275
  return {
3936
4276
  generatedAt: new Date(now).toISOString(),
3937
4277
  days,
3938
4278
  totals: {
3939
- installs: firstSeen.size,
3940
- projects: new Set(events.map((e) => `${e.installId}:${e.project}`)).size,
3941
- sessions: new Set(events.filter((e) => e.session).map((e) => e.session)).size,
3942
- events: events.length,
3943
- errorRate: events.length ? errors / events.length : 0,
4279
+ allTimeInstalls: firstSeenDay.size,
4280
+ installs: users.size,
4281
+ newInRange: [...firstSeenDay.values()].filter((d) => d >= rangeStart).length,
4282
+ newToday: newPerDay.get(today) ?? 0,
4283
+ projects: new Set(inRange.map((e) => `${e.installId}:${e.project}`)).size,
4284
+ sessions: new Set(inRange.filter((e) => e.session).map((e) => e.session)).size,
4285
+ events: inRange.length,
4286
+ errorRate: inRange.length ? errors / inRange.length : 0,
3944
4287
  tokensOut,
3945
- active1d: activeWithin(1),
3946
- active7d: activeWithin(7),
3947
- active30d: activeWithin(30),
3948
- newToday: newPerDay.get(today) ?? 0
4288
+ dau: activeBetween(today, today),
4289
+ wau,
4290
+ wauPrevious,
4291
+ mau: activeBetween(today - 29, today),
4292
+ week1Retention
3949
4293
  },
3950
- daily: [...daily].map(([date, bucket]) => ({
3951
- date,
3952
- installs: bucket.installs.size,
3953
- events: bucket.events,
3954
- newInstalls: newPerDay.get(date) ?? 0
3955
- })),
3956
- features: [...tools].map(([label, tool]) => ({
4294
+ daily: [...daily].map(([d, bucket]) => {
4295
+ cumulative += newPerDay.get(d) ?? 0;
4296
+ return {
4297
+ date: new Date(d * DAY_MS).toISOString().slice(0, 10),
4298
+ installs: bucket.installs.size,
4299
+ events: bucket.events,
4300
+ newInstalls: newPerDay.get(d) ?? 0,
4301
+ totalInstalls: cumulative
4302
+ };
4303
+ }),
4304
+ /** Installs active in the range that reached each step. */
4305
+ funnel: [
4306
+ { label: "Active", installs: funnel.active.size },
4307
+ { label: "Received context", installs: funnel.context.size },
4308
+ { label: "Saved memory", installs: funnel.memory.size },
4309
+ { label: "Came back another day", installs: returned }
4310
+ ],
4311
+ cohorts,
4312
+ features: [...features].map(([label, stats]) => ({
3957
4313
  label,
3958
- calls: tool.calls,
3959
- installs: tool.installs.size,
3960
- errorRate: tool.errors / tool.calls,
3961
- p50ms: median(tool.ms)
3962
- })).sort((a, b) => b.calls - a.calls).slice(0, 20),
3963
- kinds: [...kinds].map(([label, count]) => ({ label, count })).sort((a, b) => b.count - a.count),
4314
+ calls: stats.calls,
4315
+ installs: stats.installs.size,
4316
+ errorRate: stats.errors / stats.calls,
4317
+ p50ms: median(stats.ms),
4318
+ p95ms: p95(stats.ms)
4319
+ })).sort((a, b) => b.calls - a.calls).slice(0, 25),
3964
4320
  clients: share((e) => e.clients.length ? e.clients : ["unknown"]),
3965
4321
  versions: share((e) => [e.version]),
3966
4322
  platforms: share((e) => [`${e.platform}/${e.arch}`]),
3967
4323
  nodes: share((e) => [`node ${e.node}`]),
3968
- /** One row per install, most recently active first. */
4324
+ /** One row per install active in the range, most recently active first. */
3969
4325
  users: [...users].map(([installId, user]) => {
3970
4326
  const last = latest.get(installId);
3971
4327
  return {
3972
4328
  install: installId.slice(0, 8),
3973
- firstSeen: firstSeen.get(installId) ?? user.lastSeen,
4329
+ firstSeen: firstSeenOf(installId, user.firstEvent),
3974
4330
  lastSeen: user.lastSeen,
4331
+ activeDays: user.days.size,
3975
4332
  events: user.events,
4333
+ errors: user.errors,
3976
4334
  sessions: user.sessions.size,
3977
4335
  projects: user.projects.size,
3978
4336
  client: last?.clients.join(",") || "unknown",
@@ -3981,7 +4339,7 @@ function summarize(events, days, now = Date.now()) {
3981
4339
  topFeature: [...user.features].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ""
3982
4340
  };
3983
4341
  }).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen)).slice(0, 1e3),
3984
- recent: events.slice(-50).reverse().map((e) => ({
4342
+ recent: inRange.slice(-50).reverse().map((e) => ({
3985
4343
  receivedAt: e.receivedAt,
3986
4344
  install: e.installId.slice(0, 8),
3987
4345
  kind: e.kind,
@@ -3993,6 +4351,77 @@ function summarize(events, days, now = Date.now()) {
3993
4351
  }))
3994
4352
  };
3995
4353
  }
4354
+ function installTrace(events, prefix) {
4355
+ const own = events.filter((e) => e.installId.startsWith(prefix));
4356
+ const first = own[0];
4357
+ if (!first || !/^[0-9a-f]{8}$/.test(prefix)) return null;
4358
+ const sessions = /* @__PURE__ */ new Map();
4359
+ const perDay = /* @__PURE__ */ new Map();
4360
+ for (const event of own) {
4361
+ perDay.set(dayOf(event.receivedAt), (perDay.get(dayOf(event.receivedAt)) ?? 0) + 1);
4362
+ const key = event.session ?? "none";
4363
+ const session = sessions.get(key) ?? {
4364
+ start: event.ts,
4365
+ end: event.ts,
4366
+ events: 0,
4367
+ errors: 0,
4368
+ ms: 0,
4369
+ features: /* @__PURE__ */ new Set()
4370
+ };
4371
+ if (event.ts < session.start) session.start = event.ts;
4372
+ if (event.ts > session.end) session.end = event.ts;
4373
+ session.events += 1;
4374
+ if (!event.ok) session.errors += 1;
4375
+ session.ms += event.ms;
4376
+ session.features.add(featureOf(event));
4377
+ sessions.set(key, session);
4378
+ }
4379
+ const last = own[own.length - 1] ?? first;
4380
+ return {
4381
+ install: prefix,
4382
+ client: last.clients.join(",") || "unknown",
4383
+ version: last.version,
4384
+ platform: `${last.platform}/${last.arch}`,
4385
+ node: last.node,
4386
+ projects: new Set(own.map((e) => e.project)).size,
4387
+ perDay: [...perDay].map(([date, count]) => ({ date, events: count })),
4388
+ sessions: [...sessions].map(([id, session]) => ({ id, ...session, features: [...session.features] })).sort((a, b) => b.end.localeCompare(a.end)).slice(0, 50),
4389
+ events: own.slice(-1e3).map((e) => ({
4390
+ ts: e.ts,
4391
+ kind: e.kind,
4392
+ name: e.name,
4393
+ feature: featureOf(e),
4394
+ ms: e.ms,
4395
+ ok: e.ok,
4396
+ session: e.session ?? "none",
4397
+ tokensOut: e.tokensOut ?? 0
4398
+ }))
4399
+ };
4400
+ }
4401
+ async function loadRegistry(dataDir) {
4402
+ const registry = /* @__PURE__ */ new Map();
4403
+ const path = join6(dataDir, "installs.jsonl");
4404
+ const raw = await readFile4(path, "utf8").catch(() => null);
4405
+ if (raw !== null) {
4406
+ for (const line of raw.split("\n")) {
4407
+ try {
4408
+ const entry = JSON.parse(line);
4409
+ if (!registry.has(entry.installId)) registry.set(entry.installId, entry.firstSeen);
4410
+ } catch {
4411
+ }
4412
+ }
4413
+ return registry;
4414
+ }
4415
+ for (const event of await readEvents(dataDir, 3650)) {
4416
+ if (!registry.has(event.installId)) registry.set(event.installId, event.receivedAt);
4417
+ }
4418
+ const lines = [...registry].map(
4419
+ ([installId, firstSeen]) => JSON.stringify({ installId, firstSeen })
4420
+ );
4421
+ await appendFile(path, lines.length ? `${lines.join("\n")}
4422
+ ` : "");
4423
+ return registry;
4424
+ }
3996
4425
  async function npmDownloads(fetchImpl) {
3997
4426
  const to = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3998
4427
  const from = new Date(Date.now() - 364 * 864e5).toISOString().slice(0, 10);
@@ -4810,7 +5239,12 @@ function createCli() {
4810
5239
  registerGitHubCommands(program, runSafely);
4811
5240
  registerSymbolsCommand(program, runSafely);
4812
5241
  registerTelemetryCommands(program, runSafely);
4813
- program.hook("preAction", () => showTelemetryNoticeOnce());
5242
+ let started = Date.now();
5243
+ program.hook("preAction", () => {
5244
+ started = Date.now();
5245
+ return showTelemetryNoticeOnce();
5246
+ });
5247
+ program.hook("postAction", (_program, command) => traceCliCommand(command, started));
4814
5248
  program.action(async () => {
4815
5249
  await runSafely(() => runUseCommand());
4816
5250
  });
@@ -4951,4 +5385,4 @@ function printDbDoctor(report) {
4951
5385
  export {
4952
5386
  createCli
4953
5387
  };
4954
- //# sourceMappingURL=main-QUASSFP7.js.map
5388
+ //# sourceMappingURL=main-YJKSYFPT.js.map