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 +26 -7
- package/dist/cli/index.js +303 -52
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +50 -1
- package/dist/dashboard/static/index.html +5 -0
- package/dist/dashboard/static/style.css +47 -0
- package/dist/index.js +281 -49
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
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
|
-
> **
|
|
23
|
+
> **One project, six agents, zero context loss.**
|
|
23
24
|
>
|
|
24
|
-
> Memorix
|
|
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
|
|
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 (
|
|
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 (
|
|
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,
|
|
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
|
|
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
|
|
1136
|
-
const name2 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
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
|
|
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\`
|
|
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
|
-
|
|
3188
|
-
-
|
|
3189
|
-
-
|
|
3190
|
-
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
- **
|
|
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:
|
|
3602
|
+
sendJson(res, { id: effectiveProjectId, name: effectiveProjectName });
|
|
3408
3603
|
break;
|
|
3409
3604
|
}
|
|
3410
3605
|
case "/graph": {
|
|
3411
|
-
const graph = await loadGraphJsonl(
|
|
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(
|
|
3417
|
-
const observations2 = filterByProject(allObs,
|
|
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(
|
|
3423
|
-
const allObs = await loadObservationsJson(
|
|
3424
|
-
const observations2 = filterByProject(allObs,
|
|
3425
|
-
const nextId2 = await loadIdCounter(
|
|
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(
|
|
3444
|
-
const observations2 = filterByProject(allObs,
|
|
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
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
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
|
|
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:
|
|
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: {
|