memorix 0.5.2 → 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.
@@ -241,9 +241,15 @@ document.querySelectorAll('.nav-btn').forEach(btn => {
241
241
  // API Client
242
242
  // ============================================================
243
243
 
244
+ let selectedProject = ''; // empty = current project (default)
245
+
244
246
  async function api(endpoint) {
245
247
  try {
246
- const res = await fetch(`/api/${endpoint}`);
248
+ const sep = endpoint.includes('?') ? '&' : '?';
249
+ const url = selectedProject
250
+ ? `/api/${endpoint}${sep}project=${encodeURIComponent(selectedProject)}`
251
+ : `/api/${endpoint}`;
252
+ const res = await fetch(url);
247
253
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
248
254
  return await res.json();
249
255
  } catch (err) {
@@ -252,6 +258,49 @@ async function api(endpoint) {
252
258
  }
253
259
  }
254
260
 
261
+ // ============================================================
262
+ // Project Switcher
263
+ // ============================================================
264
+
265
+ async function initProjectSwitcher() {
266
+ const select = document.getElementById('project-select');
267
+ if (!select) return;
268
+
269
+ // Fetch project list
270
+ try {
271
+ const res = await fetch('/api/projects');
272
+ const projects = await res.json();
273
+ if (!Array.isArray(projects) || projects.length === 0) {
274
+ select.innerHTML = '<option value="">No projects</option>';
275
+ return;
276
+ }
277
+
278
+ select.innerHTML = '';
279
+ for (const p of projects) {
280
+ const opt = document.createElement('option');
281
+ opt.value = p.isCurrent ? '' : p.id;
282
+ opt.textContent = p.name + (p.isCurrent ? ' ●' : '');
283
+ opt.title = p.id;
284
+ if (p.isCurrent) opt.selected = true;
285
+ select.appendChild(opt);
286
+ }
287
+ } catch {
288
+ select.innerHTML = '<option value="">Error</option>';
289
+ }
290
+
291
+ // Switch handler
292
+ select.addEventListener('change', () => {
293
+ selectedProject = select.value;
294
+ // Clear all cached pages and reload current
295
+ Object.keys(loaded).forEach(k => delete loaded[k]);
296
+ loadPage(currentPage);
297
+ });
298
+ }
299
+
300
+ document.addEventListener('DOMContentLoaded', () => {
301
+ initProjectSwitcher();
302
+ });
303
+
255
304
  // ============================================================
256
305
  // Page Loaders
257
306
  // ============================================================
@@ -18,6 +18,11 @@
18
18
  <div class="sidebar-brand">
19
19
  <img src="/logo.png" alt="Memorix" class="brand-logo" />
20
20
  </div>
21
+ <div class="project-switcher">
22
+ <select id="project-select" title="Switch project / 切换项目">
23
+ <option value="">Loading...</option>
24
+ </select>
25
+ </div>
21
26
  <div class="sidebar-nav">
22
27
  <button class="nav-btn active" data-page="dashboard" title="Dashboard">
23
28
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -1045,4 +1045,51 @@ body {
1045
1045
  .page {
1046
1046
  padding: 16px;
1047
1047
  }
1048
+ }
1049
+
1050
+ /* ============================================================
1051
+ * Project Switcher
1052
+ * ============================================================ */
1053
+
1054
+ .project-switcher {
1055
+ padding: 4px 8px 8px;
1056
+ display: flex;
1057
+ justify-content: center;
1058
+ }
1059
+
1060
+ .project-switcher select {
1061
+ width: 44px;
1062
+ height: 28px;
1063
+ background: var(--bg-card);
1064
+ color: var(--text-primary);
1065
+ border: 1px solid var(--border-subtle);
1066
+ border-radius: 6px;
1067
+ font-size: 10px;
1068
+ font-family: var(--font-mono);
1069
+ cursor: pointer;
1070
+ padding: 0 2px;
1071
+ text-align: center;
1072
+ -webkit-appearance: none;
1073
+ -moz-appearance: none;
1074
+ appearance: none;
1075
+ transition: var(--transition-fast);
1076
+ overflow: hidden;
1077
+ text-overflow: ellipsis;
1078
+ }
1079
+
1080
+ .project-switcher select:hover {
1081
+ border-color: var(--accent-cyan);
1082
+ box-shadow: var(--glow-cyan);
1083
+ }
1084
+
1085
+ .project-switcher select:focus {
1086
+ outline: none;
1087
+ border-color: var(--accent-cyan);
1088
+ }
1089
+
1090
+ .project-switcher select option {
1091
+ background: var(--bg-surface);
1092
+ color: var(--text-primary);
1093
+ font-size: 12px;
1094
+ padding: 4px 8px;
1048
1095
  }
package/dist/index.js CHANGED
@@ -29,17 +29,138 @@ var init_esm_shims = __esm({
29
29
  });
30
30
 
31
31
  // src/store/persistence.ts
32
+ var persistence_exports = {};
33
+ __export(persistence_exports, {
34
+ getBaseDataDir: () => getBaseDataDir,
35
+ getDbFilePath: () => getDbFilePath,
36
+ getGraphFilePath: () => getGraphFilePath,
37
+ getProjectDataDir: () => getProjectDataDir,
38
+ hasExistingData: () => hasExistingData,
39
+ listProjectDirs: () => listProjectDirs,
40
+ loadGraphJsonl: () => loadGraphJsonl,
41
+ loadIdCounter: () => loadIdCounter,
42
+ loadObservationsJson: () => loadObservationsJson,
43
+ migrateGlobalData: () => migrateGlobalData,
44
+ saveGraphJsonl: () => saveGraphJsonl,
45
+ saveIdCounter: () => saveIdCounter,
46
+ saveObservationsJson: () => saveObservationsJson
47
+ });
32
48
  import { promises as fs } from "fs";
33
49
  import path2 from "path";
34
50
  import os from "os";
51
+ function sanitizeProjectId(projectId) {
52
+ return projectId.replace(/\//g, "--").replace(/[<>:"|?*\\]/g, "_");
53
+ }
35
54
  async function getProjectDataDir(projectId, baseDir) {
36
- 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);
37
58
  await fs.mkdir(dataDir, { recursive: true });
38
59
  return dataDir;
39
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
+ }
150
+ function getDbFilePath(projectDir2) {
151
+ return path2.join(projectDir2, "memorix.msp");
152
+ }
40
153
  function getGraphFilePath(projectDir2) {
41
154
  return path2.join(projectDir2, "graph.jsonl");
42
155
  }
156
+ async function hasExistingData(projectDir2) {
157
+ try {
158
+ await fs.access(getDbFilePath(projectDir2));
159
+ return true;
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
43
164
  async function saveGraphJsonl(projectDir2, entities, relations) {
44
165
  const lines = [
45
166
  ...entities.map(
@@ -645,6 +766,18 @@ async function detectInstalledAgents() {
645
766
  agents.push("kiro");
646
767
  } catch {
647
768
  }
769
+ const codexDir = path5.join(home, ".codex");
770
+ try {
771
+ await fs3.access(codexDir);
772
+ agents.push("codex");
773
+ } catch {
774
+ }
775
+ const antigravityDir = path5.join(home, ".gemini", "antigravity");
776
+ try {
777
+ await fs3.access(antigravityDir);
778
+ agents.push("antigravity");
779
+ } catch {
780
+ }
648
781
  return agents;
649
782
  }
650
783
  async function installHooks(agent, projectRoot, global = false) {
@@ -721,15 +854,34 @@ async function installAgentRules(agent, projectRoot) {
721
854
  case "copilot":
722
855
  rulesPath = path5.join(projectRoot, ".github", "copilot-instructions.md");
723
856
  break;
857
+ case "codex":
858
+ rulesPath = path5.join(projectRoot, "AGENTS.md");
859
+ break;
860
+ case "kiro":
861
+ rulesPath = path5.join(projectRoot, ".kiro", "rules", "memorix.md");
862
+ break;
724
863
  default:
725
- return;
864
+ rulesPath = path5.join(projectRoot, ".agent", "rules", "memorix.md");
865
+ break;
726
866
  }
727
867
  try {
728
868
  await fs3.mkdir(path5.dirname(rulesPath), { recursive: true });
729
- try {
730
- await fs3.access(rulesPath);
731
- } catch {
732
- await fs3.writeFile(rulesPath, rulesContent, "utf-8");
869
+ if (agent === "codex") {
870
+ try {
871
+ const existing = await fs3.readFile(rulesPath, "utf-8");
872
+ if (existing.includes("Memorix")) {
873
+ return;
874
+ }
875
+ await fs3.writeFile(rulesPath, existing + "\n\n" + rulesContent, "utf-8");
876
+ } catch {
877
+ await fs3.writeFile(rulesPath, rulesContent, "utf-8");
878
+ }
879
+ } else {
880
+ try {
881
+ await fs3.access(rulesPath);
882
+ } catch {
883
+ await fs3.writeFile(rulesPath, rulesContent, "utf-8");
884
+ }
733
885
  }
734
886
  } catch {
735
887
  }
@@ -743,22 +895,50 @@ You have access to Memorix memory tools. Follow these rules to maintain persiste
743
895
 
744
896
  At the **beginning of every conversation**, before responding to the user:
745
897
 
746
- 1. Call \`memorix_search\` with query related to the user's first message or the current project
747
- 2. If results are found, use them to understand the current project state, recent decisions, and pending tasks
748
- 3. Reference relevant memories naturally in your response
898
+ 1. Call \`memorix_search\` with a query related to the user's first message or the current project
899
+ 2. If results are found, use \`memorix_detail\` to fetch the most relevant ones
900
+ 3. Reference relevant memories naturally in your response \u2014 the user should feel you "remember" them
749
901
 
750
902
  This ensures you already know the project context without the user re-explaining.
751
903
 
752
904
  ## During Session \u2014 Capture Important Context
753
905
 
754
- Proactively call \`memorix_store\` when any of the following happen:
906
+ **Proactively** call \`memorix_store\` whenever any of the following happen:
907
+
908
+ ### Architecture & Decisions
909
+ - Technology choice, framework selection, or design pattern adopted
910
+ - Trade-off discussion with a clear conclusion
911
+ - API design, database schema, or project structure decisions
912
+
913
+ ### Bug Fixes & Problem Solving
914
+ - A bug is identified and resolved \u2014 store root cause + fix
915
+ - Workaround applied for a known issue
916
+ - Performance issue diagnosed and optimized
755
917
 
756
- - **Architecture decision**: You or the user decide on a technology, pattern, or approach
757
- - **Bug fix**: A bug is identified and resolved \u2014 store the root cause and fix
758
- - **Gotcha/pitfall**: Something unexpected or tricky is discovered
759
- - **Configuration change**: Environment, port, path, or tooling changes
918
+ ### Gotchas & Pitfalls
919
+ - Something unexpected or tricky is discovered
920
+ - A common mistake is identified and corrected
921
+ - Platform-specific behavior that caused issues
760
922
 
761
- Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`.
923
+ ### Configuration & Environment
924
+ - Environment variables, port numbers, paths changed
925
+ - Docker, nginx, Caddy, or reverse proxy config modified
926
+ - Package dependencies added, removed, or version-pinned
927
+
928
+ ### Deployment & Operations
929
+ - Server deployment steps (Docker, VPS, cloud)
930
+ - DNS, SSL/TLS certificate, domain configuration
931
+ - CI/CD pipeline setup or changes
932
+ - Database migration or data transfer procedures
933
+ - Server topology (ports, services, reverse proxy chain)
934
+ - SSH keys, access credentials setup (store pattern, NOT secrets)
935
+
936
+ ### Project Milestones
937
+ - Feature completed or shipped
938
+ - Version released or published to npm/PyPI/etc.
939
+ - Repository made public, README updated, PR submitted
940
+
941
+ Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`, \`how-it-works\`.
762
942
 
763
943
  ## Session End \u2014 Store Summary
764
944
 
@@ -766,18 +946,21 @@ When the conversation is ending or the user says goodbye:
766
946
 
767
947
  1. Call \`memorix_store\` with type \`session-request\` to record:
768
948
  - What was accomplished in this session
769
- - Current project state
949
+ - Current project state and any blockers
770
950
  - Pending tasks or next steps
771
- - Any unresolved issues
951
+ - Key files modified
772
952
 
773
- This creates a "handoff note" for the next session.
953
+ This creates a "handoff note" for the next session (or for another AI agent).
774
954
 
775
955
  ## Guidelines
776
956
 
777
- - **Don't store trivial information** (greetings, acknowledgments, simple file reads)
957
+ - **Don't store trivial information** (greetings, acknowledgments, simple file reads, ls/dir output)
778
958
  - **Do store anything you'd want to know if you lost all context**
779
- - **Use concise titles** and structured facts
959
+ - **Do store anything a different AI agent would need to continue this work**
960
+ - **Use concise titles** (~5-10 words) and structured facts
780
961
  - **Include file paths** in filesModified when relevant
962
+ - **Include related concepts** for better searchability
963
+ - **Prefer storing too much over too little** \u2014 the retention system will auto-decay stale memories
781
964
  `;
782
965
  }
783
966
  async function uninstallHooks(agent, projectRoot, global = false) {
@@ -802,7 +985,7 @@ async function uninstallHooks(agent, projectRoot, global = false) {
802
985
  }
803
986
  async function getHookStatus(projectRoot) {
804
987
  const results = [];
805
- const agents = ["claude", "copilot", "windsurf", "cursor", "kiro", "codex"];
988
+ const agents = ["claude", "copilot", "windsurf", "cursor", "kiro", "codex", "antigravity"];
806
989
  for (const agent of agents) {
807
990
  const projectPath = getProjectConfigPath(agent, projectRoot);
808
991
  const globalPath = getGlobalConfigPath(agent);
@@ -967,31 +1150,65 @@ function sendError(res, message, status = 500) {
967
1150
  function filterByProject(items, projectId) {
968
1151
  return items.filter((item) => item.projectId === projectId);
969
1152
  }
970
- async function handleApi(req, res, dataDir, projectId, projectName) {
1153
+ async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
971
1154
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
972
1155
  const apiPath = url.pathname.replace("/api", "");
1156
+ const requestedProject = url.searchParams.get("project");
1157
+ let effectiveDataDir = dataDir;
1158
+ let effectiveProjectId = projectId;
1159
+ let effectiveProjectName = projectName;
1160
+ if (requestedProject && requestedProject !== projectId) {
1161
+ const sanitized = requestedProject.replace(/\//g, "--").replace(/[<>:"|?*\\]/g, "_");
1162
+ const candidateDir = path6.join(baseDir, sanitized);
1163
+ try {
1164
+ await fs4.access(candidateDir);
1165
+ effectiveDataDir = candidateDir;
1166
+ effectiveProjectId = requestedProject;
1167
+ effectiveProjectName = requestedProject.split("/").pop() || requestedProject;
1168
+ } catch {
1169
+ }
1170
+ }
973
1171
  try {
974
1172
  switch (apiPath) {
1173
+ case "/projects": {
1174
+ try {
1175
+ const entries = await fs4.readdir(baseDir, { withFileTypes: true });
1176
+ const projects = entries.filter((e) => e.isDirectory() && e.name.includes("--")).map((e) => {
1177
+ const dirName = e.name;
1178
+ const id = dirName.replace(/--/g, "/");
1179
+ return {
1180
+ id,
1181
+ name: id.split("/").pop() || id,
1182
+ dirName,
1183
+ isCurrent: id === projectId
1184
+ };
1185
+ });
1186
+ sendJson(res, projects);
1187
+ } catch {
1188
+ sendJson(res, []);
1189
+ }
1190
+ break;
1191
+ }
975
1192
  case "/project": {
976
- sendJson(res, { id: projectId, name: projectName });
1193
+ sendJson(res, { id: effectiveProjectId, name: effectiveProjectName });
977
1194
  break;
978
1195
  }
979
1196
  case "/graph": {
980
- const graph = await loadGraphJsonl(dataDir);
1197
+ const graph = await loadGraphJsonl(effectiveDataDir);
981
1198
  sendJson(res, graph);
982
1199
  break;
983
1200
  }
984
1201
  case "/observations": {
985
- const allObs = await loadObservationsJson(dataDir);
986
- const observations2 = filterByProject(allObs, projectId);
1202
+ const allObs = await loadObservationsJson(effectiveDataDir);
1203
+ const observations2 = filterByProject(allObs, effectiveProjectId);
987
1204
  sendJson(res, observations2);
988
1205
  break;
989
1206
  }
990
1207
  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);
1208
+ const graph = await loadGraphJsonl(effectiveDataDir);
1209
+ const allObs = await loadObservationsJson(effectiveDataDir);
1210
+ const observations2 = filterByProject(allObs, effectiveProjectId);
1211
+ const nextId2 = await loadIdCounter(effectiveDataDir);
995
1212
  const typeCounts = {};
996
1213
  for (const obs of observations2) {
997
1214
  const t = obs.type || "unknown";
@@ -1009,8 +1226,8 @@ async function handleApi(req, res, dataDir, projectId, projectName) {
1009
1226
  break;
1010
1227
  }
1011
1228
  case "/retention": {
1012
- const allObs = await loadObservationsJson(dataDir);
1013
- const observations2 = filterByProject(allObs, projectId);
1229
+ const allObs = await loadObservationsJson(effectiveDataDir);
1230
+ const observations2 = filterByProject(allObs, effectiveProjectId);
1014
1231
  const now = Date.now();
1015
1232
  const scored = observations2.map((obs) => {
1016
1233
  const age = now - new Date(obs.createdAt || now).getTime();
@@ -1087,10 +1304,11 @@ function openBrowser(url) {
1087
1304
  }
1088
1305
  async function startDashboard(dataDir, port, staticDir, projectId, projectName, autoOpen = true) {
1089
1306
  const resolvedStaticDir = staticDir;
1307
+ const baseDir = getBaseDataDir();
1090
1308
  const server = createServer(async (req, res) => {
1091
1309
  const url = req.url || "/";
1092
1310
  if (url.startsWith("/api/")) {
1093
- await handleApi(req, res, dataDir, projectId, projectName);
1311
+ await handleApi(req, res, dataDir, projectId, projectName, baseDir);
1094
1312
  } else {
1095
1313
  await serveStatic(req, res, resolvedStaticDir);
1096
1314
  }
@@ -1719,12 +1937,14 @@ function detectProject(cwd) {
1719
1937
  const rootPath = getGitRoot(basePath) ?? findPackageRoot(basePath) ?? basePath;
1720
1938
  const gitRemote = getGitRemote(rootPath);
1721
1939
  if (gitRemote) {
1722
- const id = normalizeGitRemote(gitRemote);
1723
- const name2 = id.split("/").pop() ?? path3.basename(rootPath);
1724
- return { id, name: name2, gitRemote, rootPath };
1940
+ const id2 = normalizeGitRemote(gitRemote);
1941
+ const name2 = id2.split("/").pop() ?? path3.basename(rootPath);
1942
+ return { id: id2, name: name2, gitRemote, rootPath };
1725
1943
  }
1726
1944
  const name = path3.basename(rootPath);
1727
- return { id: name, name, rootPath };
1945
+ const id = `local/${name}`;
1946
+ console.error(`[memorix] Warning: no git remote found at ${rootPath}, using fallback projectId: ${id}`);
1947
+ return { id, name, rootPath };
1728
1948
  }
1729
1949
  function findPackageRoot(cwd) {
1730
1950
  let dir = path3.resolve(cwd);
@@ -3384,6 +3604,14 @@ var OBSERVATION_TYPES = [
3384
3604
  ];
3385
3605
  async function createMemorixServer(cwd) {
3386
3606
  const project = detectProject(cwd);
3607
+ try {
3608
+ const { migrateGlobalData: migrateGlobalData2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
3609
+ const migrated = await migrateGlobalData2(project.id);
3610
+ if (migrated) {
3611
+ console.error(`[memorix] Migrated legacy data to project directory: ${project.id}`);
3612
+ }
3613
+ } catch {
3614
+ }
3387
3615
  const projectDir2 = await getProjectDataDir(project.id);
3388
3616
  const graphManager = new KnowledgeGraphManager(projectDir2);
3389
3617
  await graphManager.init();
@@ -3398,15 +3626,14 @@ async function createMemorixServer(cwd) {
3398
3626
  const { getHookStatus: getHookStatus2, installHooks: installHooks2, detectInstalledAgents: detectInstalledAgents2 } = await Promise.resolve().then(() => (init_installers(), installers_exports));
3399
3627
  const workDir = cwd ?? process.cwd();
3400
3628
  const statuses = await getHookStatus2(workDir);
3401
- const anyInstalled = statuses.some((s) => s.installed);
3402
- if (!anyInstalled) {
3403
- const agents = await detectInstalledAgents2();
3404
- for (const agent of agents) {
3405
- try {
3406
- const config = await installHooks2(agent, workDir);
3407
- console.error(`[memorix] Auto-installed hooks for ${agent} \u2192 ${config.configPath}`);
3408
- } catch {
3409
- }
3629
+ const installedAgents = new Set(statuses.filter((s) => s.installed).map((s) => s.agent));
3630
+ const detectedAgents = await detectInstalledAgents2();
3631
+ for (const agent of detectedAgents) {
3632
+ if (installedAgents.has(agent)) continue;
3633
+ try {
3634
+ const config = await installHooks2(agent, workDir);
3635
+ console.error(`[memorix] Auto-installed hooks for ${agent} \u2192 ${config.configPath}`);
3636
+ } catch {
3410
3637
  }
3411
3638
  }
3412
3639
  } catch {
@@ -3543,15 +3770,20 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3543
3770
  query: z.string().describe("Search query (natural language or keywords)"),
3544
3771
  limit: z.number().optional().describe("Max results (default: 20)"),
3545
3772
  type: z.enum(OBSERVATION_TYPES).optional().describe("Filter by observation type"),
3546
- maxTokens: z.number().optional().describe("Token budget \u2014 trim results to fit (0 = unlimited)")
3773
+ maxTokens: z.number().optional().describe("Token budget \u2014 trim results to fit (0 = unlimited)"),
3774
+ scope: z.enum(["project", "global"]).optional().describe(
3775
+ 'Search scope: "project" (default) only searches current project, "global" searches all projects'
3776
+ )
3547
3777
  }
3548
3778
  },
3549
- async ({ query, limit, type, maxTokens }) => {
3779
+ async ({ query, limit, type, maxTokens, scope }) => {
3550
3780
  const result = await compactSearch({
3551
3781
  query,
3552
3782
  limit,
3553
3783
  type,
3554
- maxTokens
3784
+ maxTokens,
3785
+ // Default to current project scope; 'global' removes the project filter
3786
+ projectId: scope === "global" ? void 0 : project.id
3555
3787
  });
3556
3788
  let text = result.formatted;
3557
3789
  if (!syncAdvisoryShown && syncAdvisory) {