agent-trace 0.2.11 → 0.3.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 +848 -25
  2. package/package.json +1 -1
package/agent-trace.cjs CHANGED
@@ -24130,6 +24130,40 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24130
24130
  .pr-repo{color:var(--text-muted)}
24131
24131
  .pr-link{color:var(--text-dim);text-decoration:none;font-size:11px}
24132
24132
  .pr-link:hover{color:var(--cyan);text-decoration:underline}
24133
+ .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}
24134
+ .settings-btn:hover{border-color:var(--green);color:var(--green)}
24135
+ .settings-btn svg{width:14px;height:14px}
24136
+ .modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:1000;justify-content:center;align-items:center}
24137
+ .modal-overlay.open{display:flex}
24138
+ .modal{background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;padding:20px;width:420px;max-width:90vw;position:relative}
24139
+ .modal h3{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--text-primary)}
24140
+ .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}
24141
+ .modal-close:hover{color:var(--text-primary);background:var(--panel-hover)}
24142
+ .modal label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px;margin-top:12px}
24143
+ .modal select,.modal input{width:100%;box-sizing:border-box;padding:8px 10px;background:var(--bg);border:1px solid var(--panel-border);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:inherit}
24144
+ .modal select:focus,.modal input:focus{outline:none;border-color:var(--green)}
24145
+ .modal-actions{margin-top:16px;display:flex;gap:8px;align-items:center}
24146
+ .modal-save{padding:8px 16px;background:var(--green);color:#000;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
24147
+ .modal-save:hover{opacity:.9}
24148
+ .modal-save:disabled{opacity:.5;cursor:not-allowed}
24149
+ .modal-status{font-size:11px;color:var(--text-muted);flex:1}
24150
+ .modal-status.error{color:var(--red)}
24151
+ .modal-status.ok{color:var(--green)}
24152
+ .insight-panel{border:1px solid rgba(192,132,252,.2);border-radius:8px;padding:12px;margin-bottom:12px;background:rgba(192,132,252,.04)}
24153
+ .insight-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
24154
+ .insight-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--purple)}
24155
+ .insight-meta{font-size:10px;color:var(--text-dim)}
24156
+ .insight-summary{font-size:12px;color:var(--text-primary);line-height:1.6;margin-bottom:8px}
24157
+ .insight-section{margin-bottom:6px}
24158
+ .insight-section-title{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin-bottom:4px}
24159
+ .insight-item{font-size:12px;color:var(--text-muted);line-height:1.5;padding:2px 0 2px 12px;position:relative}
24160
+ .insight-item::before{content:'>';position:absolute;left:0;color:var(--purple)}
24161
+ .insight-cost{font-size:11px;color:var(--orange);margin-top:4px}
24162
+ .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}
24163
+ .insight-gen-btn:hover{background:rgba(192,132,252,.15)}
24164
+ .insight-gen-btn:disabled{opacity:.5;cursor:not-allowed}
24165
+ .insight-loading{font-size:12px;color:var(--text-muted);padding:8px 0}
24166
+ .insight-error{font-size:12px;color:var(--red);padding:8px 0}
24133
24167
  .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
24168
  .pcommit{display:flex;align-items:center;gap:8px;padding:2px 0;font-size:12px}
24135
24169
  .hljs{background:transparent!important;color:var(--text-primary)}
@@ -24152,11 +24186,34 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24152
24186
  </head>
24153
24187
  <body>
24154
24188
  <main class="shell">
24155
- <section class="hero">
24189
+ <section class="hero" style="position:relative">
24156
24190
  <h1>${title}</h1>
24157
24191
  <p>session observability for coding agents</p>
24192
+ <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>
24158
24193
  <div id="status" class="status-banner">Connecting...</div>
24159
24194
  </section>
24195
+ <div id="settings-modal" class="modal-overlay" onclick="if(event.target===this)closeSettings()">
24196
+ <div class="modal">
24197
+ <button class="modal-close" onclick="closeSettings()">&times;</button>
24198
+ <h3>AI Insights Settings</h3>
24199
+ <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>
24200
+ <label>Provider</label>
24201
+ <select id="cfg-provider">
24202
+ <option value="anthropic">Anthropic</option>
24203
+ <option value="openai">OpenAI</option>
24204
+ <option value="gemini">Gemini</option>
24205
+ <option value="openrouter">OpenRouter</option>
24206
+ </select>
24207
+ <label>API Key</label>
24208
+ <input type="password" id="cfg-apikey" placeholder="sk-..." autocomplete="off"/>
24209
+ <label>Model (optional)</label>
24210
+ <input type="text" id="cfg-model" placeholder="leave blank for default"/>
24211
+ <div class="modal-actions">
24212
+ <button class="modal-save" id="cfg-save" onclick="saveSettings()">Save</button>
24213
+ <span class="modal-status" id="cfg-status"></span>
24214
+ </div>
24215
+ </div>
24216
+ </div>
24160
24217
  <section class="mg">
24161
24218
  <article class="mc"><div class="label">Sessions</div><div class="val green" id="m-sessions">0</div></article>
24162
24219
  <article class="mc"><div class="label">Total Cost</div><div class="val orange" id="m-cost">$0.00</div></article>
@@ -24186,6 +24243,92 @@ var selectedId = null;
24186
24243
  var sessions = [];
24187
24244
  var costPoints = [];
24188
24245
  var replay = null;
