@jmanuelcorral/openteam 0.1.31 → 0.1.33

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("");
605
- }
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("");
618
- }
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>`;
630
- }
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(){});
699
- }
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 */}
707
- }
708
- var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
709
- setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
710
579
 
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;
724
- }
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;
737
- }
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 */}
746
- }
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
- });
759
- }
760
- connectPty(view,id,q,tk);
761
- }
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;
773
- }
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}
811
- }
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("");
867
- }
580
+ // src/telemetry/events.ts
581
+ import { z as z4 } from "zod";
868
582
 
869
- // src/commands/report.ts
870
- var TIERS2 = [
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([
871
594
  "trivial",
872
595
  "simple",
873
596
  "moderate",
874
597
  "hard"
875
- ];
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
- };
883
- }
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;
916
- }
917
- function usd2(value) {
918
- return `$${value.toFixed(5)}`;
919
- }
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
- `);
939
- }
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;
940
728
 
941
- // src/telemetry/aggregate.ts
942
- function emptyToolcallStats() {
943
- return { count: 0, ok: 0, failed: 0, totalDurationMs: 0, byTool: {} };
944
- }
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
- };
966
- }
967
- function pushUnique(list, value) {
968
- if (value !== undefined && !list.includes(value)) {
969
- list.push(value);
970
- }
971
- }
972
- function applyRoute(session, event) {
973
- session.routeCount += 1;
974
- if (event.routeKind === "local") {
975
- session.localCount += 1;
976
- } else {
977
- session.frontierCount += 1;
978
- }
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);
994
- }
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;
1002
- }
1003
- }
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);
1015
- }
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;
1032
- }
1033
- session.endpoint = endpoint;
1034
- }
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;
1040
- }
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;
1065
- }
1066
- }
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;
1080
- }
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];
1093
- }
1094
- return view;
1095
- }
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;
1105
- }
1106
- return view;
1107
- }
1108
- function byTsDesc(list) {
1109
- return [...list].sort((a, b) => b.ts - a.ts);
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";
1110
830
  }
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);
831
+ function createFsStorageProvider(root) {
832
+ function toDisk(path) {
833
+ return join(root, ...splitVirtualPath(path));
1173
834
  }
1174
835
  return {
1175
- sessions: summaries,
1176
- totals,
1177
- decisions: byTsDesc(decisions),
1178
- meetings: byTsDesc(meetings),
1179
- activity: byTsDesc(activity)
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
+ }
1180
909
  };
1181
910
  }
1182
911
 
1183
- // src/storage/path.ts
1184
- class StoragePathError extends Error {
1185
- constructor(message) {
1186
- super(message);
1187
- this.name = "StoragePathError";
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
+ }
1188
951
  }
952
+ return records;
1189
953
  }
1190
- function normalizeVirtualPath(input) {
1191
- const segments = [];
1192
- for (const rawSegment of input.split(/[\\/]+/)) {
1193
- if (rawSegment === "" || rawSegment === ".") {
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) {
1194
964
  continue;
1195
965
  }
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();
966
+ let candidate;
967
+ try {
968
+ candidate = JSON.parse(trimmed);
969
+ } catch {
1201
970
  continue;
1202
971
  }
1203
- segments.push(rawSegment);
972
+ const parsed = OpenTeamEventSchema.safeParse(candidate);
973
+ if (parsed.success) {
974
+ events.push(parsed.data);
975
+ }
1204
976
  }
1205
- return segments.join("/");
977
+ return events;
1206
978
  }
1207
- function joinVirtualPath(...segments) {
1208
- return normalizeVirtualPath(segments.join("/"));
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;
998
+ }
999
+ if (record.tokensOut !== undefined) {
1000
+ event.tokensOut = record.tokensOut;
1001
+ }
1002
+ return event;
1209
1003
  }
