llm-kb 0.4.2 → 0.6.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.
@@ -0,0 +1,1069 @@
1
+ import {
2
+ createChat,
3
+ parseCitations
4
+ } from "./chunk-Y2764FFH.js";
5
+ import "./chunk-3WBSKCCH.js";
6
+ import "./chunk-3YMNGUZZ.js";
7
+ import "./chunk-LDHOKBJA.js";
8
+ import "./chunk-5PYKQQLA.js";
9
+ import "./chunk-EAQYK3U2.js";
10
+
11
+ // src/web/server.ts
12
+ import { Hono } from "hono";
13
+
14
+ // node_modules/@hono/node-server/dist/index.mjs
15
+ import { createServer as createServerHTTP } from "http";
16
+ import { Http2ServerRequest as Http2ServerRequest2, constants as h2constants } from "http2";
17
+ import { Http2ServerRequest } from "http2";
18
+ import { Readable } from "stream";
19
+ import crypto from "crypto";
20
+ var RequestError = class extends Error {
21
+ constructor(message, options) {
22
+ super(message, options);
23
+ this.name = "RequestError";
24
+ }
25
+ };
26
+ var toRequestError = (e) => {
27
+ if (e instanceof RequestError) {
28
+ return e;
29
+ }
30
+ return new RequestError(e.message, { cause: e });
31
+ };
32
+ var GlobalRequest = global.Request;
33
+ var Request = class extends GlobalRequest {
34
+ constructor(input, options) {
35
+ if (typeof input === "object" && getRequestCache in input) {
36
+ input = input[getRequestCache]();
37
+ }
38
+ if (typeof options?.body?.getReader !== "undefined") {
39
+ ;
40
+ options.duplex ??= "half";
41
+ }
42
+ super(input, options);
43
+ }
44
+ };
45
+ var newHeadersFromIncoming = (incoming) => {
46
+ const headerRecord = [];
47
+ const rawHeaders = incoming.rawHeaders;
48
+ for (let i = 0; i < rawHeaders.length; i += 2) {
49
+ const { [i]: key, [i + 1]: value } = rawHeaders;
50
+ if (key.charCodeAt(0) !== /*:*/
51
+ 58) {
52
+ headerRecord.push([key, value]);
53
+ }
54
+ }
55
+ return new Headers(headerRecord);
56
+ };
57
+ var wrapBodyStream = /* @__PURE__ */ Symbol("wrapBodyStream");
58
+ var newRequestFromIncoming = (method, url, headers, incoming, abortController) => {
59
+ const init = {
60
+ method,
61
+ headers,
62
+ signal: abortController.signal
63
+ };
64
+ if (method === "TRACE") {
65
+ init.method = "GET";
66
+ const req = new Request(url, init);
67
+ Object.defineProperty(req, "method", {
68
+ get() {
69
+ return "TRACE";
70
+ }
71
+ });
72
+ return req;
73
+ }
74
+ if (!(method === "GET" || method === "HEAD")) {
75
+ if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) {
76
+ init.body = new ReadableStream({
77
+ start(controller) {
78
+ controller.enqueue(incoming.rawBody);
79
+ controller.close();
80
+ }
81
+ });
82
+ } else if (incoming[wrapBodyStream]) {
83
+ let reader;
84
+ init.body = new ReadableStream({
85
+ async pull(controller) {
86
+ try {
87
+ reader ||= Readable.toWeb(incoming).getReader();
88
+ const { done, value } = await reader.read();
89
+ if (done) {
90
+ controller.close();
91
+ } else {
92
+ controller.enqueue(value);
93
+ }
94
+ } catch (error) {
95
+ controller.error(error);
96
+ }
97
+ }
98
+ });
99
+ } else {
100
+ init.body = Readable.toWeb(incoming);
101
+ }
102
+ }
103
+ return new Request(url, init);
104
+ };
105
+ var getRequestCache = /* @__PURE__ */ Symbol("getRequestCache");
106
+ var requestCache = /* @__PURE__ */ Symbol("requestCache");
107
+ var incomingKey = /* @__PURE__ */ Symbol("incomingKey");
108
+ var urlKey = /* @__PURE__ */ Symbol("urlKey");
109
+ var headersKey = /* @__PURE__ */ Symbol("headersKey");
110
+ var abortControllerKey = /* @__PURE__ */ Symbol("abortControllerKey");
111
+ var getAbortController = /* @__PURE__ */ Symbol("getAbortController");
112
+ var requestPrototype = {
113
+ get method() {
114
+ return this[incomingKey].method || "GET";
115
+ },
116
+ get url() {
117
+ return this[urlKey];
118
+ },
119
+ get headers() {
120
+ return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]);
121
+ },
122
+ [getAbortController]() {
123
+ this[getRequestCache]();
124
+ return this[abortControllerKey];
125
+ },
126
+ [getRequestCache]() {
127
+ this[abortControllerKey] ||= new AbortController();
128
+ return this[requestCache] ||= newRequestFromIncoming(
129
+ this.method,
130
+ this[urlKey],
131
+ this.headers,
132
+ this[incomingKey],
133
+ this[abortControllerKey]
134
+ );
135
+ }
136
+ };
137
+ [
138
+ "body",
139
+ "bodyUsed",
140
+ "cache",
141
+ "credentials",
142
+ "destination",
143
+ "integrity",
144
+ "mode",
145
+ "redirect",
146
+ "referrer",
147
+ "referrerPolicy",
148
+ "signal",
149
+ "keepalive"
150
+ ].forEach((k) => {
151
+ Object.defineProperty(requestPrototype, k, {
152
+ get() {
153
+ return this[getRequestCache]()[k];
154
+ }
155
+ });
156
+ });
157
+ ["arrayBuffer", "blob", "clone", "formData", "json", "text"].forEach((k) => {
158
+ Object.defineProperty(requestPrototype, k, {
159
+ value: function() {
160
+ return this[getRequestCache]()[k]();
161
+ }
162
+ });
163
+ });
164
+ Object.setPrototypeOf(requestPrototype, Request.prototype);
165
+ var newRequest = (incoming, defaultHostname) => {
166
+ const req = Object.create(requestPrototype);
167
+ req[incomingKey] = incoming;
168
+ const incomingUrl = incoming.url || "";
169
+ if (incomingUrl[0] !== "/" && // short-circuit for performance. most requests are relative URL.
170
+ (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) {
171
+ if (incoming instanceof Http2ServerRequest) {
172
+ throw new RequestError("Absolute URL for :path is not allowed in HTTP/2");
173
+ }
174
+ try {
175
+ const url2 = new URL(incomingUrl);
176
+ req[urlKey] = url2.href;
177
+ } catch (e) {
178
+ throw new RequestError("Invalid absolute URL", { cause: e });
179
+ }
180
+ return req;
181
+ }
182
+ const host = (incoming instanceof Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname;
183
+ if (!host) {
184
+ throw new RequestError("Missing host header");
185
+ }
186
+ let scheme;
187
+ if (incoming instanceof Http2ServerRequest) {
188
+ scheme = incoming.scheme;
189
+ if (!(scheme === "http" || scheme === "https")) {
190
+ throw new RequestError("Unsupported scheme");
191
+ }
192
+ } else {
193
+ scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http";
194
+ }
195
+ const url = new URL(`${scheme}://${host}${incomingUrl}`);
196
+ if (url.hostname.length !== host.length && url.hostname !== host.replace(/:\d+$/, "")) {
197
+ throw new RequestError("Invalid host header");
198
+ }
199
+ req[urlKey] = url.href;
200
+ return req;
201
+ };
202
+ var responseCache = /* @__PURE__ */ Symbol("responseCache");
203
+ var getResponseCache = /* @__PURE__ */ Symbol("getResponseCache");
204
+ var cacheKey = /* @__PURE__ */ Symbol("cache");
205
+ var GlobalResponse = global.Response;
206
+ var Response2 = class _Response {
207
+ #body;
208
+ #init;
209
+ [getResponseCache]() {
210
+ delete this[cacheKey];
211
+ return this[responseCache] ||= new GlobalResponse(this.#body, this.#init);
212
+ }
213
+ constructor(body, init) {
214
+ let headers;
215
+ this.#body = body;
216
+ if (init instanceof _Response) {
217
+ const cachedGlobalResponse = init[responseCache];
218
+ if (cachedGlobalResponse) {
219
+ this.#init = cachedGlobalResponse;
220
+ this[getResponseCache]();
221
+ return;
222
+ } else {
223
+ this.#init = init.#init;
224
+ headers = new Headers(init.#init.headers);
225
+ }
226
+ } else {
227
+ this.#init = init;
228
+ }
229
+ if (typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) {
230
+ ;
231
+ this[cacheKey] = [init?.status || 200, body, headers || init?.headers];
232
+ }
233
+ }
234
+ get headers() {
235
+ const cache = this[cacheKey];
236
+ if (cache) {
237
+ if (!(cache[2] instanceof Headers)) {
238
+ cache[2] = new Headers(
239
+ cache[2] || { "content-type": "text/plain; charset=UTF-8" }
240
+ );
241
+ }
242
+ return cache[2];
243
+ }
244
+ return this[getResponseCache]().headers;
245
+ }
246
+ get status() {
247
+ return this[cacheKey]?.[0] ?? this[getResponseCache]().status;
248
+ }
249
+ get ok() {
250
+ const status = this.status;
251
+ return status >= 200 && status < 300;
252
+ }
253
+ };
254
+ ["body", "bodyUsed", "redirected", "statusText", "trailers", "type", "url"].forEach((k) => {
255
+ Object.defineProperty(Response2.prototype, k, {
256
+ get() {
257
+ return this[getResponseCache]()[k];
258
+ }
259
+ });
260
+ });
261
+ ["arrayBuffer", "blob", "clone", "formData", "json", "text"].forEach((k) => {
262
+ Object.defineProperty(Response2.prototype, k, {
263
+ value: function() {
264
+ return this[getResponseCache]()[k]();
265
+ }
266
+ });
267
+ });
268
+ Object.setPrototypeOf(Response2, GlobalResponse);
269
+ Object.setPrototypeOf(Response2.prototype, GlobalResponse.prototype);
270
+ async function readWithoutBlocking(readPromise) {
271
+ return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]);
272
+ }
273
+ function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) {
274
+ const cancel = (error) => {
275
+ reader.cancel(error).catch(() => {
276
+ });
277
+ };
278
+ writable.on("close", cancel);
279
+ writable.on("error", cancel);
280
+ (currentReadPromise ?? reader.read()).then(flow, handleStreamError);
281
+ return reader.closed.finally(() => {
282
+ writable.off("close", cancel);
283
+ writable.off("error", cancel);
284
+ });
285
+ function handleStreamError(error) {
286
+ if (error) {
287
+ writable.destroy(error);
288
+ }
289
+ }
290
+ function onDrain() {
291
+ reader.read().then(flow, handleStreamError);
292
+ }
293
+ function flow({ done, value }) {
294
+ try {
295
+ if (done) {
296
+ writable.end();
297
+ } else if (!writable.write(value)) {
298
+ writable.once("drain", onDrain);
299
+ } else {
300
+ return reader.read().then(flow, handleStreamError);
301
+ }
302
+ } catch (e) {
303
+ handleStreamError(e);
304
+ }
305
+ }
306
+ }
307
+ function writeFromReadableStream(stream, writable) {
308
+ if (stream.locked) {
309
+ throw new TypeError("ReadableStream is locked.");
310
+ } else if (writable.destroyed) {
311
+ return;
312
+ }
313
+ return writeFromReadableStreamDefaultReader(stream.getReader(), writable);
314
+ }
315
+ var buildOutgoingHttpHeaders = (headers) => {
316
+ const res = {};
317
+ if (!(headers instanceof Headers)) {
318
+ headers = new Headers(headers ?? void 0);
319
+ }
320
+ const cookies = [];
321
+ for (const [k, v] of headers) {
322
+ if (k === "set-cookie") {
323
+ cookies.push(v);
324
+ } else {
325
+ res[k] = v;
326
+ }
327
+ }
328
+ if (cookies.length > 0) {
329
+ res["set-cookie"] = cookies;
330
+ }
331
+ res["content-type"] ??= "text/plain; charset=UTF-8";
332
+ return res;
333
+ };
334
+ var X_ALREADY_SENT = "x-hono-already-sent";
335
+ if (typeof global.crypto === "undefined") {
336
+ global.crypto = crypto;
337
+ }
338
+ var outgoingEnded = /* @__PURE__ */ Symbol("outgoingEnded");
339
+ var incomingDraining = /* @__PURE__ */ Symbol("incomingDraining");
340
+ var DRAIN_TIMEOUT_MS = 500;
341
+ var MAX_DRAIN_BYTES = 64 * 1024 * 1024;
342
+ var drainIncoming = (incoming) => {
343
+ const incomingWithDrainState = incoming;
344
+ if (incoming.destroyed || incomingWithDrainState[incomingDraining]) {
345
+ return;
346
+ }
347
+ incomingWithDrainState[incomingDraining] = true;
348
+ if (incoming instanceof Http2ServerRequest2) {
349
+ try {
350
+ ;
351
+ incoming.stream?.close?.(h2constants.NGHTTP2_NO_ERROR);
352
+ } catch {
353
+ }
354
+ return;
355
+ }
356
+ let bytesRead = 0;
357
+ const cleanup = () => {
358
+ clearTimeout(timer);
359
+ incoming.off("data", onData);
360
+ incoming.off("end", cleanup);
361
+ incoming.off("error", cleanup);
362
+ };
363
+ const forceClose = () => {
364
+ cleanup();
365
+ const socket = incoming.socket;
366
+ if (socket && !socket.destroyed) {
367
+ socket.destroySoon();
368
+ }
369
+ };
370
+ const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS);
371
+ timer.unref?.();
372
+ const onData = (chunk) => {
373
+ bytesRead += chunk.length;
374
+ if (bytesRead > MAX_DRAIN_BYTES) {
375
+ forceClose();
376
+ }
377
+ };
378
+ incoming.on("data", onData);
379
+ incoming.on("end", cleanup);
380
+ incoming.on("error", cleanup);
381
+ incoming.resume();
382
+ };
383
+ var handleRequestError = () => new Response(null, {
384
+ status: 400
385
+ });
386
+ var handleFetchError = (e) => new Response(null, {
387
+ status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500
388
+ });
389
+ var handleResponseError = (e, outgoing) => {
390
+ const err = e instanceof Error ? e : new Error("unknown error", { cause: e });
391
+ if (err.code === "ERR_STREAM_PREMATURE_CLOSE") {
392
+ console.info("The user aborted a request.");
393
+ } else {
394
+ console.error(e);
395
+ if (!outgoing.headersSent) {
396
+ outgoing.writeHead(500, { "Content-Type": "text/plain" });
397
+ }
398
+ outgoing.end(`Error: ${err.message}`);
399
+ outgoing.destroy(err);
400
+ }
401
+ };
402
+ var flushHeaders = (outgoing) => {
403
+ if ("flushHeaders" in outgoing && outgoing.writable) {
404
+ outgoing.flushHeaders();
405
+ }
406
+ };
407
+ var responseViaCache = async (res, outgoing) => {
408
+ let [status, body, header] = res[cacheKey];
409
+ let hasContentLength = false;
410
+ if (!header) {
411
+ header = { "content-type": "text/plain; charset=UTF-8" };
412
+ } else if (header instanceof Headers) {
413
+ hasContentLength = header.has("content-length");
414
+ header = buildOutgoingHttpHeaders(header);
415
+ } else if (Array.isArray(header)) {
416
+ const headerObj = new Headers(header);
417
+ hasContentLength = headerObj.has("content-length");
418
+ header = buildOutgoingHttpHeaders(headerObj);
419
+ } else {
420
+ for (const key in header) {
421
+ if (key.length === 14 && key.toLowerCase() === "content-length") {
422
+ hasContentLength = true;
423
+ break;
424
+ }
425
+ }
426
+ }
427
+ if (!hasContentLength) {
428
+ if (typeof body === "string") {
429
+ header["Content-Length"] = Buffer.byteLength(body);
430
+ } else if (body instanceof Uint8Array) {
431
+ header["Content-Length"] = body.byteLength;
432
+ } else if (body instanceof Blob) {
433
+ header["Content-Length"] = body.size;
434
+ }
435
+ }
436
+ outgoing.writeHead(status, header);
437
+ if (typeof body === "string" || body instanceof Uint8Array) {
438
+ outgoing.end(body);
439
+ } else if (body instanceof Blob) {
440
+ outgoing.end(new Uint8Array(await body.arrayBuffer()));
441
+ } else {
442
+ flushHeaders(outgoing);
443
+ await writeFromReadableStream(body, outgoing)?.catch(
444
+ (e) => handleResponseError(e, outgoing)
445
+ );
446
+ }
447
+ ;
448
+ outgoing[outgoingEnded]?.();
449
+ };
450
+ var isPromise = (res) => typeof res.then === "function";
451
+ var responseViaResponseObject = async (res, outgoing, options = {}) => {
452
+ if (isPromise(res)) {
453
+ if (options.errorHandler) {
454
+ try {
455
+ res = await res;
456
+ } catch (err) {
457
+ const errRes = await options.errorHandler(err);
458
+ if (!errRes) {
459
+ return;
460
+ }
461
+ res = errRes;
462
+ }
463
+ } else {
464
+ res = await res.catch(handleFetchError);
465
+ }
466
+ }
467
+ if (cacheKey in res) {
468
+ return responseViaCache(res, outgoing);
469
+ }
470
+ const resHeaderRecord = buildOutgoingHttpHeaders(res.headers);
471
+ if (res.body) {
472
+ const reader = res.body.getReader();
473
+ const values = [];
474
+ let done = false;
475
+ let currentReadPromise = void 0;
476
+ if (resHeaderRecord["transfer-encoding"] !== "chunked") {
477
+ let maxReadCount = 2;
478
+ for (let i = 0; i < maxReadCount; i++) {
479
+ currentReadPromise ||= reader.read();
480
+ const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => {
481
+ console.error(e);
482
+ done = true;
483
+ });
484
+ if (!chunk) {
485
+ if (i === 1) {
486
+ await new Promise((resolve) => setTimeout(resolve));
487
+ maxReadCount = 3;
488
+ continue;
489
+ }
490
+ break;
491
+ }
492
+ currentReadPromise = void 0;
493
+ if (chunk.value) {
494
+ values.push(chunk.value);
495
+ }
496
+ if (chunk.done) {
497
+ done = true;
498
+ break;
499
+ }
500
+ }
501
+ if (done && !("content-length" in resHeaderRecord)) {
502
+ resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0);
503
+ }
504
+ }
505
+ outgoing.writeHead(res.status, resHeaderRecord);
506
+ values.forEach((value) => {
507
+ ;
508
+ outgoing.write(value);
509
+ });
510
+ if (done) {
511
+ outgoing.end();
512
+ } else {
513
+ if (values.length === 0) {
514
+ flushHeaders(outgoing);
515
+ }
516
+ await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise);
517
+ }
518
+ } else if (resHeaderRecord[X_ALREADY_SENT]) {
519
+ } else {
520
+ outgoing.writeHead(res.status, resHeaderRecord);
521
+ outgoing.end();
522
+ }
523
+ ;
524
+ outgoing[outgoingEnded]?.();
525
+ };
526
+ var getRequestListener = (fetchCallback, options = {}) => {
527
+ const autoCleanupIncoming = options.autoCleanupIncoming ?? true;
528
+ if (options.overrideGlobalObjects !== false && global.Request !== Request) {
529
+ Object.defineProperty(global, "Request", {
530
+ value: Request
531
+ });
532
+ Object.defineProperty(global, "Response", {
533
+ value: Response2
534
+ });
535
+ }
536
+ return async (incoming, outgoing) => {
537
+ let res, req;
538
+ try {
539
+ req = newRequest(incoming, options.hostname);
540
+ let incomingEnded = !autoCleanupIncoming || incoming.method === "GET" || incoming.method === "HEAD";
541
+ if (!incomingEnded) {
542
+ ;
543
+ incoming[wrapBodyStream] = true;
544
+ incoming.on("end", () => {
545
+ incomingEnded = true;
546
+ });
547
+ if (incoming instanceof Http2ServerRequest2) {
548
+ ;
549
+ outgoing[outgoingEnded] = () => {
550
+ if (!incomingEnded) {
551
+ setTimeout(() => {
552
+ if (!incomingEnded) {
553
+ setTimeout(() => {
554
+ drainIncoming(incoming);
555
+ });
556
+ }
557
+ });
558
+ }
559
+ };
560
+ }
561
+ outgoing.on("finish", () => {
562
+ if (!incomingEnded) {
563
+ drainIncoming(incoming);
564
+ }
565
+ });
566
+ }
567
+ outgoing.on("close", () => {
568
+ const abortController = req[abortControllerKey];
569
+ if (abortController) {
570
+ if (incoming.errored) {
571
+ req[abortControllerKey].abort(incoming.errored.toString());
572
+ } else if (!outgoing.writableFinished) {
573
+ req[abortControllerKey].abort("Client connection prematurely closed.");
574
+ }
575
+ }
576
+ if (!incomingEnded) {
577
+ setTimeout(() => {
578
+ if (!incomingEnded) {
579
+ setTimeout(() => {
580
+ drainIncoming(incoming);
581
+ });
582
+ }
583
+ });
584
+ }
585
+ });
586
+ res = fetchCallback(req, { incoming, outgoing });
587
+ if (cacheKey in res) {
588
+ return responseViaCache(res, outgoing);
589
+ }
590
+ } catch (e) {
591
+ if (!res) {
592
+ if (options.errorHandler) {
593
+ res = await options.errorHandler(req ? e : toRequestError(e));
594
+ if (!res) {
595
+ return;
596
+ }
597
+ } else if (!req) {
598
+ res = handleRequestError();
599
+ } else {
600
+ res = handleFetchError(e);
601
+ }
602
+ } else {
603
+ return handleResponseError(e, outgoing);
604
+ }
605
+ }
606
+ try {
607
+ return await responseViaResponseObject(res, outgoing, options);
608
+ } catch (e) {
609
+ return handleResponseError(e, outgoing);
610
+ }
611
+ };
612
+ };
613
+ var createAdaptorServer = (options) => {
614
+ const fetchCallback = options.fetch;
615
+ const requestListener = getRequestListener(fetchCallback, {
616
+ hostname: options.hostname,
617
+ overrideGlobalObjects: options.overrideGlobalObjects,
618
+ autoCleanupIncoming: options.autoCleanupIncoming
619
+ });
620
+ const createServer = options.createServer || createServerHTTP;
621
+ const server = createServer(options.serverOptions || {}, requestListener);
622
+ return server;
623
+ };
624
+ var serve = (options, listeningListener) => {
625
+ const server = createAdaptorServer(options);
626
+ server.listen(options?.port ?? 3e3, options.hostname, () => {
627
+ const serverInfo = server.address();
628
+ listeningListener && listeningListener(serverInfo);
629
+ });
630
+ return server;
631
+ };
632
+
633
+ // src/web/server.ts
634
+ import { createNodeWebSocket } from "@hono/node-ws";
635
+ import { readFile, readdir } from "fs/promises";
636
+ import { existsSync } from "fs";
637
+ import { join } from "path";
638
+ import { fileURLToPath } from "url";
639
+ import { dirname } from "path";
640
+ import { exec } from "child_process";
641
+
642
+ // src/web/bridge.ts
643
+ import { basename } from "path";
644
+ function getToolLabel(toolName, args) {
645
+ if (toolName === "read" || toolName === "write" || toolName === "edit") {
646
+ const file = basename(args?.path ?? "");
647
+ if (!file || !/\.[a-z0-9]{1,6}$/i.test(file)) return null;
648
+ const verb = toolName === "read" ? "Reading" : toolName === "write" ? "Writing" : "Editing";
649
+ return `${verb} ${file}`;
650
+ }
651
+ if (toolName === "bash" && args?.command) {
652
+ const cmd = args.command.toLowerCase();
653
+ if (cmd.includes("textitems") || cmd.includes("bbox") || cmd.includes(".json")) {
654
+ return "Looking up citations";
655
+ }
656
+ if (cmd.includes("readfilesync") || cmd.includes("readfile") || cmd.includes("cat ")) {
657
+ return "Searching files";
658
+ }
659
+ if (cmd.includes("grep") || cmd.includes("find") || cmd.includes("search")) {
660
+ return "Searching files";
661
+ }
662
+ return "Analyzing sources";
663
+ }
664
+ return null;
665
+ }
666
+ async function createWebChatSession(folder, ws, options) {
667
+ const { session, display } = await createChat(folder, {
668
+ authStorage: options.authStorage,
669
+ modelId: options.modelId
670
+ });
671
+ const send = (data) => {
672
+ try {
673
+ ws.send(JSON.stringify(data));
674
+ } catch (e) {
675
+ console.error("[bridge] WS send failed:", e);
676
+ }
677
+ };
678
+ let startTime = Date.now();
679
+ let filesReadCount = 0;
680
+ let shownToolCalls = /* @__PURE__ */ new Set();
681
+ let accumulatedAnswer = "";
682
+ session.subscribe((event) => {
683
+ if (event.type === "agent_start") {
684
+ startTime = Date.now();
685
+ filesReadCount = 0;
686
+ shownToolCalls = /* @__PURE__ */ new Set();
687
+ accumulatedAnswer = "";
688
+ const modelName = options.modelId ?? "claude-sonnet-4-6";
689
+ send({ type: "status", model: modelName });
690
+ }
691
+ if (event.type === "message_update") {
692
+ const ae = event.assistantMessageEvent;
693
+ if (ae.type === "thinking_start") send({ type: "thinking_start" });
694
+ if (ae.type === "thinking_delta") send({ type: "thinking_delta", text: ae.delta });
695
+ if (ae.type === "thinking_end") send({ type: "thinking_end" });
696
+ }
697
+ if (event.type === "message_update") {
698
+ const ae = event.assistantMessageEvent;
699
+ if (ae.type === "toolcall_end" && ae.toolCall) {
700
+ const label = getToolLabel(ae.toolCall.name, ae.toolCall.arguments);
701
+ if (label && !shownToolCalls.has(ae.toolCall.id)) {
702
+ shownToolCalls.add(ae.toolCall.id);
703
+ if (ae.toolCall.name === "read") filesReadCount++;
704
+ send({ type: "tool_start", id: ae.toolCall.id, label, name: ae.toolCall.name });
705
+ }
706
+ }
707
+ }
708
+ if (event.type === "tool_execution_start") {
709
+ const { toolCallId, toolName, args } = event;
710
+ if (!shownToolCalls.has(toolCallId)) {
711
+ const label = getToolLabel(toolName, args);
712
+ if (label) {
713
+ shownToolCalls.add(toolCallId);
714
+ if (toolName === "read") filesReadCount++;
715
+ send({ type: "tool_start", id: toolCallId, label, name: toolName });
716
+ }
717
+ }
718
+ }
719
+ if (event.type === "tool_execution_end") {
720
+ const { toolCallId, isError } = event;
721
+ send({ type: "tool_end", id: toolCallId, isError });
722
+ }
723
+ if (event.type === "message_update") {
724
+ const ae = event.assistantMessageEvent;
725
+ if (ae.type === "text_start") send({ type: "text_start" });
726
+ if (ae.type === "text_delta") {
727
+ accumulatedAnswer += ae.delta;
728
+ send({ type: "text_delta", text: ae.delta });
729
+ }
730
+ if (ae.type === "text_end") send({ type: "text_end" });
731
+ }
732
+ if (event.type === "agent_end") {
733
+ const elapsed = (Date.now() - startTime) / 1e3;
734
+ const messages = event.messages;
735
+ let fullAnswer = "";
736
+ for (const msg of messages) {
737
+ if (msg.role !== "assistant") continue;
738
+ for (const block of msg.content ?? []) {
739
+ if (block.type === "text" && block.text) {
740
+ fullAnswer += block.text;
741
+ }
742
+ }
743
+ }
744
+ const textToSearch = fullAnswer || accumulatedAnswer;
745
+ const parsed = parseCitations(textToSearch);
746
+ console.log(`[bridge] Full answer: ${fullAnswer.length} chars, streamed: ${accumulatedAnswer.length} chars, citations: ${parsed.citations.length}`);
747
+ if (parsed.citations.length === 0 && textToSearch.length > 0) {
748
+ console.log(`[bridge] No citations found. Last 300 chars: ...${textToSearch.slice(-300)}`);
749
+ }
750
+ const sendDone = () => send({
751
+ type: "done",
752
+ elapsed: Math.round(elapsed * 10) / 10,
753
+ filesRead: filesReadCount,
754
+ citationCount: parsed.citations.length,
755
+ answer: parsed.answer
756
+ });
757
+ if (parsed.citations.length > 0) {
758
+ send({ type: "citations", data: parsed.citations });
759
+ }
760
+ sendDone();
761
+ }
762
+ });
763
+ return {
764
+ async prompt(text) {
765
+ display.setQuestion(text);
766
+ session.setSessionName(`query: ${text}`);
767
+ await session.prompt(text);
768
+ await display.flush();
769
+ },
770
+ dispose() {
771
+ session.dispose();
772
+ }
773
+ };
774
+ }
775
+
776
+ // src/web/server.ts
777
+ async function startWebUI(options) {
778
+ const { folder, port, open } = options;
779
+ const kbDir = join(folder, ".llm-kb");
780
+ const sourcesDir = join(kbDir, "wiki", "sources");
781
+ const app = new Hono();
782
+ const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
783
+ const cliDir = dirname(fileURLToPath(import.meta.url));
784
+ app.get("/", async (c) => {
785
+ const candidates = [
786
+ join(cliDir, "public", "index.html"),
787
+ // bin/public/index.html
788
+ join(cliDir, "..", "bin", "public", "index.html"),
789
+ // from project root
790
+ join(cliDir, "..", "src", "web", "public", "index.html"),
791
+ // dev source
792
+ join(process.cwd(), "bin", "public", "index.html"),
793
+ // cwd fallback
794
+ join(process.cwd(), "src", "web", "public", "index.html")
795
+ // cwd dev fallback
796
+ ];
797
+ for (const p of candidates) {
798
+ if (existsSync(p)) {
799
+ const html = await readFile(p, "utf-8");
800
+ return c.html(html);
801
+ }
802
+ }
803
+ return c.text(`index.html not found. Searched: ${candidates.join(", ")}`, 404);
804
+ });
805
+ app.get("/api/status", async (c) => {
806
+ let sourceCount = 0;
807
+ let wikiExists = false;
808
+ let wikiConcepts = 0;
809
+ try {
810
+ const files = await readdir(sourcesDir);
811
+ sourceCount = files.filter((f) => f.endsWith(".md")).length;
812
+ } catch {
813
+ }
814
+ const wikiPath = join(kbDir, "wiki", "wiki.md");
815
+ if (existsSync(wikiPath)) {
816
+ wikiExists = true;
817
+ try {
818
+ const wiki = await readFile(wikiPath, "utf-8");
819
+ wikiConcepts = (wiki.match(/^## /gm) || []).length;
820
+ } catch {
821
+ }
822
+ }
823
+ return c.json({ sourceCount, wikiExists, wikiConcepts, folder });
824
+ });
825
+ app.get("/api/sources", async (c) => {
826
+ try {
827
+ const files = await readdir(sourcesDir);
828
+ const sources = [];
829
+ for (const f of files.filter((f2) => f2.endsWith(".json"))) {
830
+ try {
831
+ const data = JSON.parse(await readFile(join(sourcesDir, f), "utf-8"));
832
+ sources.push({
833
+ name: data.source || f.replace(".json", ".pdf"),
834
+ pages: data.totalPages || 0,
835
+ jsonFile: f,
836
+ mdFile: f.replace(".json", ".md")
837
+ });
838
+ } catch {
839
+ }
840
+ }
841
+ return c.json(sources);
842
+ } catch {
843
+ return c.json([]);
844
+ }
845
+ });
846
+ app.get("/api/sessions", async (c) => {
847
+ const sessionsDir = join(kbDir, "sessions");
848
+ const tracesDir = join(kbDir, "traces");
849
+ try {
850
+ const traceFiles = existsSync(tracesDir) ? (await readdir(tracesDir)).filter((f) => f.endsWith(".json")) : [];
851
+ const sessions = [];
852
+ for (const f of traceFiles) {
853
+ try {
854
+ const trace = JSON.parse(await readFile(join(tracesDir, f), "utf-8"));
855
+ if (trace.mode === "query" && trace.question) {
856
+ sessions.push({
857
+ id: trace.sessionId,
858
+ question: trace.question,
859
+ timestamp: trace.timestamp,
860
+ model: trace.model,
861
+ citationCount: trace.citations?.length ?? 0,
862
+ filesRead: trace.filesRead?.length ?? 0
863
+ });
864
+ }
865
+ } catch {
866
+ }
867
+ }
868
+ sessions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
869
+ return c.json(sessions);
870
+ } catch {
871
+ return c.json([]);
872
+ }
873
+ });
874
+ app.get("/api/sessions/:id", async (c) => {
875
+ const id = c.req.param("id");
876
+ const tracesDir = join(kbDir, "traces");
877
+ const tracePath = join(tracesDir, `${id}.json`);
878
+ if (!existsSync(tracePath)) return c.json({ error: "Not found" }, 404);
879
+ try {
880
+ const trace = JSON.parse(await readFile(tracePath, "utf-8"));
881
+ return c.json(trace);
882
+ } catch {
883
+ return c.json({ error: "Failed to read trace" }, 500);
884
+ }
885
+ });
886
+ app.get("/api/pdf/:filename", async (c) => {
887
+ const filename = decodeURIComponent(c.req.param("filename"));
888
+ const pdfPath = join(folder, filename);
889
+ if (existsSync(pdfPath)) {
890
+ const buf = await readFile(pdfPath);
891
+ return c.body(buf, { headers: { "Content-Type": "application/pdf" } });
892
+ }
893
+ try {
894
+ const files = await readdir(folder);
895
+ const match = files.find((f) => f.endsWith(".pdf") && f.includes(filename.replace(/^\d+\.\s*/, "")));
896
+ if (match) {
897
+ const buf = await readFile(join(folder, match));
898
+ return c.body(buf, { headers: { "Content-Type": "application/pdf" } });
899
+ }
900
+ } catch {
901
+ }
902
+ return c.text("PDF not found", 404);
903
+ });
904
+ app.get("/api/bbox/:filename", async (c) => {
905
+ const filename = decodeURIComponent(c.req.param("filename"));
906
+ const pageParam = c.req.query("page");
907
+ const baseName = filename.replace(/\.pdf$/i, "");
908
+ const pagesDir = join(sourcesDir, `${baseName}.pages`);
909
+ if (pageParam) {
910
+ const pageNum = parseInt(pageParam, 10);
911
+ const pageFile = join(pagesDir, `${pageNum}.json`);
912
+ if (existsSync(pageFile)) {
913
+ const data = JSON.parse(await readFile(pageFile, "utf-8"));
914
+ return c.json({ source: data.source, totalPages: data.totalPages, pages: [data] });
915
+ }
916
+ try {
917
+ const allDirs = await readdir(sourcesDir);
918
+ const needle = baseName.replace(/^\d+\.\s*/, "").toLowerCase();
919
+ const match = allDirs.find((d) => d.endsWith(".pages") && d.toLowerCase().includes(needle));
920
+ if (match) {
921
+ const fuzzyPageFile = join(sourcesDir, match, `${pageNum}.json`);
922
+ if (existsSync(fuzzyPageFile)) {
923
+ const data = JSON.parse(await readFile(fuzzyPageFile, "utf-8"));
924
+ return c.json({ source: data.source, totalPages: data.totalPages, pages: [data] });
925
+ }
926
+ }
927
+ } catch {
928
+ }
929
+ }
930
+ const jsonPath = join(sourcesDir, `${baseName}.json`);
931
+ if (existsSync(jsonPath)) {
932
+ const data = JSON.parse(await readFile(jsonPath, "utf-8"));
933
+ if (pageParam) {
934
+ const pageNum = parseInt(pageParam, 10);
935
+ const page = data.pages?.find((p) => p.page === pageNum);
936
+ if (!page) return c.json({ error: "Page not found" }, 404);
937
+ return c.json({ source: data.source, totalPages: data.totalPages, pages: [page] });
938
+ }
939
+ return c.json(data);
940
+ }
941
+ try {
942
+ const files = await readdir(sourcesDir);
943
+ const needle = baseName.replace(/^\d+\.\s*/, "").toLowerCase();
944
+ const match = files.find((f) => f.endsWith(".json") && f.toLowerCase().includes(needle));
945
+ if (match) {
946
+ const data = JSON.parse(await readFile(join(sourcesDir, match), "utf-8"));
947
+ if (pageParam) {
948
+ const pageNum = parseInt(pageParam, 10);
949
+ const page = data.pages?.find((p) => p.page === pageNum);
950
+ if (!page) return c.json({ error: "Page not found" }, 404);
951
+ return c.json({ source: data.source, totalPages: data.totalPages, pages: [page] });
952
+ }
953
+ return c.json(data);
954
+ }
955
+ } catch {
956
+ }
957
+ return c.json({ error: "Bbox data not found" }, 404);
958
+ });
959
+ app.get("/api/wiki", async (c) => {
960
+ const wikiPath = join(kbDir, "wiki", "wiki.md");
961
+ if (!existsSync(wikiPath)) return c.json({ content: "" });
962
+ try {
963
+ const content = await readFile(wikiPath, "utf-8");
964
+ return c.json({ content });
965
+ } catch {
966
+ return c.json({ content: "" });
967
+ }
968
+ });
969
+ app.get("/ws/chat", upgradeWebSocket((c) => {
970
+ let chatSession = null;
971
+ let creating = false;
972
+ let wsRef = null;
973
+ return {
974
+ onOpen(evt, ws) {
975
+ wsRef = ws;
976
+ console.log("[ws] Client connected");
977
+ ws.send(JSON.stringify({ type: "connected", message: "llm-kb web UI ready" }));
978
+ creating = true;
979
+ createWebChatSession(folder, {
980
+ send(data) {
981
+ try {
982
+ wsRef?.send(data);
983
+ } catch (e) {
984
+ console.error("[ws] send error:", e);
985
+ }
986
+ }
987
+ }, {
988
+ authStorage: options.authStorage,
989
+ modelId: options.modelId
990
+ }).then((session) => {
991
+ chatSession = session;
992
+ creating = false;
993
+ console.log("[ws] Agent session ready");
994
+ try {
995
+ wsRef?.send(JSON.stringify({ type: "ready" }));
996
+ } catch {
997
+ }
998
+ }).catch((err) => {
999
+ creating = false;
1000
+ console.error("[ws] Session creation failed:", err.message);
1001
+ try {
1002
+ wsRef?.send(JSON.stringify({ type: "error", message: err.message }));
1003
+ } catch {
1004
+ }
1005
+ });
1006
+ },
1007
+ onMessage(evt, ws) {
1008
+ const raw = typeof evt.data === "string" ? evt.data : "";
1009
+ let data;
1010
+ try {
1011
+ data = JSON.parse(raw);
1012
+ } catch {
1013
+ return;
1014
+ }
1015
+ console.log("[ws] Received:", data.type, data.text?.slice(0, 50));
1016
+ if (data.type === "message" && data.text) {
1017
+ if (!chatSession) {
1018
+ const msg = creating ? "Session still initializing, please wait..." : "Session not ready";
1019
+ ws.send(JSON.stringify({ type: "error", message: msg }));
1020
+ return;
1021
+ }
1022
+ chatSession.prompt(data.text).catch((err) => {
1023
+ console.error("[ws] Prompt error:", err.message);
1024
+ try {
1025
+ ws.send(JSON.stringify({ type: "error", message: err.message }));
1026
+ } catch {
1027
+ }
1028
+ });
1029
+ }
1030
+ },
1031
+ onClose() {
1032
+ console.log("[ws] Client disconnected");
1033
+ if (chatSession) {
1034
+ chatSession.dispose();
1035
+ chatSession = null;
1036
+ }
1037
+ wsRef = null;
1038
+ }
1039
+ };
1040
+ }));
1041
+ const server = serve({ fetch: app.fetch, port }, (info) => {
1042
+ console.log(`
1043
+ \u{1F310} llm-kb web UI running at http://localhost:${port}
1044
+ `);
1045
+ });
1046
+ server.on("error", (err) => {
1047
+ if (err.code === "EADDRINUSE") {
1048
+ console.error(`
1049
+ \u274C Port ${port} is already in use. Either:
1050
+ - Close the other llm-kb instance
1051
+ - Use a different port: llm-kb ui <folder> --port 3948
1052
+ `);
1053
+ process.exit(1);
1054
+ }
1055
+ throw err;
1056
+ });
1057
+ injectWebSocket(server);
1058
+ if (open) {
1059
+ const url = `http://localhost:${port}`;
1060
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
1061
+ exec(cmd, () => {
1062
+ });
1063
+ }
1064
+ await new Promise(() => {
1065
+ });
1066
+ }
1067
+ export {
1068
+ startWebUI
1069
+ };