24246
+ var insightsConfigured = false;
24247
+ var insightsCache = {};
24248
+
24249
+ window.openSettings = function() {
24250
+ document.getElementById('settings-modal').classList.add('open');
24251
+ fetch('/api/settings/insights',{cache:'no-store'}).then(function(r){return r.json();}).then(function(data){
24252
+ if(data && data.configured){
24253
+ document.getElementById('cfg-provider').value = data.provider || 'anthropic';
24254
+ if(data.model) document.getElementById('cfg-model').value = data.model;
24255
+ document.getElementById('cfg-status').className = 'modal-status ok';
24256
+ document.getElementById('cfg-status').textContent = 'Configured (' + (data.provider||'') + ')';
24257
+ insightsConfigured = true;
24258
+ }
24259
+ }).catch(function(){});
24260
+ };
24261
+
24262
+ window.closeSettings = function() {
24263
+ document.getElementById('settings-modal').classList.remove('open');
24264
+ };
24265
+
24266
+ window.saveSettings = function() {
24267
+ var btn = document.getElementById('cfg-save');
24268
+ var status = document.getElementById('cfg-status');
24269
+ btn.disabled = true;
24270
+ status.className = 'modal-status';
24271
+ status.textContent = 'Validating...';
24272
+ var body = {
24273
+ provider: document.getElementById('cfg-provider').value,
24274
+ apiKey: document.getElementById('cfg-apikey').value,
24275
+ model: document.getElementById('cfg-model').value || undefined
24276
+ };
24277
+ fetch('/api/settings/insights',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
24278
+ .then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d};});})
24279
+ .then(function(res){
24280
+ btn.disabled = false;
24281
+ if(res.ok && res.data.status === 'ok'){
24282
+ status.className = 'modal-status ok';
24283
+ status.textContent = 'Saved! (' + (res.data.provider||'') + ' / ' + (res.data.model||'default') + ')';
24284
+ insightsConfigured = true;
24285
+ document.getElementById('cfg-apikey').value = '';
24286
+ if(replay) renderReplay();
24287
+ } else {
24288
+ status.className = 'modal-status error';
24289
+ status.textContent = res.data.message || 'Save failed';
24290
+ }
24291
+ }).catch(function(e){
24292
+ btn.disabled = false;
24293
+ status.className = 'modal-status error';
24294
+ status.textContent = String(e);
24295
+ });
24296
+ };
24297
+
24298
+ window.generateInsight = function(sid) {
24299
+ var panel = document.getElementById('insight-panel');
24300
+ if(!panel) return;
24301
+ panel.innerHTML = '<div class="insight-loading">Generating insight...</div>';
24302
+ fetch('/api/session/' + encodeURIComponent(sid) + '/insights',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})
24303
+ .then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d};});})
24304
+ .then(function(res){
24305
+ if(res.ok && res.data.status === 'ok' && res.data.insight){
24306
+ insightsCache[sid] = res.data.insight;
24307
+ renderInsightContent(panel, res.data.insight);
24308
+ } else {
24309
+ panel.innerHTML = '<div class="insight-error">' + esc(res.data.message || 'Failed to generate insight') + '</div>';
24310
+ }
24311
+ }).catch(function(e){
24312
+ panel.innerHTML = '<div class="insight-error">' + esc(String(e)) + '</div>';
24313
+ });
24314
+ };
24315
+
24316
+ function renderInsightContent(panel, insight) {
24317
+ 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>';
24318
+ h += '<div class="insight-summary">' + esc(insight.summary) + '</div>';
24319
+ if(insight.highlights && insight.highlights.length > 0){
24320
+ h += '<div class="insight-section"><div class="insight-section-title">Highlights</div>';
24321
+ insight.highlights.forEach(function(item){ h += '<div class="insight-item">' + esc(item) + '</div>'; });
24322
+ h += '</div>';
24323
+ }
24324
+ if(insight.suggestions && insight.suggestions.length > 0){
24325
+ h += '<div class="insight-section"><div class="insight-section-title">Suggestions</div>';
24326
+ insight.suggestions.forEach(function(item){ h += '<div class="insight-item">' + esc(item) + '</div>'; });
24327
+ h += '</div>';
24328
+ }
24329
+ if(insight.costNote) h += '<div class="insight-cost">' + esc(insight.costNote) + '</div>';
24330
+ panel.innerHTML = h;
24331
+ }
24189
24332
 
24190
24333
  function esc(s) {
24191
24334
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(new RegExp(DQ,'g'),'&quot;');
@@ -24479,6 +24622,17 @@ function renderReplay() {
24479
24622
  }
24480
24623
  h += '</div>';
24481
24624
  }
24625
+ // AI insight panel
24626
+ h += '<div class="insight-panel" id="insight-panel">';
24627
+ var cachedInsight = insightsCache[replay.sessionId];
24628
+ if (cachedInsight) {
24629
+ // will be rendered after innerHTML set
24630
+ } else if (insightsConfigured) {
24631
+ 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>';
24632
+ } else {
24633
+ 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>';
24634
+ }
24635
+ h += '</div>';
24482
24636
  // prompt groups
24483
24637
  var groups = buildPromptGroups(replay.timeline, replay.commits);
24484
24638
  if (groups.length === 0) {
@@ -24487,6 +24641,14 @@ function renderReplay() {
24487
24641
  groups.forEach(function(g, i) { h += renderPromptCard(g, i + 1); });
24488
24642
  }
24489
24643
  area.innerHTML = h;
24644
+ if (cachedInsight) {
24645
+ var ip = document.getElementById('insight-panel');
24646
+ if (ip) renderInsightContent(ip, cachedInsight);
24647
+ }
24648
+ var genBtn = area.querySelector('.insight-gen-btn[data-sid]');
24649
+ if (genBtn) {
24650
+ genBtn.addEventListener('click', function() { generateInsight(genBtn.getAttribute('data-sid')); });
24651
+ }
24490
24652
  }
24491
24653
 
