agent-trace 0.2.2 → 0.2.4

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 +668 -402
  2. package/package.json +1 -1
package/agent-trace.cjs CHANGED
@@ -24008,440 +24008,579 @@ var import_node_fs5 = __toESM(require("node:fs"));
24008
24008
  var import_node_http = __toESM(require("node:http"));
24009
24009
 
24010
24010
  // packages/dashboard/src/web-render.ts
24011
- function escapeHtml(value) {
24012
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
24013
- }
24014
24011
  function renderDashboardHtml(options = {}) {
24015
24012
  const title = options.title ?? "agent-trace dashboard";
24016
- const safeTitle = escapeHtml(title);
24017
24013
  return `<!doctype html>
24018
24014
  <html lang="en">
24019
- <head>
24020
- <meta charset="utf-8" />
24021
- <meta name="viewport" content="width=device-width, initial-scale=1" />
24022
- <title>${safeTitle}</title>
24023
- <style>
24024
- :root {
24025
- --paper: #f7f7f2;
24026
- --ink: #121212;
24027
- --accent: #0f766e;
24028
- --accent-soft: #d1fae5;
24029
- --grid: #d6d3d1;
24030
- --warn: #9a3412;
24031
- }
24032
-
24033
- * {
24034
- box-sizing: border-box;
24035
- }
24036
-
24037
- body {
24038
- margin: 0;
24039
- color: var(--ink);
24040
- font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
24041
- background:
24042
- radial-gradient(circle at 20% 10%, rgba(15, 118, 110, 0.18), transparent 30%),
24043
- radial-gradient(circle at 80% 0%, rgba(217, 119, 6, 0.15), transparent 35%),
24044
- linear-gradient(145deg, #f5f5f4, var(--paper));
24045
- }
24015
+ <head>
24016
+ <meta charset="utf-8"/>
24017
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
24018
+ <title>${title}</title>
24019
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark-dimmed.min.css"/>
24020
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
24021
+ <style>
24022
+ :root{--bg:#000;--panel:#0a0a0a;--panel-border:#1a1a1a;--panel-hover:#111;--panel-muted:#060606;--text-primary:#d4d4d4;--text-muted:#666;--text-dim:#444;--line:#1a1a1a;--green:#4ade80;--green-dim:rgba(74,222,128,.12);--orange:#fb923c;--orange-dim:rgba(251,146,60,.12);--red:#f87171;--red-dim:rgba(248,113,113,.12);--purple:#c084fc;--purple-dim:rgba(192,132,252,.1);--cyan:#22d3ee;--cyan-dim:rgba(34,211,238,.08);--yellow:#facc15}
24023
+ *{box-sizing:border-box}
24024
+ html,body{margin:0;padding:0;min-height:100vh;background:var(--bg);color:var(--text-primary);font-family:"SF Mono","JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:13px;-webkit-font-smoothing:antialiased}
24025
+ .shell{max-width:1400px;margin:0 auto;padding:20px 16px 40px}
24026
+ .hero{border:1px solid var(--panel-border);border-radius:8px;padding:16px;background:var(--panel)}
24027
+ .hero h1{margin:0;font-size:16px;font-weight:600;letter-spacing:-.02em}
24028
+ .hero p{margin:4px 0 0;font-size:12px;color:var(--text-muted)}
24029
+ .status-banner{margin-top:10px;padding:8px 10px;border-radius:6px;border:1px solid var(--line);background:var(--panel-muted);color:var(--text-muted);font-size:12px}
24030
+ .status-banner.warning{border-color:rgba(250,204,21,.4);color:var(--yellow)}
24031
+ .mg{margin-top:14px;display:grid;gap:8px;grid-template-columns:repeat(5,minmax(0,1fr))}
24032
+ .mc{border:1px solid var(--panel-border);border-radius:8px;padding:10px 12px;background:var(--panel)}
24033
+ .mc .label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim)}
24034
+ .mc .val{margin-top:6px;font-size:18px;font-weight:700;font-variant-numeric:tabular-nums}
24035
+ .mc .det{font-size:10px;color:var(--text-dim);margin-top:2px}
24036
+ .green{color:var(--green)}.cyan{color:var(--cyan)}.orange{color:var(--orange)}.red{color:var(--red)}.purple{color:var(--purple)}.yellow{color:var(--yellow)}
24037
+ .sg{margin-top:14px;display:grid;gap:10px;grid-template-columns:1.2fr .8fr}
24038
+ .panel{border:1px solid var(--panel-border);border-radius:8px;background:var(--panel);overflow:hidden}
24039
+ .ph{padding:10px 12px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}
24040
+ .ph h2{margin:0;font-size:13px;font-weight:600;letter-spacing:-.01em}
24041
+ .ph p{margin:2px 0 0;font-size:11px;color:var(--text-dim)}
24042
+ .pc{padding:8px}
24043
+ table{width:100%;border-collapse:collapse}
24044
+ th,td{border-bottom:1px solid var(--line);text-align:left;padding:7px 8px;font-size:12px}
24045
+ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:500}
24046
+ .srow{cursor:pointer}.srow:hover{background:var(--panel-hover)}.srow.active{background:var(--green-dim)}
24047
+ .badge{display:inline-flex;align-items:center;border-radius:4px;padding:1px 6px;font-size:11px;border:1px solid var(--line);color:var(--text-muted);font-variant-numeric:tabular-nums}
24048
+ .badge.green{border-color:rgba(74,222,128,.3);color:var(--green)}
24049
+ .badge.orange{border-color:rgba(251,146,60,.3);color:var(--orange)}
24050
+ .badge.red{border-color:rgba(248,113,113,.3);color:var(--red)}
24051
+ .badge.purple{border-color:rgba(192,132,252,.3);color:var(--purple)}
24052
+ .badge.cyan{border-color:rgba(34,211,238,.3);color:var(--cyan)}
24053
+ .badge.dim{border-color:var(--line);color:var(--text-dim)}
24054
+ .badge.commit{border-color:rgba(250,204,21,.3);color:var(--yellow)}
24055
+ .ls{font-size:11px;font-variant-numeric:tabular-nums}
24056
+ .ls.green{color:var(--green);margin-right:4px}.ls.red{color:var(--red)}
24057
+ .repo-cell{max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
24058
+ .tm{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
24059
+ .tmi{border:1px solid var(--line);border-radius:6px;padding:4px 8px;font-size:11px;color:var(--text-muted)}
24060
+ .chart{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px;align-items:end;min-height:160px}
24061
+ .chart-col{display:flex;flex-direction:column;align-items:stretch;justify-content:end;gap:4px}
24062
+ .chart-bar{width:100%;border-radius:4px 4px 1px 1px;background:linear-gradient(180deg,var(--green),#166534);min-height:3px}
24063
+ .chart-label{font-size:10px;color:var(--text-dim);text-align:center}
24064
+ .chart-value{font-size:11px;color:var(--text-muted);text-align:center;font-variant-numeric:tabular-nums}
24065
+ .empty{padding:12px;color:var(--text-dim);font-size:12px}
24066
+ .pg{border:1px solid var(--panel-border);border-radius:8px;margin-bottom:8px;background:var(--panel-muted);overflow:hidden}
24067
+ .pg.expanded{border-color:#222}
24068
+ .pg-hd{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;cursor:pointer;user-select:none}
24069
+ .pg-hd:hover{background:var(--panel-hover)}
24070
+ .pg-idx{flex-shrink:0;width:28px;height:22px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;background:var(--green-dim);color:var(--green);border:1px solid rgba(74,222,128,.2)}
24071
+ .pg-txt{flex:1;min-width:0;font-size:13px;color:var(--text-primary);line-height:1.5;white-space:pre-wrap;word-break:break-word}
24072
+ .pg-txt.trunc{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
24073
+ .pg-stats{flex-shrink:0;display:flex;gap:6px;align-items:center;font-size:11px;color:var(--text-dim);font-variant-numeric:tabular-nums}
24074
+ .pg-arrow{flex-shrink:0;font-size:12px;color:var(--text-dim);transition:transform .15s}
24075
+ .pg-arrow.open{transform:rotate(90deg)}
24076
+ .pg-body{border-top:1px solid var(--line)}
24077
+ .pg-full{padding:10px 12px;border-bottom:1px solid var(--line);background:rgba(74,222,128,.04)}
24078
+ .pg-full-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px}
24079
+ .pg-full-content{font-size:12px;color:var(--text-primary);line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto}
24080
+ .erow{display:grid;grid-template-columns:18px 1fr auto;gap:8px;align-items:start;padding:6px 12px;border-bottom:1px solid var(--line);font-size:12px;min-height:32px}
24081
+ .erow:last-child{border-bottom:none}
24082
+ .eicon{width:18px;height:18px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:10px;flex-shrink:0;margin-top:1px}
24083
+ .eicon.tool{background:var(--purple-dim);color:var(--purple);border:1px solid rgba(192,132,252,.2)}
24084
+ .eicon.api{background:var(--cyan-dim);color:var(--cyan);border:1px solid rgba(34,211,238,.15)}
24085
+ .eicon.error{background:var(--red-dim);color:var(--red);border:1px solid rgba(248,113,113,.2)}
24086
+ .econtent{min-width:0}
24087
+ .elabel{color:var(--text-primary);font-weight:500}
24088
+ .edetail{margin-top:2px;color:var(--text-muted);font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
24089
+ .emeta{display:flex;gap:6px;align-items:center;flex-shrink:0;font-size:11px;color:var(--text-dim);font-variant-numeric:tabular-nums}
24090
+ .rblock{padding:10px 12px;border-top:1px solid var(--line);background:rgba(74,222,128,.03)}
24091
+ .rblock-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px}
24092
+ .rblock-text{font-size:12px;color:var(--text-primary);line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:400px;overflow-y:auto}
24093
+ .tool-fp{display:inline-block;font-size:11px;color:var(--cyan);padding:1px 5px;border-radius:3px;background:var(--cyan-dim);margin-bottom:4px}
24094
+ .tool-pat{display:inline-block;font-size:11px;color:var(--purple);padding:1px 5px;border-radius:3px;background:var(--purple-dim);margin-right:4px}
24095
+ .cblock{border:1px solid var(--line);border-radius:6px;overflow:hidden;margin-top:4px;margin-bottom:4px;background:#050505}
24096
+ .cblock-hd{padding:3px 8px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);background:#0d0d0d;border-bottom:1px solid var(--line)}
24097
+ .cblock pre{margin:0;padding:8px 10px;font-size:11px;line-height:1.6;color:var(--text-primary);overflow-x:auto;max-height:300px;overflow-y:auto;white-space:pre;tab-size:2}
24098
+ .cblock pre code{font-family:inherit}
24099
+ .diff-block{border:1px solid var(--line);border-radius:6px;overflow:hidden;margin-top:4px;margin-bottom:4px;background:#050505}
24100
+ .diff-rm{display:flex;border-bottom:1px solid var(--line);background:rgba(248,113,113,.06)}
24101
+ .diff-add{display:flex;background:rgba(74,222,128,.06)}
24102
+ .diff-lbl{flex-shrink:0;width:24px;padding:6px 0;text-align:center;font-size:11px;font-weight:700;user-select:none}
24103
+ .diff-rm .diff-lbl{color:var(--red)}.diff-add .diff-lbl{color:var(--green)}
24104
+ .diff-rm pre,.diff-add pre{margin:0;padding:6px 8px;font-size:11px;line-height:1.5;color:var(--text-primary);overflow-x:auto;max-height:200px;overflow-y:auto;white-space:pre;tab-size:2;flex:1;min-width:0}
24105
+ .fsummary{padding:8px 12px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:4px}
24106
+ .fsg{display:flex;flex-wrap:wrap;align-items:center;gap:4px}
24107
+ .fsg-label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;padding:1px 5px;border-radius:3px;font-weight:600}
24108
+ .fsg-label.written{color:var(--green);background:var(--green-dim)}
24109
+ .fsg-label.read{color:var(--text-muted);background:rgba(255,255,255,.04)}
24110
+ .fsg-path{font-size:11px;color:var(--text-muted);padding:1px 5px;border-radius:3px;background:rgba(255,255,255,.03)}
24111
+ .outcome{border:1px solid var(--panel-border);border-radius:6px;padding:10px 12px;margin-bottom:12px;background:var(--panel-muted)}
24112
+ .outcome-hd{font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}
24113
+ .outcome-row{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:8px}
24114
+ .outcome-item{display:flex;align-items:center;gap:4px}
24115
+ .outcome-lbl{font-size:11px;color:var(--text-dim)}
24116
+ .outcome-val{font-size:12px;color:var(--text-primary)}
24117
+ .outcome-commits{border-top:1px solid var(--panel-border);padding-top:6px;margin-top:4px}
24118
+ .outcome-cr{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px}
24119
+ .commit-sha{color:var(--yellow);font-weight:600;min-width:56px}
24120
+ .commit-msg{color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
24121
+ .commit-pl{font-size:10px;color:var(--text-dim)}
24122
+ .outcome-prs{border-top:1px solid var(--panel-border);padding-top:6px;margin-top:4px}
24123
+ .outcome-pr{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px}
24124
+ .pr-badge{font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;text-transform:uppercase;border:1px solid rgba(74,222,128,.3);color:var(--green)}
24125
+ .pr-label{color:var(--cyan);font-weight:600}
24126
+ .pr-repo{color:var(--text-muted)}
24127
+ .pr-link{color:var(--text-dim);text-decoration:none;font-size:11px}
24128
+ .pr-link:hover{color:var(--cyan);text-decoration:underline}
24129
+ .pcommits{border:1px solid rgba(250,204,21,.15);border-radius:4px;padding:6px 8px;margin-bottom:8px;background:rgba(250,204,21,.04)}
24130
+ .pcommit{display:flex;align-items:center;gap:8px;padding:2px 0;font-size:12px}
24131
+ .hljs{background:transparent!important;color:var(--text-primary)}
24132
+ .hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-name{color:#c084fc}
24133
+ .hljs-string,.hljs-attr{color:#4ade80}
24134
+ .hljs-number,.hljs-literal{color:#fb923c}
24135
+ .hljs-comment{color:#555;font-style:italic}
24136
+ .hljs-type,.hljs-class .hljs-title,.hljs-title.class_{color:#22d3ee}
24137
+ .hljs-function .hljs-title,.hljs-title.function_{color:#60a5fa}
24138
+ .hljs-variable,.hljs-template-variable{color:#f87171}
24139
+ .hljs-regexp{color:#fbbf24}
24140
+ .hljs-meta{color:#666}
24141
+ .hljs-punctuation{color:#888}
24142
+ .hljs-property{color:#93c5fd}
24143
+ .hljs-addition{color:#4ade80;background:rgba(74,222,128,.08)}
24144
+ .hljs-deletion{color:#f87171;background:rgba(248,113,113,.08)}
24145
+ @media(max-width:1200px){.mg{grid-template-columns:repeat(2,minmax(0,1fr))}.sg{grid-template-columns:1fr}}
24146
+ @media(max-width:760px){.shell{padding:12px 8px 24px}.mg{grid-template-columns:1fr}th:nth-child(5),td:nth-child(5),th:nth-child(7),td:nth-child(7){display:none}.erow{grid-template-columns:18px 1fr}.emeta{grid-column:2}.pg-stats{display:none}}
24147
+ </style>
24148
+ </head>
24149
+ <body>
24150
+ <main class="shell">
24151
+ <section class="hero">
24152
+ <h1>${title}</h1>
24153
+ <p>session observability for coding agents</p>
24154
+ <div id="status" class="status-banner">Connecting...</div>
24155
+ </section>
24156
+ <section class="mg">
24157
+ <article class="mc"><div class="label">Sessions</div><div class="val green" id="m-sessions">0</div></article>
24158
+ <article class="mc"><div class="label">Total Cost</div><div class="val orange" id="m-cost">$0.00</div></article>
24159
+ <article class="mc"><div class="label">Prompts</div><div class="val cyan" id="m-prompts">0</div></article>
24160
+ <article class="mc"><div class="label">Tool Calls</div><div class="val" id="m-tools">0</div></article>
24161
+ <article class="mc"><div class="label">Commits</div><div class="val green" id="m-commits">0</div><div class="det" id="m-commits-det"></div></article>
24162
+ </section>
24163
+ <section class="sg">
24164
+ <section class="panel">
24165
+ <header class="ph"><div><h2>Sessions</h2><p id="stream-label">...</p></div></header>
24166
+ <div class="pc"><div id="sessions-area" class="empty">Loading...</div></div>
24167
+ </section>
24168
+ <section class="panel">
24169
+ <header class="ph"><div><h2>Daily Cost</h2><p>7-day spend</p></div></header>
24170
+ <div class="pc"><div id="cost-chart" class="empty">Loading...</div></div>
24171
+ </section>
24172
+ </section>
24173
+ <section class="panel" style="margin-top:10px">
24174
+ <header class="ph"><div><h2>Session Replay</h2><p id="replay-label">select a session</p></div></header>
24175
+ <div class="pc" id="replay-area"><div class="empty">No session selected.</div></div>
24176
+ </section>
24177
+ </main>
24178
+ <script>
24179
+ (function(){
24180
+ var DQ = String.fromCharCode(34);
24181
+ var selectedId = null;
24182
+ var sessions = [];
24183
+ var costPoints = [];
24184
+ var replay = null;
24046
24185
 
24047
- .shell {
24048
- max-width: 1100px;
24049
- margin: 0 auto;
24050
- padding: 28px 18px 40px;
24051
- }
24186
+ function esc(s) {
24187
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(new RegExp(DQ,'g'),'&quot;');
24188
+ }
24189
+ function fmt$(v) { return '$' + v.toFixed(2); }
24190
+ function fmt$4(v) { return '$' + v.toFixed(4); }
24191
+ function ensureUtc(v) {
24192
+ if (v.endsWith('Z') || /[+-]\\d{2}:\\d{2}$/.test(v)) return v;
24193
+ return v.includes('T') ? v + 'Z' : v.replace(' ','T') + 'Z';
24194
+ }
24195
+ function fmtDate(v) { try { return new Date(ensureUtc(v)).toLocaleString(); } catch(e) { return v; } }
24196
+ function fmtTime(v) { try { return new Date(ensureUtc(v)).toLocaleTimeString(); } catch(e) { return v; } }
24197
+ function fmtDur(ms) { return ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1) + 's'; }
24052
24198
 
24053
- .hero {
24054
- border: 2px solid var(--ink);
24055
- background: #fff;
24056
- box-shadow: 8px 8px 0 var(--ink);
24057
- padding: 22px 20px;
24058
- }
24059
-
24060
- .hero h1 {
24061
- margin: 0;
24062
- font-size: clamp(1.7rem, 2.4vw, 2.3rem);
24063
- letter-spacing: -0.02em;
24064
- }
24199
+ function readStr(r, k) { var v = r[k]; return typeof v === 'string' && v.length > 0 ? v : undefined; }
24200
+ function readNum(r, k) { var v = r[k]; return typeof v === 'number' && isFinite(v) ? v : undefined; }
24201
+ function readArr(r, k) { var v = r[k]; return Array.isArray(v) ? v : []; }
24202
+ function asRec(v) { return typeof v === 'object' && v !== null && !Array.isArray(v) ? v : undefined; }
24065
24203
 
24066
- .hero p {
24067
- margin: 10px 0 0;
24068
- color: #3f3f46;
24069
- }
24070
-
24071
- .grid {
24072
- margin-top: 18px;
24073
- display: grid;
24074
- grid-template-columns: repeat(3, minmax(0, 1fr));
24075
- gap: 12px;
24076
- }
24077
-
24078
- .metric {
24079
- border: 1px solid var(--grid);
24080
- background: #fff;
24081
- padding: 12px;
24082
- }
24083
-
24084
- .metric .label {
24085
- font-size: 0.78rem;
24086
- text-transform: uppercase;
24087
- letter-spacing: 0.08em;
24088
- color: #52525b;
24089
- }
24090
-
24091
- .metric .value {
24092
- margin-top: 6px;
24093
- font-size: 1.35rem;
24094
- font-weight: 700;
24095
- }
24204
+ var EXT_MAP = {ts:'typescript',tsx:'typescript',js:'javascript',jsx:'javascript',py:'python',rb:'ruby',go:'go',rs:'rust',java:'java',css:'css',html:'html',json:'json',yaml:'yaml',yml:'yaml',md:'markdown',sh:'bash',bash:'bash',sql:'sql',toml:'yaml'};
24205
+ function guessLang(fp) { var e = (fp.split('.').pop()||'').toLowerCase(); return EXT_MAP[e] || e; }
24206
+ function highlight(code, lang) {
24207
+ if (typeof hljs === 'undefined') return esc(code);
24208
+ try { var r = hljs.highlight(code, {language: lang || 'text', ignoreIllegals: true}); return r.value; } catch(e) { return esc(code); }
24209
+ }
24096
24210
 
24097
- .panel {
24098
- margin-top: 18px;
24099
- border: 2px solid var(--ink);
24100
- background: #fff;
24101
- box-shadow: 8px 8px 0 var(--ink);
24102
- overflow: hidden;
24103
- }
24211
+ var READ_TOOLS = {Read:1,Glob:1,Grep:1,Search:1};
24212
+ var WRITE_TOOLS = {Write:1,Edit:1,NotebookEdit:1};
24104
24213
 
24105
- .panel header {
24106
- padding: 12px 14px;
24107
- background: var(--accent-soft);
24108
- border-bottom: 2px solid var(--ink);
24109
- font-weight: 700;
24110
- }
24214
+ function parseReplay(val) {
24215
+ var r = asRec(val); if (!r) return null;
24216
+ var sid = readStr(r,'sessionId'), sa = readStr(r,'startedAt'), m = asRec(r.metrics), tl = r.timeline;
24217
+ if (!sid || !sa || !m || !Array.isArray(tl)) return null;
24218
+ var envR = asRec(r.environment), gitR = asRec(r.git);
24219
+ var gitBranch = (envR ? readStr(envR,'gitBranch') : undefined) || readStr(r,'gitBranch');
24220
+ var cRaw = (gitR && Array.isArray(gitR.commits)) ? gitR.commits : Array.isArray(r.commits) ? r.commits : [];
24221
+ var commits = cRaw.map(function(e){var c=asRec(e);if(!c)return null;var sha=readStr(c,'sha');if(!sha||sha.indexOf('placeholder_')===0)return null;return{sha:sha,message:readStr(c,'message'),promptId:readStr(c,'promptId'),committedAt:readStr(c,'committedAt')};}).filter(Boolean);
24222
+ var pRaw = (gitR && Array.isArray(gitR.pullRequests)) ? gitR.pullRequests : Array.isArray(r.pullRequests) ? r.pullRequests : [];
24223
+ var prs = pRaw.map(function(e){var p=asRec(e);if(!p)return null;var repo=readStr(p,'repo'),n=readNum(p,'prNumber');if(!repo||n===undefined)return null;return{repo:repo,prNumber:n,state:readStr(p,'state')||'open',url:readStr(p,'url')};}).filter(Boolean);
24224
+ var timeline = tl.map(function(e){
24225
+ var ev=asRec(e);if(!ev)return null;
24226
+ var id=readStr(ev,'id'),type=readStr(ev,'type'),ts=readStr(ev,'timestamp');if(!id||!type||!ts)return null;
24227
+ var d=asRec(ev.details),tok=asRec(ev.tokens);
24228
+ return{id:id,type:type,timestamp:ts,promptId:readStr(ev,'promptId'),status:readStr(ev,'status'),costUsd:readNum(ev,'costUsd'),toolName:d?readStr(d,'toolName'):undefined,toolDurationMs:d?readNum(d,'toolDurationMs'):undefined,inputTokens:tok?readNum(tok,'input'):undefined,outputTokens:tok?readNum(tok,'output'):undefined,details:d};
24229
+ }).filter(Boolean);
24230
+ return{sessionId:sid,startedAt:sa,endedAt:readStr(r,'endedAt'),gitBranch:gitBranch,
24231
+ metrics:{promptCount:readNum(m,'promptCount')||0,toolCallCount:readNum(m,'toolCallCount')||0,totalCostUsd:readNum(m,'totalCostUsd')||0,totalInputTokens:readNum(m,'totalInputTokens')||0,totalOutputTokens:readNum(m,'totalOutputTokens')||0,linesAdded:readNum(m,'linesAdded')||0,linesRemoved:readNum(m,'linesRemoved')||0,modelsUsed:readArr(m,'modelsUsed').filter(function(x){return typeof x==='string'}),toolsUsed:readArr(m,'toolsUsed').filter(function(x){return typeof x==='string'}),filesTouched:readArr(m,'filesTouched').filter(function(x){return typeof x==='string'})},
24232
+ commits:commits,pullRequests:prs,timeline:timeline};
24233
+ }
24111
24234
 
24112
- table {
24113
- width: 100%;
24114
- border-collapse: collapse;
24115
- }
24235
+ function extractToolDetail(ev) {
24236
+ var d = ev.details, tn = ev.toolName || ev.type;
24237
+ if (!d) return {toolName:tn};
24238
+ var rawTi = d.toolInput || d.tool_input, inp = asRec(rawTi), tiStr = typeof rawTi === 'string' ? rawTi : undefined;
24239
+ function rs(k) { return (d ? readStr(d,k) : undefined) || (inp ? readStr(inp,k) : undefined) || (tiStr ? extractFromTruncJson(tiStr,k) : undefined); }
24240
+ return {toolName:tn,filePath:rs('filePath')||rs('file_path'),command:rs('command')||rs('cmd'),pattern:rs('pattern'),oldString:(d?readStr(d,'oldString'):undefined)||(inp?readStr(inp,'old_string'):undefined),newString:(d?readStr(d,'newString'):undefined)||(inp?readStr(inp,'new_string'):undefined),writeContent:d?readStr(d,'writeContent'):undefined,description:rs('description')};
24241
+ }
24116
24242
 
24117
- th, td {
24118
- padding: 10px 12px;
24119
- border-bottom: 1px solid var(--grid);
24120
- text-align: left;
24121
- font-size: 0.95rem;
24122
- }
24243
+ function extractFromTruncJson(raw, key) {
24244
+ var re = new RegExp(DQ + key + DQ + '\\\\s*:\\\\s*' + DQ + '([^' + DQ + ']*?)' + DQ);
24245
+ var m = re.exec(raw); return m && m[1] && m[1].length > 0 ? m[1] : undefined;
24246
+ }
24123
24247
 
24124
- th {
24125
- font-size: 0.8rem;
24126
- text-transform: uppercase;
24127
- letter-spacing: 0.06em;
24128
- color: #57534e;
24129
- background: #fafaf9;
24130
- }
24248
+ function renderToolDetail(ev) {
24249
+ var td = extractToolDetail(ev);
24250
+ if (td.toolName === 'Bash' && td.command) return '<div class="cblock"><div class="cblock-hd">bash</div><pre><code class="language-bash">' + highlight(td.command,'bash') + '</code></pre></div>';
24251
+ if (td.toolName === 'Edit' && td.filePath) {
24252
+ var h = '<div class="tool-fp">' + esc(td.filePath) + '</div>';
24253
+ if (td.oldString) {
24254
+ h += '<div class="diff-block"><div class="diff-rm"><div class="diff-lbl">-</div><pre><code>' + esc(td.oldString) + '</code></pre></div>';
24255
+ if (td.newString) h += '<div class="diff-add"><div class="diff-lbl">+</div><pre><code>' + esc(td.newString) + '</code></pre></div>';
24256
+ h += '</div>';
24257
+ }
24258
+ return h;
24259
+ }
24260
+ if ((td.toolName === 'Grep' || td.toolName === 'Glob') && td.pattern) {
24261
+ var h2 = '<span class="tool-pat">' + esc(td.toolName === 'Grep' ? '/' + td.pattern + '/' : td.pattern) + '</span>';
24262
+ if (td.filePath) h2 += ' <span class="tool-fp">' + esc(td.filePath) + '</span>';
24263
+ return h2;
24264
+ }
24265
+ if (td.toolName === 'Task' && td.description) return '<span class="edetail">' + esc(td.description) + '</span>';
24266
+ if (td.toolName === 'Write' && td.filePath) {
24267
+ var lang = guessLang(td.filePath);
24268
+ var h3 = '<div class="tool-fp">' + esc(td.filePath) + '</div>';
24269
+ if (td.writeContent) h3 += '<div class="cblock"><div class="cblock-hd">' + esc(lang) + '</div><pre><code class="language-' + esc(lang) + '">' + highlight(td.writeContent,lang) + '</code></pre></div>';
24270
+ return h3;
24271
+ }
24272
+ if (td.filePath) return '<div class="tool-fp">' + esc(td.filePath) + '</div>';
24273
+ if (td.command) return '<div class="cblock"><div class="cblock-hd">shell</div><pre><code class="language-bash">' + highlight(td.command,'bash') + '</code></pre></div>';
24274
+ return '';
24275
+ }
24131
24276
 
24132
- .status {
24133
- margin-top: 12px;
24134
- padding: 10px 12px;
24135
- border: 1px solid var(--grid);
24136
- background: #fff;
24137
- }
24277
+ function renderEventRow(ev) {
24278
+ var iconCls = ev.status === 'error' ? 'eicon error' : ev.toolName ? 'eicon tool' : 'eicon api';
24279
+ var iconChr = ev.status === 'error' ? '!' : ev.toolName ? 'T' : 'E';
24280
+ var meta = '';
24281
+ if (ev.toolDurationMs !== undefined) meta += '<span class="badge">' + fmtDur(ev.toolDurationMs) + '</span>';
24282
+ if (ev.costUsd !== undefined && ev.costUsd > 0) meta += '<span class="badge orange">' + fmt$4(ev.costUsd) + '</span>';
24283
+ if (ev.status) {
24284
+ var sc = ev.status === 'error' ? 'red' : (ev.status === 'ok' || ev.status === 'success') ? 'green' : '';
24285
+ meta += '<span class="badge ' + sc + '">' + esc(ev.status) + '</span>';
24286
+ }
24287
+ meta += '<span style="color:var(--text-dim)">' + fmtTime(ev.timestamp) + '</span>';
24288
+ return '<div class="erow"><div class="' + iconCls + '">' + iconChr + '</div><div class="econtent"><div class="elabel">' + esc(ev.toolName || ev.type) + '</div>' + renderToolDetail(ev) + '</div><div class="emeta">' + meta + '</div></div>';
24289
+ }
24138
24290
 
24139
- .status.error {
24140
- color: var(--warn);
24141
- border-color: #fca5a5;
24142
- background: #fef2f2;
24291
+ function buildPromptGroups(timeline, commits) {
24292
+ var byPrompt = {};
24293
+ commits.forEach(function(c){ if(c.promptId){if(!byPrompt[c.promptId])byPrompt[c.promptId]=[];byPrompt[c.promptId].push(c);} });
24294
+ var order = [], map = {};
24295
+ timeline.forEach(function(ev){
24296
+ if(!ev.promptId) return;
24297
+ if(!map[ev.promptId]){order.push(ev.promptId);map[ev.promptId]=[];}
24298
+ map[ev.promptId].push(ev);
24299
+ });
24300
+ return order.map(function(pid){
24301
+ var evts = map[pid] || [], promptText, responseText, cost=0,tools=0,inTok=0,outTok=0,dur=0;
24302
+ var filesR={},filesW={},toolEvts=[];
24303
+ evts.forEach(function(ev){
24304
+ var d = ev.details;
24305
+ if(!promptText && d){var pt=readStr(d,'promptText');if(pt)promptText=pt;}
24306
+ if(ev.type==='assistant_response'||ev.type==='api_call'||ev.type==='api_response'){
24307
+ if(d){var rt=readStr(d,'responseText')||readStr(d,'lastAssistantMessage');if(rt)responseText=rt;}
24308
+ }
24309
+ cost+=(ev.costUsd||0);inTok+=(ev.inputTokens||0);outTok+=(ev.outputTokens||0);
24310
+ if(ev.toolName||(ev.type==='tool_call'||ev.type==='tool_result')){
24311
+ toolEvts.push(ev);tools++;dur+=(ev.toolDurationMs||0);
24312
+ var dd=ev.details,rti=dd?(dd.toolInput||dd.tool_input):undefined,inp=asRec(rti),tiStr=typeof rti==='string'?rti:undefined;
24313
+ var fp=(dd?readStr(dd,'filePath'):undefined)||(inp?readStr(inp,'file_path'):undefined)||(tiStr?extractFromTruncJson(tiStr,'file_path'):undefined);
24314
+ if(fp){var tn=ev.toolName||'';if(WRITE_TOOLS[tn])filesW[fp]=1;else if(READ_TOOLS[tn])filesR[fp]=1;}
24143
24315
  }
24316
+ });
24317
+ // deduplicate tool events
24318
+ var deduped = [];
24319
+ toolEvts.forEach(function(ev){
24320
+ if(deduped.length>0){
24321
+ var prev=deduped[deduped.length-1];
24322
+ if(prev.toolName===ev.toolName){
24323
+ var pfp=prev.details?(readStr(prev.details,'filePath')||readStr(asRec(prev.details.toolInput)||{},'file_path')):undefined;
24324
+ var cfp=ev.details?(readStr(ev.details,'filePath')||readStr(asRec(ev.details.toolInput)||{},'file_path')):undefined;
24325
+ if(pfp&&pfp===cfp)return;
24326
+ }
24327
+ }
24328
+ deduped.push(ev);
24329
+ });
24330
+ return{promptId:pid,promptText:promptText,responseText:responseText,toolEvents:deduped,commits:byPrompt[pid]||[],totalCostUsd:cost,totalToolCalls:tools,totalInputTokens:inTok,totalOutputTokens:outTok,totalDurationMs:dur,filesRead:Object.keys(filesR),filesWritten:Object.keys(filesW)};
24331
+ }).filter(function(g){return g.promptText||g.toolEvents.length>0||g.responseText;});
24332
+ }
24144
24333
 
24145
- @media (max-width: 840px) {
24146
- .grid {
24147
- grid-template-columns: 1fr;
24148
- }
24149
-
24150
- th:nth-child(4),
24151
- td:nth-child(4) {
24152
- display: none;
24153
- }
24154
- }
24155
- </style>
24156
- </head>
24157
- <body>
24158
- <main class="shell">
24159
- <section class="hero">
24160
- <h1>${safeTitle}</h1>
24161
- <p>Session-level observability for coding agents, running locally.</p>
24162
- </section>
24163
-
24164
- <section class="grid">
24165
- <article class="metric">
24166
- <div class="label">Sessions</div>
24167
- <div class="value" id="metric-sessions">0</div>
24168
- </article>
24169
- <article class="metric">
24170
- <div class="label">Total Cost (USD)</div>
24171
- <div class="value" id="metric-cost">$0.00</div>
24172
- </article>
24173
- <article class="metric">
24174
- <div class="label">Latest Start</div>
24175
- <div class="value" id="metric-latest">-</div>
24176
- </article>
24177
- </section>
24178
-
24179
- <section class="panel">
24180
- <header>Recent Sessions</header>
24181
- <table>
24182
- <thead>
24183
- <tr>
24184
- <th>Session</th>
24185
- <th>User</th>
24186
- <th>Repo</th>
24187
- <th>Started</th>
24188
- <th>Cost</th>
24189
- <th>Replay</th>
24190
- </tr>
24191
- </thead>
24192
- <tbody id="sessions-body">
24193
- <tr><td colspan="6">Loading sessions...</td></tr>
24194
- </tbody>
24195
- </table>
24196
- </section>
24197
-
24198
- <section class="panel">
24199
- <header>Session Replay</header>
24200
- <div id="replay-meta" class="status">Select a session to inspect timeline events.</div>
24201
- <table>
24202
- <thead>
24203
- <tr>
24204
- <th>Timestamp</th>
24205
- <th>Type</th>
24206
- <th>Status</th>
24207
- <th>Cost</th>
24208
- <th>Prompt</th>
24209
- </tr>
24210
- </thead>
24211
- <tbody id="replay-body">
24212
- <tr><td colspan="5">No session selected.</td></tr>
24213
- </tbody>
24214
- </table>
24215
- </section>
24216
-
24217
- <section id="status" class="status">Fetching data from local API bridge...</section>
24218
- </main>
24219
- <script>
24220
- const sessionsBody = document.getElementById("sessions-body");
24221
- const status = document.getElementById("status");
24222
- const sessionsMetric = document.getElementById("metric-sessions");
24223
- const costMetric = document.getElementById("metric-cost");
24224
- const latestMetric = document.getElementById("metric-latest");
24225
- const replayMeta = document.getElementById("replay-meta");
24226
- const replayBody = document.getElementById("replay-body");
24227
- let selectedSessionId = null;
24334
+ function parseTextSegments(text) {
24335
+ var segs = [], re = /\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g, last = 0, m;
24336
+ while ((m = re.exec(text)) !== null) {
24337
+ if (m.index > last) segs.push({type:'text',content:text.slice(last,m.index)});
24338
+ segs.push({type:'code',lang:m[1]||'text',content:m[2]||''});
24339
+ last = m.index + m[0].length;
24340
+ }
24341
+ if (last < text.length) segs.push({type:'text',content:text.slice(last)});
24342
+ return segs.length > 0 ? segs : [{type:'text',content:text}];
24343
+ }
24228
24344
 
24229
- function formatMoney(value) {
24230
- return "$" + value.toFixed(2);
24231
- }
24345
+ function renderFormattedText(text) {
24346
+ var segs = parseTextSegments(text);
24347
+ return segs.map(function(s){
24348
+ if(s.type==='code') return '<div class="cblock"><div class="cblock-hd">' + esc(s.lang) + '</div><pre><code class="language-' + esc(s.lang) + '">' + highlight(s.content,s.lang) + '</code></pre></div>';
24349
+ return '<span>' + esc(s.content) + '</span>';
24350
+ }).join('');
24351
+ }
24232
24352
 
24233
- function formatDate(value) {
24234
- try {
24235
- return new Date(value).toLocaleString();
24236
- } catch {
24237
- return value;
24238
- }
24239
- }
24353
+ function renderPromptCard(g, idx) {
24354
+ var stats = '';
24355
+ if(g.commits.length>0) stats += '<span class="badge commit">' + (g.commits.length===1?esc(g.commits[0].sha.slice(0,7)):g.commits.length+' commits') + '</span>';
24356
+ if(g.totalToolCalls>0) stats += '<span class="badge purple">' + g.totalToolCalls + ' tools</span>';
24357
+ if(g.filesWritten.length>0) stats += '<span class="badge green">' + g.filesWritten.length + ' written</span>';
24358
+ if(g.filesRead.length>0) stats += '<span class="badge">' + g.filesRead.length + ' read</span>';
24359
+ if(g.totalCostUsd>0) stats += '<span class="badge orange">' + fmt$4(g.totalCostUsd) + '</span>';
24240
24360
 
24241
- function escapeHtml(value) {
24242
- var DQ = String.fromCharCode(34);
24243
- return String(value)
24244
- .replaceAll("&", "&amp;")
24245
- .replaceAll("<", "&lt;")
24246
- .replaceAll(">", "&gt;")
24247
- .replaceAll(DQ, "&quot;")
24248
- .replaceAll("'", "&#39;");
24249
- }
24361
+ var body = '';
24362
+ if(g.commits.length>0){
24363
+ body += '<div class="pcommits">';
24364
+ g.commits.forEach(function(c){body += '<div class="pcommit"><span class="commit-sha">' + esc(c.sha.slice(0,7)) + '</span><span class="commit-msg">' + esc(c.message||'no message') + '</span></div>';});
24365
+ body += '</div>';
24366
+ }
24367
+ g.toolEvents.forEach(function(ev){body += renderEventRow(ev);});
24368
+ if(g.filesWritten.length>0||g.filesRead.length>0){
24369
+ body += '<div class="fsummary">';
24370
+ if(g.filesWritten.length>0){body += '<div class="fsg"><span class="fsg-label written">written</span>';g.filesWritten.forEach(function(f){body += '<span class="fsg-path">' + esc(f) + '</span>';});body += '</div>';}
24371
+ if(g.filesRead.length>0){body += '<div class="fsg"><span class="fsg-label read">read</span>';g.filesRead.forEach(function(f){body += '<span class="fsg-path">' + esc(f) + '</span>';});body += '</div>';}
24372
+ body += '</div>';
24373
+ }
24374
+ if(g.responseText) body += '<div class="rblock"><div class="rblock-label">Response</div><div class="rblock-text">' + renderFormattedText(g.responseText) + '</div></div>';
24250
24375
 
24251
- function setReplayPlaceholder(message) {
24252
- replayBody.innerHTML = '<tr><td colspan="5">' + escapeHtml(message) + '</td></tr>';
24253
- }
24376
+ return '<div class="pg" id="pg-' + esc(g.promptId.slice(0,12)) + '">' +
24377
+ '<div class="pg-hd" onclick="togglePrompt(this)">' +
24378
+ '<div class="pg-idx">' + idx + '</div>' +
24379
+ '<div class="pg-txt trunc">' + esc(g.promptText || 'prompt ' + g.promptId.slice(0,8)) + '</div>' +
24380
+ '<div class="pg-stats">' + stats + '</div>' +
24381
+ '<div class="pg-arrow">&gt;</div>' +
24382
+ '</div>' +
24383
+ '<div class="pg-body" style="display:none">' + body + '</div>' +
24384
+ '</div>';
24385
+ }
24254
24386
 
24255
- function renderSessionReplay(session) {
24256
- if (typeof session !== "object" || session === null) {
24257
- replayMeta.classList.add("error");
24258
- replayMeta.textContent = "Replay payload is invalid.";
24259
- setReplayPlaceholder("Replay payload is invalid.");
24260
- return;
24261
- }
24387
+ window.togglePrompt = function(hd) {
24388
+ var pg = hd.parentElement;
24389
+ var body = pg.querySelector('.pg-body');
24390
+ var txt = pg.querySelector('.pg-txt');
24391
+ var arrow = pg.querySelector('.pg-arrow');
24392
+ var open = body.style.display !== 'none';
24393
+ body.style.display = open ? 'none' : '';
24394
+ pg.classList.toggle('expanded', !open);
24395
+ txt.classList.toggle('trunc', open);
24396
+ arrow.classList.toggle('open', !open);
24397
+ };
24262
24398
 
24263
- const timeline = Array.isArray(session.timeline) ? session.timeline : [];
24264
- const promptCount = typeof session.metrics?.promptCount === "number" ? session.metrics.promptCount : 0;
24265
- const toolCallCount = typeof session.metrics?.toolCallCount === "number" ? session.metrics.toolCallCount : 0;
24266
- const totalCostUsd = typeof session.metrics?.totalCostUsd === "number" ? session.metrics.totalCostUsd : 0;
24267
- replayMeta.classList.remove("error");
24268
- replayMeta.textContent = "Session " + session.sessionId + " | prompts " + promptCount
24269
- + " | tools " + toolCallCount + " | cost " + formatMoney(totalCostUsd);
24399
+ function renderSessions() {
24400
+ var area = document.getElementById('sessions-area');
24401
+ if (sessions.length === 0) { area.innerHTML = '<div class="empty">No sessions captured yet.</div>'; return; }
24402
+ var h = '<table><thead><tr><th>Session</th><th>Repo</th><th>Started</th><th>Prompts</th><th>Cost</th><th>Commits</th><th>Lines</th></tr></thead><tbody>';
24403
+ sessions.forEach(function(s){
24404
+ var active = s.sessionId === selectedId ? ' active' : '';
24405
+ var repo = s.gitRepo ? (s.gitBranch ? s.gitRepo + '/' + s.gitBranch : s.gitRepo) : '-';
24406
+ var commits = s.commitCount > 0 ? '<span class="badge green">' + s.commitCount + '</span>' : '<span class="badge dim">0</span>';
24407
+ var lines = (s.linesAdded > 0 || s.linesRemoved > 0) ? '<span class="ls green">+' + s.linesAdded + '</span><span class="ls red">-' + s.linesRemoved + '</span>' : '<span style="color:var(--text-dim)">-</span>';
24408
+ h += '<tr class="srow' + active + '" data-sid="' + esc(s.sessionId) + '" onclick="selectSession(this.dataset.sid)"><td>' + esc(s.sessionId.slice(0,10)) + '</td><td class="repo-cell">' + esc(repo) + '</td><td>' + fmtDate(s.startedAt) + '</td><td>' + s.promptCount + '</td><td>' + fmt$(s.totalCostUsd) + '</td><td>' + commits + '</td><td>' + lines + '</td></tr>';
24409
+ });
24410
+ h += '</tbody></table>';
24411
+ area.innerHTML = h;
24412
+ }
24270
24413
 
24271
- if (timeline.length === 0) {
24272
- setReplayPlaceholder("No timeline events for this session.");
24273
- return;
24274
- }
24414
+ function renderMetrics() {
24415
+ document.getElementById('m-sessions').textContent = sessions.length;
24416
+ document.getElementById('m-cost').textContent = fmt$(sessions.reduce(function(s,x){return s+x.totalCostUsd;},0));
24417
+ document.getElementById('m-prompts').textContent = sessions.reduce(function(s,x){return s+x.promptCount;},0);
24418
+ document.getElementById('m-tools').textContent = sessions.reduce(function(s,x){return s+x.toolCallCount;},0);
24419
+ var tc = sessions.reduce(function(s,x){return s+x.commitCount;},0);
24420
+ var sc = sessions.filter(function(x){return x.commitCount>0;}).length;
24421
+ document.getElementById('m-commits').textContent = tc;
24422
+ document.getElementById('m-commits-det').textContent = sc + '/' + sessions.length + ' sessions produced commits';
24423
+ }
24275
24424
 
24276
- const rows = timeline.map((event) => {
24277
- const timestamp = typeof event.timestamp === "string" ? formatDate(event.timestamp) : "-";
24278
- const type = typeof event.type === "string" ? event.type : "-";
24279
- const eventStatus = typeof event.status === "string" ? event.status : "-";
24280
- const cost = typeof event.costUsd === "number" ? formatMoney(event.costUsd) : "-";
24281
- const prompt = typeof event.promptId === "string" ? event.promptId : "-";
24282
- return "<tr>"
24283
- + "<td>" + escapeHtml(timestamp) + "</td>"
24284
- + "<td>" + escapeHtml(type) + "</td>"
24285
- + "<td>" + escapeHtml(eventStatus) + "</td>"
24286
- + "<td>" + escapeHtml(cost) + "</td>"
24287
- + "<td>" + escapeHtml(prompt) + "</td>"
24288
- + "</tr>";
24289
- }).join("");
24290
- replayBody.innerHTML = rows;
24291
- }
24425
+ function renderCostChart() {
24426
+ var el = document.getElementById('cost-chart');
24427
+ if (costPoints.length === 0) { el.innerHTML = '<div class="empty">No cost data yet.</div>'; return; }
24428
+ var pts = costPoints.slice(-7);
24429
+ var max = Math.max(0.01, Math.max.apply(null, pts.map(function(p){return p.totalCostUsd;})));
24430
+ var h = '<div class="chart">';
24431
+ pts.forEach(function(p){
24432
+ var ht = Math.max(4, Math.round((p.totalCostUsd / max) * 140));
24433
+ h += '<div class="chart-col"><div class="chart-bar" style="height:' + ht + 'px"></div><div class="chart-value">' + fmt$(p.totalCostUsd) + '</div><div class="chart-label">' + esc(p.date.slice(5)) + '</div></div>';
24434
+ });
24435
+ h += '</div>';
24436
+ el.innerHTML = h;
24437
+ }
24292
24438
 
24293
- async function loadSessionReplay(sessionId) {
24294
- selectedSessionId = sessionId;
24295
- try {
24296
- const response = await fetch("/api/session/" + encodeURIComponent(sessionId));
24297
- if (response.status === 404) {
24298
- replayMeta.classList.add("error");
24299
- replayMeta.textContent = "Session replay not found.";
24300
- setReplayPlaceholder("Session replay not found.");
24301
- return;
24302
- }
24303
- if (!response.ok) {
24304
- throw new Error("session replay bridge failed with status " + response.status);
24305
- }
24306
- const payload = await response.json();
24307
- if (payload?.status !== "ok" || typeof payload.session !== "object") {
24308
- throw new Error("unexpected replay payload format");
24309
- }
24310
- renderSessionReplay(payload.session);
24311
- } catch (error) {
24312
- replayMeta.classList.add("error");
24313
- replayMeta.textContent = String(error);
24314
- setReplayPlaceholder("Failed to load replay.");
24315
- }
24316
- }
24439
+ function renderReplay() {
24440
+ var area = document.getElementById('replay-area');
24441
+ var label = document.getElementById('replay-label');
24442
+ if (!replay) {
24443
+ label.textContent = selectedId ? selectedId.slice(0,12) : 'select a session';
24444
+ area.innerHTML = '<div class="empty">No replay data.</div>';
24445
+ return;
24446
+ }
24447
+ label.textContent = replay.sessionId.slice(0,12) + ' \\u2014 ' + replay.metrics.promptCount + ' prompts, ' + fmt$(replay.metrics.totalCostUsd);
24448
+ var h = '';
24449
+ // meta
24450
+ h += '<div class="tm">';
24451
+ h += '<span class="tmi">Cost <span class="badge orange">' + fmt$4(replay.metrics.totalCostUsd) + '</span></span>';
24452
+ h += '<span class="tmi">Tokens <span class="badge cyan">' + replay.metrics.totalInputTokens + ' in / ' + replay.metrics.totalOutputTokens + ' out</span></span>';
24453
+ if (replay.metrics.linesAdded > 0 || replay.metrics.linesRemoved > 0) h += '<span class="tmi">Lines <span class="badge green">+' + replay.metrics.linesAdded + '</span> <span class="badge red">-' + replay.metrics.linesRemoved + '</span></span>';
24454
+ if (replay.metrics.modelsUsed.length > 0) h += '<span class="tmi">' + esc(replay.metrics.modelsUsed.join(', ')) + '</span>';
24455
+ if (replay.metrics.filesTouched.length > 0) h += '<span class="tmi">' + replay.metrics.filesTouched.length + ' files</span>';
24456
+ h += '</div>';
24457
+ // outcome
24458
+ if (replay.commits.length > 0 || replay.pullRequests.length > 0 || replay.gitBranch) {
24459
+ h += '<div class="outcome"><div class="outcome-hd">Outcome</div><div class="outcome-row">';
24460
+ if (replay.gitBranch) h += '<span class="outcome-item"><span class="outcome-lbl">branch</span><span class="outcome-val">' + esc(replay.gitBranch) + '</span></span>';
24461
+ if (replay.commits.length > 0) h += '<span class="outcome-item"><span class="outcome-lbl">' + (replay.commits.length===1?'commit':'commits') + '</span><span class="outcome-val">' + replay.commits.length + '</span></span>';
24462
+ if (replay.metrics.linesAdded > 0 || replay.metrics.linesRemoved > 0) h += '<span class="outcome-item"><span class="outcome-lbl">lines</span><span class="outcome-val"><span class="ls green">+' + replay.metrics.linesAdded + '</span><span class="ls red">-' + replay.metrics.linesRemoved + '</span></span></span>';
24463
+ if (replay.metrics.filesTouched.length > 0) h += '<span class="outcome-item"><span class="outcome-lbl">files</span><span class="outcome-val">' + replay.metrics.filesTouched.length + '</span></span>';
24464
+ h += '</div>';
24465
+ if (replay.commits.length > 0) {
24466
+ h += '<div class="outcome-commits">';
24467
+ replay.commits.forEach(function(c){ h += '<div class="outcome-cr"><span class="commit-sha">' + esc(c.sha.slice(0,7)) + '</span><span class="commit-msg">' + esc(c.message||'-') + '</span>' + (c.promptId ? '<span class="commit-pl">prompt ' + esc(c.promptId.slice(0,6)) + '</span>' : '') + '</div>'; });
24468
+ h += '</div>';
24469
+ }
24470
+ if (replay.pullRequests.length > 0) {
24471
+ h += '<div class="outcome-prs">';
24472
+ replay.pullRequests.forEach(function(pr){ h += '<div class="outcome-pr"><span class="pr-badge">' + esc(pr.state) + '</span><span class="pr-label">PR #' + pr.prNumber + '</span><span class="pr-repo">' + esc(pr.repo) + '</span>' + (pr.url ? '<a class="pr-link" href="' + esc(pr.url) + '" target="_blank" rel="noopener noreferrer">' + esc(pr.url) + '</a>' : '') + '</div>'; });
24473
+ h += '</div>';
24474
+ }
24475
+ h += '</div>';
24476
+ }
24477
+ // prompt groups
24478
+ var groups = buildPromptGroups(replay.timeline, replay.commits);
24479
+ if (groups.length === 0) {
24480
+ h += '<div class="empty">No prompts in this session.</div>';
24481
+ } else {
24482
+ groups.forEach(function(g, i) { h += renderPromptCard(g, i + 1); });
24483
+ }
24484
+ area.innerHTML = h;
24485
+ }
24317
24486
 
24318
- function bindReplayButtons() {
24319
- const buttons = sessionsBody.querySelectorAll(".replay-button");
24320
- buttons.forEach((button) => {
24321
- button.addEventListener("click", () => {
24322
- const sessionId = button.getAttribute("data-session-id");
24323
- if (sessionId === null || sessionId.length === 0) {
24324
- return;
24325
- }
24326
- void loadSessionReplay(sessionId);
24327
- });
24328
- });
24329
- }
24487
+ function parseSummary(v) {
24488
+ var r = asRec(v); if (!r) return null;
24489
+ var sid = readStr(r,'sessionId'), uid = readStr(r,'userId'), sa = readStr(r,'startedAt');
24490
+ if (!sid || !uid || !sa) return null;
24491
+ return {sessionId:sid,userId:uid,gitRepo:typeof r.gitRepo==='string'?r.gitRepo:null,gitBranch:typeof r.gitBranch==='string'?r.gitBranch:null,startedAt:sa,endedAt:typeof r.endedAt==='string'?r.endedAt:null,promptCount:readNum(r,'promptCount')||0,toolCallCount:readNum(r,'toolCallCount')||0,totalCostUsd:readNum(r,'totalCostUsd')||0,commitCount:readNum(r,'commitCount')||0,linesAdded:readNum(r,'linesAdded')||0,linesRemoved:readNum(r,'linesRemoved')||0};
24492
+ }
24330
24493
 
24331
- function renderSessions(sessions) {
24332
- if (!Array.isArray(sessions) || sessions.length === 0) {
24333
- sessionsBody.innerHTML = '<tr><td colspan="6">No sessions yet.</td></tr>';
24334
- sessionsMetric.textContent = "0";
24335
- costMetric.textContent = "$0.00";
24336
- latestMetric.textContent = "-";
24337
- replayMeta.classList.remove("error");
24338
- replayMeta.textContent = "Select a session to inspect timeline events.";
24339
- setReplayPlaceholder("No session selected.");
24340
- return;
24341
- }
24494
+ function parseCostPoint(v) {
24495
+ var r = asRec(v); if (!r) return null;
24496
+ var d = readStr(r,'date'); if (!d) return null;
24497
+ return {date:d,totalCostUsd:readNum(r,'totalCostUsd')||0,sessionCount:readNum(r,'sessionCount')||0,promptCount:readNum(r,'promptCount')||0,toolCallCount:readNum(r,'toolCallCount')||0};
24498
+ }
24342
24499
 
24343
- const rows = sessions.map((session) => {
24344
- const repo = session.gitRepo ?? "-";
24345
- const cost = typeof session.totalCostUsd === "number" ? session.totalCostUsd : 0;
24346
- return "<tr>"
24347
- + "<td>" + escapeHtml(session.sessionId) + "</td>"
24348
- + "<td>" + escapeHtml(session.userId) + "</td>"
24349
- + "<td>" + escapeHtml(repo) + "</td>"
24350
- + "<td>" + escapeHtml(formatDate(session.startedAt)) + "</td>"
24351
- + "<td>" + escapeHtml(formatMoney(cost)) + "</td>"
24352
- + '<td><button type="button" class="replay-button" data-session-id="'
24353
- + escapeHtml(session.sessionId)
24354
- + '">View</button></td>'
24355
- + "</tr>";
24356
- }).join("");
24357
- sessionsBody.innerHTML = rows;
24358
- bindReplayButtons();
24500
+ function sortLatest(arr) { return arr.slice().sort(function(a,b){ return Date.parse(ensureUtc(b.startedAt)) - Date.parse(ensureUtc(a.startedAt)); }); }
24359
24501
 
24360
- const totalCost = sessions.reduce((sum, session) => {
24361
- const value = typeof session.totalCostUsd === "number" ? session.totalCostUsd : 0;
24362
- return sum + value;
24363
- }, 0);
24364
- const latest = sessions
24365
- .map((session) => session.startedAt)
24366
- .filter((value) => typeof value === "string")
24367
- .sort()
24368
- .at(-1) ?? "-";
24502
+ function setSessions(raw) {
24503
+ sessions = sortLatest(raw.map(parseSummary).filter(Boolean));
24504
+ renderMetrics();
24505
+ renderSessions();
24506
+ if (sessions.length > 0 && (!selectedId || !sessions.some(function(s){return s.sessionId===selectedId;}))) {
24507
+ selectSession(sessions[0].sessionId);
24508
+ }
24509
+ }
24369
24510
 
24370
- sessionsMetric.textContent = String(sessions.length);
24371
- costMetric.textContent = formatMoney(totalCost);
24372
- latestMetric.textContent = latest === "-" ? "-" : formatDate(latest);
24511
+ window.selectSession = function(sid) {
24512
+ selectedId = sid;
24513
+ renderSessions();
24514
+ loadReplay(sid);
24515
+ };
24373
24516
 
24374
- const selectedInList = selectedSessionId !== null && sessions.some((session) => session.sessionId === selectedSessionId);
24375
- if (!selectedInList && sessions[0]?.sessionId !== undefined) {
24376
- void loadSessionReplay(sessions[0].sessionId);
24377
- }
24378
- }
24517
+ function loadReplay(sid) {
24518
+ replay = null;
24519
+ renderReplay();
24520
+ fetch('/api/session/' + encodeURIComponent(sid), {cache:'no-store'}).then(function(r){
24521
+ if (r.status === 404) return null;
24522
+ if (!r.ok) throw new Error('replay failed (' + r.status + ')');
24523
+ return r.json();
24524
+ }).then(function(payload){
24525
+ if (!payload) return;
24526
+ var p = asRec(payload);
24527
+ if (!p || readStr(p,'status') !== 'ok') return;
24528
+ replay = parseReplay(p.session);
24529
+ renderReplay();
24530
+ }).catch(function(e){ document.getElementById('replay-area').innerHTML = '<div class="empty" style="color:var(--red)">' + esc(String(e)) + '</div>'; });
24531
+ }
24379
24532
 
24380
- async function loadSessions() {
24381
- try {
24382
- const response = await fetch("/api/sessions");
24383
- if (!response.ok) {
24384
- throw new Error("dashboard bridge failed with status " + response.status);
24385
- }
24386
- const payload = await response.json();
24387
- if (payload?.status !== "ok" || !Array.isArray(payload.sessions)) {
24388
- throw new Error("unexpected payload format");
24389
- }
24390
- renderSessions(payload.sessions);
24391
- status.classList.remove("error");
24392
- status.textContent = "Data source connected.";
24393
- } catch (error) {
24394
- sessionsBody.innerHTML = '<tr><td colspan="6">Failed to load sessions.</td></tr>';
24395
- status.classList.add("error");
24396
- status.textContent = String(error);
24397
- }
24398
- }
24533
+ function loadSnapshot() {
24534
+ return Promise.all([
24535
+ fetch('/api/sessions', {cache:'no-store'}),
24536
+ fetch('/api/analytics/cost/daily', {cache:'no-store'}).catch(function(){return null;})
24537
+ ]).then(function(results){
24538
+ var sr = results[0], cr = results[1];
24539
+ if (!sr.ok) throw new Error('sessions failed (' + sr.status + ')');
24540
+ return Promise.all([sr.json(), cr && cr.ok ? cr.json() : {points:[]}]);
24541
+ }).then(function(data){
24542
+ var sp = asRec(data[0]), cp = asRec(data[1]);
24543
+ if (sp && Array.isArray(sp.sessions)) setSessions(sp.sessions);
24544
+ if (cp && Array.isArray(cp.points)) { costPoints = cp.points.map(parseCostPoint).filter(Boolean); renderCostChart(); }
24545
+ document.getElementById('status').className = 'status-banner';
24546
+ }).catch(function(e){
24547
+ document.getElementById('status').className = 'status-banner warning';
24548
+ document.getElementById('status').textContent = String(e);
24549
+ });
24550
+ }
24399
24551
 
24400
- function startLiveSessionsStream() {
24401
- if (typeof EventSource === "undefined") {
24402
- status.classList.remove("error");
24403
- status.textContent = "Live stream unavailable. Snapshot mode enabled.";
24404
- return;
24552
+ function boot() {
24553
+ loadSnapshot().then(function(){
24554
+ if (typeof EventSource !== 'undefined') {
24555
+ var es = new EventSource('/api/sessions/stream');
24556
+ es.addEventListener('sessions', function(event) {
24557
+ var p = asRec(JSON.parse(event.data));
24558
+ if (p && Array.isArray(p.sessions)) {
24559
+ setSessions(p.sessions);
24560
+ document.getElementById('stream-label').textContent = 'live';
24561
+ document.getElementById('status').className = 'status-banner';
24562
+ document.getElementById('status').textContent = 'Live';
24405
24563
  }
24564
+ });
24565
+ es.addEventListener('bridge_error', function(event) {
24566
+ document.getElementById('status').className = 'status-banner warning';
24567
+ document.getElementById('status').textContent = 'Bridge error: ' + event.data;
24568
+ });
24569
+ es.onerror = function() {
24570
+ document.getElementById('stream-label').textContent = 'polling';
24571
+ document.getElementById('status').textContent = 'Polling';
24572
+ };
24573
+ } else {
24574
+ document.getElementById('stream-label').textContent = 'polling';
24575
+ }
24576
+ setInterval(function(){ loadSnapshot(); }, 15000);
24577
+ });
24578
+ }
24406
24579
 
24407
- status.classList.remove("error");
24408
- status.textContent = "Connecting to live session stream...";
24409
-
24410
- const stream = new EventSource("/api/sessions/stream");
24411
- stream.addEventListener("sessions", (event) => {
24412
- try {
24413
- const payload = JSON.parse(event.data);
24414
- if (payload?.status !== "ok" || !Array.isArray(payload.sessions)) {
24415
- throw new Error("unexpected stream payload");
24416
- }
24417
- renderSessions(payload.sessions);
24418
- status.classList.remove("error");
24419
- status.textContent = "Live session stream connected.";
24420
- } catch (error) {
24421
- status.classList.add("error");
24422
- status.textContent = String(error);
24423
- }
24424
- });
24425
-
24426
- stream.addEventListener("bridge_error", (event) => {
24427
- status.classList.add("error");
24428
- status.textContent = "Bridge error: " + event.data;
24429
- });
24430
-
24431
- stream.onerror = () => {
24432
- status.classList.add("error");
24433
- status.textContent = "Live stream disconnected. Retrying...";
24434
- };
24435
- }
24436
-
24437
- async function boot() {
24438
- await loadSessions();
24439
- startLiveSessionsStream();
24440
- }
24441
-
24442
- void boot();
24443
- </script>
24444
- </body>
24580
+ boot();
24581
+ })();
24582
+ </script>
24583
+ </body>
24445
24584
  </html>`;
24446
24585
  }
24447
24586
 
@@ -24771,6 +24910,19 @@ async function startDashboardServer(options = {}) {
24771
24910
  startSessionsSseBridge(req, res, sessionsProvider);
24772
24911
  return;
24773
24912
  }
24913
+ if (pathname === "/api/analytics/cost/daily") {
24914
+ void fetch(`${apiBaseUrl}/v1/analytics/cost/daily`).then(async (apiResponse) => {
24915
+ if (!apiResponse.ok) {
24916
+ sendJson(res, 502, { status: "error", message: `api returned status ${String(apiResponse.status)}` });
24917
+ return;
24918
+ }
24919
+ const payload = await apiResponse.json();
24920
+ sendJson(res, 200, payload);
24921
+ }).catch((error) => {
24922
+ sendJson(res, 502, { status: "error", message: `failed to fetch daily cost: ${String(error)}` });
24923
+ });
24924
+ return;
24925
+ }
24774
24926
  if (segments.length === 3 && segments[0] === "api" && segments[1] === "session") {
24775
24927
  const encodedSessionId = segments[2];
24776
24928
  let sessionId = "";
@@ -25556,6 +25708,12 @@ function toNonNegativeInteger2(value) {
25556
25708
  }
25557
25709
  return Math.floor(value);
25558
25710
  }
