@jmanuelcorral/openteam 0.1.20 → 0.1.22

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 (45) hide show
  1. package/README.md +29 -17
  2. package/dist/cli/dashboardServe.d.ts +52 -0
  3. package/dist/cli/dashboardServe.d.ts.map +1 -0
  4. package/dist/cli.js +1816 -499
  5. package/dist/commands/dashboard.d.ts +4 -4
  6. package/dist/commands/dashboard.d.ts.map +1 -1
  7. package/dist/commands/dispatch.d.ts.map +1 -1
  8. package/dist/commands/orchestratorAgent.d.ts.map +1 -1
  9. package/dist/contract/opencode.d.ts +1 -1
  10. package/dist/contract/opencode.d.ts.map +1 -1
  11. package/dist/dashboard/render.d.ts.map +1 -1
  12. package/dist/dashboard/state.d.ts +14 -6
  13. package/dist/dashboard/state.d.ts.map +1 -1
  14. package/dist/dashboard/types.d.ts +14 -3
  15. package/dist/dashboard/types.d.ts.map +1 -1
  16. package/dist/index.d.ts +8 -9
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +476 -780
  19. package/dist/orchestrator/coordinator.d.ts +14 -2
  20. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  21. package/dist/plugin/capture.d.ts +46 -0
  22. package/dist/plugin/capture.d.ts.map +1 -0
  23. package/dist/plugin/hooks.d.ts +2 -2
  24. package/dist/plugin/hooks.d.ts.map +1 -1
  25. package/dist/plugin/toolcalls.d.ts +21 -0
  26. package/dist/plugin/toolcalls.d.ts.map +1 -0
  27. package/dist/telemetry/aggregate.d.ts +89 -0
  28. package/dist/telemetry/aggregate.d.ts.map +1 -0
  29. package/dist/telemetry/decisions.d.ts +25 -0
  30. package/dist/telemetry/decisions.d.ts.map +1 -0
  31. package/dist/telemetry/eventLog.d.ts +63 -0
  32. package/dist/telemetry/eventLog.d.ts.map +1 -0
  33. package/dist/telemetry/events.d.ts +217 -0
  34. package/dist/telemetry/events.d.ts.map +1 -0
  35. package/dist/web/git.d.ts +9 -0
  36. package/dist/web/git.d.ts.map +1 -0
  37. package/dist/web/server.d.ts +8 -7
  38. package/dist/web/server.d.ts.map +1 -1
  39. package/dist/web/snapshot.d.ts +13 -7
  40. package/dist/web/snapshot.d.ts.map +1 -1
  41. package/dist/web/start.d.ts +11 -8
  42. package/dist/web/start.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/dist/web/activity.d.ts +0 -12
  45. package/dist/web/activity.d.ts.map +0 -1