24492
24654
  function parseSummary(v) {
@@ -24555,6 +24717,9 @@ function loadSnapshot() {
24555
24717
  }
24556
24718
 
24557
24719
  function boot() {
24720
+ fetch('/api/settings/insights',{cache:'no-store'}).then(function(r){return r.json();}).then(function(data){
24721
+ if(data && data.configured) insightsConfigured = true;
24722
+ }).catch(function(){});
24558
24723
  loadSnapshot().then(function(){
24559
24724
  if (typeof EventSource !== 'undefined') {
24560
24725
  var es = new EventSource('/api/sessions/stream');
@@ -24881,6 +25046,40 @@ async function startDashboardServer(options = {}) {
24881
25046
  const pathname = parsePathname(url);
24882
25047
  const segments = parsePathSegments(pathname);
24883
25048
  const method = req.method ?? "GET";
25049
+ if (method === "POST" && (pathname === "/api/settings/insights" || segments.length === 4 && segments[0] === "api" && segments[1] === "session" && segments[3] === "insights")) {
25050
+ let body = "";
25051
+ req.setEncoding("utf8");
25052
+ req.on("data", (chunk) => {
25053
+ body += chunk;
25054
+ });
25055
+ req.on("end", () => {
25056
+ let parsedBody;
25057
+ try {
25058
+ parsedBody = body.length > 0 ? JSON.parse(body) : {};
25059
+ } catch {
25060
+ sendJson(res, 400, { status: "error", message: "invalid JSON body" });
25061
+ return;
25062
+ }
25063
+ let apiPath;
25064
+ if (pathname === "/api/settings/insights") {
25065
+ apiPath = "/v1/settings/insights";
25066
+ } else {
25067
+ const sessionId = segments[2];
25068
+ apiPath = `/v1/sessions/${encodeURIComponent(sessionId ?? "")}/insights`;
25069
+ }
25070
+ void fetch(`${apiBaseUrl}${apiPath}`, {
25071
+ method: "POST",
25072
+ headers: { "Content-Type": "application/json" },
25073
+ body: JSON.stringify(parsedBody)
25074
+ }).then(async (apiResponse) => {
25075
+ const payload = await apiResponse.json();
25076
+ sendJson(res, apiResponse.status, payload);
25077
+ }).catch((error) => {
25078
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
25079
+ });
25080
+ });
25081
+ return;
25082
+ }
24884
25083
  if (method !== "GET") {
24885
25084
  sendJson(res, 405, {
24886
25085
  status: "error",
@@ -24972,6 +25171,15 @@ async function startDashboardServer(options = {}) {
24972
25171
  });
24973
25172
  return;
24974
25173
  }
25174
+ if (pathname === "/api/settings/insights") {
25175
+ void fetch(`${apiBaseUrl}/v1/settings/insights`).then(async (apiResponse) => {
25176
+ const payload = await apiResponse.json();
25177
+ sendJson(res, apiResponse.status, payload);
25178
+ }).catch((error) => {
25179
+ sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
25180
+ });
25181
+ return;
25182
+ }
24975
25183
  if (pathname === "/") {
24976
25184
  sendHtml(res, 200, renderDashboardHtml());
24977
25185
  return;
@@ -25089,6 +25297,12 @@ CREATE TABLE IF NOT EXISTS pull_requests (
25089
25297
  );
25090
25298
 
25091
25299
  CREATE INDEX IF NOT EXISTS idx_pull_requests_session ON pull_requests(session_id);
25300
+
25301
+ CREATE TABLE IF NOT EXISTS instance_settings (
25302
+ key TEXT PRIMARY KEY,
25303
+ value TEXT NOT NULL,
25304
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
25305
+ );
25092
25306
  `;
25093
25307
  function toJsonArray(value) {
25094
25308
  return JSON.stringify(value);
@@ -25382,6 +25596,21 @@ var SqliteClient = class {
25382
25596
  sessionCount: Number(raw["sessions_count"] ?? 0)
25383
25597
  }));
25384
25598
  }
25599
+ getSetting(key) {
25600
+ const row = this.db.prepare(
25601
+ "SELECT value FROM instance_settings WHERE key = ?"
25602
+ ).get(key);
25603
+ return row?.value;
25604
+ }
25605
+ upsertSetting(key, value) {
25606
+ this.db.prepare(`
25607
+ INSERT INTO instance_settings (key, value, updated_at)
25608
+ VALUES (?, ?, datetime('now'))
25609
+ ON CONFLICT(key) DO UPDATE SET
25610
+ value = excluded.value,
25611
+ updated_at = excluded.updated_at
25612
+ `).run(key, value);
25613
+ }
25385
25614
  close() {
25386
25615
  this.db.close();
25387
25616
  }
@@ -26432,6 +26661,505 @@ function calculateCostUsd(input) {
26432
26661
  // packages/runtime/src/runtime.ts
26433
26662
  var import_node_http2 = __toESM(require("node:http"));
26434
26663
 
26664
+ // packages/api/src/insights-provider.ts
26665
+ var DEFAULT_MODELS = {
26666
+ anthropic: "claude-sonnet-4-20250514",
26667
+ openai: "gpt-4o-mini",
26668
+ gemini: "gemini-2.0-flash",
26669
+ openrouter: "anthropic/claude-sonnet-4"
26670
+ };
26671
+ function extractTextContent(body) {
26672
+ if (typeof body !== "object" || body === null) return "";
26673
+ const record = body;
26674
+ if (Array.isArray(record["content"])) {
26675
+ for (const block of record["content"]) {
26676
+ if (typeof block === "object" && block !== null) {
26677
+ const b = block;
26678
+ if (b["type"] === "text" && typeof b["text"] === "string") return b["text"];
26679
+ }
26680
+ }
26681
+ }
26682
+ if (Array.isArray(record["choices"])) {
26683
+ const first = record["choices"][0];
26684
+ if (typeof first === "object" && first !== null) {
26685
+ const choice = first;
26686
+ const msg = choice["message"];
26687
+ if (typeof msg === "object" && msg !== null) {
26688
+ const m = msg;
26689
+ if (typeof m["content"] === "string") return m["content"];
26690
+ }
26691
+ }
26692
+ }
26693
+ if (Array.isArray(record["candidates"])) {
26694
+ const first = record["candidates"][0];
26695
+ if (typeof first === "object" && first !== null) {
26696
+ const candidate = first;
26697
+ const content = candidate["content"];
26698
+ if (typeof content === "object" && content !== null) {
26699
+ const c = content;
26700
+ if (Array.isArray(c["parts"])) {
26701
+ for (const part of c["parts"]) {
26702
+ if (typeof part === "object" && part !== null) {
26703
+ const p = part;
26704
+ if (typeof p["text"] === "string") return p["text"];
26705
+ }
26706
+ }
26707
+ }
26708
+ }
26709
+ }
26710
+ }
26711
+ return "";
26712
+ }
26713
+ function createAnthropicProvider(apiKey, model) {
26714
+ const endpoint = "https://api.anthropic.com/v1/messages";
26715
+ return {
26716
+ provider: "anthropic",
26717
+ model,
26718
+ async complete(system, user) {
26719
+ const response = await fetch(endpoint, {
26720
+ method: "POST",
26721
+ headers: {
26722
+ "Content-Type": "application/json",
26723
+ "x-api-key": apiKey,
26724
+ "anthropic-version": "2023-06-01"
26725
+ },
26726
+ body: JSON.stringify({
26727
+ model,
26728
+ max_tokens: 1024,
26729
+ system,
26730
+ messages: [{ role: "user", content: user }]
26731
+ })
26732
+ });
26733
+ if (!response.ok) {
26734
+ throw new Error(`anthropic api returned ${String(response.status)}`);
26735
+ }
26736
+ const body = await response.json();
26737
+ return extractTextContent(body);
26738
+ },
26739
+ async validate() {
26740
+ try {
26741
+ const response = await fetch(endpoint, {
26742
+ method: "POST",
26743
+ headers: {
26744
+ "Content-Type": "application/json",
26745
+ "x-api-key": apiKey,
26746
+ "anthropic-version": "2023-06-01"
26747
+ },
26748
+ body: JSON.stringify({
26749
+ model,
26750
+ max_tokens: 1,
26751
+ messages: [{ role: "user", content: "hi" }]
26752
+ })
26753
+ });
26754
+ return response.ok || response.status === 400;
26755
+ } catch {
26756
+ return false;
26757
+ }
26758
+ }
26759
+ };
26760
+ }
26761
+ function createOpenAiProvider(apiKey, model) {
26762
+ const endpoint = "https://api.openai.com/v1/chat/completions";
26763
+ return {
26764
+ provider: "openai",
26765
+ model,
26766
+ async complete(system, user) {
26767
+ const response = await fetch(endpoint, {
26768
+ method: "POST",
26769
+ headers: {
26770
+ "Content-Type": "application/json",
26771
+ "Authorization": `Bearer ${apiKey}`
26772
+ },
26773
+ body: JSON.stringify({
26774
+ model,
26775
+ max_tokens: 1024,
26776
+ messages: [
26777
+ { role: "system", content: system },
26778
+ { role: "user", content: user }
26779
+ ]
26780
+ })
26781
+ });
26782
+ if (!response.ok) {
26783
+ throw new Error(`openai api returned ${String(response.status)}`);
26784
+ }
26785
+ const body = await response.json();
26786
+ return extractTextContent(body);
26787
+ },
26788
+ async validate() {
26789
+ try {
26790
+ const response = await fetch(endpoint, {
26791
+ method: "POST",
26792
+ headers: {
26793
+ "Content-Type": "application/json",
26794
+ "Authorization": `Bearer ${apiKey}`
26795
+ },
26796
+ body: JSON.stringify({
26797
+ model,
26798
+ max_tokens: 1,
26799
+ messages: [{ role: "user", content: "hi" }]
26800
+ })
26801
+ });
26802
+ return response.ok;
26803
+ } catch {
26804
+ return false;
26805
+ }
26806
+ }
26807
+ };
26808
+ }
26809
+ function createGeminiProvider(apiKey, model) {
26810
+ const baseUrl = "https://generativelanguage.googleapis.com/v1beta";
26811
+ return {
26812
+ provider: "gemini",
26813
+ model,
26814
+ async complete(system, user) {
26815
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
26816
+ const response = await fetch(url, {
26817
+ method: "POST",
26818
+ headers: { "Content-Type": "application/json" },
26819
+ body: JSON.stringify({
26820
+ systemInstruction: { parts: [{ text: system }] },
26821
+ contents: [{ parts: [{ text: user }] }],
26822
+ generationConfig: { maxOutputTokens: 1024 }
26823
+ })
26824
+ });
26825
+ if (!response.ok) {
26826
+ throw new Error(`gemini api returned ${String(response.status)}`);
26827
+ }
26828
+ const body = await response.json();
26829
+ return extractTextContent(body);
26830
+ },
26831
+ async validate() {
26832
+ try {
26833
+ const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
26834
+ const response = await fetch(url, {
26835
+ method: "POST",
26836
+ headers: { "Content-Type": "application/json" },
26837
+ body: JSON.stringify({
26838
+ contents: [{ parts: [{ text: "hi" }] }],
26839
+ generationConfig: { maxOutputTokens: 1 }
26840
+ })
26841
+ });
26842
+ return response.ok;
26843
+ } catch {
26844
+ return false;
26845
+ }
26846
+ }
26847
+ };
26848
+ }
26849
+ function createOpenRouterProvider(apiKey, model) {
26850
+ const endpoint = "https://openrouter.ai/api/v1/chat/completions";
26851
+ return {
26852
+ provider: "openrouter",
26853
+ model,
26854
+ async complete(system, user) {
26855
+ const response = await fetch(endpoint, {
26856
+ method: "POST",
26857
+ headers: {
26858
+ "Content-Type": "application/json",
26859
+ "Authorization": `Bearer ${apiKey}`
26860
+ },
26861
+ body: JSON.stringify({
26862
+ model,
26863
+ max_tokens: 1024,
26864
+ messages: [
26865
+ { role: "system", content: system },
26866
+ { role: "user", content: user }
26867
+ ]
26868
+ })
26869
+ });
26870
+ if (!response.ok) {
26871
+ throw new Error(`openrouter api returned ${String(response.status)}`);
26872
+ }
26873
+ const body = await response.json();
26874
+ return extractTextContent(body);
26875
+ },
26876
+ async validate() {
26877
+ try {
26878
+ const response = await fetch(endpoint, {
26879
+ method: "POST",
26880
+ headers: {
26881
+ "Content-Type": "application/json",
26882
+ "Authorization": `Bearer ${apiKey}`
26883
+ },
26884
+ body: JSON.stringify({
26885
+ model,
26886
+ max_tokens: 1,
26887
+ messages: [{ role: "user", content: "hi" }]
26888
+ })
26889
+ });
26890
+ return response.ok;
26891
+ } catch {
26892
+ return false;
26893
+ }
26894
+ }
26895
+ };
26896
+ }
26897
+ function createLlmProvider(config) {
26898
+ const model = config.model ?? DEFAULT_MODELS[config.provider];
26899
+ switch (config.provider) {
26900
+ case "anthropic":
26901
+ return createAnthropicProvider(config.apiKey, model);
26902
+ case "openai":
26903
+ return createOpenAiProvider(config.apiKey, model);
26904
+ case "gemini":
26905
+ return createGeminiProvider(config.apiKey, model);
26906
+ case "openrouter":
26907
+ return createOpenRouterProvider(config.apiKey, model);
26908
+ }
26909
+ }
26910
+
26911
+ // packages/api/src/insights-generator.ts
26912
+ var SYSTEM_PROMPT = `You are an AI coding session analyst. You analyze telemetry from AI coding agent sessions and produce structured insights.
26913
+
26914
+ Return ONLY valid JSON with this exact schema:
26915
+ {
26916
+ "summary": "2-3 sentence overview of what the session accomplished",
26917
+ "highlights": ["1-3 notable observations about the session"],
26918
+ "suggestions": ["0-3 actionable suggestions for improving efficiency"],
26919
+ "costNote": "optional one-line note about cost efficiency, or null"
26920
+ }
26921
+
26922
+ Guidelines:
26923
+ - summary: Describe what the agent accomplished concisely. Mention key outcomes (files changed, commits, PRs).
26924
+ - highlights: Focus on interesting patterns \u2014 heavy tool usage, large diffs, cache efficiency, model choices.
26925
+ - suggestions: Only suggest things that are actionable. If the session looks efficient, return an empty array.
26926
+ - costNote: Comment on cost relative to output if noteworthy. Set to null if unremarkable.
26927
+ - Be concise and specific. Reference actual numbers from the data.`;
26928
+ function condensedTimeline(trace, maxChars) {
26929
+ const lines = [];
26930
+ for (const event of trace.timeline) {
26931
+ const parts = [event.type];
26932
+ if (event.details !== void 0) {
26933
+ const d = event.details;
26934
+ const toolName = d["toolName"] ?? d["tool_name"];
26935
+ if (typeof toolName === "string") parts.push(toolName);
26936
+ const toolInput = d["toolInput"] ?? d["tool_input"];
26937
+ if (typeof toolInput === "object" && toolInput !== null) {
26938
+ const ti = toolInput;
26939
+ const fp = ti["file_path"] ?? ti["filePath"];
26940
+ if (typeof fp === "string") parts.push(fp);
26941
+ const cmd = ti["command"];
26942
+ if (typeof cmd === "string") parts.push(cmd.slice(0, 80));
26943
+ }
26944
+ }
26945
+ if (event.costUsd !== void 0 && event.costUsd > 0) {
26946
+ parts.push(`$${event.costUsd.toFixed(4)}`);
26947
+ }
26948
+ const line = parts.join(" | ");
26949
+ lines.push(line);
26950
+ const totalLength = lines.reduce((sum, l) => sum + l.length + 1, 0);
26951
+ if (totalLength > maxChars) break;
26952
+ }
26953
+ return lines.join("\n");
26954
+ }
26955
+ function buildInsightPrompt(trace) {
26956
+ const m = trace.metrics;
26957
+ const sections = [];
26958
+ sections.push(`## Session: ${trace.sessionId}`);
26959
+ sections.push(`Started: ${trace.startedAt}${trace.endedAt !== void 0 ? ` | Ended: ${trace.endedAt}` : ""}`);
26960
+ if (trace.environment.gitRepo !== void 0) {
26961
+ sections.push(`Repo: ${trace.environment.gitRepo}${trace.environment.gitBranch !== void 0 ? ` (${trace.environment.gitBranch})` : ""}`);
26962
+ }
26963
+ sections.push(`
26964
+ ## Metrics`);
26965
+ sections.push(`Prompts: ${String(m.promptCount)} | Tool calls: ${String(m.toolCallCount)} | API calls: ${String(m.apiCallCount)}`);
26966
+ sections.push(`Cost: $${m.totalCostUsd.toFixed(4)}`);
26967
+ sections.push(`Tokens: ${String(m.totalInputTokens)} in / ${String(m.totalOutputTokens)} out | Cache: ${String(m.totalCacheReadTokens)} read / ${String(m.totalCacheWriteTokens)} write`);
26968
+ sections.push(`Lines: +${String(m.linesAdded)} / -${String(m.linesRemoved)} | Files: ${String(m.filesTouched.length)}`);
26969
+ if (m.modelsUsed.length > 0) sections.push(`Models: ${m.modelsUsed.join(", ")}`);
26970
+ if (m.toolsUsed.length > 0) sections.push(`Tools: ${m.toolsUsed.join(", ")}`);
26971
+ if (trace.git.commits.length > 0) {
26972
+ sections.push(`
26973
+ ## Commits (${String(trace.git.commits.length)})`);
26974
+ trace.git.commits.forEach((c) => {
26975
+ sections.push(`- ${c.sha.slice(0, 7)}: ${c.message ?? "no message"}`);
26976
+ });
26977
+ }
26978
+ if (trace.git.pullRequests.length > 0) {
26979
+ sections.push(`
26980
+ ## Pull Requests (${String(trace.git.pullRequests.length)})`);
26981
+ trace.git.pullRequests.forEach((pr) => {
26982
+ sections.push(`- PR #${String(pr.prNumber)} (${pr.state}) in ${pr.repo}`);
26983
+ });
26984
+ }
26985
+ sections.push(`
26986
+ ## Timeline (condensed)`);
26987
+ sections.push(condensedTimeline(trace, 4e3));
26988
+ return sections.join("\n");
26989
+ }
26990
+ function parseInsightJson(raw) {
26991
+ let jsonStr = raw.trim();
26992
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
26993
+ if (fenceMatch !== null && fenceMatch[1] !== void 0) {
26994
+ jsonStr = fenceMatch[1].trim();
26995
+ }
26996
+ try {
26997
+ const parsed = JSON.parse(jsonStr);
26998
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
26999
+ const obj = parsed;
27000
+ const summary = typeof obj["summary"] === "string" ? obj["summary"] : "";
27001
+ if (summary.length === 0) return void 0;
27002
+ const highlights = Array.isArray(obj["highlights"]) ? obj["highlights"].filter((h) => typeof h === "string") : [];
27003
+ const suggestions = Array.isArray(obj["suggestions"]) ? obj["suggestions"].filter((s) => typeof s === "string") : [];
27004
+ const costNote = typeof obj["costNote"] === "string" && obj["costNote"].length > 0 ? obj["costNote"] : void 0;
27005
+ return { summary, highlights, suggestions, costNote };
27006
+ } catch {
27007
+ return void 0;
27008
+ }
27009
+ }
27010
+ async function generateSessionInsight(trace, provider) {
27011
+ const userPrompt = buildInsightPrompt(trace);
27012
+ const rawResponse = await provider.complete(SYSTEM_PROMPT, userPrompt);
27013
+ const parsed = parseInsightJson(rawResponse);
27014
+ if (parsed === void 0) {
27015
+ return {
27016
+ sessionId: trace.sessionId,
27017
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
27018
+ provider: provider.provider,
27019
+ model: provider.model,
27020
+ summary: rawResponse.slice(0, 500) || "Failed to generate structured insight.",
27021
+ highlights: [],
27022
+ suggestions: []
27023
+ };
27024
+ }
27025
+ const result = {
27026
+ sessionId: trace.sessionId,
27027
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
27028
+ provider: provider.provider,
27029
+ model: provider.model,
27030
+ summary: parsed.summary,
27031
+ highlights: parsed.highlights,
27032
+ suggestions: parsed.suggestions
27033
+ };
27034
+ if (parsed.costNote !== void 0) {
27035
+ return { ...result, costNote: parsed.costNote };
27036
+ }
27037
+ return result;
27038
+ }
27039
+
27040
+ // packages/api/src/insights-handler.ts
27041
+ var insightsCache = /* @__PURE__ */ new Map();
27042
+ var VALID_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
27043
+ function isValidProvider(value) {
27044
+ return typeof value === "string" && VALID_PROVIDERS.includes(value);
27045
+ }
27046
+ function handleGetInsightsSettings(dependencies) {
27047
+ const accessor = dependencies.insightsConfigAccessor;
27048
+ if (accessor === void 0) {
27049
+ return {
27050
+ statusCode: 200,
27051
+ payload: { status: "ok", configured: false }
27052
+ };
27053
+ }
27054
+ const config = accessor.getConfig();
27055
+ if (config === void 0) {
27056
+ return {
27057
+ statusCode: 200,
27058
+ payload: { status: "ok", configured: false }
27059
+ };
27060
+ }
27061
+ return {
27062
+ statusCode: 200,
27063
+ payload: {
27064
+ status: "ok",
27065
+ configured: true,
27066
+ provider: config.provider,
27067
+ ...config.model !== void 0 ? { model: config.model } : {}
27068
+ }
27069
+ };
27070
+ }
27071
+ async function handlePostInsightsSettings(body, dependencies) {
27072
+ const accessor = dependencies.insightsConfigAccessor;
27073
+ if (accessor === void 0) {
27074
+ return {
27075
+ statusCode: 500,
27076
+ payload: { status: "error", message: "insights settings not available" }
27077
+ };
27078
+ }
27079
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
27080
+ return {
27081
+ statusCode: 400,
27082
+ payload: { status: "error", message: "request body must be a JSON object" }
27083
+ };
27084
+ }
27085
+ const record = body;
27086
+ const provider = record["provider"];
27087
+ const apiKey = record["apiKey"];
27088
+ const model = record["model"];
27089
+ if (!isValidProvider(provider)) {
27090
+ return {
27091
+ statusCode: 400,
27092
+ payload: { status: "error", message: "invalid provider. must be one of: anthropic, openai, gemini, openrouter" }
27093
+ };
27094
+ }
27095
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
27096
+ return {
27097
+ statusCode: 400,
27098
+ payload: { status: "error", message: "apiKey is required" }
27099
+ };
27100
+ }
27101
+ const config = {
27102
+ provider,
27103
+ apiKey,
27104
+ ...typeof model === "string" && model.length > 0 ? { model } : {}
27105
+ };
27106
+ const llm = createLlmProvider(config);
27107
+ const valid = await llm.validate();
27108
+ if (!valid) {
27109
+ return {
27110
+ statusCode: 400,
27111
+ payload: { status: "error", message: "API key validation failed. Check your key and try again." }
27112
+ };
27113
+ }
27114
+ accessor.setConfig(config);
27115
+ return {
27116
+ statusCode: 200,
27117
+ payload: {
27118
+ status: "ok",
27119
+ message: "insights configuration saved",
27120
+ provider: config.provider,
27121
+ model: llm.model
27122
+ }
27123
+ };
27124
+ }
27125
+ async function handlePostSessionInsight(sessionId, dependencies) {
27126
+ const accessor = dependencies.insightsConfigAccessor;
27127
+ if (accessor === void 0) {
27128
+ return {
27129
+ statusCode: 400,
27130
+ payload: { status: "error", message: "insights not configured" }
27131
+ };
27132
+ }
27133
+ const config = accessor.getConfig();
27134
+ if (config === void 0) {
27135
+ return {
27136
+ statusCode: 400,
27137
+ payload: { status: "error", message: "no AI provider configured. open settings to add your API key." }
27138
+ };
27139
+ }
27140
+ const cached = insightsCache.get(sessionId);
27141
+ if (cached !== void 0) {
27142
+ return {
27143
+ statusCode: 200,
27144
+ payload: { status: "ok", insight: cached }
27145
+ };
27146
+ }
27147
+ const trace = dependencies.repository.getBySessionId(sessionId);
27148
+ if (trace === void 0) {
27149
+ return {
27150
+ statusCode: 404,
27151
+ payload: { status: "error", message: "session not found" }
27152
+ };
27153
+ }
27154
+ const provider = createLlmProvider(config);
27155
+ const insight = await generateSessionInsight(trace, provider);
27156
+ insightsCache.set(sessionId, insight);
27157
+ return {
27158
+ statusCode: 200,
27159
+ payload: { status: "ok", insight }
27160
+ };
27161
+ }
27162
+
26435
27163
  // packages/api/src/mapper.ts
26436
27164
  function toSessionSummary(trace) {
26437
27165
  return {
@@ -26550,6 +27278,16 @@ async function handleApiRequest(request, dependencies) {
26550
27278
  payload: await buildDailyCostResponse(dependencies, filters)
26551
27279
  };
26552
27280
  }
27281
+ if (request.method === "GET" && pathname === "/v1/settings/insights") {
27282
+ return handleGetInsightsSettings(dependencies);
27283
+ }
27284
+ if (request.method === "POST" && pathname === "/v1/settings/insights") {
27285
+ return handlePostInsightsSettings(request.body, dependencies);
27286
+ }
27287
+ const insightsMatch = pathname.match(/^\/v1\/sessions\/([^/]+)\/insights$/);
27288
+ if (request.method === "POST" && insightsMatch !== null && insightsMatch[1] !== void 0) {
27289
+ return handlePostSessionInsight(decodeURIComponent(insightsMatch[1]), dependencies);
27290
+ }
26553
27291
  if (request.method === "GET") {
26554
27292
  const segments = parseSessionPath(pathname);
26555
27293
  if (segments.length >= 3 && segments[0] === "v1" && segments[1] === "sessions") {
@@ -26597,7 +27335,7 @@ async function handleApiRequest(request, dependencies) {
26597
27335
 
26598
27336
  // packages/api/src/http.ts
26599
27337
  function normalizeMethod(method) {
26600
- if (method === "GET") {
27338
+ if (method === "GET" || method === "POST") {
26601
27339
  return method;
26602
27340
  }
26603
27341
  return void 0;
@@ -26664,11 +27402,33 @@ async function handleApiRawHttpRequest(request, dependencies) {
26664
27402
  return handleApiRequest(
26665
27403
  {
26666
27404
  method,
26667
- url: request.url
27405
+ url: request.url,
27406
+ ...request.body !== void 0 ? { body: request.body } : {}
26668
27407
  },
26669
27408
  dependencies
26670
27409
  );
26671
27410
  }
27411
+ function readRequestBody(req) {
27412
+ return new Promise((resolve, reject) => {
27413
+ let data = "";
27414
+ req.setEncoding("utf8");
27415
+ req.on("data", (chunk) => {
27416
+ data += chunk;
27417
+ });
27418
+ req.on("end", () => {
27419
+ if (data.length === 0) {
27420
+ resolve(void 0);
27421
+ return;
27422
+ }
27423
+ try {
27424
+ resolve(JSON.parse(data));
27425
+ } catch {
27426
+ reject(new Error("invalid JSON body"));
27427
+ }
27428
+ });
27429
+ req.on("error", (error) => reject(error));
27430
+ });
27431
+ }
26672
27432
  function createApiHttpHandler(dependencies) {
26673
27433
  return (req, res) => {
26674
27434
  const method = req.method ?? "GET";
@@ -26677,17 +27437,29 @@ function createApiHttpHandler(dependencies) {
26677
27437
  startSessionsSseStream(req, res, dependencies);
26678
27438
  return;
26679
27439
  }
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
- });
27440
+ const dispatch = (body) => {
27441
+ void handleApiRawHttpRequest(
27442
+ {
27443
+ method,
27444
+ url,
27445
+ ...body !== void 0 ? { body } : {}
27446
+ },
27447
+ dependencies
27448
+ ).then((response) => {
27449
+ sendJson2(res, response.statusCode, response.payload);
27450
+ }).catch(() => {
27451
+ sendJson2(res, 500, { status: "error", message: "internal server error" });
27452
+ });
27453
+ };
27454
+ if (method === "POST") {
27455
+ readRequestBody(req).then((body) => {
27456
+ dispatch(body);
27457
+ }).catch(() => {
27458
+ sendJson2(res, 400, { status: "error", message: "invalid request body" });
27459
+ });
27460
+ return;
27461
+ }
27462
+ dispatch();
26691
27463
  };
26692
27464
  }
26693
27465
 
@@ -27014,7 +27786,7 @@ function handleCollectorRawHttpRequest(request, dependencies) {
27014
27786
  dependencies
27015
27787
  );
27016
27788
  }
27017
- async function readRequestBody(req) {
27789
+ async function readRequestBody2(req) {
27018
27790
  return new Promise((resolve, reject) => {
27019
27791
  const chunks = [];
27020
27792
  req.on("data", (chunk) => chunks.push(chunk));
@@ -27032,7 +27804,7 @@ function createCollectorHttpHandler(dependencies) {
27032
27804
  return async (req, res) => {
27033
27805
  const method = req.method ?? "GET";
27034
27806
  const url = req.url ?? "/";
27035
- const rawBody = method === "POST" ? await readRequestBody(req) : void 0;
27807
+ const rawBody = method === "POST" ? await readRequestBody2(req) : void 0;
27036
27808
  const response = handleCollectorRawHttpRequest(
27037
27809
  {
27038
27810
  method,
@@ -28740,7 +29512,8 @@ function resolveRuntimeOptions(input) {
28740
29512
  return {
28741
29513
  startedAtMs: input,
28742
29514
  persistence: new InMemoryRuntimePersistence(),
28743
- dailyCostReader: void 0
29515
+ dailyCostReader: void 0,
29516
+ insightsConfigAccessor: void 0
28744
29517
  };
28745
29518
  }
28746
29519
  const startedAtMs = input?.startedAtMs ?? Date.now();
@@ -28748,7 +29521,8 @@ function resolveRuntimeOptions(input) {
28748
29521
  return {
28749
29522
  startedAtMs,
28750
29523
  persistence,
28751
- dailyCostReader: input?.dailyCostReader
29524
+ dailyCostReader: input?.dailyCostReader,
29525
+ insightsConfigAccessor: input?.insightsConfigAccessor
28752
29526
  };
28753
29527
  }
28754
29528
  function createInMemoryRuntime(input) {
@@ -28766,7 +29540,8 @@ function createInMemoryRuntime(input) {
28766
29540
  const apiDependencies = {
28767
29541
  startedAtMs: options.startedAtMs,
28768
29542
  repository: sessionRepository,
28769
- ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {}
29543
+ ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
29544
+ ...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {}
28770
29545
  };
28771
29546
  return {
28772
29547
  sessionRepository,
@@ -28775,6 +29550,7 @@ function createInMemoryRuntime(input) {
28775
29550
  collectorService,
28776
29551
  persistence,
28777
29552
  ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
29553
+ ...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
28778
29554
  handleCollectorRaw: collectorService.handleRaw,
28779
29555
  handleApiRaw: (request) => handleApiRawHttpRequest(request, apiDependencies)
28780
29556
  };
@@ -28795,7 +29571,8 @@ async function startInMemoryRuntimeServers(runtime, options = {}) {
28795
29571
  createApiHttpHandler({
28796
29572
  startedAtMs: runtime.collectorDependencies.startedAtMs,
28797
29573
  repository: runtime.sessionRepository,
28798
- ...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {}
29574
+ ...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {},
29575
+ ...runtime.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: runtime.insightsConfigAccessor } : {}
28799
29576
  })
28800
29577
  ) : void 0;
28801
29578
  let otelReceiver;
@@ -28964,14 +29741,46 @@ function hydrateFromSqlite(runtime, sqlite, limit, eventLimit) {
28964
29741
  }
28965
29742
  return traces.length;
28966
29743
  }
29744
+ var VALID_INSIGHTS_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
29745
+ function createSqliteInsightsConfigAccessor(sqlite) {
29746
+ let cached;
29747
+ const raw = sqlite.getSetting("insights_config");
29748
+ if (raw !== void 0) {
29749
+ try {
29750
+ const parsed = JSON.parse(raw);
29751
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
29752
+ const obj = parsed;
29753
+ if (typeof obj["provider"] === "string" && VALID_INSIGHTS_PROVIDERS.includes(obj["provider"]) && typeof obj["apiKey"] === "string") {
29754
+ cached = {
29755
+ provider: obj["provider"],
29756
+ apiKey: obj["apiKey"],
29757
+ ...typeof obj["model"] === "string" && obj["model"].length > 0 ? { model: obj["model"] } : {}
29758
+ };
29759
+ }
29760
+ }
29761
+ } catch {
29762
+ }
29763
+ }
29764
+ return {
29765
+ getConfig() {
29766
+ return cached;
29767
+ },
29768
+ setConfig(config) {
29769
+ cached = config;
29770
+ sqlite.upsertSetting("insights_config", JSON.stringify(config));
29771
+ }
29772
+ };
29773
+ }
28967
29774
  function createSqliteBackedRuntime(options) {
28968
29775
  const sqlite = new SqliteClient(options.dbPath);
28969
29776
  const persistence = new SqlitePersistence(sqlite);
28970
29777
  const dailyCostReader = new SqliteDailyCostReader(sqlite);
29778
+ const insightsConfigAccessor = options.insightsConfigAccessor ?? createSqliteInsightsConfigAccessor(sqlite);
28971
29779
  const runtime = createInMemoryRuntime({
28972
29780
  ...options.startedAtMs !== void 0 ? { startedAtMs: options.startedAtMs } : {},
28973
29781
  persistence,
28974
- dailyCostReader
29782
+ dailyCostReader,
29783
+ insightsConfigAccessor
28975
29784
  });
28976
29785
  const hydratedCount = hydrateFromSqlite(runtime, sqlite, options.bootstrapLimit, options.eventLimit);
28977
29786
  const syncIntervalMs = options.syncIntervalMs ?? 5e3;
@@ -29525,9 +30334,23 @@ function parseCommitMessage2(command) {
29525
30334
  }
29526
30335
  return message;
29527
30336
  }
29528
- function extractPrUrl(payload) {
30337
+ function pickToolOutput(payload) {
29529
30338
  const record = payload;
29530
- const output = readString5(record, "tool_response") ?? readString5(record, "toolResponse") ?? readString5(record, "stdout") ?? readString5(record, "output");
30339
+ const toolResponse = record["tool_response"] ?? record["toolResponse"];
30340
+ if (typeof toolResponse === "string" && toolResponse.length > 0) {
30341
+ return toolResponse;
30342
+ }
30343
+ if (typeof toolResponse === "object" && toolResponse !== null && !Array.isArray(toolResponse)) {
30344
+ const nested = toolResponse;
30345
+ const stdout = nested["stdout"];
30346
+ if (typeof stdout === "string" && stdout.length > 0) {
30347
+ return stdout;
30348
+ }
30349
+ }
30350
+ return readString5(record, "stdout") ?? readString5(record, "output");
30351
+ }
30352
+ function extractPrUrl(payload) {
30353
+ const output = pickToolOutput(payload);
29531
30354
  const command = pickCommand2(payload);
29532
30355
  const combined = [command, output].filter((s) => s !== void 0).join("\n");
29533
30356
  if (combined.length === 0) return void 0;
@@ -29539,7 +30362,7 @@ function extractPrState(payload) {
29539
30362
  const record = payload;
29540
30363
  const explicit = readString5(record, "pr_state") ?? readString5(record, "prState");
29541
30364
  if (explicit !== void 0) return explicit;
29542
- const output = readString5(record, "tool_response") ?? readString5(record, "toolResponse") ?? readString5(record, "stdout") ?? readString5(record, "output");
30365
+ const output = pickToolOutput(payload);
29543
30366
  const command = pickCommand2(payload);
29544
30367
  const combined = [command, output].filter((s) => s !== void 0).join("\n");
29545
30368
  if (combined.length === 0) return void 0;
@@ -29564,7 +30387,7 @@ function extractPrMergedAt(payload) {
29564
30387
  const record = payload;
29565
30388
  const explicit = readString5(record, "pr_merged_at") ?? readString5(record, "prMergedAt");
29566
30389
  if (explicit !== void 0) return explicit;
29567
- const output = readString5(record, "tool_response") ?? readString5(record, "toolResponse") ?? readString5(record, "stdout") ?? readString5(record, "output");
30390
+ const output = pickToolOutput(payload);
29568
30391
  if (output === void 0) return void 0;
29569
30392
  const match = output.match(/"mergedAt"\s*:\s*"([^"]+)"/);
29570
30393
  return match?.[1] ?? void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trace",
3
- "version": "0.2.11",
3
+ "version": "0.3.0",
4
4
  "description": "Self-hosted observability for AI coding agents. One command, zero config.",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {