@jmanuelcorral/openteam 0.1.24 → 0.1.26

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.
Files changed (66) hide show
  1. package/.opencode/command/openteam.md +2 -2
  2. package/README.md +65 -16
  3. package/dist/cli/{dashboardServe.d.ts → consoleServe.d.ts} +23 -13
  4. package/dist/cli/consoleServe.d.ts.map +1 -0
  5. package/dist/cli/tunnel.d.ts +89 -0
  6. package/dist/cli/tunnel.d.ts.map +1 -0
  7. package/dist/cli.js +1168 -187
  8. package/dist/commands/console.d.ts +9 -0
  9. package/dist/commands/console.d.ts.map +1 -0
  10. package/dist/commands/dispatch.d.ts.map +1 -1
  11. package/dist/config/schema.d.ts +88 -4
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/console/backlog.d.ts.map +1 -0
  14. package/dist/console/opencodeClient.d.ts +30 -0
  15. package/dist/console/opencodeClient.d.ts.map +1 -0
  16. package/dist/console/protocol.d.ts +42 -0
  17. package/dist/console/protocol.d.ts.map +1 -0
  18. package/dist/console/pty.d.ts +47 -0
  19. package/dist/console/pty.d.ts.map +1 -0
  20. package/dist/console/render.d.ts +20 -0
  21. package/dist/console/render.d.ts.map +1 -0
  22. package/dist/console/session.d.ts +23 -0
  23. package/dist/console/session.d.ts.map +1 -0
  24. package/dist/console/sse.d.ts +21 -0
  25. package/dist/console/sse.d.ts.map +1 -0
  26. package/dist/{dashboard → console}/state.d.ts +10 -5
  27. package/dist/console/state.d.ts.map +1 -0
  28. package/dist/console/token.d.ts +24 -0
  29. package/dist/console/token.d.ts.map +1 -0
  30. package/dist/{dashboard → console}/types.d.ts +24 -3
  31. package/dist/console/types.d.ts.map +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +85 -34
  34. package/dist/plugin/capture.d.ts +12 -1
  35. package/dist/plugin/capture.d.ts.map +1 -1
  36. package/dist/plugin/commandTool.d.ts +1 -1
  37. package/dist/plugin/commandTool.d.ts.map +1 -1
  38. package/dist/telemetry/aggregate.d.ts +15 -1
  39. package/dist/telemetry/aggregate.d.ts.map +1 -1
  40. package/dist/telemetry/decisions.d.ts +1 -1
  41. package/dist/telemetry/eventLog.d.ts +1 -1
  42. package/dist/telemetry/eventLog.d.ts.map +1 -1
  43. package/dist/telemetry/events.d.ts +28 -1
  44. package/dist/telemetry/events.d.ts.map +1 -1
  45. package/dist/web/console.d.ts +28 -0
  46. package/dist/web/console.d.ts.map +1 -0
  47. package/dist/web/git.d.ts +1 -1
  48. package/dist/web/git.d.ts.map +1 -1
  49. package/dist/web/paths.d.ts +1 -1
  50. package/dist/web/paths.d.ts.map +1 -1
  51. package/dist/web/server.d.ts +20 -9
  52. package/dist/web/server.d.ts.map +1 -1
  53. package/dist/web/snapshot.d.ts +6 -6
  54. package/dist/web/snapshot.d.ts.map +1 -1
  55. package/dist/web/start.d.ts +19 -9
  56. package/dist/web/start.d.ts.map +1 -1
  57. package/package.json +4 -2
  58. package/dist/cli/dashboardServe.d.ts.map +0 -1
  59. package/dist/commands/dashboard.d.ts +0 -9
  60. package/dist/commands/dashboard.d.ts.map +0 -1
  61. package/dist/dashboard/backlog.d.ts.map +0 -1
  62. package/dist/dashboard/render.d.ts +0 -14
  63. package/dist/dashboard/render.d.ts.map +0 -1
  64. package/dist/dashboard/state.d.ts.map +0 -1
  65. package/dist/dashboard/types.d.ts.map +0 -1
  66. /package/dist/{dashboard → console}/backlog.d.ts +0 -0
package/dist/cli.js CHANGED
@@ -1,11 +1,307 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { execFile } from "node:child_process";
4
+ import { execFile, spawn } from "node:child_process";
5
5
  import { access, mkdir as mkdir2, readdir, readFile, writeFile as writeFile2 } from "node:fs/promises";
6
6
  import { dirname as dirname2, join as join2 } from "node:path";
7
7
  import { promisify } from "node:util";
8
8
 
9
+ // src/cli/consoleServe.ts
10
+ import { randomBytes } from "node:crypto";
11
+
12
+ // src/console/protocol.ts
13
+ import { z } from "zod";
14
+ var MAX_INPUT_CHARS = 1e5;
15
+ var SessionInputSchema = z.object({
16
+ text: z.string().min(1).max(MAX_INPUT_CHARS)
17
+ });
18
+ function parseSessionInput(raw) {
19
+ const result = SessionInputSchema.safeParse(raw);
20
+ return result.success ? result.data : undefined;
21
+ }
22
+ function asRecord(value) {
23
+ return typeof value === "object" && value !== null ? value : undefined;
24
+ }
25
+ function str(value) {
26
+ return typeof value === "string" && value.length > 0 ? value : undefined;
27
+ }
28
+ function eventSessionID(event) {
29
+ const props = asRecord(event.properties);
30
+ if (props === undefined) {
31
+ return;
32
+ }
33
+ return str(props.sessionID) ?? str(asRecord(props.part)?.sessionID) ?? str(asRecord(props.info)?.sessionID);
34
+ }
35
+ function eventToSessionFrame(event, sessionID) {
36
+ if (eventSessionID(event) !== sessionID) {
37
+ return;
38
+ }
39
+ const props = asRecord(event.properties) ?? {};
40
+ switch (event.type) {
41
+ case "message.part.updated": {
42
+ const part = asRecord(props.part);
43
+ if (part === undefined) {
44
+ return;
45
+ }
46
+ if (part.type === "text") {
47
+ const text = str(part.text);
48
+ return text === undefined ? undefined : { kind: "text", text };
49
+ }
50
+ if (part.type === "tool") {
51
+ const tool = str(part.tool) ?? "tool";
52
+ const status = str(asRecord(part.state)?.status) ?? "run";
53
+ return { kind: "tool", text: `⚙ ${tool} · ${status}` };
54
+ }
55
+ return;
56
+ }
57
+ case "session.idle":
58
+ return { kind: "status", text: "● sesión lista" };
59
+ case "session.error": {
60
+ const message = str(asRecord(props.error)?.message) ?? "error de sesión";
61
+ return { kind: "error", text: `✖ ${message}` };
62
+ }
63
+ case "permission.updated": {
64
+ const title = str(props.title) ?? "permiso solicitado";
65
+ const id = str(props.id);
66
+ const frame = { kind: "permission", text: `⚠ ${title}` };
67
+ if (id !== undefined) {
68
+ frame.permissionID = id;
69
+ }
70
+ return frame;
71
+ }
72
+ default:
73
+ return;
74
+ }
75
+ }
76
+ function frameToSse(frame) {
77
+ return `event: frame
78
+ data: ${JSON.stringify(frame)}
79
+
80
+ `;
81
+ }
82
+
83
+ // src/console/sse.ts
84
+ function pushSse(buffer, chunk) {
85
+ const combined = (buffer + chunk).replace(/\r\n/g, `
86
+ `);
87
+ const parts = combined.split(`
88
+
89
+ `);
90
+ const rest = parts.pop() ?? "";
91
+ const messages = [];
92
+ for (const block of parts) {
93
+ if (block.length === 0) {
94
+ continue;
95
+ }
96
+ let event;
97
+ const dataLines = [];
98
+ for (const line of block.split(`
99
+ `)) {
100
+ if (line.startsWith(":") || line.length === 0) {
101
+ continue;
102
+ }
103
+ if (line.startsWith("event:")) {
104
+ event = line.slice("event:".length).trimStart();
105
+ } else if (line.startsWith("data:")) {
106
+ dataLines.push(line.slice("data:".length).replace(/^ /, ""));
107
+ }
108
+ }
109
+ if (dataLines.length === 0 && event === undefined) {
110
+ continue;
111
+ }
112
+ const message = { data: dataLines.join(`
113
+ `) };
114
+ if (event !== undefined) {
115
+ message.event = event;
116
+ }
117
+ messages.push(message);
118
+ }
119
+ return { messages, rest };
120
+ }
121
+ function parseEventData(data) {
122
+ if (data.length === 0) {
123
+ return;
124
+ }
125
+ try {
126
+ return JSON.parse(data);
127
+ } catch {
128
+ return;
129
+ }
130
+ }
131
+
132
+ // src/console/opencodeClient.ts
133
+ function unwrapEvent(value) {
134
+ if (typeof value !== "object" || value === null) {
135
+ return;
136
+ }
137
+ const record = value;
138
+ const payload = typeof record.payload === "object" && record.payload !== null ? record.payload : record;
139
+ return typeof payload.type === "string" ? payload : undefined;
140
+ }
141
+ function messageToFrame(message, sessionID) {
142
+ const event = unwrapEvent(parseEventData(message.data));
143
+ return event === undefined ? undefined : eventToSessionFrame(event, sessionID);
144
+ }
145
+ function createConsoleClient(deps) {
146
+ const doFetch = deps.fetch ?? fetch;
147
+ const base = deps.endpoint.replace(/\/$/, "");
148
+ const authHeaders = deps.password !== undefined && deps.password.length > 0 ? { authorization: `Bearer ${deps.password}` } : {};
149
+ const post = async (path, body) => {
150
+ await doFetch(`${base}${path}`, {
151
+ method: "POST",
152
+ headers: { "content-type": "application/json", ...authHeaders },
153
+ body: JSON.stringify(body)
154
+ });
155
+ };
156
+ return {
157
+ async sendPrompt(sessionID, text) {
158
+ await post(`/session/${encodeURIComponent(sessionID)}/prompt_async`, {
159
+ parts: [{ type: "text", text }]
160
+ });
161
+ },
162
+ async respondPermission(sessionID, permissionID, response) {
163
+ await post(`/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(permissionID)}`, { response });
164
+ },
165
+ async streamFrames(sessionID, onFrame, signal) {
166
+ const res = await doFetch(`${base}/global/event`, {
167
+ headers: { accept: "text/event-stream", ...authHeaders },
168
+ signal
169
+ });
170
+ const body = res.body;
171
+ if (body === null) {
172
+ return;
173
+ }
174
+ const reader = body.getReader();
175
+ const decoder = new TextDecoder;
176
+ let buffer = "";
177
+ try {
178
+ while (!signal.aborted) {
179
+ const { value, done } = await reader.read();
180
+ if (done) {
181
+ break;
182
+ }
183
+ const parsed = pushSse(buffer, decoder.decode(value, { stream: true }));
184
+ buffer = parsed.rest;
185
+ for (const message of parsed.messages) {
186
+ const frame = messageToFrame(message, sessionID);
187
+ if (frame !== undefined) {
188
+ onFrame(frame);
189
+ }
190
+ }
191
+ }
192
+ } finally {
193
+ reader.cancel().catch(() => {});
194
+ }
195
+ }
196
+ };
197
+ }
198
+
199
+ // src/console/pty.ts
200
+ function asRecord2(value) {
201
+ return typeof value === "object" && value !== null ? value : undefined;
202
+ }
203
+ function str2(value) {
204
+ return typeof value === "string" && value.length > 0 ? value : undefined;
205
+ }
206
+ function parsePtyInfo(raw) {
207
+ const record = asRecord2(raw);
208
+ const id = str2(record?.id);
209
+ if (record === undefined || id === undefined) {
210
+ return;
211
+ }
212
+ const info = { id };
213
+ const title = str2(record.title);
214
+ if (title !== undefined) {
215
+ info.title = title;
216
+ }
217
+ const status = str2(record.status);
218
+ if (status !== undefined) {
219
+ info.status = status;
220
+ }
221
+ return info;
222
+ }
223
+ function parseConnectToken(raw) {
224
+ const record = asRecord2(raw);
225
+ const ticket = str2(record?.ticket);
226
+ if (record === undefined || ticket === undefined) {
227
+ return;
228
+ }
229
+ const token = { ticket };
230
+ const expires = record.expires_in ?? record.expiresIn;
231
+ if (typeof expires === "number" && Number.isFinite(expires)) {
232
+ token.expiresIn = expires;
233
+ }
234
+ return token;
235
+ }
236
+ function buildPtyWsUrl(endpoint, ptyID, ticket) {
237
+ const base = endpoint.replace(/\/$/, "").replace(/^http/, "ws");
238
+ const id = encodeURIComponent(ptyID);
239
+ const t = encodeURIComponent(ticket);
240
+ return `${base}/pty/${id}/connect?ticket=${t}`;
241
+ }
242
+ function createPtyClient(deps) {
243
+ const doFetch = deps.fetch ?? fetch;
244
+ const base = deps.endpoint.replace(/\/$/, "");
245
+ const authHeaders = deps.password !== undefined && deps.password.length > 0 ? { authorization: `Bearer ${deps.password}` } : {};
246
+ const json = async (path, body) => {
247
+ const res = await doFetch(`${base}${path}`, {
248
+ method: "POST",
249
+ headers: { "content-type": "application/json", ...authHeaders },
250
+ ...body !== undefined ? { body: JSON.stringify(body) } : {}
251
+ });
252
+ if (!res.ok) {
253
+ return;
254
+ }
255
+ try {
256
+ return await res.json();
257
+ } catch {
258
+ return;
259
+ }
260
+ };
261
+ return {
262
+ async createPty(input) {
263
+ return parsePtyInfo(await json("/pty", input));
264
+ },
265
+ async connectToken(ptyID) {
266
+ return parseConnectToken(await json(`/pty/${encodeURIComponent(ptyID)}/connect-token`));
267
+ },
268
+ async closePty(ptyID) {
269
+ await doFetch(`${base}/pty/${encodeURIComponent(ptyID)}`, {
270
+ method: "DELETE",
271
+ headers: authHeaders
272
+ });
273
+ },
274
+ wsUrl(ptyID, ticket) {
275
+ return buildPtyWsUrl(deps.endpoint, ptyID, ticket);
276
+ }
277
+ };
278
+ }
279
+
280
+ // src/console/token.ts
281
+ var HEX = "0123456789abcdef";
282
+ function encodeToken(bytes) {
283
+ let out = "";
284
+ for (const byte of bytes) {
285
+ out += HEX[byte >> 4 & 15];
286
+ out += HEX[byte & 15];
287
+ }
288
+ return out;
289
+ }
290
+ function safeEqualToken(a, b) {
291
+ if (a.length !== b.length) {
292
+ return false;
293
+ }
294
+ let diff = 0;
295
+ for (let i = 0;i < a.length; i += 1) {
296
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
297
+ }
298
+ return diff === 0;
299
+ }
300
+ function tokenFromQuery(query) {
301
+ const value = query?.get("token");
302
+ return value === null || value === undefined || value.length === 0 ? undefined : value;
303
+ }
304
+
9
305
  // src/web/start.ts
10
306
  import { watch as fsWatch } from "node:fs";
11
307
 
@@ -18,7 +314,7 @@ import {
18
314
  createServer as createHttpServer
19
315
  } from "node:http";
20
316
 
21
- // src/dashboard/render.ts
317
+ // src/console/render.ts
22
318
  var TIERS = [
23
319
  "trivial",
24
320
  "simple",
@@ -255,6 +551,84 @@ function meetingsPanel(meetings) {
255
551
  "</section>"
256
552
  ].join("");
257
553
  }
554
+ var STATUS_LABELS = {
555
+ running: "activa",
556
+ idle: "en reposo",
557
+ "waiting-input": "esperando input",
558
+ ended: "finalizada"
559
+ };
560
+ function statusBadge(status) {
561
+ return `<span class="badge status-${status}">${STATUS_LABELS[status]}</span>`;
562
+ }
563
+ function tabBar(sessions) {
564
+ const allTab = '<button type="button" class="tab" data-tab="all">Todas</button>';
565
+ const tabs = sessions.map((tab) => {
566
+ const id = escapeHtml(tab.sessionID);
567
+ const short = escapeHtml(shortSessionID(tab.sessionID));
568
+ return `<button type="button" class="tab" data-tab="${id}"><span class="dot status-${tab.status}"></span>${short}</button>`;
569
+ }).join("");
570
+ return `<nav class="tabs" id="tabbar">${allTab}${tabs}</nav>`;
571
+ }
572
+ function shortSessionID(sessionID) {
573
+ return sessionID.length <= 12 ? sessionID : `${sessionID.slice(0, 12)}…`;
574
+ }
575
+ function sessionHeaderPanel(tab) {
576
+ const endpoint = tab.endpoint === undefined ? '<span class="muted">endpoint no anunciado</span>' : `<code>${escapeHtml(tab.endpoint.url)}</code>`;
577
+ const dir = tab.endpoint?.directory === undefined ? "" : `<div class="stat"><span class="k">Directorio</span><span class="v mono">${escapeHtml(tab.endpoint.directory)}</span></div>`;
578
+ const title = tab.endpoint?.title === undefined ? "" : `<div class="stat"><span class="k">Título</span><span class="v">${escapeHtml(tab.endpoint.title)}</span></div>`;
579
+ const s = tab.summary;
580
+ return [
581
+ '<section class="panel">',
582
+ `<h2>Sesión ${escapeHtml(shortSessionID(tab.sessionID))} ${statusBadge(tab.status)}</h2>`,
583
+ '<div class="grid">',
584
+ `<div class="stat"><span class="k">Endpoint</span><span class="v">${endpoint}</span></div>`,
585
+ dir,
586
+ title,
587
+ `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(s.realCostUSD)}</span></div>`,
588
+ `<div class="stat"><span class="k">Tokens in/out</span><span class="v">${s.tokensIn}/${s.tokensOut}</span></div>`,
589
+ `<div class="stat"><span class="k">Local/Frontier</span><span class="v"><span class="local">${s.localCount}</span>/<span class="frontier">${s.frontierCount}</span></span></div>`,
590
+ `<div class="stat"><span class="k">Toolcalls</span><span class="v">${s.toolcalls.count} (${avgMs(s.toolcalls)})</span></div>`,
591
+ "</div>",
592
+ "</section>"
593
+ ].join("");
594
+ }
595
+ function terminalPanel(tab) {
596
+ return [
597
+ '<section class="panel term">',
598
+ "<h2>Terminal (sesión)</h2>",
599
+ '<pre class="termlog" aria-live="polite"></pre>',
600
+ '<form class="terminput" autocomplete="off">',
601
+ `<input type="text" name="text" placeholder="Enviar prompt a ${escapeHtml(shortSessionID(tab.sessionID))}…" aria-label="Prompt a la sesión">`,
602
+ '<button type="submit">Enviar</button>',
603
+ "</form>",
604
+ "</section>"
605
+ ].join("");
606
+ }
607
+ function ptyPanel(tab) {
608
+ return [
609
+ `<section class="panel pty" data-pty-session="${escapeHtml(tab.sessionID)}">`,
610
+ "<h2>Terminal (PTY)</h2>",
611
+ '<pre class="ptylog" aria-live="polite"></pre>',
612
+ '<form class="ptyinput" autocomplete="off" hidden>',
613
+ '<input type="text" name="cmd" placeholder="Comando…" aria-label="Comando de la terminal">',
614
+ '<button type="submit">Ejecutar</button>',
615
+ "</form>",
616
+ '<button type="button" class="pty-start">Abrir shell</button>',
617
+ "</section>"
618
+ ].join("");
619
+ }
620
+ function sessionView(tab, terminalEnabled, ptyEnabled) {
621
+ const body = [
622
+ sessionHeaderPanel(tab),
623
+ terminalEnabled ? terminalPanel(tab) : "",
624
+ terminalEnabled && ptyEnabled ? ptyPanel(tab) : "",
625
+ routesPanel(tab.recentRoutes),
626
+ decisionsPanel(tab.decisions),
627
+ meetingsPanel(tab.meetings),
628
+ activityPanel(tab.activity)
629
+ ].join("");
630
+ return `<div class="view" data-view="${escapeHtml(tab.sessionID)}" hidden>${body}</div>`;
631
+ }
258
632
  var STYLE = `
259
633
  :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
260
634
  *{box-sizing:border-box}
@@ -287,6 +661,35 @@ ul.items .box{font-family:monospace}
287
661
  .tag{color:var(--frontier);font-size:12px}
288
662
  .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
289
663
  code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
664
+ .mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
665
+ .tabs{display:flex;gap:6px;padding:12px 24px 0;flex-wrap:wrap;border-bottom:1px solid var(--line)}
666
+ .tabs .tab{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-bottom:none;border-radius:8px 8px 0 0;padding:8px 14px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:6px}
667
+ .tabs .tab.active{background:var(--bg);border-color:var(--local);color:var(--fg)}
668
+ .dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--muted)}
669
+ .badge{font-size:11px;padding:1px 8px;border-radius:10px;border:1px solid var(--line);color:var(--muted);vertical-align:middle}
670
+ .status-running{background:var(--good)}
671
+ .badge.status-running{color:var(--good);border-color:var(--good)}
672
+ .status-idle{background:var(--muted)}
673
+ .badge.status-idle{color:var(--muted)}
674
+ .status-waiting-input{background:var(--frontier)}
675
+ .badge.status-waiting-input{color:var(--frontier);border-color:var(--frontier)}
676
+ .status-ended{background:#6e7681}
677
+ .badge.status-ended{color:#6e7681}
678
+ .term .termlog{background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap}
679
+ .term .tl{padding:1px 0}
680
+ .term .tl.input{color:var(--local)}
681
+ .term .tl.tool{color:var(--frontier)}
682
+ .term .tl.status{color:var(--muted)}
683
+ .term .tl.error{color:#f85149}
684
+ .term .tl.permission{color:var(--frontier);font-weight:600}
685
+ .term form.terminput{display:flex;gap:8px;margin-top:10px}
686
+ .term form.terminput input{flex:1;background:#0b0d11;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit}
687
+ .term form.terminput button{background:var(--local);color:#04122b;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600}
688
+ .pty .ptylog{background:#05070a;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap;color:#c8d1dc}
689
+ .pty form.ptyinput{display:flex;gap:8px;margin-top:10px}
690
+ .pty form.ptyinput input{flex:1;background:#05070a;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit}
691
+ .pty form.ptyinput button,.pty .pty-start{background:var(--frontier);color:#1a1200;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600;margin-top:8px}
692
+ .pty .pty-start:disabled{opacity:.5;cursor:default}
290
693
  `;
