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