25711
+ function toNonNegativeDecimal(value) {
25712
+ if (!Number.isFinite(value) || value <= 0) {
25713
+ return 0;
25714
+ }
25715
+ return Number(value.toFixed(6));
25716
+ }
25559
25717
  function toSessionStatus(trace) {
25560
25718
  return trace.endedAt !== void 0 ? "completed" : "active";
25561
25719
  }
@@ -25579,7 +25737,7 @@ function toPostgresCommitRow(trace, commit) {
25579
25737
  message: toNullableString(commit.message),
25580
25738
  lines_added: toNonNegativeInteger2(commit.linesAdded),
25581
25739
  lines_removed: toNonNegativeInteger2(commit.linesRemoved),
25582
- chain_cost_usd: 0,
25740
+ chain_cost_usd: toNonNegativeDecimal(trace.metrics.totalCostUsd),
25583
25741
  committed_at: toNullableString(commit.committedAt)
25584
25742
  };
25585
25743
  }
@@ -27809,6 +27967,92 @@ var InMemoryRuntimePersistence = class extends WriterBackedRuntimePersistence {
27809
27967
  }
27810
27968
  };
27811
27969
 
27970
+ // packages/schema/src/pricing.ts
27971
+ var MODEL_PRICING = [
27972
+ // Opus 4 / 4.6
27973
+ ["claude-opus-4", {
27974
+ inputPerToken: 15 / 1e6,
27975
+ outputPerToken: 75 / 1e6,
27976
+ cacheReadPerToken: 1.5 / 1e6,
27977
+ cacheWritePerToken: 18.75 / 1e6
27978
+ }],
27979
+ // Sonnet 4 / 4.6
27980
+ ["claude-sonnet-4", {
27981
+ inputPerToken: 3 / 1e6,
27982
+ outputPerToken: 15 / 1e6,
27983
+ cacheReadPerToken: 0.3 / 1e6,
27984
+ cacheWritePerToken: 3.75 / 1e6
27985
+ }],
27986
+ // Haiku 4.5
27987
+ ["claude-haiku-4", {
27988
+ inputPerToken: 0.8 / 1e6,
27989
+ outputPerToken: 4 / 1e6,
27990
+ cacheReadPerToken: 0.08 / 1e6,
27991
+ cacheWritePerToken: 1 / 1e6
27992
+ }],
27993
+ // Claude 3.5 Sonnet
27994
+ ["claude-3-5-sonnet", {
27995
+ inputPerToken: 3 / 1e6,
27996
+ outputPerToken: 15 / 1e6,
27997
+ cacheReadPerToken: 0.3 / 1e6,
27998
+ cacheWritePerToken: 3.75 / 1e6
27999
+ }],
28000
+ // Claude 3.5 Haiku
28001
+ ["claude-3-5-haiku", {
28002
+ inputPerToken: 0.8 / 1e6,
28003
+ outputPerToken: 4 / 1e6,
28004
+ cacheReadPerToken: 0.08 / 1e6,
28005
+ cacheWritePerToken: 1 / 1e6
28006
+ }],
28007
+ // Claude 3 Opus
28008
+ ["claude-3-opus", {
28009
+ inputPerToken: 15 / 1e6,
28010
+ outputPerToken: 75 / 1e6,
28011
+ cacheReadPerToken: 1.5 / 1e6,
28012
+ cacheWritePerToken: 18.75 / 1e6
28013
+ }],
28014
+ // Claude 3 Sonnet
28015
+ ["claude-3-sonnet", {
28016
+ inputPerToken: 3 / 1e6,
28017
+ outputPerToken: 15 / 1e6,
28018
+ cacheReadPerToken: 0.3 / 1e6,
28019
+ cacheWritePerToken: 3.75 / 1e6
28020
+ }],
28021
+ // Claude 3 Haiku
28022
+ ["claude-3-haiku", {
28023
+ inputPerToken: 0.25 / 1e6,
28024
+ outputPerToken: 1.25 / 1e6,
28025
+ cacheReadPerToken: 0.03 / 1e6,
28026
+ cacheWritePerToken: 0.3 / 1e6
28027
+ }]
28028
+ ];
28029
+ var SORTED_PRICING = [...MODEL_PRICING].sort((a, b) => b[0].length - a[0].length);
28030
+ function lookupModelPricing(model) {
28031
+ const normalized = model.toLowerCase();
28032
+ for (const [pattern, pricing] of SORTED_PRICING) {
28033
+ if (normalized.startsWith(pattern)) {
28034
+ return pricing;
28035
+ }
28036
+ }
28037
+ return void 0;
28038
+ }
28039
+ function calculateCostUsd(input) {
28040
+ if (input.model === void 0) {
28041
+ return 0;
28042
+ }
28043
+ const pricing = lookupModelPricing(input.model);
28044
+ if (pricing === void 0) {
28045
+ return 0;
28046
+ }
28047
+ const totalInput = Math.max(0, input.inputTokens);
28048
+ const output = Math.max(0, input.outputTokens);
28049
+ const cacheRead = Math.max(0, input.cacheReadTokens ?? 0);
28050
+ const cacheWrite = Math.max(0, input.cacheWriteTokens ?? 0);
28051
+ const regularInput = Math.max(0, totalInput - cacheRead - cacheWrite);
28052
+ const cost = regularInput * pricing.inputPerToken + cacheRead * pricing.cacheReadPerToken + cacheWrite * pricing.cacheWritePerToken + output * pricing.outputPerToken;
28053
+ return Number(cost.toFixed(6));
28054
+ }
28055
+
27812
28056
  // packages/runtime/src/projector.ts
