claude-memory-layer 1.0.18 → 1.0.20

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 (42) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +532 -79
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/core/index.js +49 -4
  5. package/dist/core/index.js.map +2 -2
  6. package/dist/hooks/post-tool-use.js +140 -3
  7. package/dist/hooks/post-tool-use.js.map +2 -2
  8. package/dist/hooks/session-end.js +140 -3
  9. package/dist/hooks/session-end.js.map +2 -2
  10. package/dist/hooks/session-start.js +140 -3
  11. package/dist/hooks/session-start.js.map +2 -2
  12. package/dist/hooks/stop.js +140 -3
  13. package/dist/hooks/stop.js.map +2 -2
  14. package/dist/hooks/user-prompt-submit.js +379 -34
  15. package/dist/hooks/user-prompt-submit.js.map +3 -3
  16. package/dist/server/api/index.js +467 -34
  17. package/dist/server/api/index.js.map +3 -3
  18. package/dist/server/index.js +474 -41
  19. package/dist/server/index.js.map +3 -3
  20. package/dist/services/memory-service.js +140 -3
  21. package/dist/services/memory-service.js.map +2 -2
  22. package/dist/ui/app.js +362 -4
  23. package/dist/ui/index.html +90 -0
  24. package/dist/ui/style.css +41 -0
  25. package/memory/_index.md +3 -0
  26. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  27. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  28. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  29. package/package.json +3 -2
  30. package/scripts/delete-unknown-projects.js +154 -0
  31. package/src/cli/index.ts +23 -1
  32. package/src/core/embedder.ts +3 -2
  33. package/src/core/sqlite-event-store.ts +32 -0
  34. package/src/core/types.ts +2 -2
  35. package/src/core/vector-store.ts +20 -0
  36. package/src/hooks/user-prompt-submit.ts +225 -29
  37. package/src/server/api/events.ts +7 -0
  38. package/src/server/api/stats.ts +346 -0
  39. package/src/services/memory-service.ts +119 -2
  40. package/src/ui/app.js +362 -4
  41. package/src/ui/index.html +90 -0
  42. package/src/ui/style.css +41 -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)) {
@@ -1613,6 +1613,33 @@ var SQLiteEventStore = class {
1613
1613
  ids
1614
1614
  );
1615
1615
  }
