claude-memory-layer 1.0.18 → 1.0.19

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 (35) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +372 -74
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/hooks/post-tool-use.js +6 -0
  5. package/dist/hooks/post-tool-use.js.map +2 -2
  6. package/dist/hooks/session-end.js +6 -0
  7. package/dist/hooks/session-end.js.map +2 -2
  8. package/dist/hooks/session-start.js +6 -0
  9. package/dist/hooks/session-start.js.map +2 -2
  10. package/dist/hooks/stop.js +6 -0
  11. package/dist/hooks/stop.js.map +2 -2
  12. package/dist/hooks/user-prompt-submit.js +245 -31
  13. package/dist/hooks/user-prompt-submit.js.map +3 -3
  14. package/dist/server/api/index.js +329 -31
  15. package/dist/server/api/index.js.map +3 -3
  16. package/dist/server/index.js +336 -38
  17. package/dist/server/index.js.map +3 -3
  18. package/dist/services/memory-service.js +6 -0
  19. package/dist/services/memory-service.js.map +2 -2
  20. package/dist/ui/app.js +236 -4
  21. package/dist/ui/index.html +51 -0
  22. package/dist/ui/style.css +34 -0
  23. package/memory/_index.md +3 -0
  24. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  25. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  26. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  27. package/package.json +3 -2
  28. package/scripts/delete-unknown-projects.js +154 -0
  29. package/src/hooks/user-prompt-submit.ts +225 -29
  30. package/src/server/api/events.ts +1 -0
  31. package/src/server/api/stats.ts +346 -0
  32. package/src/services/memory-service.ts +7 -0
  33. package/src/ui/app.js +236 -4
  34. package/src/ui/index.html +51 -0
  35. package/src/ui/style.css +34 -0
package/dist/cli/index.js CHANGED
@@ -16,8 +16,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
16
16
  // src/cli/index.ts
17
17
  import { Command } from "commander";
18
18
  import { exec } from "child_process";
19
- import * as fs9 from "fs";
20
- import * as path9 from "path";
19
+ import * as fs10 from "fs";
20
+ import * as path10 from "path";
21
21
  import * as os6 from "os";
22
22
 
23
23
  // src/services/memory-service.ts
@@ -81,57 +81,57 @@ function toDate(value) {
81
81
  return new Date(value);
82
82
  return new Date(String(value));
83
83
  }
