@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.
- package/.opencode/command/openteam.md +2 -2
- package/README.md +65 -16
- package/dist/cli/{dashboardServe.d.ts → consoleServe.d.ts} +23 -13
- package/dist/cli/consoleServe.d.ts.map +1 -0
- package/dist/cli/tunnel.d.ts +89 -0
- package/dist/cli/tunnel.d.ts.map +1 -0
- package/dist/cli.js +1168 -187
- package/dist/commands/console.d.ts +9 -0
- package/dist/commands/console.d.ts.map +1 -0
- package/dist/commands/dispatch.d.ts.map +1 -1
- package/dist/config/schema.d.ts +88 -4
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/console/backlog.d.ts.map +1 -0
- package/dist/console/opencodeClient.d.ts +30 -0
- package/dist/console/opencodeClient.d.ts.map +1 -0
- package/dist/console/protocol.d.ts +42 -0
- package/dist/console/protocol.d.ts.map +1 -0
- package/dist/console/pty.d.ts +47 -0
- package/dist/console/pty.d.ts.map +1 -0
- package/dist/console/render.d.ts +20 -0
- package/dist/console/render.d.ts.map +1 -0
- package/dist/console/session.d.ts +23 -0
- package/dist/console/session.d.ts.map +1 -0
- package/dist/console/sse.d.ts +21 -0
- package/dist/console/sse.d.ts.map +1 -0
- package/dist/{dashboard → console}/state.d.ts +10 -5
- package/dist/console/state.d.ts.map +1 -0
- package/dist/console/token.d.ts +24 -0
- package/dist/console/token.d.ts.map +1 -0
- package/dist/{dashboard → console}/types.d.ts +24 -3
- package/dist/console/types.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +85 -34
- package/dist/plugin/capture.d.ts +12 -1
- package/dist/plugin/capture.d.ts.map +1 -1
- package/dist/plugin/commandTool.d.ts +1 -1
- package/dist/plugin/commandTool.d.ts.map +1 -1
- package/dist/telemetry/aggregate.d.ts +15 -1
- package/dist/telemetry/aggregate.d.ts.map +1 -1
- package/dist/telemetry/decisions.d.ts +1 -1
- package/dist/telemetry/eventLog.d.ts +1 -1
- package/dist/telemetry/eventLog.d.ts.map +1 -1
- package/dist/telemetry/events.d.ts +28 -1
- package/dist/telemetry/events.d.ts.map +1 -1
- package/dist/web/console.d.ts +28 -0
- package/dist/web/console.d.ts.map +1 -0
- package/dist/web/git.d.ts +1 -1
- package/dist/web/git.d.ts.map +1 -1
- package/dist/web/paths.d.ts +1 -1
- package/dist/web/paths.d.ts.map +1 -1
- package/dist/web/server.d.ts +20 -9
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/snapshot.d.ts +6 -6
- package/dist/web/snapshot.d.ts.map +1 -1
- package/dist/web/start.d.ts +19 -9
- package/dist/web/start.d.ts.map +1 -1
- package/package.json +4 -2
- package/dist/cli/dashboardServe.d.ts.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -9
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/dashboard/backlog.d.ts.map +0 -1
- package/dist/dashboard/render.d.ts +0 -14
- package/dist/dashboard/render.d.ts.map +0 -1
- package/dist/dashboard/state.d.ts.map +0 -1
- package/dist/dashboard/types.d.ts.map +0 -1
- /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/
|
|
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
|
|
827
|
+
function renderConsoleHtml(state, options = {}) {
|
|
311
828
|
const refreshMs = options.refreshMs ?? 2000;
|
|
312
|
-
const
|
|
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
|
|
854
|
+
"<title>openteam console</title>",
|
|
332
855
|
`<style>${STYLE}</style>`,
|
|
333
856
|
"</head>",
|
|
334
857
|
"<body>",
|
|
335
858
|
"<header>",
|
|
336
|
-
"<h1>openteam
|
|
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
|
|
1185
|
+
import { z as z4 } from "zod";
|
|
640
1186
|
|
|
641
1187
|
// src/capabilities/types.ts
|
|
642
|
-
import { z } from "zod";
|
|
643
|
-
var CapabilityTierSchema =
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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 =
|
|
1197
|
+
var ComplexityTierSchema = z2.enum([
|
|
652
1198
|
"trivial",
|
|
653
1199
|
"simple",
|
|
654
1200
|
"moderate",
|
|
655
1201
|
"hard"
|
|
656
1202
|
]);
|
|
657
|
-
var ModelCapabilityProfileSchema =
|
|
658
|
-
ref:
|
|
659
|
-
providerID:
|
|
660
|
-
modelID:
|
|
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:
|
|
663
|
-
contextWindow:
|
|
664
|
-
maxOutputTokens:
|
|
665
|
-
supportsToolCalling:
|
|
666
|
-
supportsVision:
|
|
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:
|
|
670
|
-
inputUSD:
|
|
671
|
-
outputUSD:
|
|
1215
|
+
costPer1M: z2.object({
|
|
1216
|
+
inputUSD: z2.number().min(0),
|
|
1217
|
+
outputUSD: z2.number().min(0)
|
|
672
1218
|
}),
|
|
673
|
-
availability:
|
|
1219
|
+
availability: z2.enum(["available", "degraded", "unavailable"])
|
|
674
1220
|
});
|
|
675
1221
|
|
|
676
1222
|
// src/config/schema.ts
|
|
677
|
-
import { z as
|
|
678
|
-
var ModelRefSchema =
|
|
679
|
-
providerID:
|
|
680
|
-
modelID:
|
|
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 =
|
|
683
|
-
var PrivacyModeSchema =
|
|
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 =
|
|
689
|
-
var
|
|
690
|
-
var
|
|
691
|
-
host:
|
|
692
|
-
port:
|
|
693
|
-
autoPortFallback:
|
|
694
|
-
refreshMs:
|
|
695
|
-
recentRoutes:
|
|
696
|
-
openBrowser:
|
|
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 =
|
|
714
|
-
id:
|
|
715
|
-
enabled:
|
|
716
|
-
baseURL:
|
|
717
|
-
discovery:
|
|
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
|
|
721
|
-
baseline:
|
|
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:
|
|
1281
|
+
router: z3.object({
|
|
731
1282
|
mode: RouterModeSchema.default("balanced"),
|
|
732
1283
|
localDefault: ModelRefSchema.default(defaultLocalModel),
|
|
733
|
-
trivialPromptMaxChars:
|
|
734
|
-
frontierPromptMinChars:
|
|
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:
|
|
742
|
-
runtimes:
|
|
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:
|
|
761
|
-
sessionUSD:
|
|
762
|
-
monthlyUSD:
|
|
763
|
-
frontierTokensPerSession:
|
|
764
|
-
hardStopOnBudgetExhaustion:
|
|
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
|
-
|
|
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 =
|
|
773
|
-
v:
|
|
774
|
-
ts:
|
|
775
|
-
sessionID:
|
|
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:
|
|
779
|
-
promptHash:
|
|
780
|
-
promptChars:
|
|
1330
|
+
type: z4.literal("route"),
|
|
1331
|
+
promptHash: z4.string().min(1),
|
|
1332
|
+
promptChars: z4.number().int().min(0),
|
|
781
1333
|
tier: ComplexityTierSchema,
|
|
782
|
-
routeKind:
|
|
1334
|
+
routeKind: z4.enum(["local", "frontier"]),
|
|
783
1335
|
selected: ModelRefSchema,
|
|
784
|
-
rationale:
|
|
785
|
-
estimatedCostUSD:
|
|
786
|
-
baselineCostUSD:
|
|
787
|
-
estimatedSavingsUSD:
|
|
788
|
-
budgetAction:
|
|
789
|
-
tokensIn:
|
|
790
|
-
tokensOut:
|
|
791
|
-
decisionID:
|
|
792
|
-
agent:
|
|
793
|
-
success:
|
|
794
|
-
batchID:
|
|
795
|
-
failureReason:
|
|
796
|
-
failureStage:
|
|
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:
|
|
800
|
-
providerID:
|
|
801
|
-
modelID:
|
|
802
|
-
agent:
|
|
803
|
-
mode:
|
|
804
|
-
messageID:
|
|
805
|
-
costUSD:
|
|
806
|
-
tokensIn:
|
|
807
|
-
tokensOut:
|
|
808
|
-
tokensReasoning:
|
|
809
|
-
tokensCacheRead:
|
|
810
|
-
tokensCacheWrite:
|
|
811
|
-
durationMs:
|
|
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:
|
|
815
|
-
tool:
|
|
816
|
-
callID:
|
|
817
|
-
agent:
|
|
818
|
-
durationMs:
|
|
819
|
-
ok:
|
|
820
|
-
title:
|
|
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:
|
|
824
|
-
batchID:
|
|
825
|
-
purpose:
|
|
826
|
-
roles:
|
|
827
|
-
decisionIDs:
|
|
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:
|
|
831
|
-
agent:
|
|
832
|
-
summary:
|
|
833
|
-
tags:
|
|
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:
|
|
837
|
-
kind:
|
|
838
|
-
agent:
|
|
839
|
-
summary:
|
|
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 =
|
|
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
|
|
852
|
-
var CostRecordSchema =
|
|
853
|
-
ts:
|
|
854
|
-
sessionID:
|
|
855
|
-
promptHash:
|
|
856
|
-
promptChars:
|
|
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:
|
|
1418
|
+
routeKind: z5.enum(["local", "frontier"]),
|
|
859
1419
|
selected: ModelRefSchema,
|
|
860
|
-
rationale:
|
|
861
|
-
estimatedCostUSD:
|
|
862
|
-
baselineCostUSD:
|
|
863
|
-
estimatedSavingsUSD:
|
|
864
|
-
budgetAction:
|
|
865
|
-
tokensIn:
|
|
866
|
-
tokensOut:
|
|
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/
|
|
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/
|
|
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
|
|
1668
|
+
function composeActivity(activityViews, decisions, meetings, limit) {
|
|
1089
1669
|
const entries = [
|
|
1090
|
-
...
|
|
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
|
-
...
|
|
1102
|
-
...
|
|
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
|
|
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/
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
2014
|
+
response.writeHead(200, JSON_HEADERS2).end(await stateJson(deps));
|
|
1209
2015
|
return;
|
|
1210
2016
|
}
|
|
1211
2017
|
if (path === "/healthz") {
|
|
1212
|
-
response.writeHead(200,
|
|
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,
|
|
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("
|
|
2054
|
+
throw new Error("console: no available port");
|
|
1249
2055
|
}
|
|
1250
2056
|
const url = `http://${deps.config.host}:${boundPort}`;
|
|
1251
|
-
log(`[openteam]
|
|
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
|
|
1569
|
-
return
|
|
2374
|
+
function startConsole(deps) {
|
|
2375
|
+
return createConsoleRuntime(deps);
|
|
1570
2376
|
}
|
|
1571
|
-
async function
|
|
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 ??
|
|
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/
|
|
1611
|
-
function
|
|
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
|
|
2425
|
+
async function runConsoleServe(deps, options) {
|
|
1619
2426
|
const config = await deps.loadConfig(deps.configPath);
|
|
1620
|
-
const start = deps.
|
|
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.
|
|
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
|
|
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("
|
|
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,
|
|
1955
|
-
const response = await
|
|
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/
|
|
2468
|
-
function
|
|
2469
|
-
const url = `http://${
|
|
3425
|
+
// src/commands/console.ts
|
|
3426
|
+
function renderConsoleStatus(console_) {
|
|
3427
|
+
const url = `http://${console_.host}:${console_.port}`;
|
|
2470
3428
|
return [
|
|
2471
|
-
"
|
|
3429
|
+
"Console (multi-sesión, se lanza desde la CLI):",
|
|
2472
3430
|
` URL: ${url}`,
|
|
2473
|
-
` Refresco: ${
|
|
2474
|
-
` Rutas: últimas ${
|
|
2475
|
-
|
|
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
|
-
"
|
|
2478
|
-
" openteam
|
|
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
|
|
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
|
|
2755
|
-
" openteam
|
|
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 === "
|
|
3904
|
+
if (command === "console") {
|
|
2946
3905
|
const config = await deps.loadConfig(configPath);
|
|
2947
|
-
return { exitCode: 0, stdout:
|
|
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,
|
|
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
|
-
'- `
|
|
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-
|
|
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
|
-
"
|
|
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] === "
|
|
3624
|
-
const flags =
|
|
4582
|
+
if (argv[0] === "console") {
|
|
4583
|
+
const flags = parseConsoleServeArgs(argv.slice(1));
|
|
3625
4584
|
if (!flags.status) {
|
|
3626
|
-
const result2 = await
|
|
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
|
`);
|