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/dist/index.js CHANGED
@@ -28,6 +28,103 @@ var init_esm_shims = __esm({
28
28
  }
29
29
  });
30
30
 
31
+ // src/store/persistence.ts
32
+ import { promises as fs } from "fs";
33
+ import path2 from "path";
34
+ import os from "os";
35
+ async function getProjectDataDir(projectId, baseDir) {
36
+ const dataDir = baseDir ?? DEFAULT_DATA_DIR;
37
+ await fs.mkdir(dataDir, { recursive: true });
38
+ return dataDir;
39
+ }
40
+ function getGraphFilePath(projectDir2) {
41
+ return path2.join(projectDir2, "graph.jsonl");
42
+ }
43
+ async function saveGraphJsonl(projectDir2, entities, relations) {
44
+ const lines = [
45
+ ...entities.map(
46
+ (e) => JSON.stringify({ type: "entity", name: e.name, entityType: e.entityType, observations: e.observations })
47
+ ),
48
+ ...relations.map(
49
+ (r) => JSON.stringify({ type: "relation", from: r.from, to: r.to, relationType: r.relationType })
50
+ )
51
+ ];
52
+ await fs.writeFile(getGraphFilePath(projectDir2), lines.join("\n"), "utf-8");
53
+ }
54
+ async function loadGraphJsonl(projectDir2) {
55
+ const filePath = getGraphFilePath(projectDir2);
56
+ try {
57
+ const data = await fs.readFile(filePath, "utf-8");
58
+ const lines = data.split("\n").filter((line) => line.trim() !== "");
59
+ return lines.reduce(
60
+ (graph, line) => {
61
+ const item = JSON.parse(line);
62
+ if (item.type === "entity") {
63
+ graph.entities.push({
64
+ name: item.name,
65
+ entityType: item.entityType,
66
+ observations: item.observations
67
+ });
68
+ }
69
+ if (item.type === "relation") {
70
+ graph.relations.push({
71
+ from: item.from,
72
+ to: item.to,
73
+ relationType: item.relationType
74
+ });
75
+ }
76
+ return graph;
77
+ },
78
+ {
79
+ entities: [],
80
+ relations: []
81
+ }
82
+ );
83
+ } catch (error) {
84
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
85
+ return { entities: [], relations: [] };
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ async function saveObservationsJson(projectDir2, observations2) {
91
+ const filePath = path2.join(projectDir2, "observations.json");
92
+ await fs.writeFile(filePath, JSON.stringify(observations2, null, 2), "utf-8");
93
+ }
94
+ async function loadObservationsJson(projectDir2) {
95
+ const filePath = path2.join(projectDir2, "observations.json");
96
+ try {
97
+ const data = await fs.readFile(filePath, "utf-8");
98
+ return JSON.parse(data);
99
+ } catch (error) {
100
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
101
+ return [];
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+ async function saveIdCounter(projectDir2, nextId2) {
107
+ const filePath = path2.join(projectDir2, "counter.json");
108
+ await fs.writeFile(filePath, JSON.stringify({ nextId: nextId2 }), "utf-8");
109
+ }
110
+ async function loadIdCounter(projectDir2) {
111
+ const filePath = path2.join(projectDir2, "counter.json");
112
+ try {
113
+ const data = await fs.readFile(filePath, "utf-8");
114
+ return JSON.parse(data).nextId ?? 1;
115
+ } catch {
116
+ return 1;
117
+ }
118
+ }
119
+ var DEFAULT_DATA_DIR;
120
+ var init_persistence = __esm({
121
+ "src/store/persistence.ts"() {
122
+ "use strict";
123
+ init_esm_shims();
124
+ DEFAULT_DATA_DIR = path2.join(os.homedir(), ".memorix", "data");
125
+ }
126
+ });
127
+
31
128
  // src/types.ts
32
129
  var OBSERVATION_ICONS;
33
130
  var init_types = __esm({
@@ -848,111 +945,212 @@ var init_retention = __esm({
848
945
  }
849
946
  });
850
947
 
851
- // src/index.ts
852
- init_esm_shims();
853
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
854
-
855
- // src/server.ts
856
- init_esm_shims();
857
- import { watch } from "fs";
858
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
859
- import { z } from "zod";
860
-
861
- // src/memory/graph.ts
862
- init_esm_shims();
863
-
864
- // src/store/persistence.ts
865
- init_esm_shims();
866
- import { promises as fs } from "fs";
867
- import path2 from "path";
868
- import os from "os";
869
- var DEFAULT_DATA_DIR = path2.join(os.homedir(), ".memorix", "data");
870
- async function getProjectDataDir(projectId, baseDir) {
871
- const dataDir = baseDir ?? DEFAULT_DATA_DIR;
872
- await fs.mkdir(dataDir, { recursive: true });
873
- return dataDir;
948
+ // src/dashboard/server.ts
949
+ var server_exports = {};
950
+ __export(server_exports, {
951
+ startDashboard: () => startDashboard
952
+ });
953
+ import { createServer } from "http";
954
+ import { promises as fs4 } from "fs";
955
+ import path6 from "path";
956
+ import { exec } from "child_process";
957
+ function sendJson(res, data, status = 200) {
958
+ res.writeHead(status, {
959
+ "Content-Type": "application/json; charset=utf-8",
960
+ "Access-Control-Allow-Origin": "*"
961
+ });
962
+ res.end(JSON.stringify(data));
874
963
  }
875
- function getGraphFilePath(projectDir2) {
876
- return path2.join(projectDir2, "graph.jsonl");
964
+ function sendError(res, message, status = 500) {
965
+ sendJson(res, { error: message }, status);
877
966
  }
878
- async function saveGraphJsonl(projectDir2, entities, relations) {
879
- const lines = [
880
- ...entities.map(
881
- (e) => JSON.stringify({ type: "entity", name: e.name, entityType: e.entityType, observations: e.observations })
882
- ),
883
- ...relations.map(
884
- (r) => JSON.stringify({ type: "relation", from: r.from, to: r.to, relationType: r.relationType })
885
- )
886
- ];
887
- await fs.writeFile(getGraphFilePath(projectDir2), lines.join("\n"), "utf-8");
967
+ function filterByProject(items, projectId) {
968
+ return items.filter((item) => item.projectId === projectId);
888
969
  }
889
- async function loadGraphJsonl(projectDir2) {
890
- const filePath = getGraphFilePath(projectDir2);
970
+ async function handleApi(req, res, dataDir, projectId, projectName) {
971
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
972
+ const apiPath = url.pathname.replace("/api", "");
891
973
  try {
892
- const data = await fs.readFile(filePath, "utf-8");
893
- const lines = data.split("\n").filter((line) => line.trim() !== "");
894
- return lines.reduce(
895
- (graph, line) => {
896
- const item = JSON.parse(line);
897
- if (item.type === "entity") {
898
- graph.entities.push({
899
- name: item.name,
900
- entityType: item.entityType,
901
- observations: item.observations
902
- });
974
+ switch (apiPath) {
975
+ case "/project": {
976
+ sendJson(res, { id: projectId, name: projectName });
977
+ break;
978
+ }
979
+ case "/graph": {
980
+ const graph = await loadGraphJsonl(dataDir);
981
+ sendJson(res, graph);
982
+ break;
983
+ }
984
+ case "/observations": {
985
+ const allObs = await loadObservationsJson(dataDir);
986
+ const observations2 = filterByProject(allObs, projectId);
987
+ sendJson(res, observations2);
988
+ break;
989
+ }
990
+ case "/stats": {
991
+ const graph = await loadGraphJsonl(dataDir);
992
+ const allObs = await loadObservationsJson(dataDir);
993
+ const observations2 = filterByProject(allObs, projectId);
994
+ const nextId2 = await loadIdCounter(dataDir);
995
+ const typeCounts = {};
996
+ for (const obs of observations2) {
997
+ const t = obs.type || "unknown";
998
+ typeCounts[t] = (typeCounts[t] || 0) + 1;
903
999
  }
904
- if (item.type === "relation") {
905
- graph.relations.push({
906
- from: item.from,
907
- to: item.to,
908
- relationType: item.relationType
909
- });
910
- }
911
- return graph;
912
- },
913
- {
914
- entities: [],
915
- relations: []
1000
+ const sorted = [...observations2].sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
1001
+ sendJson(res, {
1002
+ entities: graph.entities.length,
1003
+ relations: graph.relations.length,
1004
+ observations: observations2.length,
1005
+ nextId: nextId2,
1006
+ typeCounts,
1007
+ recentObservations: sorted
1008
+ });
1009
+ break;
1010
+ }
1011
+ case "/retention": {
1012
+ const allObs = await loadObservationsJson(dataDir);
1013
+ const observations2 = filterByProject(allObs, projectId);
1014
+ const now = Date.now();
1015
+ const scored = observations2.map((obs) => {
1016
+ const age = now - new Date(obs.createdAt || now).getTime();
1017
+ const ageHours = age / (1e3 * 60 * 60);
1018
+ const importance = obs.importance ?? 5;
1019
+ const accessCount = obs.accessCount ?? 0;
1020
+ const lambda = 0.01;
1021
+ const decayScore = importance * Math.exp(-lambda * ageHours);
1022
+ const accessBonus = Math.min(accessCount * 0.5, 3);
1023
+ const score = Math.min(decayScore + accessBonus, 10);
1024
+ const isImmune2 = importance >= 8 || obs.type === "gotcha" || obs.type === "decision";
1025
+ return {
1026
+ id: obs.id,
1027
+ title: obs.title,
1028
+ type: obs.type,
1029
+ entityName: obs.entityName,
1030
+ score: Math.round(score * 100) / 100,
1031
+ isImmune: isImmune2,
1032
+ ageHours: Math.round(ageHours * 10) / 10,
1033
+ accessCount
1034
+ };
1035
+ });
1036
+ scored.sort((a, b) => b.score - a.score);
1037
+ const activeCount = scored.filter((s) => s.score >= 3).length;
1038
+ const staleCount = scored.filter((s) => s.score < 3 && s.score >= 1).length;
1039
+ const archiveCount = scored.filter((s) => s.score < 1).length;
1040
+ const immuneCount = scored.filter((s) => s.isImmune).length;
1041
+ sendJson(res, {
1042
+ summary: { active: activeCount, stale: staleCount, archive: archiveCount, immune: immuneCount },
1043
+ items: scored
1044
+ });
1045
+ break;
916
1046
  }
917
- );
918
- } catch (error) {
919
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
920
- return { entities: [], relations: [] };
1047
+ default:
1048
+ sendError(res, "Not found", 404);
921
1049
  }
922
- throw error;
1050
+ } catch (err) {
1051
+ const message = err instanceof Error ? err.message : "Unknown error";
1052
+ sendError(res, message);
923
1053
  }
924
1054
  }
925
- async function saveObservationsJson(projectDir2, observations2) {
926
- const filePath = path2.join(projectDir2, "observations.json");
927
- await fs.writeFile(filePath, JSON.stringify(observations2, null, 2), "utf-8");
928
- }
929
- async function loadObservationsJson(projectDir2) {
930
- const filePath = path2.join(projectDir2, "observations.json");
1055
+ async function serveStatic(req, res, staticDir) {
1056
+ let urlPath = new URL(req.url || "/", `http://${req.headers.host}`).pathname;
1057
+ if (urlPath === "/" || !urlPath.includes(".")) {
1058
+ urlPath = "/index.html";
1059
+ }
1060
+ const filePath = path6.join(staticDir, urlPath);
1061
+ if (!filePath.startsWith(staticDir)) {
1062
+ sendError(res, "Forbidden", 403);
1063
+ return;
1064
+ }
931
1065
  try {
932
- const data = await fs.readFile(filePath, "utf-8");
933
- return JSON.parse(data);
934
- } catch (error) {
935
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
936
- return [];
1066
+ const data = await fs4.readFile(filePath);
1067
+ const ext = path6.extname(filePath);
1068
+ res.writeHead(200, {
1069
+ "Content-Type": MIME_TYPES[ext] || "application/octet-stream",
1070
+ "Cache-Control": "no-cache"
1071
+ });
1072
+ res.end(data);
1073
+ } catch {
1074
+ try {
1075
+ const indexData = await fs4.readFile(path6.join(staticDir, "index.html"));
1076
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1077
+ res.end(indexData);
1078
+ } catch {
1079
+ sendError(res, "Not found", 404);
937
1080
  }
938
- throw error;
939
1081
  }
940
1082
  }
941
- async function saveIdCounter(projectDir2, nextId2) {
942
- const filePath = path2.join(projectDir2, "counter.json");
943
- await fs.writeFile(filePath, JSON.stringify({ nextId: nextId2 }), "utf-8");
1083
+ function openBrowser(url) {
1084
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
1085
+ exec(cmd, () => {
1086
+ });
944
1087
  }
945
- async function loadIdCounter(projectDir2) {
946
- const filePath = path2.join(projectDir2, "counter.json");
947
- try {
948
- const data = await fs.readFile(filePath, "utf-8");
949
- return JSON.parse(data).nextId ?? 1;
950
- } catch {
951
- return 1;
952
- }
1088
+ async function startDashboard(dataDir, port, staticDir, projectId, projectName, autoOpen = true) {
1089
+ const resolvedStaticDir = staticDir;
1090
+ const server = createServer(async (req, res) => {
1091
+ const url = req.url || "/";
1092
+ if (url.startsWith("/api/")) {
1093
+ await handleApi(req, res, dataDir, projectId, projectName);
1094
+ } else {
1095
+ await serveStatic(req, res, resolvedStaticDir);
1096
+ }
1097
+ });
1098
+ return new Promise((resolve2, reject) => {
1099
+ server.on("error", (err) => {
1100
+ if (err.code === "EADDRINUSE") {
1101
+ console.error(`Port ${port} is already in use. Try: memorix dashboard --port ${port + 1}`);
1102
+ reject(err);
1103
+ } else {
1104
+ reject(err);
1105
+ }
1106
+ });
1107
+ server.listen(port, () => {
1108
+ const url = `http://localhost:${port}`;
1109
+ console.error(`
1110
+ Memorix Dashboard`);
1111
+ 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`);
1112
+ console.error(` Project: ${projectName} (${projectId})`);
1113
+ console.error(` Local: ${url}`);
1114
+ console.error(` Data dir: ${dataDir}`);
1115
+ console.error(`
1116
+ Press Ctrl+C to stop
1117
+ `);
1118
+ if (autoOpen) openBrowser(url);
1119
+ resolve2();
1120
+ });
1121
+ });
953
1122
  }
1123
+ var MIME_TYPES;
1124
+ var init_server = __esm({
1125
+ "src/dashboard/server.ts"() {
1126
+ "use strict";
1127
+ init_esm_shims();
1128
+ init_persistence();
1129
+ MIME_TYPES = {
1130
+ ".html": "text/html; charset=utf-8",
1131
+ ".css": "text/css; charset=utf-8",
1132
+ ".js": "application/javascript; charset=utf-8",
1133
+ ".json": "application/json; charset=utf-8",
1134
+ ".svg": "image/svg+xml",
1135
+ ".png": "image/png",
1136
+ ".ico": "image/x-icon"
1137
+ };
1138
+ }
1139
+ });
1140
+
1141
+ // src/index.ts
1142
+ init_esm_shims();
1143
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1144
+
1145
+ // src/server.ts
1146
+ init_esm_shims();
1147
+ import { watch } from "fs";
1148
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1149
+ import { z } from "zod";
954
1150
 
955
1151
  // src/memory/graph.ts
1152
+ init_esm_shims();
1153
+ init_persistence();
956
1154
  var KnowledgeGraphManager = class {
957
1155
  entities = [];
958
1156
  relations = [];
@@ -1075,6 +1273,7 @@ var KnowledgeGraphManager = class {
1075
1273
  // src/memory/observations.ts
1076
1274
  init_esm_shims();
1077
1275
  init_orama_store();
1276
+ init_persistence();
1078
1277
 
1079
1278
  // src/compact/token-budget.ts
1080
1279
  init_esm_shims();
@@ -1578,6 +1777,9 @@ function normalizeGitRemote(remote) {
1578
1777
  }
1579
1778
  }
1580
1779
 
1780
+ // src/server.ts
1781
+ init_persistence();
1782
+
1581
1783
  // src/rules/syncer.ts
1582
1784
  init_esm_shims();
1583
1785
  import { promises as fs2 } from "fs";
@@ -2888,10 +3090,10 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
2888
3090
  for (const [target, adapter] of this.adapters) {
2889
3091
  const configPath = adapter.getConfigPath(this.projectRoot);
2890
3092
  const globalPath = adapter.getConfigPath();
2891
- for (const path6 of [configPath, globalPath]) {
2892
- if (existsSync4(path6)) {
3093
+ for (const path7 of [configPath, globalPath]) {
3094
+ if (existsSync4(path7)) {
2893
3095
  try {
2894
- const content = readFileSync2(path6, "utf-8");
3096
+ const content = readFileSync2(path7, "utf-8");
2895
3097
  const servers = adapter.parse(content);
2896
3098
  if (servers.length > 0) {
2897
3099
  mcpConfigs[target] = servers;
@@ -3792,6 +3994,85 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3792
3994
  };
3793
3995
  }
3794
3996
  );
3997
+ let dashboardRunning = false;
3998
+ server.registerTool(
3999
+ "memorix_dashboard",
4000
+ {
4001
+ title: "Launch Dashboard",
4002
+ description: "Launch the Memorix Web Dashboard in the browser. Shows knowledge graph, observations, retention scores, and project stats in a visual interface.",
4003
+ inputSchema: {
4004
+ port: z.number().optional().describe("Port to run the dashboard on (default: 3210)")
4005
+ }
4006
+ },
4007
+ async ({ port: dashboardPort }) => {
4008
+ const portNum = dashboardPort || 3210;
4009
+ const url = `http://localhost:${portNum}`;
4010
+ if (dashboardRunning) {
4011
+ const { exec: exec2 } = await import("child_process");
4012
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
4013
+ exec2(cmd, () => {
4014
+ });
4015
+ return {
4016
+ content: [{ type: "text", text: `Dashboard is already running at ${url}. Opened in browser.` }]
4017
+ };
4018
+ }
4019
+ try {
4020
+ const pathMod = await import("path");
4021
+ const fsMod = await import("fs");
4022
+ const { fileURLToPath: fileURLToPath2 } = await import("url");
4023
+ const { startDashboard: startDashboard2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4024
+ const candidates = [
4025
+ pathMod.default.join(__dirname, "..", "dashboard", "static"),
4026
+ pathMod.default.join(__dirname, "dashboard", "static"),
4027
+ pathMod.default.join(pathMod.default.dirname(fileURLToPath2(import.meta.url)), "..", "dashboard", "static"),
4028
+ pathMod.default.join(pathMod.default.dirname(fileURLToPath2(import.meta.url)), "dashboard", "static")
4029
+ ];
4030
+ for (const [i, c] of candidates.entries()) {
4031
+ const hasIndex = fsMod.existsSync(pathMod.default.join(c, "index.html"));
4032
+ console.error(`[memorix] candidate[${i}]: ${c} (has index.html: ${hasIndex})`);
4033
+ }
4034
+ let staticDir = candidates[0];
4035
+ for (const c of candidates) {
4036
+ if (fsMod.existsSync(pathMod.default.join(c, "index.html"))) {
4037
+ staticDir = c;
4038
+ break;
4039
+ }
4040
+ }
4041
+ console.error(`[memorix] Dashboard staticDir: ${staticDir}`);
4042
+ startDashboard2(projectDir2, portNum, staticDir, project.id, project.name, false).then(() => {
4043
+ dashboardRunning = true;
4044
+ }).catch((err) => {
4045
+ console.error("[memorix] Dashboard error:", err);
4046
+ dashboardRunning = false;
4047
+ });
4048
+ await new Promise((resolve2) => setTimeout(resolve2, 800));
4049
+ dashboardRunning = true;
4050
+ const { exec: execCmd } = await import("child_process");
4051
+ const openCmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
4052
+ execCmd(openCmd, () => {
4053
+ });
4054
+ return {
4055
+ content: [{
4056
+ type: "text",
4057
+ text: [
4058
+ `Memorix Dashboard started!`,
4059
+ ``,
4060
+ `URL: ${url}`,
4061
+ `Project: ${project.name} (${project.id})`,
4062
+ `Static: ${staticDir}`,
4063
+ ``,
4064
+ `The dashboard has been opened in your default browser.`,
4065
+ `It shows your knowledge graph, observations, retention scores, and project stats.`
4066
+ ].join("\n")
4067
+ }]
4068
+ };
4069
+ } catch (err) {
4070
+ return {
4071
+ content: [{ type: "text", text: `Failed to start dashboard: ${err instanceof Error ? err.message : String(err)}` }]
4072
+ };
4073
+ }
4074
+ }
4075
+ );
3795
4076
  return { server, graphManager, projectId: project.id };
3796
4077
  }
3797
4078