@dreb/dashboard 2.35.0
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/README.md +146 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +251 -0
- package/dist/index.js.map +1 -0
- package/dist/server/auth.d.ts +164 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +396 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/event-hub.d.ts +31 -0
- package/dist/server/event-hub.d.ts.map +1 -0
- package/dist/server/event-hub.js +73 -0
- package/dist/server/event-hub.js.map +1 -0
- package/dist/server/files.d.ts +48 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +194 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/pairing-storage.d.ts +18 -0
- package/dist/server/pairing-storage.d.ts.map +1 -0
- package/dist/server/pairing-storage.js +83 -0
- package/dist/server/pairing-storage.js.map +1 -0
- package/dist/server/runtime-pool.d.ts +89 -0
- package/dist/server/runtime-pool.d.ts.map +1 -0
- package/dist/server/runtime-pool.js +377 -0
- package/dist/server/runtime-pool.js.map +1 -0
- package/dist/server/server.d.ts +30 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +557 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/subagent-log.d.ts +42 -0
- package/dist/server/subagent-log.d.ts.map +1 -0
- package/dist/server/subagent-log.js +155 -0
- package/dist/server/subagent-log.js.map +1 -0
- package/dist/shared/protocol.d.ts +261 -0
- package/dist/shared/protocol.d.ts.map +1 -0
- package/dist/shared/protocol.js +26 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/static/assets/index-Cgwg2zRA.js +77 -0
- package/dist/static/assets/index-DnFrQoI9.css +1 -0
- package/dist/static/icons/apple-touch-icon.png +0 -0
- package/dist/static/icons/favicon-32.png +0 -0
- package/dist/static/icons/punk-1024.png +0 -0
- package/dist/static/icons/punk-192.png +0 -0
- package/dist/static/icons/punk-512.png +0 -0
- package/dist/static/index.html +32 -0
- package/dist/static/manifest.webmanifest +30 -0
- package/dist/static/sw.js +110 -0
- package/package.json +57 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC runtime pool — one `dreb --mode rpc` child process per live session.
|
|
3
|
+
*
|
|
4
|
+
* dreb's RPC mode is strictly one-session-per-process (switch_session repoints
|
|
5
|
+
* the same process; it never multiplexes), so the pool spawns N children keyed
|
|
6
|
+
* by an opaque runtime key. The telegram bridge is the in-repo precedent.
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { RpcClient } from "@dreb/coding-agent/rpc";
|
|
13
|
+
import { MAX_COMPLETED_BACKGROUND_AGENTS, } from "../shared/protocol.js";
|
|
14
|
+
/** Resolve the absolute path to the dreb CLI (RpcClient defaults to a cwd-relative path). */
|
|
15
|
+
export function resolveDrebCliPath() {
|
|
16
|
+
const resolved = import.meta.resolve("@dreb/coding-agent");
|
|
17
|
+
return join(dirname(fileURLToPath(resolved)), "cli.js");
|
|
18
|
+
}
|
|
19
|
+
function formatRpcExit(info) {
|
|
20
|
+
if (info.error)
|
|
21
|
+
return `RPC process failed: ${info.error.message}`;
|
|
22
|
+
return `RPC process exited (code ${info.code}, signal ${info.signal})`;
|
|
23
|
+
}
|
|
24
|
+
export { MAX_COMPLETED_BACKGROUND_AGENTS };
|
|
25
|
+
export class RuntimePool {
|
|
26
|
+
runtimes = new Map();
|
|
27
|
+
listeners = [];
|
|
28
|
+
cliPath;
|
|
29
|
+
baseArgs;
|
|
30
|
+
clientFactory;
|
|
31
|
+
logger;
|
|
32
|
+
/**
|
|
33
|
+
* A single lazily-spawned utility runtime used to service settings/model/
|
|
34
|
+
* agent-type endpoints when no user session is live. Kept out of `runtimes`
|
|
35
|
+
* (and therefore out of the fleet) so it never shows as a session card.
|
|
36
|
+
*/
|
|
37
|
+
utilities = new Map();
|
|
38
|
+
utilityPromises = new Map();
|
|
39
|
+
starting = new Set();
|
|
40
|
+
startupPromises = new Set();
|
|
41
|
+
exitedHandles = new WeakSet();
|
|
42
|
+
closing = false;
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.cliPath = options.cliPath ?? resolveDrebCliPath();
|
|
45
|
+
this.baseArgs = options.baseArgs ?? [];
|
|
46
|
+
this.clientFactory =
|
|
47
|
+
options.clientFactory ?? ((o) => new RpcClient({ cliPath: o.cliPath, cwd: o.cwd, args: o.args }));
|
|
48
|
+
this.logger = options.logger ?? ((line) => console.warn(`[dashboard] ${line}`));
|
|
49
|
+
}
|
|
50
|
+
/** Subscribe to events from every runtime, tagged with the runtime key. */
|
|
51
|
+
onEvent(listener) {
|
|
52
|
+
this.listeners.push(listener);
|
|
53
|
+
return () => {
|
|
54
|
+
const i = this.listeners.indexOf(listener);
|
|
55
|
+
if (i !== -1)
|
|
56
|
+
this.listeners.splice(i, 1);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
list() {
|
|
60
|
+
return [...this.runtimes.values()];
|
|
61
|
+
}
|
|
62
|
+
get(key) {
|
|
63
|
+
return this.runtimes.get(key);
|
|
64
|
+
}
|
|
65
|
+
/** Spawn a new runtime in `cwd`, optionally opening an existing session file. */
|
|
66
|
+
async create(cwd, sessionPath) {
|
|
67
|
+
if (this.closing)
|
|
68
|
+
throw new Error("Runtime pool is closing");
|
|
69
|
+
const key = randomBytes(6).toString("hex");
|
|
70
|
+
const args = ["--ui", "dashboard", ...this.baseArgs];
|
|
71
|
+
if (sessionPath)
|
|
72
|
+
args.push("--session", sessionPath);
|
|
73
|
+
const client = this.clientFactory({ cliPath: this.cliPath, cwd, args });
|
|
74
|
+
const handle = {
|
|
75
|
+
key,
|
|
76
|
+
cwd,
|
|
77
|
+
client,
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
lastActivity: Date.now(),
|
|
80
|
+
attention: new Map(),
|
|
81
|
+
backgroundAgents: new Map(),
|
|
82
|
+
};
|
|
83
|
+
client.onEvent((event) => this.handleEvent(handle, event));
|
|
84
|
+
client.onExit((info) => this.handleRuntimeExit(handle, info));
|
|
85
|
+
const startup = this.startSessionRuntime(handle);
|
|
86
|
+
this.startupPromises.add(startup);
|
|
87
|
+
try {
|
|
88
|
+
return await startup;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.startupPromises.delete(startup);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async startSessionRuntime(handle) {
|
|
95
|
+
this.starting.add(handle);
|
|
96
|
+
try {
|
|
97
|
+
if (this.closing)
|
|
98
|
+
throw new Error("Runtime pool is closing");
|
|
99
|
+
await handle.client.start();
|
|
100
|
+
if (this.closing) {
|
|
101
|
+
await handle.client.stop();
|
|
102
|
+
throw new Error("Runtime pool is closing");
|
|
103
|
+
}
|
|
104
|
+
await this.seedBackgroundAgents(handle);
|
|
105
|
+
if (this.closing) {
|
|
106
|
+
await handle.client.stop();
|
|
107
|
+
throw new Error("Runtime pool is closing");
|
|
108
|
+
}
|
|
109
|
+
this.runtimes.set(handle.key, handle);
|
|
110
|
+
return handle;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.starting.delete(handle);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Stop a runtime and remove it from the pool. */
|
|
117
|
+
async stop(key) {
|
|
118
|
+
const handle = this.runtimes.get(key);
|
|
119
|
+
if (!handle)
|
|
120
|
+
return false;
|
|
121
|
+
this.handleEvent(handle, { type: "runtime_removed" });
|
|
122
|
+
this.runtimes.delete(key);
|
|
123
|
+
await handle.client.stop();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Return any live runtime suitable for process-global settings work, spawning
|
|
128
|
+
* a hidden utility runtime (in the home directory) if no user session exists.
|
|
129
|
+
* This is what lets the settings page — models, agent types, defaults — work
|
|
130
|
+
* with zero sessions open, instead of 503-ing.
|
|
131
|
+
*/
|
|
132
|
+
async ensureUtilityRuntime(cwd = homedir()) {
|
|
133
|
+
if (this.closing)
|
|
134
|
+
throw new Error("Runtime pool is closing");
|
|
135
|
+
const existing = this.utilities.get(cwd);
|
|
136
|
+
if (existing)
|
|
137
|
+
return existing;
|
|
138
|
+
let promise = this.utilityPromises.get(cwd);
|
|
139
|
+
if (!promise) {
|
|
140
|
+
const args = ["--ui", "dashboard", ...this.baseArgs];
|
|
141
|
+
const client = this.clientFactory({ cliPath: this.cliPath, cwd, args });
|
|
142
|
+
const handle = {
|
|
143
|
+
key: `utility:${cwd}`,
|
|
144
|
+
cwd,
|
|
145
|
+
client,
|
|
146
|
+
createdAt: Date.now(),
|
|
147
|
+
lastActivity: Date.now(),
|
|
148
|
+
attention: new Map(),
|
|
149
|
+
backgroundAgents: new Map(),
|
|
150
|
+
};
|
|
151
|
+
const startup = this.startUtilityRuntime(cwd, handle);
|
|
152
|
+
this.startupPromises.add(startup);
|
|
153
|
+
promise = startup
|
|
154
|
+
.catch((err) => {
|
|
155
|
+
// Allow a retry on the next request instead of caching the failure.
|
|
156
|
+
this.utilityPromises.delete(cwd);
|
|
157
|
+
throw err;
|
|
158
|
+
})
|
|
159
|
+
.finally(() => {
|
|
160
|
+
this.startupPromises.delete(startup);
|
|
161
|
+
});
|
|
162
|
+
this.utilityPromises.set(cwd, promise);
|
|
163
|
+
}
|
|
164
|
+
return promise;
|
|
165
|
+
}
|
|
166
|
+
async startUtilityRuntime(cwd, handle) {
|
|
167
|
+
this.starting.add(handle);
|
|
168
|
+
try {
|
|
169
|
+
if (this.closing)
|
|
170
|
+
throw new Error("Runtime pool is closing");
|
|
171
|
+
await handle.client.start();
|
|
172
|
+
if (this.closing) {
|
|
173
|
+
await handle.client.stop();
|
|
174
|
+
throw new Error("Runtime pool is closing");
|
|
175
|
+
}
|
|
176
|
+
this.utilities.set(cwd, handle);
|
|
177
|
+
return handle;
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
this.starting.delete(handle);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async stopAll() {
|
|
184
|
+
this.closing = true;
|
|
185
|
+
const handles = new Set([
|
|
186
|
+
...this.runtimes.values(),
|
|
187
|
+
...this.utilities.values(),
|
|
188
|
+
...this.starting.values(),
|
|
189
|
+
]);
|
|
190
|
+
const startupPromises = new Set([...this.startupPromises, ...this.utilityPromises.values()]);
|
|
191
|
+
this.runtimes.clear();
|
|
192
|
+
this.utilities.clear();
|
|
193
|
+
this.utilityPromises.clear();
|
|
194
|
+
await Promise.allSettled([...handles].map((handle) => handle.client.stop()));
|
|
195
|
+
await Promise.allSettled([...startupPromises]);
|
|
196
|
+
}
|
|
197
|
+
handleRuntimeExit(handle, info) {
|
|
198
|
+
if (this.closing || this.exitedHandles.has(handle) || !this.isLiveHandle(handle))
|
|
199
|
+
return;
|
|
200
|
+
this.exitedHandles.add(handle);
|
|
201
|
+
const message = formatRpcExit(info);
|
|
202
|
+
this.recordRuntimeError(handle, message);
|
|
203
|
+
this.logger(`runtime ${handle.key} ${message}`);
|
|
204
|
+
this.handleEvent(handle, { type: "agent_end", messages: [], aborted: true, errorMessage: message });
|
|
205
|
+
}
|
|
206
|
+
isLiveHandle(handle) {
|
|
207
|
+
return (this.runtimes.get(handle.key) === handle ||
|
|
208
|
+
this.utilities.get(handle.cwd) === handle ||
|
|
209
|
+
this.starting.has(handle));
|
|
210
|
+
}
|
|
211
|
+
handleEvent(handle, event) {
|
|
212
|
+
handle.lastActivity = Date.now();
|
|
213
|
+
const type = event.type;
|
|
214
|
+
// Track needs-attention sources: extension UI requests, parent
|
|
215
|
+
// paused, error states.
|
|
216
|
+
if (type === "extension_ui_request") {
|
|
217
|
+
const method = event.method;
|
|
218
|
+
if (method === "select" || method === "confirm" || method === "input" || method === "editor") {
|
|
219
|
+
handle.attention.set(`ui:${event.id}`, `extension ${method} awaiting response`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (type === "extension_ui_response_handled") {
|
|
223
|
+
handle.attention.delete(`ui:${event.id}`);
|
|
224
|
+
}
|
|
225
|
+
if (type === "agent_start") {
|
|
226
|
+
// A new turn clears prior UI-request attention (requests were resolved or timed out).
|
|
227
|
+
for (const k of [...handle.attention.keys()]) {
|
|
228
|
+
if (k.startsWith("ui:"))
|
|
229
|
+
handle.attention.delete(k);
|
|
230
|
+
}
|
|
231
|
+
handle.attention.delete("paused");
|
|
232
|
+
handle.attention.delete("suggest");
|
|
233
|
+
handle.attention.delete("error");
|
|
234
|
+
handle.error = undefined;
|
|
235
|
+
}
|
|
236
|
+
if (type === "suggest_next") {
|
|
237
|
+
// suggest_next as the ending action = "your move": mark needs-attention
|
|
238
|
+
// so the fleet card doesn't read idle. Cleared on the next agent_start.
|
|
239
|
+
handle.attention.set("suggest", "suggested command awaiting");
|
|
240
|
+
}
|
|
241
|
+
if (type === "parent_paused_for_background_agents") {
|
|
242
|
+
handle.attention.set("paused", `paused — ${event.runningAgentCount} background agents running`);
|
|
243
|
+
}
|
|
244
|
+
if (type === "agent_end") {
|
|
245
|
+
handle.attention.delete("paused");
|
|
246
|
+
}
|
|
247
|
+
if (type === "auto_retry_end" && event.success === false && event.finalError) {
|
|
248
|
+
this.recordRuntimeError(handle, String(event.finalError));
|
|
249
|
+
}
|
|
250
|
+
if (type === "auto_compaction_end" && event.errorMessage) {
|
|
251
|
+
this.recordRuntimeError(handle, String(event.errorMessage));
|
|
252
|
+
}
|
|
253
|
+
// Track background agents from lifecycle events.
|
|
254
|
+
if (type === "background_agent_start") {
|
|
255
|
+
handle.backgroundAgents.set(event.agentId, {
|
|
256
|
+
agentId: event.agentId,
|
|
257
|
+
agentType: event.agentType,
|
|
258
|
+
taskSummary: event.taskSummary,
|
|
259
|
+
startedAt: new Date().toISOString(),
|
|
260
|
+
status: "running",
|
|
261
|
+
sessionDir: event.sessionDir,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (type === "background_agent_end") {
|
|
265
|
+
const existing = handle.backgroundAgents.get(event.agentId);
|
|
266
|
+
if (existing) {
|
|
267
|
+
existing.status = event.success ? "completed" : "failed";
|
|
268
|
+
existing.sessionFile = event.sessionFile ?? existing.sessionFile;
|
|
269
|
+
this.pruneCompletedBackgroundAgents(handle);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const listener of this.listeners) {
|
|
273
|
+
try {
|
|
274
|
+
listener(handle.key, event);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// A broken SSE subscriber must not break event distribution.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
recordRuntimeError(handle, message) {
|
|
282
|
+
handle.error = message;
|
|
283
|
+
handle.attention.set("error", message);
|
|
284
|
+
}
|
|
285
|
+
pruneCompletedBackgroundAgents(handle) {
|
|
286
|
+
const evictable = [...handle.backgroundAgents.values()]
|
|
287
|
+
.map((agent, index) => {
|
|
288
|
+
const startedAtMs = Date.parse(agent.startedAt);
|
|
289
|
+
return { agent, index, startedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : 0 };
|
|
290
|
+
})
|
|
291
|
+
.filter(({ agent }) => agent.status !== "running")
|
|
292
|
+
.sort((a, b) => a.startedAtMs - b.startedAtMs || a.index - b.index);
|
|
293
|
+
const excess = evictable.length - MAX_COMPLETED_BACKGROUND_AGENTS;
|
|
294
|
+
if (excess <= 0)
|
|
295
|
+
return;
|
|
296
|
+
for (const { agent } of evictable.slice(0, excess)) {
|
|
297
|
+
handle.backgroundAgents.delete(agent.agentId);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
fallbackState(handle) {
|
|
301
|
+
return {
|
|
302
|
+
sessionId: handle.lastState?.sessionId ?? handle.key,
|
|
303
|
+
sessionName: handle.lastState?.sessionName,
|
|
304
|
+
thinkingLevel: handle.lastState?.thinkingLevel ?? "off",
|
|
305
|
+
isStreaming: false,
|
|
306
|
+
isCompacting: false,
|
|
307
|
+
steeringMode: handle.lastState?.steeringMode ?? "all",
|
|
308
|
+
followUpMode: handle.lastState?.followUpMode ?? "all",
|
|
309
|
+
sessionFile: handle.lastState?.sessionFile,
|
|
310
|
+
autoCompactionEnabled: handle.lastState?.autoCompactionEnabled ?? false,
|
|
311
|
+
messageCount: handle.lastState?.messageCount ?? 0,
|
|
312
|
+
pendingMessageCount: handle.lastState?.pendingMessageCount ?? 0,
|
|
313
|
+
contextUsage: handle.lastState?.contextUsage,
|
|
314
|
+
model: handle.lastState?.model,
|
|
315
|
+
modelFallbackMessage: handle.lastState?.modelFallbackMessage,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async seedBackgroundAgents(handle) {
|
|
319
|
+
try {
|
|
320
|
+
const agents = (await handle.client.listBackgroundAgents());
|
|
321
|
+
for (const agent of agents) {
|
|
322
|
+
handle.backgroundAgents.set(agent.agentId, agent);
|
|
323
|
+
}
|
|
324
|
+
this.pruneCompletedBackgroundAgents(handle);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
this.logger(`runtime ${handle.key} background-agent registry unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** Snapshot a runtime for the fleet endpoint. */
|
|
331
|
+
async describe(handle) {
|
|
332
|
+
let state;
|
|
333
|
+
try {
|
|
334
|
+
state = (await handle.client.getState());
|
|
335
|
+
handle.lastState = state;
|
|
336
|
+
if (handle.error?.startsWith("RPC process")) {
|
|
337
|
+
handle.error = undefined;
|
|
338
|
+
handle.attention.delete("error");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
343
|
+
this.recordRuntimeError(handle, message);
|
|
344
|
+
this.logger(`runtime ${handle.key} state unavailable for fleet card: ${message}`);
|
|
345
|
+
state = this.fallbackState(handle);
|
|
346
|
+
}
|
|
347
|
+
let stats;
|
|
348
|
+
try {
|
|
349
|
+
const sessionStats = await handle.client.getSessionStats();
|
|
350
|
+
stats = { tokensTotal: sessionStats.tokens.total, cost: sessionStats.cost };
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
this.logger(`runtime ${handle.key} stats unavailable for fleet card: ${err instanceof Error ? err.message : String(err)}`);
|
|
354
|
+
}
|
|
355
|
+
let lastAssistantText;
|
|
356
|
+
try {
|
|
357
|
+
const text = await handle.client.getLastAssistantText();
|
|
358
|
+
lastAssistantText = text ? text.slice(0, 200) : undefined;
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
this.logger(`runtime ${handle.key} last assistant text unavailable for fleet card: ${err instanceof Error ? err.message : String(err)}`);
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
key: handle.key,
|
|
365
|
+
cwd: handle.cwd,
|
|
366
|
+
state,
|
|
367
|
+
stats,
|
|
368
|
+
backgroundAgents: [...handle.backgroundAgents.values()],
|
|
369
|
+
needsAttention: handle.attention.size > 0,
|
|
370
|
+
error: handle.error,
|
|
371
|
+
lastAssistantText,
|
|
372
|
+
createdAt: new Date(handle.createdAt).toISOString(),
|
|
373
|
+
lastActivity: new Date(handle.lastActivity).toISOString(),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=runtime-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-pool.js","sourceRoot":"","sources":["../../src/server/runtime-pool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * RPC runtime pool — one `dreb --mode rpc` child process per live session.\n *\n * dreb's RPC mode is strictly one-session-per-process (switch_session repoints\n * the same process; it never multiplexes), so the pool spawns N children keyed\n * by an opaque runtime key. The telegram bridge is the in-repo precedent.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { RpcClient, type RpcExitInfo } from \"@dreb/coding-agent/rpc\";\nimport {\n\ttype BackgroundAgentDto,\n\tMAX_COMPLETED_BACKGROUND_AGENTS,\n\ttype RuntimeInfoDto,\n\ttype RuntimeStatsSummaryDto,\n\ttype SessionStateDto,\n} from \"../shared/protocol.js\";\n\n/** Resolve the absolute path to the dreb CLI (RpcClient defaults to a cwd-relative path). */\nexport function resolveDrebCliPath(): string {\n\tconst resolved = import.meta.resolve(\"@dreb/coding-agent\");\n\treturn join(dirname(fileURLToPath(resolved)), \"cli.js\");\n}\n\nfunction formatRpcExit(info: RpcExitInfo): string {\n\tif (info.error) return `RPC process failed: ${info.error.message}`;\n\treturn `RPC process exited (code ${info.code}, signal ${info.signal})`;\n}\n\nexport type RuntimeEventListener = (key: string, event: Record<string, unknown>) => void;\n\nexport { MAX_COMPLETED_BACKGROUND_AGENTS };\n\nexport interface RuntimeHandle {\n\tkey: string;\n\tcwd: string;\n\tclient: RpcClient;\n\t/** Session start time (ms epoch) — stable tiebreak for deterministic fleet ordering. */\n\tcreatedAt: number;\n\tlastActivity: number;\n\t/** Needs-attention sources, keyed so they can be cleared independently. */\n\tattention: Map<string, string>;\n\t/** Last runtime-level error, persisted server-side so fleet refreshes stay honest. */\n\terror?: string;\n\t/** Last known state, used to keep failed runtime cards renderable. */\n\tlastState?: SessionStateDto;\n\t/** Background agents seen via events (agentId → latest info). */\n\tbackgroundAgents: Map<string, BackgroundAgentDto>;\n}\n\nexport interface RuntimePoolOptions {\n\tcliPath?: string;\n\t/** Extra args for every runtime (e.g. --provider). */\n\tbaseArgs?: string[];\n\t/** RpcClient factory override for tests. */\n\tclientFactory?: (options: { cliPath: string; cwd: string; args: string[] }) => RpcClient;\n\tlogger?: (line: string) => void;\n}\n\nexport class RuntimePool {\n\tprivate readonly runtimes = new Map<string, RuntimeHandle>();\n\tprivate readonly listeners: RuntimeEventListener[] = [];\n\tprivate readonly cliPath: string;\n\tprivate readonly baseArgs: string[];\n\tprivate readonly clientFactory: (options: { cliPath: string; cwd: string; args: string[] }) => RpcClient;\n\tprivate readonly logger: (line: string) => void;\n\t/**\n\t * A single lazily-spawned utility runtime used to service settings/model/\n\t * agent-type endpoints when no user session is live. Kept out of `runtimes`\n\t * (and therefore out of the fleet) so it never shows as a session card.\n\t */\n\tprivate readonly utilities = new Map<string, RuntimeHandle>();\n\tprivate readonly utilityPromises = new Map<string, Promise<RuntimeHandle>>();\n\tprivate readonly starting = new Set<RuntimeHandle>();\n\tprivate readonly startupPromises = new Set<Promise<unknown>>();\n\tprivate readonly exitedHandles = new WeakSet<RuntimeHandle>();\n\tprivate closing = false;\n\n\tconstructor(options: RuntimePoolOptions = {}) {\n\t\tthis.cliPath = options.cliPath ?? resolveDrebCliPath();\n\t\tthis.baseArgs = options.baseArgs ?? [];\n\t\tthis.clientFactory =\n\t\t\toptions.clientFactory ?? ((o) => new RpcClient({ cliPath: o.cliPath, cwd: o.cwd, args: o.args }));\n\t\tthis.logger = options.logger ?? ((line) => console.warn(`[dashboard] ${line}`));\n\t}\n\n\t/** Subscribe to events from every runtime, tagged with the runtime key. */\n\tonEvent(listener: RuntimeEventListener): () => void {\n\t\tthis.listeners.push(listener);\n\t\treturn () => {\n\t\t\tconst i = this.listeners.indexOf(listener);\n\t\t\tif (i !== -1) this.listeners.splice(i, 1);\n\t\t};\n\t}\n\n\tlist(): RuntimeHandle[] {\n\t\treturn [...this.runtimes.values()];\n\t}\n\n\tget(key: string): RuntimeHandle | undefined {\n\t\treturn this.runtimes.get(key);\n\t}\n\n\t/** Spawn a new runtime in `cwd`, optionally opening an existing session file. */\n\tasync create(cwd: string, sessionPath?: string): Promise<RuntimeHandle> {\n\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\tconst key = randomBytes(6).toString(\"hex\");\n\t\tconst args = [\"--ui\", \"dashboard\", ...this.baseArgs];\n\t\tif (sessionPath) args.push(\"--session\", sessionPath);\n\t\tconst client = this.clientFactory({ cliPath: this.cliPath, cwd, args });\n\t\tconst handle: RuntimeHandle = {\n\t\t\tkey,\n\t\t\tcwd,\n\t\t\tclient,\n\t\t\tcreatedAt: Date.now(),\n\t\t\tlastActivity: Date.now(),\n\t\t\tattention: new Map(),\n\t\t\tbackgroundAgents: new Map(),\n\t\t};\n\t\tclient.onEvent((event) => this.handleEvent(handle, event as unknown as Record<string, unknown>));\n\t\tclient.onExit((info) => this.handleRuntimeExit(handle, info));\n\n\t\tconst startup = this.startSessionRuntime(handle);\n\t\tthis.startupPromises.add(startup);\n\t\ttry {\n\t\t\treturn await startup;\n\t\t} finally {\n\t\t\tthis.startupPromises.delete(startup);\n\t\t}\n\t}\n\n\tprivate async startSessionRuntime(handle: RuntimeHandle): Promise<RuntimeHandle> {\n\t\tthis.starting.add(handle);\n\t\ttry {\n\t\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\t\tawait handle.client.start();\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tawait this.seedBackgroundAgents(handle);\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tthis.runtimes.set(handle.key, handle);\n\t\t\treturn handle;\n\t\t} finally {\n\t\t\tthis.starting.delete(handle);\n\t\t}\n\t}\n\n\t/** Stop a runtime and remove it from the pool. */\n\tasync stop(key: string): Promise<boolean> {\n\t\tconst handle = this.runtimes.get(key);\n\t\tif (!handle) return false;\n\t\tthis.handleEvent(handle, { type: \"runtime_removed\" });\n\t\tthis.runtimes.delete(key);\n\t\tawait handle.client.stop();\n\t\treturn true;\n\t}\n\n\t/**\n\t * Return any live runtime suitable for process-global settings work, spawning\n\t * a hidden utility runtime (in the home directory) if no user session exists.\n\t * This is what lets the settings page — models, agent types, defaults — work\n\t * with zero sessions open, instead of 503-ing.\n\t */\n\tasync ensureUtilityRuntime(cwd = homedir()): Promise<RuntimeHandle> {\n\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\tconst existing = this.utilities.get(cwd);\n\t\tif (existing) return existing;\n\t\tlet promise = this.utilityPromises.get(cwd);\n\t\tif (!promise) {\n\t\t\tconst args = [\"--ui\", \"dashboard\", ...this.baseArgs];\n\t\t\tconst client = this.clientFactory({ cliPath: this.cliPath, cwd, args });\n\t\t\tconst handle: RuntimeHandle = {\n\t\t\t\tkey: `utility:${cwd}`,\n\t\t\t\tcwd,\n\t\t\t\tclient,\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t\tlastActivity: Date.now(),\n\t\t\t\tattention: new Map(),\n\t\t\t\tbackgroundAgents: new Map(),\n\t\t\t};\n\t\t\tconst startup = this.startUtilityRuntime(cwd, handle);\n\t\t\tthis.startupPromises.add(startup);\n\t\t\tpromise = startup\n\t\t\t\t.catch((err) => {\n\t\t\t\t\t// Allow a retry on the next request instead of caching the failure.\n\t\t\t\t\tthis.utilityPromises.delete(cwd);\n\t\t\t\t\tthrow err;\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.startupPromises.delete(startup);\n\t\t\t\t});\n\t\t\tthis.utilityPromises.set(cwd, promise);\n\t\t}\n\t\treturn promise;\n\t}\n\n\tprivate async startUtilityRuntime(cwd: string, handle: RuntimeHandle): Promise<RuntimeHandle> {\n\t\tthis.starting.add(handle);\n\t\ttry {\n\t\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\t\tawait handle.client.start();\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tthis.utilities.set(cwd, handle);\n\t\t\treturn handle;\n\t\t} finally {\n\t\t\tthis.starting.delete(handle);\n\t\t}\n\t}\n\n\tasync stopAll(): Promise<void> {\n\t\tthis.closing = true;\n\t\tconst handles = new Set<RuntimeHandle>([\n\t\t\t...this.runtimes.values(),\n\t\t\t...this.utilities.values(),\n\t\t\t...this.starting.values(),\n\t\t]);\n\t\tconst startupPromises = new Set<Promise<unknown>>([...this.startupPromises, ...this.utilityPromises.values()]);\n\t\tthis.runtimes.clear();\n\t\tthis.utilities.clear();\n\t\tthis.utilityPromises.clear();\n\t\tawait Promise.allSettled([...handles].map((handle) => handle.client.stop()));\n\t\tawait Promise.allSettled([...startupPromises]);\n\t}\n\n\tprivate handleRuntimeExit(handle: RuntimeHandle, info: RpcExitInfo): void {\n\t\tif (this.closing || this.exitedHandles.has(handle) || !this.isLiveHandle(handle)) return;\n\t\tthis.exitedHandles.add(handle);\n\t\tconst message = formatRpcExit(info);\n\t\tthis.recordRuntimeError(handle, message);\n\t\tthis.logger(`runtime ${handle.key} ${message}`);\n\t\tthis.handleEvent(handle, { type: \"agent_end\", messages: [], aborted: true, errorMessage: message });\n\t}\n\n\tprivate isLiveHandle(handle: RuntimeHandle): boolean {\n\t\treturn (\n\t\t\tthis.runtimes.get(handle.key) === handle ||\n\t\t\tthis.utilities.get(handle.cwd) === handle ||\n\t\t\tthis.starting.has(handle)\n\t\t);\n\t}\n\n\tprivate handleEvent(handle: RuntimeHandle, event: Record<string, unknown>): void {\n\t\thandle.lastActivity = Date.now();\n\t\tconst type = event.type as string;\n\n\t\t// Track needs-attention sources: extension UI requests, parent\n\t\t// paused, error states.\n\t\tif (type === \"extension_ui_request\") {\n\t\t\tconst method = event.method as string;\n\t\t\tif (method === \"select\" || method === \"confirm\" || method === \"input\" || method === \"editor\") {\n\t\t\t\thandle.attention.set(`ui:${event.id}`, `extension ${method} awaiting response`);\n\t\t\t}\n\t\t}\n\t\tif (type === \"extension_ui_response_handled\") {\n\t\t\thandle.attention.delete(`ui:${event.id}`);\n\t\t}\n\t\tif (type === \"agent_start\") {\n\t\t\t// A new turn clears prior UI-request attention (requests were resolved or timed out).\n\t\t\tfor (const k of [...handle.attention.keys()]) {\n\t\t\t\tif (k.startsWith(\"ui:\")) handle.attention.delete(k);\n\t\t\t}\n\t\t\thandle.attention.delete(\"paused\");\n\t\t\thandle.attention.delete(\"suggest\");\n\t\t\thandle.attention.delete(\"error\");\n\t\t\thandle.error = undefined;\n\t\t}\n\t\tif (type === \"suggest_next\") {\n\t\t\t// suggest_next as the ending action = \"your move\": mark needs-attention\n\t\t\t// so the fleet card doesn't read idle. Cleared on the next agent_start.\n\t\t\thandle.attention.set(\"suggest\", \"suggested command awaiting\");\n\t\t}\n\t\tif (type === \"parent_paused_for_background_agents\") {\n\t\t\thandle.attention.set(\"paused\", `paused — ${event.runningAgentCount} background agents running`);\n\t\t}\n\t\tif (type === \"agent_end\") {\n\t\t\thandle.attention.delete(\"paused\");\n\t\t}\n\t\tif (type === \"auto_retry_end\" && event.success === false && event.finalError) {\n\t\t\tthis.recordRuntimeError(handle, String(event.finalError));\n\t\t}\n\t\tif (type === \"auto_compaction_end\" && event.errorMessage) {\n\t\t\tthis.recordRuntimeError(handle, String(event.errorMessage));\n\t\t}\n\n\t\t// Track background agents from lifecycle events.\n\t\tif (type === \"background_agent_start\") {\n\t\t\thandle.backgroundAgents.set(event.agentId as string, {\n\t\t\t\tagentId: event.agentId as string,\n\t\t\t\tagentType: event.agentType as string,\n\t\t\t\ttaskSummary: event.taskSummary as string,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t\tstatus: \"running\",\n\t\t\t\tsessionDir: event.sessionDir as string | undefined,\n\t\t\t});\n\t\t}\n\t\tif (type === \"background_agent_end\") {\n\t\t\tconst existing = handle.backgroundAgents.get(event.agentId as string);\n\t\t\tif (existing) {\n\t\t\t\texisting.status = event.success ? \"completed\" : \"failed\";\n\t\t\t\texisting.sessionFile = (event.sessionFile as string | undefined) ?? existing.sessionFile;\n\t\t\t\tthis.pruneCompletedBackgroundAgents(handle);\n\t\t\t}\n\t\t}\n\n\t\tfor (const listener of this.listeners) {\n\t\t\ttry {\n\t\t\t\tlistener(handle.key, event);\n\t\t\t} catch {\n\t\t\t\t// A broken SSE subscriber must not break event distribution.\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate recordRuntimeError(handle: RuntimeHandle, message: string): void {\n\t\thandle.error = message;\n\t\thandle.attention.set(\"error\", message);\n\t}\n\n\tprivate pruneCompletedBackgroundAgents(handle: RuntimeHandle): void {\n\t\tconst evictable = [...handle.backgroundAgents.values()]\n\t\t\t.map((agent, index) => {\n\t\t\t\tconst startedAtMs = Date.parse(agent.startedAt);\n\t\t\t\treturn { agent, index, startedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : 0 };\n\t\t\t})\n\t\t\t.filter(({ agent }) => agent.status !== \"running\")\n\t\t\t.sort((a, b) => a.startedAtMs - b.startedAtMs || a.index - b.index);\n\t\tconst excess = evictable.length - MAX_COMPLETED_BACKGROUND_AGENTS;\n\t\tif (excess <= 0) return;\n\t\tfor (const { agent } of evictable.slice(0, excess)) {\n\t\t\thandle.backgroundAgents.delete(agent.agentId);\n\t\t}\n\t}\n\n\tprivate fallbackState(handle: RuntimeHandle): SessionStateDto {\n\t\treturn {\n\t\t\tsessionId: handle.lastState?.sessionId ?? handle.key,\n\t\t\tsessionName: handle.lastState?.sessionName,\n\t\t\tthinkingLevel: handle.lastState?.thinkingLevel ?? \"off\",\n\t\t\tisStreaming: false,\n\t\t\tisCompacting: false,\n\t\t\tsteeringMode: handle.lastState?.steeringMode ?? \"all\",\n\t\t\tfollowUpMode: handle.lastState?.followUpMode ?? \"all\",\n\t\t\tsessionFile: handle.lastState?.sessionFile,\n\t\t\tautoCompactionEnabled: handle.lastState?.autoCompactionEnabled ?? false,\n\t\t\tmessageCount: handle.lastState?.messageCount ?? 0,\n\t\t\tpendingMessageCount: handle.lastState?.pendingMessageCount ?? 0,\n\t\t\tcontextUsage: handle.lastState?.contextUsage,\n\t\t\tmodel: handle.lastState?.model,\n\t\t\tmodelFallbackMessage: handle.lastState?.modelFallbackMessage,\n\t\t};\n\t}\n\n\tprivate async seedBackgroundAgents(handle: RuntimeHandle): Promise<void> {\n\t\ttry {\n\t\t\tconst agents = (await handle.client.listBackgroundAgents()) as unknown as BackgroundAgentDto[];\n\t\t\tfor (const agent of agents) {\n\t\t\t\thandle.backgroundAgents.set(agent.agentId, agent);\n\t\t\t}\n\t\t\tthis.pruneCompletedBackgroundAgents(handle);\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} background-agent registry unavailable: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Snapshot a runtime for the fleet endpoint. */\n\tasync describe(handle: RuntimeHandle): Promise<RuntimeInfoDto> {\n\t\tlet state: SessionStateDto;\n\t\ttry {\n\t\t\tstate = (await handle.client.getState()) as unknown as SessionStateDto;\n\t\t\thandle.lastState = state;\n\t\t\tif (handle.error?.startsWith(\"RPC process\")) {\n\t\t\t\thandle.error = undefined;\n\t\t\t\thandle.attention.delete(\"error\");\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tthis.recordRuntimeError(handle, message);\n\t\t\tthis.logger(`runtime ${handle.key} state unavailable for fleet card: ${message}`);\n\t\t\tstate = this.fallbackState(handle);\n\t\t}\n\t\tlet stats: RuntimeStatsSummaryDto | undefined;\n\t\ttry {\n\t\t\tconst sessionStats = await handle.client.getSessionStats();\n\t\t\tstats = { tokensTotal: sessionStats.tokens.total, cost: sessionStats.cost };\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} stats unavailable for fleet card: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t\tlet lastAssistantText: string | undefined;\n\t\ttry {\n\t\t\tconst text = await handle.client.getLastAssistantText();\n\t\t\tlastAssistantText = text ? text.slice(0, 200) : undefined;\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} last assistant text unavailable for fleet card: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t\treturn {\n\t\t\tkey: handle.key,\n\t\t\tcwd: handle.cwd,\n\t\t\tstate,\n\t\t\tstats,\n\t\t\tbackgroundAgents: [...handle.backgroundAgents.values()],\n\t\t\tneedsAttention: handle.attention.size > 0,\n\t\t\terror: handle.error,\n\t\t\tlastAssistantText,\n\t\t\tcreatedAt: new Date(handle.createdAt).toISOString(),\n\t\t\tlastActivity: new Date(handle.lastActivity).toISOString(),\n\t\t};\n\t}\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard HTTP server — Express app wiring auth, the runtime pool, the SSE
|
|
3
|
+
* hub, and the file API into the REST surface the browser client consumes.
|
|
4
|
+
*
|
|
5
|
+
* Bind address discipline: local mode binds 127.0.0.1 only. The
|
|
6
|
+
* caller decides the bind address; `createDashboardServer` never listens by
|
|
7
|
+
* itself. Remote mode still passes every request through DashboardAuth.
|
|
8
|
+
*/
|
|
9
|
+
import express from "express";
|
|
10
|
+
import type { DashboardAuth } from "./auth.js";
|
|
11
|
+
import type { RuntimePool } from "./runtime-pool.js";
|
|
12
|
+
export interface DashboardServerOptions {
|
|
13
|
+
auth: DashboardAuth;
|
|
14
|
+
pool: RuntimePool;
|
|
15
|
+
/** Directory of built client assets; omit to skip static serving (tests). */
|
|
16
|
+
staticDir?: string;
|
|
17
|
+
/** Session listing (cross-project) — injected so tests can stub it. */
|
|
18
|
+
listAllSessions: () => Promise<unknown[]>;
|
|
19
|
+
deleteSession: (path: string) => Promise<unknown>;
|
|
20
|
+
logger?: (line: string) => void;
|
|
21
|
+
/** Build version of the running server process (for the settings footer / stale-server detection). */
|
|
22
|
+
serverVersion?: string;
|
|
23
|
+
/** Restart hook — when set, POST /api/server/restart invokes it (typically process exit for a supervisor to respawn). */
|
|
24
|
+
onRestart?: () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare const MAX_SSE_BUFFERED_BYTES: number;
|
|
27
|
+
/** Parse the device cookie from a Cookie header. */
|
|
28
|
+
export declare function parseDeviceCookie(cookieHeader: string | undefined): string | undefined;
|
|
29
|
+
export declare function createDashboardServer(options: DashboardServerOptions): express.Express;
|
|
30
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,OAAO,MAAM,SAAS,CAAC;AAG9B,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,WAAW,CAAC;AAG7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAuE;IACvE,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC1C,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,sGAAsG;IACtG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2HAAyH;IACzH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAGD,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AAEtD,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQtF;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,OAAO,CAskBtF","sourcesContent":["/**\n * Dashboard HTTP server — Express app wiring auth, the runtime pool, the SSE\n * hub, and the file API into the REST surface the browser client consumes.\n *\n * Bind address discipline: local mode binds 127.0.0.1 only. The\n * caller decides the bind address; `createDashboardServer` never listens by\n * itself. Remote mode still passes every request through DashboardAuth.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport type { NextFunction, Request, Response } from \"express\";\nimport express from \"express\";\nimport type { AuthStatusDto, FleetDto, ImageAttachmentDto, PairingCodeDto } from \"../shared/protocol.js\";\nimport { MAX_PROMPT_BODY_BYTES } from \"../shared/protocol.js\";\nimport type { AuthDecision, DashboardAuth } from \"./auth.js\";\nimport { EventHub } from \"./event-hub.js\";\nimport { defaultPlaces, FileApi } from \"./files.js\";\nimport type { RuntimePool } from \"./runtime-pool.js\";\nimport { readSubagentMessages } from \"./subagent-log.js\";\n\nexport interface DashboardServerOptions {\n\tauth: DashboardAuth;\n\tpool: RuntimePool;\n\t/** Directory of built client assets; omit to skip static serving (tests). */\n\tstaticDir?: string;\n\t/** Session listing (cross-project) — injected so tests can stub it. */\n\tlistAllSessions: () => Promise<unknown[]>;\n\tdeleteSession: (path: string) => Promise<unknown>;\n\tlogger?: (line: string) => void;\n\t/** Build version of the running server process (for the settings footer / stale-server detection). */\n\tserverVersion?: string;\n\t/** Restart hook — when set, POST /api/server/restart invokes it (typically process exit for a supervisor to respawn). */\n\tonRestart?: () => void;\n}\n\nconst DEVICE_COOKIE = \"dreb_dashboard_device\";\nexport const MAX_SSE_BUFFERED_BYTES = 4 * 1024 * 1024;\n\n/** Parse the device cookie from a Cookie header. */\nexport function parseDeviceCookie(cookieHeader: string | undefined): string | undefined {\n\tif (!cookieHeader) return undefined;\n\tfor (const part of cookieHeader.split(\";\")) {\n\t\tconst eq = part.indexOf(\"=\");\n\t\tif (eq === -1) continue;\n\t\tif (part.slice(0, eq).trim() === DEVICE_COOKIE) return part.slice(eq + 1).trim();\n\t}\n\treturn undefined;\n}\n\ninterface AuthedRequest extends Request {\n\tauthDecision?: AuthDecision;\n}\n\nexport function createDashboardServer(options: DashboardServerOptions): express.Express {\n\tconst { auth, pool } = options;\n\tconst serverStartedAt = new Date().toISOString();\n\tconst log = options.logger ?? ((line: string) => console.log(`[dashboard] ${line}`));\n\tconst files = new FileApi((op, path, detail) => log(`file ${op}: ${path}${detail ? ` (${detail})` : \"\"}`));\n\tconst hub = new EventHub();\n\tpool.onEvent((key, event) => hub.publish(key, event));\n\n\tconst app = express();\n\tapp.disable(\"x-powered-by\");\n\tapp.use(express.json({ limit: MAX_PROMPT_BODY_BYTES }));\n\n\t// -- auth middleware (every route, fail-closed) ---------------------------\n\tapp.use((req: AuthedRequest, res: Response, next: NextFunction) => {\n\t\tauth\n\t\t\t.authenticate({\n\t\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\t\thostHeader: req.headers.host,\n\t\t\t\toriginHeader: req.headers.origin,\n\t\t\t\tdeviceToken: parseDeviceCookie(req.headers.cookie),\n\t\t\t})\n\t\t\t.then((decision) => {\n\t\t\t\treq.authDecision = decision;\n\t\t\t\tif (decision.allowed) return next();\n\t\t\t\tconst canRenderAuthScreen = decision.needsPairing || Boolean(decision.identity);\n\t\t\t\tif (canRenderAuthScreen) {\n\t\t\t\t\t// The auth/pairing endpoints must be reachable by allowed-but-unpaired\n\t\t\t\t\t// identities, and /api/auth must also be reachable by rejected\n\t\t\t\t\t// Tailscale identities so the SPA denial screen can name them.\n\t\t\t\t\tif (req.path === \"/api/auth\" || (decision.needsPairing && req.path === \"/api/pair\")) return next();\n\t\t\t\t\t// Let the SPA shell + static assets load so the client-side pairing or\n\t\t\t\t\t// denial screen can render. No data exposure: every /api/* data route\n\t\t\t\t\t// below stays fail-closed — only non-API GETs (the app shell) are allowed.\n\t\t\t\t\tif (req.method === \"GET\" && !req.path.startsWith(\"/api/\")) return next();\n\t\t\t\t}\n\t\t\t\tlog(`denied ${req.method} ${req.path}: ${decision.reason}`);\n\t\t\t\tres.status(decision.status).json({\n\t\t\t\t\terror: decision.reason,\n\t\t\t\t\tneedsPairing: decision.needsPairing ?? false,\n\t\t\t\t\tidentity: decision.identity?.loginName,\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\t// authenticate() already catches internally; this is belt-and-suspenders.\n\t\t\t\tlog(`auth middleware error — denying: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\tres.status(500).json({ error: \"Auth subsystem error — denied\" });\n\t\t\t});\n\t});\n\n\t// -- auth/pairing ----------------------------------------------------------\n\tapp.get(\"/api/auth\", (req: AuthedRequest, res) => {\n\t\tconst decision = req.authDecision!;\n\t\tif (decision.allowed) {\n\t\t\tconst status: AuthStatusDto =\n\t\t\t\tdecision.mode === \"local\"\n\t\t\t\t\t? { mode: \"local\" }\n\t\t\t\t\t: { mode: \"remote\", identity: decision.identity.loginName, device: decision.identity.device };\n\t\t\tres.json({ ...status, needsPairing: false });\n\t\t\treturn;\n\t\t}\n\t\tres.status(decision.status).json({\n\t\t\terror: decision.reason,\n\t\t\tneedsPairing: decision.needsPairing ?? false,\n\t\t\tidentity: decision.identity?.loginName,\n\t\t});\n\t});\n\n\tapp.get(\"/api/pairing-code\", (req: AuthedRequest, res) => {\n\t\tconst decision = req.authDecision!;\n\t\tif (!decision.allowed || decision.mode !== \"local\") {\n\t\t\tres.status(403).json({ error: \"Pairing code is only available from the host machine\" });\n\t\t\treturn;\n\t\t}\n\t\tif (!auth.isRemoteEnabled) {\n\t\t\tconst body: PairingCodeDto = { enabled: false };\n\t\t\tres.json(body);\n\t\t\treturn;\n\t\t}\n\t\tconst body: PairingCodeDto = { enabled: true, ...auth.currentPairingCode() };\n\t\tres.json(body);\n\t});\n\n\tapp.post(\"/api/pair\", (req: AuthedRequest, res) => {\n\t\tconst pin = typeof req.body?.pin === \"string\" ? req.body.pin : \"\";\n\t\tauth\n\t\t\t.pair(\n\t\t\t\t{\n\t\t\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\t\t\thostHeader: req.headers.host,\n\t\t\t\t\toriginHeader: req.headers.origin,\n\t\t\t\t\tdeviceToken: undefined,\n\t\t\t\t},\n\t\t\t\tpin,\n\t\t\t)\n\t\t\t.then(({ token, device }) => {\n\t\t\t\tlog(`paired device ${device.id} (${device.identity})`);\n\t\t\t\tres.cookie(DEVICE_COOKIE, token, {\n\t\t\t\t\thttpOnly: true,\n\t\t\t\t\tsameSite: \"strict\",\n\t\t\t\t\tsecure: false, // Tailscale already encrypts; the dashboard serves plain HTTP on the tailnet.\n\t\t\t\t\texpires: new Date(device.expiresAt),\n\t\t\t\t}).json({ device });\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tconst status = typeof err?.status === \"number\" ? err.status : 500;\n\t\t\t\tlog(`pairing failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\tres.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n\t\t\t});\n\t});\n\n\tapp.get(\"/api/devices\", (_req, res) => {\n\t\tauth\n\t\t\t.listDevices()\n\t\t\t.then((devices) => res.json({ devices }))\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.delete(\"/api/devices/:id\", (req, res) => {\n\t\tauth\n\t\t\t.unpair(req.params.id)\n\t\t\t.then((removed) => {\n\t\t\t\tif (!removed) {\n\t\t\t\t\tres.status(404).json({ error: `No paired device with id ${String(req.params.id)}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlog(`unpaired device ${String(req.params.id)}`);\n\t\t\t\tres.json({ ok: true });\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- events (SSE) ----------------------------------------------------------\n\tapp.get(\"/api/events\", (req, res) => {\n\t\tres.writeHead(200, {\n\t\t\t\"content-type\": \"text/event-stream\",\n\t\t\t\"cache-control\": \"no-cache\",\n\t\t\tconnection: \"keep-alive\",\n\t\t});\n\n\t\tconst guardedWrite = (chunk: string, context: string): boolean => {\n\t\t\tif (res.destroyed || res.writableEnded) return false;\n\t\t\tconst accepted = res.write(chunk);\n\t\t\tif (!accepted && res.writableLength > MAX_SSE_BUFFERED_BYTES) {\n\t\t\t\tlog(\n\t\t\t\t\t`SSE client buffer exceeded ${MAX_SSE_BUFFERED_BYTES} bytes during ${context} (${res.writableLength} bytes queued); destroying connection`,\n\t\t\t\t);\n\t\t\t\tres.destroy();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\n\t\tif (!guardedWrite(\":ok\\n\\n\", \"initial handshake\")) return;\n\t\tconst lastIdRaw = req.headers[\"last-event-id\"] ?? req.query.lastEventId;\n\t\tconst lastEventId =\n\t\t\ttypeof lastIdRaw === \"string\" && /^\\d+$/.test(lastIdRaw) ? Number.parseInt(lastIdRaw, 10) : undefined;\n\t\tconst detach = hub.attach({ write: (chunk) => guardedWrite(chunk, \"event fanout\") }, lastEventId);\n\t\tconst keepAlive = setInterval(() => {\n\t\t\tguardedWrite(\":ka\\n\\n\", \"keepalive\");\n\t\t}, 25_000);\n\t\treq.on(\"close\", () => {\n\t\t\tclearInterval(keepAlive);\n\t\t\tdetach();\n\t\t});\n\t});\n\n\t// -- fleet -----------------------------------------------------------------\n\tapp.get(\"/api/fleet\", (_req, res) => {\n\t\t(async () => {\n\t\t\tconst runtimes = await Promise.all(pool.list().map((h) => pool.describe(h)));\n\t\t\tconst diskSessions = ((await options.listAllSessions()) as FleetDto[\"diskSessions\"]).filter((session) =>\n\t\t\t\texistsSync(session.cwd),\n\t\t\t);\n\t\t\tconst fleet: FleetDto = { runtimes, diskSessions };\n\t\t\tres.json(fleet);\n\t\t})().catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- runtimes ---------------------------------------------------------------\n\tapp.post(\"/api/runtimes\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst cwd = typeof req.body?.cwd === \"string\" ? req.body.cwd : \"\";\n\t\t\tif (!cwd || !existsSync(cwd)) {\n\t\t\t\tres.status(400).json({ error: `Working directory does not exist: ${cwd || \"(empty)\"}` });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst sessionPath = typeof req.body?.sessionPath === \"string\" ? req.body.sessionPath : undefined;\n\t\t\tconst handle = await pool.create(cwd, sessionPath);\n\t\t\tlog(`runtime ${handle.key} started in ${cwd}${sessionPath ? ` (resume ${basename(sessionPath)})` : \"\"}`);\n\t\t\tconst firstPrompt = typeof req.body?.firstPrompt === \"string\" ? req.body.firstPrompt : undefined;\n\t\t\tif (firstPrompt) await handle.client.prompt(firstPrompt);\n\t\t\tres.status(201).json(await pool.describe(handle));\n\t\t})().catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.delete(\"/api/runtimes/:key\", (req, res) => {\n\t\tpool\n\t\t\t.stop(req.params.key)\n\t\t\t.then((stopped) => {\n\t\t\t\tif (!stopped) {\n\t\t\t\t\tres.status(404).json({ error: `No runtime ${String(req.params.key)}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlog(`runtime ${String(req.params.key)} stopped`);\n\t\t\t\tres.json({ ok: true });\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t/** Helper: run an async op against a pooled runtime with uniform errors. */\n\tfunction withRuntime(\n\t\treq: Request,\n\t\tres: Response,\n\t\tfn: (handle: NonNullable<ReturnType<RuntimePool[\"get\"]>>) => Promise<unknown>,\n\t): void {\n\t\tconst handle = pool.get(String(req.params.key));\n\t\tif (!handle) {\n\t\t\tres.status(404).json({ error: `No runtime ${String(req.params.key)}` });\n\t\t\treturn;\n\t\t}\n\t\tfn(handle)\n\t\t\t.then((data) => res.json(data ?? { ok: true }))\n\t\t\t.catch((err) => {\n\t\t\t\tres.status(502).json({ error: String(err?.message ?? err) });\n\t\t\t});\n\t}\n\n\tapp.get(\"/api/runtimes/:key\", (req, res) => {\n\t\twithRuntime(req, res, (h) => pool.describe(h));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/messages\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ messages: await h.client.getMessages() }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/pending\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getPendingMessages());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/dequeue\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.clearPendingMessages());\n\t});\n\n\tfunction parseImages(body: unknown): ImageAttachmentDto[] | undefined | \"invalid\" {\n\t\tconst images = (body as { images?: unknown } | undefined)?.images;\n\t\tif (images === undefined) return undefined;\n\t\tif (!Array.isArray(images)) return \"invalid\";\n\t\tconst parsed: ImageAttachmentDto[] = [];\n\t\tfor (const image of images) {\n\t\t\tif (\n\t\t\t\t!image ||\n\t\t\t\ttypeof image !== \"object\" ||\n\t\t\t\ttypeof (image as { data?: unknown }).data !== \"string\" ||\n\t\t\t\ttypeof (image as { mimeType?: unknown }).mimeType !== \"string\"\n\t\t\t) {\n\t\t\t\treturn \"invalid\";\n\t\t\t}\n\t\t\tparsed.push({ data: (image as ImageAttachmentDto).data, mimeType: (image as ImageAttachmentDto).mimeType });\n\t\t}\n\t\treturn parsed;\n\t}\n\n\tapp.post(\"/api/runtimes/:key/prompt\", (req, res) => {\n\t\tconst { message, mode } = req.body ?? {};\n\t\tif (typeof message !== \"string\" || message.length === 0) {\n\t\t\tres.status(400).json({ error: \"message is required\" });\n\t\t\treturn;\n\t\t}\n\t\tconst images = parseImages(req.body);\n\t\tif (images === \"invalid\") {\n\t\t\tres.status(400).json({ error: \"images must be an array of {data, mimeType} objects\" });\n\t\t\treturn;\n\t\t}\n\t\tconst rpcImages = images?.map((image) => ({\n\t\t\ttype: \"image\" as const,\n\t\t\tdata: image.data,\n\t\t\tmimeType: image.mimeType,\n\t\t}));\n\t\twithRuntime(req, res, async (h) => {\n\t\t\tif (mode === \"steer\") await h.client.steer(message, rpcImages);\n\t\t\telse if (mode === \"follow_up\") await h.client.followUp(message, rpcImages);\n\t\t\telse await h.client.prompt(message, rpcImages);\n\t\t});\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abort());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort-compaction\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abortCompaction());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort-retry\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abortRetry());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/model\", (req, res) => {\n\t\tconst { provider, modelId } = req.body ?? {};\n\t\tif (typeof provider !== \"string\" || typeof modelId !== \"string\") {\n\t\t\tres.status(400).json({ error: \"provider and modelId are required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.setModel(provider, modelId));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/models\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ models: await h.client.getAvailableModels() }));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/thinking\", (req, res) => {\n\t\tconst { level } = req.body ?? {};\n\t\tif (typeof level !== \"string\") {\n\t\t\tres.status(400).json({ error: \"level is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.setThinkingLevel(level as never));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/compact\", (req, res) => {\n\t\tconst instructions = typeof req.body?.instructions === \"string\" ? req.body.instructions : undefined;\n\t\twithRuntime(req, res, (h) => h.client.compact(instructions));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/name\", (req, res) => {\n\t\tconst { name } = req.body ?? {};\n\t\tif (typeof name !== \"string\" || name.length === 0) {\n\t\t\tres.status(400).json({ error: \"name is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.setSessionName(name));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/stats\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getSessionStats());\n\t});\n\n\tapp.get(\"/api/runtimes/:key/performance\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getPerformanceStats());\n\t});\n\n\tapp.get(\"/api/runtimes/:key/resources\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getResources());\n\t});\n\n\tapp.get(\"/api/runtimes/:key/commands\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ commands: await h.client.getCommands() }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/branch\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ branch: await h.client.getGitBranch() }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/fork-messages\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ messages: await h.client.getForkMessages() }));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/fork\", (req, res) => {\n\t\tconst { entryId } = req.body ?? {};\n\t\tif (typeof entryId !== \"string\") {\n\t\t\tres.status(400).json({ error: \"entryId is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.fork(entryId));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/export-html\", (req, res) => {\n\t\tconst handle = pool.get(String(req.params.key));\n\t\tif (!handle) {\n\t\t\tres.status(404).json({ error: `No runtime ${String(req.params.key)}` });\n\t\t\treturn;\n\t\t}\n\t\thandle.client\n\t\t\t.exportHtml()\n\t\t\t.then(({ path }) => {\n\t\t\t\tres.download(path);\n\t\t\t})\n\t\t\t.catch((err) => res.status(502).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/background-agents\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ agents: await h.client.listBackgroundAgents() }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/subagents/:agentId/messages\", (req, res) => {\n\t\tconst agentId = String(req.params.agentId);\n\t\twithRuntime(req, res, async (h) => {\n\t\t\t// The runtime's registry is authoritative for status + log location.\n\t\t\tconst agents = await h.client.listBackgroundAgents();\n\t\t\tconst agent = agents.find((a) => a.agentId === agentId);\n\t\t\tif (!agent) throw new Error(`No background agent ${agentId} in this runtime`);\n\t\t\tconst messages = readSubagentMessages(agent);\n\t\t\treturn { agent, messages };\n\t\t});\n\t});\n\n\tapp.post(\"/api/runtimes/:key/extension-ui-response\", (req, res) => {\n\t\tconst handle = pool.get(String(req.params.key));\n\t\tif (!handle) {\n\t\t\tres.status(404).json({ error: `No runtime ${String(req.params.key)}` });\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\thandle.client.sendExtensionUIResponse(req.body);\n\t\t\tres.json({ ok: true });\n\t\t} catch (err) {\n\t\t\tres.status(502).json({ error: String((err as Error)?.message ?? err) });\n\t\t}\n\t});\n\n\t// -- disk sessions -----------------------------------------------------------\n\tapp.delete(\"/api/sessions\", (req, res) => {\n\t\tconst path = typeof req.body?.path === \"string\" ? req.body.path : \"\";\n\t\tif (!path) {\n\t\t\tres.status(400).json({ error: \"path is required\" });\n\t\t\treturn;\n\t\t}\n\t\toptions\n\t\t\t.deleteSession(path)\n\t\t\t.then((result) => {\n\t\t\t\tlog(`session deleted: ${path}`);\n\t\t\t\tres.json(result ?? { ok: true });\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- settings ------------------------------------------------------------------\n\t// Settings are process-global persistent defaults. They route through hidden\n\t// utility runtimes instead of whichever user session happened to open first.\n\t// Agent-definition discovery is cwd-sensitive, so callers may pass an explicit\n\t// project cwd for endpoints that need project-local .dreb/agents.\n\tfunction withAnyRuntime(\n\t\tres: Response,\n\t\tfn: (h: NonNullable<ReturnType<RuntimePool[\"get\"]>>) => Promise<unknown>,\n\t\tcwd?: string,\n\t) {\n\t\tpool\n\t\t\t.ensureUtilityRuntime(cwd)\n\t\t\t.then((handle) => fn(handle))\n\t\t\t.then((data) => res.json(data ?? { ok: true }))\n\t\t\t.catch((err) => {\n\t\t\t\tres.status(502).json({ error: String(err?.message ?? err) });\n\t\t\t});\n\t}\n\n\tapp.get(\"/api/settings\", (_req, res) => {\n\t\twithAnyRuntime(res, (h) => h.client.getSettings());\n\t});\n\n\tapp.get(\"/api/settings/models\", (_req, res) => {\n\t\twithAnyRuntime(res, async (h) => ({ models: await h.client.getAvailableModels() }));\n\t});\n\n\tapp.get(\"/api/settings/agent-types\", (req, res) => {\n\t\tconst cwd = typeof req.query.cwd === \"string\" && req.query.cwd.trim() ? req.query.cwd : undefined;\n\t\tif (cwd && !existsSync(cwd)) {\n\t\t\tres.status(400).json({ error: `cwd does not exist: ${cwd}` });\n\t\t\treturn;\n\t\t}\n\t\twithAnyRuntime(res, async (h) => ({ agentTypes: await h.client.listAgentTypes() }), cwd);\n\t});\n\n\tapp.get(\"/api/daily-cost\", (_req, res) => {\n\t\twithAnyRuntime(res, async (h) => ({ cost: await h.client.getDailyCost() }));\n\t});\n\n\tapp.put(\"/api/settings\", (req, res) => {\n\t\twithAnyRuntime(res, (h) => h.client.setSettings(req.body ?? {}));\n\t});\n\n\tapp.get(\"/api/version\", (_req, res) => {\n\t\twithAnyRuntime(res, async (h) => ({ version: await h.client.getVersion() }));\n\t});\n\n\t// -- server lifecycle ----------------------------------------------------------\n\t// Build/version of the *server* process (distinct from a freshly-spawned RPC\n\t// child's version) so a stale long-running service is visible at a glance.\n\tapp.get(\"/api/server/info\", (_req, res) => {\n\t\tres.json({\n\t\t\tversion: options.serverVersion ?? null,\n\t\t\tstartedAt: serverStartedAt,\n\t\t\t// systemd sets INVOCATION_ID; other supervisors set LISTEN_PID. Best-effort.\n\t\t\tsupervised: Boolean(process.env.INVOCATION_ID || process.env.LISTEN_PID),\n\t\t\trestartable: Boolean(options.onRestart),\n\t\t});\n\t});\n\n\tapp.post(\"/api/server/restart\", (_req, res) => {\n\t\tif (!options.onRestart) {\n\t\t\tres.status(501).json({\n\t\t\t\terror: \"Restart is unavailable — the dashboard is not running under a supervisor that can respawn it\",\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tlog(\"restart requested via API\");\n\t\tres.json({ ok: true, restarting: true });\n\t\t// Defer so the HTTP response flushes before the process exits.\n\t\tsetTimeout(() => options.onRestart?.(), 100);\n\t});\n\n\t// -- files -----------------------------------------------------------------------\n\tapp.get(\"/api/files\", (req, res) => {\n\t\tconst path = typeof req.query.path === \"string\" ? req.query.path : homedir();\n\t\tfiles\n\t\t\t.list(path)\n\t\t\t.then((listing) => res.json(listing))\n\t\t\t.catch((err) => res.status(err?.status ?? 500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.get(\"/api/files/places\", (_req, res) => {\n\t\tconst roots = [...new Set(pool.list().map((h) => h.cwd))];\n\t\tres.json({ places: defaultPlaces(homedir(), roots) });\n\t});\n\n\tapp.get(\"/api/files/download\", (req, res) => {\n\t\tconst path = typeof req.query.path === \"string\" ? req.query.path : \"\";\n\t\tfiles\n\t\t\t.resolveDownload(path)\n\t\t\t.then(({ path: real }) => {\n\t\t\t\tres.download(real);\n\t\t\t})\n\t\t\t.catch((err) => res.status(err?.status ?? 500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.post(\"/api/files/upload\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst dir = typeof req.query.dir === \"string\" ? req.query.dir : \"\";\n\t\t\tconst name = typeof req.query.name === \"string\" ? req.query.name : \"\";\n\t\t\tconst overwrite = req.query.overwrite === \"true\";\n\t\t\tconst upload = await files.prepareUpload(dir, name, overwrite);\n\t\t\ttry {\n\t\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\t\tlet settled = false;\n\t\t\t\t\tconst fail = (err: unknown) => {\n\t\t\t\t\t\tif (settled) return;\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tupload.stream.destroy();\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t};\n\t\t\t\t\treq.pipe(upload.stream);\n\t\t\t\t\tupload.stream.on(\"finish\", () => {\n\t\t\t\t\t\tif (settled) return;\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t});\n\t\t\t\t\tupload.stream.on(\"error\", fail);\n\t\t\t\t\treq.on(\"error\", fail);\n\t\t\t\t\treq.on(\"aborted\", () => fail(Object.assign(new Error(\"Upload aborted\"), { status: 499 })));\n\t\t\t\t});\n\t\t\t\tawait upload.commit();\n\t\t\t\tres.json({ path: upload.path });\n\t\t\t} catch (err) {\n\t\t\t\tawait upload.cleanup();\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t})().catch((err) => {\n\t\t\tif (!res.headersSent) res.status(err?.status ?? 500).json({ error: String(err?.message ?? err) });\n\t\t});\n\t});\n\n\tapp.post(\"/api/files/mkdir\", (req, res) => {\n\t\tconst { dir, name } = req.body ?? {};\n\t\tif (typeof dir !== \"string\" || typeof name !== \"string\") {\n\t\t\tres.status(400).json({ error: \"dir and name are required\" });\n\t\t\treturn;\n\t\t}\n\t\tfiles\n\t\t\t.mkdir(dir, name)\n\t\t\t.then((path) => res.json({ path }))\n\t\t\t.catch((err) => res.status(err?.status ?? 500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- static client -----------------------------------------------------------------\n\tif (options.staticDir) {\n\t\tapp.use(express.static(options.staticDir));\n\t\t// SPA fallback: serve index.html for non-API GETs (client-side routing).\n\t\tapp.get(/^\\/(?!api\\/).*/, (_req, res) => {\n\t\t\tres.sendFile(join(options.staticDir!, \"index.html\"));\n\t\t});\n\t}\n\n\treturn app;\n}\n"]}
|