memorix 0.5.1 โ†’ 0.6.0

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/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  <p align="center">
2
- <h1 align="center">๐Ÿง  Memorix</h1>
2
+ <img src="assets/logo.png" alt="Memorix Logo" width="120">
3
+ <h1 align="center">Memorix</h1>
3
4
  <p align="center"><strong>Cross-Agent Memory Bridge โ€” Universal memory layer for AI coding agents via MCP</strong></p>
4
5
  <p align="center">
5
6
  <a href="https://www.npmjs.com/package/memorix"><img src="https://img.shields.io/npm/v/memorix.svg?style=flat-square&color=cb3837" alt="npm version"></a>
@@ -19,9 +20,9 @@
19
20
 
20
21
  ---
21
22
 
22
- > **Your AI forgot what you discussed yesterday? Not anymore.**
23
+ > **One project, six agents, zero context loss.**
23
24
  >
24
- > Memorix stores and indexes project knowledge โ€” architecture decisions, bug fixes, gotchas, code patterns โ€” and exposes it via [MCP](https://modelcontextprotocol.io/) so **any AI coding agent** can access it. It also **syncs MCP configs, rules, skills, and workflows** across all your agents automatically.
25
+ > Memorix is a **cross-agent memory bridge** โ€” it lets Cursor, Windsurf, Claude Code, Codex, Copilot, and Antigravity **share the same project knowledge** in real-time. Architecture decisions made in one IDE are instantly available in another. Switch tools, open new windows, start fresh sessions โ€” your context follows you everywhere via [MCP](https://modelcontextprotocol.io/). It also **syncs MCP configs, rules, skills, and workflows** across all your agents automatically.
25
26
 
26
27
  ---
27
28
 
@@ -98,6 +99,22 @@ Then use `"command": "memorix"` instead of `"command": "npx"` in your config.
98
99
  - **Skills & Workflows** โ€” Copy skill folders and workflow files across agents
99
100
  - **Apply with Safety** โ€” Backup `.bak` โ†’ Atomic write โ†’ Auto-rollback on failure
100
101
 
102
+ ### ๐Ÿ”’ Project Isolation
103
+
104
+ - **Per-Project Data** โ€” Each project stores data in its own directory (`~/.memorix/data/<owner--repo>/`)
105
+ - **Git-Based Detection** โ€” Project identity derived from `git remote`, no manual config needed
106
+ - **Scoped Search** โ€” `memorix_search` defaults to current project; set `scope: "global"` to search all
107
+ - **Auto Migration** โ€” Legacy global data automatically migrates to project directories on first run
108
+ - **Zero Cross-Contamination** โ€” Architecture decisions from project A never leak into project B
109
+
110
+ ### ๐Ÿ“Š Visual Dashboard
111
+
112
+ - **Web Dashboard** โ€” `memorix_dashboard` opens a beautiful web UI at `http://localhost:3210`
113
+ - **Project Switcher** โ€” Dropdown to view any project's data without switching IDEs
114
+ - **Knowledge Graph** โ€” Interactive visualization of entities and relations
115
+ - **Retention Scores** โ€” Exponential decay scoring with immunity status
116
+ - **Light/Dark Theme** โ€” Premium glassmorphism design, bilingual (EN/ไธญๆ–‡)
117
+
101
118
  ### ๐Ÿช Auto-Memory Hooks
102
119
 
103
120
  - **Implicit Memory** โ€” Auto-captures decisions, errors, gotchas from agent activity
@@ -199,7 +216,8 @@ args = ["-y", "memorix@latest", "serve"]
199
216
  | `memorix_search` | L1: Compact index search | ~50-100/result |
200
217
  | `memorix_timeline` | L2: Chronological context | ~100-200/group |
201
218
  | `memorix_detail` | L3: Full observation details | ~500-1000/result |
202
- | `memorix_retention` | Memory decay & retention dashboard | โ€” |
219
+ | `memorix_retention` | Memory decay & retention status | โ€” |
220
+ | `memorix_dashboard` | Launch visual web dashboard in browser | โ€” |
203
221
  | `memorix_rules_sync` | Scan/deduplicate/generate rules across agents | โ€” |
204
222
  | `memorix_workspace_sync` | Migrate MCP configs, workflows, skills | โ€” |
205
223
 
@@ -266,7 +284,7 @@ Files: ["src/auth/jwt.ts", "src/config.ts"]
266
284
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
267
285
  โ”‚ MCP Protocol (stdio)
268
286
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
269
- โ”‚ Memorix MCP Server (16 tools) โ”‚
287
+ โ”‚ Memorix MCP Server (17 tools) โ”‚
270
288
  โ”‚ โ”‚
271
289
  โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
272
290
  โ”‚ โ”‚ Memory โ”‚ โ”‚ Compact โ”‚ โ”‚ Workspace Sync โ”‚ โ”‚
@@ -340,7 +358,7 @@ npm run build # Production build
340
358
 
341
359
  ```
342
360
  src/
343
- โ”œโ”€โ”€ server.ts # MCP Server entry (16 tools)
361
+ โ”œโ”€โ”€ server.ts # MCP Server entry (17 tools)
344
362
  โ”œโ”€โ”€ types.ts # All type definitions
345
363
  โ”œโ”€โ”€ memory/ # Graph, observations, retention, entity extraction
346
364
  โ”œโ”€โ”€ store/ # Orama search engine + disk persistence
@@ -349,8 +367,9 @@ src/
349
367
  โ”œโ”€โ”€ hooks/ # Auto-memory hooks (normalizer + pattern detector)
350
368
  โ”œโ”€โ”€ workspace/ # Cross-agent MCP/workflow/skills sync
351
369
  โ”œโ”€โ”€ rules/ # Cross-agent rules sync (6 adapters)
370
+ โ”œโ”€โ”€ dashboard/ # Visual web dashboard (knowledge graph, stats)
352
371
  โ”œโ”€โ”€ project/ # Git-based project detection
353
- โ””โ”€โ”€ cli/ # CLI commands (serve, hook, sync, status)
372
+ โ””โ”€โ”€ cli/ # CLI commands (serve, hook, sync, dashboard)
354
373
  ```
355
374
 
356
375
  > ๐Ÿ“š Full documentation available in [`docs/`](./docs/) โ€” architecture, modules, API reference, design decisions, and more.
package/dist/cli/index.js CHANGED
@@ -31,13 +31,16 @@ var init_esm_shims = __esm({
31
31
  // src/store/persistence.ts
32
32
  var persistence_exports = {};
33
33
  __export(persistence_exports, {
34
+ getBaseDataDir: () => getBaseDataDir,
34
35
  getDbFilePath: () => getDbFilePath,
35
36
  getGraphFilePath: () => getGraphFilePath,
36
37
  getProjectDataDir: () => getProjectDataDir,
37
38
  hasExistingData: () => hasExistingData,
39
+ listProjectDirs: () => listProjectDirs,
38
40
  loadGraphJsonl: () => loadGraphJsonl,
39
41
  loadIdCounter: () => loadIdCounter,
40
42
  loadObservationsJson: () => loadObservationsJson,
43
+ migrateGlobalData: () => migrateGlobalData,
41
44
  saveGraphJsonl: () => saveGraphJsonl,
42
45
  saveIdCounter: () => saveIdCounter,
43
46
  saveObservationsJson: () => saveObservationsJson
@@ -45,11 +48,105 @@ __export(persistence_exports, {
45
48
  import { promises as fs } from "fs";
46
49
  import path2 from "path";
47
50
  import os from "os";
51
+ function sanitizeProjectId(projectId) {
52
+ return projectId.replace(/\//g, "--").replace(/[<>:"|?*\\]/g, "_");
53
+ }
48
54
  async function getProjectDataDir(projectId, baseDir) {
49
- const dataDir = baseDir ?? DEFAULT_DATA_DIR;
55
+ const base = baseDir ?? DEFAULT_DATA_DIR;
56
+ const dirName = sanitizeProjectId(projectId);
57
+ const dataDir = path2.join(base, dirName);
50
58
  await fs.mkdir(dataDir, { recursive: true });
51
59
  return dataDir;
52
60
  }
61
+ function getBaseDataDir(baseDir) {
62
+ return baseDir ?? DEFAULT_DATA_DIR;
63
+ }
64
+ async function listProjectDirs(baseDir) {
65
+ const base = baseDir ?? DEFAULT_DATA_DIR;
66
+ try {
67
+ const entries = await fs.readdir(base, { withFileTypes: true });
68
+ return entries.filter((e) => e.isDirectory()).map((e) => path2.join(base, e.name));
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+ async function migrateGlobalData(projectId, baseDir) {
74
+ const base = baseDir ?? DEFAULT_DATA_DIR;
75
+ const globalObsPath = path2.join(base, "observations.json");
76
+ const migratedObsPath = path2.join(base, "observations.json.migrated");
77
+ let sourceObsPath = null;
78
+ try {
79
+ await fs.access(globalObsPath);
80
+ sourceObsPath = globalObsPath;
81
+ } catch {
82
+ try {
83
+ await fs.access(migratedObsPath);
84
+ sourceObsPath = migratedObsPath;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ let globalObs = [];
90
+ try {
91
+ const data = await fs.readFile(sourceObsPath, "utf-8");
92
+ globalObs = JSON.parse(data);
93
+ if (!Array.isArray(globalObs) || globalObs.length === 0) return false;
94
+ } catch {
95
+ return false;
96
+ }
97
+ const projectDir2 = await getProjectDataDir(projectId, baseDir);
98
+ const projectObsPath = path2.join(projectDir2, "observations.json");
99
+ let projectObs = [];
100
+ try {
101
+ const data = await fs.readFile(projectObsPath, "utf-8");
102
+ projectObs = JSON.parse(data);
103
+ if (!Array.isArray(projectObs)) projectObs = [];
104
+ } catch {
105
+ }
106
+ if (projectObs.length >= globalObs.length) {
107
+ return false;
108
+ }
109
+ const existingIds = new Set(projectObs.map((o) => o.id));
110
+ const merged = [...projectObs];
111
+ for (const obs of globalObs) {
112
+ if (!existingIds.has(obs.id)) {
113
+ merged.push(obs);
114
+ }
115
+ }
116
+ merged.sort((a, b) => (a.id ?? 0) - (b.id ?? 0));
117
+ for (const obs of merged) {
118
+ obs.projectId = projectId;
119
+ }
120
+ await fs.writeFile(projectObsPath, JSON.stringify(merged, null, 2), "utf-8");
121
+ for (const file of ["graph.jsonl", "counter.json"]) {
122
+ const src = path2.join(base, file);
123
+ const srcMigrated = path2.join(base, file + ".migrated");
124
+ const dst = path2.join(projectDir2, file);
125
+ for (const source of [src, srcMigrated]) {
126
+ try {
127
+ await fs.access(source);
128
+ await fs.copyFile(source, dst);
129
+ break;
130
+ } catch {
131
+ }
132
+ }
133
+ }
134
+ const maxId = merged.reduce((max, o) => Math.max(max, o.id ?? 0), 0);
135
+ await fs.writeFile(
136
+ path2.join(projectDir2, "counter.json"),
137
+ JSON.stringify({ nextId: maxId + 1 }),
138
+ "utf-8"
139
+ );
140
+ for (const file of ["observations.json", "graph.jsonl", "counter.json"]) {
141
+ const src = path2.join(base, file);
142
+ try {
143
+ await fs.access(src);
144
+ await fs.rename(src, src + ".migrated");
145
+ } catch {
146
+ }
147
+ }
148
+ return true;
149
+ }
53
150
  function getDbFilePath(projectDir2) {
54
151
  return path2.join(projectDir2, "memorix.msp");
55
152
  }
@@ -1132,12 +1229,14 @@ function detectProject(cwd) {
1132
1229
  const rootPath = getGitRoot(basePath) ?? findPackageRoot(basePath) ?? basePath;
1133
1230
  const gitRemote = getGitRemote(rootPath);
1134
1231
  if (gitRemote) {
1135
- const id = normalizeGitRemote(gitRemote);
1136
- const name2 = id.split("/").pop() ?? path3.basename(rootPath);
1137
- return { id, name: name2, gitRemote, rootPath };
1232
+ const id2 = normalizeGitRemote(gitRemote);
1233
+ const name2 = id2.split("/").pop() ?? path3.basename(rootPath);
1234
+ return { id: id2, name: name2, gitRemote, rootPath };
1138
1235
  }
1139
1236
  const name = path3.basename(rootPath);
1140
- return { id: name, name, rootPath };
1237
+ const id = `local/${name}`;
1238
+ console.error(`[memorix] Warning: no git remote found at ${rootPath}, using fallback projectId: ${id}`);
1239
+ return { id, name, rootPath };
1141
1240
  }
1142
1241
  function findPackageRoot(cwd) {
1143
1242
  let dir = path3.resolve(cwd);
@@ -3076,6 +3175,18 @@ async function detectInstalledAgents() {
3076
3175
  agents.push("kiro");
3077
3176
  } catch {
3078
3177
  }
3178
+ const codexDir = path5.join(home, ".codex");
3179
+ try {
3180
+ await fs3.access(codexDir);
3181
+ agents.push("codex");
3182
+ } catch {
3183
+ }
3184
+ const antigravityDir = path5.join(home, ".gemini", "antigravity");
3185
+ try {
3186
+ await fs3.access(antigravityDir);
3187
+ agents.push("antigravity");
3188
+ } catch {
3189
+ }
3079
3190
  return agents;
3080
3191
  }
3081
3192
  async function installHooks(agent, projectRoot, global = false) {
@@ -3152,15 +3263,34 @@ async function installAgentRules(agent, projectRoot) {
3152
3263
  case "copilot":
3153
3264
  rulesPath = path5.join(projectRoot, ".github", "copilot-instructions.md");
3154
3265
  break;
3266
+ case "codex":
3267
+ rulesPath = path5.join(projectRoot, "AGENTS.md");
3268
+ break;
3269
+ case "kiro":
3270
+ rulesPath = path5.join(projectRoot, ".kiro", "rules", "memorix.md");
3271
+ break;
3155
3272
  default:
3156
- return;
3273
+ rulesPath = path5.join(projectRoot, ".agent", "rules", "memorix.md");
3274
+ break;
3157
3275
  }
3158
3276
  try {
3159
3277
  await fs3.mkdir(path5.dirname(rulesPath), { recursive: true });
3160
- try {
3161
- await fs3.access(rulesPath);
3162
- } catch {
3163
- await fs3.writeFile(rulesPath, rulesContent, "utf-8");
3278
+ if (agent === "codex") {
3279
+ try {
3280
+ const existing = await fs3.readFile(rulesPath, "utf-8");
3281
+ if (existing.includes("Memorix")) {
3282
+ return;
3283
+ }
3284
+ await fs3.writeFile(rulesPath, existing + "\n\n" + rulesContent, "utf-8");
3285
+ } catch {
3286
+ await fs3.writeFile(rulesPath, rulesContent, "utf-8");
3287
+ }
3288
+ } else {
3289
+ try {
3290
+ await fs3.access(rulesPath);
3291
+ } catch {
3292
+ await fs3.writeFile(rulesPath, rulesContent, "utf-8");
3293
+ }
3164
3294
  }
3165
3295
  } catch {
3166
3296
  }
@@ -3174,22 +3304,50 @@ You have access to Memorix memory tools. Follow these rules to maintain persiste
3174
3304
 
3175
3305
  At the **beginning of every conversation**, before responding to the user:
3176
3306
 
3177
- 1. Call \`memorix_search\` with query related to the user's first message or the current project
3178
- 2. If results are found, use them to understand the current project state, recent decisions, and pending tasks
3179
- 3. Reference relevant memories naturally in your response
3307
+ 1. Call \`memorix_search\` with a query related to the user's first message or the current project
3308
+ 2. If results are found, use \`memorix_detail\` to fetch the most relevant ones
3309
+ 3. Reference relevant memories naturally in your response \u2014 the user should feel you "remember" them
3180
3310
 
3181
3311
  This ensures you already know the project context without the user re-explaining.
3182
3312
 
3183
3313
  ## During Session \u2014 Capture Important Context
3184
3314
 
3185
- Proactively call \`memorix_store\` when any of the following happen:
3315
+ **Proactively** call \`memorix_store\` whenever any of the following happen:
3316
+
3317
+ ### Architecture & Decisions
3318
+ - Technology choice, framework selection, or design pattern adopted
3319
+ - Trade-off discussion with a clear conclusion
3320
+ - API design, database schema, or project structure decisions
3321
+
3322
+ ### Bug Fixes & Problem Solving
3323
+ - A bug is identified and resolved \u2014 store root cause + fix
3324
+ - Workaround applied for a known issue
3325
+ - Performance issue diagnosed and optimized
3326
+
3327
+ ### Gotchas & Pitfalls
3328
+ - Something unexpected or tricky is discovered
3329
+ - A common mistake is identified and corrected
3330
+ - Platform-specific behavior that caused issues
3186
3331
 
3187
- - **Architecture decision**: You or the user decide on a technology, pattern, or approach
3188
- - **Bug fix**: A bug is identified and resolved \u2014 store the root cause and fix
3189
- - **Gotcha/pitfall**: Something unexpected or tricky is discovered
3190
- - **Configuration change**: Environment, port, path, or tooling changes
3332
+ ### Configuration & Environment
3333
+ - Environment variables, port numbers, paths changed
3334
+ - Docker, nginx, Caddy, or reverse proxy config modified
3335
+ - Package dependencies added, removed, or version-pinned
3191
3336
 
3192
- Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`.
3337
+ ### Deployment & Operations
3338
+ - Server deployment steps (Docker, VPS, cloud)
3339
+ - DNS, SSL/TLS certificate, domain configuration
3340
+ - CI/CD pipeline setup or changes
3341
+ - Database migration or data transfer procedures
3342
+ - Server topology (ports, services, reverse proxy chain)
3343
+ - SSH keys, access credentials setup (store pattern, NOT secrets)
3344
+
3345
+ ### Project Milestones
3346
+ - Feature completed or shipped
3347
+ - Version released or published to npm/PyPI/etc.
3348
+ - Repository made public, README updated, PR submitted
3349
+
3350
+ Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`, \`how-it-works\`.
3193
3351
 
3194
3352
  ## Session End \u2014 Store Summary
3195
3353
 
@@ -3197,18 +3355,21 @@ When the conversation is ending or the user says goodbye:
3197
3355
 
3198
3356
  1. Call \`memorix_store\` with type \`session-request\` to record:
3199
3357
  - What was accomplished in this session
3200
- - Current project state
3358
+ - Current project state and any blockers
3201
3359
  - Pending tasks or next steps
3202
- - Any unresolved issues
3360
+ - Key files modified
3203
3361
 
3204
- This creates a "handoff note" for the next session.
3362
+ This creates a "handoff note" for the next session (or for another AI agent).
3205
3363
 
3206
3364
  ## Guidelines
3207
3365
 
3208
- - **Don't store trivial information** (greetings, acknowledgments, simple file reads)
3366
+ - **Don't store trivial information** (greetings, acknowledgments, simple file reads, ls/dir output)
3209
3367
  - **Do store anything you'd want to know if you lost all context**
3210
- - **Use concise titles** and structured facts
3368
+ - **Do store anything a different AI agent would need to continue this work**
3369
+ - **Use concise titles** (~5-10 words) and structured facts
3211
3370
  - **Include file paths** in filesModified when relevant
3371
+ - **Include related concepts** for better searchability
3372
+ - **Prefer storing too much over too little** \u2014 the retention system will auto-decay stale memories
3212
3373
  `;
3213
3374
  }
3214
3375
  async function uninstallHooks(agent, projectRoot, global = false) {
@@ -3233,7 +3394,7 @@ async function uninstallHooks(agent, projectRoot, global = false) {
3233
3394
  }
3234
3395
  async function getHookStatus(projectRoot) {
3235
3396
  const results = [];
3236
- const agents = ["claude", "copilot", "windsurf", "cursor", "kiro", "codex"];
3397
+ const agents = ["claude", "copilot", "windsurf", "cursor", "kiro", "codex", "antigravity"];
3237
3398
  for (const agent of agents) {
3238
3399
  const projectPath = getProjectConfigPath(agent, projectRoot);
3239
3400
  const globalPath = getGlobalConfigPath(agent);
@@ -3398,31 +3559,65 @@ function sendError(res, message, status = 500) {
3398
3559
  function filterByProject(items, projectId) {
3399
3560
  return items.filter((item) => item.projectId === projectId);
3400
3561
  }
3401
- async function handleApi(req, res, dataDir, projectId, projectName) {
3562
+ async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
3402
3563
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
3403
3564
  const apiPath = url.pathname.replace("/api", "");
3565
+ const requestedProject = url.searchParams.get("project");
3566
+ let effectiveDataDir = dataDir;
3567
+ let effectiveProjectId = projectId;
3568
+ let effectiveProjectName = projectName;
3569
+ if (requestedProject && requestedProject !== projectId) {
3570
+ const sanitized = requestedProject.replace(/\//g, "--").replace(/[<>:"|?*\\]/g, "_");
3571
+ const candidateDir = path6.join(baseDir, sanitized);
3572
+ try {
3573
+ await fs4.access(candidateDir);
3574
+ effectiveDataDir = candidateDir;
3575
+ effectiveProjectId = requestedProject;
3576
+ effectiveProjectName = requestedProject.split("/").pop() || requestedProject;
3577
+ } catch {
3578
+ }
3579
+ }
3404
3580
  try {
3405
3581
  switch (apiPath) {
3582
+ case "/projects": {
3583
+ try {
3584
+ const entries = await fs4.readdir(baseDir, { withFileTypes: true });
3585
+ const projects = entries.filter((e) => e.isDirectory() && e.name.includes("--")).map((e) => {
3586
+ const dirName = e.name;
3587
+ const id = dirName.replace(/--/g, "/");
3588
+ return {
3589
+ id,
3590
+ name: id.split("/").pop() || id,
3591
+ dirName,
3592
+ isCurrent: id === projectId
3593
+ };
3594
+ });
3595
+ sendJson(res, projects);
3596
+ } catch {
3597
+ sendJson(res, []);
3598
+ }
3599
+ break;
3600
+ }
3406
3601
  case "/project": {
3407
- sendJson(res, { id: projectId, name: projectName });
3602
+ sendJson(res, { id: effectiveProjectId, name: effectiveProjectName });
3408
3603
  break;
3409
3604
  }
3410
3605
  case "/graph": {
3411
- const graph = await loadGraphJsonl(dataDir);
3606
+ const graph = await loadGraphJsonl(effectiveDataDir);
3412
3607
  sendJson(res, graph);
3413
3608
  break;
3414
3609
  }
3415
3610
  case "/observations": {
3416
- const allObs = await loadObservationsJson(dataDir);
3417
- const observations2 = filterByProject(allObs, projectId);
3611
+ const allObs = await loadObservationsJson(effectiveDataDir);
3612
+ const observations2 = filterByProject(allObs, effectiveProjectId);
3418
3613
  sendJson(res, observations2);
3419
3614
  break;
3420
3615
  }
3421
3616
  case "/stats": {
3422
- const graph = await loadGraphJsonl(dataDir);
3423
- const allObs = await loadObservationsJson(dataDir);
3424
- const observations2 = filterByProject(allObs, projectId);
3425
- const nextId2 = await loadIdCounter(dataDir);
3617
+ const graph = await loadGraphJsonl(effectiveDataDir);
3618
+ const allObs = await loadObservationsJson(effectiveDataDir);
3619
+ const observations2 = filterByProject(allObs, effectiveProjectId);
3620
+ const nextId2 = await loadIdCounter(effectiveDataDir);
3426
3621
  const typeCounts = {};
3427
3622
  for (const obs of observations2) {
3428
3623
  const t = obs.type || "unknown";
@@ -3440,8 +3635,8 @@ async function handleApi(req, res, dataDir, projectId, projectName) {
3440
3635
  break;
3441
3636
  }
3442
3637
  case "/retention": {
3443
- const allObs = await loadObservationsJson(dataDir);
3444
- const observations2 = filterByProject(allObs, projectId);
3638
+ const allObs = await loadObservationsJson(effectiveDataDir);
3639
+ const observations2 = filterByProject(allObs, effectiveProjectId);
3445
3640
  const now = Date.now();
3446
3641
  const scored = observations2.map((obs) => {
3447
3642
  const age = now - new Date(obs.createdAt || now).getTime();
@@ -3518,10 +3713,11 @@ function openBrowser(url) {
3518
3713
  }
3519
3714
  async function startDashboard(dataDir, port, staticDir, projectId, projectName, autoOpen = true) {
3520
3715
  const resolvedStaticDir = staticDir;
3716
+ const baseDir = getBaseDataDir();
3521
3717
  const server = createServer(async (req, res) => {
3522
3718
  const url = req.url || "/";
3523
3719
  if (url.startsWith("/api/")) {
3524
- await handleApi(req, res, dataDir, projectId, projectName);
3720
+ await handleApi(req, res, dataDir, projectId, projectName, baseDir);
3525
3721
  } else {
3526
3722
  await serveStatic(req, res, resolvedStaticDir);
3527
3723
  }
@@ -3579,6 +3775,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3579
3775
  import { z } from "zod";
3580
3776
  async function createMemorixServer(cwd) {
3581
3777
  const project = detectProject(cwd);
3778
+ try {
3779
+ const { migrateGlobalData: migrateGlobalData2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
3780
+ const migrated = await migrateGlobalData2(project.id);
3781
+ if (migrated) {
3782
+ console.error(`[memorix] Migrated legacy data to project directory: ${project.id}`);
3783
+ }
3784
+ } catch {
3785
+ }
3582
3786
  const projectDir2 = await getProjectDataDir(project.id);
3583
3787
  const graphManager = new KnowledgeGraphManager(projectDir2);
3584
3788
  await graphManager.init();
@@ -3593,15 +3797,14 @@ async function createMemorixServer(cwd) {
3593
3797
  const { getHookStatus: getHookStatus2, installHooks: installHooks2, detectInstalledAgents: detectInstalledAgents2 } = await Promise.resolve().then(() => (init_installers(), installers_exports));
3594
3798
  const workDir = cwd ?? process.cwd();
3595
3799
  const statuses = await getHookStatus2(workDir);
3596
- const anyInstalled = statuses.some((s) => s.installed);
3597
- if (!anyInstalled) {
3598
- const agents = await detectInstalledAgents2();
3599
- for (const agent of agents) {
3600
- try {
3601
- const config = await installHooks2(agent, workDir);
3602
- console.error(`[memorix] Auto-installed hooks for ${agent} \u2192 ${config.configPath}`);
3603
- } catch {
3604
- }
3800
+ const installedAgents = new Set(statuses.filter((s) => s.installed).map((s) => s.agent));
3801
+ const detectedAgents = await detectInstalledAgents2();
3802
+ for (const agent of detectedAgents) {
3803
+ if (installedAgents.has(agent)) continue;
3804
+ try {
3805
+ const config = await installHooks2(agent, workDir);
3806
+ console.error(`[memorix] Auto-installed hooks for ${agent} \u2192 ${config.configPath}`);
3807
+ } catch {
3605
3808
  }
3606
3809
  }
3607
3810
  } catch {
@@ -3738,15 +3941,20 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3738
3941
  query: z.string().describe("Search query (natural language or keywords)"),
3739
3942
  limit: z.number().optional().describe("Max results (default: 20)"),
3740
3943
  type: z.enum(OBSERVATION_TYPES).optional().describe("Filter by observation type"),
3741
- maxTokens: z.number().optional().describe("Token budget \u2014 trim results to fit (0 = unlimited)")
3944
+ maxTokens: z.number().optional().describe("Token budget \u2014 trim results to fit (0 = unlimited)"),
3945
+ scope: z.enum(["project", "global"]).optional().describe(
3946
+ 'Search scope: "project" (default) only searches current project, "global" searches all projects'
3947
+ )
3742
3948
  }
3743
3949
  },
3744
- async ({ query, limit, type, maxTokens }) => {
3950
+ async ({ query, limit, type, maxTokens, scope }) => {
3745
3951
  const result = await compactSearch({
3746
3952
  query,
3747
3953
  limit,
3748
3954
  type,
3749
- maxTokens
3955
+ maxTokens,
3956
+ // Default to current project scope; 'global' removes the project filter
3957
+ projectId: scope === "global" ? void 0 : project.id
3750
3958
  });
3751
3959
  let text = result.formatted;
3752
3960
  if (!syncAdvisoryShown && syncAdvisory) {
@@ -4325,7 +4533,29 @@ var init_serve = __esm({
4325
4533
  run: async ({ args }) => {
4326
4534
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
4327
4535
  const { createMemorixServer: createMemorixServer2 } = await Promise.resolve().then(() => (init_server2(), server_exports2));
4328
- const projectRoot = args.cwd || process.cwd();
4536
+ const { execSync: execSync2 } = await import("child_process");
4537
+ let projectRoot = args.cwd || process.env.INIT_CWD || process.cwd();
4538
+ try {
4539
+ execSync2("git rev-parse --show-toplevel", {
4540
+ cwd: projectRoot,
4541
+ encoding: "utf-8",
4542
+ stdio: ["pipe", "pipe", "pipe"]
4543
+ });
4544
+ } catch {
4545
+ const scriptDir = new URL(".", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
4546
+ try {
4547
+ const gitRoot = execSync2("git rev-parse --show-toplevel", {
4548
+ cwd: scriptDir,
4549
+ encoding: "utf-8",
4550
+ stdio: ["pipe", "pipe", "pipe"]
4551
+ }).trim();
4552
+ if (gitRoot) {
4553
+ projectRoot = gitRoot;
4554
+ console.error(`[memorix] CWD has no git, using script dir: ${projectRoot}`);
4555
+ }
4556
+ } catch {
4557
+ }
4558
+ }
4329
4559
  const { server, projectId } = await createMemorixServer2(projectRoot);
4330
4560
  const transport = new StdioServerTransport();
4331
4561
  await server.connect(transport);
@@ -4725,7 +4955,8 @@ function patternToObservationType(pattern) {
4725
4955
  gotcha: "gotcha",
4726
4956
  configuration: "what-changed",
4727
4957
  learning: "discovery",
4728
- implementation: "what-changed"
4958
+ implementation: "what-changed",
4959
+ deployment: "what-changed"
4729
4960
  };
4730
4961
  return map[pattern] ?? "discovery";
4731
4962
  }
@@ -4796,6 +5027,23 @@ var init_pattern_detector = __esm({
4796
5027
  ],
4797
5028
  minLength: 200,
4798
5029
  baseConfidence: 0.5
5030
+ },
5031
+ {
5032
+ type: "deployment",
5033
+ keywords: [
5034
+ /\b(deploy(ed|ing|ment)?|ship(ped|ping)?|releas(ed|ing)|publish(ed|ing)?)\b/i,
5035
+ /(้ƒจ็ฝฒ|ๅ‘ๅธƒ|ไธŠ็บฟ|่ฟ็งป|่ฟ็ปด)/,
5036
+ /\b(docker|compose|container|kubernetes|k8s|helm)\b/i,
5037
+ /\b(VPS|server|host(ing)?|cloud|AWS|Azure|GCP|Cloudflare)\b/i,
5038
+ /\b(nginx|caddy|apache|reverse.?proxy|load.?balanc)\b/i,
5039
+ /\b(SSL|TLS|cert(ificate)?|HTTPS|Let'?s?.?Encrypt|ACME)\b/i,
5040
+ /\b(DNS|domain|A.?record|CNAME|nameserver|Cloudflare)\b/i,
5041
+ /\b(CI\/CD|pipeline|GitHub.?Actions|Jenkins|GitLab.?CI)\b/i,
5042
+ /\b(scp|rsync|ssh|sftp|systemd|systemctl|service)\b/i,
5043
+ /(ๆœๅŠกๅ™จ|ๅŸŸๅ|่ฏไนฆ|ๅๅ‘ไปฃ็†|่ดŸ่ฝฝๅ‡่กก|้•œๅƒ|ๅฎนๅ™จ)/
5044
+ ],
5045
+ minLength: 80,
5046
+ baseConfidence: 0.75
4799
5047
  }
4800
5048
  ];
4801
5049
  }
@@ -5277,10 +5525,13 @@ var init_dashboard = __esm({
5277
5525
  // src/cli/index.ts
5278
5526
  init_esm_shims();
5279
5527
  import { defineCommand as defineCommand10, runMain } from "citty";
5528
+ import { createRequire as createRequire2 } from "module";
5529
+ var require2 = createRequire2(import.meta.url);
5530
+ var pkg = require2("../../package.json");
5280
5531
  var main = defineCommand10({
5281
5532
  meta: {
5282
5533
  name: "memorix",
5283
- version: "0.3.9",
5534
+ version: pkg.version,
5284
5535
  description: "Cross-Agent Memory Bridge \u2014 Universal memory layer for AI coding agents via MCP"
5285
5536
  },
5286
5537
  subCommands: {