package/dist/cli.js CHANGED
@@ -2,10 +2,1659 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execFile } from "node:child_process";
5
- import { access, mkdir as mkdir2, readdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
5
+ import { access, mkdir as mkdir2, readdir, readFile, writeFile as writeFile2 } from "node:fs/promises";
6
6
  import { dirname as dirname2, join as join2 } from "node:path";
7
7
  import { promisify } from "node:util";
8
8
 
9
+ // src/web/start.ts
10
+ import { watch as fsWatch } from "node:fs";
11
+
12
+ // src/web/paths.ts
13
+ var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
14
+ var DEFAULT_DECISIONS_PATH = ".opencode/openteam-decisions.md";
15
+
16
+ // src/web/server.ts
17
+ import {
18
+ createServer as createHttpServer
19
+ } from "node:http";
20
+
21
+ // src/dashboard/render.ts
22
+ var TIERS = [
23
+ "trivial",
24
+ "simple",
25
+ "moderate",
26
+ "hard"
27
+ ];
28
+ function escapeHtml(value) {
29
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
30
+ }
31
+ function usd(value) {
32
+ return `$${value.toFixed(5)}`;
33
+ }
34
+ function pct(value) {
35
+ return `${value.toFixed(2)}%`;
36
+ }
37
+ function ms(value) {
38
+ return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`;
39
+ }
40
+ function avgMs(stats) {
41
+ return stats.count === 0 ? "—" : ms(stats.totalDurationMs / stats.count);
42
+ }
43
+ function agentModel(agent) {
44
+ return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
45
+ }
46
+ function summaryPanel(cost) {
47
+ return [
48
+ '<section class="panel" id="panel-summary">',
49
+ "<h2>Resumen de routing</h2>",
50
+ '<div class="grid">',
51
+ `<div class="stat"><span class="k">Decisiones</span><span class="v">${cost.count}</span></div>`,
52
+ `<div class="stat"><span class="k">Local</span><span class="v">${cost.localCount}</span></div>`,
53
+ `<div class="stat"><span class="k">Frontier</span><span class="v">${cost.frontierCount}</span></div>`,
54
+ `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(cost.totalEstimatedUSD)}</span></div>`,
55
+ `<div class="stat"><span class="k">Baseline</span><span class="v">${usd(cost.totalBaselineUSD)}</span></div>`,
56
+ `<div class="stat"><span class="k">Ahorro</span><span class="v good">${usd(cost.totalSavingsUSD)} (${pct(cost.savingsPct)})</span></div>`,
57
+ `<div class="stat"><span class="k">Tokens in</span><span class="v">${cost.tokensIn}</span></div>`,
58
+ `<div class="stat"><span class="k">Tokens out</span><span class="v">${cost.tokensOut}</span></div>`,
59
+ "</div>",
60
+ "</section>"
61
+ ].join("");
62
+ }
63
+ function tierPanel(cost) {
64
+ const rows = TIERS.map((tier) => {
65
+ const summary = cost.byTier[tier];
66
+ return `<tr><td>${tier}</td><td>${summary.count}</td><td>${usd(summary.estimatedUSD)}</td><td class="good">${usd(summary.savingsUSD)}</td></tr>`;
67
+ }).join("");
68
+ return [
69
+ '<section class="panel" id="panel-tier">',
70
+ "<h2>Por tier</h2>",
71
+ "<table><thead><tr><th>Tier</th><th>Nº</th><th>Coste</th><th>Ahorro</th></tr></thead>",
72
+ `<tbody>${rows}</tbody></table>`,
73
+ "</section>"
74
+ ].join("");
75
+ }
76
+ function loopItemRow(item) {
77
+ const box = item.done ? "☑" : "☐";
78
+ const cls = item.done ? "done" : "open";
79
+ const who = item.assignee === undefined ? "" : `<span class="who">@${escapeHtml(item.assignee)}</span> `;
80
+ return `<li class="${cls}"><span class="box">${box}</span> ${who}${escapeHtml(item.text)}</li>`;
81
+ }
82
+ function loopPanel(loop) {
83
+ if (loop === undefined) {
84
+ return [
85
+ '<section class="panel" id="panel-loop">',
86
+ "<h2>Loop</h2>",
87
+ '<p class="muted">Sin backlog activo (no hay <code>openteam-backlog.md</code>).</p>',
88
+ "</section>"
89
+ ].join("");
90
+ }
91
+ const items = loop.items.map(loopItemRow).join("");
92
+ const commit = loop.lastCommit === undefined ? "" : `<p class="muted">Último commit: <code>${escapeHtml(loop.lastCommit.hash)}</code> ${escapeHtml(loop.lastCommit.subject)}</p>`;
93
+ return [
94
+ '<section class="panel" id="panel-loop">',
95
+ "<h2>Loop</h2>",
96
+ `<div class="progress" role="progressbar" aria-valuenow="${loop.progressPct}" aria-valuemin="0" aria-valuemax="100"><div class="bar" style="width:${loop.progressPct}%"></div></div>`,
97
+ `<p class="muted">${loop.done}/${loop.total} completados · ${loop.open} abiertos · ${loop.progressPct}%</p>`,
98
+ `<ul class="items">${items}</ul>`,
99
+ commit,
100
+ "</section>"
101
+ ].join("");
102
+ }
103
+ function teamPanel(team) {
104
+ if (team.length === 0) {
105
+ return [
106
+ '<section class="panel" id="panel-team">',
107
+ "<h2>Equipo</h2>",
108
+ '<p class="muted">Sin agentes en <code>.opencode/agent/</code>.</p>',
109
+ "</section>"
110
+ ].join("");
111
+ }
112
+ const rows = team.map((agent) => `<tr><td>${escapeHtml(agent.name)}</td><td>${escapeHtml(agent.mode)}</td><td>${escapeHtml(agentModel(agent))}</td></tr>`).join("");
113
+ return [
114
+ '<section class="panel" id="panel-team">',
115
+ "<h2>Equipo</h2>",
116
+ "<table><thead><tr><th>Agente</th><th>Modo</th><th>LLM</th></tr></thead>",
117
+ `<tbody>${rows}</tbody></table>`,
118
+ "</section>"
119
+ ].join("");
120
+ }
121
+ function routeRow(route) {
122
+ return `<tr><td>${route.tier}</td><td class="${route.routeKind}">${route.routeKind}</td><td>${escapeHtml(route.model)}</td><td class="hash">${escapeHtml(route.promptHash)}</td><td>${usd(route.estimatedCostUSD)}</td><td class="good">${usd(route.estimatedSavingsUSD)}</td></tr>`;
123
+ }
124
+ function routesPanel(routes) {
125
+ if (routes.length === 0) {
126
+ return [
127
+ '<section class="panel" id="panel-routes">',
128
+ "<h2>Decisiones recientes</h2>",
129
+ '<p class="muted">Sin decisiones de routing todavía.</p>',
130
+ "</section>"
131
+ ].join("");
132
+ }
133
+ const rows = routes.map(routeRow).join("");
134
+ return [
135
+ '<section class="panel" id="panel-routes">',
136
+ "<h2>Decisiones recientes</h2>",
137
+ "<table><thead><tr><th>Tier</th><th>Ruta</th><th>Modelo</th><th>Prompt (hash)</th><th>Coste</th><th>Ahorro</th></tr></thead>",
138
+ `<tbody>${rows}</tbody></table>`,
139
+ "</section>"
140
+ ].join("");
141
+ }
142
+ function activityRow(entry) {
143
+ const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
144
+ return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
145
+ }
146
+ function activityPanel(activity) {
147
+ if (activity.length === 0) {
148
+ return [
149
+ '<section class="panel" id="panel-activity">',
150
+ "<h2>Actividad</h2>",
151
+ '<p class="muted">Sin actividad registrada.</p>',
152
+ "</section>"
153
+ ].join("");
154
+ }
155
+ const rows = activity.map(activityRow).join("");
156
+ return [
157
+ '<section class="panel" id="panel-activity">',
158
+ "<h2>Actividad</h2>",
159
+ `<ul class="timeline">${rows}</ul>`,
160
+ "</section>"
161
+ ].join("");
162
+ }
163
+ function totalsPanel(totals) {
164
+ const t = totals.toolcalls;
165
+ return [
166
+ '<section class="panel" id="panel-totals">',
167
+ "<h2>Telemetría real (todas las sesiones)</h2>",
168
+ '<div class="grid">',
169
+ `<div class="stat"><span class="k">Sesiones</span><span class="v">${totals.sessions}</span></div>`,
170
+ `<div class="stat"><span class="k">Mensajes</span><span class="v">${totals.messageCount}</span></div>`,
171
+ `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(totals.realCostUSD)}</span></div>`,
172
+ `<div class="stat"><span class="k">Ahorro est.</span><span class="v good">${usd(totals.estimatedSavingsUSD)}</span></div>`,
173
+ `<div class="stat"><span class="k">Tokens in</span><span class="v">${totals.tokensIn}</span></div>`,
174
+ `<div class="stat"><span class="k">Tokens out</span><span class="v">${totals.tokensOut}</span></div>`,
175
+ `<div class="stat"><span class="k">Reasoning</span><span class="v">${totals.tokensReasoning}</span></div>`,
176
+ `<div class="stat"><span class="k">Cache r/w</span><span class="v">${totals.tokensCacheRead}/${totals.tokensCacheWrite}</span></div>`,
177
+ `<div class="stat"><span class="k">Toolcalls</span><span class="v">${t.count} <span class="good">${t.ok}✓</span> <span class="frontier">${t.failed}✗</span></span></div>`,
178
+ `<div class="stat"><span class="k">Dur. media tool</span><span class="v">${avgMs(t)}</span></div>`,
179
+ "</div>",
180
+ "</section>"
181
+ ].join("");
182
+ }
183
+ function sessionRow(session) {
184
+ const models = session.models.map(escapeHtml).join("<br>") || "—";
185
+ return [
186
+ "<tr>",
187
+ `<td class="hash">${escapeHtml(session.sessionID)}</td>`,
188
+ `<td>${models}</td>`,
189
+ `<td><span class="local">${session.localCount}</span>/<span class="frontier">${session.frontierCount}</span></td>`,
190
+ `<td>${usd(session.realCostUSD)}</td>`,
191
+ `<td>${session.tokensIn}/${session.tokensOut}</td>`,
192
+ `<td>${session.toolcalls.count} (${avgMs(session.toolcalls)})</td>`,
193
+ "</tr>"
194
+ ].join("");
195
+ }
196
+ function sessionsPanel(sessions) {
197
+ if (sessions.length === 0) {
198
+ return [
199
+ '<section class="panel" id="panel-sessions">',
200
+ "<h2>Sesiones</h2>",
201
+ '<p class="muted">Sin sesiones registradas todavía.</p>',
202
+ "</section>"
203
+ ].join("");
204
+ }
205
+ const rows = sessions.map(sessionRow).join("");
206
+ return [
207
+ '<section class="panel" id="panel-sessions">',
208
+ "<h2>Sesiones</h2>",
209
+ "<table><thead><tr><th>Sesión</th><th>Modelos</th><th>L/F</th><th>Coste</th><th>Tok in/out</th><th>Tools</th></tr></thead>",
210
+ `<tbody>${rows}</tbody></table>`,
211
+ "</section>"
212
+ ].join("");
213
+ }
214
+ function decisionRow(decision) {
215
+ const who = decision.agent === undefined ? "" : `<span class="who">${escapeHtml(decision.agent)}</span> `;
216
+ const tags = decision.tags === undefined || decision.tags.length === 0 ? "" : ` ${decision.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
217
+ return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision.summary)}${tags}</li>`;
218
+ }
219
+ function decisionsPanel(decisions) {
220
+ if (decisions.length === 0) {
221
+ return [
222
+ '<section class="panel" id="panel-decisions">',
223
+ "<h2>Decisiones</h2>",
224
+ '<p class="muted">Sin decisiones registradas (el scribe escribe en <code>openteam-decisions.md</code>).</p>',
225
+ "</section>"
226
+ ].join("");
227
+ }
228
+ const rows = decisions.map(decisionRow).join("");
229
+ return [
230
+ '<section class="panel" id="panel-decisions">',
231
+ "<h2>Decisiones</h2>",
232
+ `<ul class="timeline">${rows}</ul>`,
233
+ "</section>"
234
+ ].join("");
235
+ }
236
+ function meetingRow(meeting) {
237
+ const roles = meeting.roles.map(escapeHtml).join(", ");
238
+ const purpose = meeting.purpose === undefined ? "" : `: ${escapeHtml(meeting.purpose)}`;
239
+ return `<li class="agent"><span class="kind">reunión</span> <span class="who">${roles}</span>${purpose}</li>`;
240
+ }
241
+ function meetingsPanel(meetings) {
242
+ if (meetings.length === 0) {
243
+ return [
244
+ '<section class="panel" id="panel-meetings">',
245
+ "<h2>Reuniones</h2>",
246
+ '<p class="muted">Sin reuniones multi-agente todavía.</p>',
247
+ "</section>"
248
+ ].join("");
249
+ }
250
+ const rows = meetings.map(meetingRow).join("");
251
+ return [
252
+ '<section class="panel" id="panel-meetings">',
253
+ "<h2>Reuniones</h2>",
254
+ `<ul class="timeline">${rows}</ul>`,
255
+ "</section>"
256
+ ].join("");
257
+ }
258
+ var STYLE = `
259
+ :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
260
+ *{box-sizing:border-box}
261
+ body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}
262
+ header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}
263
+ header h1{font-size:18px;margin:0}
264
+ header .meta{color:var(--muted);font-size:12px}
265
+ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;padding:24px}
266
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
267
+ .panel h2{font-size:14px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
268
+ .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
269
+ .stat{display:flex;flex-direction:column;gap:2px}
270
+ .stat .k{color:var(--muted);font-size:12px}
271
+ .stat .v{font-size:18px;font-weight:600}
272
+ .good{color:var(--good)}
273
+ .local{color:var(--local)}
274
+ .frontier{color:var(--frontier)}
275
+ .muted{color:var(--muted)}
276
+ table{width:100%;border-collapse:collapse;font-size:13px}
277
+ th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)}
278
+ th{color:var(--muted);font-weight:600}
279
+ .hash{font-family:ui-monospace,Consolas,monospace;color:var(--muted)}
280
+ .progress{height:10px;background:#0b0d11;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
281
+ .progress .bar{height:100%;background:var(--good)}
282
+ ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}
283
+ ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
284
+ ul.items li.done{color:var(--muted)}
285
+ ul.items .box{font-family:monospace}
286
+ .who{color:var(--local)}
287
+ .tag{color:var(--frontier);font-size:12px}
288
+ .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
289
+ code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
290
+ `;
291
+ var CLIENT_JS = `
292
+ (function(){
293
+ function reloadIfChanged(prev){
294
+ fetch('/api/state').then(function(r){return r.json()}).then(function(s){
295
+ if(s.generatedAt!==prev){location.reload()}
296
+ }).catch(function(){});
297
+ }
298
+ var current=document.documentElement.getAttribute('data-generated')||'';
299
+ if('EventSource' in window){
300
+ try{
301
+ var es=new EventSource('/events');
302
+ es.addEventListener('state',function(){location.reload()});
303
+ es.onerror=function(){/* fallback below */};
304
+ }catch(e){/* ignore */}
305
+ }
306
+ var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
307
+ setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
308
+ })();
309
+ `;
310
+ function renderDashboardHtml(state, options = {}) {
311
+ const refreshMs = options.refreshMs ?? 2000;
312
+ const body = [
313
+ summaryPanel(state.cost),
314
+ totalsPanel(state.totals),
315
+ sessionsPanel(state.sessions),
316
+ loopPanel(state.loop),
317
+ teamPanel(state.team),
318
+ tierPanel(state.cost),
319
+ routesPanel(state.recentRoutes),
320
+ decisionsPanel(state.decisions),
321
+ meetingsPanel(state.meetings),
322
+ activityPanel(state.activity)
323
+ ].join("");
324
+ const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
325
+ return [
326
+ "<!doctype html>",
327
+ `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}">`,
328
+ "<head>",
329
+ '<meta charset="utf-8">',
330
+ '<meta name="viewport" content="width=device-width,initial-scale=1">',
331
+ "<title>openteam dashboard</title>",
332
+ `<style>${STYLE}</style>`,
333
+ "</head>",
334
+ "<body>",
335
+ "<header>",
336
+ "<h1>openteam dashboard</h1>",
337
+ `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
338
+ "</header>",
339
+ `<main>${body}</main>`,
340
+ `<script>${CLIENT_JS}</script>`,
341
+ "</body>",
342
+ "</html>"
343
+ ].join("");
344
+ }
345
+
346
+ // src/commands/report.ts
347
+ var TIERS2 = [
348
+ "trivial",
349
+ "simple",
350
+ "moderate",
351
+ "hard"
352
+ ];
353
+ function emptyTierSummary() {
354
+ return {
355
+ trivial: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
356
+ simple: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
357
+ moderate: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
358
+ hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
359
+ };
360
+ }
361
+ function summarizeCostRecords(records) {
362
+ const report = {
363
+ count: 0,
364
+ localCount: 0,
365
+ frontierCount: 0,
366
+ totalEstimatedUSD: 0,
367
+ totalBaselineUSD: 0,
368
+ totalSavingsUSD: 0,
369
+ savingsPct: 0,
370
+ tokensIn: 0,
371
+ tokensOut: 0,
372
+ byTier: emptyTierSummary()
373
+ };
374
+ for (const record of records) {
375
+ report.count += 1;
376
+ if (record.routeKind === "local") {
377
+ report.localCount += 1;
378
+ } else {
379
+ report.frontierCount += 1;
380
+ }
381
+ report.totalEstimatedUSD += record.estimatedCostUSD;
382
+ report.totalBaselineUSD += record.baselineCostUSD;
383
+ report.totalSavingsUSD += record.estimatedSavingsUSD;
384
+ report.tokensIn += record.tokensIn ?? 0;
385
+ report.tokensOut += record.tokensOut ?? 0;
386
+ const tier = report.byTier[record.tier];
387
+ tier.count += 1;
388
+ tier.estimatedUSD += record.estimatedCostUSD;
389
+ tier.savingsUSD += record.estimatedSavingsUSD;
390
+ }
391
+ report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
392
+ return report;
393
+ }
394
+ function usd2(value) {
395
+ return `$${value.toFixed(5)}`;
396
+ }
397
+ function renderReport(report) {
398
+ if (report.count === 0) {
399
+ return "openteam report: sin registros de telemetría todavía.";
400
+ }
401
+ const lines = [
402
+ "openteam report:",
403
+ ` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
404
+ ` coste real: ${usd2(report.totalEstimatedUSD)}`,
405
+ ` baseline: ${usd2(report.totalBaselineUSD)}`,
406
+ ` ahorro: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
407
+ ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
408
+ " por tier:"
409
+ ];
410
+ for (const tier of TIERS2) {
411
+ const summary = report.byTier[tier];
412
+ lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd2(summary.estimatedUSD)} · ahorro ${usd2(summary.savingsUSD)}`);
413
+ }
414
+ return lines.join(`
415
+ `);
416
+ }
417
+
418
+ // src/telemetry/aggregate.ts
419
+ function emptyToolcallStats() {
420
+ return { count: 0, ok: 0, failed: 0, totalDurationMs: 0, byTool: {} };
421
+ }
422
+ function emptySession(sessionID, ts) {
423
+ return {
424
+ sessionID,
425
+ firstTs: ts,
426
+ lastTs: ts,
427
+ routeCount: 0,
428
+ localCount: 0,
429
+ frontierCount: 0,
430
+ estimatedCostUSD: 0,
431
+ estimatedSavingsUSD: 0,
432
+ realCostUSD: 0,
433
+ messageCount: 0,
434
+ tokensIn: 0,
435
+ tokensOut: 0,
436
+ tokensReasoning: 0,
437
+ tokensCacheRead: 0,
438
+ tokensCacheWrite: 0,
439
+ toolcalls: emptyToolcallStats(),
440
+ models: [],
441
+ agents: []
442
+ };
443
+ }
444
+ function pushUnique(list, value) {
445
+ if (value !== undefined && !list.includes(value)) {
446
+ list.push(value);
447
+ }
448
+ }
449
+ function applyRoute(session, event) {
450
+ session.routeCount += 1;
451
+ if (event.routeKind === "local") {
452
+ session.localCount += 1;
453
+ } else {
454
+ session.frontierCount += 1;
455
+ }
456
+ session.estimatedCostUSD += event.estimatedCostUSD;
457
+ session.estimatedSavingsUSD += event.estimatedSavingsUSD;
458
+ pushUnique(session.models, `${event.selected.providerID}/${event.selected.modelID}`);
459
+ pushUnique(session.agents, event.agent);
460
+ }
461
+ function applyMessage(session, event) {
462
+ session.messageCount += 1;
463
+ session.realCostUSD += event.costUSD;
464
+ session.tokensIn += event.tokensIn;
465
+ session.tokensOut += event.tokensOut;
466
+ session.tokensReasoning += event.tokensReasoning;
467
+ session.tokensCacheRead += event.tokensCacheRead;
468
+ session.tokensCacheWrite += event.tokensCacheWrite;
469
+ pushUnique(session.models, `${event.providerID}/${event.modelID}`);
470
+ pushUnique(session.agents, event.agent);
471
+ }
472
+ function applyToolUsage(stats, event) {
473
+ stats.count += 1;
474
+ stats.totalDurationMs += event.durationMs;
475
+ if (event.ok) {
476
+ stats.ok += 1;
477
+ } else {
478
+ stats.failed += 1;
479
+ }
480
+ }
481
+ function applyToolcall(session, event) {
482
+ applyToolUsage(session.toolcalls, event);
483
+ const byTool = session.toolcalls.byTool[event.tool] ?? {
484
+ count: 0,
485
+ ok: 0,
486
+ failed: 0,
487
+ totalDurationMs: 0
488
+ };
489
+ applyToolUsage(byTool, event);
490
+ session.toolcalls.byTool[event.tool] = byTool;
491
+ pushUnique(session.agents, event.agent);
492
+ }
493
+ function mergeTool(target, source) {
494
+ target.count += source.count;
495
+ target.ok += source.ok;
496
+ target.failed += source.failed;
497
+ target.totalDurationMs += source.totalDurationMs;
498
+ }
499
+ function accumulateTotals(totals, session) {
500
+ totals.sessions += 1;
501
+ totals.routeCount += session.routeCount;
502
+ totals.localCount += session.localCount;
503
+ totals.frontierCount += session.frontierCount;
504
+ totals.estimatedCostUSD += session.estimatedCostUSD;
505
+ totals.estimatedSavingsUSD += session.estimatedSavingsUSD;
506
+ totals.realCostUSD += session.realCostUSD;
507
+ totals.messageCount += session.messageCount;
508
+ totals.tokensIn += session.tokensIn;
509
+ totals.tokensOut += session.tokensOut;
510
+ totals.tokensReasoning += session.tokensReasoning;
511
+ totals.tokensCacheRead += session.tokensCacheRead;
512
+ totals.tokensCacheWrite += session.tokensCacheWrite;
513
+ mergeTool(totals.toolcalls, session.toolcalls);
514
+ for (const [tool, usage] of Object.entries(session.toolcalls.byTool)) {
515
+ const target = totals.toolcalls.byTool[tool] ?? {
516
+ count: 0,
517
+ ok: 0,
518
+ failed: 0,
519
+ totalDurationMs: 0
520
+ };
521
+ mergeTool(target, usage);
522
+ totals.toolcalls.byTool[tool] = target;
523
+ }
524
+ }
525
+ function toDecision(event) {
526
+ const view = {
527
+ ts: event.ts,
528
+ sessionID: event.sessionID,
529
+ summary: event.summary
530
+ };
531
+ if (event.agent !== undefined) {
532
+ view.agent = event.agent;
533
+ }
534
+ if (event.tags !== undefined) {
535
+ view.tags = [...event.tags];
536
+ }
537
+ return view;
538
+ }
539
+ function toMeeting(event) {
540
+ const view = {
541
+ ts: event.ts,
542
+ sessionID: event.sessionID,
543
+ batchID: event.batchID,
544
+ roles: [...event.roles]
545
+ };
546
+ if (event.purpose !== undefined) {
547
+ view.purpose = event.purpose;
548
+ }
549
+ if (event.decisionIDs !== undefined) {
550
+ view.decisionIDs = [...event.decisionIDs];
551
+ }
552
+ return view;
553
+ }
554
+ function toActivity(event) {
555
+ const view = {
556
+ ts: event.ts,
557
+ sessionID: event.sessionID,
558
+ kind: event.kind,
559
+ summary: event.summary
560
+ };
561
+ if (event.agent !== undefined) {
562
+ view.agent = event.agent;
563
+ }
564
+ return view;
565
+ }
566
+ function byTsDesc(list) {
567
+ return [...list].sort((a, b) => b.ts - a.ts);
568
+ }
569
+ function aggregateEvents(events) {
570
+ const sessions = new Map;
571
+ const decisions = [];
572
+ const meetings = [];
573
+ const activity = [];
574
+ const sessionFor = (sessionID, ts) => {
575
+ const existing = sessions.get(sessionID);
576
+ if (existing === undefined) {
577
+ const created = emptySession(sessionID, ts);
578
+ sessions.set(sessionID, created);
579
+ return created;
580
+ }
581
+ existing.firstTs = Math.min(existing.firstTs, ts);
582
+ existing.lastTs = Math.max(existing.lastTs, ts);
583
+ return existing;
584
+ };
585
+ for (const event of events) {
586
+ switch (event.type) {
587
+ case "route":
588
+ applyRoute(sessionFor(event.sessionID, event.ts), event);
589
+ break;
590
+ case "message":
591
+ applyMessage(sessionFor(event.sessionID, event.ts), event);
592
+ break;
593
+ case "toolcall":
594
+ applyToolcall(sessionFor(event.sessionID, event.ts), event);
595
+ break;
596
+ case "decision":
597
+ decisions.push(toDecision(event));
598
+ break;
599
+ case "meeting":
600
+ meetings.push(toMeeting(event));
601
+ break;
602
+ case "activity":
603
+ activity.push(toActivity(event));
604
+ break;
605
+ }
606
+ }
607
+ const totals = {
608
+ sessions: 0,
609
+ routeCount: 0,
610
+ localCount: 0,
611
+ frontierCount: 0,
612
+ estimatedCostUSD: 0,
613
+ estimatedSavingsUSD: 0,
614
+ realCostUSD: 0,
615
+ messageCount: 0,
616
+ tokensIn: 0,
617
+ tokensOut: 0,
618
+ tokensReasoning: 0,
619
+ tokensCacheRead: 0,
620
+ tokensCacheWrite: 0,
621
+ toolcalls: emptyToolcallStats()
622
+ };
623
+ const summaries = [...sessions.values()].sort((a, b) => b.lastTs - a.lastTs);
624
+ for (const summary of summaries) {
625
+ summary.models.sort();
626
+ summary.agents.sort();
627
+ accumulateTotals(totals, summary);
628
+ }
629
+ return {
630
+ sessions: summaries,
631
+ totals,
632
+ decisions: byTsDesc(decisions),
633
+ meetings: byTsDesc(meetings),
634
+ activity: byTsDesc(activity)
635
+ };
636
+ }
637
+
638
+ // src/telemetry/events.ts
639
+ import { z as z3 } from "zod";
640
+
641
+ // src/capabilities/types.ts
642
+ import { z } from "zod";
643
+ var CapabilityTierSchema = z.union([
644
+ z.literal(0),
645
+ z.literal(1),
646
+ z.literal(2),
647
+ z.literal(3),
648
+ z.literal(4),
649
+ z.literal(5)
650
+ ]);
651
+ var ComplexityTierSchema = z.enum([
652
+ "trivial",
653
+ "simple",
654
+ "moderate",
655
+ "hard"
656
+ ]);
657
+ var ModelCapabilityProfileSchema = z.object({
658
+ ref: z.object({
659
+ providerID: z.string().min(1),
660
+ modelID: z.string().min(1)
661
+ }),
662
+ kind: z.enum(["local", "frontier", "router"]),
663
+ contextWindow: z.number().int().positive(),
664
+ maxOutputTokens: z.number().int().positive(),
665
+ supportsToolCalling: z.boolean(),
666
+ supportsVision: z.boolean(),
667
+ reasoningTier: CapabilityTierSchema,
668
+ codeQualityTier: CapabilityTierSchema,
669
+ costPer1M: z.object({
670
+ inputUSD: z.number().min(0),
671
+ outputUSD: z.number().min(0)
672
+ }),
673
+ availability: z.enum(["available", "degraded", "unavailable"])
674
+ });
675
+
676
+ // src/config/schema.ts
677
+ import { z as z2 } from "zod";
678
+ var ModelRefSchema = z2.object({
679
+ providerID: z2.string().min(1),
680
+ modelID: z2.string().min(1)
681
+ });
682
+ var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
683
+ var PrivacyModeSchema = z2.enum([
684
+ "forceLocalOnSensitive",
685
+ "consentBeforeFrontier",
686
+ "off"
687
+ ]);
688
+ var BaselineModeSchema = z2.enum(["auto", "pinned"]);
689
+ var DashboardHostSchema = z2.enum(["127.0.0.1", "localhost"]);
690
+ var DashboardConfigSchema = z2.object({
691
+ enabled: z2.boolean().default(false),
692
+ host: DashboardHostSchema.default("127.0.0.1"),
693
+ port: z2.number().int().min(1024).max(65535).default(4599),
694
+ autoPortFallback: z2.boolean().default(true),
695
+ refreshMs: z2.number().int().min(250).default(2000),
696
+ recentRoutes: z2.number().int().positive().default(50),
697
+ openBrowser: z2.boolean().default(false)
698
+ }).default({
699
+ enabled: false,
700
+ host: "127.0.0.1",
701
+ port: 4599,
702
+ autoPortFallback: true,
703
+ refreshMs: 2000,
704
+ recentRoutes: 50,
705
+ openBrowser: false
706
+ });
707
+ var defaultLocalModel = {
708
+ providerID: "ollama",
709
+ modelID: "qwen3:8b"
710
+ };
711
+ var defaultFrontierModel = {
712
+ providerID: "anthropic",
713
+ modelID: "claude-sonnet-4-5"
714
+ };
715
+ var LocalRuntimeSchema = z2.object({
716
+ id: z2.enum(["ollama", "lmstudio", "foundry-local"]),
717
+ enabled: z2.boolean().default(true),
718
+ baseURL: z2.string().url().optional(),
719
+ discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
720
+ defaultModel: ModelRefSchema
721
+ });
722
+ var OpenTeamConfigSchema = z2.object({
723
+ baseline: z2.object({
724
+ mode: BaselineModeSchema.default("auto"),
725
+ pinnedModel: ModelRefSchema.nullable().default(null),
726
+ hardDefault: ModelRefSchema.default(defaultFrontierModel)
727
+ }).default({
728
+ mode: "auto",
729
+ pinnedModel: null,
730
+ hardDefault: defaultFrontierModel
731
+ }),
732
+ router: z2.object({
733
+ mode: RouterModeSchema.default("balanced"),
734
+ localDefault: ModelRefSchema.default(defaultLocalModel),
735
+ trivialPromptMaxChars: z2.number().int().positive().default(280),
736
+ frontierPromptMinChars: z2.number().int().positive().default(2000)
737
+ }).default({
738
+ mode: "balanced",
739
+ localDefault: defaultLocalModel,
740
+ trivialPromptMaxChars: 280,
741
+ frontierPromptMinChars: 2000
742
+ }),
743
+ local: z2.object({
744
+ runtimes: z2.array(LocalRuntimeSchema).min(1).default([
745
+ {
746
+ id: "ollama",
747
+ enabled: true,
748
+ baseURL: "http://localhost:11434/v1",
749
+ defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
750
+ }
751
+ ])
752
+ }).default({
753
+ runtimes: [
754
+ {
755
+ id: "ollama",
756
+ enabled: true,
757
+ baseURL: "http://localhost:11434/v1",
758
+ defaultModel: defaultLocalModel
759
+ }
760
+ ]
761
+ }),
762
+ budgets: z2.object({
763
+ sessionUSD: z2.number().positive().optional(),
764
+ monthlyUSD: z2.number().positive().optional(),
765
+ frontierTokensPerSession: z2.number().int().positive().optional(),
766
+ hardStopOnBudgetExhaustion: z2.boolean().default(false)
767
+ }).default({ hardStopOnBudgetExhaustion: false }),
768
+ privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
769
+ dashboard: DashboardConfigSchema
770
+ });
771
+
772
+ // src/telemetry/events.ts
773
+ var EVENT_SCHEMA_VERSION = 1;
774
+ var EventBaseSchema = z3.object({
775
+ v: z3.literal(EVENT_SCHEMA_VERSION),
776
+ ts: z3.number().finite(),
777
+ sessionID: z3.string().min(1)
778
+ });
779
+ var RouteEventSchema = EventBaseSchema.extend({
780
+ type: z3.literal("route"),
781
+ promptHash: z3.string().min(1),
782
+ promptChars: z3.number().int().min(0),
783
+ tier: ComplexityTierSchema,
784
+ routeKind: z3.enum(["local", "frontier"]),
785
+ selected: ModelRefSchema,
786
+ rationale: z3.string(),
787
+ estimatedCostUSD: z3.number().finite().min(0),
788
+ baselineCostUSD: z3.number().finite().min(0),
789
+ estimatedSavingsUSD: z3.number().finite(),
790
+ budgetAction: z3.string().min(1),
791
+ tokensIn: z3.number().int().min(0).optional(),
792
+ tokensOut: z3.number().int().min(0).optional(),
793
+ decisionID: z3.string().min(1).optional(),
794
+ agent: z3.string().min(1).optional(),
795
+ success: z3.boolean().optional(),
796
+ batchID: z3.string().min(1).optional(),
797
+ failureReason: z3.string().min(1).optional(),
798
+ failureStage: z3.string().min(1).optional()
799
+ });
800
+ var MessageEventSchema = EventBaseSchema.extend({
801
+ type: z3.literal("message"),
802
+ providerID: z3.string().min(1),
803
+ modelID: z3.string().min(1),
804
+ agent: z3.string().min(1).optional(),
805
+ mode: z3.string().min(1).optional(),
806
+ messageID: z3.string().min(1).optional(),
807
+ costUSD: z3.number().finite().min(0),
808
+ tokensIn: z3.number().int().min(0),
809
+ tokensOut: z3.number().int().min(0),
810
+ tokensReasoning: z3.number().int().min(0),
811
+ tokensCacheRead: z3.number().int().min(0),
812
+ tokensCacheWrite: z3.number().int().min(0),
813
+ durationMs: z3.number().int().min(0)
814
+ });
815
+ var ToolcallEventSchema = EventBaseSchema.extend({
816
+ type: z3.literal("toolcall"),
817
+ tool: z3.string().min(1),
818
+ callID: z3.string().min(1),
819
+ agent: z3.string().min(1).optional(),
820
+ durationMs: z3.number().int().min(0),
821
+ ok: z3.boolean(),
822
+ title: z3.string().optional()
823
+ });
824
+ var MeetingEventSchema = EventBaseSchema.extend({
825
+ type: z3.literal("meeting"),
826
+ batchID: z3.string().min(1),
827
+ purpose: z3.string().optional(),
828
+ roles: z3.array(z3.string().min(1)),
829
+ decisionIDs: z3.array(z3.string().min(1)).optional()
830
+ });
831
+ var DecisionEventSchema = EventBaseSchema.extend({
832
+ type: z3.literal("decision"),
833
+ agent: z3.string().min(1).optional(),
834
+ summary: z3.string().min(1),
835
+ tags: z3.array(z3.string().min(1)).optional()
836
+ });
837
+ var ActivityEventSchema = EventBaseSchema.extend({
838
+ type: z3.literal("activity"),
839
+ kind: z3.enum(["route", "agent", "decision", "commit"]),
840
+ agent: z3.string().min(1).optional(),
841
+ summary: z3.string().min(1)
842
+ });
843
+ var OpenTeamEventSchema = z3.discriminatedUnion("type", [
844
+ RouteEventSchema,
845
+ MessageEventSchema,
846
+ ToolcallEventSchema,
847
+ MeetingEventSchema,
848
+ DecisionEventSchema,
849
+ ActivityEventSchema
850
+ ]);
851
+
852
+ // src/telemetry/types.ts
853
+ import { z as z4 } from "zod";
854
+ var CostRecordSchema = z4.object({
855
+ ts: z4.number().finite(),
856
+ sessionID: z4.string().min(1).optional(),
857
+ promptHash: z4.string().min(1),
858
+ promptChars: z4.number().int().min(0),
859
+ tier: ComplexityTierSchema,
860
+ routeKind: z4.enum(["local", "frontier"]),
861
+ selected: ModelRefSchema,
862
+ rationale: z4.string(),
863
+ estimatedCostUSD: z4.number().finite().min(0),
864
+ baselineCostUSD: z4.number().finite().min(0),
865
+ estimatedSavingsUSD: z4.number().finite(),
866
+ budgetAction: z4.string().min(1),
867
+ tokensIn: z4.number().int().min(0).optional(),
868
+ tokensOut: z4.number().int().min(0).optional()
869
+ });
870
+
871
+ // src/telemetry/read.ts
872
+ var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
873
+ function parseCostRecordsJsonl(text) {
874
+ const records = [];
875
+ for (const line of text.split(`
876
+ `)) {
877
+ const trimmed = line.trim();
878
+ if (trimmed.length === 0) {
879
+ continue;
880
+ }
881
+ let candidate;
882
+ try {
883
+ candidate = JSON.parse(trimmed);
884
+ } catch {
885
+ continue;
886
+ }
887
+ const parsed = CostRecordSchema.safeParse(candidate);
888
+ if (parsed.success) {
889
+ records.push(parsed.data);
890
+ }
891
+ }
892
+ return records;
893
+ }
894
+
895
+ // src/telemetry/eventLog.ts
896
+ var DEFAULT_SESSIONS_DIR = ".opencode/openteam/sessions";
897
+ var LEGACY_SESSION_ID = "legacy";
898
+ function defaultJoin(dir, file) {
899
+ return dir.endsWith("/") ? `${dir}${file}` : `${dir}/${file}`;
900
+ }
901
+ function parseEventsJsonl(text) {
902
+ const events = [];
903
+ for (const line of text.split(`
904
+ `)) {
905
+ const trimmed = line.trim();
906
+ if (trimmed.length === 0) {
907
+ continue;
908
+ }
909
+ let candidate;
910
+ try {
911
+ candidate = JSON.parse(trimmed);
912
+ } catch {
913
+ continue;
914
+ }
915
+ const parsed = OpenTeamEventSchema.safeParse(candidate);
916
+ if (parsed.success) {
917
+ events.push(parsed.data);
918
+ }
919
+ }
920
+ return events;
921
+ }
922
+ function costRecordToRouteEvent(record) {
923
+ const event = {
924
+ v: EVENT_SCHEMA_VERSION,
925
+ type: "route",
926
+ ts: record.ts,
927
+ sessionID: record.sessionID ?? LEGACY_SESSION_ID,
928
+ promptHash: record.promptHash,
929
+ promptChars: record.promptChars,
930
+ tier: record.tier,
931
+ routeKind: record.routeKind,
932
+ selected: record.selected,
933
+ rationale: record.rationale,
934
+ estimatedCostUSD: record.estimatedCostUSD,
935
+ baselineCostUSD: record.baselineCostUSD,
936
+ estimatedSavingsUSD: record.estimatedSavingsUSD,
937
+ budgetAction: record.budgetAction
938
+ };
939
+ if (record.tokensIn !== undefined) {
940
+ event.tokensIn = record.tokensIn;
941
+ }
942
+ if (record.tokensOut !== undefined) {
943
+ event.tokensOut = record.tokensOut;
944
+ }
945
+ return event;
946
+ }
947
+ function routeEventToCostRecord(event) {
948
+ const record = {
949
+ ts: event.ts,
950
+ sessionID: event.sessionID,
951
+ promptHash: event.promptHash,
952
+ promptChars: event.promptChars,
953
+ tier: event.tier,
954
+ routeKind: event.routeKind,
955
+ selected: event.selected,
956
+ rationale: event.rationale,
957
+ estimatedCostUSD: event.estimatedCostUSD,
958
+ baselineCostUSD: event.baselineCostUSD,
959
+ estimatedSavingsUSD: event.estimatedSavingsUSD,
960
+ budgetAction: event.budgetAction
961
+ };
962
+ if (event.tokensIn !== undefined) {
963
+ record.tokensIn = event.tokensIn;
964
+ }
965
+ if (event.tokensOut !== undefined) {
966
+ record.tokensOut = event.tokensOut;
967
+ }
968
+ return record;
969
+ }
970
+ async function readRouteCostRecords(dir, deps) {
971
+ const events = await readSessionEvents(dir, deps);
972
+ const records = [];
973
+ for (const event of events) {
974
+ if (event.type === "route") {
975
+ records.push(routeEventToCostRecord(event));
976
+ }
977
+ }
978
+ return records;
979
+ }
980
+ async function readSessionEvents(dir, deps) {
981
+ const join = deps.join ?? defaultJoin;
982
+ const files = (await deps.listFiles(dir)).filter((file) => file.endsWith(".jsonl"));
983
+ const perFile = await Promise.all(files.map(async (file) => {
984
+ const text = await deps.readText(join(dir, file));
985
+ return text === undefined ? [] : parseEventsJsonl(text);
986
+ }));
987
+ const events = perFile.flat();
988
+ if (deps.legacyTelemetryPath !== undefined) {
989
+ const legacyText = await deps.readText(deps.legacyTelemetryPath);
990
+ if (legacyText !== undefined) {
991
+ for (const record of parseCostRecordsJsonl(legacyText)) {
992
+ events.push(costRecordToRouteEvent(record));
993
+ }
994
+ }
995
+ }
996
+ return events.sort((a, b) => a.ts - b.ts);
997
+ }
998
+
999
+ // src/dashboard/backlog.ts
1000
+ var ITEM_RE = /^\s*[-*+]\s+\[( |x|X)\]\s+(.*)$/;
1001
+ var ASSIGNEE_RE = /^\[@([^\]]+)\]\s*(.*)$/;
1002
+ function parseAssignee(text) {
1003
+ const match = ASSIGNEE_RE.exec(text);
1004
+ if (match === null) {
1005
+ return { text: text.trim() };
1006
+ }
1007
+ const assignee = (match[1] ?? "").trim();
1008
+ const rest = (match[2] ?? "").trim();
1009
+ if (assignee.length === 0) {
1010
+ return { text: text.trim() };
1011
+ }
1012
+ return { assignee, text: rest };
1013
+ }
1014
+ function parseBacklog(text) {
1015
+ const items = [];
1016
+ for (const line of text.split(/\r?\n/)) {
1017
+ const match = ITEM_RE.exec(line);
1018
+ if (match === null) {
1019
+ continue;
1020
+ }
1021
+ const done = (match[1] ?? " ").toLowerCase() === "x";
1022
+ const rawText = (match[2] ?? "").trim();
1023
+ const { assignee, text: itemText } = parseAssignee(rawText);
1024
+ const item = { text: itemText, done };
1025
+ if (assignee !== undefined) {
1026
+ item.assignee = assignee;
1027
+ }
1028
+ items.push(item);
1029
+ }
1030
+ return items;
1031
+ }
1032
+ function buildLoopSnapshot(backlogPath, items) {
1033
+ const total = items.length;
1034
+ const done = items.filter((item) => item.done).length;
1035
+ const open = total - done;
1036
+ const progressPct = total === 0 ? 100 : Math.round(done / total * 100);
1037
+ return {
1038
+ backlogPath,
1039
+ total,
1040
+ done,
1041
+ open,
1042
+ progressPct,
1043
+ items: [...items]
1044
+ };
1045
+ }
1046
+
1047
+ // src/dashboard/state.ts
1048
+ var DEFAULT_RECENT_ROUTES = 50;
1049
+ var DEFAULT_ACTIVITY_LIMIT = 100;
1050
+ var DEFAULT_DECISIONS_LIMIT = 100;
1051
+ var DEFAULT_MEETINGS_LIMIT = 50;
1052
+ function toRouteView(record) {
1053
+ return {
1054
+ ts: record.ts,
1055
+ tier: record.tier,
1056
+ routeKind: record.routeKind,
1057
+ model: `${record.selected.providerID}/${record.selected.modelID}`,
1058
+ promptHash: record.promptHash,
1059
+ estimatedCostUSD: record.estimatedCostUSD,
1060
+ estimatedSavingsUSD: record.estimatedSavingsUSD,
1061
+ budgetAction: record.budgetAction
1062
+ };
1063
+ }
1064
+ function isRoute(event) {
1065
+ return event.type === "route";
1066
+ }
1067
+ function recentRoutes(records, limit) {
1068
+ return [...records].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit)).map(toRouteView);
1069
+ }
1070
+ function decisionToActivity(decision) {
1071
+ const entry = {
1072
+ ts: decision.ts,
1073
+ kind: "decision",
1074
+ summary: decision.summary
1075
+ };
1076
+ if (decision.agent !== undefined) {
1077
+ entry.agent = decision.agent;
1078
+ }
1079
+ return entry;
1080
+ }
1081
+ function meetingToActivity(meeting) {
1082
+ const roles = meeting.roles.join(", ");
1083
+ const purpose = meeting.purpose === undefined ? "" : `: ${meeting.purpose}`;
1084
+ return {
1085
+ ts: meeting.ts,
1086
+ kind: "agent",
1087
+ summary: `Reunión (${roles})${purpose}`
1088
+ };
1089
+ }
1090
+ function activityFeed(aggregate, limit) {
1091
+ const entries = [
1092
+ ...aggregate.activity.map((view) => {
1093
+ const entry = {
1094
+ ts: view.ts,
1095
+ kind: view.kind,
1096
+ summary: view.summary
1097
+ };
1098
+ if (view.agent !== undefined) {
1099
+ entry.agent = view.agent;
1100
+ }
1101
+ return entry;
1102
+ }),
1103
+ ...aggregate.decisions.map(decisionToActivity),
1104
+ ...aggregate.meetings.map(meetingToActivity)
1105
+ ];
1106
+ return entries.sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
1107
+ }
1108
+ function loopFrom(backlog) {
1109
+ if (backlog === undefined) {
1110
+ return;
1111
+ }
1112
+ const snapshot = buildLoopSnapshot(backlog.path, backlog.items);
1113
+ if (backlog.lastCommit !== undefined) {
1114
+ snapshot.lastCommit = backlog.lastCommit;
1115
+ }
1116
+ return snapshot;
1117
+ }
1118
+ function buildDashboardState(inputs) {
1119
+ const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
1120
+ const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
1121
+ const decisionsLimit = inputs.decisionsLimit ?? DEFAULT_DECISIONS_LIMIT;
1122
+ const meetingsLimit = inputs.meetingsLimit ?? DEFAULT_MEETINGS_LIMIT;
1123
+ const aggregate = aggregateEvents(inputs.events);
1124
+ const costRecords = inputs.events.filter(isRoute).map(routeEventToCostRecord);
1125
+ const state = {
1126
+ generatedAt: inputs.generatedAt,
1127
+ session: {},
1128
+ cost: summarizeCostRecords(costRecords),
1129
+ totals: aggregate.totals,
1130
+ sessions: aggregate.sessions,
1131
+ recentRoutes: recentRoutes(costRecords, recentRoutesLimit),
1132
+ decisions: aggregate.decisions.slice(0, Math.max(0, decisionsLimit)),
1133
+ meetings: aggregate.meetings.slice(0, Math.max(0, meetingsLimit)),
1134
+ team: [...inputs.team],
1135
+ activity: activityFeed(aggregate, activityLimit)
1136
+ };
1137
+ if (inputs.session?.id !== undefined) {
1138
+ state.session.id = inputs.session.id;
1139
+ }
1140
+ if (inputs.session?.startedAt !== undefined) {
1141
+ state.session.startedAt = inputs.session.startedAt;
1142
+ }
1143
+ const loop = loopFrom(inputs.backlog);
1144
+ if (loop !== undefined) {
1145
+ state.loop = loop;
1146
+ }
1147
+ return state;
1148
+ }
1149
+
1150
+ // src/web/server.ts
1151
+ var SSE_HEADERS = {
1152
+ "content-type": "text/event-stream",
1153
+ "cache-control": "no-cache",
1154
+ connection: "keep-alive"
1155
+ };
1156
+ var JSON_HEADERS = { "content-type": "application/json" };
1157
+ var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
1158
+ function isAddressInUse(error) {
1159
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
1160
+ }
1161
+ async function renderState(deps) {
1162
+ return renderDashboardHtml(buildDashboardState(await deps.readSnapshot()), {
1163
+ refreshMs: deps.config.refreshMs
1164
+ });
1165
+ }
1166
+ async function stateJson(deps) {
1167
+ return JSON.stringify(buildDashboardState(await deps.readSnapshot()));
1168
+ }
1169
+ function sseMessage(json) {
1170
+ return `event: state
1171
+ data: ${json}
1172
+
1173
+ `;
1174
+ }
1175
+ function tryListen(server, host, port) {
1176
+ return new Promise((resolve, reject) => {
1177
+ const onError = (error) => {
1178
+ server.removeListener("listening", onListening);
1179
+ if (isAddressInUse(error)) {
1180
+ resolve(false);
1181
+ } else {
1182
+ reject(error);
1183
+ }
1184
+ };
1185
+ const onListening = () => {
1186
+ server.removeListener("error", onError);
1187
+ resolve(true);
1188
+ };
1189
+ server.once("error", onError);
1190
+ server.once("listening", onListening);
1191
+ server.listen(port, host);
1192
+ });
1193
+ }
1194
+ async function createDashboardServer(deps) {
1195
+ const create = deps.createServer ?? createHttpServer;
1196
+ const log = deps.log ?? ((message) => console.log(message));
1197
+ const clients = new Set;
1198
+ const handle = async (request, response) => {
1199
+ if (request.method !== "GET") {
1200
+ response.writeHead(405).end("method not allowed");
1201
+ return;
1202
+ }
1203
+ const url2 = new URL(request.url ?? "/", "http://localhost");
1204
+ const path = url2.pathname;
1205
+ if (path === "/" || path === "/index.html") {
1206
+ response.writeHead(200, HTML_HEADERS).end(await renderState(deps));
1207
+ return;
1208
+ }
1209
+ if (path === "/api/state") {
1210
+ response.writeHead(200, JSON_HEADERS).end(await stateJson(deps));
1211
+ return;
1212
+ }
1213
+ if (path === "/healthz") {
1214
+ response.writeHead(200, JSON_HEADERS).end(JSON.stringify({ ok: true }));
1215
+ return;
1216
+ }
1217
+ if (path === "/favicon.ico") {
1218
+ response.writeHead(204).end();
1219
+ return;
1220
+ }
1221
+ if (path === "/events") {
1222
+ response.writeHead(200, SSE_HEADERS);
1223
+ response.write(sseMessage(await stateJson(deps)));
1224
+ clients.add(response);
1225
+ request.on("close", () => {
1226
+ clients.delete(response);
1227
+ });
1228
+ return;
1229
+ }
1230
+ response.writeHead(404).end("not found");
1231
+ };
1232
+ const server = create((request, response) => {
1233
+ handle(request, response);
1234
+ });
1235
+ const maxAttempts = deps.config.autoPortFallback ? 20 : 1;
1236
+ let bound = false;
1237
+ let boundPort = deps.config.port;
1238
+ for (let offset = 0;offset < maxAttempts; offset += 1) {
1239
+ const port = deps.config.port + offset;
1240
+ if (port > 65535) {
1241
+ break;
1242
+ }
1243
+ if (await tryListen(server, deps.config.host, port)) {
1244
+ boundPort = port;
1245
+ bound = true;
1246
+ break;
1247
+ }
1248
+ }
1249
+ if (!bound) {
1250
+ throw new Error("dashboard: no available port");
1251
+ }
1252
+ const url = `http://${deps.config.host}:${boundPort}`;
1253
+ log(`[openteam] dashboard en ${url}`);
1254
+ const notify = async () => {
1255
+ if (clients.size === 0) {
1256
+ return;
1257
+ }
1258
+ const message = sseMessage(await stateJson(deps));
1259
+ for (const response of clients) {
1260
+ response.write(message);
1261
+ }
1262
+ };
1263
+ const close = () => {
1264
+ for (const response of clients) {
1265
+ response.end();
1266
+ }
1267
+ clients.clear();
1268
+ server.close();
1269
+ server.closeAllConnections?.();
1270
+ };
1271
+ return { url, port: boundPort, notify, close };
1272
+ }
1273
+
1274
+ // src/commands/agents.ts
1275
+ var PROVIDER_LABELS = {
1276
+ "github-copilot": "GitHub Copilot",
1277
+ anthropic: "Anthropic",
1278
+ openai: "OpenAI",
1279
+ google: "Google",
1280
+ openrouter: "OpenRouter",
1281
+ ollama: "Ollama",
1282
+ lmstudio: "LM Studio",
1283
+ "foundry-local": "Foundry Local"
1284
+ };
1285
+ function providerLabel(providerID) {
1286
+ return PROVIDER_LABELS[providerID] ?? providerID;
1287
+ }
1288
+ function unquote(value) {
1289
+ const trimmed = value.trim();
1290
+ if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
1291
+ return trimmed.slice(1, -1);
1292
+ }
1293
+ return trimmed;
1294
+ }
1295
+ function parseModelValue(value) {
1296
+ const raw = unquote(value);
1297
+ const slash = raw.indexOf("/");
1298
+ if (slash <= 0 || slash === raw.length - 1) {
1299
+ return;
1300
+ }
1301
+ return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) };
1302
+ }
1303
+ function parseAgentFile(file) {
1304
+ const info = { name: file.name, mode: "subagent" };
1305
+ const lines = file.contents.split(/\r?\n/);
1306
+ if (lines[0]?.trim() !== "---") {
1307
+ return info;
1308
+ }
1309
+ for (let i = 1;i < lines.length; i += 1) {
1310
+ const line = lines[i] ?? "";
1311
+ if (line.trim() === "---") {
1312
+ break;
1313
+ }
1314
+ const colon = line.indexOf(":");
1315
+ if (colon <= 0) {
1316
+ continue;
1317
+ }
1318
+ const key = line.slice(0, colon).trim();
1319
+ const value = line.slice(colon + 1).trim();
1320
+ if (key === "mode") {
1321
+ info.mode = unquote(value) || "subagent";
1322
+ } else if (key === "description") {
1323
+ info.description = unquote(value);
1324
+ } else if (key === "model") {
1325
+ const model = parseModelValue(value);
1326
+ if (model !== undefined) {
1327
+ info.model = model;
1328
+ }
1329
+ }
1330
+ }
1331
+ return info;
1332
+ }
1333
+ function localProviderIDs(config) {
1334
+ const ids = new Set;
1335
+ for (const runtime of config.local.runtimes) {
1336
+ ids.add(runtime.id);
1337
+ ids.add(runtime.defaultModel.providerID);
1338
+ }
1339
+ ids.add(config.router.localDefault.providerID);
1340
+ return ids;
1341
+ }
1342
+ function subscriptionLabel(model, localProviders) {
1343
+ const isLocal = localProviders.has(model.providerID);
1344
+ if (isLocal) {
1345
+ return {
1346
+ kind: "local",
1347
+ subscription: `local · ${providerLabel(model.providerID)} (sin coste de tokens)`
1348
+ };
1349
+ }
1350
+ return {
1351
+ kind: "frontier",
1352
+ subscription: `frontier · ${providerLabel(model.providerID)} (consume tu suscripción)`
1353
+ };
1354
+ }
1355
+ function classifyAgent(info, context) {
1356
+ if (info.model !== undefined) {
1357
+ const { kind, subscription: subscription2 } = subscriptionLabel(info.model, context.localProviders);
1358
+ return {
1359
+ name: info.name,
1360
+ mode: info.mode,
1361
+ kind,
1362
+ model: info.model,
1363
+ subscription: subscription2
1364
+ };
1365
+ }
1366
+ if (context.defaultModel === undefined) {
1367
+ return {
1368
+ name: info.name,
1369
+ mode: info.mode,
1370
+ kind: "inherited",
1371
+ subscription: "hereda el default de opencode.json (sin definir) · openteam enruta local-first por mensaje"
1372
+ };
1373
+ }
1374
+ const { subscription } = subscriptionLabel(context.defaultModel, context.localProviders);
1375
+ return {
1376
+ name: info.name,
1377
+ mode: info.mode,
1378
+ kind: "inherited",
1379
+ model: context.defaultModel,
1380
+ subscription: `hereda default → ${subscription} · openteam enruta local-first por mensaje`
1381
+ };
1382
+ }
1383
+ function modeRank(mode) {
1384
+ if (mode === "primary") {
1385
+ return 0;
1386
+ }
1387
+ if (mode === "all") {
1388
+ return 1;
1389
+ }
1390
+ return 2;
1391
+ }
1392
+ function classifyAgents(files, context) {
1393
+ return files.map((file) => classifyAgent(parseAgentFile(file), context)).sort((a, b) => {
1394
+ const rank = modeRank(a.mode) - modeRank(b.mode);
1395
+ return rank !== 0 ? rank : a.name.localeCompare(b.name);
1396
+ });
1397
+ }
1398
+ function modelText(classification) {
1399
+ return classification.model === undefined ? "" : ` (${classification.model.providerID}/${classification.model.modelID})`;
1400
+ }
1401
+ function renderAgentList(classifications, context) {
1402
+ const lines = ["openteam agents — LLM por agente:", ""];
1403
+ if (classifications.length === 0) {
1404
+ lines.push(" (sin agentes en .opencode/agent/) — ejecuta 'openteam setup' o pide al orquestador que cree el equipo.");
1405
+ return lines.join(`
1406
+ `);
1407
+ }
1408
+ const nameWidth = Math.max(...classifications.map((c) => c.name.length), "agente".length);
1409
+ const modeWidth = Math.max(...classifications.map((c) => c.mode.length), "subagent".length);
1410
+ for (const c of classifications) {
1411
+ const bullet = c.mode === "primary" ? "●" : "○";
1412
+ lines.push(` ${bullet} ${c.name.padEnd(nameWidth)} [${c.mode.padEnd(modeWidth)}] ${c.subscription}${modelText(c)}`);
1413
+ }
1414
+ lines.push("");
1415
+ if (context.defaultModel !== undefined) {
1416
+ const { kind } = subscriptionLabel(context.defaultModel, context.localProviders);
1417
+ lines.push(`Default de opencode.json: ${context.defaultModel.providerID}/${context.defaultModel.modelID} (${kind} · ${providerLabel(context.defaultModel.providerID)})`);
1418
+ } else {
1419
+ lines.push("Default de opencode.json: (sin definir)");
1420
+ }
1421
+ lines.push("Leyenda: ● primary · ○ subagent · local = sin coste de tokens · frontier = consume tu suscripción.");
1422
+ return lines.join(`
1423
+ `);
1424
+ }
1425
+
1426
+ // src/telemetry/decisions.ts
1427
+ var DECISIONS_SESSION_ID = "decisions";
1428
+ var BULLET_RE = /^\s*[-*]\s+(.+?)\s*$/;
1429
+ var DATE_RE = /^(\d{4}-\d{2}-\d{2})\b\s*/;
1430
+ var AGENT_RE = /^\[([^\]]+)\]\s*/;
1431
+ var TAG_RE = /#([A-Za-z0-9][\w-]*)/g;
1432
+ function parseDate(value) {
1433
+ const parsed = Date.parse(`${value}T00:00:00Z`);
1434
+ return Number.isNaN(parsed) ? undefined : parsed;
1435
+ }
1436
+ function parseDecisionsMarkdown(text, options = {}) {
1437
+ const sessionID = options.sessionID ?? DECISIONS_SESSION_ID;
1438
+ const baseTs = options.baseTs ?? 0;
1439
+ const events = [];
1440
+ let index = 0;
1441
+ for (const line of text.split(`
1442
+ `)) {
1443
+ const bullet = BULLET_RE.exec(line);
1444
+ if (bullet === null) {
1445
+ continue;
1446
+ }
1447
+ let content = bullet[1] ?? "";
1448
+ let ts = baseTs + index;
1449
+ const dateMatch = DATE_RE.exec(content);
1450
+ if (dateMatch?.[1] !== undefined) {
1451
+ const parsed = parseDate(dateMatch[1]);
1452
+ if (parsed !== undefined) {
1453
+ ts = parsed;
1454
+ content = content.slice(dateMatch[0].length);
1455
+ }
1456
+ }
1457
+ let agent;
1458
+ const agentMatch = AGENT_RE.exec(content);
1459
+ if (agentMatch?.[1] !== undefined) {
1460
+ agent = agentMatch[1].trim();
1461
+ content = content.slice(agentMatch[0].length);
1462
+ }
1463
+ const tags = [];
1464
+ for (const tagMatch of content.matchAll(TAG_RE)) {
1465
+ const tag = tagMatch[1];
1466
+ if (tag !== undefined && !tags.includes(tag)) {
1467
+ tags.push(tag);
1468
+ }
1469
+ }
1470
+ const summary = content.replace(TAG_RE, "").replace(/\s+/g, " ").trim();
1471
+ if (summary.length === 0) {
1472
+ continue;
1473
+ }
1474
+ const event = {
1475
+ v: EVENT_SCHEMA_VERSION,
1476
+ type: "decision",
1477
+ ts,
1478
+ sessionID,
1479
+ summary
1480
+ };
1481
+ if (agent !== undefined && agent.length > 0) {
1482
+ event.agent = agent;
1483
+ }
1484
+ if (tags.length > 0) {
1485
+ event.tags = tags;
1486
+ }
1487
+ events.push(event);
1488
+ index += 1;
1489
+ }
1490
+ return events;
1491
+ }
1492
+
1493
+ // src/web/snapshot.ts
1494
+ function createSnapshotReader(deps, paths, context = {}) {
1495
+ const readEventsDeps = {
1496
+ listFiles: deps.listFiles,
1497
+ readText: deps.readText,
1498
+ ...paths.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: paths.legacyTelemetryPath } : {}
1499
+ };
1500
+ return async () => {
1501
+ const [sessionEvents, decisionsText, backlogText, agentFiles, lastCommit] = await Promise.all([
1502
+ readSessionEvents(paths.sessionsDir, readEventsDeps),
1503
+ deps.readText(paths.decisionsPath),
1504
+ deps.readText(paths.backlogPath),
1505
+ deps.listAgentFiles(paths.agentDir),
1506
+ deps.gitLastCommit?.() ?? Promise.resolve(undefined)
1507
+ ]);
1508
+ const decisionEvents = parseDecisionsMarkdown(decisionsText ?? "");
1509
+ const events = [...sessionEvents, ...decisionEvents];
1510
+ const team = agentFiles.map(parseAgentFile);
1511
+ const inputs = {
1512
+ generatedAt: deps.now(),
1513
+ events,
1514
+ team
1515
+ };
1516
+ if (backlogText !== undefined) {
1517
+ const backlog = {
1518
+ path: paths.backlogPath,
1519
+ items: parseBacklog(backlogText)
1520
+ };
1521
+ if (lastCommit !== undefined) {
1522
+ backlog.lastCommit = lastCommit;
1523
+ }
1524
+ inputs.backlog = backlog;
1525
+ }
1526
+ if (context.session !== undefined) {
1527
+ inputs.session = context.session;
1528
+ }
1529
+ if (context.recentRoutesLimit !== undefined) {
1530
+ inputs.recentRoutesLimit = context.recentRoutesLimit;
1531
+ }
1532
+ return inputs;
1533
+ };
1534
+ }
1535
+
1536
+ // src/web/watch.ts
1537
+ function watchSources(paths, onChange, deps, debounceMs = 250) {
1538
+ const setTimer = deps.setTimer ?? ((cb, ms2) => setTimeout(cb, ms2));
1539
+ const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
1540
+ const watchers = [];
1541
+ let pending;
1542
+ const trigger = () => {
1543
+ if (pending !== undefined) {
1544
+ clearTimer(pending);
1545
+ }
1546
+ pending = setTimer(() => {
1547
+ pending = undefined;
1548
+ onChange();
1549
+ }, debounceMs);
1550
+ };
1551
+ for (const path of paths) {
1552
+ try {
1553
+ watchers.push(deps.watch(path, trigger));
1554
+ } catch {}
1555
+ }
1556
+ return {
1557
+ close() {
1558
+ if (pending !== undefined) {
1559
+ clearTimer(pending);
1560
+ pending = undefined;
1561
+ }
1562
+ for (const watcher of watchers) {
1563
+ watcher.close();
1564
+ }
1565
+ }
1566
+ };
1567
+ }
1568
+
1569
+ // src/web/start.ts
1570
+ function startDashboard(deps) {
1571
+ return createDashboardRuntime(deps);
1572
+ }
1573
+ async function createDashboardRuntime(deps) {
1574
+ const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
1575
+ const decisionsPath = deps.decisionsPath ?? DEFAULT_DECISIONS_PATH;
1576
+ const readSnapshot = createSnapshotReader({
1577
+ readText: deps.readText,
1578
+ listFiles: deps.listFiles,
1579
+ listAgentFiles: deps.listAgentFiles,
1580
+ now: deps.now,
1581
+ ...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {}
1582
+ }, {
1583
+ sessionsDir: deps.sessionsDir,
1584
+ decisionsPath,
1585
+ backlogPath,
1586
+ agentDir: deps.agentDir,
1587
+ ...deps.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: deps.legacyTelemetryPath } : {}
1588
+ }, {
1589
+ recentRoutesLimit: deps.config.recentRoutes,
1590
+ ...deps.session !== undefined ? { session: deps.session } : {}
1591
+ });
1592
+ const createServer = deps.serve ?? createDashboardServer;
1593
+ const server = await createServer({
1594
+ readSnapshot,
1595
+ config: deps.config,
1596
+ ...deps.log !== undefined ? { log: deps.log } : {}
1597
+ });
1598
+ const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
1599
+ const watcher = watchSources([deps.sessionsDir, decisionsPath, backlogPath, deps.agentDir], () => {
1600
+ server.notify();
1601
+ }, { watch: watchFn });
1602
+ return {
1603
+ url: server.url,
1604
+ port: server.port,
1605
+ close() {
1606
+ watcher.close();
1607
+ server.close();
1608
+ }
1609
+ };
1610
+ }
1611
+
1612
+ // src/cli/dashboardServe.ts
1613
+ function parseDashboardServeArgs(rest) {
1614
+ return {
1615
+ status: rest.includes("--status"),
1616
+ serve: rest.includes("--serve"),
1617
+ open: rest.includes("--open")
1618
+ };
1619
+ }
1620
+ async function runDashboardServe(deps, options) {
1621
+ const config = await deps.loadConfig(deps.configPath);
1622
+ const start = deps.startDashboard ?? startDashboard;
1623
+ let runtime;
1624
+ try {
1625
+ runtime = await start({
1626
+ config: { ...config.dashboard, enabled: true },
1627
+ sessionsDir: deps.sessionsDir,
1628
+ decisionsPath: deps.decisionsPath,
1629
+ backlogPath: deps.backlogPath,
1630
+ agentDir: deps.agentDir,
1631
+ ...deps.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: deps.legacyTelemetryPath } : {},
1632
+ readText: deps.readText,
1633
+ listFiles: deps.listFiles,
1634
+ listAgentFiles: deps.listAgentFiles,
1635
+ now: deps.now,
1636
+ ...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {},
1637
+ log: deps.log
1638
+ });
1639
+ } catch (error) {
1640
+ const message = error instanceof Error ? error.message : String(error);
1641
+ deps.log(`No se pudo iniciar el dashboard: ${message}`);
1642
+ return { exitCode: 1 };
1643
+ }
1644
+ deps.log("Escuchando en loopback. Pulsa Ctrl+C para parar.");
1645
+ if (options.open && deps.openBrowser !== undefined) {
1646
+ try {
1647
+ await deps.openBrowser(runtime.url);
1648
+ } catch {
1649
+ deps.log("No se pudo abrir el navegador automáticamente.");
1650
+ }
1651
+ }
1652
+ await deps.waitForSignal();
1653
+ runtime.close();
1654
+ deps.log("Dashboard detenido.");
1655
+ return { exitCode: 0 };
1656
+ }
1657
+
9
1658
  // src/cli/setupAdapters.ts
