@byfriends/vis-server 0.3.5 → 0.4.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.
@@ -1,1044 +0,0 @@
1
- import { serve } from "@hono/node-server";
2
- import { timingSafeEqual } from "node:crypto";
3
- import { readFile, readdir, rm, stat } from "node:fs/promises";
4
- import { join, resolve, sep } from "node:path";
5
- import { Hono } from "hono";
6
- import { homedir } from "node:os";
7
- import { createReadStream } from "node:fs";
8
- import { createInterface } from "node:readline";
9
- import { spawn } from "node:child_process";
10
- //#region src/config.ts
11
- /** Resolve BYF_HOME (env > ~/.byf). */
12
- function resolveByfHome() {
13
- const envHome = process.env["BYF_HOME"];
14
- if (envHome !== void 0 && envHome.length > 0) return envHome;
15
- return join(homedir(), ".byf");
16
- }
17
- /** HTTP port for the vis API server. */
18
- function resolvePort() {
19
- const raw = process.env["PORT"];
20
- if (raw !== void 0 && raw.length > 0) {
21
- const n = Number.parseInt(raw, 10);
22
- if (Number.isFinite(n) && n > 0 && n < 65536) return n;
23
- }
24
- return 3001;
25
- }
26
- /** HTTP host for the vis API server. Defaults to loopback. */
27
- function resolveHost() {
28
- const host = (process.env["VIS_HOST"] ?? process.env["HOST"])?.trim();
29
- return host !== void 0 && host.length > 0 ? host : "127.0.0.1";
30
- }
31
- function isLoopbackHost(host) {
32
- const normalized = host.trim().toLowerCase().replaceAll("[", "").replaceAll("]", "");
33
- return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || normalized.startsWith("127.");
34
- }
35
- function resolveVisAuthToken(host = resolveHost()) {
36
- const token = process.env["VIS_AUTH_TOKEN"]?.trim();
37
- if (token !== void 0 && token.length > 0) return token;
38
- if (!isLoopbackHost(host)) throw new Error(`VIS_AUTH_TOKEN is required when binding vis-server outside loopback (host=${host})`);
39
- }
40
- //#endregion
41
- //#region src/lib/context-projector.ts
42
- const ZERO = {
43
- inputOther: 0,
44
- output: 0,
45
- inputCacheRead: 0,
46
- inputCacheCreation: 0
47
- };
48
- /** Build a conversation timeline + derived state from a sequence of
49
- * wire entries. The reconstruction mirrors agent-core's own
50
- * `appendLoopEvent` logic, so:
51
- *
52
- * - `context.append_message` records become messages as-is (the
53
- * user / tool messages and any explicit assistant injections).
54
- * - `step.begin` pushes a fresh assistant message; later
55
- * `content.part` and `tool.call` events on the same step **mutate
56
- * that same message** to grow its content / toolCalls. `step.end`
57
- * just closes the step.
58
- * - `tool.result` events emit an independent `role: 'tool'` message,
59
- * matching how agent-core surfaces tool exchanges to the model.
60
- *
61
- * Without this loop-event reconstruction the timeline would only
62
- * show user prompts — agent-core does not emit a synthetic
63
- * `context.append_message` for assistant turns. */
64
- function projectContext(entries) {
65
- let messages = [];
66
- const usage = {
67
- byScope: {
68
- session: { ...ZERO },
69
- turn: { ...ZERO }
70
- },
71
- byModel: {}
72
- };
73
- const config = {};
74
- let permissionMode = null;
75
- let openSteps = /* @__PURE__ */ new Map();
76
- for (const entry of entries) {
77
- const rec = entry.data;
78
- switch (rec.type) {
79
- case "context.append_message":
80
- messages.push({
81
- lineNo: entry.lineNo,
82
- time: rec.time,
83
- source: "append_message",
84
- message: rec.message,
85
- toolStepUuids: []
86
- });
87
- break;
88
- case "context.append_loop_event": {
89
- const ev = rec.event;
90
- if (ev.type === "step.begin") {
91
- const projected = {
92
- lineNo: entry.lineNo,
93
- time: rec.time,
94
- source: "append_message",
95
- message: {
96
- role: "assistant",
97
- content: [],
98
- toolCalls: []
99
- },
100
- toolStepUuids: [ev.uuid]
101
- };
102
- messages.push(projected);
103
- openSteps.set(ev.uuid, projected);
104
- } else if (ev.type === "content.part") {
105
- const projected = openSteps.get(ev.stepUuid);
106
- if (projected !== void 0) projected.message.content.push(ev.part);
107
- } else if (ev.type === "tool.call") {
108
- const projected = openSteps.get(ev.stepUuid);
109
- if (projected !== void 0) {
110
- const args = typeof ev.args === "string" ? ev.args : ev.args === void 0 ? null : JSON.stringify(ev.args);
111
- projected.message.toolCalls.push({
112
- type: "function",
113
- id: ev.toolCallId,
114
- name: ev.name,
115
- arguments: args
116
- });
117
- }
118
- } else if (ev.type === "step.end") openSteps.delete(ev.uuid);
119
- else if (ev.type === "tool.result") {
120
- const output = ev.result.output;
121
- const toolMsg = {
122
- role: "tool",
123
- content: typeof output === "string" ? [{
124
- type: "text",
125
- text: output
126
- }] : output,
127
- toolCalls: [],
128
- toolCallId: ev.toolCallId,
129
- ...ev.result.isError === true ? { isError: true } : {}
130
- };
131
- messages.push({
132
- lineNo: entry.lineNo,
133
- time: rec.time,
134
- source: "append_message",
135
- message: toolMsg,
136
- toolStepUuids: []
137
- });
138
- }
139
- break;
140
- }
141
- case "context.clear":
142
- messages = [];
143
- openSteps = /* @__PURE__ */ new Map();
144
- break;
145
- case "context.apply_compaction":
146
- openSteps = /* @__PURE__ */ new Map();
147
- messages = [{
148
- lineNo: entry.lineNo,
149
- time: rec.time,
150
- source: "compaction_summary",
151
- message: {
152
- role: "assistant",
153
- content: [{
154
- type: "text",
155
- text: rec.summary
156
- }],
157
- toolCalls: [],
158
- origin: { kind: "compaction_summary" }
159
- },
160
- toolStepUuids: []
161
- }];
162
- break;
163
- case "usage.record": {
164
- const scope = rec.usageScope ?? "session";
165
- addUsage(usage.byScope[scope], rec.usage);
166
- usage.byModel[rec.model] ??= { ...ZERO };
167
- addUsage(usage.byModel[rec.model], rec.usage);
168
- break;
169
- }
170
- case "config.update": {
171
- const typeChecked = rec;
172
- if (typeChecked.cwd !== void 0) config.cwd = typeChecked.cwd;
173
- if (typeChecked.modelAlias !== void 0) config.modelAlias = typeChecked.modelAlias;
174
- if (typeChecked.profileName !== void 0) config.profileName = typeChecked.profileName;
175
- if (typeChecked.thinkingLevel !== void 0) config.thinkingLevel = typeChecked.thinkingLevel;
176
- if (typeChecked.systemPrompt !== void 0) config.systemPrompt = typeChecked.systemPrompt;
177
- break;
178
- }
179
- case "permission.set_mode":
180
- permissionMode = rec.mode;
181
- break;
182
- default: break;
183
- }
184
- }
185
- return {
186
- messages,
187
- usage,
188
- config,
189
- permission: { mode: permissionMode }
190
- };
191
- }
192
- function addUsage(into, src) {
193
- into.inputOther += src.inputOther;
194
- into.output += src.output;
195
- into.inputCacheRead += src.inputCacheRead;
196
- into.inputCacheCreation += src.inputCacheCreation;
197
- }
198
- //#endregion
199
- //#region src/lib/session-store.ts
200
- const SESSION_ID_RE = /^session_[A-Za-z0-9._-]+$/;
201
- const AGENT_ID_RE = /^[A-Za-z0-9._-]+$/;
202
- /** Reject agent ids that could escape the session directory via path
203
- * joins. Defence-in-depth: the on-disk source of these ids is
204
- * agent-core (which only generates main / agent-N), but a corrupted
205
- * or hand-edited `state.json.agents` key could otherwise turn vis
206
- * into a local-file-read primitive when exposed beyond loopback. */
207
- function isSafeAgentId(id) {
208
- return AGENT_ID_RE.test(id) && id !== "." && id !== "..";
209
- }
210
- async function listSessions(home) {
211
- const sessionsDir = join(home, "sessions");
212
- const buckets = await readdir(sessionsDir, { withFileTypes: true }).catch(() => []);
213
- const index = await readSessionIndex(home);
214
- const out = [];
215
- for (const bucket of buckets) {
216
- if (!bucket.isDirectory()) continue;
217
- const bucketDir = join(sessionsDir, bucket.name);
218
- const sessionDirs = await readdir(bucketDir, { withFileTypes: true }).catch(() => []);
219
- for (const entry of sessionDirs) {
220
- if (!entry.isDirectory() || !SESSION_ID_RE.test(entry.name)) continue;
221
- const sessionDir = join(bucketDir, entry.name);
222
- const workDir = index.get(entry.name)?.workDir ?? "";
223
- const summary = await tryReadSummary(sessionDir, entry.name, workDir);
224
- if (summary !== null) out.push(summary);
225
- }
226
- }
227
- out.sort((a, b) => b.updatedAt - a.updatedAt);
228
- return out;
229
- }
230
- async function readSessionDetail(home, sessionId) {
231
- const sessionDir = await findSessionDir(home, sessionId);
232
- if (sessionDir === null) return null;
233
- const workDir = (await readSessionIndex(home)).get(sessionId)?.workDir ?? "";
234
- const state = await readState(sessionDir);
235
- if (state === null) return {
236
- sessionId,
237
- sessionDir,
238
- workDir,
239
- state: null,
240
- agents: await discoverAgentsFromDisk(sessionDir)
241
- };
242
- if (state.custom?.["imported_from_byf_cli"] === true) return null;
243
- return {
244
- sessionId,
245
- sessionDir,
246
- workDir,
247
- state,
248
- agents: await inventoryAgents(sessionDir, state)
249
- };
250
- }
251
- /** Fallback inventory used when `state.json` is unreadable: walk
252
- * `<sessionDir>/agents/*` directly and synthesize minimal AgentInfo
253
- * records for the directories that contain a `wire.jsonl`. Parent
254
- * links and `type` are unknown without state, so we mark every agent
255
- * as `independent` with a null parent — the routes only need
256
- * `agentId` + `wireExists` to serve wire/context. */
257
- async function discoverAgentsFromDisk(sessionDir) {
258
- const agentsDir = join(sessionDir, "agents");
259
- let entries;
260
- try {
261
- entries = await readdir(agentsDir, { withFileTypes: true });
262
- } catch {
263
- return [];
264
- }
265
- const out = [];
266
- for (const entry of entries) {
267
- if (!entry.isDirectory()) continue;
268
- const id = entry.name;
269
- if (!isSafeAgentId(id)) continue;
270
- const wirePath = join(agentsDir, id, "wire.jsonl");
271
- const exists = await pathExists(wirePath);
272
- let readable = exists;
273
- let info = {
274
- count: 0,
275
- protocolVersion: null
276
- };
277
- if (exists) try {
278
- info = await scanWire(wirePath);
279
- } catch {
280
- readable = false;
281
- }
282
- out.push({
283
- agentId: id,
284
- type: id === "main" ? "main" : "independent",
285
- parentAgentId: null,
286
- homedir: join(agentsDir, id),
287
- wireExists: readable,
288
- wireRecordCount: info.count,
289
- wireProtocolVersion: info.protocolVersion
290
- });
291
- }
292
- return out.toSorted((a, b) => {
293
- if (a.agentId === "main") return -1;
294
- if (b.agentId === "main") return 1;
295
- return a.agentId.localeCompare(b.agentId);
296
- });
297
- }
298
- async function tryReadSummary(sessionDir, sessionId, workDir) {
299
- const state = await readState(sessionDir);
300
- if (state === null) return brokenStateSummary(sessionDir, sessionId, workDir);
301
- if (state.custom?.["imported_from_byf_cli"] === true) return null;
302
- const mainWirePath = join(sessionDir, "agents", "main", "wire.jsonl");
303
- const mainExists = await pathExists(mainWirePath);
304
- let mainCount = 0;
305
- let protocolVersion = null;
306
- let health = "ok";
307
- if (!mainExists) health = "missing_main_wire";
308
- else try {
309
- const info = await scanWire(mainWirePath);
310
- mainCount = info.count;
311
- protocolVersion = info.protocolVersion;
312
- } catch {
313
- health = "broken_main_wire";
314
- }
315
- return {
316
- sessionId,
317
- sessionDir,
318
- workDir,
319
- title: state.title ?? null,
320
- lastPrompt: state.lastPrompt ?? null,
321
- isCustomTitle: state.isCustomTitle ?? false,
322
- createdAt: parseTs(state.createdAt),
323
- updatedAt: parseTs(state.updatedAt),
324
- agentCount: Object.keys(state.agents ?? {}).length,
325
- mainAgentExists: mainExists,
326
- mainWireRecordCount: mainCount,
327
- wireProtocolVersion: protocolVersion,
328
- health
329
- };
330
- }
331
- function brokenStateSummary(sessionDir, sessionId, workDir) {
332
- return {
333
- sessionId,
334
- sessionDir,
335
- workDir,
336
- title: null,
337
- lastPrompt: null,
338
- isCustomTitle: false,
339
- createdAt: 0,
340
- updatedAt: 0,
341
- agentCount: 0,
342
- mainAgentExists: false,
343
- mainWireRecordCount: 0,
344
- wireProtocolVersion: null,
345
- health: "broken_state"
346
- };
347
- }
348
- async function readSessionIndex(home) {
349
- const out = /* @__PURE__ */ new Map();
350
- let raw;
351
- try {
352
- raw = await readFile(join(home, "session_index.jsonl"), "utf8");
353
- } catch {
354
- return out;
355
- }
356
- for (const line of raw.split(/\r?\n/)) {
357
- if (!line.trim()) continue;
358
- try {
359
- const entry = JSON.parse(line);
360
- if (typeof entry.sessionId === "string" && typeof entry.sessionDir === "string") out.set(entry.sessionId, {
361
- sessionDir: entry.sessionDir,
362
- workDir: typeof entry.workDir === "string" ? entry.workDir : ""
363
- });
364
- } catch {}
365
- }
366
- return out;
367
- }
368
- async function inventoryAgents(sessionDir, state) {
369
- const result = [];
370
- for (const [id, meta] of Object.entries(state.agents ?? {})) {
371
- if (!isSafeAgentId(id)) continue;
372
- const wirePath = join(sessionDir, "agents", id, "wire.jsonl");
373
- const exists = await pathExists(wirePath);
374
- let readable = exists;
375
- let info = {
376
- count: 0,
377
- protocolVersion: null
378
- };
379
- if (exists) try {
380
- info = await scanWire(wirePath);
381
- } catch {
382
- readable = false;
383
- }
384
- result.push({
385
- agentId: id,
386
- type: meta.type,
387
- parentAgentId: meta.parentAgentId,
388
- homedir: meta.homedir,
389
- wireExists: readable,
390
- wireRecordCount: info.count,
391
- wireProtocolVersion: info.protocolVersion
392
- });
393
- }
394
- return result.toSorted((a, b) => {
395
- if (a.agentId === "main") return -1;
396
- if (b.agentId === "main") return 1;
397
- return a.agentId.localeCompare(b.agentId);
398
- });
399
- }
400
- async function readState(sessionDir) {
401
- try {
402
- return JSON.parse(await readFile(join(sessionDir, "state.json"), "utf8"));
403
- } catch {
404
- return null;
405
- }
406
- }
407
- async function findSessionDir(home, sessionId) {
408
- if (!SESSION_ID_RE.test(sessionId)) return null;
409
- const sessionsRoot = resolve(join(home, "sessions"));
410
- const sessionsRootPrefix = sessionsRoot + sep;
411
- try {
412
- const indexLines = (await readFile(join(home, "session_index.jsonl"), "utf8")).split(/\r?\n/);
413
- for (const line of indexLines) {
414
- if (!line.trim()) continue;
415
- const entry = JSON.parse(line);
416
- if (entry.sessionId !== sessionId || typeof entry.sessionDir !== "string") continue;
417
- const candidate = resolve(entry.sessionDir);
418
- if (!candidate.startsWith(sessionsRootPrefix)) continue;
419
- if (candidate.split(sep).pop() !== sessionId) continue;
420
- if (await pathExists(candidate)) return candidate;
421
- }
422
- } catch {}
423
- const buckets = await readdir(sessionsRoot, { withFileTypes: true }).catch(() => []);
424
- for (const bucket of buckets) {
425
- if (!bucket.isDirectory()) continue;
426
- const candidate = join(sessionsRoot, bucket.name, sessionId);
427
- if (await pathExists(candidate)) return candidate;
428
- }
429
- return null;
430
- }
431
- async function scanWire(path) {
432
- const rl = createInterface({
433
- input: createReadStream(path, { encoding: "utf8" }),
434
- crlfDelay: Infinity
435
- });
436
- let count = 0;
437
- let protocolVersion = null;
438
- for await (const line of rl) {
439
- if (line.length === 0) continue;
440
- if (protocolVersion === null) {
441
- let parsed;
442
- try {
443
- parsed = JSON.parse(line);
444
- } catch {
445
- throw new Error(`wire metadata is not valid JSON at line 1`);
446
- }
447
- if (parsed.type !== "metadata" || typeof parsed.protocol_version !== "string") throw new Error(`wire is missing a metadata header on line 1`);
448
- protocolVersion = parsed.protocol_version;
449
- }
450
- count += 1;
451
- }
452
- if (protocolVersion === null) throw new Error("wire file is empty");
453
- return {
454
- count,
455
- protocolVersion
456
- };
457
- }
458
- function parseTs(input) {
459
- if (!input) return 0;
460
- const n = Date.parse(input);
461
- return Number.isFinite(n) ? n : 0;
462
- }
463
- async function pathExists(p) {
464
- try {
465
- await stat(p);
466
- return true;
467
- } catch {
468
- return false;
469
- }
470
- }
471
- //#endregion
472
- //#region ../../../packages/agent-core/src/agent/records/migration/v1.1.ts
473
- function isLegacyToolCall(v) {
474
- if (!isRecord(v)) return false;
475
- return v["type"] === "function" && typeof v["id"] === "string" && isRecord(v["function"]);
476
- }
477
- function migrateToolCall(v) {
478
- const { function: fn, ...rest } = v;
479
- return {
480
- ...rest,
481
- name: fn.name,
482
- arguments: fn.arguments
483
- };
484
- }
485
- function isRecord(value) {
486
- return typeof value === "object" && value !== null && !Array.isArray(value);
487
- }
488
- const MIGRATIONS = [{
489
- sourceVersion: "1.0",
490
- targetVersion: "1.1",
491
- migrateRecord(record) {
492
- if (record.type !== "context.append_message") return record;
493
- const message = record["message"];
494
- let changed = false;
495
- const toolCalls = message.toolCalls.map((toolCall) => {
496
- if (!isLegacyToolCall(toolCall)) return toolCall;
497
- changed = true;
498
- return migrateToolCall(toolCall);
499
- });
500
- if (!changed) return record;
501
- return {
502
- ...record,
503
- message: {
504
- ...message,
505
- toolCalls
506
- }
507
- };
508
- }
509
- }];
510
- function resolveWireMigrations(readVersion) {
511
- if (compareWireVersions(readVersion, "1.1") >= 0) return [];
512
- const migrations = [];
513
- let version = readVersion;
514
- while (compareWireVersions(version, "1.1") < 0) {
515
- const migration = findMigration(version);
516
- if (migration === void 0) throw new Error(`Missing wire migration for version ${version}`);
517
- migrations.push(migration);
518
- version = migration.targetVersion;
519
- }
520
- return migrations;
521
- }
522
- function migrateWireRecord(record, migrations) {
523
- return migrations.reduce((current, migration) => migration.migrateRecord(current), record);
524
- }
525
- function findMigration(sourceVersion) {
526
- for (const migration of MIGRATIONS) if (migration.sourceVersion === sourceVersion) return migration;
527
- }
528
- function compareWireVersions(a, b) {
529
- const partsA = a.split(".");
530
- const partsB = b.split(".");
531
- const maxLength = Math.max(partsA.length, partsB.length);
532
- for (let i = 0; i < maxLength; i++) {
533
- const diff = Number(partsA[i] ?? "0") - Number(partsB[i] ?? "0");
534
- if (diff !== 0) return diff;
535
- }
536
- return 0;
537
- }
538
- //#endregion
539
- //#region src/lib/wire-reader.ts
540
- /** Best-effort fallback when a wire file declares a protocol_version that
541
- * `agent-core` does not know about (e.g. the historic "2.2" alias that
542
- * pre-dates the 1.x renumber). We try to apply the chain *starting* from
543
- * the oldest known version (1.0) and warn the caller. If even that fails
544
- * we just pass records through unchanged. */
545
- function bestEffortMigrations() {
546
- try {
547
- return resolveWireMigrations("1.0");
548
- } catch {
549
- return [];
550
- }
551
- }
552
- /** Read a single agent's `wire.jsonl`.
553
- *
554
- * Each record is returned as a `WireEntry` containing both the
555
- * on-disk parsed form (`raw`) and the migrated current-protocol form
556
- * (`data`). For wires that declare a protocol version `agent-core`
557
- * does not recognise (historic 2.x labels, or truly future versions),
558
- * the reader falls back to a best-effort path: records are run
559
- * through the 1.0-onwards migration chain and a warning is added to
560
- * `warnings[]` so the UI can surface the caveat. */
561
- async function readAgentWire(path) {
562
- const rl = createInterface({
563
- input: createReadStream(path, { encoding: "utf8" }),
564
- crlfDelay: Infinity
565
- });
566
- let lineNo = 0;
567
- let metadata = null;
568
- let migrations = [];
569
- const records = [];
570
- const warnings = [];
571
- for await (const line of rl) {
572
- lineNo += 1;
573
- if (line.length === 0) continue;
574
- let parsed;
575
- try {
576
- parsed = JSON.parse(line);
577
- } catch (error) {
578
- warnings.push(`line ${lineNo}: invalid JSON (${error.message})`);
579
- continue;
580
- }
581
- if (!isObject(parsed) || typeof parsed.type !== "string") {
582
- warnings.push(`line ${lineNo}: missing 'type' field`);
583
- continue;
584
- }
585
- if (metadata === null) {
586
- if (parsed.type !== "metadata") throw new Error(`Wire file missing metadata header at line ${lineNo}`);
587
- const pv = parsed["protocol_version"];
588
- const ca = parsed["created_at"];
589
- if (typeof pv !== "string" || typeof ca !== "number") throw new TypeError(`Wire metadata malformed at line ${lineNo}`);
590
- try {
591
- migrations = resolveWireMigrations(pv);
592
- } catch (error) {
593
- warnings.push(`unrecognised protocol_version "${pv}" — parsing as best-effort (${error.message})`);
594
- migrations = bestEffortMigrations();
595
- }
596
- metadata = {
597
- protocolVersion: pv,
598
- createdAt: ca
599
- };
600
- continue;
601
- }
602
- const raw = parsed;
603
- let migrated;
604
- try {
605
- migrated = migrations.length === 0 ? raw : migrateWireRecord(raw, migrations);
606
- } catch (error) {
607
- warnings.push(`line ${lineNo}: migration failed (${error.message}); using raw record`);
608
- migrated = raw;
609
- }
610
- records.push({
611
- lineNo,
612
- data: migrated,
613
- raw
614
- });
615
- }
616
- if (metadata === null) throw new Error("Wire file is empty (no metadata)");
617
- return {
618
- metadata,
619
- records,
620
- warnings
621
- };
622
- }
623
- function isObject(v) {
624
- return typeof v === "object" && v !== null && !Array.isArray(v);
625
- }
626
- //#endregion
627
- //#region src/routes/context.ts
628
- function contextRoute() {
629
- const r = new Hono();
630
- r.get("/:id/context", async (c) => {
631
- const id = c.req.param("id");
632
- const agentId = c.req.query("agent") ?? "main";
633
- if (!isSafeAgentId(agentId)) return c.json({
634
- error: "invalid agent id",
635
- code: "BAD_REQUEST"
636
- }, 400);
637
- const detail = await readSessionDetail(resolveByfHome(), id);
638
- if (!detail) return c.json({
639
- error: "session not found",
640
- code: "NOT_FOUND"
641
- }, 404);
642
- const agent = detail.agents.find((a) => a.agentId === agentId);
643
- if (!agent || !agent.wireExists) return c.json({
644
- error: "agent wire not found",
645
- code: "NOT_FOUND"
646
- }, 404);
647
- try {
648
- const proj = projectContext((await readAgentWire(join(detail.sessionDir, "agents", agentId, "wire.jsonl"))).records);
649
- return c.json({
650
- sessionId: id,
651
- agentId,
652
- messages: proj.messages,
653
- usage: proj.usage,
654
- config: proj.config,
655
- permission: proj.permission
656
- });
657
- } catch (error) {
658
- const msg = error.message;
659
- if (msg.toLowerCase().includes("unsupported protocol")) return c.json({
660
- error: msg,
661
- code: "UNSUPPORTED_PROTOCOL"
662
- }, 400);
663
- return c.json({
664
- error: msg,
665
- code: "READ_ERROR"
666
- }, 500);
667
- }
668
- });
669
- return r;
670
- }
671
- //#endregion
672
- //#region src/routes/session-detail.ts
673
- function sessionDetailRoute() {
674
- const r = new Hono();
675
- r.get("/:id", async (c) => {
676
- const id = c.req.param("id");
677
- const detail = await readSessionDetail(resolveByfHome(), id);
678
- if (!detail) return c.json({
679
- error: "session not found",
680
- code: "NOT_FOUND"
681
- }, 404);
682
- return c.json(detail);
683
- });
684
- return r;
685
- }
686
- //#endregion
687
- //#region src/lib/reveal.ts
688
- /** Resolve the platform-specific "reveal in file manager" command for
689
- * the given absolute path. Kept pure (no IO) so it can be unit-tested. */
690
- function revealCommandFor(path, platform = process.platform) {
691
- switch (platform) {
692
- case "darwin": return {
693
- command: "open",
694
- args: [path]
695
- };
696
- case "win32": return {
697
- command: "cmd",
698
- args: [
699
- "/c",
700
- "start",
701
- "\"\"",
702
- path
703
- ]
704
- };
705
- default: return {
706
- command: "xdg-open",
707
- args: [path]
708
- };
709
- }
710
- }
711
- /** Spawn the OS file manager to reveal `path`. Resolves once the launcher
712
- * process has started; rejects only if the launcher itself fails to
713
- * spawn (missing binary etc.) — not if it later fails to find the path,
714
- * since the launcher exits asynchronously after we've detached. */
715
- async function revealInOs(path) {
716
- const { command, args } = revealCommandFor(path);
717
- return new Promise((resolve, reject) => {
718
- let settled = false;
719
- const child = spawn(command, args, {
720
- detached: true,
721
- stdio: "ignore"
722
- });
723
- child.once("error", (err) => {
724
- if (settled) return;
725
- settled = true;
726
- reject(err);
727
- });
728
- child.once("spawn", () => {
729
- if (settled) return;
730
- settled = true;
731
- child.unref();
732
- resolve();
733
- });
734
- });
735
- }
736
- //#endregion
737
- //#region src/routes/sessions.ts
738
- function sessionsRoute() {
739
- const r = new Hono();
740
- r.get("/", async (c) => {
741
- const sessions = await listSessions(resolveByfHome());
742
- return c.json({ sessions });
743
- });
744
- r.delete("/:id", async (c) => {
745
- const id = c.req.param("id");
746
- const target = (await listSessions(resolveByfHome())).find((s) => s.sessionId === id);
747
- if (!target) return c.json({
748
- error: "session not found",
749
- code: "NOT_FOUND"
750
- }, 404);
751
- await rm(target.sessionDir, {
752
- recursive: true,
753
- force: true
754
- });
755
- return c.json({
756
- sessionId: id,
757
- deleted: true
758
- });
759
- });
760
- r.post("/:id/reveal", async (c) => {
761
- const id = c.req.param("id");
762
- const detail = await readSessionDetail(resolveByfHome(), id);
763
- if (!detail) return c.json({
764
- error: "session not found",
765
- code: "NOT_FOUND"
766
- }, 404);
767
- try {
768
- await revealInOs(detail.sessionDir);
769
- return c.json({
770
- sessionId: id,
771
- opened: detail.sessionDir
772
- });
773
- } catch (error) {
774
- return c.json({
775
- error: `failed to open: ${error.message}`,
776
- code: "READ_ERROR"
777
- }, 500);
778
- }
779
- });
780
- return r;
781
- }
782
- //#endregion
783
- //#region src/lib/agent-tree.ts
784
- /**
785
- * Build a parent/child tree from the flat agent inventory found on
786
- * `state.json.agents`. Roots are agents with no `parentAgentId`, plus any
787
- * agent whose `parentAgentId` does not resolve in the inventory (orphans).
788
- * The returned roots are sorted so that the `main` agent always appears
789
- * first; remaining roots fall back to a stable lexicographic order.
790
- */
791
- function buildAgentTree(agents) {
792
- const byId = /* @__PURE__ */ new Map();
793
- for (const a of agents) byId.set(a.agentId, {
794
- ...a,
795
- children: []
796
- });
797
- const roots = [];
798
- for (const node of byId.values()) if (node.parentAgentId !== null && byId.has(node.parentAgentId)) byId.get(node.parentAgentId).children.push(node);
799
- else roots.push(node);
800
- return roots.toSorted(sortAgents);
801
- }
802
- function sortAgents(a, b) {
803
- if (a.agentId === "main") return -1;
804
- if (b.agentId === "main") return 1;
805
- return a.agentId.localeCompare(b.agentId);
806
- }
807
- //#endregion
808
- //#region src/routes/subagents.ts
809
- function subagentsRoute() {
810
- const r = new Hono();
811
- r.get("/:id/agents", async (c) => {
812
- const id = c.req.param("id");
813
- const detail = await readSessionDetail(resolveByfHome(), id);
814
- if (!detail) return c.json({
815
- error: "session not found",
816
- code: "NOT_FOUND"
817
- }, 404);
818
- return c.json({
819
- sessionId: id,
820
- tree: buildAgentTree(detail.agents)
821
- });
822
- });
823
- return r;
824
- }
825
- //#endregion
826
- //#region src/routes/wire.ts
827
- function wireRoute() {
828
- const r = new Hono();
829
- r.get("/:id/wire", async (c) => {
830
- const id = c.req.param("id");
831
- const agentId = c.req.query("agent") ?? "main";
832
- if (!isSafeAgentId(agentId)) return c.json({
833
- error: "invalid agent id",
834
- code: "BAD_REQUEST"
835
- }, 400);
836
- const detail = await readSessionDetail(resolveByfHome(), id);
837
- if (!detail) return c.json({
838
- error: "session not found",
839
- code: "NOT_FOUND"
840
- }, 404);
841
- const agent = detail.agents.find((a) => a.agentId === agentId);
842
- if (!agent) return c.json({
843
- error: `agent "${agentId}" not found`,
844
- code: "NOT_FOUND"
845
- }, 404);
846
- if (!agent.wireExists) return c.json({
847
- error: "wire missing",
848
- code: "NOT_FOUND"
849
- }, 404);
850
- try {
851
- const result = await readAgentWire(join(detail.sessionDir, "agents", agentId, "wire.jsonl"));
852
- return c.json({
853
- sessionId: id,
854
- agentId,
855
- protocolVersion: result.metadata.protocolVersion,
856
- metadata: result.metadata,
857
- records: result.records,
858
- warnings: result.warnings
859
- });
860
- } catch (error) {
861
- const msg = error.message;
862
- if (msg.toLowerCase().includes("unsupported protocol")) return c.json({
863
- error: msg,
864
- code: "UNSUPPORTED_PROTOCOL"
865
- }, 400);
866
- return c.json({
867
- error: msg,
868
- code: "READ_ERROR"
869
- }, 500);
870
- }
871
- });
872
- return r;
873
- }
874
- //#endregion
875
- //#region src/app.ts
876
- /** Resolve the SPA bundle directory next to the compiled server.mjs, if it
877
- * exists. Returns `null` in dev mode where the web bundle lives elsewhere. */
878
- async function resolvePublicDir() {
879
- try {
880
- const here = import.meta.dirname;
881
- const candidate = resolve(here, "public");
882
- if ((await stat(candidate)).isDirectory()) return candidate;
883
- } catch {}
884
- return null;
885
- }
886
- const STATIC_EXT_MIME = {
887
- ".html": "text/html; charset=utf-8",
888
- ".js": "application/javascript; charset=utf-8",
889
- ".mjs": "application/javascript; charset=utf-8",
890
- ".css": "text/css; charset=utf-8",
891
- ".json": "application/json; charset=utf-8",
892
- ".svg": "image/svg+xml",
893
- ".png": "image/png",
894
- ".jpg": "image/jpeg",
895
- ".jpeg": "image/jpeg",
896
- ".gif": "image/gif",
897
- ".ico": "image/x-icon",
898
- ".woff": "font/woff",
899
- ".woff2": "font/woff2",
900
- ".ttf": "font/ttf",
901
- ".map": "application/json; charset=utf-8"
902
- };
903
- function mimeFor(path) {
904
- const i = path.lastIndexOf(".");
905
- if (i < 0) return "application/octet-stream";
906
- return STATIC_EXT_MIME[path.slice(i).toLowerCase()] ?? "application/octet-stream";
907
- }
908
- function bearerToken(value) {
909
- if (value === void 0) return null;
910
- return /^Bearer\s+(.+)$/i.exec(value)?.[1]?.trim() ?? null;
911
- }
912
- function tokenMatches(actual, expected) {
913
- const actualBytes = Buffer.from(actual);
914
- const expectedBytes = Buffer.from(expected);
915
- return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
916
- }
917
- /** Build a Hono app mounting /api/* routes, plus SPA static fallback. */
918
- async function createApp(options = {}) {
919
- const app = new Hono();
920
- const api = new Hono();
921
- const authToken = options.authToken;
922
- if (authToken !== void 0 && authToken.length > 0) api.use("*", async (c, next) => {
923
- const token = bearerToken(c.req.header("authorization"));
924
- if (token !== null && tokenMatches(token, authToken)) {
925
- await next();
926
- return;
927
- }
928
- c.header("www-authenticate", "Bearer realm=\"byf-vis\"");
929
- return c.json({
930
- error: "unauthorized",
931
- code: "UNAUTHORIZED"
932
- }, 401);
933
- });
934
- api.route("/sessions", sessionsRoute());
935
- api.route("/sessions", sessionDetailRoute());
936
- api.route("/sessions", wireRoute());
937
- api.route("/sessions", subagentsRoute());
938
- api.route("/sessions", contextRoute());
939
- app.route("/api", api);
940
- const publicDir = options.publicDir ?? await resolvePublicDir();
941
- if (publicDir !== null) app.get("*", async (c) => {
942
- const url = new URL(c.req.url);
943
- let pathname = decodeURIComponent(url.pathname);
944
- if (pathname.startsWith("/api")) return c.json({
945
- error: `api route not found: ${pathname}`,
946
- code: "NOT_FOUND"
947
- }, 404);
948
- if (pathname === "/" || pathname === "") pathname = "/index.html";
949
- const resolved = resolve(publicDir, `.${pathname}`);
950
- if (!resolved.startsWith(publicDir)) return c.text("forbidden", 403);
951
- try {
952
- if ((await stat(resolved)).isFile()) {
953
- const buf = await readFile(resolved);
954
- const body = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
955
- return new Response(body, { headers: { "content-type": mimeFor(resolved) } });
956
- }
957
- } catch {}
958
- try {
959
- const indexHtml = await readFile(join(publicDir, "index.html"));
960
- const body = new Uint8Array(indexHtml.buffer, indexHtml.byteOffset, indexHtml.byteLength);
961
- return new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
962
- } catch {
963
- return c.text("not found", 404);
964
- }
965
- });
966
- return app;
967
- }
968
- //#endregion
969
- //#region src/startup-banner.ts
970
- function formatStartupBanner(options) {
971
- const authStatus = options.authToken === void 0 ? "auth=disabled" : "auth=required";
972
- return `[vis-server] listening on http://${hostForUrl$1(options.host)}:${String(options.port)} (${authStatus}, BYF_HOME=${options.byfCodeHome})\n`;
973
- }
974
- function hostForUrl$1(host) {
975
- if (host.includes(":") && !host.startsWith("[")) return `[${host}]`;
976
- return host;
977
- }
978
- //#endregion
979
- //#region src/server.ts
980
- function hostForUrl(host) {
981
- if (host.includes(":") && !host.startsWith("[")) return `[${host}]`;
982
- return host;
983
- }
984
- /**
985
- * Start the vis HTTP server programmatically. Resolves once the server is
986
- * listening. Used by the CLI `byf vis` subcommand (in-process) and by the
987
- * standalone `index.ts` entry.
988
- */
989
- async function startVisServer(options = {}) {
990
- const host = options.host ?? resolveHost();
991
- const port = options.port ?? resolvePort();
992
- const app = await createApp({
993
- authToken: options.authToken ?? resolveVisAuthToken(host),
994
- publicDir: options.publicDir
995
- });
996
- return new Promise((resolve, reject) => {
997
- let settled = false;
998
- const server = serve({
999
- fetch: app.fetch,
1000
- hostname: host,
1001
- port
1002
- }, () => {
1003
- if (settled) return;
1004
- settled = true;
1005
- const address = server.address();
1006
- const actualPort = typeof address === "object" && address !== null ? address.port : port;
1007
- resolve({
1008
- host,
1009
- port: actualPort,
1010
- url: `http://${hostForUrl(host)}:${actualPort}`,
1011
- close: () => {
1012
- server.closeAllConnections();
1013
- server.close();
1014
- }
1015
- });
1016
- });
1017
- server.on("error", (error) => {
1018
- if (settled) return;
1019
- settled = true;
1020
- reject(error);
1021
- });
1022
- });
1023
- }
1024
- /**
1025
- * Resolve the BYF_HOME the server reads session records from. Exposed for the
1026
- * standalone entry's startup banner. CLI consumers rely on the same env var.
1027
- */
1028
- function resolveVisByfHome() {
1029
- return resolveByfHome();
1030
- }
1031
- /**
1032
- * Format the startup banner text. Exposed so the CLI can reuse the exact same
1033
- * wording without depending on startup-banner internals.
1034
- */
1035
- function formatVisStartupBanner(input) {
1036
- return formatStartupBanner({
1037
- authToken: input.authToken,
1038
- host: input.host,
1039
- byfCodeHome: resolveByfHome(),
1040
- port: input.port
1041
- });
1042
- }
1043
- //#endregion
1044
- export { resolvePort as a, resolveHost as i, resolveVisByfHome as n, resolveVisAuthToken as o, startVisServer as r, formatVisStartupBanner as t };