@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/.claude-plugin/plugin.json +1 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/lib/db.js +13 -0
- package/dist/lib/incremental-save.js +2 -2
- package/dist/lib/prompt-search.js +1 -64
- package/dist/lib/search-core.js +1 -64
- package/dist/lib/session-finalize.js +2 -2
- package/dist/public/assets/index-BBhG3dcz.css +1 -0
- package/dist/public/assets/index-DSj4axBH.js +351 -0
- package/dist/public/assets/{react-force-graph-2d-Dlcfvz01.js → react-force-graph-2d-CC2S0imw.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/server.js +1675 -1625
- package/dist/servers/db-server.js +62 -450
- package/dist/servers/search-server.js +3 -95
- package/package.json +1 -1
- package/servers/db-server.ts +100 -529
- package/servers/search-server.ts +3 -46
- package/skills/resume/SKILL.md +80 -39
- package/skills/save/SKILL.md +40 -17
- package/dist/public/assets/index-Bvl_IrPy.css +0 -1
- package/dist/public/assets/index-k5JYSPV6.js +0 -351
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dashboard/server/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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 = (
|
|
669
|
+
var getStats = (path13) => {
|
|
670
670
|
let stats;
|
|
671
671
|
try {
|
|
672
|
-
stats = statSync(
|
|
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
|
|
701
|
+
let path13 = join(
|
|
702
702
|
root,
|
|
703
703
|
!optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename
|
|
704
704
|
);
|
|
705
|
-
let stats = getStats(
|
|
705
|
+
let stats = getStats(path13);
|
|
706
706
|
if (stats && stats.isDirectory()) {
|
|
707
707
|
const indexFile = options.index ?? "index.html";
|
|
708
|
-
|
|
709
|
-
stats = getStats(
|
|
708
|
+
path13 = join(path13, indexFile);
|
|
709
|
+
stats = getStats(path13);
|
|
710
710
|
}
|
|
711
711
|
if (!stats) {
|
|
712
|
-
await options.onNotFound?.(
|
|
712
|
+
await options.onNotFound?.(path13, c);
|
|
713
713
|
return next();
|
|
714
714
|
}
|
|
715
|
-
const mimeType = getMimeType(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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?.(
|
|
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 = (
|
|
883
|
-
const paths =
|
|
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:
|
|
891
|
-
const paths = splitPath(
|
|
890
|
+
const { groups, path: path13 } = extractGroupsFromPath(routePath);
|
|
891
|
+
const paths = splitPath(path13);
|
|
892
892
|
return replaceGroupMarks(paths, groups);
|
|
893
893
|
};
|
|
894
|
-
var extractGroupsFromPath = (
|
|
894
|
+
var extractGroupsFromPath = (path13) => {
|
|
895
895
|
const groups = [];
|
|
896
|
-
|
|
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:
|
|
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
|
|
959
|
-
return tryDecodeURI(
|
|
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 = (
|
|
977
|
-
if (
|
|
976
|
+
var checkOptionalParameter = (path13) => {
|
|
977
|
+
if (path13.charCodeAt(path13.length - 1) !== 63 || !path13.includes(":")) {
|
|
978
978
|
return null;
|
|
979
979
|
}
|
|
980
|
-
const segments =
|
|
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,
|
|
1121
|
+
constructor(request, path13 = "/", matchResult = [[]]) {
|
|
1122
1122
|
this.raw = request;
|
|
1123
|
-
this.path =
|
|
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,
|
|
1860
|
-
for (const p of [
|
|
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(
|
|
1918
|
-
const subApp = this.basePath(
|
|
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(
|
|
1944
|
+
basePath(path13) {
|
|
1945
1945
|
const subApp = this.#clone();
|
|
1946
|
-
subApp._basePath = mergePath(this._basePath,
|
|
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(
|
|
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,
|
|
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(
|
|
2062
|
+
this.#addRoute(METHOD_NAME_ALL, mergePath(path13, "*"), handler);
|
|
2063
2063
|
return this;
|
|
2064
2064
|
}
|
|
2065
|
-
#addRoute(method,
|
|
2065
|
+
#addRoute(method, path13, handler) {
|
|
2066
2066
|
method = method.toUpperCase();
|
|
2067
|
-
|
|
2068
|
-
const r = { basePath: this._basePath, path:
|
|
2069
|
-
this.router.add(method,
|
|
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
|
|
2083
|
-
const matchResult = this.router.match(method,
|
|
2082
|
+
const path13 = this.getPath(request, { env });
|
|
2083
|
+
const matchResult = this.router.match(method, path13);
|
|
2084
2084
|
const c = new Context(request, {
|
|
2085
|
-
path:
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
2372
|
-
return wildcardRegExpCache[
|
|
2373
|
-
|
|
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,
|
|
2395
|
+
const [pathErrorCheckOnly, path13, handlers] = routesWithStaticPathFlag[i];
|
|
2396
2396
|
if (pathErrorCheckOnly) {
|
|
2397
|
-
staticMap[
|
|
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(
|
|
2403
|
+
paramAssoc = trie.insert(path13, j, pathErrorCheckOnly);
|
|
2404
2404
|
} catch (e) {
|
|
2405
|
-
throw e === PATH_ERROR ? new UnsupportedPathError(
|
|
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,
|
|
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(
|
|
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,
|
|
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 (
|
|
2474
|
-
|
|
2473
|
+
if (path13 === "/*") {
|
|
2474
|
+
path13 = "*";
|
|
2475
2475
|
}
|
|
2476
|
-
const paramCount = (
|
|
2477
|
-
if (/\*$/.test(
|
|
2478
|
-
const re = buildWildcardRegExp(
|
|
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][
|
|
2481
|
+
middleware[m][path13] ||= findMiddleware(middleware[m], path13) || findMiddleware(middleware[METHOD_NAME_ALL], path13) || [];
|
|
2482
2482
|
});
|
|
2483
2483
|
} else {
|
|
2484
|
-
middleware[method][
|
|
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(
|
|
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((
|
|
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((
|
|
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,
|
|
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,
|
|
2559
|
+
this.#routes.push([method, path13, handler]);
|
|
2560
2560
|
}
|
|
2561
|
-
match(method,
|
|
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,
|
|
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,
|
|
2620
|
+
insert(method, path13, handler) {
|
|
2621
2621
|
this.#order = ++this.#order;
|
|
2622
2622
|
let curNode = this;
|
|
2623
|
-
const parts = splitRoutingPath(
|
|
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,
|
|
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(
|
|
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,
|
|
2768
|
-
const results = checkOptionalParameter(
|
|
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,
|
|
2775
|
+
this.#node.insert(method, path13, handler);
|
|
2776
2776
|
}
|
|
2777
|
-
match(method,
|
|
2778
|
-
return this.#node.search(method,
|
|
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/
|
|
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
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
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
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
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
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
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
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
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
|
-
|
|
3534
|
-
const
|
|
3535
|
-
const
|
|
3536
|
-
const
|
|
3537
|
-
const
|
|
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
|
|
3540
|
-
if (
|
|
3541
|
-
const
|
|
3542
|
-
|
|
3543
|
-
|
|
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
|
-
|
|
3568
|
-
|
|
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
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
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 (
|
|
3577
|
-
|
|
3632
|
+
if (!fs5.existsSync(filePath)) {
|
|
3633
|
+
return c.json({ error: "Source file not found" }, 404);
|
|
3578
3634
|
}
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
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
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
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
|
-
|
|
3597
|
-
|
|
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(
|
|
3698
|
+
return c.json({ timeline: sortedTimeline });
|
|
3600
3699
|
} catch (error) {
|
|
3601
|
-
console.error("Failed to
|
|
3602
|
-
return c.json({ error: "Failed to
|
|
3700
|
+
console.error("Failed to build timeline:", error);
|
|
3701
|
+
return c.json({ error: "Failed to build timeline" }, 500);
|
|
3603
3702
|
}
|
|
3604
3703
|
});
|
|
3605
|
-
|
|
3606
|
-
const
|
|
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
|
|
3610
|
-
const
|
|
3611
|
-
const
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
tags
|
|
3616
|
-
|
|
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
|
|
3620
|
-
for (const tag of
|
|
3790
|
+
for (const node of nodes) {
|
|
3791
|
+
for (const tag of node.tags) {
|
|
3621
3792
|
const list = tagToNodes.get(tag) || [];
|
|
3622
|
-
list.push(
|
|
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, {
|
|
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
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
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
|
|
3679
|
-
return c.json({
|
|
3838
|
+
const edges = [...tagEdges, ...resumedEdges];
|
|
3839
|
+
return c.json({ nodes, edges });
|
|
3680
3840
|
} catch (error) {
|
|
3681
|
-
console.error("Failed to
|
|
3682
|
-
return c.json({ error: "Failed to
|
|
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
|
-
|
|
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
|
|
3692
|
-
|
|
3693
|
-
|
|
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
|
-
|
|
3696
|
-
const
|
|
3697
|
-
|
|
3698
|
-
if (
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
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
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
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
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
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
|
|
3737
|
-
return c.json({ error: "Failed to
|
|
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
|
-
|
|
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
|
|
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
|
|
3749
|
-
const
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
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
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
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
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
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
|
-
|
|
3813
|
-
|
|
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
|
|
3834
|
-
return c.json({ error: "Failed to get
|
|
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
|
-
|
|
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
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
const
|
|
3847
|
-
|
|
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
|
|
3867
|
-
|
|
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
|
|
3977
|
-
return c.json({ error: "Failed to get
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
4073
|
+
decisions.get("/:id", async (c) => {
|
|
4038
4074
|
const id = sanitizeId(c.req.param("id"));
|
|
4039
|
-
const decisionsDir =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
});
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
if (
|
|
4088
|
-
|
|
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
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
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
|
-
|
|
4140
|
-
if (
|
|
4141
|
-
|
|
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
|
|
4144
|
-
if (
|
|
4145
|
-
|
|
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
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
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
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
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
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
for (const
|
|
4187
|
-
|
|
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
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
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
|
-
|
|
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
|
|
4207
|
-
return c.json({ error: "Failed to
|
|
4315
|
+
console.error("Failed to export session:", error);
|
|
4316
|
+
return c.json({ error: "Failed to export session" }, 500);
|
|
4208
4317
|
}
|
|
4209
4318
|
});
|
|
4210
|
-
|
|
4211
|
-
const
|
|
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
|
|
4214
|
-
|
|
4215
|
-
|
|
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
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
}
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
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
|
|
4241
|
-
return c.json({ error: "Failed to
|
|
4335
|
+
console.error("Failed to export decision:", error);
|
|
4336
|
+
return c.json({ error: "Failed to export decision" }, 500);
|
|
4242
4337
|
}
|
|
4243
4338
|
});
|
|
4244
|
-
|
|
4245
|
-
const
|
|
4246
|
-
const
|
|
4247
|
-
|
|
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
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
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
|
-
|
|
4266
|
-
|
|
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
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
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
|
|
4286
|
-
return c.json({ error: "Failed to
|
|
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
|
-
|
|
4293
|
-
const
|
|
4294
|
-
const
|
|
4295
|
-
|
|
4296
|
-
|
|
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
|
-
|
|
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 =
|
|
4609
|
+
const sessionsDir = path8.join(mnemeDir2, "sessions");
|
|
4348
4610
|
try {
|
|
4349
|
-
const
|
|
4611
|
+
const sessions2 = [];
|
|
4350
4612
|
for (const id of sessionIds || []) {
|
|
4351
4613
|
const filePath = findJsonFileById(sessionsDir, id);
|
|
4352
4614
|
if (filePath) {
|
|
4353
|
-
const content =
|
|
4354
|
-
|
|
4615
|
+
const content = fs9.readFileSync(filePath, "utf-8");
|
|
4616
|
+
sessions2.push(JSON.parse(content));
|
|
4355
4617
|
}
|
|
4356
4618
|
}
|
|
4357
|
-
if (
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
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
|
-
|
|
4424
|
-
|
|
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
|
-
|
|
4434
|
-
const
|
|
4435
|
-
const
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
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
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
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 =
|
|
4681
|
+
const content = fs10.readFileSync(filePath, "utf-8");
|
|
4594
4682
|
const data = JSON.parse(content);
|
|
4595
|
-
const
|
|
4596
|
-
const sourceName =
|
|
4597
|
-
for (const pattern of
|
|
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
|
-
|
|
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 =
|
|
4623
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
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
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
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
|
-
|
|
4681
|
-
const
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
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
|
|
4704
|
-
|
|
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
|
|
4768
|
-
|
|
4769
|
-
|
|
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
|
-
|
|
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: "
|
|
4813
|
-
action: "
|
|
4814
|
-
targetId:
|
|
4815
|
-
detail: {
|
|
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
|
|
4824
|
-
return c.json({ error: "Failed to
|
|
4796
|
+
console.error("Failed to update rules:", error);
|
|
4797
|
+
return c.json({ error: "Failed to update rules" }, 500);
|
|
4825
4798
|
}
|
|
4826
4799
|
});
|
|
4827
|
-
|
|
4828
|
-
const id =
|
|
4829
|
-
|
|
4830
|
-
|
|
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
|
-
|
|
4834
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
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
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
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
|
|
4934
|
-
for (const tag of
|
|
4932
|
+
for (const item of filteredItems) {
|
|
4933
|
+
for (const tag of item.tags || []) {
|
|
4935
4934
|
const list = tagToNodes.get(tag) || [];
|
|
4936
|
-
list.push(
|
|
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 [
|
|
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
|
|
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
|
|
4985
|
-
return c.json({ error: "Failed to build
|
|
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
|
-
|
|
4989
|
-
const
|
|
4990
|
-
|
|
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
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
c.
|
|
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
|
|
5141
|
-
return c.json({ error: "Failed to
|
|
4975
|
+
console.error("Failed to read session:", error);
|
|
4976
|
+
return c.json({ error: "Failed to read session" }, 500);
|
|
5142
4977
|
}
|
|
5143
4978
|
});
|
|
5144
|
-
|
|
4979
|
+
sessions.get("/:id/markdown", async (c) => {
|
|
5145
4980
|
const id = sanitizeId(c.req.param("id"));
|
|
5146
|
-
const
|
|
4981
|
+
const sessionsDir = path11.join(getMnemeDir(), "sessions");
|
|
5147
4982
|
try {
|
|
5148
|
-
const
|
|
5149
|
-
if (!
|
|
5150
|
-
return c.json({ error: "
|
|
4983
|
+
const jsonPath = findJsonFileById(sessionsDir, id);
|
|
4984
|
+
if (!jsonPath) {
|
|
4985
|
+
return c.json({ error: "Session not found" }, 404);
|
|
5151
4986
|
}
|
|
5152
|
-
const
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
c.
|
|
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
|
|
5161
|
-
return c.json({ error: "Failed to
|
|
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
|
-
|
|
5165
|
-
const
|
|
5166
|
-
const
|
|
5167
|
-
|
|
5168
|
-
|
|
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
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
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 (
|
|
5182
|
-
|
|
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
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
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
|
|
5191
|
-
return c.json({ error: "Failed to
|
|
5049
|
+
console.error("Failed to delete session:", error);
|
|
5050
|
+
return c.json({ error: "Failed to delete session" }, 500);
|
|
5192
5051
|
}
|
|
5193
5052
|
});
|
|
5194
|
-
|
|
5195
|
-
const
|
|
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
|
-
|
|
5198
|
-
|
|
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
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
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
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
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
|
|
5231
|
-
return c.json({ error: "Failed to
|
|
5137
|
+
console.error("Failed to delete sessions:", error);
|
|
5138
|
+
return c.json({ error: "Failed to delete sessions" }, 500);
|
|
5232
5139
|
}
|
|
5233
5140
|
});
|
|
5234
|
-
|
|
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
|
|
5238
|
-
|
|
5239
|
-
let
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
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
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
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
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
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
|
-
|
|
5270
|
-
|
|
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
|
|
5276
|
-
|
|
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 =
|
|
5280
|
-
if (
|
|
5281
|
-
const content =
|
|
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 (
|
|
5340
|
+
if (fs13.existsSync(mnemeDir)) {
|
|
5291
5341
|
try {
|
|
5292
5342
|
const sessionsIndex = readRecentSessionIndexes(mnemeDir, 1);
|
|
5293
5343
|
const decisionsIndex = readRecentDecisionIndexes(mnemeDir, 1);
|