agent-trace 0.2.12 → 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.
- package/agent-trace.cjs +830 -21
- 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()">×</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,'&').replace(/</g,'<').replace(/>/g,'>').replace(new RegExp(DQ,'g'),'"');
|
|
@@ -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
|
-
|
|
26681
|
-
|
|
26682
|
-
|
|
26683
|
-
|
|
26684
|
-
|
|
26685
|
-
|
|
26686
|
-
|
|
26687
|
-
|
|
26688
|
-
|
|
26689
|
-
|
|
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
|
|
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
|
|
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;
|