84
- function createDatabase(path10, options) {
84
+ function createDatabase(path11, options) {
85
85
  if (options?.readOnly) {
86
- return new duckdb.Database(path10, { access_mode: "READ_ONLY" });
86
+ return new duckdb.Database(path11, { access_mode: "READ_ONLY" });
87
87
  }
88
- return new duckdb.Database(path10);
88
+ return new duckdb.Database(path11);
89
89
  }
90
90
  function dbRun(db, sql, params = []) {
91
- return new Promise((resolve4, reject) => {
91
+ return new Promise((resolve5, reject) => {
92
92
  if (params.length === 0) {
93
93
  db.run(sql, (err) => {
94
94
  if (err)
95
95
  reject(err);
96
96
  else
97
- resolve4();
97
+ resolve5();
98
98
  });
99
99
  } else {
100
100
  db.run(sql, ...params, (err) => {
101
101
  if (err)
102
102
  reject(err);
103
103
  else
104
- resolve4();
104
+ resolve5();
105
105
  });
106
106
  }
107
107
  });
108
108
  }
109
109
  function dbAll(db, sql, params = []) {
110
- return new Promise((resolve4, reject) => {
110
+ return new Promise((resolve5, reject) => {
111
111
  if (params.length === 0) {
112
112
  db.all(sql, (err, rows) => {
113
113
  if (err)
114
114
  reject(err);
115
115
  else
116
- resolve4(convertBigInts(rows || []));
116
+ resolve5(convertBigInts(rows || []));
117
117
  });
118
118
  } else {
119
119
  db.all(sql, ...params, (err, rows) => {
120
120
  if (err)
121
121
  reject(err);
122
122
  else
123
- resolve4(convertBigInts(rows || []));
123
+ resolve5(convertBigInts(rows || []));
124
124
  });
125
125
  }
126
126
  });
127
127
  }
128
128
  function dbClose(db) {
129
- return new Promise((resolve4, reject) => {
129
+ return new Promise((resolve5, reject) => {
130
130
  db.close((err) => {
131
131
  if (err)
132
132
  reject(err);
133
133
  else
134
- resolve4();
134
+ resolve5();
135
135
  });
136
136
  });
137
137
  }
@@ -769,12 +769,12 @@ import { randomUUID as randomUUID2 } from "crypto";
769
769
  import Database from "better-sqlite3";
770
770
  import * as fs from "fs";
771
771
  import * as nodePath from "path";
772
- function createSQLiteDatabase(path10, options) {
773
- const dir = nodePath.dirname(path10);
772
+ function createSQLiteDatabase(path11, options) {
773
+ const dir = nodePath.dirname(path11);
774
774
  if (!fs.existsSync(dir)) {
775
775
  fs.mkdirSync(dir, { recursive: true });
776
776
  }
777
- const db = new Database(path10, {
777
+ const db = new Database(path11, {
778
778
  readonly: options?.readonly ?? false
779
779
  });
780
780
  if (!options?.readonly && (options?.walMode ?? true)) {
@@ -2420,7 +2420,7 @@ var SyncWorker = class {
2420
2420
  * Sleep utility
2421
2421
  */
2422
2422
  sleep(ms) {
2423
- return new Promise((resolve4) => setTimeout(resolve4, ms));
2423
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
2424
2424
  }
2425
2425
  /**
2426
2426
  * Get sync statistics
@@ -3450,8 +3450,8 @@ _Context:_ ${sessionContext}`;
3450
3450
  matchesMetadataScope(metadata, expected) {
3451
3451
  if (!metadata)
3452
3452
  return false;
3453
- return Object.entries(expected).every(([path10, value]) => {
3454
- const actual = path10.split(".").reduce((acc, key) => {
3453
+ return Object.entries(expected).every(([path11, value]) => {
3454
+ const actual = path11.split(".").reduce((acc, key) => {
3455
3455
  if (typeof acc !== "object" || acc === null)
3456
3456
  return void 0;
3457
3457
  return acc[key];
@@ -6913,6 +6913,12 @@ var MemoryService = class {
6913
6913
  recordMemoryAccess(eventId, sessionId, confidence = 1) {
6914
6914
  this.graduation.recordAccess(eventId, sessionId, confidence);
6915
6915
  }
6916
+ /**
6917
+ * Backward-compatible alias used by some hooks
6918
+ */
6919
+ async close() {
6920
+ await this.shutdown();
6921
+ }
6916
6922
  /**
6917
6923
  * Shutdown service
6918
6924
  */
@@ -7766,8 +7772,8 @@ import { cors } from "hono/cors";
7766
7772
  import { logger } from "hono/logger";
7767
7773
  import { serve } from "@hono/node-server";
7768
7774
  import { serveStatic } from "@hono/node-server/serve-static";
7769
- import * as path8 from "path";
7770
- import * as fs8 from "fs";
7775
+ import * as path9 from "path";
7776
+ import * as fs9 from "fs";
7771
7777
 
7772
7778
  // src/server/api/index.ts
7773
7779
  import { Hono as Hono10 } from "hono";
@@ -7931,6 +7937,7 @@ eventsRouter.get("/", async (c) => {
7931
7937
  sessionId: e.sessionId,
7932
7938
  preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
7933
7939
  contentLength: e.content.length,
7940
+ metadata: e.metadata,
7934
7941
  accessCount: e.access_count || 0,
7935
7942
  lastAccessedAt: e.last_accessed_at || null
7936
7943
  })),
@@ -8057,7 +8064,175 @@ searchRouter.get("/", async (c) => {
8057
8064
 
8058
8065
  // src/server/api/stats.ts
8059
8066
  import { Hono as Hono4 } from "hono";
8067
+ import * as fs7 from "fs";
8068
+ import * as path7 from "path";
8060
8069
  var statsRouter = new Hono4();
8070
+ var DEFAULT_KPI_THRESHOLDS = {
8071
+ usefulRecallRateMin: 0.45,
8072
+ reworkRateMax: 0.25,
8073
+ postChangeFailureRateMax: 0.2,
8074
+ avgCompletionTurnsMax: 12,
8075
+ memoryHitRateMin: 0.35
8076
+ };
8077
+ function loadKpiThresholds() {
8078
+ try {
8079
+ const filePath = path7.resolve(process.cwd(), "config", "kpi-thresholds.json");
8080
+ if (!fs7.existsSync(filePath))
8081
+ return DEFAULT_KPI_THRESHOLDS;
8082
+ const parsed = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
8083
+ return {
8084
+ usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
8085
+ reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
8086
+ postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
8087
+ avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
8088
+ memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
8089
+ };
8090
+ } catch {
8091
+ return DEFAULT_KPI_THRESHOLDS;
8092
+ }
8093
+ }
8094
+ function windowToMs(window) {
8095
+ if (window === "24h")
8096
+ return 24 * 60 * 60 * 1e3;
8097
+ if (window === "7d")
8098
+ return 7 * 24 * 60 * 60 * 1e3;
8099
+ return 30 * 24 * 60 * 60 * 1e3;
8100
+ }
8101
+ function inWindow(e, now, window) {
8102
+ return now - e.timestamp.getTime() <= windowToMs(window);
8103
+ }
8104
+ function isEditToolName(name) {
8105
+ return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
8106
+ }
8107
+ function parseToolPayload(e) {
8108
+ if (e.eventType !== "tool_observation")
8109
+ return null;
8110
+ try {
8111
+ const payload = JSON.parse(e.content);
8112
+ return {
8113
+ toolName: payload?.toolName,
8114
+ success: payload?.success,
8115
+ filePath: payload?.metadata?.filePath,
8116
+ command: payload?.metadata?.command
8117
+ };
8118
+ } catch {
8119
+ return {
8120
+ toolName: e.metadata?.toolName,
8121
+ success: e.metadata?.success,
8122
+ filePath: e.metadata?.filePath,
8123
+ command: e.metadata?.command
8124
+ };
8125
+ }
8126
+ }
8127
+ function isTestLikeCommand(command) {
8128
+ if (!command)
8129
+ return false;
8130
+ return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
8131
+ }
8132
+ function safeRatio(num, den) {
8133
+ if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0)
8134
+ return 0;
8135
+ return num / den;
8136
+ }
8137
+ function round(value, digits = 4) {
8138
+ const factor = 10 ** digits;
8139
+ return Math.round(value * factor) / factor;
8140
+ }
8141
+ function computeSessionTurnCount(sessionEvents) {
8142
+ const turnIds = /* @__PURE__ */ new Set();
8143
+ for (const e of sessionEvents) {
8144
+ const turnId = e.metadata?.turnId;
8145
+ if (typeof turnId === "string" && turnId.length > 0)
8146
+ turnIds.add(turnId);
8147
+ }
8148
+ if (turnIds.size > 0)
8149
+ return turnIds.size;
8150
+ return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
8151
+ }
8152
+ function computeKpiMetrics(events, usefulRecallRate) {
8153
+ const prompts = events.filter((e) => e.eventType === "user_prompt");
8154
+ const promptCount = prompts.length;
8155
+ const memoryHitPrompts = prompts.filter((p) => p.metadata?.adherence?.checked).length;
8156
+ const memoryHitRate = round(safeRatio(memoryHitPrompts, promptCount));
8157
+ const sessions = /* @__PURE__ */ new Map();
8158
+ for (const e of events) {
8159
+ const arr = sessions.get(e.sessionId) || [];
8160
+ arr.push(e);
8161
+ sessions.set(e.sessionId, arr);
8162
+ }
8163
+ let sessionTurnTotal = 0;
8164
+ let sessionTurnSamples = 0;
8165
+ let firstValidEditMinutesTotal = 0;
8166
+ let firstValidEditSamples = 0;
8167
+ for (const sessionEvents of sessions.values()) {
8168
+ sessionEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
8169
+ const turns = computeSessionTurnCount(sessionEvents);
8170
+ if (turns > 0) {
8171
+ sessionTurnTotal += turns;
8172
+ sessionTurnSamples++;
8173
+ }
8174
+ const firstPrompt = sessionEvents.find((e) => e.eventType === "user_prompt");
8175
+ const firstEdit = sessionEvents.find((e) => {
8176
+ const payload = parseToolPayload(e);
8177
+ return payload?.toolName && isEditToolName(payload.toolName) && payload.success === true;
8178
+ });
8179
+ if (firstPrompt && firstEdit) {
8180
+ const minutes = (firstEdit.timestamp.getTime() - firstPrompt.timestamp.getTime()) / 6e4;
8181
+ if (minutes >= 0) {
8182
+ firstValidEditMinutesTotal += minutes;
8183
+ firstValidEditSamples++;
8184
+ }
8185
+ }
8186
+ }
8187
+ const avgCompletionTurns = round(safeRatio(sessionTurnTotal, sessionTurnSamples), 2);
8188
+ const timeToFirstValidEditMinutes = round(safeRatio(firstValidEditMinutesTotal, firstValidEditSamples), 2);
8189
+ const editActions = [];
8190
+ let testRunsAfterEdit = 0;
8191
+ let failedTestRunsAfterEdit = 0;
8192
+ for (const [sessionId, sessionEvents] of sessions.entries()) {
8193
+ const sorted = [...sessionEvents].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
8194
+ let seenEdit = false;
8195
+ for (const e of sorted) {
8196
+ const payload = parseToolPayload(e);
8197
+ if (!payload?.toolName)
8198
+ continue;
8199
+ if (isEditToolName(payload.toolName) && payload.success === true) {
8200
+ editActions.push({ sessionId, timestamp: e.timestamp.getTime(), filePath: payload.filePath });
8201
+ seenEdit = true;
8202
+ continue;
8203
+ }
8204
+ if (seenEdit && isTestLikeCommand(payload.command)) {
8205
+ testRunsAfterEdit++;
8206
+ if (payload.success === false)
8207
+ failedTestRunsAfterEdit++;
8208
+ }
8209
+ }
8210
+ }
8211
+ const THIRTY_MIN_MS = 30 * 60 * 1e3;
8212
+ let reworkCount = 0;
8213
+ const bySessionFile = /* @__PURE__ */ new Map();
8214
+ const sortedEdits = [...editActions].sort((a, b) => a.timestamp - b.timestamp);
8215
+ for (const edit of sortedEdits) {
8216
+ if (!edit.filePath)
8217
+ continue;
8218
+ const key = `${edit.sessionId}::${edit.filePath}`;
8219
+ const prev = bySessionFile.get(key);
8220
+ if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS) {
8221
+ reworkCount++;
8222
+ }
8223
+ bySessionFile.set(key, edit.timestamp);
8224
+ }
8225
+ const reworkRate = round(safeRatio(reworkCount, editActions.length));
8226
+ const postChangeFailureRate = round(safeRatio(failedTestRunsAfterEdit, testRunsAfterEdit));
8227
+ return {
8228
+ memoryHitRate,
8229
+ usefulRecallRate,
8230
+ avgCompletionTurns,
8231
+ timeToFirstValidEditMinutes,
8232
+ reworkRate,
8233
+ postChangeFailureRate
8234
+ };
8235
+ }
8061
8236
  statsRouter.get("/shared", async (c) => {
8062
8237
  const memoryService = getServiceFromQuery(c);
8063
8238
  try {
@@ -8337,6 +8512,129 @@ statsRouter.get("/retrieval-traces", async (c) => {
8337
8512
  await memoryService.shutdown();
8338
8513
  }
8339
8514
  });
8515
+ statsRouter.get("/kpi", async (c) => {
8516
+ const rawWindow = c.req.query("window") || "7d";
8517
+ const window = rawWindow === "24h" || rawWindow === "30d" ? rawWindow : "7d";
8518
+ const memoryService = getServiceFromQuery(c);
8519
+ try {
8520
+ await memoryService.initialize();
8521
+ const now = Date.now();
8522
+ const thresholds = loadKpiThresholds();
8523
+ const allEvents = await memoryService.getRecentEvents(2e4);
8524
+ const events = allEvents.filter((e) => inWindow(e, now, window));
8525
+ const helpfulness = await memoryService.getHelpfulnessStats();
8526
+ const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
8527
+ const metrics = computeKpiMetrics(events, usefulRecallRate);
8528
+ const windowMs = windowToMs(window);
8529
+ const prevEvents = allEvents.filter((e) => {
8530
+ const age = now - e.timestamp.getTime();
8531
+ return age > windowMs && age <= windowMs * 2;
8532
+ });
8533
+ const previousMetrics = computeKpiMetrics(prevEvents, usefulRecallRate);
8534
+ const deltas = {
8535
+ memoryHitRate: round(metrics.memoryHitRate - previousMetrics.memoryHitRate),
8536
+ usefulRecallRate: round(metrics.usefulRecallRate - previousMetrics.usefulRecallRate),
8537
+ avgCompletionTurns: round(metrics.avgCompletionTurns - previousMetrics.avgCompletionTurns, 2),
8538
+ timeToFirstValidEditMinutes: round(metrics.timeToFirstValidEditMinutes - previousMetrics.timeToFirstValidEditMinutes, 2),
8539
+ reworkRate: round(metrics.reworkRate - previousMetrics.reworkRate),
8540
+ postChangeFailureRate: round(metrics.postChangeFailureRate - previousMetrics.postChangeFailureRate)
8541
+ };
8542
+ const THIRTY_MIN_MS = 30 * 60 * 1e3;
8543
+ const trendWindowMs = 30 * 24 * 60 * 60 * 1e3;
8544
+ const trendEvents = allEvents.filter((e) => now - e.timestamp.getTime() <= trendWindowMs);
8545
+ const buckets = /* @__PURE__ */ new Map();
8546
+ for (const e of trendEvents) {
8547
+ const day = e.timestamp.toISOString().split("T")[0];
8548
+ const arr = buckets.get(day) || [];
8549
+ arr.push(e);
8550
+ buckets.set(day, arr);
8551
+ }
8552
+ const trendDaily = Array.from(buckets.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([date, dayEvents]) => {
8553
+ const dayPrompts = dayEvents.filter((e) => e.eventType === "user_prompt");
8554
+ const dayPromptCount = dayPrompts.length;
8555
+ const dayMemoryHit = dayPrompts.filter((p) => p.metadata?.adherence?.checked).length;
8556
+ const dayEdits = dayEvents.filter((e) => {
8557
+ const p = parseToolPayload(e);
8558
+ return Boolean(p?.toolName && isEditToolName(p.toolName) && p.success === true);
8559
+ });
8560
+ const dayEditActions = dayEdits.map((e) => {
8561
+ const p = parseToolPayload(e);
8562
+ return { sessionId: e.sessionId, timestamp: e.timestamp.getTime(), filePath: p?.filePath };
8563
+ }).filter((x) => Boolean(x.filePath));
8564
+ let dayReworkCount = 0;
8565
+ const dayBySessionFile = /* @__PURE__ */ new Map();
8566
+ for (const edit of dayEditActions) {
8567
+ const key = `${edit.sessionId}::${edit.filePath}`;
8568
+ const prev = dayBySessionFile.get(key);
8569
+ if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS)
8570
+ dayReworkCount++;
8571
+ dayBySessionFile.set(key, edit.timestamp);
8572
+ }
8573
+ const dayTests = dayEvents.filter((e) => {
8574
+ const p = parseToolPayload(e);
8575
+ return Boolean(p?.toolName && isTestLikeCommand(p.command));
8576
+ });
8577
+ const dayFailedTests = dayEvents.filter((e) => {
8578
+ const p = parseToolPayload(e);
8579
+ return Boolean(p?.toolName && isTestLikeCommand(p.command) && p.success === false);
8580
+ });
8581
+ const turnsBySession = /* @__PURE__ */ new Map();
8582
+ for (const e of dayEvents) {
8583
+ const arr = turnsBySession.get(e.sessionId) || [];
8584
+ arr.push(e);
8585
+ turnsBySession.set(e.sessionId, arr);
8586
+ }
8587
+ let dayTurnsTotal = 0;
8588
+ let dayTurnsSamples = 0;
8589
+ for (const sessionEvents of turnsBySession.values()) {
8590
+ const turns = computeSessionTurnCount(sessionEvents);
8591
+ if (turns > 0) {
8592
+ dayTurnsTotal += turns;
8593
+ dayTurnsSamples++;
8594
+ }
8595
+ }
8596
+ return {
8597
+ date,
8598
+ memoryHitRate: round(safeRatio(dayMemoryHit, dayPromptCount)),
8599
+ usefulRecallRate,
8600
+ reworkRate: round(safeRatio(dayReworkCount, dayEditActions.length)),
8601
+ postChangeFailureRate: round(safeRatio(dayFailedTests.length, dayTests.length)),
8602
+ avgCompletionTurns: round(safeRatio(dayTurnsTotal, dayTurnsSamples), 2)
8603
+ };
8604
+ });
8605
+ const alerts = [];
8606
+ if (metrics.usefulRecallRate < thresholds.usefulRecallRateMin) {
8607
+ alerts.push({ metric: "usefulRecallRate", level: "warn", message: "Useful recall rate is below threshold", value: metrics.usefulRecallRate, threshold: thresholds.usefulRecallRateMin });
8608
+ }
8609
+ if (metrics.reworkRate > thresholds.reworkRateMax) {
8610
+ alerts.push({ metric: "reworkRate", level: "warn", message: "Rework rate is above threshold", value: metrics.reworkRate, threshold: thresholds.reworkRateMax });
8611
+ }
8612
+ if (metrics.postChangeFailureRate > thresholds.postChangeFailureRateMax) {
8613
+ alerts.push({ metric: "postChangeFailureRate", level: "warn", message: "Post-change failure rate is above threshold", value: metrics.postChangeFailureRate, threshold: thresholds.postChangeFailureRateMax });
8614
+ }
8615
+ if (metrics.avgCompletionTurns > thresholds.avgCompletionTurnsMax) {
8616
+ alerts.push({ metric: "avgCompletionTurns", level: "warn", message: "Average completion turns is above threshold", value: metrics.avgCompletionTurns, threshold: thresholds.avgCompletionTurnsMax });
8617
+ }
8618
+ if (metrics.memoryHitRate < thresholds.memoryHitRateMin) {
8619
+ alerts.push({ metric: "memoryHitRate", level: "warn", message: "Memory hit rate is below threshold", value: metrics.memoryHitRate, threshold: thresholds.memoryHitRateMin });
8620
+ }
8621
+ return c.json({
8622
+ window,
8623
+ metrics,
8624
+ previousMetrics,
8625
+ deltas,
8626
+ trend: {
8627
+ daily: trendDaily
8628
+ },
8629
+ thresholds,
8630
+ alerts
8631
+ });
8632
+ } catch (error) {
8633
+ return c.json({ error: error.message }, 500);
8634
+ } finally {
8635
+ await memoryService.shutdown();
8636
+ }
8637
+ });
8340
8638
  statsRouter.post("/graduation/run", async (c) => {
8341
8639
  const memoryService = getServiceFromQuery(c);
8342
8640
  try {
@@ -8584,19 +8882,19 @@ turnsRouter.post("/backfill", async (c) => {
8584
8882
 
8585
8883
  // src/server/api/projects.ts
8586
8884
  import { Hono as Hono7 } from "hono";
8587
- import * as fs7 from "fs";
8588
- import * as path7 from "path";
8885
+ import * as fs8 from "fs";
8886
+ import * as path8 from "path";
8589
8887
  import * as os4 from "os";
8590
8888
  var projectsRouter = new Hono7();
8591
8889
  projectsRouter.get("/", async (c) => {
8592
8890
  try {
8593
- const projectsDir = path7.join(os4.homedir(), ".claude-code", "memory", "projects");
8594
- if (!fs7.existsSync(projectsDir)) {
8891
+ const projectsDir = path8.join(os4.homedir(), ".claude-code", "memory", "projects");
8892
+ if (!fs8.existsSync(projectsDir)) {
8595
8893
  return c.json({ projects: [] });
8596
8894
  }
8597
- const projectHashes = fs7.readdirSync(projectsDir).filter((name) => {
8598
- const fullPath = path7.join(projectsDir, name);
8599
- return fs7.statSync(fullPath).isDirectory();
8895
+ const projectHashes = fs8.readdirSync(projectsDir).filter((name) => {
8896
+ const fullPath = path8.join(projectsDir, name);
8897
+ return fs8.statSync(fullPath).isDirectory();
8600
8898
  });
8601
8899
  const registry = loadSessionRegistry();
8602
8900
  const hashToPath = /* @__PURE__ */ new Map();
@@ -8606,17 +8904,17 @@ projectsRouter.get("/", async (c) => {
8606
8904
  }
8607
8905
  }
8608
8906
  const projects = projectHashes.map((hash) => {
8609
- const dirPath = path7.join(projectsDir, hash);
8610
- const dbPath = path7.join(dirPath, "events.sqlite");
8907
+ const dirPath = path8.join(projectsDir, hash);
8908
+ const dbPath = path8.join(dirPath, "events.sqlite");
8611
8909
  let dbSize = 0;
8612
- if (fs7.existsSync(dbPath)) {
8613
- dbSize = fs7.statSync(dbPath).size;
8910
+ if (fs8.existsSync(dbPath)) {
8911
+ dbSize = fs8.statSync(dbPath).size;
8614
8912
  }
8615
8913
  const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
8616
8914
  return {
8617
8915
  hash,
8618
8916
  projectPath,
8619
- projectName: path7.basename(projectPath),
8917
+ projectName: path8.basename(projectPath),
8620
8918
  dbSize,
8621
8919
  dbSizeHuman: formatBytes(dbSize)
8622
8920
  };
@@ -8741,7 +9039,7 @@ function buildPrompt(statsContext, memoryContext, history, currentMessage) {
8741
9039
  return parts.join("\n");
8742
9040
  }
8743
9041
  function streamClaudeResponse(prompt, stream) {
8744
- return new Promise((resolve4, reject) => {
9042
+ return new Promise((resolve5, reject) => {
8745
9043
  const proc = spawn("claude", [
8746
9044
  "-p",
8747
9045
  "--output-format",
@@ -8813,7 +9111,7 @@ function streamClaudeResponse(prompt, stream) {
8813
9111
  if (code !== 0 && code !== null) {
8814
9112
  reject(new Error(`Claude CLI exited with code ${code}`));
8815
9113
  } else {
8816
- resolve4();
9114
+ resolve5();
8817
9115
  }
8818
9116
  });
8819
9117
  });
@@ -8870,14 +9168,14 @@ app.use("/*", cors());
8870
9168
  app.use("/*", logger());
8871
9169
  app.route("/api", apiRouter);
8872
9170
  app.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
8873
- var uiPath = path8.join(__dirname, "../../dist/ui");
8874
- if (fs8.existsSync(uiPath)) {
9171
+ var uiPath = path9.join(__dirname, "../../dist/ui");
9172
+ if (fs9.existsSync(uiPath)) {
8875
9173
  app.use("/*", serveStatic({ root: uiPath }));
8876
9174
  }
8877
9175
  app.get("*", (c) => {
8878
- const indexPath = path8.join(uiPath, "index.html");
8879
- if (fs8.existsSync(indexPath)) {
8880
- return c.html(fs8.readFileSync(indexPath, "utf-8"));
9176
+ const indexPath = path9.join(uiPath, "index.html");
9177
+ if (fs9.existsSync(indexPath)) {
9178
+ return c.html(fs9.readFileSync(indexPath, "utf-8"));
8881
9179
  }
8882
9180
  return c.text('UI not built. Run "npm run build:ui" first.', 404);
8883
9181
  });
@@ -9189,28 +9487,28 @@ var MongoSyncWorker = class {
9189
9487
  };
9190
9488
 
9191
9489
  // src/cli/index.ts
9192
- var CLAUDE_SETTINGS_PATH = path9.join(os6.homedir(), ".claude", "settings.json");
9490
+ var CLAUDE_SETTINGS_PATH = path10.join(os6.homedir(), ".claude", "settings.json");
9193
9491
  function getPluginPath() {
9194
9492
  const possiblePaths = [
9195
- path9.join(__dirname, ".."),
9493
+ path10.join(__dirname, ".."),
9196
9494
  // When running from dist/cli
9197
- path9.join(__dirname, "../..", "dist"),
9495
+ path10.join(__dirname, "../..", "dist"),
9198
9496
  // When running from src
9199
- path9.join(process.cwd(), "dist")
9497
+ path10.join(process.cwd(), "dist")
9200
9498
  // Current working directory
9201
9499
  ];
9202
9500
  for (const p of possiblePaths) {
9203
- const hooksPath = path9.join(p, "hooks", "user-prompt-submit.js");
9204
- if (fs9.existsSync(hooksPath)) {
9501
+ const hooksPath = path10.join(p, "hooks", "user-prompt-submit.js");
9502
+ if (fs10.existsSync(hooksPath)) {
9205
9503
  return p;
9206
9504
  }
9207
9505
  }
9208
- return path9.join(os6.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
9506
+ return path10.join(os6.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
9209
9507
  }
9210
9508
  function loadClaudeSettings() {
9211
9509
  try {
9212
- if (fs9.existsSync(CLAUDE_SETTINGS_PATH)) {
9213
- const content = fs9.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
9510
+ if (fs10.existsSync(CLAUDE_SETTINGS_PATH)) {
9511
+ const content = fs10.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
9214
9512
  return JSON.parse(content);
9215
9513
  }
9216
9514
  } catch (error) {
@@ -9219,13 +9517,13 @@ function loadClaudeSettings() {
9219
9517
  return {};
9220
9518
  }
9221
9519
  function saveClaudeSettings(settings) {
9222
- const dir = path9.dirname(CLAUDE_SETTINGS_PATH);
9223
- if (!fs9.existsSync(dir)) {
9224
- fs9.mkdirSync(dir, { recursive: true });
9520
+ const dir = path10.dirname(CLAUDE_SETTINGS_PATH);
9521
+ if (!fs10.existsSync(dir)) {
9522
+ fs10.mkdirSync(dir, { recursive: true });
9225
9523
  }
9226
9524
  const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
9227
- fs9.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
9228
- fs9.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
9525
+ fs10.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
9526
+ fs10.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
9229
9527
  }
9230
9528
  var REQUIRED_HOOK_FILES = [
9231
9529
  "user-prompt-submit.js",
@@ -9247,7 +9545,7 @@ function getHooksConfig(pluginPath) {
9247
9545
  hooks: [
9248
9546
  {
9249
9547
  type: "command",
9250
- command: `node ${path9.join(pluginPath, "hooks", fileName)}`
9548
+ command: `node ${path10.join(pluginPath, "hooks", fileName)}`
9251
9549
  }
9252
9550
  ]
9253
9551
  }
@@ -9261,12 +9559,12 @@ function getHooksConfig(pluginPath) {
9261
9559
  };
9262
9560
  }
9263
9561
  var program = new Command();
9264
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.18");
9562
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.19");
9265
9563
  program.command("install").description("Install hooks into Claude Code settings").option("--path <path>", "Custom plugin path (defaults to auto-detect)").action(async (options) => {
9266
9564
  try {
9267
9565
  const pluginPath = options.path || getPluginPath();
9268
9566
  const missingHooks = REQUIRED_HOOK_FILES.filter(
9269
- (file) => !fs9.existsSync(path9.join(pluginPath, "hooks", file))
9567
+ (file) => !fs10.existsSync(path10.join(pluginPath, "hooks", file))
9270
9568
  );
9271
9569
  if (missingHooks.length > 0) {
9272
9570
  console.error(`
@@ -9343,7 +9641,7 @@ program.command("status").description("Check plugin installation status").action
9343
9641
  console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
9344
9642
  console.log(` Stop: ${hasStopHook ? "\u2705 Installed" : "\u274C Not installed"}`);
9345
9643
  console.log(` SessionEnd: ${hasSessionEndHook ? "\u2705 Installed" : "\u274C Not installed"}`);
9346
- const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs9.existsSync(path9.join(pluginPath, "hooks", file)));
9644
+ const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs10.existsSync(path10.join(pluginPath, "hooks", file)));
9347
9645
  console.log(`
9348
9646
  Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
9349
9647
  console.log(` Path: ${pluginPath}`);
@@ -9475,7 +9773,7 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
9475
9773
  const projectPath = options.project || process.cwd();
9476
9774
  const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
9477
9775
  const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
9478
- const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path9.basename(projectPath);
9776
+ const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path10.basename(projectPath);
9479
9777
  const direction = String(options.direction || "both").toLowerCase();
9480
9778
  if (!mongoUri || !mongoDb) {
9481
9779
  console.error("\n\u274C MongoDB sync is not configured.");
@@ -9487,14 +9785,14 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
9487
9785
  process.exit(1);
9488
9786
  }
9489
9787
  const storagePath = getProjectStoragePath(projectPath);
9490
- if (!fs9.existsSync(storagePath)) {
9491
- fs9.mkdirSync(storagePath, { recursive: true });
9788
+ if (!fs10.existsSync(storagePath)) {
9789
+ fs10.mkdirSync(storagePath, { recursive: true });
9492
9790
  }
9493
9791
  const batchSizeParsed = parseInt(options.batchSize, 10);
9494
9792
  const intervalParsed = parseInt(options.interval, 10);
9495
9793
  const batchSize = Number.isFinite(batchSizeParsed) && batchSizeParsed > 0 ? batchSizeParsed : 500;
9496
9794
  const intervalMs = Number.isFinite(intervalParsed) && intervalParsed > 0 ? intervalParsed : 3e4;
9497
- const sqliteStore = new SQLiteEventStore(path9.join(storagePath, "events.sqlite"));
9795
+ const sqliteStore = new SQLiteEventStore(path10.join(storagePath, "events.sqlite"));
9498
9796
  const worker = new MongoSyncWorker(sqliteStore, {
9499
9797
  uri: mongoUri,
9500
9798
  dbName: mongoDb,
@@ -9553,7 +9851,7 @@ function renderProgress(event) {
9553
9851
  break;
9554
9852
  case "session-start": {
9555
9853
  const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
9556
- const sessionName = path9.basename(event.filePath, ".jsonl").slice(0, 8);
9854
+ const sessionName = path10.basename(event.filePath, ".jsonl").slice(0, 8);
9557
9855
  process.stdout.write(
9558
9856
  `\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
9559
9857
  );
@@ -9628,9 +9926,9 @@ async function listMarkdownFiles(root) {
9628
9926
  const stack = [root];
9629
9927
  while (stack.length > 0) {
9630
9928
  const dir = stack.pop();
9631
- const entries = await fs9.promises.readdir(dir, { withFileTypes: true });
9929
+ const entries = await fs10.promises.readdir(dir, { withFileTypes: true });
9632
9930
  for (const e of entries) {
9633
- const full = path9.join(dir, e.name);
9931
+ const full = path10.join(dir, e.name);
9634
9932
  if (e.isDirectory())
9635
9933
  stack.push(full);
9636
9934
  else if (e.isFile() && e.name.endsWith(".md") && e.name !== "_index.md")
@@ -9640,8 +9938,8 @@ async function listMarkdownFiles(root) {
9640
9938
  return out.sort();
9641
9939
  }
9642
9940
  function deriveNamespaceCategory(sourceRoot, filePath) {
9643
- const rel = path9.relative(sourceRoot, filePath);
9644
- const dirSeg = path9.dirname(rel).split(path9.sep).filter(Boolean);
9941
+ const rel = path10.relative(sourceRoot, filePath);
9942
+ const dirSeg = path10.dirname(rel).split(path10.sep).filter(Boolean);
9645
9943
  if (dirSeg.length >= 2) {
9646
9944
  const namespace = sanitizeSegment3(dirSeg[0], "default");
9647
9945
  const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment3(s, "uncategorized"));
@@ -9660,10 +9958,10 @@ function extractImportEvidence(markdown) {
9660
9958
  program.command("organize-import [sourceDir]").description("Import existing markdown memory files, or bootstrap knowledge docs from codebase/git when markdown is missing").option("-p, --project <path>", "Project path (defaults to cwd)").option("--session <id>", "Session id for imported events (default: import:organized)").option("--limit <n>", "Limit number of files to import").option("--dry-run", "Preview mapping without writing").option("--bootstrap", "Force-generate structured markdown from codebase + git history before import").option("--bootstrap-if-empty", "Auto-bootstrap when source has no markdown files (default: true)", true).option("--no-bootstrap-if-empty", "Disable auto-bootstrap when source has no markdown files").option("--force-bootstrap", "Run bootstrap even when markdown files exist").option("--repo <path>", "Repository root for bootstrap analysis (default: project path)").option("--out <path>", "Output directory for generated bootstrap markdown (default: <sourceDir>/bootstrap-kb)").option("--since <range>", 'Git history range for bootstrap (default: "180 days ago")').option("--max-commits <n>", "Max commits to analyze for bootstrap (default: 1000)").option("--incremental", "Use previous bootstrap manifest as baseline for incremental updates (default: true)", true).option("--no-incremental", "Disable incremental bootstrap; regenerate full snapshot").action(async (sourceDir, options) => {
9661
9959
  const projectPath = options.project || process.cwd();
9662
9960
  const sessionId = options.session || "import:organized";
9663
- const sourceRoot = path9.resolve(sourceDir || options.out || projectPath);
9664
- const repoPath = path9.resolve(options.repo || projectPath);
9665
- if (!fs9.existsSync(sourceRoot)) {
9666
- fs9.mkdirSync(sourceRoot, { recursive: true });
9961
+ const sourceRoot = path10.resolve(sourceDir || options.out || projectPath);
9962
+ const repoPath = path10.resolve(options.repo || projectPath);
9963
+ if (!fs10.existsSync(sourceRoot)) {
9964
+ fs10.mkdirSync(sourceRoot, { recursive: true });
9667
9965
  }
9668
9966
  const service = getMemoryServiceForProject(projectPath);
9669
9967
  try {
@@ -9673,7 +9971,7 @@ program.command("organize-import [sourceDir]").description("Import existing mark
9673
9971
  const hasMarkdown = files.length > 0;
9674
9972
  const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || !hasMarkdown && options.bootstrapIfEmpty);
9675
9973
  if (shouldBootstrap) {
9676
- const outDir = path9.resolve(options.out || path9.join(sourceRoot, "bootstrap-kb"));
9974
+ const outDir = path10.resolve(options.out || path10.join(sourceRoot, "bootstrap-kb"));
9677
9975
  const since = options.since || "180 days ago";
9678
9976
  const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1e3;
9679
9977
  console.log("\n\u{1F9E0} Bootstrapping markdown knowledge base...");
@@ -9712,13 +10010,13 @@ program.command("organize-import [sourceDir]").description("Import existing mark
9712
10010
  let imported = 0;
9713
10011
  let skipped = 0;
9714
10012
  for (const file of targets) {
9715
- const text = await fs9.promises.readFile(file, "utf8");
10013
+ const text = await fs10.promises.readFile(file, "utf8");
9716
10014
  if (!text.trim()) {
9717
10015
  skipped += 1;
9718
10016
  continue;
9719
10017
  }
9720
10018
  const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
9721
- const rel = path9.relative(activeSourceRoot, file);
10019
+ const rel = path10.relative(activeSourceRoot, file);
9722
10020
  const evidence = extractImportEvidence(text);
9723
10021
  if (options.dryRun) {
9724
10022
  console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join("/")} confidence=${evidence.confidence || "n/a"} sources=${evidence.sources.length}`);