1616
+ /**
1617
+ * Clear embedding outbox (used for embedding model migration)
1618
+ */
1619
+ async clearEmbeddingOutbox() {
1620
+ await this.initialize();
1621
+ sqliteRun(this.db, `DELETE FROM embedding_outbox`);
1622
+ }
1623
+ /**
1624
+ * Count total events
1625
+ */
1626
+ async countEvents() {
1627
+ await this.initialize();
1628
+ const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
1629
+ return row?.count || 0;
1630
+ }
1631
+ /**
1632
+ * Get events page in timestamp ascending order (stable migration/reindex scans)
1633
+ */
1634
+ async getEventsPage(limit = 1e3, offset = 0) {
1635
+ await this.initialize();
1636
+ const rows = sqliteAll(
1637
+ this.db,
1638
+ `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
1639
+ [limit, offset]
1640
+ );
1641
+ return rows.map(this.rowToEvent);
1642
+ }
1616
1643
  /**
1617
1644
  * Mark outbox items as failed
1618
1645
  */
@@ -2420,7 +2447,7 @@ var SyncWorker = class {
2420
2447
  * Sleep utility
2421
2448
  */
2422
2449
  sleep(ms) {
2423
- return new Promise((resolve4) => setTimeout(resolve4, ms));
2450
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
2424
2451
  }
2425
2452
  /**
2426
2453
  * Get sync statistics
@@ -2578,6 +2605,23 @@ var VectorStore = class {
2578
2605
  const result = await this.table.countRows();
2579
2606
  return result;
2580
2607
  }
2608
+ /**
2609
+ * Clear all vectors (used for embedding model migration)
2610
+ */
2611
+ async clearAll() {
2612
+ await this.initialize();
2613
+ if (!this.db)
2614
+ return;
2615
+ try {
2616
+ if (typeof this.db.dropTable === "function") {
2617
+ await this.db.dropTable(this.tableName);
2618
+ } else if (typeof this.db.drop_table === "function") {
2619
+ await this.db.drop_table(this.tableName);
2620
+ }
2621
+ } catch {
2622
+ }
2623
+ this.table = null;
2624
+ }
2581
2625
  /**
2582
2626
  * Check if vector exists for event
2583
2627
  */
@@ -2595,7 +2639,7 @@ var Embedder = class {
2595
2639
  pipeline = null;
2596
2640
  modelName;
2597
2641
  initialized = false;
2598
- constructor(modelName = "Xenova/all-MiniLM-L6-v2") {
2642
+ constructor(modelName = "jinaai/jina-embeddings-v5-text-nano") {
2599
2643
  this.modelName = modelName;
2600
2644
  }
2601
2645
  /**
@@ -2675,8 +2719,9 @@ var Embedder = class {
2675
2719
  };
2676
2720
  var defaultEmbedder = null;
2677
2721
  function getDefaultEmbedder() {
2722
+ const envModel = process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
2678
2723
  if (!defaultEmbedder) {
2679
- defaultEmbedder = new Embedder();
2724
+ defaultEmbedder = new Embedder(envModel || void 0);
2680
2725
  }
2681
2726
  return defaultEmbedder;
2682
2727
  }
@@ -3450,8 +3495,8 @@ _Context:_ ${sessionContext}`;
3450
3495
  matchesMetadataScope(metadata, expected) {
3451
3496
  if (!metadata)
3452
3497
  return false;
3453
- return Object.entries(expected).every(([path10, value]) => {
3454
- const actual = path10.split(".").reduce((acc, key) => {
3498
+ return Object.entries(expected).every(([path11, value]) => {
3499
+ const actual = path11.split(".").reduce((acc, key) => {
3455
3500
  if (typeof acc !== "object" || acc === null)
3456
3501
  return void 0;
3457
3502
  return acc[key];
@@ -5932,8 +5977,10 @@ var MemoryService = class {
5932
5977
  readOnly;
5933
5978
  lightweightMode;
5934
5979
  mdMirror;
5980
+ storagePath;
5935
5981
  constructor(config) {
5936
5982
  const storagePath = this.expandPath(config.storagePath);
5983
+ this.storagePath = storagePath;
5937
5984
  this.readOnly = config.readOnly ?? false;
5938
5985
  this.lightweightMode = config.lightweightMode ?? false;
5939
5986
  this.mdMirror = new MarkdownMirror2(process.cwd());
@@ -5969,7 +6016,8 @@ var MemoryService = class {
5969
6016
  );
5970
6017
  }
5971
6018
  this.vectorStore = new VectorStore(path3.join(storagePath, "vectors"));
5972
- this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
6019
+ const embeddingModel = config.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
6020
+ this.embedder = embeddingModel ? new Embedder(embeddingModel) : getDefaultEmbedder();
5973
6021
  this.matcher = getDefaultMatcher();
5974
6022
  this.retriever = createRetriever(
5975
6023
  this.sqliteStore,
@@ -6913,6 +6961,95 @@ var MemoryService = class {
6913
6961
  recordMemoryAccess(eventId, sessionId, confidence = 1) {
6914
6962
  this.graduation.recordAccess(eventId, sessionId, confidence);
6915
6963
  }
6964
+ getEmbeddingModelName() {
6965
+ return this.embedder.getModelName();
6966
+ }
6967
+ /**
6968
+ * Ensure embedding model metadata is in sync and optionally migrate vectors.
6969
+ * Migration strategy: clear vector index + clear embedding outbox + re-enqueue all events.
6970
+ */
6971
+ async ensureEmbeddingModelForImport(options) {
6972
+ await this.initialize();
6973
+ const currentModel = this.getEmbeddingModelName();
6974
+ const metaPath = path3.join(this.storagePath, "embedding-meta.json");
6975
+ let previousModel = null;
6976
+ try {
6977
+ if (fs4.existsSync(metaPath)) {
6978
+ const parsed = JSON.parse(fs4.readFileSync(metaPath, "utf-8"));
6979
+ previousModel = parsed?.model || null;
6980
+ }
6981
+ } catch {
6982
+ previousModel = null;
6983
+ }
6984
+ const stats = await this.getStats();
6985
+ const hasExistingVectors = (stats.vectorCount || 0) > 0;
6986
+ if (!previousModel && !hasExistingVectors) {
6987
+ fs4.writeFileSync(metaPath, JSON.stringify({ model: currentModel, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
6988
+ return { changed: false, previousModel: null, currentModel, enqueued: 0, reason: "initialized-meta" };
6989
+ }
6990
+ const modelChanged = previousModel !== currentModel;
6991
+ const legacyUnknownButVectorsExist = !previousModel && hasExistingVectors;
6992
+ if (!modelChanged && !legacyUnknownButVectorsExist) {
6993
+ return { changed: false, previousModel, currentModel, enqueued: 0 };
6994
+ }
6995
+ if (options?.autoMigrate === false) {
6996
+ return {
6997
+ changed: true,
6998
+ previousModel,
6999
+ currentModel,
7000
+ enqueued: 0,
7001
+ reason: legacyUnknownButVectorsExist ? "legacy-vectors-without-meta" : "model-mismatch"
7002
+ };
7003
+ }
7004
+ const wasRunning = this.vectorWorker?.isRunning() || false;
7005
+ if (wasRunning)
7006
+ this.vectorWorker?.stop();
7007
+ await this.vectorStore.clearAll();
7008
+ await this.sqliteStore.clearEmbeddingOutbox();
7009
+ const pageSize = 1e3;
7010
+ let offset = 0;
7011
+ let enqueued = 0;
7012
+ while (true) {
7013
+ const page = await this.sqliteStore.getEventsPage(pageSize, offset);
7014
+ if (page.length === 0)
7015
+ break;
7016
+ for (const event of page) {
7017
+ await this.sqliteStore.enqueueForEmbedding(event.id, event.content);
7018
+ enqueued += 1;
7019
+ }
7020
+ offset += page.length;
7021
+ if (page.length < pageSize)
7022
+ break;
7023
+ }
7024
+ fs4.writeFileSync(
7025
+ metaPath,
7026
+ JSON.stringify(
7027
+ {
7028
+ model: currentModel,
7029
+ previousModel,
7030
+ migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
7031
+ enqueued
7032
+ },
7033
+ null,
7034
+ 2
7035
+ )
7036
+ );
7037
+ if (wasRunning)
7038
+ this.vectorWorker?.start();
7039
+ return {
7040
+ changed: true,
7041
+ previousModel,
7042
+ currentModel,
7043
+ enqueued,
7044
+ reason: legacyUnknownButVectorsExist ? "legacy-vectors-without-meta" : "model-mismatch"
7045
+ };
7046
+ }
7047
+ /**
7048
+ * Backward-compatible alias used by some hooks
7049
+ */
7050
+ async close() {
7051
+ await this.shutdown();
7052
+ }
6916
7053
  /**
6917
7054
  * Shutdown service
6918
7055
  */
@@ -7766,8 +7903,8 @@ import { cors } from "hono/cors";
7766
7903
  import { logger } from "hono/logger";
7767
7904
  import { serve } from "@hono/node-server";
7768
7905
  import { serveStatic } from "@hono/node-server/serve-static";
7769
- import * as path8 from "path";
7770
- import * as fs8 from "fs";
7906
+ import * as path9 from "path";
7907
+ import * as fs9 from "fs";
7771
7908
 
7772
7909
  // src/server/api/index.ts
7773
7910
  import { Hono as Hono10 } from "hono";
@@ -7893,6 +8030,7 @@ eventsRouter.get("/", async (c) => {
7893
8030
  const eventType = c.req.query("type");
7894
8031
  const level = c.req.query("level");
7895
8032
  const sort = c.req.query("sort") || "recent";
8033
+ const q = (c.req.query("q") || "").trim().toLowerCase();
7896
8034
  const limit = parseInt(c.req.query("limit") || "100", 10);
7897
8035
  const offset = parseInt(c.req.query("offset") || "0", 10);
7898
8036
  const memoryService = getServiceFromQuery(c);
@@ -7910,6 +8048,9 @@ eventsRouter.get("/", async (c) => {
7910
8048
  if (eventType) {
7911
8049
  events = events.filter((e) => e.eventType === eventType);
7912
8050
  }
8051
+ if (q) {
8052
+ events = events.filter((e) => (e.content || "").toLowerCase().includes(q));
8053
+ }
7913
8054
  if (sort === "accessed") {
7914
8055
  events.sort((a, b) => {
7915
8056
  const aTime = a.last_accessed_at || "";
@@ -7931,6 +8072,7 @@ eventsRouter.get("/", async (c) => {
7931
8072
  sessionId: e.sessionId,
7932
8073
  preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
7933
8074
  contentLength: e.content.length,
8075
+ metadata: e.metadata,
7934
8076
  accessCount: e.access_count || 0,
7935
8077
  lastAccessedAt: e.last_accessed_at || null
7936
8078
  })),
@@ -8057,7 +8199,175 @@ searchRouter.get("/", async (c) => {
8057
8199
 
8058
8200
  // src/server/api/stats.ts
8059
8201
  import { Hono as Hono4 } from "hono";
8202
+ import * as fs7 from "fs";
8203
+ import * as path7 from "path";
8060
8204
  var statsRouter = new Hono4();
8205
+ var DEFAULT_KPI_THRESHOLDS = {
8206
+ usefulRecallRateMin: 0.45,
8207
+ reworkRateMax: 0.25,
8208
+ postChangeFailureRateMax: 0.2,
8209
+ avgCompletionTurnsMax: 12,
8210
+ memoryHitRateMin: 0.35
8211
+ };
8212
+ function loadKpiThresholds() {
8213
+ try {
8214
+ const filePath = path7.resolve(process.cwd(), "config", "kpi-thresholds.json");
8215
+ if (!fs7.existsSync(filePath))
8216
+ return DEFAULT_KPI_THRESHOLDS;
8217
+ const parsed = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
8218
+ return {
8219
+ usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
8220
+ reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
8221
+ postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
8222
+ avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
8223
+ memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
8224
+ };
8225
+ } catch {
8226
+ return DEFAULT_KPI_THRESHOLDS;
8227
+ }
8228
+ }
8229
+ function windowToMs(window) {
8230
+ if (window === "24h")
8231
+ return 24 * 60 * 60 * 1e3;
8232
+ if (window === "7d")
8233
+ return 7 * 24 * 60 * 60 * 1e3;
8234
+ return 30 * 24 * 60 * 60 * 1e3;
8235
+ }
8236
+ function inWindow(e, now, window) {
8237
+ return now - e.timestamp.getTime() <= windowToMs(window);
8238
+ }
8239
+ function isEditToolName(name) {
8240
+ return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
8241
+ }
8242
+ function parseToolPayload(e) {
8243
+ if (e.eventType !== "tool_observation")
8244
+ return null;
8245
+ try {
8246
+ const payload = JSON.parse(e.content);
8247
+ return {
8248
+ toolName: payload?.toolName,
8249
+ success: payload?.success,
8250
+ filePath: payload?.metadata?.filePath,
8251
+ command: payload?.metadata?.command
8252
+ };
8253
+ } catch {
8254
+ return {
8255
+ toolName: e.metadata?.toolName,
8256
+ success: e.metadata?.success,
8257
+ filePath: e.metadata?.filePath,
8258
+ command: e.metadata?.command
8259
+ };
8260
+ }
8261
+ }
8262
+ function isTestLikeCommand(command) {
8263
+ if (!command)
8264
+ return false;
8265
+ return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
8266
+ }
8267
+ function safeRatio(num, den) {
8268
+ if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0)
8269
+ return 0;
8270
+ return num / den;
8271
+ }
8272
+ function round(value, digits = 4) {
8273
+ const factor = 10 ** digits;
8274
+ return Math.round(value * factor) / factor;
8275
+ }
8276
+ function computeSessionTurnCount(sessionEvents) {
8277
+ const turnIds = /* @__PURE__ */ new Set();
8278
+ for (const e of sessionEvents) {
8279
+ const turnId = e.metadata?.turnId;
8280
+ if (typeof turnId === "string" && turnId.length > 0)
8281
+ turnIds.add(turnId);
8282
+ }
8283
+ if (turnIds.size > 0)
8284
+ return turnIds.size;
8285
+ return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
8286
+ }
8287
+ function computeKpiMetrics(events, usefulRecallRate) {
8288
+ const prompts = events.filter((e) => e.eventType === "user_prompt");
8289
+ const promptCount = prompts.length;
8290
+ const memoryHitPrompts = prompts.filter((p) => p.metadata?.adherence?.checked).length;
8291
+ const memoryHitRate = round(safeRatio(memoryHitPrompts, promptCount));
8292
+ const sessions = /* @__PURE__ */ new Map();
8293
+ for (const e of events) {
8294
+ const arr = sessions.get(e.sessionId) || [];
8295
+ arr.push(e);
8296
+ sessions.set(e.sessionId, arr);
8297
+ }
8298
+ let sessionTurnTotal = 0;
8299
+ let sessionTurnSamples = 0;
8300
+ let firstValidEditMinutesTotal = 0;
8301
+ let firstValidEditSamples = 0;
8302
+ for (const sessionEvents of sessions.values()) {
8303
+ sessionEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
8304
+ const turns = computeSessionTurnCount(sessionEvents);
8305
+ if (turns > 0) {
8306
+ sessionTurnTotal += turns;
8307
+ sessionTurnSamples++;
8308
+ }
8309
+ const firstPrompt = sessionEvents.find((e) => e.eventType === "user_prompt");
8310
+ const firstEdit = sessionEvents.find((e) => {
8311
+ const payload = parseToolPayload(e);
8312
+ return payload?.toolName && isEditToolName(payload.toolName) && payload.success === true;
8313
+ });
8314
+ if (firstPrompt && firstEdit) {
8315
+ const minutes = (firstEdit.timestamp.getTime() - firstPrompt.timestamp.getTime()) / 6e4;
8316
+ if (minutes >= 0) {
8317
+ firstValidEditMinutesTotal += minutes;
8318
+ firstValidEditSamples++;
8319
+ }
8320
+ }
8321
+ }
8322
+ const avgCompletionTurns = round(safeRatio(sessionTurnTotal, sessionTurnSamples), 2);
8323
+ const timeToFirstValidEditMinutes = round(safeRatio(firstValidEditMinutesTotal, firstValidEditSamples), 2);
8324
+ const editActions = [];
8325
+ let testRunsAfterEdit = 0;
8326
+ let failedTestRunsAfterEdit = 0;
8327
+ for (const [sessionId, sessionEvents] of sessions.entries()) {
8328
+ const sorted = [...sessionEvents].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
8329
+ let seenEdit = false;
8330
+ for (const e of sorted) {
8331
+ const payload = parseToolPayload(e);
8332
+ if (!payload?.toolName)
8333
+ continue;
8334
+ if (isEditToolName(payload.toolName) && payload.success === true) {
8335
+ editActions.push({ sessionId, timestamp: e.timestamp.getTime(), filePath: payload.filePath });
8336
+ seenEdit = true;
8337
+ continue;
8338
+ }
8339
+ if (seenEdit && isTestLikeCommand(payload.command)) {
8340
+ testRunsAfterEdit++;
8341
+ if (payload.success === false)
8342
+ failedTestRunsAfterEdit++;
8343
+ }
8344
+ }
8345
+ }
8346
+ const THIRTY_MIN_MS = 30 * 60 * 1e3;
8347
+ let reworkCount = 0;
8348
+ const bySessionFile = /* @__PURE__ */ new Map();
8349
+ const sortedEdits = [...editActions].sort((a, b) => a.timestamp - b.timestamp);
8350
+ for (const edit of sortedEdits) {
8351
+ if (!edit.filePath)
8352
+ continue;
8353
+ const key = `${edit.sessionId}::${edit.filePath}`;
8354
+ const prev = bySessionFile.get(key);
8355
+ if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS) {
8356
+ reworkCount++;
8357
+ }
8358
+ bySessionFile.set(key, edit.timestamp);
8359
+ }
8360
+ const reworkRate = round(safeRatio(reworkCount, editActions.length));
8361
+ const postChangeFailureRate = round(safeRatio(failedTestRunsAfterEdit, testRunsAfterEdit));
8362
+ return {
8363
+ memoryHitRate,
8364
+ usefulRecallRate,
8365
+ avgCompletionTurns,
8366
+ timeToFirstValidEditMinutes,
8367
+ reworkRate,
8368
+ postChangeFailureRate
8369
+ };
8370
+ }
8061
8371
  statsRouter.get("/shared", async (c) => {
8062
8372
  const memoryService = getServiceFromQuery(c);
8063
8373
  try {
@@ -8337,6 +8647,129 @@ statsRouter.get("/retrieval-traces", async (c) => {
8337
8647
  await memoryService.shutdown();
8338
8648
  }
8339
8649
  });
8650
+ statsRouter.get("/kpi", async (c) => {
8651
+ const rawWindow = c.req.query("window") || "7d";
8652
+ const window = rawWindow === "24h" || rawWindow === "30d" ? rawWindow : "7d";
8653
+ const memoryService = getServiceFromQuery(c);
8654
+ try {
8655
+ await memoryService.initialize();
8656
+ const now = Date.now();
8657
+ const thresholds = loadKpiThresholds();
8658
+ const allEvents = await memoryService.getRecentEvents(2e4);
8659
+ const events = allEvents.filter((e) => inWindow(e, now, window));
8660
+ const helpfulness = await memoryService.getHelpfulnessStats();
8661
+ const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
8662
+ const metrics = computeKpiMetrics(events, usefulRecallRate);
8663
+ const windowMs = windowToMs(window);
8664
+ const prevEvents = allEvents.filter((e) => {
8665
+ const age = now - e.timestamp.getTime();
8666
+ return age > windowMs && age <= windowMs * 2;
8667
+ });
8668
+ const previousMetrics = computeKpiMetrics(prevEvents, usefulRecallRate);
8669
+ const deltas = {
8670
+ memoryHitRate: round(metrics.memoryHitRate - previousMetrics.memoryHitRate),
8671
+ usefulRecallRate: round(metrics.usefulRecallRate - previousMetrics.usefulRecallRate),
8672
+ avgCompletionTurns: round(metrics.avgCompletionTurns - previousMetrics.avgCompletionTurns, 2),
8673
+ timeToFirstValidEditMinutes: round(metrics.timeToFirstValidEditMinutes - previousMetrics.timeToFirstValidEditMinutes, 2),
8674
+ reworkRate: round(metrics.reworkRate - previousMetrics.reworkRate),
8675
+ postChangeFailureRate: round(metrics.postChangeFailureRate - previousMetrics.postChangeFailureRate)
8676
+ };
8677
+ const THIRTY_MIN_MS = 30 * 60 * 1e3;
8678
+ const trendWindowMs = 30 * 24 * 60 * 60 * 1e3;
8679
+ const trendEvents = allEvents.filter((e) => now - e.timestamp.getTime() <= trendWindowMs);
8680
+ const buckets = /* @__PURE__ */ new Map();
8681
+ for (const e of trendEvents) {
8682
+ const day = e.timestamp.toISOString().split("T")[0];
8683
+ const arr = buckets.get(day) || [];
8684
+ arr.push(e);
8685
+ buckets.set(day, arr);
8686
+ }
8687
+ const trendDaily = Array.from(buckets.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([date, dayEvents]) => {
8688
+ const dayPrompts = dayEvents.filter((e) => e.eventType === "user_prompt");
8689
+ const dayPromptCount = dayPrompts.length;
8690
+ const dayMemoryHit = dayPrompts.filter((p) => p.metadata?.adherence?.checked).length;
8691
+ const dayEdits = dayEvents.filter((e) => {
8692
+ const p = parseToolPayload(e);
8693
+ return Boolean(p?.toolName && isEditToolName(p.toolName) && p.success === true);
8694
+ });
8695
+ const dayEditActions = dayEdits.map((e) => {
8696
+ const p = parseToolPayload(e);
8697
+ return { sessionId: e.sessionId, timestamp: e.timestamp.getTime(), filePath: p?.filePath };
8698
+ }).filter((x) => Boolean(x.filePath));
8699
+ let dayReworkCount = 0;
8700
+ const dayBySessionFile = /* @__PURE__ */ new Map();
8701
+ for (const edit of dayEditActions) {
8702
+ const key = `${edit.sessionId}::${edit.filePath}`;
8703
+ const prev = dayBySessionFile.get(key);
8704
+ if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS)
8705
+ dayReworkCount++;
8706
+ dayBySessionFile.set(key, edit.timestamp);
8707
+ }
8708
+ const dayTests = dayEvents.filter((e) => {
8709
+ const p = parseToolPayload(e);
8710
+ return Boolean(p?.toolName && isTestLikeCommand(p.command));
8711
+ });
8712
+ const dayFailedTests = dayEvents.filter((e) => {
8713
+ const p = parseToolPayload(e);
8714
+ return Boolean(p?.toolName && isTestLikeCommand(p.command) && p.success === false);
8715
+ });
8716
+ const turnsBySession = /* @__PURE__ */ new Map();
8717
+ for (const e of dayEvents) {
8718
+ const arr = turnsBySession.get(e.sessionId) || [];
8719
+ arr.push(e);
8720
+ turnsBySession.set(e.sessionId, arr);
8721
+ }
8722
+ let dayTurnsTotal = 0;
8723
+ let dayTurnsSamples = 0;
8724
+ for (const sessionEvents of turnsBySession.values()) {
8725
+ const turns = computeSessionTurnCount(sessionEvents);
8726
+ if (turns > 0) {
8727
+ dayTurnsTotal += turns;
8728
+ dayTurnsSamples++;
8729
+ }
8730
+ }
8731
+ return {
8732
+ date,
8733
+ memoryHitRate: round(safeRatio(dayMemoryHit, dayPromptCount)),
8734
+ usefulRecallRate,
8735
+ reworkRate: round(safeRatio(dayReworkCount, dayEditActions.length)),
8736
+ postChangeFailureRate: round(safeRatio(dayFailedTests.length, dayTests.length)),
8737
+ avgCompletionTurns: round(safeRatio(dayTurnsTotal, dayTurnsSamples), 2)
8738
+ };
8739
+ });
8740
+ const alerts = [];
8741
+ if (metrics.usefulRecallRate < thresholds.usefulRecallRateMin) {
8742
+ alerts.push({ metric: "usefulRecallRate", level: "warn", message: "Useful recall rate is below threshold", value: metrics.usefulRecallRate, threshold: thresholds.usefulRecallRateMin });
8743
+ }
8744
+ if (metrics.reworkRate > thresholds.reworkRateMax) {
8745
+ alerts.push({ metric: "reworkRate", level: "warn", message: "Rework rate is above threshold", value: metrics.reworkRate, threshold: thresholds.reworkRateMax });
8746
+ }
8747
+ if (metrics.postChangeFailureRate > thresholds.postChangeFailureRateMax) {
8748
+ alerts.push({ metric: "postChangeFailureRate", level: "warn", message: "Post-change failure rate is above threshold", value: metrics.postChangeFailureRate, threshold: thresholds.postChangeFailureRateMax });
8749
+ }
8750
+ if (metrics.avgCompletionTurns > thresholds.avgCompletionTurnsMax) {
8751
+ alerts.push({ metric: "avgCompletionTurns", level: "warn", message: "Average completion turns is above threshold", value: metrics.avgCompletionTurns, threshold: thresholds.avgCompletionTurnsMax });
8752
+ }
8753
+ if (metrics.memoryHitRate < thresholds.memoryHitRateMin) {
8754
+ alerts.push({ metric: "memoryHitRate", level: "warn", message: "Memory hit rate is below threshold", value: metrics.memoryHitRate, threshold: thresholds.memoryHitRateMin });
8755
+ }
8756
+ return c.json({
8757
+ window,
8758
+ metrics,
8759
+ previousMetrics,
8760
+ deltas,
8761
+ trend: {
8762
+ daily: trendDaily
8763
+ },
8764
+ thresholds,
8765
+ alerts
8766
+ });
8767
+ } catch (error) {
8768
+ return c.json({ error: error.message }, 500);
8769
+ } finally {
8770
+ await memoryService.shutdown();
8771
+ }
8772
+ });
8340
8773
  statsRouter.post("/graduation/run", async (c) => {
8341
8774
  const memoryService = getServiceFromQuery(c);
8342
8775
  try {
@@ -8584,19 +9017,19 @@ turnsRouter.post("/backfill", async (c) => {
8584
9017
 
8585
9018
  // src/server/api/projects.ts
8586
9019
  import { Hono as Hono7 } from "hono";
8587
- import * as fs7 from "fs";
8588
- import * as path7 from "path";
9020
+ import * as fs8 from "fs";
9021
+ import * as path8 from "path";
8589
9022
  import * as os4 from "os";
8590
9023
  var projectsRouter = new Hono7();
8591
9024
  projectsRouter.get("/", async (c) => {
8592
9025
  try {
8593
- const projectsDir = path7.join(os4.homedir(), ".claude-code", "memory", "projects");
8594
- if (!fs7.existsSync(projectsDir)) {
9026
+ const projectsDir = path8.join(os4.homedir(), ".claude-code", "memory", "projects");
9027
+ if (!fs8.existsSync(projectsDir)) {
8595
9028
  return c.json({ projects: [] });
8596
9029
  }
8597
- const projectHashes = fs7.readdirSync(projectsDir).filter((name) => {
8598
- const fullPath = path7.join(projectsDir, name);
8599
- return fs7.statSync(fullPath).isDirectory();
9030
+ const projectHashes = fs8.readdirSync(projectsDir).filter((name) => {
9031
+ const fullPath = path8.join(projectsDir, name);
9032
+ return fs8.statSync(fullPath).isDirectory();
8600
9033
  });
8601
9034
  const registry = loadSessionRegistry();
8602
9035
  const hashToPath = /* @__PURE__ */ new Map();
@@ -8606,17 +9039,17 @@ projectsRouter.get("/", async (c) => {
8606
9039
  }
8607
9040
  }
8608
9041
  const projects = projectHashes.map((hash) => {
8609
- const dirPath = path7.join(projectsDir, hash);
8610
- const dbPath = path7.join(dirPath, "events.sqlite");
9042
+ const dirPath = path8.join(projectsDir, hash);
9043
+ const dbPath = path8.join(dirPath, "events.sqlite");
8611
9044
  let dbSize = 0;
8612
- if (fs7.existsSync(dbPath)) {
8613
- dbSize = fs7.statSync(dbPath).size;
9045
+ if (fs8.existsSync(dbPath)) {
9046
+ dbSize = fs8.statSync(dbPath).size;
8614
9047
  }
8615
9048
  const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
8616
9049
  return {
8617
9050
  hash,
8618
9051
  projectPath,
8619
- projectName: path7.basename(projectPath),
9052
+ projectName: path8.basename(projectPath),
8620
9053
  dbSize,
8621
9054
  dbSizeHuman: formatBytes(dbSize)
8622
9055
  };
@@ -8741,7 +9174,7 @@ function buildPrompt(statsContext, memoryContext, history, currentMessage) {
8741
9174
  return parts.join("\n");
8742
9175
  }
8743
9176
  function streamClaudeResponse(prompt, stream) {
8744
- return new Promise((resolve4, reject) => {
9177
+ return new Promise((resolve5, reject) => {
8745
9178
  const proc = spawn("claude", [
8746
9179
  "-p",
8747
9180
  "--output-format",
@@ -8813,7 +9246,7 @@ function streamClaudeResponse(prompt, stream) {
8813
9246
  if (code !== 0 && code !== null) {
8814
9247
  reject(new Error(`Claude CLI exited with code ${code}`));
8815
9248
  } else {
8816
- resolve4();
9249
+ resolve5();
8817
9250
  }
8818
9251
  });
8819
9252
  });
@@ -8870,14 +9303,14 @@ app.use("/*", cors());
8870
9303
  app.use("/*", logger());
8871
9304
  app.route("/api", apiRouter);
8872
9305
  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)) {
9306
+ var uiPath = path9.join(__dirname, "../../dist/ui");
9307
+ if (fs9.existsSync(uiPath)) {
8875
9308
  app.use("/*", serveStatic({ root: uiPath }));
8876
9309
  }
8877
9310
  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"));
9311
+ const indexPath = path9.join(uiPath, "index.html");
9312
+ if (fs9.existsSync(indexPath)) {
9313
+ return c.html(fs9.readFileSync(indexPath, "utf-8"));
8881
9314
  }
8882
9315
  return c.text('UI not built. Run "npm run build:ui" first.', 404);
8883
9316
  });
@@ -9189,28 +9622,28 @@ var MongoSyncWorker = class {
9189
9622
  };
9190
9623
 
9191
9624
  // src/cli/index.ts
9192
- var CLAUDE_SETTINGS_PATH = path9.join(os6.homedir(), ".claude", "settings.json");
9625
+ var CLAUDE_SETTINGS_PATH = path10.join(os6.homedir(), ".claude", "settings.json");
9193
9626
  function getPluginPath() {
9194
9627
  const possiblePaths = [
9195
- path9.join(__dirname, ".."),
9628
+ path10.join(__dirname, ".."),
9196
9629
  // When running from dist/cli
9197
- path9.join(__dirname, "../..", "dist"),
9630
+ path10.join(__dirname, "../..", "dist"),
9198
9631
  // When running from src
9199
- path9.join(process.cwd(), "dist")
9632
+ path10.join(process.cwd(), "dist")
9200
9633
  // Current working directory
9201
9634
  ];
9202
9635
  for (const p of possiblePaths) {
9203
- const hooksPath = path9.join(p, "hooks", "user-prompt-submit.js");
9204
- if (fs9.existsSync(hooksPath)) {
9636
+ const hooksPath = path10.join(p, "hooks", "user-prompt-submit.js");
9637
+ if (fs10.existsSync(hooksPath)) {
9205
9638
  return p;
9206
9639
  }
9207
9640
  }
9208
- return path9.join(os6.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
9641
+ return path10.join(os6.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
9209
9642
  }
9210
9643
  function loadClaudeSettings() {
9211
9644
  try {
9212
- if (fs9.existsSync(CLAUDE_SETTINGS_PATH)) {
9213
- const content = fs9.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
9645
+ if (fs10.existsSync(CLAUDE_SETTINGS_PATH)) {
9646
+ const content = fs10.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
9214
9647
  return JSON.parse(content);
9215
9648
  }
9216
9649
  } catch (error) {
@@ -9219,13 +9652,13 @@ function loadClaudeSettings() {
9219
9652
  return {};
9220
9653
  }
9221
9654
  function saveClaudeSettings(settings) {
9222
- const dir = path9.dirname(CLAUDE_SETTINGS_PATH);
9223
- if (!fs9.existsSync(dir)) {
9224
- fs9.mkdirSync(dir, { recursive: true });
9655
+ const dir = path10.dirname(CLAUDE_SETTINGS_PATH);
9656
+ if (!fs10.existsSync(dir)) {
9657
+ fs10.mkdirSync(dir, { recursive: true });
9225
9658
  }
9226
9659
  const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
9227
- fs9.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
9228
- fs9.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
9660
+ fs10.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
9661
+ fs10.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
9229
9662
  }
9230
9663
  var REQUIRED_HOOK_FILES = [
9231
9664
  "user-prompt-submit.js",
@@ -9247,7 +9680,7 @@ function getHooksConfig(pluginPath) {
9247
9680
  hooks: [
9248
9681
  {
9249
9682
  type: "command",
9250
- command: `node ${path9.join(pluginPath, "hooks", fileName)}`
9683
+ command: `node ${path10.join(pluginPath, "hooks", fileName)}`
9251
9684
  }
9252
9685
  ]
9253
9686
  }
@@ -9261,12 +9694,12 @@ function getHooksConfig(pluginPath) {
9261
9694
  };
9262
9695
  }
9263
9696
  var program = new Command();
9264
- program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.18");
9697
+ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.20");
9265
9698
  program.command("install").description("Install hooks into Claude Code settings").option("--path <path>", "Custom plugin path (defaults to auto-detect)").action(async (options) => {
9266
9699
  try {
9267
9700
  const pluginPath = options.path || getPluginPath();
9268
9701
  const missingHooks = REQUIRED_HOOK_FILES.filter(
9269
- (file) => !fs9.existsSync(path9.join(pluginPath, "hooks", file))
9702
+ (file) => !fs10.existsSync(path10.join(pluginPath, "hooks", file))
9270
9703
  );
9271
9704
  if (missingHooks.length > 0) {
9272
9705
  console.error(`
@@ -9343,7 +9776,7 @@ program.command("status").description("Check plugin installation status").action
9343
9776
  console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
9344
9777
  console.log(` Stop: ${hasStopHook ? "\u2705 Installed" : "\u274C Not installed"}`);
9345
9778
  console.log(` SessionEnd: ${hasSessionEndHook ? "\u2705 Installed" : "\u274C Not installed"}`);
9346
- const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs9.existsSync(path9.join(pluginPath, "hooks", file)));
9779
+ const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs10.existsSync(path10.join(pluginPath, "hooks", file)));
9347
9780
  console.log(`
9348
9781
  Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
9349
9782
  console.log(` Path: ${pluginPath}`);
@@ -9475,7 +9908,7 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
9475
9908
  const projectPath = options.project || process.cwd();
9476
9909
  const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
9477
9910
  const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
9478
- const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path9.basename(projectPath);
9911
+ const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path10.basename(projectPath);
9479
9912
  const direction = String(options.direction || "both").toLowerCase();
9480
9913
  if (!mongoUri || !mongoDb) {
9481
9914
  console.error("\n\u274C MongoDB sync is not configured.");
@@ -9487,14 +9920,14 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
9487
9920
  process.exit(1);
9488
9921
  }
9489
9922
  const storagePath = getProjectStoragePath(projectPath);
9490
- if (!fs9.existsSync(storagePath)) {
9491
- fs9.mkdirSync(storagePath, { recursive: true });
9923
+ if (!fs10.existsSync(storagePath)) {
9924
+ fs10.mkdirSync(storagePath, { recursive: true });
9492
9925
  }
9493
9926
  const batchSizeParsed = parseInt(options.batchSize, 10);
9494
9927
  const intervalParsed = parseInt(options.interval, 10);
9495
9928
  const batchSize = Number.isFinite(batchSizeParsed) && batchSizeParsed > 0 ? batchSizeParsed : 500;
9496
9929
  const intervalMs = Number.isFinite(intervalParsed) && intervalParsed > 0 ? intervalParsed : 3e4;
9497
- const sqliteStore = new SQLiteEventStore(path9.join(storagePath, "events.sqlite"));
9930
+ const sqliteStore = new SQLiteEventStore(path10.join(storagePath, "events.sqlite"));
9498
9931
  const worker = new MongoSyncWorker(sqliteStore, {
9499
9932
  uri: mongoUri,
9500
9933
  dbName: mongoDb,
@@ -9553,7 +9986,7 @@ function renderProgress(event) {
9553
9986
  break;
9554
9987
  case "session-start": {
9555
9988
  const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
9556
- const sessionName = path9.basename(event.filePath, ".jsonl").slice(0, 8);
9989
+ const sessionName = path10.basename(event.filePath, ".jsonl").slice(0, 8);
9557
9990
  process.stdout.write(
9558
9991
  `\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
9559
9992
  );
@@ -9628,9 +10061,9 @@ async function listMarkdownFiles(root) {
9628
10061
  const stack = [root];
9629
10062
  while (stack.length > 0) {
9630
10063
  const dir = stack.pop();
9631
- const entries = await fs9.promises.readdir(dir, { withFileTypes: true });
10064
+ const entries = await fs10.promises.readdir(dir, { withFileTypes: true });
9632
10065
  for (const e of entries) {
9633
- const full = path9.join(dir, e.name);
10066
+ const full = path10.join(dir, e.name);
9634
10067
  if (e.isDirectory())
9635
10068
  stack.push(full);
9636
10069
  else if (e.isFile() && e.name.endsWith(".md") && e.name !== "_index.md")
@@ -9640,8 +10073,8 @@ async function listMarkdownFiles(root) {
9640
10073
  return out.sort();
9641
10074
  }
9642
10075
  function deriveNamespaceCategory(sourceRoot, filePath) {
9643
- const rel = path9.relative(sourceRoot, filePath);
9644
- const dirSeg = path9.dirname(rel).split(path9.sep).filter(Boolean);
10076
+ const rel = path10.relative(sourceRoot, filePath);
10077
+ const dirSeg = path10.dirname(rel).split(path10.sep).filter(Boolean);
9645
10078
  if (dirSeg.length >= 2) {
9646
10079
  const namespace = sanitizeSegment3(dirSeg[0], "default");
9647
10080
  const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment3(s, "uncategorized"));
@@ -9660,10 +10093,10 @@ function extractImportEvidence(markdown) {
9660
10093
  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
10094
  const projectPath = options.project || process.cwd();
9662
10095
  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 });
10096
+ const sourceRoot = path10.resolve(sourceDir || options.out || projectPath);
10097
+ const repoPath = path10.resolve(options.repo || projectPath);
10098
+ if (!fs10.existsSync(sourceRoot)) {
10099
+ fs10.mkdirSync(sourceRoot, { recursive: true });
9667
10100
  }
9668
10101
  const service = getMemoryServiceForProject(projectPath);
9669
10102
  try {
@@ -9673,7 +10106,7 @@ program.command("organize-import [sourceDir]").description("Import existing mark
9673
10106
  const hasMarkdown = files.length > 0;
9674
10107
  const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || !hasMarkdown && options.bootstrapIfEmpty);
9675
10108
  if (shouldBootstrap) {
9676
- const outDir = path9.resolve(options.out || path9.join(sourceRoot, "bootstrap-kb"));
10109
+ const outDir = path10.resolve(options.out || path10.join(sourceRoot, "bootstrap-kb"));
9677
10110
  const since = options.since || "180 days ago";
9678
10111
  const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1e3;
9679
10112
  console.log("\n\u{1F9E0} Bootstrapping markdown knowledge base...");
@@ -9712,13 +10145,13 @@ program.command("organize-import [sourceDir]").description("Import existing mark
9712
10145
  let imported = 0;
9713
10146
  let skipped = 0;
9714
10147
  for (const file of targets) {
9715
- const text = await fs9.promises.readFile(file, "utf8");
10148
+ const text = await fs10.promises.readFile(file, "utf8");
9716
10149
  if (!text.trim()) {
9717
10150
  skipped += 1;
9718
10151
  continue;
9719
10152
  }
9720
10153
  const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
9721
- const rel = path9.relative(activeSourceRoot, file);
10154
+ const rel = path10.relative(activeSourceRoot, file);
9722
10155
  const evidence = extractImportEvidence(text);
9723
10156
  if (options.dryRun) {
9724
10157
  console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join("/")} confidence=${evidence.confidence || "n/a"} sources=${evidence.sources.length}`);
@@ -9753,9 +10186,12 @@ program.command("organize-import [sourceDir]").description("Import existing mark
9753
10186
  process.exit(1);
9754
10187
  }
9755
10188
  });
9756
- program.command("import").description("Import existing Claude Code conversation history").option("-p, --project <path>", "Import from specific project path").option("-s, --session <file>", "Import specific session file (JSONL)").option("-a, --all", "Import all sessions from all projects").option("-l, --limit <number>", "Limit messages per session").option("-f, --force", "Force reimport: delete existing events and reimport with turn_id grouping").option("-v, --verbose", "Show detailed progress").action(async (options) => {
10189
+ program.command("import").description("Import existing Claude Code conversation history").option("-p, --project <path>", "Import from specific project path").option("-s, --session <file>", "Import specific session file (JSONL)").option("-a, --all", "Import all sessions from all projects").option("-l, --limit <number>", "Limit messages per session").option("-f, --force", "Force reimport: delete existing events and reimport with turn_id grouping").option("--embedding-model <name>", "Embedding model override (default: jinaai/jina-embeddings-v5-text-nano, or env CLAUDE_MEMORY_EMBEDDING_MODEL)").option("-v, --verbose", "Show detailed progress").action(async (options) => {
9757
10190
  const startTime = Date.now();
9758
10191
  const targetProjectPath = options.project || process.cwd();
10192
+ if (options.embeddingModel) {
10193
+ process.env.CLAUDE_MEMORY_EMBEDDING_MODEL = options.embeddingModel;
10194
+ }
9759
10195
  const service = getMemoryServiceForProject(targetProjectPath);
9760
10196
  const importer = createSessionHistoryImporter(service);
9761
10197
  const importOpts = {
@@ -9767,7 +10203,16 @@ program.command("import").description("Import existing Claude Code conversation
9767
10203
  try {
9768
10204
  console.log("\n\u23F3 Initializing memory service...");
9769
10205
  await service.initialize();
9770
- console.log(" \u2705 Ready\n");
10206
+ console.log(` \u2705 Ready (embedder: ${service.getEmbeddingModelName()})
10207
+ `);
10208
+ const migration = await service.ensureEmbeddingModelForImport({ autoMigrate: true });
10209
+ if (migration.changed) {
10210
+ console.log("\u{1F501} Embedding model migration detected/required");
10211
+ console.log(` Previous: ${migration.previousModel || "legacy-unknown"}`);
10212
+ console.log(` Current: ${migration.currentModel}`);
10213
+ console.log(` Re-queued embeddings: ${migration.enqueued}`);
10214
+ console.log(" (Import will continue and process embeddings with the new model)\n");
10215
+ }
9771
10216
  if (options.force) {
9772
10217
  console.log("\u{1F504} Force mode: existing events will be deleted and reimported with turn_id grouping\n");
9773
10218
  }
@@ -9790,6 +10235,14 @@ program.command("import").description("Import existing Claude Code conversation
9790
10235
  const globalService = getDefaultMemoryService();
9791
10236
  const globalImporter = createSessionHistoryImporter(globalService);
9792
10237
  await globalService.initialize();
10238
+ console.log(` \u2705 Global service ready (embedder: ${globalService.getEmbeddingModelName()})`);
10239
+ const globalMigration = await globalService.ensureEmbeddingModelForImport({ autoMigrate: true });
10240
+ if (globalMigration.changed) {
10241
+ console.log("\u{1F501} Global embedding migration detected");
10242
+ console.log(` Previous: ${globalMigration.previousModel || "legacy-unknown"}`);
10243
+ console.log(` Current: ${globalMigration.currentModel}`);
10244
+ console.log(` Re-queued embeddings: ${globalMigration.enqueued}`);
10245
+ }
9793
10246
  result = await globalImporter.importAll(importOpts);
9794
10247
  console.log("\n\u{1F9E0} Processing embeddings...");
9795
10248
  const embedCount2 = await globalService.processPendingEmbeddings();