@jmanuelcorral/openteam 0.1.23 → 0.1.25
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 +45 -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 +1170 -197
- 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/commands/setup.d.ts +0 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/config/schema.d.ts +88 -6
- 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 -36
- 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 +1 -1
- 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,73 +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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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 })
|
|
698
1247
|
}).default({
|
|
699
|
-
enabled: false,
|
|
700
1248
|
host: "127.0.0.1",
|
|
701
1249
|
port: 4599,
|
|
702
1250
|
autoPortFallback: true,
|
|
703
1251
|
refreshMs: 2000,
|
|
704
1252
|
recentRoutes: 50,
|
|
705
|
-
openBrowser: false
|
|
1253
|
+
openBrowser: false,
|
|
1254
|
+
terminal: { enabled: true }
|
|
706
1255
|
});
|
|
707
1256
|
var defaultLocalModel = {
|
|
708
1257
|
providerID: "ollama",
|
|
@@ -712,15 +1261,15 @@ var defaultFrontierModel = {
|
|
|
712
1261
|
providerID: "anthropic",
|
|
713
1262
|
modelID: "claude-sonnet-4-5"
|
|
714
1263
|
};
|
|
715
|
-
var LocalRuntimeSchema =
|
|
716
|
-
id:
|
|
717
|
-
enabled:
|
|
718
|
-
baseURL:
|
|
719
|
-
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(),
|
|
720
1269
|
defaultModel: ModelRefSchema
|
|
721
1270
|
});
|
|
722
|
-
var
|
|
723
|
-
baseline:
|
|
1271
|
+
var OpenTeamConfigObjectSchema = z3.object({
|
|
1272
|
+
baseline: z3.object({
|
|
724
1273
|
mode: BaselineModeSchema.default("auto"),
|
|
725
1274
|
pinnedModel: ModelRefSchema.nullable().default(null),
|
|
726
1275
|
hardDefault: ModelRefSchema.default(defaultFrontierModel)
|
|
@@ -729,19 +1278,19 @@ var OpenTeamConfigSchema = z2.object({
|
|
|
729
1278
|
pinnedModel: null,
|
|
730
1279
|
hardDefault: defaultFrontierModel
|
|
731
1280
|
}),
|
|
732
|
-
router:
|
|
1281
|
+
router: z3.object({
|
|
733
1282
|
mode: RouterModeSchema.default("balanced"),
|
|
734
1283
|
localDefault: ModelRefSchema.default(defaultLocalModel),
|
|
735
|
-
trivialPromptMaxChars:
|
|
736
|
-
frontierPromptMinChars:
|
|
1284
|
+
trivialPromptMaxChars: z3.number().int().positive().default(280),
|
|
1285
|
+
frontierPromptMinChars: z3.number().int().positive().default(2000)
|
|
737
1286
|
}).default({
|
|
738
1287
|
mode: "balanced",
|
|
739
1288
|
localDefault: defaultLocalModel,
|
|
740
1289
|
trivialPromptMaxChars: 280,
|
|
741
1290
|
frontierPromptMinChars: 2000
|
|
742
1291
|
}),
|
|
743
|
-
local:
|
|
744
|
-
runtimes:
|
|
1292
|
+
local: z3.object({
|
|
1293
|
+
runtimes: z3.array(LocalRuntimeSchema).min(1).default([
|
|
745
1294
|
{
|
|
746
1295
|
id: "ollama",
|
|
747
1296
|
enabled: true,
|
|
@@ -759,113 +1308,122 @@ var OpenTeamConfigSchema = z2.object({
|
|
|
759
1308
|
}
|
|
760
1309
|
]
|
|
761
1310
|
}),
|
|
762
|
-
budgets:
|
|
763
|
-
sessionUSD:
|
|
764
|
-
monthlyUSD:
|
|
765
|
-
frontierTokensPerSession:
|
|
766
|
-
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)
|
|
767
1316
|
}).default({ hardStopOnBudgetExhaustion: false }),
|
|
768
1317
|
privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
|
|
769
|
-
|
|
1318
|
+
console: ConsoleConfigSchema
|
|
770
1319
|
});
|
|
1320
|
+
var OpenTeamConfigSchema = OpenTeamConfigObjectSchema;
|
|
771
1321
|
|
|
772
1322
|
// src/telemetry/events.ts
|
|
773
1323
|
var EVENT_SCHEMA_VERSION = 1;
|
|
774
|
-
var EventBaseSchema =
|
|
775
|
-
v:
|
|
776
|
-
ts:
|
|
777
|
-
sessionID:
|
|
1324
|
+
var EventBaseSchema = z4.object({
|
|
1325
|
+
v: z4.literal(EVENT_SCHEMA_VERSION),
|
|
1326
|
+
ts: z4.number().finite(),
|
|
1327
|
+
sessionID: z4.string().min(1)
|
|
778
1328
|
});
|
|
779
1329
|
var RouteEventSchema = EventBaseSchema.extend({
|
|
780
|
-
type:
|
|
781
|
-
promptHash:
|
|
782
|
-
promptChars:
|
|
1330
|
+
type: z4.literal("route"),
|
|
1331
|
+
promptHash: z4.string().min(1),
|
|
1332
|
+
promptChars: z4.number().int().min(0),
|
|
783
1333
|
tier: ComplexityTierSchema,
|
|
784
|
-
routeKind:
|
|
1334
|
+
routeKind: z4.enum(["local", "frontier"]),
|
|
785
1335
|
selected: ModelRefSchema,
|
|
786
|
-
rationale:
|
|
787
|
-
estimatedCostUSD:
|
|
788
|
-
baselineCostUSD:
|
|
789
|
-
estimatedSavingsUSD:
|
|
790
|
-
budgetAction:
|
|
791
|
-
tokensIn:
|
|
792
|
-
tokensOut:
|
|
793
|
-
decisionID:
|
|
794
|
-
agent:
|
|
795
|
-
success:
|
|
796
|
-
batchID:
|
|
797
|
-
failureReason:
|
|
798
|
-
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()
|
|
799
1349
|
});
|
|
800
1350
|
var MessageEventSchema = EventBaseSchema.extend({
|
|
801
|
-
type:
|
|
802
|
-
providerID:
|
|
803
|
-
modelID:
|
|
804
|
-
agent:
|
|
805
|
-
mode:
|
|
806
|
-
messageID:
|
|
807
|
-
costUSD:
|
|
808
|
-
tokensIn:
|
|
809
|
-
tokensOut:
|
|
810
|
-
tokensReasoning:
|
|
811
|
-
tokensCacheRead:
|
|
812
|
-
tokensCacheWrite:
|
|
813
|
-
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)
|
|
814
1364
|
});
|
|
815
1365
|
var ToolcallEventSchema = EventBaseSchema.extend({
|
|
816
|
-
type:
|
|
817
|
-
tool:
|
|
818
|
-
callID:
|
|
819
|
-
agent:
|
|
820
|
-
durationMs:
|
|
821
|
-
ok:
|
|
822
|
-
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()
|
|
823
1373
|
});
|
|
824
1374
|
var MeetingEventSchema = EventBaseSchema.extend({
|
|
825
|
-
type:
|
|
826
|
-
batchID:
|
|
827
|
-
purpose:
|
|
828
|
-
roles:
|
|
829
|
-
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()
|
|
830
1380
|
});
|
|
831
1381
|
var DecisionEventSchema = EventBaseSchema.extend({
|
|
832
|
-
type:
|
|
833
|
-
agent:
|
|
834
|
-
summary:
|
|
835
|
-
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()
|
|
836
1386
|
});
|
|
837
1387
|
var ActivityEventSchema = EventBaseSchema.extend({
|
|
838
|
-
type:
|
|
839
|
-
kind:
|
|
840
|
-
agent:
|
|
841
|
-
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)
|
|
842
1392
|
});
|
|
843
|
-
var
|
|
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()
|
|
1399
|
+
});
|
|
1400
|
+
var OpenTeamEventSchema = z4.discriminatedUnion("type", [
|
|
844
1401
|
RouteEventSchema,
|
|
845
1402
|
MessageEventSchema,
|
|
846
1403
|
ToolcallEventSchema,
|
|
847
1404
|
MeetingEventSchema,
|
|
848
1405
|
DecisionEventSchema,
|
|
849
|
-
ActivityEventSchema
|
|
1406
|
+
ActivityEventSchema,
|
|
1407
|
+
SessionEndpointEventSchema
|
|
850
1408
|
]);
|
|
851
1409
|
|
|
852
1410
|
// src/telemetry/types.ts
|
|
853
|
-
import { z as
|
|
854
|
-
var CostRecordSchema =
|
|
855
|
-
ts:
|
|
856
|
-
sessionID:
|
|
857
|
-
promptHash:
|
|
858
|
-
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),
|
|
859
1417
|
tier: ComplexityTierSchema,
|
|
860
|
-
routeKind:
|
|
1418
|
+
routeKind: z5.enum(["local", "frontier"]),
|
|
861
1419
|
selected: ModelRefSchema,
|
|
862
|
-
rationale:
|
|
863
|
-
estimatedCostUSD:
|
|
864
|
-
baselineCostUSD:
|
|
865
|
-
estimatedSavingsUSD:
|
|
866
|
-
budgetAction:
|
|
867
|
-
tokensIn:
|
|
868
|
-
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()
|
|
869
1427
|
});
|
|
870
1428
|
|
|
871
1429
|
// src/telemetry/read.ts
|
|
@@ -996,7 +1554,7 @@ async function readSessionEvents(dir, deps) {
|
|
|
996
1554
|
return events.sort((a, b) => a.ts - b.ts);
|
|
997
1555
|
}
|
|
998
1556
|
|
|
999
|
-
// src/
|
|
1557
|
+
// src/console/backlog.ts
|
|
1000
1558
|
var ITEM_RE = /^\s*[-*+]\s+\[( |x|X)\]\s+(.*)$/;
|
|
1001
1559
|
var ASSIGNEE_RE = /^\[@([^\]]+)\]\s*(.*)$/;
|
|
1002
1560
|
function parseAssignee(text) {
|
|
@@ -1044,7 +1602,27 @@ function buildLoopSnapshot(backlogPath, items) {
|
|
|
1044
1602
|
};
|
|
1045
1603
|
}
|
|
1046
1604
|
|
|
1047
|
-
// 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
|
|
1048
1626
|
var DEFAULT_RECENT_ROUTES = 50;
|
|
1049
1627
|
var DEFAULT_ACTIVITY_LIMIT = 100;
|
|
1050
1628
|
var DEFAULT_DECISIONS_LIMIT = 100;
|
|
@@ -1087,9 +1665,9 @@ function meetingToActivity(meeting) {
|
|
|
1087
1665
|
summary: `Reunión (${roles})${purpose}`
|
|
1088
1666
|
};
|
|
1089
1667
|
}
|
|
1090
|
-
function
|
|
1668
|
+
function composeActivity(activityViews, decisions, meetings, limit) {
|
|
1091
1669
|
const entries = [
|
|
1092
|
-
...
|
|
1670
|
+
...activityViews.map((view) => {
|
|
1093
1671
|
const entry = {
|
|
1094
1672
|
ts: view.ts,
|
|
1095
1673
|
kind: view.kind,
|
|
@@ -1100,11 +1678,39 @@ function activityFeed(aggregate, limit) {
|
|
|
1100
1678
|
}
|
|
1101
1679
|
return entry;
|
|
1102
1680
|
}),
|
|
1103
|
-
...
|
|
1104
|
-
...
|
|
1681
|
+
...decisions.map(decisionToActivity),
|
|
1682
|
+
...meetings.map(meetingToActivity)
|
|
1105
1683
|
];
|
|
1106
1684
|
return entries.sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
|
|
1107
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
|
+
}
|
|
1108
1714
|
function loopFrom(backlog) {
|
|
1109
1715
|
if (backlog === undefined) {
|
|
1110
1716
|
return;
|
|
@@ -1115,19 +1721,25 @@ function loopFrom(backlog) {
|
|
|
1115
1721
|
}
|
|
1116
1722
|
return snapshot;
|
|
1117
1723
|
}
|
|
1118
|
-
function
|
|
1724
|
+
function buildConsoleState(inputs) {
|
|
1119
1725
|
const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
|
|
1120
1726
|
const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
|
|
1121
1727
|
const decisionsLimit = inputs.decisionsLimit ?? DEFAULT_DECISIONS_LIMIT;
|
|
1122
1728
|
const meetingsLimit = inputs.meetingsLimit ?? DEFAULT_MEETINGS_LIMIT;
|
|
1123
1729
|
const aggregate = aggregateEvents(inputs.events);
|
|
1124
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
|
+
});
|
|
1125
1736
|
const state = {
|
|
1126
1737
|
generatedAt: inputs.generatedAt,
|
|
1127
1738
|
session: {},
|
|
1128
1739
|
cost: summarizeCostRecords(costRecords),
|
|
1129
1740
|
totals: aggregate.totals,
|
|
1130
1741
|
sessions: aggregate.sessions,
|
|
1742
|
+
sessionsDetail,
|
|
1131
1743
|
recentRoutes: recentRoutes(costRecords, recentRoutesLimit),
|
|
1132
1744
|
decisions: aggregate.decisions.slice(0, Math.max(0, decisionsLimit)),
|
|
1133
1745
|
meetings: aggregate.meetings.slice(0, Math.max(0, meetingsLimit)),
|
|
@@ -1147,24 +1759,201 @@ function buildDashboardState(inputs) {
|
|
|
1147
1759
|
return state;
|
|
1148
1760
|
}
|
|
1149
1761
|
|
|
1150
|
-
// src/web/
|
|
1762
|
+
// src/web/console.ts
|
|
1151
1763
|
var SSE_HEADERS = {
|
|
1152
1764
|
"content-type": "text/event-stream",
|
|
1153
1765
|
"cache-control": "no-cache",
|
|
1154
1766
|
connection: "keep-alive"
|
|
1155
1767
|
};
|
|
1156
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" };
|
|
1157
1941
|
var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
|
|
1158
1942
|
function isAddressInUse(error) {
|
|
1159
1943
|
return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
|
|
1160
1944
|
}
|
|
1161
1945
|
async function renderState(deps) {
|
|
1162
|
-
return
|
|
1163
|
-
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
|
+
} : {}
|
|
1164
1953
|
});
|
|
1165
1954
|
}
|
|
1166
1955
|
async function stateJson(deps) {
|
|
1167
|
-
return JSON.stringify(
|
|
1956
|
+
return JSON.stringify(buildConsoleState(await deps.readSnapshot()));
|
|
1168
1957
|
}
|
|
1169
1958
|
function sseMessage(json) {
|
|
1170
1959
|
return `event: state
|
|
@@ -1191,27 +1980,42 @@ function tryListen(server, host, port) {
|
|
|
1191
1980
|
server.listen(port, host);
|
|
1192
1981
|
});
|
|
1193
1982
|
}
|
|
1194
|
-
async function
|
|
1983
|
+
async function createConsoleServer(deps) {
|
|
1195
1984
|
const create = deps.createServer ?? createHttpServer;
|
|
1196
1985
|
const log = deps.log ?? ((message) => console.log(message));
|
|
1197
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
|
+
};
|
|
1198
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
|
+
}
|
|
1199
2004
|
if (request.method !== "GET") {
|
|
1200
2005
|
response.writeHead(405).end("method not allowed");
|
|
1201
2006
|
return;
|
|
1202
2007
|
}
|
|
1203
|
-
const url2 = new URL(request.url ?? "/", "http://localhost");
|
|
1204
2008
|
const path = url2.pathname;
|
|
1205
2009
|
if (path === "/" || path === "/index.html") {
|
|
1206
2010
|
response.writeHead(200, HTML_HEADERS).end(await renderState(deps));
|
|
1207
2011
|
return;
|
|
1208
2012
|
}
|
|
1209
2013
|
if (path === "/api/state") {
|
|
1210
|
-
response.writeHead(200,
|
|
2014
|
+
response.writeHead(200, JSON_HEADERS2).end(await stateJson(deps));
|
|
1211
2015
|
return;
|
|
1212
2016
|
}
|
|
1213
2017
|
if (path === "/healthz") {
|
|
1214
|
-
response.writeHead(200,
|
|
2018
|
+
response.writeHead(200, JSON_HEADERS2).end(JSON.stringify({ ok: true }));
|
|
1215
2019
|
return;
|
|
1216
2020
|
}
|
|
1217
2021
|
if (path === "/favicon.ico") {
|
|
@@ -1219,7 +2023,7 @@ async function createDashboardServer(deps) {
|
|
|
1219
2023
|
return;
|
|
1220
2024
|
}
|
|
1221
2025
|
if (path === "/events") {
|
|
1222
|
-
response.writeHead(200,
|
|
2026
|
+
response.writeHead(200, SSE_HEADERS2);
|
|
1223
2027
|
response.write(sseMessage(await stateJson(deps)));
|
|
1224
2028
|
clients.add(response);
|
|
1225
2029
|
request.on("close", () => {
|
|
@@ -1247,10 +2051,10 @@ async function createDashboardServer(deps) {
|
|
|
1247
2051
|
}
|
|
1248
2052
|
}
|
|
1249
2053
|
if (!bound) {
|
|
1250
|
-
throw new Error("
|
|
2054
|
+
throw new Error("console: no available port");
|
|
1251
2055
|
}
|
|
1252
2056
|
const url = `http://${deps.config.host}:${boundPort}`;
|
|
1253
|
-
log(`[openteam]
|
|
2057
|
+
log(`[openteam] console en ${url}`);
|
|
1254
2058
|
const notify = async () => {
|
|
1255
2059
|
if (clients.size === 0) {
|
|
1256
2060
|
return;
|
|
@@ -1567,10 +2371,10 @@ function watchSources(paths, onChange, deps, debounceMs = 250) {
|
|
|
1567
2371
|
}
|
|
1568
2372
|
|
|
1569
2373
|
// src/web/start.ts
|
|
1570
|
-
function
|
|
1571
|
-
return
|
|
2374
|
+
function startConsole(deps) {
|
|
2375
|
+
return createConsoleRuntime(deps);
|
|
1572
2376
|
}
|
|
1573
|
-
async function
|
|
2377
|
+
async function createConsoleRuntime(deps) {
|
|
1574
2378
|
const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
|
|
1575
2379
|
const decisionsPath = deps.decisionsPath ?? DEFAULT_DECISIONS_PATH;
|
|
1576
2380
|
const readSnapshot = createSnapshotReader({
|
|
@@ -1589,11 +2393,12 @@ async function createDashboardRuntime(deps) {
|
|
|
1589
2393
|
recentRoutesLimit: deps.config.recentRoutes,
|
|
1590
2394
|
...deps.session !== undefined ? { session: deps.session } : {}
|
|
1591
2395
|
});
|
|
1592
|
-
const createServer = deps.serve ??
|
|
2396
|
+
const createServer = deps.serve ?? createConsoleServer;
|
|
1593
2397
|
const server = await createServer({
|
|
1594
2398
|
readSnapshot,
|
|
1595
2399
|
config: deps.config,
|
|
1596
|
-
...deps.log !== undefined ? { log: deps.log } : {}
|
|
2400
|
+
...deps.log !== undefined ? { log: deps.log } : {},
|
|
2401
|
+
...deps.console !== undefined ? { console: deps.console } : {}
|
|
1597
2402
|
});
|
|
1598
2403
|
const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
|
|
1599
2404
|
const watcher = watchSources([deps.sessionsDir, decisionsPath, backlogPath, deps.agentDir], () => {
|
|
@@ -1609,21 +2414,32 @@ async function createDashboardRuntime(deps) {
|
|
|
1609
2414
|
};
|
|
1610
2415
|
}
|
|
1611
2416
|
|
|
1612
|
-
// src/cli/
|
|
1613
|
-
function
|
|
2417
|
+
// src/cli/consoleServe.ts
|
|
2418
|
+
function parseConsoleServeArgs(rest) {
|
|
1614
2419
|
return {
|
|
1615
2420
|
status: rest.includes("--status"),
|
|
1616
2421
|
serve: rest.includes("--serve"),
|
|
1617
2422
|
open: rest.includes("--open")
|
|
1618
2423
|
};
|
|
1619
2424
|
}
|
|
1620
|
-
async function
|
|
2425
|
+
async function runConsoleServe(deps, options) {
|
|
1621
2426
|
const config = await deps.loadConfig(deps.configPath);
|
|
1622
|
-
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
|
+
}));
|
|
1623
2439
|
let runtime;
|
|
1624
2440
|
try {
|
|
1625
2441
|
runtime = await start({
|
|
1626
|
-
config:
|
|
2442
|
+
config: config.console,
|
|
1627
2443
|
sessionsDir: deps.sessionsDir,
|
|
1628
2444
|
decisionsPath: deps.decisionsPath,
|
|
1629
2445
|
backlogPath: deps.backlogPath,
|
|
@@ -1634,11 +2450,18 @@ async function runDashboardServe(deps, options) {
|
|
|
1634
2450
|
listAgentFiles: deps.listAgentFiles,
|
|
1635
2451
|
now: deps.now,
|
|
1636
2452
|
...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {},
|
|
2453
|
+
console: {
|
|
2454
|
+
token,
|
|
2455
|
+
terminalEnabled,
|
|
2456
|
+
ptyEnabled,
|
|
2457
|
+
createClient: makeClient,
|
|
2458
|
+
createPtyClient: makePtyClient
|
|
2459
|
+
},
|
|
1637
2460
|
log: deps.log
|
|
1638
2461
|
});
|
|
1639
2462
|
} catch (error) {
|
|
1640
2463
|
const message = error instanceof Error ? error.message : String(error);
|
|
1641
|
-
deps.log(`No se pudo iniciar el
|
|
2464
|
+
deps.log(`No se pudo iniciar el console: ${message}`);
|
|
1642
2465
|
return { exitCode: 1 };
|
|
1643
2466
|
}
|
|
1644
2467
|
deps.log("Escuchando en loopback. Pulsa Ctrl+C para parar.");
|
|
@@ -1651,7 +2474,7 @@ async function runDashboardServe(deps, options) {
|
|
|
1651
2474
|
}
|
|
1652
2475
|
await deps.waitForSignal();
|
|
1653
2476
|
runtime.close();
|
|
1654
|
-
deps.log("
|
|
2477
|
+
deps.log("Console detenido.");
|
|
1655
2478
|
return { exitCode: 0 };
|
|
1656
2479
|
}
|
|
1657
2480
|
|
|
@@ -1953,8 +2776,8 @@ function errorMessage(error) {
|
|
|
1953
2776
|
}
|
|
1954
2777
|
return String(error);
|
|
1955
2778
|
}
|
|
1956
|
-
async function listOpenAICompatibleModels(baseURL,
|
|
1957
|
-
const response = await
|
|
2779
|
+
async function listOpenAICompatibleModels(baseURL, fetch2) {
|
|
2780
|
+
const response = await fetch2(modelsEndpoint(baseURL), {
|
|
1958
2781
|
method: "GET",
|
|
1959
2782
|
headers: { accept: "application/json" }
|
|
1960
2783
|
});
|
|
@@ -2405,6 +3228,139 @@ function createDetect(exec) {
|
|
|
2405
3228
|
};
|
|
2406
3229
|
}
|
|
2407
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
|
+
|
|
2408
3364
|
// src/commands/baseline.ts
|
|
2409
3365
|
function formatRef(ref) {
|
|
2410
3366
|
return ref === null ? "—" : `${ref.providerID}/${ref.modelID}`;
|
|
@@ -2466,18 +3422,18 @@ function autoBaseline(config) {
|
|
|
2466
3422
|
};
|
|
2467
3423
|
}
|
|
2468
3424
|
|
|
2469
|
-
// src/commands/
|
|
2470
|
-
function
|
|
2471
|
-
const url = `http://${
|
|
3425
|
+
// src/commands/console.ts
|
|
3426
|
+
function renderConsoleStatus(console_) {
|
|
3427
|
+
const url = `http://${console_.host}:${console_.port}`;
|
|
2472
3428
|
return [
|
|
2473
|
-
"
|
|
3429
|
+
"Console (multi-sesión, se lanza desde la CLI):",
|
|
2474
3430
|
` URL: ${url}`,
|
|
2475
|
-
` Refresco: ${
|
|
2476
|
-
` Rutas: últimas ${
|
|
2477
|
-
|
|
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)",
|
|
2478
3434
|
"",
|
|
2479
|
-
"
|
|
2480
|
-
" openteam
|
|
3435
|
+
"Lánzala con:",
|
|
3436
|
+
" openteam console (Ctrl+C para parar; --open abre el navegador)",
|
|
2481
3437
|
"",
|
|
2482
3438
|
"Agrega TODAS las sesiones de opencode que escriben eventos en",
|
|
2483
3439
|
" .opencode/openteam/sessions/*.jsonl",
|
|
@@ -2618,7 +3574,7 @@ function buildOrchestratorAgent(frontier, options = {}) {
|
|
|
2618
3574
|
"- **scribe** — memoria silenciosa del equipo. Registra decisiones y",
|
|
2619
3575
|
" aprendizajes en un log compartido (`.opencode/openteam-decisions.md`) sin",
|
|
2620
3576
|
" ejecutar cambios de código. Modelo **local**; permisos de solo lectura más",
|
|
2621
|
-
" edición de ese log. **Formato parseable** para el
|
|
3577
|
+
" edición de ese log. **Formato parseable** para el console: una decisión",
|
|
2622
3578
|
" por línea como item de lista Markdown, ya **redactada** (sin prompts ni",
|
|
2623
3579
|
" datos sensibles):",
|
|
2624
3580
|
" `- YYYY-MM-DD [agente] Resumen breve de la decisión #tag1 #tag2`",
|
|
@@ -2753,8 +3709,9 @@ var HELP = [
|
|
|
2753
3709
|
" openteam baseline auto Baseline cheapest-capable (modo auto)",
|
|
2754
3710
|
" openteam doctor Diagnóstico de runtimes y config",
|
|
2755
3711
|
" openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
|
|
2756
|
-
" openteam
|
|
2757
|
-
" 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)",
|
|
2758
3715
|
" openteam report Resumen de coste/ahorro (telemetría)",
|
|
2759
3716
|
" openteam yolo status Muestra si el modo YOLO está activo",
|
|
2760
3717
|
" openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
|
|
@@ -2944,9 +3901,9 @@ async function runCli(argv, deps) {
|
|
|
2944
3901
|
if (command === "agents") {
|
|
2945
3902
|
return await runAgents(deps, configPath, opencodeConfigPath);
|
|
2946
3903
|
}
|
|
2947
|
-
if (command === "
|
|
3904
|
+
if (command === "console") {
|
|
2948
3905
|
const config = await deps.loadConfig(configPath);
|
|
2949
|
-
return { exitCode: 0, stdout:
|
|
3906
|
+
return { exitCode: 0, stdout: renderConsoleStatus(config.console) };
|
|
2950
3907
|
}
|
|
2951
3908
|
if (command === undefined || command === "help" || command === "--help") {
|
|
2952
3909
|
return { exitCode: 0, stdout: HELP };
|
|
@@ -2997,7 +3954,7 @@ function fixedActionBody(action) {
|
|
|
2997
3954
|
`);
|
|
2998
3955
|
}
|
|
2999
3956
|
function buildOpenteamCommand() {
|
|
3000
|
-
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)", [
|
|
3001
3958
|
"Usa la herramienta `openteam` para ejecutar el comando indicado por el usuario: $ARGUMENTS",
|
|
3002
3959
|
"",
|
|
3003
3960
|
"Interpreta los argumentos así y llama a la herramienta `openteam` una sola vez:",
|
|
@@ -3007,7 +3964,7 @@ function buildOpenteamCommand() {
|
|
|
3007
3964
|
'- `baseline auto` → `action: "auto"`',
|
|
3008
3965
|
'- `doctor` → `action: "doctor"`',
|
|
3009
3966
|
'- `agents` → `action: "agents"`',
|
|
3010
|
-
'- `
|
|
3967
|
+
'- `console` → `action: "console"`',
|
|
3011
3968
|
'- `report` → `action: "report"`',
|
|
3012
3969
|
"",
|
|
3013
3970
|
"Devuelve la salida de la herramienta tal cual, sin reinterpretarla."
|
|
@@ -3037,7 +3994,7 @@ function buildOpenteamCommands() {
|
|
|
3037
3994
|
file("openteam-baseline", buildBaselineCommand()),
|
|
3038
3995
|
file("openteam-doctor", renderCommand("openteam · diagnóstico de runtimes locales y configuración", fixedActionBody("doctor"))),
|
|
3039
3996
|
file("openteam-agents", renderCommand("openteam · lista los agentes y el LLM (local/frontier + suscripción) de cada uno", fixedActionBody("agents"))),
|
|
3040
|
-
file("openteam-
|
|
3997
|
+
file("openteam-console", renderCommand("openteam · estado y URL de la Console web multi-sesión", fixedActionBody("console"))),
|
|
3041
3998
|
file("openteam-report", renderCommand("openteam · resumen de coste/ahorro (telemetría)", fixedActionBody("report")))
|
|
3042
3999
|
];
|
|
3043
4000
|
}
|
|
@@ -3129,8 +4086,7 @@ function buildOpenTeamConfig(answers) {
|
|
|
3129
4086
|
localDefault
|
|
3130
4087
|
},
|
|
3131
4088
|
local: { runtimes },
|
|
3132
|
-
privacyMode: answers.privacyMode
|
|
3133
|
-
dashboard: { enabled: answers.dashboard }
|
|
4089
|
+
privacyMode: answers.privacyMode
|
|
3134
4090
|
});
|
|
3135
4091
|
}
|
|
3136
4092
|
function buildOpencodeConfig(answers) {
|
|
@@ -3374,17 +4330,12 @@ async function runSetup(deps) {
|
|
|
3374
4330
|
message: "¿Activar modo YOLO? (opencode auto-aprueba todos los permisos; no afecta a la privacidad de openteam)",
|
|
3375
4331
|
initial: false
|
|
3376
4332
|
});
|
|
3377
|
-
const dashboard = await prompt.confirm({
|
|
3378
|
-
message: "¿Exponer el dashboard web local mientras opencode está abierto? (solo loopback en el puerto 4599, sin prompts; ajustable luego en .opencode/openteam.json)",
|
|
3379
|
-
initial: false
|
|
3380
|
-
});
|
|
3381
4333
|
const answers = {
|
|
3382
4334
|
runtimes,
|
|
3383
4335
|
frontier,
|
|
3384
4336
|
routerMode,
|
|
3385
4337
|
privacyMode,
|
|
3386
|
-
yolo
|
|
3387
|
-
dashboard
|
|
4338
|
+
yolo
|
|
3388
4339
|
};
|
|
3389
4340
|
const openTeamConfig = buildOpenTeamConfig(answers);
|
|
3390
4341
|
const opencodeConfig = buildOpencodeConfig(answers);
|
|
@@ -3429,7 +4380,7 @@ async function runSetup(deps) {
|
|
|
3429
4380
|
`Baseline frontier: ${frontier.providerID}/${frontier.modelID}`,
|
|
3430
4381
|
`Runtimes locales: ${enabledSummary || "ninguno habilitado"}`,
|
|
3431
4382
|
`Modo YOLO: ${yolo ? "activado (auto-aprueba permisos)" : "desactivado"}`,
|
|
3432
|
-
|
|
4383
|
+
"Console web: se lanza aparte desde la CLI con 'openteam console' (multi-sesión, loopback)",
|
|
3433
4384
|
`Escrito: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${OPENTEAM_COMMAND_DIR}/*.md (${slashCommands.length} comandos)`,
|
|
3434
4385
|
"",
|
|
3435
4386
|
"Siguientes pasos:",
|
|
@@ -3628,10 +4579,10 @@ async function main() {
|
|
|
3628
4579
|
process.exitCode = result2.exitCode;
|
|
3629
4580
|
return;
|
|
3630
4581
|
}
|
|
3631
|
-
if (argv[0] === "
|
|
3632
|
-
const flags =
|
|
4582
|
+
if (argv[0] === "console") {
|
|
4583
|
+
const flags = parseConsoleServeArgs(argv.slice(1));
|
|
3633
4584
|
if (!flags.status) {
|
|
3634
|
-
const result2 = await
|
|
4585
|
+
const result2 = await runConsoleServe({
|
|
3635
4586
|
loadConfig,
|
|
3636
4587
|
configPath: DEFAULT_CONFIG_PATH,
|
|
3637
4588
|
sessionsDir: DEFAULT_SESSIONS_DIR,
|
|
@@ -3645,6 +4596,7 @@ async function main() {
|
|
|
3645
4596
|
now: () => new Date().toISOString(),
|
|
3646
4597
|
gitLastCommit: createGitLastCommit(nodeExec),
|
|
3647
4598
|
waitForSignal,
|
|
4599
|
+
...process.env.OPENCODE_SERVER_PASSWORD !== undefined ? { serverPassword: process.env.OPENCODE_SERVER_PASSWORD } : {},
|
|
3648
4600
|
log: (message) => process.stdout.write(`${message}
|
|
3649
4601
|
`),
|
|
3650
4602
|
openBrowser
|
|
@@ -3653,6 +4605,27 @@ async function main() {
|
|
|
3653
4605
|
return;
|
|
3654
4606
|
}
|
|
3655
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
|
+
}
|
|
3656
4629
|
const result = await runCli(argv, deps);
|
|
3657
4630
|
process.stdout.write(`${result.stdout}
|
|
3658
4631
|
`);
|