@jmanuelcorral/openteam 0.1.20 → 0.1.21
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 +14 -0
- package/dist/cli/dashboardServe.d.ts +45 -0
- package/dist/cli/dashboardServe.d.ts.map +1 -0
- package/dist/cli.js +1183 -479
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dispatch.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -80
- 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/start.d.ts +2 -2
- package/dist/web/start.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,1054 @@ import { access, mkdir as mkdir2, readdir, readFile as readFile2, writeFile as w
|
|
|
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/activity.ts
|
|
13
|
+
function createActivityBuffer(max = 100) {
|
|
14
|
+
const entries = [];
|
|
15
|
+
return {
|
|
16
|
+
push(entry) {
|
|
17
|
+
entries.push(entry);
|
|
18
|
+
if (entries.length > max) {
|
|
19
|
+
entries.splice(0, entries.length - max);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
list() {
|
|
23
|
+
return [...entries];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/web/paths.ts
|
|
29
|
+
var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
|
|
30
|
+
|
|
31
|
+
// src/web/server.ts
|
|
32
|
+
import {
|
|
33
|
+
createServer as createHttpServer
|
|
34
|
+
} from "node:http";
|
|
35
|
+
|
|
36
|
+
// src/dashboard/render.ts
|
|
37
|
+
var TIERS = [
|
|
38
|
+
"trivial",
|
|
39
|
+
"simple",
|
|
40
|
+
"moderate",
|
|
41
|
+
"hard"
|
|
42
|
+
];
|
|
43
|
+
function escapeHtml(value) {
|
|
44
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
45
|
+
}
|
|
46
|
+
function usd(value) {
|
|
47
|
+
return `$${value.toFixed(5)}`;
|
|
48
|
+
}
|
|
49
|
+
function pct(value) {
|
|
50
|
+
return `${value.toFixed(2)}%`;
|
|
51
|
+
}
|
|
52
|
+
function agentModel(agent) {
|
|
53
|
+
return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
|
|
54
|
+
}
|
|
55
|
+
function summaryPanel(cost) {
|
|
56
|
+
return [
|
|
57
|
+
'<section class="panel" id="panel-summary">',
|
|
58
|
+
"<h2>Resumen de routing</h2>",
|
|
59
|
+
'<div class="grid">',
|
|
60
|
+
`<div class="stat"><span class="k">Decisiones</span><span class="v">${cost.count}</span></div>`,
|
|
61
|
+
`<div class="stat"><span class="k">Local</span><span class="v">${cost.localCount}</span></div>`,
|
|
62
|
+
`<div class="stat"><span class="k">Frontier</span><span class="v">${cost.frontierCount}</span></div>`,
|
|
63
|
+
`<div class="stat"><span class="k">Coste real</span><span class="v">${usd(cost.totalEstimatedUSD)}</span></div>`,
|
|
64
|
+
`<div class="stat"><span class="k">Baseline</span><span class="v">${usd(cost.totalBaselineUSD)}</span></div>`,
|
|
65
|
+
`<div class="stat"><span class="k">Ahorro</span><span class="v good">${usd(cost.totalSavingsUSD)} (${pct(cost.savingsPct)})</span></div>`,
|
|
66
|
+
`<div class="stat"><span class="k">Tokens in</span><span class="v">${cost.tokensIn}</span></div>`,
|
|
67
|
+
`<div class="stat"><span class="k">Tokens out</span><span class="v">${cost.tokensOut}</span></div>`,
|
|
68
|
+
"</div>",
|
|
69
|
+
"</section>"
|
|
70
|
+
].join("");
|
|
71
|
+
}
|
|
72
|
+
function tierPanel(cost) {
|
|
73
|
+
const rows = TIERS.map((tier) => {
|
|
74
|
+
const summary = cost.byTier[tier];
|
|
75
|
+
return `<tr><td>${tier}</td><td>${summary.count}</td><td>${usd(summary.estimatedUSD)}</td><td class="good">${usd(summary.savingsUSD)}</td></tr>`;
|
|
76
|
+
}).join("");
|
|
77
|
+
return [
|
|
78
|
+
'<section class="panel" id="panel-tier">',
|
|
79
|
+
"<h2>Por tier</h2>",
|
|
80
|
+
"<table><thead><tr><th>Tier</th><th>Nº</th><th>Coste</th><th>Ahorro</th></tr></thead>",
|
|
81
|
+
`<tbody>${rows}</tbody></table>`,
|
|
82
|
+
"</section>"
|
|
83
|
+
].join("");
|
|
84
|
+
}
|
|
85
|
+
function loopItemRow(item) {
|
|
86
|
+
const box = item.done ? "☑" : "☐";
|
|
87
|
+
const cls = item.done ? "done" : "open";
|
|
88
|
+
const who = item.assignee === undefined ? "" : `<span class="who">@${escapeHtml(item.assignee)}</span> `;
|
|
89
|
+
return `<li class="${cls}"><span class="box">${box}</span> ${who}${escapeHtml(item.text)}</li>`;
|
|
90
|
+
}
|
|
91
|
+
function loopPanel(loop) {
|
|
92
|
+
if (loop === undefined) {
|
|
93
|
+
return [
|
|
94
|
+
'<section class="panel" id="panel-loop">',
|
|
95
|
+
"<h2>Loop</h2>",
|
|
96
|
+
'<p class="muted">Sin backlog activo (no hay <code>openteam-backlog.md</code>).</p>',
|
|
97
|
+
"</section>"
|
|
98
|
+
].join("");
|
|
99
|
+
}
|
|
100
|
+
const items = loop.items.map(loopItemRow).join("");
|
|
101
|
+
const commit = loop.lastCommit === undefined ? "" : `<p class="muted">Último commit: <code>${escapeHtml(loop.lastCommit.hash)}</code> ${escapeHtml(loop.lastCommit.subject)}</p>`;
|
|
102
|
+
return [
|
|
103
|
+
'<section class="panel" id="panel-loop">',
|
|
104
|
+
"<h2>Loop</h2>",
|
|
105
|
+
`<div class="progress" role="progressbar" aria-valuenow="${loop.progressPct}" aria-valuemin="0" aria-valuemax="100"><div class="bar" style="width:${loop.progressPct}%"></div></div>`,
|
|
106
|
+
`<p class="muted">${loop.done}/${loop.total} completados · ${loop.open} abiertos · ${loop.progressPct}%</p>`,
|
|
107
|
+
`<ul class="items">${items}</ul>`,
|
|
108
|
+
commit,
|
|
109
|
+
"</section>"
|
|
110
|
+
].join("");
|
|
111
|
+
}
|
|
112
|
+
function teamPanel(team) {
|
|
113
|
+
if (team.length === 0) {
|
|
114
|
+
return [
|
|
115
|
+
'<section class="panel" id="panel-team">',
|
|
116
|
+
"<h2>Equipo</h2>",
|
|
117
|
+
'<p class="muted">Sin agentes en <code>.opencode/agent/</code>.</p>',
|
|
118
|
+
"</section>"
|
|
119
|
+
].join("");
|
|
120
|
+
}
|
|
121
|
+
const rows = team.map((agent) => `<tr><td>${escapeHtml(agent.name)}</td><td>${escapeHtml(agent.mode)}</td><td>${escapeHtml(agentModel(agent))}</td></tr>`).join("");
|
|
122
|
+
return [
|
|
123
|
+
'<section class="panel" id="panel-team">',
|
|
124
|
+
"<h2>Equipo</h2>",
|
|
125
|
+
"<table><thead><tr><th>Agente</th><th>Modo</th><th>LLM</th></tr></thead>",
|
|
126
|
+
`<tbody>${rows}</tbody></table>`,
|
|
127
|
+
"</section>"
|
|
128
|
+
].join("");
|
|
129
|
+
}
|
|
130
|
+
function routeRow(route) {
|
|
131
|
+
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>`;
|
|
132
|
+
}
|
|
133
|
+
function routesPanel(routes) {
|
|
134
|
+
if (routes.length === 0) {
|
|
135
|
+
return [
|
|
136
|
+
'<section class="panel" id="panel-routes">',
|
|
137
|
+
"<h2>Decisiones recientes</h2>",
|
|
138
|
+
'<p class="muted">Sin decisiones de routing todavía.</p>',
|
|
139
|
+
"</section>"
|
|
140
|
+
].join("");
|
|
141
|
+
}
|
|
142
|
+
const rows = routes.map(routeRow).join("");
|
|
143
|
+
return [
|
|
144
|
+
'<section class="panel" id="panel-routes">',
|
|
145
|
+
"<h2>Decisiones recientes</h2>",
|
|
146
|
+
"<table><thead><tr><th>Tier</th><th>Ruta</th><th>Modelo</th><th>Prompt (hash)</th><th>Coste</th><th>Ahorro</th></tr></thead>",
|
|
147
|
+
`<tbody>${rows}</tbody></table>`,
|
|
148
|
+
"</section>"
|
|
149
|
+
].join("");
|
|
150
|
+
}
|
|
151
|
+
function activityRow(entry) {
|
|
152
|
+
const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
|
|
153
|
+
return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
|
|
154
|
+
}
|
|
155
|
+
function activityPanel(activity) {
|
|
156
|
+
if (activity.length === 0) {
|
|
157
|
+
return [
|
|
158
|
+
'<section class="panel" id="panel-activity">',
|
|
159
|
+
"<h2>Actividad</h2>",
|
|
160
|
+
'<p class="muted">Sin actividad registrada.</p>',
|
|
161
|
+
"</section>"
|
|
162
|
+
].join("");
|
|
163
|
+
}
|
|
164
|
+
const rows = activity.map(activityRow).join("");
|
|
165
|
+
return [
|
|
166
|
+
'<section class="panel" id="panel-activity">',
|
|
167
|
+
"<h2>Actividad</h2>",
|
|
168
|
+
`<ul class="timeline">${rows}</ul>`,
|
|
169
|
+
"</section>"
|
|
170
|
+
].join("");
|
|
171
|
+
}
|
|
172
|
+
var STYLE = `
|
|
173
|
+
:root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
|
|
174
|
+
*{box-sizing:border-box}
|
|
175
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}
|
|
176
|
+
header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}
|
|
177
|
+
header h1{font-size:18px;margin:0}
|
|
178
|
+
header .meta{color:var(--muted);font-size:12px}
|
|
179
|
+
main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;padding:24px}
|
|
180
|
+
.panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
|
|
181
|
+
.panel h2{font-size:14px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
|
|
182
|
+
.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
|
|
183
|
+
.stat{display:flex;flex-direction:column;gap:2px}
|
|
184
|
+
.stat .k{color:var(--muted);font-size:12px}
|
|
185
|
+
.stat .v{font-size:18px;font-weight:600}
|
|
186
|
+
.good{color:var(--good)}
|
|
187
|
+
.local{color:var(--local)}
|
|
188
|
+
.frontier{color:var(--frontier)}
|
|
189
|
+
.muted{color:var(--muted)}
|
|
190
|
+
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
191
|
+
th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)}
|
|
192
|
+
th{color:var(--muted);font-weight:600}
|
|
193
|
+
.hash{font-family:ui-monospace,Consolas,monospace;color:var(--muted)}
|
|
194
|
+
.progress{height:10px;background:#0b0d11;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
|
|
195
|
+
.progress .bar{height:100%;background:var(--good)}
|
|
196
|
+
ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}
|
|
197
|
+
ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
|
|
198
|
+
ul.items li.done{color:var(--muted)}
|
|
199
|
+
ul.items .box{font-family:monospace}
|
|
200
|
+
.who{color:var(--local)}
|
|
201
|
+
.kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
|
|
202
|
+
code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
|
|
203
|
+
`;
|
|
204
|
+
var CLIENT_JS = `
|
|
205
|
+
(function(){
|
|
206
|
+
function reloadIfChanged(prev){
|
|
207
|
+
fetch('/api/state').then(function(r){return r.json()}).then(function(s){
|
|
208
|
+
if(s.generatedAt!==prev){location.reload()}
|
|
209
|
+
}).catch(function(){});
|
|
210
|
+
}
|
|
211
|
+
var current=document.documentElement.getAttribute('data-generated')||'';
|
|
212
|
+
if('EventSource' in window){
|
|
213
|
+
try{
|
|
214
|
+
var es=new EventSource('/events');
|
|
215
|
+
es.addEventListener('state',function(){location.reload()});
|
|
216
|
+
es.onerror=function(){/* fallback below */};
|
|
217
|
+
}catch(e){/* ignore */}
|
|
218
|
+
}
|
|
219
|
+
var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
|
|
220
|
+
setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
|
|
221
|
+
})();
|
|
222
|
+
`;
|
|
223
|
+
function renderDashboardHtml(state, options = {}) {
|
|
224
|
+
const refreshMs = options.refreshMs ?? 2000;
|
|
225
|
+
const body = [
|
|
226
|
+
summaryPanel(state.cost),
|
|
227
|
+
loopPanel(state.loop),
|
|
228
|
+
teamPanel(state.team),
|
|
229
|
+
tierPanel(state.cost),
|
|
230
|
+
routesPanel(state.recentRoutes),
|
|
231
|
+
activityPanel(state.activity)
|
|
232
|
+
].join("");
|
|
233
|
+
const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
|
|
234
|
+
return [
|
|
235
|
+
"<!doctype html>",
|
|
236
|
+
`<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}">`,
|
|
237
|
+
"<head>",
|
|
238
|
+
'<meta charset="utf-8">',
|
|
239
|
+
'<meta name="viewport" content="width=device-width,initial-scale=1">',
|
|
240
|
+
"<title>openteam dashboard</title>",
|
|
241
|
+
`<style>${STYLE}</style>`,
|
|
242
|
+
"</head>",
|
|
243
|
+
"<body>",
|
|
244
|
+
"<header>",
|
|
245
|
+
"<h1>openteam dashboard</h1>",
|
|
246
|
+
`<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
|
|
247
|
+
"</header>",
|
|
248
|
+
`<main>${body}</main>`,
|
|
249
|
+
`<script>${CLIENT_JS}</script>`,
|
|
250
|
+
"</body>",
|
|
251
|
+
"</html>"
|
|
252
|
+
].join("");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/commands/report.ts
|
|
256
|
+
var TIERS2 = [
|
|
257
|
+
"trivial",
|
|
258
|
+
"simple",
|
|
259
|
+
"moderate",
|
|
260
|
+
"hard"
|
|
261
|
+
];
|
|
262
|
+
function emptyTierSummary() {
|
|
263
|
+
return {
|
|
264
|
+
trivial: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
|
|
265
|
+
simple: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
|
|
266
|
+
moderate: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
|
|
267
|
+
hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function summarizeCostRecords(records) {
|
|
271
|
+
const report = {
|
|
272
|
+
count: 0,
|
|
273
|
+
localCount: 0,
|
|
274
|
+
frontierCount: 0,
|
|
275
|
+
totalEstimatedUSD: 0,
|
|
276
|
+
totalBaselineUSD: 0,
|
|
277
|
+
totalSavingsUSD: 0,
|
|
278
|
+
savingsPct: 0,
|
|
279
|
+
tokensIn: 0,
|
|
280
|
+
tokensOut: 0,
|
|
281
|
+
byTier: emptyTierSummary()
|
|
282
|
+
};
|
|
283
|
+
for (const record of records) {
|
|
284
|
+
report.count += 1;
|
|
285
|
+
if (record.routeKind === "local") {
|
|
286
|
+
report.localCount += 1;
|
|
287
|
+
} else {
|
|
288
|
+
report.frontierCount += 1;
|
|
289
|
+
}
|
|
290
|
+
report.totalEstimatedUSD += record.estimatedCostUSD;
|
|
291
|
+
report.totalBaselineUSD += record.baselineCostUSD;
|
|
292
|
+
report.totalSavingsUSD += record.estimatedSavingsUSD;
|
|
293
|
+
report.tokensIn += record.tokensIn ?? 0;
|
|
294
|
+
report.tokensOut += record.tokensOut ?? 0;
|
|
295
|
+
const tier = report.byTier[record.tier];
|
|
296
|
+
tier.count += 1;
|
|
297
|
+
tier.estimatedUSD += record.estimatedCostUSD;
|
|
298
|
+
tier.savingsUSD += record.estimatedSavingsUSD;
|
|
299
|
+
}
|
|
300
|
+
report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
|
|
301
|
+
return report;
|
|
302
|
+
}
|
|
303
|
+
function usd2(value) {
|
|
304
|
+
return `$${value.toFixed(5)}`;
|
|
305
|
+
}
|
|
306
|
+
function renderReport(report) {
|
|
307
|
+
if (report.count === 0) {
|
|
308
|
+
return "openteam report: sin registros de telemetría todavía.";
|
|
309
|
+
}
|
|
310
|
+
const lines = [
|
|
311
|
+
"openteam report:",
|
|
312
|
+
` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
|
|
313
|
+
` coste real: ${usd2(report.totalEstimatedUSD)}`,
|
|
314
|
+
` baseline: ${usd2(report.totalBaselineUSD)}`,
|
|
315
|
+
` ahorro: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
|
|
316
|
+
` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
|
|
317
|
+
" por tier:"
|
|
318
|
+
];
|
|
319
|
+
for (const tier of TIERS2) {
|
|
320
|
+
const summary = report.byTier[tier];
|
|
321
|
+
lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd2(summary.estimatedUSD)} · ahorro ${usd2(summary.savingsUSD)}`);
|
|
322
|
+
}
|
|
323
|
+
return lines.join(`
|
|
324
|
+
`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/dashboard/backlog.ts
|
|
328
|
+
var ITEM_RE = /^\s*[-*+]\s+\[( |x|X)\]\s+(.*)$/;
|
|
329
|
+
var ASSIGNEE_RE = /^\[@([^\]]+)\]\s*(.*)$/;
|
|
330
|
+
function parseAssignee(text) {
|
|
331
|
+
const match = ASSIGNEE_RE.exec(text);
|
|
332
|
+
if (match === null) {
|
|
333
|
+
return { text: text.trim() };
|
|
334
|
+
}
|
|
335
|
+
const assignee = (match[1] ?? "").trim();
|
|
336
|
+
const rest = (match[2] ?? "").trim();
|
|
337
|
+
if (assignee.length === 0) {
|
|
338
|
+
return { text: text.trim() };
|
|
339
|
+
}
|
|
340
|
+
return { assignee, text: rest };
|
|
341
|
+
}
|
|
342
|
+
function parseBacklog(text) {
|
|
343
|
+
const items = [];
|
|
344
|
+
for (const line of text.split(/\r?\n/)) {
|
|
345
|
+
const match = ITEM_RE.exec(line);
|
|
346
|
+
if (match === null) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const done = (match[1] ?? " ").toLowerCase() === "x";
|
|
350
|
+
const rawText = (match[2] ?? "").trim();
|
|
351
|
+
const { assignee, text: itemText } = parseAssignee(rawText);
|
|
352
|
+
const item = { text: itemText, done };
|
|
353
|
+
if (assignee !== undefined) {
|
|
354
|
+
item.assignee = assignee;
|
|
355
|
+
}
|
|
356
|
+
items.push(item);
|
|
357
|
+
}
|
|
358
|
+
return items;
|
|
359
|
+
}
|
|
360
|
+
function buildLoopSnapshot(backlogPath, items) {
|
|
361
|
+
const total = items.length;
|
|
362
|
+
const done = items.filter((item) => item.done).length;
|
|
363
|
+
const open = total - done;
|
|
364
|
+
const progressPct = total === 0 ? 100 : Math.round(done / total * 100);
|
|
365
|
+
return {
|
|
366
|
+
backlogPath,
|
|
367
|
+
total,
|
|
368
|
+
done,
|
|
369
|
+
open,
|
|
370
|
+
progressPct,
|
|
371
|
+
items: [...items]
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/dashboard/state.ts
|
|
376
|
+
var DEFAULT_RECENT_ROUTES = 50;
|
|
377
|
+
var DEFAULT_ACTIVITY_LIMIT = 100;
|
|
378
|
+
function toRouteView(record) {
|
|
379
|
+
return {
|
|
380
|
+
ts: record.ts,
|
|
381
|
+
tier: record.tier,
|
|
382
|
+
routeKind: record.routeKind,
|
|
383
|
+
model: `${record.selected.providerID}/${record.selected.modelID}`,
|
|
384
|
+
promptHash: record.promptHash,
|
|
385
|
+
estimatedCostUSD: record.estimatedCostUSD,
|
|
386
|
+
estimatedSavingsUSD: record.estimatedSavingsUSD,
|
|
387
|
+
budgetAction: record.budgetAction
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function recentRoutes(records, limit) {
|
|
391
|
+
return [...records].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit)).map(toRouteView);
|
|
392
|
+
}
|
|
393
|
+
function recentActivity(entries, limit) {
|
|
394
|
+
return [...entries].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
|
|
395
|
+
}
|
|
396
|
+
function loopFrom(backlog) {
|
|
397
|
+
if (backlog === undefined) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const snapshot = buildLoopSnapshot(backlog.path, backlog.items);
|
|
401
|
+
if (backlog.lastCommit !== undefined) {
|
|
402
|
+
snapshot.lastCommit = backlog.lastCommit;
|
|
403
|
+
}
|
|
404
|
+
return snapshot;
|
|
405
|
+
}
|
|
406
|
+
function buildDashboardState(inputs) {
|
|
407
|
+
const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
|
|
408
|
+
const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
|
|
409
|
+
const state = {
|
|
410
|
+
generatedAt: inputs.generatedAt,
|
|
411
|
+
session: {},
|
|
412
|
+
cost: summarizeCostRecords(inputs.costRecords),
|
|
413
|
+
recentRoutes: recentRoutes(inputs.costRecords, recentRoutesLimit),
|
|
414
|
+
team: [...inputs.team],
|
|
415
|
+
activity: recentActivity(inputs.activity, activityLimit)
|
|
416
|
+
};
|
|
417
|
+
if (inputs.session?.id !== undefined) {
|
|
418
|
+
state.session.id = inputs.session.id;
|
|
419
|
+
}
|
|
420
|
+
if (inputs.session?.startedAt !== undefined) {
|
|
421
|
+
state.session.startedAt = inputs.session.startedAt;
|
|
422
|
+
}
|
|
423
|
+
const loop = loopFrom(inputs.backlog);
|
|
424
|
+
if (loop !== undefined) {
|
|
425
|
+
state.loop = loop;
|
|
426
|
+
}
|
|
427
|
+
return state;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/web/server.ts
|
|
431
|
+
var SSE_HEADERS = {
|
|
432
|
+
"content-type": "text/event-stream",
|
|
433
|
+
"cache-control": "no-cache",
|
|
434
|
+
connection: "keep-alive"
|
|
435
|
+
};
|
|
436
|
+
var JSON_HEADERS = { "content-type": "application/json" };
|
|
437
|
+
var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
|
|
438
|
+
function isAddressInUse(error) {
|
|
439
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
|
|
440
|
+
}
|
|
441
|
+
async function renderState(deps) {
|
|
442
|
+
return renderDashboardHtml(buildDashboardState(await deps.readSnapshot()), {
|
|
443
|
+
refreshMs: deps.config.refreshMs
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
async function stateJson(deps) {
|
|
447
|
+
return JSON.stringify(buildDashboardState(await deps.readSnapshot()));
|
|
448
|
+
}
|
|
449
|
+
function sseMessage(json) {
|
|
450
|
+
return `event: state
|
|
451
|
+
data: ${json}
|
|
452
|
+
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
function tryListen(server, host, port) {
|
|
456
|
+
return new Promise((resolve, reject) => {
|
|
457
|
+
const onError = (error) => {
|
|
458
|
+
server.removeListener("listening", onListening);
|
|
459
|
+
if (isAddressInUse(error)) {
|
|
460
|
+
resolve(false);
|
|
461
|
+
} else {
|
|
462
|
+
reject(error);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const onListening = () => {
|
|
466
|
+
server.removeListener("error", onError);
|
|
467
|
+
resolve(true);
|
|
468
|
+
};
|
|
469
|
+
server.once("error", onError);
|
|
470
|
+
server.once("listening", onListening);
|
|
471
|
+
server.listen(port, host);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
async function createDashboardServer(deps) {
|
|
475
|
+
const create = deps.createServer ?? createHttpServer;
|
|
476
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
477
|
+
const clients = new Set;
|
|
478
|
+
const handle = async (request, response) => {
|
|
479
|
+
if (request.method !== "GET") {
|
|
480
|
+
response.writeHead(405).end("method not allowed");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const url2 = new URL(request.url ?? "/", "http://localhost");
|
|
484
|
+
const path = url2.pathname;
|
|
485
|
+
if (path === "/" || path === "/index.html") {
|
|
486
|
+
response.writeHead(200, HTML_HEADERS).end(await renderState(deps));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (path === "/api/state") {
|
|
490
|
+
response.writeHead(200, JSON_HEADERS).end(await stateJson(deps));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (path === "/healthz") {
|
|
494
|
+
response.writeHead(200, JSON_HEADERS).end(JSON.stringify({ ok: true }));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (path === "/favicon.ico") {
|
|
498
|
+
response.writeHead(204).end();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (path === "/events") {
|
|
502
|
+
response.writeHead(200, SSE_HEADERS);
|
|
503
|
+
response.write(sseMessage(await stateJson(deps)));
|
|
504
|
+
clients.add(response);
|
|
505
|
+
request.on("close", () => {
|
|
506
|
+
clients.delete(response);
|
|
507
|
+
});
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
response.writeHead(404).end("not found");
|
|
511
|
+
};
|
|
512
|
+
const server = create((request, response) => {
|
|
513
|
+
handle(request, response);
|
|
514
|
+
});
|
|
515
|
+
const maxAttempts = deps.config.autoPortFallback ? 20 : 1;
|
|
516
|
+
let bound = false;
|
|
517
|
+
let boundPort = deps.config.port;
|
|
518
|
+
for (let offset = 0;offset < maxAttempts; offset += 1) {
|
|
519
|
+
const port = deps.config.port + offset;
|
|
520
|
+
if (port > 65535) {
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
if (await tryListen(server, deps.config.host, port)) {
|
|
524
|
+
boundPort = port;
|
|
525
|
+
bound = true;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!bound) {
|
|
530
|
+
throw new Error("dashboard: no available port");
|
|
531
|
+
}
|
|
532
|
+
const url = `http://${deps.config.host}:${boundPort}`;
|
|
533
|
+
log(`[openteam] dashboard en ${url}`);
|
|
534
|
+
const notify = async () => {
|
|
535
|
+
if (clients.size === 0) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const message = sseMessage(await stateJson(deps));
|
|
539
|
+
for (const response of clients) {
|
|
540
|
+
response.write(message);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const close = () => {
|
|
544
|
+
for (const response of clients) {
|
|
545
|
+
response.end();
|
|
546
|
+
}
|
|
547
|
+
clients.clear();
|
|
548
|
+
server.close();
|
|
549
|
+
server.closeAllConnections?.();
|
|
550
|
+
};
|
|
551
|
+
return { url, port: boundPort, notify, close };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/commands/agents.ts
|
|
555
|
+
var PROVIDER_LABELS = {
|
|
556
|
+
"github-copilot": "GitHub Copilot",
|
|
557
|
+
anthropic: "Anthropic",
|
|
558
|
+
openai: "OpenAI",
|
|
559
|
+
google: "Google",
|
|
560
|
+
openrouter: "OpenRouter",
|
|
561
|
+
ollama: "Ollama",
|
|
562
|
+
lmstudio: "LM Studio",
|
|
563
|
+
"foundry-local": "Foundry Local"
|
|
564
|
+
};
|
|
565
|
+
function providerLabel(providerID) {
|
|
566
|
+
return PROVIDER_LABELS[providerID] ?? providerID;
|
|
567
|
+
}
|
|
568
|
+
function unquote(value) {
|
|
569
|
+
const trimmed = value.trim();
|
|
570
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
571
|
+
return trimmed.slice(1, -1);
|
|
572
|
+
}
|
|
573
|
+
return trimmed;
|
|
574
|
+
}
|
|
575
|
+
function parseModelValue(value) {
|
|
576
|
+
const raw = unquote(value);
|
|
577
|
+
const slash = raw.indexOf("/");
|
|
578
|
+
if (slash <= 0 || slash === raw.length - 1) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) };
|
|
582
|
+
}
|
|
583
|
+
function parseAgentFile(file) {
|
|
584
|
+
const info = { name: file.name, mode: "subagent" };
|
|
585
|
+
const lines = file.contents.split(/\r?\n/);
|
|
586
|
+
if (lines[0]?.trim() !== "---") {
|
|
587
|
+
return info;
|
|
588
|
+
}
|
|
589
|
+
for (let i = 1;i < lines.length; i += 1) {
|
|
590
|
+
const line = lines[i] ?? "";
|
|
591
|
+
if (line.trim() === "---") {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
const colon = line.indexOf(":");
|
|
595
|
+
if (colon <= 0) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const key = line.slice(0, colon).trim();
|
|
599
|
+
const value = line.slice(colon + 1).trim();
|
|
600
|
+
if (key === "mode") {
|
|
601
|
+
info.mode = unquote(value) || "subagent";
|
|
602
|
+
} else if (key === "description") {
|
|
603
|
+
info.description = unquote(value);
|
|
604
|
+
} else if (key === "model") {
|
|
605
|
+
const model = parseModelValue(value);
|
|
606
|
+
if (model !== undefined) {
|
|
607
|
+
info.model = model;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return info;
|
|
612
|
+
}
|
|
613
|
+
function localProviderIDs(config) {
|
|
614
|
+
const ids = new Set;
|
|
615
|
+
for (const runtime of config.local.runtimes) {
|
|
616
|
+
ids.add(runtime.id);
|
|
617
|
+
ids.add(runtime.defaultModel.providerID);
|
|
618
|
+
}
|
|
619
|
+
ids.add(config.router.localDefault.providerID);
|
|
620
|
+
return ids;
|
|
621
|
+
}
|
|
622
|
+
function subscriptionLabel(model, localProviders) {
|
|
623
|
+
const isLocal = localProviders.has(model.providerID);
|
|
624
|
+
if (isLocal) {
|
|
625
|
+
return {
|
|
626
|
+
kind: "local",
|
|
627
|
+
subscription: `local · ${providerLabel(model.providerID)} (sin coste de tokens)`
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
kind: "frontier",
|
|
632
|
+
subscription: `frontier · ${providerLabel(model.providerID)} (consume tu suscripción)`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function classifyAgent(info, context) {
|
|
636
|
+
if (info.model !== undefined) {
|
|
637
|
+
const { kind, subscription: subscription2 } = subscriptionLabel(info.model, context.localProviders);
|
|
638
|
+
return {
|
|
639
|
+
name: info.name,
|
|
640
|
+
mode: info.mode,
|
|
641
|
+
kind,
|
|
642
|
+
model: info.model,
|
|
643
|
+
subscription: subscription2
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (context.defaultModel === undefined) {
|
|
647
|
+
return {
|
|
648
|
+
name: info.name,
|
|
649
|
+
mode: info.mode,
|
|
650
|
+
kind: "inherited",
|
|
651
|
+
subscription: "hereda el default de opencode.json (sin definir) · openteam enruta local-first por mensaje"
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const { subscription } = subscriptionLabel(context.defaultModel, context.localProviders);
|
|
655
|
+
return {
|
|
656
|
+
name: info.name,
|
|
657
|
+
mode: info.mode,
|
|
658
|
+
kind: "inherited",
|
|
659
|
+
model: context.defaultModel,
|
|
660
|
+
subscription: `hereda default → ${subscription} · openteam enruta local-first por mensaje`
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function modeRank(mode) {
|
|
664
|
+
if (mode === "primary") {
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
if (mode === "all") {
|
|
668
|
+
return 1;
|
|
669
|
+
}
|
|
670
|
+
return 2;
|
|
671
|
+
}
|
|
672
|
+
function classifyAgents(files, context) {
|
|
673
|
+
return files.map((file) => classifyAgent(parseAgentFile(file), context)).sort((a, b) => {
|
|
674
|
+
const rank = modeRank(a.mode) - modeRank(b.mode);
|
|
675
|
+
return rank !== 0 ? rank : a.name.localeCompare(b.name);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function modelText(classification) {
|
|
679
|
+
return classification.model === undefined ? "" : ` (${classification.model.providerID}/${classification.model.modelID})`;
|
|
680
|
+
}
|
|
681
|
+
function renderAgentList(classifications, context) {
|
|
682
|
+
const lines = ["openteam agents — LLM por agente:", ""];
|
|
683
|
+
if (classifications.length === 0) {
|
|
684
|
+
lines.push(" (sin agentes en .opencode/agent/) — ejecuta 'openteam setup' o pide al orquestador que cree el equipo.");
|
|
685
|
+
return lines.join(`
|
|
686
|
+
`);
|
|
687
|
+
}
|
|
688
|
+
const nameWidth = Math.max(...classifications.map((c) => c.name.length), "agente".length);
|
|
689
|
+
const modeWidth = Math.max(...classifications.map((c) => c.mode.length), "subagent".length);
|
|
690
|
+
for (const c of classifications) {
|
|
691
|
+
const bullet = c.mode === "primary" ? "●" : "○";
|
|
692
|
+
lines.push(` ${bullet} ${c.name.padEnd(nameWidth)} [${c.mode.padEnd(modeWidth)}] ${c.subscription}${modelText(c)}`);
|
|
693
|
+
}
|
|
694
|
+
lines.push("");
|
|
695
|
+
if (context.defaultModel !== undefined) {
|
|
696
|
+
const { kind } = subscriptionLabel(context.defaultModel, context.localProviders);
|
|
697
|
+
lines.push(`Default de opencode.json: ${context.defaultModel.providerID}/${context.defaultModel.modelID} (${kind} · ${providerLabel(context.defaultModel.providerID)})`);
|
|
698
|
+
} else {
|
|
699
|
+
lines.push("Default de opencode.json: (sin definir)");
|
|
700
|
+
}
|
|
701
|
+
lines.push("Leyenda: ● primary · ○ subagent · local = sin coste de tokens · frontier = consume tu suscripción.");
|
|
702
|
+
return lines.join(`
|
|
703
|
+
`);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/telemetry/read.ts
|
|
707
|
+
import { readFile } from "node:fs/promises";
|
|
708
|
+
|
|
709
|
+
// src/telemetry/types.ts
|
|
710
|
+
import { z as z3 } from "zod";
|
|
711
|
+
|
|
712
|
+
// src/capabilities/types.ts
|
|
713
|
+
import { z } from "zod";
|
|
714
|
+
var CapabilityTierSchema = z.union([
|
|
715
|
+
z.literal(0),
|
|
716
|
+
z.literal(1),
|
|
717
|
+
z.literal(2),
|
|
718
|
+
z.literal(3),
|
|
719
|
+
z.literal(4),
|
|
720
|
+
z.literal(5)
|
|
721
|
+
]);
|
|
722
|
+
var ComplexityTierSchema = z.enum([
|
|
723
|
+
"trivial",
|
|
724
|
+
"simple",
|
|
725
|
+
"moderate",
|
|
726
|
+
"hard"
|
|
727
|
+
]);
|
|
728
|
+
var ModelCapabilityProfileSchema = z.object({
|
|
729
|
+
ref: z.object({
|
|
730
|
+
providerID: z.string().min(1),
|
|
731
|
+
modelID: z.string().min(1)
|
|
732
|
+
}),
|
|
733
|
+
kind: z.enum(["local", "frontier", "router"]),
|
|
734
|
+
contextWindow: z.number().int().positive(),
|
|
735
|
+
maxOutputTokens: z.number().int().positive(),
|
|
736
|
+
supportsToolCalling: z.boolean(),
|
|
737
|
+
supportsVision: z.boolean(),
|
|
738
|
+
reasoningTier: CapabilityTierSchema,
|
|
739
|
+
codeQualityTier: CapabilityTierSchema,
|
|
740
|
+
costPer1M: z.object({
|
|
741
|
+
inputUSD: z.number().min(0),
|
|
742
|
+
outputUSD: z.number().min(0)
|
|
743
|
+
}),
|
|
744
|
+
availability: z.enum(["available", "degraded", "unavailable"])
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// src/config/schema.ts
|
|
748
|
+
import { z as z2 } from "zod";
|
|
749
|
+
var ModelRefSchema = z2.object({
|
|
750
|
+
providerID: z2.string().min(1),
|
|
751
|
+
modelID: z2.string().min(1)
|
|
752
|
+
});
|
|
753
|
+
var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
|
|
754
|
+
var PrivacyModeSchema = z2.enum([
|
|
755
|
+
"forceLocalOnSensitive",
|
|
756
|
+
"consentBeforeFrontier",
|
|
757
|
+
"off"
|
|
758
|
+
]);
|
|
759
|
+
var BaselineModeSchema = z2.enum(["auto", "pinned"]);
|
|
760
|
+
var DashboardHostSchema = z2.enum(["127.0.0.1", "localhost"]);
|
|
761
|
+
var DashboardConfigSchema = z2.object({
|
|
762
|
+
enabled: z2.boolean().default(false),
|
|
763
|
+
host: DashboardHostSchema.default("127.0.0.1"),
|
|
764
|
+
port: z2.number().int().min(1024).max(65535).default(4599),
|
|
765
|
+
autoPortFallback: z2.boolean().default(true),
|
|
766
|
+
refreshMs: z2.number().int().min(250).default(2000),
|
|
767
|
+
recentRoutes: z2.number().int().positive().default(50),
|
|
768
|
+
openBrowser: z2.boolean().default(false)
|
|
769
|
+
}).default({
|
|
770
|
+
enabled: false,
|
|
771
|
+
host: "127.0.0.1",
|
|
772
|
+
port: 4599,
|
|
773
|
+
autoPortFallback: true,
|
|
774
|
+
refreshMs: 2000,
|
|
775
|
+
recentRoutes: 50,
|
|
776
|
+
openBrowser: false
|
|
777
|
+
});
|
|
778
|
+
var defaultLocalModel = {
|
|
779
|
+
providerID: "ollama",
|
|
780
|
+
modelID: "qwen3:8b"
|
|
781
|
+
};
|
|
782
|
+
var defaultFrontierModel = {
|
|
783
|
+
providerID: "anthropic",
|
|
784
|
+
modelID: "claude-sonnet-4-5"
|
|
785
|
+
};
|
|
786
|
+
var LocalRuntimeSchema = z2.object({
|
|
787
|
+
id: z2.enum(["ollama", "lmstudio", "foundry-local"]),
|
|
788
|
+
enabled: z2.boolean().default(true),
|
|
789
|
+
baseURL: z2.string().url().optional(),
|
|
790
|
+
discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
|
|
791
|
+
defaultModel: ModelRefSchema
|
|
792
|
+
});
|
|
793
|
+
var OpenTeamConfigSchema = z2.object({
|
|
794
|
+
baseline: z2.object({
|
|
795
|
+
mode: BaselineModeSchema.default("auto"),
|
|
796
|
+
pinnedModel: ModelRefSchema.nullable().default(null),
|
|
797
|
+
hardDefault: ModelRefSchema.default(defaultFrontierModel)
|
|
798
|
+
}).default({
|
|
799
|
+
mode: "auto",
|
|
800
|
+
pinnedModel: null,
|
|
801
|
+
hardDefault: defaultFrontierModel
|
|
802
|
+
}),
|
|
803
|
+
router: z2.object({
|
|
804
|
+
mode: RouterModeSchema.default("balanced"),
|
|
805
|
+
localDefault: ModelRefSchema.default(defaultLocalModel),
|
|
806
|
+
trivialPromptMaxChars: z2.number().int().positive().default(280),
|
|
807
|
+
frontierPromptMinChars: z2.number().int().positive().default(2000)
|
|
808
|
+
}).default({
|
|
809
|
+
mode: "balanced",
|
|
810
|
+
localDefault: defaultLocalModel,
|
|
811
|
+
trivialPromptMaxChars: 280,
|
|
812
|
+
frontierPromptMinChars: 2000
|
|
813
|
+
}),
|
|
814
|
+
local: z2.object({
|
|
815
|
+
runtimes: z2.array(LocalRuntimeSchema).min(1).default([
|
|
816
|
+
{
|
|
817
|
+
id: "ollama",
|
|
818
|
+
enabled: true,
|
|
819
|
+
baseURL: "http://localhost:11434/v1",
|
|
820
|
+
defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
|
|
821
|
+
}
|
|
822
|
+
])
|
|
823
|
+
}).default({
|
|
824
|
+
runtimes: [
|
|
825
|
+
{
|
|
826
|
+
id: "ollama",
|
|
827
|
+
enabled: true,
|
|
828
|
+
baseURL: "http://localhost:11434/v1",
|
|
829
|
+
defaultModel: defaultLocalModel
|
|
830
|
+
}
|
|
831
|
+
]
|
|
832
|
+
}),
|
|
833
|
+
budgets: z2.object({
|
|
834
|
+
sessionUSD: z2.number().positive().optional(),
|
|
835
|
+
monthlyUSD: z2.number().positive().optional(),
|
|
836
|
+
frontierTokensPerSession: z2.number().int().positive().optional(),
|
|
837
|
+
hardStopOnBudgetExhaustion: z2.boolean().default(false)
|
|
838
|
+
}).default({ hardStopOnBudgetExhaustion: false }),
|
|
839
|
+
privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
|
|
840
|
+
dashboard: DashboardConfigSchema
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
// src/telemetry/types.ts
|
|
844
|
+
var CostRecordSchema = z3.object({
|
|
845
|
+
ts: z3.number().finite(),
|
|
846
|
+
sessionID: z3.string().min(1).optional(),
|
|
847
|
+
promptHash: z3.string().min(1),
|
|
848
|
+
promptChars: z3.number().int().min(0),
|
|
849
|
+
tier: ComplexityTierSchema,
|
|
850
|
+
routeKind: z3.enum(["local", "frontier"]),
|
|
851
|
+
selected: ModelRefSchema,
|
|
852
|
+
rationale: z3.string(),
|
|
853
|
+
estimatedCostUSD: z3.number().finite().min(0),
|
|
854
|
+
baselineCostUSD: z3.number().finite().min(0),
|
|
855
|
+
estimatedSavingsUSD: z3.number().finite(),
|
|
856
|
+
budgetAction: z3.string().min(1),
|
|
857
|
+
tokensIn: z3.number().int().min(0).optional(),
|
|
858
|
+
tokensOut: z3.number().int().min(0).optional()
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// src/telemetry/read.ts
|
|
862
|
+
var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
|
|
863
|
+
function parseCostRecordsJsonl(text) {
|
|
864
|
+
const records = [];
|
|
865
|
+
for (const line of text.split(`
|
|
866
|
+
`)) {
|
|
867
|
+
const trimmed = line.trim();
|
|
868
|
+
if (trimmed.length === 0) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
let candidate;
|
|
872
|
+
try {
|
|
873
|
+
candidate = JSON.parse(trimmed);
|
|
874
|
+
} catch {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const parsed = CostRecordSchema.safeParse(candidate);
|
|
878
|
+
if (parsed.success) {
|
|
879
|
+
records.push(parsed.data);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return records;
|
|
883
|
+
}
|
|
884
|
+
function isMissingFile(error) {
|
|
885
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
886
|
+
}
|
|
887
|
+
async function readCostRecords(path, deps = {}) {
|
|
888
|
+
const readFileFn = deps.readFile ?? readFile;
|
|
889
|
+
try {
|
|
890
|
+
const content = await readFileFn(path, "utf8");
|
|
891
|
+
return parseCostRecordsJsonl(content);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
if (isMissingFile(error)) {
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/web/snapshot.ts
|
|
901
|
+
function createSnapshotReader(deps, paths, context) {
|
|
902
|
+
return async () => {
|
|
903
|
+
const [telemetryText, backlogText, agentFiles, lastCommit] = await Promise.all([
|
|
904
|
+
deps.readText(paths.telemetryPath),
|
|
905
|
+
deps.readText(paths.backlogPath),
|
|
906
|
+
deps.listAgentFiles(paths.agentDir),
|
|
907
|
+
deps.gitLastCommit?.() ?? Promise.resolve(undefined)
|
|
908
|
+
]);
|
|
909
|
+
const costRecords = parseCostRecordsJsonl(telemetryText ?? "");
|
|
910
|
+
const team = agentFiles.map(parseAgentFile);
|
|
911
|
+
const inputs = {
|
|
912
|
+
generatedAt: deps.now(),
|
|
913
|
+
costRecords,
|
|
914
|
+
team,
|
|
915
|
+
activity: context.activity()
|
|
916
|
+
};
|
|
917
|
+
if (backlogText !== undefined) {
|
|
918
|
+
const backlog = {
|
|
919
|
+
path: paths.backlogPath,
|
|
920
|
+
items: parseBacklog(backlogText)
|
|
921
|
+
};
|
|
922
|
+
if (lastCommit !== undefined) {
|
|
923
|
+
backlog.lastCommit = lastCommit;
|
|
924
|
+
}
|
|
925
|
+
inputs.backlog = backlog;
|
|
926
|
+
}
|
|
927
|
+
if (context.session !== undefined) {
|
|
928
|
+
inputs.session = context.session;
|
|
929
|
+
}
|
|
930
|
+
if (context.recentRoutesLimit !== undefined) {
|
|
931
|
+
inputs.recentRoutesLimit = context.recentRoutesLimit;
|
|
932
|
+
}
|
|
933
|
+
return inputs;
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/web/watch.ts
|
|
938
|
+
function watchSources(paths, onChange, deps, debounceMs = 250) {
|
|
939
|
+
const setTimer = deps.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
|
|
940
|
+
const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
941
|
+
const watchers = [];
|
|
942
|
+
let pending;
|
|
943
|
+
const trigger = () => {
|
|
944
|
+
if (pending !== undefined) {
|
|
945
|
+
clearTimer(pending);
|
|
946
|
+
}
|
|
947
|
+
pending = setTimer(() => {
|
|
948
|
+
pending = undefined;
|
|
949
|
+
onChange();
|
|
950
|
+
}, debounceMs);
|
|
951
|
+
};
|
|
952
|
+
for (const path of paths) {
|
|
953
|
+
try {
|
|
954
|
+
watchers.push(deps.watch(path, trigger));
|
|
955
|
+
} catch {}
|
|
956
|
+
}
|
|
957
|
+
return {
|
|
958
|
+
close() {
|
|
959
|
+
if (pending !== undefined) {
|
|
960
|
+
clearTimer(pending);
|
|
961
|
+
pending = undefined;
|
|
962
|
+
}
|
|
963
|
+
for (const watcher of watchers) {
|
|
964
|
+
watcher.close();
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/web/start.ts
|
|
971
|
+
function startDashboard(deps) {
|
|
972
|
+
return createDashboardRuntime(deps);
|
|
973
|
+
}
|
|
974
|
+
async function createDashboardRuntime(deps) {
|
|
975
|
+
const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
|
|
976
|
+
const activity = createActivityBuffer(deps.config.recentRoutes);
|
|
977
|
+
const readSnapshot = createSnapshotReader({
|
|
978
|
+
readText: deps.readText,
|
|
979
|
+
listAgentFiles: deps.listAgentFiles,
|
|
980
|
+
now: deps.now,
|
|
981
|
+
...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {}
|
|
982
|
+
}, {
|
|
983
|
+
telemetryPath: deps.telemetryPath,
|
|
984
|
+
backlogPath,
|
|
985
|
+
agentDir: deps.agentDir
|
|
986
|
+
}, {
|
|
987
|
+
activity: () => activity.list(),
|
|
988
|
+
recentRoutesLimit: deps.config.recentRoutes,
|
|
989
|
+
...deps.session !== undefined ? { session: deps.session } : {}
|
|
990
|
+
});
|
|
991
|
+
const createServer = deps.serve ?? createDashboardServer;
|
|
992
|
+
const server = await createServer({
|
|
993
|
+
readSnapshot,
|
|
994
|
+
config: deps.config,
|
|
995
|
+
...deps.log !== undefined ? { log: deps.log } : {}
|
|
996
|
+
});
|
|
997
|
+
const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
|
|
998
|
+
const watcher = watchSources([deps.telemetryPath, backlogPath, deps.agentDir], () => {
|
|
999
|
+
server.notify();
|
|
1000
|
+
}, { watch: watchFn });
|
|
1001
|
+
return {
|
|
1002
|
+
url: server.url,
|
|
1003
|
+
port: server.port,
|
|
1004
|
+
pushActivity(entry) {
|
|
1005
|
+
activity.push(entry);
|
|
1006
|
+
server.notify();
|
|
1007
|
+
},
|
|
1008
|
+
close() {
|
|
1009
|
+
watcher.close();
|
|
1010
|
+
server.close();
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/cli/dashboardServe.ts
|
|
1016
|
+
function parseDashboardServeArgs(rest) {
|
|
1017
|
+
return {
|
|
1018
|
+
serve: rest.includes("--serve"),
|
|
1019
|
+
open: rest.includes("--open")
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
async function runDashboardServe(deps, options) {
|
|
1023
|
+
const config = await deps.loadConfig(deps.configPath);
|
|
1024
|
+
const start = deps.startDashboard ?? startDashboard;
|
|
1025
|
+
let runtime;
|
|
1026
|
+
try {
|
|
1027
|
+
runtime = await start({
|
|
1028
|
+
config: { ...config.dashboard, enabled: true },
|
|
1029
|
+
telemetryPath: deps.telemetryPath,
|
|
1030
|
+
backlogPath: deps.backlogPath,
|
|
1031
|
+
agentDir: deps.agentDir,
|
|
1032
|
+
readText: deps.readText,
|
|
1033
|
+
listAgentFiles: deps.listAgentFiles,
|
|
1034
|
+
now: deps.now,
|
|
1035
|
+
...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {},
|
|
1036
|
+
log: deps.log
|
|
1037
|
+
});
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1040
|
+
deps.log(`No se pudo iniciar el dashboard: ${message}`);
|
|
1041
|
+
return { exitCode: 1 };
|
|
1042
|
+
}
|
|
1043
|
+
deps.log("Escuchando en loopback. Pulsa Ctrl+C para parar.");
|
|
1044
|
+
if (options.open && deps.openBrowser !== undefined) {
|
|
1045
|
+
try {
|
|
1046
|
+
await deps.openBrowser(runtime.url);
|
|
1047
|
+
} catch {
|
|
1048
|
+
deps.log("No se pudo abrir el navegador automáticamente.");
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
await deps.waitForSignal();
|
|
1052
|
+
runtime.close();
|
|
1053
|
+
deps.log("Dashboard detenido.");
|
|
1054
|
+
return { exitCode: 0 };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
9
1057
|
// src/cli/setupAdapters.ts
|
|
10
1058
|
import {
|
|
11
1059
|
cancel,
|
|
@@ -69,42 +1117,7 @@ var CURATED_FRONTIER_PROFILES = [
|
|
|
69
1117
|
costPer1M: { inputUSD: 5, outputUSD: 20 },
|
|
70
1118
|
availability: "available"
|
|
71
1119
|
}
|
|
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
|
-
});
|
|
1120
|
+
];
|
|
108
1121
|
|
|
109
1122
|
// src/capabilities/normalize.ts
|
|
110
1123
|
function isRecord(value) {
|
|
@@ -726,318 +1739,70 @@ var clackPrompter = {
|
|
|
726
1739
|
});
|
|
727
1740
|
return unwrap(result);
|
|
728
1741
|
},
|
|
729
|
-
async text(opts) {
|
|
730
|
-
const result = await text({
|
|
731
|
-
message: opts.message,
|
|
732
|
-
...opts.placeholder !== undefined ? { placeholder: opts.placeholder } : {},
|
|
733
|
-
...opts.initial !== undefined ? { initialValue: opts.initial } : {}
|
|
734
|
-
});
|
|
735
|
-
return unwrap(result);
|
|
736
|
-
}
|
|
737
|
-
};
|
|
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"
|
|
1742
|
+
async text(opts) {
|
|
1743
|
+
const result = await text({
|
|
1744
|
+
message: opts.message,
|
|
1745
|
+
...opts.placeholder !== undefined ? { placeholder: opts.placeholder } : {},
|
|
1746
|
+
...opts.initial !== undefined ? { initialValue: opts.initial } : {}
|
|
1747
|
+
});
|
|
1748
|
+
return unwrap(result);
|
|
1749
|
+
}
|
|
984
1750
|
};
|
|
985
|
-
var
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1751
|
+
var DETECTION_RUNTIMES = [
|
|
1752
|
+
{
|
|
1753
|
+
id: "ollama",
|
|
1754
|
+
enabled: true,
|
|
1755
|
+
baseURL: "http://localhost:11434/v1",
|
|
1756
|
+
defaultModel: { providerID: "ollama", modelID: "probe" }
|
|
1757
|
+
},
|
|
1758
|
+
{
|
|
1759
|
+
id: "lmstudio",
|
|
1760
|
+
enabled: true,
|
|
1761
|
+
baseURL: "http://localhost:1234/v1",
|
|
1762
|
+
defaultModel: { providerID: "lmstudio", modelID: "probe" }
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
id: "foundry-local",
|
|
1766
|
+
enabled: true,
|
|
1767
|
+
discovery: "cli",
|
|
1768
|
+
defaultModel: { providerID: "foundry-local", modelID: "probe" }
|
|
1769
|
+
}
|
|
1770
|
+
];
|
|
1771
|
+
function createLoadFrontierProfiles() {
|
|
1772
|
+
return async () => {
|
|
1773
|
+
try {
|
|
1774
|
+
const result = await loadCapabilityProfiles({
|
|
1775
|
+
fetch: globalThis.fetch,
|
|
1776
|
+
now: () => Date.now()
|
|
1777
|
+
});
|
|
1778
|
+
return result.profiles.length > 0 ? result.profiles : [...CURATED_FRONTIER_PROFILES];
|
|
1779
|
+
} catch {
|
|
1780
|
+
return [...CURATED_FRONTIER_PROFILES];
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function createDetect(exec) {
|
|
1785
|
+
return async () => {
|
|
1786
|
+
const registry = new RuntimeRegistry({
|
|
1787
|
+
fetch: globalThis.fetch,
|
|
1788
|
+
clock: () => new Date().toISOString(),
|
|
1789
|
+
foundryDiscovery: foundryDiscoveryFromExec(exec)
|
|
1790
|
+
});
|
|
1791
|
+
const snapshots = await registry.probe(DETECTION_RUNTIMES);
|
|
1792
|
+
return snapshots.map((snapshot) => {
|
|
1793
|
+
const detected = {
|
|
1794
|
+
id: snapshot.id,
|
|
1795
|
+
baseURL: snapshot.baseURL,
|
|
1796
|
+
reachable: snapshot.reachable,
|
|
1797
|
+
models: snapshot.models.map((model) => model.modelID)
|
|
1798
|
+
};
|
|
1799
|
+
if (snapshot.error !== undefined) {
|
|
1800
|
+
detected.error = snapshot.error;
|
|
1029
1801
|
}
|
|
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
|
-
});
|
|
1802
|
+
return detected;
|
|
1803
|
+
});
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1041
1806
|
|
|
1042
1807
|
// src/commands/baseline.ts
|
|
1043
1808
|
function formatRef(ref) {
|
|
@@ -1107,9 +1872,12 @@ function renderDashboardStatus(dashboard) {
|
|
|
1107
1872
|
return [
|
|
1108
1873
|
"Dashboard: deshabilitado.",
|
|
1109
1874
|
"",
|
|
1110
|
-
'Para activarlo, pon "dashboard": { "enabled": true }
|
|
1111
|
-
"y recarga opencode. Se servirá (loopback) en:",
|
|
1112
|
-
` ${url}
|
|
1875
|
+
'Para activarlo dentro de opencode, pon "dashboard": { "enabled": true }',
|
|
1876
|
+
"en .opencode/openteam.json y recarga opencode. Se servirá (loopback) en:",
|
|
1877
|
+
` ${url}`,
|
|
1878
|
+
"",
|
|
1879
|
+
"O véelo ahora mismo sin editar la config:",
|
|
1880
|
+
" openteam dashboard --serve (añade --open para abrir el navegador)"
|
|
1113
1881
|
].join(`
|
|
1114
1882
|
`);
|
|
1115
1883
|
}
|
|
@@ -1121,7 +1889,8 @@ function renderDashboardStatus(dashboard) {
|
|
|
1121
1889
|
dashboard.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
|
|
1122
1890
|
"",
|
|
1123
1891
|
"Nota: solo escucha en loopback y nunca expone prompts (solo hashes).",
|
|
1124
|
-
"El servidor corre dentro de la sesión de opencode; se cierra al salir."
|
|
1892
|
+
"El servidor corre dentro de la sesión de opencode; se cierra al salir.",
|
|
1893
|
+
"Para verlo fuera de opencode: openteam dashboard --serve"
|
|
1125
1894
|
].join(`
|
|
1126
1895
|
`);
|
|
1127
1896
|
}
|
|
@@ -1341,78 +2110,6 @@ function buildOrchestratorAgent(frontier, options = {}) {
|
|
|
1341
2110
|
${body}`;
|
|
1342
2111
|
}
|
|
1343
2112
|
|
|
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
2113
|
// src/commands/yolo.ts
|
|
1417
2114
|
var CATCH_ALL = "*";
|
|
1418
2115
|
var ALLOW = "allow";
|
|
@@ -1461,6 +2158,7 @@ var HELP = [
|
|
|
1461
2158
|
" openteam doctor Diagnóstico de runtimes y config",
|
|
1462
2159
|
" openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
|
|
1463
2160
|
" openteam dashboard Estado y URL del dashboard web (loopback)",
|
|
2161
|
+
" openteam dashboard --serve Levanta el dashboard web (Ctrl+C para parar; --open abre el navegador)",
|
|
1464
2162
|
" openteam report Resumen de coste/ahorro (telemetría)",
|
|
1465
2163
|
" openteam yolo status Muestra si el modo YOLO está activo",
|
|
1466
2164
|
" openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
|
|
@@ -2123,65 +2821,23 @@ function loadOpenTeamConfig(raw) {
|
|
|
2123
2821
|
return OpenTeamConfigSchema.parse(raw ?? {});
|
|
2124
2822
|
}
|
|
2125
2823
|
|
|
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);
|
|
2824
|
+
// src/web/git.ts
|
|
2825
|
+
function createGitLastCommit(exec) {
|
|
2826
|
+
return async () => {
|
|
2827
|
+
const output = await exec("git", [
|
|
2828
|
+
"log",
|
|
2829
|
+
"-1",
|
|
2830
|
+
"--pretty=format:%h%x1f%s%x1f%cI"
|
|
2831
|
+
]);
|
|
2832
|
+
if (output.exitCode !== 0) {
|
|
2833
|
+
return;
|
|
2167
2834
|
}
|
|
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 [];
|
|
2835
|
+
const [hash, subject, at] = output.stdout.trim().split("\x1F");
|
|
2836
|
+
if (hash === undefined || subject === undefined || at === undefined) {
|
|
2837
|
+
return;
|
|
2182
2838
|
}
|
|
2183
|
-
|
|
2184
|
-
}
|
|
2839
|
+
return { hash, subject, at };
|
|
2840
|
+
};
|
|
2185
2841
|
}
|
|
2186
2842
|
|
|
2187
2843
|
// src/cli.ts
|
|
@@ -2214,6 +2870,32 @@ async function loadConfig(path) {
|
|
|
2214
2870
|
throw error;
|
|
2215
2871
|
}
|
|
2216
2872
|
}
|
|
2873
|
+
async function readTextOptional(path) {
|
|
2874
|
+
try {
|
|
2875
|
+
return await readFile2(path, "utf8");
|
|
2876
|
+
} catch (error) {
|
|
2877
|
+
if (isMissingFile2(error)) {
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
throw error;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
async function openBrowser(url) {
|
|
2884
|
+
if (process.platform === "win32") {
|
|
2885
|
+
await nodeExec("cmd", ["/c", "start", "", url]);
|
|
2886
|
+
} else if (process.platform === "darwin") {
|
|
2887
|
+
await nodeExec("open", [url]);
|
|
2888
|
+
} else {
|
|
2889
|
+
await nodeExec("xdg-open", [url]);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
function waitForSignal() {
|
|
2893
|
+
return new Promise((resolve) => {
|
|
2894
|
+
const done = () => resolve();
|
|
2895
|
+
process.once("SIGINT", done);
|
|
2896
|
+
process.once("SIGTERM", done);
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2217
2899
|
var deps = {
|
|
2218
2900
|
loadConfig,
|
|
2219
2901
|
saveConfig: (config, path) => writeOpenTeamConfigFile(config, path),
|
|
@@ -2299,6 +2981,28 @@ async function main() {
|
|
|
2299
2981
|
process.exitCode = result2.exitCode;
|
|
2300
2982
|
return;
|
|
2301
2983
|
}
|
|
2984
|
+
if (argv[0] === "dashboard") {
|
|
2985
|
+
const flags = parseDashboardServeArgs(argv.slice(1));
|
|
2986
|
+
if (flags.serve) {
|
|
2987
|
+
const result2 = await runDashboardServe({
|
|
2988
|
+
loadConfig,
|
|
2989
|
+
configPath: DEFAULT_CONFIG_PATH,
|
|
2990
|
+
telemetryPath: DEFAULT_TELEMETRY_PATH,
|
|
2991
|
+
backlogPath: DEFAULT_BACKLOG_PATH,
|
|
2992
|
+
agentDir: AGENT_DIR,
|
|
2993
|
+
readText: readTextOptional,
|
|
2994
|
+
listAgentFiles: deps.listAgentFiles,
|
|
2995
|
+
now: () => new Date().toISOString(),
|
|
2996
|
+
gitLastCommit: createGitLastCommit(nodeExec),
|
|
2997
|
+
waitForSignal,
|
|
2998
|
+
log: (message) => process.stdout.write(`${message}
|
|
2999
|
+
`),
|
|
3000
|
+
openBrowser
|
|
3001
|
+
}, { open: flags.open });
|
|
3002
|
+
process.exitCode = result2.exitCode;
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
2302
3006
|
const result = await runCli(argv, deps);
|
|
2303
3007
|
process.stdout.write(`${result.stdout}
|
|
2304
3008
|
`);
|