chat-logbook 0.12.0 → 0.14.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 = {};
@@ -588,13 +588,13 @@ import updateNotifier from "update-notifier";
588
588
  // ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/compose.js
589
589
  var compose = (middleware, onError, onNotFound) => {
590
590
  return (context, next) => {
591
- let index = -1;
591
+ let index2 = -1;
592
592
  return dispatch(0);
593
593
  async function dispatch(i) {
594
- if (i <= index) {
594
+ if (i <= index2) {
595
595
  throw new Error("next() called multiple times");
596
596
  }
597
- index = i;
597
+ index2 = i;
598
598
  let res;
599
599
  let isError = false;
600
600
  let handler;
@@ -692,8 +692,8 @@ var handleParsingNestedValues = (form, key, value) => {
692
692
  }
693
693
  let nestedForm = form;
694
694
  const keys = key.split(".");
695
- keys.forEach((key2, index) => {
696
- if (index === keys.length - 1) {
695
+ keys.forEach((key2, index2) => {
696
+ if (index2 === keys.length - 1) {
697
697
  nestedForm[key2] = value;
698
698
  } else {
699
699
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -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, index) => {
723
- const mark = `@${index}`;
722
+ path8 = path8.replace(/\{[^}]+\}/g, (match2, index2) => {
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];
@@ -2021,11 +2021,11 @@ function match(method, path5) {
2021
2021
  if (!match3) {
2022
2022
  return [[], emptyParam];
2023
2023
  }
2024
- const index = match3.indexOf("", 1);
2025
- return [matcher[1][index], match3];
2024
+ const index2 = match3.indexOf("", 1);
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
@@ -2057,7 +2057,7 @@ var Node = class _Node {
2057
2057
  #index;
2058
2058
  #varIndex;
2059
2059
  #children = /* @__PURE__ */ Object.create(null);
2060
- insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
2060
+ insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
2061
2061
  if (tokens.length === 0) {
2062
2062
  if (this.#index !== void 0) {
2063
2063
  throw PATH_ERROR;
@@ -2065,7 +2065,7 @@ var Node = class _Node {
2065
2065
  if (pathErrorCheckOnly) {
2066
2066
  return;
2067
2067
  }
2068
- this.#index = index;
2068
+ this.#index = index2;
2069
2069
  return;
2070
2070
  }
2071
2071
  const [token, ...restTokens] = tokens;
@@ -2115,7 +2115,7 @@ var Node = class _Node {
2115
2115
  node = this.#children[token] = new _Node();
2116
2116
  }
2117
2117
  }
2118
- node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
2118
+ node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
2119
2119
  }
2120
2120
  buildRegExpStr() {
2121
2121
  const childKeys = Object.keys(this.#children).sort(compareKey);
@@ -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, index, 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--) {
@@ -2166,7 +2166,7 @@ var Trie = class {
2166
2166
  }
2167
2167
  }
2168
2168
  }
2169
- this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
2169
+ this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
2170
2170
  return paramAssoc;
2171
2171
  }
2172
2172
  buildRegExp() {
@@ -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,16 +2999,31 @@ 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
  };
2862
3011
 
3012
+ // src/metadata/tag-colors.ts
3013
+ var TAG_COLORS = [
3014
+ "yellow",
3015
+ "orange",
3016
+ "red",
3017
+ "magenta",
3018
+ "violet",
3019
+ "blue",
3020
+ "cyan",
3021
+ "green"
3022
+ ];
3023
+ function isColorToken(value) {
3024
+ return typeof value === "string" && TAG_COLORS.includes(value);
3025
+ }
3026
+
2863
3027
  // src/archive/chat-id.ts
2864
3028
  import crypto3 from "crypto";
2865
3029
  var CROCKFORD_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
@@ -2909,6 +3073,154 @@ function loadChatVisibility(metadata2, opts) {
2909
3073
  };
2910
3074
  }
2911
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
+ hasMetadata
3092
+ }) {
3093
+ const clauses = [];
3094
+ const params = [];
3095
+ if (projects && projects.length > 0) {
3096
+ const placeholders = projects.map(() => "?").join(", ");
3097
+ clauses.push(`coalesce(c.project, '') IN (${placeholders})`);
3098
+ params.push(...projects);
3099
+ }
3100
+ if (tags3 && tags3.length > 0) {
3101
+ const realTagIds = tags3.filter((t) => t !== "");
3102
+ const wantUntagged = tags3.includes("");
3103
+ if (realTagIds.length > 0) {
3104
+ if (!hasMetadata) {
3105
+ clauses.push("0");
3106
+ } else {
3107
+ const placeholders = realTagIds.map(() => "?").join(", ");
3108
+ clauses.push(
3109
+ `c.id IN (SELECT chat_id FROM meta.chat_tags
3110
+ WHERE tag_id IN (${placeholders})
3111
+ GROUP BY chat_id HAVING count(*) = ?)`
3112
+ );
3113
+ params.push(...realTagIds, realTagIds.length);
3114
+ }
3115
+ }
3116
+ if (wantUntagged && hasMetadata) {
3117
+ clauses.push("c.id NOT IN (SELECT chat_id FROM meta.chat_tags)");
3118
+ }
3119
+ }
3120
+ return { clauses, params };
3121
+ }
3122
+
3123
+ // src/list-pagination.ts
3124
+ var ARCHIVE_DB = "archive.db";
3125
+ var METADATA_DB = "metadata.db";
3126
+ var SORT_EXPR = {
3127
+ createdAt: "c.created_at",
3128
+ updatedAt: "c.updated_at",
3129
+ deletedAt: "m.deleted_at",
3130
+ // The Title axis orders by the precomputed collation key on the ATTACHed
3131
+ // `meta.chat_sort_keys` (alias `k`), reached through the INNER JOIN added
3132
+ // below (ADR-0019). BINARY compare over `sort_key` is an index range scan.
3133
+ title: "k.sort_key"
3134
+ };
3135
+ function encodeCursor(cursor) {
3136
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
3137
+ }
3138
+ function decodeCursor(token) {
3139
+ try {
3140
+ const parsed = JSON.parse(
3141
+ Buffer.from(token, "base64url").toString("utf8")
3142
+ );
3143
+ const sortKey = parsed?.sortKey;
3144
+ if (parsed && typeof parsed === "object" && (typeof sortKey === "number" || typeof sortKey === "string") && typeof parsed.id === "string") {
3145
+ return parsed;
3146
+ }
3147
+ return null;
3148
+ } catch {
3149
+ return null;
3150
+ }
3151
+ }
3152
+ function createChatPageQuery({
3153
+ dataDir: dataDir2
3154
+ }) {
3155
+ const archive2 = new Database(path.join(dataDir2, ARCHIVE_DB), {
3156
+ readonly: true
3157
+ });
3158
+ const metadataPath = path.join(dataDir2, METADATA_DB);
3159
+ const hasMetadata = fs.existsSync(metadataPath);
3160
+ if (hasMetadata) {
3161
+ archive2.prepare("ATTACH DATABASE ? AS meta").run(metadataPath);
3162
+ }
3163
+ function queryPage(query) {
3164
+ const sortExpr = SORT_EXPR[query.sort];
3165
+ const needsDeletedJoin = query.sort === "deletedAt";
3166
+ const needsSortKeyJoin = query.sort === "title";
3167
+ if ((needsDeletedJoin || needsSortKeyJoin) && !hasMetadata) {
3168
+ return { items: [], nextCursor: null };
3169
+ }
3170
+ let from = "chats c";
3171
+ if (needsDeletedJoin) from += " JOIN meta.chats_meta m ON m.id = c.id";
3172
+ if (needsSortKeyJoin) from += " JOIN meta.chat_sort_keys k ON k.id = c.id";
3173
+ const idCol = needsDeletedJoin ? "m.id" : needsSortKeyJoin ? "k.id" : "c.id";
3174
+ const clauses = [];
3175
+ const params = [];
3176
+ if (needsDeletedJoin) {
3177
+ clauses.push("m.deleted_at IS NOT NULL");
3178
+ } else if (query.trashedOnly) {
3179
+ if (!hasMetadata) return { items: [], nextCursor: null };
3180
+ clauses.push(
3181
+ "c.id IN (SELECT id FROM meta.chats_meta WHERE is_deleted = 1)"
3182
+ );
3183
+ } else if (!query.includeTrashed && hasMetadata) {
3184
+ clauses.push(
3185
+ "c.id NOT IN (SELECT id FROM meta.chats_meta WHERE is_deleted = 1)"
3186
+ );
3187
+ }
3188
+ const filter = buildFilterClauses({
3189
+ projects: query.projects,
3190
+ tags: query.tags,
3191
+ hasMetadata
3192
+ });
3193
+ clauses.push(...filter.clauses);
3194
+ params.push(...filter.params);
3195
+ const direction = query.direction ?? "desc";
3196
+ const cmp = direction === "asc" ? ">" : "<";
3197
+ const order = direction === "asc" ? "ASC" : "DESC";
3198
+ if (query.cursor) {
3199
+ clauses.push(
3200
+ `(${sortExpr} ${cmp} ? OR (${sortExpr} = ? AND ${idCol} ${cmp} ?))`
3201
+ );
3202
+ params.push(query.cursor.sortKey, query.cursor.sortKey, query.cursor.id);
3203
+ }
3204
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
3205
+ const rows = archive2.prepare(
3206
+ `SELECT c.id AS id, ${sortExpr} AS sortKey
3207
+ FROM ${from} ${where}
3208
+ ORDER BY ${sortExpr} ${order}, ${idCol} ${order}
3209
+ LIMIT ?`
3210
+ ).all(...params, query.limit + 1);
3211
+ const items = rows.slice(0, query.limit);
3212
+ const last = items[items.length - 1];
3213
+ const nextCursor = rows.length > query.limit && last ? { sortKey: last.sortKey, id: last.id } : null;
3214
+ return { items, nextCursor };
3215
+ }
3216
+ return {
3217
+ queryPage,
3218
+ close() {
3219
+ archive2.close();
3220
+ }
3221
+ };
3222
+ }
3223
+
2912
3224
  // src/chat-reader.ts
2913
3225
  function toApiBlock(block) {
2914
3226
  if (block.type === "tool_result") {
@@ -2922,7 +3234,10 @@ function toApiBlock(block) {
2922
3234
  }
2923
3235
  function createChatReader({
2924
3236
  archive: archive2,
2925
- metadata: metadata2
3237
+ metadata: metadata2,
3238
+ tags: tags3,
3239
+ pageQuery: pageQuery2,
3240
+ countsQuery: countsQuery2
2926
3241
  }) {
2927
3242
  function findChat(id) {
2928
3243
  const code = parseChatId(id);
@@ -2931,31 +3246,52 @@ function createChatReader({
2931
3246
  }
2932
3247
  function deriveTitle(customTitle, firstUserText) {
2933
3248
  if (customTitle && customTitle.trim()) return customTitle;
2934
- const text2 = firstUserText?.trim().split("\n")[0]?.trim();
2935
- return text2 && text2.length > 0 ? text2 : "Untitled";
3249
+ return deriveBaseTitle(firstUserText);
2936
3250
  }
2937
3251
  function key(agent, sourceId) {
2938
3252
  return [agent, sourceId].join("\0");
2939
3253
  }
2940
3254
  function listChats({
2941
- includeTrashed
3255
+ includeTrashed,
3256
+ projects,
3257
+ tags: tagSelection
2942
3258
  }) {
2943
3259
  const visibility = loadChatVisibility(metadata2, { includeTrashed });
2944
- const rows = archive2.read.listChatRows();
3260
+ const rows = archive2.read.listChatRows(projects ? { projects } : void 0);
3261
+ let passesTagFilter = null;
3262
+ if (tagSelection) {
3263
+ const realTagIds = tagSelection.filter((t) => t !== "");
3264
+ const wantUntagged = tagSelection.includes("");
3265
+ const allowedByTags = realTagIds.length > 0 ? new Set(tags3.listChatIdsWithAllTags(realTagIds)) : null;
3266
+ const taggedChatIds = wantUntagged ? new Set(tags3.listTagsByChat().keys()) : null;
3267
+ passesTagFilter = (chatInternalId) => {
3268
+ if (allowedByTags && !allowedByTags.has(chatInternalId)) return false;
3269
+ if (taggedChatIds && taggedChatIds.has(chatInternalId)) return false;
3270
+ return true;
3271
+ };
3272
+ }
3273
+ const toResponse = loadHydration();
3274
+ const chats2 = [];
3275
+ for (const row of rows) {
3276
+ if (!visibility.isVisible(row.id)) continue;
3277
+ if (passesTagFilter && !passesTagFilter(row.id)) continue;
3278
+ chats2.push(toResponse(row, visibility));
3279
+ }
3280
+ return chats2;
3281
+ }
3282
+ function loadHydration(scope) {
2945
3283
  const tsRangeByKey = new Map(
2946
- archive2.read.listChatTsRanges().map((r) => [key(r.agent, r.sourceId), r])
3284
+ archive2.read.listChatTsRanges(scope).map((r) => [key(r.agent, r.sourceId), r])
2947
3285
  );
2948
3286
  const sourcePathByKey = new Map(
2949
- archive2.read.listLatestRawSourcePaths().map((r) => [key(r.agent, r.sourceId), r.sourcePath])
3287
+ archive2.read.listLatestRawSourcePaths(scope).map((r) => [key(r.agent, r.sourceId), r.sourcePath])
2950
3288
  );
2951
3289
  const firstUserTextByKey = new Map(
2952
- archive2.read.listFirstUserTexts().map((r) => [key(r.agent, r.sourceId), r.text])
3290
+ archive2.read.listFirstUserTexts(scope).map((r) => [key(r.agent, r.sourceId), r.text])
2953
3291
  );
2954
3292
  const customTitleById = metadata2.listCustomTitles();
2955
- const chats2 = [];
2956
- for (const row of rows) {
2957
- if (!visibility.isVisible(row.id)) continue;
2958
- const isDeleted = visibility.isTrashed(row.id);
3293
+ const tagsByChatId = tags3.listTagsByChat();
3294
+ return (row, visibility) => {
2959
3295
  const rowKey = key(row.agent, row.sourceId);
2960
3296
  const tsRange = tsRangeByKey.get(rowKey);
2961
3297
  const firstSeenAtMs = row.firstSeenAt.getTime();
@@ -2972,12 +3308,73 @@ function createChatReader({
2972
3308
  sourceFilePath: sourcePathByKey.get(rowKey) ?? null,
2973
3309
  createdAt: tsRange?.minTs ?? firstSeenAtMs,
2974
3310
  updatedAt: tsRange?.maxTs ?? firstSeenAtMs,
2975
- deletedAt: visibility.deletedAt(row.id)
3311
+ deletedAt: visibility.deletedAt(row.id),
3312
+ tags: tagsByChatId.get(row.id) ?? []
2976
3313
  };
2977
- if (isDeleted) chat.isDeleted = true;
2978
- chats2.push(chat);
3314
+ if (visibility.isTrashed(row.id)) chat.isDeleted = true;
3315
+ return chat;
3316
+ };
3317
+ }
3318
+ function listChatsPage({
3319
+ sort,
3320
+ direction,
3321
+ limit,
3322
+ cursor,
3323
+ includeTrashed = false,
3324
+ trashedOnly = false,
3325
+ projects,
3326
+ tags: tagSelection
3327
+ }) {
3328
+ if (!pageQuery2) {
3329
+ throw new Error("listChatsPage requires a pageQuery dependency");
3330
+ }
3331
+ const decoded = cursor ? decodeCursor(cursor) ?? void 0 : void 0;
3332
+ const page = pageQuery2.queryPage({
3333
+ sort,
3334
+ direction,
3335
+ limit,
3336
+ cursor: decoded,
3337
+ includeTrashed,
3338
+ trashedOnly,
3339
+ projects,
3340
+ tags: tagSelection
3341
+ });
3342
+ const visibility = loadChatVisibility(metadata2, {
3343
+ includeTrashed: includeTrashed || trashedOnly
3344
+ });
3345
+ const pageIds = page.items.map((item) => item.id);
3346
+ const rows = archive2.read.listChatRowsByIds(pageIds);
3347
+ const rowById = new Map(rows.map((r) => [r.id, r]));
3348
+ const toResponse = loadHydration({
3349
+ sourceIds: rows.map((r) => r.sourceId)
3350
+ });
3351
+ const chats2 = [];
3352
+ for (const item of page.items) {
3353
+ const row = rowById.get(item.id);
3354
+ if (row) chats2.push(toResponse(row, visibility));
2979
3355
  }
2980
- return chats2;
3356
+ return {
3357
+ chats: chats2,
3358
+ nextCursor: page.nextCursor ? encodeCursor(page.nextCursor) : null
3359
+ };
3360
+ }
3361
+ function listCounts({
3362
+ includeTrashed = false
3363
+ }) {
3364
+ if (!countsQuery2) {
3365
+ throw new Error("listCounts requires a countsQuery dependency");
3366
+ }
3367
+ return countsQuery2.queryCounts({ includeTrashed });
3368
+ }
3369
+ function listFilteredTotal({
3370
+ includeTrashed = false,
3371
+ projects,
3372
+ tags: tags4
3373
+ }) {
3374
+ if (!countsQuery2) {
3375
+ throw new Error("listFilteredTotal requires a countsQuery dependency");
3376
+ }
3377
+ return countsQuery2.queryFilteredTotal({ includeTrashed, projects, tags: tags4 });
2981
3378
  }
2982
3379
  function getMessages(id, { includeTrashed }) {
2983
3380
  const row = findChat(id);
@@ -2991,19 +3388,122 @@ function createChatReader({
2991
3388
  timestamp: m.ts.toISOString()
2992
3389
  }));
2993
3390
  }
2994
- return { listChats, getMessages, findChat };
3391
+ return {
3392
+ listChats,
3393
+ listChatsPage,
3394
+ listCounts,
3395
+ listFilteredTotal,
3396
+ getMessages,
3397
+ findChat
3398
+ };
2995
3399
  }
2996
3400
 
3401
+ // src/list-contract.ts
3402
+ var MAX_PAGE_LIMIT = 200;
3403
+
2997
3404
  // src/app.ts
2998
- function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDistDir2 }) {
3405
+ var STREAM_HEARTBEAT_MS = 25e3;
3406
+ function createApp({
3407
+ archive: archive2,
3408
+ metadata: metadata2,
3409
+ tags: tags3,
3410
+ pageQuery: pageQuery2,
3411
+ countsQuery: countsQuery2,
3412
+ listEvents: listEvents2,
3413
+ webDistDir: webDistDir2
3414
+ }) {
2999
3415
  const app2 = new Hono2();
3000
- const reader = createChatReader({ archive: archive2, metadata: metadata2 });
3416
+ const reader = createChatReader({
3417
+ archive: archive2,
3418
+ metadata: metadata2,
3419
+ tags: tags3,
3420
+ pageQuery: pageQuery2,
3421
+ countsQuery: countsQuery2
3422
+ });
3001
3423
  app2.get("/api/chats", (c) => {
3424
+ const includeTrashed = c.req.query("includeTrashed") === "true";
3425
+ const limitParam = c.req.query("limit");
3426
+ if (limitParam !== void 0) {
3427
+ if (!pageQuery2) {
3428
+ return c.json({ error: "Pagination is not available" }, 501);
3429
+ }
3430
+ const limit = Number.parseInt(limitParam, 10);
3431
+ if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_PAGE_LIMIT) {
3432
+ return c.json({ error: "Invalid limit" }, 400);
3433
+ }
3434
+ const sort = c.req.query("sort") ?? "updatedAt";
3435
+ if (sort !== "createdAt" && sort !== "updatedAt" && sort !== "deletedAt" && sort !== "title") {
3436
+ return c.json({ error: "Invalid sort" }, 400);
3437
+ }
3438
+ const direction = c.req.query("direction") ?? "desc";
3439
+ if (direction !== "asc" && direction !== "desc") {
3440
+ return c.json({ error: "Invalid direction" }, 400);
3441
+ }
3442
+ const trashedOnly = c.req.query("trashedOnly") === "true";
3443
+ const projects2 = c.req.queries("project");
3444
+ const tagsParam2 = c.req.query("tags");
3445
+ const tagSelection = tagsParam2 === void 0 ? void 0 : tagsParam2.split(",");
3446
+ const { chats: chats3, nextCursor } = reader.listChatsPage({
3447
+ sort,
3448
+ direction,
3449
+ limit,
3450
+ cursor: c.req.query("cursor"),
3451
+ includeTrashed,
3452
+ trashedOnly,
3453
+ projects: projects2,
3454
+ tags: tagSelection
3455
+ });
3456
+ return c.json({ chats: chats3, nextCursor });
3457
+ }
3458
+ const projects = c.req.queries("project");
3459
+ const tagsParam = c.req.query("tags");
3460
+ const tags4 = tagsParam === void 0 ? void 0 : tagsParam.split(",");
3002
3461
  const chats2 = reader.listChats({
3003
- includeTrashed: c.req.query("includeTrashed") === "true"
3462
+ includeTrashed,
3463
+ projects,
3464
+ tags: tags4
3004
3465
  });
3005
3466
  return c.json({ chats: chats2 });
3006
3467
  });
3468
+ app2.get("/api/chats/stream", (c) => {
3469
+ if (!listEvents2) {
3470
+ return c.json({ error: "Live updates are not available" }, 501);
3471
+ }
3472
+ const hub = listEvents2;
3473
+ return streamSSE(c, async (stream2) => {
3474
+ const unsubscribe = hub.subscribe((event) => {
3475
+ void stream2.writeSSE({ event: event.type, data: "" });
3476
+ });
3477
+ await new Promise((resolve3) => {
3478
+ const heartbeat = setInterval(() => {
3479
+ void stream2.writeSSE({ event: "ping", data: "" });
3480
+ }, STREAM_HEARTBEAT_MS);
3481
+ stream2.onAbort(() => {
3482
+ clearInterval(heartbeat);
3483
+ unsubscribe();
3484
+ resolve3();
3485
+ });
3486
+ });
3487
+ });
3488
+ });
3489
+ app2.get("/api/chats/counts", (c) => {
3490
+ if (!countsQuery2) {
3491
+ return c.json({ error: "Counts are not available" }, 501);
3492
+ }
3493
+ const includeTrashed = c.req.query("includeTrashed") === "true";
3494
+ return c.json(reader.listCounts({ includeTrashed }));
3495
+ });
3496
+ app2.get("/api/chats/list-total", (c) => {
3497
+ if (!countsQuery2) {
3498
+ return c.json({ error: "Counts are not available" }, 501);
3499
+ }
3500
+ const includeTrashed = c.req.query("includeTrashed") === "true";
3501
+ const projects = c.req.queries("project");
3502
+ const tagsParam = c.req.query("tags");
3503
+ const tags4 = tagsParam === void 0 ? void 0 : tagsParam.split(",");
3504
+ const total = reader.listFilteredTotal({ includeTrashed, projects, tags: tags4 });
3505
+ return c.json({ total });
3506
+ });
3007
3507
  app2.delete("/api/chats/:id", (c) => {
3008
3508
  const row = reader.findChat(c.req.param("id"));
3009
3509
  if (!row) {
@@ -3051,6 +3551,85 @@ function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDist
3051
3551
  }
3052
3552
  return c.json({ messages: messages2 });
3053
3553
  });
3554
+ app2.get("/api/tags", (c) => {
3555
+ return c.json({ tags: tags3.listTags() });
3556
+ });
3557
+ app2.post("/api/tags", async (c) => {
3558
+ let body;
3559
+ try {
3560
+ body = await c.req.json();
3561
+ } catch {
3562
+ return c.json({ error: "Invalid JSON body" }, 400);
3563
+ }
3564
+ const name = body && typeof body === "object" ? body.name : void 0;
3565
+ const color = body && typeof body === "object" ? body.color : void 0;
3566
+ if (typeof name !== "string" || name.trim().length === 0) {
3567
+ return c.json({ error: "Invalid name" }, 400);
3568
+ }
3569
+ if (!isColorToken(color)) {
3570
+ return c.json({ error: "Invalid color" }, 400);
3571
+ }
3572
+ const tag = tags3.createTag(name.trim(), color);
3573
+ return c.json({ tag }, 201);
3574
+ });
3575
+ app2.patch("/api/tags/:id", async (c) => {
3576
+ const id = c.req.param("id");
3577
+ if (!tags3.getTag(id)) {
3578
+ return c.json({ error: "Tag not found" }, 404);
3579
+ }
3580
+ let body;
3581
+ try {
3582
+ body = await c.req.json();
3583
+ } catch {
3584
+ return c.json({ error: "Invalid JSON body" }, 400);
3585
+ }
3586
+ const name = body && typeof body === "object" ? body.name : void 0;
3587
+ const color = body && typeof body === "object" ? body.color : void 0;
3588
+ if (name !== void 0) {
3589
+ if (typeof name !== "string" || name.trim().length === 0) {
3590
+ return c.json({ error: "Invalid name" }, 400);
3591
+ }
3592
+ }
3593
+ if (color !== void 0 && !isColorToken(color)) {
3594
+ return c.json({ error: "Invalid color" }, 400);
3595
+ }
3596
+ if (typeof name === "string") tags3.renameTag(id, name.trim());
3597
+ if (isColorToken(color)) tags3.recolorTag(id, color);
3598
+ return c.body(null, 204);
3599
+ });
3600
+ app2.delete("/api/tags/:id", (c) => {
3601
+ if (!tags3.getTag(c.req.param("id"))) {
3602
+ return c.json({ error: "Tag not found" }, 404);
3603
+ }
3604
+ const result = tags3.deleteTag(c.req.param("id"));
3605
+ return c.json(result);
3606
+ });
3607
+ app2.post("/api/chats/:id/tags", async (c) => {
3608
+ const row = reader.findChat(c.req.param("id"));
3609
+ if (!row) {
3610
+ return c.json({ error: "Chat not found" }, 404);
3611
+ }
3612
+ let body;
3613
+ try {
3614
+ body = await c.req.json();
3615
+ } catch {
3616
+ return c.json({ error: "Invalid JSON body" }, 400);
3617
+ }
3618
+ const tagId = body && typeof body === "object" ? body.tagId : void 0;
3619
+ if (typeof tagId !== "string" || !tags3.getTag(tagId)) {
3620
+ return c.json({ error: "Tag not found" }, 404);
3621
+ }
3622
+ tags3.assignTag(row.id, tagId);
3623
+ return c.body(null, 204);
3624
+ });
3625
+ app2.delete("/api/chats/:id/tags/:tagId", (c) => {
3626
+ const row = reader.findChat(c.req.param("id"));
3627
+ if (!row) {
3628
+ return c.json({ error: "Chat not found" }, 404);
3629
+ }
3630
+ tags3.removeTag(row.id, c.req.param("tagId"));
3631
+ return c.body(null, 204);
3632
+ });
3054
3633
  if (webDistDir2) {
3055
3634
  app2.use("*", serveStatic({ root: webDistDir2 }));
3056
3635
  app2.use("*", serveStatic({ root: webDistDir2, path: "index.html" }));
@@ -4374,7 +4953,7 @@ var QueryPromise = class {
4374
4953
  function mapResultRow(columns, row, joinsNotNullableMap) {
4375
4954
  const nullifyMap = {};
4376
4955
  const result = columns.reduce(
4377
- (result2, { path: path5, field }, columnIndex) => {
4956
+ (result2, { path: path8, field }, columnIndex) => {
4378
4957
  let decoder;
4379
4958
  if (is(field, Column)) {
4380
4959
  decoder = field;
@@ -4386,8 +4965,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
4386
4965
  decoder = field.sql.decoder;
4387
4966
  }
4388
4967
  let node = result2;
4389
- for (const [pathChunkIndex, pathChunk] of path5.entries()) {
4390
- if (pathChunkIndex < path5.length - 1) {
4968
+ for (const [pathChunkIndex, pathChunk] of path8.entries()) {
4969
+ if (pathChunkIndex < path8.length - 1) {
4391
4970
  if (!(pathChunk in node)) {
4392
4971
  node[pathChunk] = {};
4393
4972
  }
@@ -4395,8 +4974,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
4395
4974
  } else {
4396
4975
  const rawValue = row[columnIndex];
4397
4976
  const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
4398
- if (joinsNotNullableMap && is(field, Column) && path5.length === 2) {
4399
- const objectName = path5[0];
4977
+ if (joinsNotNullableMap && is(field, Column) && path8.length === 2) {
4978
+ const objectName = path8[0];
4400
4979
  if (!(objectName in nullifyMap)) {
4401
4980
  nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
4402
4981
  } else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
@@ -4440,8 +5019,8 @@ function haveSameKeys(left, right) {
4440
5019
  if (leftKeys.length !== rightKeys.length) {
4441
5020
  return false;
4442
5021
  }
4443
- for (const [index, key] of leftKeys.entries()) {
4444
- if (key !== rightKeys[index]) {
5022
+ for (const [index2, key] of leftKeys.entries()) {
5023
+ if (key !== rightKeys[index2]) {
4445
5024
  return false;
4446
5025
  }
4447
5026
  }
@@ -4821,19 +5400,19 @@ function extractTablesRelationalConfig(schema, configHelpers) {
4821
5400
  const relations2 = value.config(
4822
5401
  configHelpers(value.table)
4823
5402
  );
4824
- let primaryKey;
5403
+ let primaryKey2;
4825
5404
  for (const [relationName, relation] of Object.entries(relations2)) {
4826
5405
  if (tableName) {
4827
5406
  const tableConfig = tablesConfig[tableName];
4828
5407
  tableConfig.relations[relationName] = relation;
4829
- if (primaryKey) {
4830
- tableConfig.primaryKey.push(...primaryKey);
5408
+ if (primaryKey2) {
5409
+ tableConfig.primaryKey.push(...primaryKey2);
4831
5410
  }
4832
5411
  } else {
4833
5412
  if (!(dbName in relationsBuffer)) {
4834
5413
  relationsBuffer[dbName] = {
4835
5414
  relations: {},
4836
- primaryKey
5415
+ primaryKey: primaryKey2
4837
5416
  };
4838
5417
  }
4839
5418
  relationsBuffer[dbName].relations[relationName] = relation;
@@ -4956,10 +5535,10 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
4956
5535
  }
4957
5536
 
4958
5537
  // src/storage/openStore.ts
4959
- import fs2 from "fs";
4960
- import path from "path";
5538
+ import fs3 from "fs";
5539
+ import path2 from "path";
4961
5540
  import { fileURLToPath } from "url";
4962
- import Database from "better-sqlite3";
5541
+ import Database2 from "better-sqlite3";
4963
5542
 
4964
5543
  // ../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
4965
5544
  import Client from "better-sqlite3";
@@ -5677,10 +6256,49 @@ var Index = class {
5677
6256
  this.config = { ...config, table };
5678
6257
  }
5679
6258
  };
6259
+ function index(name) {
6260
+ return new IndexBuilderOn(name, false);
6261
+ }
5680
6262
  function uniqueIndex(name) {
5681
6263
  return new IndexBuilderOn(name, true);
5682
6264
  }
5683
6265
 
6266
+ // ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/sqlite-core/primary-keys.js
6267
+ function primaryKey(...config) {
6268
+ if (config[0].columns) {
6269
+ return new PrimaryKeyBuilder2(config[0].columns, config[0].name);
6270
+ }
6271
+ return new PrimaryKeyBuilder2(config);
6272
+ }
6273
+ var PrimaryKeyBuilder2 = class {
6274
+ static [entityKind] = "SQLitePrimaryKeyBuilder";
6275
+ /** @internal */
6276
+ columns;
6277
+ /** @internal */
6278
+ name;
6279
+ constructor(columns, name) {
6280
+ this.columns = columns;
6281
+ this.name = name;
6282
+ }
6283
+ /** @internal */
6284
+ build(table) {
6285
+ return new PrimaryKey2(table, this.columns, this.name);
6286
+ }
6287
+ };
6288
+ var PrimaryKey2 = class {
6289
+ constructor(table, columns, name) {
6290
+ this.table = table;
6291
+ this.columns = columns;
6292
+ this.name = name;
6293
+ }
6294
+ static [entityKind] = "SQLitePrimaryKey";
6295
+ columns;
6296
+ name;
6297
+ getName() {
6298
+ return this.name ?? `${this.table[SQLiteTable.Symbol.Name]}_${this.columns.map((column) => column.name).join("_")}_pk`;
6299
+ }
6300
+ };
6301
+
5684
6302
  // ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/sqlite-core/utils.js
5685
6303
  function extractUsedTable(table) {
5686
6304
  if (is(table, SQLiteTable)) {
@@ -6029,8 +6647,8 @@ var SQLiteDialect = class {
6029
6647
  }
6030
6648
  const joinsArray = [];
6031
6649
  if (joins) {
6032
- for (const [index, joinMeta] of joins.entries()) {
6033
- if (index === 0) {
6650
+ for (const [index2, joinMeta] of joins.entries()) {
6651
+ if (index2 === 0) {
6034
6652
  joinsArray.push(sql` `);
6035
6653
  }
6036
6654
  const table = joinMeta.table;
@@ -6050,7 +6668,7 @@ var SQLiteDialect = class {
6050
6668
  sql`${sql.raw(joinMeta.joinType)} join ${table}${onSql}`
6051
6669
  );
6052
6670
  }
6053
- if (index < joins.length - 1) {
6671
+ if (index2 < joins.length - 1) {
6054
6672
  joinsArray.push(sql` `);
6055
6673
  }
6056
6674
  }
@@ -6063,9 +6681,9 @@ var SQLiteDialect = class {
6063
6681
  buildOrderBy(orderBy) {
6064
6682
  const orderByList = [];
6065
6683
  if (orderBy) {
6066
- for (const [index, orderByValue] of orderBy.entries()) {
6684
+ for (const [index2, orderByValue] of orderBy.entries()) {
6067
6685
  orderByList.push(orderByValue);
6068
- if (index < orderBy.length - 1) {
6686
+ if (index2 < orderBy.length - 1) {
6069
6687
  orderByList.push(sql`, `);
6070
6688
  }
6071
6689
  }
@@ -6118,9 +6736,9 @@ var SQLiteDialect = class {
6118
6736
  const havingSql = having ? sql` having ${having}` : void 0;
6119
6737
  const groupByList = [];
6120
6738
  if (groupBy) {
6121
- for (const [index, groupByValue] of groupBy.entries()) {
6739
+ for (const [index2, groupByValue] of groupBy.entries()) {
6122
6740
  groupByList.push(groupByValue);
6123
- if (index < groupBy.length - 1) {
6741
+ if (index2 < groupBy.length - 1) {
6124
6742
  groupByList.push(sql`, `);
6125
6743
  }
6126
6744
  }
@@ -8527,20 +9145,20 @@ function drizzle(...params) {
8527
9145
 
8528
9146
  // ../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
8529
9147
  import crypto4 from "crypto";
8530
- import fs from "fs";
9148
+ import fs2 from "fs";
8531
9149
  function readMigrationFiles(config) {
8532
9150
  const migrationFolderTo = config.migrationsFolder;
8533
9151
  const migrationQueries = [];
8534
9152
  const journalPath = `${migrationFolderTo}/meta/_journal.json`;
8535
- if (!fs.existsSync(journalPath)) {
9153
+ if (!fs2.existsSync(journalPath)) {
8536
9154
  throw new Error(`Can't find meta/_journal.json file`);
8537
9155
  }
8538
- const journalAsString = fs.readFileSync(`${migrationFolderTo}/meta/_journal.json`).toString();
9156
+ const journalAsString = fs2.readFileSync(`${migrationFolderTo}/meta/_journal.json`).toString();
8539
9157
  const journal = JSON.parse(journalAsString);
8540
9158
  for (const journalEntry of journal.entries) {
8541
9159
  const migrationPath = `${migrationFolderTo}/${journalEntry.tag}.sql`;
8542
9160
  try {
8543
- const query = fs.readFileSync(`${migrationFolderTo}/${journalEntry.tag}.sql`).toString();
9161
+ const query = fs2.readFileSync(`${migrationFolderTo}/${journalEntry.tag}.sql`).toString();
8544
9162
  const result = query.split("--> statement-breakpoint").map((it) => {
8545
9163
  return it;
8546
9164
  });
@@ -8565,12 +9183,12 @@ function migrate(db, config) {
8565
9183
 
8566
9184
  // src/storage/openStore.ts
8567
9185
  function resolveMigrationsFolder(callerUrl, migrationsSubdir) {
8568
- const here = path.dirname(fileURLToPath(callerUrl));
9186
+ const here = path2.dirname(fileURLToPath(callerUrl));
8569
9187
  const candidates = [
8570
- path.join(here, "../..", migrationsSubdir),
8571
- path.join(here, ".", migrationsSubdir)
9188
+ path2.join(here, "../..", migrationsSubdir),
9189
+ path2.join(here, ".", migrationsSubdir)
8572
9190
  ];
8573
- const found = candidates.find((p) => fs2.existsSync(p));
9191
+ const found = candidates.find((p) => fs3.existsSync(p));
8574
9192
  if (!found) {
8575
9193
  throw new Error(`Could not locate migrations folder "${migrationsSubdir}"`);
8576
9194
  }
@@ -8584,8 +9202,8 @@ function openStore({
8584
9202
  schema
8585
9203
  }) {
8586
9204
  const migrationsFolder = resolveMigrationsFolder(callerUrl, migrationsSubdir);
8587
- fs2.mkdirSync(dataDir2, { recursive: true });
8588
- const sqlite = new Database(path.join(dataDir2, dbFile));
9205
+ fs3.mkdirSync(dataDir2, { recursive: true });
9206
+ const sqlite = new Database2(path2.join(dataDir2, dbFile));
8589
9207
  const db = drizzle(sqlite, { schema });
8590
9208
  migrate(db, { migrationsFolder });
8591
9209
  return { db, sqlite };
@@ -8618,10 +9236,33 @@ var chats = sqliteTable(
8618
9236
  agent: text("agent").notNull(),
8619
9237
  sourceId: text("source_id").notNull(),
8620
9238
  firstSeenAt: integer("first_seen_at", { mode: "timestamp_ms" }).notNull(),
9239
+ // Denormalized "conversation start" = min(messages.ts), kept current at
9240
+ // ingest and backfilled in migration 0010. The createdAt sort pages by this
9241
+ // (not first_seen_at, the ingest time) so the paged order matches the
9242
+ // reader's displayed createdAt (issue #143, reconciling ADR-0017's caveat).
9243
+ // Initialized to first_seen_at for a chat with no messages; since
9244
+ // first_seen_at is the ingest time and so >= every message ts, the running
9245
+ // min over (first_seen_at, message ts) equals min(messages.ts) once any
9246
+ // message exists, and first_seen_at when none — never NULL.
9247
+ createdAt: integer("created_at", { mode: "timestamp_ms" }),
9248
+ // Denormalized "most recent activity" = max(messages.ts), kept current at
9249
+ // ingest and backfilled in migration 0009. Lets the activity sort run as a
9250
+ // keyset index range scan instead of an aggregate that defeats the index
9251
+ // (ADR-0017). Initialized to first_seen_at for a chat with no messages, so
9252
+ // it is never NULL and always agrees with the reader's derived `updatedAt`.
9253
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }),
8621
9254
  project: text("project"),
8622
9255
  projectPath: text("project_path")
8623
9256
  },
8624
- (t) => [uniqueIndex("chats_agent_source_idx").on(t.agent, t.sourceId)]
9257
+ (t) => [
9258
+ uniqueIndex("chats_agent_source_idx").on(t.agent, t.sourceId),
9259
+ // Backs the server-side Project filter (`WHERE coalesce(project,'') IN …`).
9260
+ index("chats_project_idx").on(t.project),
9261
+ // Covering keyset indexes for the two server-side list sorts (ADR-0017):
9262
+ // (sortKey, id) so the ORDER BY + LIMIT is an index range scan either way.
9263
+ index("chats_created_keyset_idx").on(t.createdAt, t.id),
9264
+ index("chats_updated_keyset_idx").on(t.updatedAt, t.id)
9265
+ ]
8625
9266
  );
8626
9267
  var rawMessages = sqliteTable(
8627
9268
  "raw_messages",
@@ -8669,9 +9310,17 @@ var ingestionEvents = sqliteTable("ingestion_events", {
8669
9310
  // src/archive/read-seam.ts
8670
9311
  function createArchiveReadSeam(db) {
8671
9312
  return {
8672
- listChatRows() {
9313
+ listChatRows(opts) {
9314
+ const projects = opts?.projects;
9315
+ if (projects) {
9316
+ return db.select().from(chats).where(inArray(sql`coalesce(${chats.project}, '')`, projects)).all();
9317
+ }
8673
9318
  return db.select().from(chats).all();
8674
9319
  },
9320
+ listChatRowsByIds(ids) {
9321
+ if (ids.length === 0) return [];
9322
+ return db.select().from(chats).where(inArray(chats.id, ids)).all();
9323
+ },
8675
9324
  findChatBySourceId(sourceId) {
8676
9325
  return db.select().from(chats).where(eq(chats.sourceId, sourceId)).get() ?? null;
8677
9326
  },
@@ -8681,15 +9330,17 @@ function createArchiveReadSeam(db) {
8681
9330
  listMessagesByChat(agent, sourceId) {
8682
9331
  return db.select().from(messages).where(and(eq(messages.agent, agent), eq(messages.sourceId, sourceId))).orderBy(asc(messages.ts)).all();
8683
9332
  },
8684
- listChatTsRanges() {
9333
+ listChatTsRanges(opts) {
9334
+ const scope = opts?.sourceIds !== void 0 ? inArray(messages.sourceId, opts.sourceIds) : sql`1 = 1`;
8685
9335
  return db.select({
8686
9336
  agent: messages.agent,
8687
9337
  sourceId: messages.sourceId,
8688
9338
  minTs: sql`min(${messages.ts})`,
8689
9339
  maxTs: sql`max(${messages.ts})`
8690
- }).from(messages).groupBy(messages.agent, messages.sourceId).all();
9340
+ }).from(messages).where(scope).groupBy(messages.agent, messages.sourceId).all();
8691
9341
  },
8692
- listLatestRawSourcePaths() {
9342
+ listLatestRawSourcePaths(opts) {
9343
+ const scope = opts?.sourceIds !== void 0 ? inArray(rawMessages.sourceId, opts.sourceIds) : sql`1 = 1`;
8693
9344
  const ranked = db.select({
8694
9345
  agent: rawMessages.agent,
8695
9346
  sourceId: rawMessages.sourceId,
@@ -8697,14 +9348,18 @@ function createArchiveReadSeam(db) {
8697
9348
  rn: sql`row_number() over (partition by ${rawMessages.agent}, ${rawMessages.sourceId} order by ${rawMessages.ingestedAt} desc)`.as(
8698
9349
  "rn"
8699
9350
  )
8700
- }).from(rawMessages).as("ranked_raw");
9351
+ }).from(rawMessages).where(scope).as("ranked_raw");
8701
9352
  return db.select({
8702
9353
  agent: ranked.agent,
8703
9354
  sourceId: ranked.sourceId,
8704
9355
  sourcePath: ranked.sourcePath
8705
9356
  }).from(ranked).where(eq(ranked.rn, 1)).all();
8706
9357
  },
8707
- listFirstUserTexts() {
9358
+ listFirstUserTexts(opts) {
9359
+ const scope = opts?.sourceIds !== void 0 ? and(
9360
+ eq(messages.role, "user"),
9361
+ inArray(messages.sourceId, opts.sourceIds)
9362
+ ) : eq(messages.role, "user");
8708
9363
  const ranked = db.select({
8709
9364
  agent: messages.agent,
8710
9365
  sourceId: messages.sourceId,
@@ -8712,7 +9367,7 @@ function createArchiveReadSeam(db) {
8712
9367
  rn: sql`row_number() over (partition by ${messages.agent}, ${messages.sourceId} order by ${messages.ts} asc)`.as(
8713
9368
  "rn"
8714
9369
  )
8715
- }).from(messages).where(eq(messages.role, "user")).as("ranked_user");
9370
+ }).from(messages).where(scope).as("ranked_user");
8716
9371
  return db.select({
8717
9372
  agent: ranked.agent,
8718
9373
  sourceId: ranked.sourceId,
@@ -8795,6 +9450,12 @@ function createArchiveRepository({
8795
9450
  agent,
8796
9451
  sourceId,
8797
9452
  firstSeenAt,
9453
+ // Seed both denormalized sort keys to first_seen_at; message upserts
9454
+ // pull createdAt down (min) and push updatedAt up (max) as ts arrive.
9455
+ // first_seen_at is the ingest time and so >= every message ts, making
9456
+ // it a safe upper bound for the running min.
9457
+ createdAt: firstSeenAt,
9458
+ updatedAt: firstSeenAt,
8798
9459
  project: project ?? null,
8799
9460
  projectPath: projectPath ?? null
8800
9461
  }).run();
@@ -8831,6 +9492,10 @@ function createArchiveRepository({
8831
9492
  )
8832
9493
  ).get();
8833
9494
  const ts = parseTs(message.ts);
9495
+ db.update(chats).set({
9496
+ createdAt: sql`min(coalesce(${chats.createdAt}, ${ts.getTime()}), ${ts.getTime()})`,
9497
+ updatedAt: sql`max(coalesce(${chats.updatedAt}, 0), ${ts.getTime()})`
9498
+ }).where(and(eq(chats.agent, agent), eq(chats.sourceId, sourceId))).run();
8834
9499
  if (!existing2) {
8835
9500
  db.insert(messages).values({
8836
9501
  agent,
@@ -8951,34 +9616,92 @@ function createCheckpointRepository({
8951
9616
  }
8952
9617
 
8953
9618
  // src/metadata/repository.ts
8954
- import fs3 from "fs";
8955
- import path2 from "path";
9619
+ import fs4 from "fs";
9620
+ import path3 from "path";
8956
9621
 
8957
9622
  // src/metadata/schema.ts
8958
9623
  var schema_exports3 = {};
8959
9624
  __export(schema_exports3, {
8960
- chatsMeta: () => chatsMeta
9625
+ chatSortKeys: () => chatSortKeys,
9626
+ chatTags: () => chatTags,
9627
+ chatsMeta: () => chatsMeta,
9628
+ tags: () => tags
8961
9629
  });
8962
- var chatsMeta = sqliteTable("chats_meta", {
9630
+ var chatsMeta = sqliteTable(
9631
+ "chats_meta",
9632
+ {
9633
+ id: text("id").primaryKey(),
9634
+ isDeleted: integer("is_deleted", { mode: "boolean" }).notNull().default(false),
9635
+ customTitle: text("custom_title"),
9636
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
9637
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
9638
+ // Set when a chat is moved to Trash; null while active. Drives the Trash
9639
+ // view's independent "Deleted time" sort axis.
9640
+ deletedAt: integer("deleted_at", { mode: "timestamp_ms" })
9641
+ },
9642
+ (table) => [
9643
+ // Covering keyset index for the Trash view's deleted-time axis (#145,
9644
+ // ADR-0017): `(deleted_at, id)` so the page's ORDER BY + LIMIT is an index
9645
+ // range scan, not a full-trash sort. Partial on `deleted_at IS NOT NULL`,
9646
+ // which holds exactly for trashed rows (restore nulls it), so the index
9647
+ // carries only the Trash set and the page query's matching predicate lets
9648
+ // the planner use it.
9649
+ index("chats_meta_deleted_at_idx").on(table.deletedAt, table.id).where(sql`${table.deletedAt} is not null`)
9650
+ ]
9651
+ );
9652
+ var chatSortKeys = sqliteTable(
9653
+ "chat_sort_keys",
9654
+ {
9655
+ id: text("id").primaryKey(),
9656
+ textKey: text("text_key"),
9657
+ sortKey: text("sort_key")
9658
+ },
9659
+ (table) => [
9660
+ // Covering keyset index for the Title axis (ADR-0017/0019): `(sort_key, id)`
9661
+ // so `ORDER BY sort_key, id` + LIMIT is an index range scan either
9662
+ // direction, exactly like the time axes — never a full-table sort.
9663
+ index("chat_sort_keys_sort_key_idx").on(table.sortKey, table.id)
9664
+ ]
9665
+ );
9666
+ var tags = sqliteTable("tags", {
8963
9667
  id: text("id").primaryKey(),
8964
- isDeleted: integer("is_deleted", { mode: "boolean" }).notNull().default(false),
8965
- customTitle: text("custom_title"),
9668
+ name: text("name").notNull(),
9669
+ color: text("color").notNull(),
8966
9670
  createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
8967
- updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
8968
- // Set when a chat is moved to Trash; null while active. Drives the Trash
8969
- // view's independent "Deleted time" sort axis.
8970
- deletedAt: integer("deleted_at", { mode: "timestamp_ms" })
9671
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull()
8971
9672
  });
9673
+ var chatTags = sqliteTable(
9674
+ "chat_tags",
9675
+ {
9676
+ chatId: text("chat_id").notNull(),
9677
+ tagId: text("tag_id").notNull().references(() => tags.id, { onDelete: "cascade" })
9678
+ },
9679
+ (table) => [
9680
+ primaryKey({ columns: [table.chatId, table.tagId] }),
9681
+ index("chat_tags_tag_id_idx").on(table.tagId)
9682
+ ]
9683
+ );
9684
+
9685
+ // src/metadata/title-sort-key.ts
9686
+ function encodeNumericRun(run2) {
9687
+ const core = run2.replace(/^0+(?=\d)/, "");
9688
+ const len = String(core.length);
9689
+ return String(len.length) + len + core;
9690
+ }
9691
+ function computeSortKey(input) {
9692
+ const folded = input.normalize("NFKD").toLowerCase().replace(/\p{M}+/gu, "");
9693
+ return folded.replace(/\d+/g, encodeNumericRun);
9694
+ }
8972
9695
 
8973
9696
  // src/metadata/repository.ts
8974
9697
  var DB_FILE = "metadata.db";
8975
9698
  var LEGACY_DB_FILE = "data.db";
8976
9699
  function migrateLegacyDbFile(dataDir2) {
8977
- const legacy = path2.join(dataDir2, LEGACY_DB_FILE);
8978
- const current = path2.join(dataDir2, DB_FILE);
8979
- if (!fs3.existsSync(legacy)) return;
8980
- if (fs3.existsSync(current)) return;
8981
- fs3.renameSync(legacy, current);
9700
+ const legacy = path3.join(dataDir2, LEGACY_DB_FILE);
9701
+ const current = path3.join(dataDir2, DB_FILE);
9702
+ if (!fs4.existsSync(legacy)) return;
9703
+ if (fs4.existsSync(current)) return;
9704
+ fs4.renameSync(legacy, current);
8982
9705
  }
8983
9706
  var CLAUDE_CODE_AGENT = "claude-code";
8984
9707
  function createMetadataRepository({
@@ -9050,6 +9773,27 @@ function createMetadataRepository({
9050
9773
  target: chatsMeta.id,
9051
9774
  set: { customTitle: title, updatedAt: now }
9052
9775
  }).run();
9776
+ if (title !== null) {
9777
+ const sortKey = computeSortKey(title);
9778
+ db.insert(chatSortKeys).values({ id: internalId, sortKey }).onConflictDoUpdate({ target: chatSortKeys.id, set: { sortKey } }).run();
9779
+ } else {
9780
+ db.update(chatSortKeys).set({ sortKey: sql`${chatSortKeys.textKey}` }).where(eq(chatSortKeys.id, internalId)).run();
9781
+ }
9782
+ },
9783
+ getTitleSortKey(internalId) {
9784
+ const row = db.select({
9785
+ textKey: chatSortKeys.textKey,
9786
+ sortKey: chatSortKeys.sortKey
9787
+ }).from(chatSortKeys).where(eq(chatSortKeys.id, internalId)).get();
9788
+ return row ?? null;
9789
+ },
9790
+ setTitleTextKey(internalId, textKey) {
9791
+ const customTitle = db.select({ customTitle: chatsMeta.customTitle }).from(chatsMeta).where(eq(chatsMeta.id, internalId)).get()?.customTitle;
9792
+ const sortKey = customTitle && customTitle.trim() ? computeSortKey(customTitle) : textKey;
9793
+ db.insert(chatSortKeys).values({ id: internalId, textKey, sortKey }).onConflictDoUpdate({
9794
+ target: chatSortKeys.id,
9795
+ set: { textKey, sortKey }
9796
+ }).run();
9053
9797
  }
9054
9798
  };
9055
9799
  }
@@ -9070,8 +9814,98 @@ function rekeyLegacyRows(sqlite, db, lookupInternalId, ensureChat) {
9070
9814
  sqlite.pragma(`user_version = ${REKEY_USER_VERSION}`);
9071
9815
  }
9072
9816
 
9817
+ // src/metadata/tags.ts
9818
+ import { randomUUID } from "crypto";
9819
+ var DB_FILE2 = "metadata.db";
9820
+ function createTagRepository({
9821
+ dataDir: dataDir2
9822
+ }) {
9823
+ const { db } = openStore({
9824
+ dataDir: dataDir2,
9825
+ dbFile: DB_FILE2,
9826
+ callerUrl: import.meta.url,
9827
+ migrationsSubdir: "drizzle",
9828
+ schema: schema_exports3
9829
+ });
9830
+ function toTag(row) {
9831
+ return { id: row.id, name: row.name, color: row.color };
9832
+ }
9833
+ function assertColor(color) {
9834
+ if (!isColorToken(color)) {
9835
+ throw new Error(`Invalid tag color token: ${String(color)}`);
9836
+ }
9837
+ }
9838
+ return {
9839
+ createTag(name, color) {
9840
+ assertColor(color);
9841
+ const now = /* @__PURE__ */ new Date();
9842
+ const row = {
9843
+ id: randomUUID(),
9844
+ name,
9845
+ color,
9846
+ createdAt: now,
9847
+ updatedAt: now
9848
+ };
9849
+ db.insert(tags).values(row).run();
9850
+ return toTag(row);
9851
+ },
9852
+ listTags() {
9853
+ return db.select({ id: tags.id, name: tags.name, color: tags.color }).from(tags).all().map(toTag);
9854
+ },
9855
+ getTag(id) {
9856
+ const row = db.select({ id: tags.id, name: tags.name, color: tags.color }).from(tags).where(eq(tags.id, id)).get();
9857
+ return row ? toTag(row) : null;
9858
+ },
9859
+ renameTag(id, name) {
9860
+ db.update(tags).set({ name, updatedAt: /* @__PURE__ */ new Date() }).where(eq(tags.id, id)).run();
9861
+ },
9862
+ recolorTag(id, color) {
9863
+ assertColor(color);
9864
+ db.update(tags).set({ color, updatedAt: /* @__PURE__ */ new Date() }).where(eq(tags.id, id)).run();
9865
+ },
9866
+ deleteTag(id) {
9867
+ return db.transaction((tx) => {
9868
+ const removedFromChats = tx.select({ chatId: chatTags.chatId }).from(chatTags).where(eq(chatTags.tagId, id)).all().length;
9869
+ tx.delete(chatTags).where(eq(chatTags.tagId, id)).run();
9870
+ tx.delete(tags).where(eq(tags.id, id)).run();
9871
+ return { removedFromChats };
9872
+ });
9873
+ },
9874
+ assignTag(chatId, tagId) {
9875
+ db.insert(chatTags).values({ chatId, tagId }).onConflictDoNothing().run();
9876
+ },
9877
+ removeTag(chatId, tagId) {
9878
+ db.delete(chatTags).where(and(eq(chatTags.chatId, chatId), eq(chatTags.tagId, tagId))).run();
9879
+ },
9880
+ listTagsForChat(chatId) {
9881
+ return db.select({ id: tags.id, name: tags.name, color: tags.color }).from(chatTags).innerJoin(tags, eq(chatTags.tagId, tags.id)).where(eq(chatTags.chatId, chatId)).all().map(toTag);
9882
+ },
9883
+ listTagsByChat(chatIds) {
9884
+ const base = db.select({
9885
+ chatId: chatTags.chatId,
9886
+ id: tags.id,
9887
+ name: tags.name,
9888
+ color: tags.color
9889
+ }).from(chatTags).innerJoin(tags, eq(chatTags.tagId, tags.id));
9890
+ const rows = chatIds === void 0 ? base.all() : chatIds.length === 0 ? [] : base.where(inArray(chatTags.chatId, chatIds)).all();
9891
+ const byChat = /* @__PURE__ */ new Map();
9892
+ for (const row of rows) {
9893
+ const list = byChat.get(row.chatId) ?? [];
9894
+ list.push(toTag(row));
9895
+ byChat.set(row.chatId, list);
9896
+ }
9897
+ return byChat;
9898
+ },
9899
+ listChatIdsWithAllTags(tagIds) {
9900
+ const distinct = [...new Set(tagIds)];
9901
+ if (distinct.length === 0) return [];
9902
+ 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);
9903
+ }
9904
+ };
9905
+ }
9906
+
9073
9907
  // src/ingestion/ingest.ts
9074
- import fs4 from "fs";
9908
+ import fs5 from "fs";
9075
9909
  async function runIngestion(opts) {
9076
9910
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
9077
9911
  const result = {
@@ -9131,7 +9965,7 @@ async function runIngestion(opts) {
9131
9965
  }
9132
9966
  function safeStat(p) {
9133
9967
  try {
9134
- return fs4.statSync(p);
9968
+ return fs5.statSync(p);
9135
9969
  } catch {
9136
9970
  return null;
9137
9971
  }
@@ -9240,7 +10074,7 @@ var ReaddirpStream = class extends Readable3 {
9240
10074
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
9241
10075
  const statMethod = opts.lstat ? lstat : stat;
9242
10076
  if (wantBigintFsStats) {
9243
- this._stat = (path5) => statMethod(path5, { bigint: true });
10077
+ this._stat = (path8) => statMethod(path8, { bigint: true });
9244
10078
  } else {
9245
10079
  this._stat = statMethod;
9246
10080
  }
@@ -9265,8 +10099,8 @@ var ReaddirpStream = class extends Readable3 {
9265
10099
  const par = this.parent;
9266
10100
  const fil = par && par.files;
9267
10101
  if (fil && fil.length > 0) {
9268
- const { path: path5, depth } = par;
9269
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path5));
10102
+ const { path: path8, depth } = par;
10103
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path8));
9270
10104
  const awaited = await Promise.all(slice);
9271
10105
  for (const entry of awaited) {
9272
10106
  if (!entry)
@@ -9306,20 +10140,20 @@ var ReaddirpStream = class extends Readable3 {
9306
10140
  this.reading = false;
9307
10141
  }
9308
10142
  }
9309
- async _exploreDir(path5, depth) {
10143
+ async _exploreDir(path8, depth) {
9310
10144
  let files;
9311
10145
  try {
9312
- files = await readdir(path5, this._rdOptions);
10146
+ files = await readdir(path8, this._rdOptions);
9313
10147
  } catch (error) {
9314
10148
  this._onError(error);
9315
10149
  }
9316
- return { files, depth, path: path5 };
10150
+ return { files, depth, path: path8 };
9317
10151
  }
9318
- async _formatEntry(dirent, path5) {
10152
+ async _formatEntry(dirent, path8) {
9319
10153
  let entry;
9320
10154
  const basename3 = this._isDirent ? dirent.name : dirent;
9321
10155
  try {
9322
- const fullPath = presolve(pjoin(path5, basename3));
10156
+ const fullPath = presolve(pjoin(path8, basename3));
9323
10157
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
9324
10158
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
9325
10159
  } catch (err) {
@@ -9719,16 +10553,16 @@ var delFromSet = (main, prop, item) => {
9719
10553
  };
9720
10554
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
9721
10555
  var FsWatchInstances = /* @__PURE__ */ new Map();
9722
- function createFsWatchInstance(path5, options, listener, errHandler, emitRaw) {
10556
+ function createFsWatchInstance(path8, options, listener, errHandler, emitRaw) {
9723
10557
  const handleEvent = (rawEvent, evPath) => {
9724
- listener(path5);
9725
- emitRaw(rawEvent, evPath, { watchedPath: path5 });
9726
- if (evPath && path5 !== evPath) {
9727
- fsWatchBroadcast(sp.resolve(path5, evPath), KEY_LISTENERS, sp.join(path5, evPath));
10558
+ listener(path8);
10559
+ emitRaw(rawEvent, evPath, { watchedPath: path8 });
10560
+ if (evPath && path8 !== evPath) {
10561
+ fsWatchBroadcast(sp.resolve(path8, evPath), KEY_LISTENERS, sp.join(path8, evPath));
9728
10562
  }
9729
10563
  };
9730
10564
  try {
9731
- return fs_watch(path5, {
10565
+ return fs_watch(path8, {
9732
10566
  persistent: options.persistent
9733
10567
  }, handleEvent);
9734
10568
  } catch (error) {
@@ -9744,12 +10578,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
9744
10578
  listener(val1, val2, val3);
9745
10579
  });
9746
10580
  };
9747
- var setFsWatchListener = (path5, fullPath, options, handlers) => {
10581
+ var setFsWatchListener = (path8, fullPath, options, handlers) => {
9748
10582
  const { listener, errHandler, rawEmitter } = handlers;
9749
10583
  let cont = FsWatchInstances.get(fullPath);
9750
10584
  let watcher2;
9751
10585
  if (!options.persistent) {
9752
- watcher2 = createFsWatchInstance(path5, options, listener, errHandler, rawEmitter);
10586
+ watcher2 = createFsWatchInstance(path8, options, listener, errHandler, rawEmitter);
9753
10587
  if (!watcher2)
9754
10588
  return;
9755
10589
  return watcher2.close.bind(watcher2);
@@ -9760,7 +10594,7 @@ var setFsWatchListener = (path5, fullPath, options, handlers) => {
9760
10594
  addAndConvert(cont, KEY_RAW, rawEmitter);
9761
10595
  } else {
9762
10596
  watcher2 = createFsWatchInstance(
9763
- path5,
10597
+ path8,
9764
10598
  options,
9765
10599
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
9766
10600
  errHandler,
@@ -9775,7 +10609,7 @@ var setFsWatchListener = (path5, fullPath, options, handlers) => {
9775
10609
  cont.watcherUnusable = true;
9776
10610
  if (isWindows && error.code === "EPERM") {
9777
10611
  try {
9778
- const fd = await open(path5, "r");
10612
+ const fd = await open(path8, "r");
9779
10613
  await fd.close();
9780
10614
  broadcastErr(error);
9781
10615
  } catch (err) {
@@ -9806,7 +10640,7 @@ var setFsWatchListener = (path5, fullPath, options, handlers) => {
9806
10640
  };
9807
10641
  };
9808
10642
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
9809
- var setFsWatchFileListener = (path5, fullPath, options, handlers) => {
10643
+ var setFsWatchFileListener = (path8, fullPath, options, handlers) => {
9810
10644
  const { listener, rawEmitter } = handlers;
9811
10645
  let cont = FsWatchFileInstances.get(fullPath);
9812
10646
  const copts = cont && cont.options;
@@ -9828,7 +10662,7 @@ var setFsWatchFileListener = (path5, fullPath, options, handlers) => {
9828
10662
  });
9829
10663
  const currmtime = curr.mtimeMs;
9830
10664
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
9831
- foreach(cont.listeners, (listener2) => listener2(path5, curr));
10665
+ foreach(cont.listeners, (listener2) => listener2(path8, curr));
9832
10666
  }
9833
10667
  })
9834
10668
  };
@@ -9858,13 +10692,13 @@ var NodeFsHandler = class {
9858
10692
  * @param listener on fs change
9859
10693
  * @returns closer for the watcher instance
9860
10694
  */
9861
- _watchWithNodeFs(path5, listener) {
10695
+ _watchWithNodeFs(path8, listener) {
9862
10696
  const opts = this.fsw.options;
9863
- const directory = sp.dirname(path5);
9864
- const basename3 = sp.basename(path5);
10697
+ const directory = sp.dirname(path8);
10698
+ const basename3 = sp.basename(path8);
9865
10699
  const parent = this.fsw._getWatchedDir(directory);
9866
10700
  parent.add(basename3);
9867
- const absolutePath = sp.resolve(path5);
10701
+ const absolutePath = sp.resolve(path8);
9868
10702
  const options = {
9869
10703
  persistent: opts.persistent
9870
10704
  };
@@ -9874,12 +10708,12 @@ var NodeFsHandler = class {
9874
10708
  if (opts.usePolling) {
9875
10709
  const enableBin = opts.interval !== opts.binaryInterval;
9876
10710
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
9877
- closer = setFsWatchFileListener(path5, absolutePath, options, {
10711
+ closer = setFsWatchFileListener(path8, absolutePath, options, {
9878
10712
  listener,
9879
10713
  rawEmitter: this.fsw._emitRaw
9880
10714
  });
9881
10715
  } else {
9882
- closer = setFsWatchListener(path5, absolutePath, options, {
10716
+ closer = setFsWatchListener(path8, absolutePath, options, {
9883
10717
  listener,
9884
10718
  errHandler: this._boundHandleError,
9885
10719
  rawEmitter: this.fsw._emitRaw
@@ -9901,7 +10735,7 @@ var NodeFsHandler = class {
9901
10735
  let prevStats = stats;
9902
10736
  if (parent.has(basename3))
9903
10737
  return;
9904
- const listener = async (path5, newStats) => {
10738
+ const listener = async (path8, newStats) => {
9905
10739
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
9906
10740
  return;
9907
10741
  if (!newStats || newStats.mtimeMs === 0) {
@@ -9915,11 +10749,11 @@ var NodeFsHandler = class {
9915
10749
  this.fsw._emit(EV.CHANGE, file, newStats2);
9916
10750
  }
9917
10751
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
9918
- this.fsw._closeFile(path5);
10752
+ this.fsw._closeFile(path8);
9919
10753
  prevStats = newStats2;
9920
10754
  const closer2 = this._watchWithNodeFs(file, listener);
9921
10755
  if (closer2)
9922
- this.fsw._addPathCloser(path5, closer2);
10756
+ this.fsw._addPathCloser(path8, closer2);
9923
10757
  } else {
9924
10758
  prevStats = newStats2;
9925
10759
  }
@@ -9951,7 +10785,7 @@ var NodeFsHandler = class {
9951
10785
  * @param item basename of this item
9952
10786
  * @returns true if no more processing is needed for this entry.
9953
10787
  */
9954
- async _handleSymlink(entry, directory, path5, item) {
10788
+ async _handleSymlink(entry, directory, path8, item) {
9955
10789
  if (this.fsw.closed) {
9956
10790
  return;
9957
10791
  }
@@ -9961,7 +10795,7 @@ var NodeFsHandler = class {
9961
10795
  this.fsw._incrReadyCount();
9962
10796
  let linkPath;
9963
10797
  try {
9964
- linkPath = await fsrealpath(path5);
10798
+ linkPath = await fsrealpath(path8);
9965
10799
  } catch (e) {
9966
10800
  this.fsw._emitReady();
9967
10801
  return true;
@@ -9971,12 +10805,12 @@ var NodeFsHandler = class {
9971
10805
  if (dir.has(item)) {
9972
10806
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
9973
10807
  this.fsw._symlinkPaths.set(full, linkPath);
9974
- this.fsw._emit(EV.CHANGE, path5, entry.stats);
10808
+ this.fsw._emit(EV.CHANGE, path8, entry.stats);
9975
10809
  }
9976
10810
  } else {
9977
10811
  dir.add(item);
9978
10812
  this.fsw._symlinkPaths.set(full, linkPath);
9979
- this.fsw._emit(EV.ADD, path5, entry.stats);
10813
+ this.fsw._emit(EV.ADD, path8, entry.stats);
9980
10814
  }
9981
10815
  this.fsw._emitReady();
9982
10816
  return true;
@@ -9994,39 +10828,39 @@ var NodeFsHandler = class {
9994
10828
  return;
9995
10829
  const previous = this.fsw._getWatchedDir(wh.path);
9996
10830
  const current = /* @__PURE__ */ new Set();
9997
- let stream = this.fsw._readdirp(directory, {
10831
+ let stream2 = this.fsw._readdirp(directory, {
9998
10832
  fileFilter: (entry) => wh.filterPath(entry),
9999
10833
  directoryFilter: (entry) => wh.filterDir(entry)
10000
10834
  });
10001
- if (!stream)
10835
+ if (!stream2)
10002
10836
  return;
10003
- stream.on(STR_DATA, async (entry) => {
10837
+ stream2.on(STR_DATA, async (entry) => {
10004
10838
  if (this.fsw.closed) {
10005
- stream = void 0;
10839
+ stream2 = void 0;
10006
10840
  return;
10007
10841
  }
10008
10842
  const item = entry.path;
10009
- let path5 = sp.join(directory, item);
10843
+ let path8 = sp.join(directory, item);
10010
10844
  current.add(item);
10011
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path5, item)) {
10845
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path8, item)) {
10012
10846
  return;
10013
10847
  }
10014
10848
  if (this.fsw.closed) {
10015
- stream = void 0;
10849
+ stream2 = void 0;
10016
10850
  return;
10017
10851
  }
10018
10852
  if (item === target || !target && !previous.has(item)) {
10019
10853
  this.fsw._incrReadyCount();
10020
- path5 = sp.join(dir, sp.relative(dir, path5));
10021
- this._addToNodeFs(path5, initialAdd, wh, depth + 1);
10854
+ path8 = sp.join(dir, sp.relative(dir, path8));
10855
+ this._addToNodeFs(path8, initialAdd, wh, depth + 1);
10022
10856
  }
10023
10857
  }).on(EV.ERROR, this._boundHandleError);
10024
10858
  return new Promise((resolve3, reject) => {
10025
- if (!stream)
10859
+ if (!stream2)
10026
10860
  return reject();
10027
- stream.once(STR_END, () => {
10861
+ stream2.once(STR_END, () => {
10028
10862
  if (this.fsw.closed) {
10029
- stream = void 0;
10863
+ stream2 = void 0;
10030
10864
  return;
10031
10865
  }
10032
10866
  const wasThrottled = throttler ? throttler.clear() : false;
@@ -10036,7 +10870,7 @@ var NodeFsHandler = class {
10036
10870
  }).forEach((item) => {
10037
10871
  this.fsw._remove(directory, item);
10038
10872
  });
10039
- stream = void 0;
10873
+ stream2 = void 0;
10040
10874
  if (wasThrottled)
10041
10875
  this._handleRead(directory, false, wh, target, dir, depth, throttler);
10042
10876
  });
@@ -10087,13 +10921,13 @@ var NodeFsHandler = class {
10087
10921
  * @param depth Child path actually targeted for watch
10088
10922
  * @param target Child path actually targeted for watch
10089
10923
  */
10090
- async _addToNodeFs(path5, initialAdd, priorWh, depth, target) {
10924
+ async _addToNodeFs(path8, initialAdd, priorWh, depth, target) {
10091
10925
  const ready = this.fsw._emitReady;
10092
- if (this.fsw._isIgnored(path5) || this.fsw.closed) {
10926
+ if (this.fsw._isIgnored(path8) || this.fsw.closed) {
10093
10927
  ready();
10094
10928
  return false;
10095
10929
  }
10096
- const wh = this.fsw._getWatchHelpers(path5);
10930
+ const wh = this.fsw._getWatchHelpers(path8);
10097
10931
  if (priorWh) {
10098
10932
  wh.filterPath = (entry) => priorWh.filterPath(entry);
10099
10933
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -10109,8 +10943,8 @@ var NodeFsHandler = class {
10109
10943
  const follow = this.fsw.options.followSymlinks;
10110
10944
  let closer;
10111
10945
  if (stats.isDirectory()) {
10112
- const absPath = sp.resolve(path5);
10113
- const targetPath = follow ? await fsrealpath(path5) : path5;
10946
+ const absPath = sp.resolve(path8);
10947
+ const targetPath = follow ? await fsrealpath(path8) : path8;
10114
10948
  if (this.fsw.closed)
10115
10949
  return;
10116
10950
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -10120,29 +10954,29 @@ var NodeFsHandler = class {
10120
10954
  this.fsw._symlinkPaths.set(absPath, targetPath);
10121
10955
  }
10122
10956
  } else if (stats.isSymbolicLink()) {
10123
- const targetPath = follow ? await fsrealpath(path5) : path5;
10957
+ const targetPath = follow ? await fsrealpath(path8) : path8;
10124
10958
  if (this.fsw.closed)
10125
10959
  return;
10126
10960
  const parent = sp.dirname(wh.watchPath);
10127
10961
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
10128
10962
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
10129
- closer = await this._handleDir(parent, stats, initialAdd, depth, path5, wh, targetPath);
10963
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path8, wh, targetPath);
10130
10964
  if (this.fsw.closed)
10131
10965
  return;
10132
10966
  if (targetPath !== void 0) {
10133
- this.fsw._symlinkPaths.set(sp.resolve(path5), targetPath);
10967
+ this.fsw._symlinkPaths.set(sp.resolve(path8), targetPath);
10134
10968
  }
10135
10969
  } else {
10136
10970
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
10137
10971
  }
10138
10972
  ready();
10139
10973
  if (closer)
10140
- this.fsw._addPathCloser(path5, closer);
10974
+ this.fsw._addPathCloser(path8, closer);
10141
10975
  return false;
10142
10976
  } catch (error) {
10143
10977
  if (this.fsw._handleError(error)) {
10144
10978
  ready();
10145
- return path5;
10979
+ return path8;
10146
10980
  }
10147
10981
  }
10148
10982
  }
@@ -10185,24 +11019,24 @@ function createPattern(matcher) {
10185
11019
  }
10186
11020
  return () => false;
10187
11021
  }
10188
- function normalizePath(path5) {
10189
- if (typeof path5 !== "string")
11022
+ function normalizePath(path8) {
11023
+ if (typeof path8 !== "string")
10190
11024
  throw new Error("string expected");
10191
- path5 = sp2.normalize(path5);
10192
- path5 = path5.replace(/\\/g, "/");
11025
+ path8 = sp2.normalize(path8);
11026
+ path8 = path8.replace(/\\/g, "/");
10193
11027
  let prepend = false;
10194
- if (path5.startsWith("//"))
11028
+ if (path8.startsWith("//"))
10195
11029
  prepend = true;
10196
- path5 = path5.replace(DOUBLE_SLASH_RE, "/");
11030
+ path8 = path8.replace(DOUBLE_SLASH_RE, "/");
10197
11031
  if (prepend)
10198
- path5 = "/" + path5;
10199
- return path5;
11032
+ path8 = "/" + path8;
11033
+ return path8;
10200
11034
  }
10201
11035
  function matchPatterns(patterns, testString, stats) {
10202
- const path5 = normalizePath(testString);
10203
- for (let index = 0; index < patterns.length; index++) {
10204
- const pattern = patterns[index];
10205
- if (pattern(path5, stats)) {
11036
+ const path8 = normalizePath(testString);
11037
+ for (let index2 = 0; index2 < patterns.length; index2++) {
11038
+ const pattern = patterns[index2];
11039
+ if (pattern(path8, stats)) {
10206
11040
  return true;
10207
11041
  }
10208
11042
  }
@@ -10240,19 +11074,19 @@ var toUnix = (string) => {
10240
11074
  }
10241
11075
  return str;
10242
11076
  };
10243
- var normalizePathToUnix = (path5) => toUnix(sp2.normalize(toUnix(path5)));
10244
- var normalizeIgnored = (cwd = "") => (path5) => {
10245
- if (typeof path5 === "string") {
10246
- return normalizePathToUnix(sp2.isAbsolute(path5) ? path5 : sp2.join(cwd, path5));
11077
+ var normalizePathToUnix = (path8) => toUnix(sp2.normalize(toUnix(path8)));
11078
+ var normalizeIgnored = (cwd = "") => (path8) => {
11079
+ if (typeof path8 === "string") {
11080
+ return normalizePathToUnix(sp2.isAbsolute(path8) ? path8 : sp2.join(cwd, path8));
10247
11081
  } else {
10248
- return path5;
11082
+ return path8;
10249
11083
  }
10250
11084
  };
10251
- var getAbsolutePath = (path5, cwd) => {
10252
- if (sp2.isAbsolute(path5)) {
10253
- return path5;
11085
+ var getAbsolutePath = (path8, cwd) => {
11086
+ if (sp2.isAbsolute(path8)) {
11087
+ return path8;
10254
11088
  }
10255
- return sp2.join(cwd, path5);
11089
+ return sp2.join(cwd, path8);
10256
11090
  };
10257
11091
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
10258
11092
  var DirEntry = class {
@@ -10317,10 +11151,10 @@ var WatchHelper = class {
10317
11151
  dirParts;
10318
11152
  followSymlinks;
10319
11153
  statMethod;
10320
- constructor(path5, follow, fsw) {
11154
+ constructor(path8, follow, fsw) {
10321
11155
  this.fsw = fsw;
10322
- const watchPath = path5;
10323
- this.path = path5 = path5.replace(REPLACER_RE, "");
11156
+ const watchPath = path8;
11157
+ this.path = path8 = path8.replace(REPLACER_RE, "");
10324
11158
  this.watchPath = watchPath;
10325
11159
  this.fullWatchPath = sp2.resolve(watchPath);
10326
11160
  this.dirParts = [];
@@ -10460,20 +11294,20 @@ var FSWatcher = class extends EventEmitter {
10460
11294
  this._closePromise = void 0;
10461
11295
  let paths = unifyPaths(paths_);
10462
11296
  if (cwd) {
10463
- paths = paths.map((path5) => {
10464
- const absPath = getAbsolutePath(path5, cwd);
11297
+ paths = paths.map((path8) => {
11298
+ const absPath = getAbsolutePath(path8, cwd);
10465
11299
  return absPath;
10466
11300
  });
10467
11301
  }
10468
- paths.forEach((path5) => {
10469
- this._removeIgnoredPath(path5);
11302
+ paths.forEach((path8) => {
11303
+ this._removeIgnoredPath(path8);
10470
11304
  });
10471
11305
  this._userIgnored = void 0;
10472
11306
  if (!this._readyCount)
10473
11307
  this._readyCount = 0;
10474
11308
  this._readyCount += paths.length;
10475
- Promise.all(paths.map(async (path5) => {
10476
- const res = await this._nodeFsHandler._addToNodeFs(path5, !_internal, void 0, 0, _origAdd);
11309
+ Promise.all(paths.map(async (path8) => {
11310
+ const res = await this._nodeFsHandler._addToNodeFs(path8, !_internal, void 0, 0, _origAdd);
10477
11311
  if (res)
10478
11312
  this._emitReady();
10479
11313
  return res;
@@ -10495,17 +11329,17 @@ var FSWatcher = class extends EventEmitter {
10495
11329
  return this;
10496
11330
  const paths = unifyPaths(paths_);
10497
11331
  const { cwd } = this.options;
10498
- paths.forEach((path5) => {
10499
- if (!sp2.isAbsolute(path5) && !this._closers.has(path5)) {
11332
+ paths.forEach((path8) => {
11333
+ if (!sp2.isAbsolute(path8) && !this._closers.has(path8)) {
10500
11334
  if (cwd)
10501
- path5 = sp2.join(cwd, path5);
10502
- path5 = sp2.resolve(path5);
11335
+ path8 = sp2.join(cwd, path8);
11336
+ path8 = sp2.resolve(path8);
10503
11337
  }
10504
- this._closePath(path5);
10505
- this._addIgnoredPath(path5);
10506
- if (this._watched.has(path5)) {
11338
+ this._closePath(path8);
11339
+ this._addIgnoredPath(path8);
11340
+ if (this._watched.has(path8)) {
10507
11341
  this._addIgnoredPath({
10508
- path: path5,
11342
+ path: path8,
10509
11343
  recursive: true
10510
11344
  });
10511
11345
  }
@@ -10528,7 +11362,7 @@ var FSWatcher = class extends EventEmitter {
10528
11362
  if (promise instanceof Promise)
10529
11363
  closers.push(promise);
10530
11364
  }));
10531
- this._streams.forEach((stream) => stream.destroy());
11365
+ this._streams.forEach((stream2) => stream2.destroy());
10532
11366
  this._userIgnored = void 0;
10533
11367
  this._readyCount = 0;
10534
11368
  this._readyEmitted = false;
@@ -10549,8 +11383,8 @@ var FSWatcher = class extends EventEmitter {
10549
11383
  const watchList = {};
10550
11384
  this._watched.forEach((entry, dir) => {
10551
11385
  const key = this.options.cwd ? sp2.relative(this.options.cwd, dir) : dir;
10552
- const index = key || ONE_DOT;
10553
- watchList[index] = entry.getChildren().sort();
11386
+ const index2 = key || ONE_DOT;
11387
+ watchList[index2] = entry.getChildren().sort();
10554
11388
  });
10555
11389
  return watchList;
10556
11390
  }
@@ -10569,38 +11403,38 @@ var FSWatcher = class extends EventEmitter {
10569
11403
  * @param stats arguments to be passed with event
10570
11404
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
10571
11405
  */
10572
- async _emit(event, path5, stats) {
11406
+ async _emit(event, path8, stats) {
10573
11407
  if (this.closed)
10574
11408
  return;
10575
11409
  const opts = this.options;
10576
11410
  if (isWindows)
10577
- path5 = sp2.normalize(path5);
11411
+ path8 = sp2.normalize(path8);
10578
11412
  if (opts.cwd)
10579
- path5 = sp2.relative(opts.cwd, path5);
10580
- const args = [path5];
11413
+ path8 = sp2.relative(opts.cwd, path8);
11414
+ const args = [path8];
10581
11415
  if (stats != null)
10582
11416
  args.push(stats);
10583
11417
  const awf = opts.awaitWriteFinish;
10584
11418
  let pw;
10585
- if (awf && (pw = this._pendingWrites.get(path5))) {
11419
+ if (awf && (pw = this._pendingWrites.get(path8))) {
10586
11420
  pw.lastChange = /* @__PURE__ */ new Date();
10587
11421
  return this;
10588
11422
  }
10589
11423
  if (opts.atomic) {
10590
11424
  if (event === EVENTS.UNLINK) {
10591
- this._pendingUnlinks.set(path5, [event, ...args]);
11425
+ this._pendingUnlinks.set(path8, [event, ...args]);
10592
11426
  setTimeout(() => {
10593
- this._pendingUnlinks.forEach((entry, path6) => {
11427
+ this._pendingUnlinks.forEach((entry, path9) => {
10594
11428
  this.emit(...entry);
10595
11429
  this.emit(EVENTS.ALL, ...entry);
10596
- this._pendingUnlinks.delete(path6);
11430
+ this._pendingUnlinks.delete(path9);
10597
11431
  });
10598
11432
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
10599
11433
  return this;
10600
11434
  }
10601
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path5)) {
11435
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path8)) {
10602
11436
  event = EVENTS.CHANGE;
10603
- this._pendingUnlinks.delete(path5);
11437
+ this._pendingUnlinks.delete(path8);
10604
11438
  }
10605
11439
  }
10606
11440
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -10618,16 +11452,16 @@ var FSWatcher = class extends EventEmitter {
10618
11452
  this.emitWithAll(event, args);
10619
11453
  }
10620
11454
  };
10621
- this._awaitWriteFinish(path5, awf.stabilityThreshold, event, awfEmit);
11455
+ this._awaitWriteFinish(path8, awf.stabilityThreshold, event, awfEmit);
10622
11456
  return this;
10623
11457
  }
10624
11458
  if (event === EVENTS.CHANGE) {
10625
- const isThrottled = !this._throttle(EVENTS.CHANGE, path5, 50);
11459
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path8, 50);
10626
11460
  if (isThrottled)
10627
11461
  return this;
10628
11462
  }
10629
11463
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
10630
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path5) : path5;
11464
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path8) : path8;
10631
11465
  let stats2;
10632
11466
  try {
10633
11467
  stats2 = await stat3(fullPath);
@@ -10658,23 +11492,23 @@ var FSWatcher = class extends EventEmitter {
10658
11492
  * @param timeout duration of time to suppress duplicate actions
10659
11493
  * @returns tracking object or false if action should be suppressed
10660
11494
  */
10661
- _throttle(actionType, path5, timeout) {
11495
+ _throttle(actionType, path8, timeout) {
10662
11496
  if (!this._throttled.has(actionType)) {
10663
11497
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
10664
11498
  }
10665
11499
  const action2 = this._throttled.get(actionType);
10666
11500
  if (!action2)
10667
11501
  throw new Error("invalid throttle");
10668
- const actionPath = action2.get(path5);
11502
+ const actionPath = action2.get(path8);
10669
11503
  if (actionPath) {
10670
11504
  actionPath.count++;
10671
11505
  return false;
10672
11506
  }
10673
11507
  let timeoutObject;
10674
11508
  const clear = () => {
10675
- const item = action2.get(path5);
11509
+ const item = action2.get(path8);
10676
11510
  const count = item ? item.count : 0;
10677
- action2.delete(path5);
11511
+ action2.delete(path8);
10678
11512
  clearTimeout(timeoutObject);
10679
11513
  if (item)
10680
11514
  clearTimeout(item.timeoutObject);
@@ -10682,7 +11516,7 @@ var FSWatcher = class extends EventEmitter {
10682
11516
  };
10683
11517
  timeoutObject = setTimeout(clear, timeout);
10684
11518
  const thr = { timeoutObject, clear, count: 0 };
10685
- action2.set(path5, thr);
11519
+ action2.set(path8, thr);
10686
11520
  return thr;
10687
11521
  }
10688
11522
  _incrReadyCount() {
@@ -10696,44 +11530,44 @@ var FSWatcher = class extends EventEmitter {
10696
11530
  * @param event
10697
11531
  * @param awfEmit Callback to be called when ready for event to be emitted.
10698
11532
  */
10699
- _awaitWriteFinish(path5, threshold, event, awfEmit) {
11533
+ _awaitWriteFinish(path8, threshold, event, awfEmit) {
10700
11534
  const awf = this.options.awaitWriteFinish;
10701
11535
  if (typeof awf !== "object")
10702
11536
  return;
10703
11537
  const pollInterval = awf.pollInterval;
10704
11538
  let timeoutHandler;
10705
- let fullPath = path5;
10706
- if (this.options.cwd && !sp2.isAbsolute(path5)) {
10707
- fullPath = sp2.join(this.options.cwd, path5);
11539
+ let fullPath = path8;
11540
+ if (this.options.cwd && !sp2.isAbsolute(path8)) {
11541
+ fullPath = sp2.join(this.options.cwd, path8);
10708
11542
  }
10709
11543
  const now = /* @__PURE__ */ new Date();
10710
11544
  const writes = this._pendingWrites;
10711
11545
  function awaitWriteFinishFn(prevStat) {
10712
11546
  statcb(fullPath, (err, curStat) => {
10713
- if (err || !writes.has(path5)) {
11547
+ if (err || !writes.has(path8)) {
10714
11548
  if (err && err.code !== "ENOENT")
10715
11549
  awfEmit(err);
10716
11550
  return;
10717
11551
  }
10718
11552
  const now2 = Number(/* @__PURE__ */ new Date());
10719
11553
  if (prevStat && curStat.size !== prevStat.size) {
10720
- writes.get(path5).lastChange = now2;
11554
+ writes.get(path8).lastChange = now2;
10721
11555
  }
10722
- const pw = writes.get(path5);
11556
+ const pw = writes.get(path8);
10723
11557
  const df = now2 - pw.lastChange;
10724
11558
  if (df >= threshold) {
10725
- writes.delete(path5);
11559
+ writes.delete(path8);
10726
11560
  awfEmit(void 0, curStat);
10727
11561
  } else {
10728
11562
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
10729
11563
  }
10730
11564
  });
10731
11565
  }
10732
- if (!writes.has(path5)) {
10733
- writes.set(path5, {
11566
+ if (!writes.has(path8)) {
11567
+ writes.set(path8, {
10734
11568
  lastChange: now,
10735
11569
  cancelWait: () => {
10736
- writes.delete(path5);
11570
+ writes.delete(path8);
10737
11571
  clearTimeout(timeoutHandler);
10738
11572
  return event;
10739
11573
  }
@@ -10744,8 +11578,8 @@ var FSWatcher = class extends EventEmitter {
10744
11578
  /**
10745
11579
  * Determines whether user has asked to ignore this path.
10746
11580
  */
10747
- _isIgnored(path5, stats) {
10748
- if (this.options.atomic && DOT_RE.test(path5))
11581
+ _isIgnored(path8, stats) {
11582
+ if (this.options.atomic && DOT_RE.test(path8))
10749
11583
  return true;
10750
11584
  if (!this._userIgnored) {
10751
11585
  const { cwd } = this.options;
@@ -10755,17 +11589,17 @@ var FSWatcher = class extends EventEmitter {
10755
11589
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
10756
11590
  this._userIgnored = anymatch(list, void 0);
10757
11591
  }
10758
- return this._userIgnored(path5, stats);
11592
+ return this._userIgnored(path8, stats);
10759
11593
  }
10760
- _isntIgnored(path5, stat4) {
10761
- return !this._isIgnored(path5, stat4);
11594
+ _isntIgnored(path8, stat4) {
11595
+ return !this._isIgnored(path8, stat4);
10762
11596
  }
10763
11597
  /**
10764
11598
  * Provides a set of common helpers and properties relating to symlink handling.
10765
11599
  * @param path file or directory pattern being watched
10766
11600
  */
10767
- _getWatchHelpers(path5) {
10768
- return new WatchHelper(path5, this.options.followSymlinks, this);
11601
+ _getWatchHelpers(path8) {
11602
+ return new WatchHelper(path8, this.options.followSymlinks, this);
10769
11603
  }
10770
11604
  // Directory helpers
10771
11605
  // -----------------
@@ -10797,63 +11631,63 @@ var FSWatcher = class extends EventEmitter {
10797
11631
  * @param item base path of item/directory
10798
11632
  */
10799
11633
  _remove(directory, item, isDirectory) {
10800
- const path5 = sp2.join(directory, item);
10801
- const fullPath = sp2.resolve(path5);
10802
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path5) || this._watched.has(fullPath);
10803
- if (!this._throttle("remove", path5, 100))
11634
+ const path8 = sp2.join(directory, item);
11635
+ const fullPath = sp2.resolve(path8);
11636
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path8) || this._watched.has(fullPath);
11637
+ if (!this._throttle("remove", path8, 100))
10804
11638
  return;
10805
11639
  if (!isDirectory && this._watched.size === 1) {
10806
11640
  this.add(directory, item, true);
10807
11641
  }
10808
- const wp = this._getWatchedDir(path5);
11642
+ const wp = this._getWatchedDir(path8);
10809
11643
  const nestedDirectoryChildren = wp.getChildren();
10810
- nestedDirectoryChildren.forEach((nested) => this._remove(path5, nested));
11644
+ nestedDirectoryChildren.forEach((nested) => this._remove(path8, nested));
10811
11645
  const parent = this._getWatchedDir(directory);
10812
11646
  const wasTracked = parent.has(item);
10813
11647
  parent.remove(item);
10814
11648
  if (this._symlinkPaths.has(fullPath)) {
10815
11649
  this._symlinkPaths.delete(fullPath);
10816
11650
  }
10817
- let relPath = path5;
11651
+ let relPath = path8;
10818
11652
  if (this.options.cwd)
10819
- relPath = sp2.relative(this.options.cwd, path5);
11653
+ relPath = sp2.relative(this.options.cwd, path8);
10820
11654
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
10821
11655
  const event = this._pendingWrites.get(relPath).cancelWait();
10822
11656
  if (event === EVENTS.ADD)
10823
11657
  return;
10824
11658
  }
10825
- this._watched.delete(path5);
11659
+ this._watched.delete(path8);
10826
11660
  this._watched.delete(fullPath);
10827
11661
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
10828
- if (wasTracked && !this._isIgnored(path5))
10829
- this._emit(eventName, path5);
10830
- this._closePath(path5);
11662
+ if (wasTracked && !this._isIgnored(path8))
11663
+ this._emit(eventName, path8);
11664
+ this._closePath(path8);
10831
11665
  }
10832
11666
  /**
10833
11667
  * Closes all watchers for a path
10834
11668
  */
10835
- _closePath(path5) {
10836
- this._closeFile(path5);
10837
- const dir = sp2.dirname(path5);
10838
- this._getWatchedDir(dir).remove(sp2.basename(path5));
11669
+ _closePath(path8) {
11670
+ this._closeFile(path8);
11671
+ const dir = sp2.dirname(path8);
11672
+ this._getWatchedDir(dir).remove(sp2.basename(path8));
10839
11673
  }
10840
11674
  /**
10841
11675
  * Closes only file-specific watchers
10842
11676
  */
10843
- _closeFile(path5) {
10844
- const closers = this._closers.get(path5);
11677
+ _closeFile(path8) {
11678
+ const closers = this._closers.get(path8);
10845
11679
  if (!closers)
10846
11680
  return;
10847
11681
  closers.forEach((closer) => closer());
10848
- this._closers.delete(path5);
11682
+ this._closers.delete(path8);
10849
11683
  }
10850
- _addPathCloser(path5, closer) {
11684
+ _addPathCloser(path8, closer) {
10851
11685
  if (!closer)
10852
11686
  return;
10853
- let list = this._closers.get(path5);
11687
+ let list = this._closers.get(path8);
10854
11688
  if (!list) {
10855
11689
  list = [];
10856
- this._closers.set(path5, list);
11690
+ this._closers.set(path8, list);
10857
11691
  }
10858
11692
  list.push(closer);
10859
11693
  }
@@ -10861,18 +11695,18 @@ var FSWatcher = class extends EventEmitter {
10861
11695
  if (this.closed)
10862
11696
  return;
10863
11697
  const options = { type: EVENTS.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
10864
- let stream = readdirp(root, options);
10865
- this._streams.add(stream);
10866
- stream.once(STR_CLOSE, () => {
10867
- stream = void 0;
11698
+ let stream2 = readdirp(root, options);
11699
+ this._streams.add(stream2);
11700
+ stream2.once(STR_CLOSE, () => {
11701
+ stream2 = void 0;
10868
11702
  });
10869
- stream.once(STR_END, () => {
10870
- if (stream) {
10871
- this._streams.delete(stream);
10872
- stream = void 0;
11703
+ stream2.once(STR_END, () => {
11704
+ if (stream2) {
11705
+ this._streams.delete(stream2);
11706
+ stream2 = void 0;
10873
11707
  }
10874
11708
  });
10875
- return stream;
11709
+ return stream2;
10876
11710
  }
10877
11711
  };
10878
11712
  function watch(paths, options = {}) {
@@ -10951,13 +11785,13 @@ function startWatcher(opts) {
10951
11785
  }
10952
11786
  return {
10953
11787
  ready,
10954
- notifyChange(path5) {
10955
- if (watcher2) watcher2.emit("change", path5);
10956
- else scheduleIngest(path5);
11788
+ notifyChange(path8) {
11789
+ if (watcher2) watcher2.emit("change", path8);
11790
+ else scheduleIngest(path8);
10957
11791
  },
10958
- notifyUnlink(path5) {
10959
- if (watcher2) watcher2.emit("unlink", path5);
10960
- else recordUnlink(path5);
11792
+ notifyUnlink(path8) {
11793
+ if (watcher2) watcher2.emit("unlink", path8);
11794
+ else recordUnlink(path8);
10961
11795
  },
10962
11796
  async close() {
10963
11797
  closed = true;
@@ -10969,39 +11803,39 @@ function startWatcher(opts) {
10969
11803
  }
10970
11804
 
10971
11805
  // src/plugins/claude-code/plugin.ts
10972
- import fs5 from "fs";
10973
- import path3 from "path";
11806
+ import fs6 from "fs";
11807
+ import path4 from "path";
10974
11808
  import readline from "readline";
10975
11809
  var ClaudeCodePlugin = class {
10976
11810
  id = "claude-code";
10977
11811
  displayName = "Claude Code";
10978
11812
  async *discover(env) {
10979
- const projectsDir = path3.join(env.homeDir, ".claude", "projects");
10980
- if (!fs5.existsSync(projectsDir)) return;
10981
- const projects = fs5.readdirSync(projectsDir, { withFileTypes: true });
11813
+ const projectsDir = path4.join(env.homeDir, ".claude", "projects");
11814
+ if (!fs6.existsSync(projectsDir)) return;
11815
+ const projects = fs6.readdirSync(projectsDir, { withFileTypes: true });
10982
11816
  for (const project of projects) {
10983
11817
  if (!project.isDirectory()) continue;
10984
- const projectPath = path3.join(projectsDir, project.name);
10985
- const files = fs5.readdirSync(projectPath, { withFileTypes: true });
11818
+ const projectPath = path4.join(projectsDir, project.name);
11819
+ const files = fs6.readdirSync(projectPath, { withFileTypes: true });
10986
11820
  for (const file of files) {
10987
11821
  if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
10988
- const sourcePath = path3.join(projectPath, file.name);
11822
+ const sourcePath = path4.join(projectPath, file.name);
10989
11823
  const sourceId = file.name.replace(/\.jsonl$/, "");
10990
11824
  const cwd = await readCwdFromJsonl(sourcePath);
10991
11825
  yield {
10992
11826
  sourceId,
10993
11827
  sourcePath,
10994
11828
  watchPaths: [sourcePath],
10995
- project: cwd ? path3.basename(cwd) : void 0,
11829
+ project: cwd ? path4.basename(cwd) : void 0,
10996
11830
  projectPath: cwd ?? void 0
10997
11831
  };
10998
11832
  }
10999
11833
  }
11000
11834
  }
11001
11835
  async *extractRaw(ref) {
11002
- if (!fs5.existsSync(ref.sourcePath)) return;
11003
- const stream = fs5.createReadStream(ref.sourcePath, { encoding: "utf-8" });
11004
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
11836
+ if (!fs6.existsSync(ref.sourcePath)) return;
11837
+ const stream2 = fs6.createReadStream(ref.sourcePath, { encoding: "utf-8" });
11838
+ const rl = readline.createInterface({ input: stream2, crlfDelay: Infinity });
11005
11839
  let lineNo = 0;
11006
11840
  for await (const line of rl) {
11007
11841
  lineNo += 1;
@@ -11049,8 +11883,8 @@ var ClaudeCodePlugin = class {
11049
11883
  };
11050
11884
  async function readCwdFromJsonl(sourcePath) {
11051
11885
  try {
11052
- const stream = fs5.createReadStream(sourcePath, { encoding: "utf-8" });
11053
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
11886
+ const stream2 = fs6.createReadStream(sourcePath, { encoding: "utf-8" });
11887
+ const rl = readline.createInterface({ input: stream2, crlfDelay: Infinity });
11054
11888
  try {
11055
11889
  for await (const line of rl) {
11056
11890
  if (!line) continue;
@@ -11066,7 +11900,7 @@ async function readCwdFromJsonl(sourcePath) {
11066
11900
  }
11067
11901
  } finally {
11068
11902
  rl.close();
11069
- stream.destroy();
11903
+ stream2.destroy();
11070
11904
  }
11071
11905
  } catch {
11072
11906
  }
@@ -11155,10 +11989,131 @@ Environment:
11155
11989
  PORT=<n> Set the HTTP port (overridden by --port)
11156
11990
  `;
11157
11991
 
11992
+ // src/config/data-dir.ts
11993
+ import path5 from "path";
11994
+ function resolveDataDir(env, homeDir) {
11995
+ const override = env.CHAT_LOGBOOK_DATA_DIR?.trim();
11996
+ if (override) {
11997
+ return path5.resolve(override);
11998
+ }
11999
+ return path5.join(homeDir, ".chat-logbook");
12000
+ }
12001
+
12002
+ // src/list-counts.ts
12003
+ import fs7 from "fs";
12004
+ import path6 from "path";
12005
+ import Database3 from "better-sqlite3";
12006
+ var ARCHIVE_DB2 = "archive.db";
12007
+ var METADATA_DB2 = "metadata.db";
12008
+ function createChatCountsQuery({
12009
+ dataDir: dataDir2
12010
+ }) {
12011
+ const archive2 = new Database3(path6.join(dataDir2, ARCHIVE_DB2), {
12012
+ readonly: true
12013
+ });
12014
+ const metadataPath = path6.join(dataDir2, METADATA_DB2);
12015
+ const hasMetadata = fs7.existsSync(metadataPath);
12016
+ if (hasMetadata) {
12017
+ archive2.prepare("ATTACH DATABASE ? AS meta").run(metadataPath);
12018
+ }
12019
+ function queryCounts({
12020
+ includeTrashed = false
12021
+ }) {
12022
+ const viewClause = viewPredicate(includeTrashed, hasMetadata);
12023
+ const total = archive2.prepare(`SELECT count(*) AS n FROM chats c ${viewClause}`).get().n;
12024
+ const projects = archive2.prepare(
12025
+ `SELECT coalesce(c.project, '') AS project,
12026
+ count(*) AS count,
12027
+ max(c.updated_at) AS lastActiveAt
12028
+ FROM chats c ${viewClause}
12029
+ GROUP BY coalesce(c.project, '')`
12030
+ ).all();
12031
+ const tags3 = hasMetadata ? archive2.prepare(
12032
+ `SELECT ct.tag_id AS tagId, count(*) AS count
12033
+ FROM meta.chat_tags ct
12034
+ JOIN chats c ON c.id = ct.chat_id
12035
+ ${viewClause}
12036
+ GROUP BY ct.tag_id`
12037
+ ).all() : [];
12038
+ const untagged = hasMetadata ? archive2.prepare(
12039
+ `SELECT count(*) AS n
12040
+ FROM chats c ${viewClause}
12041
+ AND c.id NOT IN (SELECT chat_id FROM meta.chat_tags)`
12042
+ ).get().n : total;
12043
+ return { total, projects, tags: tags3, untagged };
12044
+ }
12045
+ function queryFilteredTotal({
12046
+ includeTrashed = false,
12047
+ projects,
12048
+ tags: tags3
12049
+ }) {
12050
+ const clauses = [];
12051
+ const params = [];
12052
+ if (hasMetadata) {
12053
+ clauses.push(
12054
+ 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)"
12055
+ );
12056
+ } else if (includeTrashed) {
12057
+ return 0;
12058
+ }
12059
+ const filter = buildFilterClauses({ projects, tags: tags3, hasMetadata });
12060
+ clauses.push(...filter.clauses);
12061
+ params.push(...filter.params);
12062
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
12063
+ return archive2.prepare(`SELECT count(*) AS n FROM chats c ${where}`).get(...params).n;
12064
+ }
12065
+ return {
12066
+ queryCounts,
12067
+ queryFilteredTotal,
12068
+ close() {
12069
+ archive2.close();
12070
+ }
12071
+ };
12072
+ }
12073
+ function viewPredicate(includeTrashed, hasMetadata) {
12074
+ if (!hasMetadata) {
12075
+ return includeTrashed ? "WHERE 0" : "";
12076
+ }
12077
+ 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)";
12078
+ }
12079
+
12080
+ // src/list-events.ts
12081
+ function createListEventHub() {
12082
+ const listeners = /* @__PURE__ */ new Set();
12083
+ return {
12084
+ publish(event) {
12085
+ for (const listener of listeners) listener(event);
12086
+ },
12087
+ subscribe(listener) {
12088
+ listeners.add(listener);
12089
+ return () => {
12090
+ listeners.delete(listener);
12091
+ };
12092
+ }
12093
+ };
12094
+ }
12095
+
12096
+ // src/metadata/reconcile-title-sort-keys.ts
12097
+ function reconcileTitleSortKeys({
12098
+ archive: archive2,
12099
+ metadata: metadata2
12100
+ }) {
12101
+ const key = (agent, sourceId) => [agent, sourceId].join("\0");
12102
+ const firstTextByKey = new Map(
12103
+ archive2.read.listFirstUserTexts().map((r) => [key(r.agent, r.sourceId), r.text])
12104
+ );
12105
+ for (const row of archive2.read.listChatRows()) {
12106
+ const baseTitle = deriveBaseTitle(
12107
+ firstTextByKey.get(key(row.agent, row.sourceId))
12108
+ );
12109
+ metadata2.setTitleTextKey(row.id, computeSortKey(baseTitle));
12110
+ }
12111
+ }
12112
+
11158
12113
  // src/index.ts
11159
- var __dirname = path4.dirname(fileURLToPath2(import.meta.url));
11160
- var pkgPath = path4.join(__dirname, "../../package.json");
11161
- var pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
12114
+ var __dirname = path7.dirname(fileURLToPath2(import.meta.url));
12115
+ var pkgPath = path7.join(__dirname, "../../package.json");
12116
+ var pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
11162
12117
  var action = parseCliArgs(process.argv.slice(2), {
11163
12118
  PORT: process.env.PORT
11164
12119
  });
@@ -11175,26 +12130,58 @@ if (action.kind === "error") {
11175
12130
  process.exit(1);
11176
12131
  }
11177
12132
  updateNotifier({ pkg }).notify({ defer: false, isGlobal: true });
11178
- var dataDir = path4.join(os.homedir(), ".chat-logbook");
11179
- var webDistDir = path4.join(__dirname, "../../web/dist");
12133
+ var dataDir = resolveDataDir(
12134
+ { CHAT_LOGBOOK_DATA_DIR: process.env.CHAT_LOGBOOK_DATA_DIR },
12135
+ os.homedir()
12136
+ );
12137
+ var webDistDir = path7.join(__dirname, "../../web/dist");
11180
12138
  var port = action.port;
11181
12139
  var archive = createArchiveRepository({ dataDir });
11182
12140
  var checkpoint = createCheckpointRepository({ dataDir });
11183
12141
  var metadata = createMetadataRepository({ dataDir });
11184
- var app = createApp({ archive, metadata, webDistDir });
12142
+ var tags2 = createTagRepository({ dataDir });
12143
+ var pageQuery = createChatPageQuery({ dataDir });
12144
+ var countsQuery = createChatCountsQuery({ dataDir });
12145
+ var listEvents = createListEventHub();
12146
+ var app = createApp({
12147
+ archive,
12148
+ metadata,
12149
+ tags: tags2,
12150
+ pageQuery,
12151
+ countsQuery,
12152
+ listEvents,
12153
+ webDistDir
12154
+ });
11185
12155
  var initialIngest = startIngestionInBackground({
11186
12156
  plugins,
11187
12157
  archive,
11188
12158
  checkpoint,
11189
12159
  env: { homeDir: os.homedir() }
11190
12160
  });
12161
+ function reconcileTitles() {
12162
+ try {
12163
+ reconcileTitleSortKeys({ archive, metadata });
12164
+ } catch (err) {
12165
+ console.error("[title-sort-key] reconcile failed:", err);
12166
+ }
12167
+ }
11191
12168
  var watcher = startWatcher({
11192
12169
  plugins,
11193
12170
  archive,
11194
12171
  checkpoint,
11195
- env: { homeDir: os.homedir() }
12172
+ env: { homeDir: os.homedir() },
12173
+ // Each watcher-driven ingest can add a chat or change a first user message;
12174
+ // refresh the Title keys so the Title axis stays current, then push a
12175
+ // `changed` event so connected clients reconcile their loaded window (#132).
12176
+ onIngest: () => {
12177
+ reconcileTitles();
12178
+ listEvents.publish({ type: "changed" });
12179
+ }
11196
12180
  });
11197
- void initialIngest.done.then(() => watcher.ready).catch(() => {
12181
+ void initialIngest.done.then(() => {
12182
+ reconcileTitles();
12183
+ return watcher.ready;
12184
+ }).catch(() => {
11198
12185
  });
11199
12186
  function shutdown() {
11200
12187
  void watcher.close().finally(() => process.exit(0));