@hir4ta/mneme 0.20.2 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -5
- package/README.ja.md +45 -283
- package/README.md +48 -280
- package/dist/lib/db.js +7 -5
- package/dist/lib/incremental-save.js +122 -28
- package/dist/lib/prompt-search.js +570 -0
- package/dist/lib/search-core.js +516 -0
- package/dist/lib/session-finalize.js +983 -0
- package/dist/lib/session-init.js +397 -0
- package/dist/lib/suppress-sqlite-warning.js +8 -0
- package/dist/public/assets/index-Bvl_IrPy.css +1 -0
- package/dist/public/assets/index-k5JYSPV6.js +351 -0
- package/dist/public/assets/{react-force-graph-2d-CGnpkwRw.js → react-force-graph-2d-Dlcfvz01.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/server.js +565 -37
- package/dist/servers/db-server.js +1301 -98
- package/dist/servers/search-server.js +613 -333
- package/hooks/hooks.json +1 -0
- package/hooks/lib/common.sh +55 -0
- package/hooks/post-tool-use.sh +52 -58
- package/hooks/pre-compact.sh +30 -42
- package/hooks/session-end.sh +30 -142
- package/hooks/session-start.sh +32 -337
- package/hooks/stop.sh +31 -42
- package/hooks/user-prompt-submit.sh +58 -212
- package/package.json +10 -3
- package/scripts/export-weekly-knowledge-html.ts +906 -0
- package/scripts/search-benchmark.queries.json +78 -0
- package/scripts/search-benchmark.ts +120 -0
- package/scripts/validate-source-artifacts.mjs +378 -0
- package/servers/db-server.ts +995 -65
- package/servers/search-server.ts +117 -528
- package/skills/harvest/SKILL.md +78 -0
- package/skills/init-mneme/{skill.md → SKILL.md} +7 -1
- package/skills/resume/{skill.md → SKILL.md} +24 -9
- package/skills/save/SKILL.md +131 -0
- package/skills/search/SKILL.md +76 -0
- package/skills/using-mneme/SKILL.md +38 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/public/assets/index-CeHiZXwl.js +0 -345
- package/dist/public/assets/index-t_srr1OD.css +0 -1
- package/learn_claude_code/figma_exports/claude_code_map.svg +0 -107
- package/learn_claude_code/figma_exports/claude_code_whiteboard.excalidraw +0 -2578
- package/skills/AGENTS.override.md +0 -5
- package/skills/harvest/skill.md +0 -295
- package/skills/plan/skill.md +0 -422
- package/skills/report/skill.md +0 -74
- package/skills/review/skill.md +0 -419
- package/skills/save/skill.md +0 -496
- package/skills/search/skill.md +0 -175
- package/skills/using-mneme/skill.md +0 -185
package/dist/server.js
CHANGED
|
@@ -45,7 +45,7 @@ var newHeadersFromIncoming = (incoming) => {
|
|
|
45
45
|
}
|
|
46
46
|
return new Headers(headerRecord);
|
|
47
47
|
};
|
|
48
|
-
var wrapBodyStream = Symbol("wrapBodyStream");
|
|
48
|
+
var wrapBodyStream = /* @__PURE__ */ Symbol("wrapBodyStream");
|
|
49
49
|
var newRequestFromIncoming = (method, url, headers, incoming, abortController) => {
|
|
50
50
|
const init = {
|
|
51
51
|
method,
|
|
@@ -93,13 +93,13 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
|
|
|
93
93
|
}
|
|
94
94
|
return new Request2(url, init);
|
|
95
95
|
};
|
|
96
|
-
var getRequestCache = Symbol("getRequestCache");
|
|
97
|
-
var requestCache = Symbol("requestCache");
|
|
98
|
-
var incomingKey = Symbol("incomingKey");
|
|
99
|
-
var urlKey = Symbol("urlKey");
|
|
100
|
-
var headersKey = Symbol("headersKey");
|
|
101
|
-
var abortControllerKey = Symbol("abortControllerKey");
|
|
102
|
-
var getAbortController = Symbol("getAbortController");
|
|
96
|
+
var getRequestCache = /* @__PURE__ */ Symbol("getRequestCache");
|
|
97
|
+
var requestCache = /* @__PURE__ */ Symbol("requestCache");
|
|
98
|
+
var incomingKey = /* @__PURE__ */ Symbol("incomingKey");
|
|
99
|
+
var urlKey = /* @__PURE__ */ Symbol("urlKey");
|
|
100
|
+
var headersKey = /* @__PURE__ */ Symbol("headersKey");
|
|
101
|
+
var abortControllerKey = /* @__PURE__ */ Symbol("abortControllerKey");
|
|
102
|
+
var getAbortController = /* @__PURE__ */ Symbol("getAbortController");
|
|
103
103
|
var requestPrototype = {
|
|
104
104
|
get method() {
|
|
105
105
|
return this[incomingKey].method || "GET";
|
|
@@ -190,9 +190,9 @@ var newRequest = (incoming, defaultHostname) => {
|
|
|
190
190
|
req[urlKey] = url.href;
|
|
191
191
|
return req;
|
|
192
192
|
};
|
|
193
|
-
var responseCache = Symbol("responseCache");
|
|
194
|
-
var getResponseCache = Symbol("getResponseCache");
|
|
195
|
-
var cacheKey = Symbol("cache");
|
|
193
|
+
var responseCache = /* @__PURE__ */ Symbol("responseCache");
|
|
194
|
+
var getResponseCache = /* @__PURE__ */ Symbol("getResponseCache");
|
|
195
|
+
var cacheKey = /* @__PURE__ */ Symbol("cache");
|
|
196
196
|
var GlobalResponse = global.Response;
|
|
197
197
|
var Response2 = class _Response {
|
|
198
198
|
#body;
|
|
@@ -324,7 +324,7 @@ var X_ALREADY_SENT = "x-hono-already-sent";
|
|
|
324
324
|
if (typeof global.crypto === "undefined") {
|
|
325
325
|
global.crypto = crypto;
|
|
326
326
|
}
|
|
327
|
-
var outgoingEnded = Symbol("outgoingEnded");
|
|
327
|
+
var outgoingEnded = /* @__PURE__ */ Symbol("outgoingEnded");
|
|
328
328
|
var handleRequestError = () => new Response(null, {
|
|
329
329
|
status: 400
|
|
330
330
|
});
|
|
@@ -953,9 +953,11 @@ var getPath = (request) => {
|
|
|
953
953
|
const charCode = url.charCodeAt(i);
|
|
954
954
|
if (charCode === 37) {
|
|
955
955
|
const queryIndex = url.indexOf("?", i);
|
|
956
|
-
const
|
|
956
|
+
const hashIndex = url.indexOf("#", i);
|
|
957
|
+
const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
|
|
958
|
+
const path4 = url.slice(start, end);
|
|
957
959
|
return tryDecodeURI(path4.includes("%25") ? path4.replace(/%25/g, "%2525") : path4);
|
|
958
|
-
} else if (charCode === 63) {
|
|
960
|
+
} else if (charCode === 63 || charCode === 35) {
|
|
959
961
|
break;
|
|
960
962
|
}
|
|
961
963
|
}
|
|
@@ -2182,7 +2184,7 @@ var Hono = class _Hono {
|
|
|
2182
2184
|
var emptyParam = [];
|
|
2183
2185
|
function match(method, path4) {
|
|
2184
2186
|
const matchers = this.buildAllMatchers();
|
|
2185
|
-
const match2 = (method2, path22) => {
|
|
2187
|
+
const match2 = ((method2, path22) => {
|
|
2186
2188
|
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
|
2187
2189
|
const staticMatch = matcher[2][path22];
|
|
2188
2190
|
if (staticMatch) {
|
|
@@ -2194,7 +2196,7 @@ function match(method, path4) {
|
|
|
2194
2196
|
}
|
|
2195
2197
|
const index = match3.indexOf("", 1);
|
|
2196
2198
|
return [matcher[1][index], match3];
|
|
2197
|
-
};
|
|
2199
|
+
});
|
|
2198
2200
|
this.match = match2;
|
|
2199
2201
|
return match2(method, path4);
|
|
2200
2202
|
}
|
|
@@ -2877,11 +2879,7 @@ var cors = (options) => {
|
|
|
2877
2879
|
};
|
|
2878
2880
|
};
|
|
2879
2881
|
|
|
2880
|
-
// lib/
|
|
2881
|
-
import { execSync } from "node:child_process";
|
|
2882
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync } from "node:fs";
|
|
2883
|
-
import { dirname, join as join2 } from "node:path";
|
|
2884
|
-
import { fileURLToPath } from "node:url";
|
|
2882
|
+
// lib/suppress-sqlite-warning.ts
|
|
2885
2883
|
var originalEmit = process.emit;
|
|
2886
2884
|
process.emit = (event, ...args) => {
|
|
2887
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")) {
|
|
@@ -2889,6 +2887,12 @@ process.emit = (event, ...args) => {
|
|
|
2889
2887
|
}
|
|
2890
2888
|
return originalEmit.apply(process, [event, ...args]);
|
|
2891
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";
|
|
2892
2896
|
var { DatabaseSync } = await import("node:sqlite");
|
|
2893
2897
|
var __filename = fileURLToPath(import.meta.url);
|
|
2894
2898
|
var __dirname = dirname(__filename);
|
|
@@ -3331,7 +3335,12 @@ function isIndexStale(index, maxAgeMs = 5 * 60 * 1e3) {
|
|
|
3331
3335
|
|
|
3332
3336
|
// dashboard/server/index.ts
|
|
3333
3337
|
function sanitizeId(id) {
|
|
3334
|
-
|
|
3338
|
+
const normalized = decodeURIComponent(id).trim();
|
|
3339
|
+
if (!normalized) return "";
|
|
3340
|
+
if (normalized.includes("..") || normalized.includes("/") || normalized.includes("\\")) {
|
|
3341
|
+
return "";
|
|
3342
|
+
}
|
|
3343
|
+
return /^[a-zA-Z0-9:_-]+$/.test(normalized) ? normalized : "";
|
|
3335
3344
|
}
|
|
3336
3345
|
function safeParseJsonFile(filePath) {
|
|
3337
3346
|
try {
|
|
@@ -3348,6 +3357,7 @@ var getProjectRoot = () => {
|
|
|
3348
3357
|
var getMnemeDir = () => {
|
|
3349
3358
|
return path3.join(getProjectRoot(), ".mneme");
|
|
3350
3359
|
};
|
|
3360
|
+
var ALLOWED_RULE_FILES = /* @__PURE__ */ new Set(["dev-rules", "review-guidelines"]);
|
|
3351
3361
|
var listJsonFiles = (dir) => {
|
|
3352
3362
|
if (!fs4.existsSync(dir)) {
|
|
3353
3363
|
return [];
|
|
@@ -3364,6 +3374,61 @@ var listJsonFiles = (dir) => {
|
|
|
3364
3374
|
return [];
|
|
3365
3375
|
});
|
|
3366
3376
|
};
|
|
3377
|
+
function writeAuditLog(entry) {
|
|
3378
|
+
try {
|
|
3379
|
+
const now = /* @__PURE__ */ new Date();
|
|
3380
|
+
const auditDir = path3.join(getMnemeDir(), "audit");
|
|
3381
|
+
fs4.mkdirSync(auditDir, { recursive: true });
|
|
3382
|
+
const auditFile = path3.join(
|
|
3383
|
+
auditDir,
|
|
3384
|
+
`${now.toISOString().slice(0, 10)}.jsonl`
|
|
3385
|
+
);
|
|
3386
|
+
const payload = {
|
|
3387
|
+
timestamp: now.toISOString(),
|
|
3388
|
+
actor: getCurrentUser(),
|
|
3389
|
+
...entry
|
|
3390
|
+
};
|
|
3391
|
+
fs4.appendFileSync(auditFile, `${JSON.stringify(payload)}
|
|
3392
|
+
`);
|
|
3393
|
+
} catch (error) {
|
|
3394
|
+
console.error("Failed to write audit log:", error);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
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
|
+
}
|
|
3367
3432
|
var listDatedJsonFiles = (dir) => {
|
|
3368
3433
|
const files = listJsonFiles(dir);
|
|
3369
3434
|
return files.filter((filePath) => {
|
|
@@ -3550,23 +3615,30 @@ app.get("/api/sessions/graph", async (c) => {
|
|
|
3550
3615
|
tags: session.tags || [],
|
|
3551
3616
|
createdAt: session.createdAt
|
|
3552
3617
|
}));
|
|
3553
|
-
const
|
|
3554
|
-
for (
|
|
3555
|
-
for (
|
|
3556
|
-
const
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3618
|
+
const tagToNodes = /* @__PURE__ */ new Map();
|
|
3619
|
+
for (const item of filteredItems) {
|
|
3620
|
+
for (const tag of item.tags || []) {
|
|
3621
|
+
const list = tagToNodes.get(tag) || [];
|
|
3622
|
+
list.push(item.id);
|
|
3623
|
+
tagToNodes.set(tag, list);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
3627
|
+
for (const [, nodeIds] of tagToNodes) {
|
|
3628
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
3629
|
+
for (let j = i + 1; j < nodeIds.length; j++) {
|
|
3630
|
+
const key = nodeIds[i] < nodeIds[j] ? `${nodeIds[i]}|${nodeIds[j]}` : `${nodeIds[j]}|${nodeIds[i]}`;
|
|
3631
|
+
const existing = edgeMap.get(key);
|
|
3632
|
+
if (existing) {
|
|
3633
|
+
existing.weight++;
|
|
3634
|
+
} else {
|
|
3635
|
+
const [source, target] = key.split("|");
|
|
3636
|
+
edgeMap.set(key, { source, target, weight: 1 });
|
|
3637
|
+
}
|
|
3567
3638
|
}
|
|
3568
3639
|
}
|
|
3569
3640
|
}
|
|
3641
|
+
const edges = Array.from(edgeMap.values());
|
|
3570
3642
|
return c.json({ nodes, edges });
|
|
3571
3643
|
} catch (error) {
|
|
3572
3644
|
console.error("Failed to build session graph:", error);
|
|
@@ -3648,6 +3720,11 @@ app.delete("/api/sessions/:id", async (c) => {
|
|
|
3648
3720
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
3649
3721
|
rebuildSessionIndexForMonth(mnemeDir2, year, month);
|
|
3650
3722
|
}
|
|
3723
|
+
writeAuditLog({
|
|
3724
|
+
entity: "session",
|
|
3725
|
+
action: "delete",
|
|
3726
|
+
targetId: id
|
|
3727
|
+
});
|
|
3651
3728
|
}
|
|
3652
3729
|
return c.json({
|
|
3653
3730
|
deleted: dryRun ? 0 : 1,
|
|
@@ -3725,6 +3802,11 @@ app.delete("/api/sessions", async (c) => {
|
|
|
3725
3802
|
if (fs4.existsSync(linkPath)) {
|
|
3726
3803
|
fs4.unlinkSync(linkPath);
|
|
3727
3804
|
}
|
|
3805
|
+
writeAuditLog({
|
|
3806
|
+
entity: "session",
|
|
3807
|
+
action: "delete",
|
|
3808
|
+
targetId: session.id
|
|
3809
|
+
});
|
|
3728
3810
|
}
|
|
3729
3811
|
}
|
|
3730
3812
|
return c.json({
|
|
@@ -3970,6 +4052,27 @@ app.get("/api/decisions/:id", async (c) => {
|
|
|
3970
4052
|
return c.json({ error: "Failed to read decision" }, 500);
|
|
3971
4053
|
}
|
|
3972
4054
|
});
|
|
4055
|
+
app.delete("/api/decisions/:id", async (c) => {
|
|
4056
|
+
const id = sanitizeId(c.req.param("id"));
|
|
4057
|
+
const decisionsDir = path3.join(getMnemeDir(), "decisions");
|
|
4058
|
+
try {
|
|
4059
|
+
const filePath = findJsonFileById(decisionsDir, id);
|
|
4060
|
+
if (!filePath) {
|
|
4061
|
+
return c.json({ error: "Decision not found" }, 404);
|
|
4062
|
+
}
|
|
4063
|
+
fs4.unlinkSync(filePath);
|
|
4064
|
+
rebuildAllDecisionIndexes(getMnemeDir());
|
|
4065
|
+
writeAuditLog({
|
|
4066
|
+
entity: "decision",
|
|
4067
|
+
action: "delete",
|
|
4068
|
+
targetId: id
|
|
4069
|
+
});
|
|
4070
|
+
return c.json({ deleted: 1, id });
|
|
4071
|
+
} catch (error) {
|
|
4072
|
+
console.error("Failed to delete decision:", error);
|
|
4073
|
+
return c.json({ error: "Failed to delete decision" }, 500);
|
|
4074
|
+
}
|
|
4075
|
+
});
|
|
3973
4076
|
app.get("/api/info", async (c) => {
|
|
3974
4077
|
const projectRoot = getProjectRoot();
|
|
3975
4078
|
const mnemeDir2 = getMnemeDir();
|
|
@@ -3981,6 +4084,9 @@ app.get("/api/info", async (c) => {
|
|
|
3981
4084
|
});
|
|
3982
4085
|
app.get("/api/rules/:id", async (c) => {
|
|
3983
4086
|
const id = c.req.param("id");
|
|
4087
|
+
if (!ALLOWED_RULE_FILES.has(id)) {
|
|
4088
|
+
return c.json({ error: "Invalid rule type" }, 400);
|
|
4089
|
+
}
|
|
3984
4090
|
const dir = rulesDir();
|
|
3985
4091
|
try {
|
|
3986
4092
|
const filePath = path3.join(dir, `${id}.json`);
|
|
@@ -3999,7 +4105,7 @@ app.get("/api/rules/:id", async (c) => {
|
|
|
3999
4105
|
});
|
|
4000
4106
|
app.put("/api/rules/:id", async (c) => {
|
|
4001
4107
|
const id = c.req.param("id");
|
|
4002
|
-
if (id
|
|
4108
|
+
if (!ALLOWED_RULE_FILES.has(id)) {
|
|
4003
4109
|
return c.json({ error: "Invalid rule type" }, 400);
|
|
4004
4110
|
}
|
|
4005
4111
|
const dir = rulesDir();
|
|
@@ -4013,12 +4119,58 @@ app.put("/api/rules/:id", async (c) => {
|
|
|
4013
4119
|
return c.json({ error: "Invalid rules format" }, 400);
|
|
4014
4120
|
}
|
|
4015
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
|
+
});
|
|
4016
4128
|
return c.json(body);
|
|
4017
4129
|
} catch (error) {
|
|
4018
4130
|
console.error("Failed to update rules:", error);
|
|
4019
4131
|
return c.json({ error: "Failed to update rules" }, 500);
|
|
4020
4132
|
}
|
|
4021
4133
|
});
|
|
4134
|
+
app.delete("/api/rules/:id/:ruleId", async (c) => {
|
|
4135
|
+
const id = c.req.param("id");
|
|
4136
|
+
if (!ALLOWED_RULE_FILES.has(id)) {
|
|
4137
|
+
return c.json({ error: "Invalid rule type" }, 400);
|
|
4138
|
+
}
|
|
4139
|
+
const ruleId = sanitizeId(c.req.param("ruleId"));
|
|
4140
|
+
if (!ruleId) {
|
|
4141
|
+
return c.json({ error: "Invalid rule id" }, 400);
|
|
4142
|
+
}
|
|
4143
|
+
const filePath = path3.join(rulesDir(), `${id}.json`);
|
|
4144
|
+
if (!fs4.existsSync(filePath)) {
|
|
4145
|
+
return c.json({ error: "Rules not found" }, 404);
|
|
4146
|
+
}
|
|
4147
|
+
try {
|
|
4148
|
+
const doc = safeParseJsonFile(filePath);
|
|
4149
|
+
if (!doc || !Array.isArray(doc.items)) {
|
|
4150
|
+
return c.json({ error: "Invalid rules format" }, 500);
|
|
4151
|
+
}
|
|
4152
|
+
const nextItems = doc.items.filter((item) => item.id !== ruleId);
|
|
4153
|
+
if (nextItems.length === doc.items.length) {
|
|
4154
|
+
return c.json({ error: "Rule not found" }, 404);
|
|
4155
|
+
}
|
|
4156
|
+
const nextDoc = {
|
|
4157
|
+
...doc,
|
|
4158
|
+
items: nextItems,
|
|
4159
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4160
|
+
};
|
|
4161
|
+
fs4.writeFileSync(filePath, JSON.stringify(nextDoc, null, 2));
|
|
4162
|
+
writeAuditLog({
|
|
4163
|
+
entity: "rule",
|
|
4164
|
+
action: "delete",
|
|
4165
|
+
targetId: ruleId,
|
|
4166
|
+
detail: { ruleType: id }
|
|
4167
|
+
});
|
|
4168
|
+
return c.json({ deleted: 1, id: ruleId, ruleType: id });
|
|
4169
|
+
} catch (error) {
|
|
4170
|
+
console.error("Failed to delete rule:", error);
|
|
4171
|
+
return c.json({ error: "Failed to delete rule" }, 500);
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4022
4174
|
app.get("/api/timeline", async (c) => {
|
|
4023
4175
|
const sessionsDir = path3.join(getMnemeDir(), "sessions");
|
|
4024
4176
|
try {
|
|
@@ -4457,6 +4609,382 @@ app.get("/api/patterns/stats", async (c) => {
|
|
|
4457
4609
|
return c.json({ error: "Failed to get pattern stats" }, 500);
|
|
4458
4610
|
}
|
|
4459
4611
|
});
|
|
4612
|
+
app.delete("/api/patterns/:id", async (c) => {
|
|
4613
|
+
const id = sanitizeId(c.req.param("id"));
|
|
4614
|
+
const sourceFile = c.req.query("source");
|
|
4615
|
+
if (!id) {
|
|
4616
|
+
return c.json({ error: "Invalid pattern id" }, 400);
|
|
4617
|
+
}
|
|
4618
|
+
if (!sourceFile) {
|
|
4619
|
+
return c.json({ error: "Missing source file" }, 400);
|
|
4620
|
+
}
|
|
4621
|
+
const safeSource = sourceFile.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
4622
|
+
const filePath = path3.join(patternsDir(), `${safeSource}.json`);
|
|
4623
|
+
if (!fs4.existsSync(filePath)) {
|
|
4624
|
+
return c.json({ error: "Pattern source file not found" }, 404);
|
|
4625
|
+
}
|
|
4626
|
+
try {
|
|
4627
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
4628
|
+
const data = JSON.parse(content);
|
|
4629
|
+
let deleted = 0;
|
|
4630
|
+
if (Array.isArray(data.items)) {
|
|
4631
|
+
const nextItems = data.items.filter((item) => item.id !== id);
|
|
4632
|
+
deleted = data.items.length - nextItems.length;
|
|
4633
|
+
data.items = nextItems;
|
|
4634
|
+
} else if (Array.isArray(data.patterns)) {
|
|
4635
|
+
const nextPatterns = data.patterns.filter((item) => item.id !== id);
|
|
4636
|
+
deleted = data.patterns.length - nextPatterns.length;
|
|
4637
|
+
data.patterns = nextPatterns;
|
|
4638
|
+
} else {
|
|
4639
|
+
return c.json({ error: "Invalid pattern file format" }, 500);
|
|
4640
|
+
}
|
|
4641
|
+
if (deleted === 0) {
|
|
4642
|
+
return c.json({ error: "Pattern not found" }, 404);
|
|
4643
|
+
}
|
|
4644
|
+
fs4.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
4645
|
+
writeAuditLog({
|
|
4646
|
+
entity: "pattern",
|
|
4647
|
+
action: "delete",
|
|
4648
|
+
targetId: id,
|
|
4649
|
+
detail: { sourceFile: safeSource }
|
|
4650
|
+
});
|
|
4651
|
+
return c.json({ deleted, id, sourceFile: safeSource });
|
|
4652
|
+
} catch (error) {
|
|
4653
|
+
console.error("Failed to delete pattern:", error);
|
|
4654
|
+
return c.json({ error: "Failed to delete pattern" }, 500);
|
|
4655
|
+
}
|
|
4656
|
+
});
|
|
4657
|
+
app.get("/api/units", async (c) => {
|
|
4658
|
+
const status = c.req.query("status");
|
|
4659
|
+
const doc = readUnits();
|
|
4660
|
+
const items = status && ["pending", "approved", "rejected"].includes(status) ? doc.items.filter((item) => item.status === status) : doc.items;
|
|
4661
|
+
return c.json({
|
|
4662
|
+
...doc,
|
|
4663
|
+
items: items.sort(
|
|
4664
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
4665
|
+
)
|
|
4666
|
+
});
|
|
4667
|
+
});
|
|
4668
|
+
app.get("/api/units/:id", async (c) => {
|
|
4669
|
+
const id = sanitizeId(c.req.param("id"));
|
|
4670
|
+
if (!id) {
|
|
4671
|
+
return c.json({ error: "Invalid unit id" }, 400);
|
|
4672
|
+
}
|
|
4673
|
+
const doc = readUnits();
|
|
4674
|
+
const item = doc.items.find((unit) => unit.id === id);
|
|
4675
|
+
if (!item) {
|
|
4676
|
+
return c.json({ error: "Unit not found" }, 404);
|
|
4677
|
+
}
|
|
4678
|
+
return c.json(item);
|
|
4679
|
+
});
|
|
4680
|
+
app.get("/api/approval-queue", async (c) => {
|
|
4681
|
+
const doc = readUnits();
|
|
4682
|
+
const pending = doc.items.filter((item) => item.status === "pending");
|
|
4683
|
+
return c.json({
|
|
4684
|
+
pending,
|
|
4685
|
+
totalPending: pending.length,
|
|
4686
|
+
byType: pending.reduce(
|
|
4687
|
+
(acc, item) => {
|
|
4688
|
+
acc[item.type] = (acc[item.type] || 0) + 1;
|
|
4689
|
+
return acc;
|
|
4690
|
+
},
|
|
4691
|
+
{}
|
|
4692
|
+
)
|
|
4693
|
+
});
|
|
4694
|
+
});
|
|
4695
|
+
app.post("/api/units/generate", async (c) => {
|
|
4696
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4697
|
+
const existing = readUnits();
|
|
4698
|
+
const bySourceKey = new Map(
|
|
4699
|
+
existing.items.map((item) => [`${item.sourceType}:${item.sourceId}`, item])
|
|
4700
|
+
);
|
|
4701
|
+
const generated = [];
|
|
4702
|
+
try {
|
|
4703
|
+
const decisionFiles = listDatedJsonFiles(
|
|
4704
|
+
path3.join(getMnemeDir(), "decisions")
|
|
4705
|
+
);
|
|
4706
|
+
for (const filePath of decisionFiles) {
|
|
4707
|
+
const decision = safeParseJsonFile(filePath);
|
|
4708
|
+
if (!decision) continue;
|
|
4709
|
+
const sourceId = String(decision.id || "");
|
|
4710
|
+
if (!sourceId) continue;
|
|
4711
|
+
const sourceType = "decision";
|
|
4712
|
+
const key = `${sourceType}:${sourceId}`;
|
|
4713
|
+
const previous = bySourceKey.get(key);
|
|
4714
|
+
generated.push({
|
|
4715
|
+
id: previous?.id || makeUnitId(sourceType, sourceId),
|
|
4716
|
+
type: "decision",
|
|
4717
|
+
kind: "policy",
|
|
4718
|
+
title: String(decision.title || sourceId),
|
|
4719
|
+
summary: String(
|
|
4720
|
+
decision.decision || decision.reasoning || decision.title || ""
|
|
4721
|
+
),
|
|
4722
|
+
tags: Array.isArray(decision.tags) ? decision.tags.map((tag) => String(tag)) : [],
|
|
4723
|
+
sourceId,
|
|
4724
|
+
sourceType,
|
|
4725
|
+
sourceRefs: [{ type: sourceType, id: sourceId }],
|
|
4726
|
+
status: previous?.status || "pending",
|
|
4727
|
+
createdAt: previous?.createdAt || now,
|
|
4728
|
+
updatedAt: now,
|
|
4729
|
+
reviewedAt: previous?.reviewedAt,
|
|
4730
|
+
reviewedBy: previous?.reviewedBy
|
|
4731
|
+
});
|
|
4732
|
+
}
|
|
4733
|
+
const ruleFiles = ["dev-rules", "review-guidelines"];
|
|
4734
|
+
for (const ruleFile of ruleFiles) {
|
|
4735
|
+
const filePath = path3.join(rulesDir(), `${ruleFile}.json`);
|
|
4736
|
+
const doc = safeParseJsonFile(
|
|
4737
|
+
filePath
|
|
4738
|
+
);
|
|
4739
|
+
if (!doc || !Array.isArray(doc.items)) continue;
|
|
4740
|
+
for (const rule of doc.items) {
|
|
4741
|
+
const ruleId = String(rule.id || "");
|
|
4742
|
+
if (!ruleId) continue;
|
|
4743
|
+
const sourceType = "rule";
|
|
4744
|
+
const sourceId = `${ruleFile}:${ruleId}`;
|
|
4745
|
+
const key = `${sourceType}:${sourceId}`;
|
|
4746
|
+
const previous = bySourceKey.get(key);
|
|
4747
|
+
const title = String(rule.text || rule.title || rule.rule || ruleId) || ruleId;
|
|
4748
|
+
const summary = String(rule.rationale || rule.description || "") || title;
|
|
4749
|
+
generated.push({
|
|
4750
|
+
id: previous?.id || makeUnitId(sourceType, sourceId),
|
|
4751
|
+
type: "rule",
|
|
4752
|
+
kind: "policy",
|
|
4753
|
+
title,
|
|
4754
|
+
summary,
|
|
4755
|
+
tags: Array.isArray(rule.tags) ? rule.tags.map((tag) => String(tag)) : [ruleFile],
|
|
4756
|
+
sourceId,
|
|
4757
|
+
sourceType,
|
|
4758
|
+
sourceRefs: [{ type: sourceType, id: sourceId }],
|
|
4759
|
+
status: previous?.status || "pending",
|
|
4760
|
+
createdAt: previous?.createdAt || now,
|
|
4761
|
+
updatedAt: now,
|
|
4762
|
+
reviewedAt: previous?.reviewedAt,
|
|
4763
|
+
reviewedBy: previous?.reviewedBy
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
const patternFiles = listJsonFiles(patternsDir());
|
|
4768
|
+
for (const patternFile of patternFiles) {
|
|
4769
|
+
const sourceName = path3.basename(patternFile, ".json");
|
|
4770
|
+
const doc = safeParseJsonFile(patternFile);
|
|
4771
|
+
const items = doc?.items || doc?.patterns || [];
|
|
4772
|
+
for (const pattern of items) {
|
|
4773
|
+
const patternId = String(pattern.id || "");
|
|
4774
|
+
if (!patternId) continue;
|
|
4775
|
+
const sourceType = "pattern";
|
|
4776
|
+
const sourceId = `${sourceName}:${patternId}`;
|
|
4777
|
+
const key = `${sourceType}:${sourceId}`;
|
|
4778
|
+
const previous = bySourceKey.get(key);
|
|
4779
|
+
const title = String(
|
|
4780
|
+
pattern.title || pattern.errorPattern || pattern.description || patternId
|
|
4781
|
+
);
|
|
4782
|
+
const summary = String(
|
|
4783
|
+
pattern.solution || pattern.description || pattern.errorPattern || ""
|
|
4784
|
+
);
|
|
4785
|
+
generated.push({
|
|
4786
|
+
id: previous?.id || makeUnitId(sourceType, sourceId),
|
|
4787
|
+
type: "pattern",
|
|
4788
|
+
kind: getPatternKind(String(pattern.type || "")),
|
|
4789
|
+
title,
|
|
4790
|
+
summary,
|
|
4791
|
+
tags: Array.isArray(pattern.tags) ? pattern.tags.map((tag) => String(tag)) : [sourceName],
|
|
4792
|
+
sourceId,
|
|
4793
|
+
sourceType,
|
|
4794
|
+
sourceRefs: [{ type: sourceType, id: sourceId }],
|
|
4795
|
+
status: previous?.status || "pending",
|
|
4796
|
+
createdAt: previous?.createdAt || now,
|
|
4797
|
+
updatedAt: now,
|
|
4798
|
+
reviewedAt: previous?.reviewedAt,
|
|
4799
|
+
reviewedBy: previous?.reviewedBy
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
const next = {
|
|
4804
|
+
schemaVersion: 1,
|
|
4805
|
+
updatedAt: now,
|
|
4806
|
+
items: generated.sort(
|
|
4807
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
4808
|
+
)
|
|
4809
|
+
};
|
|
4810
|
+
writeUnits(next);
|
|
4811
|
+
writeAuditLog({
|
|
4812
|
+
entity: "unit",
|
|
4813
|
+
action: "create",
|
|
4814
|
+
targetId: "units",
|
|
4815
|
+
detail: { count: generated.length }
|
|
4816
|
+
});
|
|
4817
|
+
return c.json({
|
|
4818
|
+
generated: generated.length,
|
|
4819
|
+
pending: generated.filter((item) => item.status === "pending").length,
|
|
4820
|
+
updatedAt: now
|
|
4821
|
+
});
|
|
4822
|
+
} catch (error) {
|
|
4823
|
+
console.error("Failed to generate units:", error);
|
|
4824
|
+
return c.json({ error: "Failed to generate units" }, 500);
|
|
4825
|
+
}
|
|
4826
|
+
});
|
|
4827
|
+
app.patch("/api/units/:id/status", async (c) => {
|
|
4828
|
+
const id = sanitizeId(c.req.param("id"));
|
|
4829
|
+
const body = await c.req.json();
|
|
4830
|
+
if (!id) {
|
|
4831
|
+
return c.json({ error: "Invalid unit id" }, 400);
|
|
4832
|
+
}
|
|
4833
|
+
if (!body.status || !["pending", "approved", "rejected"].includes(body.status)) {
|
|
4834
|
+
return c.json({ error: "Invalid status" }, 400);
|
|
4835
|
+
}
|
|
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
|
+
});
|
|
4881
|
+
app.get("/api/knowledge-graph", async (c) => {
|
|
4882
|
+
try {
|
|
4883
|
+
const mnemeDir2 = getMnemeDir();
|
|
4884
|
+
const sessionItems = readAllSessionIndexes(mnemeDir2).items;
|
|
4885
|
+
const units = readUnits().items.filter(
|
|
4886
|
+
(item) => item.status === "approved"
|
|
4887
|
+
);
|
|
4888
|
+
const sessionDataMap = /* @__PURE__ */ new Map();
|
|
4889
|
+
for (const item of sessionItems.filter((i) => i.hasSummary)) {
|
|
4890
|
+
try {
|
|
4891
|
+
const sessionPath = path3.join(mnemeDir2, item.filePath);
|
|
4892
|
+
const raw2 = fs4.readFileSync(sessionPath, "utf-8");
|
|
4893
|
+
const session = JSON.parse(raw2);
|
|
4894
|
+
if (session.resumedFrom) {
|
|
4895
|
+
sessionDataMap.set(item.id, {
|
|
4896
|
+
resumedFrom: session.resumedFrom
|
|
4897
|
+
});
|
|
4898
|
+
}
|
|
4899
|
+
} catch {
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
const nodes = [
|
|
4903
|
+
...sessionItems.filter((item) => item.hasSummary).map((item) => ({
|
|
4904
|
+
id: `session:${item.id}`,
|
|
4905
|
+
entityType: "session",
|
|
4906
|
+
entityId: item.id,
|
|
4907
|
+
title: item.title,
|
|
4908
|
+
tags: item.tags || [],
|
|
4909
|
+
createdAt: item.createdAt,
|
|
4910
|
+
branch: item.branch || null,
|
|
4911
|
+
resumedFrom: sessionDataMap.get(item.id)?.resumedFrom || null,
|
|
4912
|
+
unitSubtype: null,
|
|
4913
|
+
sourceId: null,
|
|
4914
|
+
appliedCount: null,
|
|
4915
|
+
acceptedCount: null
|
|
4916
|
+
})),
|
|
4917
|
+
...units.map((item) => ({
|
|
4918
|
+
id: `unit:${item.id}`,
|
|
4919
|
+
entityType: "unit",
|
|
4920
|
+
entityId: item.id,
|
|
4921
|
+
title: item.title,
|
|
4922
|
+
tags: item.tags || [],
|
|
4923
|
+
createdAt: item.createdAt,
|
|
4924
|
+
unitSubtype: item.type || null,
|
|
4925
|
+
sourceId: item.sourceId || null,
|
|
4926
|
+
appliedCount: null,
|
|
4927
|
+
acceptedCount: null,
|
|
4928
|
+
branch: null,
|
|
4929
|
+
resumedFrom: null
|
|
4930
|
+
}))
|
|
4931
|
+
];
|
|
4932
|
+
const tagToNodes = /* @__PURE__ */ new Map();
|
|
4933
|
+
for (const node of nodes) {
|
|
4934
|
+
for (const tag of node.tags) {
|
|
4935
|
+
const list = tagToNodes.get(tag) || [];
|
|
4936
|
+
list.push(node.id);
|
|
4937
|
+
tagToNodes.set(tag, list);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
4941
|
+
for (const [tag, nodeIds] of tagToNodes) {
|
|
4942
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
4943
|
+
for (let j = i + 1; j < nodeIds.length; j++) {
|
|
4944
|
+
const key = nodeIds[i] < nodeIds[j] ? `${nodeIds[i]}|${nodeIds[j]}` : `${nodeIds[j]}|${nodeIds[i]}`;
|
|
4945
|
+
const existing = edgeMap.get(key);
|
|
4946
|
+
if (existing) {
|
|
4947
|
+
existing.weight++;
|
|
4948
|
+
existing.sharedTags.push(tag);
|
|
4949
|
+
} else {
|
|
4950
|
+
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
|
+
});
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
const tagEdges = Array.from(edgeMap.values());
|
|
4964
|
+
const nodeIdSet = new Set(nodes.map((n) => n.id));
|
|
4965
|
+
const resumedEdges = [];
|
|
4966
|
+
for (const node of nodes) {
|
|
4967
|
+
if (node.entityType === "session" && node.resumedFrom) {
|
|
4968
|
+
const targetId = `session:${node.resumedFrom}`;
|
|
4969
|
+
if (nodeIdSet.has(targetId)) {
|
|
4970
|
+
resumedEdges.push({
|
|
4971
|
+
source: targetId,
|
|
4972
|
+
target: node.id,
|
|
4973
|
+
weight: 1,
|
|
4974
|
+
sharedTags: [],
|
|
4975
|
+
edgeType: "resumedFrom",
|
|
4976
|
+
directed: true
|
|
4977
|
+
});
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
const edges = [...tagEdges, ...resumedEdges];
|
|
4982
|
+
return c.json({ nodes, edges });
|
|
4983
|
+
} catch (error) {
|
|
4984
|
+
console.error("Failed to build knowledge graph:", error);
|
|
4985
|
+
return c.json({ error: "Failed to build knowledge graph" }, 500);
|
|
4986
|
+
}
|
|
4987
|
+
});
|
|
4460
4988
|
function sessionToMarkdown(session) {
|
|
4461
4989
|
const lines = [];
|
|
4462
4990
|
lines.push(`# ${session.title || "Untitled Session"}`);
|