@jmanuelcorral/openteam 0.1.15 → 0.1.17

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/index.js CHANGED
@@ -25,6 +25,24 @@ var PrivacyModeSchema = z.enum([
25
25
  "off"
26
26
  ]);
27
27
  var BaselineModeSchema = z.enum(["auto", "pinned"]);
28
+ var DashboardHostSchema = z.enum(["127.0.0.1", "localhost"]);
29
+ var DashboardConfigSchema = z.object({
30
+ enabled: z.boolean().default(false),
31
+ host: DashboardHostSchema.default("127.0.0.1"),
32
+ port: z.number().int().min(1024).max(65535).default(4599),
33
+ autoPortFallback: z.boolean().default(true),
34
+ refreshMs: z.number().int().min(250).default(2000),
35
+ recentRoutes: z.number().int().positive().default(50),
36
+ openBrowser: z.boolean().default(false)
37
+ }).default({
38
+ enabled: false,
39
+ host: "127.0.0.1",
40
+ port: 4599,
41
+ autoPortFallback: true,
42
+ refreshMs: 2000,
43
+ recentRoutes: 50,
44
+ openBrowser: false
45
+ });
28
46
  var defaultLocalModel = {
29
47
  providerID: "ollama",
30
48
  modelID: "qwen3:8b"
@@ -86,7 +104,8 @@ var OpenTeamConfigSchema = z.object({
86
104
  frontierTokensPerSession: z.number().int().positive().optional(),
87
105
  hardStopOnBudgetExhaustion: z.boolean().default(false)
88
106
  }).default({ hardStopOnBudgetExhaustion: false }),
89
- privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive")
107
+ privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
108
+ dashboard: DashboardConfigSchema
90
109
  });
91
110
 
92
111
  // src/config/persist.ts
@@ -948,6 +967,32 @@ function autoBaseline(config) {
948
967
  };
949
968
  }
950
969
 
970
+ // src/commands/dashboard.ts
971
+ function renderDashboardStatus(dashboard) {
972
+ const url = `http://${dashboard.host}:${dashboard.port}`;
973
+ if (!dashboard.enabled) {
974
+ return [
975
+ "Dashboard: deshabilitado.",
976
+ "",
977
+ 'Para activarlo, pon "dashboard": { "enabled": true } en .opencode/openteam.json',
978
+ "y recarga opencode. Se servirá (loopback) en:",
979
+ ` ${url}`
980
+ ].join(`
981
+ `);
982
+ }
983
+ return [
984
+ "Dashboard: habilitado.",
985
+ ` URL: ${url}`,
986
+ ` Refresco: ${dashboard.refreshMs} ms (SSE + polling)`,
987
+ ` Rutas: últimas ${dashboard.recentRoutes}`,
988
+ dashboard.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
989
+ "",
990
+ "Nota: solo escucha en loopback y nunca expone prompts (solo hashes).",
991
+ "El servidor corre dentro de la sesión de opencode; se cierra al salir."
992
+ ].join(`
993
+ `);
994
+ }
995
+
951
996
  // src/commands/doctor.ts