27813
28057
  function asRecord5(value) {
27814
28058
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -27893,11 +28137,33 @@ function buildNormalizedDetails(payload) {
27893
28137
  addCamelAlias(normalized, "file_path", "filePath");
27894
28138
  return normalized;
27895
28139
  }
28140
+ function computeEventCost(payload) {
28141
+ const explicit = readNumber3(payload, ["cost_usd", "costUsd"]);
28142
+ if (explicit !== void 0 && explicit > 0) {
28143
+ return explicit;
28144
+ }
28145
+ const model = readString4(payload, ["model"]);
28146
+ const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]);
28147
+ const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]);
28148
+ if (model === void 0 || inputTokens === void 0 || outputTokens === void 0) {
28149
+ return explicit;
28150
+ }
28151
+ const cacheReadTokens = readNumber3(payload, ["cache_read_tokens", "cacheReadTokens", "cache_read_input_tokens"]);
28152
+ const cacheWriteTokens = readNumber3(payload, ["cache_write_tokens", "cacheWriteTokens", "cache_creation_input_tokens"]);
28153
+ const calculated = calculateCostUsd({
28154
+ model,
28155
+ inputTokens,
28156
+ outputTokens,
28157
+ cacheReadTokens: cacheReadTokens ?? 0,
28158
+ cacheWriteTokens: cacheWriteTokens ?? 0
28159
+ });
28160
+ return calculated > 0 ? calculated : explicit;
28161
+ }
27896
28162
  function toTimelineEvent(envelope) {
27897
28163
  const payload = asRecord5(envelope.payload);
27898
28164
  const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]);
27899
28165
  const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]);
27900
- const costUsd = readNumber3(payload, ["cost_usd", "costUsd"]);
28166
+ const costUsd = computeEventCost(payload);
27901
28167
  const details = buildNormalizedDetails(envelope.payload);
27902
28168
  return {
27903
28169
  id: envelope.eventId,
@@ -27979,7 +28245,7 @@ function toUpdatedTrace(existing, envelope) {
27979
28245
  const mergedTimeline = [...existing.timeline, timelineEvent];
27980
28246
  const endedAt = shouldMarkEnded(envelope.eventType) ? envelope.eventTimestamp : existing.endedAt;
27981
28247
  const latestTime = endedAt ?? envelope.eventTimestamp;
27982
- const cost = readNumber3(payload, ["cost_usd", "costUsd"]) ?? 0;
28248
+ const cost = computeEventCost(payload) ?? 0;
27983
28249
  const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]) ?? 0;
27984
28250
  const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]) ?? 0;
27985
28251
  const linesAdded = readNumber3(payload, ["lines_added", "linesAdded"]) ?? 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trace",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Self-hosted observability for AI coding agents. One command, zero config.",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {