agent-trace 0.3.0 → 0.4.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 (2) hide show
  1. package/agent-trace.cjs +2470 -51
  2. package/package.json +1 -1
package/agent-trace.cjs CHANGED
@@ -24010,6 +24010,7 @@ var import_node_http = __toESM(require("node:http"));
24010
24010
  // packages/dashboard/src/web-render.ts
24011
24011
  function renderDashboardHtml(options = {}) {
24012
24012
  const title = options.title ?? "agent-trace dashboard";
24013
+ const currentUserEmailJson = options.currentUserEmail !== void 0 ? JSON.stringify(options.currentUserEmail) : "null";
24013
24014
  return `<!doctype html>
24014
24015
  <html lang="en">
24015
24016
  <head>
@@ -24141,7 +24142,8 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24141
24142
  .modal-close:hover{color:var(--text-primary);background:var(--panel-hover)}
24142
24143
  .modal label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px;margin-top:12px}
24143
24144
  .modal select,.modal input{width:100%;box-sizing:border-box;padding:8px 10px;background:var(--bg);border:1px solid var(--panel-border);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:inherit}
24144
- .modal select:focus,.modal input:focus{outline:none;border-color:var(--green)}
24145
+ .modal select:focus,.modal input:focus,.modal textarea:focus{outline:none;border-color:var(--green)}
24146
+ .modal textarea{width:100%;box-sizing:border-box;padding:8px 10px;background:var(--bg);border:1px solid var(--panel-border);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:inherit;resize:vertical;min-height:80px}
24145
24147
  .modal-actions{margin-top:16px;display:flex;gap:8px;align-items:center}
24146
24148
  .modal-save{padding:8px 16px;background:var(--green);color:#000;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
24147
24149
  .modal-save:hover{opacity:.9}
@@ -24180,8 +24182,92 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24180
24182
  .hljs-property{color:#93c5fd}
24181
24183
  .hljs-addition{color:#4ade80;background:rgba(74,222,128,.08)}
24182
24184
  .hljs-deletion{color:#f87171;background:rgba(248,113,113,.08)}
24183
- @media(max-width:1200px){.mg{grid-template-columns:repeat(2,minmax(0,1fr))}.sg{grid-template-columns:1fr}}
24184
- @media(max-width:760px){.shell{padding:12px 8px 24px}.mg{grid-template-columns:1fr}th:nth-child(5),td:nth-child(5),th:nth-child(7),td:nth-child(7){display:none}.erow{grid-template-columns:18px 1fr}.emeta{grid-column:2}.pg-stats{display:none}}
24185
+ .tab-bar{display:flex;gap:0;margin-top:10px;border-bottom:1px solid var(--line)}
24186
+ .tab-btn{padding:6px 14px;font-size:12px;font-weight:600;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;font-family:inherit;letter-spacing:.02em;transition:color .15s,border-color .15s}
24187
+ .tab-btn:hover{color:var(--text-primary)}
24188
+ .tab-btn.active{color:var(--green);border-bottom-color:var(--green)}
24189
+ .tab-content{display:none}.tab-content.active{display:block}
24190
+ .team-mg{margin-top:14px;display:grid;gap:8px;grid-template-columns:repeat(5,minmax(0,1fr))}
24191
+ .budget-bar{margin-top:12px;padding:10px 12px;border:1px solid var(--panel-border);border-radius:8px;background:var(--panel)}
24192
+ .budget-track{height:8px;border-radius:4px;background:var(--panel-muted);margin-top:6px;overflow:hidden}
24193
+ .budget-fill{height:100%;border-radius:4px;transition:width .3s}
24194
+ .budget-fill.green{background:var(--green)}.budget-fill.orange{background:var(--orange)}.budget-fill.red{background:var(--red)}.budget-fill.pulse{animation:pulse 1s infinite}
24195
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
24196
+ .budget-label{font-size:12px;color:var(--text-muted);display:flex;justify-content:space-between;align-items:center}
24197
+ .budget-set-btn{padding:4px 10px;font-size:11px;background:var(--panel-muted);border:1px solid var(--panel-border);border-radius:4px;color:var(--text-muted);cursor:pointer;font-family:inherit}
24198
+ .budget-set-btn:hover{border-color:var(--green);color:var(--green)}
24199
+ .team-table{width:100%;border-collapse:collapse}
24200
+ .team-table th,.team-table td{padding:7px 8px;font-size:12px;border-bottom:1px solid var(--line);text-align:left}
24201
+ .team-table th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:500}
24202
+ .team-row{cursor:pointer}.team-row:hover{background:var(--panel-hover)}
24203
+ .time-range{display:flex;gap:4px;margin-left:auto}
24204
+ .time-range-btn{padding:3px 8px;font-size:10px;background:var(--panel-muted);border:1px solid var(--panel-border);border-radius:4px;color:var(--text-muted);cursor:pointer;font-family:inherit}
24205
+ .time-range-btn.active{border-color:var(--green);color:var(--green)}
24206
+ .filter-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:12px;font-weight:600;border:1px solid rgba(74,222,128,.4);color:var(--green);border-radius:6px;margin-left:8px;cursor:pointer;background:var(--green-dim)}
24207
+ .filter-chip:hover{background:rgba(74,222,128,.2)}
24208
+ .back-team-link{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:11px;border:1px solid var(--line);color:var(--text-muted);border-radius:6px;margin-left:8px;cursor:pointer}
24209
+ .back-team-link:hover{background:var(--panel-hover);color:var(--text-primary)}
24210
+ .auth-gate{padding:40px 20px;text-align:center}
24211
+ .auth-gate h2{font-size:16px;color:var(--text-primary);margin-bottom:8px}
24212
+ .auth-gate p{font-size:12px;color:var(--text-muted);margin-bottom:16px}
24213
+ .auth-gate input{width:300px;max-width:80vw;padding:8px 10px;background:var(--bg);border:1px solid var(--panel-border);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:inherit}
24214
+ .auth-gate input:focus{outline:none;border-color:var(--green)}
24215
+ .auth-gate button{margin-top:8px;padding:8px 20px;background:var(--green);color:#000;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
24216
+ .auth-gate .auth-error{color:var(--red);font-size:11px;margin-top:8px}
24217
+ /* Insights tab styles */
24218
+ .ins-grid{display:grid;gap:10px;grid-template-columns:1fr 1fr;margin-top:14px}
24219
+ .ins-grid-3{display:grid;gap:10px;grid-template-columns:1fr 1fr 1fr;margin-top:14px}
24220
+ .ins-full{grid-column:1/-1}
24221
+ .ins-summary{margin-top:14px;display:grid;gap:8px;grid-template-columns:repeat(6,minmax(0,1fr))}
24222
+ .ins-panel{border:1px solid var(--panel-border);border-radius:8px;background:var(--panel);overflow:hidden}
24223
+ .ins-ph{padding:10px 12px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}
24224
+ .ins-ph h3{margin:0;font-size:13px;font-weight:600;letter-spacing:-.01em}
24225
+ .ins-ph p{margin:2px 0 0;font-size:11px;color:var(--text-dim)}
24226
+ .ins-body{padding:12px}
24227
+ .ins-loading{padding:40px 20px;text-align:center;color:var(--text-dim);font-size:12px}
24228
+ .svg-chart{width:100%;overflow:visible}
24229
+ .heatmap-grid{display:grid;grid-template-columns:28px repeat(24,1fr);gap:1px;font-size:9px}
24230
+ .heatmap-cell{aspect-ratio:1;border-radius:2px;background:var(--panel-muted);transition:background .15s}
24231
+ .heatmap-label{display:flex;align-items:center;justify-content:center;color:var(--text-dim);font-size:9px}
24232
+ .hbar-row{display:flex;align-items:center;gap:8px;padding:3px 0}
24233
+ .hbar-label{flex-shrink:0;width:90px;font-size:11px;color:var(--text-muted);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
24234
+ .hbar-track{flex:1;height:16px;background:var(--panel-muted);border-radius:3px;overflow:hidden}
24235
+ .hbar-fill{height:100%;border-radius:3px;transition:width .3s}
24236
+ .hbar-value{flex-shrink:0;width:52px;font-size:11px;color:var(--text-dim);font-variant-numeric:tabular-nums}
24237
+ .member-card{border:1px solid var(--panel-border);border-radius:8px;background:var(--panel);overflow:hidden;margin-bottom:10px}
24238
+ .member-card-hd{padding:10px 12px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none}
24239
+ .member-card-hd:hover{background:var(--panel-hover)}
24240
+ .member-card-name{font-size:13px;font-weight:600;color:var(--text-primary)}
24241
+ .member-card-email{font-size:10px;color:var(--text-dim)}
24242
+ .member-card-stats{display:flex;gap:6px;font-size:11px}
24243
+ .member-card-body{display:none;border-top:1px solid var(--line);padding:12px}
24244
+ .member-card-body.open{display:block}
24245
+ .member-card-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px}
24246
+ .member-card-metric{padding:8px;border:1px solid var(--panel-border);border-radius:6px;background:var(--panel-muted)}
24247
+ .member-card-metric .m-label{font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim)}
24248
+ .member-card-metric .m-val{font-size:16px;font-weight:700;margin-top:2px;font-variant-numeric:tabular-nums}
24249
+ .member-mini-chart{display:flex;align-items:end;gap:1px;height:40px}
24250
+ .member-mini-bar{flex:1;border-radius:1px 1px 0 0;min-height:1px;transition:height .3s}
24251
+ .donut-legend{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
24252
+ .donut-legend-item{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-muted)}
24253
+ .donut-legend-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0}
24254
+ .ins-score{width:80px;height:80px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;border:3px solid;margin:0 auto 8px}
24255
+ .ins-ai-panel{border:1px solid rgba(192,132,252,.2);border-radius:8px;padding:16px;background:rgba(192,132,252,.04);margin-top:14px}
24256
+ .ins-ai-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
24257
+ .ins-ai-title{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--purple)}
24258
+ .configure-analysis-btn{background:var(--panel-muted);border:1px solid var(--panel-border);border-radius:6px;padding:4px 10px;cursor:pointer;color:var(--text-muted);font-size:11px;font-family:inherit;transition:border-color .15s,color .15s}
24259
+ .configure-analysis-btn:hover{border-color:var(--purple);color:var(--purple)}
24260
+ .context-indicator{font-size:10px;color:var(--text-dim);margin-left:8px}
24261
+ .ins-ai-body{font-size:12px;color:var(--text-primary);line-height:1.7}
24262
+ .ins-ai-section{margin-top:12px}
24263
+ .ins-ai-section h4{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin:0 0 4px}
24264
+ .ins-ai-item{font-size:12px;color:var(--text-muted);padding:2px 0 2px 14px;position:relative;line-height:1.6}
24265
+ .ins-ai-item::before{content:'>';position:absolute;left:2px;color:var(--purple)}
24266
+ .ins-forecast{border:1px solid rgba(251,146,60,.2);border-radius:8px;padding:12px;background:rgba(251,146,60,.04)}
24267
+ .ins-forecast-val{font-size:24px;font-weight:700;color:var(--orange)}
24268
+ .ins-forecast-label{font-size:11px;color:var(--text-dim);margin-top:2px}
24269
+ @media(max-width:1200px){.mg{grid-template-columns:repeat(2,minmax(0,1fr))}.sg{grid-template-columns:1fr}.team-mg{grid-template-columns:repeat(2,minmax(0,1fr))}.ins-grid{grid-template-columns:1fr}.ins-grid-3{grid-template-columns:1fr}.ins-summary{grid-template-columns:repeat(3,minmax(0,1fr))}}
24270
+ @media(max-width:760px){.shell{padding:12px 8px 24px}.mg{grid-template-columns:1fr}.team-mg{grid-template-columns:1fr}.ins-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.member-card-metrics{grid-template-columns:repeat(2,1fr)}th:nth-child(5),td:nth-child(5),th:nth-child(7),td:nth-child(7){display:none}.erow{grid-template-columns:18px 1fr}.emeta{grid-column:2}.pg-stats{display:none}}
24185
24271
  </style>
24186
24272
  </head>
24187
24273
  <body>
@@ -24190,8 +24276,21 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24190
24276
  <h1>${title}</h1>
24191
24277
  <p>session observability for coding agents</p>
24192
24278
  <button class="settings-btn" onclick="openSettings()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 6 0Z"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/></svg>AI</button>
24279
+ <div id="tab-bar" class="tab-bar" style="display:none">
24280
+ <button class="tab-btn active" onclick="switchTab('sessions')">Sessions</button>
24281
+ <button class="tab-btn" onclick="switchTab('team')" id="team-tab-btn">Team</button>
24282
+ <button class="tab-btn" onclick="switchTab('insights')" id="insights-tab-btn">Insights</button>
24283
+ </div>
24193
24284
  <div id="status" class="status-banner">Connecting...</div>
24194
24285
  </section>
24286
+ <div id="auth-gate" class="auth-gate" style="display:none">
24287
+ <h2>Authentication Required</h2>
24288
+ <p>This team server requires an auth token.</p>
24289
+ <input type="password" id="auth-token-input" placeholder="Enter team auth token..." onkeydown="if(event.key==='Enter')submitAuthToken()"/>
24290
+ <br/>
24291
+ <button onclick="submitAuthToken()">Authenticate</button>
24292
+ <div class="auth-error" id="auth-error"></div>
24293
+ </div>
24195
24294
  <div id="settings-modal" class="modal-overlay" onclick="if(event.target===this)closeSettings()">
24196
24295
  <div class="modal">
24197
24296
  <button class="modal-close" onclick="closeSettings()">&times;</button>
@@ -24214,6 +24313,7 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24214
24313
  </div>
24215
24314
  </div>
24216
24315
  </div>
24316
+ <div id="tab-sessions" class="tab-content active">
24217
24317
  <section class="mg">
24218
24318
  <article class="mc"><div class="label">Sessions</div><div class="val green" id="m-sessions">0</div></article>
24219
24319
  <article class="mc"><div class="label">Total Cost</div><div class="val orange" id="m-cost">$0.00</div></article>
@@ -24223,7 +24323,7 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24223
24323
  </section>
24224
24324
  <section class="sg">
24225
24325
  <section class="panel">
24226
- <header class="ph"><div><h2>Sessions</h2><p id="stream-label">...</p></div></header>
24326
+ <header class="ph"><div><h2>My Sessions</h2><p id="stream-label">...</p></div></header>
24227
24327
  <div class="pc"><div id="sessions-area" class="empty">Loading...</div></div>
24228
24328
  </section>
24229
24329
  <section class="panel">
@@ -24235,6 +24335,119 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24235
24335
  <header class="ph"><div><h2>Session Replay</h2><p id="replay-label">select a session</p></div></header>
24236
24336
  <div class="pc" id="replay-area"><div class="empty">No session selected.</div></div>
24237
24337
  </section>
24338
+ </div>
24339
+ <div id="tab-team" class="tab-content">
24340
+ <section class="team-mg">
24341
+ <article class="mc"><div class="label">Members</div><div class="val cyan" id="tm-members">0</div></article>
24342
+ <article class="mc"><div class="label">Total Cost</div><div class="val orange" id="tm-cost">$0.00</div></article>
24343
+ <article class="mc"><div class="label">Sessions</div><div class="val green" id="tm-sessions">0</div></article>
24344
+ <article class="mc"><div class="label">Commits</div><div class="val green" id="tm-commits">0</div></article>
24345
+ <article class="mc"><div class="label">$/Commit</div><div class="val" id="tm-cpc">$0.00</div></article>
24346
+ </section>
24347
+ <div id="tm-budget-area" class="budget-bar" style="display:none">
24348
+ <div class="budget-label"><span id="tm-budget-label">Budget</span><button class="budget-set-btn" onclick="openBudgetModal()">Set Budget</button></div>
24349
+ <div class="budget-track"><div class="budget-fill green" id="tm-budget-fill" style="width:0%"></div></div>
24350
+ </div>
24351
+ <div id="tm-no-budget" style="margin-top:12px;text-align:right"><button class="budget-set-btn" onclick="openBudgetModal()">Set Budget</button></div>
24352
+ <section class="sg" style="margin-top:14px">
24353
+ <section class="panel">
24354
+ <header class="ph"><div><h2>Team Members</h2><p id="tm-period-label"></p></div><div class="time-range" id="tm-time-range">
24355
+ <button class="time-range-btn" onclick="setTeamRange('week')">This week</button>
24356
+ <button class="time-range-btn" onclick="setTeamRange('month')">This month</button>
24357
+ <button class="time-range-btn active" onclick="setTeamRange('30d')">Last 30 days</button>
24358
+ </div></header>
24359
+ <div class="pc"><div id="tm-members-area" class="empty">Loading...</div></div>
24360
+ <div id="tm-member-sessions" style="display:none">
24361
+ <header class="ph" style="border-top:1px solid var(--line)"><div><h2 id="tm-member-sessions-title">Sessions</h2><p id="tm-member-sessions-count"></p></div><div><button class="time-range-btn active" onclick="closeMemberSessions()">Back to Team</button></div></header>
24362
+ <div class="pc"><div id="tm-member-sessions-area"></div></div>
24363
+ </div>
24364
+ </section>
24365
+ <section class="panel">
24366
+ <header class="ph"><div><h2>Daily Cost</h2><p>stacked by member</p></div></header>
24367
+ <div class="pc"><div id="tm-cost-chart" class="empty">Loading...</div></div>
24368
+ </section>
24369
+ </section>
24370
+ </div>
24371
+ <div id="tab-insights" class="tab-content">
24372
+ <section class="ins-summary">
24373
+ <article class="mc"><div class="label">Avg $/Session</div><div class="val orange" id="ins-avg-cost">-</div></article>
24374
+ <article class="mc"><div class="label">Avg Commits/Session</div><div class="val green" id="ins-avg-commits">-</div></article>
24375
+ <article class="mc"><div class="label">$/Commit</div><div class="val cyan" id="ins-cost-commit">-</div></article>
24376
+ <article class="mc"><div class="label">Total Tokens</div><div class="val purple" id="ins-tokens">-</div></article>
24377
+ <article class="mc"><div class="label">Efficiency Score</div><div class="val green" id="ins-efficiency">-</div></article>
24378
+ <article class="mc"><div class="label">Forecast (EOM)</div><div class="val orange" id="ins-forecast">-</div></article>
24379
+ </section>
24380
+ <div class="ins-grid" style="margin-top:14px">
24381
+ <div class="ins-panel ins-full">
24382
+ <div class="ins-ph"><div><h3>Cost Trend</h3><p>daily spend with cumulative overlay</p></div><div class="time-range" id="ins-time-range">
24383
+ <button class="time-range-btn" onclick="setInsightsRange('week')">This week</button>
24384
+ <button class="time-range-btn" onclick="setInsightsRange('month')">This month</button>
24385
+ <button class="time-range-btn active" onclick="setInsightsRange('30d')">Last 30 days</button>
24386
+ </div></div>
24387
+ <div class="ins-body" id="ins-cost-trend"><div class="ins-loading">Loading analytics...</div></div>
24388
+ </div>
24389
+ </div>
24390
+ <div class="ins-grid">
24391
+ <div class="ins-panel">
24392
+ <div class="ins-ph"><div><h3>Cost Distribution</h3><p>by team member</p></div></div>
24393
+ <div class="ins-body" id="ins-donut-area"><div class="ins-loading">...</div></div>
24394
+ </div>
24395
+ <div class="ins-panel">
24396
+ <div class="ins-ph"><div><h3>Activity Heatmap</h3><p>sessions by hour &amp; day of week</p></div></div>
24397
+ <div class="ins-body" id="ins-heatmap-area"><div class="ins-loading">...</div></div>
24398
+ </div>
24399
+ </div>
24400
+ <div class="ins-grid">
24401
+ <div class="ins-panel">
24402
+ <div class="ins-ph"><div><h3>Top Models</h3><p>AI models by cost</p></div></div>
24403
+ <div class="ins-body" id="ins-models-area"><div class="ins-loading">...</div></div>
24404
+ </div>
24405
+ <div class="ins-panel">
24406
+ <div class="ins-ph"><div><h3>Top Tools</h3><p>most used tools</p></div></div>
24407
+ <div class="ins-body" id="ins-tools-area"><div class="ins-loading">...</div></div>
24408
+ </div>
24409
+ </div>
24410
+ <div class="ins-panel" style="margin-top:14px">
24411
+ <div class="ins-ph"><div><h3>Per-Member Analytics</h3><p>deep dive into each team member</p></div></div>
24412
+ <div class="ins-body" id="ins-members-area"><div class="ins-loading">...</div></div>
24413
+ </div>
24414
+ <div class="ins-ai-panel" id="ins-ai-area">
24415
+ <div class="ins-ai-header"><div class="ins-ai-title">AI Team Analysis</div><div><span class="context-indicator" id="context-indicator"></span><button class="configure-analysis-btn" onclick="openContextModal()">Configure Analysis</button></div></div>
24416
+ <div id="ins-ai-content">
24417
+ <p style="font-size:12px;color:var(--text-muted);margin:0 0 12px">Generate an AI-powered deep analysis of your team's performance, cost efficiency, and productivity patterns.</p>
24418
+ <button class="insight-gen-btn" id="ins-ai-btn" onclick="generateTeamInsight()">Generate Team Analysis</button>
24419
+ <div id="ins-ai-status" style="font-size:11px;color:var(--text-dim);margin-top:6px"></div>
24420
+ </div>
24421
+ </div>
24422
+ </div>
24423
+ <div id="budget-modal" class="modal-overlay" onclick="if(event.target===this)closeBudgetModal()">
24424
+ <div class="modal">
24425
+ <button class="modal-close" onclick="closeBudgetModal()">&times;</button>
24426
+ <h3>Set Monthly Budget</h3>
24427
+ <label>Monthly Limit (USD)</label>
24428
+ <input type="number" id="budget-limit" placeholder="e.g. 3500" min="0" step="100"/>
24429
+ <label>Alert Threshold (%)</label>
24430
+ <input type="number" id="budget-threshold" value="80" min="0" max="100" step="5"/>
24431
+ <div class="modal-actions">
24432
+ <button class="modal-save" onclick="saveBudget()">Save Budget</button>
24433
+ <span class="modal-status" id="budget-status"></span>
24434
+ </div>
24435
+ </div>
24436
+ </div>
24437
+ <div id="context-modal" class="modal-overlay" onclick="if(event.target===this)closeContextModal()">
24438
+ <div class="modal" style="width:520px">
24439
+ <button class="modal-close" onclick="closeContextModal()">&times;</button>
24440
+ <h3>Configure Team Analysis</h3>
24441
+ <label>Company Context</label>
24442
+ <textarea id="context-company" rows="5" placeholder="Describe your company, team structure, project priorities, naming conventions..."></textarea>
24443
+ <label>Analysis Guidelines</label>
24444
+ <textarea id="context-guidelines" rows="5" placeholder="What should the AI focus on? What metrics matter most? Any specific thresholds or criteria for performance reviews?"></textarea>
24445
+ <div class="modal-actions">
24446
+ <button class="modal-save" id="context-save-btn" onclick="saveContext()">Save</button>
24447
+ <span class="modal-status" id="context-status"></span>
24448
+ </div>
24449
+ </div>
24450
+ </div>
24238
24451
  </main>
24239
24452
  <script>
24240
24453
  (function(){
@@ -24245,6 +24458,316 @@ var costPoints = [];
24245
24458
  var replay = null;
24246
24459
  var insightsConfigured = false;
24247
24460
  var insightsCache = {};
24461
+ var teamData = null;
24462
+ var teamRange = '30d';
24463
+ var teamESource = null;
24464
+ var authToken = localStorage.getItem('agent_trace_auth_token') || '';
24465
+ var authRequired = false;
24466
+ var currentTab = 'sessions';
24467
+ var userIdFilter = null;
24468
+ var currentUserEmail = ${currentUserEmailJson};
24469
+ var teamMemberFilter = null;
24470
+
24471
+ function getAuthHeaders() {
24472
+ var h = {};
24473
+ if (authToken) h['Authorization'] = 'Bearer ' + authToken;
24474
+ return h;
24475
+ }
24476
+
24477
+ function authFetch(url, opts) {
24478
+ opts = opts || {};
24479
+ opts.headers = Object.assign({}, opts.headers || {}, getAuthHeaders());
24480
+ return fetch(url, opts);
24481
+ }
24482
+
24483
+ function checkAuth() {
24484
+ return fetch('/api/auth/check', { headers: getAuthHeaders() })
24485
+ .then(function(r) { return r.json(); })
24486
+ .then(function(data) {
24487
+ authRequired = data.authRequired;
24488
+ if (authRequired && !data.authValid) {
24489
+ document.getElementById('auth-gate').style.display = 'block';
24490
+ document.getElementById('tab-sessions').classList.remove('active');
24491
+ return false;
24492
+ }
24493
+ document.getElementById('auth-gate').style.display = 'none';
24494
+ return true;
24495
+ })
24496
+ .catch(function() { return true; });
24497
+ }
24498
+
24499
+ window.submitAuthToken = function() {
24500
+ var input = document.getElementById('auth-token-input');
24501
+ authToken = input.value.trim();
24502
+ localStorage.setItem('agent_trace_auth_token', authToken);
24503
+ document.getElementById('auth-error').textContent = '';
24504
+ checkAuth().then(function(ok) {
24505
+ if (!ok) {
24506
+ document.getElementById('auth-error').textContent = 'Invalid token. Please try again.';
24507
+ } else {
24508
+ document.getElementById('tab-sessions').classList.add('active');
24509
+ startStreaming();
24510
+ }
24511
+ });
24512
+ };
24513
+
24514
+ window.switchTab = function(tab) {
24515
+ currentTab = tab;
24516
+ var btns = document.querySelectorAll('.tab-btn');
24517
+ for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
24518
+ if (tab === 'sessions') btns[0].classList.add('active');
24519
+ else if (tab === 'team') btns[1].classList.add('active');
24520
+ else if (tab === 'insights') btns[2].classList.add('active');
24521
+ document.getElementById('tab-sessions').classList.toggle('active', tab === 'sessions');
24522
+ document.getElementById('tab-team').classList.toggle('active', tab === 'team');
24523
+ document.getElementById('tab-insights').classList.toggle('active', tab === 'insights');
24524
+ if (tab === 'team' && !teamESource) startTeamStream();
24525
+ if (tab === 'insights') loadInsightsData();
24526
+ };
24527
+
24528
+ function getTeamDateRange() {
24529
+ var now = new Date();
24530
+ var y = now.getFullYear();
24531
+ var m = now.getMonth();
24532
+ var from, to;
24533
+ if (teamRange === 'week') {
24534
+ var day = now.getDay();
24535
+ var monday = new Date(now);
24536
+ monday.setDate(now.getDate() - (day === 0 ? 6 : day - 1));
24537
+ from = monday.toISOString().slice(0, 10);
24538
+ to = now.toISOString().slice(0, 10);
24539
+ } else if (teamRange === 'month') {
24540
+ from = y + '-' + String(m + 1).padStart(2, '0') + '-01';
24541
+ var lastDay = new Date(y, m + 1, 0).getDate();
24542
+ to = y + '-' + String(m + 1).padStart(2, '0') + '-' + String(lastDay).padStart(2, '0');
24543
+ } else {
24544
+ var d30 = new Date(now);
24545
+ d30.setDate(d30.getDate() - 30);
24546
+ from = d30.toISOString().slice(0, 10);
24547
+ to = now.toISOString().slice(0, 10);
24548
+ }
24549
+ return { from: from, to: to };
24550
+ }
24551
+
24552
+ window.setTeamRange = function(range) {
24553
+ teamRange = range;
24554
+ var btns = document.querySelectorAll('.time-range-btn');
24555
+ for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
24556
+ if (range === 'week') btns[0].classList.add('active');
24557
+ else if (range === 'month') btns[1].classList.add('active');
24558
+ else btns[2].classList.add('active');
24559
+ if (teamESource) { teamESource.close(); teamESource = null; }
24560
+ startTeamStream();
24561
+ };
24562
+
24563
+ function startTeamStream() {
24564
+ var r = getTeamDateRange();
24565
+ var qs = '?from=' + r.from + '&to=' + r.to;
24566
+ teamESource = new EventSource('/api/team/stream' + qs);
24567
+ teamESource.addEventListener('team', function(e) {
24568
+ try { teamData = JSON.parse(e.data); renderTeam(); } catch(err) {}
24569
+ });
24570
+ }
24571
+
24572
+ function fmtCost(v) { return '$' + (v||0).toFixed(2); }
24573
+ function fmtNum(v) { return String(v||0).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','); }
24574
+
24575
+ function timeAgo(isoStr) {
24576
+ if (!isoStr) return '';
24577
+ var diff = Date.now() - new Date(isoStr).getTime();
24578
+ if (diff < 60000) return 'just now';
24579
+ if (diff < 3600000) return Math.floor(diff/60000) + 'm ago';
24580
+ if (diff < 86400000) return Math.floor(diff/3600000) + 'h ago';
24581
+ return Math.floor(diff/86400000) + 'd ago';
24582
+ }
24583
+
24584
+ function renderTeam() {
24585
+ if (!teamData) return;
24586
+ var ov = teamData.overview || {};
24587
+ var mb = teamData.members || {};
24588
+ var cs = teamData.cost || {};
24589
+ var bg = teamData.budget || {};
24590
+
24591
+ document.getElementById('tm-members').textContent = fmtNum(ov.memberCount);
24592
+ document.getElementById('tm-cost').textContent = fmtCost(ov.totalCostUsd);
24593
+ document.getElementById('tm-sessions').textContent = fmtNum(ov.totalSessions);
24594
+ document.getElementById('tm-commits').textContent = fmtNum(ov.totalCommits);
24595
+ document.getElementById('tm-cpc').textContent = fmtCost(ov.costPerCommit);
24596
+
24597
+ // Show team tab if multiple members exist
24598
+ if (ov.memberCount > 1) {
24599
+ document.getElementById('tab-bar').style.display = 'flex';
24600
+ }
24601
+
24602
+ // Budget bar
24603
+ if (bg.budget) {
24604
+ document.getElementById('tm-budget-area').style.display = 'block';
24605
+ document.getElementById('tm-no-budget').style.display = 'none';
24606
+ var pct = bg.percentUsed || 0;
24607
+ var fill = document.getElementById('tm-budget-fill');
24608
+ fill.style.width = Math.min(pct, 100) + '%';
24609
+ fill.className = 'budget-fill';
24610
+ if (pct > 100) { fill.classList.add('red', 'pulse'); }
24611
+ else if (pct > 80) { fill.classList.add('red'); }
24612
+ else if (pct > 60) { fill.classList.add('orange'); }
24613
+ else { fill.classList.add('green'); }
24614
+ document.getElementById('tm-budget-label').textContent = Math.round(pct) + '% of $' + bg.budget.monthlyLimitUsd.toLocaleString() + '/mo (' + fmtCost(bg.currentMonthSpend) + ' spent)';
24615
+ } else {
24616
+ document.getElementById('tm-budget-area').style.display = 'none';
24617
+ document.getElementById('tm-no-budget').style.display = 'block';
24618
+ }
24619
+
24620
+ // Period label
24621
+ if (ov.period) {
24622
+ document.getElementById('tm-period-label').textContent = ov.period.from + ' to ' + ov.period.to;
24623
+ }
24624
+
24625
+ // Members table
24626
+ var members = (mb.members || []);
24627
+ if (members.length === 0) {
24628
+ document.getElementById('tm-members-area').innerHTML = '<div class="empty">No team data yet.</div>';
24629
+ } else {
24630
+ var html = '<table class="team-table"><thead><tr><th>Member</th><th>Sessions</th><th>Cost</th><th>Commits</th><th>Lines</th><th>Last Active</th></tr></thead><tbody>';
24631
+ for (var i = 0; i < members.length; i++) {
24632
+ var m = members[i];
24633
+ var name = m.displayName || m.userId;
24634
+ html += '<tr class="team-row" onclick="filterByMember(\\x27' + (m.userId||'').replace(/'/g,"\\\\'") + '\\x27)">';
24635
+ html += '<td><span style="color:var(--text-primary)">' + esc(name) + '</span>';
24636
+ if (m.displayName && m.userId !== m.displayName) html += '<br/><span style="font-size:10px;color:var(--text-dim)">' + esc(m.userId) + '</span>';
24637
+ html += '</td>';
24638
+ html += '<td>' + m.sessionCount + '</td>';
24639
+ html += '<td class="orange">' + fmtCost(m.totalCostUsd) + '</td>';
24640
+ html += '<td>' + m.commitCount + '</td>';
24641
+ html += '<td><span class="ls green">+' + fmtNum(m.linesAdded) + '</span><span class="ls red">-' + fmtNum(m.linesRemoved) + '</span></td>';
24642
+ html += '<td style="color:var(--text-dim)">' + timeAgo(m.lastActiveAt) + '</td>';
24643
+ html += '</tr>';
24644
+ }
24645
+ html += '</tbody></table>';
24646
+ document.getElementById('tm-members-area').innerHTML = html;
24647
+ }
24648
+
24649
+ // Team daily cost chart (stacked)
24650
+ var points = (cs.points || []).slice(-7);
24651
+ if (points.length === 0) {
24652
+ document.getElementById('tm-cost-chart').innerHTML = '<div class="empty">No cost data.</div>';
24653
+ } else {
24654
+ var maxCost = 0;
24655
+ for (var i = 0; i < points.length; i++) {
24656
+ if (points[i].totalCostUsd > maxCost) maxCost = points[i].totalCostUsd;
24657
+ }
24658
+ var colors = ['var(--green)', 'var(--cyan)', 'var(--orange)', 'var(--purple)', 'var(--yellow)', 'var(--red)', 'var(--text-muted)'];
24659
+ var chartHtml = '<div class="chart">';
24660
+ for (var i = 0; i < points.length; i++) {
24661
+ var p = points[i];
24662
+ var barH = maxCost > 0 ? Math.max(3, Math.round((p.totalCostUsd / maxCost) * 140)) : 3;
24663
+ chartHtml += '<div class="chart-col">';
24664
+ chartHtml += '<div class="chart-value">' + fmtCost(p.totalCostUsd) + '</div>';
24665
+ // Stacked bar segments
24666
+ var bm = p.byMember || [];
24667
+ if (bm.length <= 1) {
24668
+ chartHtml += '<div class="chart-bar" style="height:' + barH + 'px"></div>';
24669
+ } else {
24670
+ chartHtml += '<div style="display:flex;flex-direction:column-reverse;height:' + barH + 'px">';
24671
+ for (var j = 0; j < bm.length; j++) {
24672
+ var segH = maxCost > 0 ? Math.max(1, Math.round((bm[j].totalCostUsd / maxCost) * 140)) : 1;
24673
+ var col = colors[j % colors.length];
24674
+ chartHtml += '<div style="height:' + segH + 'px;background:' + col + ';min-height:1px;border-radius:1px"></div>';
24675
+ }
24676
+ chartHtml += '</div>';
24677
+ }
24678
+ chartHtml += '<div class="chart-label">' + p.date.slice(5) + '</div>';
24679
+ chartHtml += '</div>';
24680
+ }
24681
+ chartHtml += '</div>';
24682
+ document.getElementById('tm-cost-chart').innerHTML = chartHtml;
24683
+ }
24684
+ }
24685
+
24686
+ function esc(s) {
24687
+ if (!s) return '';
24688
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
24689
+ }
24690
+
24691
+ window.filterByMember = function(userId) {
24692
+ teamMemberFilter = userId;
24693
+ renderMemberSessions();
24694
+ };
24695
+
24696
+ window.clearFilter = function() {
24697
+ userIdFilter = null;
24698
+ renderSessions();
24699
+ };
24700
+
24701
+ window.closeMemberSessions = function() {
24702
+ teamMemberFilter = null;
24703
+ document.getElementById('tm-member-sessions').style.display = 'none';
24704
+ };
24705
+
24706
+ function renderMemberSessions() {
24707
+ if (!teamMemberFilter) return;
24708
+ var memberSessions = sessions.filter(function(s) { return s.userId === teamMemberFilter; });
24709
+ var memberName = teamMemberFilter;
24710
+ memberSessions.forEach(function(s) { if (s.userDisplayName) memberName = s.userDisplayName; });
24711
+ document.getElementById('tm-member-sessions-title').textContent = memberName + ' \u2014 Sessions';
24712
+ document.getElementById('tm-member-sessions-count').textContent = memberSessions.length + ' sessions';
24713
+ document.getElementById('tm-member-sessions').style.display = 'block';
24714
+ var area = document.getElementById('tm-member-sessions-area');
24715
+ if (memberSessions.length === 0) {
24716
+ area.innerHTML = '<div class="empty">No sessions for this member.</div>';
24717
+ return;
24718
+ }
24719
+ var h = '<table><thead><tr><th>Session</th><th>Repo</th><th>Started</th><th>Prompts</th><th>Cost</th><th>Commits</th><th>Lines</th></tr></thead><tbody>';
24720
+ memberSessions.forEach(function(s) {
24721
+ var repo = s.gitRepo ? (s.gitBranch ? s.gitRepo + '/' + s.gitBranch : s.gitRepo) : '-';
24722
+ var commits = s.commitCount > 0 ? '<span class="badge green">' + s.commitCount + '</span>' : '<span class="badge dim">0</span>';
24723
+ var lines = (s.linesAdded > 0 || s.linesRemoved > 0) ? '<span class="ls green">+' + s.linesAdded + '</span><span class="ls red">-' + s.linesRemoved + '</span>' : '<span style="color:var(--text-dim)">-</span>';
24724
+ h += '<tr class="srow" data-sid="' + esc(s.sessionId) + '" onclick="selectSession(this.dataset.sid);switchTab(\\x27sessions\\x27)"><td>' + esc(s.sessionId.slice(0,10)) + '</td><td class="repo-cell">' + esc(repo) + '</td><td>' + fmtDate(s.startedAt) + '</td><td>' + s.promptCount + '</td><td>' + fmt$(s.totalCostUsd) + '</td><td>' + commits + '</td><td>' + lines + '</td></tr>';
24725
+ });
24726
+ h += '</tbody></table>';
24727
+ area.innerHTML = h;
24728
+ document.getElementById('tm-member-sessions').scrollIntoView({ behavior: 'smooth', block: 'start' });
24729
+ }
24730
+
24731
+ window.openBudgetModal = function() {
24732
+ document.getElementById('budget-modal').classList.add('open');
24733
+ if (teamData && teamData.budget && teamData.budget.budget) {
24734
+ document.getElementById('budget-limit').value = teamData.budget.budget.monthlyLimitUsd;
24735
+ document.getElementById('budget-threshold').value = teamData.budget.budget.alertThresholdPercent;
24736
+ }
24737
+ };
24738
+
24739
+ window.closeBudgetModal = function() {
24740
+ document.getElementById('budget-modal').classList.remove('open');
24741
+ };
24742
+
24743
+ window.saveBudget = function() {
24744
+ var limit = parseFloat(document.getElementById('budget-limit').value);
24745
+ var threshold = parseFloat(document.getElementById('budget-threshold').value) || 80;
24746
+ if (isNaN(limit) || limit < 0) {
24747
+ document.getElementById('budget-status').textContent = 'Invalid limit';
24748
+ document.getElementById('budget-status').className = 'modal-status error';
24749
+ return;
24750
+ }
24751
+ document.getElementById('budget-status').textContent = 'Saving...';
24752
+ document.getElementById('budget-status').className = 'modal-status';
24753
+ authFetch('/api/team/budget', {
24754
+ method: 'POST',
24755
+ headers: { 'Content-Type': 'application/json' },
24756
+ body: JSON.stringify({ monthlyLimitUsd: limit, alertThresholdPercent: threshold })
24757
+ }).then(function(r) { return r.json(); }).then(function(data) {
24758
+ if (data.status === 'ok') {
24759
+ document.getElementById('budget-status').textContent = 'Saved!';
24760
+ document.getElementById('budget-status').className = 'modal-status ok';
24761
+ setTimeout(closeBudgetModal, 800);
24762
+ } else {
24763
+ document.getElementById('budget-status').textContent = data.message || 'Error';
24764
+ document.getElementById('budget-status').className = 'modal-status error';
24765
+ }
24766
+ }).catch(function() {
24767
+ document.getElementById('budget-status').textContent = 'Network error';
24768
+ document.getElementById('budget-status').className = 'modal-status error';
24769
+ });
24770
+ };
24248
24771
 
24249
24772
  window.openSettings = function() {
24250
24773
  document.getElementById('settings-modal').classList.add('open');
@@ -24545,9 +25068,16 @@ window.togglePrompt = function(hd) {
24545
25068
 
24546
25069
  function renderSessions() {
24547
25070
  var area = document.getElementById('sessions-area');
24548
- if (sessions.length === 0) { area.innerHTML = '<div class="empty">No sessions captured yet.</div>'; return; }
25071
+ var label = document.getElementById('stream-label');
25072
+ var filtered = sessions;
25073
+ // Sessions tab always shows only the current user's sessions
25074
+ if (currentUserEmail) {
25075
+ filtered = sessions.filter(function(s) { return s.userId === currentUserEmail; });
25076
+ label.textContent = filtered.length + ' sessions (you)';
25077
+ }
25078
+ if (filtered.length === 0) { area.innerHTML = '<div class="empty">' + (currentUserEmail ? 'No sessions for you yet. Start a Claude Code session.' : 'No sessions captured yet.') + '</div>'; return; }
24549
25079
  var h = '<table><thead><tr><th>Session</th><th>Repo</th><th>Started</th><th>Prompts</th><th>Cost</th><th>Commits</th><th>Lines</th></tr></thead><tbody>';
24550
- sessions.forEach(function(s){
25080
+ filtered.forEach(function(s){
24551
25081
  var active = s.sessionId === selectedId ? ' active' : '';
24552
25082
  var repo = s.gitRepo ? (s.gitBranch ? s.gitRepo + '/' + s.gitBranch : s.gitRepo) : '-';
24553
25083
  var commits = s.commitCount > 0 ? '<span class="badge green">' + s.commitCount + '</span>' : '<span class="badge dim">0</span>';
@@ -24559,14 +25089,15 @@ function renderSessions() {
24559
25089
  }
24560
25090
 
24561
25091
  function renderMetrics() {
24562
- document.getElementById('m-sessions').textContent = sessions.length;
24563
- document.getElementById('m-cost').textContent = fmt$(sessions.reduce(function(s,x){return s+x.totalCostUsd;},0));
24564
- document.getElementById('m-prompts').textContent = sessions.reduce(function(s,x){return s+x.promptCount;},0);
24565
- document.getElementById('m-tools').textContent = sessions.reduce(function(s,x){return s+x.toolCallCount;},0);
24566
- var tc = sessions.reduce(function(s,x){return s+x.commitCount;},0);
24567
- var sc = sessions.filter(function(x){return x.commitCount>0;}).length;
25092
+ var my = currentUserEmail ? sessions.filter(function(s){return s.userId === currentUserEmail;}) : sessions;
25093
+ document.getElementById('m-sessions').textContent = my.length;
25094
+ document.getElementById('m-cost').textContent = fmt$(my.reduce(function(s,x){return s+x.totalCostUsd;},0));
25095
+ document.getElementById('m-prompts').textContent = my.reduce(function(s,x){return s+x.promptCount;},0);
25096
+ document.getElementById('m-tools').textContent = my.reduce(function(s,x){return s+x.toolCallCount;},0);
25097
+ var tc = my.reduce(function(s,x){return s+x.commitCount;},0);
25098
+ var sc = my.filter(function(x){return x.commitCount>0;}).length;
24568
25099
  document.getElementById('m-commits').textContent = tc;
24569
- document.getElementById('m-commits-det').textContent = sc + '/' + sessions.length + ' sessions produced commits';
25100
+ document.getElementById('m-commits-det').textContent = sc + '/' + my.length + ' sessions produced commits';
24570
25101
  }
24571
25102
 
24572
25103
  function renderCostChart() {
@@ -24670,8 +25201,10 @@ function setSessions(raw) {
24670
25201
  sessions = sortLatest(raw.map(parseSummary).filter(Boolean));
24671
25202
  renderMetrics();
24672
25203
  renderSessions();
24673
- if (sessions.length > 0 && (!selectedId || !sessions.some(function(s){return s.sessionId===selectedId;}))) {
24674
- selectSession(sessions[0].sessionId);
25204
+ if (teamMemberFilter) renderMemberSessions();
25205
+ var my = currentUserEmail ? sessions.filter(function(s){return s.userId === currentUserEmail;}) : sessions;
25206
+ if (my.length > 0 && (!selectedId || !my.some(function(s){return s.sessionId===selectedId;}))) {
25207
+ selectSession(my[0].sessionId);
24675
25208
  }
24676
25209
  }
24677
25210
 
@@ -24716,7 +25249,7 @@ function loadSnapshot() {
24716
25249
  });
24717
25250
  }
24718
25251
 
24719
- function boot() {
25252
+ function startStreaming() {
24720
25253
  fetch('/api/settings/insights',{cache:'no-store'}).then(function(r){return r.json();}).then(function(data){
24721
25254
  if(data && data.configured) insightsConfigured = true;
24722
25255
  }).catch(function(){});
@@ -24730,6 +25263,12 @@ function boot() {
24730
25263
  document.getElementById('stream-label').textContent = 'live';
24731
25264
  document.getElementById('status').className = 'status-banner';
24732
25265
  document.getElementById('status').textContent = 'Live';
25266
+ // Auto-detect multi-user and show team tab
25267
+ var userIds = {};
25268
+ sessions.forEach(function(s){ if(s.userId && s.userId !== 'unknown_user') userIds[s.userId] = 1; });
25269
+ if (Object.keys(userIds).length > 1) {
25270
+ document.getElementById('tab-bar').style.display = 'flex';
25271
+ }
24733
25272
  }
24734
25273
  });
24735
25274
  es.addEventListener('bridge_error', function(event) {
@@ -24747,6 +25286,645 @@ function boot() {
24747
25286
  });
24748
25287
  }
24749
25288
 
25289
+ // ===== INSIGHTS TAB =====
25290
+ var insightsData = null;
25291
+ var insightsRange = '30d';
25292
+ var insightsLoading = false;
25293
+ var MEMBER_COLORS = ['#4ade80','#22d3ee','#fb923c','#c084fc','#facc15','#f87171','#60a5fa','#a78bfa','#34d399','#fbbf24'];
25294
+
25295
+ function getInsightsDateRange() {
25296
+ var now = new Date();
25297
+ var y = now.getFullYear();
25298
+ var m = now.getMonth();
25299
+ var from, to;
25300
+ if (insightsRange === 'week') {
25301
+ var day = now.getDay();
25302
+ var monday = new Date(now);
25303
+ monday.setDate(now.getDate() - (day === 0 ? 6 : day - 1));
25304
+ from = monday.toISOString().slice(0, 10);
25305
+ to = now.toISOString().slice(0, 10);
25306
+ } else if (insightsRange === 'month') {
25307
+ from = y + '-' + String(m + 1).padStart(2, '0') + '-01';
25308
+ var lastDay = new Date(y, m + 1, 0).getDate();
25309
+ to = y + '-' + String(m + 1).padStart(2, '0') + '-' + String(lastDay).padStart(2, '0');
25310
+ } else {
25311
+ var d30 = new Date(now);
25312
+ d30.setDate(d30.getDate() - 30);
25313
+ from = d30.toISOString().slice(0, 10);
25314
+ to = now.toISOString().slice(0, 10);
25315
+ }
25316
+ return { from: from, to: to };
25317
+ }
25318
+
25319
+ window.setInsightsRange = function(range) {
25320
+ insightsRange = range;
25321
+ var btns = document.querySelectorAll('#ins-time-range .time-range-btn');
25322
+ for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
25323
+ if (range === 'week') btns[0].classList.add('active');
25324
+ else if (range === 'month') btns[1].classList.add('active');
25325
+ else btns[2].classList.add('active');
25326
+ insightsData = null;
25327
+ loadInsightsData();
25328
+ };
25329
+
25330
+ function loadInsightsData() {
25331
+ if (insightsLoading) return;
25332
+ if (insightsData) { renderInsights(); return; }
25333
+ insightsLoading = true;
25334
+ var r = getInsightsDateRange();
25335
+ authFetch('/api/team/analytics?from=' + r.from + '&to=' + r.to)
25336
+ .then(function(res) { return res.json(); })
25337
+ .then(function(data) {
25338
+ insightsLoading = false;
25339
+ if (data && data.status === 'ok') {
25340
+ insightsData = data;
25341
+ renderInsights();
25342
+ }
25343
+ })
25344
+ .catch(function() { insightsLoading = false; });
25345
+ }
25346
+
25347
+ function renderInsights() {
25348
+ if (!insightsData) return;
25349
+ var d = insightsData;
25350
+
25351
+ // Summary cards
25352
+ document.getElementById('ins-avg-cost').textContent = '$' + (d.avgCostPerSession || 0).toFixed(2);
25353
+ document.getElementById('ins-avg-commits').textContent = (d.avgCommitsPerSession || 0).toFixed(1);
25354
+ document.getElementById('ins-cost-commit').textContent = '$' + (d.avgCostPerCommit || 0).toFixed(2);
25355
+ document.getElementById('ins-tokens').textContent = formatLargeNum(d.totalTokensUsed || 0);
25356
+ document.getElementById('ins-efficiency').textContent = (d.costEfficiencyScore || 0) + '/100';
25357
+
25358
+ // Forecast: linear extrapolation to end of month
25359
+ var forecast = computeForecast(d.costTrend || []);
25360
+ document.getElementById('ins-forecast').textContent = '$' + forecast.toFixed(0);
25361
+
25362
+ renderCostTrendChart(d.costTrend || []);
25363
+ renderDonutChart(d.memberAnalytics || []);
25364
+ renderHeatmap(d.hourlyHeatmap || []);
25365
+ renderTopModels(d.topModels || []);
25366
+ renderTopTools(d.topTools || []);
25367
+ renderMemberCards(d.memberAnalytics || []);
25368
+ }
25369
+
25370
+ function formatLargeNum(n) {
25371
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
25372
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
25373
+ return String(n);
25374
+ }
25375
+
25376
+ function computeForecast(costTrend) {
25377
+ if (!costTrend || costTrend.length < 2) return 0;
25378
+ var now = new Date();
25379
+ var daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
25380
+ var dayOfMonth = now.getDate();
25381
+ var totalSoFar = 0;
25382
+ for (var i = 0; i < costTrend.length; i++) totalSoFar += costTrend[i].costUsd;
25383
+ if (dayOfMonth === 0) return totalSoFar;
25384
+ return (totalSoFar / dayOfMonth) * daysInMonth;
25385
+ }
25386
+
25387
+ function renderCostTrendChart(trend) {
25388
+ var el = document.getElementById('ins-cost-trend');
25389
+ if (!trend || trend.length === 0) { el.innerHTML = '<div class="ins-loading">No cost data available.</div>'; return; }
25390
+
25391
+ var W = 800, H = 200, PL = 50, PR = 50, PT = 20, PB = 30;
25392
+ var cw = W - PL - PR, ch = H - PT - PB;
25393
+ var maxDaily = 0.01, maxCum = 0.01;
25394
+ for (var i = 0; i < trend.length; i++) {
25395
+ if (trend[i].costUsd > maxDaily) maxDaily = trend[i].costUsd;
25396
+ if (trend[i].cumulativeCostUsd > maxCum) maxCum = trend[i].cumulativeCostUsd;
25397
+ }
25398
+
25399
+ var svg = '<svg class="svg-chart" viewBox="0 0 ' + W + ' ' + H + '" preserveAspectRatio="none">';
25400
+
25401
+ // Grid lines
25402
+ for (var g = 0; g <= 4; g++) {
25403
+ var gy = PT + (ch / 4) * g;
25404
+ svg += '<line x1="' + PL + '" y1="' + gy + '" x2="' + (W - PR) + '" y2="' + gy + '" stroke="#1a1a1a" stroke-width="1"/>';
25405
+ var label = '$' + ((maxDaily * (4 - g) / 4)).toFixed(2);
25406
+ svg += '<text x="' + (PL - 4) + '" y="' + (gy + 3) + '" text-anchor="end" fill="#444" font-size="9" font-family="inherit">' + label + '</text>';
25407
+ }
25408
+
25409
+ // Bars (daily cost)
25410
+ var barW = Math.max(4, (cw / trend.length) - 2);
25411
+ for (var i = 0; i < trend.length; i++) {
25412
+ var x = PL + (i / trend.length) * cw + 1;
25413
+ var barH = (trend[i].costUsd / maxDaily) * ch;
25414
+ var y = PT + ch - barH;
25415
+ svg += '<rect x="' + x + '" y="' + y + '" width="' + barW + '" height="' + barH + '" rx="2" fill="rgba(74,222,128,0.3)"/>';
25416
+ // Date label every N bars
25417
+ if (trend.length <= 14 || i % Math.ceil(trend.length / 10) === 0) {
25418
+ svg += '<text x="' + (x + barW / 2) + '" y="' + (H - 4) + '" text-anchor="middle" fill="#444" font-size="8" font-family="inherit">' + trend[i].date.slice(5) + '</text>';
25419
+ }
25420
+ }
25421
+
25422
+ // Cumulative line
25423
+ var linePts = '';
25424
+ var areaPts = '';
25425
+ for (var i = 0; i < trend.length; i++) {
25426
+ var lx = PL + (i / (trend.length - 1 || 1)) * cw;
25427
+ var ly = PT + ch - (trend[i].cumulativeCostUsd / maxCum) * ch;
25428
+ linePts += (i === 0 ? 'M' : 'L') + lx + ',' + ly;
25429
+ areaPts += (i === 0 ? 'M' : 'L') + lx + ',' + ly;
25430
+ }
25431
+ // Area fill
25432
+ areaPts += 'L' + (PL + cw) + ',' + (PT + ch) + 'L' + PL + ',' + (PT + ch) + 'Z';
25433
+ svg += '<defs><linearGradient id="cumGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="rgba(34,211,238,0.15)"/><stop offset="100%" stop-color="rgba(34,211,238,0)"/></linearGradient></defs>';
25434
+ svg += '<path d="' + areaPts + '" fill="url(#cumGrad)"/>';
25435
+ svg += '<path d="' + linePts + '" fill="none" stroke="#22d3ee" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
25436
+
25437
+ // Dots on cumulative line
25438
+ for (var i = 0; i < trend.length; i++) {
25439
+ var dx = PL + (i / (trend.length - 1 || 1)) * cw;
25440
+ var dy = PT + ch - (trend[i].cumulativeCostUsd / maxCum) * ch;
25441
+ svg += '<circle cx="' + dx + '" cy="' + dy + '" r="3" fill="#22d3ee" stroke="#000" stroke-width="1"/>';
25442
+ }
25443
+
25444
+ // Right Y axis label for cumulative
25445
+ svg += '<text x="' + (W - 4) + '" y="' + (PT + 3) + '" text-anchor="end" fill="#22d3ee" font-size="9" font-family="inherit">$' + maxCum.toFixed(0) + ' total</text>';
25446
+
25447
+ svg += '</svg>';
25448
+ svg += '<div style="display:flex;gap:16px;margin-top:4px;font-size:10px;color:var(--text-dim)">';
25449
+ svg += '<span><span style="display:inline-block;width:10px;height:10px;background:rgba(74,222,128,0.3);border-radius:2px;vertical-align:middle;margin-right:3px"></span>Daily cost</span>';
25450
+ svg += '<span><span style="display:inline-block;width:10px;height:2px;background:#22d3ee;vertical-align:middle;margin-right:3px"></span>Cumulative</span>';
25451
+ svg += '</div>';
25452
+ el.innerHTML = svg;
25453
+ }
25454
+
25455
+ function renderDonutChart(members) {
25456
+ var el = document.getElementById('ins-donut-area');
25457
+ if (!members || members.length === 0) { el.innerHTML = '<div class="ins-loading">No member data.</div>'; return; }
25458
+
25459
+ var total = 0;
25460
+ for (var i = 0; i < members.length; i++) total += members[i].totalCostUsd;
25461
+ if (total <= 0) { el.innerHTML = '<div class="ins-loading">No cost data.</div>'; return; }
25462
+
25463
+ var R = 70, r = 42, cx = 90, cy = 90;
25464
+ var svg = '<svg width="180" height="180" viewBox="0 0 180 180">';
25465
+ var startAngle = -Math.PI / 2;
25466
+
25467
+ for (var i = 0; i < members.length; i++) {
25468
+ var pct = members[i].totalCostUsd / total;
25469
+ var endAngle = startAngle + pct * 2 * Math.PI;
25470
+ var largeArc = pct > 0.5 ? 1 : 0;
25471
+ var x1 = cx + R * Math.cos(startAngle);
25472
+ var y1 = cy + R * Math.sin(startAngle);
25473
+ var x2 = cx + R * Math.cos(endAngle);
25474
+ var y2 = cy + R * Math.sin(endAngle);
25475
+ var ix1 = cx + r * Math.cos(endAngle);
25476
+ var iy1 = cy + r * Math.sin(endAngle);
25477
+ var ix2 = cx + r * Math.cos(startAngle);
25478
+ var iy2 = cy + r * Math.sin(startAngle);
25479
+ var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
25480
+ svg += '<path d="M' + x1 + ',' + y1 + ' A' + R + ',' + R + ' 0 ' + largeArc + ',1 ' + x2 + ',' + y2 + ' L' + ix1 + ',' + iy1 + ' A' + r + ',' + r + ' 0 ' + largeArc + ',0 ' + ix2 + ',' + iy2 + 'Z" fill="' + col + '" opacity="0.85"/>';
25481
+ startAngle = endAngle;
25482
+ }
25483
+
25484
+ // Center label
25485
+ svg += '<text x="' + cx + '" y="' + (cy - 4) + '" text-anchor="middle" fill="#d4d4d4" font-size="16" font-weight="700" font-family="inherit">$' + total.toFixed(0) + '</text>';
25486
+ svg += '<text x="' + cx + '" y="' + (cy + 10) + '" text-anchor="middle" fill="#666" font-size="9" font-family="inherit">total</text>';
25487
+ svg += '</svg>';
25488
+
25489
+ // Legend
25490
+ var legend = '<div class="donut-legend">';
25491
+ for (var i = 0; i < members.length; i++) {
25492
+ var m = members[i];
25493
+ var pctLabel = ((m.totalCostUsd / total) * 100).toFixed(0);
25494
+ legend += '<div class="donut-legend-item"><div class="donut-legend-swatch" style="background:' + MEMBER_COLORS[i % MEMBER_COLORS.length] + '"></div>' + esc(m.displayName || m.userId) + ' (' + pctLabel + '%, $' + m.totalCostUsd.toFixed(2) + ')</div>';
25495
+ }
25496
+ legend += '</div>';
25497
+ el.innerHTML = '<div style="display:flex;align-items:flex-start;gap:20px;flex-wrap:wrap"><div>' + svg + '</div><div style="flex:1;min-width:160px">' + legend + '</div></div>';
25498
+ }
25499
+
25500
+ function renderHeatmap(heatmap) {
25501
+ var el = document.getElementById('ins-heatmap-area');
25502
+ if (!heatmap || heatmap.length === 0) { el.innerHTML = '<div class="ins-loading">No activity data.</div>'; return; }
25503
+
25504
+ var maxCount = 1;
25505
+ for (var i = 0; i < heatmap.length; i++) {
25506
+ if (heatmap[i].count > maxCount) maxCount = heatmap[i].count;
25507
+ }
25508
+
25509
+ var dayLabels = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
25510
+ var grid = {};
25511
+ for (var i = 0; i < heatmap.length; i++) {
25512
+ grid[heatmap[i].day + '-' + heatmap[i].hour] = heatmap[i].count;
25513
+ }
25514
+
25515
+ var h = '<div class="heatmap-grid">';
25516
+ // Header row
25517
+ h += '<div class="heatmap-label"></div>';
25518
+ for (var hr = 0; hr < 24; hr++) {
25519
+ h += '<div class="heatmap-label">' + (hr % 3 === 0 ? hr + 'h' : '') + '</div>';
25520
+ }
25521
+ // Data rows
25522
+ for (var d = 1; d <= 7; d++) {
25523
+ var dayIdx = d % 7; // Mon=1 -> dayIdx=1, Sun=0 -> dayIdx=0
25524
+ h += '<div class="heatmap-label">' + dayLabels[dayIdx] + '</div>';
25525
+ for (var hr = 0; hr < 24; hr++) {
25526
+ var count = grid[dayIdx + '-' + hr] || 0;
25527
+ var intensity = count / maxCount;
25528
+ var bg;
25529
+ if (count === 0) bg = 'var(--panel-muted)';
25530
+ else if (intensity < 0.25) bg = 'rgba(74,222,128,0.15)';
25531
+ else if (intensity < 0.5) bg = 'rgba(74,222,128,0.3)';
25532
+ else if (intensity < 0.75) bg = 'rgba(74,222,128,0.55)';
25533
+ else bg = 'rgba(74,222,128,0.8)';
25534
+ h += '<div class="heatmap-cell" style="background:' + bg + '" title="' + dayLabels[dayIdx] + ' ' + hr + ':00 - ' + count + ' sessions"></div>';
25535
+ }
25536
+ }
25537
+ h += '</div>';
25538
+ h += '<div style="display:flex;align-items:center;gap:4px;margin-top:6px;font-size:9px;color:var(--text-dim)"><span>Less</span>';
25539
+ h += '<div style="width:10px;height:10px;background:var(--panel-muted);border-radius:2px"></div>';
25540
+ h += '<div style="width:10px;height:10px;background:rgba(74,222,128,0.15);border-radius:2px"></div>';
25541
+ h += '<div style="width:10px;height:10px;background:rgba(74,222,128,0.3);border-radius:2px"></div>';
25542
+ h += '<div style="width:10px;height:10px;background:rgba(74,222,128,0.55);border-radius:2px"></div>';
25543
+ h += '<div style="width:10px;height:10px;background:rgba(74,222,128,0.8);border-radius:2px"></div>';
25544
+ h += '<span>More</span></div>';
25545
+ el.innerHTML = h;
25546
+ }
25547
+
25548
+ function renderTopModels(models) {
25549
+ var el = document.getElementById('ins-models-area');
25550
+ if (!models || models.length === 0) { el.innerHTML = '<div class="ins-loading">No model data.</div>'; return; }
25551
+
25552
+ var maxCost = 0.01;
25553
+ for (var i = 0; i < models.length; i++) {
25554
+ if (models[i].totalCostUsd > maxCost) maxCost = models[i].totalCostUsd;
25555
+ }
25556
+
25557
+ var h = '';
25558
+ for (var i = 0; i < models.length; i++) {
25559
+ var m = models[i];
25560
+ var pct = (m.totalCostUsd / maxCost) * 100;
25561
+ var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
25562
+ h += '<div class="hbar-row">';
25563
+ h += '<div class="hbar-label" title="' + esc(m.model) + '">' + esc((m.model.indexOf('/') >= 0 ? m.model.split('/').pop() : m.model)) + '</div>';
25564
+ h += '<div class="hbar-track"><div class="hbar-fill" style="width:' + pct + '%;background:' + col + '"></div></div>';
25565
+ h += '<div class="hbar-value">$' + m.totalCostUsd.toFixed(2) + '</div>';
25566
+ h += '</div>';
25567
+ }
25568
+ el.innerHTML = h;
25569
+ }
25570
+
25571
+ function renderTopTools(tools) {
25572
+ var el = document.getElementById('ins-tools-area');
25573
+ if (!tools || tools.length === 0) { el.innerHTML = '<div class="ins-loading">No tool data.</div>'; return; }
25574
+
25575
+ var maxCalls = 1;
25576
+ for (var i = 0; i < tools.length; i++) {
25577
+ if (tools[i].callCount > maxCalls) maxCalls = tools[i].callCount;
25578
+ }
25579
+
25580
+ var h = '';
25581
+ var shown = tools.slice(0, 10);
25582
+ for (var i = 0; i < shown.length; i++) {
25583
+ var t = shown[i];
25584
+ var pct = (t.callCount / maxCalls) * 100;
25585
+ var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
25586
+ h += '<div class="hbar-row">';
25587
+ h += '<div class="hbar-label">' + esc(t.tool) + '</div>';
25588
+ h += '<div class="hbar-track"><div class="hbar-fill" style="width:' + pct + '%;background:' + col + '"></div></div>';
25589
+ h += '<div class="hbar-value">' + fmtNum(t.callCount) + '</div>';
25590
+ h += '</div>';
25591
+ }
25592
+ el.innerHTML = h;
25593
+ }
25594
+
25595
+ function renderMemberCards(members) {
25596
+ var el = document.getElementById('ins-members-area');
25597
+ if (!members || members.length === 0) { el.innerHTML = '<div class="ins-loading">No member data.</div>'; return; }
25598
+
25599
+ var h = '';
25600
+ for (var i = 0; i < members.length; i++) {
25601
+ var m = members[i];
25602
+ var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
25603
+ var name = m.displayName || m.userId;
25604
+ h += '<div class="member-card">';
25605
+ h += '<div class="member-card-hd" onclick="toggleMemberCard(this)">';
25606
+ h += '<div><div class="member-card-name" style="color:' + col + '">' + esc(name) + '</div>';
25607
+ if (m.displayName && m.userId !== m.displayName) h += '<div class="member-card-email">' + esc(m.userId) + '</div>';
25608
+ h += '</div>';
25609
+ h += '<div class="member-card-stats">';
25610
+ h += '<span class="badge orange">' + fmtCost(m.totalCostUsd) + '</span>';
25611
+ h += '<span class="badge green">' + m.sessionCount + ' sessions</span>';
25612
+ h += '<span class="badge">' + m.commitCount + ' commits</span>';
25613
+ h += '</div>';
25614
+ h += '</div>';
25615
+ h += '<div class="member-card-body" id="mc-body-' + i + '">';
25616
+
25617
+ // Metrics grid
25618
+ h += '<div class="member-card-metrics">';
25619
+ h += '<div class="member-card-metric"><div class="m-label">Avg $/Session</div><div class="m-val orange">$' + (m.avgSessionCostUsd || 0).toFixed(2) + '</div></div>';
25620
+ h += '<div class="member-card-metric"><div class="m-label">$/Commit</div><div class="m-val cyan">$' + (m.costPerCommit || 0).toFixed(2) + '</div></div>';
25621
+ h += '<div class="member-card-metric"><div class="m-label">Lines Changed</div><div class="m-val"><span class="green">+' + fmtNum(m.linesAdded) + '</span> <span class="red">-' + fmtNum(m.linesRemoved) + '</span></div></div>';
25622
+ h += '<div class="member-card-metric"><div class="m-label">Pull Requests</div><div class="m-val purple">' + m.prCount + '</div></div>';
25623
+ h += '</div>';
25624
+
25625
+ // Tokens
25626
+ h += '<div style="margin-bottom:12px;font-size:11px;color:var(--text-dim)">';
25627
+ h += '<span style="margin-right:12px">Input tokens: <span class="cyan">' + formatLargeNum(m.totalInputTokens) + '</span></span>';
25628
+ h += '<span>Output tokens: <span class="cyan">' + formatLargeNum(m.totalOutputTokens) + '</span></span>';
25629
+ h += '</div>';
25630
+
25631
+ // Models used
25632
+ if (m.models && m.models.length > 0) {
25633
+ h += '<div style="margin-bottom:10px"><div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px">Models</div>';
25634
+ for (var j = 0; j < m.models.length; j++) {
25635
+ var mdl = m.models[j];
25636
+ h += '<div class="hbar-row"><div class="hbar-label" style="width:120px">' + esc((mdl.model.indexOf('/') >= 0 ? mdl.model.split('/').pop() : mdl.model)) + '</div>';
25637
+ var modelMax = m.models[0].totalCostUsd || 0.01;
25638
+ h += '<div class="hbar-track"><div class="hbar-fill" style="width:' + ((mdl.totalCostUsd / modelMax) * 100) + '%;background:' + col + '"></div></div>';
25639
+ h += '<div class="hbar-value">$' + mdl.totalCostUsd.toFixed(2) + '</div></div>';
25640
+ }
25641
+ h += '</div>';
25642
+ }
25643
+
25644
+ // Repos
25645
+ if (m.repos && m.repos.length > 0) {
25646
+ h += '<div style="margin-bottom:10px"><div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px">Repositories</div>';
25647
+ for (var j = 0; j < m.repos.length; j++) {
25648
+ var rp = m.repos[j];
25649
+ h += '<div style="display:flex;align-items:center;gap:8px;padding:2px 0;font-size:11px">';
25650
+ h += '<span style="color:var(--text-primary)">' + esc(rp.repo) + '</span>';
25651
+ h += '<span class="badge">' + rp.sessionCount + ' sessions</span>';
25652
+ h += '<span class="badge green">' + rp.commitCount + ' commits</span>';
25653
+ h += '</div>';
25654
+ }
25655
+ h += '</div>';
25656
+ }
25657
+
25658
+ // Activity mini-charts (hourly)
25659
+ h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">';
25660
+ // Hourly
25661
+ h += '<div><div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px">Hourly Activity</div>';
25662
+ h += '<div class="member-mini-chart">';
25663
+ var maxHour = 1;
25664
+ if (m.hourlyActivity) { for (var hr = 0; hr < 24; hr++) { if (m.hourlyActivity[hr] > maxHour) maxHour = m.hourlyActivity[hr]; } }
25665
+ for (var hr = 0; hr < 24; hr++) {
25666
+ var val = (m.hourlyActivity && m.hourlyActivity[hr]) || 0;
25667
+ var barH = val > 0 ? Math.max(2, Math.round((val / maxHour) * 36)) : 1;
25668
+ h += '<div class="member-mini-bar" style="height:' + barH + 'px;background:' + (val > 0 ? col : 'var(--panel-muted)') + '"></div>';
25669
+ }
25670
+ h += '</div><div style="display:flex;justify-content:space-between;font-size:8px;color:var(--text-dim);margin-top:2px"><span>0h</span><span>12h</span><span>23h</span></div>';
25671
+ h += '</div>';
25672
+
25673
+ // Daily
25674
+ h += '<div><div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px">Daily Activity</div>';
25675
+ h += '<div class="member-mini-chart" style="gap:3px">';
25676
+ var dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
25677
+ var maxDay = 1;
25678
+ if (m.dailyActivity) { for (var dd = 0; dd < 7; dd++) { if (m.dailyActivity[dd] > maxDay) maxDay = m.dailyActivity[dd]; } }
25679
+ for (var dd = 0; dd < 7; dd++) {
25680
+ var val = (m.dailyActivity && m.dailyActivity[dd]) || 0;
25681
+ var barH = val > 0 ? Math.max(2, Math.round((val / maxDay) * 36)) : 1;
25682
+ h += '<div style="display:flex;flex-direction:column;align-items:center;flex:1"><div class="member-mini-bar" style="height:' + barH + 'px;background:' + (val > 0 ? col : 'var(--panel-muted)') + ';width:100%"></div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">' + dayNames[dd].charAt(0) + '</div></div>';
25683
+ }
25684
+ h += '</div>';
25685
+ h += '</div>';
25686
+ h += '</div>';
25687
+
25688
+ h += '</div>'; // member-card-body
25689
+ h += '</div>'; // member-card
25690
+ }
25691
+ el.innerHTML = h;
25692
+ }
25693
+
25694
+ window.toggleMemberCard = function(hd) {
25695
+ var body = hd.nextElementSibling;
25696
+ body.classList.toggle('open');
25697
+ };
25698
+
25699
+ // ===== AI TEAM INSIGHTS =====
25700
+ var teamInsightData = null;
25701
+
25702
+ window.generateTeamInsight = function(force) {
25703
+ var btn = document.getElementById('ins-ai-btn');
25704
+ var status = document.getElementById('ins-ai-status');
25705
+ var content = document.getElementById('ins-ai-content');
25706
+ if (!btn) return;
25707
+
25708
+ btn.disabled = true;
25709
+ btn.textContent = 'Analyzing...';
25710
+ if (status) {
25711
+ status.textContent = 'Sending team data to AI provider. This may take 15-30 seconds...';
25712
+ status.style.color = 'var(--text-dim)';
25713
+ }
25714
+
25715
+ var r = getInsightsDateRange();
25716
+ var forceQs = force ? '&force=true' : '';
25717
+ authFetch('/api/team/insights/generate?from=' + r.from + '&to=' + r.to + forceQs, {
25718
+ method: 'POST',
25719
+ headers: { 'Content-Type': 'application/json' },
25720
+ body: '{}'
25721
+ })
25722
+ .then(function(res) { return res.json().then(function(d) { return { ok: res.ok, data: d }; }); })
25723
+ .then(function(res) {
25724
+ btn.disabled = false;
25725
+ btn.textContent = 'Regenerate Team Analysis';
25726
+ if (status) status.textContent = '';
25727
+ if (res.ok && res.data.status === 'ok' && res.data.insight) {
25728
+ teamInsightData = res.data.insight;
25729
+ renderTeamInsight(res.data.insight);
25730
+ } else {
25731
+ var msg = (res.data && res.data.message) ? res.data.message : 'Failed to generate team insight';
25732
+ if (status) {
25733
+ status.textContent = msg;
25734
+ status.style.color = 'var(--red)';
25735
+ }
25736
+ }
25737
+ })
25738
+ .catch(function(e) {
25739
+ btn.disabled = false;
25740
+ btn.textContent = 'Generate Team Analysis';
25741
+ if (status) {
25742
+ status.textContent = String(e);
25743
+ status.style.color = 'var(--red)';
25744
+ }
25745
+ });
25746
+ };
25747
+
25748
+ function renderTeamInsight(insight) {
25749
+ var el = document.getElementById('ins-ai-content');
25750
+ if (!el) return;
25751
+
25752
+ var h = '';
25753
+
25754
+ // Header with provider info
25755
+ h += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">';
25756
+ h += '<div style="font-size:10px;color:var(--text-dim)">' + esc(insight.provider || '') + ' / ' + esc(insight.model || '') + ' &mdash; ' + new Date(insight.generatedAt).toLocaleString() + '</div>';
25757
+ h += '<button class="insight-gen-btn" onclick="generateTeamInsight(true)" style="font-size:10px;padding:4px 10px">Regenerate</button>';
25758
+ h += '</div>';
25759
+
25760
+ // Executive Summary
25761
+ h += '<div style="border:1px solid rgba(192,132,252,.15);border-radius:8px;padding:12px;margin-bottom:14px;background:rgba(192,132,252,.06)">';
25762
+ h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--purple);margin-bottom:6px;font-weight:600">Executive Summary</div>';
25763
+ h += '<div style="font-size:13px;color:var(--text-primary);line-height:1.7">' + esc(insight.executiveSummary) + '</div>';
25764
+ h += '</div>';
25765
+
25766
+ // Cost & Productivity Analysis side by side
25767
+ h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">';
25768
+
25769
+ // Cost Analysis
25770
+ h += '<div style="border:1px solid rgba(251,146,60,.15);border-radius:8px;padding:12px;background:rgba(251,146,60,.04)">';
25771
+ h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--orange);margin-bottom:6px;font-weight:600">Cost Analysis</div>';
25772
+ h += '<div style="font-size:12px;color:var(--text-primary);line-height:1.7">' + esc(insight.costAnalysis) + '</div>';
25773
+ h += '</div>';
25774
+
25775
+ // Productivity Analysis
25776
+ h += '<div style="border:1px solid rgba(74,222,128,.15);border-radius:8px;padding:12px;background:rgba(74,222,128,.04)">';
25777
+ h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--green);margin-bottom:6px;font-weight:600">Productivity Analysis</div>';
25778
+ h += '<div style="font-size:12px;color:var(--text-primary);line-height:1.7">' + esc(insight.productivityAnalysis) + '</div>';
25779
+ h += '</div>';
25780
+
25781
+ h += '</div>';
25782
+
25783
+ // Member Highlights
25784
+ if (insight.memberHighlights && insight.memberHighlights.length > 0) {
25785
+ h += '<div style="margin-bottom:14px">';
25786
+ h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:8px;font-weight:600">Member Highlights</div>';
25787
+ for (var i = 0; i < insight.memberHighlights.length; i++) {
25788
+ var mh = insight.memberHighlights[i];
25789
+ var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
25790
+ h += '<div style="border:1px solid var(--panel-border);border-radius:8px;padding:10px 12px;margin-bottom:8px;background:var(--panel);border-left:3px solid ' + col + '">';
25791
+ h += '<div style="font-size:12px;font-weight:600;color:' + col + ';margin-bottom:4px">' + esc(mh.displayName || mh.userId) + '</div>';
25792
+ h += '<div style="font-size:11px;color:var(--text-muted);line-height:1.6">';
25793
+ h += '<div style="margin-bottom:3px"><span style="color:var(--green);font-weight:600">Strength:</span> ' + esc(mh.strength) + '</div>';
25794
+ if (mh.concern) {
25795
+ h += '<div style="margin-bottom:3px"><span style="color:var(--orange);font-weight:600">Watch:</span> ' + esc(mh.concern) + '</div>';
25796
+ }
25797
+ h += '<div><span style="color:var(--cyan);font-weight:600">Recommendation:</span> ' + esc(mh.recommendation) + '</div>';
25798
+ h += '</div>';
25799
+ h += '</div>';
25800
+ }
25801
+ h += '</div>';
25802
+ }
25803
+
25804
+ // Risks and Recommendations side by side
25805
+ h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">';
25806
+
25807
+ // Risks
25808
+ if (insight.risks && insight.risks.length > 0) {
25809
+ h += '<div style="border:1px solid rgba(248,113,113,.15);border-radius:8px;padding:12px;background:rgba(248,113,113,.04)">';
25810
+ h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--red);margin-bottom:8px;font-weight:600">Risks</div>';
25811
+ for (var i = 0; i < insight.risks.length; i++) {
25812
+ h += '<div class="ins-ai-item">' + esc(insight.risks[i]) + '</div>';
25813
+ }
25814
+ h += '</div>';
25815
+ } else {
25816
+ h += '<div></div>';
25817
+ }
25818
+
25819
+ // Recommendations
25820
+ if (insight.recommendations && insight.recommendations.length > 0) {
25821
+ h += '<div style="border:1px solid rgba(34,211,238,.15);border-radius:8px;padding:12px;background:rgba(34,211,238,.04)">';
25822
+ h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--cyan);margin-bottom:8px;font-weight:600">Recommendations</div>';
25823
+ for (var i = 0; i < insight.recommendations.length; i++) {
25824
+ h += '<div class="ins-ai-item" style="color:var(--text-muted)">' + esc(insight.recommendations[i]) + '</div>';
25825
+ }
25826
+ h += '</div>';
25827
+ } else {
25828
+ h += '<div></div>';
25829
+ }
25830
+
25831
+ h += '</div>';
25832
+
25833
+ // Forecast
25834
+ if (insight.forecast) {
25835
+ h += '<div class="ins-forecast" style="margin-bottom:8px">';
25836
+ h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--orange);margin-bottom:4px;font-weight:600">Forecast</div>';
25837
+ h += '<div style="font-size:12px;color:var(--text-primary);line-height:1.6">' + esc(insight.forecast) + '</div>';
25838
+ h += '</div>';
25839
+ }
25840
+
25841
+ el.innerHTML = h;
25842
+ }
25843
+
25844
+ // ===== TEAM INSIGHTS CONTEXT =====
25845
+ window.openContextModal = function() {
25846
+ var modal = document.getElementById('context-modal');
25847
+ if (!modal) return;
25848
+ var statusEl = document.getElementById('context-status');
25849
+ if (statusEl) statusEl.textContent = '';
25850
+ authFetch('/api/team/insights/context')
25851
+ .then(function(r) { return r.json(); })
25852
+ .then(function(data) {
25853
+ if (data.configured && data.context) {
25854
+ var co = document.getElementById('context-company');
25855
+ var gu = document.getElementById('context-guidelines');
25856
+ if (co) co.value = data.context.companyContext || '';
25857
+ if (gu) gu.value = data.context.analysisGuidelines || '';
25858
+ }
25859
+ modal.classList.add('open');
25860
+ })
25861
+ .catch(function() { modal.classList.add('open'); });
25862
+ };
25863
+
25864
+ window.closeContextModal = function() {
25865
+ var modal = document.getElementById('context-modal');
25866
+ if (modal) modal.classList.remove('open');
25867
+ };
25868
+
25869
+ window.saveContext = function() {
25870
+ var btn = document.getElementById('context-save-btn');
25871
+ var statusEl = document.getElementById('context-status');
25872
+ var co = document.getElementById('context-company');
25873
+ var gu = document.getElementById('context-guidelines');
25874
+ if (!co || !gu) return;
25875
+ var body = { companyContext: co.value, analysisGuidelines: gu.value };
25876
+ if (!body.companyContext && !body.analysisGuidelines) {
25877
+ if (statusEl) { statusEl.textContent = 'Enter at least one field'; statusEl.className = 'modal-status error'; }
25878
+ return;
25879
+ }
25880
+ if (btn) btn.disabled = true;
25881
+ if (statusEl) { statusEl.textContent = 'Saving...'; statusEl.className = 'modal-status'; }
25882
+ authFetch('/api/team/insights/context', {
25883
+ method: 'POST',
25884
+ headers: { 'Content-Type': 'application/json' },
25885
+ body: JSON.stringify(body)
25886
+ })
25887
+ .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
25888
+ .then(function(res) {
25889
+ if (btn) btn.disabled = false;
25890
+ if (res.ok && res.data.status === 'ok') {
25891
+ if (statusEl) { statusEl.textContent = 'Saved'; statusEl.className = 'modal-status ok'; }
25892
+ updateContextIndicator();
25893
+ setTimeout(function() { window.closeContextModal(); }, 800);
25894
+ } else {
25895
+ var msg = (res.data && res.data.message) ? res.data.message : 'Failed to save';
25896
+ if (statusEl) { statusEl.textContent = msg; statusEl.className = 'modal-status error'; }
25897
+ }
25898
+ })
25899
+ .catch(function(e) {
25900
+ if (btn) btn.disabled = false;
25901
+ if (statusEl) { statusEl.textContent = String(e); statusEl.className = 'modal-status error'; }
25902
+ });
25903
+ };
25904
+
25905
+ function updateContextIndicator() {
25906
+ authFetch('/api/team/insights/context')
25907
+ .then(function(r) { return r.json(); })
25908
+ .then(function(data) {
25909
+ var el = document.getElementById('context-indicator');
25910
+ if (!el) return;
25911
+ if (data.configured && data.context && data.context.updatedAt) {
25912
+ el.textContent = 'Custom context active \xB7 saved ' + new Date(data.context.updatedAt).toLocaleDateString();
25913
+ } else {
25914
+ el.textContent = '';
25915
+ }
25916
+ })
25917
+ .catch(function() {});
25918
+ }
25919
+
25920
+ function boot() {
25921
+ checkAuth().then(function(ok) {
25922
+ if (!ok) return;
25923
+ startStreaming();
25924
+ updateContextIndicator();
25925
+ });
25926
+ }
25927
+
24750
25928
  boot();
24751
25929
  })();
24752
25930
  </script>
@@ -25046,6 +26224,97 @@ async function startDashboardServer(options = {}) {
25046
26224
  const pathname = parsePathname(url);
25047
26225
  const segments = parsePathSegments(pathname);
25048
26226
  const method = req.method ?? "GET";
26227
+ if (method === "POST" && pathname === "/api/team/budget") {
26228
+ let body = "";
26229
+ req.setEncoding("utf8");
26230
+ req.on("data", (chunk) => {
26231
+ body += chunk;
26232
+ });
26233
+ req.on("end", () => {
26234
+ let parsedBody;
26235
+ try {
26236
+ parsedBody = body.length > 0 ? JSON.parse(body) : {};
26237
+ } catch {
26238
+ sendJson(res, 400, { status: "error", message: "invalid JSON body" });
26239
+ return;
26240
+ }
26241
+ const authHeader = req.headers["authorization"];
26242
+ const headers = { "Content-Type": "application/json" };
26243
+ if (typeof authHeader === "string") {
26244
+ headers["Authorization"] = authHeader;
26245
+ }
26246
+ void fetch(`${apiBaseUrl}/v1/team/budget`, {
26247
+ method: "POST",
26248
+ headers,
26249
+ body: JSON.stringify(parsedBody)
26250
+ }).then(async (apiResponse) => {
26251
+ const payload = await apiResponse.json();
26252
+ sendJson(res, apiResponse.status, payload);
26253
+ }).catch((error) => {
26254
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
26255
+ });
26256
+ });
26257
+ return;
26258
+ }
26259
+ if (method === "POST" && pathname === "/api/team/insights/context") {
26260
+ let body = "";
26261
+ req.setEncoding("utf8");
26262
+ req.on("data", (chunk) => {
26263
+ body += chunk;
26264
+ });
26265
+ req.on("end", () => {
26266
+ let parsedBody;
26267
+ try {
26268
+ parsedBody = body.length > 0 ? JSON.parse(body) : {};
26269
+ } catch {
26270
+ sendJson(res, 400, { status: "error", message: "invalid JSON body" });
26271
+ return;
26272
+ }
26273
+ const authHeader = req.headers["authorization"];
26274
+ const headers = { "Content-Type": "application/json" };
26275
+ if (typeof authHeader === "string") {
26276
+ headers["Authorization"] = authHeader;
26277
+ }
26278
+ void fetch(`${apiBaseUrl}/v1/team/insights/context`, {
26279
+ method: "POST",
26280
+ headers,
26281
+ body: JSON.stringify(parsedBody)
26282
+ }).then(async (apiResponse) => {
26283
+ const payload = await apiResponse.json();
26284
+ sendJson(res, apiResponse.status, payload);
26285
+ }).catch((error) => {
26286
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
26287
+ });
26288
+ });
26289
+ return;
26290
+ }
26291
+ if (method === "POST" && pathname === "/api/team/insights/generate") {
26292
+ let body = "";
26293
+ req.setEncoding("utf8");
26294
+ req.on("data", (chunk) => {
26295
+ body += chunk;
26296
+ });
26297
+ req.on("end", () => {
26298
+ const parsedUrl = new URL(url, "http://localhost");
26299
+ const queryString = parsedUrl.search;
26300
+ const authHeader = req.headers["authorization"];
26301
+ const headers = { "Content-Type": "application/json" };
26302
+ if (typeof authHeader === "string") {
26303
+ headers["Authorization"] = authHeader;
26304
+ }
26305
+ void fetch(`${apiBaseUrl}/v1/team/insights/generate${queryString}`, {
26306
+ method: "POST",
26307
+ headers,
26308
+ body: body.length > 0 ? body : "{}"
26309
+ }).then(async (apiResponse) => {
26310
+ const payload = await apiResponse.json();
26311
+ sendJson(res, apiResponse.status, payload);
26312
+ }).catch((error) => {
26313
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
26314
+ });
26315
+ });
26316
+ return;
26317
+ }
25049
26318
  if (method === "POST" && (pathname === "/api/settings/insights" || segments.length === 4 && segments[0] === "api" && segments[1] === "session" && segments[3] === "insights")) {
25050
26319
  let body = "";
25051
26320
  req.setEncoding("utf8");
@@ -25129,6 +26398,88 @@ async function startDashboardServer(options = {}) {
25129
26398
  });
25130
26399
  return;
25131
26400
  }
26401
+ if (pathname === "/api/auth/check") {
26402
+ const authHeader = req.headers["authorization"];
26403
+ const headers = {};
26404
+ if (typeof authHeader === "string") {
26405
+ headers["Authorization"] = authHeader;
26406
+ }
26407
+ void fetch(`${apiBaseUrl}/v1/auth/check`, { headers }).then(async (apiResponse) => {
26408
+ const payload = await apiResponse.json();
26409
+ sendJson(res, apiResponse.status, payload);
26410
+ }).catch((error) => {
26411
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
26412
+ });
26413
+ return;
26414
+ }
26415
+ if (pathname.startsWith("/api/team/")) {
26416
+ const authHeader = req.headers["authorization"];
26417
+ const headers = {};
26418
+ if (typeof authHeader === "string") {
26419
+ headers["Authorization"] = authHeader;
26420
+ }
26421
+ const parsedUrl = new URL(url, "http://localhost");
26422
+ const queryString = parsedUrl.search;
26423
+ const teamPath = pathname.replace("/api/team/", "/v1/team/");
26424
+ if (pathname === "/api/team/stream") {
26425
+ res.statusCode = 200;
26426
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
26427
+ res.setHeader("Cache-Control", "no-cache");
26428
+ res.setHeader("Connection", "keep-alive");
26429
+ res.setHeader("X-Accel-Buffering", "no");
26430
+ res.flushHeaders();
26431
+ let closed = false;
26432
+ let writing = false;
26433
+ const writeTeamSnapshot = async () => {
26434
+ if (closed || writing) return;
26435
+ writing = true;
26436
+ try {
26437
+ const [overviewRes, membersRes, costRes, budgetRes] = await Promise.all([
26438
+ fetch(`${apiBaseUrl}/v1/team/overview${queryString}`, { headers }),
26439
+ fetch(`${apiBaseUrl}/v1/team/members${queryString}`, { headers }),
26440
+ fetch(`${apiBaseUrl}/v1/team/cost/daily${queryString}`, { headers }),
26441
+ fetch(`${apiBaseUrl}/v1/team/budget`, { headers })
26442
+ ]);
26443
+ const overview = await overviewRes.json();
26444
+ const members = await membersRes.json();
26445
+ const cost = await costRes.json();
26446
+ const budget = await budgetRes.json();
26447
+ const payload = JSON.stringify({ overview, members, cost, budget, emittedAt: (/* @__PURE__ */ new Date()).toISOString() });
26448
+ res.write("event: team\n");
26449
+ res.write(`data: ${payload}
26450
+
26451
+ `);
26452
+ } catch (error) {
26453
+ res.write("event: bridge_error\n");
26454
+ res.write(`data: ${JSON.stringify({ message: String(error) })}
26455
+
26456
+ `);
26457
+ } finally {
26458
+ writing = false;
26459
+ }
26460
+ };
26461
+ void writeTeamSnapshot();
26462
+ const interval = setInterval(() => {
26463
+ void writeTeamSnapshot();
26464
+ }, 2e3);
26465
+ const cleanup = () => {
26466
+ if (closed) return;
26467
+ closed = true;
26468
+ clearInterval(interval);
26469
+ if (!res.writableEnded) res.end();
26470
+ };
26471
+ req.on("close", cleanup);
26472
+ res.on("close", cleanup);
26473
+ return;
26474
+ }
26475
+ void fetch(`${apiBaseUrl}${teamPath}${queryString}`, { headers }).then(async (apiResponse) => {
26476
+ const payload = await apiResponse.json();
26477
+ sendJson(res, apiResponse.status, payload);
26478
+ }).catch((error) => {
26479
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
26480
+ });
26481
+ return;
26482
+ }
25132
26483
  if (segments.length === 3 && segments[0] === "api" && segments[1] === "session") {
25133
26484
  const encodedSessionId = segments[2];
25134
26485
  let sessionId = "";
@@ -25181,7 +26532,7 @@ async function startDashboardServer(options = {}) {
25181
26532
  return;
25182
26533
  }
25183
26534
  if (pathname === "/") {
25184
- sendHtml(res, 200, renderDashboardHtml());
26535
+ sendHtml(res, 200, renderDashboardHtml(options.currentUserEmail !== void 0 ? { currentUserEmail: options.currentUserEmail } : {}));
25185
26536
  return;
25186
26537
  }
25187
26538
  sendJson(res, 404, {
@@ -25341,6 +26692,7 @@ var SqliteClient = class {
25341
26692
  this.migrateDeduplicateEvents();
25342
26693
  this.db.exec(SCHEMA_SQL);
25343
26694
  this.migrateRebuildBrokenTraces();
26695
+ this.migrateTeamColumns();
25344
26696
  }
25345
26697
  async insertJsonEachRow(request) {
25346
26698
  if (request.rows.length === 0) return;
@@ -25352,18 +26704,20 @@ var SqliteClient = class {
25352
26704
  if (rows.length === 0) return;
25353
26705
  const upsert = this.db.prepare(`
25354
26706
  INSERT INTO session_traces
25355
- (session_id, version, started_at, ended_at, user_id, git_repo, git_branch,
26707
+ (session_id, version, started_at, ended_at, user_id, user_email, user_display_name, git_repo, git_branch,
25356
26708
  prompt_count, tool_call_count, api_call_count, total_cost_usd,
25357
26709
  total_input_tokens, total_output_tokens, total_cache_read_tokens, total_cache_write_tokens,
25358
26710
  lines_added, lines_removed,
25359
26711
  models_used, tools_used, files_touched, commit_count, updated_at)
25360
26712
  VALUES
25361
- (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
26713
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
25362
26714
  ON CONFLICT(session_id) DO UPDATE SET
25363
26715
  version = excluded.version,
25364
26716
  started_at = excluded.started_at,
25365
26717
  ended_at = excluded.ended_at,
25366
26718
  user_id = excluded.user_id,
26719
+ user_email = COALESCE(excluded.user_email, session_traces.user_email),
26720
+ user_display_name = COALESCE(excluded.user_display_name, session_traces.user_display_name),
25367
26721
  git_repo = excluded.git_repo,
25368
26722
  git_branch = excluded.git_branch,
25369
26723
  prompt_count = excluded.prompt_count,
@@ -25390,6 +26744,8 @@ var SqliteClient = class {
25390
26744
  row.started_at,
25391
26745
  row.ended_at,
25392
26746
  row.user_id,
26747
+ row.user_email ?? null,
26748
+ row.user_display_name ?? null,
25393
26749
  row.git_repo,
25394
26750
  row.git_branch,
25395
26751
  row.prompt_count,
@@ -25611,6 +26967,32 @@ var SqliteClient = class {
25611
26967
  updated_at = excluded.updated_at
25612
26968
  `).run(key, value);
25613
26969
  }
26970
+ getTeamBudget() {
26971
+ const row = this.db.prepare(
26972
+ "SELECT monthly_limit_usd, alert_threshold_percent FROM team_budgets WHERE id = 1"
26973
+ ).get();
26974
+ if (row === void 0) return void 0;
26975
+ return {
26976
+ monthlyLimitUsd: row.monthly_limit_usd,
26977
+ alertThresholdPercent: row.alert_threshold_percent
26978
+ };
26979
+ }
26980
+ upsertTeamBudget(limitUsd, alertPercent) {
26981
+ this.db.prepare(`
26982
+ INSERT INTO team_budgets (id, monthly_limit_usd, alert_threshold_percent, updated_at)
26983
+ VALUES (1, ?, ?, datetime('now'))
26984
+ ON CONFLICT(id) DO UPDATE SET
26985
+ monthly_limit_usd = excluded.monthly_limit_usd,
26986
+ alert_threshold_percent = excluded.alert_threshold_percent,
26987
+ updated_at = excluded.updated_at
26988
+ `).run(limitUsd, alertPercent);
26989
+ }
26990
+ getMonthSpend(yearMonth) {
26991
+ const row = this.db.prepare(
26992
+ "SELECT COALESCE(SUM(total_cost_usd), 0) AS spend FROM session_traces WHERE substr(started_at, 1, 7) = ?"
26993
+ ).get(yearMonth);
26994
+ return row.spend;
26995
+ }
25614
26996
  close() {
25615
26997
  this.db.close();
25616
26998
  }
@@ -25742,6 +27124,42 @@ var SqliteClient = class {
25742
27124
  console.log("[agent-trace] migrating: rebuilding session traces with correct models/tools");
25743
27125
  this.rebuildSessionTracesFromEvents();
25744
27126
  }
27127
+ /**
27128
+ * Migration: add team-related columns and tables.
27129
+ */
27130
+ migrateTeamColumns() {
27131
+ const addColumnIfMissing = (table, column, definition) => {
27132
+ const cols = this.db.prepare(`PRAGMA table_info('${table}')`).all();
27133
+ if (!cols.some((c) => c.name === column)) {
27134
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
27135
+ }
27136
+ };
27137
+ const tracesExist = this.db.prepare(
27138
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_traces'"
27139
+ ).get();
27140
+ if (tracesExist !== void 0) {
27141
+ addColumnIfMissing("session_traces", "user_email", "TEXT");
27142
+ addColumnIfMissing("session_traces", "user_display_name", "TEXT");
27143
+ }
27144
+ const eventsExist = this.db.prepare(
27145
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='agent_events'"
27146
+ ).get();
27147
+ if (eventsExist !== void 0) {
27148
+ addColumnIfMissing("agent_events", "user_email", "TEXT");
27149
+ }
27150
+ this.db.exec(`
27151
+ CREATE TABLE IF NOT EXISTS team_budgets (
27152
+ id INTEGER PRIMARY KEY DEFAULT 1,
27153
+ monthly_limit_usd REAL NOT NULL,
27154
+ alert_threshold_percent REAL NOT NULL DEFAULT 80,
27155
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
27156
+ )
27157
+ `);
27158
+ this.db.exec(`
27159
+ CREATE INDEX IF NOT EXISTS idx_traces_started_at ON session_traces(started_at);
27160
+ CREATE INDEX IF NOT EXISTS idx_traces_user_id ON session_traces(user_id)
27161
+ `);
27162
+ }
25745
27163
  /**
25746
27164
  * Rebuild session_traces by aggregating deduplicated agent_events.
25747
27165
  * Called after dedup migration so the dashboard has correct metrics immediately.
@@ -26715,7 +28133,7 @@ function createAnthropicProvider(apiKey, model) {
26715
28133
  return {
26716
28134
  provider: "anthropic",
26717
28135
  model,
26718
- async complete(system, user) {
28136
+ async complete(system, user, maxTokens) {
26719
28137
  const response = await fetch(endpoint, {
26720
28138
  method: "POST",
26721
28139
  headers: {
@@ -26725,7 +28143,7 @@ function createAnthropicProvider(apiKey, model) {
26725
28143
  },
26726
28144
  body: JSON.stringify({
26727
28145
  model,
26728
- max_tokens: 1024,
28146
+ max_tokens: maxTokens ?? 1024,
26729
28147
  system,
26730
28148
  messages: [{ role: "user", content: user }]
26731
28149
  })
@@ -26763,7 +28181,7 @@ function createOpenAiProvider(apiKey, model) {
26763
28181
  return {
26764
28182
  provider: "openai",
26765
28183
  model,
26766
- async complete(system, user) {
28184
+ async complete(system, user, maxTokens) {
26767
28185
  const response = await fetch(endpoint, {
26768
28186
  method: "POST",
26769
28187
  headers: {
@@ -26772,7 +28190,7 @@ function createOpenAiProvider(apiKey, model) {
26772
28190
  },
26773
28191
  body: JSON.stringify({
26774
28192
  model,
26775
- max_tokens: 1024,
28193
+ max_tokens: maxTokens ?? 1024,
26776
28194
  messages: [
26777
28195
  { role: "system", content: system },
26778
28196
  { role: "user", content: user }
@@ -26811,7 +28229,7 @@ function createGeminiProvider(apiKey, model) {
26811
28229
  return {
26812
28230
  provider: "gemini",
26813
28231
  model,
26814
- async complete(system, user) {
28232
+ async complete(system, user, maxTokens) {
26815
28233
  const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
26816
28234
  const response = await fetch(url, {
26817
28235
  method: "POST",
@@ -26819,7 +28237,7 @@ function createGeminiProvider(apiKey, model) {
26819
28237
  body: JSON.stringify({
26820
28238
  systemInstruction: { parts: [{ text: system }] },
26821
28239
  contents: [{ parts: [{ text: user }] }],
26822
- generationConfig: { maxOutputTokens: 1024 }
28240
+ generationConfig: { maxOutputTokens: maxTokens ?? 1024 }
26823
28241
  })
26824
28242
  });
26825
28243
  if (!response.ok) {
@@ -26851,7 +28269,7 @@ function createOpenRouterProvider(apiKey, model) {
26851
28269
  return {
26852
28270
  provider: "openrouter",
26853
28271
  model,
26854
- async complete(system, user) {
28272
+ async complete(system, user, maxTokens) {
26855
28273
  const response = await fetch(endpoint, {
26856
28274
  method: "POST",
26857
28275
  headers: {
@@ -26860,7 +28278,7 @@ function createOpenRouterProvider(apiKey, model) {
26860
28278
  },
26861
28279
  body: JSON.stringify({
26862
28280
  model,
26863
- max_tokens: 1024,
28281
+ max_tokens: maxTokens ?? 1024,
26864
28282
  messages: [
26865
28283
  { role: "system", content: system },
26866
28284
  { role: "user", content: user }
@@ -27037,8 +28455,239 @@ async function generateSessionInsight(trace, provider) {
27037
28455
  return result;
27038
28456
  }
27039
28457
 
28458
+ // packages/api/src/team-insights-generator.ts
28459
+ var TEAM_SYSTEM_PROMPT = `You are the AI analytics engine for agent-trace, a developer tool that tracks AI coding agent usage across engineering teams. A manager is viewing their team dashboard and has requested your analysis.
28460
+
28461
+ <role>
28462
+ You are a data analyst who turns raw telemetry into actionable management insights. You write like a sharp, senior engineering director \u2014 concise, evidence-based, zero fluff. Every sentence must reference a specific number from the data.
28463
+ </role>
28464
+
28465
+ <output_format>
28466
+ Respond with ONLY a single JSON object (no markdown fences, no commentary before or after). The JSON must conform exactly to this schema:
28467
+
28468
+ {"executiveSummary":"string","costAnalysis":"string","productivityAnalysis":"string","memberHighlights":[{"userId":"string","displayName":"string or null","strength":"string","concern":"string or null","recommendation":"string"}],"risks":["string"],"recommendations":["string"],"forecast":"string or null"}
28469
+ </output_format>
28470
+
28471
+ <field_guidelines>
28472
+ executiveSummary: 3-4 sentences. Open with the single most important finding. Include total spend, member count, commit count, and the key efficiency metric. End with overall team health assessment.
28473
+
28474
+ costAnalysis: 2-3 sentences. Rank members by cost-efficiency ($/commit). Call out the best and worst. If someone has high spend with zero or few commits, flag it directly. Compare individual $/commit to team average.
28475
+
28476
+ productivityAnalysis: 2-3 sentences. Analyze commit velocity, lines of code per session, and tool diversity. Note which tools correlate with higher output. Identify if anyone is underutilizing available tools.
28477
+
28478
+ memberHighlights: One entry per member in the data. userId must exactly match the data.
28479
+ - strength: Reference their specific numbers. What do they do well relative to the team?
28480
+ - concern: Reference specific numbers. Set to null only if genuinely nothing is concerning. Low commits, high cost, late-night patterns, or low tool diversity are all valid concerns.
28481
+ - recommendation: One specific, actionable suggestion. "Try model X to reduce cost" or "Pair with alice on tool Y adoption."
28482
+
28483
+ risks: 1-3 items. Only risks supported by the data. Examples: budget trajectory, workload imbalance (compare session counts), burnout signals (late-night activity), single-model dependency, low commit rates.
28484
+
28485
+ recommendations: 3-5 items. Concrete and actionable. Reference specific members, models, or tools. Examples: "Switch bob from claude-opus to claude-sonnet to save ~40% on cost", "Rebalance: dave has 4x the sessions of carol."
28486
+
28487
+ forecast: If daily cost data has 3+ days, extrapolate monthly spend. Otherwise null.
28488
+ </field_guidelines>
28489
+
28490
+ <constraints>
28491
+ - Do NOT wrap the JSON in markdown code fences
28492
+ - Do NOT include any text before or after the JSON
28493
+ - Every claim must cite a number from the provided data
28494
+ - Keep each string field under 500 characters
28495
+ - The memberHighlights array must include ALL members present in the data
28496
+ - Be constructive: frame concerns as improvement opportunities
28497
+ </constraints>`;
28498
+ function buildTeamAnalyticsPrompt(traces, from, to) {
28499
+ const sections = [];
28500
+ const memberMap = /* @__PURE__ */ new Map();
28501
+ let totalCost = 0;
28502
+ let totalCommits = 0;
28503
+ let totalPRs = 0;
28504
+ let totalLinesAdded = 0;
28505
+ let totalLinesRemoved = 0;
28506
+ let totalSessions = 0;
28507
+ for (const trace of traces) {
28508
+ const userId = trace.user.id;
28509
+ if (userId === "unknown_user") continue;
28510
+ const existing = memberMap.get(userId) ?? {
28511
+ userId,
28512
+ displayName: trace.user.displayName ?? null,
28513
+ sessionCount: 0,
28514
+ totalCostUsd: 0,
28515
+ commitCount: 0,
28516
+ prCount: 0,
28517
+ linesAdded: 0,
28518
+ linesRemoved: 0,
28519
+ totalInputTokens: 0,
28520
+ totalOutputTokens: 0,
28521
+ models: [],
28522
+ tools: [],
28523
+ repos: [],
28524
+ peakHours: [],
28525
+ avgSessionCostUsd: 0,
28526
+ costPerCommit: 0
28527
+ };
28528
+ existing.sessionCount += 1;
28529
+ existing.totalCostUsd += trace.metrics.totalCostUsd;
28530
+ existing.commitCount += trace.git.commits.length;
28531
+ existing.prCount += trace.git.pullRequests.length;
28532
+ existing.linesAdded += trace.metrics.linesAdded;
28533
+ existing.linesRemoved += trace.metrics.linesRemoved;
28534
+ existing.totalInputTokens += trace.metrics.totalInputTokens;
28535
+ existing.totalOutputTokens += trace.metrics.totalOutputTokens;
28536
+ if (trace.user.displayName !== void 0) {
28537
+ existing.displayName = trace.user.displayName;
28538
+ }
28539
+ for (const model of trace.metrics.modelsUsed) {
28540
+ if (!existing.models.includes(model)) existing.models.push(model);
28541
+ }
28542
+ for (const tool of trace.metrics.toolsUsed) {
28543
+ if (!existing.tools.includes(tool)) existing.tools.push(tool);
28544
+ }
28545
+ const repo = trace.environment.gitRepo;
28546
+ if (repo !== void 0 && repo.length > 0 && !existing.repos.includes(repo)) {
28547
+ existing.repos.push(repo);
28548
+ }
28549
+ const hour = new Date(trace.startedAt).getUTCHours();
28550
+ existing.peakHours.push(hour);
28551
+ totalCost += trace.metrics.totalCostUsd;
28552
+ totalCommits += trace.git.commits.length;
28553
+ totalPRs += trace.git.pullRequests.length;
28554
+ totalLinesAdded += trace.metrics.linesAdded;
28555
+ totalLinesRemoved += trace.metrics.linesRemoved;
28556
+ totalSessions += 1;
28557
+ memberMap.set(userId, existing);
28558
+ }
28559
+ sections.push(`<team_data>`);
28560
+ sections.push(`<period from="${from}" to="${to}" />`);
28561
+ sections.push(`<summary members="${String(memberMap.size)}" sessions="${String(totalSessions)}" total_cost_usd="${totalCost.toFixed(2)}" total_commits="${String(totalCommits)}" total_prs="${String(totalPRs)}" lines_added="${String(totalLinesAdded)}" lines_removed="${String(totalLinesRemoved)}" avg_cost_per_session="${totalSessions > 0 ? (totalCost / totalSessions).toFixed(4) : "0"}" avg_cost_per_commit="${totalCommits > 0 ? (totalCost / totalCommits).toFixed(2) : "N/A"}" />`);
28562
+ const members = [...memberMap.values()].sort((a, b) => b.totalCostUsd - a.totalCostUsd);
28563
+ for (const m of members) {
28564
+ m.avgSessionCostUsd = m.sessionCount > 0 ? m.totalCostUsd / m.sessionCount : 0;
28565
+ m.costPerCommit = m.commitCount > 0 ? m.totalCostUsd / m.commitCount : 0;
28566
+ const hourCounts = new Array(24).fill(0);
28567
+ for (const h of m.peakHours) hourCounts[h] = (hourCounts[h] ?? 0) + 1;
28568
+ const peakHour = hourCounts.indexOf(Math.max(...hourCounts));
28569
+ const lateNight = m.peakHours.filter((h) => h >= 22 || h <= 5).length;
28570
+ sections.push(`<member id="${m.userId}" display_name="${m.displayName ?? "none"}">`);
28571
+ sections.push(` sessions="${String(m.sessionCount)}" cost_usd="${m.totalCostUsd.toFixed(2)}" avg_session_cost="${m.avgSessionCostUsd.toFixed(4)}"`);
28572
+ sections.push(` commits="${String(m.commitCount)}" prs="${String(m.prCount)}" cost_per_commit="${m.commitCount > 0 ? m.costPerCommit.toFixed(2) : "N/A (0 commits)"}"`);
28573
+ sections.push(` lines_added="${String(m.linesAdded)}" lines_removed="${String(m.linesRemoved)}"`);
28574
+ sections.push(` input_tokens="${String(m.totalInputTokens)}" output_tokens="${String(m.totalOutputTokens)}"`);
28575
+ sections.push(` models="${m.models.join(", ")}"`);
28576
+ sections.push(` tools="${m.tools.slice(0, 12).join(", ")}${m.tools.length > 12 ? " +" + String(m.tools.length - 12) + " more" : ""}"`);
28577
+ sections.push(` repos="${m.repos.join(", ")}"`);
28578
+ sections.push(` peak_hour_utc="${String(peakHour)}" late_night_sessions="${String(lateNight)}"`);
28579
+ sections.push(`</member>`);
28580
+ }
28581
+ const dailyCost = /* @__PURE__ */ new Map();
28582
+ for (const trace of traces) {
28583
+ if (trace.user.id === "unknown_user") continue;
28584
+ const date = trace.startedAt.slice(0, 10);
28585
+ dailyCost.set(date, (dailyCost.get(date) ?? 0) + trace.metrics.totalCostUsd);
28586
+ }
28587
+ const sortedDays = [...dailyCost.entries()].sort(([a], [b]) => a.localeCompare(b));
28588
+ if (sortedDays.length > 0) {
28589
+ sections.push(`<daily_cost_trend>`);
28590
+ for (const [date, cost] of sortedDays.slice(-14)) {
28591
+ sections.push(` <day date="${date}" cost_usd="${cost.toFixed(2)}" />`);
28592
+ }
28593
+ sections.push(`</daily_cost_trend>`);
28594
+ }
28595
+ sections.push(`</team_data>`);
28596
+ sections.push(``);
28597
+ sections.push(`Analyze this team's AI coding agent usage. Respond with only the JSON object, no other text.`);
28598
+ return sections.join("\n");
28599
+ }
28600
+ function parseTeamInsightJson(raw) {
28601
+ let jsonStr = raw.trim();
28602
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
28603
+ if (fenceMatch !== null && fenceMatch[1] !== void 0) {
28604
+ jsonStr = fenceMatch[1].trim();
28605
+ }
28606
+ if (!jsonStr.startsWith("{")) {
28607
+ const braceStart = jsonStr.indexOf("{");
28608
+ const braceEnd = jsonStr.lastIndexOf("}");
28609
+ if (braceStart >= 0 && braceEnd > braceStart) {
28610
+ jsonStr = jsonStr.slice(braceStart, braceEnd + 1);
28611
+ }
28612
+ }
28613
+ try {
28614
+ const parsed = JSON.parse(jsonStr);
28615
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
28616
+ const obj = parsed;
28617
+ const executiveSummary = typeof obj["executiveSummary"] === "string" ? obj["executiveSummary"] : "";
28618
+ if (executiveSummary.length === 0) return void 0;
28619
+ const costAnalysis = typeof obj["costAnalysis"] === "string" ? obj["costAnalysis"] : "";
28620
+ const productivityAnalysis = typeof obj["productivityAnalysis"] === "string" ? obj["productivityAnalysis"] : "";
28621
+ const rawMembers = Array.isArray(obj["memberHighlights"]) ? obj["memberHighlights"] : [];
28622
+ const memberHighlights = rawMembers.filter((m) => typeof m === "object" && m !== null).map((m) => ({
28623
+ userId: typeof m["userId"] === "string" ? m["userId"] : "",
28624
+ displayName: typeof m["displayName"] === "string" ? m["displayName"] : null,
28625
+ strength: typeof m["strength"] === "string" ? m["strength"] : "",
28626
+ concern: typeof m["concern"] === "string" && m["concern"].length > 0 ? m["concern"] : null,
28627
+ recommendation: typeof m["recommendation"] === "string" ? m["recommendation"] : ""
28628
+ })).filter((m) => m.userId.length > 0);
28629
+ const risks = Array.isArray(obj["risks"]) ? obj["risks"].filter((r) => typeof r === "string") : [];
28630
+ const recommendations = Array.isArray(obj["recommendations"]) ? obj["recommendations"].filter((r) => typeof r === "string") : [];
28631
+ const forecast = typeof obj["forecast"] === "string" && obj["forecast"].length > 0 ? obj["forecast"] : null;
28632
+ return {
28633
+ executiveSummary,
28634
+ costAnalysis,
28635
+ productivityAnalysis,
28636
+ memberHighlights,
28637
+ risks,
28638
+ recommendations,
28639
+ forecast
28640
+ };
28641
+ } catch {
28642
+ return void 0;
28643
+ }
28644
+ }
28645
+ async function generateTeamInsight(traces, from, to, provider, context) {
28646
+ const userPrompt = buildTeamAnalyticsPrompt(traces, from, to);
28647
+ let systemPrompt = TEAM_SYSTEM_PROMPT;
28648
+ if (context !== void 0) {
28649
+ if (context.companyContext.length > 0) {
28650
+ systemPrompt += `
28651
+
28652
+ <company_context>
28653
+ ${context.companyContext}
28654
+ </company_context>`;
28655
+ }
28656
+ if (context.analysisGuidelines.length > 0) {
28657
+ systemPrompt += `
28658
+
28659
+ <manager_guidelines>
28660
+ ${context.analysisGuidelines}
28661
+ </manager_guidelines>`;
28662
+ }
28663
+ }
28664
+ const rawResponse = await provider.complete(systemPrompt, userPrompt, 4096);
28665
+ const parsed = parseTeamInsightJson(rawResponse);
28666
+ if (parsed === void 0) {
28667
+ return {
28668
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
28669
+ provider: provider.provider,
28670
+ model: provider.model,
28671
+ executiveSummary: rawResponse.slice(0, 800) || "Failed to generate structured team insight.",
28672
+ costAnalysis: "",
28673
+ productivityAnalysis: "",
28674
+ memberHighlights: [],
28675
+ risks: [],
28676
+ recommendations: [],
28677
+ forecast: null
28678
+ };
28679
+ }
28680
+ return {
28681
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
28682
+ provider: provider.provider,
28683
+ model: provider.model,
28684
+ ...parsed
28685
+ };
28686
+ }
28687
+
27040
28688
  // packages/api/src/insights-handler.ts
27041
28689
  var insightsCache = /* @__PURE__ */ new Map();
28690
+ var teamInsightCache;
27042
28691
  var VALID_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
27043
28692
  function isValidProvider(value) {
27044
28693
  return typeof value === "string" && VALID_PROVIDERS.includes(value);
@@ -27159,12 +28808,117 @@ async function handlePostSessionInsight(sessionId, dependencies) {
27159
28808
  payload: { status: "ok", insight }
27160
28809
  };
27161
28810
  }
28811
+ async function handlePostTeamInsight(searchParams, dependencies) {
28812
+ const accessor = dependencies.insightsConfigAccessor;
28813
+ if (accessor === void 0) {
28814
+ return {
28815
+ statusCode: 400,
28816
+ payload: { status: "error", message: "insights not configured" }
28817
+ };
28818
+ }
28819
+ const config = accessor.getConfig();
28820
+ if (config === void 0) {
28821
+ return {
28822
+ statusCode: 400,
28823
+ payload: { status: "error", message: "no AI provider configured. open settings to add your API key." }
28824
+ };
28825
+ }
28826
+ const forceRegenerate = searchParams.get("force") === "true";
28827
+ if (!forceRegenerate && teamInsightCache !== void 0) {
28828
+ const age = Date.now() - Date.parse(teamInsightCache.generatedAt);
28829
+ if (age < 5 * 60 * 1e3) {
28830
+ return {
28831
+ statusCode: 200,
28832
+ payload: { status: "ok", insight: teamInsightCache }
28833
+ };
28834
+ }
28835
+ }
28836
+ const fromParam = searchParams.get("from");
28837
+ const toParam = searchParams.get("to");
28838
+ const now = /* @__PURE__ */ new Date();
28839
+ const year = now.getFullYear();
28840
+ const month = String(now.getMonth() + 1).padStart(2, "0");
28841
+ const lastDay = new Date(year, now.getMonth() + 1, 0).getDate();
28842
+ const from = fromParam !== null && fromParam.length === 10 ? fromParam : `${year}-${month}-01`;
28843
+ const to = toParam !== null && toParam.length === 10 ? toParam : `${year}-${month}-${String(lastDay).padStart(2, "0")}`;
28844
+ const filters = { from, to };
28845
+ const traces = dependencies.repository.list(filters);
28846
+ if (traces.length === 0) {
28847
+ return {
28848
+ statusCode: 400,
28849
+ payload: { status: "error", message: "no session data available for the selected period" }
28850
+ };
28851
+ }
28852
+ const provider = createLlmProvider(config);
28853
+ const teamContext = accessor.getTeamInsightsContext();
28854
+ const insight = await generateTeamInsight(traces, from, to, provider, teamContext);
28855
+ teamInsightCache = insight;
28856
+ return {
28857
+ statusCode: 200,
28858
+ payload: { status: "ok", insight }
28859
+ };
28860
+ }
28861
+ function handleGetTeamInsightsContext(dependencies) {
28862
+ const accessor = dependencies.insightsConfigAccessor;
28863
+ if (accessor === void 0) {
28864
+ return {
28865
+ statusCode: 200,
28866
+ payload: { status: "ok", configured: false }
28867
+ };
28868
+ }
28869
+ const context = accessor.getTeamInsightsContext();
28870
+ if (context === void 0) {
28871
+ return {
28872
+ statusCode: 200,
28873
+ payload: { status: "ok", configured: false }
28874
+ };
28875
+ }
28876
+ return {
28877
+ statusCode: 200,
28878
+ payload: { status: "ok", configured: true, context }
28879
+ };
28880
+ }
28881
+ function handlePostTeamInsightsContext(body, dependencies) {
28882
+ const accessor = dependencies.insightsConfigAccessor;
28883
+ if (accessor === void 0) {
28884
+ return {
28885
+ statusCode: 500,
28886
+ payload: { status: "error", message: "insights settings not available" }
28887
+ };
28888
+ }
28889
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
28890
+ return {
28891
+ statusCode: 400,
28892
+ payload: { status: "error", message: "request body must be a JSON object" }
28893
+ };
28894
+ }
28895
+ const record = body;
28896
+ const companyContext = typeof record["companyContext"] === "string" ? record["companyContext"] : "";
28897
+ const analysisGuidelines = typeof record["analysisGuidelines"] === "string" ? record["analysisGuidelines"] : "";
28898
+ if (companyContext.length === 0 && analysisGuidelines.length === 0) {
28899
+ return {
28900
+ statusCode: 400,
28901
+ payload: { status: "error", message: "at least one of companyContext or analysisGuidelines is required" }
28902
+ };
28903
+ }
28904
+ const context = {
28905
+ companyContext,
28906
+ analysisGuidelines,
28907
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
28908
+ };
28909
+ accessor.setTeamInsightsContext(context);
28910
+ return {
28911
+ statusCode: 200,
28912
+ payload: { status: "ok" }
28913
+ };
28914
+ }
27162
28915
 
27163
28916
  // packages/api/src/mapper.ts
27164
28917
  function toSessionSummary(trace) {
27165
28918
  return {
27166
28919
  sessionId: trace.sessionId,
27167
28920
  userId: trace.user.id,
28921
+ userDisplayName: trace.user.displayName ?? null,
27168
28922
  gitRepo: trace.environment.gitRepo ?? null,
27169
28923
  gitBranch: trace.environment.gitBranch ?? null,
27170
28924
  startedAt: trace.startedAt,
@@ -27178,6 +28932,353 @@ function toSessionSummary(trace) {
27178
28932
  };
27179
28933
  }
27180
28934
 
28935
+ // packages/api/src/team-handler.ts
28936
+ function currentYearMonth() {
28937
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
28938
+ }
28939
+ function defaultDateRange() {
28940
+ const now = /* @__PURE__ */ new Date();
28941
+ const year = now.getFullYear();
28942
+ const month = String(now.getMonth() + 1).padStart(2, "0");
28943
+ const from = `${year}-${month}-01`;
28944
+ const lastDay = new Date(year, now.getMonth() + 1, 0).getDate();
28945
+ const to = `${year}-${month}-${String(lastDay).padStart(2, "0")}`;
28946
+ return { from, to };
28947
+ }
28948
+ function parseDateRange(searchParams) {
28949
+ const fromParam = searchParams.get("from");
28950
+ const toParam = searchParams.get("to");
28951
+ const defaults = defaultDateRange();
28952
+ return {
28953
+ from: fromParam !== null && fromParam.length === 10 ? fromParam : defaults.from,
28954
+ to: toParam !== null && toParam.length === 10 ? toParam : defaults.to
28955
+ };
28956
+ }
28957
+ function toMetricDate(startedAt) {
28958
+ const parsed = Date.parse(startedAt);
28959
+ if (Number.isNaN(parsed)) {
28960
+ return startedAt.slice(0, 10);
28961
+ }
28962
+ return new Date(parsed).toISOString().slice(0, 10);
28963
+ }
28964
+ function handleTeamOverview(searchParams, dependencies) {
28965
+ const { from, to } = parseDateRange(searchParams);
28966
+ const filters = { from, to };
28967
+ const traces = dependencies.repository.list(filters);
28968
+ let totalCostUsd = 0;
28969
+ let totalCommits = 0;
28970
+ let totalPullRequests = 0;
28971
+ let totalLinesAdded = 0;
28972
+ let totalLinesRemoved = 0;
28973
+ const memberIds = /* @__PURE__ */ new Set();
28974
+ for (const trace of traces) {
28975
+ totalCostUsd += trace.metrics.totalCostUsd;
28976
+ totalCommits += trace.git.commits.length;
28977
+ totalPullRequests += trace.git.pullRequests.length;
28978
+ totalLinesAdded += trace.metrics.linesAdded;
28979
+ totalLinesRemoved += trace.metrics.linesRemoved;
28980
+ if (trace.user.id !== "unknown_user") {
28981
+ memberIds.add(trace.user.id);
28982
+ }
28983
+ }
28984
+ const payload = {
28985
+ status: "ok",
28986
+ period: { from, to },
28987
+ totalCostUsd: Number(totalCostUsd.toFixed(6)),
28988
+ totalSessions: traces.length,
28989
+ totalCommits,
28990
+ totalPullRequests,
28991
+ totalLinesAdded,
28992
+ totalLinesRemoved,
28993
+ memberCount: memberIds.size,
28994
+ costPerCommit: totalCommits > 0 ? Number((totalCostUsd / totalCommits).toFixed(2)) : 0,
28995
+ costPerPullRequest: totalPullRequests > 0 ? Number((totalCostUsd / totalPullRequests).toFixed(2)) : 0
28996
+ };
28997
+ return { statusCode: 200, payload };
28998
+ }
28999
+ function handleTeamMembers(searchParams, dependencies) {
29000
+ const { from, to } = parseDateRange(searchParams);
29001
+ const filters = { from, to };
29002
+ const traces = dependencies.repository.list(filters);
29003
+ const memberMap = /* @__PURE__ */ new Map();
29004
+ for (const trace of traces) {
29005
+ const userId = trace.user.id;
29006
+ if (userId === "unknown_user") continue;
29007
+ const existing = memberMap.get(userId) ?? {
29008
+ displayName: trace.user.displayName ?? null,
29009
+ sessionCount: 0,
29010
+ totalCostUsd: 0,
29011
+ commitCount: 0,
29012
+ prCount: 0,
29013
+ linesAdded: 0,
29014
+ linesRemoved: 0,
29015
+ lastActiveAt: trace.startedAt
29016
+ };
29017
+ existing.sessionCount += 1;
29018
+ existing.totalCostUsd += trace.metrics.totalCostUsd;
29019
+ existing.commitCount += trace.git.commits.length;
29020
+ existing.prCount += trace.git.pullRequests.length;
29021
+ existing.linesAdded += trace.metrics.linesAdded;
29022
+ existing.linesRemoved += trace.metrics.linesRemoved;
29023
+ if (trace.user.displayName !== void 0) {
29024
+ existing.displayName = trace.user.displayName;
29025
+ }
29026
+ const traceEnd = trace.endedAt ?? trace.startedAt;
29027
+ if (traceEnd > existing.lastActiveAt) {
29028
+ existing.lastActiveAt = traceEnd;
29029
+ }
29030
+ memberMap.set(userId, existing);
29031
+ }
29032
+ const members = [...memberMap.entries()].map(([userId, data]) => ({
29033
+ userId,
29034
+ displayName: data.displayName,
29035
+ sessionCount: data.sessionCount,
29036
+ totalCostUsd: Number(data.totalCostUsd.toFixed(6)),
29037
+ commitCount: data.commitCount,
29038
+ prCount: data.prCount,
29039
+ linesAdded: data.linesAdded,
29040
+ linesRemoved: data.linesRemoved,
29041
+ costPerCommit: data.commitCount > 0 ? Number((data.totalCostUsd / data.commitCount).toFixed(2)) : 0,
29042
+ lastActiveAt: data.lastActiveAt
29043
+ })).sort((a, b) => b.totalCostUsd - a.totalCostUsd);
29044
+ const payload = { status: "ok", members };
29045
+ return { statusCode: 200, payload };
29046
+ }
29047
+ function handleTeamCostDaily(searchParams, dependencies) {
29048
+ const { from, to } = parseDateRange(searchParams);
29049
+ const filters = { from, to };
29050
+ const traces = dependencies.repository.list(filters);
29051
+ const byDate = /* @__PURE__ */ new Map();
29052
+ for (const trace of traces) {
29053
+ const date = toMetricDate(trace.startedAt);
29054
+ const entry = byDate.get(date) ?? {
29055
+ totalCostUsd: 0,
29056
+ sessionCount: 0,
29057
+ byMember: /* @__PURE__ */ new Map()
29058
+ };
29059
+ entry.totalCostUsd += trace.metrics.totalCostUsd;
29060
+ entry.sessionCount += 1;
29061
+ const userId = trace.user.id;
29062
+ const memberEntry = entry.byMember.get(userId) ?? { totalCostUsd: 0, sessionCount: 0 };
29063
+ memberEntry.totalCostUsd += trace.metrics.totalCostUsd;
29064
+ memberEntry.sessionCount += 1;
29065
+ entry.byMember.set(userId, memberEntry);
29066
+ byDate.set(date, entry);
29067
+ }
29068
+ const points = [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, entry]) => {
29069
+ const byMember = [...entry.byMember.entries()].map(
29070
+ ([userId, m]) => ({
29071
+ userId,
29072
+ totalCostUsd: Number(m.totalCostUsd.toFixed(6)),
29073
+ sessionCount: m.sessionCount
29074
+ })
29075
+ );
29076
+ return {
29077
+ date,
29078
+ totalCostUsd: Number(entry.totalCostUsd.toFixed(6)),
29079
+ sessionCount: entry.sessionCount,
29080
+ byMember
29081
+ };
29082
+ });
29083
+ const payload = { status: "ok", points };
29084
+ return { statusCode: 200, payload };
29085
+ }
29086
+ function handleGetTeamBudget(dependencies) {
29087
+ const budget = dependencies.teamBudgetStore?.getTeamBudget() ?? null;
29088
+ const yearMonth = currentYearMonth();
29089
+ const currentMonthSpend = dependencies.teamBudgetStore?.getMonthSpend(yearMonth) ?? 0;
29090
+ const percentUsed = budget !== null && budget.monthlyLimitUsd > 0 ? Number((currentMonthSpend / budget.monthlyLimitUsd * 100).toFixed(1)) : 0;
29091
+ const payload = {
29092
+ status: "ok",
29093
+ budget,
29094
+ currentMonthSpend: Number(currentMonthSpend.toFixed(6)),
29095
+ percentUsed
29096
+ };
29097
+ return { statusCode: 200, payload };
29098
+ }
29099
+ function handlePostTeamBudget(body, dependencies) {
29100
+ if (dependencies.teamBudgetStore === void 0) {
29101
+ return {
29102
+ statusCode: 501,
29103
+ payload: { status: "error", message: "budget storage not available" }
29104
+ };
29105
+ }
29106
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
29107
+ return {
29108
+ statusCode: 400,
29109
+ payload: { status: "error", message: "invalid request body" }
29110
+ };
29111
+ }
29112
+ const record = body;
29113
+ const monthlyLimitUsd = record["monthlyLimitUsd"];
29114
+ if (typeof monthlyLimitUsd !== "number" || !Number.isFinite(monthlyLimitUsd) || monthlyLimitUsd < 0) {
29115
+ return {
29116
+ statusCode: 400,
29117
+ payload: { status: "error", message: "monthlyLimitUsd must be a non-negative number" }
29118
+ };
29119
+ }
29120
+ const alertThresholdPercent = typeof record["alertThresholdPercent"] === "number" && Number.isFinite(record["alertThresholdPercent"]) && record["alertThresholdPercent"] >= 0 ? record["alertThresholdPercent"] : 80;
29121
+ dependencies.teamBudgetStore.upsertTeamBudget(monthlyLimitUsd, alertThresholdPercent);
29122
+ const payload = {
29123
+ status: "ok",
29124
+ budget: { monthlyLimitUsd, alertThresholdPercent }
29125
+ };
29126
+ return { statusCode: 200, payload };
29127
+ }
29128
+ function handleTeamAnalytics(searchParams, dependencies) {
29129
+ const { from, to } = parseDateRange(searchParams);
29130
+ const filters = { from, to };
29131
+ const traces = dependencies.repository.list(filters);
29132
+ const memberMap = /* @__PURE__ */ new Map();
29133
+ const teamModels = /* @__PURE__ */ new Map();
29134
+ const teamTools = /* @__PURE__ */ new Map();
29135
+ const heatmap = /* @__PURE__ */ new Map();
29136
+ const dailyCost = /* @__PURE__ */ new Map();
29137
+ let totalCost = 0;
29138
+ let totalCommits = 0;
29139
+ let totalTokens = 0;
29140
+ for (const trace of traces) {
29141
+ const userId = trace.user.id;
29142
+ if (userId === "unknown_user") continue;
29143
+ const startDate = new Date(trace.startedAt);
29144
+ const hour = startDate.getUTCHours();
29145
+ const dayOfWeek = startDate.getUTCDay();
29146
+ const dateStr = toMetricDate(trace.startedAt);
29147
+ const member = memberMap.get(userId) ?? {
29148
+ displayName: trace.user.displayName ?? null,
29149
+ totalCostUsd: 0,
29150
+ sessionCount: 0,
29151
+ commitCount: 0,
29152
+ prCount: 0,
29153
+ linesAdded: 0,
29154
+ linesRemoved: 0,
29155
+ totalInputTokens: 0,
29156
+ totalOutputTokens: 0,
29157
+ models: /* @__PURE__ */ new Map(),
29158
+ tools: /* @__PURE__ */ new Map(),
29159
+ repos: /* @__PURE__ */ new Map(),
29160
+ hourlyActivity: new Array(24).fill(0),
29161
+ dailyActivity: new Array(7).fill(0)
29162
+ };
29163
+ member.totalCostUsd += trace.metrics.totalCostUsd;
29164
+ member.sessionCount += 1;
29165
+ member.commitCount += trace.git.commits.length;
29166
+ member.prCount += trace.git.pullRequests.length;
29167
+ member.linesAdded += trace.metrics.linesAdded;
29168
+ member.linesRemoved += trace.metrics.linesRemoved;
29169
+ member.totalInputTokens += trace.metrics.totalInputTokens;
29170
+ member.totalOutputTokens += trace.metrics.totalOutputTokens;
29171
+ member.hourlyActivity[hour] = (member.hourlyActivity[hour] ?? 0) + 1;
29172
+ member.dailyActivity[dayOfWeek] = (member.dailyActivity[dayOfWeek] ?? 0) + 1;
29173
+ if (trace.user.displayName !== void 0) {
29174
+ member.displayName = trace.user.displayName;
29175
+ }
29176
+ const costPerModel = trace.metrics.modelsUsed.length > 0 ? trace.metrics.totalCostUsd / trace.metrics.modelsUsed.length : 0;
29177
+ for (const model of trace.metrics.modelsUsed) {
29178
+ const existing = member.models.get(model) ?? { count: 0, costUsd: 0 };
29179
+ existing.count += 1;
29180
+ existing.costUsd += costPerModel;
29181
+ member.models.set(model, existing);
29182
+ const teamEntry = teamModels.get(model) ?? { count: 0, costUsd: 0 };
29183
+ teamEntry.count += 1;
29184
+ teamEntry.costUsd += costPerModel;
29185
+ teamModels.set(model, teamEntry);
29186
+ }
29187
+ for (const tool of trace.metrics.toolsUsed) {
29188
+ member.tools.set(tool, (member.tools.get(tool) ?? 0) + 1);
29189
+ teamTools.set(tool, (teamTools.get(tool) ?? 0) + 1);
29190
+ }
29191
+ for (const event of trace.timeline) {
29192
+ if (event.type === "tool_call" || event.type === "tool_result") {
29193
+ const details = event.details;
29194
+ const toolName = details !== void 0 ? details["toolName"] : void 0;
29195
+ if (toolName !== void 0 && toolName.length > 0) {
29196
+ member.tools.set(toolName, (member.tools.get(toolName) ?? 0) + 1);
29197
+ teamTools.set(toolName, (teamTools.get(toolName) ?? 0) + 1);
29198
+ }
29199
+ }
29200
+ }
29201
+ const repo = trace.environment.gitRepo;
29202
+ if (repo !== void 0 && repo.length > 0) {
29203
+ const repoEntry = member.repos.get(repo) ?? { sessions: 0, commits: 0 };
29204
+ repoEntry.sessions += 1;
29205
+ repoEntry.commits += trace.git.commits.length;
29206
+ member.repos.set(repo, repoEntry);
29207
+ }
29208
+ memberMap.set(userId, member);
29209
+ const heatKey = `${String(hour)}-${String(dayOfWeek)}`;
29210
+ heatmap.set(heatKey, (heatmap.get(heatKey) ?? 0) + 1);
29211
+ const dayEntry = dailyCost.get(dateStr) ?? { costUsd: 0, sessions: 0 };
29212
+ dayEntry.costUsd += trace.metrics.totalCostUsd;
29213
+ dayEntry.sessions += 1;
29214
+ dailyCost.set(dateStr, dayEntry);
29215
+ totalCost += trace.metrics.totalCostUsd;
29216
+ totalCommits += trace.git.commits.length;
29217
+ totalTokens += trace.metrics.totalInputTokens + trace.metrics.totalOutputTokens;
29218
+ }
29219
+ const memberAnalytics = [...memberMap.entries()].map(([userId, m]) => {
29220
+ const models = [...m.models.entries()].map(([model, data]) => ({ model, sessionCount: data.count, totalCostUsd: Number(data.costUsd.toFixed(6)) })).sort((a, b) => b.totalCostUsd - a.totalCostUsd);
29221
+ const tools = [...m.tools.entries()].map(([tool, callCount]) => ({ tool, callCount })).sort((a, b) => b.callCount - a.callCount);
29222
+ const repos = [...m.repos.entries()].map(([repo, data]) => ({ repo, sessionCount: data.sessions, commitCount: data.commits })).sort((a, b) => b.sessionCount - a.sessionCount);
29223
+ return {
29224
+ userId,
29225
+ displayName: m.displayName,
29226
+ totalCostUsd: Number(m.totalCostUsd.toFixed(6)),
29227
+ sessionCount: m.sessionCount,
29228
+ commitCount: m.commitCount,
29229
+ prCount: m.prCount,
29230
+ linesAdded: m.linesAdded,
29231
+ linesRemoved: m.linesRemoved,
29232
+ avgSessionCostUsd: m.sessionCount > 0 ? Number((m.totalCostUsd / m.sessionCount).toFixed(4)) : 0,
29233
+ costPerCommit: m.commitCount > 0 ? Number((m.totalCostUsd / m.commitCount).toFixed(2)) : 0,
29234
+ totalInputTokens: m.totalInputTokens,
29235
+ totalOutputTokens: m.totalOutputTokens,
29236
+ models,
29237
+ tools,
29238
+ repos,
29239
+ hourlyActivity: m.hourlyActivity,
29240
+ dailyActivity: m.dailyActivity
29241
+ };
29242
+ }).sort((a, b) => b.totalCostUsd - a.totalCostUsd);
29243
+ const topModels = [...teamModels.entries()].map(([model, data]) => ({ model, sessionCount: data.count, totalCostUsd: Number(data.costUsd.toFixed(6)) })).sort((a, b) => b.totalCostUsd - a.totalCostUsd).slice(0, 10);
29244
+ const topTools = [...teamTools.entries()].map(([tool, callCount]) => ({ tool, callCount })).sort((a, b) => b.callCount - a.callCount).slice(0, 15);
29245
+ const hourlyHeatmap = [];
29246
+ for (let day = 0; day < 7; day++) {
29247
+ for (let hour = 0; hour < 24; hour++) {
29248
+ const count = heatmap.get(`${String(hour)}-${String(day)}`) ?? 0;
29249
+ hourlyHeatmap.push({ hour, day, count });
29250
+ }
29251
+ }
29252
+ let cumulative = 0;
29253
+ const costTrend = [...dailyCost.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, data]) => {
29254
+ cumulative += data.costUsd;
29255
+ return {
29256
+ date,
29257
+ costUsd: Number(data.costUsd.toFixed(6)),
29258
+ sessionCount: data.sessions,
29259
+ cumulativeCostUsd: Number(cumulative.toFixed(6))
29260
+ };
29261
+ });
29262
+ const totalSessions = traces.filter((t) => t.user.id !== "unknown_user").length;
29263
+ const commitsPerDollar = totalCost > 0 ? totalCommits / totalCost : 0;
29264
+ const costEfficiencyScore = Math.min(100, Math.round(commitsPerDollar * 20));
29265
+ const payload = {
29266
+ status: "ok",
29267
+ period: { from, to },
29268
+ memberAnalytics,
29269
+ topModels,
29270
+ topTools,
29271
+ hourlyHeatmap,
29272
+ costTrend,
29273
+ avgCostPerSession: totalSessions > 0 ? Number((totalCost / totalSessions).toFixed(4)) : 0,
29274
+ avgCommitsPerSession: totalSessions > 0 ? Number((totalCommits / totalSessions).toFixed(2)) : 0,
29275
+ avgCostPerCommit: totalCommits > 0 ? Number((totalCost / totalCommits).toFixed(2)) : 0,
29276
+ totalTokensUsed: totalTokens,
29277
+ costEfficiencyScore
29278
+ };
29279
+ return { statusCode: 200, payload };
29280
+ }
29281
+
27181
29282
  // packages/api/src/handler.ts
