@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.
Files changed (51) hide show
  1. package/.claude-plugin/plugin.json +2 -5
  2. package/README.ja.md +45 -283
  3. package/README.md +48 -280
  4. package/dist/lib/db.js +7 -5
  5. package/dist/lib/incremental-save.js +122 -28
  6. package/dist/lib/prompt-search.js +570 -0
  7. package/dist/lib/search-core.js +516 -0
  8. package/dist/lib/session-finalize.js +983 -0
  9. package/dist/lib/session-init.js +397 -0
  10. package/dist/lib/suppress-sqlite-warning.js +8 -0
  11. package/dist/public/assets/index-Bvl_IrPy.css +1 -0
  12. package/dist/public/assets/index-k5JYSPV6.js +351 -0
  13. package/dist/public/assets/{react-force-graph-2d-CGnpkwRw.js → react-force-graph-2d-Dlcfvz01.js} +1 -1
  14. package/dist/public/index.html +2 -2
  15. package/dist/server.js +565 -37
  16. package/dist/servers/db-server.js +1301 -98
  17. package/dist/servers/search-server.js +613 -333
  18. package/hooks/hooks.json +1 -0
  19. package/hooks/lib/common.sh +55 -0
  20. package/hooks/post-tool-use.sh +52 -58
  21. package/hooks/pre-compact.sh +30 -42
  22. package/hooks/session-end.sh +30 -142
  23. package/hooks/session-start.sh +32 -337
  24. package/hooks/stop.sh +31 -42
  25. package/hooks/user-prompt-submit.sh +58 -212
  26. package/package.json +10 -3
  27. package/scripts/export-weekly-knowledge-html.ts +906 -0
  28. package/scripts/search-benchmark.queries.json +78 -0
  29. package/scripts/search-benchmark.ts +120 -0
  30. package/scripts/validate-source-artifacts.mjs +378 -0
  31. package/servers/db-server.ts +995 -65
  32. package/servers/search-server.ts +117 -528
  33. package/skills/harvest/SKILL.md +78 -0
  34. package/skills/init-mneme/{skill.md → SKILL.md} +7 -1
  35. package/skills/resume/{skill.md → SKILL.md} +24 -9
  36. package/skills/save/SKILL.md +131 -0
  37. package/skills/search/SKILL.md +76 -0
  38. package/skills/using-mneme/SKILL.md +38 -0
  39. package/tsconfig.tsbuildinfo +1 -0
  40. package/dist/public/assets/index-CeHiZXwl.js +0 -345
  41. package/dist/public/assets/index-t_srr1OD.css +0 -1
  42. package/learn_claude_code/figma_exports/claude_code_map.svg +0 -107
  43. package/learn_claude_code/figma_exports/claude_code_whiteboard.excalidraw +0 -2578
  44. package/skills/AGENTS.override.md +0 -5
  45. package/skills/harvest/skill.md +0 -295
  46. package/skills/plan/skill.md +0 -422
  47. package/skills/report/skill.md +0 -74
  48. package/skills/review/skill.md +0 -419
  49. package/skills/save/skill.md +0 -496
  50. package/skills/search/skill.md +0 -175
  51. 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 path4 = url.slice(start, queryIndex === -1 ? void 0 : queryIndex);
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/db.ts
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
- return id.replace(/[^a-zA-Z0-9_-]/g, "");
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 edges = [];
3554
- for (let i = 0; i < filteredItems.length; i++) {
3555
- for (let j = i + 1; j < filteredItems.length; j++) {
3556
- const s1 = filteredItems[i];
3557
- const s2 = filteredItems[j];
3558
- const sharedTags = (s1.tags || []).filter(
3559
- (t) => (s2.tags || []).includes(t)
3560
- );
3561
- if (sharedTags.length > 0) {
3562
- edges.push({
3563
- source: s1.id,
3564
- target: s2.id,
3565
- weight: sharedTags.length
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 !== "dev-rules" && id !== "review-guidelines") {
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"}`);