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