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