chat-logbook 0.13.0 → 0.15.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/api/dist/index.js CHANGED
@@ -5,9 +5,9 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/index.ts
8
- import fs6 from "fs";
8
+ import fs8 from "fs";
9
9
  import os from "os";
10
- import path4 from "path";
10
+ import path7 from "path";
11
11
  import { exec } from "child_process";
12
12
  import { fileURLToPath as fileURLToPath2 } from "url";
13
13
 
@@ -304,13 +304,13 @@ function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromi
304
304
  }
305
305
  }
306
306
  }
307
- function writeFromReadableStream(stream, writable) {
308
- if (stream.locked) {
307
+ function writeFromReadableStream(stream2, writable) {
308
+ if (stream2.locked) {
309
309
  throw new TypeError("ReadableStream is locked.");
310
310
  } else if (writable.destroyed) {
311
311
  return;
312
312
  }
313
- return writeFromReadableStreamDefaultReader(stream.getReader(), writable);
313
+ return writeFromReadableStreamDefaultReader(stream2.getReader(), writable);
314
314
  }
315
315
  var buildOutgoingHttpHeaders = (headers) => {
316
316
  const res = {};
@@ -705,26 +705,26 @@ var handleParsingNestedValues = (form, key, value) => {
705
705
  };
706
706
 
707
707
  // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/utils/url.js
708
- var splitPath = (path5) => {
709
- const paths = path5.split("/");
708
+ var splitPath = (path8) => {
709
+ const paths = path8.split("/");
710
710
  if (paths[0] === "") {
711
711
  paths.shift();
712
712
  }
713
713
  return paths;
714
714
  };
715
715
  var splitRoutingPath = (routePath) => {
716
- const { groups, path: path5 } = extractGroupsFromPath(routePath);
717
- const paths = splitPath(path5);
716
+ const { groups, path: path8 } = extractGroupsFromPath(routePath);
717
+ const paths = splitPath(path8);
718
718
  return replaceGroupMarks(paths, groups);
719
719
  };
720
- var extractGroupsFromPath = (path5) => {
720
+ var extractGroupsFromPath = (path8) => {
721
721
  const groups = [];
722
- path5 = path5.replace(/\{[^}]+\}/g, (match2, index2) => {
722
+ path8 = path8.replace(/\{[^}]+\}/g, (match2, index2) => {
723
723
  const mark = `@${index2}`;
724
724
  groups.push([mark, match2]);
725
725
  return mark;
726
726
  });
727
- return { groups, path: path5 };
727
+ return { groups, path: path8 };
728
728
  };
729
729
  var replaceGroupMarks = (paths, groups) => {
730
730
  for (let i = groups.length - 1; i >= 0; i--) {
@@ -781,8 +781,8 @@ var getPath = (request) => {
781
781
  const queryIndex = url.indexOf("?", i);
782
782
  const hashIndex = url.indexOf("#", i);
783
783
  const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
784
- const path5 = url.slice(start, end);
785
- return tryDecodeURI(path5.includes("%25") ? path5.replace(/%25/g, "%2525") : path5);
784
+ const path8 = url.slice(start, end);
785
+ return tryDecodeURI(path8.includes("%25") ? path8.replace(/%25/g, "%2525") : path8);
786
786
  } else if (charCode === 63 || charCode === 35) {
787
787
  break;
788
788
  }
@@ -799,11 +799,11 @@ var mergePath = (base, sub, ...rest) => {
799
799
  }
800
800
  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
801
801
  };
802
- var checkOptionalParameter = (path5) => {
803
- if (path5.charCodeAt(path5.length - 1) !== 63 || !path5.includes(":")) {
802
+ var checkOptionalParameter = (path8) => {
803
+ if (path8.charCodeAt(path8.length - 1) !== 63 || !path8.includes(":")) {
804
804
  return null;
805
805
  }
806
- const segments = path5.split("/");
806
+ const segments = path8.split("/");
807
807
  const results = [];
808
808
  let basePath = "";
809
809
  segments.forEach((segment) => {
@@ -944,9 +944,9 @@ var HonoRequest = class {
944
944
  */
945
945
  path;
946
946
  bodyCache = {};
947
- constructor(request, path5 = "/", matchResult = [[]]) {
947
+ constructor(request, path8 = "/", matchResult = [[]]) {
948
948
  this.raw = request;
949
- this.path = path5;
949
+ this.path = path8;
950
950
  this.#matchResult = matchResult;
951
951
  this.#validatedData = {};
952
952
  }
@@ -1683,8 +1683,8 @@ var Hono = class _Hono {
1683
1683
  return this;
1684
1684
  };
1685
1685
  });
1686
- this.on = (method, path5, ...handlers) => {
1687
- for (const p of [path5].flat()) {
1686
+ this.on = (method, path8, ...handlers) => {
1687
+ for (const p of [path8].flat()) {
1688
1688
  this.#path = p;
1689
1689
  for (const m of [method].flat()) {
1690
1690
  handlers.map((handler) => {
@@ -1741,8 +1741,8 @@ var Hono = class _Hono {
1741
1741
  * app.route("/api", app2) // GET /api/user
1742
1742
  * ```
1743
1743
  */
1744
- route(path5, app2) {
1745
- const subApp = this.basePath(path5);
1744
+ route(path8, app2) {
1745
+ const subApp = this.basePath(path8);
1746
1746
  app2.routes.map((r) => {
1747
1747
  let handler;
1748
1748
  if (app2.errorHandler === errorHandler) {
@@ -1768,9 +1768,9 @@ var Hono = class _Hono {
1768
1768
  * const api = new Hono().basePath('/api')
1769
1769
  * ```
1770
1770
  */
1771
- basePath(path5) {
1771
+ basePath(path8) {
1772
1772
  const subApp = this.#clone();
1773
- subApp._basePath = mergePath(this._basePath, path5);
1773
+ subApp._basePath = mergePath(this._basePath, path8);
1774
1774
  return subApp;
1775
1775
  }
1776
1776
  /**
@@ -1844,7 +1844,7 @@ var Hono = class _Hono {
1844
1844
  * })
1845
1845
  * ```
1846
1846
  */
1847
- mount(path5, applicationHandler, options) {
1847
+ mount(path8, applicationHandler, options) {
1848
1848
  let replaceRequest;
1849
1849
  let optionHandler;
1850
1850
  if (options) {
@@ -1871,7 +1871,7 @@ var Hono = class _Hono {
1871
1871
  return [c.env, executionContext];
1872
1872
  };
1873
1873
  replaceRequest ||= (() => {
1874
- const mergedPath = mergePath(this._basePath, path5);
1874
+ const mergedPath = mergePath(this._basePath, path8);
1875
1875
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
1876
1876
  return (request) => {
1877
1877
  const url = new URL(request.url);
@@ -1886,14 +1886,14 @@ var Hono = class _Hono {
1886
1886
  }
1887
1887
  await next();
1888
1888
  };
1889
- this.#addRoute(METHOD_NAME_ALL, mergePath(path5, "*"), handler);
1889
+ this.#addRoute(METHOD_NAME_ALL, mergePath(path8, "*"), handler);
1890
1890
  return this;
1891
1891
  }
1892
- #addRoute(method, path5, handler) {
1892
+ #addRoute(method, path8, handler) {
1893
1893
  method = method.toUpperCase();
1894
- path5 = mergePath(this._basePath, path5);
1895
- const r = { basePath: this._basePath, path: path5, method, handler };
1896
- this.router.add(method, path5, [handler, r]);
1894
+ path8 = mergePath(this._basePath, path8);
1895
+ const r = { basePath: this._basePath, path: path8, method, handler };
1896
+ this.router.add(method, path8, [handler, r]);
1897
1897
  this.routes.push(r);
1898
1898
  }
1899
1899
  #handleError(err, c) {
@@ -1906,10 +1906,10 @@ var Hono = class _Hono {
1906
1906
  if (method === "HEAD") {
1907
1907
  return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
1908
1908
  }
1909
- const path5 = this.getPath(request, { env });
1910
- const matchResult = this.router.match(method, path5);
1909
+ const path8 = this.getPath(request, { env });
1910
+ const matchResult = this.router.match(method, path8);
1911
1911
  const c = new Context(request, {
1912
- path: path5,
1912
+ path: path8,
1913
1913
  matchResult,
1914
1914
  env,
1915
1915
  executionCtx,
@@ -2009,7 +2009,7 @@ var Hono = class _Hono {
2009
2009
 
2010
2010
  // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/router/reg-exp-router/matcher.js
2011
2011
  var emptyParam = [];
2012
- function match(method, path5) {
2012
+ function match(method, path8) {
2013
2013
  const matchers = this.buildAllMatchers();
2014
2014
  const match2 = ((method2, path22) => {
2015
2015
  const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
@@ -2025,7 +2025,7 @@ function match(method, path5) {
2025
2025
  return [matcher[1][index2], match3];
2026
2026
  });
2027
2027
  this.match = match2;
2028
- return match2(method, path5);
2028
+ return match2(method, path8);
2029
2029
  }
2030
2030
 
2031
2031
  // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/router/reg-exp-router/node.js
@@ -2140,12 +2140,12 @@ var Node = class _Node {
2140
2140
  var Trie = class {
2141
2141
  #context = { varIndex: 0 };
2142
2142
  #root = new Node();
2143
- insert(path5, index2, pathErrorCheckOnly) {
2143
+ insert(path8, index2, pathErrorCheckOnly) {
2144
2144
  const paramAssoc = [];
2145
2145
  const groups = [];
2146
2146
  for (let i = 0; ; ) {
2147
2147
  let replaced = false;
2148
- path5 = path5.replace(/\{[^}]+\}/g, (m) => {
2148
+ path8 = path8.replace(/\{[^}]+\}/g, (m) => {
2149
2149
  const mark = `@\\${i}`;
2150
2150
  groups[i] = [mark, m];
2151
2151
  i++;
@@ -2156,7 +2156,7 @@ var Trie = class {
2156
2156
  break;
2157
2157
  }
2158
2158
  }
2159
- const tokens = path5.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
2159
+ const tokens = path8.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
2160
2160
  for (let i = groups.length - 1; i >= 0; i--) {
2161
2161
  const [mark] = groups[i];
2162
2162
  for (let j = tokens.length - 1; j >= 0; j--) {
@@ -2195,9 +2195,9 @@ var Trie = class {
2195
2195
  // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/router/reg-exp-router/router.js
2196
2196
  var nullMatcher = [/^$/, [], /* @__PURE__ */ Object.create(null)];
2197
2197
  var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
2198
- function buildWildcardRegExp(path5) {
2199
- return wildcardRegExpCache[path5] ??= new RegExp(
2200
- path5 === "*" ? "" : `^${path5.replace(
2198
+ function buildWildcardRegExp(path8) {
2199
+ return wildcardRegExpCache[path8] ??= new RegExp(
2200
+ path8 === "*" ? "" : `^${path8.replace(
2201
2201
  /\/\*$|([.\\+*[^\]$()])/g,
2202
2202
  (_, metaChar) => metaChar ? `\\${metaChar}` : "(?:|/.*)"
2203
2203
  )}$`
@@ -2219,17 +2219,17 @@ function buildMatcherFromPreprocessedRoutes(routes) {
2219
2219
  );
2220
2220
  const staticMap = /* @__PURE__ */ Object.create(null);
2221
2221
  for (let i = 0, j = -1, len = routesWithStaticPathFlag.length; i < len; i++) {
2222
- const [pathErrorCheckOnly, path5, handlers] = routesWithStaticPathFlag[i];
2222
+ const [pathErrorCheckOnly, path8, handlers] = routesWithStaticPathFlag[i];
2223
2223
  if (pathErrorCheckOnly) {
2224
- staticMap[path5] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
2224
+ staticMap[path8] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
2225
2225
  } else {
2226
2226
  j++;
2227
2227
  }
2228
2228
  let paramAssoc;
2229
2229
  try {
2230
- paramAssoc = trie.insert(path5, j, pathErrorCheckOnly);
2230
+ paramAssoc = trie.insert(path8, j, pathErrorCheckOnly);
2231
2231
  } catch (e) {
2232
- throw e === PATH_ERROR ? new UnsupportedPathError(path5) : e;
2232
+ throw e === PATH_ERROR ? new UnsupportedPathError(path8) : e;
2233
2233
  }
2234
2234
  if (pathErrorCheckOnly) {
2235
2235
  continue;
@@ -2263,12 +2263,12 @@ function buildMatcherFromPreprocessedRoutes(routes) {
2263
2263
  }
2264
2264
  return [regexp, handlerMap, staticMap];
2265
2265
  }
2266
- function findMiddleware(middleware, path5) {
2266
+ function findMiddleware(middleware, path8) {
2267
2267
  if (!middleware) {
2268
2268
  return void 0;
2269
2269
  }
2270
2270
  for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
2271
- if (buildWildcardRegExp(k).test(path5)) {
2271
+ if (buildWildcardRegExp(k).test(path8)) {
2272
2272
  return [...middleware[k]];
2273
2273
  }
2274
2274
  }
@@ -2282,7 +2282,7 @@ var RegExpRouter = class {
2282
2282
  this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
2283
2283
  this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
2284
2284
  }
2285
- add(method, path5, handler) {
2285
+ add(method, path8, handler) {
2286
2286
  const middleware = this.#middleware;
2287
2287
  const routes = this.#routes;
2288
2288
  if (!middleware || !routes) {
@@ -2297,18 +2297,18 @@ var RegExpRouter = class {
2297
2297
  });
2298
2298
  });
2299
2299
  }
2300
- if (path5 === "/*") {
2301
- path5 = "*";
2300
+ if (path8 === "/*") {
2301
+ path8 = "*";
2302
2302
  }
2303
- const paramCount = (path5.match(/\/:/g) || []).length;
2304
- if (/\*$/.test(path5)) {
2305
- const re = buildWildcardRegExp(path5);
2303
+ const paramCount = (path8.match(/\/:/g) || []).length;
2304
+ if (/\*$/.test(path8)) {
2305
+ const re = buildWildcardRegExp(path8);
2306
2306
  if (method === METHOD_NAME_ALL) {
2307
2307
  Object.keys(middleware).forEach((m) => {
2308
- middleware[m][path5] ||= findMiddleware(middleware[m], path5) || findMiddleware(middleware[METHOD_NAME_ALL], path5) || [];
2308
+ middleware[m][path8] ||= findMiddleware(middleware[m], path8) || findMiddleware(middleware[METHOD_NAME_ALL], path8) || [];
2309
2309
  });
2310
2310
  } else {
2311
- middleware[method][path5] ||= findMiddleware(middleware[method], path5) || findMiddleware(middleware[METHOD_NAME_ALL], path5) || [];
2311
+ middleware[method][path8] ||= findMiddleware(middleware[method], path8) || findMiddleware(middleware[METHOD_NAME_ALL], path8) || [];
2312
2312
  }
2313
2313
  Object.keys(middleware).forEach((m) => {
2314
2314
  if (method === METHOD_NAME_ALL || method === m) {
@@ -2326,7 +2326,7 @@ var RegExpRouter = class {
2326
2326
  });
2327
2327
  return;
2328
2328
  }
2329
- const paths = checkOptionalParameter(path5) || [path5];
2329
+ const paths = checkOptionalParameter(path8) || [path8];
2330
2330
  for (let i = 0, len = paths.length; i < len; i++) {
2331
2331
  const path22 = paths[i];
2332
2332
  Object.keys(routes).forEach((m) => {
@@ -2353,13 +2353,13 @@ var RegExpRouter = class {
2353
2353
  const routes = [];
2354
2354
  let hasOwnRoute = method === METHOD_NAME_ALL;
2355
2355
  [this.#middleware, this.#routes].forEach((r) => {
2356
- const ownRoute = r[method] ? Object.keys(r[method]).map((path5) => [path5, r[method][path5]]) : [];
2356
+ const ownRoute = r[method] ? Object.keys(r[method]).map((path8) => [path8, r[method][path8]]) : [];
2357
2357
  if (ownRoute.length !== 0) {
2358
2358
  hasOwnRoute ||= true;
2359
2359
  routes.push(...ownRoute);
2360
2360
  } else if (method !== METHOD_NAME_ALL) {
2361
2361
  routes.push(
2362
- ...Object.keys(r[METHOD_NAME_ALL]).map((path5) => [path5, r[METHOD_NAME_ALL][path5]])
2362
+ ...Object.keys(r[METHOD_NAME_ALL]).map((path8) => [path8, r[METHOD_NAME_ALL][path8]])
2363
2363
  );
2364
2364
  }
2365
2365
  });
@@ -2379,13 +2379,13 @@ var SmartRouter = class {
2379
2379
  constructor(init) {
2380
2380
  this.#routers = init.routers;
2381
2381
  }
2382
- add(method, path5, handler) {
2382
+ add(method, path8, handler) {
2383
2383
  if (!this.#routes) {
2384
2384
  throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
2385
2385
  }
2386
- this.#routes.push([method, path5, handler]);
2386
+ this.#routes.push([method, path8, handler]);
2387
2387
  }
2388
- match(method, path5) {
2388
+ match(method, path8) {
2389
2389
  if (!this.#routes) {
2390
2390
  throw new Error("Fatal error");
2391
2391
  }
@@ -2400,7 +2400,7 @@ var SmartRouter = class {
2400
2400
  for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) {
2401
2401
  router.add(...routes[i2]);
2402
2402
  }
2403
- res = router.match(method, path5);
2403
+ res = router.match(method, path8);
2404
2404
  } catch (e) {
2405
2405
  if (e instanceof UnsupportedPathError) {
2406
2406
  continue;
@@ -2450,10 +2450,10 @@ var Node2 = class _Node2 {
2450
2450
  }
2451
2451
  this.#patterns = [];
2452
2452
  }
2453
- insert(method, path5, handler) {
2453
+ insert(method, path8, handler) {
2454
2454
  this.#order = ++this.#order;
2455
2455
  let curNode = this;
2456
- const parts = splitRoutingPath(path5);
2456
+ const parts = splitRoutingPath(path8);
2457
2457
  const possibleKeys = [];
2458
2458
  for (let i = 0, len = parts.length; i < len; i++) {
2459
2459
  const p = parts[i];
@@ -2502,12 +2502,12 @@ var Node2 = class _Node2 {
2502
2502
  }
2503
2503
  }
2504
2504
  }
2505
- search(method, path5) {
2505
+ search(method, path8) {
2506
2506
  const handlerSets = [];
2507
2507
  this.#params = emptyParams;
2508
2508
  const curNode = this;
2509
2509
  let curNodes = [curNode];
2510
- const parts = splitPath(path5);
2510
+ const parts = splitPath(path8);
2511
2511
  const curNodesQueue = [];
2512
2512
  const len = parts.length;
2513
2513
  let partOffsets = null;
@@ -2549,13 +2549,13 @@ var Node2 = class _Node2 {
2549
2549
  if (matcher instanceof RegExp) {
2550
2550
  if (partOffsets === null) {
2551
2551
  partOffsets = new Array(len);
2552
- let offset = path5[0] === "/" ? 1 : 0;
2552
+ let offset = path8[0] === "/" ? 1 : 0;
2553
2553
  for (let p = 0; p < len; p++) {
2554
2554
  partOffsets[p] = offset;
2555
2555
  offset += parts[p].length + 1;
2556
2556
  }
2557
2557
  }
2558
- const restPathString = path5.substring(partOffsets[i]);
2558
+ const restPathString = path8.substring(partOffsets[i]);
2559
2559
  const m = matcher.exec(restPathString);
2560
2560
  if (m) {
2561
2561
  params[name] = m[0];
@@ -2608,18 +2608,18 @@ var TrieRouter = class {
2608
2608
  constructor() {
2609
2609
  this.#node = new Node2();
2610
2610
  }
2611
- add(method, path5, handler) {
2612
- const results = checkOptionalParameter(path5);
2611
+ add(method, path8, handler) {
2612
+ const results = checkOptionalParameter(path8);
2613
2613
  if (results) {
2614
2614
  for (let i = 0, len = results.length; i < len; i++) {
2615
2615
  this.#node.insert(method, results[i], handler);
2616
2616
  }
2617
2617
  return;
2618
2618
  }
2619
- this.#node.insert(method, path5, handler);
2619
+ this.#node.insert(method, path8, handler);
2620
2620
  }
2621
- match(method, path5) {
2622
- return this.#node.search(method, path5);
2621
+ match(method, path8) {
2622
+ return this.#node.search(method, path8);
2623
2623
  }
2624
2624
  };
2625
2625
 
@@ -2638,6 +2638,155 @@ var Hono2 = class extends Hono {
2638
2638
  }
2639
2639
  };
2640
2640
 
2641
+ // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/utils/stream.js
2642
+ var StreamingApi = class {
2643
+ writer;
2644
+ encoder;
2645
+ writable;
2646
+ abortSubscribers = [];
2647
+ responseReadable;
2648
+ /**
2649
+ * Whether the stream has been aborted.
2650
+ */
2651
+ aborted = false;
2652
+ /**
2653
+ * Whether the stream has been closed normally.
2654
+ */
2655
+ closed = false;
2656
+ constructor(writable, _readable) {
2657
+ this.writable = writable;
2658
+ this.writer = writable.getWriter();
2659
+ this.encoder = new TextEncoder();
2660
+ const reader = _readable.getReader();
2661
+ this.abortSubscribers.push(async () => {
2662
+ await reader.cancel();
2663
+ });
2664
+ this.responseReadable = new ReadableStream({
2665
+ async pull(controller) {
2666
+ const { done, value } = await reader.read();
2667
+ done ? controller.close() : controller.enqueue(value);
2668
+ },
2669
+ cancel: () => {
2670
+ this.abort();
2671
+ }
2672
+ });
2673
+ }
2674
+ async write(input) {
2675
+ try {
2676
+ if (typeof input === "string") {
2677
+ input = this.encoder.encode(input);
2678
+ }
2679
+ await this.writer.write(input);
2680
+ } catch {
2681
+ }
2682
+ return this;
2683
+ }
2684
+ async writeln(input) {
2685
+ await this.write(input + "\n");
2686
+ return this;
2687
+ }
2688
+ sleep(ms) {
2689
+ return new Promise((res) => setTimeout(res, ms));
2690
+ }
2691
+ async close() {
2692
+ try {
2693
+ await this.writer.close();
2694
+ } catch {
2695
+ }
2696
+ this.closed = true;
2697
+ }
2698
+ async pipe(body) {
2699
+ this.writer.releaseLock();
2700
+ await body.pipeTo(this.writable, { preventClose: true });
2701
+ this.writer = this.writable.getWriter();
2702
+ }
2703
+ onAbort(listener) {
2704
+ this.abortSubscribers.push(listener);
2705
+ }
2706
+ /**
2707
+ * Abort the stream.
2708
+ * You can call this method when stream is aborted by external event.
2709
+ */
2710
+ abort() {
2711
+ if (!this.aborted) {
2712
+ this.aborted = true;
2713
+ this.abortSubscribers.forEach((subscriber) => subscriber());
2714
+ }
2715
+ }
2716
+ };
2717
+
2718
+ // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/helper/streaming/utils.js
2719
+ var isOldBunVersion = () => {
2720
+ const version2 = typeof Bun !== "undefined" ? Bun.version : void 0;
2721
+ if (version2 === void 0) {
2722
+ return false;
2723
+ }
2724
+ const result = version2.startsWith("1.1") || version2.startsWith("1.0") || version2.startsWith("0.");
2725
+ isOldBunVersion = () => result;
2726
+ return result;
2727
+ };
2728
+
2729
+ // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/helper/streaming/sse.js
2730
+ var SSEStreamingApi = class extends StreamingApi {
2731
+ constructor(writable, readable) {
2732
+ super(writable, readable);
2733
+ }
2734
+ async writeSSE(message) {
2735
+ const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
2736
+ const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
2737
+ return `data: ${line}`;
2738
+ }).join("\n");
2739
+ for (const key of ["event", "id", "retry"]) {
2740
+ if (message[key] && /[\r\n]/.test(message[key])) {
2741
+ throw new Error(`${key} must not contain "\\r" or "\\n"`);
2742
+ }
2743
+ }
2744
+ const sseData = [
2745
+ message.event && `event: ${message.event}`,
2746
+ dataLines,
2747
+ message.id && `id: ${message.id}`,
2748
+ message.retry && `retry: ${message.retry}`
2749
+ ].filter(Boolean).join("\n") + "\n\n";
2750
+ await this.write(sseData);
2751
+ }
2752
+ };
2753
+ var run = async (stream2, cb, onError) => {
2754
+ try {
2755
+ await cb(stream2);
2756
+ } catch (e) {
2757
+ if (e instanceof Error && onError) {
2758
+ await onError(e, stream2);
2759
+ await stream2.writeSSE({
2760
+ event: "error",
2761
+ data: e.message
2762
+ });
2763
+ } else {
2764
+ console.error(e);
2765
+ }
2766
+ } finally {
2767
+ stream2.close();
2768
+ }
2769
+ };
2770
+ var contextStash = /* @__PURE__ */ new WeakMap();
2771
+ var streamSSE = (c, cb, onError) => {
2772
+ const { readable, writable } = new TransformStream();
2773
+ const stream2 = new SSEStreamingApi(writable, readable);
2774
+ if (isOldBunVersion()) {
2775
+ c.req.raw.signal.addEventListener("abort", () => {
2776
+ if (!stream2.closed) {
2777
+ stream2.abort();
2778
+ }
2779
+ });
2780
+ }
2781
+ contextStash.set(stream2.responseReadable, c);
2782
+ c.header("Transfer-Encoding", "chunked");
2783
+ c.header("Content-Type", "text/event-stream");
2784
+ c.header("Cache-Control", "no-cache");
2785
+ c.header("Connection", "keep-alive");
2786
+ run(stream2, cb, onError);
2787
+ return c.newResponse(stream2.responseReadable);
2788
+ };
2789
+
2641
2790
  // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/utils/mime.js
2642
2791
  var getMimeType = (filename, mimes = baseMimes) => {
2643
2792
  const regexp = /\.([a-zA-Z0-9]+?)$/;
@@ -2728,32 +2877,32 @@ var pr54206Applied = () => {
2728
2877
  return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18;
2729
2878
  };
2730
2879
  var useReadableToWeb = pr54206Applied();
2731
- var createStreamBody = (stream) => {
2880
+ var createStreamBody = (stream2) => {
2732
2881
  if (useReadableToWeb) {
2733
- return Readable2.toWeb(stream);
2882
+ return Readable2.toWeb(stream2);
2734
2883
  }
2735
2884
  const body = new ReadableStream({
2736
2885
  start(controller) {
2737
- stream.on("data", (chunk) => {
2886
+ stream2.on("data", (chunk) => {
2738
2887
  controller.enqueue(chunk);
2739
2888
  });
2740
- stream.on("error", (err) => {
2889
+ stream2.on("error", (err) => {
2741
2890
  controller.error(err);
2742
2891
  });
2743
- stream.on("end", () => {
2892
+ stream2.on("end", () => {
2744
2893
  controller.close();
2745
2894
  });
2746
2895
  },
2747
2896
  cancel() {
2748
- stream.destroy();
2897
+ stream2.destroy();
2749
2898
  }
2750
2899
  });
2751
2900
  return body;
2752
2901
  };
2753
- var getStats = (path5) => {
2902
+ var getStats = (path8) => {
2754
2903
  let stats;
2755
2904
  try {
2756
- stats = statSync(path5);
2905
+ stats = statSync(path8);
2757
2906
  } catch {
2758
2907
  }
2759
2908
  return stats;
@@ -2796,21 +2945,21 @@ var serveStatic = (options = { root: "" }) => {
2796
2945
  return next();
2797
2946
  }
2798
2947
  }
2799
- let path5 = join(
2948
+ let path8 = join(
2800
2949
  root,
2801
2950
  !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename
2802
2951
  );
2803
- let stats = getStats(path5);
2952
+ let stats = getStats(path8);
2804
2953
  if (stats && stats.isDirectory()) {
2805
2954
  const indexFile = options.index ?? "index.html";
2806
- path5 = join(path5, indexFile);
2807
- stats = getStats(path5);
2955
+ path8 = join(path8, indexFile);
2956
+ stats = getStats(path8);
2808
2957
  }
2809
2958
  if (!stats) {
2810
- await options.onNotFound?.(path5, c);
2959
+ await options.onNotFound?.(path8, c);
2811
2960
  return next();
2812
2961
  }
2813
- const mimeType = getMimeType(path5);
2962
+ const mimeType = getMimeType(path8);
2814
2963
  c.header("Content-Type", mimeType || "application/octet-stream");
2815
2964
  if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
2816
2965
  const acceptEncodingSet = new Set(
@@ -2820,12 +2969,12 @@ var serveStatic = (options = { root: "" }) => {
2820
2969
  if (!acceptEncodingSet.has(encoding)) {
2821
2970
  continue;
2822
2971
  }
2823
- const precompressedStats = getStats(path5 + ENCODINGS[encoding]);
2972
+ const precompressedStats = getStats(path8 + ENCODINGS[encoding]);
2824
2973
  if (precompressedStats) {
2825
2974
  c.header("Content-Encoding", encoding);
2826
2975
  c.header("Vary", "Accept-Encoding", { append: true });
2827
2976
  stats = precompressedStats;
2828
- path5 = path5 + ENCODINGS[encoding];
2977
+ path8 = path8 + ENCODINGS[encoding];
2829
2978
  break;
2830
2979
  }
2831
2980
  }
@@ -2839,7 +2988,7 @@ var serveStatic = (options = { root: "" }) => {
2839
2988
  result = c.body(null);
2840
2989
  } else if (!range) {
2841
2990
  c.header("Content-Length", size.toString());
2842
- result = c.body(createStreamBody(createReadStream(path5)), 200);
2991
+ result = c.body(createStreamBody(createReadStream(path8)), 200);
2843
2992
  } else {
2844
2993
  c.header("Accept-Ranges", "bytes");
2845
2994
  c.header("Date", stats.birthtime.toUTCString());
@@ -2850,12 +2999,12 @@ var serveStatic = (options = { root: "" }) => {
2850
2999
  end = size - 1;
2851
3000
  }
2852
3001
  const chunksize = end - start + 1;
2853
- const stream = createReadStream(path5, { start, end });
3002
+ const stream2 = createReadStream(path8, { start, end });
2854
3003
  c.header("Content-Length", chunksize.toString());
2855
3004
  c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
2856
- result = c.body(createStreamBody(stream), 206);
3005
+ result = c.body(createStreamBody(stream2), 206);
2857
3006
  }
2858
- await options.onFound?.(path5, c);
3007
+ await options.onFound?.(path8, c);
2859
3008
  return result;
2860
3009
  };
2861
3010
  };
@@ -2924,6 +3073,165 @@ function loadChatVisibility(metadata2, opts) {
2924
3073
  };
2925
3074
  }
2926
3075
 
3076
+ // src/title.ts
3077
+ function deriveBaseTitle(firstUserText) {
3078
+ const text2 = firstUserText?.trim().split("\n")[0]?.trim();
3079
+ return text2 && text2.length > 0 ? text2 : "Untitled";
3080
+ }
3081
+
3082
+ // src/list-pagination.ts
3083
+ import fs from "fs";
3084
+ import path from "path";
3085
+ import Database from "better-sqlite3";
3086
+
3087
+ // src/list-filter.ts
3088
+ function buildFilterClauses({
3089
+ projects,
3090
+ tags: tags3,
3091
+ tagMode = "all",
3092
+ hasMetadata
3093
+ }) {
3094
+ const clauses = [];
3095
+ const params = [];
3096
+ if (projects && projects.length > 0) {
3097
+ const placeholders = projects.map(() => "?").join(", ");
3098
+ clauses.push(`coalesce(c.project, '') IN (${placeholders})`);
3099
+ params.push(...projects);
3100
+ }
3101
+ if (tags3 && tags3.length > 0) {
3102
+ const realTagIds = tags3.filter((t) => t !== "");
3103
+ const wantUntagged = tags3.includes("");
3104
+ let realFragment = null;
3105
+ if (realTagIds.length > 0) {
3106
+ if (!hasMetadata) {
3107
+ realFragment = "0";
3108
+ } else {
3109
+ const placeholders = realTagIds.map(() => "?").join(", ");
3110
+ if (tagMode === "any") {
3111
+ realFragment = `c.id IN (SELECT DISTINCT chat_id FROM meta.chat_tags
3112
+ WHERE tag_id IN (${placeholders}))`;
3113
+ params.push(...realTagIds);
3114
+ } else {
3115
+ realFragment = `c.id IN (SELECT chat_id FROM meta.chat_tags
3116
+ WHERE tag_id IN (${placeholders})
3117
+ GROUP BY chat_id HAVING count(*) = ?)`;
3118
+ params.push(...realTagIds, realTagIds.length);
3119
+ }
3120
+ }
3121
+ }
3122
+ const untaggedFragment = wantUntagged && hasMetadata ? "c.id NOT IN (SELECT chat_id FROM meta.chat_tags)" : null;
3123
+ if (tagMode === "any" && realFragment && untaggedFragment) {
3124
+ clauses.push(`(${realFragment} OR ${untaggedFragment})`);
3125
+ } else {
3126
+ if (realFragment) clauses.push(realFragment);
3127
+ if (untaggedFragment) clauses.push(untaggedFragment);
3128
+ }
3129
+ }
3130
+ return { clauses, params };
3131
+ }
3132
+
3133
+ // src/list-pagination.ts
3134
+ var ARCHIVE_DB = "archive.db";
3135
+ var METADATA_DB = "metadata.db";
3136
+ var SORT_EXPR = {
3137
+ createdAt: "c.created_at",
3138
+ updatedAt: "c.updated_at",
3139
+ deletedAt: "m.deleted_at",
3140
+ // The Title axis orders by the precomputed collation key on the ATTACHed
3141
+ // `meta.chat_sort_keys` (alias `k`), reached through the INNER JOIN added
3142
+ // below (ADR-0019). BINARY compare over `sort_key` is an index range scan.
3143
+ title: "k.sort_key"
3144
+ };
3145
+ function encodeCursor(cursor) {
3146
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
3147
+ }
3148
+ function decodeCursor(token) {
3149
+ try {
3150
+ const parsed = JSON.parse(
3151
+ Buffer.from(token, "base64url").toString("utf8")
3152
+ );
3153
+ const sortKey = parsed?.sortKey;
3154
+ if (parsed && typeof parsed === "object" && (typeof sortKey === "number" || typeof sortKey === "string") && typeof parsed.id === "string") {
3155
+ return parsed;
3156
+ }
3157
+ return null;
3158
+ } catch {
3159
+ return null;
3160
+ }
3161
+ }
3162
+ function createChatPageQuery({
3163
+ dataDir: dataDir2
3164
+ }) {
3165
+ const archive2 = new Database(path.join(dataDir2, ARCHIVE_DB), {
3166
+ readonly: true
3167
+ });
3168
+ const metadataPath = path.join(dataDir2, METADATA_DB);
3169
+ const hasMetadata = fs.existsSync(metadataPath);
3170
+ if (hasMetadata) {
3171
+ archive2.prepare("ATTACH DATABASE ? AS meta").run(metadataPath);
3172
+ }
3173
+ function queryPage(query) {
3174
+ const sortExpr = SORT_EXPR[query.sort];
3175
+ const needsDeletedJoin = query.sort === "deletedAt";
3176
+ const needsSortKeyJoin = query.sort === "title";
3177
+ if ((needsDeletedJoin || needsSortKeyJoin) && !hasMetadata) {
3178
+ return { items: [], nextCursor: null };
3179
+ }
3180
+ let from = "chats c";
3181
+ if (needsDeletedJoin) from += " JOIN meta.chats_meta m ON m.id = c.id";
3182
+ if (needsSortKeyJoin) from += " JOIN meta.chat_sort_keys k ON k.id = c.id";
3183
+ const idCol = needsDeletedJoin ? "m.id" : needsSortKeyJoin ? "k.id" : "c.id";
3184
+ const clauses = [];
3185
+ const params = [];
3186
+ if (needsDeletedJoin) {
3187
+ clauses.push("m.deleted_at IS NOT NULL");
3188
+ } else if (query.trashedOnly) {
3189
+ if (!hasMetadata) return { items: [], nextCursor: null };
3190
+ clauses.push(
3191
+ "c.id IN (SELECT id FROM meta.chats_meta WHERE is_deleted = 1)"
3192
+ );
3193
+ } else if (!query.includeTrashed && hasMetadata) {
3194
+ clauses.push(
3195
+ "c.id NOT IN (SELECT id FROM meta.chats_meta WHERE is_deleted = 1)"
3196
+ );
3197
+ }
3198
+ const filter = buildFilterClauses({
3199
+ projects: query.projects,
3200
+ tags: query.tags,
3201
+ tagMode: query.tagMode,
3202
+ hasMetadata
3203
+ });
3204
+ clauses.push(...filter.clauses);
3205
+ params.push(...filter.params);
3206
+ const direction = query.direction ?? "desc";
3207
+ const cmp = direction === "asc" ? ">" : "<";
3208
+ const order = direction === "asc" ? "ASC" : "DESC";
3209
+ if (query.cursor) {
3210
+ clauses.push(
3211
+ `(${sortExpr} ${cmp} ? OR (${sortExpr} = ? AND ${idCol} ${cmp} ?))`
3212
+ );
3213
+ params.push(query.cursor.sortKey, query.cursor.sortKey, query.cursor.id);
3214
+ }
3215
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
3216
+ const rows = archive2.prepare(
3217
+ `SELECT c.id AS id, ${sortExpr} AS sortKey
3218
+ FROM ${from} ${where}
3219
+ ORDER BY ${sortExpr} ${order}, ${idCol} ${order}
3220
+ LIMIT ?`
3221
+ ).all(...params, query.limit + 1);
3222
+ const items = rows.slice(0, query.limit);
3223
+ const last = items[items.length - 1];
3224
+ const nextCursor = rows.length > query.limit && last ? { sortKey: last.sortKey, id: last.id } : null;
3225
+ return { items, nextCursor };
3226
+ }
3227
+ return {
3228
+ queryPage,
3229
+ close() {
3230
+ archive2.close();
3231
+ }
3232
+ };
3233
+ }
3234
+
2927
3235
  // src/chat-reader.ts
2928
3236
  function toApiBlock(block) {
2929
3237
  if (block.type === "tool_result") {
@@ -2938,7 +3246,9 @@ function toApiBlock(block) {
2938
3246
  function createChatReader({
2939
3247
  archive: archive2,
2940
3248
  metadata: metadata2,
2941
- tags: tags3
3249
+ tags: tags3,
3250
+ pageQuery: pageQuery2,
3251
+ countsQuery: countsQuery2
2942
3252
  }) {
2943
3253
  function findChat(id) {
2944
3254
  const code = parseChatId(id);
@@ -2947,47 +3257,24 @@ function createChatReader({
2947
3257
  }
2948
3258
  function deriveTitle(customTitle, firstUserText) {
2949
3259
  if (customTitle && customTitle.trim()) return customTitle;
2950
- const text2 = firstUserText?.trim().split("\n")[0]?.trim();
2951
- return text2 && text2.length > 0 ? text2 : "Untitled";
3260
+ return deriveBaseTitle(firstUserText);
2952
3261
  }
2953
3262
  function key(agent, sourceId) {
2954
3263
  return [agent, sourceId].join("\0");
2955
3264
  }
2956
- function listChats({
2957
- includeTrashed,
2958
- projects,
2959
- tags: tagSelection
2960
- }) {
2961
- const visibility = loadChatVisibility(metadata2, { includeTrashed });
2962
- const rows = archive2.read.listChatRows(projects ? { projects } : void 0);
2963
- let passesTagFilter = null;
2964
- if (tagSelection) {
2965
- const realTagIds = tagSelection.filter((t) => t !== "");
2966
- const wantUntagged = tagSelection.includes("");
2967
- const allowedByTags = realTagIds.length > 0 ? new Set(tags3.listChatIdsWithAllTags(realTagIds)) : null;
2968
- const taggedChatIds = wantUntagged ? new Set(tags3.listTagsByChat().keys()) : null;
2969
- passesTagFilter = (chatInternalId) => {
2970
- if (allowedByTags && !allowedByTags.has(chatInternalId)) return false;
2971
- if (taggedChatIds && taggedChatIds.has(chatInternalId)) return false;
2972
- return true;
2973
- };
2974
- }
3265
+ function loadHydration(scope) {
2975
3266
  const tsRangeByKey = new Map(
2976
- archive2.read.listChatTsRanges().map((r) => [key(r.agent, r.sourceId), r])
3267
+ archive2.read.listChatTsRanges(scope).map((r) => [key(r.agent, r.sourceId), r])
2977
3268
  );
2978
3269
  const sourcePathByKey = new Map(
2979
- archive2.read.listLatestRawSourcePaths().map((r) => [key(r.agent, r.sourceId), r.sourcePath])
3270
+ archive2.read.listLatestRawSourcePaths(scope).map((r) => [key(r.agent, r.sourceId), r.sourcePath])
2980
3271
  );
2981
3272
  const firstUserTextByKey = new Map(
2982
- archive2.read.listFirstUserTexts().map((r) => [key(r.agent, r.sourceId), r.text])
3273
+ archive2.read.listFirstUserTexts(scope).map((r) => [key(r.agent, r.sourceId), r.text])
2983
3274
  );
2984
3275
  const customTitleById = metadata2.listCustomTitles();
2985
3276
  const tagsByChatId = tags3.listTagsByChat();
2986
- const chats2 = [];
2987
- for (const row of rows) {
2988
- if (!visibility.isVisible(row.id)) continue;
2989
- if (passesTagFilter && !passesTagFilter(row.id)) continue;
2990
- const isDeleted = visibility.isTrashed(row.id);
3277
+ return (row, visibility) => {
2991
3278
  const rowKey = key(row.agent, row.sourceId);
2992
3279
  const tsRange = tsRangeByKey.get(rowKey);
2993
3280
  const firstSeenAtMs = row.firstSeenAt.getTime();
@@ -3007,10 +3294,78 @@ function createChatReader({
3007
3294
  deletedAt: visibility.deletedAt(row.id),
3008
3295
  tags: tagsByChatId.get(row.id) ?? []
3009
3296
  };
3010
- if (isDeleted) chat.isDeleted = true;
3011
- chats2.push(chat);
3297
+ if (visibility.isTrashed(row.id)) chat.isDeleted = true;
3298
+ return chat;
3299
+ };
3300
+ }
3301
+ function listChatsPage({
3302
+ sort,
3303
+ direction,
3304
+ limit,
3305
+ cursor,
3306
+ includeTrashed = false,
3307
+ trashedOnly = false,
3308
+ projects,
3309
+ tags: tagSelection,
3310
+ tagMode
3311
+ }) {
3312
+ if (!pageQuery2) {
3313
+ throw new Error("listChatsPage requires a pageQuery dependency");
3314
+ }
3315
+ const decoded = cursor ? decodeCursor(cursor) ?? void 0 : void 0;
3316
+ const page = pageQuery2.queryPage({
3317
+ sort,
3318
+ direction,
3319
+ limit,
3320
+ cursor: decoded,
3321
+ includeTrashed,
3322
+ trashedOnly,
3323
+ projects,
3324
+ tags: tagSelection,
3325
+ tagMode
3326
+ });
3327
+ const visibility = loadChatVisibility(metadata2, {
3328
+ includeTrashed: includeTrashed || trashedOnly
3329
+ });
3330
+ const pageIds = page.items.map((item) => item.id);
3331
+ const rows = archive2.read.listChatRowsByIds(pageIds);
3332
+ const rowById = new Map(rows.map((r) => [r.id, r]));
3333
+ const toResponse = loadHydration({
3334
+ sourceIds: rows.map((r) => r.sourceId)
3335
+ });
3336
+ const chats2 = [];
3337
+ for (const item of page.items) {
3338
+ const row = rowById.get(item.id);
3339
+ if (row) chats2.push(toResponse(row, visibility));
3340
+ }
3341
+ return {
3342
+ chats: chats2,
3343
+ nextCursor: page.nextCursor ? encodeCursor(page.nextCursor) : null
3344
+ };
3345
+ }
3346
+ function listCounts({
3347
+ includeTrashed = false
3348
+ }) {
3349
+ if (!countsQuery2) {
3350
+ throw new Error("listCounts requires a countsQuery dependency");
3351
+ }
3352
+ return countsQuery2.queryCounts({ includeTrashed });
3353
+ }
3354
+ function listFilteredTotal({
3355
+ includeTrashed = false,
3356
+ projects,
3357
+ tags: tags4,
3358
+ tagMode
3359
+ }) {
3360
+ if (!countsQuery2) {
3361
+ throw new Error("listFilteredTotal requires a countsQuery dependency");
3012
3362
  }
3013
- return chats2;
3363
+ return countsQuery2.queryFilteredTotal({
3364
+ includeTrashed,
3365
+ projects,
3366
+ tags: tags4,
3367
+ tagMode
3368
+ });
3014
3369
  }
3015
3370
  function getMessages(id, { includeTrashed }) {
3016
3371
  const row = findChat(id);
@@ -3024,23 +3379,126 @@ function createChatReader({
3024
3379
  timestamp: m.ts.toISOString()
3025
3380
  }));
3026
3381
  }
3027
- return { listChats, getMessages, findChat };
3382
+ return {
3383
+ listChatsPage,
3384
+ listCounts,
3385
+ listFilteredTotal,
3386
+ getMessages,
3387
+ findChat
3388
+ };
3028
3389
  }
3029
3390
 
3391
+ // src/list-contract.ts
3392
+ var MAX_PAGE_LIMIT = 200;
3393
+
3030
3394
  // src/app.ts
3031
- function createApp({ archive: archive2, metadata: metadata2, tags: tags3, webDistDir: webDistDir2 }) {
3395
+ var STREAM_HEARTBEAT_MS = 25e3;
3396
+ function createApp({
3397
+ archive: archive2,
3398
+ metadata: metadata2,
3399
+ tags: tags3,
3400
+ pageQuery: pageQuery2,
3401
+ countsQuery: countsQuery2,
3402
+ listEvents: listEvents2,
3403
+ webDistDir: webDistDir2
3404
+ }) {
3032
3405
  const app2 = new Hono2();
3033
- const reader = createChatReader({ archive: archive2, metadata: metadata2, tags: tags3 });
3406
+ const reader = createChatReader({
3407
+ archive: archive2,
3408
+ metadata: metadata2,
3409
+ tags: tags3,
3410
+ pageQuery: pageQuery2,
3411
+ countsQuery: countsQuery2
3412
+ });
3034
3413
  app2.get("/api/chats", (c) => {
3414
+ const includeTrashed = c.req.query("includeTrashed") === "true";
3415
+ const limitParam = c.req.query("limit");
3416
+ if (limitParam === void 0) {
3417
+ return c.json({ error: "Missing limit" }, 400);
3418
+ }
3419
+ if (!pageQuery2) {
3420
+ return c.json({ error: "Pagination is not available" }, 501);
3421
+ }
3422
+ const limit = Number.parseInt(limitParam, 10);
3423
+ if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_PAGE_LIMIT) {
3424
+ return c.json({ error: "Invalid limit" }, 400);
3425
+ }
3426
+ const sort = c.req.query("sort") ?? "updatedAt";
3427
+ if (sort !== "createdAt" && sort !== "updatedAt" && sort !== "deletedAt" && sort !== "title") {
3428
+ return c.json({ error: "Invalid sort" }, 400);
3429
+ }
3430
+ const direction = c.req.query("direction") ?? "desc";
3431
+ if (direction !== "asc" && direction !== "desc") {
3432
+ return c.json({ error: "Invalid direction" }, 400);
3433
+ }
3434
+ const trashedOnly = c.req.query("trashedOnly") === "true";
3435
+ const projects = c.req.queries("project");
3436
+ const tagsParam = c.req.query("tags");
3437
+ const tagSelection = tagsParam === void 0 ? void 0 : tagsParam.split(",");
3438
+ const tagMode = c.req.query("tagMode") ?? "all";
3439
+ if (tagMode !== "all" && tagMode !== "any") {
3440
+ return c.json({ error: "Invalid tagMode" }, 400);
3441
+ }
3442
+ const { chats: chats2, nextCursor } = reader.listChatsPage({
3443
+ sort,
3444
+ direction,
3445
+ limit,
3446
+ cursor: c.req.query("cursor"),
3447
+ includeTrashed,
3448
+ trashedOnly,
3449
+ projects,
3450
+ tags: tagSelection,
3451
+ tagMode
3452
+ });
3453
+ return c.json({ chats: chats2, nextCursor });
3454
+ });
3455
+ app2.get("/api/chats/stream", (c) => {
3456
+ if (!listEvents2) {
3457
+ return c.json({ error: "Live updates are not available" }, 501);
3458
+ }
3459
+ const hub = listEvents2;
3460
+ return streamSSE(c, async (stream2) => {
3461
+ const unsubscribe = hub.subscribe((event) => {
3462
+ void stream2.writeSSE({ event: event.type, data: "" });
3463
+ });
3464
+ await new Promise((resolve3) => {
3465
+ const heartbeat = setInterval(() => {
3466
+ void stream2.writeSSE({ event: "ping", data: "" });
3467
+ }, STREAM_HEARTBEAT_MS);
3468
+ stream2.onAbort(() => {
3469
+ clearInterval(heartbeat);
3470
+ unsubscribe();
3471
+ resolve3();
3472
+ });
3473
+ });
3474
+ });
3475
+ });
3476
+ app2.get("/api/chats/counts", (c) => {
3477
+ if (!countsQuery2) {
3478
+ return c.json({ error: "Counts are not available" }, 501);
3479
+ }
3480
+ const includeTrashed = c.req.query("includeTrashed") === "true";
3481
+ return c.json(reader.listCounts({ includeTrashed }));
3482
+ });
3483
+ app2.get("/api/chats/list-total", (c) => {
3484
+ if (!countsQuery2) {
3485
+ return c.json({ error: "Counts are not available" }, 501);
3486
+ }
3487
+ const includeTrashed = c.req.query("includeTrashed") === "true";
3035
3488
  const projects = c.req.queries("project");
3036
3489
  const tagsParam = c.req.query("tags");
3037
3490
  const tags4 = tagsParam === void 0 ? void 0 : tagsParam.split(",");
3038
- const chats2 = reader.listChats({
3039
- includeTrashed: c.req.query("includeTrashed") === "true",
3491
+ const tagMode = c.req.query("tagMode") ?? "all";
3492
+ if (tagMode !== "all" && tagMode !== "any") {
3493
+ return c.json({ error: "Invalid tagMode" }, 400);
3494
+ }
3495
+ const total = reader.listFilteredTotal({
3496
+ includeTrashed,
3040
3497
  projects,
3041
- tags: tags4
3498
+ tags: tags4,
3499
+ tagMode
3042
3500
  });
3043
- return c.json({ chats: chats2 });
3501
+ return c.json({ total });
3044
3502
  });
3045
3503
  app2.delete("/api/chats/:id", (c) => {
3046
3504
  const row = reader.findChat(c.req.param("id"));
@@ -4491,7 +4949,7 @@ var QueryPromise = class {
4491
4949
  function mapResultRow(columns, row, joinsNotNullableMap) {
4492
4950
  const nullifyMap = {};
4493
4951
  const result = columns.reduce(
4494
- (result2, { path: path5, field }, columnIndex) => {
4952
+ (result2, { path: path8, field }, columnIndex) => {
4495
4953
  let decoder;
4496
4954
  if (is(field, Column)) {
4497
4955
  decoder = field;
@@ -4503,8 +4961,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
4503
4961
  decoder = field.sql.decoder;
4504
4962
  }
4505
4963
  let node = result2;
4506
- for (const [pathChunkIndex, pathChunk] of path5.entries()) {
4507
- if (pathChunkIndex < path5.length - 1) {
4964
+ for (const [pathChunkIndex, pathChunk] of path8.entries()) {
4965
+ if (pathChunkIndex < path8.length - 1) {
4508
4966
  if (!(pathChunk in node)) {
4509
4967
  node[pathChunk] = {};
4510
4968
  }
@@ -4512,8 +4970,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
4512
4970
  } else {
4513
4971
  const rawValue = row[columnIndex];
4514
4972
  const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
4515
- if (joinsNotNullableMap && is(field, Column) && path5.length === 2) {
4516
- const objectName = path5[0];
4973
+ if (joinsNotNullableMap && is(field, Column) && path8.length === 2) {
4974
+ const objectName = path8[0];
4517
4975
  if (!(objectName in nullifyMap)) {
4518
4976
  nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
4519
4977
  } else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
@@ -5073,10 +5531,10 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
5073
5531
  }
5074
5532
 
5075
5533
  // src/storage/openStore.ts
5076
- import fs2 from "fs";
5077
- import path from "path";
5534
+ import fs3 from "fs";
5535
+ import path2 from "path";
5078
5536
  import { fileURLToPath } from "url";
5079
- import Database from "better-sqlite3";
5537
+ import Database2 from "better-sqlite3";
5080
5538
 
5081
5539
  // ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/better-sqlite3/driver.js
5082
5540
  import Client from "better-sqlite3";
@@ -8683,20 +9141,20 @@ function drizzle(...params) {
8683
9141
 
8684
9142
  // ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/migrator.js
8685
9143
  import crypto4 from "crypto";
8686
- import fs from "fs";
9144
+ import fs2 from "fs";
8687
9145
  function readMigrationFiles(config) {
8688
9146
  const migrationFolderTo = config.migrationsFolder;
8689
9147
  const migrationQueries = [];
8690
9148
  const journalPath = `${migrationFolderTo}/meta/_journal.json`;
8691
- if (!fs.existsSync(journalPath)) {
9149
+ if (!fs2.existsSync(journalPath)) {
8692
9150
  throw new Error(`Can't find meta/_journal.json file`);
8693
9151
  }
8694
- const journalAsString = fs.readFileSync(`${migrationFolderTo}/meta/_journal.json`).toString();
9152
+ const journalAsString = fs2.readFileSync(`${migrationFolderTo}/meta/_journal.json`).toString();
8695
9153
  const journal = JSON.parse(journalAsString);
8696
9154
  for (const journalEntry of journal.entries) {
8697
9155
  const migrationPath = `${migrationFolderTo}/${journalEntry.tag}.sql`;
8698
9156
  try {
8699
- const query = fs.readFileSync(`${migrationFolderTo}/${journalEntry.tag}.sql`).toString();
9157
+ const query = fs2.readFileSync(`${migrationFolderTo}/${journalEntry.tag}.sql`).toString();
8700
9158
  const result = query.split("--> statement-breakpoint").map((it) => {
8701
9159
  return it;
8702
9160
  });
@@ -8721,12 +9179,12 @@ function migrate(db, config) {
8721
9179
 
8722
9180
  // src/storage/openStore.ts
8723
9181
  function resolveMigrationsFolder(callerUrl, migrationsSubdir) {
8724
- const here = path.dirname(fileURLToPath(callerUrl));
9182
+ const here = path2.dirname(fileURLToPath(callerUrl));
8725
9183
  const candidates = [
8726
- path.join(here, "../..", migrationsSubdir),
8727
- path.join(here, ".", migrationsSubdir)
9184
+ path2.join(here, "../..", migrationsSubdir),
9185
+ path2.join(here, ".", migrationsSubdir)
8728
9186
  ];
8729
- const found = candidates.find((p) => fs2.existsSync(p));
9187
+ const found = candidates.find((p) => fs3.existsSync(p));
8730
9188
  if (!found) {
8731
9189
  throw new Error(`Could not locate migrations folder "${migrationsSubdir}"`);
8732
9190
  }
@@ -8740,8 +9198,8 @@ function openStore({
8740
9198
  schema
8741
9199
  }) {
8742
9200
  const migrationsFolder = resolveMigrationsFolder(callerUrl, migrationsSubdir);
8743
- fs2.mkdirSync(dataDir2, { recursive: true });
8744
- const sqlite = new Database(path.join(dataDir2, dbFile));
9201
+ fs3.mkdirSync(dataDir2, { recursive: true });
9202
+ const sqlite = new Database2(path2.join(dataDir2, dbFile));
8745
9203
  const db = drizzle(sqlite, { schema });
8746
9204
  migrate(db, { migrationsFolder });
8747
9205
  return { db, sqlite };
@@ -8774,13 +9232,32 @@ var chats = sqliteTable(
8774
9232
  agent: text("agent").notNull(),
8775
9233
  sourceId: text("source_id").notNull(),
8776
9234
  firstSeenAt: integer("first_seen_at", { mode: "timestamp_ms" }).notNull(),
9235
+ // Denormalized "conversation start" = min(messages.ts), kept current at
9236
+ // ingest and backfilled in migration 0010. The createdAt sort pages by this
9237
+ // (not first_seen_at, the ingest time) so the paged order matches the
9238
+ // reader's displayed createdAt (issue #143, reconciling ADR-0017's caveat).
9239
+ // Initialized to first_seen_at for a chat with no messages; since
9240
+ // first_seen_at is the ingest time and so >= every message ts, the running
9241
+ // min over (first_seen_at, message ts) equals min(messages.ts) once any
9242
+ // message exists, and first_seen_at when none — never NULL.
9243
+ createdAt: integer("created_at", { mode: "timestamp_ms" }),
9244
+ // Denormalized "most recent activity" = max(messages.ts), kept current at
9245
+ // ingest and backfilled in migration 0009. Lets the activity sort run as a
9246
+ // keyset index range scan instead of an aggregate that defeats the index
9247
+ // (ADR-0017). Initialized to first_seen_at for a chat with no messages, so
9248
+ // it is never NULL and always agrees with the reader's derived `updatedAt`.
9249
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }),
8777
9250
  project: text("project"),
8778
9251
  projectPath: text("project_path")
8779
9252
  },
8780
9253
  (t) => [
8781
9254
  uniqueIndex("chats_agent_source_idx").on(t.agent, t.sourceId),
8782
9255
  // Backs the server-side Project filter (`WHERE coalesce(project,'') IN …`).
8783
- index("chats_project_idx").on(t.project)
9256
+ index("chats_project_idx").on(t.project),
9257
+ // Covering keyset indexes for the two server-side list sorts (ADR-0017):
9258
+ // (sortKey, id) so the ORDER BY + LIMIT is an index range scan either way.
9259
+ index("chats_created_keyset_idx").on(t.createdAt, t.id),
9260
+ index("chats_updated_keyset_idx").on(t.updatedAt, t.id)
8784
9261
  ]
8785
9262
  );
8786
9263
  var rawMessages = sqliteTable(
@@ -8836,6 +9313,10 @@ function createArchiveReadSeam(db) {
8836
9313
  }
8837
9314
  return db.select().from(chats).all();
8838
9315
  },
9316
+ listChatRowsByIds(ids) {
9317
+ if (ids.length === 0) return [];
9318
+ return db.select().from(chats).where(inArray(chats.id, ids)).all();
9319
+ },
8839
9320
  findChatBySourceId(sourceId) {
8840
9321
  return db.select().from(chats).where(eq(chats.sourceId, sourceId)).get() ?? null;
8841
9322
  },
@@ -8845,15 +9326,17 @@ function createArchiveReadSeam(db) {
8845
9326
  listMessagesByChat(agent, sourceId) {
8846
9327
  return db.select().from(messages).where(and(eq(messages.agent, agent), eq(messages.sourceId, sourceId))).orderBy(asc(messages.ts)).all();
8847
9328
  },
8848
- listChatTsRanges() {
9329
+ listChatTsRanges(opts) {
9330
+ const scope = opts?.sourceIds !== void 0 ? inArray(messages.sourceId, opts.sourceIds) : sql`1 = 1`;
8849
9331
  return db.select({
8850
9332
  agent: messages.agent,
8851
9333
  sourceId: messages.sourceId,
8852
9334
  minTs: sql`min(${messages.ts})`,
8853
9335
  maxTs: sql`max(${messages.ts})`
8854
- }).from(messages).groupBy(messages.agent, messages.sourceId).all();
9336
+ }).from(messages).where(scope).groupBy(messages.agent, messages.sourceId).all();
8855
9337
  },
8856
- listLatestRawSourcePaths() {
9338
+ listLatestRawSourcePaths(opts) {
9339
+ const scope = opts?.sourceIds !== void 0 ? inArray(rawMessages.sourceId, opts.sourceIds) : sql`1 = 1`;
8857
9340
  const ranked = db.select({
8858
9341
  agent: rawMessages.agent,
8859
9342
  sourceId: rawMessages.sourceId,
@@ -8861,14 +9344,18 @@ function createArchiveReadSeam(db) {
8861
9344
  rn: sql`row_number() over (partition by ${rawMessages.agent}, ${rawMessages.sourceId} order by ${rawMessages.ingestedAt} desc)`.as(
8862
9345
  "rn"
8863
9346
  )
8864
- }).from(rawMessages).as("ranked_raw");
9347
+ }).from(rawMessages).where(scope).as("ranked_raw");
8865
9348
  return db.select({
8866
9349
  agent: ranked.agent,
8867
9350
  sourceId: ranked.sourceId,
8868
9351
  sourcePath: ranked.sourcePath
8869
9352
  }).from(ranked).where(eq(ranked.rn, 1)).all();
8870
9353
  },
8871
- listFirstUserTexts() {
9354
+ listFirstUserTexts(opts) {
9355
+ const scope = opts?.sourceIds !== void 0 ? and(
9356
+ eq(messages.role, "user"),
9357
+ inArray(messages.sourceId, opts.sourceIds)
9358
+ ) : eq(messages.role, "user");
8872
9359
  const ranked = db.select({
8873
9360
  agent: messages.agent,
8874
9361
  sourceId: messages.sourceId,
@@ -8876,7 +9363,7 @@ function createArchiveReadSeam(db) {
8876
9363
  rn: sql`row_number() over (partition by ${messages.agent}, ${messages.sourceId} order by ${messages.ts} asc)`.as(
8877
9364
  "rn"
8878
9365
  )
8879
- }).from(messages).where(eq(messages.role, "user")).as("ranked_user");
9366
+ }).from(messages).where(scope).as("ranked_user");
8880
9367
  return db.select({
8881
9368
  agent: ranked.agent,
8882
9369
  sourceId: ranked.sourceId,
@@ -8959,6 +9446,12 @@ function createArchiveRepository({
8959
9446
  agent,
8960
9447
  sourceId,
8961
9448
  firstSeenAt,
9449
+ // Seed both denormalized sort keys to first_seen_at; message upserts
9450
+ // pull createdAt down (min) and push updatedAt up (max) as ts arrive.
9451
+ // first_seen_at is the ingest time and so >= every message ts, making
9452
+ // it a safe upper bound for the running min.
9453
+ createdAt: firstSeenAt,
9454
+ updatedAt: firstSeenAt,
8962
9455
  project: project ?? null,
8963
9456
  projectPath: projectPath ?? null
8964
9457
  }).run();
@@ -8995,6 +9488,10 @@ function createArchiveRepository({
8995
9488
  )
8996
9489
  ).get();
8997
9490
  const ts = parseTs(message.ts);
9491
+ db.update(chats).set({
9492
+ createdAt: sql`min(coalesce(${chats.createdAt}, ${ts.getTime()}), ${ts.getTime()})`,
9493
+ updatedAt: sql`max(coalesce(${chats.updatedAt}, 0), ${ts.getTime()})`
9494
+ }).where(and(eq(chats.agent, agent), eq(chats.sourceId, sourceId))).run();
8998
9495
  if (!existing2) {
8999
9496
  db.insert(messages).values({
9000
9497
  agent,
@@ -9115,26 +9612,53 @@ function createCheckpointRepository({
9115
9612
  }
9116
9613
 
9117
9614
  // src/metadata/repository.ts
9118
- import fs3 from "fs";
9119
- import path2 from "path";
9615
+ import fs4 from "fs";
9616
+ import path3 from "path";
9120
9617
 
9121
9618
  // src/metadata/schema.ts
9122
9619
  var schema_exports3 = {};
9123
9620
  __export(schema_exports3, {
9621
+ chatSortKeys: () => chatSortKeys,
9124
9622
  chatTags: () => chatTags,
9125
9623
  chatsMeta: () => chatsMeta,
9126
9624
  tags: () => tags
9127
9625
  });
9128
- var chatsMeta = sqliteTable("chats_meta", {
9129
- id: text("id").primaryKey(),
9130
- isDeleted: integer("is_deleted", { mode: "boolean" }).notNull().default(false),
9131
- customTitle: text("custom_title"),
9132
- createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
9133
- updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
9134
- // Set when a chat is moved to Trash; null while active. Drives the Trash
9135
- // view's independent "Deleted time" sort axis.
9136
- deletedAt: integer("deleted_at", { mode: "timestamp_ms" })
9137
- });
9626
+ var chatsMeta = sqliteTable(
9627
+ "chats_meta",
9628
+ {
9629
+ id: text("id").primaryKey(),
9630
+ isDeleted: integer("is_deleted", { mode: "boolean" }).notNull().default(false),
9631
+ customTitle: text("custom_title"),
9632
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
9633
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
9634
+ // Set when a chat is moved to Trash; null while active. Drives the Trash
9635
+ // view's independent "Deleted time" sort axis.
9636
+ deletedAt: integer("deleted_at", { mode: "timestamp_ms" })
9637
+ },
9638
+ (table) => [
9639
+ // Covering keyset index for the Trash view's deleted-time axis (#145,
9640
+ // ADR-0017): `(deleted_at, id)` so the page's ORDER BY + LIMIT is an index
9641
+ // range scan, not a full-trash sort. Partial on `deleted_at IS NOT NULL`,
9642
+ // which holds exactly for trashed rows (restore nulls it), so the index
9643
+ // carries only the Trash set and the page query's matching predicate lets
9644
+ // the planner use it.
9645
+ index("chats_meta_deleted_at_idx").on(table.deletedAt, table.id).where(sql`${table.deletedAt} is not null`)
9646
+ ]
9647
+ );
9648
+ var chatSortKeys = sqliteTable(
9649
+ "chat_sort_keys",
9650
+ {
9651
+ id: text("id").primaryKey(),
9652
+ textKey: text("text_key"),
9653
+ sortKey: text("sort_key")
9654
+ },
9655
+ (table) => [
9656
+ // Covering keyset index for the Title axis (ADR-0017/0019): `(sort_key, id)`
9657
+ // so `ORDER BY sort_key, id` + LIMIT is an index range scan either
9658
+ // direction, exactly like the time axes — never a full-table sort.
9659
+ index("chat_sort_keys_sort_key_idx").on(table.sortKey, table.id)
9660
+ ]
9661
+ );
9138
9662
  var tags = sqliteTable("tags", {
9139
9663
  id: text("id").primaryKey(),
9140
9664
  name: text("name").notNull(),
@@ -9154,15 +9678,26 @@ var chatTags = sqliteTable(
9154
9678
  ]
9155
9679
  );
9156
9680
 
9681
+ // src/metadata/title-sort-key.ts
9682
+ function encodeNumericRun(run2) {
9683
+ const core = run2.replace(/^0+(?=\d)/, "");
9684
+ const len = String(core.length);
9685
+ return String(len.length) + len + core;
9686
+ }
9687
+ function computeSortKey(input) {
9688
+ const folded = input.normalize("NFKD").toLowerCase().replace(/\p{M}+/gu, "");
9689
+ return folded.replace(/\d+/g, encodeNumericRun);
9690
+ }
9691
+
9157
9692
  // src/metadata/repository.ts
9158
9693
  var DB_FILE = "metadata.db";
9159
9694
  var LEGACY_DB_FILE = "data.db";
9160
9695
  function migrateLegacyDbFile(dataDir2) {
9161
- const legacy = path2.join(dataDir2, LEGACY_DB_FILE);
9162
- const current = path2.join(dataDir2, DB_FILE);
9163
- if (!fs3.existsSync(legacy)) return;
9164
- if (fs3.existsSync(current)) return;
9165
- fs3.renameSync(legacy, current);
9696
+ const legacy = path3.join(dataDir2, LEGACY_DB_FILE);
9697
+ const current = path3.join(dataDir2, DB_FILE);
9698
+ if (!fs4.existsSync(legacy)) return;
9699
+ if (fs4.existsSync(current)) return;
9700
+ fs4.renameSync(legacy, current);
9166
9701
  }
9167
9702
  var CLAUDE_CODE_AGENT = "claude-code";
9168
9703
  function createMetadataRepository({
@@ -9234,6 +9769,27 @@ function createMetadataRepository({
9234
9769
  target: chatsMeta.id,
9235
9770
  set: { customTitle: title, updatedAt: now }
9236
9771
  }).run();
9772
+ if (title !== null) {
9773
+ const sortKey = computeSortKey(title);
9774
+ db.insert(chatSortKeys).values({ id: internalId, sortKey }).onConflictDoUpdate({ target: chatSortKeys.id, set: { sortKey } }).run();
9775
+ } else {
9776
+ db.update(chatSortKeys).set({ sortKey: sql`${chatSortKeys.textKey}` }).where(eq(chatSortKeys.id, internalId)).run();
9777
+ }
9778
+ },
9779
+ getTitleSortKey(internalId) {
9780
+ const row = db.select({
9781
+ textKey: chatSortKeys.textKey,
9782
+ sortKey: chatSortKeys.sortKey
9783
+ }).from(chatSortKeys).where(eq(chatSortKeys.id, internalId)).get();
9784
+ return row ?? null;
9785
+ },
9786
+ setTitleTextKey(internalId, textKey) {
9787
+ const customTitle = db.select({ customTitle: chatsMeta.customTitle }).from(chatsMeta).where(eq(chatsMeta.id, internalId)).get()?.customTitle;
9788
+ const sortKey = customTitle && customTitle.trim() ? computeSortKey(customTitle) : textKey;
9789
+ db.insert(chatSortKeys).values({ id: internalId, textKey, sortKey }).onConflictDoUpdate({
9790
+ target: chatSortKeys.id,
9791
+ set: { textKey, sortKey }
9792
+ }).run();
9237
9793
  }
9238
9794
  };
9239
9795
  }
@@ -9335,17 +9891,12 @@ function createTagRepository({
9335
9891
  byChat.set(row.chatId, list);
9336
9892
  }
9337
9893
  return byChat;
9338
- },
9339
- listChatIdsWithAllTags(tagIds) {
9340
- const distinct = [...new Set(tagIds)];
9341
- if (distinct.length === 0) return [];
9342
- return db.select({ chatId: chatTags.chatId }).from(chatTags).where(inArray(chatTags.tagId, distinct)).groupBy(chatTags.chatId).having(sql`count(${chatTags.tagId}) = ${distinct.length}`).all().map((r) => r.chatId);
9343
9894
  }
9344
9895
  };
9345
9896
  }
9346
9897
 
9347
9898
  // src/ingestion/ingest.ts
9348
- import fs4 from "fs";
9899
+ import fs5 from "fs";
9349
9900
  async function runIngestion(opts) {
9350
9901
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
9351
9902
  const result = {
@@ -9405,7 +9956,7 @@ async function runIngestion(opts) {
9405
9956
  }
9406
9957
  function safeStat(p) {
9407
9958
  try {
9408
- return fs4.statSync(p);
9959
+ return fs5.statSync(p);
9409
9960
  } catch {
9410
9961
  return null;
9411
9962
  }
@@ -9514,7 +10065,7 @@ var ReaddirpStream = class extends Readable3 {
9514
10065
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
9515
10066
  const statMethod = opts.lstat ? lstat : stat;
9516
10067
  if (wantBigintFsStats) {
9517
- this._stat = (path5) => statMethod(path5, { bigint: true });
10068
+ this._stat = (path8) => statMethod(path8, { bigint: true });
9518
10069
  } else {
9519
10070
  this._stat = statMethod;
9520
10071
  }
@@ -9539,8 +10090,8 @@ var ReaddirpStream = class extends Readable3 {
9539
10090
  const par = this.parent;
9540
10091
  const fil = par && par.files;
9541
10092
  if (fil && fil.length > 0) {
9542
- const { path: path5, depth } = par;
9543
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path5));
10093
+ const { path: path8, depth } = par;
10094
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path8));
9544
10095
  const awaited = await Promise.all(slice);
9545
10096
  for (const entry of awaited) {
9546
10097
  if (!entry)
@@ -9580,20 +10131,20 @@ var ReaddirpStream = class extends Readable3 {
9580
10131
  this.reading = false;
9581
10132
  }
9582
10133
  }
9583
- async _exploreDir(path5, depth) {
10134
+ async _exploreDir(path8, depth) {
9584
10135
  let files;
9585
10136
  try {
9586
- files = await readdir(path5, this._rdOptions);
10137
+ files = await readdir(path8, this._rdOptions);
9587
10138
  } catch (error) {
9588
10139
  this._onError(error);
9589
10140
  }
9590
- return { files, depth, path: path5 };
10141
+ return { files, depth, path: path8 };
9591
10142
  }
9592
- async _formatEntry(dirent, path5) {
10143
+ async _formatEntry(dirent, path8) {
9593
10144
  let entry;
9594
10145
  const basename3 = this._isDirent ? dirent.name : dirent;
9595
10146
  try {
9596
- const fullPath = presolve(pjoin(path5, basename3));
10147
+ const fullPath = presolve(pjoin(path8, basename3));
9597
10148
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
9598
10149
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
9599
10150
  } catch (err) {
@@ -9993,16 +10544,16 @@ var delFromSet = (main, prop, item) => {
9993
10544
  };
9994
10545
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
9995
10546
  var FsWatchInstances = /* @__PURE__ */ new Map();
9996
- function createFsWatchInstance(path5, options, listener, errHandler, emitRaw) {
10547
+ function createFsWatchInstance(path8, options, listener, errHandler, emitRaw) {
9997
10548
  const handleEvent = (rawEvent, evPath) => {
9998
- listener(path5);
9999
- emitRaw(rawEvent, evPath, { watchedPath: path5 });
10000
- if (evPath && path5 !== evPath) {
10001
- fsWatchBroadcast(sp.resolve(path5, evPath), KEY_LISTENERS, sp.join(path5, evPath));
10549
+ listener(path8);
10550
+ emitRaw(rawEvent, evPath, { watchedPath: path8 });
10551
+ if (evPath && path8 !== evPath) {
10552
+ fsWatchBroadcast(sp.resolve(path8, evPath), KEY_LISTENERS, sp.join(path8, evPath));
10002
10553
  }
10003
10554
  };
10004
10555
  try {
10005
- return fs_watch(path5, {
10556
+ return fs_watch(path8, {
10006
10557
  persistent: options.persistent
10007
10558
  }, handleEvent);
10008
10559
  } catch (error) {
@@ -10018,12 +10569,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
10018
10569
  listener(val1, val2, val3);
10019
10570
  });
10020
10571
  };
10021
- var setFsWatchListener = (path5, fullPath, options, handlers) => {
10572
+ var setFsWatchListener = (path8, fullPath, options, handlers) => {
10022
10573
  const { listener, errHandler, rawEmitter } = handlers;
10023
10574
  let cont = FsWatchInstances.get(fullPath);
10024
10575
  let watcher2;
10025
10576
  if (!options.persistent) {
10026
- watcher2 = createFsWatchInstance(path5, options, listener, errHandler, rawEmitter);
10577
+ watcher2 = createFsWatchInstance(path8, options, listener, errHandler, rawEmitter);
10027
10578
  if (!watcher2)
10028
10579
  return;
10029
10580
  return watcher2.close.bind(watcher2);
@@ -10034,7 +10585,7 @@ var setFsWatchListener = (path5, fullPath, options, handlers) => {
10034
10585
  addAndConvert(cont, KEY_RAW, rawEmitter);
10035
10586
  } else {
10036
10587
  watcher2 = createFsWatchInstance(
10037
- path5,
10588
+ path8,
10038
10589
  options,
10039
10590
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
10040
10591
  errHandler,
@@ -10049,7 +10600,7 @@ var setFsWatchListener = (path5, fullPath, options, handlers) => {
10049
10600
  cont.watcherUnusable = true;
10050
10601
  if (isWindows && error.code === "EPERM") {
10051
10602
  try {
10052
- const fd = await open(path5, "r");
10603
+ const fd = await open(path8, "r");
10053
10604
  await fd.close();
10054
10605
  broadcastErr(error);
10055
10606
  } catch (err) {
@@ -10080,7 +10631,7 @@ var setFsWatchListener = (path5, fullPath, options, handlers) => {
10080
10631
  };
10081
10632
  };
10082
10633
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
10083
- var setFsWatchFileListener = (path5, fullPath, options, handlers) => {
10634
+ var setFsWatchFileListener = (path8, fullPath, options, handlers) => {
10084
10635
  const { listener, rawEmitter } = handlers;
10085
10636
  let cont = FsWatchFileInstances.get(fullPath);
10086
10637
  const copts = cont && cont.options;
@@ -10102,7 +10653,7 @@ var setFsWatchFileListener = (path5, fullPath, options, handlers) => {
10102
10653
  });
10103
10654
  const currmtime = curr.mtimeMs;
10104
10655
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
10105
- foreach(cont.listeners, (listener2) => listener2(path5, curr));
10656
+ foreach(cont.listeners, (listener2) => listener2(path8, curr));
10106
10657
  }
10107
10658
  })
10108
10659
  };
@@ -10132,13 +10683,13 @@ var NodeFsHandler = class {
10132
10683
  * @param listener on fs change
10133
10684
  * @returns closer for the watcher instance
10134
10685
  */
10135
- _watchWithNodeFs(path5, listener) {
10686
+ _watchWithNodeFs(path8, listener) {
10136
10687
  const opts = this.fsw.options;
10137
- const directory = sp.dirname(path5);
10138
- const basename3 = sp.basename(path5);
10688
+ const directory = sp.dirname(path8);
10689
+ const basename3 = sp.basename(path8);
10139
10690
  const parent = this.fsw._getWatchedDir(directory);
10140
10691
  parent.add(basename3);
10141
- const absolutePath = sp.resolve(path5);
10692
+ const absolutePath = sp.resolve(path8);
10142
10693
  const options = {
10143
10694
  persistent: opts.persistent
10144
10695
  };
@@ -10148,12 +10699,12 @@ var NodeFsHandler = class {
10148
10699
  if (opts.usePolling) {
10149
10700
  const enableBin = opts.interval !== opts.binaryInterval;
10150
10701
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
10151
- closer = setFsWatchFileListener(path5, absolutePath, options, {
10702
+ closer = setFsWatchFileListener(path8, absolutePath, options, {
10152
10703
  listener,
10153
10704
  rawEmitter: this.fsw._emitRaw
10154
10705
  });
10155
10706
  } else {
10156
- closer = setFsWatchListener(path5, absolutePath, options, {
10707
+ closer = setFsWatchListener(path8, absolutePath, options, {
10157
10708
  listener,
10158
10709
  errHandler: this._boundHandleError,
10159
10710
  rawEmitter: this.fsw._emitRaw
@@ -10175,7 +10726,7 @@ var NodeFsHandler = class {
10175
10726
  let prevStats = stats;
10176
10727
  if (parent.has(basename3))
10177
10728
  return;
10178
- const listener = async (path5, newStats) => {
10729
+ const listener = async (path8, newStats) => {
10179
10730
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
10180
10731
  return;
10181
10732
  if (!newStats || newStats.mtimeMs === 0) {
@@ -10189,11 +10740,11 @@ var NodeFsHandler = class {
10189
10740
  this.fsw._emit(EV.CHANGE, file, newStats2);
10190
10741
  }
10191
10742
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
10192
- this.fsw._closeFile(path5);
10743
+ this.fsw._closeFile(path8);
10193
10744
  prevStats = newStats2;
10194
10745
  const closer2 = this._watchWithNodeFs(file, listener);
10195
10746
  if (closer2)
10196
- this.fsw._addPathCloser(path5, closer2);
10747
+ this.fsw._addPathCloser(path8, closer2);
10197
10748
  } else {
10198
10749
  prevStats = newStats2;
10199
10750
  }
@@ -10225,7 +10776,7 @@ var NodeFsHandler = class {
10225
10776
  * @param item basename of this item
10226
10777
  * @returns true if no more processing is needed for this entry.
10227
10778
  */
10228
- async _handleSymlink(entry, directory, path5, item) {
10779
+ async _handleSymlink(entry, directory, path8, item) {
10229
10780
  if (this.fsw.closed) {
10230
10781
  return;
10231
10782
  }
@@ -10235,7 +10786,7 @@ var NodeFsHandler = class {
10235
10786
  this.fsw._incrReadyCount();
10236
10787
  let linkPath;
10237
10788
  try {
10238
- linkPath = await fsrealpath(path5);
10789
+ linkPath = await fsrealpath(path8);
10239
10790
  } catch (e) {
10240
10791
  this.fsw._emitReady();
10241
10792
  return true;
@@ -10245,12 +10796,12 @@ var NodeFsHandler = class {
10245
10796
  if (dir.has(item)) {
10246
10797
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
10247
10798
  this.fsw._symlinkPaths.set(full, linkPath);
10248
- this.fsw._emit(EV.CHANGE, path5, entry.stats);
10799
+ this.fsw._emit(EV.CHANGE, path8, entry.stats);
10249
10800
  }
10250
10801
  } else {
10251
10802
  dir.add(item);
10252
10803
  this.fsw._symlinkPaths.set(full, linkPath);
10253
- this.fsw._emit(EV.ADD, path5, entry.stats);
10804
+ this.fsw._emit(EV.ADD, path8, entry.stats);
10254
10805
  }
10255
10806
  this.fsw._emitReady();
10256
10807
  return true;
@@ -10268,39 +10819,39 @@ var NodeFsHandler = class {
10268
10819
  return;
10269
10820
  const previous = this.fsw._getWatchedDir(wh.path);
10270
10821
  const current = /* @__PURE__ */ new Set();
10271
- let stream = this.fsw._readdirp(directory, {
10822
+ let stream2 = this.fsw._readdirp(directory, {
10272
10823
  fileFilter: (entry) => wh.filterPath(entry),
10273
10824
  directoryFilter: (entry) => wh.filterDir(entry)
10274
10825
  });
10275
- if (!stream)
10826
+ if (!stream2)
10276
10827
  return;
10277
- stream.on(STR_DATA, async (entry) => {
10828
+ stream2.on(STR_DATA, async (entry) => {
10278
10829
  if (this.fsw.closed) {
10279
- stream = void 0;
10830
+ stream2 = void 0;
10280
10831
  return;
10281
10832
  }
10282
10833
  const item = entry.path;
10283
- let path5 = sp.join(directory, item);
10834
+ let path8 = sp.join(directory, item);
10284
10835
  current.add(item);
10285
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path5, item)) {
10836
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path8, item)) {
10286
10837
  return;
10287
10838
  }
10288
10839
  if (this.fsw.closed) {
10289
- stream = void 0;
10840
+ stream2 = void 0;
10290
10841
  return;
10291
10842
  }
10292
10843
  if (item === target || !target && !previous.has(item)) {
10293
10844
  this.fsw._incrReadyCount();
10294
- path5 = sp.join(dir, sp.relative(dir, path5));
10295
- this._addToNodeFs(path5, initialAdd, wh, depth + 1);
10845
+ path8 = sp.join(dir, sp.relative(dir, path8));
10846
+ this._addToNodeFs(path8, initialAdd, wh, depth + 1);
10296
10847
  }
10297
10848
  }).on(EV.ERROR, this._boundHandleError);
10298
10849
  return new Promise((resolve3, reject) => {
10299
- if (!stream)
10850
+ if (!stream2)
10300
10851
  return reject();
10301
- stream.once(STR_END, () => {
10852
+ stream2.once(STR_END, () => {
10302
10853
  if (this.fsw.closed) {
10303
- stream = void 0;
10854
+ stream2 = void 0;
10304
10855
  return;
10305
10856
  }
10306
10857
  const wasThrottled = throttler ? throttler.clear() : false;
@@ -10310,7 +10861,7 @@ var NodeFsHandler = class {
10310
10861
  }).forEach((item) => {
10311
10862
  this.fsw._remove(directory, item);
10312
10863
  });
10313
- stream = void 0;
10864
+ stream2 = void 0;
10314
10865
  if (wasThrottled)
10315
10866
  this._handleRead(directory, false, wh, target, dir, depth, throttler);
10316
10867
  });
@@ -10361,13 +10912,13 @@ var NodeFsHandler = class {
10361
10912
  * @param depth Child path actually targeted for watch
10362
10913
  * @param target Child path actually targeted for watch
10363
10914
  */
10364
- async _addToNodeFs(path5, initialAdd, priorWh, depth, target) {
10915
+ async _addToNodeFs(path8, initialAdd, priorWh, depth, target) {
10365
10916
  const ready = this.fsw._emitReady;
10366
- if (this.fsw._isIgnored(path5) || this.fsw.closed) {
10917
+ if (this.fsw._isIgnored(path8) || this.fsw.closed) {
10367
10918
  ready();
10368
10919
  return false;
10369
10920
  }
10370
- const wh = this.fsw._getWatchHelpers(path5);
10921
+ const wh = this.fsw._getWatchHelpers(path8);
10371
10922
  if (priorWh) {
10372
10923
  wh.filterPath = (entry) => priorWh.filterPath(entry);
10373
10924
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -10383,8 +10934,8 @@ var NodeFsHandler = class {
10383
10934
  const follow = this.fsw.options.followSymlinks;
10384
10935
  let closer;
10385
10936
  if (stats.isDirectory()) {
10386
- const absPath = sp.resolve(path5);
10387
- const targetPath = follow ? await fsrealpath(path5) : path5;
10937
+ const absPath = sp.resolve(path8);
10938
+ const targetPath = follow ? await fsrealpath(path8) : path8;
10388
10939
  if (this.fsw.closed)
10389
10940
  return;
10390
10941
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -10394,29 +10945,29 @@ var NodeFsHandler = class {
10394
10945
  this.fsw._symlinkPaths.set(absPath, targetPath);
10395
10946
  }
10396
10947
  } else if (stats.isSymbolicLink()) {
10397
- const targetPath = follow ? await fsrealpath(path5) : path5;
10948
+ const targetPath = follow ? await fsrealpath(path8) : path8;
10398
10949
  if (this.fsw.closed)
10399
10950
  return;
10400
10951
  const parent = sp.dirname(wh.watchPath);
10401
10952
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
10402
10953
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
10403
- closer = await this._handleDir(parent, stats, initialAdd, depth, path5, wh, targetPath);
10954
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path8, wh, targetPath);
10404
10955
  if (this.fsw.closed)
10405
10956
  return;
10406
10957
  if (targetPath !== void 0) {
10407
- this.fsw._symlinkPaths.set(sp.resolve(path5), targetPath);
10958
+ this.fsw._symlinkPaths.set(sp.resolve(path8), targetPath);
10408
10959
  }
10409
10960
  } else {
10410
10961
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
10411
10962
  }
10412
10963
  ready();
10413
10964
  if (closer)
10414
- this.fsw._addPathCloser(path5, closer);
10965
+ this.fsw._addPathCloser(path8, closer);
10415
10966
  return false;
10416
10967
  } catch (error) {
10417
10968
  if (this.fsw._handleError(error)) {
10418
10969
  ready();
10419
- return path5;
10970
+ return path8;
10420
10971
  }
10421
10972
  }
10422
10973
  }
@@ -10459,24 +11010,24 @@ function createPattern(matcher) {
10459
11010
  }
10460
11011
  return () => false;
10461
11012
  }
10462
- function normalizePath(path5) {
10463
- if (typeof path5 !== "string")
11013
+ function normalizePath(path8) {
11014
+ if (typeof path8 !== "string")
10464
11015
  throw new Error("string expected");
10465
- path5 = sp2.normalize(path5);
10466
- path5 = path5.replace(/\\/g, "/");
11016
+ path8 = sp2.normalize(path8);
11017
+ path8 = path8.replace(/\\/g, "/");
10467
11018
  let prepend = false;
10468
- if (path5.startsWith("//"))
11019
+ if (path8.startsWith("//"))
10469
11020
  prepend = true;
10470
- path5 = path5.replace(DOUBLE_SLASH_RE, "/");
11021
+ path8 = path8.replace(DOUBLE_SLASH_RE, "/");
10471
11022
  if (prepend)
10472
- path5 = "/" + path5;
10473
- return path5;
11023
+ path8 = "/" + path8;
11024
+ return path8;
10474
11025
  }
10475
11026
  function matchPatterns(patterns, testString, stats) {
10476
- const path5 = normalizePath(testString);
11027
+ const path8 = normalizePath(testString);
10477
11028
  for (let index2 = 0; index2 < patterns.length; index2++) {
10478
11029
  const pattern = patterns[index2];
10479
- if (pattern(path5, stats)) {
11030
+ if (pattern(path8, stats)) {
10480
11031
  return true;
10481
11032
  }
10482
11033
  }
@@ -10514,19 +11065,19 @@ var toUnix = (string) => {
10514
11065
  }
10515
11066
  return str;
10516
11067
  };
10517
- var normalizePathToUnix = (path5) => toUnix(sp2.normalize(toUnix(path5)));
10518
- var normalizeIgnored = (cwd = "") => (path5) => {
10519
- if (typeof path5 === "string") {
10520
- return normalizePathToUnix(sp2.isAbsolute(path5) ? path5 : sp2.join(cwd, path5));
11068
+ var normalizePathToUnix = (path8) => toUnix(sp2.normalize(toUnix(path8)));
11069
+ var normalizeIgnored = (cwd = "") => (path8) => {
11070
+ if (typeof path8 === "string") {
11071
+ return normalizePathToUnix(sp2.isAbsolute(path8) ? path8 : sp2.join(cwd, path8));
10521
11072
  } else {
10522
- return path5;
11073
+ return path8;
10523
11074
  }
10524
11075
  };
10525
- var getAbsolutePath = (path5, cwd) => {
10526
- if (sp2.isAbsolute(path5)) {
10527
- return path5;
11076
+ var getAbsolutePath = (path8, cwd) => {
11077
+ if (sp2.isAbsolute(path8)) {
11078
+ return path8;
10528
11079
  }
10529
- return sp2.join(cwd, path5);
11080
+ return sp2.join(cwd, path8);
10530
11081
  };
10531
11082
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
10532
11083
  var DirEntry = class {
@@ -10591,10 +11142,10 @@ var WatchHelper = class {
10591
11142
  dirParts;
10592
11143
  followSymlinks;
10593
11144
  statMethod;
10594
- constructor(path5, follow, fsw) {
11145
+ constructor(path8, follow, fsw) {
10595
11146
  this.fsw = fsw;
10596
- const watchPath = path5;
10597
- this.path = path5 = path5.replace(REPLACER_RE, "");
11147
+ const watchPath = path8;
11148
+ this.path = path8 = path8.replace(REPLACER_RE, "");
10598
11149
  this.watchPath = watchPath;
10599
11150
  this.fullWatchPath = sp2.resolve(watchPath);
10600
11151
  this.dirParts = [];
@@ -10734,20 +11285,20 @@ var FSWatcher = class extends EventEmitter {
10734
11285
  this._closePromise = void 0;
10735
11286
  let paths = unifyPaths(paths_);
10736
11287
  if (cwd) {
10737
- paths = paths.map((path5) => {
10738
- const absPath = getAbsolutePath(path5, cwd);
11288
+ paths = paths.map((path8) => {
11289
+ const absPath = getAbsolutePath(path8, cwd);
10739
11290
  return absPath;
10740
11291
  });
10741
11292
  }
10742
- paths.forEach((path5) => {
10743
- this._removeIgnoredPath(path5);
11293
+ paths.forEach((path8) => {
11294
+ this._removeIgnoredPath(path8);
10744
11295
  });
10745
11296
  this._userIgnored = void 0;
10746
11297
  if (!this._readyCount)
10747
11298
  this._readyCount = 0;
10748
11299
  this._readyCount += paths.length;
10749
- Promise.all(paths.map(async (path5) => {
10750
- const res = await this._nodeFsHandler._addToNodeFs(path5, !_internal, void 0, 0, _origAdd);
11300
+ Promise.all(paths.map(async (path8) => {
11301
+ const res = await this._nodeFsHandler._addToNodeFs(path8, !_internal, void 0, 0, _origAdd);
10751
11302
  if (res)
10752
11303
  this._emitReady();
10753
11304
  return res;
@@ -10769,17 +11320,17 @@ var FSWatcher = class extends EventEmitter {
10769
11320
  return this;
10770
11321
  const paths = unifyPaths(paths_);
10771
11322
  const { cwd } = this.options;
10772
- paths.forEach((path5) => {
10773
- if (!sp2.isAbsolute(path5) && !this._closers.has(path5)) {
11323
+ paths.forEach((path8) => {
11324
+ if (!sp2.isAbsolute(path8) && !this._closers.has(path8)) {
10774
11325
  if (cwd)
10775
- path5 = sp2.join(cwd, path5);
10776
- path5 = sp2.resolve(path5);
11326
+ path8 = sp2.join(cwd, path8);
11327
+ path8 = sp2.resolve(path8);
10777
11328
  }
10778
- this._closePath(path5);
10779
- this._addIgnoredPath(path5);
10780
- if (this._watched.has(path5)) {
11329
+ this._closePath(path8);
11330
+ this._addIgnoredPath(path8);
11331
+ if (this._watched.has(path8)) {
10781
11332
  this._addIgnoredPath({
10782
- path: path5,
11333
+ path: path8,
10783
11334
  recursive: true
10784
11335
  });
10785
11336
  }
@@ -10802,7 +11353,7 @@ var FSWatcher = class extends EventEmitter {
10802
11353
  if (promise instanceof Promise)
10803
11354
  closers.push(promise);
10804
11355
  }));
10805
- this._streams.forEach((stream) => stream.destroy());
11356
+ this._streams.forEach((stream2) => stream2.destroy());
10806
11357
  this._userIgnored = void 0;
10807
11358
  this._readyCount = 0;
10808
11359
  this._readyEmitted = false;
@@ -10843,38 +11394,38 @@ var FSWatcher = class extends EventEmitter {
10843
11394
  * @param stats arguments to be passed with event
10844
11395
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
10845
11396
  */
10846
- async _emit(event, path5, stats) {
11397
+ async _emit(event, path8, stats) {
10847
11398
  if (this.closed)
10848
11399
  return;
10849
11400
  const opts = this.options;
10850
11401
  if (isWindows)
10851
- path5 = sp2.normalize(path5);
11402
+ path8 = sp2.normalize(path8);
10852
11403
  if (opts.cwd)
10853
- path5 = sp2.relative(opts.cwd, path5);
10854
- const args = [path5];
11404
+ path8 = sp2.relative(opts.cwd, path8);
11405
+ const args = [path8];
10855
11406
  if (stats != null)
10856
11407
  args.push(stats);
10857
11408
  const awf = opts.awaitWriteFinish;
10858
11409
  let pw;
10859
- if (awf && (pw = this._pendingWrites.get(path5))) {
11410
+ if (awf && (pw = this._pendingWrites.get(path8))) {
10860
11411
  pw.lastChange = /* @__PURE__ */ new Date();
10861
11412
  return this;
10862
11413
  }
10863
11414
  if (opts.atomic) {
10864
11415
  if (event === EVENTS.UNLINK) {
10865
- this._pendingUnlinks.set(path5, [event, ...args]);
11416
+ this._pendingUnlinks.set(path8, [event, ...args]);
10866
11417
  setTimeout(() => {
10867
- this._pendingUnlinks.forEach((entry, path6) => {
11418
+ this._pendingUnlinks.forEach((entry, path9) => {
10868
11419
  this.emit(...entry);
10869
11420
  this.emit(EVENTS.ALL, ...entry);
10870
- this._pendingUnlinks.delete(path6);
11421
+ this._pendingUnlinks.delete(path9);
10871
11422
  });
10872
11423
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
10873
11424
  return this;
10874
11425
  }
10875
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path5)) {
11426
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path8)) {
10876
11427
  event = EVENTS.CHANGE;
10877
- this._pendingUnlinks.delete(path5);
11428
+ this._pendingUnlinks.delete(path8);
10878
11429
  }
10879
11430
  }
10880
11431
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -10892,16 +11443,16 @@ var FSWatcher = class extends EventEmitter {
10892
11443
  this.emitWithAll(event, args);
10893
11444
  }
10894
11445
  };
10895
- this._awaitWriteFinish(path5, awf.stabilityThreshold, event, awfEmit);
11446
+ this._awaitWriteFinish(path8, awf.stabilityThreshold, event, awfEmit);
10896
11447
  return this;
10897
11448
  }
10898
11449
  if (event === EVENTS.CHANGE) {
10899
- const isThrottled = !this._throttle(EVENTS.CHANGE, path5, 50);
11450
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path8, 50);
10900
11451
  if (isThrottled)
10901
11452
  return this;
10902
11453
  }
10903
11454
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
10904
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path5) : path5;
11455
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path8) : path8;
10905
11456
  let stats2;
10906
11457
  try {
10907
11458
  stats2 = await stat3(fullPath);
@@ -10932,23 +11483,23 @@ var FSWatcher = class extends EventEmitter {
10932
11483
  * @param timeout duration of time to suppress duplicate actions
10933
11484
  * @returns tracking object or false if action should be suppressed
10934
11485
  */
10935
- _throttle(actionType, path5, timeout) {
11486
+ _throttle(actionType, path8, timeout) {
10936
11487
  if (!this._throttled.has(actionType)) {
10937
11488
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
10938
11489
  }
10939
11490
  const action2 = this._throttled.get(actionType);
10940
11491
  if (!action2)
10941
11492
  throw new Error("invalid throttle");
10942
- const actionPath = action2.get(path5);
11493
+ const actionPath = action2.get(path8);
10943
11494
  if (actionPath) {
10944
11495
  actionPath.count++;
10945
11496
  return false;
10946
11497
  }
10947
11498
  let timeoutObject;
10948
11499
  const clear = () => {
10949
- const item = action2.get(path5);
11500
+ const item = action2.get(path8);
10950
11501
  const count = item ? item.count : 0;
10951
- action2.delete(path5);
11502
+ action2.delete(path8);
10952
11503
  clearTimeout(timeoutObject);
10953
11504
  if (item)
10954
11505
  clearTimeout(item.timeoutObject);
@@ -10956,7 +11507,7 @@ var FSWatcher = class extends EventEmitter {
10956
11507
  };
10957
11508
  timeoutObject = setTimeout(clear, timeout);
10958
11509
  const thr = { timeoutObject, clear, count: 0 };
10959
- action2.set(path5, thr);
11510
+ action2.set(path8, thr);
10960
11511
  return thr;
10961
11512
  }
10962
11513
  _incrReadyCount() {
@@ -10970,44 +11521,44 @@ var FSWatcher = class extends EventEmitter {
10970
11521
  * @param event
10971
11522
  * @param awfEmit Callback to be called when ready for event to be emitted.
10972
11523
  */
10973
- _awaitWriteFinish(path5, threshold, event, awfEmit) {
11524
+ _awaitWriteFinish(path8, threshold, event, awfEmit) {
10974
11525
  const awf = this.options.awaitWriteFinish;
10975
11526
  if (typeof awf !== "object")
10976
11527
  return;
10977
11528
  const pollInterval = awf.pollInterval;
10978
11529
  let timeoutHandler;
10979
- let fullPath = path5;
10980
- if (this.options.cwd && !sp2.isAbsolute(path5)) {
10981
- fullPath = sp2.join(this.options.cwd, path5);
11530
+ let fullPath = path8;
11531
+ if (this.options.cwd && !sp2.isAbsolute(path8)) {
11532
+ fullPath = sp2.join(this.options.cwd, path8);
10982
11533
  }
10983
11534
  const now = /* @__PURE__ */ new Date();
10984
11535
  const writes = this._pendingWrites;
10985
11536
  function awaitWriteFinishFn(prevStat) {
10986
11537
  statcb(fullPath, (err, curStat) => {
10987
- if (err || !writes.has(path5)) {
11538
+ if (err || !writes.has(path8)) {
10988
11539
  if (err && err.code !== "ENOENT")
10989
11540
  awfEmit(err);
10990
11541
  return;
10991
11542
  }
10992
11543
  const now2 = Number(/* @__PURE__ */ new Date());
10993
11544
  if (prevStat && curStat.size !== prevStat.size) {
10994
- writes.get(path5).lastChange = now2;
11545
+ writes.get(path8).lastChange = now2;
10995
11546
  }
10996
- const pw = writes.get(path5);
11547
+ const pw = writes.get(path8);
10997
11548
  const df = now2 - pw.lastChange;
10998
11549
  if (df >= threshold) {
10999
- writes.delete(path5);
11550
+ writes.delete(path8);
11000
11551
  awfEmit(void 0, curStat);
11001
11552
  } else {
11002
11553
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
11003
11554
  }
11004
11555
  });
11005
11556
  }
11006
- if (!writes.has(path5)) {
11007
- writes.set(path5, {
11557
+ if (!writes.has(path8)) {
11558
+ writes.set(path8, {
11008
11559
  lastChange: now,
11009
11560
  cancelWait: () => {
11010
- writes.delete(path5);
11561
+ writes.delete(path8);
11011
11562
  clearTimeout(timeoutHandler);
11012
11563
  return event;
11013
11564
  }
@@ -11018,8 +11569,8 @@ var FSWatcher = class extends EventEmitter {
11018
11569
  /**
11019
11570
  * Determines whether user has asked to ignore this path.
11020
11571
  */
11021
- _isIgnored(path5, stats) {
11022
- if (this.options.atomic && DOT_RE.test(path5))
11572
+ _isIgnored(path8, stats) {
11573
+ if (this.options.atomic && DOT_RE.test(path8))
11023
11574
  return true;
11024
11575
  if (!this._userIgnored) {
11025
11576
  const { cwd } = this.options;
@@ -11029,17 +11580,17 @@ var FSWatcher = class extends EventEmitter {
11029
11580
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
11030
11581
  this._userIgnored = anymatch(list, void 0);
11031
11582
  }
11032
- return this._userIgnored(path5, stats);
11583
+ return this._userIgnored(path8, stats);
11033
11584
  }
11034
- _isntIgnored(path5, stat4) {
11035
- return !this._isIgnored(path5, stat4);
11585
+ _isntIgnored(path8, stat4) {
11586
+ return !this._isIgnored(path8, stat4);
11036
11587
  }
11037
11588
  /**
11038
11589
  * Provides a set of common helpers and properties relating to symlink handling.
11039
11590
  * @param path file or directory pattern being watched
11040
11591
  */
11041
- _getWatchHelpers(path5) {
11042
- return new WatchHelper(path5, this.options.followSymlinks, this);
11592
+ _getWatchHelpers(path8) {
11593
+ return new WatchHelper(path8, this.options.followSymlinks, this);
11043
11594
  }
11044
11595
  // Directory helpers
11045
11596
  // -----------------
@@ -11071,63 +11622,63 @@ var FSWatcher = class extends EventEmitter {
11071
11622
  * @param item base path of item/directory
11072
11623
  */
11073
11624
  _remove(directory, item, isDirectory) {
11074
- const path5 = sp2.join(directory, item);
11075
- const fullPath = sp2.resolve(path5);
11076
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path5) || this._watched.has(fullPath);
11077
- if (!this._throttle("remove", path5, 100))
11625
+ const path8 = sp2.join(directory, item);
11626
+ const fullPath = sp2.resolve(path8);
11627
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path8) || this._watched.has(fullPath);
11628
+ if (!this._throttle("remove", path8, 100))
11078
11629
  return;
11079
11630
  if (!isDirectory && this._watched.size === 1) {
11080
11631
  this.add(directory, item, true);
11081
11632
  }
11082
- const wp = this._getWatchedDir(path5);
11633
+ const wp = this._getWatchedDir(path8);
11083
11634
  const nestedDirectoryChildren = wp.getChildren();
11084
- nestedDirectoryChildren.forEach((nested) => this._remove(path5, nested));
11635
+ nestedDirectoryChildren.forEach((nested) => this._remove(path8, nested));
11085
11636
  const parent = this._getWatchedDir(directory);
11086
11637
  const wasTracked = parent.has(item);
11087
11638
  parent.remove(item);
11088
11639
  if (this._symlinkPaths.has(fullPath)) {
11089
11640
  this._symlinkPaths.delete(fullPath);
11090
11641
  }
11091
- let relPath = path5;
11642
+ let relPath = path8;
11092
11643
  if (this.options.cwd)
11093
- relPath = sp2.relative(this.options.cwd, path5);
11644
+ relPath = sp2.relative(this.options.cwd, path8);
11094
11645
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
11095
11646
  const event = this._pendingWrites.get(relPath).cancelWait();
11096
11647
  if (event === EVENTS.ADD)
11097
11648
  return;
11098
11649
  }
11099
- this._watched.delete(path5);
11650
+ this._watched.delete(path8);
11100
11651
  this._watched.delete(fullPath);
11101
11652
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
11102
- if (wasTracked && !this._isIgnored(path5))
11103
- this._emit(eventName, path5);
11104
- this._closePath(path5);
11653
+ if (wasTracked && !this._isIgnored(path8))
11654
+ this._emit(eventName, path8);
11655
+ this._closePath(path8);
11105
11656
  }
11106
11657
  /**
11107
11658
  * Closes all watchers for a path
11108
11659
  */
11109
- _closePath(path5) {
11110
- this._closeFile(path5);
11111
- const dir = sp2.dirname(path5);
11112
- this._getWatchedDir(dir).remove(sp2.basename(path5));
11660
+ _closePath(path8) {
11661
+ this._closeFile(path8);
11662
+ const dir = sp2.dirname(path8);
11663
+ this._getWatchedDir(dir).remove(sp2.basename(path8));
11113
11664
  }
11114
11665
  /**
11115
11666
  * Closes only file-specific watchers
11116
11667
  */
11117
- _closeFile(path5) {
11118
- const closers = this._closers.get(path5);
11668
+ _closeFile(path8) {
11669
+ const closers = this._closers.get(path8);
11119
11670
  if (!closers)
11120
11671
  return;
11121
11672
  closers.forEach((closer) => closer());
11122
- this._closers.delete(path5);
11673
+ this._closers.delete(path8);
11123
11674
  }
11124
- _addPathCloser(path5, closer) {
11675
+ _addPathCloser(path8, closer) {
11125
11676
  if (!closer)
11126
11677
  return;
11127
- let list = this._closers.get(path5);
11678
+ let list = this._closers.get(path8);
11128
11679
  if (!list) {
11129
11680
  list = [];
11130
- this._closers.set(path5, list);
11681
+ this._closers.set(path8, list);
11131
11682
  }
11132
11683
  list.push(closer);
11133
11684
  }
@@ -11135,18 +11686,18 @@ var FSWatcher = class extends EventEmitter {
11135
11686
  if (this.closed)
11136
11687
  return;
11137
11688
  const options = { type: EVENTS.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
11138
- let stream = readdirp(root, options);
11139
- this._streams.add(stream);
11140
- stream.once(STR_CLOSE, () => {
11141
- stream = void 0;
11689
+ let stream2 = readdirp(root, options);
11690
+ this._streams.add(stream2);
11691
+ stream2.once(STR_CLOSE, () => {
11692
+ stream2 = void 0;
11142
11693
  });
11143
- stream.once(STR_END, () => {
11144
- if (stream) {
11145
- this._streams.delete(stream);
11146
- stream = void 0;
11694
+ stream2.once(STR_END, () => {
11695
+ if (stream2) {
11696
+ this._streams.delete(stream2);
11697
+ stream2 = void 0;
11147
11698
  }
11148
11699
  });
11149
- return stream;
11700
+ return stream2;
11150
11701
  }
11151
11702
  };
11152
11703
  function watch(paths, options = {}) {
@@ -11225,13 +11776,13 @@ function startWatcher(opts) {
11225
11776
  }
11226
11777
  return {
11227
11778
  ready,
11228
- notifyChange(path5) {
11229
- if (watcher2) watcher2.emit("change", path5);
11230
- else scheduleIngest(path5);
11779
+ notifyChange(path8) {
11780
+ if (watcher2) watcher2.emit("change", path8);
11781
+ else scheduleIngest(path8);
11231
11782
  },
11232
- notifyUnlink(path5) {
11233
- if (watcher2) watcher2.emit("unlink", path5);
11234
- else recordUnlink(path5);
11783
+ notifyUnlink(path8) {
11784
+ if (watcher2) watcher2.emit("unlink", path8);
11785
+ else recordUnlink(path8);
11235
11786
  },
11236
11787
  async close() {
11237
11788
  closed = true;
@@ -11243,39 +11794,39 @@ function startWatcher(opts) {
11243
11794
  }
11244
11795
 
11245
11796
  // src/plugins/claude-code/plugin.ts
11246
- import fs5 from "fs";
11247
- import path3 from "path";
11797
+ import fs6 from "fs";
11798
+ import path4 from "path";
11248
11799
  import readline from "readline";
11249
11800
  var ClaudeCodePlugin = class {
11250
11801
  id = "claude-code";
11251
11802
  displayName = "Claude Code";
11252
11803
  async *discover(env) {
11253
- const projectsDir = path3.join(env.homeDir, ".claude", "projects");
11254
- if (!fs5.existsSync(projectsDir)) return;
11255
- const projects = fs5.readdirSync(projectsDir, { withFileTypes: true });
11804
+ const projectsDir = path4.join(env.homeDir, ".claude", "projects");
11805
+ if (!fs6.existsSync(projectsDir)) return;
11806
+ const projects = fs6.readdirSync(projectsDir, { withFileTypes: true });
11256
11807
  for (const project of projects) {
11257
11808
  if (!project.isDirectory()) continue;
11258
- const projectPath = path3.join(projectsDir, project.name);
11259
- const files = fs5.readdirSync(projectPath, { withFileTypes: true });
11809
+ const projectPath = path4.join(projectsDir, project.name);
11810
+ const files = fs6.readdirSync(projectPath, { withFileTypes: true });
11260
11811
  for (const file of files) {
11261
11812
  if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
11262
- const sourcePath = path3.join(projectPath, file.name);
11813
+ const sourcePath = path4.join(projectPath, file.name);
11263
11814
  const sourceId = file.name.replace(/\.jsonl$/, "");
11264
11815
  const cwd = await readCwdFromJsonl(sourcePath);
11265
11816
  yield {
11266
11817
  sourceId,
11267
11818
  sourcePath,
11268
11819
  watchPaths: [sourcePath],
11269
- project: cwd ? path3.basename(cwd) : void 0,
11820
+ project: cwd ? path4.basename(cwd) : void 0,
11270
11821
  projectPath: cwd ?? void 0
11271
11822
  };
11272
11823
  }
11273
11824
  }
11274
11825
  }
11275
11826
  async *extractRaw(ref) {
11276
- if (!fs5.existsSync(ref.sourcePath)) return;
11277
- const stream = fs5.createReadStream(ref.sourcePath, { encoding: "utf-8" });
11278
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
11827
+ if (!fs6.existsSync(ref.sourcePath)) return;
11828
+ const stream2 = fs6.createReadStream(ref.sourcePath, { encoding: "utf-8" });
11829
+ const rl = readline.createInterface({ input: stream2, crlfDelay: Infinity });
11279
11830
  let lineNo = 0;
11280
11831
  for await (const line of rl) {
11281
11832
  lineNo += 1;
@@ -11323,8 +11874,8 @@ var ClaudeCodePlugin = class {
11323
11874
  };
11324
11875
  async function readCwdFromJsonl(sourcePath) {
11325
11876
  try {
11326
- const stream = fs5.createReadStream(sourcePath, { encoding: "utf-8" });
11327
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
11877
+ const stream2 = fs6.createReadStream(sourcePath, { encoding: "utf-8" });
11878
+ const rl = readline.createInterface({ input: stream2, crlfDelay: Infinity });
11328
11879
  try {
11329
11880
  for await (const line of rl) {
11330
11881
  if (!line) continue;
@@ -11340,7 +11891,7 @@ async function readCwdFromJsonl(sourcePath) {
11340
11891
  }
11341
11892
  } finally {
11342
11893
  rl.close();
11343
- stream.destroy();
11894
+ stream2.destroy();
11344
11895
  }
11345
11896
  } catch {
11346
11897
  }
@@ -11429,10 +11980,132 @@ Environment:
11429
11980
  PORT=<n> Set the HTTP port (overridden by --port)
11430
11981
  `;
11431
11982
 
11983
+ // src/config/data-dir.ts
11984
+ import path5 from "path";
11985
+ function resolveDataDir(env, homeDir) {
11986
+ const override = env.CHAT_LOGBOOK_DATA_DIR?.trim();
11987
+ if (override) {
11988
+ return path5.resolve(override);
11989
+ }
11990
+ return path5.join(homeDir, ".chat-logbook");
11991
+ }
11992
+
11993
+ // src/list-counts.ts
11994
+ import fs7 from "fs";
11995
+ import path6 from "path";
11996
+ import Database3 from "better-sqlite3";
11997
+ var ARCHIVE_DB2 = "archive.db";
11998
+ var METADATA_DB2 = "metadata.db";
11999
+ function createChatCountsQuery({
12000
+ dataDir: dataDir2
12001
+ }) {
12002
+ const archive2 = new Database3(path6.join(dataDir2, ARCHIVE_DB2), {
12003
+ readonly: true
12004
+ });
12005
+ const metadataPath = path6.join(dataDir2, METADATA_DB2);
12006
+ const hasMetadata = fs7.existsSync(metadataPath);
12007
+ if (hasMetadata) {
12008
+ archive2.prepare("ATTACH DATABASE ? AS meta").run(metadataPath);
12009
+ }
12010
+ function queryCounts({
12011
+ includeTrashed = false
12012
+ }) {
12013
+ const viewClause = viewPredicate(includeTrashed, hasMetadata);
12014
+ const total = archive2.prepare(`SELECT count(*) AS n FROM chats c ${viewClause}`).get().n;
12015
+ const projects = archive2.prepare(
12016
+ `SELECT coalesce(c.project, '') AS project,
12017
+ count(*) AS count,
12018
+ max(c.updated_at) AS lastActiveAt
12019
+ FROM chats c ${viewClause}
12020
+ GROUP BY coalesce(c.project, '')`
12021
+ ).all();
12022
+ const tags3 = hasMetadata ? archive2.prepare(
12023
+ `SELECT ct.tag_id AS tagId, count(*) AS count
12024
+ FROM meta.chat_tags ct
12025
+ JOIN chats c ON c.id = ct.chat_id
12026
+ ${viewClause}
12027
+ GROUP BY ct.tag_id`
12028
+ ).all() : [];
12029
+ const untagged = hasMetadata ? archive2.prepare(
12030
+ `SELECT count(*) AS n
12031
+ FROM chats c ${viewClause}
12032
+ AND c.id NOT IN (SELECT chat_id FROM meta.chat_tags)`
12033
+ ).get().n : total;
12034
+ return { total, projects, tags: tags3, untagged };
12035
+ }
12036
+ function queryFilteredTotal({
12037
+ includeTrashed = false,
12038
+ projects,
12039
+ tags: tags3,
12040
+ tagMode = "all"
12041
+ }) {
12042
+ const clauses = [];
12043
+ const params = [];
12044
+ if (hasMetadata) {
12045
+ clauses.push(
12046
+ includeTrashed ? "c.id IN (SELECT id FROM meta.chats_meta WHERE is_deleted = 1)" : "c.id NOT IN (SELECT id FROM meta.chats_meta WHERE is_deleted = 1)"
12047
+ );
12048
+ } else if (includeTrashed) {
12049
+ return 0;
12050
+ }
12051
+ const filter = buildFilterClauses({ projects, tags: tags3, tagMode, hasMetadata });
12052
+ clauses.push(...filter.clauses);
12053
+ params.push(...filter.params);
12054
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
12055
+ return archive2.prepare(`SELECT count(*) AS n FROM chats c ${where}`).get(...params).n;
12056
+ }
12057
+ return {
12058
+ queryCounts,
12059
+ queryFilteredTotal,
12060
+ close() {
12061
+ archive2.close();
12062
+ }
12063
+ };
12064
+ }
12065
+ function viewPredicate(includeTrashed, hasMetadata) {
12066
+ if (!hasMetadata) {
12067
+ return includeTrashed ? "WHERE 0" : "";
12068
+ }
12069
+ return includeTrashed ? "WHERE c.id IN (SELECT id FROM meta.chats_meta WHERE is_deleted = 1)" : "WHERE c.id NOT IN (SELECT id FROM meta.chats_meta WHERE is_deleted = 1)";
12070
+ }
12071
+
12072
+ // src/list-events.ts
12073
+ function createListEventHub() {
12074
+ const listeners = /* @__PURE__ */ new Set();
12075
+ return {
12076
+ publish(event) {
12077
+ for (const listener of listeners) listener(event);
12078
+ },
12079
+ subscribe(listener) {
12080
+ listeners.add(listener);
12081
+ return () => {
12082
+ listeners.delete(listener);
12083
+ };
12084
+ }
12085
+ };
12086
+ }
12087
+
12088
+ // src/metadata/reconcile-title-sort-keys.ts
12089
+ function reconcileTitleSortKeys({
12090
+ archive: archive2,
12091
+ metadata: metadata2
12092
+ }) {
12093
+ const key = (agent, sourceId) => [agent, sourceId].join("\0");
12094
+ const firstTextByKey = new Map(
12095
+ archive2.read.listFirstUserTexts().map((r) => [key(r.agent, r.sourceId), r.text])
12096
+ );
12097
+ for (const row of archive2.read.listChatRows()) {
12098
+ const baseTitle = deriveBaseTitle(
12099
+ firstTextByKey.get(key(row.agent, row.sourceId))
12100
+ );
12101
+ metadata2.setTitleTextKey(row.id, computeSortKey(baseTitle));
12102
+ }
12103
+ }
12104
+
11432
12105
  // src/index.ts
11433
- var __dirname = path4.dirname(fileURLToPath2(import.meta.url));
11434
- var pkgPath = path4.join(__dirname, "../../package.json");
11435
- var pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
12106
+ var __dirname = path7.dirname(fileURLToPath2(import.meta.url));
12107
+ var pkgPath = path7.join(__dirname, "../../package.json");
12108
+ var pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
11436
12109
  var action = parseCliArgs(process.argv.slice(2), {
11437
12110
  PORT: process.env.PORT
11438
12111
  });
@@ -11449,27 +12122,58 @@ if (action.kind === "error") {
11449
12122
  process.exit(1);
11450
12123
  }
11451
12124
  updateNotifier({ pkg }).notify({ defer: false, isGlobal: true });
11452
- var dataDir = path4.join(os.homedir(), ".chat-logbook");
11453
- var webDistDir = path4.join(__dirname, "../../web/dist");
12125
+ var dataDir = resolveDataDir(
12126
+ { CHAT_LOGBOOK_DATA_DIR: process.env.CHAT_LOGBOOK_DATA_DIR },
12127
+ os.homedir()
12128
+ );
12129
+ var webDistDir = path7.join(__dirname, "../../web/dist");
11454
12130
  var port = action.port;
11455
12131
  var archive = createArchiveRepository({ dataDir });
11456
12132
  var checkpoint = createCheckpointRepository({ dataDir });
11457
12133
  var metadata = createMetadataRepository({ dataDir });
11458
12134
  var tags2 = createTagRepository({ dataDir });
11459
- var app = createApp({ archive, metadata, tags: tags2, webDistDir });
12135
+ var pageQuery = createChatPageQuery({ dataDir });
12136
+ var countsQuery = createChatCountsQuery({ dataDir });
12137
+ var listEvents = createListEventHub();
12138
+ var app = createApp({
12139
+ archive,
12140
+ metadata,
12141
+ tags: tags2,
12142
+ pageQuery,
12143
+ countsQuery,
12144
+ listEvents,
12145
+ webDistDir
12146
+ });
11460
12147
  var initialIngest = startIngestionInBackground({
11461
12148
  plugins,
11462
12149
  archive,
11463
12150
  checkpoint,
11464
12151
  env: { homeDir: os.homedir() }
11465
12152
  });
12153
+ function reconcileTitles() {
12154
+ try {
12155
+ reconcileTitleSortKeys({ archive, metadata });
12156
+ } catch (err) {
12157
+ console.error("[title-sort-key] reconcile failed:", err);
12158
+ }
12159
+ }
11466
12160
  var watcher = startWatcher({
11467
12161
  plugins,
11468
12162
  archive,
11469
12163
  checkpoint,
11470
- env: { homeDir: os.homedir() }
12164
+ env: { homeDir: os.homedir() },
12165
+ // Each watcher-driven ingest can add a chat or change a first user message;
12166
+ // refresh the Title keys so the Title axis stays current, then push a
12167
+ // `changed` event so connected clients reconcile their loaded window (#132).
12168
+ onIngest: () => {
12169
+ reconcileTitles();
12170
+ listEvents.publish({ type: "changed" });
12171
+ }
11471
12172
  });
11472
- void initialIngest.done.then(() => watcher.ready).catch(() => {
12173
+ void initialIngest.done.then(() => {
12174
+ reconcileTitles();
12175
+ return watcher.ready;
12176
+ }).catch(() => {
11473
12177
  });
11474
12178
  function shutdown() {
11475
12179
  void watcher.close().finally(() => process.exit(0));