@jmanuelcorral/openteam 0.1.31 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
4
 
3
5
  // src/cli.ts
4
6
  import { execFile, spawn } from "node:child_process";
@@ -301,1381 +303,1505 @@ function tokenFromQuery(query) {
301
303
  return value === null || value === undefined || value.length === 0 ? undefined : value;
302
304
  }
303
305
 
304
- // src/web/start.ts
305
- import { watch as fsWatch } from "node:fs";
306
-
307
- // src/web/paths.ts
308
- var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
309
- var DEFAULT_DECISIONS_PATH = ".opencode/openteam-decisions.md";
310
-
311
- // src/web/server.ts
312
- import {
313
- createServer as createHttpServer
314
- } from "node:http";
315
-
316
- // src/console/render.ts
317
- var TIERS = [
318
- "trivial",
319
- "simple",
320
- "moderate",
321
- "hard"
322
- ];
323
- function escapeHtml(value) {
324
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
325
- }
326
- function usd(value) {
327
- return `$${value.toFixed(5)}`;
328
- }
329
- function pct(value) {
330
- return `${value.toFixed(2)}%`;
306
+ // src/telemetry/aggregate.ts
307
+ function emptyToolcallStats() {
308
+ return { count: 0, ok: 0, failed: 0, totalDurationMs: 0, byTool: {} };
331
309
  }
332
- function ms(value) {
333
- return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`;
310
+ function emptySession(sessionID, ts) {
311
+ return {
312
+ sessionID,
313
+ firstTs: ts,
314
+ lastTs: ts,
315
+ routeCount: 0,
316
+ localCount: 0,
317
+ frontierCount: 0,
318
+ estimatedCostUSD: 0,
319
+ estimatedSavingsUSD: 0,
320
+ realCostUSD: 0,
321
+ messageCount: 0,
322
+ tokensIn: 0,
323
+ tokensOut: 0,
324
+ tokensReasoning: 0,
325
+ tokensCacheRead: 0,
326
+ tokensCacheWrite: 0,
327
+ toolcalls: emptyToolcallStats(),
328
+ models: [],
329
+ agents: []
330
+ };
334
331
  }
335
- function avgMs(stats) {
336
- return stats.count === 0 ? "—" : ms(stats.totalDurationMs / stats.count);
332
+ function pushUnique(list, value) {
333
+ if (value !== undefined && !list.includes(value)) {
334
+ list.push(value);
335
+ }
337
336
  }
338
- function agentModel(agent) {
339
- return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
337
+ function applyRoute(session, event) {
338
+ session.routeCount += 1;
339
+ if (event.routeKind === "local") {
340
+ session.localCount += 1;
341
+ } else {
342
+ session.frontierCount += 1;
343
+ }
344
+ session.estimatedCostUSD += event.estimatedCostUSD;
345
+ session.estimatedSavingsUSD += event.estimatedSavingsUSD;
346
+ pushUnique(session.models, `${event.selected.providerID}/${event.selected.modelID}`);
347
+ pushUnique(session.agents, event.agent);
340
348
  }
341
- function summaryPanel(cost) {
342
- return [
343
- '<section class="panel" id="panel-summary">',
344
- "<h2>Resumen de routing</h2>",
345
- '<div class="grid">',
346
- `<div class="stat"><span class="k">Decisiones</span><span class="v">${cost.count}</span></div>`,
347
- `<div class="stat"><span class="k">Local</span><span class="v">${cost.localCount}</span></div>`,
348
- `<div class="stat"><span class="k">Frontier</span><span class="v">${cost.frontierCount}</span></div>`,
349
- `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(cost.totalEstimatedUSD)}</span></div>`,
350
- `<div class="stat"><span class="k">Baseline</span><span class="v">${usd(cost.totalBaselineUSD)}</span></div>`,
351
- `<div class="stat"><span class="k">Ahorro</span><span class="v good">${usd(cost.totalSavingsUSD)} (${pct(cost.savingsPct)})</span></div>`,
352
- `<div class="stat"><span class="k">Tokens in</span><span class="v">${cost.tokensIn}</span></div>`,
353
- `<div class="stat"><span class="k">Tokens out</span><span class="v">${cost.tokensOut}</span></div>`,
354
- "</div>",
355
- "</section>"
356
- ].join("");
349
+ function applyMessage(session, event) {
350
+ session.messageCount += 1;
351
+ session.realCostUSD += event.costUSD;
352
+ session.tokensIn += event.tokensIn;
353
+ session.tokensOut += event.tokensOut;
354
+ session.tokensReasoning += event.tokensReasoning;
355
+ session.tokensCacheRead += event.tokensCacheRead;
356
+ session.tokensCacheWrite += event.tokensCacheWrite;
357
+ pushUnique(session.models, `${event.providerID}/${event.modelID}`);
358
+ pushUnique(session.agents, event.agent);
357
359
  }
358
- function tierPanel(cost) {
359
- const rows = TIERS.map((tier) => {
360
- const summary = cost.byTier[tier];
361
- return `<tr><td>${tier}</td><td>${summary.count}</td><td>${usd(summary.estimatedUSD)}</td><td class="good">${usd(summary.savingsUSD)}</td></tr>`;
362
- }).join("");
363
- return [
364
- '<section class="panel" id="panel-tier">',
365
- "<h2>Por tier</h2>",
366
- "<table><thead><tr><th>Tier</th><th>Nº</th><th>Coste</th><th>Ahorro</th></tr></thead>",
367
- `<tbody>${rows}</tbody></table>`,
368
- "</section>"
369
- ].join("");
360
+ function applyToolUsage(stats, event) {
361
+ stats.count += 1;
362
+ stats.totalDurationMs += event.durationMs;
363
+ if (event.ok) {
364
+ stats.ok += 1;
365
+ } else {
366
+ stats.failed += 1;
367
+ }
370
368
  }
371
- function loopItemRow(item) {
372
- const box = item.done ? "☑" : "☐";
373
- const cls = item.done ? "done" : "open";
374
- const who = item.assignee === undefined ? "" : `<span class="who">@${escapeHtml(item.assignee)}</span> `;
375
- return `<li class="${cls}"><span class="box">${box}</span> ${who}${escapeHtml(item.text)}</li>`;
369
+ function applyToolcall(session, event) {
370
+ applyToolUsage(session.toolcalls, event);
371
+ const byTool = session.toolcalls.byTool[event.tool] ?? {
372
+ count: 0,
373
+ ok: 0,
374
+ failed: 0,
375
+ totalDurationMs: 0
376
+ };
377
+ applyToolUsage(byTool, event);
378
+ session.toolcalls.byTool[event.tool] = byTool;
379
+ pushUnique(session.agents, event.agent);
376
380
  }
377
- function loopPanel(loop) {
378
- if (loop === undefined) {
379
- return [
380
- '<section class="panel" id="panel-loop">',
381
- "<h2>Loop</h2>",
382
- '<p class="muted">Sin backlog activo (no hay <code>openteam-backlog.md</code>).</p>',
383
- "</section>"
384
- ].join("");
381
+ function applyEndpoint(session, event) {
382
+ if (session.endpoint !== undefined && session.endpoint.lastSeenTs >= event.ts) {
383
+ return;
385
384
  }
386
- const items = loop.items.map(loopItemRow).join("");
387
- const commit = loop.lastCommit === undefined ? "" : `<p class="muted">Último commit: <code>${escapeHtml(loop.lastCommit.hash)}</code> ${escapeHtml(loop.lastCommit.subject)}</p>`;
388
- return [
389
- '<section class="panel" id="panel-loop">',
390
- "<h2>Loop</h2>",
391
- `<div class="progress" role="progressbar" aria-valuenow="${loop.progressPct}" aria-valuemin="0" aria-valuemax="100"><div class="bar" style="width:${loop.progressPct}%"></div></div>`,
392
- `<p class="muted">${loop.done}/${loop.total} completados · ${loop.open} abiertos · ${loop.progressPct}%</p>`,
393
- `<ul class="items">${items}</ul>`,
394
- commit,
395
- "</section>"
396
- ].join("");
397
- }
398
- function teamPanel(team) {
399
- if (team.length === 0) {
400
- return [
401
- '<section class="panel" id="panel-team">',
402
- "<h2>Equipo</h2>",
403
- '<p class="muted">Sin agentes en <code>.opencode/agent/</code>.</p>',
404
- "</section>"
405
- ].join("");
385
+ const endpoint = {
386
+ url: event.url,
387
+ lastSeenTs: event.ts
388
+ };
389
+ if (event.directory !== undefined) {
390
+ endpoint.directory = event.directory;
406
391
  }
407
- const rows = team.map((agent) => `<tr><td>${escapeHtml(agent.name)}</td><td>${escapeHtml(agent.mode)}</td><td>${escapeHtml(agentModel(agent))}</td></tr>`).join("");
408
- return [
409
- '<section class="panel" id="panel-team">',
410
- "<h2>Equipo</h2>",
411
- "<table><thead><tr><th>Agente</th><th>Modo</th><th>LLM</th></tr></thead>",
412
- `<tbody>${rows}</tbody></table>`,
413
- "</section>"
414
- ].join("");
415
- }
416
- function routeRow(route) {
417
- 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>`;
418
- }
419
- function routesPanel(routes) {
420
- if (routes.length === 0) {
421
- return [
422
- '<section class="panel" id="panel-routes">',
423
- "<h2>Decisiones recientes</h2>",
424
- '<p class="muted">Sin decisiones de routing todavía.</p>',
425
- "</section>"
426
- ].join("");
392
+ if (event.worktree !== undefined) {
393
+ endpoint.worktree = event.worktree;
427
394
  }
428
- const rows = routes.map(routeRow).join("");
429
- return [
430
- '<section class="panel" id="panel-routes">',
431
- "<h2>Decisiones recientes</h2>",
432
- "<table><thead><tr><th>Tier</th><th>Ruta</th><th>Modelo</th><th>Prompt (hash)</th><th>Coste</th><th>Ahorro</th></tr></thead>",
433
- `<tbody>${rows}</tbody></table>`,
434
- "</section>"
435
- ].join("");
395
+ if (event.title !== undefined) {
396
+ endpoint.title = event.title;
397
+ }
398
+ session.endpoint = endpoint;
436
399
  }
437
- function activityRow(entry) {
438
- const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
439
- return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
400
+ function mergeTool(target, source) {
401
+ target.count += source.count;
402
+ target.ok += source.ok;
403
+ target.failed += source.failed;
404
+ target.totalDurationMs += source.totalDurationMs;
440
405
  }
441
- function activityPanel(activity) {
442
- if (activity.length === 0) {
443
- return [
444
- '<section class="panel" id="panel-activity">',
445
- "<h2>Actividad</h2>",
446
- '<p class="muted">Sin actividad registrada.</p>',
447
- "</section>"
448
- ].join("");
406
+ function accumulateTotals(totals, session) {
407
+ totals.sessions += 1;
408
+ totals.routeCount += session.routeCount;
409
+ totals.localCount += session.localCount;
410
+ totals.frontierCount += session.frontierCount;
411
+ totals.estimatedCostUSD += session.estimatedCostUSD;
412
+ totals.estimatedSavingsUSD += session.estimatedSavingsUSD;
413
+ totals.realCostUSD += session.realCostUSD;
414
+ totals.messageCount += session.messageCount;
415
+ totals.tokensIn += session.tokensIn;
416
+ totals.tokensOut += session.tokensOut;
417
+ totals.tokensReasoning += session.tokensReasoning;
418
+ totals.tokensCacheRead += session.tokensCacheRead;
419
+ totals.tokensCacheWrite += session.tokensCacheWrite;
420
+ mergeTool(totals.toolcalls, session.toolcalls);
421
+ for (const [tool, usage] of Object.entries(session.toolcalls.byTool)) {
422
+ const target = totals.toolcalls.byTool[tool] ?? {
423
+ count: 0,
424
+ ok: 0,
425
+ failed: 0,
426
+ totalDurationMs: 0
427
+ };
428
+ mergeTool(target, usage);
429
+ totals.toolcalls.byTool[tool] = target;
449
430
  }
450
- const rows = activity.map(activityRow).join("");
451
- return [
452
- '<section class="panel" id="panel-activity">',
453
- "<h2>Actividad</h2>",
454
- `<ul class="timeline">${rows}</ul>`,
455
- "</section>"
456
- ].join("");
457
431
  }
458
- function totalsPanel(totals) {
459
- const t = totals.toolcalls;
460
- return [
461
- '<section class="panel" id="panel-totals">',
462
- "<h2>Telemetría real (todas las sesiones)</h2>",
463
- '<div class="grid">',
464
- `<div class="stat"><span class="k">Sesiones</span><span class="v">${totals.sessions}</span></div>`,
465
- `<div class="stat"><span class="k">Mensajes</span><span class="v">${totals.messageCount}</span></div>`,
466
- `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(totals.realCostUSD)}</span></div>`,
467
- `<div class="stat"><span class="k">Ahorro est.</span><span class="v good">${usd(totals.estimatedSavingsUSD)}</span></div>`,
468
- `<div class="stat"><span class="k">Tokens in</span><span class="v">${totals.tokensIn}</span></div>`,
469
- `<div class="stat"><span class="k">Tokens out</span><span class="v">${totals.tokensOut}</span></div>`,
470
- `<div class="stat"><span class="k">Reasoning</span><span class="v">${totals.tokensReasoning}</span></div>`,
471
- `<div class="stat"><span class="k">Cache r/w</span><span class="v">${totals.tokensCacheRead}/${totals.tokensCacheWrite}</span></div>`,
472
- `<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>`,
473
- `<div class="stat"><span class="k">Dur. media tool</span><span class="v">${avgMs(t)}</span></div>`,
474
- "</div>",
475
- "</section>"
476
- ].join("");
477
- }
478
- function sessionRow(session) {
479
- const models = session.models.map(escapeHtml).join("<br>") || "—";
480
- return [
481
- "<tr>",
482
- `<td class="hash">${escapeHtml(session.sessionID)}</td>`,
483
- `<td>${models}</td>`,
484
- `<td><span class="local">${session.localCount}</span>/<span class="frontier">${session.frontierCount}</span></td>`,
485
- `<td>${usd(session.realCostUSD)}</td>`,
486
- `<td>${session.tokensIn}/${session.tokensOut}</td>`,
487
- `<td>${session.toolcalls.count} (${avgMs(session.toolcalls)})</td>`,
488
- "</tr>"
489
- ].join("");
490
- }
491
- function sessionsPanel(sessions) {
492
- if (sessions.length === 0) {
493
- return [
494
- '<section class="panel" id="panel-sessions">',
495
- "<h2>Sesiones</h2>",
496
- '<p class="muted">Sin sesiones registradas todavía.</p>',
497
- "</section>"
498
- ].join("");
432
+ function toDecision(event) {
433
+ const view = {
434
+ ts: event.ts,
435
+ sessionID: event.sessionID,
436
+ summary: event.summary
437
+ };
438
+ if (event.agent !== undefined) {
439
+ view.agent = event.agent;
499
440
  }
500
- const rows = sessions.map(sessionRow).join("");
501
- return [
502
- '<section class="panel" id="panel-sessions">',
503
- "<h2>Sesiones</h2>",
504
- "<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>",
505
- `<tbody>${rows}</tbody></table>`,
506
- "</section>"
507
- ].join("");
441
+ if (event.tags !== undefined) {
442
+ view.tags = [...event.tags];
443
+ }
444
+ return view;
508
445
  }
509
- function decisionRow(decision) {
510
- const who = decision.agent === undefined ? "" : `<span class="who">${escapeHtml(decision.agent)}</span> `;
511
- const tags = decision.tags === undefined || decision.tags.length === 0 ? "" : ` ${decision.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
512
- return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision.summary)}${tags}</li>`;
446
+ function toMeeting(event) {
447
+ const view = {
448
+ ts: event.ts,
449
+ sessionID: event.sessionID,
450
+ batchID: event.batchID,
451
+ roles: [...event.roles]
452
+ };
453
+ if (event.purpose !== undefined) {
454
+ view.purpose = event.purpose;
455
+ }
456
+ if (event.decisionIDs !== undefined) {
457
+ view.decisionIDs = [...event.decisionIDs];
458
+ }
459
+ return view;
513
460
  }
514
- function decisionsPanel(decisions) {
515
- if (decisions.length === 0) {
516
- return [
517
- '<section class="panel" id="panel-decisions">',
518
- "<h2>Decisiones</h2>",
519
- '<p class="muted">Sin decisiones registradas (el scribe escribe en <code>openteam-decisions.md</code>).</p>',
520
- "</section>"
521
- ].join("");
461
+ function toActivity(event) {
462
+ const view = {
463
+ ts: event.ts,
464
+ sessionID: event.sessionID,
465
+ kind: event.kind,
466
+ summary: event.summary
467
+ };
468
+ if (event.agent !== undefined) {
469
+ view.agent = event.agent;
522
470
  }
523
- const rows = decisions.map(decisionRow).join("");
524
- return [
525
- '<section class="panel" id="panel-decisions">',
526
- "<h2>Decisiones</h2>",
527
- `<ul class="timeline">${rows}</ul>`,
528
- "</section>"
529
- ].join("");
471
+ return view;
530
472
  }
531
- function meetingRow(meeting) {
532
- const roles = meeting.roles.map(escapeHtml).join(", ");
533
- const purpose = meeting.purpose === undefined ? "" : `: ${escapeHtml(meeting.purpose)}`;
534
- return `<li class="agent"><span class="kind">reunión</span> <span class="who">${roles}</span>${purpose}</li>`;
473
+ function byTsDesc(list) {
474
+ return [...list].sort((a, b) => b.ts - a.ts);
535
475
  }
536
- function meetingsPanel(meetings) {
537
- if (meetings.length === 0) {
538
- return [
539
- '<section class="panel" id="panel-meetings">',
540
- "<h2>Reuniones</h2>",
541
- '<p class="muted">Sin reuniones multi-agente todavía.</p>',
542
- "</section>"
543
- ].join("");
476
+ function aggregateEvents(events) {
477
+ const sessions = new Map;
478
+ const decisions = [];
479
+ const meetings = [];
480
+ const activity = [];
481
+ const sessionFor = (sessionID, ts) => {
482
+ const existing = sessions.get(sessionID);
483
+ if (existing === undefined) {
484
+ const created = emptySession(sessionID, ts);
485
+ sessions.set(sessionID, created);
486
+ return created;
487
+ }
488
+ existing.firstTs = Math.min(existing.firstTs, ts);
489
+ existing.lastTs = Math.max(existing.lastTs, ts);
490
+ return existing;
491
+ };
492
+ for (const event of events) {
493
+ switch (event.type) {
494
+ case "route":
495
+ applyRoute(sessionFor(event.sessionID, event.ts), event);
496
+ break;
497
+ case "message":
498
+ applyMessage(sessionFor(event.sessionID, event.ts), event);
499
+ break;
500
+ case "toolcall":
501
+ applyToolcall(sessionFor(event.sessionID, event.ts), event);
502
+ break;
503
+ case "session-endpoint":
504
+ applyEndpoint(sessionFor(event.sessionID, event.ts), event);
505
+ break;
506
+ case "decision":
507
+ decisions.push(toDecision(event));
508
+ break;
509
+ case "meeting":
510
+ meetings.push(toMeeting(event));
511
+ break;
512
+ case "activity":
513
+ activity.push(toActivity(event));
514
+ break;
515
+ }
544
516
  }
545
- const rows = meetings.map(meetingRow).join("");
546
- return [
547
- '<section class="panel" id="panel-meetings">',
548
- "<h2>Reuniones</h2>",
549
- `<ul class="timeline">${rows}</ul>`,
550
- "</section>"
551
- ].join("");
517
+ const totals = {
518
+ sessions: 0,
519
+ routeCount: 0,
520
+ localCount: 0,
521
+ frontierCount: 0,
522
+ estimatedCostUSD: 0,
523
+ estimatedSavingsUSD: 0,
524
+ realCostUSD: 0,
525
+ messageCount: 0,
526
+ tokensIn: 0,
527
+ tokensOut: 0,
528
+ tokensReasoning: 0,
529
+ tokensCacheRead: 0,
530
+ tokensCacheWrite: 0,
531
+ toolcalls: emptyToolcallStats()
532
+ };
533
+ const summaries = [...sessions.values()].sort((a, b) => b.lastTs - a.lastTs);
534
+ for (const summary of summaries) {
535
+ summary.models.sort();
536
+ summary.agents.sort();
537
+ accumulateTotals(totals, summary);
538
+ }
539
+ return {
540
+ sessions: summaries,
541
+ totals,
542
+ decisions: byTsDesc(decisions),
543
+ meetings: byTsDesc(meetings),
544
+ activity: byTsDesc(activity)
545
+ };
552
546
  }
553
- var STATUS_LABELS = {
554
- running: "activa",
555
- idle: "en reposo",
556
- "waiting-input": "esperando input",
557
- ended: "finalizada"
558
- };
559
- function statusBadge(status) {
560
- return `<span class="badge status-${status}">${STATUS_LABELS[status]}</span>`;
547
+
548
+ // src/storage/path.ts
549
+ class StoragePathError extends Error {
550
+ constructor(message) {
551
+ super(message);
552
+ this.name = "StoragePathError";
553
+ }
561
554
  }
562
- function tabBar(sessions) {
563
- const allTab = '<button type="button" class="tab" data-tab="all">Todas</button>';
564
- const tabs = sessions.map((tab) => {
565
- const id = escapeHtml(tab.sessionID);
566
- const short = escapeHtml(shortSessionID(tab.sessionID));
567
- return `<button type="button" class="tab" data-tab="${id}"><span class="dot status-${tab.status}"></span>${short}</button>`;
568
- }).join("");
569
- return `<nav class="tabs" id="tabbar">${allTab}${tabs}</nav>`;
555
+ function normalizeVirtualPath(input) {
556
+ const segments = [];
557
+ for (const rawSegment of input.split(/[\\/]+/)) {
558
+ if (rawSegment === "" || rawSegment === ".") {
559
+ continue;
560
+ }
561
+ if (rawSegment === "..") {
562
+ if (segments.length === 0) {
563
+ throw new StoragePathError(`la ruta "${input}" escapa por encima de la raíz`);
564
+ }
565
+ segments.pop();
566
+ continue;
567
+ }
568
+ segments.push(rawSegment);
569
+ }
570
+ return segments.join("/");
570
571
  }
571
- function shortSessionID(sessionID) {
572
- return sessionID.length <= 12 ? sessionID : `${sessionID.slice(0, 12)}…`;
572
+ function joinVirtualPath(...segments) {
573
+ return normalizeVirtualPath(segments.join("/"));
573
574
  }
574
- function sessionHeaderPanel(tab) {
575
- const endpoint = tab.endpoint === undefined ? '<span class="muted">endpoint no anunciado</span>' : `<code>${escapeHtml(tab.endpoint.url)}</code>`;
576
- const dir = tab.endpoint?.directory === undefined ? "" : `<div class="stat"><span class="k">Directorio</span><span class="v mono">${escapeHtml(tab.endpoint.directory)}</span></div>`;
577
- const title = tab.endpoint?.title === undefined ? "" : `<div class="stat"><span class="k">Título</span><span class="v">${escapeHtml(tab.endpoint.title)}</span></div>`;
578
- const s = tab.summary;
579
- return [
580
- '<section class="panel">',
581
- `<h2>Sesión ${escapeHtml(shortSessionID(tab.sessionID))} ${statusBadge(tab.status)}</h2>`,
582
- '<div class="grid">',
583
- `<div class="stat"><span class="k">Endpoint</span><span class="v">${endpoint}</span></div>`,
584
- dir,
585
- title,
586
- `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(s.realCostUSD)}</span></div>`,
587
- `<div class="stat"><span class="k">Tokens in/out</span><span class="v">${s.tokensIn}/${s.tokensOut}</span></div>`,
588
- `<div class="stat"><span class="k">Local/Frontier</span><span class="v"><span class="local">${s.localCount}</span>/<span class="frontier">${s.frontierCount}</span></span></div>`,
589
- `<div class="stat"><span class="k">Toolcalls</span><span class="v">${s.toolcalls.count} (${avgMs(s.toolcalls)})</span></div>`,
590
- "</div>",
591
- "</section>"
592
- ].join("");
575
+ function splitVirtualPath(path) {
576
+ const normalized = normalizeVirtualPath(path);
577
+ return normalized === "" ? [] : normalized.split("/");
593
578
  }
594
- function terminalPanel(tab) {
595
- return [
596
- '<section class="panel term">',
597
- "<h2>Terminal (sesión)</h2>",
598
- '<pre class="termlog" aria-live="polite"></pre>',
599
- '<form class="terminput" autocomplete="off">',
600
- `<input type="text" name="text" placeholder="Enviar prompt a ${escapeHtml(shortSessionID(tab.sessionID))}…" aria-label="Prompt a la sesión">`,
601
- '<button type="submit">Enviar</button>',
602
- "</form>",
603
- "</section>"
604
- ].join("");
579
+
580
+ // src/telemetry/events.ts
581
+ import { z as z4 } from "zod";
582
+
583
+ // src/capabilities/types.ts
584
+ import { z as z2 } from "zod";
585
+ var CapabilityTierSchema = z2.union([
586
+ z2.literal(0),
587
+ z2.literal(1),
588
+ z2.literal(2),
589
+ z2.literal(3),
590
+ z2.literal(4),
591
+ z2.literal(5)
592
+ ]);
593
+ var ComplexityTierSchema = z2.enum([
594
+ "trivial",
595
+ "simple",
596
+ "moderate",
597
+ "hard"
598
+ ]);
599
+ var ModelCapabilityProfileSchema = z2.object({
600
+ ref: z2.object({
601
+ providerID: z2.string().min(1),
602
+ modelID: z2.string().min(1)
603
+ }),
604
+ kind: z2.enum(["local", "frontier", "router"]),
605
+ contextWindow: z2.number().int().positive(),
606
+ maxOutputTokens: z2.number().int().positive(),
607
+ supportsToolCalling: z2.boolean(),
608
+ supportsVision: z2.boolean(),
609
+ reasoningTier: CapabilityTierSchema,
610
+ codeQualityTier: CapabilityTierSchema,
611
+ costPer1M: z2.object({
612
+ inputUSD: z2.number().min(0),
613
+ outputUSD: z2.number().min(0)
614
+ }),
615
+ availability: z2.enum(["available", "degraded", "unavailable"])
616
+ });
617
+
618
+ // src/config/schema.ts
619
+ import { z as z3 } from "zod";
620
+ var ModelRefSchema = z3.object({
621
+ providerID: z3.string().min(1),
622
+ modelID: z3.string().min(1)
623
+ });
624
+ var RouterModeSchema = z3.enum(["economy", "balanced", "quality"]);
625
+ var PrivacyModeSchema = z3.enum([
626
+ "forceLocalOnSensitive",
627
+ "consentBeforeFrontier",
628
+ "off"
629
+ ]);
630
+ var BaselineModeSchema = z3.enum(["auto", "pinned"]);
631
+ var ConsoleHostSchema = z3.enum(["127.0.0.1", "localhost"]);
632
+ var ConsoleConfigSchema = z3.object({
633
+ host: ConsoleHostSchema.default("127.0.0.1"),
634
+ port: z3.number().int().min(1024).max(65535).default(4599),
635
+ autoPortFallback: z3.boolean().default(true),
636
+ refreshMs: z3.number().int().min(250).default(2000),
637
+ recentRoutes: z3.number().int().positive().default(50),
638
+ openBrowser: z3.boolean().default(false),
639
+ remoteStorage: z3.boolean().default(false),
640
+ terminal: z3.object({
641
+ enabled: z3.boolean().default(true),
642
+ pty: z3.boolean().optional()
643
+ }).default({ enabled: true })
644
+ }).default({
645
+ host: "127.0.0.1",
646
+ port: 4599,
647
+ autoPortFallback: true,
648
+ refreshMs: 2000,
649
+ recentRoutes: 50,
650
+ openBrowser: false,
651
+ remoteStorage: false,
652
+ terminal: { enabled: true }
653
+ });
654
+ var StorageConfigSchema = z3.object({
655
+ sqliteIndex: z3.object({
656
+ enabled: z3.boolean().default(false),
657
+ path: z3.string().min(1).default(".opencode/openteam/index.sqlite")
658
+ }).default({ enabled: false, path: ".opencode/openteam/index.sqlite" })
659
+ }).default({
660
+ sqliteIndex: { enabled: false, path: ".opencode/openteam/index.sqlite" }
661
+ });
662
+ var defaultLocalModel = {
663
+ providerID: "ollama",
664
+ modelID: "qwen3:8b"
665
+ };
666
+ var defaultFrontierModel = {
667
+ providerID: "anthropic",
668
+ modelID: "claude-sonnet-4-5"
669
+ };
670
+ var LocalRuntimeSchema = z3.object({
671
+ id: z3.enum(["ollama", "lmstudio", "foundry-local"]),
672
+ enabled: z3.boolean().default(true),
673
+ baseURL: z3.string().url().optional(),
674
+ discovery: z3.enum(["cli", "sdk", "manual"]).optional(),
675
+ defaultModel: ModelRefSchema
676
+ });
677
+ var OpenTeamConfigObjectSchema = z3.object({
678
+ baseline: z3.object({
679
+ mode: BaselineModeSchema.default("auto"),
680
+ pinnedModel: ModelRefSchema.nullable().default(null),
681
+ hardDefault: ModelRefSchema.default(defaultFrontierModel)
682
+ }).default({
683
+ mode: "auto",
684
+ pinnedModel: null,
685
+ hardDefault: defaultFrontierModel
686
+ }),
687
+ router: z3.object({
688
+ mode: RouterModeSchema.default("balanced"),
689
+ localDefault: ModelRefSchema.default(defaultLocalModel),
690
+ trivialPromptMaxChars: z3.number().int().positive().default(280),
691
+ frontierPromptMinChars: z3.number().int().positive().default(2000)
692
+ }).default({
693
+ mode: "balanced",
694
+ localDefault: defaultLocalModel,
695
+ trivialPromptMaxChars: 280,
696
+ frontierPromptMinChars: 2000
697
+ }),
698
+ local: z3.object({
699
+ runtimes: z3.array(LocalRuntimeSchema).min(1).default([
700
+ {
701
+ id: "ollama",
702
+ enabled: true,
703
+ baseURL: "http://localhost:11434/v1",
704
+ defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
705
+ }
706
+ ])
707
+ }).default({
708
+ runtimes: [
709
+ {
710
+ id: "ollama",
711
+ enabled: true,
712
+ baseURL: "http://localhost:11434/v1",
713
+ defaultModel: defaultLocalModel
714
+ }
715
+ ]
716
+ }),
717
+ budgets: z3.object({
718
+ sessionUSD: z3.number().positive().optional(),
719
+ monthlyUSD: z3.number().positive().optional(),
720
+ frontierTokensPerSession: z3.number().int().positive().optional(),
721
+ hardStopOnBudgetExhaustion: z3.boolean().default(false)
722
+ }).default({ hardStopOnBudgetExhaustion: false }),
723
+ privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
724
+ console: ConsoleConfigSchema,
725
+ storage: StorageConfigSchema
726
+ });
727
+ var OpenTeamConfigSchema = OpenTeamConfigObjectSchema;
728
+
729
+ // src/telemetry/events.ts
730
+ var EVENT_SCHEMA_VERSION = 1;
731
+ var EventBaseSchema = z4.object({
732
+ v: z4.literal(EVENT_SCHEMA_VERSION),
733
+ ts: z4.number().finite(),
734
+ sessionID: z4.string().min(1)
735
+ });
736
+ var RouteEventSchema = EventBaseSchema.extend({
737
+ type: z4.literal("route"),
738
+ promptHash: z4.string().min(1),
739
+ promptChars: z4.number().int().min(0),
740
+ tier: ComplexityTierSchema,
741
+ routeKind: z4.enum(["local", "frontier"]),
742
+ selected: ModelRefSchema,
743
+ rationale: z4.string(),
744
+ estimatedCostUSD: z4.number().finite().min(0),
745
+ baselineCostUSD: z4.number().finite().min(0),
746
+ estimatedSavingsUSD: z4.number().finite(),
747
+ budgetAction: z4.string().min(1),
748
+ tokensIn: z4.number().int().min(0).optional(),
749
+ tokensOut: z4.number().int().min(0).optional(),
750
+ decisionID: z4.string().min(1).optional(),
751
+ agent: z4.string().min(1).optional(),
752
+ success: z4.boolean().optional(),
753
+ batchID: z4.string().min(1).optional(),
754
+ failureReason: z4.string().min(1).optional(),
755
+ failureStage: z4.string().min(1).optional()
756
+ });
757
+ var MessageEventSchema = EventBaseSchema.extend({
758
+ type: z4.literal("message"),
759
+ providerID: z4.string().min(1),
760
+ modelID: z4.string().min(1),
761
+ agent: z4.string().min(1).optional(),
762
+ mode: z4.string().min(1).optional(),
763
+ messageID: z4.string().min(1).optional(),
764
+ costUSD: z4.number().finite().min(0),
765
+ tokensIn: z4.number().int().min(0),
766
+ tokensOut: z4.number().int().min(0),
767
+ tokensReasoning: z4.number().int().min(0),
768
+ tokensCacheRead: z4.number().int().min(0),
769
+ tokensCacheWrite: z4.number().int().min(0),
770
+ durationMs: z4.number().int().min(0)
771
+ });
772
+ var ToolcallEventSchema = EventBaseSchema.extend({
773
+ type: z4.literal("toolcall"),
774
+ tool: z4.string().min(1),
775
+ callID: z4.string().min(1),
776
+ agent: z4.string().min(1).optional(),
777
+ durationMs: z4.number().int().min(0),
778
+ ok: z4.boolean(),
779
+ title: z4.string().optional()
780
+ });
781
+ var MeetingEventSchema = EventBaseSchema.extend({
782
+ type: z4.literal("meeting"),
783
+ batchID: z4.string().min(1),
784
+ purpose: z4.string().optional(),
785
+ roles: z4.array(z4.string().min(1)),
786
+ decisionIDs: z4.array(z4.string().min(1)).optional()
787
+ });
788
+ var DecisionEventSchema = EventBaseSchema.extend({
789
+ type: z4.literal("decision"),
790
+ agent: z4.string().min(1).optional(),
791
+ summary: z4.string().min(1),
792
+ tags: z4.array(z4.string().min(1)).optional()
793
+ });
794
+ var ActivityEventSchema = EventBaseSchema.extend({
795
+ type: z4.literal("activity"),
796
+ kind: z4.enum(["route", "agent", "decision", "commit"]),
797
+ agent: z4.string().min(1).optional(),
798
+ summary: z4.string().min(1)
799
+ });
800
+ var SessionEndpointEventSchema = EventBaseSchema.extend({
801
+ type: z4.literal("session-endpoint"),
802
+ url: z4.string().url(),
803
+ directory: z4.string().min(1).optional(),
804
+ worktree: z4.string().min(1).optional(),
805
+ title: z4.string().min(1).optional()
806
+ });
807
+ var OpenTeamEventSchema = z4.discriminatedUnion("type", [
808
+ RouteEventSchema,
809
+ MessageEventSchema,
810
+ ToolcallEventSchema,
811
+ MeetingEventSchema,
812
+ DecisionEventSchema,
813
+ ActivityEventSchema,
814
+ SessionEndpointEventSchema
815
+ ]);
816
+
817
+ // src/storage/fsStorageProvider.ts
818
+ import {
819
+ appendFile,
820
+ mkdir,
821
+ readdir,
822
+ readFile,
823
+ rm,
824
+ stat,
825
+ writeFile
826
+ } from "node:fs/promises";
827
+ import { dirname, join } from "node:path";
828
+ function isMissing(error) {
829
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
830
+ }
831
+ function createFsStorageProvider(root) {
832
+ function toDisk(path) {
833
+ return join(root, ...splitVirtualPath(path));
834
+ }
835
+ return {
836
+ async read(path) {
837
+ try {
838
+ return await readFile(toDisk(path), "utf8");
839
+ } catch (error) {
840
+ if (isMissing(error)) {
841
+ return;
842
+ }
843
+ throw error;
844
+ }
845
+ },
846
+ async write(path, content) {
847
+ const disk = toDisk(path);
848
+ await mkdir(dirname(disk), { recursive: true });
849
+ await writeFile(disk, content, "utf8");
850
+ },
851
+ async append(path, content) {
852
+ const disk = toDisk(path);
853
+ await mkdir(dirname(disk), { recursive: true });
854
+ await appendFile(disk, content, "utf8");
855
+ },
856
+ async exists(path) {
857
+ try {
858
+ await stat(toDisk(path));
859
+ return true;
860
+ } catch (error) {
861
+ if (isMissing(error)) {
862
+ return false;
863
+ }
864
+ throw error;
865
+ }
866
+ },
867
+ async list(dir) {
868
+ try {
869
+ const names = await readdir(toDisk(dir));
870
+ return names.sort();
871
+ } catch (error) {
872
+ if (isMissing(error)) {
873
+ return [];
874
+ }
875
+ throw error;
876
+ }
877
+ },
878
+ async isDirectory(path) {
879
+ try {
880
+ return (await stat(toDisk(path))).isDirectory();
881
+ } catch (error) {
882
+ if (isMissing(error)) {
883
+ return false;
884
+ }
885
+ throw error;
886
+ }
887
+ },
888
+ async mkdir(dir) {
889
+ await mkdir(toDisk(dir), { recursive: true });
890
+ },
891
+ async delete(path) {
892
+ await rm(toDisk(path), { force: true });
893
+ },
894
+ async stat(path) {
895
+ try {
896
+ const stats = await stat(toDisk(path));
897
+ return {
898
+ size: stats.size,
899
+ mtimeMs: stats.mtimeMs,
900
+ isDirectory: stats.isDirectory()
901
+ };
902
+ } catch (error) {
903
+ if (isMissing(error)) {
904
+ return;
905
+ }
906
+ throw error;
907
+ }
908
+ }
909
+ };
605
910
  }
606
- function ptyPanel(tab) {
607
- return [
608
- `<section class="panel pty" data-pty-session="${escapeHtml(tab.sessionID)}">`,
609
- "<h2>Terminal (PTY)</h2>",
610
- '<pre class="ptylog" aria-live="polite"></pre>',
611
- '<form class="ptyinput" autocomplete="off" hidden>',
612
- '<input type="text" name="cmd" placeholder="Comando…" aria-label="Comando de la terminal">',
613
- '<button type="submit">Ejecutar</button>',
614
- "</form>",
615
- '<button type="button" class="pty-start">Abrir shell</button>',
616
- "</section>"
617
- ].join("");
911
+
912
+ // src/telemetry/types.ts
913
+ import { z as z5 } from "zod";
914
+ var CostRecordSchema = z5.object({
915
+ ts: z5.number().finite(),
916
+ sessionID: z5.string().min(1).optional(),
917
+ promptHash: z5.string().min(1),
918
+ promptChars: z5.number().int().min(0),
919
+ tier: ComplexityTierSchema,
920
+ routeKind: z5.enum(["local", "frontier"]),
921
+ selected: ModelRefSchema,
922
+ rationale: z5.string(),
923
+ estimatedCostUSD: z5.number().finite().min(0),
924
+ baselineCostUSD: z5.number().finite().min(0),
925
+ estimatedSavingsUSD: z5.number().finite(),
926
+ budgetAction: z5.string().min(1),
927
+ tokensIn: z5.number().int().min(0).optional(),
928
+ tokensOut: z5.number().int().min(0).optional()
929
+ });
930
+
931
+ // src/telemetry/read.ts
932
+ var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
933
+ function parseCostRecordsJsonl(text) {
934
+ const records = [];
935
+ for (const line of text.split(`
936
+ `)) {
937
+ const trimmed = line.trim();
938
+ if (trimmed.length === 0) {
939
+ continue;
940
+ }
941
+ let candidate;
942
+ try {
943
+ candidate = JSON.parse(trimmed);
944
+ } catch {
945
+ continue;
946
+ }
947
+ const parsed = CostRecordSchema.safeParse(candidate);
948
+ if (parsed.success) {
949
+ records.push(parsed.data);
950
+ }
951
+ }
952
+ return records;
618
953
  }
619
- function sessionView(tab, terminalEnabled, ptyEnabled) {
620
- const body = [
621
- sessionHeaderPanel(tab),
622
- terminalEnabled ? terminalPanel(tab) : "",
623
- terminalEnabled && ptyEnabled ? ptyPanel(tab) : "",
624
- routesPanel(tab.recentRoutes),
625
- decisionsPanel(tab.decisions),
626
- meetingsPanel(tab.meetings),
627
- activityPanel(tab.activity)
628
- ].join("");
629
- return `<div class="view" data-view="${escapeHtml(tab.sessionID)}" hidden>${body}</div>`;
954
+
955
+ // src/telemetry/eventLog.ts
956
+ var DEFAULT_SESSIONS_DIR = ".opencode/openteam/sessions";
957
+ var LEGACY_SESSION_ID = "legacy";
958
+ function parseEventsJsonl(text) {
959
+ const events = [];
960
+ for (const line of text.split(`
961
+ `)) {
962
+ const trimmed = line.trim();
963
+ if (trimmed.length === 0) {
964
+ continue;
965
+ }
966
+ let candidate;
967
+ try {
968
+ candidate = JSON.parse(trimmed);
969
+ } catch {
970
+ continue;
971
+ }
972
+ const parsed = OpenTeamEventSchema.safeParse(candidate);
973
+ if (parsed.success) {
974
+ events.push(parsed.data);
975
+ }
976
+ }
977
+ return events;
630
978
  }
631
- var STYLE = `
632
- :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
633
- *{box-sizing:border-box}
634
- body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}
635
- header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}
636
- header h1{font-size:18px;margin:0}
637
- header .meta{color:var(--muted);font-size:12px}
638
- main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;padding:24px}
639
- .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
640
- .panel h2{font-size:14px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
641
- .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
642
- .stat{display:flex;flex-direction:column;gap:2px}
643
- .stat .k{color:var(--muted);font-size:12px}
644
- .stat .v{font-size:18px;font-weight:600}
645
- .good{color:var(--good)}
646
- .local{color:var(--local)}
647
- .frontier{color:var(--frontier)}
648
- .muted{color:var(--muted)}
649
- table{width:100%;border-collapse:collapse;font-size:13px}
650
- th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)}
651
- th{color:var(--muted);font-weight:600}
652
- .hash{font-family:ui-monospace,Consolas,monospace;color:var(--muted)}
653
- .progress{height:10px;background:#0b0d11;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
654
- .progress .bar{height:100%;background:var(--good)}
655
- ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}
656
- ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
657
- ul.items li.done{color:var(--muted)}
658
- ul.items .box{font-family:monospace}
659
- .who{color:var(--local)}
660
- .tag{color:var(--frontier);font-size:12px}
661
- .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
662
- code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
663
- .mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
664
- .tabs{display:flex;gap:6px;padding:12px 24px 0;flex-wrap:wrap;border-bottom:1px solid var(--line)}
665
- .tabs .tab{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-bottom:none;border-radius:8px 8px 0 0;padding:8px 14px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:6px}
666
- .tabs .tab.active{background:var(--bg);border-color:var(--local);color:var(--fg)}
667
- .dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--muted)}
668
- .badge{font-size:11px;padding:1px 8px;border-radius:10px;border:1px solid var(--line);color:var(--muted);vertical-align:middle}
669
- .status-running{background:var(--good)}
670
- .badge.status-running{color:var(--good);border-color:var(--good)}
671
- .status-idle{background:var(--muted)}
672
- .badge.status-idle{color:var(--muted)}
673
- .status-waiting-input{background:var(--frontier)}
674
- .badge.status-waiting-input{color:var(--frontier);border-color:var(--frontier)}
675
- .status-ended{background:#6e7681}
676
- .badge.status-ended{color:#6e7681}
677
- .term .termlog{background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap}
678
- .term .tl{padding:1px 0}
679
- .term .tl.input{color:var(--local)}
680
- .term .tl.tool{color:var(--frontier)}
681
- .term .tl.status{color:var(--muted)}
682
- .term .tl.error{color:#f85149}
683
- .term .tl.permission{color:var(--frontier);font-weight:600}
684
- .term form.terminput{display:flex;gap:8px;margin-top:10px}
685
- .term form.terminput input{flex:1;background:#0b0d11;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit}
686
- .term form.terminput button{background:var(--local);color:#04122b;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600}
687
- .pty .ptylog{background:#05070a;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap;color:#c8d1dc}
688
- .pty form.ptyinput{display:flex;gap:8px;margin-top:10px}
689
- .pty form.ptyinput input{flex:1;background:#05070a;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit}
690
- .pty form.ptyinput button,.pty .pty-start{background:var(--frontier);color:#1a1200;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600;margin-top:8px}
691
- .pty .pty-start:disabled{opacity:.5;cursor:default}
692
- `;
693
- var CLIENT_JS = `
694
- (function(){
695
- function reloadIfChanged(prev){
696
- fetch('/api/state').then(function(r){return r.json()}).then(function(s){
697
- if(s.generatedAt!==prev){location.reload()}
698
- }).catch(function(){});
979
+ function costRecordToRouteEvent(record) {
980
+ const event = {
981
+ v: EVENT_SCHEMA_VERSION,
982
+ type: "route",
983
+ ts: record.ts,
984
+ sessionID: record.sessionID ?? LEGACY_SESSION_ID,
985
+ promptHash: record.promptHash,
986
+ promptChars: record.promptChars,
987
+ tier: record.tier,
988
+ routeKind: record.routeKind,
989
+ selected: record.selected,
990
+ rationale: record.rationale,
991
+ estimatedCostUSD: record.estimatedCostUSD,
992
+ baselineCostUSD: record.baselineCostUSD,
993
+ estimatedSavingsUSD: record.estimatedSavingsUSD,
994
+ budgetAction: record.budgetAction
995
+ };
996
+ if (record.tokensIn !== undefined) {
997
+ event.tokensIn = record.tokensIn;
699
998
  }
700
- var current=document.documentElement.getAttribute('data-generated')||'';
701
- if('EventSource' in window){
702
- try{
703
- var es=new EventSource('/events');
704
- es.addEventListener('state',function(){location.reload()});
705
- es.onerror=function(){/* fallback below */};
706
- }catch(e){/* ignore */}
999
+ if (record.tokensOut !== undefined) {
1000
+ event.tokensOut = record.tokensOut;
707
1001
  }
708
- var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
709
- setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
710
-
711
- function selectTab(name){
712
- var tabs=document.querySelectorAll('#tabbar .tab');
713
- for(var i=0;i<tabs.length;i++){tabs[i].classList.toggle('active',tabs[i].getAttribute('data-tab')===name)}
714
- var views=document.querySelectorAll('main .view');
715
- var matched=false;
716
- for(var j=0;j<views.length;j++){
717
- var show=views[j].getAttribute('data-view')===name;
718
- views[j].hidden=!show;
719
- if(show){matched=true;if(name!=='all'){connectTerminal(views[j],name)}}
720
- }
721
- if(!matched){return false}
722
- try{sessionStorage.setItem('openteam.tab',name)}catch(e){}
723
- return true;
1002
+ return event;
1003
+ }
1004
+ function routeEventToCostRecord(event) {
1005
+ const record = {
1006
+ ts: event.ts,
1007
+ sessionID: event.sessionID,
1008
+ promptHash: event.promptHash,
1009
+ promptChars: event.promptChars,
1010
+ tier: event.tier,
1011
+ routeKind: event.routeKind,
1012
+ selected: event.selected,
1013
+ rationale: event.rationale,
1014
+ estimatedCostUSD: event.estimatedCostUSD,
1015
+ baselineCostUSD: event.baselineCostUSD,
1016
+ estimatedSavingsUSD: event.estimatedSavingsUSD,
1017
+ budgetAction: event.budgetAction
1018
+ };
1019
+ if (event.tokensIn !== undefined) {
1020
+ record.tokensIn = event.tokensIn;
1021
+ }
1022
+ if (event.tokensOut !== undefined) {
1023
+ record.tokensOut = event.tokensOut;
724
1024
  }
725
- function connectTerminal(view,id){
726
- if(view.__wired){return}
727
- var root=document.documentElement;
728
- var token=root.getAttribute('data-token');
729
- if(root.getAttribute('data-terminal')!=='1'||!token){return}
730
- var log=view.querySelector('.termlog');
731
- if(!log){return}
732
- view.__wired=true;
733
- function append(cls,text){
734
- var line=document.createElement('div');
735
- line.className='tl '+cls;line.textContent=text;
736
- log.appendChild(line);log.scrollTop=log.scrollHeight;
1025
+ return record;
1026
+ }
1027
+ async function readRouteCostRecords(dir, deps) {
1028
+ const events = await readSessionEvents(dir, deps);
1029
+ const records = [];
1030
+ for (const event of events) {
1031
+ if (event.type === "route") {
1032
+ records.push(routeEventToCostRecord(event));
737
1033
  }
738
- var q='/console/session/'+encodeURIComponent(id);
739
- var tk='token='+encodeURIComponent(token);
740
- if('EventSource' in window){
741
- try{
742
- var es=new EventSource(q+'/stream?'+tk);
743
- es.addEventListener('frame',function(e){try{var f=JSON.parse(e.data);append(f.kind,f.text)}catch(_){}});
744
- es.onerror=function(){/* reconecta solo */};
745
- }catch(_){/* ignore */}
1034
+ }
1035
+ return records;
1036
+ }
1037
+ async function readSessionEvents(dir, deps) {
1038
+ const files = (await deps.storage.list(dir)).filter((file) => file.endsWith(".jsonl"));
1039
+ const perFile = await Promise.all(files.map(async (file) => {
1040
+ const text = await deps.storage.read(joinVirtualPath(dir, file));
1041
+ return text === undefined ? [] : parseEventsJsonl(text);
1042
+ }));
1043
+ const events = perFile.flat();
1044
+ if (deps.legacyTelemetryPath !== undefined) {
1045
+ const legacyText = await deps.storage.read(deps.legacyTelemetryPath);
1046
+ if (legacyText !== undefined) {
1047
+ for (const record of parseCostRecordsJsonl(legacyText)) {
1048
+ events.push(costRecordToRouteEvent(record));
1049
+ }
746
1050
  }
747
- var form=view.querySelector('form.terminput');
748
- if(form){
749
- form.addEventListener('submit',function(e){
750
- e.preventDefault();
751
- var inp=form.querySelector('input[name=text]');
752
- var text=inp.value;
753
- if(!text){return}
754
- inp.value='';append('input','› '+text);
755
- fetch(q+'/input?'+tk,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({text:text})})
756
- .then(function(r){if(!r.ok){append('error','✖ envío rechazado ('+r.status+')')}})
757
- .catch(function(){append('error','✖ no se pudo enviar')});
758
- });
1051
+ }
1052
+ return events.sort((a, b) => a.ts - b.ts);
1053
+ }
1054
+
1055
+ // src/storage/index/sqliteIndex.ts
1056
+ var SCHEMA = `
1057
+ CREATE TABLE IF NOT EXISTS events (
1058
+ session_id TEXT NOT NULL,
1059
+ ts INTEGER NOT NULL,
1060
+ type TEXT NOT NULL,
1061
+ payload TEXT NOT NULL
1062
+ );
1063
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, ts);
1064
+ `;
1065
+ var INSERT_SQL = "INSERT INTO events (session_id, ts, type, payload) VALUES (?, ?, ?, ?)";
1066
+ var ROLLUP_SQL = `
1067
+ SELECT
1068
+ session_id AS sessionID,
1069
+ COUNT(*) AS eventCount,
1070
+ SUM(CASE WHEN type = 'route' THEN 1 ELSE 0 END) AS routeCount,
1071
+ SUM(CASE WHEN type = 'message' THEN 1 ELSE 0 END) AS messageCount,
1072
+ SUM(CASE WHEN type = 'toolcall' THEN 1 ELSE 0 END) AS toolcallCount,
1073
+ COALESCE(SUM(CASE WHEN type = 'message'
1074
+ THEN json_extract(payload, '$.costUSD') ELSE 0 END), 0) AS realCostUSD,
1075
+ COALESCE(SUM(CASE WHEN type = 'message'
1076
+ THEN json_extract(payload, '$.tokensIn') ELSE 0 END), 0) AS tokensIn,
1077
+ COALESCE(SUM(CASE WHEN type = 'message'
1078
+ THEN json_extract(payload, '$.tokensOut') ELSE 0 END), 0) AS tokensOut,
1079
+ MIN(ts) AS firstTs,
1080
+ MAX(ts) AS lastTs
1081
+ FROM events
1082
+ GROUP BY session_id
1083
+ ORDER BY lastTs DESC, session_id ASC
1084
+ `;
1085
+ function insertEvents(db, events) {
1086
+ const stmt = db.query(INSERT_SQL);
1087
+ db.run("BEGIN");
1088
+ try {
1089
+ for (const event of events) {
1090
+ stmt.run(event.sessionID, event.ts, event.type, JSON.stringify(event));
759
1091
  }
760
- connectPty(view,id,q,tk);
1092
+ db.run("COMMIT");
1093
+ } catch (error) {
1094
+ db.run("ROLLBACK");
1095
+ throw error;
761
1096
  }
762
- function connectPty(view,id,q,tk){
763
- var root=document.documentElement;
764
- if(root.getAttribute('data-pty')!=='1'){return}
765
- var pane=view.querySelector('.pty');
766
- if(!pane){return}
767
- var log=pane.querySelector('.ptylog');
768
- var startBtn=pane.querySelector('.pty-start');
769
- var input=pane.querySelector('form.ptyinput');
770
- function plog(text){
771
- var d=document.createElement('span');d.textContent=text;
772
- log.appendChild(d);log.scrollTop=log.scrollHeight;
1097
+ }
1098
+ function toNumber(value) {
1099
+ return typeof value === "number" ? value : Number(value ?? 0);
1100
+ }
1101
+ function createSqliteEventIndex(db) {
1102
+ db.exec(SCHEMA);
1103
+ const allEvents = () => {
1104
+ const rows = db.query("SELECT payload FROM events ORDER BY ts ASC").all();
1105
+ const events = [];
1106
+ for (const row of rows) {
1107
+ let candidate;
1108
+ try {
1109
+ candidate = JSON.parse(row.payload);
1110
+ } catch {
1111
+ continue;
1112
+ }
1113
+ const parsed = OpenTeamEventSchema.safeParse(candidate);
1114
+ if (parsed.success) {
1115
+ events.push(parsed.data);
1116
+ }
773
1117
  }
774
- if(!startBtn){return}
775
- startBtn.addEventListener('click',function(){
776
- startBtn.disabled=true;plog('Abriendo shell…
777
- ');
778
- fetch(q+'/pty?'+tk,{method:'POST',headers:{'content-type':'application/json'},body:'{}'})
779
- .then(function(r){return r.ok?r.json():Promise.reject(r.status)})
780
- .then(function(info){openPtyWs(info.wsUrl)})
781
- .catch(function(s){plog('✖ no se pudo abrir PTY ('+s+')
782
- ');startBtn.disabled=false});
783
- });
784
- function openPtyWs(url){
785
- try{
786
- var ws=new WebSocket(url);
787
- ws.binaryType='arraybuffer';
788
- ws.onmessage=function(e){
789
- var data=e.data;
790
- if(data instanceof ArrayBuffer){data=new TextDecoder().decode(new Uint8Array(data))}
791
- plog(data);
792
- };
793
- ws.onclose=function(){plog('
794
- [shell cerrada]
795
- ');startBtn.disabled=false};
796
- ws.onerror=function(){plog('
797
- error de WebSocket
798
- ')};
799
- if(input){
800
- input.hidden=false;
801
- input.addEventListener('submit',function(e){
802
- e.preventDefault();
803
- var c=input.querySelector('input[name=cmd]');
804
- if(!c.value){return}
805
- ws.send(c.value+'\r');c.value='';
806
- });
807
- }
808
- }catch(_){plog('
809
- ✖ WebSocket no disponible
810
- ');startBtn.disabled=false}
1118
+ return events;
1119
+ };
1120
+ return {
1121
+ rebuild(events) {
1122
+ db.run("DELETE FROM events");
1123
+ insertEvents(db, events);
1124
+ },
1125
+ append(events) {
1126
+ insertEvents(db, events);
1127
+ },
1128
+ allEvents,
1129
+ sessionRollups() {
1130
+ const rows = db.query(ROLLUP_SQL).all();
1131
+ return rows.map((row) => ({
1132
+ sessionID: String(row.sessionID),
1133
+ eventCount: toNumber(row.eventCount),
1134
+ routeCount: toNumber(row.routeCount),
1135
+ messageCount: toNumber(row.messageCount),
1136
+ toolcallCount: toNumber(row.toolcallCount),
1137
+ realCostUSD: toNumber(row.realCostUSD),
1138
+ tokensIn: toNumber(row.tokensIn),
1139
+ tokensOut: toNumber(row.tokensOut),
1140
+ firstTs: toNumber(row.firstTs),
1141
+ lastTs: toNumber(row.lastTs)
1142
+ }));
1143
+ },
1144
+ aggregate() {
1145
+ return aggregateEvents(allEvents());
1146
+ },
1147
+ close() {
1148
+ db.close();
811
1149
  }
812
- }
813
- var bar=document.getElementById('tabbar');
814
- if(bar){
815
- bar.addEventListener('click',function(e){
816
- var t=e.target;
817
- while(t&&t!==bar&&!t.getAttribute('data-tab')){t=t.parentNode}
818
- if(t&&t.getAttribute('data-tab')){selectTab(t.getAttribute('data-tab'))}
819
- });
820
- }
821
- var saved='all';
822
- try{saved=sessionStorage.getItem('openteam.tab')||'all'}catch(e){}
823
- if(!selectTab(saved)){selectTab('all')}
824
- })();
825
- `;
826
- function renderConsoleHtml(state, options = {}) {
827
- const refreshMs = options.refreshMs ?? 2000;
828
- const terminalEnabled = options.terminalEnabled === true;
829
- const ptyEnabled = options.ptyEnabled === true;
830
- const allBody = [
831
- summaryPanel(state.cost),
832
- totalsPanel(state.totals),
833
- sessionsPanel(state.sessions),
834
- loopPanel(state.loop),
835
- teamPanel(state.team),
836
- tierPanel(state.cost),
837
- routesPanel(state.recentRoutes),
838
- decisionsPanel(state.decisions),
839
- meetingsPanel(state.meetings),
840
- activityPanel(state.activity)
841
- ].join("");
842
- const allView = `<div class="view" data-view="all">${allBody}</div>`;
843
- const sessionViews = state.sessionsDetail.map((tab) => sessionView(tab, terminalEnabled, ptyEnabled)).join("");
844
- const body = allView + sessionViews;
845
- const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
846
- const tokenAttr = terminalEnabled && options.consoleToken !== undefined ? ` data-token="${escapeHtml(options.consoleToken)}" data-terminal="1"${ptyEnabled ? ' data-pty="1"' : ""}` : "";
847
- return [
848
- "<!doctype html>",
849
- `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}"${tokenAttr}>`,
850
- "<head>",
851
- '<meta charset="utf-8">',
852
- '<meta name="viewport" content="width=device-width,initial-scale=1">',
853
- "<title>openteam console</title>",
854
- `<style>${STYLE}</style>`,
855
- "</head>",
856
- "<body>",
857
- "<header>",
858
- "<h1>openteam console</h1>",
859
- `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
860
- "</header>",
861
- tabBar(state.sessionsDetail),
862
- `<main>${body}</main>`,
863
- `<script>${CLIENT_JS}</script>`,
864
- "</body>",
865
- "</html>"
866
- ].join("");
1150
+ };
1151
+ }
1152
+ async function rebuildSqliteIndexFromStorage(index, dir, deps) {
1153
+ const events = await readSessionEvents(dir, deps);
1154
+ index.rebuild(events);
1155
+ return events.length;
867
1156
  }
868
1157
 
869
- // src/commands/report.ts
870
- var TIERS2 = [
1158
+ // src/storage/index/bunSqlite.ts
1159
+ async function createBunSqliteDatabase(path) {
1160
+ const mod = await import("bun:sqlite");
1161
+ return new mod.Database(path);
1162
+ }
1163
+ async function buildSqliteIndexFromConfig(path, sessionsDir, deps) {
1164
+ const db = await createBunSqliteDatabase(path);
1165
+ const index = createSqliteEventIndex(db);
1166
+ const events = await rebuildSqliteIndexFromStorage(index, sessionsDir, deps);
1167
+ return { index, events };
1168
+ }
1169
+
1170
+ // src/web/start.ts
1171
+ import { watch as fsWatch } from "node:fs";
1172
+
1173
+ // src/web/paths.ts
1174
+ var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
1175
+ var DEFAULT_DECISIONS_PATH = ".opencode/openteam-decisions.md";
1176
+
1177
+ // src/web/server.ts
1178
+ import {
1179
+ createServer as createHttpServer
1180
+ } from "node:http";
1181
+
1182
+ // src/console/render.ts
1183
+ var TIERS = [
871
1184
  "trivial",
872
1185
  "simple",
873
1186
  "moderate",
874
1187
  "hard"
875
1188
  ];
876
- function emptyTierSummary() {
877
- return {
878
- trivial: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
879
- simple: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
880
- moderate: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
881
- hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
882
- };
1189
+ function escapeHtml(value) {
1190
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
883
1191
  }
884
- function summarizeCostRecords(records) {
885
- const report = {
886
- count: 0,
887
- localCount: 0,
888
- frontierCount: 0,
889
- totalEstimatedUSD: 0,
890
- totalBaselineUSD: 0,
891
- totalSavingsUSD: 0,
892
- savingsPct: 0,
893
- tokensIn: 0,
894
- tokensOut: 0,
895
- byTier: emptyTierSummary()
896
- };
897
- for (const record of records) {
898
- report.count += 1;
899
- if (record.routeKind === "local") {
900
- report.localCount += 1;
901
- } else {
902
- report.frontierCount += 1;
903
- }
904
- report.totalEstimatedUSD += record.estimatedCostUSD;
905
- report.totalBaselineUSD += record.baselineCostUSD;
906
- report.totalSavingsUSD += record.estimatedSavingsUSD;
907
- report.tokensIn += record.tokensIn ?? 0;
908
- report.tokensOut += record.tokensOut ?? 0;
909
- const tier = report.byTier[record.tier];
910
- tier.count += 1;
911
- tier.estimatedUSD += record.estimatedCostUSD;
912
- tier.savingsUSD += record.estimatedSavingsUSD;
913
- }
914
- report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
915
- return report;
1192
+ function usd(value) {
1193
+ return `$${value.toFixed(5)}`;
1194
+ }
1195
+ function pct(value) {
1196
+ return `${value.toFixed(2)}%`;
1197
+ }
1198
+ function ms(value) {
1199
+ return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`;
916
1200
  }
917
- function usd2(value) {
918
- return `$${value.toFixed(5)}`;
1201
+ function avgMs(stats) {
1202
+ return stats.count === 0 ? "—" : ms(stats.totalDurationMs / stats.count);
919
1203
  }
920
- function renderReport(report) {
921
- if (report.count === 0) {
922
- return "openteam report: sin registros de telemetría todavía.";
923
- }
924
- const lines = [
925
- "openteam report:",
926
- ` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
927
- ` coste real: ${usd2(report.totalEstimatedUSD)}`,
928
- ` baseline: ${usd2(report.totalBaselineUSD)}`,
929
- ` ahorro: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
930
- ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
931
- " por tier:"
932
- ];
933
- for (const tier of TIERS2) {
934
- const summary = report.byTier[tier];
935
- lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd2(summary.estimatedUSD)} · ahorro ${usd2(summary.savingsUSD)}`);
936
- }
937
- return lines.join(`
938
- `);
1204
+ function agentModel(agent) {
1205
+ return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
939
1206
  }
940
-
941
- // src/telemetry/aggregate.ts
942
- function emptyToolcallStats() {
943
- return { count: 0, ok: 0, failed: 0, totalDurationMs: 0, byTool: {} };
1207
+ function summaryPanel(cost) {
1208
+ return [
1209
+ '<section class="panel" id="panel-summary">',
1210
+ "<h2>Resumen de routing</h2>",
1211
+ '<div class="grid">',
1212
+ `<div class="stat"><span class="k">Decisiones</span><span class="v">${cost.count}</span></div>`,
1213
+ `<div class="stat"><span class="k">Local</span><span class="v">${cost.localCount}</span></div>`,
1214
+ `<div class="stat"><span class="k">Frontier</span><span class="v">${cost.frontierCount}</span></div>`,
1215
+ `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(cost.totalEstimatedUSD)}</span></div>`,
1216
+ `<div class="stat"><span class="k">Baseline</span><span class="v">${usd(cost.totalBaselineUSD)}</span></div>`,
1217
+ `<div class="stat"><span class="k">Ahorro</span><span class="v good">${usd(cost.totalSavingsUSD)} (${pct(cost.savingsPct)})</span></div>`,
1218
+ `<div class="stat"><span class="k">Tokens in</span><span class="v">${cost.tokensIn}</span></div>`,
1219
+ `<div class="stat"><span class="k">Tokens out</span><span class="v">${cost.tokensOut}</span></div>`,
1220
+ "</div>",
1221
+ "</section>"
1222
+ ].join("");
944
1223
  }
945
- function emptySession(sessionID, ts) {
946
- return {
947
- sessionID,
948
- firstTs: ts,
949
- lastTs: ts,
950
- routeCount: 0,
951
- localCount: 0,
952
- frontierCount: 0,
953
- estimatedCostUSD: 0,
954
- estimatedSavingsUSD: 0,
955
- realCostUSD: 0,
956
- messageCount: 0,
957
- tokensIn: 0,
958
- tokensOut: 0,
959
- tokensReasoning: 0,
960
- tokensCacheRead: 0,
961
- tokensCacheWrite: 0,
962
- toolcalls: emptyToolcallStats(),
963
- models: [],
964
- agents: []
965
- };
1224
+ function tierPanel(cost) {
1225
+ const rows = TIERS.map((tier) => {
1226
+ const summary = cost.byTier[tier];
1227
+ return `<tr><td>${tier}</td><td>${summary.count}</td><td>${usd(summary.estimatedUSD)}</td><td class="good">${usd(summary.savingsUSD)}</td></tr>`;
1228
+ }).join("");
1229
+ return [
1230
+ '<section class="panel" id="panel-tier">',
1231
+ "<h2>Por tier</h2>",
1232
+ "<table><thead><tr><th>Tier</th><th>Nº</th><th>Coste</th><th>Ahorro</th></tr></thead>",
1233
+ `<tbody>${rows}</tbody></table>`,
1234
+ "</section>"
1235
+ ].join("");
966
1236
  }
967
- function pushUnique(list, value) {
968
- if (value !== undefined && !list.includes(value)) {
969
- list.push(value);
970
- }
1237
+ function loopItemRow(item) {
1238
+ const box = item.done ? "☑" : "☐";
1239
+ const cls = item.done ? "done" : "open";
1240
+ const who = item.assignee === undefined ? "" : `<span class="who">@${escapeHtml(item.assignee)}</span> `;
1241
+ return `<li class="${cls}"><span class="box">${box}</span> ${who}${escapeHtml(item.text)}</li>`;
971
1242
  }
972
- function applyRoute(session, event) {
973
- session.routeCount += 1;
974
- if (event.routeKind === "local") {
975
- session.localCount += 1;
976
- } else {
977
- session.frontierCount += 1;
1243
+ function loopPanel(loop) {
1244
+ if (loop === undefined) {
1245
+ return [
1246
+ '<section class="panel" id="panel-loop">',
1247
+ "<h2>Loop</h2>",
1248
+ '<p class="muted">Sin backlog activo (no hay <code>openteam-backlog.md</code>).</p>',
1249
+ "</section>"
1250
+ ].join("");
978
1251
  }
979
- session.estimatedCostUSD += event.estimatedCostUSD;
980
- session.estimatedSavingsUSD += event.estimatedSavingsUSD;
981
- pushUnique(session.models, `${event.selected.providerID}/${event.selected.modelID}`);
982
- pushUnique(session.agents, event.agent);
983
- }
984
- function applyMessage(session, event) {
985
- session.messageCount += 1;
986
- session.realCostUSD += event.costUSD;
987
- session.tokensIn += event.tokensIn;
988
- session.tokensOut += event.tokensOut;
989
- session.tokensReasoning += event.tokensReasoning;
990
- session.tokensCacheRead += event.tokensCacheRead;
991
- session.tokensCacheWrite += event.tokensCacheWrite;
992
- pushUnique(session.models, `${event.providerID}/${event.modelID}`);
993
- pushUnique(session.agents, event.agent);
1252
+ const items = loop.items.map(loopItemRow).join("");
1253
+ const commit = loop.lastCommit === undefined ? "" : `<p class="muted">Último commit: <code>${escapeHtml(loop.lastCommit.hash)}</code> ${escapeHtml(loop.lastCommit.subject)}</p>`;
1254
+ return [
1255
+ '<section class="panel" id="panel-loop">',
1256
+ "<h2>Loop</h2>",
1257
+ `<div class="progress" role="progressbar" aria-valuenow="${loop.progressPct}" aria-valuemin="0" aria-valuemax="100"><div class="bar" style="width:${loop.progressPct}%"></div></div>`,
1258
+ `<p class="muted">${loop.done}/${loop.total} completados · ${loop.open} abiertos · ${loop.progressPct}%</p>`,
1259
+ `<ul class="items">${items}</ul>`,
1260
+ commit,
1261
+ "</section>"
1262
+ ].join("");
994
1263
  }
995
- function applyToolUsage(stats, event) {
996
- stats.count += 1;
997
- stats.totalDurationMs += event.durationMs;
998
- if (event.ok) {
999
- stats.ok += 1;
1000
- } else {
1001
- stats.failed += 1;
1264
+ function teamPanel(team) {
1265
+ if (team.length === 0) {
1266
+ return [
1267
+ '<section class="panel" id="panel-team">',
1268
+ "<h2>Equipo</h2>",
1269
+ '<p class="muted">Sin agentes en <code>.opencode/agent/</code>.</p>',
1270
+ "</section>"
1271
+ ].join("");
1002
1272
  }
1273
+ const rows = team.map((agent) => `<tr><td>${escapeHtml(agent.name)}</td><td>${escapeHtml(agent.mode)}</td><td>${escapeHtml(agentModel(agent))}</td></tr>`).join("");
1274
+ return [
1275
+ '<section class="panel" id="panel-team">',
1276
+ "<h2>Equipo</h2>",
1277
+ "<table><thead><tr><th>Agente</th><th>Modo</th><th>LLM</th></tr></thead>",
1278
+ `<tbody>${rows}</tbody></table>`,
1279
+ "</section>"
1280
+ ].join("");
1003
1281
  }
1004
- function applyToolcall(session, event) {
1005
- applyToolUsage(session.toolcalls, event);
1006
- const byTool = session.toolcalls.byTool[event.tool] ?? {
1007
- count: 0,
1008
- ok: 0,
1009
- failed: 0,
1010
- totalDurationMs: 0
1011
- };
1012
- applyToolUsage(byTool, event);
1013
- session.toolcalls.byTool[event.tool] = byTool;
1014
- pushUnique(session.agents, event.agent);
1282
+ function routeRow(route) {
1283
+ 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>`;
1015
1284
  }
1016
- function applyEndpoint(session, event) {
1017
- if (session.endpoint !== undefined && session.endpoint.lastSeenTs >= event.ts) {
1018
- return;
1019
- }
1020
- const endpoint = {
1021
- url: event.url,
1022
- lastSeenTs: event.ts
1023
- };
1024
- if (event.directory !== undefined) {
1025
- endpoint.directory = event.directory;
1026
- }
1027
- if (event.worktree !== undefined) {
1028
- endpoint.worktree = event.worktree;
1029
- }
1030
- if (event.title !== undefined) {
1031
- endpoint.title = event.title;
1285
+ function routesPanel(routes) {
1286
+ if (routes.length === 0) {
1287
+ return [
1288
+ '<section class="panel" id="panel-routes">',
1289
+ "<h2>Decisiones recientes</h2>",
1290
+ '<p class="muted">Sin decisiones de routing todavía.</p>',
1291
+ "</section>"
1292
+ ].join("");
1032
1293
  }
1033
- session.endpoint = endpoint;
1294
+ const rows = routes.map(routeRow).join("");
1295
+ return [
1296
+ '<section class="panel" id="panel-routes">',
1297
+ "<h2>Decisiones recientes</h2>",
1298
+ "<table><thead><tr><th>Tier</th><th>Ruta</th><th>Modelo</th><th>Prompt (hash)</th><th>Coste</th><th>Ahorro</th></tr></thead>",
1299
+ `<tbody>${rows}</tbody></table>`,
1300
+ "</section>"
1301
+ ].join("");
1034
1302
  }
1035
- function mergeTool(target, source) {
1036
- target.count += source.count;
1037
- target.ok += source.ok;
1038
- target.failed += source.failed;
1039
- target.totalDurationMs += source.totalDurationMs;
1303
+ function activityRow(entry) {
1304
+ const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
1305
+ return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
1040
1306
  }
1041
- function accumulateTotals(totals, session) {
1042
- totals.sessions += 1;
1043
- totals.routeCount += session.routeCount;
1044
- totals.localCount += session.localCount;
1045
- totals.frontierCount += session.frontierCount;
1046
- totals.estimatedCostUSD += session.estimatedCostUSD;
1047
- totals.estimatedSavingsUSD += session.estimatedSavingsUSD;
1048
- totals.realCostUSD += session.realCostUSD;
1049
- totals.messageCount += session.messageCount;
1050
- totals.tokensIn += session.tokensIn;
1051
- totals.tokensOut += session.tokensOut;
1052
- totals.tokensReasoning += session.tokensReasoning;
1053
- totals.tokensCacheRead += session.tokensCacheRead;
1054
- totals.tokensCacheWrite += session.tokensCacheWrite;
1055
- mergeTool(totals.toolcalls, session.toolcalls);
1056
- for (const [tool, usage] of Object.entries(session.toolcalls.byTool)) {
1057
- const target = totals.toolcalls.byTool[tool] ?? {
1058
- count: 0,
1059
- ok: 0,
1060
- failed: 0,
1061
- totalDurationMs: 0
1062
- };
1063
- mergeTool(target, usage);
1064
- totals.toolcalls.byTool[tool] = target;
1307
+ function activityPanel(activity) {
1308
+ if (activity.length === 0) {
1309
+ return [
1310
+ '<section class="panel" id="panel-activity">',
1311
+ "<h2>Actividad</h2>",
1312
+ '<p class="muted">Sin actividad registrada.</p>',
1313
+ "</section>"
1314
+ ].join("");
1065
1315
  }
1316
+ const rows = activity.map(activityRow).join("");
1317
+ return [
1318
+ '<section class="panel" id="panel-activity">',
1319
+ "<h2>Actividad</h2>",
1320
+ `<ul class="timeline">${rows}</ul>`,
1321
+ "</section>"
1322
+ ].join("");
1323
+ }
1324
+ function totalsPanel(totals) {
1325
+ const t = totals.toolcalls;
1326
+ return [
1327
+ '<section class="panel" id="panel-totals">',
1328
+ "<h2>Telemetría real (todas las sesiones)</h2>",
1329
+ '<div class="grid">',
1330
+ `<div class="stat"><span class="k">Sesiones</span><span class="v">${totals.sessions}</span></div>`,
1331
+ `<div class="stat"><span class="k">Mensajes</span><span class="v">${totals.messageCount}</span></div>`,
1332
+ `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(totals.realCostUSD)}</span></div>`,
1333
+ `<div class="stat"><span class="k">Ahorro est.</span><span class="v good">${usd(totals.estimatedSavingsUSD)}</span></div>`,
1334
+ `<div class="stat"><span class="k">Tokens in</span><span class="v">${totals.tokensIn}</span></div>`,
1335
+ `<div class="stat"><span class="k">Tokens out</span><span class="v">${totals.tokensOut}</span></div>`,
1336
+ `<div class="stat"><span class="k">Reasoning</span><span class="v">${totals.tokensReasoning}</span></div>`,
1337
+ `<div class="stat"><span class="k">Cache r/w</span><span class="v">${totals.tokensCacheRead}/${totals.tokensCacheWrite}</span></div>`,
1338
+ `<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>`,
1339
+ `<div class="stat"><span class="k">Dur. media tool</span><span class="v">${avgMs(t)}</span></div>`,
1340
+ "</div>",
1341
+ "</section>"
1342
+ ].join("");
1066
1343
  }
1067
- function toDecision(event) {
1068
- const view = {
1069
- ts: event.ts,
1070
- sessionID: event.sessionID,
1071
- summary: event.summary
1072
- };
1073
- if (event.agent !== undefined) {
1074
- view.agent = event.agent;
1075
- }
1076
- if (event.tags !== undefined) {
1077
- view.tags = [...event.tags];
1078
- }
1079
- return view;
1344
+ function sessionRow(session) {
1345
+ const models = session.models.map(escapeHtml).join("<br>") || "—";
1346
+ return [
1347
+ "<tr>",
1348
+ `<td class="hash">${escapeHtml(session.sessionID)}</td>`,
1349
+ `<td>${models}</td>`,
1350
+ `<td><span class="local">${session.localCount}</span>/<span class="frontier">${session.frontierCount}</span></td>`,
1351
+ `<td>${usd(session.realCostUSD)}</td>`,
1352
+ `<td>${session.tokensIn}/${session.tokensOut}</td>`,
1353
+ `<td>${session.toolcalls.count} (${avgMs(session.toolcalls)})</td>`,
1354
+ "</tr>"
1355
+ ].join("");
1080
1356
  }
1081
- function toMeeting(event) {
1082
- const view = {
1083
- ts: event.ts,
1084
- sessionID: event.sessionID,
1085
- batchID: event.batchID,
1086
- roles: [...event.roles]
1087
- };
1088
- if (event.purpose !== undefined) {
1089
- view.purpose = event.purpose;
1090
- }
1091
- if (event.decisionIDs !== undefined) {
1092
- view.decisionIDs = [...event.decisionIDs];
1357
+ function sessionsPanel(sessions) {
1358
+ if (sessions.length === 0) {
1359
+ return [
1360
+ '<section class="panel" id="panel-sessions">',
1361
+ "<h2>Sesiones</h2>",
1362
+ '<p class="muted">Sin sesiones registradas todavía.</p>',
1363
+ "</section>"
1364
+ ].join("");
1093
1365
  }
1094
- return view;
1366
+ const rows = sessions.map(sessionRow).join("");
1367
+ return [
1368
+ '<section class="panel" id="panel-sessions">',
1369
+ "<h2>Sesiones</h2>",
1370
+ "<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>",
1371
+ `<tbody>${rows}</tbody></table>`,
1372
+ "</section>"
1373
+ ].join("");
1095
1374
  }
1096
- function toActivity(event) {
1097
- const view = {
1098
- ts: event.ts,
1099
- sessionID: event.sessionID,
1100
- kind: event.kind,
1101
- summary: event.summary
1102
- };
1103
- if (event.agent !== undefined) {
1104
- view.agent = event.agent;
1375
+ function decisionRow(decision) {
1376
+ const who = decision.agent === undefined ? "" : `<span class="who">${escapeHtml(decision.agent)}</span> `;
1377
+ const tags = decision.tags === undefined || decision.tags.length === 0 ? "" : ` ${decision.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
1378
+ return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision.summary)}${tags}</li>`;
1379
+ }
1380
+ function decisionsPanel(decisions) {
1381
+ if (decisions.length === 0) {
1382
+ return [
1383
+ '<section class="panel" id="panel-decisions">',
1384
+ "<h2>Decisiones</h2>",
1385
+ '<p class="muted">Sin decisiones registradas (el scribe escribe en <code>openteam-decisions.md</code>).</p>',
1386
+ "</section>"
1387
+ ].join("");
1105
1388
  }
1106
- return view;
1389
+ const rows = decisions.map(decisionRow).join("");
1390
+ return [
1391
+ '<section class="panel" id="panel-decisions">',
1392
+ "<h2>Decisiones</h2>",
1393
+ `<ul class="timeline">${rows}</ul>`,
1394
+ "</section>"
1395
+ ].join("");
1107
1396
  }
1108
- function byTsDesc(list) {
1109
- return [...list].sort((a, b) => b.ts - a.ts);
1397
+ function meetingRow(meeting) {
1398
+ const roles = meeting.roles.map(escapeHtml).join(", ");
1399
+ const purpose = meeting.purpose === undefined ? "" : `: ${escapeHtml(meeting.purpose)}`;
1400
+ return `<li class="agent"><span class="kind">reunión</span> <span class="who">${roles}</span>${purpose}</li>`;
1110
1401
  }
1111
- function aggregateEvents(events) {
1112
- const sessions = new Map;
1113
- const decisions = [];
1114
- const meetings = [];
1115
- const activity = [];
1116
- const sessionFor = (sessionID, ts) => {
1117
- const existing = sessions.get(sessionID);
1118
- if (existing === undefined) {
1119
- const created = emptySession(sessionID, ts);
1120
- sessions.set(sessionID, created);
1121
- return created;
1122
- }
1123
- existing.firstTs = Math.min(existing.firstTs, ts);
1124
- existing.lastTs = Math.max(existing.lastTs, ts);
1125
- return existing;
1126
- };
1127
- for (const event of events) {
1128
- switch (event.type) {
1129
- case "route":
1130
- applyRoute(sessionFor(event.sessionID, event.ts), event);
1131
- break;
1132
- case "message":
1133
- applyMessage(sessionFor(event.sessionID, event.ts), event);
1134
- break;
1135
- case "toolcall":
1136
- applyToolcall(sessionFor(event.sessionID, event.ts), event);
1137
- break;
1138
- case "session-endpoint":
1139
- applyEndpoint(sessionFor(event.sessionID, event.ts), event);
1140
- break;
1141
- case "decision":
1142
- decisions.push(toDecision(event));
1143
- break;
1144
- case "meeting":
1145
- meetings.push(toMeeting(event));
1146
- break;
1147
- case "activity":
1148
- activity.push(toActivity(event));
1149
- break;
1150
- }
1151
- }
1152
- const totals = {
1153
- sessions: 0,
1154
- routeCount: 0,
1155
- localCount: 0,
1156
- frontierCount: 0,
1157
- estimatedCostUSD: 0,
1158
- estimatedSavingsUSD: 0,
1159
- realCostUSD: 0,
1160
- messageCount: 0,
1161
- tokensIn: 0,
1162
- tokensOut: 0,
1163
- tokensReasoning: 0,
1164
- tokensCacheRead: 0,
1165
- tokensCacheWrite: 0,
1166
- toolcalls: emptyToolcallStats()
1167
- };
1168
- const summaries = [...sessions.values()].sort((a, b) => b.lastTs - a.lastTs);
1169
- for (const summary of summaries) {
1170
- summary.models.sort();
1171
- summary.agents.sort();
1172
- accumulateTotals(totals, summary);
1402
+ function meetingsPanel(meetings) {
1403
+ if (meetings.length === 0) {
1404
+ return [
1405
+ '<section class="panel" id="panel-meetings">',
1406
+ "<h2>Reuniones</h2>",
1407
+ '<p class="muted">Sin reuniones multi-agente todavía.</p>',
1408
+ "</section>"
1409
+ ].join("");
1173
1410
  }
1174
- return {
1175
- sessions: summaries,
1176
- totals,
1177
- decisions: byTsDesc(decisions),
1178
- meetings: byTsDesc(meetings),
1179
- activity: byTsDesc(activity)
1180
- };
1411
+ const rows = meetings.map(meetingRow).join("");
1412
+ return [
1413
+ '<section class="panel" id="panel-meetings">',
1414
+ "<h2>Reuniones</h2>",
1415
+ `<ul class="timeline">${rows}</ul>`,
1416
+ "</section>"
1417
+ ].join("");
1181
1418
  }
1182
-
1183
- // src/storage/path.ts
1184
- class StoragePathError extends Error {
1185
- constructor(message) {
1186
- super(message);
1187
- this.name = "StoragePathError";
1188
- }
1419
+ var STATUS_LABELS = {
1420
+ running: "activa",
1421
+ idle: "en reposo",
1422
+ "waiting-input": "esperando input",
1423
+ ended: "finalizada"
1424
+ };
1425
+ function statusBadge(status) {
1426
+ return `<span class="badge status-${status}">${STATUS_LABELS[status]}</span>`;
1189
1427
  }
1190
- function normalizeVirtualPath(input) {
1191
- const segments = [];
1192
- for (const rawSegment of input.split(/[\\/]+/)) {
1193
- if (rawSegment === "" || rawSegment === ".") {
1194
- continue;
1195
- }
1196
- if (rawSegment === "..") {
1197
- if (segments.length === 0) {
1198
- throw new StoragePathError(`la ruta "${input}" escapa por encima de la raíz`);
1199
- }
1200
- segments.pop();
1201
- continue;
1202
- }
1203
- segments.push(rawSegment);
1204
- }
1205
- return segments.join("/");
1428
+ function tabBar(sessions) {
1429
+ const allTab = '<button type="button" class="tab" data-tab="all">Todas</button>';
1430
+ const tabs = sessions.map((tab) => {
1431
+ const id = escapeHtml(tab.sessionID);
1432
+ const short = escapeHtml(shortSessionID(tab.sessionID));
1433
+ return `<button type="button" class="tab" data-tab="${id}"><span class="dot status-${tab.status}"></span>${short}</button>`;
1434
+ }).join("");
1435
+ return `<nav class="tabs" id="tabbar">${allTab}${tabs}</nav>`;
1206
1436
  }
1207
- function joinVirtualPath(...segments) {
1208
- return normalizeVirtualPath(segments.join("/"));
1437
+ function shortSessionID(sessionID) {
1438
+ return sessionID.length <= 12 ? sessionID : `${sessionID.slice(0, 12)}…`;
1439
+ }
1440
+ function sessionHeaderPanel(tab) {
1441
+ const endpoint = tab.endpoint === undefined ? '<span class="muted">endpoint no anunciado</span>' : `<code>${escapeHtml(tab.endpoint.url)}</code>`;
1442
+ const dir = tab.endpoint?.directory === undefined ? "" : `<div class="stat"><span class="k">Directorio</span><span class="v mono">${escapeHtml(tab.endpoint.directory)}</span></div>`;
1443
+ const title = tab.endpoint?.title === undefined ? "" : `<div class="stat"><span class="k">Título</span><span class="v">${escapeHtml(tab.endpoint.title)}</span></div>`;
1444
+ const s = tab.summary;
1445
+ return [
1446
+ '<section class="panel">',
1447
+ `<h2>Sesión ${escapeHtml(shortSessionID(tab.sessionID))} ${statusBadge(tab.status)}</h2>`,
1448
+ '<div class="grid">',
1449
+ `<div class="stat"><span class="k">Endpoint</span><span class="v">${endpoint}</span></div>`,
1450
+ dir,
1451
+ title,
1452
+ `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(s.realCostUSD)}</span></div>`,
1453
+ `<div class="stat"><span class="k">Tokens in/out</span><span class="v">${s.tokensIn}/${s.tokensOut}</span></div>`,
1454
+ `<div class="stat"><span class="k">Local/Frontier</span><span class="v"><span class="local">${s.localCount}</span>/<span class="frontier">${s.frontierCount}</span></span></div>`,
1455
+ `<div class="stat"><span class="k">Toolcalls</span><span class="v">${s.toolcalls.count} (${avgMs(s.toolcalls)})</span></div>`,
1456
+ "</div>",
1457
+ "</section>"
1458
+ ].join("");
1459
+ }
1460
+ function terminalPanel(tab) {
1461
+ return [
1462
+ '<section class="panel term">',
1463
+ "<h2>Terminal (sesión)</h2>",
1464
+ '<pre class="termlog" aria-live="polite"></pre>',
1465
+ '<form class="terminput" autocomplete="off">',
1466
+ `<input type="text" name="text" placeholder="Enviar prompt a ${escapeHtml(shortSessionID(tab.sessionID))}…" aria-label="Prompt a la sesión">`,
1467
+ '<button type="submit">Enviar</button>',
1468
+ "</form>",
1469
+ "</section>"
1470
+ ].join("");
1209
1471
  }
1210
- function splitVirtualPath(path) {
1211
- const normalized = normalizeVirtualPath(path);
1212
- return normalized === "" ? [] : normalized.split("/");
1472
+ function ptyPanel(tab) {
1473
+ return [
1474
+ `<section class="panel pty" data-pty-session="${escapeHtml(tab.sessionID)}">`,
1475
+ "<h2>Terminal (PTY)</h2>",
1476
+ '<pre class="ptylog" aria-live="polite"></pre>',
1477
+ '<form class="ptyinput" autocomplete="off" hidden>',
1478
+ '<input type="text" name="cmd" placeholder="Comando…" aria-label="Comando de la terminal">',
1479
+ '<button type="submit">Ejecutar</button>',
1480
+ "</form>",
1481
+ '<button type="button" class="pty-start">Abrir shell</button>',
1482
+ "</section>"
1483
+ ].join("");
1213
1484
  }
1214
-
1215
- // src/telemetry/events.ts
1216
- import { z as z4 } from "zod";
1217
-
1218
- // src/capabilities/types.ts
1219
- import { z as z2 } from "zod";
1220
- var CapabilityTierSchema = z2.union([
1221
- z2.literal(0),
1222
- z2.literal(1),
1223
- z2.literal(2),
1224
- z2.literal(3),
1225
- z2.literal(4),
1226
- z2.literal(5)
1227
- ]);
1228
- var ComplexityTierSchema = z2.enum([
1229
- "trivial",
1230
- "simple",
1231
- "moderate",
1232
- "hard"
1233
- ]);
1234
- var ModelCapabilityProfileSchema = z2.object({
1235
- ref: z2.object({
1236
- providerID: z2.string().min(1),
1237
- modelID: z2.string().min(1)
1238
- }),
1239
- kind: z2.enum(["local", "frontier", "router"]),
1240
- contextWindow: z2.number().int().positive(),
1241
- maxOutputTokens: z2.number().int().positive(),
1242
- supportsToolCalling: z2.boolean(),
1243
- supportsVision: z2.boolean(),
1244
- reasoningTier: CapabilityTierSchema,
1245
- codeQualityTier: CapabilityTierSchema,
1246
- costPer1M: z2.object({
1247
- inputUSD: z2.number().min(0),
1248
- outputUSD: z2.number().min(0)
1249
- }),
1250
- availability: z2.enum(["available", "degraded", "unavailable"])
1251
- });
1252
-
1253
- // src/config/schema.ts
1254
- import { z as z3 } from "zod";
1255
- var ModelRefSchema = z3.object({
1256
- providerID: z3.string().min(1),
1257
- modelID: z3.string().min(1)
1258
- });
1259
- var RouterModeSchema = z3.enum(["economy", "balanced", "quality"]);
1260
- var PrivacyModeSchema = z3.enum([
1261
- "forceLocalOnSensitive",
1262
- "consentBeforeFrontier",
1263
- "off"
1264
- ]);
1265
- var BaselineModeSchema = z3.enum(["auto", "pinned"]);
1266
- var ConsoleHostSchema = z3.enum(["127.0.0.1", "localhost"]);
1267
- var ConsoleConfigSchema = z3.object({
1268
- host: ConsoleHostSchema.default("127.0.0.1"),
1269
- port: z3.number().int().min(1024).max(65535).default(4599),
1270
- autoPortFallback: z3.boolean().default(true),
1271
- refreshMs: z3.number().int().min(250).default(2000),
1272
- recentRoutes: z3.number().int().positive().default(50),
1273
- openBrowser: z3.boolean().default(false),
1274
- remoteStorage: z3.boolean().default(false),
1275
- terminal: z3.object({
1276
- enabled: z3.boolean().default(true),
1277
- pty: z3.boolean().optional()
1278
- }).default({ enabled: true })
1279
- }).default({
1280
- host: "127.0.0.1",
1281
- port: 4599,
1282
- autoPortFallback: true,
1283
- refreshMs: 2000,
1284
- recentRoutes: 50,
1285
- openBrowser: false,
1286
- remoteStorage: false,
1287
- terminal: { enabled: true }
1288
- });
1289
- var defaultLocalModel = {
1290
- providerID: "ollama",
1291
- modelID: "qwen3:8b"
1292
- };
1293
- var defaultFrontierModel = {
1294
- providerID: "anthropic",
1295
- modelID: "claude-sonnet-4-5"
1296
- };
1297
- var LocalRuntimeSchema = z3.object({
1298
- id: z3.enum(["ollama", "lmstudio", "foundry-local"]),
1299
- enabled: z3.boolean().default(true),
1300
- baseURL: z3.string().url().optional(),
1301
- discovery: z3.enum(["cli", "sdk", "manual"]).optional(),
1302
- defaultModel: ModelRefSchema
1303
- });
1304
- var OpenTeamConfigObjectSchema = z3.object({
1305
- baseline: z3.object({
1306
- mode: BaselineModeSchema.default("auto"),
1307
- pinnedModel: ModelRefSchema.nullable().default(null),
1308
- hardDefault: ModelRefSchema.default(defaultFrontierModel)
1309
- }).default({
1310
- mode: "auto",
1311
- pinnedModel: null,
1312
- hardDefault: defaultFrontierModel
1313
- }),
1314
- router: z3.object({
1315
- mode: RouterModeSchema.default("balanced"),
1316
- localDefault: ModelRefSchema.default(defaultLocalModel),
1317
- trivialPromptMaxChars: z3.number().int().positive().default(280),
1318
- frontierPromptMinChars: z3.number().int().positive().default(2000)
1319
- }).default({
1320
- mode: "balanced",
1321
- localDefault: defaultLocalModel,
1322
- trivialPromptMaxChars: 280,
1323
- frontierPromptMinChars: 2000
1324
- }),
1325
- local: z3.object({
1326
- runtimes: z3.array(LocalRuntimeSchema).min(1).default([
1327
- {
1328
- id: "ollama",
1329
- enabled: true,
1330
- baseURL: "http://localhost:11434/v1",
1331
- defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
1332
- }
1333
- ])
1334
- }).default({
1335
- runtimes: [
1336
- {
1337
- id: "ollama",
1338
- enabled: true,
1339
- baseURL: "http://localhost:11434/v1",
1340
- defaultModel: defaultLocalModel
1341
- }
1342
- ]
1343
- }),
1344
- budgets: z3.object({
1345
- sessionUSD: z3.number().positive().optional(),
1346
- monthlyUSD: z3.number().positive().optional(),
1347
- frontierTokensPerSession: z3.number().int().positive().optional(),
1348
- hardStopOnBudgetExhaustion: z3.boolean().default(false)
1349
- }).default({ hardStopOnBudgetExhaustion: false }),
1350
- privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
1351
- console: ConsoleConfigSchema
1352
- });
1353
- var OpenTeamConfigSchema = OpenTeamConfigObjectSchema;
1354
-
1355
- // src/telemetry/events.ts
1356
- var EVENT_SCHEMA_VERSION = 1;
1357
- var EventBaseSchema = z4.object({
1358
- v: z4.literal(EVENT_SCHEMA_VERSION),
1359
- ts: z4.number().finite(),
1360
- sessionID: z4.string().min(1)
1361
- });
1362
- var RouteEventSchema = EventBaseSchema.extend({
1363
- type: z4.literal("route"),
1364
- promptHash: z4.string().min(1),
1365
- promptChars: z4.number().int().min(0),
1366
- tier: ComplexityTierSchema,
1367
- routeKind: z4.enum(["local", "frontier"]),
1368
- selected: ModelRefSchema,
1369
- rationale: z4.string(),
1370
- estimatedCostUSD: z4.number().finite().min(0),
1371
- baselineCostUSD: z4.number().finite().min(0),
1372
- estimatedSavingsUSD: z4.number().finite(),
1373
- budgetAction: z4.string().min(1),
1374
- tokensIn: z4.number().int().min(0).optional(),
1375
- tokensOut: z4.number().int().min(0).optional(),
1376
- decisionID: z4.string().min(1).optional(),
1377
- agent: z4.string().min(1).optional(),
1378
- success: z4.boolean().optional(),
1379
- batchID: z4.string().min(1).optional(),
1380
- failureReason: z4.string().min(1).optional(),
1381
- failureStage: z4.string().min(1).optional()
1382
- });
1383
- var MessageEventSchema = EventBaseSchema.extend({
1384
- type: z4.literal("message"),
1385
- providerID: z4.string().min(1),
1386
- modelID: z4.string().min(1),
1387
- agent: z4.string().min(1).optional(),
1388
- mode: z4.string().min(1).optional(),
1389
- messageID: z4.string().min(1).optional(),
1390
- costUSD: z4.number().finite().min(0),
1391
- tokensIn: z4.number().int().min(0),
1392
- tokensOut: z4.number().int().min(0),
1393
- tokensReasoning: z4.number().int().min(0),
1394
- tokensCacheRead: z4.number().int().min(0),
1395
- tokensCacheWrite: z4.number().int().min(0),
1396
- durationMs: z4.number().int().min(0)
1397
- });
1398
- var ToolcallEventSchema = EventBaseSchema.extend({
1399
- type: z4.literal("toolcall"),
1400
- tool: z4.string().min(1),
1401
- callID: z4.string().min(1),
1402
- agent: z4.string().min(1).optional(),
1403
- durationMs: z4.number().int().min(0),
1404
- ok: z4.boolean(),
1405
- title: z4.string().optional()
1406
- });
1407
- var MeetingEventSchema = EventBaseSchema.extend({
1408
- type: z4.literal("meeting"),
1409
- batchID: z4.string().min(1),
1410
- purpose: z4.string().optional(),
1411
- roles: z4.array(z4.string().min(1)),
1412
- decisionIDs: z4.array(z4.string().min(1)).optional()
1413
- });
1414
- var DecisionEventSchema = EventBaseSchema.extend({
1415
- type: z4.literal("decision"),
1416
- agent: z4.string().min(1).optional(),
1417
- summary: z4.string().min(1),
1418
- tags: z4.array(z4.string().min(1)).optional()
1419
- });
1420
- var ActivityEventSchema = EventBaseSchema.extend({
1421
- type: z4.literal("activity"),
1422
- kind: z4.enum(["route", "agent", "decision", "commit"]),
1423
- agent: z4.string().min(1).optional(),
1424
- summary: z4.string().min(1)
1425
- });
1426
- var SessionEndpointEventSchema = EventBaseSchema.extend({
1427
- type: z4.literal("session-endpoint"),
1428
- url: z4.string().url(),
1429
- directory: z4.string().min(1).optional(),
1430
- worktree: z4.string().min(1).optional(),
1431
- title: z4.string().min(1).optional()
1432
- });
1433
- var OpenTeamEventSchema = z4.discriminatedUnion("type", [
1434
- RouteEventSchema,
1435
- MessageEventSchema,
1436
- ToolcallEventSchema,
1437
- MeetingEventSchema,
1438
- DecisionEventSchema,
1439
- ActivityEventSchema,
1440
- SessionEndpointEventSchema
1441
- ]);
1442
-
1443
- // src/storage/fsStorageProvider.ts
1444
- import {
1445
- appendFile,
1446
- mkdir,
1447
- readdir,
1448
- readFile,
1449
- rm,
1450
- stat,
1451
- writeFile
1452
- } from "node:fs/promises";
1453
- import { dirname, join } from "node:path";
1454
- function isMissing(error) {
1455
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1485
+ function sessionView(tab, terminalEnabled, ptyEnabled) {
1486
+ const body = [
1487
+ sessionHeaderPanel(tab),
1488
+ terminalEnabled ? terminalPanel(tab) : "",
1489
+ terminalEnabled && ptyEnabled ? ptyPanel(tab) : "",
1490
+ routesPanel(tab.recentRoutes),
1491
+ decisionsPanel(tab.decisions),
1492
+ meetingsPanel(tab.meetings),
1493
+ activityPanel(tab.activity)
1494
+ ].join("");
1495
+ return `<div class="view" data-view="${escapeHtml(tab.sessionID)}" hidden>${body}</div>`;
1456
1496
  }
1457
- function createFsStorageProvider(root) {
1458
- function toDisk(path) {
1459
- return join(root, ...splitVirtualPath(path));
1497
+ var STYLE = `
1498
+ :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
1499
+ *{box-sizing:border-box}
1500
+ body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}
1501
+ header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}
1502
+ header h1{font-size:18px;margin:0}
1503
+ header .meta{color:var(--muted);font-size:12px}
1504
+ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;padding:24px}
1505
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
1506
+ .panel h2{font-size:14px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
1507
+ .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
1508
+ .stat{display:flex;flex-direction:column;gap:2px}
1509
+ .stat .k{color:var(--muted);font-size:12px}
1510
+ .stat .v{font-size:18px;font-weight:600}
1511
+ .good{color:var(--good)}
1512
+ .local{color:var(--local)}
1513
+ .frontier{color:var(--frontier)}
1514
+ .muted{color:var(--muted)}
1515
+ table{width:100%;border-collapse:collapse;font-size:13px}
1516
+ th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)}
1517
+ th{color:var(--muted);font-weight:600}
1518
+ .hash{font-family:ui-monospace,Consolas,monospace;color:var(--muted)}
1519
+ .progress{height:10px;background:#0b0d11;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
1520
+ .progress .bar{height:100%;background:var(--good)}
1521
+ ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}
1522
+ ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
1523
+ ul.items li.done{color:var(--muted)}
1524
+ ul.items .box{font-family:monospace}
1525
+ .who{color:var(--local)}
1526
+ .tag{color:var(--frontier);font-size:12px}
1527
+ .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
1528
+ code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
1529
+ .mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
1530
+ .tabs{display:flex;gap:6px;padding:12px 24px 0;flex-wrap:wrap;border-bottom:1px solid var(--line)}
1531
+ .tabs .tab{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-bottom:none;border-radius:8px 8px 0 0;padding:8px 14px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:6px}
1532
+ .tabs .tab.active{background:var(--bg);border-color:var(--local);color:var(--fg)}
1533
+ .dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--muted)}
1534
+ .badge{font-size:11px;padding:1px 8px;border-radius:10px;border:1px solid var(--line);color:var(--muted);vertical-align:middle}
1535
+ .status-running{background:var(--good)}
1536
+ .badge.status-running{color:var(--good);border-color:var(--good)}
1537
+ .status-idle{background:var(--muted)}
1538
+ .badge.status-idle{color:var(--muted)}
1539
+ .status-waiting-input{background:var(--frontier)}
1540
+ .badge.status-waiting-input{color:var(--frontier);border-color:var(--frontier)}
1541
+ .status-ended{background:#6e7681}
1542
+ .badge.status-ended{color:#6e7681}
1543
+ .term .termlog{background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap}
1544
+ .term .tl{padding:1px 0}
1545
+ .term .tl.input{color:var(--local)}
1546
+ .term .tl.tool{color:var(--frontier)}
1547
+ .term .tl.status{color:var(--muted)}
1548
+ .term .tl.error{color:#f85149}
1549
+ .term .tl.permission{color:var(--frontier);font-weight:600}
1550
+ .term form.terminput{display:flex;gap:8px;margin-top:10px}
1551
+ .term form.terminput input{flex:1;background:#0b0d11;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit}
1552
+ .term form.terminput button{background:var(--local);color:#04122b;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600}
1553
+ .pty .ptylog{background:#05070a;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap;color:#c8d1dc}
1554
+ .pty form.ptyinput{display:flex;gap:8px;margin-top:10px}
1555
+ .pty form.ptyinput input{flex:1;background:#05070a;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit}
1556
+ .pty form.ptyinput button,.pty .pty-start{background:var(--frontier);color:#1a1200;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600;margin-top:8px}
1557
+ .pty .pty-start:disabled{opacity:.5;cursor:default}
1558
+ `;
1559
+ var CLIENT_JS = `
1560
+ (function(){
1561
+ function reloadIfChanged(prev){
1562
+ fetch('/api/state').then(function(r){return r.json()}).then(function(s){
1563
+ if(s.generatedAt!==prev){location.reload()}
1564
+ }).catch(function(){});
1460
1565
  }
1461
- return {
1462
- async read(path) {
1463
- try {
1464
- return await readFile(toDisk(path), "utf8");
1465
- } catch (error) {
1466
- if (isMissing(error)) {
1467
- return;
1468
- }
1469
- throw error;
1470
- }
1471
- },
1472
- async write(path, content) {
1473
- const disk = toDisk(path);
1474
- await mkdir(dirname(disk), { recursive: true });
1475
- await writeFile(disk, content, "utf8");
1476
- },
1477
- async append(path, content) {
1478
- const disk = toDisk(path);
1479
- await mkdir(dirname(disk), { recursive: true });
1480
- await appendFile(disk, content, "utf8");
1481
- },
1482
- async exists(path) {
1483
- try {
1484
- await stat(toDisk(path));
1485
- return true;
1486
- } catch (error) {
1487
- if (isMissing(error)) {
1488
- return false;
1489
- }
1490
- throw error;
1491
- }
1492
- },
1493
- async list(dir) {
1494
- try {
1495
- const names = await readdir(toDisk(dir));
1496
- return names.sort();
1497
- } catch (error) {
1498
- if (isMissing(error)) {
1499
- return [];
1500
- }
1501
- throw error;
1502
- }
1503
- },
1504
- async isDirectory(path) {
1505
- try {
1506
- return (await stat(toDisk(path))).isDirectory();
1507
- } catch (error) {
1508
- if (isMissing(error)) {
1509
- return false;
1510
- }
1511
- throw error;
1512
- }
1513
- },
1514
- async mkdir(dir) {
1515
- await mkdir(toDisk(dir), { recursive: true });
1516
- },
1517
- async delete(path) {
1518
- await rm(toDisk(path), { force: true });
1519
- },
1520
- async stat(path) {
1521
- try {
1522
- const stats = await stat(toDisk(path));
1523
- return {
1524
- size: stats.size,
1525
- mtimeMs: stats.mtimeMs,
1526
- isDirectory: stats.isDirectory()
1527
- };
1528
- } catch (error) {
1529
- if (isMissing(error)) {
1530
- return;
1531
- }
1532
- throw error;
1533
- }
1534
- }
1535
- };
1536
- }
1537
-
1538
- // src/telemetry/types.ts
1539
- import { z as z5 } from "zod";
1540
- var CostRecordSchema = z5.object({
1541
- ts: z5.number().finite(),
1542
- sessionID: z5.string().min(1).optional(),
1543
- promptHash: z5.string().min(1),
1544
- promptChars: z5.number().int().min(0),
1545
- tier: ComplexityTierSchema,
1546
- routeKind: z5.enum(["local", "frontier"]),
1547
- selected: ModelRefSchema,
1548
- rationale: z5.string(),
1549
- estimatedCostUSD: z5.number().finite().min(0),
1550
- baselineCostUSD: z5.number().finite().min(0),
1551
- estimatedSavingsUSD: z5.number().finite(),
1552
- budgetAction: z5.string().min(1),
1553
- tokensIn: z5.number().int().min(0).optional(),
1554
- tokensOut: z5.number().int().min(0).optional()
1555
- });
1566
+ var current=document.documentElement.getAttribute('data-generated')||'';
1567
+ if('EventSource' in window){
1568
+ try{
1569
+ var es=new EventSource('/events');
1570
+ es.addEventListener('state',function(){location.reload()});
1571
+ es.onerror=function(){/* fallback below */};
1572
+ }catch(e){/* ignore */}
1573
+ }
1574
+ var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
1575
+ setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
1556
1576
 
1557
- // src/telemetry/read.ts
1558
- var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
1559
- function parseCostRecordsJsonl(text) {
1560
- const records = [];
1561
- for (const line of text.split(`
1562
- `)) {
1563
- const trimmed = line.trim();
1564
- if (trimmed.length === 0) {
1565
- continue;
1566
- }
1567
- let candidate;
1568
- try {
1569
- candidate = JSON.parse(trimmed);
1570
- } catch {
1571
- continue;
1572
- }
1573
- const parsed = CostRecordSchema.safeParse(candidate);
1574
- if (parsed.success) {
1575
- records.push(parsed.data);
1577
+ function selectTab(name){
1578
+ var tabs=document.querySelectorAll('#tabbar .tab');
1579
+ for(var i=0;i<tabs.length;i++){tabs[i].classList.toggle('active',tabs[i].getAttribute('data-tab')===name)}
1580
+ var views=document.querySelectorAll('main .view');
1581
+ var matched=false;
1582
+ for(var j=0;j<views.length;j++){
1583
+ var show=views[j].getAttribute('data-view')===name;
1584
+ views[j].hidden=!show;
1585
+ if(show){matched=true;if(name!=='all'){connectTerminal(views[j],name)}}
1576
1586
  }
1587
+ if(!matched){return false}
1588
+ try{sessionStorage.setItem('openteam.tab',name)}catch(e){}
1589
+ return true;
1577
1590
  }
1578
- return records;
1579
- }
1580
-
1581
- // src/telemetry/eventLog.ts
1582
- var DEFAULT_SESSIONS_DIR = ".opencode/openteam/sessions";
1583
- var LEGACY_SESSION_ID = "legacy";
1584
- function parseEventsJsonl(text) {
1585
- const events = [];
1586
- for (const line of text.split(`
1587
- `)) {
1588
- const trimmed = line.trim();
1589
- if (trimmed.length === 0) {
1590
- continue;
1591
+ function connectTerminal(view,id){
1592
+ if(view.__wired){return}
1593
+ var root=document.documentElement;
1594
+ var token=root.getAttribute('data-token');
1595
+ if(root.getAttribute('data-terminal')!=='1'||!token){return}
1596
+ var log=view.querySelector('.termlog');
1597
+ if(!log){return}
1598
+ view.__wired=true;
1599
+ function append(cls,text){
1600
+ var line=document.createElement('div');
1601
+ line.className='tl '+cls;line.textContent=text;
1602
+ log.appendChild(line);log.scrollTop=log.scrollHeight;
1591
1603
  }
1592
- let candidate;
1593
- try {
1594
- candidate = JSON.parse(trimmed);
1595
- } catch {
1596
- continue;
1604
+ var q='/console/session/'+encodeURIComponent(id);
1605
+ var tk='token='+encodeURIComponent(token);
1606
+ if('EventSource' in window){
1607
+ try{
1608
+ var es=new EventSource(q+'/stream?'+tk);
1609
+ es.addEventListener('frame',function(e){try{var f=JSON.parse(e.data);append(f.kind,f.text)}catch(_){}});
1610
+ es.onerror=function(){/* reconecta solo */};
1611
+ }catch(_){/* ignore */}
1597
1612
  }
1598
- const parsed = OpenTeamEventSchema.safeParse(candidate);
1599
- if (parsed.success) {
1600
- events.push(parsed.data);
1613
+ var form=view.querySelector('form.terminput');
1614
+ if(form){
1615
+ form.addEventListener('submit',function(e){
1616
+ e.preventDefault();
1617
+ var inp=form.querySelector('input[name=text]');
1618
+ var text=inp.value;
1619
+ if(!text){return}
1620
+ inp.value='';append('input','› '+text);
1621
+ fetch(q+'/input?'+tk,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({text:text})})
1622
+ .then(function(r){if(!r.ok){append('error','✖ envío rechazado ('+r.status+')')}})
1623
+ .catch(function(){append('error','✖ no se pudo enviar')});
1624
+ });
1601
1625
  }
1626
+ connectPty(view,id,q,tk);
1602
1627
  }
1603
- return events;
1604
- }
1605
- function costRecordToRouteEvent(record) {
1606
- const event = {
1607
- v: EVENT_SCHEMA_VERSION,
1608
- type: "route",
1609
- ts: record.ts,
1610
- sessionID: record.sessionID ?? LEGACY_SESSION_ID,
1611
- promptHash: record.promptHash,
1612
- promptChars: record.promptChars,
1613
- tier: record.tier,
1614
- routeKind: record.routeKind,
1615
- selected: record.selected,
1616
- rationale: record.rationale,
1617
- estimatedCostUSD: record.estimatedCostUSD,
1618
- baselineCostUSD: record.baselineCostUSD,
1619
- estimatedSavingsUSD: record.estimatedSavingsUSD,
1620
- budgetAction: record.budgetAction
1621
- };
1622
- if (record.tokensIn !== undefined) {
1623
- event.tokensIn = record.tokensIn;
1628
+ function connectPty(view,id,q,tk){
1629
+ var root=document.documentElement;
1630
+ if(root.getAttribute('data-pty')!=='1'){return}
1631
+ var pane=view.querySelector('.pty');
1632
+ if(!pane){return}
1633
+ var log=pane.querySelector('.ptylog');
1634
+ var startBtn=pane.querySelector('.pty-start');
1635
+ var input=pane.querySelector('form.ptyinput');
1636
+ function plog(text){
1637
+ var d=document.createElement('span');d.textContent=text;
1638
+ log.appendChild(d);log.scrollTop=log.scrollHeight;
1639
+ }
1640
+ if(!startBtn){return}
1641
+ startBtn.addEventListener('click',function(){
1642
+ startBtn.disabled=true;plog('Abriendo shell…
1643
+ ');
1644
+ fetch(q+'/pty?'+tk,{method:'POST',headers:{'content-type':'application/json'},body:'{}'})
1645
+ .then(function(r){return r.ok?r.json():Promise.reject(r.status)})
1646
+ .then(function(info){openPtyWs(info.wsUrl)})
1647
+ .catch(function(s){plog('✖ no se pudo abrir PTY ('+s+')
1648
+ ');startBtn.disabled=false});
1649
+ });
1650
+ function openPtyWs(url){
1651
+ try{
1652
+ var ws=new WebSocket(url);
1653
+ ws.binaryType='arraybuffer';
1654
+ ws.onmessage=function(e){
1655
+ var data=e.data;
1656
+ if(data instanceof ArrayBuffer){data=new TextDecoder().decode(new Uint8Array(data))}
1657
+ plog(data);
1658
+ };
1659
+ ws.onclose=function(){plog('
1660
+ [shell cerrada]
1661
+ ');startBtn.disabled=false};
1662
+ ws.onerror=function(){plog('
1663
+ ✖ error de WebSocket
1664
+ ')};
1665
+ if(input){
1666
+ input.hidden=false;
1667
+ input.addEventListener('submit',function(e){
1668
+ e.preventDefault();
1669
+ var c=input.querySelector('input[name=cmd]');
1670
+ if(!c.value){return}
1671
+ ws.send(c.value+'\r');c.value='';
1672
+ });
1673
+ }
1674
+ }catch(_){plog('
1675
+ ✖ WebSocket no disponible
1676
+ ');startBtn.disabled=false}
1677
+ }
1624
1678
  }
1625
- if (record.tokensOut !== undefined) {
1626
- event.tokensOut = record.tokensOut;
1679
+ var bar=document.getElementById('tabbar');
1680
+ if(bar){
1681
+ bar.addEventListener('click',function(e){
1682
+ var t=e.target;
1683
+ while(t&&t!==bar&&!t.getAttribute('data-tab')){t=t.parentNode}
1684
+ if(t&&t.getAttribute('data-tab')){selectTab(t.getAttribute('data-tab'))}
1685
+ });
1627
1686
  }
1628
- return event;
1687
+ var saved='all';
1688
+ try{saved=sessionStorage.getItem('openteam.tab')||'all'}catch(e){}
1689
+ if(!selectTab(saved)){selectTab('all')}
1690
+ })();
1691
+ `;
1692
+ function renderConsoleHtml(state, options = {}) {
1693
+ const refreshMs = options.refreshMs ?? 2000;
1694
+ const terminalEnabled = options.terminalEnabled === true;
1695
+ const ptyEnabled = options.ptyEnabled === true;
1696
+ const allBody = [
1697
+ summaryPanel(state.cost),
1698
+ totalsPanel(state.totals),
1699
+ sessionsPanel(state.sessions),
1700
+ loopPanel(state.loop),
1701
+ teamPanel(state.team),
1702
+ tierPanel(state.cost),
1703
+ routesPanel(state.recentRoutes),
1704
+ decisionsPanel(state.decisions),
1705
+ meetingsPanel(state.meetings),
1706
+ activityPanel(state.activity)
1707
+ ].join("");
1708
+ const allView = `<div class="view" data-view="all">${allBody}</div>`;
1709
+ const sessionViews = state.sessionsDetail.map((tab) => sessionView(tab, terminalEnabled, ptyEnabled)).join("");
1710
+ const body = allView + sessionViews;
1711
+ const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
1712
+ const tokenAttr = terminalEnabled && options.consoleToken !== undefined ? ` data-token="${escapeHtml(options.consoleToken)}" data-terminal="1"${ptyEnabled ? ' data-pty="1"' : ""}` : "";
1713
+ return [
1714
+ "<!doctype html>",
1715
+ `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}"${tokenAttr}>`,
1716
+ "<head>",
1717
+ '<meta charset="utf-8">',
1718
+ '<meta name="viewport" content="width=device-width,initial-scale=1">',
1719
+ "<title>openteam console</title>",
1720
+ `<style>${STYLE}</style>`,
1721
+ "</head>",
1722
+ "<body>",
1723
+ "<header>",
1724
+ "<h1>openteam console</h1>",
1725
+ `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
1726
+ "</header>",
1727
+ tabBar(state.sessionsDetail),
1728
+ `<main>${body}</main>`,
1729
+ `<script>${CLIENT_JS}</script>`,
1730
+ "</body>",
1731
+ "</html>"
1732
+ ].join("");
1629
1733
  }
1630
- function routeEventToCostRecord(event) {
1631
- const record = {
1632
- ts: event.ts,
1633
- sessionID: event.sessionID,
1634
- promptHash: event.promptHash,
1635
- promptChars: event.promptChars,
1636
- tier: event.tier,
1637
- routeKind: event.routeKind,
1638
- selected: event.selected,
1639
- rationale: event.rationale,
1640
- estimatedCostUSD: event.estimatedCostUSD,
1641
- baselineCostUSD: event.baselineCostUSD,
1642
- estimatedSavingsUSD: event.estimatedSavingsUSD,
1643
- budgetAction: event.budgetAction
1734
+
1735
+ // src/commands/report.ts
1736
+ var TIERS2 = [
1737
+ "trivial",
1738
+ "simple",
1739
+ "moderate",
1740
+ "hard"
1741
+ ];
1742
+ function emptyTierSummary() {
1743
+ return {
1744
+ trivial: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
1745
+ simple: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
1746
+ moderate: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
1747
+ hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
1644
1748
  };
1645
- if (event.tokensIn !== undefined) {
1646
- record.tokensIn = event.tokensIn;
1647
- }
1648
- if (event.tokensOut !== undefined) {
1649
- record.tokensOut = event.tokensOut;
1650
- }
1651
- return record;
1652
1749
  }
1653
- async function readRouteCostRecords(dir, deps) {
1654
- const events = await readSessionEvents(dir, deps);
1655
- const records = [];
1656
- for (const event of events) {
1657
- if (event.type === "route") {
1658
- records.push(routeEventToCostRecord(event));
1750
+ function summarizeCostRecords(records) {
1751
+ const report = {
1752
+ count: 0,
1753
+ localCount: 0,
1754
+ frontierCount: 0,
1755
+ totalEstimatedUSD: 0,
1756
+ totalBaselineUSD: 0,
1757
+ totalSavingsUSD: 0,
1758
+ savingsPct: 0,
1759
+ tokensIn: 0,
1760
+ tokensOut: 0,
1761
+ byTier: emptyTierSummary()
1762
+ };
1763
+ for (const record of records) {
1764
+ report.count += 1;
1765
+ if (record.routeKind === "local") {
1766
+ report.localCount += 1;
1767
+ } else {
1768
+ report.frontierCount += 1;
1659
1769
  }
1770
+ report.totalEstimatedUSD += record.estimatedCostUSD;
1771
+ report.totalBaselineUSD += record.baselineCostUSD;
1772
+ report.totalSavingsUSD += record.estimatedSavingsUSD;
1773
+ report.tokensIn += record.tokensIn ?? 0;
1774
+ report.tokensOut += record.tokensOut ?? 0;
1775
+ const tier = report.byTier[record.tier];
1776
+ tier.count += 1;
1777
+ tier.estimatedUSD += record.estimatedCostUSD;
1778
+ tier.savingsUSD += record.estimatedSavingsUSD;
1660
1779
  }
1661
- return records;
1780
+ report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
1781
+ return report;
1662
1782
  }
1663
- async function readSessionEvents(dir, deps) {
1664
- const files = (await deps.storage.list(dir)).filter((file) => file.endsWith(".jsonl"));
1665
- const perFile = await Promise.all(files.map(async (file) => {
1666
- const text = await deps.storage.read(joinVirtualPath(dir, file));
1667
- return text === undefined ? [] : parseEventsJsonl(text);
1668
- }));
1669
- const events = perFile.flat();
1670
- if (deps.legacyTelemetryPath !== undefined) {
1671
- const legacyText = await deps.storage.read(deps.legacyTelemetryPath);
1672
- if (legacyText !== undefined) {
1673
- for (const record of parseCostRecordsJsonl(legacyText)) {
1674
- events.push(costRecordToRouteEvent(record));
1675
- }
1676
- }
1783
+ function usd2(value) {
1784
+ return `$${value.toFixed(5)}`;
1785
+ }
1786
+ function renderReport(report) {
1787
+ if (report.count === 0) {
1788
+ return "openteam report: sin registros de telemetría todavía.";
1677
1789
  }
1678
- return events.sort((a, b) => a.ts - b.ts);
1790
+ const lines = [
1791
+ "openteam report:",
1792
+ ` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
1793
+ ` coste real: ${usd2(report.totalEstimatedUSD)}`,
1794
+ ` baseline: ${usd2(report.totalBaselineUSD)}`,
1795
+ ` ahorro: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
1796
+ ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
1797
+ " por tier:"
1798
+ ];
1799
+ for (const tier of TIERS2) {
1800
+ const summary = report.byTier[tier];
1801
+ lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd2(summary.estimatedUSD)} · ahorro ${usd2(summary.savingsUSD)}`);
1802
+ }
1803
+ return lines.join(`
1804
+ `);
1679
1805
  }
1680
1806
 
1681
1807
  // src/console/backlog.ts
@@ -2771,6 +2897,20 @@ async function runConsoleServe(deps, options) {
2771
2897
  if (config.console.remoteStorage) {
2772
2898
  deps.log("Storage remoto expuesto en /storage/* (read-only, requiere token).");
2773
2899
  }
2900
+ if (config.storage.sqliteIndex.enabled) {
2901
+ const buildIndex = deps.buildSqliteIndex ?? buildSqliteIndexFromConfig;
2902
+ try {
2903
+ const result = await buildIndex(config.storage.sqliteIndex.path, deps.sessionsDir, {
2904
+ storage: deps.storage,
2905
+ ...deps.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: deps.legacyTelemetryPath } : {}
2906
+ });
2907
+ result.index.close();
2908
+ deps.log(`Índice SQLite reconstruido (${result.events} eventos) en ${config.storage.sqliteIndex.path}.`);
2909
+ } catch (error) {
2910
+ const message = error instanceof Error ? error.message : String(error);
2911
+ deps.log(`Índice SQLite deshabilitado (no disponible): ${message}`);
2912
+ }
2913
+ }
2774
2914
  if (options.open && deps.openBrowser !== undefined) {
2775
2915
  try {
2776
2916
  await deps.openBrowser(runtime.url);