memorix 0.5.0 → 0.5.2

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
@@ -19,9 +19,9 @@
19
19
 
20
20
  ---
21
21
 
22
- > **Your AI forgot what you discussed yesterday? Not anymore.**
22
+ > **One project, six agents, zero context loss.**
23
23
  >
24
- > Memorix stores and indexes project knowledge architecture decisions, bug fixes, gotchas, code patternsand 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.
24
+ > 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
25
 
26
26
  ---
27
27
 
@@ -199,7 +199,8 @@ args = ["-y", "memorix@latest", "serve"]
199
199
  | `memorix_search` | L1: Compact index search | ~50-100/result |
200
200
  | `memorix_timeline` | L2: Chronological context | ~100-200/group |
201
201
  | `memorix_detail` | L3: Full observation details | ~500-1000/result |
202
- | `memorix_retention` | Memory decay & retention dashboard | — |
202
+ | `memorix_retention` | Memory decay & retention status | — |
203
+ | `memorix_dashboard` | Launch visual web dashboard in browser | — |
203
204
  | `memorix_rules_sync` | Scan/deduplicate/generate rules across agents | — |
204
205
  | `memorix_workspace_sync` | Migrate MCP configs, workflows, skills | — |
205
206
 
@@ -266,7 +267,7 @@ Files: ["src/auth/jwt.ts", "src/config.ts"]
266
267
  └────────────────────────┬─────────────────────────────────────┘
267
268
  │ MCP Protocol (stdio)
268
269
  ┌────────────────────────▼─────────────────────────────────────┐
269
- │ Memorix MCP Server (16 tools) │
270
+ │ Memorix MCP Server (17 tools) │
270
271
  │ │
271
272
  │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
272
273
  │ │ Memory │ │ Compact │ │ Workspace Sync │ │
@@ -340,7 +341,7 @@ npm run build # Production build
340
341
 
341
342
  ```
342
343
  src/
343
- ├── server.ts # MCP Server entry (16 tools)
344
+ ├── server.ts # MCP Server entry (17 tools)
344
345
  ├── types.ts # All type definitions
345
346
  ├── memory/ # Graph, observations, retention, entity extraction
346
347
  ├── store/ # Orama search engine + disk persistence
@@ -349,8 +350,9 @@ src/
349
350
  ├── hooks/ # Auto-memory hooks (normalizer + pattern detector)
350
351
  ├── workspace/ # Cross-agent MCP/workflow/skills sync
351
352
  ├── rules/ # Cross-agent rules sync (6 adapters)
353
+ ├── dashboard/ # Visual web dashboard (knowledge graph, stats)
352
354
  ├── project/ # Git-based project detection
353
- └── cli/ # CLI commands (serve, hook, sync, status)
355
+ └── cli/ # CLI commands (serve, hook, sync, dashboard)
354
356
  ```
355
357
 
356
358
  > 📚 Full documentation available in [`docs/`](./docs/) — architecture, modules, API reference, design decisions, and more.