27182
29283
  function buildError(message) {
27183
29284
  return {
@@ -27195,15 +29296,19 @@ function buildHealth(startedAtMs) {
27195
29296
  function parseFilters(searchParams) {
27196
29297
  const userId = searchParams.get("userId");
27197
29298
  const repo = searchParams.get("repo");
29299
+ const from = searchParams.get("from");
29300
+ const to = searchParams.get("to");
27198
29301
  return {
27199
29302
  ...userId !== null ? { userId } : {},
27200
- ...repo !== null ? { repo } : {}
29303
+ ...repo !== null ? { repo } : {},
29304
+ ...from !== null && from.length === 10 ? { from } : {},
29305
+ ...to !== null && to.length === 10 ? { to } : {}
27201
29306
  };
27202
29307
  }
27203
29308
  function parseSessionPath(pathname) {
27204
29309
  return pathname.split("/").filter((segment) => segment.length > 0);
27205
29310
  }
27206
- function toMetricDate(startedAt) {
29311
+ function toMetricDate2(startedAt) {
27207
29312
  const parsed = Date.parse(startedAt);
27208
29313
  if (Number.isNaN(parsed)) {
27209
29314
  return startedAt.slice(0, 10);
@@ -27214,7 +29319,7 @@ function buildDailyCostResponseFromTraces(dependencies, filters) {
27214
29319
  const traces = dependencies.repository.list(filters);
27215
29320
  const byDate = /* @__PURE__ */ new Map();
27216
29321
  traces.forEach((trace) => {
27217
- const date = toMetricDate(trace.startedAt);
29322
+ const date = toMetricDate2(trace.startedAt);
27218
29323
  const current = byDate.get(date) ?? {
27219
29324
  totalCostUsd: 0,
27220
29325
  sessionCount: 0,
@@ -27278,6 +29383,33 @@ async function handleApiRequest(request, dependencies) {
27278
29383
  payload: await buildDailyCostResponse(dependencies, filters)
27279
29384
  };
27280
29385
  }
29386
+ if (request.method === "GET" && pathname === "/v1/team/overview") {
29387
+ return handleTeamOverview(parsedUrl.searchParams, dependencies);
29388
+ }
29389
+ if (request.method === "GET" && pathname === "/v1/team/members") {
29390
+ return handleTeamMembers(parsedUrl.searchParams, dependencies);
29391
+ }
29392
+ if (request.method === "GET" && pathname === "/v1/team/cost/daily") {
29393
+ return handleTeamCostDaily(parsedUrl.searchParams, dependencies);
29394
+ }
29395
+ if (request.method === "GET" && pathname === "/v1/team/analytics") {
29396
+ return handleTeamAnalytics(parsedUrl.searchParams, dependencies);
29397
+ }
29398
+ if (request.method === "GET" && pathname === "/v1/team/budget") {
29399
+ return handleGetTeamBudget(dependencies);
29400
+ }
29401
+ if (request.method === "POST" && pathname === "/v1/team/budget") {
29402
+ return handlePostTeamBudget(request.body, dependencies);
29403
+ }
29404
+ if (request.method === "GET" && pathname === "/v1/team/insights/context") {
29405
+ return handleGetTeamInsightsContext(dependencies);
29406
+ }
29407
+ if (request.method === "POST" && pathname === "/v1/team/insights/context") {
29408
+ return handlePostTeamInsightsContext(request.body, dependencies);
29409
+ }
29410
+ if (request.method === "POST" && pathname === "/v1/team/insights/generate") {
29411
+ return handlePostTeamInsight(parsedUrl.searchParams, dependencies);
29412
+ }
27281
29413
  if (request.method === "GET" && pathname === "/v1/settings/insights") {
27282
29414
  return handleGetInsightsSettings(dependencies);
27283
29415
  }
@@ -27340,6 +29472,16 @@ function normalizeMethod(method) {
27340
29472
  }
27341
29473
  return void 0;
27342
29474
  }
29475
+ function checkApiAuth(authorizationHeader) {
29476
+ const teamAuthToken = process.env["TEAM_AUTH_TOKEN"];
29477
+ if (teamAuthToken === void 0 || teamAuthToken.length === 0) {
29478
+ return true;
29479
+ }
29480
+ if (authorizationHeader === void 0 || authorizationHeader.length === 0) {
29481
+ return false;
29482
+ }
29483
+ return authorizationHeader === `Bearer ${teamAuthToken}`;
29484
+ }
27343
29485
  function sendJson2(res, statusCode, payload) {
27344
29486
  const body = JSON.stringify(payload);
27345
29487
  res.statusCode = statusCode;
@@ -27433,6 +29575,18 @@ function createApiHttpHandler(dependencies) {
27433
29575
  return (req, res) => {
27434
29576
  const method = req.method ?? "GET";
27435
29577
  const url = req.url ?? "/";
29578
+ const authHeader = typeof req.headers["authorization"] === "string" ? req.headers["authorization"] : void 0;
29579
+ if (method === "GET" && parsePathname2(url) === "/v1/auth/check") {
29580
+ const teamAuthToken = process.env["TEAM_AUTH_TOKEN"];
29581
+ const authRequired = teamAuthToken !== void 0 && teamAuthToken.length > 0;
29582
+ const authValid = !authRequired || checkApiAuth(authHeader);
29583
+ sendJson2(res, 200, { status: "ok", authRequired, authValid });
29584
+ return;
29585
+ }
29586
+ if (!checkApiAuth(authHeader)) {
29587
+ sendJson2(res, 401, { status: "error", message: "authorization required" });
29588
+ return;
29589
+ }
27436
29590
  if (isSseSessionsRoute(method, url)) {
27437
29591
  startSessionsSseStream(req, res, dependencies);
27438
29592
  return;
@@ -27481,6 +29635,18 @@ var InMemorySessionRepository = class {
27481
29635
  if (filters.repo !== void 0 && trace.environment.gitRepo !== filters.repo) {
27482
29636
  return false;
27483
29637
  }
29638
+ if (filters.from !== void 0) {
29639
+ const traceDate = trace.startedAt.slice(0, 10);
29640
+ if (traceDate < filters.from) {
29641
+ return false;
29642
+ }
29643
+ }
29644
+ if (filters.to !== void 0) {
29645
+ const traceDate = trace.startedAt.slice(0, 10);
29646
+ if (traceDate > filters.to) {
29647
+ return false;
29648
+ }
29649
+ }
27484
29650
  return true;
27485
29651
  });
27486
29652
  }
@@ -27734,6 +29900,26 @@ function parsePathname3(url) {
27734
29900
  return url;
27735
29901
  }
27736
29902
  }
29903
+ function checkTeamAuth(authorizationHeader) {
29904
+ const teamAuthToken = process.env["TEAM_AUTH_TOKEN"];
29905
+ if (teamAuthToken === void 0 || teamAuthToken.length === 0) {
29906
+ return void 0;
29907
+ }
29908
+ if (authorizationHeader === void 0 || authorizationHeader.length === 0) {
29909
+ return {
29910
+ statusCode: 401,
29911
+ payload: { status: "error", message: "authorization required" }
29912
+ };
29913
+ }
29914
+ const expected = `Bearer ${teamAuthToken}`;
29915
+ if (authorizationHeader !== expected) {
29916
+ return {
29917
+ statusCode: 403,
29918
+ payload: { status: "error", message: "invalid authorization token" }
29919
+ };
29920
+ }
29921
+ return void 0;
29922
+ }
27737
29923
  function handleCollectorRawHttpRequest(request, dependencies) {
27738
29924
  const method = normalizeMethod2(request.method);
27739
29925
  if (method === void 0) {
@@ -27745,6 +29931,12 @@ function handleCollectorRawHttpRequest(request, dependencies) {
27745
29931
  }
27746
29932
  };
27747
29933
  }
29934
+ if (method === "POST") {
29935
+ const authError = checkTeamAuth(request.authorizationHeader);
29936
+ if (authError !== void 0) {
29937
+ return authError;
29938
+ }
29939
+ }
27748
29940
  const pathname = parsePathname3(request.url);
27749
29941
  if (method === "POST" && pathname === "/v1/hooks") {
27750
29942
  const rawBody = request.rawBody ?? "";
@@ -27805,11 +29997,13 @@ function createCollectorHttpHandler(dependencies) {
27805
29997
  const method = req.method ?? "GET";
27806
29998
  const url = req.url ?? "/";
27807
29999
  const rawBody = method === "POST" ? await readRequestBody2(req) : void 0;
30000
+ const authorizationHeader = typeof req.headers["authorization"] === "string" ? req.headers["authorization"] : void 0;
27808
30001
  const response = handleCollectorRawHttpRequest(
27809
30002
  {
27810
30003
  method,
27811
30004
  url,
27812
- ...rawBody !== void 0 ? { rawBody } : {}
30005
+ ...rawBody !== void 0 ? { rawBody } : {},
30006
+ ...authorizationHeader !== void 0 ? { authorizationHeader } : {}
27813
30007
  },
27814
30008
  dependencies
27815
30009
  );
@@ -29298,12 +31492,16 @@ function toBaseTrace(envelope) {
29298
31492
  const gitRepo = readString4(payload, ["git_repo", "gitRepo"]);
29299
31493
  const gitBranch = readString4(payload, ["git_branch", "gitBranch"]);
29300
31494
  const projectPath = readString4(payload, ["project_path", "projectPath"]);
29301
- const userId = readString4(payload, ["user_id", "userId"]) ?? "unknown_user";
31495
+ const userEmail = readString4(payload, ["user_email", "userEmail"]);
31496
+ const userName = readString4(payload, ["user_name", "userName"]);
31497
+ const userId = userEmail ?? readString4(payload, ["user_id", "userId"]) ?? "unknown_user";
29302
31498
  return {
29303
31499
  sessionId: envelope.sessionId,
29304
31500
  agentType: "claude_code",
29305
31501
  user: {
29306
- id: userId
31502
+ id: userId,
31503
+ ...userEmail !== void 0 ? { email: userEmail } : {},
31504
+ ...userName !== void 0 ? { displayName: userName } : {}
29307
31505
  },
29308
31506
  environment: {
29309
31507
  ...projectPath !== void 0 ? { projectPath } : {},
@@ -29360,6 +31558,13 @@ function toUpdatedTrace(existing, envelope) {
29360
31558
  const mergedTimeline = [...existing.timeline, timelineEvent];
29361
31559
  const endedAt = shouldMarkEnded(envelope.eventType) ? envelope.eventTimestamp : existing.endedAt;
29362
31560
  const latestTime = endedAt ?? envelope.eventTimestamp;
31561
+ const newEmail = readString4(payload, ["user_email", "userEmail"]);
31562
+ const newName = readString4(payload, ["user_name", "userName"]);
31563
+ const updatedUser = existing.user.id === "unknown_user" && newEmail !== void 0 ? { id: newEmail, email: newEmail, ...newName !== void 0 ? { displayName: newName } : {} } : {
31564
+ ...existing.user,
31565
+ ...existing.user.email === void 0 && newEmail !== void 0 ? { email: newEmail } : {},
31566
+ ...existing.user.displayName === void 0 && newName !== void 0 ? { displayName: newName } : {}
31567
+ };
29363
31568
  const cost = computeEventCost(payload) ?? 0;
29364
31569
  const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]) ?? 0;
29365
31570
  const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]) ?? 0;
@@ -29428,6 +31633,7 @@ function toUpdatedTrace(existing, envelope) {
29428
31633
  }
29429
31634
  return {
29430
31635
  ...existing,
31636
+ user: updatedUser,
29431
31637
  ...endedAt !== void 0 ? { endedAt } : {},
29432
31638
  activeDurationMs: updateDurationMs(existing.startedAt, latestTime),
29433
31639
  timeline: mergedTimeline,
@@ -29513,7 +31719,8 @@ function resolveRuntimeOptions(input) {
29513
31719
  startedAtMs: input,
29514
31720
  persistence: new InMemoryRuntimePersistence(),
29515
31721
  dailyCostReader: void 0,
29516
- insightsConfigAccessor: void 0
31722
+ insightsConfigAccessor: void 0,
31723
+ teamBudgetStore: void 0
29517
31724
  };
29518
31725
  }
29519
31726
  const startedAtMs = input?.startedAtMs ?? Date.now();
@@ -29522,7 +31729,8 @@ function resolveRuntimeOptions(input) {
29522
31729
  startedAtMs,
29523
31730
  persistence,
29524
31731
  dailyCostReader: input?.dailyCostReader,
29525
- insightsConfigAccessor: input?.insightsConfigAccessor
31732
+ insightsConfigAccessor: input?.insightsConfigAccessor,
31733
+ teamBudgetStore: input?.teamBudgetStore
29526
31734
  };
29527
31735
  }
29528
31736
  function createInMemoryRuntime(input) {
@@ -29541,7 +31749,8 @@ function createInMemoryRuntime(input) {
29541
31749
  startedAtMs: options.startedAtMs,
29542
31750
  repository: sessionRepository,
29543
31751
  ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
29544
- ...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {}
31752
+ ...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
31753
+ ...options.teamBudgetStore !== void 0 ? { teamBudgetStore: options.teamBudgetStore } : {}
29545
31754
  };
29546
31755
  return {
29547
31756
  sessionRepository,
@@ -29551,6 +31760,7 @@ function createInMemoryRuntime(input) {
29551
31760
  persistence,
29552
31761
  ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
29553
31762
  ...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
31763
+ ...options.teamBudgetStore !== void 0 ? { teamBudgetStore: options.teamBudgetStore } : {},
29554
31764
  handleCollectorRaw: collectorService.handleRaw,
29555
31765
  handleApiRaw: (request) => handleApiRawHttpRequest(request, apiDependencies)
29556
31766
  };
@@ -29572,7 +31782,8 @@ async function startInMemoryRuntimeServers(runtime, options = {}) {
29572
31782
  startedAtMs: runtime.collectorDependencies.startedAtMs,
29573
31783
  repository: runtime.sessionRepository,
29574
31784
  ...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {},
29575
- ...runtime.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: runtime.insightsConfigAccessor } : {}
31785
+ ...runtime.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: runtime.insightsConfigAccessor } : {},
31786
+ ...runtime.teamBudgetStore !== void 0 ? { teamBudgetStore: runtime.teamBudgetStore } : {}
29576
31787
  })
29577
31788
  ) : void 0;
29578
31789
  let otelReceiver;
@@ -29744,6 +31955,7 @@ function hydrateFromSqlite(runtime, sqlite, limit, eventLimit) {
29744
31955
  var VALID_INSIGHTS_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
29745
31956
  function createSqliteInsightsConfigAccessor(sqlite) {
29746
31957
  let cached;
31958
+ let cachedContext;
29747
31959
  const raw = sqlite.getSetting("insights_config");
29748
31960
  if (raw !== void 0) {
29749
31961
  try {
@@ -29761,6 +31973,23 @@ function createSqliteInsightsConfigAccessor(sqlite) {
29761
31973
  } catch {
29762
31974
  }
29763
31975
  }
31976
+ const rawContext = sqlite.getSetting("team_insights_context");
31977
+ if (rawContext !== void 0) {
31978
+ try {
31979
+ const parsed = JSON.parse(rawContext);
31980
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
31981
+ const obj = parsed;
31982
+ if (typeof obj["companyContext"] === "string" || typeof obj["analysisGuidelines"] === "string") {
31983
+ cachedContext = {
31984
+ companyContext: typeof obj["companyContext"] === "string" ? obj["companyContext"] : "",
31985
+ analysisGuidelines: typeof obj["analysisGuidelines"] === "string" ? obj["analysisGuidelines"] : "",
31986
+ updatedAt: typeof obj["updatedAt"] === "string" ? obj["updatedAt"] : (/* @__PURE__ */ new Date()).toISOString()
31987
+ };
31988
+ }
31989
+ }
31990
+ } catch {
31991
+ }
31992
+ }
29764
31993
  return {
29765
31994
  getConfig() {
29766
31995
  return cached;
@@ -29768,6 +31997,26 @@ function createSqliteInsightsConfigAccessor(sqlite) {
29768
31997
  setConfig(config) {
29769
31998
  cached = config;
29770
31999
  sqlite.upsertSetting("insights_config", JSON.stringify(config));
32000
+ },
32001
+ getTeamInsightsContext() {
32002
+ return cachedContext;
32003
+ },
32004
+ setTeamInsightsContext(context) {
32005
+ cachedContext = context;
32006
+ sqlite.upsertSetting("team_insights_context", JSON.stringify(context));
32007
+ }
32008
+ };
32009
+ }
32010
+ function createSqliteTeamBudgetStore(sqlite) {
32011
+ return {
32012
+ getTeamBudget() {
32013
+ return sqlite.getTeamBudget() ?? void 0;
32014
+ },
32015
+ upsertTeamBudget(limitUsd, alertPercent) {
32016
+ sqlite.upsertTeamBudget(limitUsd, alertPercent);
32017
+ },
32018
+ getMonthSpend(yearMonth) {
32019
+ return sqlite.getMonthSpend(yearMonth);
29771
32020
  }
29772
32021
  };
29773
32022
  }
@@ -29776,11 +32025,13 @@ function createSqliteBackedRuntime(options) {
29776
32025
  const persistence = new SqlitePersistence(sqlite);
29777
32026
  const dailyCostReader = new SqliteDailyCostReader(sqlite);
29778
32027
  const insightsConfigAccessor = options.insightsConfigAccessor ?? createSqliteInsightsConfigAccessor(sqlite);
32028
+ const teamBudgetStore = createSqliteTeamBudgetStore(sqlite);
29779
32029
  const runtime = createInMemoryRuntime({
29780
32030
  ...options.startedAtMs !== void 0 ? { startedAtMs: options.startedAtMs } : {},
29781
32031
  persistence,
29782
32032
  dailyCostReader,
29783
- insightsConfigAccessor
32033
+ insightsConfigAccessor,
32034
+ teamBudgetStore
29784
32035
  });
29785
32036
  const hydratedCount = hydrateFromSqlite(runtime, sqlite, options.bootstrapLimit, options.eventLimit);
29786
32037
  const syncIntervalMs = options.syncIntervalMs ?? 5e3;
@@ -29826,6 +32077,9 @@ function parseArgs(argv) {
29826
32077
  let privacyTier;
29827
32078
  let installHooks;
29828
32079
  let forward = false;
32080
+ let teamUrl;
32081
+ let teamPrivacyTier;
32082
+ let teamToken;
29829
32083
  for (let i = 3; i < argv.length; i += 1) {
29830
32084
  const token = argv[i];
29831
32085
  if (token === "--forward") {
@@ -29862,6 +32116,28 @@ function parseArgs(argv) {
29862
32116
  i += 1;
29863
32117
  continue;
29864
32118
  }
32119
+ if (token === "--team-url") {
32120
+ const value = argv[i + 1];
32121
+ if (typeof value === "string" && value.length > 0) {
32122
+ teamUrl = value;
32123
+ }
32124
+ i += 1;
32125
+ continue;
32126
+ }
32127
+ if (token === "--team-privacy-tier") {
32128
+ const value = argv[i + 1];
32129
+ teamPrivacyTier = parsePrivacyTier(value);
32130
+ i += 1;
32131
+ continue;
32132
+ }
32133
+ if (token === "--team-token") {
32134
+ const value = argv[i + 1];
32135
+ if (typeof value === "string" && value.length > 0) {
32136
+ teamToken = value;
32137
+ }
32138
+ i += 1;
32139
+ continue;
32140
+ }
29865
32141
  }
29866
32142
  return {
29867
32143
  command,
@@ -29869,10 +32145,16 @@ function parseArgs(argv) {
29869
32145
  ...collectorUrl !== void 0 ? { collectorUrl } : {},
29870
32146
  ...privacyTier !== void 0 ? { privacyTier } : {},
29871
32147
  ...installHooks !== void 0 ? { installHooks } : {},
29872
- ...forward ? { forward: true } : {}
32148
+ ...forward ? { forward: true } : {},
32149
+ ...teamUrl !== void 0 ? { teamUrl } : {},
32150
+ ...teamPrivacyTier !== void 0 ? { teamPrivacyTier } : {},
32151
+ ...teamToken !== void 0 ? { teamToken } : {}
29873
32152
  };
29874
32153
  }
29875
32154
 
32155
+ // packages/cli/src/init.ts
32156
+ var import_node_child_process = require("node:child_process");
32157
+
29876
32158
  // packages/cli/src/claude-hooks.ts
29877
32159
  var HOOK_EVENTS = [
29878
32160
  "SessionStart",
@@ -29924,12 +32206,22 @@ function parseConfig(raw) {
29924
32206
  if (typeof parsed["updatedAt"] !== "string" || parsed["updatedAt"].length === 0) {
29925
32207
  return void 0;
29926
32208
  }
32209
+ const userEmail = typeof parsed["userEmail"] === "string" && parsed["userEmail"].length > 0 ? parsed["userEmail"] : void 0;
32210
+ const userName = typeof parsed["userName"] === "string" && parsed["userName"].length > 0 ? parsed["userName"] : void 0;
32211
+ const teamCollectorUrl = typeof parsed["teamCollectorUrl"] === "string" && parsed["teamCollectorUrl"].length > 0 ? parsed["teamCollectorUrl"] : void 0;
32212
+ const teamPrivacyTier = ensurePrivacyTier(parsed["teamPrivacyTier"]) ? parsed["teamPrivacyTier"] : void 0;
32213
+ const teamAuthToken = typeof parsed["teamAuthToken"] === "string" && parsed["teamAuthToken"].length > 0 ? parsed["teamAuthToken"] : void 0;
29927
32214
  return {
29928
32215
  version: "1.0",
29929
32216
  collectorUrl: parsed["collectorUrl"],
29930
32217
  privacyTier: parsed["privacyTier"],
29931
32218
  hookCommand: parsed["hookCommand"],
29932
- updatedAt: parsed["updatedAt"]
32219
+ updatedAt: parsed["updatedAt"],
32220
+ ...userEmail !== void 0 ? { userEmail } : {},
32221
+ ...userName !== void 0 ? { userName } : {},
32222
+ ...teamCollectorUrl !== void 0 ? { teamCollectorUrl } : {},
32223
+ ...teamPrivacyTier !== void 0 ? { teamPrivacyTier } : {},
32224
+ ...teamAuthToken !== void 0 ? { teamAuthToken } : {}
29933
32225
  };
29934
32226
  }
29935
32227
  function isRecord2(value) {
@@ -30157,6 +32449,17 @@ function deriveOtelLogsEndpoint(collectorUrl) {
30157
32449
  return "http://127.0.0.1:4717";
30158
32450
  }
30159
32451
  }
32452
+ function readGitConfig(key) {
32453
+ try {
32454
+ const output = (0, import_node_child_process.execFileSync)("git", ["config", "--global", key], {
32455
+ encoding: "utf8",
32456
+ stdio: ["ignore", "pipe", "ignore"]
32457
+ }).trim();
32458
+ return output.length > 0 ? output : void 0;
32459
+ } catch {
32460
+ return void 0;
32461
+ }
32462
+ }
30160
32463
  function buildTelemetryEnv(collectorUrl, privacyTier) {
30161
32464
  const enablePromptAndToolDetail = privacyTier >= 2;
30162
32465
  const otelLogsEndpoint = deriveOtelLogsEndpoint(collectorUrl);
@@ -30173,12 +32476,19 @@ function buildTelemetryEnv(collectorUrl, privacyTier) {
30173
32476
  }
30174
32477
  function runInit(input, store = new FileCliConfigStore()) {
30175
32478
  const timestamp = nowIso(input.nowIso);
32479
+ const userEmail = readGitConfig("user.email");
32480
+ const userName = readGitConfig("user.name");
30176
32481
  const config = {
30177
32482
  version: "1.0",
30178
32483
  collectorUrl: input.collectorUrl ?? "http://127.0.0.1:8317/v1/hooks",
30179
32484
  privacyTier: ensurePrivacyTierOrDefault(input.privacyTier),
30180
32485
  hookCommand: "agent-trace hook-handler --forward",
30181
- updatedAt: timestamp
32486
+ updatedAt: timestamp,
32487
+ ...userEmail !== void 0 ? { userEmail } : {},
32488
+ ...userName !== void 0 ? { userName } : {},
32489
+ ...input.teamUrl !== void 0 ? { teamCollectorUrl: input.teamUrl } : {},
32490
+ ...input.teamPrivacyTier !== void 0 ? { teamPrivacyTier: input.teamPrivacyTier } : {},
32491
+ ...input.teamToken !== void 0 ? { teamAuthToken: input.teamToken } : {}
30182
32492
  };
30183
32493
  const telemetryEnv = buildTelemetryEnv(config.collectorUrl, config.privacyTier);
30184
32494
  const hooks = buildClaudeHookConfig(config.hookCommand, timestamp);
@@ -30228,7 +32538,7 @@ function runStatus(configDir, store = new FileCliConfigStore()) {
30228
32538
 
30229
32539
  // packages/cli/src/hook-handler.ts
30230
32540
  var import_node_crypto4 = __toESM(require("node:crypto"));
30231
- var import_node_child_process = require("node:child_process");
32541
+ var import_node_child_process2 = require("node:child_process");
30232
32542
  var import_node_fs4 = __toESM(require("node:fs"));
30233
32543
  var import_node_path4 = __toESM(require("node:path"));
30234
32544
  function isIsoDate2(value) {
@@ -30661,7 +32971,7 @@ var FileHookSessionBaselineStore = class {
30661
32971
  };
30662
32972
  function runGitCommand(args, repositoryPath) {
30663
32973
  try {
30664
- const output = (0, import_node_child_process.execFileSync)("git", [...args], {
32974
+ const output = (0, import_node_child_process2.execFileSync)("git", [...args], {
30665
32975
  ...repositoryPath !== void 0 ? { cwd: repositoryPath } : {},
30666
32976
  encoding: "utf8",
30667
32977
  stdio: ["ignore", "pipe", "ignore"]
@@ -30889,9 +33199,23 @@ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvi
30889
33199
  }
30890
33200
  const now = input.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
30891
33201
  const privacyTier = getPrivacyTier(store, input.configDir);
33202
+ const config = store.readConfig(input.configDir);
30892
33203
  const baselineStore = new FileHookSessionBaselineStore(store, input.configDir);
30893
33204
  const enrichment = enrichHookPayloadWithGitContext(payload, gitContextProvider, baselineStore, now);
30894
- const envelope = toEnvelope(enrichment.payload, privacyTier, now, {
33205
+ let enrichedPayload = enrichment.payload;
33206
+ if (config?.userEmail !== void 0) {
33207
+ const record = enrichedPayload;
33208
+ if (record["user_email"] === void 0) {
33209
+ enrichedPayload = { ...enrichedPayload, user_email: config.userEmail, user_id: config.userEmail };
33210
+ }
33211
+ }
33212
+ if (config?.userName !== void 0) {
33213
+ const record = enrichedPayload;
33214
+ if (record["user_name"] === void 0) {
33215
+ enrichedPayload = { ...enrichedPayload, user_name: config.userName };
33216
+ }
33217
+ }
33218
+ const envelope = toEnvelope(enrichedPayload, privacyTier, now, {
30895
33219
  ...enrichment.enriched ? { git_enriched: "1" } : {},
30896
33220
  ...enrichment.usedSessionBaselineDelta ? { git_session_delta: "1" } : {}
30897
33221
  });
@@ -30907,14 +33231,64 @@ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvi
30907
33231
  envelope
30908
33232
  };
30909
33233
  }
33234
+ function redactForPrivacy(envelope, targetTier) {
33235
+ if (targetTier >= 3) {
33236
+ return envelope;
33237
+ }
33238
+ const payload = envelope.payload;
33239
+ const redacted = { ...payload };
33240
+ if (targetTier <= 1) {
33241
+ const tier1StripKeys = [
33242
+ "tool_input",
33243
+ "toolInput",
33244
+ "tool_response",
33245
+ "toolResponse",
33246
+ "prompt_text",
33247
+ "promptText",
33248
+ "command",
33249
+ "bash_command",
33250
+ "bashCommand",
33251
+ "stdout",
33252
+ "output",
33253
+ "commit_message",
33254
+ "commitMessage",
33255
+ "last_assistant_message",
33256
+ "lastAssistantMessage",
33257
+ "response_text",
33258
+ "responseText"
33259
+ ];
33260
+ for (const key of tier1StripKeys) {
33261
+ delete redacted[key];
33262
+ }
33263
+ } else if (targetTier === 2) {
33264
+ const tier2StripKeys = [
33265
+ "prompt_text",
33266
+ "promptText",
33267
+ "last_assistant_message",
33268
+ "lastAssistantMessage",
33269
+ "response_text",
33270
+ "responseText"
33271
+ ];
33272
+ for (const key of tier2StripKeys) {
33273
+ delete redacted[key];
33274
+ }
33275
+ }
33276
+ return {
33277
+ ...envelope,
33278
+ privacyTier: targetTier,
33279
+ payload: redacted
33280
+ };
33281
+ }
30910
33282
  var FetchCollectorHttpClient = class {
30911
- async postJson(url, payload) {
33283
+ async postJson(url, payload, extraHeaders) {
30912
33284
  try {
33285
+ const headers = {
33286
+ "Content-Type": "application/json",
33287
+ ...extraHeaders
33288
+ };
30913
33289
  const response = await fetch(url, {
30914
33290
  method: "POST",
30915
- headers: {
30916
- "Content-Type": "application/json"
30917
- },
33291
+ headers,
30918
33292
  body: JSON.stringify(payload)
30919
33293
  });
30920
33294
  const body = await response.text();
@@ -30936,6 +33310,17 @@ var FetchCollectorHttpClient = class {
30936
33310
  function isSuccessStatus(statusCode) {
30937
33311
  return statusCode >= 200 && statusCode < 300;
30938
33312
  }
33313
+ function getTeamConfig(store, configDir) {
33314
+ const config = store.readConfig(configDir);
33315
+ if (config === void 0) {
33316
+ return {};
33317
+ }
33318
+ return {
33319
+ ...config.teamCollectorUrl !== void 0 ? { teamCollectorUrl: config.teamCollectorUrl } : {},
33320
+ ...config.teamPrivacyTier !== void 0 ? { teamPrivacyTier: config.teamPrivacyTier } : {},
33321
+ ...config.teamAuthToken !== void 0 ? { teamAuthToken: config.teamAuthToken } : {}
33322
+ };
33323
+ }
30939
33324
  async function runHookHandlerAndForward(input, client = new FetchCollectorHttpClient(), store = new FileCliConfigStore(), gitContextProvider = new ShellHookGitContextProvider()) {
30940
33325
  const hookResult = runHookHandler(
30941
33326
  {
@@ -30953,7 +33338,36 @@ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpCl
30953
33338
  };
30954
33339
  }
30955
33340
  const collectorUrl = getCollectorUrl(store, input.configDir, input.collectorUrl);
30956
- const postResult = await client.postJson(collectorUrl, hookResult.envelope);
33341
+ const teamConfig = getTeamConfig(store, input.configDir);
33342
+ const localPost = client.postJson(collectorUrl, hookResult.envelope);
33343
+ let teamPostPromise = Promise.resolve(void 0);
33344
+ if (teamConfig.teamCollectorUrl !== void 0 && teamConfig.teamCollectorUrl.length > 0) {
33345
+ const teamTier = teamConfig.teamPrivacyTier ?? 1;
33346
+ const redactedEnvelope = redactForPrivacy(hookResult.envelope, teamTier);
33347
+ const teamHeaders = {};
33348
+ if (teamConfig.teamAuthToken !== void 0 && teamConfig.teamAuthToken.length > 0) {
33349
+ teamHeaders["Authorization"] = `Bearer ${teamConfig.teamAuthToken}`;
33350
+ }
33351
+ teamPostPromise = client.postJson(teamConfig.teamCollectorUrl, redactedEnvelope, teamHeaders).catch((error) => ({
33352
+ ok: false,
33353
+ statusCode: 0,
33354
+ body: "",
33355
+ error: String(error)
33356
+ }));
33357
+ }
33358
+ const [postResult, teamPostResult] = await Promise.all([localPost, teamPostPromise]);
33359
+ let teamForwardError;
33360
+ if (teamPostResult !== void 0) {
33361
+ if (!teamPostResult.ok) {
33362
+ teamForwardError = teamPostResult.error ?? "failed to send to team collector";
33363
+ } else if (!isSuccessStatus(teamPostResult.statusCode)) {
33364
+ teamForwardError = `team collector returned status ${String(teamPostResult.statusCode)}`;
33365
+ }
33366
+ if (teamForwardError !== void 0) {
33367
+ process.stderr.write(`[agent-trace] team forward warning: ${teamForwardError}
33368
+ `);
33369
+ }
33370
+ }
30957
33371
  if (!postResult.ok) {
30958
33372
  return {
30959
33373
  ok: false,
@@ -30977,7 +33391,8 @@ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpCl
30977
33391
  envelope: hookResult.envelope,
30978
33392
  collectorUrl,
30979
33393
  statusCode: postResult.statusCode,
30980
- body: postResult.body
33394
+ body: postResult.body,
33395
+ ...teamForwardError !== void 0 ? { teamForwardError } : {}
30981
33396
  };
30982
33397
  }
30983
33398
 
@@ -31102,13 +33517,17 @@ async function startServer() {
31102
33517
  throw error;
31103
33518
  }
31104
33519
  const apiBaseUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${String(apiPort)}`;
33520
+ const cliConfigStore = new FileCliConfigStore();
33521
+ const cliConfig = cliConfigStore.readConfig();
33522
+ const currentUserEmail = cliConfig?.userEmail;
31105
33523
  let dashboardAddress;
31106
33524
  try {
31107
33525
  const dashboard = await startDashboardServer({
31108
33526
  host,
31109
33527
  port: dashboardPort,
31110
33528
  apiBaseUrl,
31111
- startedAtMs
33529
+ startedAtMs,
33530
+ ...currentUserEmail !== void 0 ? { currentUserEmail } : {}
31112
33531
  });
31113
33532
  dashboardAddress = dashboard.address;
31114
33533
  const originalClose = servers.close;