1210
- function splitVirtualPath(path) {
1211
- const normalized = normalizeVirtualPath(path);
1212
- return normalized === "" ? [] : normalized.split("/");
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;
1024
+ }
1025
+ return record;
1213
1026
  }
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
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));
1033
+ }
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));
1341
1049
  }
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;
1050
+ }
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));
1091
+ }
1092
+ db.run("COMMIT");
1093
+ } catch (error) {
1094
+ db.run("ROLLBACK");
1095
+ throw error;
1096
+ }
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
+ }
1117
+ }
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();
1149
+ }
1150
+ };
1151
+ }
1152
+ async function rebuildSqliteIndexFromStorage(index, dir, deps) {
1153
+ const events = await readSessionEvents(dir, deps);
1154
+ index.rebuild(events);
1155
+ return events.length;
1156
+ }
1354
1157
 
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
- ]);
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
+ }
1442
1169
 
1443
- // src/storage/fsStorageProvider.ts
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
1444
1178
  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";
1179
+ createServer as createHttpServer
1180
+ } from "node:http";
1181
+
1182
+ // src/console/render.ts
1183
+ var TIERS = [
1184
+ "trivial",
1185
+ "simple",
1186
+ "moderate",
1187
+ "hard"
1188
+ ];
1189
+ function escapeHtml(value) {
1190
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1191
+ }
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`;
1200
+ }
1201
+ function avgMs(stats) {
1202
+ return stats.count === 0 ? "—" : ms(stats.totalDurationMs / stats.count);
1203
+ }
1204
+ function agentModel(agent) {
1205
+ return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
1206
+ }
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("");
1223
+ }
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("");
1236
+ }
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>`;
1242
+ }
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("");
1251
+ }
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("");
1263
+ }
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("");
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("");
1281
+ }
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>`;
1284
+ }
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("");
1293
+ }
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("");
1302
+ }
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>`;
1456
1306
  }
