@jmanuelcorral/openteam 0.1.19 → 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/dist/cli.js CHANGED
@@ -6,7 +6,1055 @@ 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/cli/initAdapters.ts
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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
+
1057
+ // src/cli/setupAdapters.ts
10
1058
  import {
11
1059
  cancel,
12
1060
  confirm,
@@ -68,43 +1116,8 @@ var CURATED_FRONTIER_PROFILES = [
68
1116
  codeQualityTier: 5,
69
1117
  costPer1M: { inputUSD: 5, outputUSD: 20 },
70
1118
  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
- });
1119
+ }
1120
+ ];
108
1121
 
109
1122
  // src/capabilities/normalize.ts
110
1123
  function isRecord(value) {
@@ -670,7 +1683,7 @@ class RuntimeRegistry {
670
1683
  }
671
1684
  }
672
1685
 
673
- // src/cli/initAdapters.ts
1686
+ // src/cli/setupAdapters.ts
674
1687
  class PromptCancelledError extends Error {
675
1688
  constructor() {
676
1689
  super("prompt cancelado");
@@ -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 init' 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 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
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
- budgets: z2.object({
1033
- sessionUSD: z2.number().positive().optional(),
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 } en .opencode/openteam.json',
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";
@@ -1454,13 +2151,14 @@ var HELP = [
1454
2151
  "openteam — comandos runtime",
1455
2152
  "",
1456
2153
  "Uso:",
1457
- " openteam init Configura opencode + openteam (interactivo)",
2154
+ " openteam setup Configura opencode + openteam (interactivo)",
1458
2155
  " openteam baseline show Muestra el baseline efectivo",
1459
2156
  " openteam baseline set <p/model> Fija el baseline (modo pinned)",
1460
2157
  " openteam baseline auto Baseline cheapest-capable (modo auto)",
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)",
@@ -1551,7 +2249,7 @@ ${HELP}`
1551
2249
  if (config === undefined) {
1552
2250
  return {
1553
2251
  exitCode: 1,
1554
- stdout: `No existe ${opencodeConfigPath}. Ejecuta 'openteam init' primero.`
2252
+ stdout: `No existe ${opencodeConfigPath}. Ejecuta 'openteam setup' primero.`
1555
2253
  };
1556
2254
  }
1557
2255
  if (sub === "status") {
@@ -1669,7 +2367,7 @@ ${HELP}`
1669
2367
  }
1670
2368
  }
1671
2369
 
1672
- // src/commands/init.ts
2370
+ // src/commands/setup.ts
1673
2371
  import { join } from "node:path";
1674
2372
 
1675
2373
  // src/config/persist.ts
@@ -1714,7 +2412,7 @@ function buildOpenteamCommand() {
1714
2412
  `);
1715
2413
  }
1716
2414
 
1717
- // src/commands/init.ts
2415
+ // src/commands/setup.ts
1718
2416
  var OPENTEAM_PLUGIN_SPEC = "@jmanuelcorral/openteam";
1719
2417
  var OPENCODE_CONFIG_PATH = "opencode.json";
1720
2418
  var KNOWN_RUNTIMES = [
@@ -1953,10 +2651,10 @@ async function chooseRuntimeModel(prompt, detected, meta) {
1953
2651
  }
1954
2652
  return prompt.select(opts);
1955
2653
  }
1956
- async function runInit(deps) {
2654
+ async function runSetup(deps) {
1957
2655
  const { prompt } = deps;
1958
2656
  try {
1959
- prompt.intro("openteam init — configura opencode con routing local-first");
2657
+ prompt.intro("openteam setup — configura opencode con routing local-first");
1960
2658
  const detected = await deps.detect();
1961
2659
  const detectedById = new Map(detected.map((item) => [item.id, item]));
1962
2660
  const reachableIds = detected.filter((item) => item.reachable).map((item) => item.id);
@@ -2085,8 +2783,8 @@ async function runInit(deps) {
2085
2783
  initial: false
2086
2784
  });
2087
2785
  if (!overwrite) {
2088
- prompt.outro("init cancelado: no se sobrescribió ninguna configuración.");
2089
- return { exitCode: 1, stdout: "init cancelado por el usuario" };
2786
+ prompt.outro("setup cancelado: no se sobrescribió ninguna configuración.");
2787
+ return { exitCode: 1, stdout: "setup cancelado por el usuario" };
2090
2788
  }
2091
2789
  }
2092
2790
  await deps.writeFile(openTeamPath, serializeOpenTeamConfig(openTeamConfig));
@@ -2110,11 +2808,11 @@ async function runInit(deps) {
2110
2808
  prompt.outro("Listo. Abre opencode y selecciona el agente 'openteam'.");
2111
2809
  return {
2112
2810
  exitCode: 0,
2113
- stdout: `openteam init completado: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${OPENTEAM_COMMAND_PATH}`
2811
+ stdout: `openteam setup completado: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${OPENTEAM_COMMAND_PATH}`
2114
2812
  };
2115
2813
  } catch (error) {
2116
2814
  const message = error instanceof Error ? error.message : String(error);
2117
- return { exitCode: 1, stdout: `Error en init: ${message}` };
2815
+ return { exitCode: 1, stdout: `Error en setup: ${message}` };
2118
2816
  }
2119
2817
  }
2120
2818
 
@@ -2123,65 +2821,23 @@ function loadOpenTeamConfig(raw) {
2123
2821
  return OpenTeamConfigSchema.parse(raw ?? {});
2124
2822
  }
2125
2823
 
2126
- // src/telemetry/read.ts
2127
- import { readFile } from "node:fs/promises";
2128
-
2129
- // src/telemetry/types.ts
2130
- import { z as z3 } from "zod";
2131
- var CostRecordSchema = z3.object({
2132
- ts: z3.number().finite(),
2133
- sessionID: z3.string().min(1).optional(),
2134
- promptHash: z3.string().min(1),
2135
- promptChars: z3.number().int().min(0),
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
- return records;
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
- throw error;
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),
@@ -2268,7 +2950,7 @@ var deps = {
2268
2950
  orchestratorAgentPath: ORCHESTRATOR_AGENT_PATH,
2269
2951
  agentDir: AGENT_DIR
2270
2952
  };
2271
- function createInitDeps() {
2953
+ function createSetupDeps() {
2272
2954
  return {
2273
2955
  cwd: process.cwd(),
2274
2956
  detect: createDetect(nodeExec),
@@ -2290,8 +2972,8 @@ function createInitDeps() {
2290
2972
  }
2291
2973
  async function main() {
2292
2974
  const argv = process.argv.slice(2);
2293
- if (argv[0] === "init") {
2294
- const result2 = await runInit(createInitDeps());
2975
+ if (argv[0] === "setup" || argv[0] === "init") {
2976
+ const result2 = await runSetup(createSetupDeps());
2295
2977
  if (result2.stdout.length > 0) {
2296
2978
  process.stdout.write(`${result2.stdout}
2297
2979
  `);
@@ -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
  `);