@hir4ta/mneme 0.22.2 → 0.22.4

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/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // dashboard/server/index.ts
2
- import fs4 from "node:fs";
3
- import path3 from "node:path";
2
+ import fs13 from "node:fs";
3
+ import path12 from "node:path";
4
4
 
5
5
  // node_modules/@hono/node-server/dist/index.mjs
6
6
  import { createServer as createServerHTTP } from "http";
@@ -666,10 +666,10 @@ var createStreamBody = (stream) => {
666
666
  });
667
667
  return body;
668
668
  };
669
- var getStats = (path4) => {
669
+ var getStats = (path13) => {
670
670
  let stats;
671
671
  try {
672
- stats = statSync(path4);
672
+ stats = statSync(path13);
673
673
  } catch {
674
674
  }
675
675
  return stats;
@@ -698,21 +698,21 @@ var serveStatic = (options = { root: "" }) => {
698
698
  return next();
699
699
  }
700
700
  }
701
- let path4 = join(
701
+ let path13 = join(
702
702
  root,
703
703
  !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename
704
704
  );
705
- let stats = getStats(path4);
705
+ let stats = getStats(path13);
706
706
  if (stats && stats.isDirectory()) {
707
707
  const indexFile = options.index ?? "index.html";
708
- path4 = join(path4, indexFile);
709
- stats = getStats(path4);
708
+ path13 = join(path13, indexFile);
709
+ stats = getStats(path13);
710
710
  }
711
711
  if (!stats) {
712
- await options.onNotFound?.(path4, c);
712
+ await options.onNotFound?.(path13, c);
713
713
  return next();
714
714
  }
715
- const mimeType = getMimeType(path4);
715
+ const mimeType = getMimeType(path13);
716
716
  c.header("Content-Type", mimeType || "application/octet-stream");
717
717
  if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
718
718
  const acceptEncodingSet = new Set(
@@ -722,12 +722,12 @@ var serveStatic = (options = { root: "" }) => {
722
722
  if (!acceptEncodingSet.has(encoding)) {
723
723
  continue;
724
724
  }
725
- const precompressedStats = getStats(path4 + ENCODINGS[encoding]);
725
+ const precompressedStats = getStats(path13 + ENCODINGS[encoding]);
726
726
  if (precompressedStats) {
727
727
  c.header("Content-Encoding", encoding);
728
728
  c.header("Vary", "Accept-Encoding", { append: true });
729
729
  stats = precompressedStats;
730
- path4 = path4 + ENCODINGS[encoding];
730
+ path13 = path13 + ENCODINGS[encoding];
731
731
  break;
732
732
  }
733
733
  }
@@ -741,7 +741,7 @@ var serveStatic = (options = { root: "" }) => {
741
741
  result = c.body(null);
742
742
  } else if (!range) {
743
743
  c.header("Content-Length", size.toString());
744
- result = c.body(createStreamBody(createReadStream(path4)), 200);
744
+ result = c.body(createStreamBody(createReadStream(path13)), 200);
745
745
  } else {
746
746
  c.header("Accept-Ranges", "bytes");
747
747
  c.header("Date", stats.birthtime.toUTCString());
@@ -752,12 +752,12 @@ var serveStatic = (options = { root: "" }) => {
752
752
  end = size - 1;
753
753
  }
754
754
  const chunksize = end - start + 1;
755
- const stream = createReadStream(path4, { start, end });
755
+ const stream = createReadStream(path13, { start, end });
756
756
  c.header("Content-Length", chunksize.toString());
757
757
  c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
758
758
  result = c.body(createStreamBody(stream), 206);
759
759
  }
760
- await options.onFound?.(path4, c);
760
+ await options.onFound?.(path13, c);
761
761
  return result;
762
762
  };
763
763
  };
@@ -879,26 +879,26 @@ var handleParsingNestedValues = (form, key, value) => {
879
879
  };
880
880
 
881
881
  // node_modules/hono/dist/utils/url.js
882
- var splitPath = (path4) => {
883
- const paths = path4.split("/");
882
+ var splitPath = (path13) => {
883
+ const paths = path13.split("/");
884
884
  if (paths[0] === "") {
885
885
  paths.shift();
886
886
  }
887
887
  return paths;
888
888
  };
889
889
  var splitRoutingPath = (routePath) => {
890
- const { groups, path: path4 } = extractGroupsFromPath(routePath);
891
- const paths = splitPath(path4);
890
+ const { groups, path: path13 } = extractGroupsFromPath(routePath);
891
+ const paths = splitPath(path13);
892
892
  return replaceGroupMarks(paths, groups);
893
893
  };
894
- var extractGroupsFromPath = (path4) => {
894
+ var extractGroupsFromPath = (path13) => {
895
895
  const groups = [];
896
- path4 = path4.replace(/\{[^}]+\}/g, (match2, index) => {
896
+ path13 = path13.replace(/\{[^}]+\}/g, (match2, index) => {
897
897
  const mark = `@${index}`;
898
898
  groups.push([mark, match2]);
899
899
  return mark;
900
900
  });
901
- return { groups, path: path4 };
901
+ return { groups, path: path13 };
902
902
  };
903
903
  var replaceGroupMarks = (paths, groups) => {
904
904
  for (let i = groups.length - 1; i >= 0; i--) {
@@ -955,8 +955,8 @@ var getPath = (request) => {
955
955
  const queryIndex = url.indexOf("?", i);
956
956
  const hashIndex = url.indexOf("#", i);
957
957
  const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
958
- const path4 = url.slice(start, end);
959
- return tryDecodeURI(path4.includes("%25") ? path4.replace(/%25/g, "%2525") : path4);
958
+ const path13 = url.slice(start, end);
959
+ return tryDecodeURI(path13.includes("%25") ? path13.replace(/%25/g, "%2525") : path13);
960
960
  } else if (charCode === 63 || charCode === 35) {
961
961
  break;
962
962
  }
@@ -973,11 +973,11 @@ var mergePath = (base, sub, ...rest) => {
973
973
  }
974
974
  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
975
975
  };
976
- var checkOptionalParameter = (path4) => {
977
- if (path4.charCodeAt(path4.length - 1) !== 63 || !path4.includes(":")) {
976
+ var checkOptionalParameter = (path13) => {
977
+ if (path13.charCodeAt(path13.length - 1) !== 63 || !path13.includes(":")) {
978
978
  return null;
979
979
  }
980
- const segments = path4.split("/");
980
+ const segments = path13.split("/");
981
981
  const results = [];
982
982
  let basePath = "";
983
983
  segments.forEach((segment) => {
@@ -1118,9 +1118,9 @@ var HonoRequest = class {
1118
1118
  */
1119
1119
  path;
1120
1120
  bodyCache = {};
1121
- constructor(request, path4 = "/", matchResult = [[]]) {
1121
+ constructor(request, path13 = "/", matchResult = [[]]) {
1122
1122
  this.raw = request;
1123
- this.path = path4;
1123
+ this.path = path13;
1124
1124
  this.#matchResult = matchResult;
1125
1125
  this.#validatedData = {};
1126
1126
  }
@@ -1856,8 +1856,8 @@ var Hono = class _Hono {
1856
1856
  return this;
1857
1857
  };
1858
1858
  });
1859
- this.on = (method, path4, ...handlers) => {
1860
- for (const p of [path4].flat()) {
1859
+ this.on = (method, path13, ...handlers) => {
1860
+ for (const p of [path13].flat()) {
1861
1861
  this.#path = p;
1862
1862
  for (const m of [method].flat()) {
1863
1863
  handlers.map((handler) => {
@@ -1914,8 +1914,8 @@ var Hono = class _Hono {
1914
1914
  * app.route("/api", app2) // GET /api/user
1915
1915
  * ```
1916
1916
  */
1917
- route(path4, app2) {
1918
- const subApp = this.basePath(path4);
1917
+ route(path13, app2) {
1918
+ const subApp = this.basePath(path13);
1919
1919
  app2.routes.map((r) => {
1920
1920
  let handler;
1921
1921
  if (app2.errorHandler === errorHandler) {
@@ -1941,9 +1941,9 @@ var Hono = class _Hono {
1941
1941
  * const api = new Hono().basePath('/api')
1942
1942
  * ```
1943
1943
  */
1944
- basePath(path4) {
1944
+ basePath(path13) {
1945
1945
  const subApp = this.#clone();
1946
- subApp._basePath = mergePath(this._basePath, path4);
1946
+ subApp._basePath = mergePath(this._basePath, path13);
1947
1947
  return subApp;
1948
1948
  }
1949
1949
  /**
@@ -2017,7 +2017,7 @@ var Hono = class _Hono {
2017
2017
  * })
2018
2018
  * ```
2019
2019
  */
2020
- mount(path4, applicationHandler, options) {
2020
+ mount(path13, applicationHandler, options) {
2021
2021
  let replaceRequest;
2022
2022
  let optionHandler;
2023
2023
  if (options) {
@@ -2044,7 +2044,7 @@ var Hono = class _Hono {
2044
2044
  return [c.env, executionContext];
2045
2045
  };
2046
2046
  replaceRequest ||= (() => {
2047
- const mergedPath = mergePath(this._basePath, path4);
2047
+ const mergedPath = mergePath(this._basePath, path13);
2048
2048
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
2049
2049
  return (request) => {
2050
2050
  const url = new URL(request.url);
@@ -2059,14 +2059,14 @@ var Hono = class _Hono {
2059
2059
  }
2060
2060
  await next();
2061
2061
  };
2062
- this.#addRoute(METHOD_NAME_ALL, mergePath(path4, "*"), handler);
2062
+ this.#addRoute(METHOD_NAME_ALL, mergePath(path13, "*"), handler);
2063
2063
  return this;
2064
2064
  }
2065
- #addRoute(method, path4, handler) {
2065
+ #addRoute(method, path13, handler) {
2066
2066
  method = method.toUpperCase();
2067
- path4 = mergePath(this._basePath, path4);
2068
- const r = { basePath: this._basePath, path: path4, method, handler };
2069
- this.router.add(method, path4, [handler, r]);
2067
+ path13 = mergePath(this._basePath, path13);
2068
+ const r = { basePath: this._basePath, path: path13, method, handler };
2069
+ this.router.add(method, path13, [handler, r]);
2070
2070
  this.routes.push(r);
2071
2071
  }
2072
2072
  #handleError(err, c) {
@@ -2079,10 +2079,10 @@ var Hono = class _Hono {
2079
2079
  if (method === "HEAD") {
2080
2080
  return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
2081
2081
  }
2082
- const path4 = this.getPath(request, { env });
2083
- const matchResult = this.router.match(method, path4);
2082
+ const path13 = this.getPath(request, { env });
2083
+ const matchResult = this.router.match(method, path13);
2084
2084
  const c = new Context(request, {
2085
- path: path4,
2085
+ path: path13,
2086
2086
  matchResult,
2087
2087
  env,
2088
2088
  executionCtx,
@@ -2182,7 +2182,7 @@ var Hono = class _Hono {
2182
2182
 
2183
2183
  // node_modules/hono/dist/router/reg-exp-router/matcher.js
2184
2184
  var emptyParam = [];
2185
- function match(method, path4) {
2185
+ function match(method, path13) {
2186
2186
  const matchers = this.buildAllMatchers();
2187
2187
  const match2 = ((method2, path22) => {
2188
2188
  const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
@@ -2198,7 +2198,7 @@ function match(method, path4) {
2198
2198
  return [matcher[1][index], match3];
2199
2199
  });
2200
2200
  this.match = match2;
2201
- return match2(method, path4);
2201
+ return match2(method, path13);
2202
2202
  }
2203
2203
 
2204
2204
  // node_modules/hono/dist/router/reg-exp-router/node.js
@@ -2313,12 +2313,12 @@ var Node = class _Node {
2313
2313
  var Trie = class {
2314
2314
  #context = { varIndex: 0 };
2315
2315
  #root = new Node();
2316
- insert(path4, index, pathErrorCheckOnly) {
2316
+ insert(path13, index, pathErrorCheckOnly) {
2317
2317
  const paramAssoc = [];
2318
2318
  const groups = [];
2319
2319
  for (let i = 0; ; ) {
2320
2320
  let replaced = false;
2321
- path4 = path4.replace(/\{[^}]+\}/g, (m) => {
2321
+ path13 = path13.replace(/\{[^}]+\}/g, (m) => {
2322
2322
  const mark = `@\\${i}`;
2323
2323
  groups[i] = [mark, m];
2324
2324
  i++;
@@ -2329,7 +2329,7 @@ var Trie = class {
2329
2329
  break;
2330
2330
  }
2331
2331
  }
2332
- const tokens = path4.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
2332
+ const tokens = path13.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
2333
2333
  for (let i = groups.length - 1; i >= 0; i--) {
2334
2334
  const [mark] = groups[i];
2335
2335
  for (let j = tokens.length - 1; j >= 0; j--) {
@@ -2368,9 +2368,9 @@ var Trie = class {
2368
2368
  // node_modules/hono/dist/router/reg-exp-router/router.js
2369
2369
  var nullMatcher = [/^$/, [], /* @__PURE__ */ Object.create(null)];
2370
2370
  var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
2371
- function buildWildcardRegExp(path4) {
2372
- return wildcardRegExpCache[path4] ??= new RegExp(
2373
- path4 === "*" ? "" : `^${path4.replace(
2371
+ function buildWildcardRegExp(path13) {
2372
+ return wildcardRegExpCache[path13] ??= new RegExp(
2373
+ path13 === "*" ? "" : `^${path13.replace(
2374
2374
  /\/\*$|([.\\+*[^\]$()])/g,
2375
2375
  (_, metaChar) => metaChar ? `\\${metaChar}` : "(?:|/.*)"
2376
2376
  )}$`
@@ -2392,17 +2392,17 @@ function buildMatcherFromPreprocessedRoutes(routes) {
2392
2392
  );
2393
2393
  const staticMap = /* @__PURE__ */ Object.create(null);
2394
2394
  for (let i = 0, j = -1, len = routesWithStaticPathFlag.length; i < len; i++) {
2395
- const [pathErrorCheckOnly, path4, handlers] = routesWithStaticPathFlag[i];
2395
+ const [pathErrorCheckOnly, path13, handlers] = routesWithStaticPathFlag[i];
2396
2396
  if (pathErrorCheckOnly) {
2397
- staticMap[path4] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
2397
+ staticMap[path13] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
2398
2398
  } else {
2399
2399
  j++;
2400
2400
  }
2401
2401
  let paramAssoc;
2402
2402
  try {
2403
- paramAssoc = trie.insert(path4, j, pathErrorCheckOnly);
2403
+ paramAssoc = trie.insert(path13, j, pathErrorCheckOnly);
2404
2404
  } catch (e) {
2405
- throw e === PATH_ERROR ? new UnsupportedPathError(path4) : e;
2405
+ throw e === PATH_ERROR ? new UnsupportedPathError(path13) : e;
2406
2406
  }
2407
2407
  if (pathErrorCheckOnly) {
2408
2408
  continue;
@@ -2436,12 +2436,12 @@ function buildMatcherFromPreprocessedRoutes(routes) {
2436
2436
  }
2437
2437
  return [regexp, handlerMap, staticMap];
2438
2438
  }
2439
- function findMiddleware(middleware, path4) {
2439
+ function findMiddleware(middleware, path13) {
2440
2440
  if (!middleware) {
2441
2441
  return void 0;
2442
2442
  }
2443
2443
  for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
2444
- if (buildWildcardRegExp(k).test(path4)) {
2444
+ if (buildWildcardRegExp(k).test(path13)) {
2445
2445
  return [...middleware[k]];
2446
2446
  }
2447
2447
  }
@@ -2455,7 +2455,7 @@ var RegExpRouter = class {
2455
2455
  this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
2456
2456
  this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
2457
2457
  }
2458
- add(method, path4, handler) {
2458
+ add(method, path13, handler) {
2459
2459
  const middleware = this.#middleware;
2460
2460
  const routes = this.#routes;
2461
2461
  if (!middleware || !routes) {
@@ -2470,18 +2470,18 @@ var RegExpRouter = class {
2470
2470
  });
2471
2471
  });
2472
2472
  }
2473
- if (path4 === "/*") {
2474
- path4 = "*";
2473
+ if (path13 === "/*") {
2474
+ path13 = "*";
2475
2475
  }
2476
- const paramCount = (path4.match(/\/:/g) || []).length;
2477
- if (/\*$/.test(path4)) {
2478
- const re = buildWildcardRegExp(path4);
2476
+ const paramCount = (path13.match(/\/:/g) || []).length;
2477
+ if (/\*$/.test(path13)) {
2478
+ const re = buildWildcardRegExp(path13);
2479
2479
  if (method === METHOD_NAME_ALL) {
2480
2480
  Object.keys(middleware).forEach((m) => {
2481
- middleware[m][path4] ||= findMiddleware(middleware[m], path4) || findMiddleware(middleware[METHOD_NAME_ALL], path4) || [];
2481
+ middleware[m][path13] ||= findMiddleware(middleware[m], path13) || findMiddleware(middleware[METHOD_NAME_ALL], path13) || [];
2482
2482
  });
2483
2483
  } else {
2484
- middleware[method][path4] ||= findMiddleware(middleware[method], path4) || findMiddleware(middleware[METHOD_NAME_ALL], path4) || [];
2484
+ middleware[method][path13] ||= findMiddleware(middleware[method], path13) || findMiddleware(middleware[METHOD_NAME_ALL], path13) || [];
2485
2485
  }
2486
2486
  Object.keys(middleware).forEach((m) => {
2487
2487
  if (method === METHOD_NAME_ALL || method === m) {
@@ -2499,7 +2499,7 @@ var RegExpRouter = class {
2499
2499
  });
2500
2500
  return;
2501
2501
  }
2502
- const paths = checkOptionalParameter(path4) || [path4];
2502
+ const paths = checkOptionalParameter(path13) || [path13];
2503
2503
  for (let i = 0, len = paths.length; i < len; i++) {
2504
2504
  const path22 = paths[i];
2505
2505
  Object.keys(routes).forEach((m) => {
@@ -2526,13 +2526,13 @@ var RegExpRouter = class {
2526
2526
  const routes = [];
2527
2527
  let hasOwnRoute = method === METHOD_NAME_ALL;
2528
2528
  [this.#middleware, this.#routes].forEach((r) => {
2529
- const ownRoute = r[method] ? Object.keys(r[method]).map((path4) => [path4, r[method][path4]]) : [];
2529
+ const ownRoute = r[method] ? Object.keys(r[method]).map((path13) => [path13, r[method][path13]]) : [];
2530
2530
  if (ownRoute.length !== 0) {
2531
2531
  hasOwnRoute ||= true;
2532
2532
  routes.push(...ownRoute);
2533
2533
  } else if (method !== METHOD_NAME_ALL) {
2534
2534
  routes.push(
2535
- ...Object.keys(r[METHOD_NAME_ALL]).map((path4) => [path4, r[METHOD_NAME_ALL][path4]])
2535
+ ...Object.keys(r[METHOD_NAME_ALL]).map((path13) => [path13, r[METHOD_NAME_ALL][path13]])
2536
2536
  );
2537
2537
  }
2538
2538
  });
@@ -2552,13 +2552,13 @@ var SmartRouter = class {
2552
2552
  constructor(init) {
2553
2553
  this.#routers = init.routers;
2554
2554
  }
2555
- add(method, path4, handler) {
2555
+ add(method, path13, handler) {
2556
2556
  if (!this.#routes) {
2557
2557
  throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
2558
2558
  }
2559
- this.#routes.push([method, path4, handler]);
2559
+ this.#routes.push([method, path13, handler]);
2560
2560
  }
2561
- match(method, path4) {
2561
+ match(method, path13) {
2562
2562
  if (!this.#routes) {
2563
2563
  throw new Error("Fatal error");
2564
2564
  }
@@ -2573,7 +2573,7 @@ var SmartRouter = class {
2573
2573
  for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) {
2574
2574
  router.add(...routes[i2]);
2575
2575
  }
2576
- res = router.match(method, path4);
2576
+ res = router.match(method, path13);
2577
2577
  } catch (e) {
2578
2578
  if (e instanceof UnsupportedPathError) {
2579
2579
  continue;
@@ -2617,10 +2617,10 @@ var Node2 = class _Node2 {
2617
2617
  }
2618
2618
  this.#patterns = [];
2619
2619
  }
2620
- insert(method, path4, handler) {
2620
+ insert(method, path13, handler) {
2621
2621
  this.#order = ++this.#order;
2622
2622
  let curNode = this;
2623
- const parts = splitRoutingPath(path4);
2623
+ const parts = splitRoutingPath(path13);
2624
2624
  const possibleKeys = [];
2625
2625
  for (let i = 0, len = parts.length; i < len; i++) {
2626
2626
  const p = parts[i];
@@ -2671,12 +2671,12 @@ var Node2 = class _Node2 {
2671
2671
  }
2672
2672
  return handlerSets;
2673
2673
  }
2674
- search(method, path4) {
2674
+ search(method, path13) {
2675
2675
  const handlerSets = [];
2676
2676
  this.#params = emptyParams;
2677
2677
  const curNode = this;
2678
2678
  let curNodes = [curNode];
2679
- const parts = splitPath(path4);
2679
+ const parts = splitPath(path13);
2680
2680
  const curNodesQueue = [];
2681
2681
  for (let i = 0, len = parts.length; i < len; i++) {
2682
2682
  const part = parts[i];
@@ -2764,18 +2764,18 @@ var TrieRouter = class {
2764
2764
  constructor() {
2765
2765
  this.#node = new Node2();
2766
2766
  }
2767
- add(method, path4, handler) {
2768
- const results = checkOptionalParameter(path4);
2767
+ add(method, path13, handler) {
2768
+ const results = checkOptionalParameter(path13);
2769
2769
  if (results) {
2770
2770
  for (let i = 0, len = results.length; i < len; i++) {
2771
2771
  this.#node.insert(method, results[i], handler);
2772
2772
  }
2773
2773
  return;
2774
2774
  }
2775
- this.#node.insert(method, path4, handler);
2775
+ this.#node.insert(method, path13, handler);
2776
2776
  }
2777
- match(method, path4) {
2778
- return this.#node.search(method, path4);
2777
+ match(method, path13) {
2778
+ return this.#node.search(method, path13);
2779
2779
  }
2780
2780
  };
2781
2781
 
@@ -2879,100 +2879,6 @@ var cors = (options) => {
2879
2879
  };
2880
2880
  };
2881
2881
 
2882
- // lib/suppress-sqlite-warning.ts
2883
- var originalEmit = process.emit;
2884
- process.emit = (event, ...args) => {
2885
- if (event === "warning" && typeof args[0] === "object" && args[0] !== null && "name" in args[0] && args[0].name === "ExperimentalWarning" && "message" in args[0] && typeof args[0].message === "string" && args[0].message.includes("SQLite")) {
2886
- return false;
2887
- }
2888
- return originalEmit.apply(process, [event, ...args]);
2889
- };
2890
-
2891
- // lib/db.ts
2892
- import { execSync } from "node:child_process";
2893
- import { existsSync as existsSync2, mkdirSync, readFileSync } from "node:fs";
2894
- import { dirname, join as join2 } from "node:path";
2895
- import { fileURLToPath } from "node:url";
2896
- var { DatabaseSync } = await import("node:sqlite");
2897
- var __filename = fileURLToPath(import.meta.url);
2898
- var __dirname = dirname(__filename);
2899
- function getCurrentUser() {
2900
- try {
2901
- return execSync("git config user.name", { encoding: "utf-8" }).trim();
2902
- } catch {
2903
- try {
2904
- return execSync("whoami", { encoding: "utf-8" }).trim();
2905
- } catch {
2906
- return "unknown";
2907
- }
2908
- }
2909
- }
2910
- function getLocalDbPath(projectPath) {
2911
- return join2(projectPath, ".mneme", "local.db");
2912
- }
2913
- function configurePragmas(db) {
2914
- db.exec("PRAGMA journal_mode = WAL");
2915
- db.exec("PRAGMA busy_timeout = 5000");
2916
- db.exec("PRAGMA synchronous = NORMAL");
2917
- }
2918
- function openLocalDatabase(projectPath) {
2919
- const dbPath = getLocalDbPath(projectPath);
2920
- if (!existsSync2(dbPath)) {
2921
- return null;
2922
- }
2923
- const db = new DatabaseSync(dbPath);
2924
- configurePragmas(db);
2925
- return db;
2926
- }
2927
- function getInteractionsBySessionIds(db, sessionIds) {
2928
- if (sessionIds.length === 0) {
2929
- return [];
2930
- }
2931
- const placeholders = sessionIds.map(() => "?").join(", ");
2932
- const stmt = db.prepare(`
2933
- SELECT * FROM interactions
2934
- WHERE session_id IN (${placeholders})
2935
- ORDER BY timestamp ASC, id ASC
2936
- `);
2937
- return stmt.all(...sessionIds);
2938
- }
2939
- function deleteInteractions(db, sessionId) {
2940
- const stmt = db.prepare("DELETE FROM interactions WHERE session_id = ?");
2941
- stmt.run(sessionId);
2942
- }
2943
- function deleteBackups(db, sessionId) {
2944
- const stmt = db.prepare(
2945
- "DELETE FROM pre_compact_backups WHERE session_id = ?"
2946
- );
2947
- stmt.run(sessionId);
2948
- }
2949
- function countInteractions(db, filter) {
2950
- const conditions = [];
2951
- const params = [];
2952
- if (filter.sessionId) {
2953
- conditions.push("session_id = ?");
2954
- params.push(filter.sessionId);
2955
- }
2956
- if (filter.projectPath) {
2957
- conditions.push("project_path = ?");
2958
- params.push(filter.projectPath);
2959
- }
2960
- if (filter.repository) {
2961
- conditions.push("repository = ?");
2962
- params.push(filter.repository);
2963
- }
2964
- if (filter.before) {
2965
- conditions.push("timestamp < ?");
2966
- params.push(filter.before);
2967
- }
2968
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2969
- const stmt = db.prepare(
2970
- `SELECT COUNT(*) as count FROM interactions ${whereClause}`
2971
- );
2972
- const result = stmt.get(...params);
2973
- return result.count;
2974
- }
2975
-
2976
2882
  // lib/index/manager.ts
2977
2883
  import * as fs3 from "node:fs";
2978
2884
  import * as path2 from "node:path";
@@ -3333,7 +3239,117 @@ function isIndexStale(index, maxAgeMs = 5 * 60 * 1e3) {
3333
3239
  return now - updatedAt > maxAgeMs;
3334
3240
  }
3335
3241
 
3336
- // dashboard/server/index.ts
3242
+ // dashboard/server/lib/helpers.ts
3243
+ import fs4 from "node:fs";
3244
+ import path3 from "node:path";
3245
+
3246
+ // lib/suppress-sqlite-warning.ts
3247
+ var originalEmit = process.emit;
3248
+ process.emit = (event, ...args) => {
3249
+ if (event === "warning" && typeof args[0] === "object" && args[0] !== null && "name" in args[0] && args[0].name === "ExperimentalWarning" && "message" in args[0] && typeof args[0].message === "string" && args[0].message.includes("SQLite")) {
3250
+ return false;
3251
+ }
3252
+ return originalEmit.apply(process, [event, ...args]);
3253
+ };
3254
+
3255
+ // lib/db.ts
3256
+ import { execSync } from "node:child_process";
3257
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "node:fs";
3258
+ import { dirname as dirname2, join as join4 } from "node:path";
3259
+ import { fileURLToPath } from "node:url";
3260
+ var { DatabaseSync } = await import("node:sqlite");
3261
+ var __filename = fileURLToPath(import.meta.url);
3262
+ var __dirname = dirname2(__filename);
3263
+ function getCurrentUser() {
3264
+ try {
3265
+ return execSync("git config user.name", { encoding: "utf-8" }).trim();
3266
+ } catch {
3267
+ try {
3268
+ return execSync("whoami", { encoding: "utf-8" }).trim();
3269
+ } catch {
3270
+ return "unknown";
3271
+ }
3272
+ }
3273
+ }
3274
+ function getLocalDbPath(projectPath) {
3275
+ return join4(projectPath, ".mneme", "local.db");
3276
+ }
3277
+ function configurePragmas(db) {
3278
+ db.exec("PRAGMA journal_mode = WAL");
3279
+ db.exec("PRAGMA busy_timeout = 5000");
3280
+ db.exec("PRAGMA synchronous = NORMAL");
3281
+ }
3282
+ function openLocalDatabase(projectPath) {
3283
+ const dbPath = getLocalDbPath(projectPath);
3284
+ if (!existsSync5(dbPath)) {
3285
+ return null;
3286
+ }
3287
+ const db = new DatabaseSync(dbPath);
3288
+ configurePragmas(db);
3289
+ return db;
3290
+ }
3291
+ function getInteractionsBySessionIds(db, sessionIds) {
3292
+ if (sessionIds.length === 0) {
3293
+ return [];
3294
+ }
3295
+ const placeholders = sessionIds.map(() => "?").join(", ");
3296
+ const stmt = db.prepare(`
3297
+ SELECT * FROM interactions
3298
+ WHERE session_id IN (${placeholders})
3299
+ ORDER BY timestamp ASC, id ASC
3300
+ `);
3301
+ return stmt.all(...sessionIds);
3302
+ }
3303
+ function getInteractionsByClaudeSessionIds(db, claudeSessionIds) {
3304
+ if (claudeSessionIds.length === 0) {
3305
+ return [];
3306
+ }
3307
+ const placeholders = claudeSessionIds.map(() => "?").join(", ");
3308
+ const stmt = db.prepare(`
3309
+ SELECT * FROM interactions
3310
+ WHERE claude_session_id IN (${placeholders})
3311
+ ORDER BY timestamp ASC, id ASC
3312
+ `);
3313
+ return stmt.all(...claudeSessionIds);
3314
+ }
3315
+ function deleteInteractions(db, sessionId) {
3316
+ const stmt = db.prepare("DELETE FROM interactions WHERE session_id = ?");
3317
+ stmt.run(sessionId);
3318
+ }
3319
+ function deleteBackups(db, sessionId) {
3320
+ const stmt = db.prepare(
3321
+ "DELETE FROM pre_compact_backups WHERE session_id = ?"
3322
+ );
3323
+ stmt.run(sessionId);
3324
+ }
3325
+ function countInteractions(db, filter) {
3326
+ const conditions = [];
3327
+ const params = [];
3328
+ if (filter.sessionId) {
3329
+ conditions.push("session_id = ?");
3330
+ params.push(filter.sessionId);
3331
+ }
3332
+ if (filter.projectPath) {
3333
+ conditions.push("project_path = ?");
3334
+ params.push(filter.projectPath);
3335
+ }
3336
+ if (filter.repository) {
3337
+ conditions.push("repository = ?");
3338
+ params.push(filter.repository);
3339
+ }
3340
+ if (filter.before) {
3341
+ conditions.push("timestamp < ?");
3342
+ params.push(filter.before);
3343
+ }
3344
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3345
+ const stmt = db.prepare(
3346
+ `SELECT COUNT(*) as count FROM interactions ${whereClause}`
3347
+ );
3348
+ const result = stmt.get(...params);
3349
+ return result.count;
3350
+ }
3351
+
3352
+ // dashboard/server/lib/helpers.ts
3337
3353
  function sanitizeId(id) {
3338
3354
  const normalized = decodeURIComponent(id).trim();
3339
3355
  if (!normalized) return "";
@@ -3350,7 +3366,6 @@ function safeParseJsonFile(filePath) {
3350
3366
  return null;
3351
3367
  }
3352
3368
  }
3353
- var app = new Hono2();
3354
3369
  var getProjectRoot = () => {
3355
3370
  return process.env.MNEME_PROJECT_ROOT || process.cwd();
3356
3371
  };
@@ -3394,41 +3409,6 @@ function writeAuditLog(entry) {
3394
3409
  console.error("Failed to write audit log:", error);
3395
3410
  }
3396
3411
  }
3397
- var getUnitsPath = () => path3.join(getMnemeDir(), "units", "units.json");
3398
- function readUnits() {
3399
- const filePath = getUnitsPath();
3400
- if (!fs4.existsSync(filePath)) {
3401
- return {
3402
- schemaVersion: 1,
3403
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3404
- items: []
3405
- };
3406
- }
3407
- const parsed = safeParseJsonFile(filePath);
3408
- if (!parsed || !Array.isArray(parsed.items)) {
3409
- return {
3410
- schemaVersion: 1,
3411
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3412
- items: []
3413
- };
3414
- }
3415
- return parsed;
3416
- }
3417
- function writeUnits(doc) {
3418
- const filePath = getUnitsPath();
3419
- fs4.mkdirSync(path3.dirname(filePath), { recursive: true });
3420
- fs4.writeFileSync(filePath, JSON.stringify(doc, null, 2));
3421
- }
3422
- function makeUnitId(sourceType, sourceId) {
3423
- const safe = sourceId.replace(/[^a-zA-Z0-9_-]/g, "-");
3424
- return `mc-${sourceType}-${safe}`;
3425
- }
3426
- function getPatternKind(type) {
3427
- if (type === "error-solution" || type === "bad") {
3428
- return "pitfall";
3429
- }
3430
- return "playbook";
3431
- }
3432
3412
  var listDatedJsonFiles = (dir) => {
3433
3413
  const files = listJsonFiles(dir);
3434
3414
  return files.filter((filePath) => {
@@ -3465,519 +3445,575 @@ var findJsonFileById = (dir, id) => {
3465
3445
  return null;
3466
3446
  };
3467
3447
  var rulesDir = () => path3.join(getMnemeDir(), "rules");
3468
- app.use(
3469
- "/api/*",
3470
- cors({
3471
- origin: (origin) => {
3472
- if (!origin) return void 0;
3473
- if (origin.startsWith("http://localhost:")) return origin;
3474
- return null;
3448
+ var patternsDir = () => path3.join(getMnemeDir(), "patterns");
3449
+
3450
+ // dashboard/server/routes/analytics.ts
3451
+ import fs6 from "node:fs";
3452
+ import path5 from "node:path";
3453
+
3454
+ // dashboard/server/routes/dev-rules.ts
3455
+ import fs5 from "node:fs";
3456
+ import path4 from "node:path";
3457
+ function collectDevRules() {
3458
+ const items = [];
3459
+ const decisionFiles = listDatedJsonFiles(
3460
+ path4.join(getMnemeDir(), "decisions")
3461
+ );
3462
+ for (const filePath of decisionFiles) {
3463
+ const sourceName = path4.basename(filePath, ".json");
3464
+ const raw2 = safeParseJsonFile(filePath);
3465
+ if (!raw2) continue;
3466
+ const entries = [];
3467
+ if (Array.isArray(raw2.items)) {
3468
+ entries.push(...raw2.items);
3469
+ } else if (raw2.id) {
3470
+ entries.push(raw2);
3475
3471
  }
3476
- })
3477
- );
3478
- function parsePaginationParams(c) {
3479
- return {
3480
- page: Math.max(1, Number.parseInt(c.req.query("page") || "1", 10)),
3481
- limit: Math.min(
3482
- 100,
3483
- Math.max(1, Number.parseInt(c.req.query("limit") || "20", 10))
3484
- ),
3485
- tag: c.req.query("tag"),
3486
- type: c.req.query("type"),
3487
- project: c.req.query("project"),
3488
- search: c.req.query("search"),
3489
- showUntitled: c.req.query("showUntitled") === "true",
3490
- allMonths: c.req.query("allMonths") === "true"
3491
- };
3492
- }
3493
- function paginateArray(items, page, limit) {
3494
- const total = items.length;
3495
- const totalPages = Math.ceil(total / limit);
3496
- const start = (page - 1) * limit;
3497
- const data = items.slice(start, start + limit);
3498
- return {
3499
- data,
3500
- pagination: {
3501
- page,
3502
- limit,
3503
- total,
3504
- totalPages,
3505
- hasNext: page < totalPages,
3506
- hasPrev: page > 1
3472
+ for (const entry of entries) {
3473
+ const id = String(entry.id || "");
3474
+ if (!id) continue;
3475
+ items.push({
3476
+ id,
3477
+ type: "decision",
3478
+ title: String(entry.title || entry.text || id),
3479
+ summary: String(
3480
+ entry.text || entry.decision || entry.reasoning || entry.title || ""
3481
+ ),
3482
+ tags: Array.isArray(entry.tags) ? entry.tags.map((t) => String(t)) : [],
3483
+ status: entry.status || "approved",
3484
+ priority: entry.priority ? String(entry.priority) : void 0,
3485
+ sourceFile: sourceName,
3486
+ createdAt: String(entry.createdAt || raw2.createdAt || ""),
3487
+ updatedAt: entry.updatedAt ? String(entry.updatedAt) : void 0
3488
+ });
3507
3489
  }
3508
- };
3509
- }
3510
- app.get("/api/project", (c) => {
3511
- const projectRoot = getProjectRoot();
3512
- const projectName = path3.basename(projectRoot);
3513
- let repository = null;
3514
- try {
3515
- const gitConfigPath = path3.join(projectRoot, ".git", "config");
3516
- if (fs4.existsSync(gitConfigPath)) {
3517
- const gitConfig = fs4.readFileSync(gitConfigPath, "utf-8");
3518
- const match2 = gitConfig.match(
3519
- /url\s*=\s*.*[:/]([^/]+\/[^/]+?)(?:\.git)?$/m
3520
- );
3521
- if (match2) {
3522
- repository = match2[1];
3523
- }
3490
+ }
3491
+ const patternFiles = listJsonFiles(patternsDir());
3492
+ for (const filePath of patternFiles) {
3493
+ const sourceName = path4.basename(filePath, ".json");
3494
+ const doc = safeParseJsonFile(filePath);
3495
+ const entries = doc?.items || doc?.patterns || [];
3496
+ for (const entry of entries) {
3497
+ const id = String(entry.id || "");
3498
+ if (!id) continue;
3499
+ items.push({
3500
+ id,
3501
+ type: "pattern",
3502
+ title: String(
3503
+ entry.title || entry.errorPattern || entry.description || id
3504
+ ),
3505
+ summary: String(
3506
+ entry.solution || entry.description || entry.errorPattern || ""
3507
+ ),
3508
+ tags: Array.isArray(entry.tags) ? entry.tags.map((t) => String(t)) : [sourceName],
3509
+ status: entry.status || "approved",
3510
+ priority: entry.priority ? String(entry.priority) : void 0,
3511
+ sourceFile: sourceName,
3512
+ createdAt: String(entry.createdAt || ""),
3513
+ updatedAt: entry.updatedAt ? String(entry.updatedAt) : void 0
3514
+ });
3524
3515
  }
3525
- } catch {
3526
3516
  }
3527
- return c.json({
3528
- name: projectName,
3529
- path: projectRoot,
3530
- repository
3531
- });
3517
+ const ruleFileNames = ["dev-rules", "review-guidelines"];
3518
+ for (const ruleFile of ruleFileNames) {
3519
+ const filePath = path4.join(rulesDir(), `${ruleFile}.json`);
3520
+ const doc = safeParseJsonFile(
3521
+ filePath
3522
+ );
3523
+ if (!doc || !Array.isArray(doc.items)) continue;
3524
+ for (const entry of doc.items) {
3525
+ const id = String(entry.id || "");
3526
+ if (!id) continue;
3527
+ items.push({
3528
+ id,
3529
+ type: "rule",
3530
+ title: String(entry.text || entry.title || entry.rule || id),
3531
+ summary: String(entry.rationale || entry.description || ""),
3532
+ tags: Array.isArray(entry.tags) ? entry.tags.map((t) => String(t)) : [ruleFile],
3533
+ status: entry.status || "approved",
3534
+ priority: entry.priority ? String(entry.priority) : void 0,
3535
+ sourceFile: ruleFile,
3536
+ createdAt: String(entry.createdAt || ""),
3537
+ updatedAt: entry.updatedAt ? String(entry.updatedAt) : void 0
3538
+ });
3539
+ }
3540
+ }
3541
+ return items;
3542
+ }
3543
+ var devRules = new Hono2();
3544
+ devRules.get("/", async (c) => {
3545
+ try {
3546
+ const status = c.req.query("status");
3547
+ const items = collectDevRules();
3548
+ const filtered = status && ["approved", "rejected"].includes(status) ? items.filter((item) => item.status === status) : items;
3549
+ return c.json({
3550
+ items: filtered,
3551
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3552
+ });
3553
+ } catch (error) {
3554
+ console.error("Failed to read dev rules:", error);
3555
+ return c.json({ error: "Failed to read dev rules" }, 500);
3556
+ }
3532
3557
  });
3533
- app.get("/api/sessions", async (c) => {
3534
- const useIndex = c.req.query("useIndex") !== "false";
3535
- const usePagination = c.req.query("paginate") !== "false";
3536
- const mnemeDir2 = getMnemeDir();
3537
- const params = parsePaginationParams(c);
3558
+ devRules.patch("/:type/:sourceFile/:id/status", async (c) => {
3559
+ const type = c.req.param("type");
3560
+ const sourceFile = c.req.param("sourceFile");
3561
+ const id = sanitizeId(c.req.param("id"));
3562
+ const body = await c.req.json();
3563
+ if (!id || !sourceFile) {
3564
+ return c.json({ error: "Invalid parameters" }, 400);
3565
+ }
3566
+ if (!body.status || !["approved", "rejected"].includes(body.status)) {
3567
+ return c.json({ error: "Invalid status" }, 400);
3568
+ }
3538
3569
  try {
3539
- let items;
3540
- if (useIndex) {
3541
- const index = params.allMonths ? readAllSessionIndexes(mnemeDir2) : readRecentSessionIndexes(mnemeDir2);
3542
- items = index.items;
3543
- } else {
3544
- const sessionsDir = path3.join(mnemeDir2, "sessions");
3545
- const files = listDatedJsonFiles(sessionsDir);
3546
- if (files.length === 0) {
3547
- return usePagination ? c.json({
3548
- data: [],
3549
- pagination: {
3550
- page: 1,
3551
- limit: params.limit,
3552
- total: 0,
3553
- totalPages: 0,
3554
- hasNext: false,
3555
- hasPrev: false
3556
- }
3557
- }) : c.json([]);
3558
- }
3559
- items = files.map((filePath) => {
3560
- const content = fs4.readFileSync(filePath, "utf-8");
3561
- return JSON.parse(content);
3562
- });
3563
- items.sort(
3564
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
3570
+ let filePath;
3571
+ if (type === "decision") {
3572
+ const found = findJsonFileById(
3573
+ path4.join(getMnemeDir(), "decisions"),
3574
+ sourceFile
3565
3575
  );
3576
+ if (!found) return c.json({ error: "Source file not found" }, 404);
3577
+ filePath = found;
3578
+ } else if (type === "pattern") {
3579
+ filePath = path4.join(patternsDir(), `${sourceFile}.json`);
3580
+ } else {
3581
+ filePath = path4.join(rulesDir(), `${sourceFile}.json`);
3566
3582
  }
3567
- let filtered = items;
3568
- if (!params.showUntitled) {
3569
- filtered = filtered.filter((s) => s.hasSummary === true);
3583
+ if (!fs5.existsSync(filePath)) {
3584
+ return c.json({ error: "Source file not found" }, 404);
3570
3585
  }
3571
- if (params.tag) {
3572
- filtered = filtered.filter(
3573
- (s) => s.tags?.includes(params.tag)
3586
+ const raw2 = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
3587
+ const items = raw2.items || raw2.patterns || (raw2.id ? [raw2] : []);
3588
+ const target = items.find((item) => String(item.id) === id);
3589
+ if (!target) {
3590
+ return c.json({ error: "Item not found" }, 404);
3591
+ }
3592
+ target.status = body.status;
3593
+ target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3594
+ if (!raw2.items && !raw2.patterns && raw2.id) {
3595
+ Object.assign(raw2, target);
3596
+ }
3597
+ fs5.writeFileSync(filePath, `${JSON.stringify(raw2, null, 2)}
3598
+ `);
3599
+ writeAuditLog({
3600
+ entity: "dev-rule",
3601
+ action: "update",
3602
+ targetId: id,
3603
+ detail: { type, sourceFile, status: body.status }
3604
+ });
3605
+ return c.json({ id, type, sourceFile, status: body.status });
3606
+ } catch (error) {
3607
+ console.error("Failed to update dev rule status:", error);
3608
+ return c.json({ error: "Failed to update status" }, 500);
3609
+ }
3610
+ });
3611
+ devRules.delete("/:type/:sourceFile/:id", async (c) => {
3612
+ const type = c.req.param("type");
3613
+ const sourceFile = c.req.param("sourceFile");
3614
+ const id = sanitizeId(c.req.param("id"));
3615
+ if (!id || !sourceFile) {
3616
+ return c.json({ error: "Invalid parameters" }, 400);
3617
+ }
3618
+ try {
3619
+ let filePath;
3620
+ if (type === "decision") {
3621
+ const found = findJsonFileById(
3622
+ path4.join(getMnemeDir(), "decisions"),
3623
+ sourceFile
3574
3624
  );
3625
+ if (!found) return c.json({ error: "Source file not found" }, 404);
3626
+ filePath = found;
3627
+ } else if (type === "pattern") {
3628
+ filePath = path4.join(patternsDir(), `${sourceFile}.json`);
3629
+ } else {
3630
+ filePath = path4.join(rulesDir(), `${sourceFile}.json`);
3575
3631
  }
3576
- if (params.type) {
3577
- filtered = filtered.filter((s) => s.sessionType === params.type);
3632
+ if (!fs5.existsSync(filePath)) {
3633
+ return c.json({ error: "Source file not found" }, 404);
3578
3634
  }
3579
- if (params.project) {
3580
- const projectQuery = params.project;
3581
- filtered = filtered.filter((s) => {
3582
- const ctx = s.context;
3583
- const projectName = ctx?.projectName;
3584
- const repository = ctx?.repository;
3585
- return projectName === projectQuery || repository === projectQuery || repository?.endsWith(`/${projectQuery}`);
3586
- });
3635
+ const raw2 = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
3636
+ const arrayKey = raw2.items ? "items" : raw2.patterns ? "patterns" : null;
3637
+ if (arrayKey) {
3638
+ const before = raw2[arrayKey].length;
3639
+ raw2[arrayKey] = raw2[arrayKey].filter(
3640
+ (item) => String(item.id) !== id
3641
+ );
3642
+ if (raw2[arrayKey].length === before) {
3643
+ return c.json({ error: "Item not found" }, 404);
3644
+ }
3645
+ fs5.writeFileSync(filePath, `${JSON.stringify(raw2, null, 2)}
3646
+ `);
3647
+ } else if (raw2.id && String(raw2.id) === id) {
3648
+ fs5.unlinkSync(filePath);
3649
+ } else {
3650
+ return c.json({ error: "Item not found" }, 404);
3587
3651
  }
3588
- if (params.search) {
3589
- const query = params.search.toLowerCase();
3590
- filtered = filtered.filter((s) => {
3591
- const title = (s.title || "").toLowerCase();
3592
- const goal = (s.goal || "").toLowerCase();
3593
- return title.includes(query) || goal.includes(query);
3652
+ writeAuditLog({
3653
+ entity: "dev-rule",
3654
+ action: "delete",
3655
+ targetId: id,
3656
+ detail: { type, sourceFile }
3657
+ });
3658
+ return c.json({ deleted: 1, id });
3659
+ } catch (error) {
3660
+ console.error("Failed to delete dev rule:", error);
3661
+ return c.json({ error: "Failed to delete dev rule" }, 500);
3662
+ }
3663
+ });
3664
+ var dev_rules_default = devRules;
3665
+
3666
+ // dashboard/server/routes/analytics.ts
3667
+ var analytics = new Hono2();
3668
+ analytics.get("/timeline", async (c) => {
3669
+ const sessionsDir = path5.join(getMnemeDir(), "sessions");
3670
+ try {
3671
+ const files = listDatedJsonFiles(sessionsDir);
3672
+ if (files.length === 0) {
3673
+ return c.json({ timeline: {} });
3674
+ }
3675
+ const sessions2 = files.map((filePath) => {
3676
+ const content = fs6.readFileSync(filePath, "utf-8");
3677
+ return JSON.parse(content);
3678
+ });
3679
+ const grouped = {};
3680
+ for (const session of sessions2) {
3681
+ const date = session.createdAt?.split("T")[0] || "unknown";
3682
+ if (!grouped[date]) grouped[date] = [];
3683
+ grouped[date].push({
3684
+ id: session.id,
3685
+ title: session.title || "Untitled",
3686
+ sessionType: session.sessionType,
3687
+ branch: session.context?.branch,
3688
+ tags: session.tags || [],
3689
+ createdAt: session.createdAt
3594
3690
  });
3595
3691
  }
3596
- if (!usePagination) {
3597
- return c.json(filtered);
3692
+ const sortedTimeline = {};
3693
+ for (const date of Object.keys(grouped).sort().reverse()) {
3694
+ sortedTimeline[date] = grouped[date].sort(
3695
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
3696
+ );
3598
3697
  }
3599
- return c.json(paginateArray(filtered, params.page, params.limit));
3698
+ return c.json({ timeline: sortedTimeline });
3600
3699
  } catch (error) {
3601
- console.error("Failed to read sessions:", error);
3602
- return c.json({ error: "Failed to read sessions" }, 500);
3700
+ console.error("Failed to build timeline:", error);
3701
+ return c.json({ error: "Failed to build timeline" }, 500);
3603
3702
  }
3604
3703
  });
3605
- app.get("/api/sessions/graph", async (c) => {
3606
- const mnemeDir2 = getMnemeDir();
3607
- const showUntitled = c.req.query("showUntitled") === "true";
3704
+ analytics.get("/tag-network", async (c) => {
3705
+ const sessionsDir = path5.join(getMnemeDir(), "sessions");
3608
3706
  try {
3609
- const sessionsIndex = readAllSessionIndexes(mnemeDir2);
3610
- const filteredItems = showUntitled ? sessionsIndex.items : sessionsIndex.items.filter((s) => s.hasSummary === true);
3611
- const nodes = filteredItems.map((session) => ({
3612
- id: session.id,
3613
- title: session.title,
3614
- type: session.sessionType || "unknown",
3615
- tags: session.tags || [],
3616
- createdAt: session.createdAt
3707
+ const files = listDatedJsonFiles(sessionsDir);
3708
+ const tagCounts = /* @__PURE__ */ new Map();
3709
+ const coOccurrences = /* @__PURE__ */ new Map();
3710
+ for (const filePath of files) {
3711
+ const content = fs6.readFileSync(filePath, "utf-8");
3712
+ const session = JSON.parse(content);
3713
+ const tags = session.tags || [];
3714
+ for (const tag of tags) {
3715
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
3716
+ }
3717
+ for (let i = 0; i < tags.length; i++) {
3718
+ for (let j = i + 1; j < tags.length; j++) {
3719
+ const key = [tags[i], tags[j]].sort().join("|");
3720
+ coOccurrences.set(key, (coOccurrences.get(key) || 0) + 1);
3721
+ }
3722
+ }
3723
+ }
3724
+ const nodes = Array.from(tagCounts.entries()).map(([id, count]) => ({
3725
+ id,
3726
+ count
3617
3727
  }));
3728
+ const edges = Array.from(coOccurrences.entries()).map(([key, weight]) => {
3729
+ const [source, target] = key.split("|");
3730
+ return { source, target, weight };
3731
+ });
3732
+ return c.json({ nodes, edges });
3733
+ } catch (error) {
3734
+ console.error("Failed to build tag network:", error);
3735
+ return c.json({ error: "Failed to build tag network" }, 500);
3736
+ }
3737
+ });
3738
+ analytics.get("/knowledge-graph", async (c) => {
3739
+ try {
3740
+ const mnemeDir2 = getMnemeDir();
3741
+ const sessionItems = readAllSessionIndexes(mnemeDir2).items;
3742
+ const devRules2 = collectDevRules().filter(
3743
+ (item) => item.status === "approved"
3744
+ );
3745
+ const sessionDataMap = /* @__PURE__ */ new Map();
3746
+ for (const item of sessionItems.filter((i) => i.hasSummary)) {
3747
+ try {
3748
+ const sessionPath = path5.join(mnemeDir2, item.filePath);
3749
+ const raw2 = fs6.readFileSync(sessionPath, "utf-8");
3750
+ const session = JSON.parse(raw2);
3751
+ if (session.resumedFrom) {
3752
+ sessionDataMap.set(item.id, {
3753
+ resumedFrom: session.resumedFrom
3754
+ });
3755
+ }
3756
+ } catch {
3757
+ }
3758
+ }
3759
+ const nodes = [
3760
+ ...sessionItems.filter((item) => item.hasSummary).map((item) => ({
3761
+ id: `session:${item.id}`,
3762
+ entityType: "session",
3763
+ entityId: item.id,
3764
+ title: item.title,
3765
+ tags: item.tags || [],
3766
+ createdAt: item.createdAt,
3767
+ branch: item.branch || null,
3768
+ resumedFrom: sessionDataMap.get(item.id)?.resumedFrom || null,
3769
+ unitSubtype: null,
3770
+ sourceId: null,
3771
+ appliedCount: null,
3772
+ acceptedCount: null
3773
+ })),
3774
+ ...devRules2.map((item) => ({
3775
+ id: `rule:${item.type}:${item.id}`,
3776
+ entityType: "rule",
3777
+ entityId: item.id,
3778
+ title: item.title,
3779
+ tags: item.tags || [],
3780
+ createdAt: item.createdAt,
3781
+ unitSubtype: item.type || null,
3782
+ sourceId: item.sourceFile || null,
3783
+ appliedCount: null,
3784
+ acceptedCount: null,
3785
+ branch: null,
3786
+ resumedFrom: null
3787
+ }))
3788
+ ];
3618
3789
  const tagToNodes = /* @__PURE__ */ new Map();
3619
- for (const item of filteredItems) {
3620
- for (const tag of item.tags || []) {
3790
+ for (const node of nodes) {
3791
+ for (const tag of node.tags) {
3621
3792
  const list = tagToNodes.get(tag) || [];
3622
- list.push(item.id);
3793
+ list.push(node.id);
3623
3794
  tagToNodes.set(tag, list);
3624
3795
  }
3625
3796
  }
3626
3797
  const edgeMap = /* @__PURE__ */ new Map();
3627
- for (const [, nodeIds] of tagToNodes) {
3798
+ for (const [tag, nodeIds] of tagToNodes) {
3628
3799
  for (let i = 0; i < nodeIds.length; i++) {
3629
3800
  for (let j = i + 1; j < nodeIds.length; j++) {
3630
3801
  const key = nodeIds[i] < nodeIds[j] ? `${nodeIds[i]}|${nodeIds[j]}` : `${nodeIds[j]}|${nodeIds[i]}`;
3631
3802
  const existing = edgeMap.get(key);
3632
3803
  if (existing) {
3633
3804
  existing.weight++;
3805
+ existing.sharedTags.push(tag);
3634
3806
  } else {
3635
3807
  const [source, target] = key.split("|");
3636
- edgeMap.set(key, { source, target, weight: 1 });
3808
+ edgeMap.set(key, {
3809
+ source,
3810
+ target,
3811
+ weight: 1,
3812
+ sharedTags: [tag],
3813
+ edgeType: "sharedTags",
3814
+ directed: false
3815
+ });
3637
3816
  }
3638
3817
  }
3639
3818
  }
3640
3819
  }
3641
- const edges = Array.from(edgeMap.values());
3642
- return c.json({ nodes, edges });
3643
- } catch (error) {
3644
- console.error("Failed to build session graph:", error);
3645
- return c.json({ error: "Failed to build session graph" }, 500);
3646
- }
3647
- });
3648
- app.get("/api/sessions/:id", async (c) => {
3649
- const id = sanitizeId(c.req.param("id"));
3650
- const sessionsDir = path3.join(getMnemeDir(), "sessions");
3651
- try {
3652
- const filePath = findJsonFileById(sessionsDir, id);
3653
- if (!filePath) {
3654
- return c.json({ error: "Session not found" }, 404);
3655
- }
3656
- const session = safeParseJsonFile(filePath);
3657
- if (!session) {
3658
- return c.json({ error: "Failed to parse session" }, 500);
3659
- }
3660
- return c.json(session);
3661
- } catch (error) {
3662
- console.error("Failed to read session:", error);
3663
- return c.json({ error: "Failed to read session" }, 500);
3664
- }
3665
- });
3666
- app.get("/api/sessions/:id/markdown", async (c) => {
3667
- const id = sanitizeId(c.req.param("id"));
3668
- const sessionsDir = path3.join(getMnemeDir(), "sessions");
3669
- try {
3670
- const jsonPath = findJsonFileById(sessionsDir, id);
3671
- if (!jsonPath) {
3672
- return c.json({ error: "Session not found" }, 404);
3673
- }
3674
- const mdPath = jsonPath.replace(/\.json$/, ".md");
3675
- if (!fs4.existsSync(mdPath)) {
3676
- return c.json({ exists: false, content: null });
3820
+ const tagEdges = Array.from(edgeMap.values());
3821
+ const nodeIdSet = new Set(nodes.map((n) => n.id));
3822
+ const resumedEdges = [];
3823
+ for (const node of nodes) {
3824
+ if (node.entityType === "session" && node.resumedFrom) {
3825
+ const targetId = `session:${node.resumedFrom}`;
3826
+ if (nodeIdSet.has(targetId)) {
3827
+ resumedEdges.push({
3828
+ source: targetId,
3829
+ target: node.id,
3830
+ weight: 1,
3831
+ sharedTags: [],
3832
+ edgeType: "resumedFrom",
3833
+ directed: true
3834
+ });
3835
+ }
3836
+ }
3677
3837
  }
3678
- const content = fs4.readFileSync(mdPath, "utf-8");
3679
- return c.json({ exists: true, content });
3838
+ const edges = [...tagEdges, ...resumedEdges];
3839
+ return c.json({ nodes, edges });
3680
3840
  } catch (error) {
3681
- console.error("Failed to read session markdown:", error);
3682
- return c.json({ error: "Failed to read session markdown" }, 500);
3841
+ console.error("Failed to build knowledge graph:", error);
3842
+ return c.json({ error: "Failed to build knowledge graph" }, 500);
3683
3843
  }
3684
3844
  });
3685
- app.delete("/api/sessions/:id", async (c) => {
3686
- const id = sanitizeId(c.req.param("id"));
3687
- const dryRun = c.req.query("dry-run") === "true";
3845
+ analytics.get("/stats/overview", async (c) => {
3688
3846
  const mnemeDir2 = getMnemeDir();
3689
- const sessionsDir = path3.join(mnemeDir2, "sessions");
3690
3847
  try {
3691
- const filePath = findJsonFileById(sessionsDir, id);
3692
- if (!filePath) {
3693
- return c.json({ error: "Session not found" }, 404);
3848
+ const sessionsIndex = readAllSessionIndexes(mnemeDir2);
3849
+ const decisionsIndex = readAllDecisionIndexes(mnemeDir2);
3850
+ const validSessions = sessionsIndex.items.filter(
3851
+ (session) => session.interactionCount > 0 || session.hasSummary === true
3852
+ );
3853
+ const sessionTypeCount = {};
3854
+ for (const session of validSessions) {
3855
+ const type = session.sessionType || "unknown";
3856
+ sessionTypeCount[type] = (sessionTypeCount[type] || 0) + 1;
3694
3857
  }
3695
- const sessionData = safeParseJsonFile(filePath);
3696
- const db = openLocalDatabase(getProjectRoot());
3697
- let interactionCount = 0;
3698
- if (db) {
3699
- interactionCount = countInteractions(db, { sessionId: id });
3700
- if (!dryRun) {
3701
- deleteInteractions(db, id);
3702
- deleteBackups(db, id);
3858
+ let totalPatterns = 0;
3859
+ const patternsByType = {};
3860
+ const patternsPath = path5.join(mnemeDir2, "patterns");
3861
+ if (fs6.existsSync(patternsPath)) {
3862
+ const patternFiles = listJsonFiles(patternsPath);
3863
+ for (const filePath of patternFiles) {
3864
+ try {
3865
+ const content = fs6.readFileSync(filePath, "utf-8");
3866
+ const data = JSON.parse(content);
3867
+ const patterns2 = data.patterns || [];
3868
+ for (const pattern of patterns2) {
3869
+ totalPatterns++;
3870
+ const type = pattern.type || "unknown";
3871
+ patternsByType[type] = (patternsByType[type] || 0) + 1;
3872
+ }
3873
+ } catch {
3874
+ }
3703
3875
  }
3704
- db.close();
3705
3876
  }
3706
- if (!dryRun) {
3707
- fs4.unlinkSync(filePath);
3708
- const mdPath = filePath.replace(/\.json$/, ".md");
3709
- if (fs4.existsSync(mdPath)) {
3710
- fs4.unlinkSync(mdPath);
3711
- }
3712
- const sessionLinksDir = path3.join(mnemeDir2, "session-links");
3713
- const linkPath = path3.join(sessionLinksDir, `${id}.json`);
3714
- if (fs4.existsSync(linkPath)) {
3715
- fs4.unlinkSync(linkPath);
3716
- }
3717
- if (sessionData?.createdAt) {
3718
- const date = new Date(sessionData.createdAt);
3719
- const year = date.getFullYear().toString();
3720
- const month = (date.getMonth() + 1).toString().padStart(2, "0");
3721
- rebuildSessionIndexForMonth(mnemeDir2, year, month);
3877
+ let totalRules = 0;
3878
+ const rulesByType = {};
3879
+ const rulesPath = path5.join(mnemeDir2, "rules");
3880
+ if (fs6.existsSync(rulesPath)) {
3881
+ for (const ruleType of ["dev-rules", "review-guidelines"]) {
3882
+ const rulePath = path5.join(rulesPath, `${ruleType}.json`);
3883
+ if (fs6.existsSync(rulePath)) {
3884
+ try {
3885
+ const content = fs6.readFileSync(rulePath, "utf-8");
3886
+ const data = JSON.parse(content);
3887
+ const items = data.items || [];
3888
+ const activeItems = items.filter(
3889
+ (item) => item.status === "active"
3890
+ );
3891
+ rulesByType[ruleType] = activeItems.length;
3892
+ totalRules += activeItems.length;
3893
+ } catch {
3894
+ }
3895
+ }
3722
3896
  }
3723
- writeAuditLog({
3724
- entity: "session",
3725
- action: "delete",
3726
- targetId: id
3727
- });
3728
3897
  }
3729
3898
  return c.json({
3730
- deleted: dryRun ? 0 : 1,
3731
- interactionsDeleted: dryRun ? 0 : interactionCount,
3732
- dryRun,
3733
- sessionId: id
3899
+ sessions: {
3900
+ total: validSessions.length,
3901
+ byType: sessionTypeCount
3902
+ },
3903
+ decisions: {
3904
+ total: decisionsIndex.items.length
3905
+ },
3906
+ patterns: {
3907
+ total: totalPatterns,
3908
+ byType: patternsByType
3909
+ },
3910
+ rules: {
3911
+ total: totalRules,
3912
+ byType: rulesByType
3913
+ }
3734
3914
  });
3735
3915
  } catch (error) {
3736
- console.error("Failed to delete session:", error);
3737
- return c.json({ error: "Failed to delete session" }, 500);
3916
+ console.error("Failed to get stats overview:", error);
3917
+ return c.json({ error: "Failed to get stats overview" }, 500);
3738
3918
  }
3739
3919
  });
3740
- app.delete("/api/sessions", async (c) => {
3741
- const dryRun = c.req.query("dry-run") === "true";
3742
- const projectFilter = c.req.query("project");
3743
- const repositoryFilter = c.req.query("repository");
3744
- const beforeFilter = c.req.query("before");
3920
+ analytics.get("/stats/activity", async (c) => {
3745
3921
  const mnemeDir2 = getMnemeDir();
3746
- const sessionsDir = path3.join(mnemeDir2, "sessions");
3922
+ const daysParam = Number.parseInt(c.req.query("days") || "30", 10);
3923
+ const MAX_DAYS = 365;
3924
+ const safeDays = Math.min(Math.max(1, daysParam), MAX_DAYS);
3747
3925
  try {
3748
- const files = listDatedJsonFiles(sessionsDir);
3749
- const sessionsToDelete = [];
3750
- for (const filePath of files) {
3751
- try {
3752
- const content = fs4.readFileSync(filePath, "utf-8");
3753
- const session = JSON.parse(content);
3754
- let shouldDelete = true;
3755
- if (projectFilter) {
3756
- const sessionProject = session.context?.projectDir;
3757
- if (sessionProject !== projectFilter) {
3758
- shouldDelete = false;
3759
- }
3760
- }
3761
- if (repositoryFilter && shouldDelete) {
3762
- const sessionRepo = session.context?.repository;
3763
- if (sessionRepo !== repositoryFilter) {
3764
- shouldDelete = false;
3765
- }
3766
- }
3767
- if (beforeFilter && shouldDelete) {
3768
- const sessionDate = session.createdAt?.split("T")[0];
3769
- if (!sessionDate || sessionDate >= beforeFilter) {
3770
- shouldDelete = false;
3771
- }
3772
- }
3773
- if (shouldDelete) {
3774
- sessionsToDelete.push({ id: session.id, path: filePath });
3775
- }
3776
- } catch {
3777
- }
3926
+ const sessionsIndex = readAllSessionIndexes(mnemeDir2);
3927
+ const decisionsIndex = readAllDecisionIndexes(mnemeDir2);
3928
+ const now = /* @__PURE__ */ new Date();
3929
+ const startDate = new Date(
3930
+ now.getTime() - (safeDays - 1) * 24 * 60 * 60 * 1e3
3931
+ );
3932
+ const activityByDate = {};
3933
+ for (let i = 0; i < safeDays; i++) {
3934
+ const d = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1e3);
3935
+ const dateKey = d.toISOString().split("T")[0];
3936
+ activityByDate[dateKey] = { sessions: 0, decisions: 0 };
3778
3937
  }
3779
- let totalInteractions = 0;
3780
- const db = openLocalDatabase(getProjectRoot());
3781
- if (db) {
3782
- for (const session of sessionsToDelete) {
3783
- totalInteractions += countInteractions(db, { sessionId: session.id });
3784
- }
3785
- if (!dryRun) {
3786
- for (const session of sessionsToDelete) {
3787
- deleteInteractions(db, session.id);
3788
- deleteBackups(db, session.id);
3789
- }
3938
+ for (const session of sessionsIndex.items) {
3939
+ const dateKey = session.createdAt.split("T")[0];
3940
+ if (activityByDate[dateKey]) {
3941
+ activityByDate[dateKey].sessions += 1;
3790
3942
  }
3791
- db.close();
3792
3943
  }
3793
- if (!dryRun) {
3794
- for (const session of sessionsToDelete) {
3795
- fs4.unlinkSync(session.path);
3796
- const mdPath = session.path.replace(/\.json$/, ".md");
3797
- if (fs4.existsSync(mdPath)) {
3798
- fs4.unlinkSync(mdPath);
3799
- }
3800
- const sessionLinksDir = path3.join(mnemeDir2, "session-links");
3801
- const linkPath = path3.join(sessionLinksDir, `${session.id}.json`);
3802
- if (fs4.existsSync(linkPath)) {
3803
- fs4.unlinkSync(linkPath);
3804
- }
3805
- writeAuditLog({
3806
- entity: "session",
3807
- action: "delete",
3808
- targetId: session.id
3809
- });
3944
+ for (const decision of decisionsIndex.items) {
3945
+ const dateKey = decision.createdAt.split("T")[0];
3946
+ if (activityByDate[dateKey]) {
3947
+ activityByDate[dateKey].decisions += 1;
3810
3948
  }
3811
3949
  }
3812
- return c.json({
3813
- deleted: dryRun ? 0 : sessionsToDelete.length,
3814
- interactionsDeleted: dryRun ? 0 : totalInteractions,
3815
- wouldDelete: sessionsToDelete.length,
3816
- dryRun,
3817
- filters: {
3818
- project: projectFilter || null,
3819
- repository: repositoryFilter || null,
3820
- before: beforeFilter || null
3821
- }
3822
- });
3823
- } catch (error) {
3824
- console.error("Failed to delete sessions:", error);
3825
- return c.json({ error: "Failed to delete sessions" }, 500);
3826
- }
3827
- });
3828
- app.get("/api/current-user", async (c) => {
3829
- try {
3830
- const user = getCurrentUser();
3831
- return c.json({ user });
3950
+ const activity = Object.entries(activityByDate).map(([date, counts]) => ({ date, ...counts })).sort((a, b) => a.date.localeCompare(b.date));
3951
+ return c.json({ activity, days: safeDays });
3832
3952
  } catch (error) {
3833
- console.error("Failed to get current user:", error);
3834
- return c.json({ error: "Failed to get current user" }, 500);
3953
+ console.error("Failed to get activity stats:", error);
3954
+ return c.json({ error: "Failed to get activity stats" }, 500);
3835
3955
  }
3836
3956
  });
3837
- app.get("/api/sessions/:id/interactions", async (c) => {
3838
- const id = sanitizeId(c.req.param("id"));
3957
+ analytics.get("/stats/tags", async (c) => {
3839
3958
  const mnemeDir2 = getMnemeDir();
3840
- const sessionLinksDir = path3.join(mnemeDir2, "session-links");
3841
- const sessionsDir = path3.join(mnemeDir2, "sessions");
3842
3959
  try {
3843
- const sessionFilePath = findJsonFileById(sessionsDir, id);
3844
- let projectDir = getProjectRoot();
3845
- if (sessionFilePath) {
3846
- const sessionData = safeParseJsonFile(sessionFilePath);
3847
- if (sessionData?.context?.projectDir) {
3848
- projectDir = sessionData.context.projectDir;
3849
- }
3850
- }
3851
- const db = openLocalDatabase(projectDir);
3852
- if (!db) {
3853
- return c.json({ interactions: [], count: 0 });
3854
- }
3855
- let masterId = id;
3856
- const myLinkFile = path3.join(sessionLinksDir, `${id}.json`);
3857
- if (fs4.existsSync(myLinkFile)) {
3858
- try {
3859
- const myLinkData = JSON.parse(fs4.readFileSync(myLinkFile, "utf-8"));
3860
- if (myLinkData.masterSessionId) {
3861
- masterId = myLinkData.masterSessionId;
3862
- }
3863
- } catch {
3960
+ const sessionsIndex = readAllSessionIndexes(mnemeDir2);
3961
+ const tagCount = {};
3962
+ for (const session of sessionsIndex.items) {
3963
+ for (const tag of session.tags || []) {
3964
+ tagCount[tag] = (tagCount[tag] || 0) + 1;
3864
3965
  }
3865
3966
  }
3866
- const sessionIds = [masterId];
3867
- if (masterId !== id) {
3868
- sessionIds.push(id);
3869
- }
3870
- if (fs4.existsSync(sessionLinksDir)) {
3871
- const linkFiles = fs4.readdirSync(sessionLinksDir);
3872
- for (const linkFile of linkFiles) {
3873
- if (!linkFile.endsWith(".json")) continue;
3874
- const linkPath = path3.join(sessionLinksDir, linkFile);
3875
- try {
3876
- const linkData = JSON.parse(fs4.readFileSync(linkPath, "utf-8"));
3877
- if (linkData.masterSessionId === masterId) {
3878
- const childId = linkFile.replace(".json", "");
3879
- if (!sessionIds.includes(childId)) {
3880
- sessionIds.push(childId);
3881
- }
3882
- }
3883
- } catch {
3884
- }
3885
- }
3886
- }
3887
- const sessionFiles = listDatedJsonFiles(sessionsDir);
3888
- for (const sessionFile of sessionFiles) {
3889
- try {
3890
- const sessionData = JSON.parse(fs4.readFileSync(sessionFile, "utf-8"));
3891
- if (sessionData.resumedFrom === masterId && sessionData.id !== masterId) {
3892
- if (!sessionIds.includes(sessionData.id)) {
3893
- sessionIds.push(sessionData.id);
3894
- }
3895
- }
3896
- } catch {
3897
- }
3898
- }
3899
- const interactions = getInteractionsBySessionIds(db, sessionIds);
3900
- db.close();
3901
- const groupedInteractions = [];
3902
- let currentInteraction = null;
3903
- for (const interaction of interactions) {
3904
- if (interaction.role === "user") {
3905
- if (currentInteraction) {
3906
- groupedInteractions.push(currentInteraction);
3907
- }
3908
- let hasPlanMode;
3909
- let planTools;
3910
- let toolsUsed;
3911
- let toolDetails;
3912
- let inPlanMode;
3913
- let slashCommand;
3914
- let toolResults;
3915
- let progressEvents;
3916
- if (interaction.tool_calls) {
3917
- try {
3918
- const metadata = JSON.parse(interaction.tool_calls);
3919
- if (metadata.hasPlanMode) {
3920
- hasPlanMode = true;
3921
- planTools = metadata.planTools || [];
3922
- }
3923
- if (metadata.inPlanMode) {
3924
- inPlanMode = true;
3925
- }
3926
- if (metadata.toolsUsed && Array.isArray(metadata.toolsUsed) && metadata.toolsUsed.length > 0) {
3927
- toolsUsed = metadata.toolsUsed;
3928
- }
3929
- if (metadata.toolDetails && Array.isArray(metadata.toolDetails) && metadata.toolDetails.length > 0) {
3930
- toolDetails = metadata.toolDetails;
3931
- }
3932
- if (metadata.slashCommand) {
3933
- slashCommand = metadata.slashCommand;
3934
- }
3935
- if (metadata.toolResults && Array.isArray(metadata.toolResults) && metadata.toolResults.length > 0) {
3936
- toolResults = metadata.toolResults;
3937
- }
3938
- if (metadata.progressEvents && Array.isArray(metadata.progressEvents) && metadata.progressEvents.length > 0) {
3939
- progressEvents = metadata.progressEvents;
3940
- }
3941
- } catch {
3942
- }
3943
- }
3944
- currentInteraction = {
3945
- id: `int-${String(groupedInteractions.length + 1).padStart(3, "0")}`,
3946
- timestamp: interaction.timestamp,
3947
- user: interaction.content,
3948
- assistant: "",
3949
- thinking: null,
3950
- isCompactSummary: !!interaction.is_compact_summary,
3951
- ...hasPlanMode !== void 0 && { hasPlanMode },
3952
- ...planTools !== void 0 && planTools.length > 0 && { planTools },
3953
- ...toolsUsed !== void 0 && toolsUsed.length > 0 && { toolsUsed },
3954
- ...toolDetails !== void 0 && toolDetails.length > 0 && { toolDetails },
3955
- ...interaction.agent_id && { agentId: interaction.agent_id },
3956
- ...interaction.agent_type && { agentType: interaction.agent_type },
3957
- // New metadata fields
3958
- ...inPlanMode && { inPlanMode },
3959
- ...slashCommand && { slashCommand },
3960
- ...toolResults !== void 0 && toolResults.length > 0 && { toolResults },
3961
- ...progressEvents !== void 0 && progressEvents.length > 0 && { progressEvents }
3962
- };
3963
- } else if (interaction.role === "assistant" && currentInteraction) {
3964
- currentInteraction.assistant = interaction.content;
3965
- currentInteraction.thinking = interaction.thinking || null;
3966
- }
3967
- }
3968
- if (currentInteraction) {
3969
- groupedInteractions.push(currentInteraction);
3970
- }
3971
- return c.json({
3972
- interactions: groupedInteractions,
3973
- count: groupedInteractions.length
3974
- });
3967
+ const tags = Object.entries(tagCount).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count).slice(0, 20);
3968
+ return c.json({ tags });
3975
3969
  } catch (error) {
3976
- console.error("Failed to get session interactions:", error);
3977
- return c.json({ error: "Failed to get session interactions" }, 500);
3970
+ console.error("Failed to get tag stats:", error);
3971
+ return c.json({ error: "Failed to get tag stats" }, 500);
3978
3972
  }
3979
3973
  });
3980
- app.get("/api/decisions", async (c) => {
3974
+ var analytics_default = analytics;
3975
+
3976
+ // dashboard/server/routes/decisions.ts
3977
+ import fs7 from "node:fs";
3978
+ import path6 from "node:path";
3979
+
3980
+ // dashboard/server/lib/pagination.ts
3981
+ function parsePaginationParams(c) {
3982
+ return {
3983
+ page: Math.max(1, Number.parseInt(c.req.query("page") || "1", 10)),
3984
+ limit: Math.min(
3985
+ 100,
3986
+ Math.max(1, Number.parseInt(c.req.query("limit") || "20", 10))
3987
+ ),
3988
+ tag: c.req.query("tag"),
3989
+ type: c.req.query("type"),
3990
+ project: c.req.query("project"),
3991
+ search: c.req.query("search"),
3992
+ showUntitled: c.req.query("showUntitled") === "true",
3993
+ allMonths: c.req.query("allMonths") === "true"
3994
+ };
3995
+ }
3996
+ function paginateArray(items, page, limit) {
3997
+ const total = items.length;
3998
+ const totalPages = Math.ceil(total / limit);
3999
+ const start = (page - 1) * limit;
4000
+ const data = items.slice(start, start + limit);
4001
+ return {
4002
+ data,
4003
+ pagination: {
4004
+ page,
4005
+ limit,
4006
+ total,
4007
+ totalPages,
4008
+ hasNext: page < totalPages,
4009
+ hasPrev: page > 1
4010
+ }
4011
+ };
4012
+ }
4013
+
4014
+ // dashboard/server/routes/decisions.ts
4015
+ var decisions = new Hono2();
4016
+ decisions.get("/", async (c) => {
3981
4017
  const useIndex = c.req.query("useIndex") !== "false";
3982
4018
  const usePagination = c.req.query("paginate") !== "false";
3983
4019
  const mnemeDir2 = getMnemeDir();
@@ -3988,7 +4024,7 @@ app.get("/api/decisions", async (c) => {
3988
4024
  const index = params.allMonths ? readAllDecisionIndexes(mnemeDir2) : readRecentDecisionIndexes(mnemeDir2);
3989
4025
  items = index.items;
3990
4026
  } else {
3991
- const decisionsDir = path3.join(mnemeDir2, "decisions");
4027
+ const decisionsDir = path6.join(mnemeDir2, "decisions");
3992
4028
  const files = listDatedJsonFiles(decisionsDir);
3993
4029
  if (files.length === 0) {
3994
4030
  return usePagination ? c.json({
@@ -4004,7 +4040,7 @@ app.get("/api/decisions", async (c) => {
4004
4040
  }) : c.json([]);
4005
4041
  }
4006
4042
  items = files.map((filePath) => {
4007
- const content = fs4.readFileSync(filePath, "utf-8");
4043
+ const content = fs7.readFileSync(filePath, "utf-8");
4008
4044
  return JSON.parse(content);
4009
4045
  });
4010
4046
  items.sort(
@@ -4034,9 +4070,9 @@ app.get("/api/decisions", async (c) => {
4034
4070
  return c.json({ error: "Failed to read decisions" }, 500);
4035
4071
  }
4036
4072
  });
4037
- app.get("/api/decisions/:id", async (c) => {
4073
+ decisions.get("/:id", async (c) => {
4038
4074
  const id = sanitizeId(c.req.param("id"));
4039
- const decisionsDir = path3.join(getMnemeDir(), "decisions");
4075
+ const decisionsDir = path6.join(getMnemeDir(), "decisions");
4040
4076
  try {
4041
4077
  const filePath = findJsonFileById(decisionsDir, id);
4042
4078
  if (!filePath) {
@@ -4052,15 +4088,60 @@ app.get("/api/decisions/:id", async (c) => {
4052
4088
  return c.json({ error: "Failed to read decision" }, 500);
4053
4089
  }
4054
4090
  });
4055
- app.delete("/api/decisions/:id", async (c) => {
4091
+ decisions.get("/:id/impact", async (c) => {
4092
+ const decisionId = sanitizeId(c.req.param("id"));
4093
+ const sessionsDir = path6.join(getMnemeDir(), "sessions");
4094
+ const patternsPath = path6.join(getMnemeDir(), "patterns");
4095
+ try {
4096
+ const impactedSessions = [];
4097
+ const impactedPatterns = [];
4098
+ const sessionFiles = listDatedJsonFiles(sessionsDir);
4099
+ for (const filePath of sessionFiles) {
4100
+ const content = fs7.readFileSync(filePath, "utf-8");
4101
+ const session = JSON.parse(content);
4102
+ const hasReference = session.relatedSessions?.includes(decisionId) || session.interactions?.some(
4103
+ (i) => i.reasoning?.includes(decisionId) || i.choice?.includes(decisionId)
4104
+ );
4105
+ if (hasReference) {
4106
+ impactedSessions.push({
4107
+ id: session.id,
4108
+ title: session.title || "Untitled"
4109
+ });
4110
+ }
4111
+ }
4112
+ const patternFiles = listJsonFiles(patternsPath);
4113
+ for (const filePath of patternFiles) {
4114
+ const content = fs7.readFileSync(filePath, "utf-8");
4115
+ const data = JSON.parse(content);
4116
+ const patterns2 = data.patterns || [];
4117
+ for (const pattern of patterns2) {
4118
+ if (pattern.sourceId?.includes(decisionId) || pattern.description?.includes(decisionId)) {
4119
+ impactedPatterns.push({
4120
+ id: `${path6.basename(filePath, ".json")}-${pattern.type}`,
4121
+ description: pattern.description || "No description"
4122
+ });
4123
+ }
4124
+ }
4125
+ }
4126
+ return c.json({
4127
+ decisionId,
4128
+ impactedSessions,
4129
+ impactedPatterns
4130
+ });
4131
+ } catch (error) {
4132
+ console.error("Failed to analyze decision impact:", error);
4133
+ return c.json({ error: "Failed to analyze decision impact" }, 500);
4134
+ }
4135
+ });
4136
+ decisions.delete("/:id", async (c) => {
4056
4137
  const id = sanitizeId(c.req.param("id"));
4057
- const decisionsDir = path3.join(getMnemeDir(), "decisions");
4138
+ const decisionsDir = path6.join(getMnemeDir(), "decisions");
4058
4139
  try {
4059
4140
  const filePath = findJsonFileById(decisionsDir, id);
4060
4141
  if (!filePath) {
4061
4142
  return c.json({ error: "Decision not found" }, 404);
4062
4143
  }
4063
- fs4.unlinkSync(filePath);
4144
+ fs7.unlinkSync(filePath);
4064
4145
  rebuildAllDecisionIndexes(getMnemeDir());
4065
4146
  writeAuditLog({
4066
4147
  entity: "decision",
@@ -4073,227 +4154,408 @@ app.delete("/api/decisions/:id", async (c) => {
4073
4154
  return c.json({ error: "Failed to delete decision" }, 500);
4074
4155
  }
4075
4156
  });
4076
- app.get("/api/info", async (c) => {
4077
- const projectRoot = getProjectRoot();
4078
- const mnemeDir2 = getMnemeDir();
4079
- return c.json({
4080
- projectRoot,
4081
- mnemeDir: mnemeDir2,
4082
- exists: fs4.existsSync(mnemeDir2)
4083
- });
4084
- });
4085
- app.get("/api/rules/:id", async (c) => {
4086
- const id = c.req.param("id");
4087
- if (!ALLOWED_RULE_FILES.has(id)) {
4088
- return c.json({ error: "Invalid rule type" }, 400);
4089
- }
4090
- const dir = rulesDir();
4091
- try {
4092
- const filePath = path3.join(dir, `${id}.json`);
4093
- if (!fs4.existsSync(filePath)) {
4094
- return c.json({ error: "Rules not found" }, 404);
4095
- }
4096
- const rules = safeParseJsonFile(filePath);
4097
- if (!rules) {
4098
- return c.json({ error: "Failed to parse rules" }, 500);
4099
- }
4100
- return c.json(rules);
4101
- } catch (error) {
4102
- console.error("Failed to read rules:", error);
4103
- return c.json({ error: "Failed to read rules" }, 500);
4104
- }
4105
- });
4106
- app.put("/api/rules/:id", async (c) => {
4107
- const id = c.req.param("id");
4108
- if (!ALLOWED_RULE_FILES.has(id)) {
4109
- return c.json({ error: "Invalid rule type" }, 400);
4110
- }
4111
- const dir = rulesDir();
4112
- try {
4113
- const filePath = path3.join(dir, `${id}.json`);
4114
- if (!fs4.existsSync(filePath)) {
4115
- return c.json({ error: "Rules not found" }, 404);
4116
- }
4117
- const body = await c.req.json();
4118
- if (!body.items || !Array.isArray(body.items)) {
4119
- return c.json({ error: "Invalid rules format" }, 400);
4120
- }
4121
- fs4.writeFileSync(filePath, JSON.stringify(body, null, 2));
4122
- writeAuditLog({
4123
- entity: "rule",
4124
- action: "update",
4125
- targetId: id,
4126
- detail: { itemCount: body.items.length }
4127
- });
4128
- return c.json(body);
4129
- } catch (error) {
4130
- console.error("Failed to update rules:", error);
4131
- return c.json({ error: "Failed to update rules" }, 500);
4157
+ var decisions_default = decisions;
4158
+
4159
+ // dashboard/server/routes/export.ts
4160
+ import fs8 from "node:fs";
4161
+ import path7 from "node:path";
4162
+ function sessionToMarkdown(session) {
4163
+ const lines = [];
4164
+ lines.push(`# ${session.title || "Untitled Session"}`);
4165
+ lines.push("");
4166
+ lines.push(`**ID:** ${session.id}`);
4167
+ lines.push(`**Created:** ${session.createdAt}`);
4168
+ if (session.sessionType) {
4169
+ lines.push(`**Type:** ${session.sessionType}`);
4132
4170
  }
4133
- });
4134
- app.delete("/api/rules/:id/:ruleId", async (c) => {
4135
- const id = c.req.param("id");
4136
- if (!ALLOWED_RULE_FILES.has(id)) {
4137
- return c.json({ error: "Invalid rule type" }, 400);
4171
+ if (session.context) {
4172
+ const ctx = session.context;
4173
+ if (ctx.branch) lines.push(`**Branch:** ${ctx.branch}`);
4174
+ if (ctx.user) lines.push(`**User:** ${ctx.user}`);
4138
4175
  }
4139
- const ruleId = sanitizeId(c.req.param("ruleId"));
4140
- if (!ruleId) {
4141
- return c.json({ error: "Invalid rule id" }, 400);
4176
+ lines.push("");
4177
+ if (session.goal) {
4178
+ lines.push("## Goal");
4179
+ lines.push("");
4180
+ lines.push(session.goal);
4181
+ lines.push("");
4142
4182
  }
4143
- const filePath = path3.join(rulesDir(), `${id}.json`);
4144
- if (!fs4.existsSync(filePath)) {
4145
- return c.json({ error: "Rules not found" }, 404);
4183
+ const tags = session.tags;
4184
+ if (tags && tags.length > 0) {
4185
+ lines.push("## Tags");
4186
+ lines.push("");
4187
+ lines.push(tags.map((t) => `\`${t}\``).join(", "));
4188
+ lines.push("");
4146
4189
  }
4147
- try {
4148
- const doc = safeParseJsonFile(filePath);
4149
- if (!doc || !Array.isArray(doc.items)) {
4150
- return c.json({ error: "Invalid rules format" }, 500);
4190
+ const interactions = session.interactions;
4191
+ if (interactions && interactions.length > 0) {
4192
+ lines.push("## Interactions");
4193
+ lines.push("");
4194
+ for (const interaction of interactions) {
4195
+ lines.push(`### ${interaction.choice || "Interaction"}`);
4196
+ lines.push("");
4197
+ if (interaction.reasoning) {
4198
+ lines.push(`**Reasoning:** ${interaction.reasoning}`);
4199
+ lines.push("");
4200
+ }
4201
+ if (interaction.timestamp) {
4202
+ lines.push(`*${interaction.timestamp}*`);
4203
+ lines.push("");
4204
+ }
4205
+ lines.push("---");
4206
+ lines.push("");
4151
4207
  }
4152
- const nextItems = doc.items.filter((item) => item.id !== ruleId);
4153
- if (nextItems.length === doc.items.length) {
4154
- return c.json({ error: "Rule not found" }, 404);
4208
+ }
4209
+ if (session.outcome) {
4210
+ lines.push("## Outcome");
4211
+ lines.push("");
4212
+ lines.push(session.outcome);
4213
+ lines.push("");
4214
+ }
4215
+ const relatedSessions = session.relatedSessions;
4216
+ if (relatedSessions && relatedSessions.length > 0) {
4217
+ lines.push("## Related Sessions");
4218
+ lines.push("");
4219
+ for (const relId of relatedSessions) {
4220
+ lines.push(`- ${relId}`);
4155
4221
  }
4156
- const nextDoc = {
4157
- ...doc,
4158
- items: nextItems,
4159
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4160
- };
4161
- fs4.writeFileSync(filePath, JSON.stringify(nextDoc, null, 2));
4162
- writeAuditLog({
4163
- entity: "rule",
4164
- action: "delete",
4165
- targetId: ruleId,
4166
- detail: { ruleType: id }
4167
- });
4168
- return c.json({ deleted: 1, id: ruleId, ruleType: id });
4169
- } catch (error) {
4170
- console.error("Failed to delete rule:", error);
4171
- return c.json({ error: "Failed to delete rule" }, 500);
4222
+ lines.push("");
4172
4223
  }
4173
- });
4174
- app.get("/api/timeline", async (c) => {
4175
- const sessionsDir = path3.join(getMnemeDir(), "sessions");
4176
- try {
4177
- const files = listDatedJsonFiles(sessionsDir);
4178
- if (files.length === 0) {
4179
- return c.json({ timeline: {} });
4224
+ lines.push("---");
4225
+ lines.push("*Exported from mneme*");
4226
+ return lines.join("\n");
4227
+ }
4228
+ function decisionToMarkdown(decision) {
4229
+ const lines = [];
4230
+ lines.push(`# ${decision.title || "Untitled Decision"}`);
4231
+ lines.push("");
4232
+ lines.push(`**ID:** ${decision.id}`);
4233
+ lines.push(`**Status:** ${decision.status || "unknown"}`);
4234
+ lines.push(`**Created:** ${decision.createdAt}`);
4235
+ if (decision.updatedAt) {
4236
+ lines.push(`**Updated:** ${decision.updatedAt}`);
4237
+ }
4238
+ lines.push("");
4239
+ if (decision.decision) {
4240
+ lines.push("## Decision");
4241
+ lines.push("");
4242
+ lines.push(decision.decision);
4243
+ lines.push("");
4244
+ }
4245
+ if (decision.rationale) {
4246
+ lines.push("## Rationale");
4247
+ lines.push("");
4248
+ lines.push(decision.rationale);
4249
+ lines.push("");
4250
+ }
4251
+ const tags = decision.tags;
4252
+ if (tags && tags.length > 0) {
4253
+ lines.push("## Tags");
4254
+ lines.push("");
4255
+ lines.push(tags.map((t) => `\`${t}\``).join(", "));
4256
+ lines.push("");
4257
+ }
4258
+ const alternatives = decision.alternatives;
4259
+ if (alternatives && alternatives.length > 0) {
4260
+ lines.push("## Alternatives Considered");
4261
+ lines.push("");
4262
+ for (const alt of alternatives) {
4263
+ lines.push(`### ${alt.title || "Alternative"}`);
4264
+ if (alt.description) {
4265
+ lines.push("");
4266
+ lines.push(alt.description);
4267
+ }
4268
+ if (alt.pros) {
4269
+ lines.push("");
4270
+ lines.push("**Pros:**");
4271
+ for (const pro of alt.pros) {
4272
+ lines.push(`- ${pro}`);
4273
+ }
4274
+ }
4275
+ if (alt.cons) {
4276
+ lines.push("");
4277
+ lines.push("**Cons:**");
4278
+ for (const con of alt.cons) {
4279
+ lines.push(`- ${con}`);
4280
+ }
4281
+ }
4282
+ lines.push("");
4180
4283
  }
4181
- const sessions = files.map((filePath) => {
4182
- const content = fs4.readFileSync(filePath, "utf-8");
4183
- return JSON.parse(content);
4184
- });
4185
- const grouped = {};
4186
- for (const session of sessions) {
4187
- const date = session.createdAt?.split("T")[0] || "unknown";
4188
- if (!grouped[date]) grouped[date] = [];
4189
- grouped[date].push({
4190
- id: session.id,
4191
- title: session.title || "Untitled",
4192
- sessionType: session.sessionType,
4193
- branch: session.context?.branch,
4194
- tags: session.tags || [],
4195
- createdAt: session.createdAt
4196
- });
4284
+ }
4285
+ const relatedSessions = decision.relatedSessions;
4286
+ if (relatedSessions && relatedSessions.length > 0) {
4287
+ lines.push("## Related Sessions");
4288
+ lines.push("");
4289
+ for (const relId of relatedSessions) {
4290
+ lines.push(`- ${relId}`);
4197
4291
  }
4198
- const sortedTimeline = {};
4199
- for (const date of Object.keys(grouped).sort().reverse()) {
4200
- sortedTimeline[date] = grouped[date].sort(
4201
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
4202
- );
4292
+ lines.push("");
4293
+ }
4294
+ lines.push("---");
4295
+ lines.push("*Exported from mneme*");
4296
+ return lines.join("\n");
4297
+ }
4298
+ var exportRoutes = new Hono2();
4299
+ exportRoutes.get("/sessions/:id/markdown", async (c) => {
4300
+ const id = c.req.param("id");
4301
+ const sessionsDir = path7.join(getMnemeDir(), "sessions");
4302
+ try {
4303
+ const filePath = findJsonFileById(sessionsDir, id);
4304
+ if (!filePath) {
4305
+ return c.json({ error: "Session not found" }, 404);
4203
4306
  }
4204
- return c.json({ timeline: sortedTimeline });
4307
+ const content = fs8.readFileSync(filePath, "utf-8");
4308
+ const session = JSON.parse(content);
4309
+ const markdown = sessionToMarkdown(session);
4310
+ const filename = `session-${id}.md`;
4311
+ c.header("Content-Type", "text/markdown; charset=utf-8");
4312
+ c.header("Content-Disposition", `attachment; filename="${filename}"`);
4313
+ return c.text(markdown);
4205
4314
  } catch (error) {
4206
- console.error("Failed to build timeline:", error);
4207
- return c.json({ error: "Failed to build timeline" }, 500);
4315
+ console.error("Failed to export session:", error);
4316
+ return c.json({ error: "Failed to export session" }, 500);
4208
4317
  }
4209
4318
  });
4210
- app.get("/api/tag-network", async (c) => {
4211
- const sessionsDir = path3.join(getMnemeDir(), "sessions");
4319
+ exportRoutes.get("/decisions/:id/markdown", async (c) => {
4320
+ const id = sanitizeId(c.req.param("id"));
4321
+ const decisionsDir = path7.join(getMnemeDir(), "decisions");
4212
4322
  try {
4213
- const files = listDatedJsonFiles(sessionsDir);
4214
- const tagCounts = /* @__PURE__ */ new Map();
4215
- const coOccurrences = /* @__PURE__ */ new Map();
4216
- for (const filePath of files) {
4217
- const content = fs4.readFileSync(filePath, "utf-8");
4218
- const session = JSON.parse(content);
4219
- const tags = session.tags || [];
4220
- for (const tag of tags) {
4221
- tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
4222
- }
4223
- for (let i = 0; i < tags.length; i++) {
4224
- for (let j = i + 1; j < tags.length; j++) {
4225
- const key = [tags[i], tags[j]].sort().join("|");
4226
- coOccurrences.set(key, (coOccurrences.get(key) || 0) + 1);
4227
- }
4228
- }
4323
+ const filePath = findJsonFileById(decisionsDir, id);
4324
+ if (!filePath) {
4325
+ return c.json({ error: "Decision not found" }, 404);
4229
4326
  }
4230
- const nodes = Array.from(tagCounts.entries()).map(([id, count]) => ({
4231
- id,
4232
- count
4233
- }));
4234
- const edges = Array.from(coOccurrences.entries()).map(([key, weight]) => {
4235
- const [source, target] = key.split("|");
4236
- return { source, target, weight };
4237
- });
4238
- return c.json({ nodes, edges });
4327
+ const content = fs8.readFileSync(filePath, "utf-8");
4328
+ const decision = JSON.parse(content);
4329
+ const markdown = decisionToMarkdown(decision);
4330
+ const filename = `decision-${id}.md`;
4331
+ c.header("Content-Type", "text/markdown; charset=utf-8");
4332
+ c.header("Content-Disposition", `attachment; filename="${filename}"`);
4333
+ return c.text(markdown);
4239
4334
  } catch (error) {
4240
- console.error("Failed to build tag network:", error);
4241
- return c.json({ error: "Failed to build tag network" }, 500);
4335
+ console.error("Failed to export decision:", error);
4336
+ return c.json({ error: "Failed to export decision" }, 500);
4242
4337
  }
4243
4338
  });
4244
- app.get("/api/decisions/:id/impact", async (c) => {
4245
- const decisionId = sanitizeId(c.req.param("id"));
4246
- const sessionsDir = path3.join(getMnemeDir(), "sessions");
4247
- const patternsDir2 = path3.join(getMnemeDir(), "patterns");
4339
+ exportRoutes.post("/sessions/bulk", async (c) => {
4340
+ const body = await c.req.json();
4341
+ const { ids } = body;
4342
+ if (!ids || ids.length === 0) {
4343
+ return c.json({ error: "No session IDs provided" }, 400);
4344
+ }
4345
+ const sessionsDir = path7.join(getMnemeDir(), "sessions");
4346
+ const markdowns = [];
4248
4347
  try {
4249
- const impactedSessions = [];
4250
- const impactedPatterns = [];
4251
- const sessionFiles = listDatedJsonFiles(sessionsDir);
4252
- for (const filePath of sessionFiles) {
4253
- const content = fs4.readFileSync(filePath, "utf-8");
4254
- const session = JSON.parse(content);
4255
- const hasReference = session.relatedSessions?.includes(decisionId) || session.interactions?.some(
4256
- (i) => i.reasoning?.includes(decisionId) || i.choice?.includes(decisionId)
4257
- );
4258
- if (hasReference) {
4259
- impactedSessions.push({
4260
- id: session.id,
4261
- title: session.title || "Untitled"
4262
- });
4348
+ for (const id of ids) {
4349
+ const filePath = findJsonFileById(sessionsDir, id);
4350
+ if (filePath) {
4351
+ const content = fs8.readFileSync(filePath, "utf-8");
4352
+ const session = JSON.parse(content);
4353
+ markdowns.push(sessionToMarkdown(session));
4263
4354
  }
4264
4355
  }
4265
- const patternFiles = listJsonFiles(patternsDir2);
4266
- for (const filePath of patternFiles) {
4267
- const content = fs4.readFileSync(filePath, "utf-8");
4268
- const data = JSON.parse(content);
4269
- const patterns = data.patterns || [];
4270
- for (const pattern of patterns) {
4271
- if (pattern.sourceId?.includes(decisionId) || pattern.description?.includes(decisionId)) {
4272
- impactedPatterns.push({
4273
- id: `${path3.basename(filePath, ".json")}-${pattern.type}`,
4274
- description: pattern.description || "No description"
4275
- });
4276
- }
4277
- }
4356
+ if (markdowns.length === 0) {
4357
+ return c.json({ error: "No sessions found" }, 404);
4278
4358
  }
4279
- return c.json({
4280
- decisionId,
4281
- impactedSessions,
4282
- impactedPatterns
4283
- });
4359
+ const combined = markdowns.join("\n\n---\n\n");
4360
+ const filename = `sessions-export-${Date.now()}.md`;
4361
+ c.header("Content-Type", "text/markdown; charset=utf-8");
4362
+ c.header("Content-Disposition", `attachment; filename="${filename}"`);
4363
+ return c.text(combined);
4284
4364
  } catch (error) {
4285
- console.error("Failed to analyze decision impact:", error);
4286
- return c.json({ error: "Failed to analyze decision impact" }, 500);
4365
+ console.error("Failed to export sessions:", error);
4366
+ return c.json({ error: "Failed to export sessions" }, 500);
4287
4367
  }
4288
4368
  });
4369
+ var export_default = exportRoutes;
4370
+
4371
+ // dashboard/server/routes/misc.ts
4372
+ import fs9 from "node:fs";
4373
+ import path8 from "node:path";
4374
+
4375
+ // dashboard/server/lib/ai-summary.ts
4289
4376
  var getOpenAIKey = () => {
4290
4377
  return process.env.OPENAI_API_KEY || null;
4291
4378
  };
4292
- app.get("/api/summary/weekly", async (c) => {
4293
- const mnemeDir2 = getMnemeDir();
4294
- const apiKey = getOpenAIKey();
4295
- try {
4296
- const sessionsIndex = readRecentSessionIndexes(mnemeDir2);
4379
+ function getTopTags(sessions2, limit) {
4380
+ const tagCount = {};
4381
+ for (const session of sessions2) {
4382
+ for (const tag of session.tags || []) {
4383
+ tagCount[tag] = (tagCount[tag] || 0) + 1;
4384
+ }
4385
+ }
4386
+ return Object.entries(tagCount).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count).slice(0, limit);
4387
+ }
4388
+ function getSessionTypeBreakdown(sessions2) {
4389
+ const breakdown = {};
4390
+ for (const session of sessions2) {
4391
+ const type = session.sessionType || "unknown";
4392
+ breakdown[type] = (breakdown[type] || 0) + 1;
4393
+ }
4394
+ return breakdown;
4395
+ }
4396
+ function buildSummaryPrompt(sessions2, decisions2) {
4397
+ const sessionList = sessions2.map((s) => `- ${s.title} (${s.sessionType || "unknown"})`).join("\n");
4398
+ const decisionList = decisions2.map((d) => `- ${d.title} (${d.status})`).join("\n");
4399
+ return `Provide a brief weekly development summary (2-3 sentences) based on this activity:
4400
+
4401
+ Sessions (${sessions2.length}):
4402
+ ${sessionList || "None"}
4403
+
4404
+ Decisions (${decisions2.length}):
4405
+ ${decisionList || "None"}
4406
+
4407
+ Focus on key accomplishments and patterns.`;
4408
+ }
4409
+ async function generateAISummary(apiKey, prompt) {
4410
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
4411
+ method: "POST",
4412
+ headers: {
4413
+ "Content-Type": "application/json",
4414
+ Authorization: `Bearer ${apiKey}`
4415
+ },
4416
+ body: JSON.stringify({
4417
+ model: "gpt-4o-mini",
4418
+ messages: [{ role: "user", content: prompt }],
4419
+ max_tokens: 200,
4420
+ temperature: 0.7
4421
+ })
4422
+ });
4423
+ if (!response.ok) {
4424
+ throw new Error("OpenAI API request failed");
4425
+ }
4426
+ const data = await response.json();
4427
+ return data.choices?.[0]?.message?.content || "Unable to generate summary.";
4428
+ }
4429
+
4430
+ // dashboard/server/routes/misc.ts
4431
+ var misc = new Hono2();
4432
+ misc.get("/project", (c) => {
4433
+ const projectRoot = getProjectRoot();
4434
+ const projectName = path8.basename(projectRoot);
4435
+ let repository = null;
4436
+ try {
4437
+ const gitConfigPath = path8.join(projectRoot, ".git", "config");
4438
+ if (fs9.existsSync(gitConfigPath)) {
4439
+ const gitConfig = fs9.readFileSync(gitConfigPath, "utf-8");
4440
+ const match2 = gitConfig.match(
4441
+ /url\s*=\s*.*[:/]([^/]+\/[^/]+?)(?:\.git)?$/m
4442
+ );
4443
+ if (match2) {
4444
+ repository = match2[1];
4445
+ }
4446
+ }
4447
+ } catch {
4448
+ }
4449
+ return c.json({
4450
+ name: projectName,
4451
+ path: projectRoot,
4452
+ repository
4453
+ });
4454
+ });
4455
+ misc.get("/info", async (c) => {
4456
+ const projectRoot = getProjectRoot();
4457
+ const mnemeDir2 = getMnemeDir();
4458
+ return c.json({
4459
+ projectRoot,
4460
+ mnemeDir: mnemeDir2,
4461
+ exists: fs9.existsSync(mnemeDir2)
4462
+ });
4463
+ });
4464
+ misc.get("/current-user", async (c) => {
4465
+ try {
4466
+ const user = getCurrentUser();
4467
+ return c.json({ user });
4468
+ } catch (error) {
4469
+ console.error("Failed to get current user:", error);
4470
+ return c.json({ error: "Failed to get current user" }, 500);
4471
+ }
4472
+ });
4473
+ misc.get("/tags", async (c) => {
4474
+ const tagsPath = path8.join(getMnemeDir(), "tags.json");
4475
+ try {
4476
+ if (!fs9.existsSync(tagsPath)) {
4477
+ return c.json({ version: 1, tags: [] });
4478
+ }
4479
+ const tags = safeParseJsonFile(tagsPath);
4480
+ if (!tags) {
4481
+ return c.json({ error: "Failed to parse tags" }, 500);
4482
+ }
4483
+ return c.json(tags);
4484
+ } catch (error) {
4485
+ console.error("Failed to read tags:", error);
4486
+ return c.json({ error: "Failed to read tags" }, 500);
4487
+ }
4488
+ });
4489
+ misc.get("/indexes/status", async (c) => {
4490
+ const mnemeDir2 = getMnemeDir();
4491
+ try {
4492
+ const sessionsIndex = readAllSessionIndexes(mnemeDir2);
4493
+ const decisionsIndex = readAllDecisionIndexes(mnemeDir2);
4494
+ return c.json({
4495
+ sessions: {
4496
+ exists: sessionsIndex.items.length > 0,
4497
+ itemCount: sessionsIndex.items.length,
4498
+ updatedAt: sessionsIndex.updatedAt,
4499
+ isStale: isIndexStale(sessionsIndex)
4500
+ },
4501
+ decisions: {
4502
+ exists: decisionsIndex.items.length > 0,
4503
+ itemCount: decisionsIndex.items.length,
4504
+ updatedAt: decisionsIndex.updatedAt,
4505
+ isStale: isIndexStale(decisionsIndex)
4506
+ }
4507
+ });
4508
+ } catch (error) {
4509
+ console.error("Failed to get index status:", error);
4510
+ return c.json({ error: "Failed to get index status" }, 500);
4511
+ }
4512
+ });
4513
+ misc.post("/indexes/rebuild", async (c) => {
4514
+ const mnemeDir2 = getMnemeDir();
4515
+ try {
4516
+ const sessionIndexes = rebuildAllSessionIndexes(mnemeDir2);
4517
+ const decisionIndexes = rebuildAllDecisionIndexes(mnemeDir2);
4518
+ let sessionCount = 0;
4519
+ let sessionUpdatedAt = "";
4520
+ for (const index of sessionIndexes.values()) {
4521
+ sessionCount += index.items.length;
4522
+ if (index.updatedAt > sessionUpdatedAt) {
4523
+ sessionUpdatedAt = index.updatedAt;
4524
+ }
4525
+ }
4526
+ let decisionCount = 0;
4527
+ let decisionUpdatedAt = "";
4528
+ for (const index of decisionIndexes.values()) {
4529
+ decisionCount += index.items.length;
4530
+ if (index.updatedAt > decisionUpdatedAt) {
4531
+ decisionUpdatedAt = index.updatedAt;
4532
+ }
4533
+ }
4534
+ return c.json({
4535
+ success: true,
4536
+ sessions: {
4537
+ itemCount: sessionCount,
4538
+ monthCount: sessionIndexes.size,
4539
+ updatedAt: sessionUpdatedAt || (/* @__PURE__ */ new Date()).toISOString()
4540
+ },
4541
+ decisions: {
4542
+ itemCount: decisionCount,
4543
+ monthCount: decisionIndexes.size,
4544
+ updatedAt: decisionUpdatedAt || (/* @__PURE__ */ new Date()).toISOString()
4545
+ }
4546
+ });
4547
+ } catch (error) {
4548
+ return c.json(
4549
+ { error: "Failed to rebuild indexes", details: String(error) },
4550
+ 500
4551
+ );
4552
+ }
4553
+ });
4554
+ misc.get("/summary/weekly", async (c) => {
4555
+ const mnemeDir2 = getMnemeDir();
4556
+ const apiKey = getOpenAIKey();
4557
+ try {
4558
+ const sessionsIndex = readRecentSessionIndexes(mnemeDir2);
4297
4559
  const decisionsIndex = readRecentDecisionIndexes(mnemeDir2);
4298
4560
  const now = /* @__PURE__ */ new Date();
4299
4561
  const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
@@ -4331,7 +4593,7 @@ app.get("/api/summary/weekly", async (c) => {
4331
4593
  return c.json({ error: "Failed to generate weekly summary" }, 500);
4332
4594
  }
4333
4595
  });
4334
- app.post("/api/summary/generate", async (c) => {
4596
+ misc.post("/summary/generate", async (c) => {
4335
4597
  const apiKey = getOpenAIKey();
4336
4598
  if (!apiKey) {
4337
4599
  return c.json(
@@ -4344,22 +4606,22 @@ app.post("/api/summary/generate", async (c) => {
4344
4606
  const body = await c.req.json();
4345
4607
  const { sessionIds, prompt: customPrompt } = body;
4346
4608
  const mnemeDir2 = getMnemeDir();
4347
- const sessionsDir = path3.join(mnemeDir2, "sessions");
4609
+ const sessionsDir = path8.join(mnemeDir2, "sessions");
4348
4610
  try {
4349
- const sessions = [];
4611
+ const sessions2 = [];
4350
4612
  for (const id of sessionIds || []) {
4351
4613
  const filePath = findJsonFileById(sessionsDir, id);
4352
4614
  if (filePath) {
4353
- const content = fs4.readFileSync(filePath, "utf-8");
4354
- sessions.push(JSON.parse(content));
4615
+ const content = fs9.readFileSync(filePath, "utf-8");
4616
+ sessions2.push(JSON.parse(content));
4355
4617
  }
4356
4618
  }
4357
- if (sessions.length === 0) {
4619
+ if (sessions2.length === 0) {
4358
4620
  return c.json({ error: "No sessions found" }, 404);
4359
4621
  }
4360
4622
  const prompt = customPrompt || `Summarize the following development sessions concisely:
4361
4623
 
4362
- ${sessions.map((s) => `- ${s.title}: ${s.goal || "No goal specified"}`).join("\n")}`;
4624
+ ${sessions2.map((s) => `- ${s.title}: ${s.goal || "No goal specified"}`).join("\n")}`;
4363
4625
  const summary = await generateAISummary(apiKey, prompt);
4364
4626
  return c.json({ summary });
4365
4627
  } catch (error) {
@@ -4367,222 +4629,48 @@ ${sessions.map((s) => `- ${s.title}: ${s.goal || "No goal specified"}`).join("\n
4367
4629
  return c.json({ error: "Failed to generate summary" }, 500);
4368
4630
  }
4369
4631
  });
4370
- function getTopTags(sessions, limit) {
4371
- const tagCount = {};
4372
- for (const session of sessions) {
4373
- for (const tag of session.tags || []) {
4374
- tagCount[tag] = (tagCount[tag] || 0) + 1;
4375
- }
4376
- }
4377
- return Object.entries(tagCount).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count).slice(0, limit);
4378
- }
4379
- function getSessionTypeBreakdown(sessions) {
4380
- const breakdown = {};
4381
- for (const session of sessions) {
4382
- const type = session.sessionType || "unknown";
4383
- breakdown[type] = (breakdown[type] || 0) + 1;
4384
- }
4385
- return breakdown;
4386
- }
4387
- function buildSummaryPrompt(sessions, decisions) {
4388
- const sessionList = sessions.map((s) => `- ${s.title} (${s.sessionType || "unknown"})`).join("\n");
4389
- const decisionList = decisions.map((d) => `- ${d.title} (${d.status})`).join("\n");
4390
- return `Provide a brief weekly development summary (2-3 sentences) based on this activity:
4391
-
4392
- Sessions (${sessions.length}):
4393
- ${sessionList || "None"}
4394
-
4395
- Decisions (${decisions.length}):
4396
- ${decisionList || "None"}
4632
+ var misc_default = misc;
4397
4633
 
4398
- Focus on key accomplishments and patterns.`;
4399
- }
4400
- async function generateAISummary(apiKey, prompt) {
4401
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
4402
- method: "POST",
4403
- headers: {
4404
- "Content-Type": "application/json",
4405
- Authorization: `Bearer ${apiKey}`
4406
- },
4407
- body: JSON.stringify({
4408
- model: "gpt-4o-mini",
4409
- messages: [{ role: "user", content: prompt }],
4410
- max_tokens: 200,
4411
- temperature: 0.7
4412
- })
4413
- });
4414
- if (!response.ok) {
4415
- throw new Error("OpenAI API request failed");
4416
- }
4417
- const data = await response.json();
4418
- return data.choices?.[0]?.message?.content || "Unable to generate summary.";
4419
- }
4420
- app.get("/api/stats/overview", async (c) => {
4421
- const mnemeDir2 = getMnemeDir();
4634
+ // dashboard/server/routes/patterns.ts
4635
+ import fs10 from "node:fs";
4636
+ import path9 from "node:path";
4637
+ var patterns = new Hono2();
4638
+ patterns.get("/", async (c) => {
4639
+ const dir = patternsDir();
4422
4640
  try {
4423
- const sessionsIndex = readAllSessionIndexes(mnemeDir2);
4424
- const decisionsIndex = readAllDecisionIndexes(mnemeDir2);
4425
- const validSessions = sessionsIndex.items.filter(
4426
- (session) => session.interactionCount > 0 || session.hasSummary === true
4427
- );
4428
- const sessionTypeCount = {};
4429
- for (const session of validSessions) {
4430
- const type = session.sessionType || "unknown";
4431
- sessionTypeCount[type] = (sessionTypeCount[type] || 0) + 1;
4641
+ if (!fs10.existsSync(dir)) {
4642
+ return c.json({ patterns: [] });
4432
4643
  }
4433
- let totalPatterns = 0;
4434
- const patternsByType = {};
4435
- const patternsPath = path3.join(mnemeDir2, "patterns");
4436
- if (fs4.existsSync(patternsPath)) {
4437
- const patternFiles = listJsonFiles(patternsPath);
4438
- for (const filePath of patternFiles) {
4439
- try {
4440
- const content = fs4.readFileSync(filePath, "utf-8");
4441
- const data = JSON.parse(content);
4442
- const patterns = data.patterns || [];
4443
- for (const pattern of patterns) {
4444
- totalPatterns++;
4445
- const type = pattern.type || "unknown";
4446
- patternsByType[type] = (patternsByType[type] || 0) + 1;
4447
- }
4448
- } catch {
4644
+ const files = listJsonFiles(dir);
4645
+ const allPatterns = [];
4646
+ for (const filePath of files) {
4647
+ try {
4648
+ const content = fs10.readFileSync(filePath, "utf-8");
4649
+ const data = JSON.parse(content);
4650
+ const items = data.items || data.patterns || [];
4651
+ for (const pattern of items) {
4652
+ allPatterns.push({
4653
+ ...pattern,
4654
+ sourceFile: path9.basename(filePath, ".json")
4655
+ });
4449
4656
  }
4657
+ } catch {
4450
4658
  }
4451
4659
  }
4452
- let totalRules = 0;
4453
- const rulesByType = {};
4454
- const rulesPath = path3.join(mnemeDir2, "rules");
4455
- if (fs4.existsSync(rulesPath)) {
4456
- for (const ruleType of ["dev-rules", "review-guidelines"]) {
4457
- const rulePath = path3.join(rulesPath, `${ruleType}.json`);
4458
- if (fs4.existsSync(rulePath)) {
4459
- try {
4460
- const content = fs4.readFileSync(rulePath, "utf-8");
4461
- const data = JSON.parse(content);
4462
- const items = data.items || [];
4463
- const activeItems = items.filter(
4464
- (item) => item.status === "active"
4465
- );
4466
- rulesByType[ruleType] = activeItems.length;
4467
- totalRules += activeItems.length;
4468
- } catch {
4469
- }
4470
- }
4471
- }
4472
- }
4473
- return c.json({
4474
- sessions: {
4475
- total: validSessions.length,
4476
- byType: sessionTypeCount
4477
- },
4478
- decisions: {
4479
- total: decisionsIndex.items.length
4480
- },
4481
- patterns: {
4482
- total: totalPatterns,
4483
- byType: patternsByType
4484
- },
4485
- rules: {
4486
- total: totalRules,
4487
- byType: rulesByType
4488
- }
4489
- });
4490
- } catch (error) {
4491
- console.error("Failed to get stats overview:", error);
4492
- return c.json({ error: "Failed to get stats overview" }, 500);
4493
- }
4494
- });
4495
- app.get("/api/stats/activity", async (c) => {
4496
- const mnemeDir2 = getMnemeDir();
4497
- const daysParam = Number.parseInt(c.req.query("days") || "30", 10);
4498
- const MAX_DAYS = 365;
4499
- const safeDays = Math.min(Math.max(1, daysParam), MAX_DAYS);
4500
- try {
4501
- const sessionsIndex = readAllSessionIndexes(mnemeDir2);
4502
- const decisionsIndex = readAllDecisionIndexes(mnemeDir2);
4503
- const now = /* @__PURE__ */ new Date();
4504
- const startDate = new Date(
4505
- now.getTime() - (safeDays - 1) * 24 * 60 * 60 * 1e3
4506
- );
4507
- const activityByDate = {};
4508
- for (let i = 0; i < safeDays; i++) {
4509
- const d = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1e3);
4510
- const dateKey = d.toISOString().split("T")[0];
4511
- activityByDate[dateKey] = { sessions: 0, decisions: 0 };
4512
- }
4513
- for (const session of sessionsIndex.items) {
4514
- const dateKey = session.createdAt.split("T")[0];
4515
- if (activityByDate[dateKey]) {
4516
- activityByDate[dateKey].sessions += 1;
4517
- }
4518
- }
4519
- for (const decision of decisionsIndex.items) {
4520
- const dateKey = decision.createdAt.split("T")[0];
4521
- if (activityByDate[dateKey]) {
4522
- activityByDate[dateKey].decisions += 1;
4523
- }
4524
- }
4525
- const activity = Object.entries(activityByDate).map(([date, counts]) => ({ date, ...counts })).sort((a, b) => a.date.localeCompare(b.date));
4526
- return c.json({ activity, days: safeDays });
4527
- } catch (error) {
4528
- console.error("Failed to get activity stats:", error);
4529
- return c.json({ error: "Failed to get activity stats" }, 500);
4530
- }
4531
- });
4532
- app.get("/api/stats/tags", async (c) => {
4533
- const mnemeDir2 = getMnemeDir();
4534
- try {
4535
- const sessionsIndex = readAllSessionIndexes(mnemeDir2);
4536
- const tagCount = {};
4537
- for (const session of sessionsIndex.items) {
4538
- for (const tag of session.tags || []) {
4539
- tagCount[tag] = (tagCount[tag] || 0) + 1;
4540
- }
4541
- }
4542
- const tags = Object.entries(tagCount).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count).slice(0, 20);
4543
- return c.json({ tags });
4544
- } catch (error) {
4545
- console.error("Failed to get tag stats:", error);
4546
- return c.json({ error: "Failed to get tag stats" }, 500);
4547
- }
4548
- });
4549
- var patternsDir = () => path3.join(getMnemeDir(), "patterns");
4550
- app.get("/api/patterns", async (c) => {
4551
- const dir = patternsDir();
4552
- try {
4553
- if (!fs4.existsSync(dir)) {
4554
- return c.json({ patterns: [] });
4555
- }
4556
- const files = listJsonFiles(dir);
4557
- const allPatterns = [];
4558
- for (const filePath of files) {
4559
- try {
4560
- const content = fs4.readFileSync(filePath, "utf-8");
4561
- const data = JSON.parse(content);
4562
- const patterns = data.items || data.patterns || [];
4563
- for (const pattern of patterns) {
4564
- allPatterns.push({
4565
- ...pattern,
4566
- sourceFile: path3.basename(filePath, ".json")
4567
- });
4568
- }
4569
- } catch {
4570
- }
4571
- }
4572
- allPatterns.sort(
4573
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
4574
- );
4575
- return c.json({ patterns: allPatterns });
4576
- } catch (error) {
4577
- console.error("Failed to read patterns:", error);
4578
- return c.json({ error: "Failed to read patterns" }, 500);
4579
- }
4580
- });
4581
- app.get("/api/patterns/stats", async (c) => {
4582
- const dir = patternsDir();
4583
- try {
4584
- if (!fs4.existsSync(dir)) {
4585
- return c.json({ total: 0, byType: {}, bySource: {} });
4660
+ allPatterns.sort(
4661
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
4662
+ );
4663
+ return c.json({ patterns: allPatterns });
4664
+ } catch (error) {
4665
+ console.error("Failed to read patterns:", error);
4666
+ return c.json({ error: "Failed to read patterns" }, 500);
4667
+ }
4668
+ });
4669
+ patterns.get("/stats", async (c) => {
4670
+ const dir = patternsDir();
4671
+ try {
4672
+ if (!fs10.existsSync(dir)) {
4673
+ return c.json({ total: 0, byType: {}, bySource: {} });
4586
4674
  }
4587
4675
  const files = listJsonFiles(dir);
4588
4676
  let total = 0;
@@ -4590,11 +4678,11 @@ app.get("/api/patterns/stats", async (c) => {
4590
4678
  const bySource = {};
4591
4679
  for (const filePath of files) {
4592
4680
  try {
4593
- const content = fs4.readFileSync(filePath, "utf-8");
4681
+ const content = fs10.readFileSync(filePath, "utf-8");
4594
4682
  const data = JSON.parse(content);
4595
- const patterns = data.items || data.patterns || [];
4596
- const sourceName = path3.basename(filePath, ".json");
4597
- for (const pattern of patterns) {
4683
+ const items = data.items || data.patterns || [];
4684
+ const sourceName = path9.basename(filePath, ".json");
4685
+ for (const pattern of items) {
4598
4686
  total++;
4599
4687
  const type = pattern.type || "unknown";
4600
4688
  byType[type] = (byType[type] || 0) + 1;
@@ -4609,7 +4697,7 @@ app.get("/api/patterns/stats", async (c) => {
4609
4697
  return c.json({ error: "Failed to get pattern stats" }, 500);
4610
4698
  }
4611
4699
  });
4612
- app.delete("/api/patterns/:id", async (c) => {
4700
+ patterns.delete("/:id", async (c) => {
4613
4701
  const id = sanitizeId(c.req.param("id"));
4614
4702
  const sourceFile = c.req.query("source");
4615
4703
  if (!id) {
@@ -4619,12 +4707,12 @@ app.delete("/api/patterns/:id", async (c) => {
4619
4707
  return c.json({ error: "Missing source file" }, 400);
4620
4708
  }
4621
4709
  const safeSource = sourceFile.replace(/[^a-zA-Z0-9_-]/g, "");
4622
- const filePath = path3.join(patternsDir(), `${safeSource}.json`);
4623
- if (!fs4.existsSync(filePath)) {
4710
+ const filePath = path9.join(patternsDir(), `${safeSource}.json`);
4711
+ if (!fs10.existsSync(filePath)) {
4624
4712
  return c.json({ error: "Pattern source file not found" }, 404);
4625
4713
  }
4626
4714
  try {
4627
- const content = fs4.readFileSync(filePath, "utf-8");
4715
+ const content = fs10.readFileSync(filePath, "utf-8");
4628
4716
  const data = JSON.parse(content);
4629
4717
  let deleted = 0;
4630
4718
  if (Array.isArray(data.items)) {
@@ -4641,7 +4729,7 @@ app.delete("/api/patterns/:id", async (c) => {
4641
4729
  if (deleted === 0) {
4642
4730
  return c.json({ error: "Pattern not found" }, 404);
4643
4731
  }
4644
- fs4.writeFileSync(filePath, JSON.stringify(data, null, 2));
4732
+ fs10.writeFileSync(filePath, JSON.stringify(data, null, 2));
4645
4733
  writeAuditLog({
4646
4734
  entity: "pattern",
4647
4735
  action: "delete",
@@ -4654,631 +4742,593 @@ app.delete("/api/patterns/:id", async (c) => {
4654
4742
  return c.json({ error: "Failed to delete pattern" }, 500);
4655
4743
  }
4656
4744
  });
4657
- app.get("/api/units", async (c) => {
4658
- const status = c.req.query("status");
4659
- const doc = readUnits();
4660
- const items = status && ["pending", "approved", "rejected"].includes(status) ? doc.items.filter((item) => item.status === status) : doc.items;
4661
- return c.json({
4662
- ...doc,
4663
- items: items.sort(
4664
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
4665
- )
4666
- });
4667
- });
4668
- app.get("/api/units/:id", async (c) => {
4669
- const id = sanitizeId(c.req.param("id"));
4670
- if (!id) {
4671
- return c.json({ error: "Invalid unit id" }, 400);
4745
+ var patterns_default = patterns;
4746
+
4747
+ // dashboard/server/routes/rules.ts
4748
+ import fs11 from "node:fs";
4749
+ import path10 from "node:path";
4750
+ var rules = new Hono2();
4751
+ rules.get("/:id", async (c) => {
4752
+ const id = c.req.param("id");
4753
+ if (!ALLOWED_RULE_FILES.has(id)) {
4754
+ return c.json({ error: "Invalid rule type" }, 400);
4672
4755
  }
4673
- const doc = readUnits();
4674
- const item = doc.items.find((unit) => unit.id === id);
4675
- if (!item) {
4676
- return c.json({ error: "Unit not found" }, 404);
4756
+ const dir = rulesDir();
4757
+ try {
4758
+ const filePath = path10.join(dir, `${id}.json`);
4759
+ if (!fs11.existsSync(filePath)) {
4760
+ return c.json({ error: "Rules not found" }, 404);
4761
+ }
4762
+ const rulesData = safeParseJsonFile(filePath);
4763
+ if (!rulesData) {
4764
+ return c.json({ error: "Failed to parse rules" }, 500);
4765
+ }
4766
+ return c.json(rulesData);
4767
+ } catch (error) {
4768
+ console.error("Failed to read rules:", error);
4769
+ return c.json({ error: "Failed to read rules" }, 500);
4677
4770
  }
4678
- return c.json(item);
4679
4771
  });
4680
- app.get("/api/approval-queue", async (c) => {
4681
- const doc = readUnits();
4682
- const pending = doc.items.filter((item) => item.status === "pending");
4683
- return c.json({
4684
- pending,
4685
- totalPending: pending.length,
4686
- byType: pending.reduce(
4687
- (acc, item) => {
4688
- acc[item.type] = (acc[item.type] || 0) + 1;
4689
- return acc;
4690
- },
4691
- {}
4692
- )
4693
- });
4694
- });
4695
- app.post("/api/units/generate", async (c) => {
4696
- const now = (/* @__PURE__ */ new Date()).toISOString();
4697
- const existing = readUnits();
4698
- const bySourceKey = new Map(
4699
- existing.items.map((item) => [`${item.sourceType}:${item.sourceId}`, item])
4700
- );
4701
- const generated = [];
4772
+ rules.put("/:id", async (c) => {
4773
+ const id = c.req.param("id");
4774
+ if (!ALLOWED_RULE_FILES.has(id)) {
4775
+ return c.json({ error: "Invalid rule type" }, 400);
4776
+ }
4777
+ const dir = rulesDir();
4702
4778
  try {
4703
- const decisionFiles = listDatedJsonFiles(
4704
- path3.join(getMnemeDir(), "decisions")
4705
- );
4706
- for (const filePath of decisionFiles) {
4707
- const decision = safeParseJsonFile(filePath);
4708
- if (!decision) continue;
4709
- const sourceId = String(decision.id || "");
4710
- if (!sourceId) continue;
4711
- const sourceType = "decision";
4712
- const key = `${sourceType}:${sourceId}`;
4713
- const previous = bySourceKey.get(key);
4714
- generated.push({
4715
- id: previous?.id || makeUnitId(sourceType, sourceId),
4716
- type: "decision",
4717
- kind: "policy",
4718
- title: String(decision.title || sourceId),
4719
- summary: String(
4720
- decision.decision || decision.reasoning || decision.title || ""
4721
- ),
4722
- tags: Array.isArray(decision.tags) ? decision.tags.map((tag) => String(tag)) : [],
4723
- sourceId,
4724
- sourceType,
4725
- sourceRefs: [{ type: sourceType, id: sourceId }],
4726
- status: previous?.status || "pending",
4727
- createdAt: previous?.createdAt || now,
4728
- updatedAt: now,
4729
- reviewedAt: previous?.reviewedAt,
4730
- reviewedBy: previous?.reviewedBy
4731
- });
4732
- }
4733
- const ruleFiles = ["dev-rules", "review-guidelines"];
4734
- for (const ruleFile of ruleFiles) {
4735
- const filePath = path3.join(rulesDir(), `${ruleFile}.json`);
4736
- const doc = safeParseJsonFile(
4737
- filePath
4738
- );
4739
- if (!doc || !Array.isArray(doc.items)) continue;
4740
- for (const rule of doc.items) {
4741
- const ruleId = String(rule.id || "");
4742
- if (!ruleId) continue;
4743
- const sourceType = "rule";
4744
- const sourceId = `${ruleFile}:${ruleId}`;
4745
- const key = `${sourceType}:${sourceId}`;
4746
- const previous = bySourceKey.get(key);
4747
- const title = String(rule.text || rule.title || rule.rule || ruleId) || ruleId;
4748
- const summary = String(rule.rationale || rule.description || "") || title;
4749
- generated.push({
4750
- id: previous?.id || makeUnitId(sourceType, sourceId),
4751
- type: "rule",
4752
- kind: "policy",
4753
- title,
4754
- summary,
4755
- tags: Array.isArray(rule.tags) ? rule.tags.map((tag) => String(tag)) : [ruleFile],
4756
- sourceId,
4757
- sourceType,
4758
- sourceRefs: [{ type: sourceType, id: sourceId }],
4759
- status: previous?.status || "pending",
4760
- createdAt: previous?.createdAt || now,
4761
- updatedAt: now,
4762
- reviewedAt: previous?.reviewedAt,
4763
- reviewedBy: previous?.reviewedBy
4764
- });
4765
- }
4779
+ const filePath = path10.join(dir, `${id}.json`);
4780
+ if (!fs11.existsSync(filePath)) {
4781
+ return c.json({ error: "Rules not found" }, 404);
4766
4782
  }
4767
- const patternFiles = listJsonFiles(patternsDir());
4768
- for (const patternFile of patternFiles) {
4769
- const sourceName = path3.basename(patternFile, ".json");
4770
- const doc = safeParseJsonFile(patternFile);
4771
- const items = doc?.items || doc?.patterns || [];
4772
- for (const pattern of items) {
4773
- const patternId = String(pattern.id || "");
4774
- if (!patternId) continue;
4775
- const sourceType = "pattern";
4776
- const sourceId = `${sourceName}:${patternId}`;
4777
- const key = `${sourceType}:${sourceId}`;
4778
- const previous = bySourceKey.get(key);
4779
- const title = String(
4780
- pattern.title || pattern.errorPattern || pattern.description || patternId
4781
- );
4782
- const summary = String(
4783
- pattern.solution || pattern.description || pattern.errorPattern || ""
4784
- );
4785
- generated.push({
4786
- id: previous?.id || makeUnitId(sourceType, sourceId),
4787
- type: "pattern",
4788
- kind: getPatternKind(String(pattern.type || "")),
4789
- title,
4790
- summary,
4791
- tags: Array.isArray(pattern.tags) ? pattern.tags.map((tag) => String(tag)) : [sourceName],
4792
- sourceId,
4793
- sourceType,
4794
- sourceRefs: [{ type: sourceType, id: sourceId }],
4795
- status: previous?.status || "pending",
4796
- createdAt: previous?.createdAt || now,
4797
- updatedAt: now,
4798
- reviewedAt: previous?.reviewedAt,
4799
- reviewedBy: previous?.reviewedBy
4800
- });
4801
- }
4783
+ const body = await c.req.json();
4784
+ if (!body.items || !Array.isArray(body.items)) {
4785
+ return c.json({ error: "Invalid rules format" }, 400);
4802
4786
  }
4803
- const next = {
4804
- schemaVersion: 1,
4805
- updatedAt: now,
4806
- items: generated.sort(
4807
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
4808
- )
4809
- };
4810
- writeUnits(next);
4787
+ fs11.writeFileSync(filePath, JSON.stringify(body, null, 2));
4811
4788
  writeAuditLog({
4812
- entity: "unit",
4813
- action: "create",
4814
- targetId: "units",
4815
- detail: { count: generated.length }
4816
- });
4817
- return c.json({
4818
- generated: generated.length,
4819
- pending: generated.filter((item) => item.status === "pending").length,
4820
- updatedAt: now
4789
+ entity: "rule",
4790
+ action: "update",
4791
+ targetId: id,
4792
+ detail: { itemCount: body.items.length }
4821
4793
  });
4794
+ return c.json(body);
4822
4795
  } catch (error) {
4823
- console.error("Failed to generate units:", error);
4824
- return c.json({ error: "Failed to generate units" }, 500);
4796
+ console.error("Failed to update rules:", error);
4797
+ return c.json({ error: "Failed to update rules" }, 500);
4825
4798
  }
4826
4799
  });
4827
- app.patch("/api/units/:id/status", async (c) => {
4828
- const id = sanitizeId(c.req.param("id"));
4829
- const body = await c.req.json();
4830
- if (!id) {
4831
- return c.json({ error: "Invalid unit id" }, 400);
4800
+ rules.delete("/:id/:ruleId", async (c) => {
4801
+ const id = c.req.param("id");
4802
+ if (!ALLOWED_RULE_FILES.has(id)) {
4803
+ return c.json({ error: "Invalid rule type" }, 400);
4832
4804
  }
4833
- if (!body.status || !["pending", "approved", "rejected"].includes(body.status)) {
4834
- return c.json({ error: "Invalid status" }, 400);
4805
+ const ruleId = sanitizeId(c.req.param("ruleId"));
4806
+ if (!ruleId) {
4807
+ return c.json({ error: "Invalid rule id" }, 400);
4808
+ }
4809
+ const filePath = path10.join(rulesDir(), `${id}.json`);
4810
+ if (!fs11.existsSync(filePath)) {
4811
+ return c.json({ error: "Rules not found" }, 404);
4812
+ }
4813
+ try {
4814
+ const doc = safeParseJsonFile(filePath);
4815
+ if (!doc || !Array.isArray(doc.items)) {
4816
+ return c.json({ error: "Invalid rules format" }, 500);
4817
+ }
4818
+ const nextItems = doc.items.filter((item) => item.id !== ruleId);
4819
+ if (nextItems.length === doc.items.length) {
4820
+ return c.json({ error: "Rule not found" }, 404);
4821
+ }
4822
+ const nextDoc = {
4823
+ ...doc,
4824
+ items: nextItems,
4825
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4826
+ };
4827
+ fs11.writeFileSync(filePath, JSON.stringify(nextDoc, null, 2));
4828
+ writeAuditLog({
4829
+ entity: "rule",
4830
+ action: "delete",
4831
+ targetId: ruleId,
4832
+ detail: { ruleType: id }
4833
+ });
4834
+ return c.json({ deleted: 1, id: ruleId, ruleType: id });
4835
+ } catch (error) {
4836
+ console.error("Failed to delete rule:", error);
4837
+ return c.json({ error: "Failed to delete rule" }, 500);
4835
4838
  }
4836
- const doc = readUnits();
4837
- const index = doc.items.findIndex((item) => item.id === id);
4838
- if (index === -1) {
4839
- return c.json({ error: "Unit not found" }, 404);
4840
- }
4841
- const now = (/* @__PURE__ */ new Date()).toISOString();
4842
- const actor = getCurrentUser();
4843
- const nextItem = {
4844
- ...doc.items[index],
4845
- status: body.status,
4846
- updatedAt: now,
4847
- reviewedAt: now,
4848
- reviewedBy: actor
4849
- };
4850
- doc.items[index] = nextItem;
4851
- doc.updatedAt = now;
4852
- writeUnits(doc);
4853
- writeAuditLog({
4854
- entity: "unit",
4855
- action: "update",
4856
- targetId: id,
4857
- detail: { status: body.status }
4858
- });
4859
- return c.json(nextItem);
4860
- });
4861
- app.delete("/api/units/:id", async (c) => {
4862
- const id = sanitizeId(c.req.param("id"));
4863
- if (!id) {
4864
- return c.json({ error: "Invalid unit id" }, 400);
4865
- }
4866
- const doc = readUnits();
4867
- const nextItems = doc.items.filter((item) => item.id !== id);
4868
- if (nextItems.length === doc.items.length) {
4869
- return c.json({ error: "Unit not found" }, 404);
4870
- }
4871
- doc.items = nextItems;
4872
- doc.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4873
- writeUnits(doc);
4874
- writeAuditLog({
4875
- entity: "unit",
4876
- action: "delete",
4877
- targetId: id
4878
- });
4879
- return c.json({ deleted: 1, id });
4880
4839
  });
4881
- app.get("/api/knowledge-graph", async (c) => {
4840
+ var rules_default = rules;
4841
+
4842
+ // dashboard/server/routes/sessions.ts
4843
+ import fs12 from "node:fs";
4844
+ import path11 from "node:path";
4845
+ var sessions = new Hono2();
4846
+ sessions.get("/", async (c) => {
4847
+ const useIndex = c.req.query("useIndex") !== "false";
4848
+ const usePagination = c.req.query("paginate") !== "false";
4849
+ const mnemeDir2 = getMnemeDir();
4850
+ const params = parsePaginationParams(c);
4882
4851
  try {
4883
- const mnemeDir2 = getMnemeDir();
4884
- const sessionItems = readAllSessionIndexes(mnemeDir2).items;
4885
- const units = readUnits().items.filter(
4886
- (item) => item.status === "approved"
4887
- );
4888
- const sessionDataMap = /* @__PURE__ */ new Map();
4889
- for (const item of sessionItems.filter((i) => i.hasSummary)) {
4890
- try {
4891
- const sessionPath = path3.join(mnemeDir2, item.filePath);
4892
- const raw2 = fs4.readFileSync(sessionPath, "utf-8");
4893
- const session = JSON.parse(raw2);
4894
- if (session.resumedFrom) {
4895
- sessionDataMap.set(item.id, {
4896
- resumedFrom: session.resumedFrom
4897
- });
4898
- }
4899
- } catch {
4852
+ let items;
4853
+ if (useIndex) {
4854
+ const index = params.allMonths ? readAllSessionIndexes(mnemeDir2) : readRecentSessionIndexes(mnemeDir2);
4855
+ items = index.items;
4856
+ } else {
4857
+ const sessionsDir = path11.join(mnemeDir2, "sessions");
4858
+ const files = listDatedJsonFiles(sessionsDir);
4859
+ if (files.length === 0) {
4860
+ return usePagination ? c.json({
4861
+ data: [],
4862
+ pagination: {
4863
+ page: 1,
4864
+ limit: params.limit,
4865
+ total: 0,
4866
+ totalPages: 0,
4867
+ hasNext: false,
4868
+ hasPrev: false
4869
+ }
4870
+ }) : c.json([]);
4900
4871
  }
4872
+ items = files.map((filePath) => {
4873
+ const content = fs12.readFileSync(filePath, "utf-8");
4874
+ return JSON.parse(content);
4875
+ });
4876
+ items.sort(
4877
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
4878
+ );
4901
4879
  }
4902
- const nodes = [
4903
- ...sessionItems.filter((item) => item.hasSummary).map((item) => ({
4904
- id: `session:${item.id}`,
4905
- entityType: "session",
4906
- entityId: item.id,
4907
- title: item.title,
4908
- tags: item.tags || [],
4909
- createdAt: item.createdAt,
4910
- branch: item.branch || null,
4911
- resumedFrom: sessionDataMap.get(item.id)?.resumedFrom || null,
4912
- unitSubtype: null,
4913
- sourceId: null,
4914
- appliedCount: null,
4915
- acceptedCount: null
4916
- })),
4917
- ...units.map((item) => ({
4918
- id: `unit:${item.id}`,
4919
- entityType: "unit",
4920
- entityId: item.id,
4921
- title: item.title,
4922
- tags: item.tags || [],
4923
- createdAt: item.createdAt,
4924
- unitSubtype: item.type || null,
4925
- sourceId: item.sourceId || null,
4926
- appliedCount: null,
4927
- acceptedCount: null,
4928
- branch: null,
4929
- resumedFrom: null
4930
- }))
4931
- ];
4880
+ let filtered = items;
4881
+ if (!params.showUntitled) {
4882
+ filtered = filtered.filter((s) => s.hasSummary === true);
4883
+ }
4884
+ if (params.tag) {
4885
+ filtered = filtered.filter(
4886
+ (s) => s.tags?.includes(params.tag)
4887
+ );
4888
+ }
4889
+ if (params.type) {
4890
+ filtered = filtered.filter((s) => s.sessionType === params.type);
4891
+ }
4892
+ if (params.project) {
4893
+ const projectQuery = params.project;
4894
+ filtered = filtered.filter((s) => {
4895
+ const ctx = s.context;
4896
+ const projectName = ctx?.projectName;
4897
+ const repository = ctx?.repository;
4898
+ return projectName === projectQuery || repository === projectQuery || repository?.endsWith(`/${projectQuery}`);
4899
+ });
4900
+ }
4901
+ if (params.search) {
4902
+ const query = params.search.toLowerCase();
4903
+ filtered = filtered.filter((s) => {
4904
+ const title = (s.title || "").toLowerCase();
4905
+ const goal = (s.goal || "").toLowerCase();
4906
+ return title.includes(query) || goal.includes(query);
4907
+ });
4908
+ }
4909
+ if (!usePagination) {
4910
+ return c.json(filtered);
4911
+ }
4912
+ return c.json(paginateArray(filtered, params.page, params.limit));
4913
+ } catch (error) {
4914
+ console.error("Failed to read sessions:", error);
4915
+ return c.json({ error: "Failed to read sessions" }, 500);
4916
+ }
4917
+ });
4918
+ sessions.get("/graph", async (c) => {
4919
+ const mnemeDir2 = getMnemeDir();
4920
+ const showUntitled = c.req.query("showUntitled") === "true";
4921
+ try {
4922
+ const sessionsIndex = readAllSessionIndexes(mnemeDir2);
4923
+ const filteredItems = showUntitled ? sessionsIndex.items : sessionsIndex.items.filter((s) => s.hasSummary === true);
4924
+ const nodes = filteredItems.map((session) => ({
4925
+ id: session.id,
4926
+ title: session.title,
4927
+ type: session.sessionType || "unknown",
4928
+ tags: session.tags || [],
4929
+ createdAt: session.createdAt
4930
+ }));
4932
4931
  const tagToNodes = /* @__PURE__ */ new Map();
4933
- for (const node of nodes) {
4934
- for (const tag of node.tags) {
4932
+ for (const item of filteredItems) {
4933
+ for (const tag of item.tags || []) {
4935
4934
  const list = tagToNodes.get(tag) || [];
4936
- list.push(node.id);
4935
+ list.push(item.id);
4937
4936
  tagToNodes.set(tag, list);
4938
4937
  }
4939
4938
  }
4940
4939
  const edgeMap = /* @__PURE__ */ new Map();
4941
- for (const [tag, nodeIds] of tagToNodes) {
4940
+ for (const [, nodeIds] of tagToNodes) {
4942
4941
  for (let i = 0; i < nodeIds.length; i++) {
4943
4942
  for (let j = i + 1; j < nodeIds.length; j++) {
4944
4943
  const key = nodeIds[i] < nodeIds[j] ? `${nodeIds[i]}|${nodeIds[j]}` : `${nodeIds[j]}|${nodeIds[i]}`;
4945
4944
  const existing = edgeMap.get(key);
4946
4945
  if (existing) {
4947
4946
  existing.weight++;
4948
- existing.sharedTags.push(tag);
4949
4947
  } else {
4950
4948
  const [source, target] = key.split("|");
4951
- edgeMap.set(key, {
4952
- source,
4953
- target,
4954
- weight: 1,
4955
- sharedTags: [tag],
4956
- edgeType: "sharedTags",
4957
- directed: false
4958
- });
4949
+ edgeMap.set(key, { source, target, weight: 1 });
4959
4950
  }
4960
4951
  }
4961
4952
  }
4962
4953
  }
4963
- const tagEdges = Array.from(edgeMap.values());
4964
- const nodeIdSet = new Set(nodes.map((n) => n.id));
4965
- const resumedEdges = [];
4966
- for (const node of nodes) {
4967
- if (node.entityType === "session" && node.resumedFrom) {
4968
- const targetId = `session:${node.resumedFrom}`;
4969
- if (nodeIdSet.has(targetId)) {
4970
- resumedEdges.push({
4971
- source: targetId,
4972
- target: node.id,
4973
- weight: 1,
4974
- sharedTags: [],
4975
- edgeType: "resumedFrom",
4976
- directed: true
4977
- });
4978
- }
4979
- }
4980
- }
4981
- const edges = [...tagEdges, ...resumedEdges];
4954
+ const edges = Array.from(edgeMap.values());
4982
4955
  return c.json({ nodes, edges });
4983
4956
  } catch (error) {
4984
- console.error("Failed to build knowledge graph:", error);
4985
- return c.json({ error: "Failed to build knowledge graph" }, 500);
4957
+ console.error("Failed to build session graph:", error);
4958
+ return c.json({ error: "Failed to build session graph" }, 500);
4986
4959
  }
4987
4960
  });
4988
- function sessionToMarkdown(session) {
4989
- const lines = [];
4990
- lines.push(`# ${session.title || "Untitled Session"}`);
4991
- lines.push("");
4992
- lines.push(`**ID:** ${session.id}`);
4993
- lines.push(`**Created:** ${session.createdAt}`);
4994
- if (session.sessionType) {
4995
- lines.push(`**Type:** ${session.sessionType}`);
4996
- }
4997
- if (session.context) {
4998
- const ctx = session.context;
4999
- if (ctx.branch) lines.push(`**Branch:** ${ctx.branch}`);
5000
- if (ctx.user) lines.push(`**User:** ${ctx.user}`);
5001
- }
5002
- lines.push("");
5003
- if (session.goal) {
5004
- lines.push("## Goal");
5005
- lines.push("");
5006
- lines.push(session.goal);
5007
- lines.push("");
5008
- }
5009
- const tags = session.tags;
5010
- if (tags && tags.length > 0) {
5011
- lines.push("## Tags");
5012
- lines.push("");
5013
- lines.push(tags.map((t) => `\`${t}\``).join(", "));
5014
- lines.push("");
5015
- }
5016
- const interactions = session.interactions;
5017
- if (interactions && interactions.length > 0) {
5018
- lines.push("## Interactions");
5019
- lines.push("");
5020
- for (const interaction of interactions) {
5021
- lines.push(`### ${interaction.choice || "Interaction"}`);
5022
- lines.push("");
5023
- if (interaction.reasoning) {
5024
- lines.push(`**Reasoning:** ${interaction.reasoning}`);
5025
- lines.push("");
5026
- }
5027
- if (interaction.timestamp) {
5028
- lines.push(`*${interaction.timestamp}*`);
5029
- lines.push("");
5030
- }
5031
- lines.push("---");
5032
- lines.push("");
5033
- }
5034
- }
5035
- if (session.outcome) {
5036
- lines.push("## Outcome");
5037
- lines.push("");
5038
- lines.push(session.outcome);
5039
- lines.push("");
5040
- }
5041
- const relatedSessions = session.relatedSessions;
5042
- if (relatedSessions && relatedSessions.length > 0) {
5043
- lines.push("## Related Sessions");
5044
- lines.push("");
5045
- for (const relId of relatedSessions) {
5046
- lines.push(`- ${relId}`);
5047
- }
5048
- lines.push("");
5049
- }
5050
- lines.push("---");
5051
- lines.push("*Exported from mneme*");
5052
- return lines.join("\n");
5053
- }
5054
- function decisionToMarkdown(decision) {
5055
- const lines = [];
5056
- lines.push(`# ${decision.title || "Untitled Decision"}`);
5057
- lines.push("");
5058
- lines.push(`**ID:** ${decision.id}`);
5059
- lines.push(`**Status:** ${decision.status || "unknown"}`);
5060
- lines.push(`**Created:** ${decision.createdAt}`);
5061
- if (decision.updatedAt) {
5062
- lines.push(`**Updated:** ${decision.updatedAt}`);
5063
- }
5064
- lines.push("");
5065
- if (decision.decision) {
5066
- lines.push("## Decision");
5067
- lines.push("");
5068
- lines.push(decision.decision);
5069
- lines.push("");
5070
- }
5071
- if (decision.rationale) {
5072
- lines.push("## Rationale");
5073
- lines.push("");
5074
- lines.push(decision.rationale);
5075
- lines.push("");
5076
- }
5077
- const tags = decision.tags;
5078
- if (tags && tags.length > 0) {
5079
- lines.push("## Tags");
5080
- lines.push("");
5081
- lines.push(tags.map((t) => `\`${t}\``).join(", "));
5082
- lines.push("");
5083
- }
5084
- const alternatives = decision.alternatives;
5085
- if (alternatives && alternatives.length > 0) {
5086
- lines.push("## Alternatives Considered");
5087
- lines.push("");
5088
- for (const alt of alternatives) {
5089
- lines.push(`### ${alt.title || "Alternative"}`);
5090
- if (alt.description) {
5091
- lines.push("");
5092
- lines.push(alt.description);
5093
- }
5094
- if (alt.pros) {
5095
- lines.push("");
5096
- lines.push("**Pros:**");
5097
- for (const pro of alt.pros) {
5098
- lines.push(`- ${pro}`);
5099
- }
5100
- }
5101
- if (alt.cons) {
5102
- lines.push("");
5103
- lines.push("**Cons:**");
5104
- for (const con of alt.cons) {
5105
- lines.push(`- ${con}`);
5106
- }
5107
- }
5108
- lines.push("");
5109
- }
5110
- }
5111
- const relatedSessions = decision.relatedSessions;
5112
- if (relatedSessions && relatedSessions.length > 0) {
5113
- lines.push("## Related Sessions");
5114
- lines.push("");
5115
- for (const relId of relatedSessions) {
5116
- lines.push(`- ${relId}`);
5117
- }
5118
- lines.push("");
5119
- }
5120
- lines.push("---");
5121
- lines.push("*Exported from mneme*");
5122
- return lines.join("\n");
5123
- }
5124
- app.get("/api/export/sessions/:id/markdown", async (c) => {
5125
- const id = c.req.param("id");
5126
- const sessionsDir = path3.join(getMnemeDir(), "sessions");
4961
+ sessions.get("/:id", async (c) => {
4962
+ const id = sanitizeId(c.req.param("id"));
4963
+ const sessionsDir = path11.join(getMnemeDir(), "sessions");
5127
4964
  try {
5128
4965
  const filePath = findJsonFileById(sessionsDir, id);
5129
4966
  if (!filePath) {
5130
4967
  return c.json({ error: "Session not found" }, 404);
5131
4968
  }
5132
- const content = fs4.readFileSync(filePath, "utf-8");
5133
- const session = JSON.parse(content);
5134
- const markdown = sessionToMarkdown(session);
5135
- const filename = `session-${id}.md`;
5136
- c.header("Content-Type", "text/markdown; charset=utf-8");
5137
- c.header("Content-Disposition", `attachment; filename="${filename}"`);
5138
- return c.text(markdown);
4969
+ const session = safeParseJsonFile(filePath);
4970
+ if (!session) {
4971
+ return c.json({ error: "Failed to parse session" }, 500);
4972
+ }
4973
+ return c.json(session);
5139
4974
  } catch (error) {
5140
- console.error("Failed to export session:", error);
5141
- return c.json({ error: "Failed to export session" }, 500);
4975
+ console.error("Failed to read session:", error);
4976
+ return c.json({ error: "Failed to read session" }, 500);
5142
4977
  }
5143
4978
  });
5144
- app.get("/api/export/decisions/:id/markdown", async (c) => {
4979
+ sessions.get("/:id/markdown", async (c) => {
5145
4980
  const id = sanitizeId(c.req.param("id"));
5146
- const decisionsDir = path3.join(getMnemeDir(), "decisions");
4981
+ const sessionsDir = path11.join(getMnemeDir(), "sessions");
5147
4982
  try {
5148
- const filePath = findJsonFileById(decisionsDir, id);
5149
- if (!filePath) {
5150
- return c.json({ error: "Decision not found" }, 404);
4983
+ const jsonPath = findJsonFileById(sessionsDir, id);
4984
+ if (!jsonPath) {
4985
+ return c.json({ error: "Session not found" }, 404);
5151
4986
  }
5152
- const content = fs4.readFileSync(filePath, "utf-8");
5153
- const decision = JSON.parse(content);
5154
- const markdown = decisionToMarkdown(decision);
5155
- const filename = `decision-${id}.md`;
5156
- c.header("Content-Type", "text/markdown; charset=utf-8");
5157
- c.header("Content-Disposition", `attachment; filename="${filename}"`);
5158
- return c.text(markdown);
4987
+ const mdPath = jsonPath.replace(/\.json$/, ".md");
4988
+ if (!fs12.existsSync(mdPath)) {
4989
+ return c.json({ exists: false, content: null });
4990
+ }
4991
+ const content = fs12.readFileSync(mdPath, "utf-8");
4992
+ return c.json({ exists: true, content });
5159
4993
  } catch (error) {
5160
- console.error("Failed to export decision:", error);
5161
- return c.json({ error: "Failed to export decision" }, 500);
4994
+ console.error("Failed to read session markdown:", error);
4995
+ return c.json({ error: "Failed to read session markdown" }, 500);
5162
4996
  }
5163
4997
  });
5164
- app.post("/api/export/sessions/bulk", async (c) => {
5165
- const body = await c.req.json();
5166
- const { ids } = body;
5167
- if (!ids || ids.length === 0) {
5168
- return c.json({ error: "No session IDs provided" }, 400);
5169
- }
5170
- const sessionsDir = path3.join(getMnemeDir(), "sessions");
5171
- const markdowns = [];
4998
+ sessions.delete("/:id", async (c) => {
4999
+ const id = sanitizeId(c.req.param("id"));
5000
+ const dryRun = c.req.query("dry-run") === "true";
5001
+ const mnemeDir2 = getMnemeDir();
5002
+ const sessionsDir = path11.join(mnemeDir2, "sessions");
5172
5003
  try {
5173
- for (const id of ids) {
5174
- const filePath = findJsonFileById(sessionsDir, id);
5175
- if (filePath) {
5176
- const content = fs4.readFileSync(filePath, "utf-8");
5177
- const session = JSON.parse(content);
5178
- markdowns.push(sessionToMarkdown(session));
5004
+ const filePath = findJsonFileById(sessionsDir, id);
5005
+ if (!filePath) {
5006
+ return c.json({ error: "Session not found" }, 404);
5007
+ }
5008
+ const sessionData = safeParseJsonFile(filePath);
5009
+ const db = openLocalDatabase(getProjectRoot());
5010
+ let interactionCount = 0;
5011
+ if (db) {
5012
+ interactionCount = countInteractions(db, { sessionId: id });
5013
+ if (!dryRun) {
5014
+ deleteInteractions(db, id);
5015
+ deleteBackups(db, id);
5179
5016
  }
5017
+ db.close();
5180
5018
  }
5181
- if (markdowns.length === 0) {
5182
- return c.json({ error: "No sessions found" }, 404);
5019
+ if (!dryRun) {
5020
+ fs12.unlinkSync(filePath);
5021
+ const mdPath = filePath.replace(/\.json$/, ".md");
5022
+ if (fs12.existsSync(mdPath)) {
5023
+ fs12.unlinkSync(mdPath);
5024
+ }
5025
+ const sessionLinksDir = path11.join(mnemeDir2, "session-links");
5026
+ const linkPath = path11.join(sessionLinksDir, `${id}.json`);
5027
+ if (fs12.existsSync(linkPath)) {
5028
+ fs12.unlinkSync(linkPath);
5029
+ }
5030
+ if (sessionData?.createdAt) {
5031
+ const date = new Date(sessionData.createdAt);
5032
+ const year = date.getFullYear().toString();
5033
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
5034
+ rebuildSessionIndexForMonth(mnemeDir2, year, month);
5035
+ }
5036
+ writeAuditLog({
5037
+ entity: "session",
5038
+ action: "delete",
5039
+ targetId: id
5040
+ });
5183
5041
  }
5184
- const combined = markdowns.join("\n\n---\n\n");
5185
- const filename = `sessions-export-${Date.now()}.md`;
5186
- c.header("Content-Type", "text/markdown; charset=utf-8");
5187
- c.header("Content-Disposition", `attachment; filename="${filename}"`);
5188
- return c.text(combined);
5042
+ return c.json({
5043
+ deleted: dryRun ? 0 : 1,
5044
+ interactionsDeleted: dryRun ? 0 : interactionCount,
5045
+ dryRun,
5046
+ sessionId: id
5047
+ });
5189
5048
  } catch (error) {
5190
- console.error("Failed to export sessions:", error);
5191
- return c.json({ error: "Failed to export sessions" }, 500);
5049
+ console.error("Failed to delete session:", error);
5050
+ return c.json({ error: "Failed to delete session" }, 500);
5192
5051
  }
5193
5052
  });
5194
- app.get("/api/tags", async (c) => {
5195
- const tagsPath = path3.join(getMnemeDir(), "tags.json");
5053
+ sessions.delete("/", async (c) => {
5054
+ const dryRun = c.req.query("dry-run") === "true";
5055
+ const projectFilter = c.req.query("project");
5056
+ const repositoryFilter = c.req.query("repository");
5057
+ const beforeFilter = c.req.query("before");
5058
+ const mnemeDir2 = getMnemeDir();
5059
+ const sessionsDir = path11.join(mnemeDir2, "sessions");
5196
5060
  try {
5197
- if (!fs4.existsSync(tagsPath)) {
5198
- return c.json({ version: 1, tags: [] });
5061
+ const files = listDatedJsonFiles(sessionsDir);
5062
+ const sessionsToDelete = [];
5063
+ for (const filePath of files) {
5064
+ try {
5065
+ const content = fs12.readFileSync(filePath, "utf-8");
5066
+ const session = JSON.parse(content);
5067
+ let shouldDelete = true;
5068
+ if (projectFilter) {
5069
+ const sessionProject = session.context?.projectDir;
5070
+ if (sessionProject !== projectFilter) {
5071
+ shouldDelete = false;
5072
+ }
5073
+ }
5074
+ if (repositoryFilter && shouldDelete) {
5075
+ const sessionRepo = session.context?.repository;
5076
+ if (sessionRepo !== repositoryFilter) {
5077
+ shouldDelete = false;
5078
+ }
5079
+ }
5080
+ if (beforeFilter && shouldDelete) {
5081
+ const sessionDate = session.createdAt?.split("T")[0];
5082
+ if (!sessionDate || sessionDate >= beforeFilter) {
5083
+ shouldDelete = false;
5084
+ }
5085
+ }
5086
+ if (shouldDelete) {
5087
+ sessionsToDelete.push({ id: session.id, path: filePath });
5088
+ }
5089
+ } catch {
5090
+ }
5199
5091
  }
5200
- const tags = safeParseJsonFile(tagsPath);
5201
- if (!tags) {
5202
- return c.json({ error: "Failed to parse tags" }, 500);
5092
+ let totalInteractions = 0;
5093
+ const db = openLocalDatabase(getProjectRoot());
5094
+ if (db) {
5095
+ for (const session of sessionsToDelete) {
5096
+ totalInteractions += countInteractions(db, { sessionId: session.id });
5097
+ }
5098
+ if (!dryRun) {
5099
+ for (const session of sessionsToDelete) {
5100
+ deleteInteractions(db, session.id);
5101
+ deleteBackups(db, session.id);
5102
+ }
5103
+ }
5104
+ db.close();
5105
+ }
5106
+ if (!dryRun) {
5107
+ for (const session of sessionsToDelete) {
5108
+ fs12.unlinkSync(session.path);
5109
+ const mdPath = session.path.replace(/\.json$/, ".md");
5110
+ if (fs12.existsSync(mdPath)) {
5111
+ fs12.unlinkSync(mdPath);
5112
+ }
5113
+ const sessionLinksDir = path11.join(mnemeDir2, "session-links");
5114
+ const linkPath = path11.join(sessionLinksDir, `${session.id}.json`);
5115
+ if (fs12.existsSync(linkPath)) {
5116
+ fs12.unlinkSync(linkPath);
5117
+ }
5118
+ writeAuditLog({
5119
+ entity: "session",
5120
+ action: "delete",
5121
+ targetId: session.id
5122
+ });
5123
+ }
5203
5124
  }
5204
- return c.json(tags);
5205
- } catch (error) {
5206
- console.error("Failed to read tags:", error);
5207
- return c.json({ error: "Failed to read tags" }, 500);
5208
- }
5209
- });
5210
- app.get("/api/indexes/status", async (c) => {
5211
- const mnemeDir2 = getMnemeDir();
5212
- try {
5213
- const sessionsIndex = readAllSessionIndexes(mnemeDir2);
5214
- const decisionsIndex = readAllDecisionIndexes(mnemeDir2);
5215
5125
  return c.json({
5216
- sessions: {
5217
- exists: sessionsIndex.items.length > 0,
5218
- itemCount: sessionsIndex.items.length,
5219
- updatedAt: sessionsIndex.updatedAt,
5220
- isStale: isIndexStale(sessionsIndex)
5221
- },
5222
- decisions: {
5223
- exists: decisionsIndex.items.length > 0,
5224
- itemCount: decisionsIndex.items.length,
5225
- updatedAt: decisionsIndex.updatedAt,
5226
- isStale: isIndexStale(decisionsIndex)
5126
+ deleted: dryRun ? 0 : sessionsToDelete.length,
5127
+ interactionsDeleted: dryRun ? 0 : totalInteractions,
5128
+ wouldDelete: sessionsToDelete.length,
5129
+ dryRun,
5130
+ filters: {
5131
+ project: projectFilter || null,
5132
+ repository: repositoryFilter || null,
5133
+ before: beforeFilter || null
5227
5134
  }
5228
5135
  });
5229
5136
  } catch (error) {
5230
- console.error("Failed to get index status:", error);
5231
- return c.json({ error: "Failed to get index status" }, 500);
5137
+ console.error("Failed to delete sessions:", error);
5138
+ return c.json({ error: "Failed to delete sessions" }, 500);
5232
5139
  }
5233
5140
  });
5234
- app.post("/api/indexes/rebuild", async (c) => {
5141
+ sessions.get("/:id/interactions", async (c) => {
5142
+ const id = sanitizeId(c.req.param("id"));
5235
5143
  const mnemeDir2 = getMnemeDir();
5144
+ const sessionLinksDir = path11.join(mnemeDir2, "session-links");
5145
+ const sessionsDir = path11.join(mnemeDir2, "sessions");
5236
5146
  try {
5237
- const sessionIndexes = rebuildAllSessionIndexes(mnemeDir2);
5238
- const decisionIndexes = rebuildAllDecisionIndexes(mnemeDir2);
5239
- let sessionCount = 0;
5240
- let sessionUpdatedAt = "";
5241
- for (const index of sessionIndexes.values()) {
5242
- sessionCount += index.items.length;
5243
- if (index.updatedAt > sessionUpdatedAt) {
5244
- sessionUpdatedAt = index.updatedAt;
5147
+ const sessionFilePath = findJsonFileById(sessionsDir, id);
5148
+ let projectDir = getProjectRoot();
5149
+ let primaryClaudeSessionId = null;
5150
+ if (sessionFilePath) {
5151
+ const sessionData = safeParseJsonFile(sessionFilePath);
5152
+ if (sessionData?.context?.projectDir) {
5153
+ projectDir = sessionData.context.projectDir;
5154
+ }
5155
+ if (sessionData?.sessionId) {
5156
+ primaryClaudeSessionId = sessionData.sessionId;
5245
5157
  }
5246
5158
  }
5247
- let decisionCount = 0;
5248
- let decisionUpdatedAt = "";
5249
- for (const index of decisionIndexes.values()) {
5250
- decisionCount += index.items.length;
5251
- if (index.updatedAt > decisionUpdatedAt) {
5252
- decisionUpdatedAt = index.updatedAt;
5159
+ const db = openLocalDatabase(projectDir);
5160
+ if (!db) {
5161
+ return c.json({ interactions: [], count: 0 });
5162
+ }
5163
+ let masterId = id;
5164
+ const myLinkFile = path11.join(sessionLinksDir, `${id}.json`);
5165
+ if (fs12.existsSync(myLinkFile)) {
5166
+ try {
5167
+ const myLinkData = JSON.parse(fs12.readFileSync(myLinkFile, "utf-8"));
5168
+ if (myLinkData.masterSessionId) {
5169
+ masterId = myLinkData.masterSessionId;
5170
+ }
5171
+ } catch {
5253
5172
  }
5254
5173
  }
5255
- return c.json({
5256
- success: true,
5257
- sessions: {
5258
- itemCount: sessionCount,
5259
- monthCount: sessionIndexes.size,
5260
- updatedAt: sessionUpdatedAt || (/* @__PURE__ */ new Date()).toISOString()
5261
- },
5262
- decisions: {
5263
- itemCount: decisionCount,
5264
- monthCount: decisionIndexes.size,
5265
- updatedAt: decisionUpdatedAt || (/* @__PURE__ */ new Date()).toISOString()
5174
+ const sessionIds = [masterId];
5175
+ const claudeSessionIds = [];
5176
+ if (primaryClaudeSessionId) {
5177
+ claudeSessionIds.push(primaryClaudeSessionId);
5178
+ }
5179
+ if (masterId !== id) {
5180
+ sessionIds.push(id);
5181
+ }
5182
+ if (fs12.existsSync(sessionLinksDir)) {
5183
+ const linkFiles = fs12.readdirSync(sessionLinksDir);
5184
+ for (const linkFile of linkFiles) {
5185
+ if (!linkFile.endsWith(".json")) continue;
5186
+ const linkPath = path11.join(sessionLinksDir, linkFile);
5187
+ try {
5188
+ const linkData = JSON.parse(fs12.readFileSync(linkPath, "utf-8"));
5189
+ if (linkData.masterSessionId === masterId) {
5190
+ const childId = linkFile.replace(".json", "");
5191
+ if (!sessionIds.includes(childId)) {
5192
+ sessionIds.push(childId);
5193
+ }
5194
+ const childSessionFile = findJsonFileById(sessionsDir, childId);
5195
+ if (childSessionFile) {
5196
+ const childData = safeParseJsonFile(
5197
+ childSessionFile
5198
+ );
5199
+ if (childData?.sessionId) {
5200
+ claudeSessionIds.push(childData.sessionId);
5201
+ }
5202
+ }
5203
+ }
5204
+ } catch {
5205
+ }
5206
+ }
5207
+ }
5208
+ const sessionFiles = listDatedJsonFiles(sessionsDir);
5209
+ for (const sessionFile of sessionFiles) {
5210
+ try {
5211
+ const sessionData = JSON.parse(fs12.readFileSync(sessionFile, "utf-8"));
5212
+ if (sessionData.resumedFrom === masterId && sessionData.id !== masterId) {
5213
+ if (!sessionIds.includes(sessionData.id)) {
5214
+ sessionIds.push(sessionData.id);
5215
+ }
5216
+ if (sessionData.sessionId) {
5217
+ claudeSessionIds.push(sessionData.sessionId);
5218
+ }
5219
+ }
5220
+ } catch {
5221
+ }
5222
+ }
5223
+ const interactions = claudeSessionIds.length > 0 ? getInteractionsByClaudeSessionIds(db, claudeSessionIds) : getInteractionsBySessionIds(db, sessionIds);
5224
+ db.close();
5225
+ const groupedInteractions = [];
5226
+ let currentInteraction = null;
5227
+ for (const interaction of interactions) {
5228
+ if (interaction.role === "user") {
5229
+ if (currentInteraction) {
5230
+ groupedInteractions.push(currentInteraction);
5231
+ }
5232
+ let hasPlanMode;
5233
+ let planTools;
5234
+ let toolsUsed;
5235
+ let toolDetails;
5236
+ let inPlanMode;
5237
+ let slashCommand;
5238
+ let toolResults;
5239
+ let progressEvents;
5240
+ if (interaction.tool_calls) {
5241
+ try {
5242
+ const metadata = JSON.parse(interaction.tool_calls);
5243
+ if (metadata.hasPlanMode) {
5244
+ hasPlanMode = true;
5245
+ planTools = metadata.planTools || [];
5246
+ }
5247
+ if (metadata.inPlanMode) {
5248
+ inPlanMode = true;
5249
+ }
5250
+ if (metadata.toolsUsed && Array.isArray(metadata.toolsUsed) && metadata.toolsUsed.length > 0) {
5251
+ toolsUsed = metadata.toolsUsed;
5252
+ }
5253
+ if (metadata.toolDetails && Array.isArray(metadata.toolDetails) && metadata.toolDetails.length > 0) {
5254
+ toolDetails = metadata.toolDetails;
5255
+ }
5256
+ if (metadata.slashCommand) {
5257
+ slashCommand = metadata.slashCommand;
5258
+ }
5259
+ if (metadata.toolResults && Array.isArray(metadata.toolResults) && metadata.toolResults.length > 0) {
5260
+ toolResults = metadata.toolResults;
5261
+ }
5262
+ if (metadata.progressEvents && Array.isArray(metadata.progressEvents) && metadata.progressEvents.length > 0) {
5263
+ progressEvents = metadata.progressEvents;
5264
+ }
5265
+ } catch {
5266
+ }
5267
+ }
5268
+ currentInteraction = {
5269
+ id: `int-${String(groupedInteractions.length + 1).padStart(3, "0")}`,
5270
+ timestamp: interaction.timestamp,
5271
+ user: interaction.content,
5272
+ assistant: "",
5273
+ thinking: null,
5274
+ isCompactSummary: !!interaction.is_compact_summary,
5275
+ ...hasPlanMode !== void 0 && { hasPlanMode },
5276
+ ...planTools !== void 0 && planTools.length > 0 && { planTools },
5277
+ ...toolsUsed !== void 0 && toolsUsed.length > 0 && { toolsUsed },
5278
+ ...toolDetails !== void 0 && toolDetails.length > 0 && { toolDetails },
5279
+ ...interaction.agent_id && { agentId: interaction.agent_id },
5280
+ ...interaction.agent_type && { agentType: interaction.agent_type },
5281
+ ...inPlanMode && { inPlanMode },
5282
+ ...slashCommand && { slashCommand },
5283
+ ...toolResults !== void 0 && toolResults.length > 0 && { toolResults },
5284
+ ...progressEvents !== void 0 && progressEvents.length > 0 && { progressEvents }
5285
+ };
5286
+ } else if (interaction.role === "assistant" && currentInteraction) {
5287
+ currentInteraction.assistant = interaction.content;
5288
+ currentInteraction.thinking = interaction.thinking || null;
5266
5289
  }
5290
+ }
5291
+ if (currentInteraction) {
5292
+ groupedInteractions.push(currentInteraction);
5293
+ }
5294
+ return c.json({
5295
+ interactions: groupedInteractions,
5296
+ count: groupedInteractions.length
5267
5297
  });
5268
5298
  } catch (error) {
5269
- return c.json(
5270
- { error: "Failed to rebuild indexes", details: String(error) },
5271
- 500
5272
- );
5299
+ console.error("Failed to get session interactions:", error);
5300
+ return c.json({ error: "Failed to get session interactions" }, 500);
5273
5301
  }
5274
5302
  });
5275
- var distPath = path3.join(import.meta.dirname, "public");
5276
- if (fs4.existsSync(distPath)) {
5303
+ var sessions_default = sessions;
5304
+
5305
+ // dashboard/server/index.ts
5306
+ var app = new Hono2();
5307
+ app.use(
5308
+ "/api/*",
5309
+ cors({
5310
+ origin: (origin) => {
5311
+ if (!origin) return void 0;
5312
+ if (origin.startsWith("http://localhost:")) return origin;
5313
+ return null;
5314
+ }
5315
+ })
5316
+ );
5317
+ app.route("/api/sessions", sessions_default);
5318
+ app.route("/api/decisions", decisions_default);
5319
+ app.route("/api/rules", rules_default);
5320
+ app.route("/api/patterns", patterns_default);
5321
+ app.route("/api/dev-rules", dev_rules_default);
5322
+ app.route("/api/export", export_default);
5323
+ app.route("/api", analytics_default);
5324
+ app.route("/api", misc_default);
5325
+ var distPath = path12.join(import.meta.dirname, "public");
5326
+ if (fs13.existsSync(distPath)) {
5277
5327
  app.use("/*", serveStatic({ root: distPath }));
5278
5328
  app.get("*", async (c) => {
5279
- const indexPath = path3.join(distPath, "index.html");
5280
- if (fs4.existsSync(indexPath)) {
5281
- const content = fs4.readFileSync(indexPath, "utf-8");
5329
+ const indexPath = path12.join(distPath, "index.html");
5330
+ if (fs13.existsSync(indexPath)) {
5331
+ const content = fs13.readFileSync(indexPath, "utf-8");
5282
5332
  return c.html(content);
5283
5333
  }
5284
5334
  return c.notFound();
@@ -5287,7 +5337,7 @@ if (fs4.existsSync(distPath)) {
5287
5337
  var requestedPort = parseInt(process.env.PORT || "7777", 10);
5288
5338
  var maxPortAttempts = 10;
5289
5339
  var mnemeDir = getMnemeDir();
5290
- if (fs4.existsSync(mnemeDir)) {
5340
+ if (fs13.existsSync(mnemeDir)) {
5291
5341
  try {
5292
5342
  const sessionsIndex = readRecentSessionIndexes(mnemeDir, 1);
5293
5343
  const decisionsIndex = readRecentDecisionIndexes(mnemeDir, 1);