10
1659
  import {
11
1660
  cancel,
@@ -61,50 +1710,15 @@ var CURATED_FRONTIER_PROFILES = [
61
1710
  ref: { providerID: "openai", modelID: "gpt-5.3-codex" },
62
1711
  kind: "frontier",
63
1712
  contextWindow: 400000,
64
- maxOutputTokens: 64000,
65
- supportsToolCalling: true,
66
- supportsVision: true,
67
- reasoningTier: 5,
68
- codeQualityTier: 5,
69
- costPer1M: { inputUSD: 5, outputUSD: 20 },
70
- availability: "available"
71
- }
72
- ];
73
-
74
- // src/capabilities/types.ts
75
- import { z } from "zod";
76
- var CapabilityTierSchema = z.union([
77
- z.literal(0),
78
- z.literal(1),
79
- z.literal(2),
80
- z.literal(3),
81
- z.literal(4),
82
- z.literal(5)
83
- ]);
84
- var ComplexityTierSchema = z.enum([
85
- "trivial",
86
- "simple",
87
- "moderate",
88
- "hard"
89
- ]);
90
- var ModelCapabilityProfileSchema = z.object({
91
- ref: z.object({
92
- providerID: z.string().min(1),
93
- modelID: z.string().min(1)
94
- }),
95
- kind: z.enum(["local", "frontier", "router"]),
96
- contextWindow: z.number().int().positive(),
97
- maxOutputTokens: z.number().int().positive(),
98
- supportsToolCalling: z.boolean(),
99
- supportsVision: z.boolean(),
100
- reasoningTier: CapabilityTierSchema,
101
- codeQualityTier: CapabilityTierSchema,
102
- costPer1M: z.object({
103
- inputUSD: z.number().min(0),
104
- outputUSD: z.number().min(0)
105
- }),
106
- availability: z.enum(["available", "degraded", "unavailable"])
107
- });
1713
+ maxOutputTokens: 64000,
1714
+ supportsToolCalling: true,
1715
+ supportsVision: true,
1716
+ reasoningTier: 5,
1717
+ codeQualityTier: 5,
1718
+ costPer1M: { inputUSD: 5, outputUSD: 20 },
1719
+ availability: "available"
1720
+ }
1721
+ ];
108
1722
 
