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.
- package/config/kpi-thresholds.json +7 -0
- package/dist/cli/index.js +532 -79
- package/dist/cli/index.js.map +3 -3
- package/dist/core/index.js +49 -4
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +140 -3
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +140 -3
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +140 -3
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +140 -3
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +379 -34
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +467 -34
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +474 -41
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +140 -3
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +362 -4
- package/dist/ui/index.html +90 -0
- package/dist/ui/style.css +41 -0
- package/memory/_index.md +3 -0
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/cli/index.ts +23 -1
- package/src/core/embedder.ts +3 -2
- package/src/core/sqlite-event-store.ts +32 -0
- package/src/core/types.ts +2 -2
- package/src/core/vector-store.ts +20 -0
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +7 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +119 -2
- package/src/ui/app.js +362 -4
- package/src/ui/index.html +90 -0
- 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
|
|
20
|
-
import * as
|
|
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(
|
|
84
|
+
function createDatabase(path11, options) {
|
|
85
85
|
if (options?.readOnly) {
|
|
86
|
-
return new duckdb.Database(
|
|
86
|
+
return new duckdb.Database(path11, { access_mode: "READ_ONLY" });
|
|
87
87
|
}
|
|
88
|
-
return new duckdb.Database(
|
|
88
|
+
return new duckdb.Database(path11);
|
|
89
89
|
}
|
|
90
90
|
function dbRun(db, sql, params = []) {
|
|
91
|
-
return new Promise((
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
resolve5();
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
function dbAll(db, sql, params = []) {
|
|
110
|
-
return new Promise((
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
resolve5(convertBigInts(rows || []));
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
function dbClose(db) {
|
|
129
|
-
return new Promise((
|
|
129
|
+
return new Promise((resolve5, reject) => {
|
|
130
130
|
db.close((err) => {
|
|
131
131
|
if (err)
|
|
132
132
|
reject(err);
|
|
133
133
|
else
|
|
134
|
-
|
|
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(
|
|
773
|
-
const dir = nodePath.dirname(
|
|
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(
|
|
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((
|
|
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 = "
|
|
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(([
|
|
3454
|
-
const actual =
|
|
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
|
-
|
|
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
|
|
7770
|
-
import * as
|
|
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
|
|
8588
|
-
import * as
|
|
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 =
|
|
8594
|
-
if (!
|
|
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 =
|
|
8598
|
-
const fullPath =
|
|
8599
|
-
return
|
|
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 =
|
|
8610
|
-
const dbPath =
|
|
9042
|
+
const dirPath = path8.join(projectsDir, hash);
|
|
9043
|
+
const dbPath = path8.join(dirPath, "events.sqlite");
|
|
8611
9044
|
let dbSize = 0;
|
|
8612
|
-
if (
|
|
8613
|
-
dbSize =
|
|
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:
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
8874
|
-
if (
|
|
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 =
|
|
8879
|
-
if (
|
|
8880
|
-
return c.html(
|
|
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 =
|
|
9625
|
+
var CLAUDE_SETTINGS_PATH = path10.join(os6.homedir(), ".claude", "settings.json");
|
|
9193
9626
|
function getPluginPath() {
|
|
9194
9627
|
const possiblePaths = [
|
|
9195
|
-
|
|
9628
|
+
path10.join(__dirname, ".."),
|
|
9196
9629
|
// When running from dist/cli
|
|
9197
|
-
|
|
9630
|
+
path10.join(__dirname, "../..", "dist"),
|
|
9198
9631
|
// When running from src
|
|
9199
|
-
|
|
9632
|
+
path10.join(process.cwd(), "dist")
|
|
9200
9633
|
// Current working directory
|
|
9201
9634
|
];
|
|
9202
9635
|
for (const p of possiblePaths) {
|
|
9203
|
-
const hooksPath =
|
|
9204
|
-
if (
|
|
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
|
|
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 (
|
|
9213
|
-
const content =
|
|
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 =
|
|
9223
|
-
if (!
|
|
9224
|
-
|
|
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
|
-
|
|
9228
|
-
|
|
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 ${
|
|
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.
|
|
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) => !
|
|
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) =>
|
|
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 ||
|
|
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 (!
|
|
9491
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
10064
|
+
const entries = await fs10.promises.readdir(dir, { withFileTypes: true });
|
|
9632
10065
|
for (const e of entries) {
|
|
9633
|
-
const full =
|
|
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 =
|
|
9644
|
-
const dirSeg =
|
|
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 =
|
|
9664
|
-
const repoPath =
|
|
9665
|
-
if (!
|
|
9666
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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();
|