memorix 0.9.5 → 0.9.7
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/CHANGELOG.md +14 -0
- package/dist/cli/index.js +159 -78
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +124 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.9.7] — 2026-02-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Claude Code hooks never triggering auto-memory** — Claude Code sends `hook_event_name` (snake_case) but the normalizer expected `hookEventName` (camelCase). This caused **every event** (SessionStart, UserPromptSubmit, PostToolUse, PreCompact, Stop) to be misidentified as `post_tool`, breaking event routing, prompt extraction, memory injection, and session tracking. Also fixed `session_id` → `sessionId` and `tool_response` → `toolResult` field mappings.
|
|
9
|
+
- **Empty content extraction from Claude Code tool events** — `extractContent()` now unpacks `toolInput` fields (Bash commands, Write file content, etc.) when no other content is available. Previously tool events produced empty or near-empty content strings.
|
|
10
|
+
- **User prompts silently dropped** — `MIN_STORE_LENGTH=100` was too high for typical user prompts. Added `MIN_PROMPT_LENGTH=20` specifically for `user_prompt` events.
|
|
11
|
+
- **Post-tool events too aggressively filtered** — Tool events with substantial content (>200 chars) are now stored even without keyword pattern matches.
|
|
12
|
+
|
|
13
|
+
## [0.9.6] — 2026-02-25
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Cross-IDE project identity fragmentation** — Data was stored in per-project subdirectories (`~/.memorix/data/<projectId>/`), but different IDEs often detected different projectIds for the same repo (e.g. `placeholder/repo` vs `local/repo` vs `local/Kiro`). This caused observations to silently split across directories, making cross-IDE relay unreliable. Now **all data is stored in a single flat directory** (`~/.memorix/data/`). projectId is metadata only, not used for directory partitioning. Existing per-project subdirectories are automatically merged on first startup (IDs remapped, graphs deduplicated, subdirs backed up to `.migrated-subdirs/`).
|
|
17
|
+
- **`scope: 'project'` parameter now works** — Previously accepted but ignored. Now properly filters search results by the current project's ID via Orama where-clause.
|
|
18
|
+
|
|
5
19
|
## [0.9.5] — 2026-02-25
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/dist/cli/index.js
CHANGED
|
@@ -112,7 +112,7 @@ __export(persistence_exports, {
|
|
|
112
112
|
loadIdCounter: () => loadIdCounter,
|
|
113
113
|
loadObservationsJson: () => loadObservationsJson,
|
|
114
114
|
loadSessionsJson: () => loadSessionsJson,
|
|
115
|
-
|
|
115
|
+
migrateSubdirsToFlat: () => migrateSubdirsToFlat,
|
|
116
116
|
saveGraphJsonl: () => saveGraphJsonl,
|
|
117
117
|
saveIdCounter: () => saveIdCounter,
|
|
118
118
|
saveObservationsJson: () => saveObservationsJson,
|
|
@@ -121,18 +121,13 @@ __export(persistence_exports, {
|
|
|
121
121
|
import { promises as fs2 } from "fs";
|
|
122
122
|
import path3 from "path";
|
|
123
123
|
import os from "os";
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
async function getProjectDataDir(projectId, baseDir) {
|
|
128
|
-
if (projectId === "__invalid__") {
|
|
124
|
+
async function getProjectDataDir(_projectId, baseDir) {
|
|
125
|
+
if (_projectId === "__invalid__") {
|
|
129
126
|
throw new Error("Cannot create data directory for invalid project");
|
|
130
127
|
}
|
|
131
128
|
const base = baseDir ?? DEFAULT_DATA_DIR;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
await fs2.mkdir(dataDir, { recursive: true });
|
|
135
|
-
return dataDir;
|
|
129
|
+
await fs2.mkdir(base, { recursive: true });
|
|
130
|
+
return base;
|
|
136
131
|
}
|
|
137
132
|
function getBaseDataDir(baseDir) {
|
|
138
133
|
return baseDir ?? DEFAULT_DATA_DIR;
|
|
@@ -146,78 +141,141 @@ async function listProjectDirs(baseDir) {
|
|
|
146
141
|
return [];
|
|
147
142
|
}
|
|
148
143
|
}
|
|
149
|
-
async function
|
|
144
|
+
async function migrateSubdirsToFlat(baseDir) {
|
|
150
145
|
const base = baseDir ?? DEFAULT_DATA_DIR;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
let sourceObsPath = null;
|
|
146
|
+
await fs2.mkdir(base, { recursive: true });
|
|
147
|
+
let entries;
|
|
154
148
|
try {
|
|
155
|
-
await fs2.
|
|
156
|
-
sourceObsPath = globalObsPath;
|
|
149
|
+
entries = await fs2.readdir(base, { withFileTypes: true });
|
|
157
150
|
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const dataDirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => path3.join(base, e.name));
|
|
154
|
+
if (dataDirs.length === 0) return false;
|
|
155
|
+
const subdirData = [];
|
|
156
|
+
for (const dir of dataDirs) {
|
|
157
|
+
const obsPath = path3.join(dir, "observations.json");
|
|
158
158
|
try {
|
|
159
|
-
await fs2.
|
|
160
|
-
|
|
159
|
+
const data = await fs2.readFile(obsPath, "utf-8");
|
|
160
|
+
const obs = JSON.parse(data);
|
|
161
|
+
if (Array.isArray(obs) && obs.length > 0) {
|
|
162
|
+
let graph = { entities: [], relations: [] };
|
|
163
|
+
try {
|
|
164
|
+
const graphData = await fs2.readFile(path3.join(dir, "graph.jsonl"), "utf-8");
|
|
165
|
+
const lines = graphData.split("\n").filter((l) => l.trim());
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const item = JSON.parse(line);
|
|
168
|
+
if (item.type === "entity") graph.entities.push(item);
|
|
169
|
+
if (item.type === "relation") graph.relations.push(item);
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
subdirData.push({ dir, obs, graph });
|
|
174
|
+
}
|
|
161
175
|
} catch {
|
|
162
|
-
return false;
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
|
-
|
|
178
|
+
if (subdirData.length === 0) return false;
|
|
179
|
+
let baseObs = [];
|
|
166
180
|
try {
|
|
167
|
-
const data = await fs2.readFile(
|
|
168
|
-
|
|
169
|
-
if (!Array.isArray(
|
|
181
|
+
const data = await fs2.readFile(path3.join(base, "observations.json"), "utf-8");
|
|
182
|
+
baseObs = JSON.parse(data);
|
|
183
|
+
if (!Array.isArray(baseObs)) baseObs = [];
|
|
170
184
|
} catch {
|
|
171
|
-
return false;
|
|
172
185
|
}
|
|
173
|
-
|
|
174
|
-
const projectObsPath = path3.join(projectDir2, "observations.json");
|
|
175
|
-
let projectObs = [];
|
|
186
|
+
let baseGraph = { entities: [], relations: [] };
|
|
176
187
|
try {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
const graphData = await fs2.readFile(path3.join(base, "graph.jsonl"), "utf-8");
|
|
189
|
+
const lines = graphData.split("\n").filter((l) => l.trim());
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
const item = JSON.parse(line);
|
|
192
|
+
if (item.type === "entity") baseGraph.entities.push(item);
|
|
193
|
+
if (item.type === "relation") baseGraph.relations.push(item);
|
|
194
|
+
}
|
|
180
195
|
} catch {
|
|
181
196
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
const allObs = [...baseObs];
|
|
198
|
+
for (const { obs } of subdirData) {
|
|
199
|
+
for (const o of obs) {
|
|
200
|
+
const isDuplicate = allObs.some(
|
|
201
|
+
(existing) => existing.title === o.title && existing.createdAt === o.createdAt
|
|
202
|
+
);
|
|
203
|
+
if (!isDuplicate) {
|
|
204
|
+
allObs.push(o);
|
|
205
|
+
}
|
|
190
206
|
}
|
|
191
207
|
}
|
|
192
|
-
|
|
193
|
-
for (
|
|
194
|
-
|
|
208
|
+
allObs.sort((a, b) => (a.createdAt || "").localeCompare(b.createdAt || ""));
|
|
209
|
+
for (let i = 0; i < allObs.length; i++) {
|
|
210
|
+
allObs[i].id = i + 1;
|
|
195
211
|
}
|
|
196
|
-
|
|
197
|
-
for (const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
} catch {
|
|
212
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const e of baseGraph.entities) entityMap.set(e.name, e);
|
|
214
|
+
for (const { graph } of subdirData) {
|
|
215
|
+
for (const e of graph.entities) {
|
|
216
|
+
if (!entityMap.has(e.name)) {
|
|
217
|
+
entityMap.set(e.name, e);
|
|
218
|
+
} else {
|
|
219
|
+
const existing = entityMap.get(e.name);
|
|
220
|
+
const obsSet = /* @__PURE__ */ new Set([...existing.observations || [], ...e.observations || []]);
|
|
221
|
+
existing.observations = [...obsSet];
|
|
207
222
|
}
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
|
-
const
|
|
225
|
+
const relationSet = /* @__PURE__ */ new Set();
|
|
226
|
+
const mergedRelations = [];
|
|
227
|
+
for (const rel of [...baseGraph.relations, ...subdirData.flatMap((d) => d.graph.relations)]) {
|
|
228
|
+
const key = `${rel.from}|${rel.to}|${rel.relationType}`;
|
|
229
|
+
if (!relationSet.has(key)) {
|
|
230
|
+
relationSet.add(key);
|
|
231
|
+
mergedRelations.push(rel);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
await fs2.writeFile(path3.join(base, "observations.json"), JSON.stringify(allObs, null, 2), "utf-8");
|
|
211
235
|
await fs2.writeFile(
|
|
212
|
-
path3.join(
|
|
213
|
-
JSON.stringify({ nextId:
|
|
236
|
+
path3.join(base, "counter.json"),
|
|
237
|
+
JSON.stringify({ nextId: allObs.length + 1 }),
|
|
214
238
|
"utf-8"
|
|
215
239
|
);
|
|
216
|
-
|
|
217
|
-
|
|
240
|
+
const graphLines = [
|
|
241
|
+
...[...entityMap.values()].map((e) => JSON.stringify({ type: "entity", name: e.name, entityType: e.entityType, observations: e.observations })),
|
|
242
|
+
...mergedRelations.map((r) => JSON.stringify({ type: "relation", from: r.from, to: r.to, relationType: r.relationType }))
|
|
243
|
+
];
|
|
244
|
+
if (graphLines.length > 0) {
|
|
245
|
+
await fs2.writeFile(path3.join(base, "graph.jsonl"), graphLines.join("\n"), "utf-8");
|
|
246
|
+
}
|
|
247
|
+
let allSessions = [];
|
|
248
|
+
try {
|
|
249
|
+
const data = await fs2.readFile(path3.join(base, "sessions.json"), "utf-8");
|
|
250
|
+
allSessions = JSON.parse(data);
|
|
251
|
+
if (!Array.isArray(allSessions)) allSessions = [];
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
for (const { dir } of subdirData) {
|
|
218
255
|
try {
|
|
219
|
-
await fs2.
|
|
220
|
-
|
|
256
|
+
const data = await fs2.readFile(path3.join(dir, "sessions.json"), "utf-8");
|
|
257
|
+
const sessions = JSON.parse(data);
|
|
258
|
+
if (Array.isArray(sessions)) allSessions.push(...sessions);
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (allSessions.length > 0) {
|
|
263
|
+
await fs2.writeFile(path3.join(base, "sessions.json"), JSON.stringify(allSessions, null, 2), "utf-8");
|
|
264
|
+
}
|
|
265
|
+
const backupDir = path3.join(base, ".migrated-subdirs");
|
|
266
|
+
await fs2.mkdir(backupDir, { recursive: true });
|
|
267
|
+
for (const { dir } of subdirData) {
|
|
268
|
+
const dirName = path3.basename(dir);
|
|
269
|
+
try {
|
|
270
|
+
await fs2.rename(dir, path3.join(backupDir, dirName));
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const dir of dataDirs) {
|
|
275
|
+
const dirName = path3.basename(dir);
|
|
276
|
+
try {
|
|
277
|
+
await fs2.access(dir);
|
|
278
|
+
await fs2.rename(dir, path3.join(backupDir, dirName));
|
|
221
279
|
} catch {
|
|
222
280
|
}
|
|
223
281
|
}
|
|
@@ -5384,10 +5442,10 @@ async function createMemorixServer(cwd, existingServer) {
|
|
|
5384
5442
|
);
|
|
5385
5443
|
}
|
|
5386
5444
|
try {
|
|
5387
|
-
const {
|
|
5388
|
-
const migrated = await
|
|
5445
|
+
const { migrateSubdirsToFlat: migrateSubdirsToFlat2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
|
|
5446
|
+
const migrated = await migrateSubdirsToFlat2();
|
|
5389
5447
|
if (migrated) {
|
|
5390
|
-
console.error(`[memorix] Migrated
|
|
5448
|
+
console.error(`[memorix] Migrated per-project subdirectories into flat storage`);
|
|
5391
5449
|
}
|
|
5392
5450
|
} catch {
|
|
5393
5451
|
}
|
|
@@ -5610,10 +5668,10 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
|
|
|
5610
5668
|
maxTokens: safeMaxTokens,
|
|
5611
5669
|
since,
|
|
5612
5670
|
until,
|
|
5613
|
-
//
|
|
5614
|
-
//
|
|
5615
|
-
//
|
|
5616
|
-
projectId: void 0
|
|
5671
|
+
// All data is in a single flat directory. projectId is metadata only.
|
|
5672
|
+
// scope: 'project' → filter by current projectId in Orama
|
|
5673
|
+
// scope: 'global' or omitted → search all observations (default)
|
|
5674
|
+
projectId: scope === "project" ? project.id : void 0
|
|
5617
5675
|
});
|
|
5618
5676
|
let text = result.formatted;
|
|
5619
5677
|
if (!syncAdvisoryShown && syncAdvisory) {
|
|
@@ -6836,9 +6894,8 @@ var init_sync = __esm({
|
|
|
6836
6894
|
function detectAgent(payload) {
|
|
6837
6895
|
if ("agent_action_name" in payload) return "windsurf";
|
|
6838
6896
|
if ("hook_event_name" in payload && "conversation_id" in payload) return "cursor";
|
|
6839
|
-
if ("
|
|
6840
|
-
|
|
6841
|
-
}
|
|
6897
|
+
if ("hook_event_name" in payload) return "claude";
|
|
6898
|
+
if ("hookEventName" in payload) return "copilot";
|
|
6842
6899
|
if ("event_type" in payload) return "kiro";
|
|
6843
6900
|
if ("hook_type" in payload) return "codex";
|
|
6844
6901
|
return "claude";
|
|
@@ -6849,8 +6906,9 @@ function extractEventName(payload, agent) {
|
|
|
6849
6906
|
return payload.agent_action_name ?? "";
|
|
6850
6907
|
case "cursor":
|
|
6851
6908
|
return payload.hook_event_name ?? "";
|
|
6852
|
-
case "copilot":
|
|
6853
6909
|
case "claude":
|
|
6910
|
+
return payload.hook_event_name ?? payload.hookEventName ?? "";
|
|
6911
|
+
case "copilot":
|
|
6854
6912
|
return payload.hookEventName ?? "";
|
|
6855
6913
|
case "kiro":
|
|
6856
6914
|
return payload.event_type ?? "";
|
|
@@ -6862,7 +6920,7 @@ function extractEventName(payload, agent) {
|
|
|
6862
6920
|
}
|
|
6863
6921
|
function normalizeClaude(payload, event) {
|
|
6864
6922
|
const result = {
|
|
6865
|
-
sessionId: payload.sessionId ?? "",
|
|
6923
|
+
sessionId: payload.session_id ?? payload.sessionId ?? "",
|
|
6866
6924
|
cwd: payload.cwd ?? "",
|
|
6867
6925
|
transcriptPath: payload.transcript_path
|
|
6868
6926
|
};
|
|
@@ -6870,10 +6928,18 @@ function normalizeClaude(payload, event) {
|
|
|
6870
6928
|
if (toolName) {
|
|
6871
6929
|
result.toolName = toolName;
|
|
6872
6930
|
result.toolInput = payload.tool_input;
|
|
6873
|
-
|
|
6874
|
-
if (
|
|
6875
|
-
|
|
6876
|
-
|
|
6931
|
+
const toolResponse = payload.tool_response ?? payload.tool_result;
|
|
6932
|
+
if (typeof toolResponse === "string") {
|
|
6933
|
+
result.toolResult = toolResponse;
|
|
6934
|
+
} else if (toolResponse && typeof toolResponse === "object") {
|
|
6935
|
+
result.toolResult = JSON.stringify(toolResponse);
|
|
6936
|
+
}
|
|
6937
|
+
const toolInput = payload.tool_input;
|
|
6938
|
+
if (/^bash$/i.test(toolName) && toolInput?.command) {
|
|
6939
|
+
result.command = toolInput.command;
|
|
6940
|
+
}
|
|
6941
|
+
if (/^(write|edit|multi_edit|multiedittool)$/i.test(toolName)) {
|
|
6942
|
+
result.filePath = toolInput?.file_path ?? toolInput?.filePath;
|
|
6877
6943
|
}
|
|
6878
6944
|
}
|
|
6879
6945
|
if (event === "user_prompt") {
|
|
@@ -7173,6 +7239,19 @@ function extractContent(input) {
|
|
|
7173
7239
|
parts.push(`Edit: ${edit.oldString} \u2192 ${edit.newString}`);
|
|
7174
7240
|
}
|
|
7175
7241
|
}
|
|
7242
|
+
if (parts.length === 0 && input.toolInput && typeof input.toolInput === "object") {
|
|
7243
|
+
if (input.toolName) parts.push(`Tool: ${input.toolName}`);
|
|
7244
|
+
if (input.toolInput.command) parts.push(`Command: ${input.toolInput.command}`);
|
|
7245
|
+
if (input.toolInput.file_path) parts.push(`File: ${input.toolInput.file_path}`);
|
|
7246
|
+
if (input.toolInput.content) {
|
|
7247
|
+
const content = input.toolInput.content;
|
|
7248
|
+
parts.push(content.slice(0, 1e3));
|
|
7249
|
+
}
|
|
7250
|
+
if (parts.length <= 1) {
|
|
7251
|
+
const summary = JSON.stringify(input.toolInput).slice(0, 500);
|
|
7252
|
+
parts.push(summary);
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7176
7255
|
return parts.join("\n").slice(0, MAX_CONTENT_LENGTH);
|
|
7177
7256
|
}
|
|
7178
7257
|
function deriveEntityName(input) {
|
|
@@ -7363,7 +7442,7 @@ ${lines.join("\n")}`;
|
|
|
7363
7442
|
return { observation: null, output: defaultOutput };
|
|
7364
7443
|
}
|
|
7365
7444
|
const toolPattern = detectBestPattern(toolContent);
|
|
7366
|
-
if (!toolPattern) {
|
|
7445
|
+
if (!toolPattern && toolContent.length < 200) {
|
|
7367
7446
|
return { observation: null, output: defaultOutput };
|
|
7368
7447
|
}
|
|
7369
7448
|
markTriggered(toolKey);
|
|
@@ -7379,7 +7458,8 @@ ${lines.join("\n")}`;
|
|
|
7379
7458
|
return { observation: null, output: defaultOutput };
|
|
7380
7459
|
}
|
|
7381
7460
|
const content = extractContent(input);
|
|
7382
|
-
|
|
7461
|
+
const minLen = input.event === "user_prompt" ? MIN_PROMPT_LENGTH : MIN_STORE_LENGTH;
|
|
7462
|
+
if (content.length < minLen) {
|
|
7383
7463
|
return { observation: null, output: defaultOutput };
|
|
7384
7464
|
}
|
|
7385
7465
|
detectBestPattern(content);
|
|
@@ -7426,7 +7506,7 @@ async function runHook() {
|
|
|
7426
7506
|
}
|
|
7427
7507
|
process.stdout.write(JSON.stringify(output));
|
|
7428
7508
|
}
|
|
7429
|
-
var cooldowns, COOLDOWN_MS, MIN_STORE_LENGTH, MIN_EDIT_LENGTH, NOISE_COMMANDS, MAX_CONTENT_LENGTH;
|
|
7509
|
+
var cooldowns, COOLDOWN_MS, MIN_STORE_LENGTH, MIN_PROMPT_LENGTH, MIN_EDIT_LENGTH, NOISE_COMMANDS, MAX_CONTENT_LENGTH;
|
|
7430
7510
|
var init_handler = __esm({
|
|
7431
7511
|
"src/hooks/handler.ts"() {
|
|
7432
7512
|
"use strict";
|
|
@@ -7436,6 +7516,7 @@ var init_handler = __esm({
|
|
|
7436
7516
|
cooldowns = /* @__PURE__ */ new Map();
|
|
7437
7517
|
COOLDOWN_MS = 3e4;
|
|
7438
7518
|
MIN_STORE_LENGTH = 100;
|
|
7519
|
+
MIN_PROMPT_LENGTH = 20;
|
|
7439
7520
|
MIN_EDIT_LENGTH = 30;
|
|
7440
7521
|
NOISE_COMMANDS = [
|
|
7441
7522
|
/^(ls|dir|cd|pwd|echo|cat|type|head|tail|wc|find|which|where|whoami)\b/i,
|