952
997
  function runtimeLine(snapshot) {
953
998
  const mark = snapshot.reachable ? "✓" : "✗";
@@ -1097,6 +1142,7 @@ var HELP = [
1097
1142
  " openteam baseline auto Baseline cheapest-capable (modo auto)",
1098
1143
  " openteam doctor Diagnóstico de runtimes y config",
1099
1144
  " openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
1145
+ " openteam dashboard Estado y URL del dashboard web (loopback)",
1100
1146
  " openteam report Resumen de coste/ahorro (telemetría)",
1101
1147
  " openteam yolo status Muestra si el modo YOLO está activo",
1102
1148
  " openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
@@ -1286,6 +1332,10 @@ async function runCli(argv, deps) {
1286
1332
  if (command === "agents") {
1287
1333
  return await runAgents(deps, configPath, opencodeConfigPath);
1288
1334
  }
1335
+ if (command === "dashboard") {
1336
+ const config = await deps.loadConfig(configPath);
1337
+ return { exitCode: 0, stdout: renderDashboardStatus(config.dashboard) };
1338
+ }
1289
1339
  if (command === undefined || command === "help" || command === "--help") {
1290
1340
  return { exitCode: 0, stdout: HELP };
1291
1341
  }
@@ -1316,13 +1366,23 @@ function commandArgv(action, model) {
1316
1366
  return ["report"];
1317
1367
  case "agents":
1318
1368
  return ["agents"];
1369
+ case "dashboard":
1370
+ return ["dashboard"];
1319
1371
  }
1320
1372
  }
1321
1373
  function createCommandTool(deps) {
1322
1374
  return tool({
1323
- description: "Comandos runtime de openteam: baseline (show/set/auto), doctor, report y agents (LLM por agente: local/frontier + suscripción).",
1375
+ description: "Comandos runtime de openteam: baseline (show/set/auto), doctor, report, agents (LLM por agente: local/frontier + suscripción) y dashboard (estado/URL del panel web).",
1324
1376
  args: {
1325
- action: tool.schema.enum(["show", "set", "auto", "doctor", "report", "agents"]).describe("Acción a ejecutar"),
1377
+ action: tool.schema.enum([
1378
+ "show",
1379
+ "set",
1380
+ "auto",
1381
+ "doctor",
1382
+ "report",
1383
+ "agents",
1384
+ "dashboard"
1385
+ ]).describe("Acción a ejecutar"),
1326
1386
  model: tool.schema.string().optional().describe("Modelo provider/model (solo para action=set)")
1327
1387
  },
1328
1388
  async execute(args) {
@@ -1965,6 +2025,579 @@ async function readCostRecords(path, deps = {}) {
1965
2025
  }
1966
2026
  }
1967
2027
 
2028
+ // src/web/paths.ts
2029
+ var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
2030
+
2031
+ // src/web/start.ts
2032
+ import { watch as fsWatch } from "node:fs";
2033
+
2034
+ // src/web/activity.ts
2035
+ function createActivityBuffer(max = 100) {
2036
+ const entries = [];
2037
+ return {
2038
+ push(entry) {
2039
+ entries.push(entry);
2040
+ if (entries.length > max) {
2041
+ entries.splice(0, entries.length - max);
2042
+ }
2043
+ },
2044
+ list() {
2045
+ return [...entries];
2046
+ }
2047
+ };
2048
+ }
2049
+
2050
+ // src/dashboard/render.ts
2051
+ var TIERS2 = [
2052
+ "trivial",
2053
+ "simple",
2054
+ "moderate",
2055
+ "hard"
2056
+ ];
2057
+ function escapeHtml(value) {
2058
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2059
+ }
2060
+ function usd2(value) {
2061
+ return `$${value.toFixed(5)}`;
2062
+ }
2063
+ function pct(value) {
2064
+ return `${value.toFixed(2)}%`;
2065
+ }
2066
+ function agentModel(agent) {
2067
+ return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
2068
+ }
2069
+ function summaryPanel(cost) {
2070
+ return [
2071
+ '<section class="panel" id="panel-summary">',
2072
+ "<h2>Resumen de routing</h2>",
2073
+ '<div class="grid">',
2074
+ `<div class="stat"><span class="k">Decisiones</span><span class="v">${cost.count}</span></div>`,
2075
+ `<div class="stat"><span class="k">Local</span><span class="v">${cost.localCount}</span></div>`,
2076
+ `<div class="stat"><span class="k">Frontier</span><span class="v">${cost.frontierCount}</span></div>`,
2077
+ `<div class="stat"><span class="k">Coste real</span><span class="v">${usd2(cost.totalEstimatedUSD)}</span></div>`,
2078
+ `<div class="stat"><span class="k">Baseline</span><span class="v">${usd2(cost.totalBaselineUSD)}</span></div>`,
2079
+ `<div class="stat"><span class="k">Ahorro</span><span class="v good">${usd2(cost.totalSavingsUSD)} (${pct(cost.savingsPct)})</span></div>`,
2080
+ `<div class="stat"><span class="k">Tokens in</span><span class="v">${cost.tokensIn}</span></div>`,
2081
+ `<div class="stat"><span class="k">Tokens out</span><span class="v">${cost.tokensOut}</span></div>`,
2082
+ "</div>",
2083
+ "</section>"
2084
+ ].join("");
2085
+ }
2086
+ function tierPanel(cost) {
2087
+ const rows = TIERS2.map((tier) => {
2088
+ const summary = cost.byTier[tier];
2089
+ return `<tr><td>${tier}</td><td>${summary.count}</td><td>${usd2(summary.estimatedUSD)}</td><td class="good">${usd2(summary.savingsUSD)}</td></tr>`;
2090
+ }).join("");
2091
+ return [
2092
+ '<section class="panel" id="panel-tier">',
2093
+ "<h2>Por tier</h2>",
2094
+ "<table><thead><tr><th>Tier</th><th>Nº</th><th>Coste</th><th>Ahorro</th></tr></thead>",
2095
+ `<tbody>${rows}</tbody></table>`,
2096
+ "</section>"
2097
+ ].join("");
2098
+ }
2099
+ function loopItemRow(item) {
2100
+ const box = item.done ? "☑" : "☐";
2101
+ const cls = item.done ? "done" : "open";
2102
+ const who = item.assignee === undefined ? "" : `<span class="who">@${escapeHtml(item.assignee)}</span> `;
2103
+ return `<li class="${cls}"><span class="box">${box}</span> ${who}${escapeHtml(item.text)}</li>`;
2104
+ }
2105
+ function loopPanel(loop) {
2106
+ if (loop === undefined) {
2107
+ return [
2108
+ '<section class="panel" id="panel-loop">',
2109
+ "<h2>Loop</h2>",
2110
+ '<p class="muted">Sin backlog activo (no hay <code>openteam-backlog.md</code>).</p>',
2111
+ "</section>"
2112
+ ].join("");
2113
+ }
2114
+ const items = loop.items.map(loopItemRow).join("");
2115
+ const commit = loop.lastCommit === undefined ? "" : `<p class="muted">Último commit: <code>${escapeHtml(loop.lastCommit.hash)}</code> ${escapeHtml(loop.lastCommit.subject)}</p>`;
2116
+ return [
2117
+ '<section class="panel" id="panel-loop">',
2118
+ "<h2>Loop</h2>",
2119
+ `<div class="progress" role="progressbar" aria-valuenow="${loop.progressPct}" aria-valuemin="0" aria-valuemax="100"><div class="bar" style="width:${loop.progressPct}%"></div></div>`,
2120
+ `<p class="muted">${loop.done}/${loop.total} completados · ${loop.open} abiertos · ${loop.progressPct}%</p>`,
2121
+ `<ul class="items">${items}</ul>`,
2122
+ commit,
2123
+ "</section>"
2124
+ ].join("");
2125
+ }
2126
+ function teamPanel(team) {
2127
+ if (team.length === 0) {
2128
+ return [
2129
+ '<section class="panel" id="panel-team">',
2130
+ "<h2>Equipo</h2>",
2131
+ '<p class="muted">Sin agentes en <code>.opencode/agent/</code>.</p>',
2132
+ "</section>"
2133
+ ].join("");
2134
+ }
2135
+ const rows = team.map((agent) => `<tr><td>${escapeHtml(agent.name)}</td><td>${escapeHtml(agent.mode)}</td><td>${escapeHtml(agentModel(agent))}</td></tr>`).join("");
2136
+ return [
2137
+ '<section class="panel" id="panel-team">',
2138
+ "<h2>Equipo</h2>",
2139
+ "<table><thead><tr><th>Agente</th><th>Modo</th><th>LLM</th></tr></thead>",
2140
+ `<tbody>${rows}</tbody></table>`,
2141
+ "</section>"
2142
+ ].join("");
2143
+ }
2144
+ function routeRow(route) {
2145
+ 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>${usd2(route.estimatedCostUSD)}</td><td class="good">${usd2(route.estimatedSavingsUSD)}</td></tr>`;
2146
+ }
2147
+ function routesPanel(routes) {
2148
+ if (routes.length === 0) {
2149
+ return [
2150
+ '<section class="panel" id="panel-routes">',
2151
+ "<h2>Decisiones recientes</h2>",
2152
+ '<p class="muted">Sin decisiones de routing todavía.</p>',
2153
+ "</section>"
2154
+ ].join("");
2155
+ }
2156
+ const rows = routes.map(routeRow).join("");
2157
+ return [
2158
+ '<section class="panel" id="panel-routes">',
2159
+ "<h2>Decisiones recientes</h2>",
2160
+ "<table><thead><tr><th>Tier</th><th>Ruta</th><th>Modelo</th><th>Prompt (hash)</th><th>Coste</th><th>Ahorro</th></tr></thead>",
2161
+ `<tbody>${rows}</tbody></table>`,
2162
+ "</section>"
2163
+ ].join("");
2164
+ }
2165
+ function activityRow(entry) {
2166
+ const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
2167
+ return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
2168
+ }
2169
+ function activityPanel(activity) {
2170
+ if (activity.length === 0) {
2171
+ return [
2172
+ '<section class="panel" id="panel-activity">',
2173
+ "<h2>Actividad</h2>",
2174
+ '<p class="muted">Sin actividad registrada.</p>',
2175
+ "</section>"
2176
+ ].join("");
2177
+ }
2178
+ const rows = activity.map(activityRow).join("");
2179
+ return [
2180
+ '<section class="panel" id="panel-activity">',
2181
+ "<h2>Actividad</h2>",
2182
+ `<ul class="timeline">${rows}</ul>`,
2183
+ "</section>"
2184
+ ].join("");
2185
+ }
2186
+ var STYLE = `
2187
+ :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
2188
+ *{box-sizing:border-box}
2189
+ body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}
2190
+ header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}
2191
+ header h1{font-size:18px;margin:0}
2192
+ header .meta{color:var(--muted);font-size:12px}
2193
+ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;padding:24px}
2194
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
2195
+ .panel h2{font-size:14px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
2196
+ .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
2197
+ .stat{display:flex;flex-direction:column;gap:2px}
2198
+ .stat .k{color:var(--muted);font-size:12px}
2199
+ .stat .v{font-size:18px;font-weight:600}
2200
+ .good{color:var(--good)}
2201
+ .local{color:var(--local)}
2202
+ .frontier{color:var(--frontier)}
2203
+ .muted{color:var(--muted)}
2204
+ table{width:100%;border-collapse:collapse;font-size:13px}
2205
+ th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)}
2206
+ th{color:var(--muted);font-weight:600}
2207
+ .hash{font-family:ui-monospace,Consolas,monospace;color:var(--muted)}
2208
+ .progress{height:10px;background:#0b0d11;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
2209
+ .progress .bar{height:100%;background:var(--good)}
2210
+ ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}
2211
+ ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
2212
+ ul.items li.done{color:var(--muted)}
2213
+ ul.items .box{font-family:monospace}
2214
+ .who{color:var(--local)}
2215
+ .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
2216
+ code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
2217
+ `;
2218
+ var CLIENT_JS = `
2219
+ (function(){
2220
+ function reloadIfChanged(prev){
2221
+ fetch('/api/state').then(function(r){return r.json()}).then(function(s){
2222
+ if(s.generatedAt!==prev){location.reload()}
2223
+ }).catch(function(){});
2224
+ }
2225
+ var current=document.documentElement.getAttribute('data-generated')||'';
2226
+ if('EventSource' in window){
2227
+ try{
2228
+ var es=new EventSource('/events');
2229
+ es.addEventListener('state',function(){location.reload()});
2230
+ es.onerror=function(){/* fallback below */};
2231
+ }catch(e){/* ignore */}
2232
+ }
2233
+ var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
2234
+ setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
2235
+ })();
2236
+ `;
2237
+ function renderDashboardHtml(state, options = {}) {
2238
+ const refreshMs = options.refreshMs ?? 2000;
2239
+ const body = [
2240
+ summaryPanel(state.cost),
2241
+ loopPanel(state.loop),
2242
+ teamPanel(state.team),
2243
+ tierPanel(state.cost),
2244
+ routesPanel(state.recentRoutes),
2245
+ activityPanel(state.activity)
2246
+ ].join("");
2247
+ const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
2248
+ return [
2249
+ "<!doctype html>",
2250
+ `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}">`,
2251
+ "<head>",
2252
+ '<meta charset="utf-8">',
2253
+ '<meta name="viewport" content="width=device-width,initial-scale=1">',
2254
+ "<title>openteam dashboard</title>",
2255
+ `<style>${STYLE}</style>`,
2256
+ "</head>",
2257
+ "<body>",
2258
+ "<header>",
2259
+ "<h1>openteam dashboard</h1>",
2260
+ `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
2261
+ "</header>",
2262
+ `<main>${body}</main>`,
2263
+ `<script>${CLIENT_JS}</script>`,
2264
+ "</body>",
2265
+ "</html>"
2266
+ ].join("");
2267
+ }
2268
+
2269
+ // src/dashboard/backlog.ts
2270
+ var ITEM_RE = /^\s*[-*+]\s+\[( |x|X)\]\s+(.*)$/;
2271
+ var ASSIGNEE_RE = /^\[@([^\]]+)\]\s*(.*)$/;
2272
+ function parseAssignee(text) {
2273
+ const match = ASSIGNEE_RE.exec(text);
2274
+ if (match === null) {
2275
+ return { text: text.trim() };
2276
+ }
2277
+ const assignee = (match[1] ?? "").trim();
2278
+ const rest = (match[2] ?? "").trim();
2279
+ if (assignee.length === 0) {
2280
+ return { text: text.trim() };
2281
+ }
2282
+ return { assignee, text: rest };
2283
+ }
2284
+ function parseBacklog(text) {
2285
+ const items = [];
2286
+ for (const line of text.split(/\r?\n/)) {
2287
+ const match = ITEM_RE.exec(line);
2288
+ if (match === null) {
2289
+ continue;
2290
+ }
2291
+ const done = (match[1] ?? " ").toLowerCase() === "x";
2292
+ const rawText = (match[2] ?? "").trim();
2293
+ const { assignee, text: itemText } = parseAssignee(rawText);
2294
+ const item = { text: itemText, done };
2295
+ if (assignee !== undefined) {
2296
+ item.assignee = assignee;
2297
+ }
2298
+ items.push(item);
2299
+ }
2300
+ return items;
2301
+ }
2302
+ function buildLoopSnapshot(backlogPath, items) {
2303
+ const total = items.length;
2304
+ const done = items.filter((item) => item.done).length;
2305
+ const open = total - done;
2306
+ const progressPct = total === 0 ? 100 : Math.round(done / total * 100);
2307
+ return {
2308
+ backlogPath,
2309
+ total,
2310
+ done,
2311
+ open,
2312
+ progressPct,
2313
+ items: [...items]
2314
+ };
2315
+ }
2316
+
2317
+ // src/dashboard/state.ts
2318
+ var DEFAULT_RECENT_ROUTES = 50;
2319
+ var DEFAULT_ACTIVITY_LIMIT = 100;
2320
+ function toRouteView(record) {
2321
+ return {
2322
+ ts: record.ts,
2323
+ tier: record.tier,
2324
+ routeKind: record.routeKind,
2325
+ model: `${record.selected.providerID}/${record.selected.modelID}`,
2326
+ promptHash: record.promptHash,
2327
+ estimatedCostUSD: record.estimatedCostUSD,
2328
+ estimatedSavingsUSD: record.estimatedSavingsUSD,
2329
+ budgetAction: record.budgetAction
2330
+ };
2331
+ }
2332
+ function recentRoutes(records, limit) {
2333
+ return [...records].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit)).map(toRouteView);
2334
+ }
2335
+ function recentActivity(entries, limit) {
2336
+ return [...entries].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
2337
+ }
2338
+ function loopFrom(backlog) {
2339
+ if (backlog === undefined) {
2340
+ return;
2341
+ }
2342
+ const snapshot = buildLoopSnapshot(backlog.path, backlog.items);
2343
+ if (backlog.lastCommit !== undefined) {
2344
+ snapshot.lastCommit = backlog.lastCommit;
2345
+ }
2346
+ return snapshot;
2347
+ }
2348
+ function buildDashboardState(inputs) {
2349
+ const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
2350
+ const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
2351
+ const state = {
2352
+ generatedAt: inputs.generatedAt,
2353
+ session: {},
2354
+ cost: summarizeCostRecords(inputs.costRecords),
2355
+ recentRoutes: recentRoutes(inputs.costRecords, recentRoutesLimit),
2356
+ team: [...inputs.team],
2357
+ activity: recentActivity(inputs.activity, activityLimit)
2358
+ };
2359
+ if (inputs.session?.id !== undefined) {
2360
+ state.session.id = inputs.session.id;
2361
+ }
2362
+ if (inputs.session?.startedAt !== undefined) {
2363
+ state.session.startedAt = inputs.session.startedAt;
2364
+ }
2365
+ const loop = loopFrom(inputs.backlog);
2366
+ if (loop !== undefined) {
2367
+ state.loop = loop;
2368
+ }
2369
+ return state;
2370
+ }
2371
+
2372
+ // src/web/server.ts
2373
+ var SSE_HEADERS = {
2374
+ "content-type": "text/event-stream",
2375
+ "cache-control": "no-cache",
2376
+ connection: "keep-alive"
2377
+ };
2378
+ var JSON_HEADERS = { "content-type": "application/json" };
2379
+ var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
2380
+ function isAddressInUse(error) {
2381
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
2382
+ }
2383
+ async function renderState(deps) {
2384
+ return renderDashboardHtml(buildDashboardState(await deps.readSnapshot()), {
2385
+ refreshMs: deps.config.refreshMs
2386
+ });
2387
+ }
2388
+ async function stateJson(deps) {
2389
+ return JSON.stringify(buildDashboardState(await deps.readSnapshot()));
2390
+ }
2391
+ function sseMessage(json) {
2392
+ return `event: state
2393
+ data: ${json}
2394
+
2395
+ `;
2396
+ }
2397
+ function createDashboardServer(deps) {
2398
+ const serve = deps.serve ?? Bun.serve;
2399
+ const log = deps.log ?? ((message) => console.log(message));
2400
+ const encoder = new TextEncoder;
2401
+ const clients = new Set;
2402
+ const handler = async (request) => {
2403
+ const url2 = new URL(request.url);
2404
+ if (request.method !== "GET") {
2405
+ return new Response("method not allowed", { status: 405 });
2406
+ }
2407
+ if (url2.pathname === "/" || url2.pathname === "/index.html") {
2408
+ return new Response(await renderState(deps), { headers: HTML_HEADERS });
2409
+ }
2410
+ if (url2.pathname === "/api/state") {
2411
+ return new Response(await stateJson(deps), { headers: JSON_HEADERS });
2412
+ }
2413
+ if (url2.pathname === "/healthz") {
2414
+ return new Response(JSON.stringify({ ok: true }), {
2415
+ headers: JSON_HEADERS
2416
+ });
2417
+ }
2418
+ if (url2.pathname === "/favicon.ico") {
2419
+ return new Response(null, { status: 204 });
2420
+ }
2421
+ if (url2.pathname === "/events") {
2422
+ const initial = await stateJson(deps);
2423
+ const stream = new ReadableStream({
2424
+ start(controller) {
2425
+ controller.enqueue(encoder.encode(sseMessage(initial)));
2426
+ clients.add(controller);
2427
+ },
2428
+ cancel(controller) {
2429
+ clients.delete(controller);
2430
+ }
2431
+ });
2432
+ return new Response(stream, { headers: SSE_HEADERS });
2433
+ }
2434
+ return new Response("not found", { status: 404 });
2435
+ };
2436
+ const maxAttempts = deps.config.autoPortFallback ? 20 : 1;
2437
+ let server;
2438
+ let lastError;
2439
+ for (let offset = 0;offset < maxAttempts; offset += 1) {
2440
+ const port = deps.config.port + offset;
2441
+ if (port > 65535) {
2442
+ break;
2443
+ }
2444
+ try {
2445
+ server = serve({
2446
+ hostname: deps.config.host,
2447
+ port,
2448
+ fetch: handler
2449
+ });
2450
+ break;
2451
+ } catch (error) {
2452
+ lastError = error;
2453
+ if (!isAddressInUse(error)) {
2454
+ throw error;
2455
+ }
2456
+ }
2457
+ }
2458
+ if (server === undefined) {
2459
+ throw lastError ?? new Error("dashboard: no available port");
2460
+ }
2461
+ const boundPort = server.port ?? deps.config.port;
2462
+ const url = `http://${deps.config.host}:${boundPort}`;
2463
+ log(`[openteam] dashboard en ${url}`);
2464
+ const notify = async () => {
2465
+ if (clients.size === 0) {
2466
+ return;
2467
+ }
2468
+ const message = encoder.encode(sseMessage(await stateJson(deps)));
2469
+ for (const controller of clients) {
2470
+ try {
2471
+ controller.enqueue(message);
2472
+ } catch {
2473
+ clients.delete(controller);
2474
+ }
2475
+ }
2476
+ };
2477
+ const close = () => {
2478
+ for (const controller of clients) {
2479
+ try {
2480
+ controller.close();
2481
+ } catch {}
2482
+ }
2483
+ clients.clear();
2484
+ server.stop(true);
2485
+ };
2486
+ return { url, port: boundPort, notify, close };
2487
+ }
2488
+
2489
+ // src/web/snapshot.ts
2490
+ function createSnapshotReader(deps, paths, context) {
2491
+ return async () => {
2492
+ const [telemetryText, backlogText, agentFiles, lastCommit] = await Promise.all([
2493
+ deps.readText(paths.telemetryPath),
2494
+ deps.readText(paths.backlogPath),
2495
+ deps.listAgentFiles(paths.agentDir),
2496
+ deps.gitLastCommit?.() ?? Promise.resolve(undefined)
2497
+ ]);
2498
+ const costRecords = parseCostRecordsJsonl(telemetryText ?? "");
2499
+ const team = agentFiles.map(parseAgentFile);
2500
+ const inputs = {
2501
+ generatedAt: deps.now(),
2502
+ costRecords,
2503
+ team,
2504
+ activity: context.activity()
2505
+ };
2506
+ if (backlogText !== undefined) {
2507
+ const backlog = {
2508
+ path: paths.backlogPath,
2509
+ items: parseBacklog(backlogText)
2510
+ };
2511
+ if (lastCommit !== undefined) {
2512
+ backlog.lastCommit = lastCommit;
2513
+ }
2514
+ inputs.backlog = backlog;
2515
+ }
2516
+ if (context.session !== undefined) {
2517
+ inputs.session = context.session;
2518
+ }
2519
+ if (context.recentRoutesLimit !== undefined) {
2520
+ inputs.recentRoutesLimit = context.recentRoutesLimit;
2521
+ }
2522
+ return inputs;
2523
+ };
2524
+ }
2525
+
2526
+ // src/web/watch.ts
2527
+ function watchSources(paths, onChange, deps, debounceMs = 250) {
2528
+ const setTimer = deps.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
2529
+ const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
2530
+ const watchers = [];
2531
+ let pending;
2532
+ const trigger = () => {
2533
+ if (pending !== undefined) {
2534
+ clearTimer(pending);
2535
+ }
2536
+ pending = setTimer(() => {
2537
+ pending = undefined;
2538
+ onChange();
2539
+ }, debounceMs);
2540
+ };
2541
+ for (const path of paths) {
2542
+ try {
2543
+ watchers.push(deps.watch(path, trigger));
2544
+ } catch {}
2545
+ }
2546
+ return {
2547
+ close() {
2548
+ if (pending !== undefined) {
2549
+ clearTimer(pending);
2550
+ pending = undefined;
2551
+ }
2552
+ for (const watcher of watchers) {
2553
+ watcher.close();
2554
+ }
2555
+ }
2556
+ };
2557
+ }
2558
+
2559
+ // src/web/start.ts
2560
+ function startDashboard(deps) {
2561
+ const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
2562
+ const activity = createActivityBuffer(deps.config.recentRoutes);
2563
+ const readSnapshot = createSnapshotReader({
2564
+ readText: deps.readText,
2565
+ listAgentFiles: deps.listAgentFiles,
2566
+ now: deps.now,
2567
+ ...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {}
2568
+ }, {
2569
+ telemetryPath: deps.telemetryPath,
2570
+ backlogPath,
2571
+ agentDir: deps.agentDir
2572
+ }, {
2573
+ activity: () => activity.list(),
2574
+ recentRoutesLimit: deps.config.recentRoutes,
2575
+ ...deps.session !== undefined ? { session: deps.session } : {}
2576
+ });
2577
+ const createServer = deps.serve ?? createDashboardServer;
2578
+ const server = createServer({
2579
+ readSnapshot,
2580
+ config: deps.config,
2581
+ ...deps.log !== undefined ? { log: deps.log } : {}
2582
+ });
2583
+ const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
2584
+ const watcher = watchSources([deps.telemetryPath, backlogPath, deps.agentDir], () => {
2585
+ server.notify();
2586
+ }, { watch: watchFn });
2587
+ return {
2588
+ url: server.url,
2589
+ port: server.port,
2590
+ pushActivity(entry) {
2591
+ activity.push(entry);
2592
+ server.notify();
2593
+ },
2594
+ close() {
2595
+ watcher.close();
2596
+ server.close();
2597
+ }
2598
+ };
2599
+ }
2600
+
1968
2601
  // src/index.ts
