@jmanuelcorral/openteam 0.1.21 → 0.1.23
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/README.md +29 -31
- package/dist/cli/dashboardServe.d.ts +15 -8
- package/dist/cli/dashboardServe.d.ts.map +1 -1
- package/dist/cli.js +936 -286
- package/dist/commands/dashboard.d.ts +4 -4
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/orchestratorAgent.d.ts.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/slashCommand.d.ts +32 -8
- package/dist/commands/slashCommand.d.ts.map +1 -1
- package/dist/contract/opencode.d.ts +1 -1
- package/dist/contract/opencode.d.ts.map +1 -1
- package/dist/dashboard/render.d.ts.map +1 -1
- package/dist/dashboard/state.d.ts +14 -6
- package/dist/dashboard/state.d.ts.map +1 -1
- package/dist/dashboard/types.d.ts +14 -3
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +476 -802
- package/dist/orchestrator/coordinator.d.ts +14 -2
- package/dist/orchestrator/coordinator.d.ts.map +1 -1
- package/dist/plugin/capture.d.ts +46 -0
- package/dist/plugin/capture.d.ts.map +1 -0
- package/dist/plugin/hooks.d.ts +2 -2
- package/dist/plugin/hooks.d.ts.map +1 -1
- package/dist/plugin/toolcalls.d.ts +21 -0
- package/dist/plugin/toolcalls.d.ts.map +1 -0
- package/dist/telemetry/aggregate.d.ts +89 -0
- package/dist/telemetry/aggregate.d.ts.map +1 -0
- package/dist/telemetry/decisions.d.ts +25 -0
- package/dist/telemetry/decisions.d.ts.map +1 -0
- package/dist/telemetry/eventLog.d.ts +63 -0
- package/dist/telemetry/eventLog.d.ts.map +1 -0
- package/dist/telemetry/events.d.ts +217 -0
- package/dist/telemetry/events.d.ts.map +1 -0
- package/dist/web/snapshot.d.ts +13 -7
- package/dist/web/snapshot.d.ts.map +1 -1
- package/dist/web/start.d.ts +10 -7
- package/dist/web/start.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/web/activity.d.ts +0 -12
- package/dist/web/activity.d.ts.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -2,31 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
|
-
import { access, mkdir as mkdir2, readdir, readFile
|
|
5
|
+
import { access, mkdir as mkdir2, readdir, readFile, writeFile as writeFile2 } from "node:fs/promises";
|
|
6
6
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
|
|
9
9
|
// src/web/start.ts
|
|
10
10
|
import { watch as fsWatch } from "node:fs";
|
|
11
11
|
|
|
12
|
-
// src/web/activity.ts
|
|
13
|
-
function createActivityBuffer(max = 100) {
|
|
14
|
-
const entries = [];
|
|
15
|
-
return {
|
|
16
|
-
push(entry) {
|
|
17
|
-
entries.push(entry);
|
|
18
|
-
if (entries.length > max) {
|
|
19
|
-
entries.splice(0, entries.length - max);
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
list() {
|
|
23
|
-
return [...entries];
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
12
|
// src/web/paths.ts
|
|
29
13
|
var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
|
|
14
|
+
var DEFAULT_DECISIONS_PATH = ".opencode/openteam-decisions.md";
|
|
30
15
|
|
|
31
16
|
// src/web/server.ts
|
|
32
17
|
import {
|
|
@@ -49,6 +34,12 @@ function usd(value) {
|
|
|
49
34
|
function pct(value) {
|
|
50
35
|
return `${value.toFixed(2)}%`;
|
|
51
36
|
}
|
|
37
|
+
function ms(value) {
|
|
38
|
+
return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`;
|
|
39
|
+
}
|
|
40
|
+
function avgMs(stats) {
|
|
41
|
+
return stats.count === 0 ? "—" : ms(stats.totalDurationMs / stats.count);
|
|
42
|
+
}
|
|
52
43
|
function agentModel(agent) {
|
|
53
44
|
return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
|
|
54
45
|
}
|
|
@@ -169,6 +160,101 @@ function activityPanel(activity) {
|
|
|
169
160
|
"</section>"
|
|
170
161
|
].join("");
|
|
171
162
|
}
|
|
163
|
+
function totalsPanel(totals) {
|
|
164
|
+
const t = totals.toolcalls;
|
|
165
|
+
return [
|
|
166
|
+
'<section class="panel" id="panel-totals">',
|
|
167
|
+
"<h2>Telemetría real (todas las sesiones)</h2>",
|
|
168
|
+
'<div class="grid">',
|
|
169
|
+
`<div class="stat"><span class="k">Sesiones</span><span class="v">${totals.sessions}</span></div>`,
|
|
170
|
+
`<div class="stat"><span class="k">Mensajes</span><span class="v">${totals.messageCount}</span></div>`,
|
|
171
|
+
`<div class="stat"><span class="k">Coste real</span><span class="v">${usd(totals.realCostUSD)}</span></div>`,
|
|
172
|
+
`<div class="stat"><span class="k">Ahorro est.</span><span class="v good">${usd(totals.estimatedSavingsUSD)}</span></div>`,
|
|
173
|
+
`<div class="stat"><span class="k">Tokens in</span><span class="v">${totals.tokensIn}</span></div>`,
|
|
174
|
+
`<div class="stat"><span class="k">Tokens out</span><span class="v">${totals.tokensOut}</span></div>`,
|
|
175
|
+
`<div class="stat"><span class="k">Reasoning</span><span class="v">${totals.tokensReasoning}</span></div>`,
|
|
176
|
+
`<div class="stat"><span class="k">Cache r/w</span><span class="v">${totals.tokensCacheRead}/${totals.tokensCacheWrite}</span></div>`,
|
|
177
|
+
`<div class="stat"><span class="k">Toolcalls</span><span class="v">${t.count} <span class="good">${t.ok}✓</span> <span class="frontier">${t.failed}✗</span></span></div>`,
|
|
178
|
+
`<div class="stat"><span class="k">Dur. media tool</span><span class="v">${avgMs(t)}</span></div>`,
|
|
179
|
+
"</div>",
|
|
180
|
+
"</section>"
|
|
181
|
+
].join("");
|
|
182
|
+
}
|
|
183
|
+
function sessionRow(session) {
|
|
184
|
+
const models = session.models.map(escapeHtml).join("<br>") || "—";
|
|
185
|
+
return [
|
|
186
|
+
"<tr>",
|
|
187
|
+
`<td class="hash">${escapeHtml(session.sessionID)}</td>`,
|
|
188
|
+
`<td>${models}</td>`,
|
|
189
|
+
`<td><span class="local">${session.localCount}</span>/<span class="frontier">${session.frontierCount}</span></td>`,
|
|
190
|
+
`<td>${usd(session.realCostUSD)}</td>`,
|
|
191
|
+
`<td>${session.tokensIn}/${session.tokensOut}</td>`,
|
|
192
|
+
`<td>${session.toolcalls.count} (${avgMs(session.toolcalls)})</td>`,
|
|
193
|
+
"</tr>"
|
|
194
|
+
].join("");
|
|
195
|
+
}
|
|
196
|
+
function sessionsPanel(sessions) {
|
|
197
|
+
if (sessions.length === 0) {
|
|
198
|
+
return [
|
|
199
|
+
'<section class="panel" id="panel-sessions">',
|
|
200
|
+
"<h2>Sesiones</h2>",
|
|
201
|
+
'<p class="muted">Sin sesiones registradas todavía.</p>',
|
|
202
|
+
"</section>"
|
|
203
|
+
].join("");
|
|
204
|
+
}
|
|
205
|
+
const rows = sessions.map(sessionRow).join("");
|
|
206
|
+
return [
|
|
207
|
+
'<section class="panel" id="panel-sessions">',
|
|
208
|
+
"<h2>Sesiones</h2>",
|
|
209
|
+
"<table><thead><tr><th>Sesión</th><th>Modelos</th><th>L/F</th><th>Coste</th><th>Tok in/out</th><th>Tools</th></tr></thead>",
|
|
210
|
+
`<tbody>${rows}</tbody></table>`,
|
|
211
|
+
"</section>"
|
|
212
|
+
].join("");
|
|
213
|
+
}
|
|
214
|
+
function decisionRow(decision) {
|
|
215
|
+
const who = decision.agent === undefined ? "" : `<span class="who">${escapeHtml(decision.agent)}</span> `;
|
|
216
|
+
const tags = decision.tags === undefined || decision.tags.length === 0 ? "" : ` ${decision.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
|
|
217
|
+
return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision.summary)}${tags}</li>`;
|
|
218
|
+
}
|
|
219
|
+
function decisionsPanel(decisions) {
|
|
220
|
+
if (decisions.length === 0) {
|
|
221
|
+
return [
|
|
222
|
+
'<section class="panel" id="panel-decisions">',
|
|
223
|
+
"<h2>Decisiones</h2>",
|
|
224
|
+
'<p class="muted">Sin decisiones registradas (el scribe escribe en <code>openteam-decisions.md</code>).</p>',
|
|
225
|
+
"</section>"
|
|
226
|
+
].join("");
|
|
227
|
+
}
|
|
228
|
+
const rows = decisions.map(decisionRow).join("");
|
|
229
|
+
return [
|
|
230
|
+
'<section class="panel" id="panel-decisions">',
|
|
231
|
+
"<h2>Decisiones</h2>",
|
|
232
|
+
`<ul class="timeline">${rows}</ul>`,
|
|
233
|
+
"</section>"
|
|
234
|
+
].join("");
|
|
235
|
+
}
|
|
236
|
+
function meetingRow(meeting) {
|
|
237
|
+
const roles = meeting.roles.map(escapeHtml).join(", ");
|
|
238
|
+
const purpose = meeting.purpose === undefined ? "" : `: ${escapeHtml(meeting.purpose)}`;
|
|
239
|
+
return `<li class="agent"><span class="kind">reunión</span> <span class="who">${roles}</span>${purpose}</li>`;
|
|
240
|
+
}
|
|
241
|
+
function meetingsPanel(meetings) {
|
|
242
|
+
if (meetings.length === 0) {
|
|
243
|
+
return [
|
|
244
|
+
'<section class="panel" id="panel-meetings">',
|
|
245
|
+
"<h2>Reuniones</h2>",
|
|
246
|
+
'<p class="muted">Sin reuniones multi-agente todavía.</p>',
|
|
247
|
+
"</section>"
|
|
248
|
+
].join("");
|
|
249
|
+
}
|
|
250
|
+
const rows = meetings.map(meetingRow).join("");
|
|
251
|
+
return [
|
|
252
|
+
'<section class="panel" id="panel-meetings">',
|
|
253
|
+
"<h2>Reuniones</h2>",
|
|
254
|
+
`<ul class="timeline">${rows}</ul>`,
|
|
255
|
+
"</section>"
|
|
256
|
+
].join("");
|
|
257
|
+
}
|
|
172
258
|
var STYLE = `
|
|
173
259
|
:root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
|
|
174
260
|
*{box-sizing:border-box}
|
|
@@ -198,6 +284,7 @@ ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
|
|
|
198
284
|
ul.items li.done{color:var(--muted)}
|
|
199
285
|
ul.items .box{font-family:monospace}
|
|
200
286
|
.who{color:var(--local)}
|
|
287
|
+
.tag{color:var(--frontier);font-size:12px}
|
|
201
288
|
.kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
|
|
202
289
|
code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
|
|
203
290
|
`;
|
|
@@ -224,10 +311,14 @@ function renderDashboardHtml(state, options = {}) {
|
|
|
224
311
|
const refreshMs = options.refreshMs ?? 2000;
|
|
225
312
|
const body = [
|
|
226
313
|
summaryPanel(state.cost),
|
|
314
|
+
totalsPanel(state.totals),
|
|
315
|
+
sessionsPanel(state.sessions),
|
|
227
316
|
loopPanel(state.loop),
|
|
228
317
|
teamPanel(state.team),
|
|
229
318
|
tierPanel(state.cost),
|
|
230
319
|
routesPanel(state.recentRoutes),
|
|
320
|
+
decisionsPanel(state.decisions),
|
|
321
|
+
meetingsPanel(state.meetings),
|
|
231
322
|
activityPanel(state.activity)
|
|
232
323
|
].join("");
|
|
233
324
|
const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
|
|
@@ -307,21 +398,602 @@ function renderReport(report) {
|
|
|
307
398
|
if (report.count === 0) {
|
|
308
399
|
return "openteam report: sin registros de telemetría todavía.";
|
|
309
400
|
}
|
|
310
|
-
const lines = [
|
|
311
|
-
"openteam report:",
|
|
312
|
-
` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
|
|
313
|
-
` coste real: ${usd2(report.totalEstimatedUSD)}`,
|
|
314
|
-
` baseline: ${usd2(report.totalBaselineUSD)}`,
|
|
315
|
-
` ahorro: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
|
|
316
|
-
` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
|
|
317
|
-
" por tier:"
|
|
318
|
-
];
|
|
319
|
-
for (const tier of TIERS2) {
|
|
320
|
-
const summary = report.byTier[tier];
|
|
321
|
-
lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd2(summary.estimatedUSD)} · ahorro ${usd2(summary.savingsUSD)}`);
|
|
401
|
+
const lines = [
|
|
402
|
+
"openteam report:",
|
|
403
|
+
` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
|
|
404
|
+
` coste real: ${usd2(report.totalEstimatedUSD)}`,
|
|
405
|
+
` baseline: ${usd2(report.totalBaselineUSD)}`,
|
|
406
|
+
` ahorro: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
|
|
407
|
+
` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
|
|
408
|
+
" por tier:"
|
|
409
|
+
];
|
|
410
|
+
for (const tier of TIERS2) {
|
|
411
|
+
const summary = report.byTier[tier];
|
|
412
|
+
lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd2(summary.estimatedUSD)} · ahorro ${usd2(summary.savingsUSD)}`);
|
|
413
|
+
}
|
|
414
|
+
return lines.join(`
|
|
415
|
+
`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/telemetry/aggregate.ts
|
|
419
|
+
function emptyToolcallStats() {
|
|
420
|
+
return { count: 0, ok: 0, failed: 0, totalDurationMs: 0, byTool: {} };
|
|
421
|
+
}
|
|
422
|
+
function emptySession(sessionID, ts) {
|
|
423
|
+
return {
|
|
424
|
+
sessionID,
|
|
425
|
+
firstTs: ts,
|
|
426
|
+
lastTs: ts,
|
|
427
|
+
routeCount: 0,
|
|
428
|
+
localCount: 0,
|
|
429
|
+
frontierCount: 0,
|
|
430
|
+
estimatedCostUSD: 0,
|
|
431
|
+
estimatedSavingsUSD: 0,
|
|
432
|
+
realCostUSD: 0,
|
|
433
|
+
messageCount: 0,
|
|
434
|
+
tokensIn: 0,
|
|
435
|
+
tokensOut: 0,
|
|
436
|
+
tokensReasoning: 0,
|
|
437
|
+
tokensCacheRead: 0,
|
|
438
|
+
tokensCacheWrite: 0,
|
|
439
|
+
toolcalls: emptyToolcallStats(),
|
|
440
|
+
models: [],
|
|
441
|
+
agents: []
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function pushUnique(list, value) {
|
|
445
|
+
if (value !== undefined && !list.includes(value)) {
|
|
446
|
+
list.push(value);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function applyRoute(session, event) {
|
|
450
|
+
session.routeCount += 1;
|
|
451
|
+
if (event.routeKind === "local") {
|
|
452
|
+
session.localCount += 1;
|
|
453
|
+
} else {
|
|
454
|
+
session.frontierCount += 1;
|
|
455
|
+
}
|
|
456
|
+
session.estimatedCostUSD += event.estimatedCostUSD;
|
|
457
|
+
session.estimatedSavingsUSD += event.estimatedSavingsUSD;
|
|
458
|
+
pushUnique(session.models, `${event.selected.providerID}/${event.selected.modelID}`);
|
|
459
|
+
pushUnique(session.agents, event.agent);
|
|
460
|
+
}
|
|
461
|
+
function applyMessage(session, event) {
|
|
462
|
+
session.messageCount += 1;
|
|
463
|
+
session.realCostUSD += event.costUSD;
|
|
464
|
+
session.tokensIn += event.tokensIn;
|
|
465
|
+
session.tokensOut += event.tokensOut;
|
|
466
|
+
session.tokensReasoning += event.tokensReasoning;
|
|
467
|
+
session.tokensCacheRead += event.tokensCacheRead;
|
|
468
|
+
session.tokensCacheWrite += event.tokensCacheWrite;
|
|
469
|
+
pushUnique(session.models, `${event.providerID}/${event.modelID}`);
|
|
470
|
+
pushUnique(session.agents, event.agent);
|
|
471
|
+
}
|
|
472
|
+
function applyToolUsage(stats, event) {
|
|
473
|
+
stats.count += 1;
|
|
474
|
+
stats.totalDurationMs += event.durationMs;
|
|
475
|
+
if (event.ok) {
|
|
476
|
+
stats.ok += 1;
|
|
477
|
+
} else {
|
|
478
|
+
stats.failed += 1;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function applyToolcall(session, event) {
|
|
482
|
+
applyToolUsage(session.toolcalls, event);
|
|
483
|
+
const byTool = session.toolcalls.byTool[event.tool] ?? {
|
|
484
|
+
count: 0,
|
|
485
|
+
ok: 0,
|
|
486
|
+
failed: 0,
|
|
487
|
+
totalDurationMs: 0
|
|
488
|
+
};
|
|
489
|
+
applyToolUsage(byTool, event);
|
|
490
|
+
session.toolcalls.byTool[event.tool] = byTool;
|
|
491
|
+
pushUnique(session.agents, event.agent);
|
|
492
|
+
}
|
|
493
|
+
function mergeTool(target, source) {
|
|
494
|
+
target.count += source.count;
|
|
495
|
+
target.ok += source.ok;
|
|
496
|
+
target.failed += source.failed;
|
|
497
|
+
target.totalDurationMs += source.totalDurationMs;
|
|
498
|
+
}
|
|
499
|
+
function accumulateTotals(totals, session) {
|
|
500
|
+
totals.sessions += 1;
|
|
501
|
+
totals.routeCount += session.routeCount;
|
|
502
|
+
totals.localCount += session.localCount;
|
|
503
|
+
totals.frontierCount += session.frontierCount;
|
|
504
|
+
totals.estimatedCostUSD += session.estimatedCostUSD;
|
|
505
|
+
totals.estimatedSavingsUSD += session.estimatedSavingsUSD;
|
|
506
|
+
totals.realCostUSD += session.realCostUSD;
|
|
507
|
+
totals.messageCount += session.messageCount;
|
|
508
|
+
totals.tokensIn += session.tokensIn;
|
|
509
|
+
totals.tokensOut += session.tokensOut;
|
|
510
|
+
totals.tokensReasoning += session.tokensReasoning;
|
|
511
|
+
totals.tokensCacheRead += session.tokensCacheRead;
|
|
512
|
+
totals.tokensCacheWrite += session.tokensCacheWrite;
|
|
513
|
+
mergeTool(totals.toolcalls, session.toolcalls);
|
|
514
|
+
for (const [tool, usage] of Object.entries(session.toolcalls.byTool)) {
|
|
515
|
+
const target = totals.toolcalls.byTool[tool] ?? {
|
|
516
|
+
count: 0,
|
|
517
|
+
ok: 0,
|
|
518
|
+
failed: 0,
|
|
519
|
+
totalDurationMs: 0
|
|
520
|
+
};
|
|
521
|
+
mergeTool(target, usage);
|
|
522
|
+
totals.toolcalls.byTool[tool] = target;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function toDecision(event) {
|
|
526
|
+
const view = {
|
|
527
|
+
ts: event.ts,
|
|
528
|
+
sessionID: event.sessionID,
|
|
529
|
+
summary: event.summary
|
|
530
|
+
};
|
|
531
|
+
if (event.agent !== undefined) {
|
|
532
|
+
view.agent = event.agent;
|
|
533
|
+
}
|
|
534
|
+
if (event.tags !== undefined) {
|
|
535
|
+
view.tags = [...event.tags];
|
|
536
|
+
}
|
|
537
|
+
return view;
|
|
538
|
+
}
|
|
539
|
+
function toMeeting(event) {
|
|
540
|
+
const view = {
|
|
541
|
+
ts: event.ts,
|
|
542
|
+
sessionID: event.sessionID,
|
|
543
|
+
batchID: event.batchID,
|
|
544
|
+
roles: [...event.roles]
|
|
545
|
+
};
|
|
546
|
+
if (event.purpose !== undefined) {
|
|
547
|
+
view.purpose = event.purpose;
|
|
548
|
+
}
|
|
549
|
+
if (event.decisionIDs !== undefined) {
|
|
550
|
+
view.decisionIDs = [...event.decisionIDs];
|
|
551
|
+
}
|
|
552
|
+
return view;
|
|
553
|
+
}
|
|
554
|
+
function toActivity(event) {
|
|
555
|
+
const view = {
|
|
556
|
+
ts: event.ts,
|
|
557
|
+
sessionID: event.sessionID,
|
|
558
|
+
kind: event.kind,
|
|
559
|
+
summary: event.summary
|
|
560
|
+
};
|
|
561
|
+
if (event.agent !== undefined) {
|
|
562
|
+
view.agent = event.agent;
|
|
563
|
+
}
|
|
564
|
+
return view;
|
|
565
|
+
}
|
|
566
|
+
function byTsDesc(list) {
|
|
567
|
+
return [...list].sort((a, b) => b.ts - a.ts);
|
|
568
|
+
}
|
|
569
|
+
function aggregateEvents(events) {
|
|
570
|
+
const sessions = new Map;
|
|
571
|
+
const decisions = [];
|
|
572
|
+
const meetings = [];
|
|
573
|
+
const activity = [];
|
|
574
|
+
const sessionFor = (sessionID, ts) => {
|
|
575
|
+
const existing = sessions.get(sessionID);
|
|
576
|
+
if (existing === undefined) {
|
|
577
|
+
const created = emptySession(sessionID, ts);
|
|
578
|
+
sessions.set(sessionID, created);
|
|
579
|
+
return created;
|
|
580
|
+
}
|
|
581
|
+
existing.firstTs = Math.min(existing.firstTs, ts);
|
|
582
|
+
existing.lastTs = Math.max(existing.lastTs, ts);
|
|
583
|
+
return existing;
|
|
584
|
+
};
|
|
585
|
+
for (const event of events) {
|
|
586
|
+
switch (event.type) {
|
|
587
|
+
case "route":
|
|
588
|
+
applyRoute(sessionFor(event.sessionID, event.ts), event);
|
|
589
|
+
break;
|
|
590
|
+
case "message":
|
|
591
|
+
applyMessage(sessionFor(event.sessionID, event.ts), event);
|
|
592
|
+
break;
|
|
593
|
+
case "toolcall":
|
|
594
|
+
applyToolcall(sessionFor(event.sessionID, event.ts), event);
|
|
595
|
+
break;
|
|
596
|
+
case "decision":
|
|
597
|
+
decisions.push(toDecision(event));
|
|
598
|
+
break;
|
|
599
|
+
case "meeting":
|
|
600
|
+
meetings.push(toMeeting(event));
|
|
601
|
+
break;
|
|
602
|
+
case "activity":
|
|
603
|
+
activity.push(toActivity(event));
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const totals = {
|
|
608
|
+
sessions: 0,
|
|
609
|
+
routeCount: 0,
|
|
610
|
+
localCount: 0,
|
|
611
|
+
frontierCount: 0,
|
|
612
|
+
estimatedCostUSD: 0,
|
|
613
|
+
estimatedSavingsUSD: 0,
|
|
614
|
+
realCostUSD: 0,
|
|
615
|
+
messageCount: 0,
|
|
616
|
+
tokensIn: 0,
|
|
617
|
+
tokensOut: 0,
|
|
618
|
+
tokensReasoning: 0,
|
|
619
|
+
tokensCacheRead: 0,
|
|
620
|
+
tokensCacheWrite: 0,
|
|
621
|
+
toolcalls: emptyToolcallStats()
|
|
622
|
+
};
|
|
623
|
+
const summaries = [...sessions.values()].sort((a, b) => b.lastTs - a.lastTs);
|
|
624
|
+
for (const summary of summaries) {
|
|
625
|
+
summary.models.sort();
|
|
626
|
+
summary.agents.sort();
|
|
627
|
+
accumulateTotals(totals, summary);
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
sessions: summaries,
|
|
631
|
+
totals,
|
|
632
|
+
decisions: byTsDesc(decisions),
|
|
633
|
+
meetings: byTsDesc(meetings),
|
|
634
|
+
activity: byTsDesc(activity)
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/telemetry/events.ts
|
|
639
|
+
import { z as z3 } from "zod";
|
|
640
|
+
|
|
641
|
+
// src/capabilities/types.ts
|
|
642
|
+
import { z } from "zod";
|
|
643
|
+
var CapabilityTierSchema = z.union([
|
|
644
|
+
z.literal(0),
|
|
645
|
+
z.literal(1),
|
|
646
|
+
z.literal(2),
|
|
647
|
+
z.literal(3),
|
|
648
|
+
z.literal(4),
|
|
649
|
+
z.literal(5)
|
|
650
|
+
]);
|
|
651
|
+
var ComplexityTierSchema = z.enum([
|
|
652
|
+
"trivial",
|
|
653
|
+
"simple",
|
|
654
|
+
"moderate",
|
|
655
|
+
"hard"
|
|
656
|
+
]);
|
|
657
|
+
var ModelCapabilityProfileSchema = z.object({
|
|
658
|
+
ref: z.object({
|
|
659
|
+
providerID: z.string().min(1),
|
|
660
|
+
modelID: z.string().min(1)
|
|
661
|
+
}),
|
|
662
|
+
kind: z.enum(["local", "frontier", "router"]),
|
|
663
|
+
contextWindow: z.number().int().positive(),
|
|
664
|
+
maxOutputTokens: z.number().int().positive(),
|
|
665
|
+
supportsToolCalling: z.boolean(),
|
|
666
|
+
supportsVision: z.boolean(),
|
|
667
|
+
reasoningTier: CapabilityTierSchema,
|
|
668
|
+
codeQualityTier: CapabilityTierSchema,
|
|
669
|
+
costPer1M: z.object({
|
|
670
|
+
inputUSD: z.number().min(0),
|
|
671
|
+
outputUSD: z.number().min(0)
|
|
672
|
+
}),
|
|
673
|
+
availability: z.enum(["available", "degraded", "unavailable"])
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// src/config/schema.ts
|
|
677
|
+
import { z as z2 } from "zod";
|
|
678
|
+
var ModelRefSchema = z2.object({
|
|
679
|
+
providerID: z2.string().min(1),
|
|
680
|
+
modelID: z2.string().min(1)
|
|
681
|
+
});
|
|
682
|
+
var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
|
|
683
|
+
var PrivacyModeSchema = z2.enum([
|
|
684
|
+
"forceLocalOnSensitive",
|
|
685
|
+
"consentBeforeFrontier",
|
|
686
|
+
"off"
|
|
687
|
+
]);
|
|
688
|
+
var BaselineModeSchema = z2.enum(["auto", "pinned"]);
|
|
689
|
+
var DashboardHostSchema = z2.enum(["127.0.0.1", "localhost"]);
|
|
690
|
+
var DashboardConfigSchema = z2.object({
|
|
691
|
+
enabled: z2.boolean().default(false),
|
|
692
|
+
host: DashboardHostSchema.default("127.0.0.1"),
|
|
693
|
+
port: z2.number().int().min(1024).max(65535).default(4599),
|
|
694
|
+
autoPortFallback: z2.boolean().default(true),
|
|
695
|
+
refreshMs: z2.number().int().min(250).default(2000),
|
|
696
|
+
recentRoutes: z2.number().int().positive().default(50),
|
|
697
|
+
openBrowser: z2.boolean().default(false)
|
|
698
|
+
}).default({
|
|
699
|
+
enabled: false,
|
|
700
|
+
host: "127.0.0.1",
|
|
701
|
+
port: 4599,
|
|
702
|
+
autoPortFallback: true,
|
|
703
|
+
refreshMs: 2000,
|
|
704
|
+
recentRoutes: 50,
|
|
705
|
+
openBrowser: false
|
|
706
|
+
});
|
|
707
|
+
var defaultLocalModel = {
|
|
708
|
+
providerID: "ollama",
|
|
709
|
+
modelID: "qwen3:8b"
|
|
710
|
+
};
|
|
711
|
+
var defaultFrontierModel = {
|
|
712
|
+
providerID: "anthropic",
|
|
713
|
+
modelID: "claude-sonnet-4-5"
|
|
714
|
+
};
|
|
715
|
+
var LocalRuntimeSchema = z2.object({
|
|
716
|
+
id: z2.enum(["ollama", "lmstudio", "foundry-local"]),
|
|
717
|
+
enabled: z2.boolean().default(true),
|
|
718
|
+
baseURL: z2.string().url().optional(),
|
|
719
|
+
discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
|
|
720
|
+
defaultModel: ModelRefSchema
|
|
721
|
+
});
|
|
722
|
+
var OpenTeamConfigSchema = z2.object({
|
|
723
|
+
baseline: z2.object({
|
|
724
|
+
mode: BaselineModeSchema.default("auto"),
|
|
725
|
+
pinnedModel: ModelRefSchema.nullable().default(null),
|
|
726
|
+
hardDefault: ModelRefSchema.default(defaultFrontierModel)
|
|
727
|
+
}).default({
|
|
728
|
+
mode: "auto",
|
|
729
|
+
pinnedModel: null,
|
|
730
|
+
hardDefault: defaultFrontierModel
|
|
731
|
+
}),
|
|
732
|
+
router: z2.object({
|
|
733
|
+
mode: RouterModeSchema.default("balanced"),
|
|
734
|
+
localDefault: ModelRefSchema.default(defaultLocalModel),
|
|
735
|
+
trivialPromptMaxChars: z2.number().int().positive().default(280),
|
|
736
|
+
frontierPromptMinChars: z2.number().int().positive().default(2000)
|
|
737
|
+
}).default({
|
|
738
|
+
mode: "balanced",
|
|
739
|
+
localDefault: defaultLocalModel,
|
|
740
|
+
trivialPromptMaxChars: 280,
|
|
741
|
+
frontierPromptMinChars: 2000
|
|
742
|
+
}),
|
|
743
|
+
local: z2.object({
|
|
744
|
+
runtimes: z2.array(LocalRuntimeSchema).min(1).default([
|
|
745
|
+
{
|
|
746
|
+
id: "ollama",
|
|
747
|
+
enabled: true,
|
|
748
|
+
baseURL: "http://localhost:11434/v1",
|
|
749
|
+
defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
|
|
750
|
+
}
|
|
751
|
+
])
|
|
752
|
+
}).default({
|
|
753
|
+
runtimes: [
|
|
754
|
+
{
|
|
755
|
+
id: "ollama",
|
|
756
|
+
enabled: true,
|
|
757
|
+
baseURL: "http://localhost:11434/v1",
|
|
758
|
+
defaultModel: defaultLocalModel
|
|
759
|
+
}
|
|
760
|
+
]
|
|
761
|
+
}),
|
|
762
|
+
budgets: z2.object({
|
|
763
|
+
sessionUSD: z2.number().positive().optional(),
|
|
764
|
+
monthlyUSD: z2.number().positive().optional(),
|
|
765
|
+
frontierTokensPerSession: z2.number().int().positive().optional(),
|
|
766
|
+
hardStopOnBudgetExhaustion: z2.boolean().default(false)
|
|
767
|
+
}).default({ hardStopOnBudgetExhaustion: false }),
|
|
768
|
+
privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
|
|
769
|
+
dashboard: DashboardConfigSchema
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// src/telemetry/events.ts
|
|
773
|
+
var EVENT_SCHEMA_VERSION = 1;
|
|
774
|
+
var EventBaseSchema = z3.object({
|
|
775
|
+
v: z3.literal(EVENT_SCHEMA_VERSION),
|
|
776
|
+
ts: z3.number().finite(),
|
|
777
|
+
sessionID: z3.string().min(1)
|
|
778
|
+
});
|
|
779
|
+
var RouteEventSchema = EventBaseSchema.extend({
|
|
780
|
+
type: z3.literal("route"),
|
|
781
|
+
promptHash: z3.string().min(1),
|
|
782
|
+
promptChars: z3.number().int().min(0),
|
|
783
|
+
tier: ComplexityTierSchema,
|
|
784
|
+
routeKind: z3.enum(["local", "frontier"]),
|
|
785
|
+
selected: ModelRefSchema,
|
|
786
|
+
rationale: z3.string(),
|
|
787
|
+
estimatedCostUSD: z3.number().finite().min(0),
|
|
788
|
+
baselineCostUSD: z3.number().finite().min(0),
|
|
789
|
+
estimatedSavingsUSD: z3.number().finite(),
|
|
790
|
+
budgetAction: z3.string().min(1),
|
|
791
|
+
tokensIn: z3.number().int().min(0).optional(),
|
|
792
|
+
tokensOut: z3.number().int().min(0).optional(),
|
|
793
|
+
decisionID: z3.string().min(1).optional(),
|
|
794
|
+
agent: z3.string().min(1).optional(),
|
|
795
|
+
success: z3.boolean().optional(),
|
|
796
|
+
batchID: z3.string().min(1).optional(),
|
|
797
|
+
failureReason: z3.string().min(1).optional(),
|
|
798
|
+
failureStage: z3.string().min(1).optional()
|
|
799
|
+
});
|
|
800
|
+
var MessageEventSchema = EventBaseSchema.extend({
|
|
801
|
+
type: z3.literal("message"),
|
|
802
|
+
providerID: z3.string().min(1),
|
|
803
|
+
modelID: z3.string().min(1),
|
|
804
|
+
agent: z3.string().min(1).optional(),
|
|
805
|
+
mode: z3.string().min(1).optional(),
|
|
806
|
+
messageID: z3.string().min(1).optional(),
|
|
807
|
+
costUSD: z3.number().finite().min(0),
|
|
808
|
+
tokensIn: z3.number().int().min(0),
|
|
809
|
+
tokensOut: z3.number().int().min(0),
|
|
810
|
+
tokensReasoning: z3.number().int().min(0),
|
|
811
|
+
tokensCacheRead: z3.number().int().min(0),
|
|
812
|
+
tokensCacheWrite: z3.number().int().min(0),
|
|
813
|
+
durationMs: z3.number().int().min(0)
|
|
814
|
+
});
|
|
815
|
+
var ToolcallEventSchema = EventBaseSchema.extend({
|
|
816
|
+
type: z3.literal("toolcall"),
|
|
817
|
+
tool: z3.string().min(1),
|
|
818
|
+
callID: z3.string().min(1),
|
|
819
|
+
agent: z3.string().min(1).optional(),
|
|
820
|
+
durationMs: z3.number().int().min(0),
|
|
821
|
+
ok: z3.boolean(),
|
|
822
|
+
title: z3.string().optional()
|
|
823
|
+
});
|
|
824
|
+
var MeetingEventSchema = EventBaseSchema.extend({
|
|
825
|
+
type: z3.literal("meeting"),
|
|
826
|
+
batchID: z3.string().min(1),
|
|
827
|
+
purpose: z3.string().optional(),
|
|
828
|
+
roles: z3.array(z3.string().min(1)),
|
|
829
|
+
decisionIDs: z3.array(z3.string().min(1)).optional()
|
|
830
|
+
});
|
|
831
|
+
var DecisionEventSchema = EventBaseSchema.extend({
|
|
832
|
+
type: z3.literal("decision"),
|
|
833
|
+
agent: z3.string().min(1).optional(),
|
|
834
|
+
summary: z3.string().min(1),
|
|
835
|
+
tags: z3.array(z3.string().min(1)).optional()
|
|
836
|
+
});
|
|
837
|
+
var ActivityEventSchema = EventBaseSchema.extend({
|
|
838
|
+
type: z3.literal("activity"),
|
|
839
|
+
kind: z3.enum(["route", "agent", "decision", "commit"]),
|
|
840
|
+
agent: z3.string().min(1).optional(),
|
|
841
|
+
summary: z3.string().min(1)
|
|
842
|
+
});
|
|
843
|
+
var OpenTeamEventSchema = z3.discriminatedUnion("type", [
|
|
844
|
+
RouteEventSchema,
|
|
845
|
+
MessageEventSchema,
|
|
846
|
+
ToolcallEventSchema,
|
|
847
|
+
MeetingEventSchema,
|
|
848
|
+
DecisionEventSchema,
|
|
849
|
+
ActivityEventSchema
|
|
850
|
+
]);
|
|
851
|
+
|
|
852
|
+
// src/telemetry/types.ts
|
|
853
|
+
import { z as z4 } from "zod";
|
|
854
|
+
var CostRecordSchema = z4.object({
|
|
855
|
+
ts: z4.number().finite(),
|
|
856
|
+
sessionID: z4.string().min(1).optional(),
|
|
857
|
+
promptHash: z4.string().min(1),
|
|
858
|
+
promptChars: z4.number().int().min(0),
|
|
859
|
+
tier: ComplexityTierSchema,
|
|
860
|
+
routeKind: z4.enum(["local", "frontier"]),
|
|
861
|
+
selected: ModelRefSchema,
|
|
862
|
+
rationale: z4.string(),
|
|
863
|
+
estimatedCostUSD: z4.number().finite().min(0),
|
|
864
|
+
baselineCostUSD: z4.number().finite().min(0),
|
|
865
|
+
estimatedSavingsUSD: z4.number().finite(),
|
|
866
|
+
budgetAction: z4.string().min(1),
|
|
867
|
+
tokensIn: z4.number().int().min(0).optional(),
|
|
868
|
+
tokensOut: z4.number().int().min(0).optional()
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// src/telemetry/read.ts
|
|
872
|
+
var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
|
|
873
|
+
function parseCostRecordsJsonl(text) {
|
|
874
|
+
const records = [];
|
|
875
|
+
for (const line of text.split(`
|
|
876
|
+
`)) {
|
|
877
|
+
const trimmed = line.trim();
|
|
878
|
+
if (trimmed.length === 0) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
let candidate;
|
|
882
|
+
try {
|
|
883
|
+
candidate = JSON.parse(trimmed);
|
|
884
|
+
} catch {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const parsed = CostRecordSchema.safeParse(candidate);
|
|
888
|
+
if (parsed.success) {
|
|
889
|
+
records.push(parsed.data);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return records;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/telemetry/eventLog.ts
|
|
896
|
+
var DEFAULT_SESSIONS_DIR = ".opencode/openteam/sessions";
|
|
897
|
+
var LEGACY_SESSION_ID = "legacy";
|
|
898
|
+
function defaultJoin(dir, file) {
|
|
899
|
+
return dir.endsWith("/") ? `${dir}${file}` : `${dir}/${file}`;
|
|
900
|
+
}
|
|
901
|
+
function parseEventsJsonl(text) {
|
|
902
|
+
const events = [];
|
|
903
|
+
for (const line of text.split(`
|
|
904
|
+
`)) {
|
|
905
|
+
const trimmed = line.trim();
|
|
906
|
+
if (trimmed.length === 0) {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
let candidate;
|
|
910
|
+
try {
|
|
911
|
+
candidate = JSON.parse(trimmed);
|
|
912
|
+
} catch {
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const parsed = OpenTeamEventSchema.safeParse(candidate);
|
|
916
|
+
if (parsed.success) {
|
|
917
|
+
events.push(parsed.data);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return events;
|
|
921
|
+
}
|
|
922
|
+
function costRecordToRouteEvent(record) {
|
|
923
|
+
const event = {
|
|
924
|
+
v: EVENT_SCHEMA_VERSION,
|
|
925
|
+
type: "route",
|
|
926
|
+
ts: record.ts,
|
|
927
|
+
sessionID: record.sessionID ?? LEGACY_SESSION_ID,
|
|
928
|
+
promptHash: record.promptHash,
|
|
929
|
+
promptChars: record.promptChars,
|
|
930
|
+
tier: record.tier,
|
|
931
|
+
routeKind: record.routeKind,
|
|
932
|
+
selected: record.selected,
|
|
933
|
+
rationale: record.rationale,
|
|
934
|
+
estimatedCostUSD: record.estimatedCostUSD,
|
|
935
|
+
baselineCostUSD: record.baselineCostUSD,
|
|
936
|
+
estimatedSavingsUSD: record.estimatedSavingsUSD,
|
|
937
|
+
budgetAction: record.budgetAction
|
|
938
|
+
};
|
|
939
|
+
if (record.tokensIn !== undefined) {
|
|
940
|
+
event.tokensIn = record.tokensIn;
|
|
941
|
+
}
|
|
942
|
+
if (record.tokensOut !== undefined) {
|
|
943
|
+
event.tokensOut = record.tokensOut;
|
|
944
|
+
}
|
|
945
|
+
return event;
|
|
946
|
+
}
|
|
947
|
+
function routeEventToCostRecord(event) {
|
|
948
|
+
const record = {
|
|
949
|
+
ts: event.ts,
|
|
950
|
+
sessionID: event.sessionID,
|
|
951
|
+
promptHash: event.promptHash,
|
|
952
|
+
promptChars: event.promptChars,
|
|
953
|
+
tier: event.tier,
|
|
954
|
+
routeKind: event.routeKind,
|
|
955
|
+
selected: event.selected,
|
|
956
|
+
rationale: event.rationale,
|
|
957
|
+
estimatedCostUSD: event.estimatedCostUSD,
|
|
958
|
+
baselineCostUSD: event.baselineCostUSD,
|
|
959
|
+
estimatedSavingsUSD: event.estimatedSavingsUSD,
|
|
960
|
+
budgetAction: event.budgetAction
|
|
961
|
+
};
|
|
962
|
+
if (event.tokensIn !== undefined) {
|
|
963
|
+
record.tokensIn = event.tokensIn;
|
|
964
|
+
}
|
|
965
|
+
if (event.tokensOut !== undefined) {
|
|
966
|
+
record.tokensOut = event.tokensOut;
|
|
967
|
+
}
|
|
968
|
+
return record;
|
|
969
|
+
}
|
|
970
|
+
async function readRouteCostRecords(dir, deps) {
|
|
971
|
+
const events = await readSessionEvents(dir, deps);
|
|
972
|
+
const records = [];
|
|
973
|
+
for (const event of events) {
|
|
974
|
+
if (event.type === "route") {
|
|
975
|
+
records.push(routeEventToCostRecord(event));
|
|
976
|
+
}
|
|
322
977
|
}
|
|
323
|
-
return
|
|
324
|
-
|
|
978
|
+
return records;
|
|
979
|
+
}
|
|
980
|
+
async function readSessionEvents(dir, deps) {
|
|
981
|
+
const join = deps.join ?? defaultJoin;
|
|
982
|
+
const files = (await deps.listFiles(dir)).filter((file) => file.endsWith(".jsonl"));
|
|
983
|
+
const perFile = await Promise.all(files.map(async (file) => {
|
|
984
|
+
const text = await deps.readText(join(dir, file));
|
|
985
|
+
return text === undefined ? [] : parseEventsJsonl(text);
|
|
986
|
+
}));
|
|
987
|
+
const events = perFile.flat();
|
|
988
|
+
if (deps.legacyTelemetryPath !== undefined) {
|
|
989
|
+
const legacyText = await deps.readText(deps.legacyTelemetryPath);
|
|
990
|
+
if (legacyText !== undefined) {
|
|
991
|
+
for (const record of parseCostRecordsJsonl(legacyText)) {
|
|
992
|
+
events.push(costRecordToRouteEvent(record));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return events.sort((a, b) => a.ts - b.ts);
|
|
325
997
|
}
|
|
326
998
|
|
|
327
999
|
// src/dashboard/backlog.ts
|
|
@@ -375,6 +1047,8 @@ function buildLoopSnapshot(backlogPath, items) {
|
|
|
375
1047
|
// src/dashboard/state.ts
|
|
376
1048
|
var DEFAULT_RECENT_ROUTES = 50;
|
|
377
1049
|
var DEFAULT_ACTIVITY_LIMIT = 100;
|
|
1050
|
+
var DEFAULT_DECISIONS_LIMIT = 100;
|
|
1051
|
+
var DEFAULT_MEETINGS_LIMIT = 50;
|
|
378
1052
|
function toRouteView(record) {
|
|
379
1053
|
return {
|
|
380
1054
|
ts: record.ts,
|
|
@@ -387,11 +1061,49 @@ function toRouteView(record) {
|
|
|
387
1061
|
budgetAction: record.budgetAction
|
|
388
1062
|
};
|
|
389
1063
|
}
|
|
1064
|
+
function isRoute(event) {
|
|
1065
|
+
return event.type === "route";
|
|
1066
|
+
}
|
|
390
1067
|
function recentRoutes(records, limit) {
|
|
391
1068
|
return [...records].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit)).map(toRouteView);
|
|
392
1069
|
}
|
|
393
|
-
function
|
|
394
|
-
|
|
1070
|
+
function decisionToActivity(decision) {
|
|
1071
|
+
const entry = {
|
|
1072
|
+
ts: decision.ts,
|
|
1073
|
+
kind: "decision",
|
|
1074
|
+
summary: decision.summary
|
|
1075
|
+
};
|
|
1076
|
+
if (decision.agent !== undefined) {
|
|
1077
|
+
entry.agent = decision.agent;
|
|
1078
|
+
}
|
|
1079
|
+
return entry;
|
|
1080
|
+
}
|
|
1081
|
+
function meetingToActivity(meeting) {
|
|
1082
|
+
const roles = meeting.roles.join(", ");
|
|
1083
|
+
const purpose = meeting.purpose === undefined ? "" : `: ${meeting.purpose}`;
|
|
1084
|
+
return {
|
|
1085
|
+
ts: meeting.ts,
|
|
1086
|
+
kind: "agent",
|
|
1087
|
+
summary: `Reunión (${roles})${purpose}`
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function activityFeed(aggregate, limit) {
|
|
1091
|
+
const entries = [
|
|
1092
|
+
...aggregate.activity.map((view) => {
|
|
1093
|
+
const entry = {
|
|
1094
|
+
ts: view.ts,
|
|
1095
|
+
kind: view.kind,
|
|
1096
|
+
summary: view.summary
|
|
1097
|
+
};
|
|
1098
|
+
if (view.agent !== undefined) {
|
|
1099
|
+
entry.agent = view.agent;
|
|
1100
|
+
}
|
|
1101
|
+
return entry;
|
|
1102
|
+
}),
|
|
1103
|
+
...aggregate.decisions.map(decisionToActivity),
|
|
1104
|
+
...aggregate.meetings.map(meetingToActivity)
|
|
1105
|
+
];
|
|
1106
|
+
return entries.sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
|
|
395
1107
|
}
|
|
396
1108
|
function loopFrom(backlog) {
|
|
397
1109
|
if (backlog === undefined) {
|
|
@@ -406,13 +1118,21 @@ function loopFrom(backlog) {
|
|
|
406
1118
|
function buildDashboardState(inputs) {
|
|
407
1119
|
const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
|
|
408
1120
|
const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
|
|
1121
|
+
const decisionsLimit = inputs.decisionsLimit ?? DEFAULT_DECISIONS_LIMIT;
|
|
1122
|
+
const meetingsLimit = inputs.meetingsLimit ?? DEFAULT_MEETINGS_LIMIT;
|
|
1123
|
+
const aggregate = aggregateEvents(inputs.events);
|
|
1124
|
+
const costRecords = inputs.events.filter(isRoute).map(routeEventToCostRecord);
|
|
409
1125
|
const state = {
|
|
410
1126
|
generatedAt: inputs.generatedAt,
|
|
411
1127
|
session: {},
|
|
412
|
-
cost: summarizeCostRecords(
|
|
413
|
-
|
|
1128
|
+
cost: summarizeCostRecords(costRecords),
|
|
1129
|
+
totals: aggregate.totals,
|
|
1130
|
+
sessions: aggregate.sessions,
|
|
1131
|
+
recentRoutes: recentRoutes(costRecords, recentRoutesLimit),
|
|
1132
|
+
decisions: aggregate.decisions.slice(0, Math.max(0, decisionsLimit)),
|
|
1133
|
+
meetings: aggregate.meetings.slice(0, Math.max(0, meetingsLimit)),
|
|
414
1134
|
team: [...inputs.team],
|
|
415
|
-
activity:
|
|
1135
|
+
activity: activityFeed(aggregate, activityLimit)
|
|
416
1136
|
};
|
|
417
1137
|
if (inputs.session?.id !== undefined) {
|
|
418
1138
|
state.session.id = inputs.session.id;
|
|
@@ -703,216 +1423,95 @@ function renderAgentList(classifications, context) {
|
|
|
703
1423
|
`);
|
|
704
1424
|
}
|
|
705
1425
|
|
|
706
|
-
// src/telemetry/
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
]);
|
|
722
|
-
var ComplexityTierSchema = z.enum([
|
|
723
|
-
"trivial",
|
|
724
|
-
"simple",
|
|
725
|
-
"moderate",
|
|
726
|
-
"hard"
|
|
727
|
-
]);
|
|
728
|
-
var ModelCapabilityProfileSchema = z.object({
|
|
729
|
-
ref: z.object({
|
|
730
|
-
providerID: z.string().min(1),
|
|
731
|
-
modelID: z.string().min(1)
|
|
732
|
-
}),
|
|
733
|
-
kind: z.enum(["local", "frontier", "router"]),
|
|
734
|
-
contextWindow: z.number().int().positive(),
|
|
735
|
-
maxOutputTokens: z.number().int().positive(),
|
|
736
|
-
supportsToolCalling: z.boolean(),
|
|
737
|
-
supportsVision: z.boolean(),
|
|
738
|
-
reasoningTier: CapabilityTierSchema,
|
|
739
|
-
codeQualityTier: CapabilityTierSchema,
|
|
740
|
-
costPer1M: z.object({
|
|
741
|
-
inputUSD: z.number().min(0),
|
|
742
|
-
outputUSD: z.number().min(0)
|
|
743
|
-
}),
|
|
744
|
-
availability: z.enum(["available", "degraded", "unavailable"])
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
// src/config/schema.ts
|
|
748
|
-
import { z as z2 } from "zod";
|
|
749
|
-
var ModelRefSchema = z2.object({
|
|
750
|
-
providerID: z2.string().min(1),
|
|
751
|
-
modelID: z2.string().min(1)
|
|
752
|
-
});
|
|
753
|
-
var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
|
|
754
|
-
var PrivacyModeSchema = z2.enum([
|
|
755
|
-
"forceLocalOnSensitive",
|
|
756
|
-
"consentBeforeFrontier",
|
|
757
|
-
"off"
|
|
758
|
-
]);
|
|
759
|
-
var BaselineModeSchema = z2.enum(["auto", "pinned"]);
|
|
760
|
-
var DashboardHostSchema = z2.enum(["127.0.0.1", "localhost"]);
|
|
761
|
-
var DashboardConfigSchema = z2.object({
|
|
762
|
-
enabled: z2.boolean().default(false),
|
|
763
|
-
host: DashboardHostSchema.default("127.0.0.1"),
|
|
764
|
-
port: z2.number().int().min(1024).max(65535).default(4599),
|
|
765
|
-
autoPortFallback: z2.boolean().default(true),
|
|
766
|
-
refreshMs: z2.number().int().min(250).default(2000),
|
|
767
|
-
recentRoutes: z2.number().int().positive().default(50),
|
|
768
|
-
openBrowser: z2.boolean().default(false)
|
|
769
|
-
}).default({
|
|
770
|
-
enabled: false,
|
|
771
|
-
host: "127.0.0.1",
|
|
772
|
-
port: 4599,
|
|
773
|
-
autoPortFallback: true,
|
|
774
|
-
refreshMs: 2000,
|
|
775
|
-
recentRoutes: 50,
|
|
776
|
-
openBrowser: false
|
|
777
|
-
});
|
|
778
|
-
var defaultLocalModel = {
|
|
779
|
-
providerID: "ollama",
|
|
780
|
-
modelID: "qwen3:8b"
|
|
781
|
-
};
|
|
782
|
-
var defaultFrontierModel = {
|
|
783
|
-
providerID: "anthropic",
|
|
784
|
-
modelID: "claude-sonnet-4-5"
|
|
785
|
-
};
|
|
786
|
-
var LocalRuntimeSchema = z2.object({
|
|
787
|
-
id: z2.enum(["ollama", "lmstudio", "foundry-local"]),
|
|
788
|
-
enabled: z2.boolean().default(true),
|
|
789
|
-
baseURL: z2.string().url().optional(),
|
|
790
|
-
discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
|
|
791
|
-
defaultModel: ModelRefSchema
|
|
792
|
-
});
|
|
793
|
-
var OpenTeamConfigSchema = z2.object({
|
|
794
|
-
baseline: z2.object({
|
|
795
|
-
mode: BaselineModeSchema.default("auto"),
|
|
796
|
-
pinnedModel: ModelRefSchema.nullable().default(null),
|
|
797
|
-
hardDefault: ModelRefSchema.default(defaultFrontierModel)
|
|
798
|
-
}).default({
|
|
799
|
-
mode: "auto",
|
|
800
|
-
pinnedModel: null,
|
|
801
|
-
hardDefault: defaultFrontierModel
|
|
802
|
-
}),
|
|
803
|
-
router: z2.object({
|
|
804
|
-
mode: RouterModeSchema.default("balanced"),
|
|
805
|
-
localDefault: ModelRefSchema.default(defaultLocalModel),
|
|
806
|
-
trivialPromptMaxChars: z2.number().int().positive().default(280),
|
|
807
|
-
frontierPromptMinChars: z2.number().int().positive().default(2000)
|
|
808
|
-
}).default({
|
|
809
|
-
mode: "balanced",
|
|
810
|
-
localDefault: defaultLocalModel,
|
|
811
|
-
trivialPromptMaxChars: 280,
|
|
812
|
-
frontierPromptMinChars: 2000
|
|
813
|
-
}),
|
|
814
|
-
local: z2.object({
|
|
815
|
-
runtimes: z2.array(LocalRuntimeSchema).min(1).default([
|
|
816
|
-
{
|
|
817
|
-
id: "ollama",
|
|
818
|
-
enabled: true,
|
|
819
|
-
baseURL: "http://localhost:11434/v1",
|
|
820
|
-
defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
|
|
821
|
-
}
|
|
822
|
-
])
|
|
823
|
-
}).default({
|
|
824
|
-
runtimes: [
|
|
825
|
-
{
|
|
826
|
-
id: "ollama",
|
|
827
|
-
enabled: true,
|
|
828
|
-
baseURL: "http://localhost:11434/v1",
|
|
829
|
-
defaultModel: defaultLocalModel
|
|
830
|
-
}
|
|
831
|
-
]
|
|
832
|
-
}),
|
|
833
|
-
budgets: z2.object({
|
|
834
|
-
sessionUSD: z2.number().positive().optional(),
|
|
835
|
-
monthlyUSD: z2.number().positive().optional(),
|
|
836
|
-
frontierTokensPerSession: z2.number().int().positive().optional(),
|
|
837
|
-
hardStopOnBudgetExhaustion: z2.boolean().default(false)
|
|
838
|
-
}).default({ hardStopOnBudgetExhaustion: false }),
|
|
839
|
-
privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
|
|
840
|
-
dashboard: DashboardConfigSchema
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
// src/telemetry/types.ts
|
|
844
|
-
var CostRecordSchema = z3.object({
|
|
845
|
-
ts: z3.number().finite(),
|
|
846
|
-
sessionID: z3.string().min(1).optional(),
|
|
847
|
-
promptHash: z3.string().min(1),
|
|
848
|
-
promptChars: z3.number().int().min(0),
|
|
849
|
-
tier: ComplexityTierSchema,
|
|
850
|
-
routeKind: z3.enum(["local", "frontier"]),
|
|
851
|
-
selected: ModelRefSchema,
|
|
852
|
-
rationale: z3.string(),
|
|
853
|
-
estimatedCostUSD: z3.number().finite().min(0),
|
|
854
|
-
baselineCostUSD: z3.number().finite().min(0),
|
|
855
|
-
estimatedSavingsUSD: z3.number().finite(),
|
|
856
|
-
budgetAction: z3.string().min(1),
|
|
857
|
-
tokensIn: z3.number().int().min(0).optional(),
|
|
858
|
-
tokensOut: z3.number().int().min(0).optional()
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
// src/telemetry/read.ts
|
|
862
|
-
var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
|
|
863
|
-
function parseCostRecordsJsonl(text) {
|
|
864
|
-
const records = [];
|
|
1426
|
+
// src/telemetry/decisions.ts
|
|
1427
|
+
var DECISIONS_SESSION_ID = "decisions";
|
|
1428
|
+
var BULLET_RE = /^\s*[-*]\s+(.+?)\s*$/;
|
|
1429
|
+
var DATE_RE = /^(\d{4}-\d{2}-\d{2})\b\s*/;
|
|
1430
|
+
var AGENT_RE = /^\[([^\]]+)\]\s*/;
|
|
1431
|
+
var TAG_RE = /#([A-Za-z0-9][\w-]*)/g;
|
|
1432
|
+
function parseDate(value) {
|
|
1433
|
+
const parsed = Date.parse(`${value}T00:00:00Z`);
|
|
1434
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
1435
|
+
}
|
|
1436
|
+
function parseDecisionsMarkdown(text, options = {}) {
|
|
1437
|
+
const sessionID = options.sessionID ?? DECISIONS_SESSION_ID;
|
|
1438
|
+
const baseTs = options.baseTs ?? 0;
|
|
1439
|
+
const events = [];
|
|
1440
|
+
let index = 0;
|
|
865
1441
|
for (const line of text.split(`
|
|
866
1442
|
`)) {
|
|
867
|
-
const
|
|
868
|
-
if (
|
|
1443
|
+
const bullet = BULLET_RE.exec(line);
|
|
1444
|
+
if (bullet === null) {
|
|
869
1445
|
continue;
|
|
870
1446
|
}
|
|
871
|
-
let
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1447
|
+
let content = bullet[1] ?? "";
|
|
1448
|
+
let ts = baseTs + index;
|
|
1449
|
+
const dateMatch = DATE_RE.exec(content);
|
|
1450
|
+
if (dateMatch?.[1] !== undefined) {
|
|
1451
|
+
const parsed = parseDate(dateMatch[1]);
|
|
1452
|
+
if (parsed !== undefined) {
|
|
1453
|
+
ts = parsed;
|
|
1454
|
+
content = content.slice(dateMatch[0].length);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
let agent;
|
|
1458
|
+
const agentMatch = AGENT_RE.exec(content);
|
|
1459
|
+
if (agentMatch?.[1] !== undefined) {
|
|
1460
|
+
agent = agentMatch[1].trim();
|
|
1461
|
+
content = content.slice(agentMatch[0].length);
|
|
1462
|
+
}
|
|
1463
|
+
const tags = [];
|
|
1464
|
+
for (const tagMatch of content.matchAll(TAG_RE)) {
|
|
1465
|
+
const tag = tagMatch[1];
|
|
1466
|
+
if (tag !== undefined && !tags.includes(tag)) {
|
|
1467
|
+
tags.push(tag);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const summary = content.replace(TAG_RE, "").replace(/\s+/g, " ").trim();
|
|
1471
|
+
if (summary.length === 0) {
|
|
875
1472
|
continue;
|
|
876
1473
|
}
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
1474
|
+
const event = {
|
|
1475
|
+
v: EVENT_SCHEMA_VERSION,
|
|
1476
|
+
type: "decision",
|
|
1477
|
+
ts,
|
|
1478
|
+
sessionID,
|
|
1479
|
+
summary
|
|
1480
|
+
};
|
|
1481
|
+
if (agent !== undefined && agent.length > 0) {
|
|
1482
|
+
event.agent = agent;
|
|
880
1483
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
function isMissingFile(error) {
|
|
885
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
886
|
-
}
|
|
887
|
-
async function readCostRecords(path, deps = {}) {
|
|
888
|
-
const readFileFn = deps.readFile ?? readFile;
|
|
889
|
-
try {
|
|
890
|
-
const content = await readFileFn(path, "utf8");
|
|
891
|
-
return parseCostRecordsJsonl(content);
|
|
892
|
-
} catch (error) {
|
|
893
|
-
if (isMissingFile(error)) {
|
|
894
|
-
return [];
|
|
1484
|
+
if (tags.length > 0) {
|
|
1485
|
+
event.tags = tags;
|
|
895
1486
|
}
|
|
896
|
-
|
|
1487
|
+
events.push(event);
|
|
1488
|
+
index += 1;
|
|
897
1489
|
}
|
|
1490
|
+
return events;
|
|
898
1491
|
}
|
|
899
1492
|
|
|
900
1493
|
// src/web/snapshot.ts
|
|
901
|
-
function createSnapshotReader(deps, paths, context) {
|
|
1494
|
+
function createSnapshotReader(deps, paths, context = {}) {
|
|
1495
|
+
const readEventsDeps = {
|
|
1496
|
+
listFiles: deps.listFiles,
|
|
1497
|
+
readText: deps.readText,
|
|
1498
|
+
...paths.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: paths.legacyTelemetryPath } : {}
|
|
1499
|
+
};
|
|
902
1500
|
return async () => {
|
|
903
|
-
const [
|
|
904
|
-
|
|
1501
|
+
const [sessionEvents, decisionsText, backlogText, agentFiles, lastCommit] = await Promise.all([
|
|
1502
|
+
readSessionEvents(paths.sessionsDir, readEventsDeps),
|
|
1503
|
+
deps.readText(paths.decisionsPath),
|
|
905
1504
|
deps.readText(paths.backlogPath),
|
|
906
1505
|
deps.listAgentFiles(paths.agentDir),
|
|
907
1506
|
deps.gitLastCommit?.() ?? Promise.resolve(undefined)
|
|
908
1507
|
]);
|
|
909
|
-
const
|
|
1508
|
+
const decisionEvents = parseDecisionsMarkdown(decisionsText ?? "");
|
|
1509
|
+
const events = [...sessionEvents, ...decisionEvents];
|
|
910
1510
|
const team = agentFiles.map(parseAgentFile);
|
|
911
1511
|
const inputs = {
|
|
912
1512
|
generatedAt: deps.now(),
|
|
913
|
-
|
|
914
|
-
team
|
|
915
|
-
activity: context.activity()
|
|
1513
|
+
events,
|
|
1514
|
+
team
|
|
916
1515
|
};
|
|
917
1516
|
if (backlogText !== undefined) {
|
|
918
1517
|
const backlog = {
|
|
@@ -936,7 +1535,7 @@ function createSnapshotReader(deps, paths, context) {
|
|
|
936
1535
|
|
|
937
1536
|
// src/web/watch.ts
|
|
938
1537
|
function watchSources(paths, onChange, deps, debounceMs = 250) {
|
|
939
|
-
const setTimer = deps.setTimer ?? ((cb,
|
|
1538
|
+
const setTimer = deps.setTimer ?? ((cb, ms2) => setTimeout(cb, ms2));
|
|
940
1539
|
const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
941
1540
|
const watchers = [];
|
|
942
1541
|
let pending;
|
|
@@ -973,18 +1572,20 @@ function startDashboard(deps) {
|
|
|
973
1572
|
}
|
|
974
1573
|
async function createDashboardRuntime(deps) {
|
|
975
1574
|
const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
|
|
976
|
-
const
|
|
1575
|
+
const decisionsPath = deps.decisionsPath ?? DEFAULT_DECISIONS_PATH;
|
|
977
1576
|
const readSnapshot = createSnapshotReader({
|
|
978
1577
|
readText: deps.readText,
|
|
1578
|
+
listFiles: deps.listFiles,
|
|
979
1579
|
listAgentFiles: deps.listAgentFiles,
|
|
980
1580
|
now: deps.now,
|
|
981
1581
|
...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {}
|
|
982
1582
|
}, {
|
|
983
|
-
|
|
1583
|
+
sessionsDir: deps.sessionsDir,
|
|
1584
|
+
decisionsPath,
|
|
984
1585
|
backlogPath,
|
|
985
|
-
agentDir: deps.agentDir
|
|
1586
|
+
agentDir: deps.agentDir,
|
|
1587
|
+
...deps.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: deps.legacyTelemetryPath } : {}
|
|
986
1588
|
}, {
|
|
987
|
-
activity: () => activity.list(),
|
|
988
1589
|
recentRoutesLimit: deps.config.recentRoutes,
|
|
989
1590
|
...deps.session !== undefined ? { session: deps.session } : {}
|
|
990
1591
|
});
|
|
@@ -995,16 +1596,12 @@ async function createDashboardRuntime(deps) {
|
|
|
995
1596
|
...deps.log !== undefined ? { log: deps.log } : {}
|
|
996
1597
|
});
|
|
997
1598
|
const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
|
|
998
|
-
const watcher = watchSources([deps.
|
|
1599
|
+
const watcher = watchSources([deps.sessionsDir, decisionsPath, backlogPath, deps.agentDir], () => {
|
|
999
1600
|
server.notify();
|
|
1000
1601
|
}, { watch: watchFn });
|
|
1001
1602
|
return {
|
|
1002
1603
|
url: server.url,
|
|
1003
1604
|
port: server.port,
|
|
1004
|
-
pushActivity(entry) {
|
|
1005
|
-
activity.push(entry);
|
|
1006
|
-
server.notify();
|
|
1007
|
-
},
|
|
1008
1605
|
close() {
|
|
1009
1606
|
watcher.close();
|
|
1010
1607
|
server.close();
|
|
@@ -1015,6 +1612,7 @@ async function createDashboardRuntime(deps) {
|
|
|
1015
1612
|
// src/cli/dashboardServe.ts
|
|
1016
1613
|
function parseDashboardServeArgs(rest) {
|
|
1017
1614
|
return {
|
|
1615
|
+
status: rest.includes("--status"),
|
|
1018
1616
|
serve: rest.includes("--serve"),
|
|
1019
1617
|
open: rest.includes("--open")
|
|
1020
1618
|
};
|
|
@@ -1026,10 +1624,13 @@ async function runDashboardServe(deps, options) {
|
|
|
1026
1624
|
try {
|
|
1027
1625
|
runtime = await start({
|
|
1028
1626
|
config: { ...config.dashboard, enabled: true },
|
|
1029
|
-
|
|
1627
|
+
sessionsDir: deps.sessionsDir,
|
|
1628
|
+
decisionsPath: deps.decisionsPath,
|
|
1030
1629
|
backlogPath: deps.backlogPath,
|
|
1031
1630
|
agentDir: deps.agentDir,
|
|
1631
|
+
...deps.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: deps.legacyTelemetryPath } : {},
|
|
1032
1632
|
readText: deps.readText,
|
|
1633
|
+
listFiles: deps.listFiles,
|
|
1033
1634
|
listAgentFiles: deps.listAgentFiles,
|
|
1034
1635
|
now: deps.now,
|
|
1035
1636
|
...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {},
|
|
@@ -1868,29 +2469,19 @@ function autoBaseline(config) {
|
|
|
1868
2469
|
// src/commands/dashboard.ts
|
|
1869
2470
|
function renderDashboardStatus(dashboard) {
|
|
1870
2471
|
const url = `http://${dashboard.host}:${dashboard.port}`;
|
|
1871
|
-
if (!dashboard.enabled) {
|
|
1872
|
-
return [
|
|
1873
|
-
"Dashboard: deshabilitado.",
|
|
1874
|
-
"",
|
|
1875
|
-
'Para activarlo dentro de opencode, pon "dashboard": { "enabled": true }',
|
|
1876
|
-
"en .opencode/openteam.json y recarga opencode. Se servirá (loopback) en:",
|
|
1877
|
-
` ${url}`,
|
|
1878
|
-
"",
|
|
1879
|
-
"O véelo ahora mismo sin editar la config:",
|
|
1880
|
-
" openteam dashboard --serve (añade --open para abrir el navegador)"
|
|
1881
|
-
].join(`
|
|
1882
|
-
`);
|
|
1883
|
-
}
|
|
1884
2472
|
return [
|
|
1885
|
-
"Dashboard:
|
|
2473
|
+
"Dashboard (multi-sesión, se lanza desde la CLI):",
|
|
1886
2474
|
` URL: ${url}`,
|
|
1887
2475
|
` Refresco: ${dashboard.refreshMs} ms (SSE + polling)`,
|
|
1888
2476
|
` Rutas: últimas ${dashboard.recentRoutes}`,
|
|
1889
2477
|
dashboard.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
|
|
1890
2478
|
"",
|
|
1891
|
-
"
|
|
1892
|
-
"
|
|
1893
|
-
"
|
|
2479
|
+
"Lánzalo con:",
|
|
2480
|
+
" openteam dashboard (Ctrl+C para parar; --open abre el navegador)",
|
|
2481
|
+
"",
|
|
2482
|
+
"Agrega TODAS las sesiones de opencode que escriben eventos en",
|
|
2483
|
+
" .opencode/openteam/sessions/*.jsonl",
|
|
2484
|
+
"Solo escucha en loopback y nunca expone prompts (solo hashes)."
|
|
1894
2485
|
].join(`
|
|
1895
2486
|
`);
|
|
1896
2487
|
}
|
|
@@ -2027,7 +2618,12 @@ function buildOrchestratorAgent(frontier, options = {}) {
|
|
|
2027
2618
|
"- **scribe** — memoria silenciosa del equipo. Registra decisiones y",
|
|
2028
2619
|
" aprendizajes en un log compartido (`.opencode/openteam-decisions.md`) sin",
|
|
2029
2620
|
" ejecutar cambios de código. Modelo **local**; permisos de solo lectura más",
|
|
2030
|
-
" edición de ese log.",
|
|
2621
|
+
" edición de ese log. **Formato parseable** para el dashboard: una decisión",
|
|
2622
|
+
" por línea como item de lista Markdown, ya **redactada** (sin prompts ni",
|
|
2623
|
+
" datos sensibles):",
|
|
2624
|
+
" `- YYYY-MM-DD [agente] Resumen breve de la decisión #tag1 #tag2`",
|
|
2625
|
+
" La fecha, el `[agente]` y los `#tags` son opcionales; el resumen es",
|
|
2626
|
+
" obligatorio.",
|
|
2031
2627
|
"- **ralph** — automatización y triage. Monitoriza el trabajo pendiente",
|
|
2032
2628
|
" (issues/tareas), lo prioriza, coordina la ejecución delegando en los",
|
|
2033
2629
|
" especialistas y **escala al humano** ante bloqueos, riesgos o aprobaciones.",
|
|
@@ -2157,8 +2753,8 @@ var HELP = [
|
|
|
2157
2753
|
" openteam baseline auto Baseline cheapest-capable (modo auto)",
|
|
2158
2754
|
" openteam doctor Diagnóstico de runtimes y config",
|
|
2159
2755
|
" openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
|
|
2160
|
-
" openteam dashboard
|
|
2161
|
-
" openteam dashboard --
|
|
2756
|
+
" openteam dashboard Lanza el dashboard web multi-sesión (Ctrl+C para parar; --open abre el navegador)",
|
|
2757
|
+
" openteam dashboard --status Muestra la config del dashboard sin lanzarlo",
|
|
2162
2758
|
" openteam report Resumen de coste/ahorro (telemetría)",
|
|
2163
2759
|
" openteam yolo status Muestra si el modo YOLO está activo",
|
|
2164
2760
|
" openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
|
|
@@ -2387,13 +2983,21 @@ async function writeOpenTeamConfigFile(config, path = DEFAULT_CONFIG_PATH, deps
|
|
|
2387
2983
|
}
|
|
2388
2984
|
|
|
2389
2985
|
// src/commands/slashCommand.ts
|
|
2390
|
-
var
|
|
2391
|
-
|
|
2986
|
+
var OPENTEAM_COMMAND_DIR = ".opencode/command";
|
|
2987
|
+
var OPENTEAM_COMMAND_PATH = `${OPENTEAM_COMMAND_DIR}/openteam.md`;
|
|
2988
|
+
function renderCommand(description, body) {
|
|
2989
|
+
return ["---", `description: ${description}`, "---", "", body, ""].join(`
|
|
2990
|
+
`);
|
|
2991
|
+
}
|
|
2992
|
+
function fixedActionBody(action) {
|
|
2392
2993
|
return [
|
|
2393
|
-
"
|
|
2394
|
-
"
|
|
2395
|
-
|
|
2396
|
-
|
|
2994
|
+
`Llama a la herramienta \`openteam\` una sola vez con \`action: "${action}"\`.`,
|
|
2995
|
+
"Devuelve su salida tal cual, sin reinterpretarla."
|
|
2996
|
+
].join(`
|
|
2997
|
+
`);
|
|
2998
|
+
}
|
|
2999
|
+
function buildOpenteamCommand() {
|
|
3000
|
+
return renderCommand("Comandos runtime de openteam (baseline show|set|auto, doctor, agents, dashboard, report)", [
|
|
2397
3001
|
"Usa la herramienta `openteam` para ejecutar el comando indicado por el usuario: $ARGUMENTS",
|
|
2398
3002
|
"",
|
|
2399
3003
|
"Interpreta los argumentos así y llama a la herramienta `openteam` una sola vez:",
|
|
@@ -2406,10 +3010,36 @@ function buildOpenteamCommand() {
|
|
|
2406
3010
|
'- `dashboard` → `action: "dashboard"`',
|
|
2407
3011
|
'- `report` → `action: "report"`',
|
|
2408
3012
|
"",
|
|
2409
|
-
"Devuelve la salida de la herramienta tal cual, sin reinterpretarla."
|
|
2410
|
-
""
|
|
3013
|
+
"Devuelve la salida de la herramienta tal cual, sin reinterpretarla."
|
|
2411
3014
|
].join(`
|
|
2412
|
-
`);
|
|
3015
|
+
`));
|
|
3016
|
+
}
|
|
3017
|
+
function buildBaselineCommand() {
|
|
3018
|
+
return renderCommand("openteam · baseline frontier cheapest-capable (show | set <provider/model> | auto)", [
|
|
3019
|
+
"Usa la herramienta `openteam` para el baseline frontier: $ARGUMENTS",
|
|
3020
|
+
"",
|
|
3021
|
+
'- vacío o `show` → `action: "show"`',
|
|
3022
|
+
'- `set <provider/model>` → `action: "set"`, `model: "<provider/model>"`',
|
|
3023
|
+
'- `auto` → `action: "auto"`',
|
|
3024
|
+
"",
|
|
3025
|
+
"Llama a la herramienta una sola vez y devuelve su salida tal cual."
|
|
3026
|
+
].join(`
|
|
3027
|
+
`));
|
|
3028
|
+
}
|
|
3029
|
+
function buildOpenteamCommands() {
|
|
3030
|
+
const file = (name, contents) => ({
|
|
3031
|
+
name,
|
|
3032
|
+
path: `${OPENTEAM_COMMAND_DIR}/${name}.md`,
|
|
3033
|
+
contents
|
|
3034
|
+
});
|
|
3035
|
+
return [
|
|
3036
|
+
file("openteam", buildOpenteamCommand()),
|
|
3037
|
+
file("openteam-baseline", buildBaselineCommand()),
|
|
3038
|
+
file("openteam-doctor", renderCommand("openteam · diagnóstico de runtimes locales y configuración", fixedActionBody("doctor"))),
|
|
3039
|
+
file("openteam-agents", renderCommand("openteam · lista los agentes y el LLM (local/frontier + suscripción) de cada uno", fixedActionBody("agents"))),
|
|
3040
|
+
file("openteam-dashboard", renderCommand("openteam · estado y URL del dashboard web multi-sesión", fixedActionBody("dashboard"))),
|
|
3041
|
+
file("openteam-report", renderCommand("openteam · resumen de coste/ahorro (telemetría)", fixedActionBody("report")))
|
|
3042
|
+
];
|
|
2413
3043
|
}
|
|
2414
3044
|
|
|
2415
3045
|
// src/commands/setup.ts
|
|
@@ -2759,11 +3389,10 @@ async function runSetup(deps) {
|
|
|
2759
3389
|
const openTeamConfig = buildOpenTeamConfig(answers);
|
|
2760
3390
|
const opencodeConfig = buildOpencodeConfig(answers);
|
|
2761
3391
|
const orchestratorAgent = buildOrchestratorAgent(frontier, { yolo });
|
|
2762
|
-
const
|
|
3392
|
+
const slashCommands = buildOpenteamCommands();
|
|
2763
3393
|
const openTeamPath = join(deps.cwd, DEFAULT_CONFIG_PATH);
|
|
2764
3394
|
const opencodePath = join(deps.cwd, OPENCODE_CONFIG_PATH);
|
|
2765
3395
|
const agentPath = join(deps.cwd, ORCHESTRATOR_AGENT_PATH);
|
|
2766
|
-
const commandPath = join(deps.cwd, OPENTEAM_COMMAND_PATH);
|
|
2767
3396
|
const existing = [];
|
|
2768
3397
|
if (await deps.fileExists(openTeamPath)) {
|
|
2769
3398
|
existing.push(DEFAULT_CONFIG_PATH);
|
|
@@ -2774,8 +3403,10 @@ async function runSetup(deps) {
|
|
|
2774
3403
|
if (await deps.fileExists(agentPath)) {
|
|
2775
3404
|
existing.push(ORCHESTRATOR_AGENT_PATH);
|
|
2776
3405
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
3406
|
+
for (const command of slashCommands) {
|
|
3407
|
+
if (await deps.fileExists(join(deps.cwd, command.path))) {
|
|
3408
|
+
existing.push(command.path);
|
|
3409
|
+
}
|
|
2779
3410
|
}
|
|
2780
3411
|
if (existing.length > 0) {
|
|
2781
3412
|
const overwrite = await prompt.confirm({
|
|
@@ -2790,25 +3421,27 @@ async function runSetup(deps) {
|
|
|
2790
3421
|
await deps.writeFile(openTeamPath, serializeOpenTeamConfig(openTeamConfig));
|
|
2791
3422
|
await deps.writeFile(opencodePath, serializeOpencodeConfig(opencodeConfig));
|
|
2792
3423
|
await deps.writeFile(agentPath, orchestratorAgent);
|
|
2793
|
-
|
|
3424
|
+
for (const command of slashCommands) {
|
|
3425
|
+
await deps.writeFile(join(deps.cwd, command.path), command.contents);
|
|
3426
|
+
}
|
|
2794
3427
|
const enabledSummary = runtimes.filter((runtime) => runtime.enabled).map((runtime) => `${runtime.id} (${runtime.defaultModelID})`).join(", ");
|
|
2795
3428
|
prompt.note([
|
|
2796
3429
|
`Baseline frontier: ${frontier.providerID}/${frontier.modelID}`,
|
|
2797
3430
|
`Runtimes locales: ${enabledSummary || "ninguno habilitado"}`,
|
|
2798
3431
|
`Modo YOLO: ${yolo ? "activado (auto-aprueba permisos)" : "desactivado"}`,
|
|
2799
3432
|
`Dashboard web: ${dashboard ? "activado (http://127.0.0.1:4599 mientras opencode esté abierto)" : "desactivado"}`,
|
|
2800
|
-
`Escrito: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${
|
|
3433
|
+
`Escrito: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${OPENTEAM_COMMAND_DIR}/*.md (${slashCommands.length} comandos)`,
|
|
2801
3434
|
"",
|
|
2802
3435
|
"Siguientes pasos:",
|
|
2803
3436
|
` 1. Autentica el provider frontier: opencode auth login`,
|
|
2804
3437
|
reachableIds.length === 0 ? " 2. Arranca tu runtime local (Ollama/LM Studio/Foundry) antes de usarlo" : " 2. Abre opencode en este repo; openteam enruta local-first automáticamente",
|
|
2805
|
-
" 3. Pulsa Tab y elige el agente 'openteam', o
|
|
3438
|
+
" 3. Pulsa Tab y elige el agente 'openteam', o teclea / y elige un comando /openteam…"
|
|
2806
3439
|
].join(`
|
|
2807
3440
|
`), "openteam configurado");
|
|
2808
3441
|
prompt.outro("Listo. Abre opencode y selecciona el agente 'openteam'.");
|
|
2809
3442
|
return {
|
|
2810
3443
|
exitCode: 0,
|
|
2811
|
-
stdout: `openteam setup completado: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${
|
|
3444
|
+
stdout: `openteam setup completado: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${OPENTEAM_COMMAND_DIR}/*.md`
|
|
2812
3445
|
};
|
|
2813
3446
|
} catch (error) {
|
|
2814
3447
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2843,7 +3476,7 @@ function createGitLastCommit(exec) {
|
|
|
2843
3476
|
// src/cli.ts
|
|
2844
3477
|
var execFileAsync = promisify(execFile);
|
|
2845
3478
|
var AGENT_DIR = dirname2(ORCHESTRATOR_AGENT_PATH);
|
|
2846
|
-
function
|
|
3479
|
+
function isMissingFile(error) {
|
|
2847
3480
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2848
3481
|
}
|
|
2849
3482
|
var nodeExec = async (command, args) => {
|
|
@@ -2861,10 +3494,10 @@ var nodeExec = async (command, args) => {
|
|
|
2861
3494
|
};
|
|
2862
3495
|
async function loadConfig(path) {
|
|
2863
3496
|
try {
|
|
2864
|
-
const content = await
|
|
3497
|
+
const content = await readFile(path, "utf8");
|
|
2865
3498
|
return loadOpenTeamConfig(JSON.parse(content));
|
|
2866
3499
|
} catch (error) {
|
|
2867
|
-
if (
|
|
3500
|
+
if (isMissingFile(error)) {
|
|
2868
3501
|
return loadOpenTeamConfig({});
|
|
2869
3502
|
}
|
|
2870
3503
|
throw error;
|
|
@@ -2872,14 +3505,24 @@ async function loadConfig(path) {
|
|
|
2872
3505
|
}
|
|
2873
3506
|
async function readTextOptional(path) {
|
|
2874
3507
|
try {
|
|
2875
|
-
return await
|
|
3508
|
+
return await readFile(path, "utf8");
|
|
2876
3509
|
} catch (error) {
|
|
2877
|
-
if (
|
|
3510
|
+
if (isMissingFile(error)) {
|
|
2878
3511
|
return;
|
|
2879
3512
|
}
|
|
2880
3513
|
throw error;
|
|
2881
3514
|
}
|
|
2882
3515
|
}
|
|
3516
|
+
async function listDirFiles(dir) {
|
|
3517
|
+
try {
|
|
3518
|
+
return await readdir(dir);
|
|
3519
|
+
} catch (error) {
|
|
3520
|
+
if (isMissingFile(error)) {
|
|
3521
|
+
return [];
|
|
3522
|
+
}
|
|
3523
|
+
throw error;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
2883
3526
|
async function openBrowser(url) {
|
|
2884
3527
|
if (process.platform === "win32") {
|
|
2885
3528
|
await nodeExec("cmd", ["/c", "start", "", url]);
|
|
@@ -2901,10 +3544,10 @@ var deps = {
|
|
|
2901
3544
|
saveConfig: (config, path) => writeOpenTeamConfigFile(config, path),
|
|
2902
3545
|
readOpencodeConfig: async (path) => {
|
|
2903
3546
|
try {
|
|
2904
|
-
const content = await
|
|
3547
|
+
const content = await readFile(path, "utf8");
|
|
2905
3548
|
return JSON.parse(content);
|
|
2906
3549
|
} catch (error) {
|
|
2907
|
-
if (
|
|
3550
|
+
if (isMissingFile(error)) {
|
|
2908
3551
|
return;
|
|
2909
3552
|
}
|
|
2910
3553
|
throw error;
|
|
@@ -2924,7 +3567,7 @@ var deps = {
|
|
|
2924
3567
|
try {
|
|
2925
3568
|
entries = await readdir(dir);
|
|
2926
3569
|
} catch (error) {
|
|
2927
|
-
if (
|
|
3570
|
+
if (isMissingFile(error)) {
|
|
2928
3571
|
return [];
|
|
2929
3572
|
}
|
|
2930
3573
|
throw error;
|
|
@@ -2932,7 +3575,7 @@ var deps = {
|
|
|
2932
3575
|
const mdFiles = entries.filter((entry) => entry.endsWith(".md"));
|
|
2933
3576
|
return Promise.all(mdFiles.map(async (entry) => ({
|
|
2934
3577
|
name: entry.replace(/\.md$/, ""),
|
|
2935
|
-
contents: await
|
|
3578
|
+
contents: await readFile(join2(dir, entry), "utf8")
|
|
2936
3579
|
})));
|
|
2937
3580
|
},
|
|
2938
3581
|
probe: (config) => {
|
|
@@ -2943,7 +3586,11 @@ var deps = {
|
|
|
2943
3586
|
});
|
|
2944
3587
|
return registry.probe(config.local.runtimes);
|
|
2945
3588
|
},
|
|
2946
|
-
readTelemetry: (path) =>
|
|
3589
|
+
readTelemetry: (path) => readRouteCostRecords(DEFAULT_SESSIONS_DIR, {
|
|
3590
|
+
listFiles: listDirFiles,
|
|
3591
|
+
readText: readTextOptional,
|
|
3592
|
+
legacyTelemetryPath: path
|
|
3593
|
+
}),
|
|
2947
3594
|
configPath: DEFAULT_CONFIG_PATH,
|
|
2948
3595
|
telemetryPath: DEFAULT_TELEMETRY_PATH,
|
|
2949
3596
|
opencodeConfigPath: OPENCODE_CONFIG_PATH,
|
|
@@ -2983,14 +3630,17 @@ async function main() {
|
|
|
2983
3630
|
}
|
|
2984
3631
|
if (argv[0] === "dashboard") {
|
|
2985
3632
|
const flags = parseDashboardServeArgs(argv.slice(1));
|
|
2986
|
-
if (flags.
|
|
3633
|
+
if (!flags.status) {
|
|
2987
3634
|
const result2 = await runDashboardServe({
|
|
2988
3635
|
loadConfig,
|
|
2989
3636
|
configPath: DEFAULT_CONFIG_PATH,
|
|
2990
|
-
|
|
3637
|
+
sessionsDir: DEFAULT_SESSIONS_DIR,
|
|
3638
|
+
decisionsPath: DEFAULT_DECISIONS_PATH,
|
|
2991
3639
|
backlogPath: DEFAULT_BACKLOG_PATH,
|
|
2992
3640
|
agentDir: AGENT_DIR,
|
|
3641
|
+
legacyTelemetryPath: DEFAULT_TELEMETRY_PATH,
|
|
2993
3642
|
readText: readTextOptional,
|
|
3643
|
+
listFiles: listDirFiles,
|
|
2994
3644
|
listAgentFiles: deps.listAgentFiles,
|
|
2995
3645
|
now: () => new Date().toISOString(),
|
|
2996
3646
|
gitLastCommit: createGitLastCommit(nodeExec),
|