109
1723
  // src/capabilities/normalize.ts
110
1724
  function isRecord(value) {
@@ -735,309 +2349,61 @@ var clackPrompter = {
735
2349
  return unwrap(result);
736
2350
  }
737
2351
  };
738
- var DETECTION_RUNTIMES = [
739
- {
740
- id: "ollama",
741
- enabled: true,
742
- baseURL: "http://localhost:11434/v1",
743
- defaultModel: { providerID: "ollama", modelID: "probe" }
744
- },
745
- {
746
- id: "lmstudio",
747
- enabled: true,
748
- baseURL: "http://localhost:1234/v1",
749
- defaultModel: { providerID: "lmstudio", modelID: "probe" }
750
- },
751
- {
752
- id: "foundry-local",
753
- enabled: true,
754
- discovery: "cli",
755
- defaultModel: { providerID: "foundry-local", modelID: "probe" }
756
- }
757
- ];
758
- function createLoadFrontierProfiles() {
759
- return async () => {
760
- try {
761
- const result = await loadCapabilityProfiles({
762
- fetch: globalThis.fetch,
763
- now: () => Date.now()
764
- });
765
- return result.profiles.length > 0 ? result.profiles : [...CURATED_FRONTIER_PROFILES];
766
- } catch {
767
- return [...CURATED_FRONTIER_PROFILES];
768
- }
769
- };
770
- }
771
- function createDetect(exec) {
772
- return async () => {
773
- const registry = new RuntimeRegistry({
774
- fetch: globalThis.fetch,
775
- clock: () => new Date().toISOString(),
776
- foundryDiscovery: foundryDiscoveryFromExec(exec)
777
- });
778
- const snapshots = await registry.probe(DETECTION_RUNTIMES);
779
- return snapshots.map((snapshot) => {
780
- const detected = {
781
- id: snapshot.id,
782
- baseURL: snapshot.baseURL,
783
- reachable: snapshot.reachable,
784
- models: snapshot.models.map((model) => model.modelID)
785
- };
786
- if (snapshot.error !== undefined) {
787
- detected.error = snapshot.error;
788
- }
789
- return detected;
790
- });
791
- };
792
- }
793
-
794
- // src/commands/agents.ts
795
- var PROVIDER_LABELS = {
796
- "github-copilot": "GitHub Copilot",
797
- anthropic: "Anthropic",
798
- openai: "OpenAI",
799
- google: "Google",
800
- openrouter: "OpenRouter",
801
- ollama: "Ollama",
802
- lmstudio: "LM Studio",
803
- "foundry-local": "Foundry Local"
804
- };
805
- function providerLabel(providerID) {
806
- return PROVIDER_LABELS[providerID] ?? providerID;
807
- }
808
- function unquote(value) {
809
- const trimmed = value.trim();
810
- if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
811
- return trimmed.slice(1, -1);
812
- }
813
- return trimmed;
814
- }
815
- function parseModelValue(value) {
816
- const raw = unquote(value);
817
- const slash = raw.indexOf("/");
818
- if (slash <= 0 || slash === raw.length - 1) {
819
- return;
820
- }
821
- return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) };
822
- }
823
- function parseAgentFile(file) {
824
- const info = { name: file.name, mode: "subagent" };
825
- const lines = file.contents.split(/\r?\n/);
826
- if (lines[0]?.trim() !== "---") {
827
- return info;
828
- }
829
- for (let i = 1;i < lines.length; i += 1) {
830
- const line = lines[i] ?? "";
831
- if (line.trim() === "---") {
832
- break;
833
- }
834
- const colon = line.indexOf(":");
835
- if (colon <= 0) {
836
- continue;
837
- }
838
- const key = line.slice(0, colon).trim();
839
- const value = line.slice(colon + 1).trim();
840
- if (key === "mode") {
841
- info.mode = unquote(value) || "subagent";
842
- } else if (key === "description") {
843
- info.description = unquote(value);
844
- } else if (key === "model") {
845
- const model = parseModelValue(value);
846
- if (model !== undefined) {
847
- info.model = model;
848
- }
849
- }
850
- }
851
- return info;
852
- }
853
- function localProviderIDs(config) {
854
- const ids = new Set;
855
- for (const runtime of config.local.runtimes) {
856
- ids.add(runtime.id);
857
- ids.add(runtime.defaultModel.providerID);
858
- }
859
- ids.add(config.router.localDefault.providerID);
860
- return ids;
861
- }
862
- function subscriptionLabel(model, localProviders) {
863
- const isLocal = localProviders.has(model.providerID);
864
- if (isLocal) {
865
- return {
866
- kind: "local",
867
- subscription: `local · ${providerLabel(model.providerID)} (sin coste de tokens)`
868
- };
869
- }
870
- return {
871
- kind: "frontier",
872
- subscription: `frontier · ${providerLabel(model.providerID)} (consume tu suscripción)`
873
- };
874
- }
875
- function classifyAgent(info, context) {
876
- if (info.model !== undefined) {
877
- const { kind, subscription: subscription2 } = subscriptionLabel(info.model, context.localProviders);
878
- return {
879
- name: info.name,
880
- mode: info.mode,
881
- kind,
882
- model: info.model,
883
- subscription: subscription2
884
- };
885
- }
886
- if (context.defaultModel === undefined) {
887
- return {
888
- name: info.name,
889
- mode: info.mode,
890
- kind: "inherited",
891
- subscription: "hereda el default de opencode.json (sin definir) · openteam enruta local-first por mensaje"
892
- };
893
- }
894
- const { subscription } = subscriptionLabel(context.defaultModel, context.localProviders);
895
- return {
896
- name: info.name,
897
- mode: info.mode,
898
- kind: "inherited",
899
- model: context.defaultModel,
900
- subscription: `hereda default → ${subscription} · openteam enruta local-first por mensaje`
901
- };
902
- }
903
- function modeRank(mode) {
904
- if (mode === "primary") {
905
- return 0;
906
- }
907
- if (mode === "all") {
908
- return 1;
909
- }
910
- return 2;
911
- }
912
- function classifyAgents(files, context) {
913
- return files.map((file) => classifyAgent(parseAgentFile(file), context)).sort((a, b) => {
914
- const rank = modeRank(a.mode) - modeRank(b.mode);
915
- return rank !== 0 ? rank : a.name.localeCompare(b.name);
916
- });
917
- }
918
- function modelText(classification) {
919
- return classification.model === undefined ? "" : ` (${classification.model.providerID}/${classification.model.modelID})`;
920
- }
921
- function renderAgentList(classifications, context) {
922
- const lines = ["openteam agents — LLM por agente:", ""];
923
- if (classifications.length === 0) {
924
- lines.push(" (sin agentes en .opencode/agent/) — ejecuta 'openteam setup' o pide al orquestador que cree el equipo.");
925
- return lines.join(`
926
- `);
927
- }
928
- const nameWidth = Math.max(...classifications.map((c) => c.name.length), "agente".length);
929
- const modeWidth = Math.max(...classifications.map((c) => c.mode.length), "subagent".length);
930
- for (const c of classifications) {
931
- const bullet = c.mode === "primary" ? "●" : "○";
932
- lines.push(` ${bullet} ${c.name.padEnd(nameWidth)} [${c.mode.padEnd(modeWidth)}] ${c.subscription}${modelText(c)}`);
933
- }
934
- lines.push("");
935
- if (context.defaultModel !== undefined) {
936
- const { kind } = subscriptionLabel(context.defaultModel, context.localProviders);
937
- lines.push(`Default de opencode.json: ${context.defaultModel.providerID}/${context.defaultModel.modelID} (${kind} · ${providerLabel(context.defaultModel.providerID)})`);
938
- } else {
939
- lines.push("Default de opencode.json: (sin definir)");
940
- }
941
- lines.push("Leyenda: ● primary · ○ subagent · local = sin coste de tokens · frontier = consume tu suscripción.");
942
- return lines.join(`
943
- `);
944
- }
945
-
946
- // src/config/schema.ts
947
- import { z as z2 } from "zod";
948
- var ModelRefSchema = z2.object({
949
- providerID: z2.string().min(1),
950
- modelID: z2.string().min(1)
951
- });
952
- var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
953
- var PrivacyModeSchema = z2.enum([
954
- "forceLocalOnSensitive",
955
- "consentBeforeFrontier",
956
- "off"
957
- ]);
958
- var BaselineModeSchema = z2.enum(["auto", "pinned"]);
959
- var DashboardHostSchema = z2.enum(["127.0.0.1", "localhost"]);
960
- var DashboardConfigSchema = z2.object({
961
- enabled: z2.boolean().default(false),
962
- host: DashboardHostSchema.default("127.0.0.1"),
963
- port: z2.number().int().min(1024).max(65535).default(4599),
964
- autoPortFallback: z2.boolean().default(true),
965
- refreshMs: z2.number().int().min(250).default(2000),
966
- recentRoutes: z2.number().int().positive().default(50),
967
- openBrowser: z2.boolean().default(false)
968
- }).default({
969
- enabled: false,
970
- host: "127.0.0.1",
971
- port: 4599,
972
- autoPortFallback: true,
973
- refreshMs: 2000,
974
- recentRoutes: 50,
975
- openBrowser: false
976
- });
977
- var defaultLocalModel = {
978
- providerID: "ollama",
979
- modelID: "qwen3:8b"
980
- };
981
- var defaultFrontierModel = {
982
- providerID: "anthropic",
983
- modelID: "claude-sonnet-4-5"
984
- };
985
- var LocalRuntimeSchema = z2.object({
986
- id: z2.enum(["ollama", "lmstudio", "foundry-local"]),
987
- enabled: z2.boolean().default(true),
988
- baseURL: z2.string().url().optional(),
989
- discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
990
- defaultModel: ModelRefSchema
991
- });
992
- var OpenTeamConfigSchema = z2.object({
993
- baseline: z2.object({
994
- mode: BaselineModeSchema.default("auto"),
995
- pinnedModel: ModelRefSchema.nullable().default(null),
996
- hardDefault: ModelRefSchema.default(defaultFrontierModel)
997
- }).default({
998
- mode: "auto",
999
- pinnedModel: null,
1000
- hardDefault: defaultFrontierModel
1001
- }),
1002
- router: z2.object({
1003
- mode: RouterModeSchema.default("balanced"),
1004
- localDefault: ModelRefSchema.default(defaultLocalModel),
1005
- trivialPromptMaxChars: z2.number().int().positive().default(280),
1006
- frontierPromptMinChars: z2.number().int().positive().default(2000)
1007
- }).default({
1008
- mode: "balanced",
1009
- localDefault: defaultLocalModel,
1010
- trivialPromptMaxChars: 280,
1011
- frontierPromptMinChars: 2000
1012
- }),
1013
- local: z2.object({
1014
- runtimes: z2.array(LocalRuntimeSchema).min(1).default([
1015
- {
1016
- id: "ollama",
1017
- enabled: true,
1018
- baseURL: "http://localhost:11434/v1",
1019
- defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
1020
- }
1021
- ])
1022
- }).default({
1023
- runtimes: [
1024
- {
1025
- id: "ollama",
1026
- enabled: true,
1027
- baseURL: "http://localhost:11434/v1",
1028
- defaultModel: defaultLocalModel
2352
+ var DETECTION_RUNTIMES = [
2353
+ {
2354
+ id: "ollama",
2355
+ enabled: true,
2356
+ baseURL: "http://localhost:11434/v1",
2357
+ defaultModel: { providerID: "ollama", modelID: "probe" }
2358
+ },
2359
+ {
2360
+ id: "lmstudio",
2361
+ enabled: true,
2362
+ baseURL: "http://localhost:1234/v1",
2363
+ defaultModel: { providerID: "lmstudio", modelID: "probe" }
2364
+ },
2365
+ {
2366
+ id: "foundry-local",
2367
+ enabled: true,
2368
+ discovery: "cli",
2369
+ defaultModel: { providerID: "foundry-local", modelID: "probe" }
2370
+ }
2371
+ ];
2372
+ function createLoadFrontierProfiles() {
2373
+ return async () => {
2374
+ try {
2375
+ const result = await loadCapabilityProfiles({
2376
+ fetch: globalThis.fetch,
2377
+ now: () => Date.now()
2378
+ });
2379
+ return result.profiles.length > 0 ? result.profiles : [...CURATED_FRONTIER_PROFILES];
2380
+ } catch {
2381
+ return [...CURATED_FRONTIER_PROFILES];
2382
+ }
2383
+ };
2384
+ }
2385
+ function createDetect(exec) {
2386
+ return async () => {
2387
+ const registry = new RuntimeRegistry({
2388
+ fetch: globalThis.fetch,
2389
+ clock: () => new Date().toISOString(),
2390
+ foundryDiscovery: foundryDiscoveryFromExec(exec)
2391
+ });
2392
+ const snapshots = await registry.probe(DETECTION_RUNTIMES);
2393
+ return snapshots.map((snapshot) => {
2394
+ const detected = {
2395
+ id: snapshot.id,
2396
+ baseURL: snapshot.baseURL,
2397
+ reachable: snapshot.reachable,
2398
+ models: snapshot.models.map((model) => model.modelID)
2399
+ };
2400
+ if (snapshot.error !== undefined) {
2401
+ detected.error = snapshot.error;
1029
2402
  }
1030
- ]
1031
- }),
1032
- budgets: z2.object({
1033
- sessionUSD: z2.number().positive().optional(),
1034
- monthlyUSD: z2.number().positive().optional(),
1035
- frontierTokensPerSession: z2.number().int().positive().optional(),
1036
- hardStopOnBudgetExhaustion: z2.boolean().default(false)
1037
- }).default({ hardStopOnBudgetExhaustion: false }),
1038
- privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
1039
- dashboard: DashboardConfigSchema
1040
- });
2403
+ return detected;
2404
+ });
2405
+ };
2406
+ }
1041
2407
 
1042
2408
  // src/commands/baseline.ts
1043
2409
  function formatRef(ref) {
@@ -1103,25 +2469,19 @@ function autoBaseline(config) {
1103
2469
  // src/commands/dashboard.ts
1104
2470
  function renderDashboardStatus(dashboard) {
1105
2471
  const url = `http://${dashboard.host}:${dashboard.port}`;
1106
- if (!dashboard.enabled) {
1107
- return [
1108
- "Dashboard: deshabilitado.",
1109
- "",
1110
- 'Para activarlo, pon "dashboard": { "enabled": true } en .opencode/openteam.json',
1111
- "y recarga opencode. Se servirá (loopback) en:",
1112
- ` ${url}`
1113
- ].join(`
1114
- `);
1115
- }
1116
2472
  return [
1117
- "Dashboard: habilitado.",
2473
+ "Dashboard (multi-sesión, se lanza desde la CLI):",
1118
2474
  ` URL: ${url}`,
1119
2475
  ` Refresco: ${dashboard.refreshMs} ms (SSE + polling)`,
1120
2476
  ` Rutas: últimas ${dashboard.recentRoutes}`,
1121
2477
  dashboard.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
1122
2478
  "",
1123
- "Nota: solo escucha en loopback y nunca expone prompts (solo hashes).",
1124
- "El servidor corre dentro de la sesión de opencode; se cierra al salir."
2479
+ "Lánzalo con:",
2480
+ " openteam dashboard (Ctrl+C para parar; --open abre el navegador)",
2481
+ "",
2482
+ "Agrega TODAS las sesiones de opencode que escriben eventos en",
2483
+ " .opencode/openteam/sessions/*.jsonl",
2484
+ "Solo escucha en loopback y nunca expone prompts (solo hashes)."
1125
2485
  ].join(`
1126
2486
  `);
1127
2487
  }
@@ -1258,7 +2618,12 @@ function buildOrchestratorAgent(frontier, options = {}) {
1258
2618
  "- **scribe** — memoria silenciosa del equipo. Registra decisiones y",
1259
2619
  " aprendizajes en un log compartido (`.opencode/openteam-decisions.md`) sin",
1260
2620
  " ejecutar cambios de código. Modelo **local**; permisos de solo lectura más",
1261
- " edición de ese log.",
2621
+ " edición de ese log. **Formato parseable** para el dashboard: una decisión",
2622
+ " por línea como item de lista Markdown, ya **redactada** (sin prompts ni",
2623
+ " datos sensibles):",
2624
+ " `- YYYY-MM-DD [agente] Resumen breve de la decisión #tag1 #tag2`",
2625
+ " La fecha, el `[agente]` y los `#tags` son opcionales; el resumen es",
2626
+ " obligatorio.",
1262
2627
  "- **ralph** — automatización y triage. Monitoriza el trabajo pendiente",
1263
2628
  " (issues/tareas), lo prioriza, coordina la ejecución delegando en los",
1264
2629
  " especialistas y **escala al humano** ante bloqueos, riesgos o aprobaciones.",
@@ -1341,78 +2706,6 @@ function buildOrchestratorAgent(frontier, options = {}) {
1341
2706
  ${body}`;
1342
2707
  }
1343
2708
 
1344
- // src/commands/report.ts
1345
- var TIERS = [
1346
- "trivial",
1347
- "simple",
1348
- "moderate",
1349
- "hard"
1350
- ];
1351
- function emptyTierSummary() {
1352
- return {
1353
- trivial: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
1354
- simple: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
1355
- moderate: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
1356
- hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
1357
- };
1358
- }
1359
- function summarizeCostRecords(records) {
1360
- const report = {
1361
- count: 0,
1362
- localCount: 0,
1363
- frontierCount: 0,
1364
- totalEstimatedUSD: 0,
1365
- totalBaselineUSD: 0,
1366
- totalSavingsUSD: 0,
1367
- savingsPct: 0,
1368
- tokensIn: 0,
1369
- tokensOut: 0,
1370
- byTier: emptyTierSummary()
1371
- };
1372
- for (const record of records) {
1373
- report.count += 1;
1374
- if (record.routeKind === "local") {
1375
- report.localCount += 1;
1376
- } else {
1377
- report.frontierCount += 1;
1378
- }
1379
- report.totalEstimatedUSD += record.estimatedCostUSD;
1380
- report.totalBaselineUSD += record.baselineCostUSD;
1381
- report.totalSavingsUSD += record.estimatedSavingsUSD;
1382
- report.tokensIn += record.tokensIn ?? 0;
1383
- report.tokensOut += record.tokensOut ?? 0;
1384
- const tier = report.byTier[record.tier];
1385
- tier.count += 1;
1386
- tier.estimatedUSD += record.estimatedCostUSD;
1387
- tier.savingsUSD += record.estimatedSavingsUSD;
1388
- }
1389
- report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
1390
- return report;
1391
- }
1392
- function usd(value) {
1393
- return `$${value.toFixed(5)}`;
1394
- }
1395
- function renderReport(report) {
1396
- if (report.count === 0) {
1397
- return "openteam report: sin registros de telemetría todavía.";
1398
- }
1399
- const lines = [
1400
- "openteam report:",
1401
- ` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
1402
- ` coste real: ${usd(report.totalEstimatedUSD)}`,
1403
- ` baseline: ${usd(report.totalBaselineUSD)}`,
1404
- ` ahorro: ${usd(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
1405
- ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
1406
- " por tier:"
1407
- ];
1408
- for (const tier of TIERS) {
1409
- const summary = report.byTier[tier];
1410
- lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd(summary.estimatedUSD)} · ahorro ${usd(summary.savingsUSD)}`);
1411
- }
1412
- return lines.join(`
1413
- `);
1414
- }
1415
-
1416
2709
  // src/commands/yolo.ts
1417
2710
  var CATCH_ALL = "*";
1418
2711
  var ALLOW = "allow";
@@ -1460,7 +2753,8 @@ var HELP = [
1460
2753
  " openteam baseline auto Baseline cheapest-capable (modo auto)",
1461
2754
  " openteam doctor Diagnóstico de runtimes y config",
1462
2755
  " openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
1463
- " openteam dashboard Estado y URL del dashboard web (loopback)",
2756
+ " openteam dashboard Lanza el dashboard web multi-sesión (Ctrl+C para parar; --open abre el navegador)",
2757
+ " openteam dashboard --status Muestra la config del dashboard sin lanzarlo",
1464
2758
  " openteam report Resumen de coste/ahorro (telemetría)",
1465
2759
  " openteam yolo status Muestra si el modo YOLO está activo",
1466
2760
  " openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
@@ -2123,71 +3417,29 @@ function loadOpenTeamConfig(raw) {
2123
3417
  return OpenTeamConfigSchema.parse(raw ?? {});
2124
3418
  }
2125
3419
 
2126
- // src/telemetry/read.ts
2127
- import { readFile } from "node:fs/promises";
2128
-
2129
- // src/telemetry/types.ts
2130
- import { z as z3 } from "zod";
2131
- var CostRecordSchema = z3.object({
2132
- ts: z3.number().finite(),
2133
- sessionID: z3.string().min(1).optional(),
2134
- promptHash: z3.string().min(1),
2135
- promptChars: z3.number().int().min(0),
2136
- tier: ComplexityTierSchema,
2137
- routeKind: z3.enum(["local", "frontier"]),
2138
- selected: ModelRefSchema,
2139
- rationale: z3.string(),
2140
- estimatedCostUSD: z3.number().finite().min(0),
2141
- baselineCostUSD: z3.number().finite().min(0),
2142
- estimatedSavingsUSD: z3.number().finite(),
2143
- budgetAction: z3.string().min(1),
2144
- tokensIn: z3.number().int().min(0).optional(),
2145
- tokensOut: z3.number().int().min(0).optional()
2146
- });
2147
-
2148
- // src/telemetry/read.ts
2149
- var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
2150
- function parseCostRecordsJsonl(text2) {
2151
- const records = [];
2152
- for (const line of text2.split(`
2153
- `)) {
2154
- const trimmed = line.trim();
2155
- if (trimmed.length === 0) {
2156
- continue;
2157
- }
2158
- let candidate;
2159
- try {
2160
- candidate = JSON.parse(trimmed);
2161
- } catch {
2162
- continue;
2163
- }
2164
- const parsed = CostRecordSchema.safeParse(candidate);
2165
- if (parsed.success) {
2166
- records.push(parsed.data);
3420
+ // src/web/git.ts
3421
+ function createGitLastCommit(exec) {
3422
+ return async () => {
3423
+ const output = await exec("git", [
3424
+ "log",
3425
+ "-1",
3426
+ "--pretty=format:%h%x1f%s%x1f%cI"
3427
+ ]);
3428
+ if (output.exitCode !== 0) {
3429
+ return;
2167
3430
  }
2168
- }
2169
- return records;
2170
- }
2171
- function isMissingFile(error) {
2172
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2173
- }
2174
- async function readCostRecords(path, deps = {}) {
2175
- const readFileFn = deps.readFile ?? readFile;
2176
- try {
2177
- const content = await readFileFn(path, "utf8");
2178
- return parseCostRecordsJsonl(content);
2179
- } catch (error) {
2180
- if (isMissingFile(error)) {
2181
- return [];
3431
+ const [hash, subject, at] = output.stdout.trim().split("\x1F");
3432
+ if (hash === undefined || subject === undefined || at === undefined) {
3433
+ return;
2182
3434
  }
2183
- throw error;
2184
- }
3435
+ return { hash, subject, at };
3436
+ };
2185
3437
  }
2186
3438
 
2187
3439
  // src/cli.ts
2188
3440
  var execFileAsync = promisify(execFile);
2189
3441
  var AGENT_DIR = dirname2(ORCHESTRATOR_AGENT_PATH);
2190
- function isMissingFile2(error) {
3442
+ function isMissingFile(error) {
2191
3443
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2192
3444
  }
2193
3445
  var nodeExec = async (command, args) => {
@@ -2205,24 +3457,60 @@ var nodeExec = async (command, args) => {
2205
3457
  };
2206
3458
  async function loadConfig(path) {
2207
3459
  try {
2208
- const content = await readFile2(path, "utf8");
3460
+ const content = await readFile(path, "utf8");
2209
3461
  return loadOpenTeamConfig(JSON.parse(content));
2210
3462
  } catch (error) {
2211
- if (isMissingFile2(error)) {
3463
+ if (isMissingFile(error)) {
2212
3464
  return loadOpenTeamConfig({});
2213
3465
  }
2214
3466
  throw error;
2215
3467
  }
2216
3468
  }
3469
+ async function readTextOptional(path) {
3470
+ try {
3471
+ return await readFile(path, "utf8");
3472
+ } catch (error) {
3473
+ if (isMissingFile(error)) {
3474
+ return;
3475
+ }
3476
+ throw error;
3477
+ }
3478
+ }
3479
+ async function listDirFiles(dir) {
3480
+ try {
3481
+ return await readdir(dir);
3482
+ } catch (error) {
3483
+ if (isMissingFile(error)) {
3484
+ return [];
3485
+ }
3486
+ throw error;
3487
+ }
3488
+ }
3489
+ async function openBrowser(url) {
3490
+ if (process.platform === "win32") {
3491
+ await nodeExec("cmd", ["/c", "start", "", url]);
3492
+ } else if (process.platform === "darwin") {
3493
+ await nodeExec("open", [url]);
3494
+ } else {
3495
+ await nodeExec("xdg-open", [url]);
3496
+ }
3497
+ }
3498
+ function waitForSignal() {
3499
+ return new Promise((resolve) => {
3500
+ const done = () => resolve();
3501
+ process.once("SIGINT", done);
3502
+ process.once("SIGTERM", done);
3503
+ });
3504
+ }
2217
3505
  var deps = {
2218
3506
  loadConfig,
2219
3507
  saveConfig: (config, path) => writeOpenTeamConfigFile(config, path),
2220
3508
  readOpencodeConfig: async (path) => {
2221
3509
  try {
2222
- const content = await readFile2(path, "utf8");
3510
+ const content = await readFile(path, "utf8");
2223
3511
  return JSON.parse(content);
2224
3512
  } catch (error) {
2225
- if (isMissingFile2(error)) {
3513
+ if (isMissingFile(error)) {
2226
3514
  return;
2227
3515
  }
2228
3516
  throw error;
@@ -2242,7 +3530,7 @@ var deps = {
2242
3530
  try {
2243
3531
  entries = await readdir(dir);
2244
3532
  } catch (error) {
2245
- if (isMissingFile2(error)) {
3533
+ if (isMissingFile(error)) {
2246
3534
  return [];
2247
3535
  }
2248
3536
  throw error;
@@ -2250,7 +3538,7 @@ var deps = {
2250
3538
  const mdFiles = entries.filter((entry) => entry.endsWith(".md"));
2251
3539
  return Promise.all(mdFiles.map(async (entry) => ({
2252
3540
  name: entry.replace(/\.md$/, ""),
2253
- contents: await readFile2(join2(dir, entry), "utf8")
3541
+ contents: await readFile(join2(dir, entry), "utf8")
2254
3542
  })));
2255
3543
  },
2256
3544
  probe: (config) => {
@@ -2261,7 +3549,11 @@ var deps = {
2261
3549
  });
2262
3550
  return registry.probe(config.local.runtimes);
2263
3551
  },
2264
- readTelemetry: (path) => readCostRecords(path),
3552
+ readTelemetry: (path) => readRouteCostRecords(DEFAULT_SESSIONS_DIR, {
3553
+ listFiles: listDirFiles,
3554
+ readText: readTextOptional,
3555
+ legacyTelemetryPath: path
3556
+ }),
2265
3557
  configPath: DEFAULT_CONFIG_PATH,
2266
3558
  telemetryPath: DEFAULT_TELEMETRY_PATH,
2267
3559
  opencodeConfigPath: OPENCODE_CONFIG_PATH,
@@ -2299,6 +3591,31 @@ async function main() {
2299
3591
  process.exitCode = result2.exitCode;
2300
3592
  return;
2301
3593
  }
3594
+ if (argv[0] === "dashboard") {
3595
+ const flags = parseDashboardServeArgs(argv.slice(1));
3596
+ if (!flags.status) {
3597
+ const result2 = await runDashboardServe({
3598
+ loadConfig,
3599
+ configPath: DEFAULT_CONFIG_PATH,
3600
+ sessionsDir: DEFAULT_SESSIONS_DIR,
3601
+ decisionsPath: DEFAULT_DECISIONS_PATH,
3602
+ backlogPath: DEFAULT_BACKLOG_PATH,
3603
+ agentDir: AGENT_DIR,
3604
+ legacyTelemetryPath: DEFAULT_TELEMETRY_PATH,
3605
+ readText: readTextOptional,
3606
+ listFiles: listDirFiles,
3607
+ listAgentFiles: deps.listAgentFiles,
3608
+ now: () => new Date().toISOString(),
3609
+ gitLastCommit: createGitLastCommit(nodeExec),
3610
+ waitForSignal,
3611
+ log: (message) => process.stdout.write(`${message}
3612
+ `),
3613
+ openBrowser
3614
+ }, { open: flags.open });
3615
+ process.exitCode = result2.exitCode;
3616
+ return;
3617
+ }
3618
+ }
2302
3619
  const result = await runCli(argv, deps);
2303
3620
  process.stdout.write(`${result.stdout}
2304
3621
  `);