agent-trace 0.2.12 → 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 +3304 -76
  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>
@@ -24130,6 +24131,41 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24130
24131
  .pr-repo{color:var(--text-muted)}
24131
24132
  .pr-link{color:var(--text-dim);text-decoration:none;font-size:11px}
24132
24133
  .pr-link:hover{color:var(--cyan);text-decoration:underline}
24134
+ .settings-btn{position:absolute;right:16px;top:16px;background:var(--panel-muted);border:1px solid var(--panel-border);border-radius:6px;padding:6px 10px;cursor:pointer;color:var(--text-muted);font-size:12px;font-family:inherit;display:flex;align-items:center;gap:4px;transition:border-color .15s,color .15s}
24135
+ .settings-btn:hover{border-color:var(--green);color:var(--green)}
24136
+ .settings-btn svg{width:14px;height:14px}
24137
+ .modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:1000;justify-content:center;align-items:center}
24138
+ .modal-overlay.open{display:flex}
24139
+ .modal{background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;padding:20px;width:420px;max-width:90vw;position:relative}
24140
+ .modal h3{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--text-primary)}
24141
+ .modal-close{position:absolute;right:12px;top:12px;background:none;border:none;color:var(--text-dim);font-size:18px;cursor:pointer;padding:4px 8px;border-radius:4px}
24142
+ .modal-close:hover{color:var(--text-primary);background:var(--panel-hover)}
24143
+ .modal label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px;margin-top:12px}
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}
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}
24147
+ .modal-actions{margin-top:16px;display:flex;gap:8px;align-items:center}
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}
24149
+ .modal-save:hover{opacity:.9}
24150
+ .modal-save:disabled{opacity:.5;cursor:not-allowed}
24151
+ .modal-status{font-size:11px;color:var(--text-muted);flex:1}
24152
+ .modal-status.error{color:var(--red)}
24153
+ .modal-status.ok{color:var(--green)}
24154
+ .insight-panel{border:1px solid rgba(192,132,252,.2);border-radius:8px;padding:12px;margin-bottom:12px;background:rgba(192,132,252,.04)}
24155
+ .insight-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
24156
+ .insight-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--purple)}
24157
+ .insight-meta{font-size:10px;color:var(--text-dim)}
24158
+ .insight-summary{font-size:12px;color:var(--text-primary);line-height:1.6;margin-bottom:8px}
24159
+ .insight-section{margin-bottom:6px}
24160
+ .insight-section-title{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin-bottom:4px}
24161
+ .insight-item{font-size:12px;color:var(--text-muted);line-height:1.5;padding:2px 0 2px 12px;position:relative}
24162
+ .insight-item::before{content:'>';position:absolute;left:0;color:var(--purple)}
24163
+ .insight-cost{font-size:11px;color:var(--orange);margin-top:4px}
24164
+ .insight-gen-btn{padding:6px 14px;background:var(--purple-dim);color:var(--purple);border:1px solid rgba(192,132,252,.3);border-radius:6px;font-size:12px;cursor:pointer;font-family:inherit;transition:background .15s}
24165
+ .insight-gen-btn:hover{background:rgba(192,132,252,.15)}
24166
+ .insight-gen-btn:disabled{opacity:.5;cursor:not-allowed}
24167
+ .insight-loading{font-size:12px;color:var(--text-muted);padding:8px 0}
24168
+ .insight-error{font-size:12px;color:var(--red);padding:8px 0}
24133
24169
  .pcommits{border:1px solid rgba(250,204,21,.15);border-radius:4px;padding:6px 8px;margin-bottom:8px;background:rgba(250,204,21,.04)}
24134
24170
  .pcommit{display:flex;align-items:center;gap:8px;padding:2px 0;font-size:12px}
24135
24171
  .hljs{background:transparent!important;color:var(--text-primary)}
@@ -24146,17 +24182,138 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24146
24182
  .hljs-property{color:#93c5fd}
24147
24183
  .hljs-addition{color:#4ade80;background:rgba(74,222,128,.08)}
24148
24184
  .hljs-deletion{color:#f87171;background:rgba(248,113,113,.08)}
24149
- @media(max-width:1200px){.mg{grid-template-columns:repeat(2,minmax(0,1fr))}.sg{grid-template-columns:1fr}}
24150
- @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}}
24151
24271
  </style>
24152
24272
  </head>
24153
24273
  <body>
24154
24274
  <main class="shell">
24155
- <section class="hero">
24275
+ <section class="hero" style="position:relative">
24156
24276
  <h1>${title}</h1>
24157
24277
  <p>session observability for coding agents</p>
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>
24158
24284
  <div id="status" class="status-banner">Connecting...</div>
24159
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>
24294
+ <div id="settings-modal" class="modal-overlay" onclick="if(event.target===this)closeSettings()">
24295
+ <div class="modal">
24296
+ <button class="modal-close" onclick="closeSettings()">&times;</button>
24297
+ <h3>AI Insights Settings</h3>
24298
+ <p style="font-size:11px;color:var(--text-muted);margin:0 0 8px">Configure your own API key to generate AI-powered session insights.</p>
24299
+ <label>Provider</label>
24300
+ <select id="cfg-provider">
24301
+ <option value="anthropic">Anthropic</option>
24302
+ <option value="openai">OpenAI</option>
24303
+ <option value="gemini">Gemini</option>
24304
+ <option value="openrouter">OpenRouter</option>
24305
+ </select>
24306
+ <label>API Key</label>
24307
+ <input type="password" id="cfg-apikey" placeholder="sk-..." autocomplete="off"/>
24308
+ <label>Model (optional)</label>
24309
+ <input type="text" id="cfg-model" placeholder="leave blank for default"/>
24310
+ <div class="modal-actions">
24311
+ <button class="modal-save" id="cfg-save" onclick="saveSettings()">Save</button>
24312
+ <span class="modal-status" id="cfg-status"></span>
24313
+ </div>
24314
+ </div>
24315
+ </div>
24316
+ <div id="tab-sessions" class="tab-content active">
24160
24317
  <section class="mg">
24161
24318
  <article class="mc"><div class="label">Sessions</div><div class="val green" id="m-sessions">0</div></article>
24162
24319
  <article class="mc"><div class="label">Total Cost</div><div class="val orange" id="m-cost">$0.00</div></article>
@@ -24166,7 +24323,7 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24166
24323
  </section>
24167
24324
  <section class="sg">
24168
24325
  <section class="panel">
24169
- <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>
24170
24327
  <div class="pc"><div id="sessions-area" class="empty">Loading...</div></div>
24171
24328
  </section>
24172
24329
  <section class="panel">
@@ -24178,6 +24335,119 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24178
24335
  <header class="ph"><div><h2>Session Replay</h2><p id="replay-label">select a session</p></div></header>
24179
24336
  <div class="pc" id="replay-area"><div class="empty">No session selected.</div></div>
24180
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>
24181
24451
  </main>
24182
24452
  <script>
24183
24453
  (function(){
@@ -24186,6 +24456,402 @@ var selectedId = null;
24186
24456
  var sessions = [];
24187
24457
  var costPoints = [];
24188
24458
  var replay = null;
24459
+ var insightsConfigured = false;
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
+ };
24771
+
24772
+ window.openSettings = function() {
24773
+ document.getElementById('settings-modal').classList.add('open');
24774
+ fetch('/api/settings/insights',{cache:'no-store'}).then(function(r){return r.json();}).then(function(data){
24775
+ if(data && data.configured){
24776
+ document.getElementById('cfg-provider').value = data.provider || 'anthropic';
24777
+ if(data.model) document.getElementById('cfg-model').value = data.model;
24778
+ document.getElementById('cfg-status').className = 'modal-status ok';
24779
+ document.getElementById('cfg-status').textContent = 'Configured (' + (data.provider||'') + ')';
24780
+ insightsConfigured = true;
24781
+ }
24782
+ }).catch(function(){});
24783
+ };
24784
+
24785
+ window.closeSettings = function() {
24786
+ document.getElementById('settings-modal').classList.remove('open');
24787
+ };
24788
+
24789
+ window.saveSettings = function() {
24790
+ var btn = document.getElementById('cfg-save');
24791
+ var status = document.getElementById('cfg-status');
24792
+ btn.disabled = true;
24793
+ status.className = 'modal-status';
24794
+ status.textContent = 'Validating...';
24795
+ var body = {
24796
+ provider: document.getElementById('cfg-provider').value,
24797
+ apiKey: document.getElementById('cfg-apikey').value,
24798
+ model: document.getElementById('cfg-model').value || undefined
24799
+ };
24800
+ fetch('/api/settings/insights',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
24801
+ .then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d};});})
24802
+ .then(function(res){
24803
+ btn.disabled = false;
24804
+ if(res.ok && res.data.status === 'ok'){
24805
+ status.className = 'modal-status ok';
24806
+ status.textContent = 'Saved! (' + (res.data.provider||'') + ' / ' + (res.data.model||'default') + ')';
24807
+ insightsConfigured = true;
24808
+ document.getElementById('cfg-apikey').value = '';
24809
+ if(replay) renderReplay();
24810
+ } else {
24811
+ status.className = 'modal-status error';
24812
+ status.textContent = res.data.message || 'Save failed';
24813
+ }
24814
+ }).catch(function(e){
24815
+ btn.disabled = false;
24816
+ status.className = 'modal-status error';
24817
+ status.textContent = String(e);
24818
+ });
24819
+ };
24820
+
24821
+ window.generateInsight = function(sid) {
24822
+ var panel = document.getElementById('insight-panel');
24823
+ if(!panel) return;
24824
+ panel.innerHTML = '<div class="insight-loading">Generating insight...</div>';
24825
+ fetch('/api/session/' + encodeURIComponent(sid) + '/insights',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})
24826
+ .then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d};});})
24827
+ .then(function(res){
24828
+ if(res.ok && res.data.status === 'ok' && res.data.insight){
24829
+ insightsCache[sid] = res.data.insight;
24830
+ renderInsightContent(panel, res.data.insight);
24831
+ } else {
24832
+ panel.innerHTML = '<div class="insight-error">' + esc(res.data.message || 'Failed to generate insight') + '</div>';
24833
+ }
24834
+ }).catch(function(e){
24835
+ panel.innerHTML = '<div class="insight-error">' + esc(String(e)) + '</div>';
24836
+ });
24837
+ };
24838
+
24839
+ function renderInsightContent(panel, insight) {
24840
+ var h = '<div class="insight-hd"><span class="insight-title">AI Insight</span><span class="insight-meta">' + esc(insight.provider||'') + ' / ' + esc(insight.model||'') + '</span></div>';
24841
+ h += '<div class="insight-summary">' + esc(insight.summary) + '</div>';
24842
+ if(insight.highlights && insight.highlights.length > 0){
24843
+ h += '<div class="insight-section"><div class="insight-section-title">Highlights</div>';
24844
+ insight.highlights.forEach(function(item){ h += '<div class="insight-item">' + esc(item) + '</div>'; });
24845
+ h += '</div>';
24846
+ }
24847
+ if(insight.suggestions && insight.suggestions.length > 0){
24848
+ h += '<div class="insight-section"><div class="insight-section-title">Suggestions</div>';
24849
+ insight.suggestions.forEach(function(item){ h += '<div class="insight-item">' + esc(item) + '</div>'; });
24850
+ h += '</div>';
24851
+ }
24852
+ if(insight.costNote) h += '<div class="insight-cost">' + esc(insight.costNote) + '</div>';
24853
+ panel.innerHTML = h;
24854
+ }
24189
24855
 
24190
24856
  function esc(s) {
24191
24857
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(new RegExp(DQ,'g'),'&quot;');
@@ -24402,9 +25068,16 @@ window.togglePrompt = function(hd) {
24402
25068
 
24403
25069
  function renderSessions() {
24404
25070
  var area = document.getElementById('sessions-area');
24405
- 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; }
24406
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>';
24407
- sessions.forEach(function(s){
25080
+ filtered.forEach(function(s){
24408
25081
  var active = s.sessionId === selectedId ? ' active' : '';
24409
25082
  var repo = s.gitRepo ? (s.gitBranch ? s.gitRepo + '/' + s.gitBranch : s.gitRepo) : '-';
24410
25083
  var commits = s.commitCount > 0 ? '<span class="badge green">' + s.commitCount + '</span>' : '<span class="badge dim">0</span>';
@@ -24416,14 +25089,15 @@ function renderSessions() {
24416
25089
  }
24417
25090
 
24418
25091
  function renderMetrics() {
24419
- document.getElementById('m-sessions').textContent = sessions.length;
24420
- document.getElementById('m-cost').textContent = fmt$(sessions.reduce(function(s,x){return s+x.totalCostUsd;},0));
24421
- document.getElementById('m-prompts').textContent = sessions.reduce(function(s,x){return s+x.promptCount;},0);
24422
- document.getElementById('m-tools').textContent = sessions.reduce(function(s,x){return s+x.toolCallCount;},0);
24423
- var tc = sessions.reduce(function(s,x){return s+x.commitCount;},0);
24424
- 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;
24425
25099
  document.getElementById('m-commits').textContent = tc;
24426
- 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';
24427
25101
  }
24428
25102
 
24429
25103
  function renderCostChart() {
@@ -24479,6 +25153,17 @@ function renderReplay() {
24479
25153
  }
24480
25154
  h += '</div>';
24481
25155
  }
25156
+ // AI insight panel
25157
+ h += '<div class="insight-panel" id="insight-panel">';
25158
+ var cachedInsight = insightsCache[replay.sessionId];
25159
+ if (cachedInsight) {
25160
+ // will be rendered after innerHTML set
25161
+ } else if (insightsConfigured) {
25162
+ h += '<div style="display:flex;align-items:center;justify-content:space-between"><span class="insight-title">AI Insight</span><button class="insight-gen-btn" data-sid="' + esc(replay.sessionId) + '">Generate Insight</button></div>';
25163
+ } else {
25164
+ h += '<div style="display:flex;align-items:center;justify-content:space-between"><span class="insight-title">AI Insight</span><span style="font-size:11px;color:var(--text-dim)">Configure an API key in settings to enable AI insights</span></div>';
25165
+ }
25166
+ h += '</div>';
24482
25167
  // prompt groups
24483
25168
  var groups = buildPromptGroups(replay.timeline, replay.commits);
24484
25169
  if (groups.length === 0) {
@@ -24487,6 +25172,14 @@ function renderReplay() {
24487
25172
  groups.forEach(function(g, i) { h += renderPromptCard(g, i + 1); });
24488
25173
  }
24489
25174
  area.innerHTML = h;
25175
+ if (cachedInsight) {
25176
+ var ip = document.getElementById('insight-panel');
25177
+ if (ip) renderInsightContent(ip, cachedInsight);
25178
+ }
25179
+ var genBtn = area.querySelector('.insight-gen-btn[data-sid]');
25180
+ if (genBtn) {
25181
+ genBtn.addEventListener('click', function() { generateInsight(genBtn.getAttribute('data-sid')); });
25182
+ }
24490
25183
  }
24491
25184
 
24492
25185
  function parseSummary(v) {
@@ -24508,8 +25201,10 @@ function setSessions(raw) {
24508
25201
  sessions = sortLatest(raw.map(parseSummary).filter(Boolean));
24509
25202
  renderMetrics();
24510
25203
  renderSessions();
24511
- if (sessions.length > 0 && (!selectedId || !sessions.some(function(s){return s.sessionId===selectedId;}))) {
24512
- 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);
24513
25208
  }
24514
25209
  }
24515
25210
 
@@ -24554,7 +25249,10 @@ function loadSnapshot() {
24554
25249
  });
24555
25250
  }
24556
25251
 
24557
- function boot() {
25252
+ function startStreaming() {
25253
+ fetch('/api/settings/insights',{cache:'no-store'}).then(function(r){return r.json();}).then(function(data){
25254
+ if(data && data.configured) insightsConfigured = true;
25255
+ }).catch(function(){});
24558
25256
  loadSnapshot().then(function(){
24559
25257
  if (typeof EventSource !== 'undefined') {
24560
25258
  var es = new EventSource('/api/sessions/stream');
@@ -24565,6 +25263,12 @@ function boot() {
24565
25263
  document.getElementById('stream-label').textContent = 'live';
24566
25264
  document.getElementById('status').className = 'status-banner';
24567
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
+ }
24568
25272
  }
24569
25273
  });
24570
25274
  es.addEventListener('bridge_error', function(event) {
@@ -24582,6 +25286,645 @@ function boot() {
24582
25286
  });
24583
25287
  }
24584
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
+
24585
25928
  boot();
24586
25929
  })();
24587
25930
  </script>
@@ -24881,6 +26224,131 @@ async function startDashboardServer(options = {}) {
24881
26224
  const pathname = parsePathname(url);
24882
26225
  const segments = parsePathSegments(pathname);
24883
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
+ }
26318
+ if (method === "POST" && (pathname === "/api/settings/insights" || segments.length === 4 && segments[0] === "api" && segments[1] === "session" && segments[3] === "insights")) {
26319
+ let body = "";
26320
+ req.setEncoding("utf8");
26321
+ req.on("data", (chunk) => {
26322
+ body += chunk;
26323
+ });
26324
+ req.on("end", () => {
26325
+ let parsedBody;
26326
+ try {
26327
+ parsedBody = body.length > 0 ? JSON.parse(body) : {};
26328
+ } catch {
26329
+ sendJson(res, 400, { status: "error", message: "invalid JSON body" });
26330
+ return;
26331
+ }
26332
+ let apiPath;
26333
+ if (pathname === "/api/settings/insights") {
26334
+ apiPath = "/v1/settings/insights";
26335
+ } else {
26336
+ const sessionId = segments[2];
26337
+ apiPath = `/v1/sessions/${encodeURIComponent(sessionId ?? "")}/insights`;
26338
+ }
26339
+ void fetch(`${apiBaseUrl}${apiPath}`, {
26340
+ method: "POST",
26341
+ headers: { "Content-Type": "application/json" },
26342
+ body: JSON.stringify(parsedBody)
26343
+ }).then(async (apiResponse) => {
26344
+ const payload = await apiResponse.json();
26345
+ sendJson(res, apiResponse.status, payload);
26346
+ }).catch((error) => {
26347
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
26348
+ });
26349
+ });
26350
+ return;
26351
+ }
24884
26352
  if (method !== "GET") {
24885
26353
  sendJson(res, 405, {
24886
26354
  status: "error",
@@ -24930,6 +26398,88 @@ async function startDashboardServer(options = {}) {
24930
26398
  });
24931
26399
  return;
24932
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
+ }
24933
26483
  if (segments.length === 3 && segments[0] === "api" && segments[1] === "session") {
24934
26484
  const encodedSessionId = segments[2];
24935
26485
  let sessionId = "";
@@ -24972,8 +26522,17 @@ async function startDashboardServer(options = {}) {
24972
26522
  });
24973
26523
  return;
24974
26524
  }
26525
+ if (pathname === "/api/settings/insights") {
26526
+ void fetch(`${apiBaseUrl}/v1/settings/insights`).then(async (apiResponse) => {
26527
+ const payload = await apiResponse.json();
26528
+ sendJson(res, apiResponse.status, payload);
26529
+ }).catch((error) => {
26530
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
26531
+ });
26532
+ return;
26533
+ }
24975
26534
  if (pathname === "/") {
24976
- sendHtml(res, 200, renderDashboardHtml());
26535
+ sendHtml(res, 200, renderDashboardHtml(options.currentUserEmail !== void 0 ? { currentUserEmail: options.currentUserEmail } : {}));
24977
26536
  return;
24978
26537
  }
24979
26538
  sendJson(res, 404, {
@@ -25089,6 +26648,12 @@ CREATE TABLE IF NOT EXISTS pull_requests (
25089
26648
  );
25090
26649
 
25091
26650
  CREATE INDEX IF NOT EXISTS idx_pull_requests_session ON pull_requests(session_id);
26651
+
26652
+ CREATE TABLE IF NOT EXISTS instance_settings (
26653
+ key TEXT PRIMARY KEY,
26654
+ value TEXT NOT NULL,
26655
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
26656
+ );
25092
26657
  `;
25093
26658
  function toJsonArray(value) {
25094
26659
  return JSON.stringify(value);
@@ -25127,6 +26692,7 @@ var SqliteClient = class {
25127
26692
  this.migrateDeduplicateEvents();
25128
26693
  this.db.exec(SCHEMA_SQL);
25129
26694
  this.migrateRebuildBrokenTraces();
26695
+ this.migrateTeamColumns();
25130
26696
  }
25131
26697
  async insertJsonEachRow(request) {
25132
26698
  if (request.rows.length === 0) return;
@@ -25138,18 +26704,20 @@ var SqliteClient = class {
25138
26704
  if (rows.length === 0) return;
25139
26705
  const upsert = this.db.prepare(`
25140
26706
  INSERT INTO session_traces
25141
- (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,
25142
26708
  prompt_count, tool_call_count, api_call_count, total_cost_usd,
25143
26709
  total_input_tokens, total_output_tokens, total_cache_read_tokens, total_cache_write_tokens,
25144
26710
  lines_added, lines_removed,
25145
26711
  models_used, tools_used, files_touched, commit_count, updated_at)
25146
26712
  VALUES
25147
- (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
26713
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
25148
26714
  ON CONFLICT(session_id) DO UPDATE SET
25149
26715
  version = excluded.version,
25150
26716
  started_at = excluded.started_at,
25151
26717
  ended_at = excluded.ended_at,
25152
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),
25153
26721
  git_repo = excluded.git_repo,
25154
26722
  git_branch = excluded.git_branch,
25155
26723
  prompt_count = excluded.prompt_count,
@@ -25176,6 +26744,8 @@ var SqliteClient = class {
25176
26744
  row.started_at,
25177
26745
  row.ended_at,
25178
26746
  row.user_id,
26747
+ row.user_email ?? null,
26748
+ row.user_display_name ?? null,
25179
26749
  row.git_repo,
25180
26750
  row.git_branch,
25181
26751
  row.prompt_count,
@@ -25382,6 +26952,47 @@ var SqliteClient = class {
25382
26952
  sessionCount: Number(raw["sessions_count"] ?? 0)
25383
26953
  }));
25384
26954
  }
26955
+ getSetting(key) {
26956
+ const row = this.db.prepare(
26957
+ "SELECT value FROM instance_settings WHERE key = ?"
26958
+ ).get(key);
26959
+ return row?.value;
26960
+ }
26961
+ upsertSetting(key, value) {
26962
+ this.db.prepare(`
26963
+ INSERT INTO instance_settings (key, value, updated_at)
26964
+ VALUES (?, ?, datetime('now'))
26965
+ ON CONFLICT(key) DO UPDATE SET
26966
+ value = excluded.value,
26967
+ updated_at = excluded.updated_at
26968
+ `).run(key, value);
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
+ }
25385
26996
  close() {
25386
26997
  this.db.close();
25387
26998
  }
@@ -25510,8 +27121,44 @@ var SqliteClient = class {
25510
27121
  if (broken === void 0) {
25511
27122
  return;
25512
27123
  }
25513
- console.log("[agent-trace] migrating: rebuilding session traces with correct models/tools");
25514
- this.rebuildSessionTracesFromEvents();
27124
+ console.log("[agent-trace] migrating: rebuilding session traces with correct models/tools");
27125
+ this.rebuildSessionTracesFromEvents();
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
+ `);
25515
27162
  }
25516
27163
  /**
25517
27164
  * Rebuild session_traces by aggregating deduplicated agent_events.
@@ -26411,32 +28058,867 @@ function lookupModelPricing(model) {
26411
28058
  return pricing;
26412
28059
  }
26413
28060
  }
26414
- return void 0;
28061
+ return void 0;
28062
+ }
28063
+ function calculateCostUsd(input) {
28064
+ if (input.model === void 0) {
28065
+ return 0;
28066
+ }
28067
+ const pricing = lookupModelPricing(input.model);
28068
+ if (pricing === void 0) {
28069
+ return 0;
28070
+ }
28071
+ const baseInput = Math.max(0, input.inputTokens);
28072
+ const output = Math.max(0, input.outputTokens);
28073
+ const cacheRead = Math.max(0, input.cacheReadTokens ?? 0);
28074
+ const cacheWrite = Math.max(0, input.cacheWriteTokens ?? 0);
28075
+ const cost = baseInput * pricing.inputPerToken + cacheRead * pricing.cacheReadPerToken + cacheWrite * pricing.cacheWritePerToken + output * pricing.outputPerToken;
28076
+ return Number(cost.toFixed(6));
28077
+ }
28078
+
28079
+ // packages/runtime/src/runtime.ts
28080
+ var import_node_http2 = __toESM(require("node:http"));
28081
+
28082
+ // packages/api/src/insights-provider.ts
28083
+ var DEFAULT_MODELS = {
28084
+ anthropic: "claude-sonnet-4-20250514",
28085
+ openai: "gpt-4o-mini",
28086
+ gemini: "gemini-2.0-flash",
28087
+ openrouter: "anthropic/claude-sonnet-4"
28088
+ };
28089
+ function extractTextContent(body) {
28090
+ if (typeof body !== "object" || body === null) return "";
28091
+ const record = body;
28092
+ if (Array.isArray(record["content"])) {
28093
+ for (const block of record["content"]) {
28094
+ if (typeof block === "object" && block !== null) {
28095
+ const b = block;
28096
+ if (b["type"] === "text" && typeof b["text"] === "string") return b["text"];
28097
+ }
28098
+ }
28099
+ }
28100
+ if (Array.isArray(record["choices"])) {
28101
+ const first = record["choices"][0];
28102
+ if (typeof first === "object" && first !== null) {
28103
+ const choice = first;
28104
+ const msg = choice["message"];
28105
+ if (typeof msg === "object" && msg !== null) {
28106
+ const m = msg;
28107
+ if (typeof m["content"] === "string") return m["content"];
28108
+ }
28109
+ }
28110
+ }
28111
+ if (Array.isArray(record["candidates"])) {
28112
+ const first = record["candidates"][0];
28113
+ if (typeof first === "object" && first !== null) {
28114
+ const candidate = first;
28115
+ const content = candidate["content"];
28116
+ if (typeof content === "object" && content !== null) {
28117
+ const c = content;
28118
+ if (Array.isArray(c["parts"])) {
28119
+ for (const part of c["parts"]) {
28120
+ if (typeof part === "object" && part !== null) {
28121
+ const p = part;
28122
+ if (typeof p["text"] === "string") return p["text"];
28123
+ }
28124
+ }
28125
+ }
28126
+ }
28127
+ }
28128
+ }
28129
+ return "";
28130
+ }
28131
+ function createAnthropicProvider(apiKey, model) {
28132
+ const endpoint = "https://api.anthropic.com/v1/messages";
28133
+ return {
28134
+ provider: "anthropic",
28135
+ model,
28136
+ async complete(system, user, maxTokens) {
28137
+ const response = await fetch(endpoint, {
28138
+ method: "POST",
28139
+ headers: {
28140
+ "Content-Type": "application/json",
28141
+ "x-api-key": apiKey,
28142
+ "anthropic-version": "2023-06-01"
28143
+ },
28144
+ body: JSON.stringify({
28145
+ model,
28146
+ max_tokens: maxTokens ?? 1024,
28147
+ system,
28148
+ messages: [{ role: "user", content: user }]
28149
+ })
28150
+ });
28151
+ if (!response.ok) {
28152
+ throw new Error(`anthropic api returned ${String(response.status)}`);
28153
+ }
28154
+ const body = await response.json();
28155
+ return extractTextContent(body);
28156
+ },
28157
+ async validate() {
28158
+ try {
28159
+ const response = await fetch(endpoint, {
28160
+ method: "POST",
28161
+ headers: {
28162
+ "Content-Type": "application/json",
28163
+ "x-api-key": apiKey,
28164
+ "anthropic-version": "2023-06-01"
28165
+ },
28166
+ body: JSON.stringify({
28167
+ model,
28168
+ max_tokens: 1,
28169
+ messages: [{ role: "user", content: "hi" }]
28170
+ })
28171
+ });
28172
+ return response.ok || response.status === 400;
28173
+ } catch {
28174
+ return false;
28175
+ }
28176
+ }
28177
+ };
28178
+ }
28179
+ function createOpenAiProvider(apiKey, model) {
28180
+ const endpoint = "https://api.openai.com/v1/chat/completions";
28181
+ return {
28182
+ provider: "openai",
28183
+ model,
28184
+ async complete(system, user, maxTokens) {
28185
+ const response = await fetch(endpoint, {
28186
+ method: "POST",
28187
+ headers: {
28188
+ "Content-Type": "application/json",
28189
+ "Authorization": `Bearer ${apiKey}`
28190
+ },
28191
+ body: JSON.stringify({
28192
+ model,
28193
+ max_tokens: maxTokens ?? 1024,
28194
+ messages: [
28195
+ { role: "system", content: system },
28196
+ { role: "user", content: user }
28197
+ ]
28198
+ })
28199
+ });
28200
+ if (!response.ok) {
28201
+ throw new Error(`openai api returned ${String(response.status)}`);
28202
+ }
28203
+ const body = await response.json();
28204
+ return extractTextContent(body);
28205
+ },
28206
+ async validate() {
28207
+ try {
28208
+ const response = await fetch(endpoint, {
28209
+ method: "POST",
28210
+ headers: {
28211
+ "Content-Type": "application/json",
28212
+ "Authorization": `Bearer ${apiKey}`
28213
+ },
28214
+ body: JSON.stringify({
28215
+ model,
28216
+ max_tokens: 1,
28217
+ messages: [{ role: "user", content: "hi" }]
28218
+ })
28219
+ });
28220
+ return response.ok;
28221
+ } catch {
28222
+ return false;
28223
+ }
28224
+ }
28225
+ };
28226
+ }
28227
+ function createGeminiProvider(apiKey, model) {
28228
+ const baseUrl = "https://generativelanguage.googleapis.com/v1beta";
28229
+ return {
28230
+ provider: "gemini",
28231
+ model,
28232
+ async complete(system, user, maxTokens) {
28233
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
28234
+ const response = await fetch(url, {
28235
+ method: "POST",
28236
+ headers: { "Content-Type": "application/json" },
28237
+ body: JSON.stringify({
28238
+ systemInstruction: { parts: [{ text: system }] },
28239
+ contents: [{ parts: [{ text: user }] }],
28240
+ generationConfig: { maxOutputTokens: maxTokens ?? 1024 }
28241
+ })
28242
+ });
28243
+ if (!response.ok) {
28244
+ throw new Error(`gemini api returned ${String(response.status)}`);
28245
+ }
28246
+ const body = await response.json();
28247
+ return extractTextContent(body);
28248
+ },
28249
+ async validate() {
28250
+ try {
28251
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
28252
+ const response = await fetch(url, {
28253
+ method: "POST",
28254
+ headers: { "Content-Type": "application/json" },
28255
+ body: JSON.stringify({
28256
+ contents: [{ parts: [{ text: "hi" }] }],
28257
+ generationConfig: { maxOutputTokens: 1 }
28258
+ })
28259
+ });
28260
+ return response.ok;
28261
+ } catch {
28262
+ return false;
28263
+ }
28264
+ }
28265
+ };
28266
+ }
28267
+ function createOpenRouterProvider(apiKey, model) {
28268
+ const endpoint = "https://openrouter.ai/api/v1/chat/completions";
28269
+ return {
28270
+ provider: "openrouter",
28271
+ model,
28272
+ async complete(system, user, maxTokens) {
28273
+ const response = await fetch(endpoint, {
28274
+ method: "POST",
28275
+ headers: {
28276
+ "Content-Type": "application/json",
28277
+ "Authorization": `Bearer ${apiKey}`
28278
+ },
28279
+ body: JSON.stringify({
28280
+ model,
28281
+ max_tokens: maxTokens ?? 1024,
28282
+ messages: [
28283
+ { role: "system", content: system },
28284
+ { role: "user", content: user }
28285
+ ]
28286
+ })
28287
+ });
28288
+ if (!response.ok) {
28289
+ throw new Error(`openrouter api returned ${String(response.status)}`);
28290
+ }
28291
+ const body = await response.json();
28292
+ return extractTextContent(body);
28293
+ },
28294
+ async validate() {
28295
+ try {
28296
+ const response = await fetch(endpoint, {
28297
+ method: "POST",
28298
+ headers: {
28299
+ "Content-Type": "application/json",
28300
+ "Authorization": `Bearer ${apiKey}`
28301
+ },
28302
+ body: JSON.stringify({
28303
+ model,
28304
+ max_tokens: 1,
28305
+ messages: [{ role: "user", content: "hi" }]
28306
+ })
28307
+ });
28308
+ return response.ok;
28309
+ } catch {
28310
+ return false;
28311
+ }
28312
+ }
28313
+ };
28314
+ }
28315
+ function createLlmProvider(config) {
28316
+ const model = config.model ?? DEFAULT_MODELS[config.provider];
28317
+ switch (config.provider) {
28318
+ case "anthropic":
28319
+ return createAnthropicProvider(config.apiKey, model);
28320
+ case "openai":
28321
+ return createOpenAiProvider(config.apiKey, model);
28322
+ case "gemini":
28323
+ return createGeminiProvider(config.apiKey, model);
28324
+ case "openrouter":
28325
+ return createOpenRouterProvider(config.apiKey, model);
28326
+ }
28327
+ }
28328
+
28329
+ // packages/api/src/insights-generator.ts
28330
+ var SYSTEM_PROMPT = `You are an AI coding session analyst. You analyze telemetry from AI coding agent sessions and produce structured insights.
28331
+
28332
+ Return ONLY valid JSON with this exact schema:
28333
+ {
28334
+ "summary": "2-3 sentence overview of what the session accomplished",
28335
+ "highlights": ["1-3 notable observations about the session"],
28336
+ "suggestions": ["0-3 actionable suggestions for improving efficiency"],
28337
+ "costNote": "optional one-line note about cost efficiency, or null"
28338
+ }
28339
+
28340
+ Guidelines:
28341
+ - summary: Describe what the agent accomplished concisely. Mention key outcomes (files changed, commits, PRs).
28342
+ - highlights: Focus on interesting patterns \u2014 heavy tool usage, large diffs, cache efficiency, model choices.
28343
+ - suggestions: Only suggest things that are actionable. If the session looks efficient, return an empty array.
28344
+ - costNote: Comment on cost relative to output if noteworthy. Set to null if unremarkable.
28345
+ - Be concise and specific. Reference actual numbers from the data.`;
28346
+ function condensedTimeline(trace, maxChars) {
28347
+ const lines = [];
28348
+ for (const event of trace.timeline) {
28349
+ const parts = [event.type];
28350
+ if (event.details !== void 0) {
28351
+ const d = event.details;
28352
+ const toolName = d["toolName"] ?? d["tool_name"];
28353
+ if (typeof toolName === "string") parts.push(toolName);
28354
+ const toolInput = d["toolInput"] ?? d["tool_input"];
28355
+ if (typeof toolInput === "object" && toolInput !== null) {
28356
+ const ti = toolInput;
28357
+ const fp = ti["file_path"] ?? ti["filePath"];
28358
+ if (typeof fp === "string") parts.push(fp);
28359
+ const cmd = ti["command"];
28360
+ if (typeof cmd === "string") parts.push(cmd.slice(0, 80));
28361
+ }
28362
+ }
28363
+ if (event.costUsd !== void 0 && event.costUsd > 0) {
28364
+ parts.push(`$${event.costUsd.toFixed(4)}`);
28365
+ }
28366
+ const line = parts.join(" | ");
28367
+ lines.push(line);
28368
+ const totalLength = lines.reduce((sum, l) => sum + l.length + 1, 0);
28369
+ if (totalLength > maxChars) break;
28370
+ }
28371
+ return lines.join("\n");
28372
+ }
28373
+ function buildInsightPrompt(trace) {
28374
+ const m = trace.metrics;
28375
+ const sections = [];
28376
+ sections.push(`## Session: ${trace.sessionId}`);
28377
+ sections.push(`Started: ${trace.startedAt}${trace.endedAt !== void 0 ? ` | Ended: ${trace.endedAt}` : ""}`);
28378
+ if (trace.environment.gitRepo !== void 0) {
28379
+ sections.push(`Repo: ${trace.environment.gitRepo}${trace.environment.gitBranch !== void 0 ? ` (${trace.environment.gitBranch})` : ""}`);
28380
+ }
28381
+ sections.push(`
28382
+ ## Metrics`);
28383
+ sections.push(`Prompts: ${String(m.promptCount)} | Tool calls: ${String(m.toolCallCount)} | API calls: ${String(m.apiCallCount)}`);
28384
+ sections.push(`Cost: $${m.totalCostUsd.toFixed(4)}`);
28385
+ sections.push(`Tokens: ${String(m.totalInputTokens)} in / ${String(m.totalOutputTokens)} out | Cache: ${String(m.totalCacheReadTokens)} read / ${String(m.totalCacheWriteTokens)} write`);
28386
+ sections.push(`Lines: +${String(m.linesAdded)} / -${String(m.linesRemoved)} | Files: ${String(m.filesTouched.length)}`);
28387
+ if (m.modelsUsed.length > 0) sections.push(`Models: ${m.modelsUsed.join(", ")}`);
28388
+ if (m.toolsUsed.length > 0) sections.push(`Tools: ${m.toolsUsed.join(", ")}`);
28389
+ if (trace.git.commits.length > 0) {
28390
+ sections.push(`
28391
+ ## Commits (${String(trace.git.commits.length)})`);
28392
+ trace.git.commits.forEach((c) => {
28393
+ sections.push(`- ${c.sha.slice(0, 7)}: ${c.message ?? "no message"}`);
28394
+ });
28395
+ }
28396
+ if (trace.git.pullRequests.length > 0) {
28397
+ sections.push(`
28398
+ ## Pull Requests (${String(trace.git.pullRequests.length)})`);
28399
+ trace.git.pullRequests.forEach((pr) => {
28400
+ sections.push(`- PR #${String(pr.prNumber)} (${pr.state}) in ${pr.repo}`);
28401
+ });
28402
+ }
28403
+ sections.push(`
28404
+ ## Timeline (condensed)`);
28405
+ sections.push(condensedTimeline(trace, 4e3));
28406
+ return sections.join("\n");
28407
+ }
28408
+ function parseInsightJson(raw) {
28409
+ let jsonStr = raw.trim();
28410
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
28411
+ if (fenceMatch !== null && fenceMatch[1] !== void 0) {
28412
+ jsonStr = fenceMatch[1].trim();
28413
+ }
28414
+ try {
28415
+ const parsed = JSON.parse(jsonStr);
28416
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
28417
+ const obj = parsed;
28418
+ const summary = typeof obj["summary"] === "string" ? obj["summary"] : "";
28419
+ if (summary.length === 0) return void 0;
28420
+ const highlights = Array.isArray(obj["highlights"]) ? obj["highlights"].filter((h) => typeof h === "string") : [];
28421
+ const suggestions = Array.isArray(obj["suggestions"]) ? obj["suggestions"].filter((s) => typeof s === "string") : [];
28422
+ const costNote = typeof obj["costNote"] === "string" && obj["costNote"].length > 0 ? obj["costNote"] : void 0;
28423
+ return { summary, highlights, suggestions, costNote };
28424
+ } catch {
28425
+ return void 0;
28426
+ }
28427
+ }
28428
+ async function generateSessionInsight(trace, provider) {
28429
+ const userPrompt = buildInsightPrompt(trace);
28430
+ const rawResponse = await provider.complete(SYSTEM_PROMPT, userPrompt);
28431
+ const parsed = parseInsightJson(rawResponse);
28432
+ if (parsed === void 0) {
28433
+ return {
28434
+ sessionId: trace.sessionId,
28435
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
28436
+ provider: provider.provider,
28437
+ model: provider.model,
28438
+ summary: rawResponse.slice(0, 500) || "Failed to generate structured insight.",
28439
+ highlights: [],
28440
+ suggestions: []
28441
+ };
28442
+ }
28443
+ const result = {
28444
+ sessionId: trace.sessionId,
28445
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
28446
+ provider: provider.provider,
28447
+ model: provider.model,
28448
+ summary: parsed.summary,
28449
+ highlights: parsed.highlights,
28450
+ suggestions: parsed.suggestions
28451
+ };
28452
+ if (parsed.costNote !== void 0) {
28453
+ return { ...result, costNote: parsed.costNote };
28454
+ }
28455
+ return result;
28456
+ }
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
+
28688
+ // packages/api/src/insights-handler.ts
28689
+ var insightsCache = /* @__PURE__ */ new Map();
28690
+ var teamInsightCache;
28691
+ var VALID_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
28692
+ function isValidProvider(value) {
28693
+ return typeof value === "string" && VALID_PROVIDERS.includes(value);
28694
+ }
28695
+ function handleGetInsightsSettings(dependencies) {
28696
+ const accessor = dependencies.insightsConfigAccessor;
28697
+ if (accessor === void 0) {
28698
+ return {
28699
+ statusCode: 200,
28700
+ payload: { status: "ok", configured: false }
28701
+ };
28702
+ }
28703
+ const config = accessor.getConfig();
28704
+ if (config === void 0) {
28705
+ return {
28706
+ statusCode: 200,
28707
+ payload: { status: "ok", configured: false }
28708
+ };
28709
+ }
28710
+ return {
28711
+ statusCode: 200,
28712
+ payload: {
28713
+ status: "ok",
28714
+ configured: true,
28715
+ provider: config.provider,
28716
+ ...config.model !== void 0 ? { model: config.model } : {}
28717
+ }
28718
+ };
28719
+ }
28720
+ async function handlePostInsightsSettings(body, dependencies) {
28721
+ const accessor = dependencies.insightsConfigAccessor;
28722
+ if (accessor === void 0) {
28723
+ return {
28724
+ statusCode: 500,
28725
+ payload: { status: "error", message: "insights settings not available" }
28726
+ };
28727
+ }
28728
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
28729
+ return {
28730
+ statusCode: 400,
28731
+ payload: { status: "error", message: "request body must be a JSON object" }
28732
+ };
28733
+ }
28734
+ const record = body;
28735
+ const provider = record["provider"];
28736
+ const apiKey = record["apiKey"];
28737
+ const model = record["model"];
28738
+ if (!isValidProvider(provider)) {
28739
+ return {
28740
+ statusCode: 400,
28741
+ payload: { status: "error", message: "invalid provider. must be one of: anthropic, openai, gemini, openrouter" }
28742
+ };
28743
+ }
28744
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
28745
+ return {
28746
+ statusCode: 400,
28747
+ payload: { status: "error", message: "apiKey is required" }
28748
+ };
28749
+ }
28750
+ const config = {
28751
+ provider,
28752
+ apiKey,
28753
+ ...typeof model === "string" && model.length > 0 ? { model } : {}
28754
+ };
28755
+ const llm = createLlmProvider(config);
28756
+ const valid = await llm.validate();
28757
+ if (!valid) {
28758
+ return {
28759
+ statusCode: 400,
28760
+ payload: { status: "error", message: "API key validation failed. Check your key and try again." }
28761
+ };
28762
+ }
28763
+ accessor.setConfig(config);
28764
+ return {
28765
+ statusCode: 200,
28766
+ payload: {
28767
+ status: "ok",
28768
+ message: "insights configuration saved",
28769
+ provider: config.provider,
28770
+ model: llm.model
28771
+ }
28772
+ };
28773
+ }
28774
+ async function handlePostSessionInsight(sessionId, dependencies) {
28775
+ const accessor = dependencies.insightsConfigAccessor;
28776
+ if (accessor === void 0) {
28777
+ return {
28778
+ statusCode: 400,
28779
+ payload: { status: "error", message: "insights not configured" }
28780
+ };
28781
+ }
28782
+ const config = accessor.getConfig();
28783
+ if (config === void 0) {
28784
+ return {
28785
+ statusCode: 400,
28786
+ payload: { status: "error", message: "no AI provider configured. open settings to add your API key." }
28787
+ };
28788
+ }
28789
+ const cached = insightsCache.get(sessionId);
28790
+ if (cached !== void 0) {
28791
+ return {
28792
+ statusCode: 200,
28793
+ payload: { status: "ok", insight: cached }
28794
+ };
28795
+ }
28796
+ const trace = dependencies.repository.getBySessionId(sessionId);
28797
+ if (trace === void 0) {
28798
+ return {
28799
+ statusCode: 404,
28800
+ payload: { status: "error", message: "session not found" }
28801
+ };
28802
+ }
28803
+ const provider = createLlmProvider(config);
28804
+ const insight = await generateSessionInsight(trace, provider);
28805
+ insightsCache.set(sessionId, insight);
28806
+ return {
28807
+ statusCode: 200,
28808
+ payload: { status: "ok", insight }
28809
+ };
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
+ };
26415
28880
  }
26416
- function calculateCostUsd(input) {
26417
- if (input.model === void 0) {
26418
- return 0;
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
+ };
26419
28888
  }
26420
- const pricing = lookupModelPricing(input.model);
26421
- if (pricing === void 0) {
26422
- return 0;
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
+ };
26423
28894
  }
26424
- const baseInput = Math.max(0, input.inputTokens);
26425
- const output = Math.max(0, input.outputTokens);
26426
- const cacheRead = Math.max(0, input.cacheReadTokens ?? 0);
26427
- const cacheWrite = Math.max(0, input.cacheWriteTokens ?? 0);
26428
- const cost = baseInput * pricing.inputPerToken + cacheRead * pricing.cacheReadPerToken + cacheWrite * pricing.cacheWritePerToken + output * pricing.outputPerToken;
26429
- return Number(cost.toFixed(6));
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
+ };
26430
28914
  }
26431
28915
 
26432
- // packages/runtime/src/runtime.ts
26433
- var import_node_http2 = __toESM(require("node:http"));
26434
-
26435
28916
  // packages/api/src/mapper.ts
26436
28917
  function toSessionSummary(trace) {
26437
28918
  return {
26438
28919
  sessionId: trace.sessionId,
26439
28920
  userId: trace.user.id,
28921
+ userDisplayName: trace.user.displayName ?? null,
26440
28922
  gitRepo: trace.environment.gitRepo ?? null,
26441
28923
  gitBranch: trace.environment.gitBranch ?? null,
26442
28924
  startedAt: trace.startedAt,
@@ -26450,6 +28932,353 @@ function toSessionSummary(trace) {
26450
28932
  };
26451
28933
  }
26452
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
+
26453
29282
  // packages/api/src/handler.ts
26454
29283
  function buildError(message) {
26455
29284
  return {
@@ -26467,15 +29296,19 @@ function buildHealth(startedAtMs) {
26467
29296
  function parseFilters(searchParams) {
26468
29297
  const userId = searchParams.get("userId");
26469
29298
  const repo = searchParams.get("repo");
29299
+ const from = searchParams.get("from");
29300
+ const to = searchParams.get("to");
26470
29301
  return {
26471
29302
  ...userId !== null ? { userId } : {},
26472
- ...repo !== null ? { repo } : {}
29303
+ ...repo !== null ? { repo } : {},
29304
+ ...from !== null && from.length === 10 ? { from } : {},
29305
+ ...to !== null && to.length === 10 ? { to } : {}
26473
29306
  };
26474
29307
  }
26475
29308
  function parseSessionPath(pathname) {
26476
29309
  return pathname.split("/").filter((segment) => segment.length > 0);
26477
29310
  }
26478
- function toMetricDate(startedAt) {
29311
+ function toMetricDate2(startedAt) {
26479
29312
  const parsed = Date.parse(startedAt);
26480
29313
  if (Number.isNaN(parsed)) {
26481
29314
  return startedAt.slice(0, 10);
@@ -26486,7 +29319,7 @@ function buildDailyCostResponseFromTraces(dependencies, filters) {
26486
29319
  const traces = dependencies.repository.list(filters);
26487
29320
  const byDate = /* @__PURE__ */ new Map();
26488
29321
  traces.forEach((trace) => {
26489
- const date = toMetricDate(trace.startedAt);
29322
+ const date = toMetricDate2(trace.startedAt);
26490
29323
  const current = byDate.get(date) ?? {
26491
29324
  totalCostUsd: 0,
26492
29325
  sessionCount: 0,
@@ -26550,6 +29383,43 @@ async function handleApiRequest(request, dependencies) {
26550
29383
  payload: await buildDailyCostResponse(dependencies, filters)
26551
29384
  };
26552
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
+ }
29413
+ if (request.method === "GET" && pathname === "/v1/settings/insights") {
29414
+ return handleGetInsightsSettings(dependencies);
29415
+ }
29416
+ if (request.method === "POST" && pathname === "/v1/settings/insights") {
29417
+ return handlePostInsightsSettings(request.body, dependencies);
29418
+ }
29419
+ const insightsMatch = pathname.match(/^\/v1\/sessions\/([^/]+)\/insights$/);
29420
+ if (request.method === "POST" && insightsMatch !== null && insightsMatch[1] !== void 0) {
29421
+ return handlePostSessionInsight(decodeURIComponent(insightsMatch[1]), dependencies);
29422
+ }
26553
29423
  if (request.method === "GET") {
26554
29424
  const segments = parseSessionPath(pathname);
26555
29425
  if (segments.length >= 3 && segments[0] === "v1" && segments[1] === "sessions") {
@@ -26597,11 +29467,21 @@ async function handleApiRequest(request, dependencies) {
26597
29467
 
26598
29468
  // packages/api/src/http.ts
26599
29469
  function normalizeMethod(method) {
26600
- if (method === "GET") {
29470
+ if (method === "GET" || method === "POST") {
26601
29471
  return method;
26602
29472
  }
26603
29473
  return void 0;
26604
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
+ }
26605
29485
  function sendJson2(res, statusCode, payload) {
26606
29486
  const body = JSON.stringify(payload);
26607
29487
  res.statusCode = statusCode;
@@ -26664,30 +29544,76 @@ async function handleApiRawHttpRequest(request, dependencies) {
26664
29544
  return handleApiRequest(
26665
29545
  {
26666
29546
  method,
26667
- url: request.url
29547
+ url: request.url,
29548
+ ...request.body !== void 0 ? { body: request.body } : {}
26668
29549
  },
26669
29550
  dependencies
26670
29551
  );
26671
29552
  }
29553
+ function readRequestBody(req) {
29554
+ return new Promise((resolve, reject) => {
29555
+ let data = "";
29556
+ req.setEncoding("utf8");
29557
+ req.on("data", (chunk) => {
29558
+ data += chunk;
29559
+ });
29560
+ req.on("end", () => {
29561
+ if (data.length === 0) {
29562
+ resolve(void 0);
29563
+ return;
29564
+ }
29565
+ try {
29566
+ resolve(JSON.parse(data));
29567
+ } catch {
29568
+ reject(new Error("invalid JSON body"));
29569
+ }
29570
+ });
29571
+ req.on("error", (error) => reject(error));
29572
+ });
29573
+ }
26672
29574
  function createApiHttpHandler(dependencies) {
26673
29575
  return (req, res) => {
26674
29576
  const method = req.method ?? "GET";
26675
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
+ }
26676
29590
  if (isSseSessionsRoute(method, url)) {
26677
29591
  startSessionsSseStream(req, res, dependencies);
26678
29592
  return;
26679
29593
  }
26680
- void handleApiRawHttpRequest(
26681
- {
26682
- method,
26683
- url
26684
- },
26685
- dependencies
26686
- ).then((response) => {
26687
- sendJson2(res, response.statusCode, response.payload);
26688
- }).catch(() => {
26689
- sendJson2(res, 500, { status: "error", message: "internal server error" });
26690
- });
29594
+ const dispatch = (body) => {
29595
+ void handleApiRawHttpRequest(
29596
+ {
29597
+ method,
29598
+ url,
29599
+ ...body !== void 0 ? { body } : {}
29600
+ },
29601
+ dependencies
29602
+ ).then((response) => {
29603
+ sendJson2(res, response.statusCode, response.payload);
29604
+ }).catch(() => {
29605
+ sendJson2(res, 500, { status: "error", message: "internal server error" });
29606
+ });
29607
+ };
29608
+ if (method === "POST") {
29609
+ readRequestBody(req).then((body) => {
29610
+ dispatch(body);
29611
+ }).catch(() => {
29612
+ sendJson2(res, 400, { status: "error", message: "invalid request body" });
29613
+ });
29614
+ return;
29615
+ }
29616
+ dispatch();
26691
29617
  };
26692
29618
  }
26693
29619
 
@@ -26709,6 +29635,18 @@ var InMemorySessionRepository = class {
26709
29635
  if (filters.repo !== void 0 && trace.environment.gitRepo !== filters.repo) {
26710
29636
  return false;
26711
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
+ }
26712
29650
  return true;
26713
29651
  });
26714
29652
  }
@@ -26962,6 +29900,26 @@ function parsePathname3(url) {
26962
29900
  return url;
26963
29901
  }
26964
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
+ }
26965
29923
  function handleCollectorRawHttpRequest(request, dependencies) {
26966
29924
  const method = normalizeMethod2(request.method);
26967
29925
  if (method === void 0) {
@@ -26973,6 +29931,12 @@ function handleCollectorRawHttpRequest(request, dependencies) {
26973
29931
  }
26974
29932
  };
26975
29933
  }
29934
+ if (method === "POST") {
29935
+ const authError = checkTeamAuth(request.authorizationHeader);
29936
+ if (authError !== void 0) {
29937
+ return authError;
29938
+ }
29939
+ }
26976
29940
  const pathname = parsePathname3(request.url);
26977
29941
  if (method === "POST" && pathname === "/v1/hooks") {
26978
29942
  const rawBody = request.rawBody ?? "";
@@ -27014,7 +29978,7 @@ function handleCollectorRawHttpRequest(request, dependencies) {
27014
29978
  dependencies
27015
29979
  );
27016
29980
  }
27017
- async function readRequestBody(req) {
29981
+ async function readRequestBody2(req) {
27018
29982
  return new Promise((resolve, reject) => {
27019
29983
  const chunks = [];
27020
29984
  req.on("data", (chunk) => chunks.push(chunk));
@@ -27032,12 +29996,14 @@ function createCollectorHttpHandler(dependencies) {
27032
29996
  return async (req, res) => {
27033
29997
  const method = req.method ?? "GET";
27034
29998
  const url = req.url ?? "/";
27035
- const rawBody = method === "POST" ? await readRequestBody(req) : void 0;
29999
+ const rawBody = method === "POST" ? await readRequestBody2(req) : void 0;
30000
+ const authorizationHeader = typeof req.headers["authorization"] === "string" ? req.headers["authorization"] : void 0;
27036
30001
  const response = handleCollectorRawHttpRequest(
27037
30002
  {
27038
30003
  method,
27039
30004
  url,
27040
- ...rawBody !== void 0 ? { rawBody } : {}
30005
+ ...rawBody !== void 0 ? { rawBody } : {},
30006
+ ...authorizationHeader !== void 0 ? { authorizationHeader } : {}
27041
30007
  },
27042
30008
  dependencies
27043
30009
  );
@@ -28526,12 +31492,16 @@ function toBaseTrace(envelope) {
28526
31492
  const gitRepo = readString4(payload, ["git_repo", "gitRepo"]);
28527
31493
  const gitBranch = readString4(payload, ["git_branch", "gitBranch"]);
28528
31494
  const projectPath = readString4(payload, ["project_path", "projectPath"]);
28529
- 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";
28530
31498
  return {
28531
31499
  sessionId: envelope.sessionId,
28532
31500
  agentType: "claude_code",
28533
31501
  user: {
28534
- id: userId
31502
+ id: userId,
31503
+ ...userEmail !== void 0 ? { email: userEmail } : {},
31504
+ ...userName !== void 0 ? { displayName: userName } : {}
28535
31505
  },
28536
31506
  environment: {
28537
31507
  ...projectPath !== void 0 ? { projectPath } : {},
@@ -28588,6 +31558,13 @@ function toUpdatedTrace(existing, envelope) {
28588
31558
  const mergedTimeline = [...existing.timeline, timelineEvent];
28589
31559
  const endedAt = shouldMarkEnded(envelope.eventType) ? envelope.eventTimestamp : existing.endedAt;
28590
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
+ };
28591
31568
  const cost = computeEventCost(payload) ?? 0;
28592
31569
  const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]) ?? 0;
28593
31570
  const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]) ?? 0;
@@ -28656,6 +31633,7 @@ function toUpdatedTrace(existing, envelope) {
28656
31633
  }
28657
31634
  return {
28658
31635
  ...existing,
31636
+ user: updatedUser,
28659
31637
  ...endedAt !== void 0 ? { endedAt } : {},
28660
31638
  activeDurationMs: updateDurationMs(existing.startedAt, latestTime),
28661
31639
  timeline: mergedTimeline,
@@ -28740,7 +31718,9 @@ function resolveRuntimeOptions(input) {
28740
31718
  return {
28741
31719
  startedAtMs: input,
28742
31720
  persistence: new InMemoryRuntimePersistence(),
28743
- dailyCostReader: void 0
31721
+ dailyCostReader: void 0,
31722
+ insightsConfigAccessor: void 0,
31723
+ teamBudgetStore: void 0
28744
31724
  };
28745
31725
  }
28746
31726
  const startedAtMs = input?.startedAtMs ?? Date.now();
@@ -28748,7 +31728,9 @@ function resolveRuntimeOptions(input) {
28748
31728
  return {
28749
31729
  startedAtMs,
28750
31730
  persistence,
28751
- dailyCostReader: input?.dailyCostReader
31731
+ dailyCostReader: input?.dailyCostReader,
31732
+ insightsConfigAccessor: input?.insightsConfigAccessor,
31733
+ teamBudgetStore: input?.teamBudgetStore
28752
31734
  };
28753
31735
  }
28754
31736
  function createInMemoryRuntime(input) {
@@ -28766,7 +31748,9 @@ function createInMemoryRuntime(input) {
28766
31748
  const apiDependencies = {
28767
31749
  startedAtMs: options.startedAtMs,
28768
31750
  repository: sessionRepository,
28769
- ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {}
31751
+ ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
31752
+ ...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
31753
+ ...options.teamBudgetStore !== void 0 ? { teamBudgetStore: options.teamBudgetStore } : {}
28770
31754
  };
28771
31755
  return {
28772
31756
  sessionRepository,
@@ -28775,6 +31759,8 @@ function createInMemoryRuntime(input) {
28775
31759
  collectorService,
28776
31760
  persistence,
28777
31761
  ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
31762
+ ...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
31763
+ ...options.teamBudgetStore !== void 0 ? { teamBudgetStore: options.teamBudgetStore } : {},
28778
31764
  handleCollectorRaw: collectorService.handleRaw,
28779
31765
  handleApiRaw: (request) => handleApiRawHttpRequest(request, apiDependencies)
28780
31766
  };
@@ -28795,7 +31781,9 @@ async function startInMemoryRuntimeServers(runtime, options = {}) {
28795
31781
  createApiHttpHandler({
28796
31782
  startedAtMs: runtime.collectorDependencies.startedAtMs,
28797
31783
  repository: runtime.sessionRepository,
28798
- ...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {}
31784
+ ...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {},
31785
+ ...runtime.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: runtime.insightsConfigAccessor } : {},
31786
+ ...runtime.teamBudgetStore !== void 0 ? { teamBudgetStore: runtime.teamBudgetStore } : {}
28799
31787
  })
28800
31788
  ) : void 0;
28801
31789
  let otelReceiver;
@@ -28964,14 +31952,86 @@ function hydrateFromSqlite(runtime, sqlite, limit, eventLimit) {
28964
31952
  }
28965
31953
  return traces.length;
28966
31954
  }
31955
+ var VALID_INSIGHTS_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
31956
+ function createSqliteInsightsConfigAccessor(sqlite) {
31957
+ let cached;
31958
+ let cachedContext;
31959
+ const raw = sqlite.getSetting("insights_config");
31960
+ if (raw !== void 0) {
31961
+ try {
31962
+ const parsed = JSON.parse(raw);
31963
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
31964
+ const obj = parsed;
31965
+ if (typeof obj["provider"] === "string" && VALID_INSIGHTS_PROVIDERS.includes(obj["provider"]) && typeof obj["apiKey"] === "string") {
31966
+ cached = {
31967
+ provider: obj["provider"],
31968
+ apiKey: obj["apiKey"],
31969
+ ...typeof obj["model"] === "string" && obj["model"].length > 0 ? { model: obj["model"] } : {}
31970
+ };
31971
+ }
31972
+ }
31973
+ } catch {
31974
+ }
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
+ }
31993
+ return {
31994
+ getConfig() {
31995
+ return cached;
31996
+ },
31997
+ setConfig(config) {
31998
+ cached = config;
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);
32020
+ }
32021
+ };
32022
+ }
28967
32023
  function createSqliteBackedRuntime(options) {
28968
32024
  const sqlite = new SqliteClient(options.dbPath);
28969
32025
  const persistence = new SqlitePersistence(sqlite);
28970
32026
  const dailyCostReader = new SqliteDailyCostReader(sqlite);
32027
+ const insightsConfigAccessor = options.insightsConfigAccessor ?? createSqliteInsightsConfigAccessor(sqlite);
32028
+ const teamBudgetStore = createSqliteTeamBudgetStore(sqlite);
28971
32029
  const runtime = createInMemoryRuntime({
28972
32030
  ...options.startedAtMs !== void 0 ? { startedAtMs: options.startedAtMs } : {},
28973
32031
  persistence,
28974
- dailyCostReader
32032
+ dailyCostReader,
32033
+ insightsConfigAccessor,
32034
+ teamBudgetStore
28975
32035
  });
28976
32036
  const hydratedCount = hydrateFromSqlite(runtime, sqlite, options.bootstrapLimit, options.eventLimit);
28977
32037
  const syncIntervalMs = options.syncIntervalMs ?? 5e3;
@@ -29017,6 +32077,9 @@ function parseArgs(argv) {
29017
32077
  let privacyTier;
29018
32078
  let installHooks;
29019
32079
  let forward = false;
32080
+ let teamUrl;
32081
+ let teamPrivacyTier;
32082
+ let teamToken;
29020
32083
  for (let i = 3; i < argv.length; i += 1) {
29021
32084
  const token = argv[i];
29022
32085
  if (token === "--forward") {
@@ -29053,6 +32116,28 @@ function parseArgs(argv) {
29053
32116
  i += 1;
29054
32117
  continue;
29055
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
+ }
29056
32141
  }
29057
32142
  return {
29058
32143
  command,
@@ -29060,10 +32145,16 @@ function parseArgs(argv) {
29060
32145
  ...collectorUrl !== void 0 ? { collectorUrl } : {},
29061
32146
  ...privacyTier !== void 0 ? { privacyTier } : {},
29062
32147
  ...installHooks !== void 0 ? { installHooks } : {},
29063
- ...forward ? { forward: true } : {}
32148
+ ...forward ? { forward: true } : {},
32149
+ ...teamUrl !== void 0 ? { teamUrl } : {},
32150
+ ...teamPrivacyTier !== void 0 ? { teamPrivacyTier } : {},
32151
+ ...teamToken !== void 0 ? { teamToken } : {}
29064
32152
  };
29065
32153
  }
29066
32154
 
32155
+ // packages/cli/src/init.ts
32156
+ var import_node_child_process = require("node:child_process");
32157
+
29067
32158
  // packages/cli/src/claude-hooks.ts
29068
32159
  var HOOK_EVENTS = [
29069
32160
  "SessionStart",
@@ -29115,12 +32206,22 @@ function parseConfig(raw) {
29115
32206
  if (typeof parsed["updatedAt"] !== "string" || parsed["updatedAt"].length === 0) {
29116
32207
  return void 0;
29117
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;
29118
32214
  return {
29119
32215
  version: "1.0",
29120
32216
  collectorUrl: parsed["collectorUrl"],
29121
32217
  privacyTier: parsed["privacyTier"],
29122
32218
  hookCommand: parsed["hookCommand"],
29123
- 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 } : {}
29124
32225
  };
29125
32226
  }
29126
32227
  function isRecord2(value) {
@@ -29348,6 +32449,17 @@ function deriveOtelLogsEndpoint(collectorUrl) {
29348
32449
  return "http://127.0.0.1:4717";
29349
32450
  }
29350
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
+ }
29351
32463
  function buildTelemetryEnv(collectorUrl, privacyTier) {
29352
32464
  const enablePromptAndToolDetail = privacyTier >= 2;
29353
32465
  const otelLogsEndpoint = deriveOtelLogsEndpoint(collectorUrl);
@@ -29364,12 +32476,19 @@ function buildTelemetryEnv(collectorUrl, privacyTier) {
29364
32476
  }
29365
32477
  function runInit(input, store = new FileCliConfigStore()) {
29366
32478
  const timestamp = nowIso(input.nowIso);
32479
+ const userEmail = readGitConfig("user.email");
32480
+ const userName = readGitConfig("user.name");
29367
32481
  const config = {
29368
32482
  version: "1.0",
29369
32483
  collectorUrl: input.collectorUrl ?? "http://127.0.0.1:8317/v1/hooks",
29370
32484
  privacyTier: ensurePrivacyTierOrDefault(input.privacyTier),
29371
32485
  hookCommand: "agent-trace hook-handler --forward",
29372
- 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 } : {}
29373
32492
  };
29374
32493
  const telemetryEnv = buildTelemetryEnv(config.collectorUrl, config.privacyTier);
29375
32494
  const hooks = buildClaudeHookConfig(config.hookCommand, timestamp);
@@ -29419,7 +32538,7 @@ function runStatus(configDir, store = new FileCliConfigStore()) {
29419
32538
 
29420
32539
  // packages/cli/src/hook-handler.ts
29421
32540
  var import_node_crypto4 = __toESM(require("node:crypto"));
29422
- var import_node_child_process = require("node:child_process");
32541
+ var import_node_child_process2 = require("node:child_process");
29423
32542
  var import_node_fs4 = __toESM(require("node:fs"));
29424
32543
  var import_node_path4 = __toESM(require("node:path"));
29425
32544
  function isIsoDate2(value) {
@@ -29852,7 +32971,7 @@ var FileHookSessionBaselineStore = class {
29852
32971
  };
29853
32972
  function runGitCommand(args, repositoryPath) {
29854
32973
  try {
29855
- const output = (0, import_node_child_process.execFileSync)("git", [...args], {
32974
+ const output = (0, import_node_child_process2.execFileSync)("git", [...args], {
29856
32975
  ...repositoryPath !== void 0 ? { cwd: repositoryPath } : {},
29857
32976
  encoding: "utf8",
29858
32977
  stdio: ["ignore", "pipe", "ignore"]
@@ -30080,9 +33199,23 @@ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvi
30080
33199
  }
30081
33200
  const now = input.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
30082
33201
  const privacyTier = getPrivacyTier(store, input.configDir);
33202
+ const config = store.readConfig(input.configDir);
30083
33203
  const baselineStore = new FileHookSessionBaselineStore(store, input.configDir);
30084
33204
  const enrichment = enrichHookPayloadWithGitContext(payload, gitContextProvider, baselineStore, now);
30085
- 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, {
30086
33219
  ...enrichment.enriched ? { git_enriched: "1" } : {},
30087
33220
  ...enrichment.usedSessionBaselineDelta ? { git_session_delta: "1" } : {}
30088
33221
  });
@@ -30098,14 +33231,64 @@ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvi
30098
33231
  envelope
30099
33232
  };
30100
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
+ }
30101
33282
  var FetchCollectorHttpClient = class {
30102
- async postJson(url, payload) {
33283
+ async postJson(url, payload, extraHeaders) {
30103
33284
  try {
33285
+ const headers = {
33286
+ "Content-Type": "application/json",
33287
+ ...extraHeaders
33288
+ };
30104
33289
  const response = await fetch(url, {
30105
33290
  method: "POST",
30106
- headers: {
30107
- "Content-Type": "application/json"
30108
- },
33291
+ headers,
30109
33292
  body: JSON.stringify(payload)
30110
33293
  });
30111
33294
  const body = await response.text();
@@ -30127,6 +33310,17 @@ var FetchCollectorHttpClient = class {
30127
33310
  function isSuccessStatus(statusCode) {
30128
33311
  return statusCode >= 200 && statusCode < 300;
30129
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
+ }
30130
33324
  async function runHookHandlerAndForward(input, client = new FetchCollectorHttpClient(), store = new FileCliConfigStore(), gitContextProvider = new ShellHookGitContextProvider()) {
30131
33325
  const hookResult = runHookHandler(
30132
33326
  {
@@ -30144,7 +33338,36 @@ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpCl
30144
33338
  };
30145
33339
  }
30146
33340
  const collectorUrl = getCollectorUrl(store, input.configDir, input.collectorUrl);
30147
- 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
+ }
30148
33371
  if (!postResult.ok) {
30149
33372
  return {
30150
33373
  ok: false,
@@ -30168,7 +33391,8 @@ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpCl
30168
33391
  envelope: hookResult.envelope,
30169
33392
  collectorUrl,
30170
33393
  statusCode: postResult.statusCode,
30171
- body: postResult.body
33394
+ body: postResult.body,
33395
+ ...teamForwardError !== void 0 ? { teamForwardError } : {}
30172
33396
  };
30173
33397
  }
30174
33398
 
@@ -30293,13 +33517,17 @@ async function startServer() {
30293
33517
  throw error;
30294
33518
  }
30295
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;
30296
33523
  let dashboardAddress;
30297
33524
  try {
30298
33525
  const dashboard = await startDashboardServer({
30299
33526
  host,
30300
33527
  port: dashboardPort,
30301
33528
  apiBaseUrl,
30302
- startedAtMs
33529
+ startedAtMs,
33530
+ ...currentUserEmail !== void 0 ? { currentUserEmail } : {}
30303
33531
  });
30304
33532
  dashboardAddress = dashboard.address;
30305
33533
  const originalClose = servers.close;