package/dist/cli/index.js CHANGED
@@ -2631,10 +2631,10 @@ var init_engine2 = __esm({
2631
2631
  for (const [target, adapter] of this.adapters) {
2632
2632
  const configPath = adapter.getConfigPath(this.projectRoot);
2633
2633
  const globalPath = adapter.getConfigPath();
2634
- for (const path6 of [configPath, globalPath]) {
2635
- if (existsSync4(path6)) {
2634
+ for (const path8 of [configPath, globalPath]) {
2635
+ if (existsSync4(path8)) {
2636
2636
  try {
2637
- const content = readFileSync2(path6, "utf-8");
2637
+ const content = readFileSync2(path8, "utf-8");
2638
2638
  const servers = adapter.parse(content);
2639
2639
  if (servers.length > 0) {
2640
2640
  mcpConfigs[target] = servers;
@@ -3376,9 +3376,202 @@ var init_retention = __esm({
3376
3376
  }
3377
3377
  });
3378
3378
 
3379
- // src/server.ts
3379
+ // src/dashboard/server.ts
3380
3380
  var server_exports = {};
3381
3381
  __export(server_exports, {
3382
+ startDashboard: () => startDashboard
3383
+ });
3384
+ import { createServer } from "http";
3385
+ import { promises as fs4 } from "fs";
3386
+ import path6 from "path";
3387
+ import { exec } from "child_process";
3388
+ function sendJson(res, data, status = 200) {
3389
+ res.writeHead(status, {
3390
+ "Content-Type": "application/json; charset=utf-8",
3391
+ "Access-Control-Allow-Origin": "*"
3392
+ });
3393
+ res.end(JSON.stringify(data));
3394
+ }
3395
+ function sendError(res, message, status = 500) {
3396
+ sendJson(res, { error: message }, status);
3397
+ }
3398
+ function filterByProject(items, projectId) {
3399
+ return items.filter((item) => item.projectId === projectId);
3400
+ }
3401
+ async function handleApi(req, res, dataDir, projectId, projectName) {
3402
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
3403
+ const apiPath = url.pathname.replace("/api", "");
3404
+ try {
3405
+ switch (apiPath) {
3406
+ case "/project": {
3407
+ sendJson(res, { id: projectId, name: projectName });
3408
+ break;
3409
+ }
3410
+ case "/graph": {
3411
+ const graph = await loadGraphJsonl(dataDir);
3412
+ sendJson(res, graph);
3413
+ break;
3414
+ }
3415
+ case "/observations": {
3416
+ const allObs = await loadObservationsJson(dataDir);
3417
+ const observations2 = filterByProject(allObs, projectId);
3418
+ sendJson(res, observations2);
3419
+ break;
3420
+ }
3421
+ 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);
3426
+ const typeCounts = {};
3427
+ for (const obs of observations2) {
3428
+ const t = obs.type || "unknown";
3429
+ typeCounts[t] = (typeCounts[t] || 0) + 1;
3430
+ }
3431
+ const sorted = [...observations2].sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
3432
+ sendJson(res, {
3433
+ entities: graph.entities.length,
3434
+ relations: graph.relations.length,
3435
+ observations: observations2.length,
3436
+ nextId: nextId2,
3437
+ typeCounts,
3438
+ recentObservations: sorted
3439
+ });
3440
+ break;
3441
+ }
3442
+ case "/retention": {
3443
+ const allObs = await loadObservationsJson(dataDir);
3444
+ const observations2 = filterByProject(allObs, projectId);
3445
+ const now = Date.now();
3446
+ const scored = observations2.map((obs) => {
3447
+ const age = now - new Date(obs.createdAt || now).getTime();
3448
+ const ageHours = age / (1e3 * 60 * 60);
3449
+ const importance = obs.importance ?? 5;
3450
+ const accessCount = obs.accessCount ?? 0;
3451
+ const lambda = 0.01;
3452
+ const decayScore = importance * Math.exp(-lambda * ageHours);
3453
+ const accessBonus = Math.min(accessCount * 0.5, 3);
3454
+ const score = Math.min(decayScore + accessBonus, 10);
3455
+ const isImmune2 = importance >= 8 || obs.type === "gotcha" || obs.type === "decision";
3456
+ return {
3457
+ id: obs.id,
3458
+ title: obs.title,
3459
+ type: obs.type,
3460
+ entityName: obs.entityName,
3461
+ score: Math.round(score * 100) / 100,
3462
+ isImmune: isImmune2,
3463
+ ageHours: Math.round(ageHours * 10) / 10,
3464
+ accessCount
3465
+ };
3466
+ });
3467
+ scored.sort((a, b) => b.score - a.score);
3468
+ const activeCount = scored.filter((s) => s.score >= 3).length;
3469
+ const staleCount = scored.filter((s) => s.score < 3 && s.score >= 1).length;
3470
+ const archiveCount = scored.filter((s) => s.score < 1).length;
3471
+ const immuneCount = scored.filter((s) => s.isImmune).length;
3472
+ sendJson(res, {
3473
+ summary: { active: activeCount, stale: staleCount, archive: archiveCount, immune: immuneCount },
3474
+ items: scored
3475
+ });
3476
+ break;
3477
+ }
3478
+ default:
3479
+ sendError(res, "Not found", 404);
3480
+ }
3481
+ } catch (err) {
3482
+ const message = err instanceof Error ? err.message : "Unknown error";
3483
+ sendError(res, message);
3484
+ }
3485
+ }
3486
+ async function serveStatic(req, res, staticDir) {
3487
+ let urlPath = new URL(req.url || "/", `http://${req.headers.host}`).pathname;
3488
+ if (urlPath === "/" || !urlPath.includes(".")) {
3489
+ urlPath = "/index.html";
3490
+ }
3491
+ const filePath = path6.join(staticDir, urlPath);
3492
+ if (!filePath.startsWith(staticDir)) {
3493
+ sendError(res, "Forbidden", 403);
3494
+ return;
3495
+ }
3496
+ try {
3497
+ const data = await fs4.readFile(filePath);
3498
+ const ext = path6.extname(filePath);
3499
+ res.writeHead(200, {
3500
+ "Content-Type": MIME_TYPES[ext] || "application/octet-stream",
3501
+ "Cache-Control": "no-cache"
3502
+ });
3503
+ res.end(data);
3504
+ } catch {
3505
+ try {
3506
+ const indexData = await fs4.readFile(path6.join(staticDir, "index.html"));
3507
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3508
+ res.end(indexData);
3509
+ } catch {
3510
+ sendError(res, "Not found", 404);
3511
+ }
3512
+ }
3513
+ }
3514
+ function openBrowser(url) {
3515
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
3516
+ exec(cmd, () => {
3517
+ });
3518
+ }
3519
+ async function startDashboard(dataDir, port, staticDir, projectId, projectName, autoOpen = true) {
3520
+ const resolvedStaticDir = staticDir;
3521
+ const server = createServer(async (req, res) => {
3522
+ const url = req.url || "/";
3523
+ if (url.startsWith("/api/")) {
3524
+ await handleApi(req, res, dataDir, projectId, projectName);
3525
+ } else {
3526
+ await serveStatic(req, res, resolvedStaticDir);
3527
+ }
3528
+ });
3529
+ return new Promise((resolve2, reject) => {
3530
+ server.on("error", (err) => {
3531
+ if (err.code === "EADDRINUSE") {
3532
+ console.error(`Port ${port} is already in use. Try: memorix dashboard --port ${port + 1}`);
3533
+ reject(err);
3534
+ } else {
3535
+ reject(err);
3536
+ }
3537
+ });
3538
+ server.listen(port, () => {
3539
+ const url = `http://localhost:${port}`;
3540
+ console.error(`
3541
+ Memorix Dashboard`);
3542
+ console.error(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
3543
+ console.error(` Project: ${projectName} (${projectId})`);
3544
+ console.error(` Local: ${url}`);
3545
+ console.error(` Data dir: ${dataDir}`);
3546
+ console.error(`
3547
+ Press Ctrl+C to stop
3548
+ `);
3549
+ if (autoOpen) openBrowser(url);
3550
+ resolve2();
3551
+ });
3552
+ });
3553
+ }
3554
+ var MIME_TYPES;
3555
+ var init_server = __esm({
3556
+ "src/dashboard/server.ts"() {
3557
+ "use strict";
3558
+ init_esm_shims();
3559
+ init_persistence();
3560
+ MIME_TYPES = {
3561
+ ".html": "text/html; charset=utf-8",
3562
+ ".css": "text/css; charset=utf-8",
3563
+ ".js": "application/javascript; charset=utf-8",
3564
+ ".json": "application/json; charset=utf-8",
3565
+ ".svg": "image/svg+xml",
3566
+ ".png": "image/png",
3567
+ ".ico": "image/x-icon"
3568
+ };
3569
+ }
3570
+ });
3571
+
3572
+ // src/server.ts
3573
+ var server_exports2 = {};
3574
+ __export(server_exports2, {
3382
3575
  createMemorixServer: () => createMemorixServer
3383
3576
  });
3384
3577
  import { watch } from "fs";
@@ -3996,10 +4189,89 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3996
4189
  };
3997
4190
  }
3998
4191
  );
4192
+ let dashboardRunning = false;
4193
+ server.registerTool(
4194
+ "memorix_dashboard",
4195
+ {
4196
+ title: "Launch Dashboard",
4197
+ description: "Launch the Memorix Web Dashboard in the browser. Shows knowledge graph, observations, retention scores, and project stats in a visual interface.",
4198
+ inputSchema: {
4199
+ port: z.number().optional().describe("Port to run the dashboard on (default: 3210)")
4200
+ }
4201
+ },
4202
+ async ({ port: dashboardPort }) => {
4203
+ const portNum = dashboardPort || 3210;
4204
+ const url = `http://localhost:${portNum}`;
4205
+ if (dashboardRunning) {
4206
+ const { exec: exec2 } = await import("child_process");
4207
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
4208
+ exec2(cmd, () => {
4209
+ });
4210
+ return {
4211
+ content: [{ type: "text", text: `Dashboard is already running at ${url}. Opened in browser.` }]
4212
+ };
4213
+ }
4214
+ try {
4215
+ const pathMod = await import("path");
4216
+ const fsMod = await import("fs");
4217
+ const { fileURLToPath: fileURLToPath3 } = await import("url");
4218
+ const { startDashboard: startDashboard2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4219
+ const candidates = [
4220
+ pathMod.default.join(__dirname, "..", "dashboard", "static"),
4221
+ pathMod.default.join(__dirname, "dashboard", "static"),
4222
+ pathMod.default.join(pathMod.default.dirname(fileURLToPath3(import.meta.url)), "..", "dashboard", "static"),
4223
+ pathMod.default.join(pathMod.default.dirname(fileURLToPath3(import.meta.url)), "dashboard", "static")
4224
+ ];
4225
+ for (const [i, c] of candidates.entries()) {
4226
+ const hasIndex = fsMod.existsSync(pathMod.default.join(c, "index.html"));
4227
+ console.error(`[memorix] candidate[${i}]: ${c} (has index.html: ${hasIndex})`);
4228
+ }
4229
+ let staticDir = candidates[0];
4230
+ for (const c of candidates) {
4231
+ if (fsMod.existsSync(pathMod.default.join(c, "index.html"))) {
4232
+ staticDir = c;
4233
+ break;
4234
+ }
4235
+ }
4236
+ console.error(`[memorix] Dashboard staticDir: ${staticDir}`);
4237
+ startDashboard2(projectDir2, portNum, staticDir, project.id, project.name, false).then(() => {
4238
+ dashboardRunning = true;
4239
+ }).catch((err) => {
4240
+ console.error("[memorix] Dashboard error:", err);
4241
+ dashboardRunning = false;
4242
+ });
4243
+ await new Promise((resolve2) => setTimeout(resolve2, 800));
4244
+ dashboardRunning = true;
4245
+ const { exec: execCmd } = await import("child_process");
4246
+ const openCmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
4247
+ execCmd(openCmd, () => {
4248
+ });
4249
+ return {
4250
+ content: [{
4251
+ type: "text",
4252
+ text: [
4253
+ `Memorix Dashboard started!`,
4254
+ ``,
4255
+ `URL: ${url}`,
4256
+ `Project: ${project.name} (${project.id})`,
4257
+ `Static: ${staticDir}`,
4258
+ ``,
4259
+ `The dashboard has been opened in your default browser.`,
4260
+ `It shows your knowledge graph, observations, retention scores, and project stats.`
4261
+ ].join("\n")
4262
+ }]
4263
+ };
4264
+ } catch (err) {
4265
+ return {
4266
+ content: [{ type: "text", text: `Failed to start dashboard: ${err instanceof Error ? err.message : String(err)}` }]
4267
+ };
4268
+ }
4269
+ }
4270
+ );
3999
4271
  return { server, graphManager, projectId: project.id };
4000
4272
  }
4001
4273
  var OBSERVATION_TYPES;
4002
- var init_server = __esm({
4274
+ var init_server2 = __esm({
4003
4275
  "src/server.ts"() {
4004
4276
  "use strict";
4005
4277
  init_esm_shims();
@@ -4052,7 +4324,7 @@ var init_serve = __esm({
4052
4324
  },
4053
4325
  run: async ({ args }) => {
4054
4326
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
4055
- const { createMemorixServer: createMemorixServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4327
+ const { createMemorixServer: createMemorixServer2 } = await Promise.resolve().then(() => (init_server2(), server_exports2));
4056
4328
  const projectRoot = args.cwd || process.cwd();
4057
4329
  const { server, projectId } = await createMemorixServer2(projectRoot);
4058
4330
  const transport = new StdioServerTransport();
@@ -4164,8 +4436,8 @@ var init_sync = __esm({
4164
4436
  run: async ({ args }) => {
4165
4437
  const { detectProject: detectProject2 } = await Promise.resolve().then(() => (init_detector(), detector_exports));
4166
4438
  const { RulesSyncer: RulesSyncer2 } = await Promise.resolve().then(() => (init_syncer(), syncer_exports));
4167
- const { promises: fs4 } = await import("fs");
4168
- const path6 = await import("path");
4439
+ const { promises: fs5 } = await import("fs");
4440
+ const path8 = await import("path");
4169
4441
  p2.intro("memorix sync");
4170
4442
  const project = detectProject2();
4171
4443
  p2.log.info(`Project: ${project.name} (${project.id})`);
@@ -4233,9 +4505,9 @@ var init_sync = __esm({
4233
4505
  process.exit(0);
4234
4506
  }
4235
4507
  for (const file of files) {
4236
- const fullPath = path6.join(project.rootPath, file.filePath);
4237
- await fs4.mkdir(path6.dirname(fullPath), { recursive: true });
4238
- await fs4.writeFile(fullPath, file.content, "utf-8");
4508
+ const fullPath = path8.join(project.rootPath, file.filePath);
4509
+ await fs5.mkdir(path8.dirname(fullPath), { recursive: true });
4510
+ await fs5.writeFile(fullPath, file.content, "utf-8");
4239
4511
  p2.log.success(`Written: ${file.filePath}`);
4240
4512
  }
4241
4513
  p2.outro(`Synced ${files.length} rule(s) to ${target} format`);
@@ -4960,13 +5232,58 @@ var init_hooks = __esm({
4960
5232
  }
4961
5233
  });
4962
5234
 
5235
+ // src/cli/commands/dashboard.ts
5236
+ var dashboard_exports = {};
5237
+ __export(dashboard_exports, {
5238
+ default: () => dashboard_default
5239
+ });
5240
+ import { defineCommand as defineCommand9 } from "citty";
5241
+ import { fileURLToPath as fileURLToPath2 } from "url";
5242
+ import path7 from "path";
5243
+ var dashboard_default;
5244
+ var init_dashboard = __esm({
5245
+ "src/cli/commands/dashboard.ts"() {
5246
+ "use strict";
5247
+ init_esm_shims();
5248
+ dashboard_default = defineCommand9({
5249
+ meta: {
5250
+ name: "dashboard",
5251
+ description: "Launch the Memorix Web Dashboard"
5252
+ },
5253
+ args: {
5254
+ port: {
5255
+ type: "string",
5256
+ description: "Port to run the dashboard on (default: 3210)",
5257
+ default: "3210"
5258
+ }
5259
+ },
5260
+ run: async ({ args }) => {
5261
+ const { detectProject: detectProject2 } = await Promise.resolve().then(() => (init_detector(), detector_exports));
5262
+ const { getProjectDataDir: getProjectDataDir2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
5263
+ const { startDashboard: startDashboard2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5264
+ const project = detectProject2();
5265
+ const dataDir = await getProjectDataDir2(project.id);
5266
+ const port = parseInt(args.port, 10) || 3210;
5267
+ const cliDir = path7.dirname(fileURLToPath2(import.meta.url));
5268
+ const staticDir = path7.join(cliDir, "..", "dashboard", "static");
5269
+ await startDashboard2(dataDir, port, staticDir, project.id, project.name);
5270
+ await new Promise(() => {
5271
+ });
5272
+ }
5273
+ });
5274
+ }
5275
+ });
5276
+
4963
5277
  // src/cli/index.ts
4964
5278
  init_esm_shims();
4965
- import { defineCommand as defineCommand9, runMain } from "citty";
4966
- var main = defineCommand9({
5279
+ import { defineCommand as defineCommand10, runMain } from "citty";
5280
+ import { createRequire as createRequire2 } from "module";
5281
+ var require2 = createRequire2(import.meta.url);
5282
+ var pkg = require2("../../package.json");
5283
+ var main = defineCommand10({
4967
5284
  meta: {
4968
5285
  name: "memorix",
4969
- version: "0.3.9",
5286
+ version: pkg.version,
4970
5287
  description: "Cross-Agent Memory Bridge \u2014 Universal memory layer for AI coding agents via MCP"
4971
5288
  },
4972
5289
  subCommands: {
@@ -4974,7 +5291,8 @@ var main = defineCommand9({
4974
5291
  status: () => Promise.resolve().then(() => (init_status(), status_exports)).then((m) => m.default),
4975
5292
  sync: () => Promise.resolve().then(() => (init_sync(), sync_exports)).then((m) => m.default),
4976
5293
  hook: () => Promise.resolve().then(() => (init_hook(), hook_exports)).then((m) => m.default),
4977
- hooks: () => Promise.resolve().then(() => (init_hooks(), hooks_exports)).then((m) => m.default)
5294
+ hooks: () => Promise.resolve().then(() => (init_hooks(), hooks_exports)).then((m) => m.default),
5295
+ dashboard: () => Promise.resolve().then(() => (init_dashboard(), dashboard_exports)).then((m) => m.default)
4978
5296
  },
4979
5297
  run() {
4980
5298
  }