1969
2602
  function createShellExec($) {
1970
2603
  return async (command, args) => {
@@ -2014,6 +2647,45 @@ function createTelemetrySink(rawOptions) {
2014
2647
  function isMissingFile2(error) {
2015
2648
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2016
2649
  }
2650
+ async function readTextOptional(path) {
2651
+ try {
2652
+ return await readFile2(path, "utf8");
2653
+ } catch (error) {
2654
+ if (isMissingFile2(error)) {
2655
+ return;
2656
+ }
2657
+ throw error;
2658
+ }
2659
+ }
2660
+ function createGitLastCommit(exec) {
2661
+ return async () => {
2662
+ const output = await exec("git", [
2663
+ "log",
2664
+ "-1",
2665
+ "--pretty=format:%h%x1f%s%x1f%cI"
2666
+ ]);
2667
+ if (output.exitCode !== 0) {
2668
+ return;
2669
+ }
2670
+ const [hash, subject, at] = output.stdout.trim().split("\x1F");
2671
+ if (hash === undefined || subject === undefined || at === undefined) {
2672
+ return;
2673
+ }
2674
+ return { hash, subject, at };
2675
+ };
2676
+ }
2677
+ var DASHBOARD_EVENT_SUMMARIES = {
2678
+ "session.created": "Sesión creada",
2679
+ "session.idle": "Sesión en reposo",
2680
+ "session.error": "Error de sesión"
2681
+ };
2682
+ function activityFromEvent(event, now) {
2683
+ const summary = DASHBOARD_EVENT_SUMMARIES[event.type];
2684
+ if (summary === undefined) {
2685
+ return;
2686
+ }
2687
+ return { ts: now(), kind: "agent", summary };
2688
+ }
2017
2689
  function createCliDeps(config, registry, telemetryPath) {
2018
2690
  return {
2019
2691
  loadConfig: async (path) => {
@@ -2089,6 +2761,26 @@ var server = async (ctx, rawOptions) => {
2089
2761
  sink: createTelemetrySink(rawOptions)
2090
2762
  });
2091
2763
  const cliDeps = createCliDeps(config, registry, telemetryPath);
2764
+ let dashboard;
2765
+ if (config.dashboard.enabled) {
2766
+ try {
2767
+ dashboard = startDashboard({
2768
+ config: config.dashboard,
2769
+ telemetryPath,
2770
+ agentDir: dirname2(ORCHESTRATOR_AGENT_PATH),
2771
+ backlogPath: DEFAULT_BACKLOG_PATH,
2772
+ readText: readTextOptional,
2773
+ listAgentFiles: cliDeps.listAgentFiles,
2774
+ now: () => new Date().toISOString(),
2775
+ gitLastCommit: createGitLastCommit(createShellExec(ctx.$)),
2776
+ log: (message) => console.log(`[openteam] ${message}`)
2777
+ });
2778
+ console.log(`[openteam] dashboard en ${dashboard.url}`);
2779
+ } catch (error) {
2780
+ const message = error instanceof Error ? error.message : String(error);
2781
+ console.warn(`[openteam] no se pudo iniciar el dashboard: ${message}`);
2782
+ }
2783
+ }
2092
2784
  return {
2093
2785
  ...hooks,
2094
2786
  tool: {
@@ -2098,6 +2790,12 @@ var server = async (ctx, rawOptions) => {
2098
2790
  if (event.type === "session.created" || event.type === "session.idle") {
2099
2791
  cache.refresh();
2100
2792
  }
2793
+ if (dashboard !== undefined) {
2794
+ const entry = activityFromEvent(event, () => Date.now());
2795
+ if (entry !== undefined) {
2796
+ dashboard.pushActivity(entry);
2797
+ }
2798
+ }
2101
2799
  }
2102
2800
  };
2103
2801
  };
@@ -2113,5 +2811,6 @@ export {
2113
2811
  isMissingFile2 as isMissingFile,
2114
2812
  src_default as default,
2115
2813
  createFileAppender,
2116
- createCliDeps
2814
+ createCliDeps,
2815
+ activityFromEvent
2117
2816
  };