1457
- function createFsStorageProvider(root) {
1458
- function toDisk(path) {
1459
- return join(root, ...splitVirtualPath(path));
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("");
1460
1315
  }
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
- };
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("");
1343
+ }
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("");
1356
+ }
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("");
1365
+ }
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("");
1374
+ }
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("");
1388
+ }
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("");
1396
+ }
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>`;
1401
+ }
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("");
1410
+ }
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("");
1418
+ }
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>`;
1427
+ }
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>`;
1436
+ }
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("");
1471
+ }
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("");
1484
+ }
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>`;
1536
1496
  }
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(){});
1565
+ }
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));
1537
1576
 
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
- });
1556
-
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);
@@ -4044,6 +4184,7 @@ var HELP = [
4044
4184
  " openteam console Lanza la Console web multi-sesión (Ctrl+C para parar; --open abre el navegador)",
4045
4185
  " openteam console --status Muestra la config de la Console sin lanzarla",
4046
4186
  " openteam tunnel Expone la Console vía Dev Tunnels (--terminal expone opencode; Ctrl+C para cerrar)",
4187
+ " openteam mcp Servidor MCP de solo lectura (memoria del equipo) sobre stdio",
4047
4188
  " openteam report Resumen de coste/ahorro (telemetría)",
4048
4189
  " openteam yolo status Muestra si el modo YOLO está activo",
4049
4190
  " openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
@@ -4804,6 +4945,220 @@ function loadOpenTeamConfig(raw) {
4804
4945
  return OpenTeamConfigSchema.parse(raw ?? {});
4805
4946
  }
4806
4947
 
4948
+ // src/mcp/teamMemory.ts
4949
+ function buildTeamMemory(events) {
4950
+ const aggregate = aggregateEvents(events);
4951
+ return {
4952
+ sessions: aggregate.sessions,
4953
+ totals: aggregate.totals,
4954
+ decisions: aggregate.decisions,
4955
+ meetings: aggregate.meetings
4956
+ };
4957
+ }
4958
+ function applyLimit(list, limit) {
4959
+ if (limit === undefined || limit <= 0) {
4960
+ return [...list];
4961
+ }
4962
+ return list.slice(0, limit);
4963
+ }
4964
+ function queryDecisions(memory, query = {}) {
4965
+ const filtered = memory.decisions.filter((decision) => {
4966
+ if (query.agent !== undefined && decision.agent !== query.agent) {
4967
+ return false;
4968
+ }
4969
+ if (query.tag !== undefined && !(decision.tags ?? []).includes(query.tag)) {
4970
+ return false;
4971
+ }
4972
+ return true;
4973
+ });
4974
+ return applyLimit(filtered, query.limit);
4975
+ }
4976
+ function queryMeetings(memory, query = {}) {
4977
+ return applyLimit(memory.meetings, query.limit);
4978
+ }
4979
+ function querySessions(memory, query = {}) {
4980
+ return applyLimit(memory.sessions, query.limit);
4981
+ }
4982
+ function costSummary(memory) {
4983
+ const { totals } = memory;
4984
+ return {
4985
+ sessions: totals.sessions,
4986
+ messageCount: totals.messageCount,
4987
+ realCostUSD: totals.realCostUSD,
4988
+ estimatedCostUSD: totals.estimatedCostUSD,
4989
+ estimatedSavingsUSD: totals.estimatedSavingsUSD,
4990
+ tokensIn: totals.tokensIn,
4991
+ tokensOut: totals.tokensOut
4992
+ };
4993
+ }
4994
+
4995
+ // src/mcp/server.ts
4996
+ var MCP_PROTOCOL_VERSION = "2024-11-05";
4997
+ var DEFAULT_SERVER_INFO = {
4998
+ name: "openteam-memory",
4999
+ version: "1"
5000
+ };
5001
+ var LIMIT_PROP = {
5002
+ limit: {
5003
+ type: "integer",
5004
+ minimum: 1,
5005
+ description: "Máximo de elementos a devolver."
5006
+ }
5007
+ };
5008
+ var MCP_TOOLS = [
5009
+ {
5010
+ name: "list_decisions",
5011
+ description: "Decisiones/aprendizajes del equipo (redactadas), más recientes primero. Filtra por agente y/o etiqueta.",
5012
+ inputSchema: {
5013
+ type: "object",
5014
+ properties: {
5015
+ agent: { type: "string", description: "Filtra por agente." },
5016
+ tag: { type: "string", description: "Filtra por etiqueta." },
5017
+ ...LIMIT_PROP
5018
+ }
5019
+ }
5020
+ },
5021
+ {
5022
+ name: "list_meetings",
5023
+ description: "Reuniones (batches multi-agente del orquestador), más recientes primero.",
5024
+ inputSchema: { type: "object", properties: { ...LIMIT_PROP } }
5025
+ },
5026
+ {
5027
+ name: "list_sessions",
5028
+ description: "Resúmenes de sesión (coste/tokens reales, modelos, toolcalls), más activas primero.",
5029
+ inputSchema: { type: "object", properties: { ...LIMIT_PROP } }
5030
+ },
5031
+ {
5032
+ name: "cost_summary",
5033
+ description: "Resumen de coste/tokens del equipo, con ahorro estimado frente al baseline all-frontier.",
5034
+ inputSchema: { type: "object", properties: {} }
5035
+ }
5036
+ ];
5037
+ function response(id, result) {
5038
+ return { jsonrpc: "2.0", id, result };
5039
+ }
5040
+ function errorResponse(id, code, message) {
5041
+ return { jsonrpc: "2.0", id, error: { code, message } };
5042
+ }
5043
+ function textContent(data) {
5044
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
5045
+ }
5046
+ function optionalString(value) {
5047
+ return typeof value === "string" && value.length > 0 ? value : undefined;
5048
+ }
5049
+ function optionalLimit(value) {
5050
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
5051
+ }
5052
+ async function callTool(name, args, deps) {
5053
+ const memory = await deps.readMemory();
5054
+ switch (name) {
5055
+ case "list_decisions": {
5056
+ const query = {};
5057
+ const agent = optionalString(args.agent);
5058
+ const tag = optionalString(args.tag);
5059
+ const limit = optionalLimit(args.limit);
5060
+ if (agent !== undefined) {
5061
+ query.agent = agent;
5062
+ }
5063
+ if (tag !== undefined) {
5064
+ query.tag = tag;
5065
+ }
5066
+ if (limit !== undefined) {
5067
+ query.limit = limit;
5068
+ }
5069
+ return textContent(queryDecisions(memory, query));
5070
+ }
5071
+ case "list_meetings":
5072
+ return textContent(queryMeetings(memory, { limit: optionalLimit(args.limit) ?? 0 }));
5073
+ case "list_sessions":
5074
+ return textContent(querySessions(memory, { limit: optionalLimit(args.limit) ?? 0 }));
5075
+ case "cost_summary":
5076
+ return textContent(costSummary(memory));
5077
+ default:
5078
+ return;
5079
+ }
5080
+ }
5081
+ async function handleMcpMessage(message, deps) {
5082
+ const isNotification = message.id === undefined;
5083
+ const id = message.id ?? null;
5084
+ const serverInfo = deps.serverInfo ?? DEFAULT_SERVER_INFO;
5085
+ switch (message.method) {
5086
+ case "initialize":
5087
+ return response(id, {
5088
+ protocolVersion: MCP_PROTOCOL_VERSION,
5089
+ capabilities: { tools: {} },
5090
+ serverInfo
5091
+ });
5092
+ case "tools/list":
5093
+ return response(id, { tools: MCP_TOOLS });
5094
+ case "tools/call": {
5095
+ const params = message.params ?? {};
5096
+ const name = optionalString(params.name);
5097
+ if (name === undefined) {
5098
+ return errorResponse(id, -32602, "tools/call requiere 'name'");
5099
+ }
5100
+ const args = typeof params.arguments === "object" && params.arguments !== null ? params.arguments : {};
5101
+ const result = await callTool(name, args, deps);
5102
+ if (result === undefined) {
5103
+ return errorResponse(id, -32602, `tool desconocida: ${name}`);
5104
+ }
5105
+ return response(id, result);
5106
+ }
5107
+ default:
5108
+ if (isNotification) {
5109
+ return null;
5110
+ }
5111
+ return errorResponse(id, -32601, `método no soportado: ${message.method}`);
5112
+ }
5113
+ }
5114
+
5115
+ // src/mcp/stdio.ts
5116
+ function parseErrorResponse() {
5117
+ return JSON.stringify({
5118
+ jsonrpc: "2.0",
5119
+ id: null,
5120
+ error: { code: -32700, message: "JSON inválido" }
5121
+ });
5122
+ }
5123
+ async function runMcpStdio(deps, io) {
5124
+ for await (const rawLine of io.lines) {
5125
+ const line = rawLine.trim();
5126
+ if (line.length === 0) {
5127
+ continue;
5128
+ }
5129
+ let message;
5130
+ try {
5131
+ message = JSON.parse(line);
5132
+ } catch {
5133
+ io.write(`${parseErrorResponse()}
5134
+ `);
5135
+ continue;
5136
+ }
5137
+ const reply = await handleMcpMessage(message, deps);
5138
+ if (reply !== null) {
5139
+ io.write(`${JSON.stringify(reply)}
5140
+ `);
5141
+ }
5142
+ }
5143
+ }
5144
+ async function* linesFromReadable(readable) {
5145
+ let buffer = "";
5146
+ for await (const chunk of readable) {
5147
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
5148
+ let newlineIndex = buffer.indexOf(`
5149
+ `);
5150
+ while (newlineIndex >= 0) {
5151
+ yield buffer.slice(0, newlineIndex);
5152
+ buffer = buffer.slice(newlineIndex + 1);
5153
+ newlineIndex = buffer.indexOf(`
5154
+ `);
5155
+ }
5156
+ }
5157
+ if (buffer.length > 0) {
5158
+ yield buffer;
5159
+ }
5160
+ }
5161
+
4807
5162
  // src/web/git.ts
4808
5163
  function createGitLastCommit(exec) {
4809
5164
  return async () => {
@@ -4970,6 +5325,18 @@ async function main() {
4970
5325
  process.exitCode = result2.exitCode;
4971
5326
  return;
4972
5327
  }
5328
+ if (argv[0] === "mcp") {
5329
+ await runMcpStdio({
5330
+ readMemory: async () => buildTeamMemory(await readSessionEvents(DEFAULT_SESSIONS_DIR, {
5331
+ storage,
5332
+ legacyTelemetryPath: DEFAULT_TELEMETRY_PATH
5333
+ }))
5334
+ }, {
5335
+ lines: linesFromReadable(process.stdin),
5336
+ write: (line) => process.stdout.write(line)
5337
+ });
5338
+ return;
5339
+ }
4973
5340
  const result = await runCli(argv, deps);
4974
5341
  process.stdout.write(`${result.stdout}
4975
5342
  `);