291
694
  var CLIENT_JS = `
292
695
  (function(){
@@ -305,11 +708,127 @@ var CLIENT_JS = `
305
708
  }
306
709
  var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
307
710
  setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
711
+
712
+ function selectTab(name){
713
+ var tabs=document.querySelectorAll('#tabbar .tab');
714
+ for(var i=0;i<tabs.length;i++){tabs[i].classList.toggle('active',tabs[i].getAttribute('data-tab')===name)}
715
+ var views=document.querySelectorAll('main .view');
716
+ var matched=false;
717
+ for(var j=0;j<views.length;j++){
718
+ var show=views[j].getAttribute('data-view')===name;
719
+ views[j].hidden=!show;
720
+ if(show){matched=true;if(name!=='all'){connectTerminal(views[j],name)}}
721
+ }
722
+ if(!matched){return false}
723
+ try{sessionStorage.setItem('openteam.tab',name)}catch(e){}
724
+ return true;
725
+ }
726
+ function connectTerminal(view,id){
727
+ if(view.__wired){return}
728
+ var root=document.documentElement;
729
+ var token=root.getAttribute('data-token');
730
+ if(root.getAttribute('data-terminal')!=='1'||!token){return}
731
+ var log=view.querySelector('.termlog');
732
+ if(!log){return}
733
+ view.__wired=true;
734
+ function append(cls,text){
735
+ var line=document.createElement('div');
736
+ line.className='tl '+cls;line.textContent=text;
737
+ log.appendChild(line);log.scrollTop=log.scrollHeight;
738
+ }
739
+ var q='/console/session/'+encodeURIComponent(id);
740
+ var tk='token='+encodeURIComponent(token);
741
+ if('EventSource' in window){
742
+ try{
743
+ var es=new EventSource(q+'/stream?'+tk);
744
+ es.addEventListener('frame',function(e){try{var f=JSON.parse(e.data);append(f.kind,f.text)}catch(_){}});
745
+ es.onerror=function(){/* reconecta solo */};
746
+ }catch(_){/* ignore */}
747
+ }
748
+ var form=view.querySelector('form.terminput');
749
+ if(form){
750
+ form.addEventListener('submit',function(e){
751
+ e.preventDefault();
752
+ var inp=form.querySelector('input[name=text]');
753
+ var text=inp.value;
754
+ if(!text){return}
755
+ inp.value='';append('input','› '+text);
756
+ fetch(q+'/input?'+tk,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({text:text})})
757
+ .then(function(r){if(!r.ok){append('error','✖ envío rechazado ('+r.status+')')}})
758
+ .catch(function(){append('error','✖ no se pudo enviar')});
759
+ });
760
+ }
761
+ connectPty(view,id,q,tk);
762
+ }
763
+ function connectPty(view,id,q,tk){
764
+ var root=document.documentElement;
765
+ if(root.getAttribute('data-pty')!=='1'){return}
766
+ var pane=view.querySelector('.pty');
767
+ if(!pane){return}
768
+ var log=pane.querySelector('.ptylog');
769
+ var startBtn=pane.querySelector('.pty-start');
770
+ var input=pane.querySelector('form.ptyinput');
771
+ function plog(text){
772
+ var d=document.createElement('span');d.textContent=text;
773
+ log.appendChild(d);log.scrollTop=log.scrollHeight;
774
+ }
775
+ if(!startBtn){return}
776
+ startBtn.addEventListener('click',function(){
777
+ startBtn.disabled=true;plog('Abriendo shell…
778
+ ');
779
+ fetch(q+'/pty?'+tk,{method:'POST',headers:{'content-type':'application/json'},body:'{}'})
780
+ .then(function(r){return r.ok?r.json():Promise.reject(r.status)})
781
+ .then(function(info){openPtyWs(info.wsUrl)})
782
+ .catch(function(s){plog('✖ no se pudo abrir PTY ('+s+')
783
+ ');startBtn.disabled=false});
784
+ });
785
+ function openPtyWs(url){
786
+ try{
787
+ var ws=new WebSocket(url);
788
+ ws.binaryType='arraybuffer';
789
+ ws.onmessage=function(e){
790
+ var data=e.data;
791
+ if(data instanceof ArrayBuffer){data=new TextDecoder().decode(new Uint8Array(data))}
792
+ plog(data);
793
+ };
794
+ ws.onclose=function(){plog('
795
+ [shell cerrada]
796
+ ');startBtn.disabled=false};
797
+ ws.onerror=function(){plog('
798
+ ✖ error de WebSocket
799
+ ')};
800
+ if(input){
801
+ input.hidden=false;
802
+ input.addEventListener('submit',function(e){
803
+ e.preventDefault();
804
+ var c=input.querySelector('input[name=cmd]');
805
+ if(!c.value){return}
806
+ ws.send(c.value+'\r');c.value='';
807
+ });
808
+ }
809
+ }catch(_){plog('
810
+ ✖ WebSocket no disponible
811
+ ');startBtn.disabled=false}
812
+ }
813
+ }
814
+ var bar=document.getElementById('tabbar');
815
+ if(bar){
816
+ bar.addEventListener('click',function(e){
817
+ var t=e.target;
818
+ while(t&&t!==bar&&!t.getAttribute('data-tab')){t=t.parentNode}
819
+ if(t&&t.getAttribute('data-tab')){selectTab(t.getAttribute('data-tab'))}
820
+ });
821
+ }
822
+ var saved='all';
823
+ try{saved=sessionStorage.getItem('openteam.tab')||'all'}catch(e){}
824
+ if(!selectTab(saved)){selectTab('all')}
308
825
  })();
309
826
  `;
310
- function renderDashboardHtml(state, options = {}) {
827
+ function renderConsoleHtml(state, options = {}) {
311
828
  const refreshMs = options.refreshMs ?? 2000;
312
- const body = [
829
+ const terminalEnabled = options.terminalEnabled === true;
830
+ const ptyEnabled = options.ptyEnabled === true;
831
+ const allBody = [
313
832
  summaryPanel(state.cost),
314
833
  totalsPanel(state.totals),
315
834
  sessionsPanel(state.sessions),
@@ -321,21 +840,26 @@ function renderDashboardHtml(state, options = {}) {
321
840
  meetingsPanel(state.meetings),
322
841
  activityPanel(state.activity)
323
842
  ].join("");
843
+ const allView = `<div class="view" data-view="all">${allBody}</div>`;
844
+ const sessionViews = state.sessionsDetail.map((tab) => sessionView(tab, terminalEnabled, ptyEnabled)).join("");
845
+ const body = allView + sessionViews;
324
846
  const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
847
+ const tokenAttr = terminalEnabled && options.consoleToken !== undefined ? ` data-token="${escapeHtml(options.consoleToken)}" data-terminal="1"${ptyEnabled ? ' data-pty="1"' : ""}` : "";
325
848
  return [
326
849
  "<!doctype html>",
327
- `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}">`,
850
+ `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}"${tokenAttr}>`,
328
851
  "<head>",
329
852
  '<meta charset="utf-8">',
330
853
  '<meta name="viewport" content="width=device-width,initial-scale=1">',
331
- "<title>openteam dashboard</title>",
854
+ "<title>openteam console</title>",
332
855
  `<style>${STYLE}</style>`,
333
856
  "</head>",
334
857
  "<body>",
335
858
  "<header>",
336
- "<h1>openteam dashboard</h1>",
859
+ "<h1>openteam console</h1>",
337
860
  `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
338
861
  "</header>",
862
+ tabBar(state.sessionsDetail),
339
863
  `<main>${body}</main>`,
340
864
  `<script>${CLIENT_JS}</script>`,
341
865
  "</body>",
@@ -490,6 +1014,25 @@ function applyToolcall(session, event) {
490
1014
  session.toolcalls.byTool[event.tool] = byTool;
491
1015
  pushUnique(session.agents, event.agent);
492
1016
  }
1017
+ function applyEndpoint(session, event) {
1018
+ if (session.endpoint !== undefined && session.endpoint.lastSeenTs >= event.ts) {
1019
+ return;
1020
+ }
1021
+ const endpoint = {
1022
+ url: event.url,
1023
+ lastSeenTs: event.ts
1024
+ };
1025
+ if (event.directory !== undefined) {
1026
+ endpoint.directory = event.directory;
1027
+ }
1028
+ if (event.worktree !== undefined) {
1029
+ endpoint.worktree = event.worktree;
1030
+ }
1031
+ if (event.title !== undefined) {
1032
+ endpoint.title = event.title;
1033
+ }
1034
+ session.endpoint = endpoint;
1035
+ }
493
1036
  function mergeTool(target, source) {
494
1037
  target.count += source.count;
495
1038
  target.ok += source.ok;
@@ -593,6 +1136,9 @@ function aggregateEvents(events) {
593
1136
  case "toolcall":
594
1137
  applyToolcall(sessionFor(event.sessionID, event.ts), event);
595
1138
  break;
1139
+ case "session-endpoint":
1140
+ applyEndpoint(sessionFor(event.sessionID, event.ts), event);
1141
+ break;
596
1142
  case "decision":
597
1143
  decisions.push(toDecision(event));
598
1144
  break;
@@ -636,71 +1182,76 @@ function aggregateEvents(events) {
636
1182
  }
637
1183
 
638
1184
  // src/telemetry/events.ts
639
- import { z as z3 } from "zod";
1185
+ import { z as z4 } from "zod";
640
1186
 
641
1187
  // src/capabilities/types.ts
642
- import { z } from "zod";
643
- var CapabilityTierSchema = z.union([
644
- z.literal(0),
645
- z.literal(1),
646
- z.literal(2),
647
- z.literal(3),
648
- z.literal(4),
649
- z.literal(5)
1188
+ import { z as z2 } from "zod";
1189
+ var CapabilityTierSchema = z2.union([
1190
+ z2.literal(0),
1191
+ z2.literal(1),
1192
+ z2.literal(2),
1193
+ z2.literal(3),
1194
+ z2.literal(4),
1195
+ z2.literal(5)
650
1196
  ]);
651
- var ComplexityTierSchema = z.enum([
1197
+ var ComplexityTierSchema = z2.enum([
652
1198
  "trivial",
653
1199
  "simple",
654
1200
  "moderate",
655
1201
  "hard"
656
1202
  ]);
657
- var ModelCapabilityProfileSchema = z.object({
658
- ref: z.object({
659
- providerID: z.string().min(1),
660
- modelID: z.string().min(1)
1203
+ var ModelCapabilityProfileSchema = z2.object({
1204
+ ref: z2.object({
1205
+ providerID: z2.string().min(1),
1206
+ modelID: z2.string().min(1)
661
1207
  }),
662
- kind: z.enum(["local", "frontier", "router"]),
663
- contextWindow: z.number().int().positive(),
664
- maxOutputTokens: z.number().int().positive(),
665
- supportsToolCalling: z.boolean(),
666
- supportsVision: z.boolean(),
1208
+ kind: z2.enum(["local", "frontier", "router"]),
1209
+ contextWindow: z2.number().int().positive(),
1210
+ maxOutputTokens: z2.number().int().positive(),
1211
+ supportsToolCalling: z2.boolean(),
1212
+ supportsVision: z2.boolean(),
667
1213
  reasoningTier: CapabilityTierSchema,
668
1214
  codeQualityTier: CapabilityTierSchema,
669
- costPer1M: z.object({
670
- inputUSD: z.number().min(0),
671
- outputUSD: z.number().min(0)
1215
+ costPer1M: z2.object({
1216
+ inputUSD: z2.number().min(0),
1217
+ outputUSD: z2.number().min(0)
672
1218
  }),
673
- availability: z.enum(["available", "degraded", "unavailable"])
1219
+ availability: z2.enum(["available", "degraded", "unavailable"])
674
1220
  });
675
1221
 
676
1222
  // src/config/schema.ts
677
- import { z as z2 } from "zod";
678
- var ModelRefSchema = z2.object({
679
- providerID: z2.string().min(1),
680
- modelID: z2.string().min(1)
1223
+ import { z as z3 } from "zod";
1224
+ var ModelRefSchema = z3.object({
1225
+ providerID: z3.string().min(1),
1226
+ modelID: z3.string().min(1)
681
1227
  });
682
- var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
683
- var PrivacyModeSchema = z2.enum([
1228
+ var RouterModeSchema = z3.enum(["economy", "balanced", "quality"]);
1229
+ var PrivacyModeSchema = z3.enum([
684
1230
  "forceLocalOnSensitive",
685
1231
  "consentBeforeFrontier",
686
1232
  "off"
687
1233
  ]);
688
- var BaselineModeSchema = z2.enum(["auto", "pinned"]);
689
- var DashboardHostSchema = z2.enum(["127.0.0.1", "localhost"]);
690
- var DashboardConfigSchema = z2.object({
691
- host: DashboardHostSchema.default("127.0.0.1"),
692
- port: z2.number().int().min(1024).max(65535).default(4599),
693
- autoPortFallback: z2.boolean().default(true),
694
- refreshMs: z2.number().int().min(250).default(2000),
695
- recentRoutes: z2.number().int().positive().default(50),
696
- openBrowser: z2.boolean().default(false)
1234
+ var BaselineModeSchema = z3.enum(["auto", "pinned"]);
1235
+ var ConsoleHostSchema = z3.enum(["127.0.0.1", "localhost"]);
1236
+ var ConsoleConfigSchema = z3.object({
1237
+ host: ConsoleHostSchema.default("127.0.0.1"),
1238
+ port: z3.number().int().min(1024).max(65535).default(4599),
1239
+ autoPortFallback: z3.boolean().default(true),
1240
+ refreshMs: z3.number().int().min(250).default(2000),
1241
+ recentRoutes: z3.number().int().positive().default(50),
1242
+ openBrowser: z3.boolean().default(false),
1243
+ terminal: z3.object({
1244
+ enabled: z3.boolean().default(true),
1245
+ pty: z3.boolean().optional()
1246
+ }).default({ enabled: true })
697
1247
  }).default({
698
1248
  host: "127.0.0.1",
699
1249
  port: 4599,
700
1250
  autoPortFallback: true,
701
1251
  refreshMs: 2000,
702
1252
  recentRoutes: 50,
703
- openBrowser: false
1253
+ openBrowser: false,
1254
+ terminal: { enabled: true }
704
1255
  });
705
1256
  var defaultLocalModel = {
706
1257
  providerID: "ollama",
@@ -710,15 +1261,15 @@ var defaultFrontierModel = {
710
1261
  providerID: "anthropic",
711
1262
  modelID: "claude-sonnet-4-5"
712
1263
  };
713
- var LocalRuntimeSchema = z2.object({
714
- id: z2.enum(["ollama", "lmstudio", "foundry-local"]),
715
- enabled: z2.boolean().default(true),
716
- baseURL: z2.string().url().optional(),
717
- discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
1264
+ var LocalRuntimeSchema = z3.object({
1265
+ id: z3.enum(["ollama", "lmstudio", "foundry-local"]),
1266
+ enabled: z3.boolean().default(true),
1267
+ baseURL: z3.string().url().optional(),
1268
+ discovery: z3.enum(["cli", "sdk", "manual"]).optional(),
718
1269
  defaultModel: ModelRefSchema
719
1270
  });
720
- var OpenTeamConfigSchema = z2.object({
721
- baseline: z2.object({
1271
+ var OpenTeamConfigObjectSchema = z3.object({
1272
+ baseline: z3.object({
722
1273
  mode: BaselineModeSchema.default("auto"),
723
1274
  pinnedModel: ModelRefSchema.nullable().default(null),
724
1275
  hardDefault: ModelRefSchema.default(defaultFrontierModel)
@@ -727,19 +1278,19 @@ var OpenTeamConfigSchema = z2.object({
727
1278
  pinnedModel: null,
728
1279
  hardDefault: defaultFrontierModel
729
1280
  }),
730
- router: z2.object({
1281
+ router: z3.object({
731
1282
  mode: RouterModeSchema.default("balanced"),
732
1283
  localDefault: ModelRefSchema.default(defaultLocalModel),
733
- trivialPromptMaxChars: z2.number().int().positive().default(280),
734
- frontierPromptMinChars: z2.number().int().positive().default(2000)
1284
+ trivialPromptMaxChars: z3.number().int().positive().default(280),
1285
+ frontierPromptMinChars: z3.number().int().positive().default(2000)
735
1286
  }).default({
736
1287
  mode: "balanced",
737
1288
  localDefault: defaultLocalModel,
738
1289
  trivialPromptMaxChars: 280,
739
1290
  frontierPromptMinChars: 2000
740
1291
  }),
741
- local: z2.object({
742
- runtimes: z2.array(LocalRuntimeSchema).min(1).default([
1292
+ local: z3.object({
1293
+ runtimes: z3.array(LocalRuntimeSchema).min(1).default([
743
1294
  {
744
1295
  id: "ollama",
745
1296
  enabled: true,
@@ -757,113 +1308,122 @@ var OpenTeamConfigSchema = z2.object({
757
1308
  }
758
1309
  ]
759
1310
  }),
760
- budgets: z2.object({
761
- sessionUSD: z2.number().positive().optional(),
762
- monthlyUSD: z2.number().positive().optional(),
763
- frontierTokensPerSession: z2.number().int().positive().optional(),
764
- hardStopOnBudgetExhaustion: z2.boolean().default(false)
1311
+ budgets: z3.object({
1312
+ sessionUSD: z3.number().positive().optional(),
1313
+ monthlyUSD: z3.number().positive().optional(),
1314
+ frontierTokensPerSession: z3.number().int().positive().optional(),
1315
+ hardStopOnBudgetExhaustion: z3.boolean().default(false)
765
1316
  }).default({ hardStopOnBudgetExhaustion: false }),
766
1317
  privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
767
- dashboard: DashboardConfigSchema
1318
+ console: ConsoleConfigSchema
768
1319
  });
1320
+ var OpenTeamConfigSchema = OpenTeamConfigObjectSchema;
769
1321
 
770
1322
  // src/telemetry/events.ts
771
1323
  var EVENT_SCHEMA_VERSION = 1;
772
- var EventBaseSchema = z3.object({
773
- v: z3.literal(EVENT_SCHEMA_VERSION),
774
- ts: z3.number().finite(),
775
- sessionID: z3.string().min(1)
1324
+ var EventBaseSchema = z4.object({
1325
+ v: z4.literal(EVENT_SCHEMA_VERSION),
1326
+ ts: z4.number().finite(),
1327
+ sessionID: z4.string().min(1)
776
1328
  });
777
1329
  var RouteEventSchema = EventBaseSchema.extend({
778
- type: z3.literal("route"),
779
- promptHash: z3.string().min(1),
780
- promptChars: z3.number().int().min(0),
1330
+ type: z4.literal("route"),
1331
+ promptHash: z4.string().min(1),
1332
+ promptChars: z4.number().int().min(0),
781
1333
  tier: ComplexityTierSchema,
782
- routeKind: z3.enum(["local", "frontier"]),
1334
+ routeKind: z4.enum(["local", "frontier"]),
783
1335
  selected: ModelRefSchema,
784
- rationale: z3.string(),
785
- estimatedCostUSD: z3.number().finite().min(0),
786
- baselineCostUSD: z3.number().finite().min(0),
787
- estimatedSavingsUSD: z3.number().finite(),
788
- budgetAction: z3.string().min(1),
789
- tokensIn: z3.number().int().min(0).optional(),
790
- tokensOut: z3.number().int().min(0).optional(),
791
- decisionID: z3.string().min(1).optional(),
792
- agent: z3.string().min(1).optional(),
793
- success: z3.boolean().optional(),
794
- batchID: z3.string().min(1).optional(),
795
- failureReason: z3.string().min(1).optional(),
796
- failureStage: z3.string().min(1).optional()
1336
+ rationale: z4.string(),
1337
+ estimatedCostUSD: z4.number().finite().min(0),
1338
+ baselineCostUSD: z4.number().finite().min(0),
1339
+ estimatedSavingsUSD: z4.number().finite(),
1340
+ budgetAction: z4.string().min(1),
1341
+ tokensIn: z4.number().int().min(0).optional(),
1342
+ tokensOut: z4.number().int().min(0).optional(),
1343
+ decisionID: z4.string().min(1).optional(),
1344
+ agent: z4.string().min(1).optional(),
1345
+ success: z4.boolean().optional(),
1346
+ batchID: z4.string().min(1).optional(),
1347
+ failureReason: z4.string().min(1).optional(),
1348
+ failureStage: z4.string().min(1).optional()
797
1349
  });
798
1350
  var MessageEventSchema = EventBaseSchema.extend({
799
- type: z3.literal("message"),
800
- providerID: z3.string().min(1),
801
- modelID: z3.string().min(1),
802
- agent: z3.string().min(1).optional(),
803
- mode: z3.string().min(1).optional(),
804
- messageID: z3.string().min(1).optional(),
805
- costUSD: z3.number().finite().min(0),
806
- tokensIn: z3.number().int().min(0),
807
- tokensOut: z3.number().int().min(0),
808
- tokensReasoning: z3.number().int().min(0),
809
- tokensCacheRead: z3.number().int().min(0),
810
- tokensCacheWrite: z3.number().int().min(0),
811
- durationMs: z3.number().int().min(0)
1351
+ type: z4.literal("message"),
1352
+ providerID: z4.string().min(1),
1353
+ modelID: z4.string().min(1),
1354
+ agent: z4.string().min(1).optional(),
1355
+ mode: z4.string().min(1).optional(),
1356
+ messageID: z4.string().min(1).optional(),
1357
+ costUSD: z4.number().finite().min(0),
1358
+ tokensIn: z4.number().int().min(0),
1359
+ tokensOut: z4.number().int().min(0),
1360
+ tokensReasoning: z4.number().int().min(0),
1361
+ tokensCacheRead: z4.number().int().min(0),
1362
+ tokensCacheWrite: z4.number().int().min(0),
1363
+ durationMs: z4.number().int().min(0)
812
1364
  });
813
1365
  var ToolcallEventSchema = EventBaseSchema.extend({
814
- type: z3.literal("toolcall"),
815
- tool: z3.string().min(1),
816
- callID: z3.string().min(1),
817
- agent: z3.string().min(1).optional(),
818
- durationMs: z3.number().int().min(0),
819
- ok: z3.boolean(),
820
- title: z3.string().optional()
1366
+ type: z4.literal("toolcall"),
1367
+ tool: z4.string().min(1),
1368
+ callID: z4.string().min(1),
1369
+ agent: z4.string().min(1).optional(),
1370
+ durationMs: z4.number().int().min(0),
1371
+ ok: z4.boolean(),
1372
+ title: z4.string().optional()
821
1373
  });
822
1374
  var MeetingEventSchema = EventBaseSchema.extend({
823
- type: z3.literal("meeting"),
824
- batchID: z3.string().min(1),
825
- purpose: z3.string().optional(),
826
- roles: z3.array(z3.string().min(1)),
827
- decisionIDs: z3.array(z3.string().min(1)).optional()
1375
+ type: z4.literal("meeting"),
1376
+ batchID: z4.string().min(1),
1377
+ purpose: z4.string().optional(),
1378
+ roles: z4.array(z4.string().min(1)),
1379
+ decisionIDs: z4.array(z4.string().min(1)).optional()
828
1380
  });
829
1381
  var DecisionEventSchema = EventBaseSchema.extend({
830
- type: z3.literal("decision"),
831
- agent: z3.string().min(1).optional(),
832
- summary: z3.string().min(1),
833
- tags: z3.array(z3.string().min(1)).optional()
1382
+ type: z4.literal("decision"),
1383
+ agent: z4.string().min(1).optional(),
1384
+ summary: z4.string().min(1),
1385
+ tags: z4.array(z4.string().min(1)).optional()
834
1386
  });
835
1387
  var ActivityEventSchema = EventBaseSchema.extend({
836
- type: z3.literal("activity"),
837
- kind: z3.enum(["route", "agent", "decision", "commit"]),
838
- agent: z3.string().min(1).optional(),
839
- summary: z3.string().min(1)
1388
+ type: z4.literal("activity"),
1389
+ kind: z4.enum(["route", "agent", "decision", "commit"]),
1390
+ agent: z4.string().min(1).optional(),
1391
+ summary: z4.string().min(1)
1392
+ });
1393
+ var SessionEndpointEventSchema = EventBaseSchema.extend({
1394
+ type: z4.literal("session-endpoint"),
1395
+ url: z4.string().url(),
1396
+ directory: z4.string().min(1).optional(),
1397
+ worktree: z4.string().min(1).optional(),
1398
+ title: z4.string().min(1).optional()
840
1399
  });
841
- var OpenTeamEventSchema = z3.discriminatedUnion("type", [
1400
+ var OpenTeamEventSchema = z4.discriminatedUnion("type", [
842
1401
  RouteEventSchema,
843
1402
  MessageEventSchema,
844
1403
  ToolcallEventSchema,
845
1404
  MeetingEventSchema,
846
1405
  DecisionEventSchema,
847
- ActivityEventSchema
1406
+ ActivityEventSchema,
1407
+ SessionEndpointEventSchema
848
1408
  ]);
849
1409
 
850
1410
  // src/telemetry/types.ts
851
- import { z as z4 } from "zod";
852
- var CostRecordSchema = z4.object({
853
- ts: z4.number().finite(),
854
- sessionID: z4.string().min(1).optional(),
855
- promptHash: z4.string().min(1),
856
- promptChars: z4.number().int().min(0),
1411
+ import { z as z5 } from "zod";
1412
+ var CostRecordSchema = z5.object({
1413
+ ts: z5.number().finite(),
1414
+ sessionID: z5.string().min(1).optional(),
1415
+ promptHash: z5.string().min(1),
1416
+ promptChars: z5.number().int().min(0),
857
1417
  tier: ComplexityTierSchema,
858
- routeKind: z4.enum(["local", "frontier"]),
1418
+ routeKind: z5.enum(["local", "frontier"]),
859
1419
  selected: ModelRefSchema,
860
- rationale: z4.string(),
861
- estimatedCostUSD: z4.number().finite().min(0),
862
- baselineCostUSD: z4.number().finite().min(0),
863
- estimatedSavingsUSD: z4.number().finite(),
864
- budgetAction: z4.string().min(1),
865
- tokensIn: z4.number().int().min(0).optional(),
866
- tokensOut: z4.number().int().min(0).optional()
1420
+ rationale: z5.string(),
1421
+ estimatedCostUSD: z5.number().finite().min(0),
1422
+ baselineCostUSD: z5.number().finite().min(0),
1423
+ estimatedSavingsUSD: z5.number().finite(),
1424
+ budgetAction: z5.string().min(1),
1425
+ tokensIn: z5.number().int().min(0).optional(),
1426
+ tokensOut: z5.number().int().min(0).optional()
867
1427
  });
868
1428
 
869
1429
  // src/telemetry/read.ts
@@ -994,7 +1554,7 @@ async function readSessionEvents(dir, deps) {
994
1554
  return events.sort((a, b) => a.ts - b.ts);
995
1555
  }
996
1556
 
997
- // src/dashboard/backlog.ts
1557
+ // src/console/backlog.ts
998
1558
  var ITEM_RE = /^\s*[-*+]\s+\[( |x|X)\]\s+(.*)$/;
999
1559
  var ASSIGNEE_RE = /^\[@([^\]]+)\]\s*(.*)$/;
1000
1560
  function parseAssignee(text) {
@@ -1042,7 +1602,27 @@ function buildLoopSnapshot(backlogPath, items) {
1042
1602
  };
1043
1603
  }
1044
1604
 
1045
- // src/dashboard/state.ts
1605
+ // src/console/session.ts
1606
+ var DEFAULT_SESSION_STATUS_THRESHOLDS = {
1607
+ runningWindowMs: 30000,
1608
+ idleWindowMs: 30 * 60000
1609
+ };
1610
+ function deriveSessionStatus(lastTs, nowMs, options = {}) {
1611
+ const thresholds = options.thresholds ?? DEFAULT_SESSION_STATUS_THRESHOLDS;
1612
+ const age = nowMs - lastTs;
1613
+ if (age > thresholds.idleWindowMs) {
1614
+ return "ended";
1615
+ }
1616
+ if (options.waitingInput === true) {
1617
+ return "waiting-input";
1618
+ }
1619
+ if (age <= thresholds.runningWindowMs) {
1620
+ return "running";
1621
+ }
1622
+ return "idle";
1623
+ }
1624
+
1625
+ // src/console/state.ts
1046
1626
  var DEFAULT_RECENT_ROUTES = 50;
1047
1627
  var DEFAULT_ACTIVITY_LIMIT = 100;
1048
1628
  var DEFAULT_DECISIONS_LIMIT = 100;
@@ -1085,9 +1665,9 @@ function meetingToActivity(meeting) {
1085
1665
  summary: `Reunión (${roles})${purpose}`
1086
1666
  };
1087
1667
  }
1088
- function activityFeed(aggregate, limit) {
1668
+ function composeActivity(activityViews, decisions, meetings, limit) {
1089
1669
  const entries = [
1090
- ...aggregate.activity.map((view) => {
1670
+ ...activityViews.map((view) => {
1091
1671
  const entry = {
1092
1672
  ts: view.ts,
1093
1673
  kind: view.kind,
@@ -1098,11 +1678,39 @@ function activityFeed(aggregate, limit) {
1098
1678
  }
1099
1679
  return entry;
1100
1680
  }),
1101
- ...aggregate.decisions.map(decisionToActivity),
1102
- ...aggregate.meetings.map(meetingToActivity)
1681
+ ...decisions.map(decisionToActivity),
1682
+ ...meetings.map(meetingToActivity)
1103
1683
  ];
1104
1684
  return entries.sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
1105
1685
  }
1686
+ function activityFeed(aggregate, limit) {
1687
+ return composeActivity(aggregate.activity, aggregate.decisions, aggregate.meetings, limit);
1688
+ }
1689
+ function buildSessionTabs(aggregate, costRecords, nowMs, limits, options) {
1690
+ return aggregate.sessions.map((summary) => {
1691
+ const id = summary.sessionID;
1692
+ const routes = costRecords.filter((record) => record.sessionID === id);
1693
+ const decisions = aggregate.decisions.filter((decision) => decision.sessionID === id).slice(0, Math.max(0, limits.decisionsLimit));
1694
+ const meetings = aggregate.meetings.filter((meeting) => meeting.sessionID === id).slice(0, Math.max(0, limits.meetingsLimit));
1695
+ const activity = composeActivity(aggregate.activity.filter((view) => view.sessionID === id), aggregate.decisions.filter((decision) => decision.sessionID === id), aggregate.meetings.filter((meeting) => meeting.sessionID === id), limits.activityLimit);
1696
+ const tab = {
1697
+ sessionID: id,
1698
+ status: deriveSessionStatus(summary.lastTs, nowMs, {
1699
+ thresholds: options.thresholds,
1700
+ waitingInput: options.waitingInputSessionIDs.has(id)
1701
+ }),
1702
+ summary,
1703
+ recentRoutes: recentRoutes(routes, limits.recentRoutesLimit),
1704
+ decisions,
1705
+ meetings,
1706
+ activity
1707
+ };
1708
+ if (summary.endpoint !== undefined) {
1709
+ tab.endpoint = summary.endpoint;
1710
+ }
1711
+ return tab;
1712
+ });
1713
+ }
1106
1714
  function loopFrom(backlog) {
1107
1715
  if (backlog === undefined) {
1108
1716
  return;
@@ -1113,19 +1721,25 @@ function loopFrom(backlog) {
1113
1721
  }
1114
1722
  return snapshot;
1115
1723
  }
1116
- function buildDashboardState(inputs) {
1724
+ function buildConsoleState(inputs) {
1117
1725
  const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
1118
1726
  const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
1119
1727
  const decisionsLimit = inputs.decisionsLimit ?? DEFAULT_DECISIONS_LIMIT;
1120
1728
  const meetingsLimit = inputs.meetingsLimit ?? DEFAULT_MEETINGS_LIMIT;
1121
1729
  const aggregate = aggregateEvents(inputs.events);
1122
1730
  const costRecords = inputs.events.filter(isRoute).map(routeEventToCostRecord);
1731
+ const nowMs = Date.parse(inputs.generatedAt);
1732
+ const sessionsDetail = buildSessionTabs(aggregate, costRecords, Number.isNaN(nowMs) ? 0 : nowMs, { recentRoutesLimit, activityLimit, decisionsLimit, meetingsLimit }, {
1733
+ thresholds: inputs.statusThresholds ?? DEFAULT_SESSION_STATUS_THRESHOLDS,
1734
+ waitingInputSessionIDs: new Set(inputs.waitingInputSessionIDs ?? [])
1735
+ });
1123
1736
  const state = {
1124
1737
  generatedAt: inputs.generatedAt,
1125
1738
  session: {},
1126
1739
  cost: summarizeCostRecords(costRecords),
1127
1740
  totals: aggregate.totals,
1128
1741
  sessions: aggregate.sessions,
1742
+ sessionsDetail,
1129
1743
  recentRoutes: recentRoutes(costRecords, recentRoutesLimit),
1130
1744
  decisions: aggregate.decisions.slice(0, Math.max(0, decisionsLimit)),
1131
1745
  meetings: aggregate.meetings.slice(0, Math.max(0, meetingsLimit)),
@@ -1145,24 +1759,201 @@ function buildDashboardState(inputs) {
1145
1759
  return state;
1146
1760
  }
1147
1761
 
1148
- // src/web/server.ts
1762
+ // src/web/console.ts
1149
1763
  var SSE_HEADERS = {
1150
1764
  "content-type": "text/event-stream",
1151
1765
  "cache-control": "no-cache",
1152
1766
  connection: "keep-alive"
1153
1767
  };
1154
1768
  var JSON_HEADERS = { "content-type": "application/json" };
1769
+ function bearerFromHeader(request) {
1770
+ const header = request.headers.authorization;
1771
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
1772
+ return;
1773
+ }
1774
+ const value = header.slice("Bearer ".length).trim();
1775
+ return value.length === 0 ? undefined : value;
1776
+ }
1777
+ function authorized(request, query, token) {
1778
+ const provided = tokenFromQuery(query) ?? bearerFromHeader(request);
1779
+ return provided !== undefined && safeEqualToken(provided, token);
1780
+ }
1781
+ async function readJsonBody(request, limit) {
1782
+ return new Promise((resolve) => {
1783
+ let raw = "";
1784
+ let aborted = false;
1785
+ request.on("data", (chunk) => {
1786
+ if (aborted) {
1787
+ return;
1788
+ }
1789
+ raw += chunk.toString("utf8");
1790
+ if (raw.length > limit) {
1791
+ aborted = true;
1792
+ resolve(undefined);
1793
+ }
1794
+ });
1795
+ request.on("end", () => {
1796
+ if (aborted) {
1797
+ return;
1798
+ }
1799
+ try {
1800
+ resolve(JSON.parse(raw));
1801
+ } catch {
1802
+ resolve(undefined);
1803
+ }
1804
+ });
1805
+ request.on("error", () => resolve(undefined));
1806
+ });
1807
+ }
1808
+ function parseConsolePath(path) {
1809
+ const match = /^\/console\/session\/([^/]+)\/(stream|input|permission|pty)$/.exec(path);
1810
+ if (match === null) {
1811
+ return;
1812
+ }
1813
+ return {
1814
+ sessionID: decodeURIComponent(match[1]),
1815
+ action: match[2]
1816
+ };
1817
+ }
1818
+ async function handleStream(request, response, sessionID, deps) {
1819
+ const endpoint = await deps.resolveEndpoint(sessionID);
1820
+ if (endpoint === undefined) {
1821
+ response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "no endpoint" }));
1822
+ return;
1823
+ }
1824
+ response.writeHead(200, SSE_HEADERS);
1825
+ response.write(`event: open
1826
+ data: {}
1827
+
1828
+ `);
1829
+ const controller = new AbortController;
1830
+ request.on("close", () => controller.abort());
1831
+ const client = deps.createClient(endpoint);
1832
+ try {
1833
+ await client.streamFrames(sessionID, (frame) => response.write(frameToSse(frame)), controller.signal);
1834
+ } catch {} finally {
1835
+ controller.abort();
1836
+ response.end();
1837
+ }
1838
+ }
1839
+ async function handleInput(request, response, sessionID, deps) {
1840
+ const endpoint = await deps.resolveEndpoint(sessionID);
1841
+ if (endpoint === undefined) {
1842
+ response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "no endpoint" }));
1843
+ return;
1844
+ }
1845
+ const body = await readJsonBody(request, MAX_INPUT_CHARS + 1024);
1846
+ const input = parseSessionInput(body);
1847
+ if (input === undefined) {
1848
+ response.writeHead(400, JSON_HEADERS).end(JSON.stringify({ error: "invalid input" }));
1849
+ return;
1850
+ }
1851
+ await deps.createClient(endpoint).sendPrompt(sessionID, input.text);
1852
+ response.writeHead(202, JSON_HEADERS).end(JSON.stringify({ ok: true }));
1853
+ }
1854
+ async function handlePermission(request, response, sessionID, deps) {
1855
+ const endpoint = await deps.resolveEndpoint(sessionID);
1856
+ if (endpoint === undefined) {
1857
+ response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "no endpoint" }));
1858
+ return;
1859
+ }
1860
+ const body = await readJsonBody(request, 4096);
1861
+ const record = typeof body === "object" && body !== null ? body : {};
1862
+ const permissionID = record.permissionID;
1863
+ const responseValue = record.response;
1864
+ if (typeof permissionID !== "string" || permissionID.length === 0 || responseValue !== "once" && responseValue !== "always" && responseValue !== "reject") {
1865
+ response.writeHead(400, JSON_HEADERS).end(JSON.stringify({ error: "invalid permission" }));
1866
+ return;
1867
+ }
1868
+ await deps.createClient(endpoint).respondPermission(sessionID, permissionID, responseValue);
1869
+ response.writeHead(202, JSON_HEADERS).end(JSON.stringify({ ok: true }));
1870
+ }
1871
+ async function handlePty(_request, response, sessionID, deps) {
1872
+ if (deps.ptyEnabled !== true || deps.createPtyClient === undefined) {
1873
+ response.writeHead(403, JSON_HEADERS).end(JSON.stringify({ error: "pty disabled" }));
1874
+ return;
1875
+ }
1876
+ const endpoint = await deps.resolveEndpoint(sessionID);
1877
+ if (endpoint === undefined) {
1878
+ response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "no endpoint" }));
1879
+ return;
1880
+ }
1881
+ const client = deps.createPtyClient(endpoint);
1882
+ const pty = await client.createPty({ command: "", title: "openteam" });
1883
+ if (pty === undefined) {
1884
+ response.writeHead(502, JSON_HEADERS).end(JSON.stringify({ error: "pty create failed" }));
1885
+ return;
1886
+ }
1887
+ const token = await client.connectToken(pty.id);
1888
+ if (token === undefined) {
1889
+ response.writeHead(502, JSON_HEADERS).end(JSON.stringify({ error: "connect token failed" }));
1890
+ return;
1891
+ }
1892
+ response.writeHead(200, JSON_HEADERS).end(JSON.stringify({
1893
+ ptyID: pty.id,
1894
+ wsUrl: client.wsUrl(pty.id, token.ticket)
1895
+ }));
1896
+ }
1897
+ async function handleConsoleRoute(request, response, url, deps) {
1898
+ if (!url.pathname.startsWith("/console/")) {
1899
+ return false;
1900
+ }
1901
+ const route = parseConsolePath(url.pathname);
1902
+ if (route === undefined) {
1903
+ response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "not found" }));
1904
+ return true;
1905
+ }
1906
+ if (!deps.terminalEnabled) {
1907
+ response.writeHead(403, JSON_HEADERS).end(JSON.stringify({ error: "terminal disabled" }));
1908
+ return true;
1909
+ }
1910
+ if (!authorized(request, url.searchParams, deps.token)) {
1911
+ response.writeHead(401, JSON_HEADERS).end(JSON.stringify({ error: "unauthorized" }));
1912
+ return true;
1913
+ }
1914
+ if (route.action === "stream" && request.method === "GET") {
1915
+ await handleStream(request, response, route.sessionID, deps);
1916
+ return true;
1917
+ }
1918
+ if (route.action === "input" && request.method === "POST") {
1919
+ await handleInput(request, response, route.sessionID, deps);
1920
+ return true;
1921
+ }
1922
+ if (route.action === "permission" && request.method === "POST") {
1923
+ await handlePermission(request, response, route.sessionID, deps);
1924
+ return true;
1925
+ }
1926
+ if (route.action === "pty" && request.method === "POST") {
1927
+ await handlePty(request, response, route.sessionID, deps);
1928
+ return true;
1929
+ }
1930
+ response.writeHead(405, JSON_HEADERS).end(JSON.stringify({ error: "method not allowed" }));
1931
+ return true;
1932
+ }
1933
+
1934
+ // src/web/server.ts
1935
+ var SSE_HEADERS2 = {
1936
+ "content-type": "text/event-stream",
1937
+ "cache-control": "no-cache",
1938
+ connection: "keep-alive"
1939
+ };
1940
+ var JSON_HEADERS2 = { "content-type": "application/json" };
1155
1941
  var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
1156
1942
  function isAddressInUse(error) {
1157
1943
  return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
1158
1944
  }
1159
1945
  async function renderState(deps) {
1160
- return renderDashboardHtml(buildDashboardState(await deps.readSnapshot()), {
1161
- refreshMs: deps.config.refreshMs
1946
+ return renderConsoleHtml(buildConsoleState(await deps.readSnapshot()), {
1947
+ refreshMs: deps.config.refreshMs,
1948
+ ...deps.console !== undefined ? {
1949
+ consoleToken: deps.console.token,
1950
+ terminalEnabled: deps.console.terminalEnabled,
1951
+ ...deps.console.ptyEnabled !== undefined ? { ptyEnabled: deps.console.ptyEnabled } : {}
1952
+ } : {}
1162
1953
  });
1163
1954
  }
1164
1955
  async function stateJson(deps) {
1165
- return JSON.stringify(buildDashboardState(await deps.readSnapshot()));
1956
+ return JSON.stringify(buildConsoleState(await deps.readSnapshot()));
1166
1957
  }
1167
1958
  function sseMessage(json) {
1168
1959
  return `event: state
@@ -1189,27 +1980,42 @@ function tryListen(server, host, port) {
1189
1980
  server.listen(port, host);
1190
1981
  });
1191
1982
  }
1192
- async function createDashboardServer(deps) {
1983
+ async function createConsoleServer(deps) {
1193
1984
  const create = deps.createServer ?? createHttpServer;
1194
1985
  const log = deps.log ?? ((message) => console.log(message));
1195
1986
  const clients = new Set;
1987
+ const resolveEndpoint = async (sessionID) => {
1988
+ const state = buildConsoleState(await deps.readSnapshot());
1989
+ return state.sessionsDetail.find((tab) => tab.sessionID === sessionID)?.endpoint?.url;
1990
+ };
1991
+ const consoleDeps = deps.console === undefined ? undefined : {
1992
+ token: deps.console.token,
1993
+ terminalEnabled: deps.console.terminalEnabled,
1994
+ resolveEndpoint,
1995
+ createClient: deps.console.createClient,
1996
+ ...deps.console.ptyEnabled !== undefined ? { ptyEnabled: deps.console.ptyEnabled } : {},
1997
+ ...deps.console.createPtyClient !== undefined ? { createPtyClient: deps.console.createPtyClient } : {}
1998
+ };
1196
1999
  const handle = async (request, response) => {
2000
+ const url2 = new URL(request.url ?? "/", "http://localhost");
2001
+ if (consoleDeps !== undefined && await handleConsoleRoute(request, response, url2, consoleDeps)) {
2002
+ return;
2003
+ }
1197
2004
  if (request.method !== "GET") {
1198
2005
  response.writeHead(405).end("method not allowed");
1199
2006
  return;
1200
2007
  }
1201
- const url2 = new URL(request.url ?? "/", "http://localhost");
1202
2008
  const path = url2.pathname;
1203
2009
  if (path === "/" || path === "/index.html") {
1204
2010
  response.writeHead(200, HTML_HEADERS).end(await renderState(deps));
1205
2011
  return;
1206
2012
  }
1207
2013
  if (path === "/api/state") {
1208
- response.writeHead(200, JSON_HEADERS).end(await stateJson(deps));
2014
+ response.writeHead(200, JSON_HEADERS2).end(await stateJson(deps));
1209
2015
  return;
1210
2016
  }
1211
2017
  if (path === "/healthz") {
1212
- response.writeHead(200, JSON_HEADERS).end(JSON.stringify({ ok: true }));
2018
+ response.writeHead(200, JSON_HEADERS2).end(JSON.stringify({ ok: true }));
1213
2019
  return;
1214
2020
  }
1215
2021
  if (path === "/favicon.ico") {
@@ -1217,7 +2023,7 @@ async function createDashboardServer(deps) {
1217
2023
  return;
1218
2024
  }
1219
2025
  if (path === "/events") {
1220
- response.writeHead(200, SSE_HEADERS);
2026
+ response.writeHead(200, SSE_HEADERS2);
1221
2027
  response.write(sseMessage(await stateJson(deps)));
1222
2028
  clients.add(response);
1223
2029
  request.on("close", () => {
@@ -1245,10 +2051,10 @@ async function createDashboardServer(deps) {
1245
2051
  }
1246
2052
  }
1247
2053
  if (!bound) {
1248
- throw new Error("dashboard: no available port");
2054
+ throw new Error("console: no available port");
1249
2055
  }
1250
2056
  const url = `http://${deps.config.host}:${boundPort}`;
1251
- log(`[openteam] dashboard en ${url}`);
2057
+ log(`[openteam] console en ${url}`);
1252
2058
  const notify = async () => {
1253
2059
  if (clients.size === 0) {
1254
2060
  return;
@@ -1565,10 +2371,10 @@ function watchSources(paths, onChange, deps, debounceMs = 250) {
1565
2371
  }
1566
2372
 
1567
2373
  // src/web/start.ts
1568
- function startDashboard(deps) {
1569
- return createDashboardRuntime(deps);
2374
+ function startConsole(deps) {
2375
+ return createConsoleRuntime(deps);
1570
2376
  }
1571
- async function createDashboardRuntime(deps) {
2377
+ async function createConsoleRuntime(deps) {
1572
2378
  const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
1573
2379
  const decisionsPath = deps.decisionsPath ?? DEFAULT_DECISIONS_PATH;
1574
2380
  const readSnapshot = createSnapshotReader({
@@ -1587,11 +2393,12 @@ async function createDashboardRuntime(deps) {
1587
2393
  recentRoutesLimit: deps.config.recentRoutes,
1588
2394
  ...deps.session !== undefined ? { session: deps.session } : {}
1589
2395
  });
1590
- const createServer = deps.serve ?? createDashboardServer;
2396
+ const createServer = deps.serve ?? createConsoleServer;
1591
2397
  const server = await createServer({
1592
2398
  readSnapshot,
1593
2399
  config: deps.config,
1594
- ...deps.log !== undefined ? { log: deps.log } : {}
2400
+ ...deps.log !== undefined ? { log: deps.log } : {},
2401
+ ...deps.console !== undefined ? { console: deps.console } : {}
1595
2402
  });
1596
2403
  const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
1597
2404
  const watcher = watchSources([deps.sessionsDir, decisionsPath, backlogPath, deps.agentDir], () => {
@@ -1607,21 +2414,32 @@ async function createDashboardRuntime(deps) {
1607
2414
  };
1608
2415
  }
1609
2416
 
1610
- // src/cli/dashboardServe.ts
1611
- function parseDashboardServeArgs(rest) {
2417
+ // src/cli/consoleServe.ts
2418
+ function parseConsoleServeArgs(rest) {
1612
2419
  return {
1613
2420
  status: rest.includes("--status"),
1614
2421
  serve: rest.includes("--serve"),
1615
2422
  open: rest.includes("--open")
1616
2423
  };
1617
2424
  }
1618
- async function runDashboardServe(deps, options) {
2425
+ async function runConsoleServe(deps, options) {
1619
2426
  const config = await deps.loadConfig(deps.configPath);
1620
- const start = deps.startDashboard ?? startDashboard;
2427
+ const start = deps.startConsole ?? startConsole;
2428
+ const terminalEnabled = config.console.terminal.enabled;
2429
+ const ptyEnabled = config.console.terminal.pty === true;
2430
+ const token = (deps.generateToken ?? (() => encodeToken(randomBytes(32))))();
2431
+ const makeClient = deps.createConsoleClient ?? ((endpoint) => createConsoleClient({
2432
+ endpoint,
2433
+ ...deps.serverPassword !== undefined ? { password: deps.serverPassword } : {}
2434
+ }));
2435
+ const makePtyClient = deps.createPtyClient ?? ((endpoint) => createPtyClient({
2436
+ endpoint,
2437
+ ...deps.serverPassword !== undefined ? { password: deps.serverPassword } : {}
2438
+ }));
1621
2439
  let runtime;
1622
2440
  try {
1623
2441
  runtime = await start({
1624
- config: config.dashboard,
2442
+ config: config.console,
1625
2443
  sessionsDir: deps.sessionsDir,
1626
2444
  decisionsPath: deps.decisionsPath,
1627
2445
  backlogPath: deps.backlogPath,
@@ -1632,11 +2450,18 @@ async function runDashboardServe(deps, options) {
1632
2450
  listAgentFiles: deps.listAgentFiles,
1633
2451
  now: deps.now,
1634
2452
  ...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {},
2453
+ console: {
2454
+ token,
2455
+ terminalEnabled,
2456
+ ptyEnabled,
2457
+ createClient: makeClient,
2458
+ createPtyClient: makePtyClient
2459
+ },
1635
2460
  log: deps.log
1636
2461
  });
1637
2462
  } catch (error) {
1638
2463
  const message = error instanceof Error ? error.message : String(error);
1639
- deps.log(`No se pudo iniciar el dashboard: ${message}`);
2464
+ deps.log(`No se pudo iniciar el console: ${message}`);
1640
2465
  return { exitCode: 1 };
1641
2466
  }
1642
2467
  deps.log("Escuchando en loopback. Pulsa Ctrl+C para parar.");
@@ -1649,7 +2474,7 @@ async function runDashboardServe(deps, options) {
1649
2474
  }
1650
2475
  await deps.waitForSignal();
1651
2476
  runtime.close();
1652
- deps.log("Dashboard detenido.");
2477
+ deps.log("Console detenido.");
1653
2478
  return { exitCode: 0 };
1654
2479
  }
1655
2480
 
@@ -1951,8 +2776,8 @@ function errorMessage(error) {
1951
2776
  }
1952
2777
  return String(error);
1953
2778
  }
1954
- async function listOpenAICompatibleModels(baseURL, fetch) {
1955
- const response = await fetch(modelsEndpoint(baseURL), {
2779
+ async function listOpenAICompatibleModels(baseURL, fetch2) {
2780
+ const response = await fetch2(modelsEndpoint(baseURL), {
1956
2781
  method: "GET",
1957
2782
  headers: { accept: "application/json" }
1958
2783
  });
@@ -2403,6 +3228,139 @@ function createDetect(exec) {
2403
3228
  };
2404
3229
  }
2405
3230
 
3231
+ // src/cli/tunnel.ts
3232
+ function parsePort(value) {
3233
+ if (value === undefined) {
3234
+ return;
3235
+ }
3236
+ const n = Number.parseInt(value, 10);
3237
+ return Number.isInteger(n) && n >= 1024 && n <= 65535 ? n : undefined;
3238
+ }
3239
+ function parseTunnelArgs(rest) {
3240
+ const options = {
3241
+ terminal: rest.includes("--terminal") || rest.includes("--expose-opencode"),
3242
+ allowAnonymous: rest.includes("--allow-anonymous"),
3243
+ yes: rest.includes("--yes") || rest.includes("-y")
3244
+ };
3245
+ for (let i = 0;i < rest.length; i += 1) {
3246
+ if (rest[i] === "--port") {
3247
+ const port = parsePort(rest[i + 1]);
3248
+ if (port !== undefined) {
3249
+ options.consolePort = port;
3250
+ }
3251
+ } else if (rest[i] === "--opencode-port") {
3252
+ const port = parsePort(rest[i + 1]);
3253
+ if (port !== undefined) {
3254
+ options.opencodePort = port;
3255
+ }
3256
+ }
3257
+ }
3258
+ return options;
3259
+ }
3260
+ function parseDevtunnelUrls(stdout) {
3261
+ const urls = [];
3262
+ const seen = new Set;
3263
+ const re = /Hosting port (\d+) at (https:\/\/\S+)/g;
3264
+ let match = re.exec(stdout);
3265
+ while (match !== null) {
3266
+ const port = Number.parseInt(match[1], 10);
3267
+ const url = match[2].replace(/[.,)]+$/, "");
3268
+ const key = `${port} ${url}`;
3269
+ if (!seen.has(key)) {
3270
+ seen.add(key);
3271
+ urls.push({ port, url });
3272
+ }
3273
+ match = re.exec(stdout);
3274
+ }
3275
+ return urls;
3276
+ }
3277
+ function planTunnel(input) {
3278
+ const { options, consolePort, opencodePort, hasServerPassword } = input;
3279
+ const ports = [consolePort];
3280
+ const warnings = [];
3281
+ const errors = [];
3282
+ let exposeTerminal = false;
3283
+ if (options.terminal) {
3284
+ if (hasServerPassword) {
3285
+ exposeTerminal = true;
3286
+ ports.push(opencodePort);
3287
+ warnings.push("Terminal remota EXPUESTA: opencode puede ejecutar comandos y editar ficheros a través del túnel.");
3288
+ } else {
3289
+ warnings.push("OPENCODE_SERVER_PASSWORD no está definido: la terminal remota queda deshabilitada; solo se expone la Console (observación).");
3290
+ }
3291
+ }
3292
+ let allowAnonymous = false;
3293
+ if (options.allowAnonymous) {
3294
+ if (options.yes) {
3295
+ allowAnonymous = true;
3296
+ warnings.push("Túnel ANÓNIMO: cualquiera con la URL accede sin autenticación. Úsalo solo temporalmente.");
3297
+ } else {
3298
+ errors.push("El túnel anónimo (--allow-anonymous) requiere confirmación explícita con --yes (doble confirmación).");
3299
+ }
3300
+ }
3301
+ return {
3302
+ ports,
3303
+ exposeTerminal,
3304
+ allowAnonymous,
3305
+ warnings,
3306
+ errors,
3307
+ ok: errors.length === 0
3308
+ };
3309
+ }
3310
+ function buildDevtunnelArgs(plan) {
3311
+ const args = ["host"];
3312
+ for (const port of plan.ports) {
3313
+ args.push("-p", String(port));
3314
+ }
3315
+ if (plan.allowAnonymous) {
3316
+ args.push("--allow-anonymous");
3317
+ }
3318
+ return args;
3319
+ }
3320
+ async function runTunnel(deps, options) {
3321
+ if (!await deps.isInstalled()) {
3322
+ deps.log("No se encontró 'devtunnel'. Instálalo con: winget install Microsoft.devtunnel");
3323
+ return { exitCode: 1 };
3324
+ }
3325
+ if (!await deps.isLoggedIn()) {
3326
+ deps.log("No hay sesión en Dev Tunnels. Inicia sesión con: devtunnel user login");
3327
+ return { exitCode: 1 };
3328
+ }
3329
+ const plan = planTunnel({
3330
+ options,
3331
+ consolePort: options.consolePort ?? deps.consolePort,
3332
+ opencodePort: options.opencodePort ?? deps.opencodePort,
3333
+ hasServerPassword: deps.hasServerPassword
3334
+ });
3335
+ for (const warning of plan.warnings) {
3336
+ deps.log(`⚠ ${warning}`);
3337
+ }
3338
+ if (!plan.ok) {
3339
+ for (const error of plan.errors) {
3340
+ deps.log(`✖ ${error}`);
3341
+ }
3342
+ return { exitCode: 1 };
3343
+ }
3344
+ const child = deps.spawn("devtunnel", buildDevtunnelArgs(plan));
3345
+ const seen = new Set;
3346
+ const onData = (chunk) => {
3347
+ for (const { port, url } of parseDevtunnelUrls(chunk.toString("utf8"))) {
3348
+ const label = port === deps.consolePort ? "Console" : "opencode";
3349
+ const line = `${label} (puerto ${port}): ${url}`;
3350
+ if (!seen.has(line)) {
3351
+ seen.add(line);
3352
+ deps.log(line);
3353
+ }
3354
+ }
3355
+ };
3356
+ child.stdout?.on("data", onData);
3357
+ deps.log("Túnel activo. Pulsa Ctrl+C para cerrarlo.");
3358
+ await deps.waitForSignal();
3359
+ child.kill();
3360
+ deps.log("Túnel cerrado.");
3361
+ return { exitCode: 0 };
3362
+ }
3363
+
2406
3364
  // src/commands/baseline.ts
2407
3365
  function formatRef(ref) {
2408
3366
  return ref === null ? "—" : `${ref.providerID}/${ref.modelID}`;
@@ -2464,18 +3422,18 @@ function autoBaseline(config) {
2464
3422
  };
2465
3423
  }
2466
3424
 
2467
- // src/commands/dashboard.ts
2468
- function renderDashboardStatus(dashboard) {
2469
- const url = `http://${dashboard.host}:${dashboard.port}`;
3425
+ // src/commands/console.ts
3426
+ function renderConsoleStatus(console_) {
3427
+ const url = `http://${console_.host}:${console_.port}`;
2470
3428
  return [
2471
- "Dashboard (multi-sesión, se lanza desde la CLI):",
3429
+ "Console (multi-sesión, se lanza desde la CLI):",
2472
3430
  ` URL: ${url}`,
2473
- ` Refresco: ${dashboard.refreshMs} ms (SSE + polling)`,
2474
- ` Rutas: últimas ${dashboard.recentRoutes}`,
2475
- dashboard.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
3431
+ ` Refresco: ${console_.refreshMs} ms (SSE + polling)`,
3432
+ ` Rutas: últimas ${console_.recentRoutes}`,
3433
+ console_.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
2476
3434
  "",
2477
- "Lánzalo con:",
2478
- " openteam dashboard (Ctrl+C para parar; --open abre el navegador)",
3435
+ "Lánzala con:",
3436
+ " openteam console (Ctrl+C para parar; --open abre el navegador)",
2479
3437
  "",
2480
3438
  "Agrega TODAS las sesiones de opencode que escriben eventos en",
2481
3439
  " .opencode/openteam/sessions/*.jsonl",
@@ -2616,7 +3574,7 @@ function buildOrchestratorAgent(frontier, options = {}) {
2616
3574
  "- **scribe** — memoria silenciosa del equipo. Registra decisiones y",
2617
3575
  " aprendizajes en un log compartido (`.opencode/openteam-decisions.md`) sin",
2618
3576
  " ejecutar cambios de código. Modelo **local**; permisos de solo lectura más",
2619
- " edición de ese log. **Formato parseable** para el dashboard: una decisión",
3577
+ " edición de ese log. **Formato parseable** para el console: una decisión",
2620
3578
  " por línea como item de lista Markdown, ya **redactada** (sin prompts ni",
2621
3579
  " datos sensibles):",
2622
3580
  " `- YYYY-MM-DD [agente] Resumen breve de la decisión #tag1 #tag2`",
@@ -2751,8 +3709,9 @@ var HELP = [
2751
3709
  " openteam baseline auto Baseline cheapest-capable (modo auto)",
2752
3710
  " openteam doctor Diagnóstico de runtimes y config",
2753
3711
  " openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
2754
- " openteam dashboard Lanza el dashboard web multi-sesión (Ctrl+C para parar; --open abre el navegador)",
2755
- " openteam dashboard --status Muestra la config del dashboard sin lanzarlo",
3712
+ " openteam console Lanza la Console web multi-sesión (Ctrl+C para parar; --open abre el navegador)",
3713
+ " openteam console --status Muestra la config de la Console sin lanzarla",
3714
+ " openteam tunnel Expone la Console vía Dev Tunnels (--terminal expone opencode; Ctrl+C para cerrar)",
2756
3715
  " openteam report Resumen de coste/ahorro (telemetría)",
2757
3716
  " openteam yolo status Muestra si el modo YOLO está activo",
2758
3717
  " openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
@@ -2942,9 +3901,9 @@ async function runCli(argv, deps) {
2942
3901
  if (command === "agents") {
2943
3902
  return await runAgents(deps, configPath, opencodeConfigPath);
2944
3903
  }
2945
- if (command === "dashboard") {
3904
+ if (command === "console") {
2946
3905
  const config = await deps.loadConfig(configPath);
2947
- return { exitCode: 0, stdout: renderDashboardStatus(config.dashboard) };
3906
+ return { exitCode: 0, stdout: renderConsoleStatus(config.console) };
2948
3907
  }
2949
3908
  if (command === undefined || command === "help" || command === "--help") {
2950
3909
  return { exitCode: 0, stdout: HELP };
@@ -2995,7 +3954,7 @@ function fixedActionBody(action) {
2995
3954
  `);
2996
3955
  }
2997
3956
  function buildOpenteamCommand() {
2998
- return renderCommand("Comandos runtime de openteam (baseline show|set|auto, doctor, agents, dashboard, report)", [
3957
+ return renderCommand("Comandos runtime de openteam (baseline show|set|auto, doctor, agents, console, report)", [
2999
3958
  "Usa la herramienta `openteam` para ejecutar el comando indicado por el usuario: $ARGUMENTS",
3000
3959
  "",
3001
3960
  "Interpreta los argumentos así y llama a la herramienta `openteam` una sola vez:",
@@ -3005,7 +3964,7 @@ function buildOpenteamCommand() {
3005
3964
  '- `baseline auto` → `action: "auto"`',
3006
3965
  '- `doctor` → `action: "doctor"`',
3007
3966
  '- `agents` → `action: "agents"`',
3008
- '- `dashboard` → `action: "dashboard"`',
3967
+ '- `console` → `action: "console"`',
3009
3968
  '- `report` → `action: "report"`',
3010
3969
  "",
3011
3970
  "Devuelve la salida de la herramienta tal cual, sin reinterpretarla."
@@ -3035,7 +3994,7 @@ function buildOpenteamCommands() {
3035
3994
  file("openteam-baseline", buildBaselineCommand()),
3036
3995
  file("openteam-doctor", renderCommand("openteam · diagnóstico de runtimes locales y configuración", fixedActionBody("doctor"))),
3037
3996
  file("openteam-agents", renderCommand("openteam · lista los agentes y el LLM (local/frontier + suscripción) de cada uno", fixedActionBody("agents"))),
3038
- file("openteam-dashboard", renderCommand("openteam · estado y URL del dashboard web multi-sesión", fixedActionBody("dashboard"))),
3997
+ file("openteam-console", renderCommand("openteam · estado y URL de la Console web multi-sesión", fixedActionBody("console"))),
3039
3998
  file("openteam-report", renderCommand("openteam · resumen de coste/ahorro (telemetría)", fixedActionBody("report")))
3040
3999
  ];
3041
4000
  }
@@ -3421,7 +4380,7 @@ async function runSetup(deps) {
3421
4380
  `Baseline frontier: ${frontier.providerID}/${frontier.modelID}`,
3422
4381
  `Runtimes locales: ${enabledSummary || "ninguno habilitado"}`,
3423
4382
  `Modo YOLO: ${yolo ? "activado (auto-aprueba permisos)" : "desactivado"}`,
3424
- "Dashboard web: se lanza aparte desde la CLI con 'openteam dashboard' (multi-sesión, loopback)",
4383
+ "Console web: se lanza aparte desde la CLI con 'openteam console' (multi-sesión, loopback)",
3425
4384
  `Escrito: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${OPENTEAM_COMMAND_DIR}/*.md (${slashCommands.length} comandos)`,
3426
4385
  "",
3427
4386
  "Siguientes pasos:",
@@ -3620,10 +4579,10 @@ async function main() {
3620
4579
  process.exitCode = result2.exitCode;
3621
4580
  return;
3622
4581
  }
3623
- if (argv[0] === "dashboard") {
3624
- const flags = parseDashboardServeArgs(argv.slice(1));
4582
+ if (argv[0] === "console") {
4583
+ const flags = parseConsoleServeArgs(argv.slice(1));
3625
4584
  if (!flags.status) {
3626
- const result2 = await runDashboardServe({
4585
+ const result2 = await runConsoleServe({
3627
4586
  loadConfig,
3628
4587
  configPath: DEFAULT_CONFIG_PATH,
3629
4588
  sessionsDir: DEFAULT_SESSIONS_DIR,
@@ -3637,6 +4596,7 @@ async function main() {
3637
4596
  now: () => new Date().toISOString(),
3638
4597
  gitLastCommit: createGitLastCommit(nodeExec),
3639
4598
  waitForSignal,
4599
+ ...process.env.OPENCODE_SERVER_PASSWORD !== undefined ? { serverPassword: process.env.OPENCODE_SERVER_PASSWORD } : {},
3640
4600
  log: (message) => process.stdout.write(`${message}
3641
4601
  `),
3642
4602
  openBrowser
@@ -3645,6 +4605,27 @@ async function main() {
3645
4605
  return;
3646
4606
  }
3647
4607
  }
4608
+ if (argv[0] === "tunnel") {
4609
+ const config = await loadConfig(DEFAULT_CONFIG_PATH);
4610
+ const options = parseTunnelArgs(argv.slice(1));
4611
+ const opencodePortEnv = Number.parseInt(process.env.OPENCODE_PORT ?? "", 10);
4612
+ const result2 = await runTunnel({
4613
+ isInstalled: async () => (await nodeExec("devtunnel", ["--version"])).exitCode === 0,
4614
+ isLoggedIn: async () => {
4615
+ const out = await nodeExec("devtunnel", ["user", "show"]);
4616
+ return out.exitCode === 0 && !out.stdout.toLowerCase().includes("not logged");
4617
+ },
4618
+ spawn: (command, args) => spawn(command, args),
4619
+ consolePort: config.console.port,
4620
+ opencodePort: Number.isInteger(opencodePortEnv) ? opencodePortEnv : 4096,
4621
+ hasServerPassword: process.env.OPENCODE_SERVER_PASSWORD !== undefined && process.env.OPENCODE_SERVER_PASSWORD.length > 0,
4622
+ waitForSignal,
4623
+ log: (message) => process.stdout.write(`${message}
4624
+ `)
4625
+ }, options);
4626
+ process.exitCode = result2.exitCode;
4627
+ return;
4628
+ }
3648
4629
  const result = await runCli(argv, deps);
3649
4630
  process.stdout.write(`${result.stdout}
3650
4631
  `);