memorix 0.5.2 → 0.6.1

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/cli/index.js CHANGED
@@ -31,13 +31,16 @@ var init_esm_shims = __esm({
31
31
  // src/store/persistence.ts
32
32
  var persistence_exports = {};
33
33
  __export(persistence_exports, {
34
+ getBaseDataDir: () => getBaseDataDir,
34
35
  getDbFilePath: () => getDbFilePath,
35
36
  getGraphFilePath: () => getGraphFilePath,
36
37
  getProjectDataDir: () => getProjectDataDir,
37
38
  hasExistingData: () => hasExistingData,
39
+ listProjectDirs: () => listProjectDirs,
38
40
  loadGraphJsonl: () => loadGraphJsonl,
39
41
  loadIdCounter: () => loadIdCounter,
40
42
  loadObservationsJson: () => loadObservationsJson,
43
+ migrateGlobalData: () => migrateGlobalData,
41
44
  saveGraphJsonl: () => saveGraphJsonl,
42
45
  saveIdCounter: () => saveIdCounter,
43
46
  saveObservationsJson: () => saveObservationsJson
@@ -45,11 +48,105 @@ __export(persistence_exports, {
45
48
  import { promises as fs } from "fs";
46
49
  import path2 from "path";
47
50
  import os from "os";
51
+ function sanitizeProjectId(projectId) {
52
+ return projectId.replace(/\//g, "--").replace(/[<>:"|?*\\]/g, "_");
53
+ }
48
54
  async function getProjectDataDir(projectId, baseDir) {
49
- const dataDir = baseDir ?? DEFAULT_DATA_DIR;
55
+ const base = baseDir ?? DEFAULT_DATA_DIR;
56
+ const dirName = sanitizeProjectId(projectId);
57
+ const dataDir = path2.join(base, dirName);
50
58
  await fs.mkdir(dataDir, { recursive: true });
51
59
  return dataDir;
52
60
  }
61
+ function getBaseDataDir(baseDir) {
62
+ return baseDir ?? DEFAULT_DATA_DIR;
63
+ }
64
+ async function listProjectDirs(baseDir) {
65
+ const base = baseDir ?? DEFAULT_DATA_DIR;
66
+ try {
67
+ const entries = await fs.readdir(base, { withFileTypes: true });
68
+ return entries.filter((e) => e.isDirectory()).map((e) => path2.join(base, e.name));
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+ async function migrateGlobalData(projectId, baseDir) {
74
+ const base = baseDir ?? DEFAULT_DATA_DIR;
75
+ const globalObsPath = path2.join(base, "observations.json");
76
+ const migratedObsPath = path2.join(base, "observations.json.migrated");
77
+ let sourceObsPath = null;
78
+ try {
79
+ await fs.access(globalObsPath);
80
+ sourceObsPath = globalObsPath;
81
+ } catch {
82
+ try {
83
+ await fs.access(migratedObsPath);
84
+ sourceObsPath = migratedObsPath;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ let globalObs = [];
90
+ try {
91
+ const data = await fs.readFile(sourceObsPath, "utf-8");
92
+ globalObs = JSON.parse(data);
93
+ if (!Array.isArray(globalObs) || globalObs.length === 0) return false;
94
+ } catch {
95
+ return false;
96
+ }
97
+ const projectDir2 = await getProjectDataDir(projectId, baseDir);
98
+ const projectObsPath = path2.join(projectDir2, "observations.json");
99
+ let projectObs = [];
100
+ try {
101
+ const data = await fs.readFile(projectObsPath, "utf-8");
102
+ projectObs = JSON.parse(data);
103
+ if (!Array.isArray(projectObs)) projectObs = [];
104
+ } catch {
105
+ }
106
+ if (projectObs.length >= globalObs.length) {
107
+ return false;
108
+ }
109
+ const existingIds = new Set(projectObs.map((o) => o.id));
110
+ const merged = [...projectObs];
111
+ for (const obs of globalObs) {
112
+ if (!existingIds.has(obs.id)) {
113
+ merged.push(obs);
114
+ }
115
+ }
116
+ merged.sort((a, b) => (a.id ?? 0) - (b.id ?? 0));
117
+ for (const obs of merged) {
118
+ obs.projectId = projectId;
119
+ }
120
+ await fs.writeFile(projectObsPath, JSON.stringify(merged, null, 2), "utf-8");
121
+ for (const file of ["graph.jsonl", "counter.json"]) {
122
+ const src = path2.join(base, file);
123
+ const srcMigrated = path2.join(base, file + ".migrated");
124
+ const dst = path2.join(projectDir2, file);
125
+ for (const source of [src, srcMigrated]) {
126
+ try {
127
+ await fs.access(source);
128
+ await fs.copyFile(source, dst);
129
+ break;
130
+ } catch {
131
+ }
132
+ }
133
+ }
134
+ const maxId = merged.reduce((max, o) => Math.max(max, o.id ?? 0), 0);
135
+ await fs.writeFile(
136
+ path2.join(projectDir2, "counter.json"),
137
+ JSON.stringify({ nextId: maxId + 1 }),
138
+ "utf-8"
139
+ );
140
+ for (const file of ["observations.json", "graph.jsonl", "counter.json"]) {
141
+ const src = path2.join(base, file);
142
+ try {
143
+ await fs.access(src);
144
+ await fs.rename(src, src + ".migrated");
145
+ } catch {
146
+ }
147
+ }
148
+ return true;
149
+ }
53
150
  function getDbFilePath(projectDir2) {
54
151
  return path2.join(projectDir2, "memorix.msp");
55
152
  }
@@ -1132,12 +1229,14 @@ function detectProject(cwd) {
1132
1229
  const rootPath = getGitRoot(basePath) ?? findPackageRoot(basePath) ?? basePath;
1133
1230
  const gitRemote = getGitRemote(rootPath);
1134
1231
  if (gitRemote) {
1135
- const id = normalizeGitRemote(gitRemote);
1136
- const name2 = id.split("/").pop() ?? path3.basename(rootPath);
1137
- return { id, name: name2, gitRemote, rootPath };
1232
+ const id2 = normalizeGitRemote(gitRemote);
1233
+ const name2 = id2.split("/").pop() ?? path3.basename(rootPath);
1234
+ return { id: id2, name: name2, gitRemote, rootPath };
1138
1235
  }
1139
1236
  const name = path3.basename(rootPath);
1140
- return { id: name, name, rootPath };
1237
+ const id = `local/${name}`;
1238
+ console.error(`[memorix] Warning: no git remote found at ${rootPath}, using fallback projectId: ${id}`);
1239
+ return { id, name, rootPath };
1141
1240
  }
1142
1241
  function findPackageRoot(cwd) {
1143
1242
  let dir = path3.resolve(cwd);
@@ -1700,6 +1799,76 @@ var init_copilot = __esm({
1700
1799
  }
1701
1800
  });
1702
1801
 
1802
+ // src/rules/adapters/kiro.ts
1803
+ import matter7 from "gray-matter";
1804
+ var KiroAdapter;
1805
+ var init_kiro = __esm({
1806
+ "src/rules/adapters/kiro.ts"() {
1807
+ "use strict";
1808
+ init_esm_shims();
1809
+ init_utils();
1810
+ KiroAdapter = class {
1811
+ source = "kiro";
1812
+ filePatterns = [
1813
+ ".kiro/steering/*.md",
1814
+ "AGENTS.md"
1815
+ ];
1816
+ parse(filePath, content) {
1817
+ if (filePath.includes(".kiro/steering/")) {
1818
+ return this.parseSteeringRule(filePath, content);
1819
+ }
1820
+ if (filePath.endsWith("AGENTS.md")) {
1821
+ return this.parseAgentsMd(filePath, content);
1822
+ }
1823
+ return [];
1824
+ }
1825
+ generate(rules) {
1826
+ return rules.map((rule, i) => {
1827
+ const fm = {};
1828
+ if (rule.description) fm.description = rule.description;
1829
+ const fileName = rule.id.replace(/^kiro:/, "").replace(/[^a-zA-Z0-9-_]/g, "-") || `rule-${i}`;
1830
+ const body = Object.keys(fm).length > 0 ? matter7.stringify(rule.content, fm) : rule.content;
1831
+ return {
1832
+ filePath: `.kiro/steering/${fileName}.md`,
1833
+ content: body
1834
+ };
1835
+ });
1836
+ }
1837
+ parseSteeringRule(filePath, content) {
1838
+ const { data, content: body } = matter7(content);
1839
+ const trimmed = body.trim();
1840
+ if (!trimmed) return [];
1841
+ const trigger = data.trigger;
1842
+ const alwaysApply = !trigger || trigger === "always";
1843
+ return [{
1844
+ id: generateRuleId("kiro", filePath),
1845
+ content: trimmed,
1846
+ description: data.description,
1847
+ source: "kiro",
1848
+ scope: alwaysApply ? "global" : "path-specific",
1849
+ paths: data.globs,
1850
+ alwaysApply,
1851
+ priority: alwaysApply ? 10 : 5,
1852
+ hash: hashContent(trimmed)
1853
+ }];
1854
+ }
1855
+ parseAgentsMd(filePath, content) {
1856
+ const trimmed = content.trim();
1857
+ if (!trimmed) return [];
1858
+ return [{
1859
+ id: generateRuleId("kiro", filePath),
1860
+ content: trimmed,
1861
+ source: "kiro",
1862
+ scope: "project",
1863
+ alwaysApply: true,
1864
+ priority: 10,
1865
+ hash: hashContent(trimmed)
1866
+ }];
1867
+ }
1868
+ };
1869
+ }
1870
+ });
1871
+
1703
1872
  // src/rules/syncer.ts
1704
1873
  var syncer_exports = {};
1705
1874
  __export(syncer_exports, {
@@ -1718,6 +1887,7 @@ var init_syncer = __esm({
1718
1887
  init_windsurf();
1719
1888
  init_antigravity();
1720
1889
  init_copilot();
1890
+ init_kiro();
1721
1891
  RulesSyncer = class {
1722
1892
  projectRoot;
1723
1893
  adapters;
@@ -1730,7 +1900,8 @@ var init_syncer = __esm({
1730
1900
  new CodexAdapter(),
1731
1901
  new WindsurfAdapter(),
1732
1902
  new AntigravityAdapter(),
1733
- new CopilotAdapter()
1903
+ new CopilotAdapter(),
1904
+ new KiroAdapter()
1734
1905
  ];
1735
1906
  for (const a of all) {
1736
1907
  this.adapters.set(a.source, a);
@@ -2339,8 +2510,60 @@ var init_antigravity2 = __esm({
2339
2510
  }
2340
2511
  });
2341
2512
 
2513
+ // src/workspace/mcp-adapters/kiro.ts
2514
+ import { homedir as homedir7 } from "os";
2515
+ import { join as join7 } from "path";
2516
+ var KiroMCPAdapter;
2517
+ var init_kiro2 = __esm({
2518
+ "src/workspace/mcp-adapters/kiro.ts"() {
2519
+ "use strict";
2520
+ init_esm_shims();
2521
+ KiroMCPAdapter = class {
2522
+ source = "kiro";
2523
+ parse(content) {
2524
+ try {
2525
+ const config = JSON.parse(content);
2526
+ const servers = config.mcpServers ?? {};
2527
+ return Object.entries(servers).map(([name, entry]) => ({
2528
+ name,
2529
+ command: entry.command ?? "",
2530
+ args: entry.args ?? [],
2531
+ ...entry.env && Object.keys(entry.env).length > 0 ? { env: entry.env } : {},
2532
+ ...entry.url ? { url: entry.url } : {}
2533
+ }));
2534
+ } catch {
2535
+ return [];
2536
+ }
2537
+ }
2538
+ generate(servers) {
2539
+ const mcpServers = {};
2540
+ for (const s of servers) {
2541
+ const entry = {};
2542
+ if (s.url) {
2543
+ entry.url = s.url;
2544
+ } else {
2545
+ entry.command = s.command;
2546
+ entry.args = s.args;
2547
+ }
2548
+ if (s.env && Object.keys(s.env).length > 0) {
2549
+ entry.env = s.env;
2550
+ }
2551
+ mcpServers[s.name] = entry;
2552
+ }
2553
+ return JSON.stringify({ mcpServers }, null, 2);
2554
+ }
2555
+ getConfigPath(projectRoot) {
2556
+ if (projectRoot) {
2557
+ return join7(projectRoot, ".kiro", "settings", "mcp.json");
2558
+ }
2559
+ return join7(homedir7(), ".kiro", "settings", "mcp.json");
2560
+ }
2561
+ };
2562
+ }
2563
+ });
2564
+
2342
2565
  // src/workspace/workflow-sync.ts
2343
- import matter7 from "gray-matter";
2566
+ import matter8 from "gray-matter";
2344
2567
  var WorkflowSyncer;
2345
2568
  var init_workflow_sync = __esm({
2346
2569
  "src/workspace/workflow-sync.ts"() {
@@ -2355,7 +2578,7 @@ var init_workflow_sync = __esm({
2355
2578
  let description = "";
2356
2579
  let content = raw;
2357
2580
  try {
2358
- const parsed = matter7(raw);
2581
+ const parsed = matter8(raw);
2359
2582
  description = parsed.data?.description ?? "";
2360
2583
  content = parsed.content.trim();
2361
2584
  } catch {
@@ -2377,7 +2600,7 @@ var init_workflow_sync = __esm({
2377
2600
  if (wf.description) {
2378
2601
  fm.description = wf.description;
2379
2602
  }
2380
- const content = matter7.stringify(wf.content, fm);
2603
+ const content = matter8.stringify(wf.content, fm);
2381
2604
  return {
2382
2605
  filePath: `.agents/skills/${safeName}/SKILL.md`,
2383
2606
  content
@@ -2394,7 +2617,7 @@ var init_workflow_sync = __esm({
2394
2617
  }
2395
2618
  fm.globs = "";
2396
2619
  fm.alwaysApply = "false";
2397
- const content = matter7.stringify(wf.content, fm);
2620
+ const content = matter8.stringify(wf.content, fm);
2398
2621
  return {
2399
2622
  filePath: `.cursor/rules/${safeName}.mdc`,
2400
2623
  content
@@ -2582,8 +2805,8 @@ var init_applier = __esm({
2582
2805
 
2583
2806
  // src/workspace/engine.ts
2584
2807
  import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync4, cpSync, mkdirSync as mkdirSync2 } from "fs";
2585
- import { join as join8 } from "path";
2586
- import { homedir as homedir7 } from "os";
2808
+ import { join as join9 } from "path";
2809
+ import { homedir as homedir8 } from "os";
2587
2810
  var WorkspaceSyncEngine;
2588
2811
  var init_engine2 = __esm({
2589
2812
  "src/workspace/engine.ts"() {
@@ -2595,6 +2818,7 @@ var init_engine2 = __esm({
2595
2818
  init_claude_code2();
2596
2819
  init_copilot2();
2597
2820
  init_antigravity2();
2821
+ init_kiro2();
2598
2822
  init_workflow_sync();
2599
2823
  init_syncer();
2600
2824
  init_sanitizer();
@@ -2608,7 +2832,8 @@ var init_engine2 = __esm({
2608
2832
  ["codex", new CodexMCPAdapter()],
2609
2833
  ["claude-code", new ClaudeCodeMCPAdapter()],
2610
2834
  ["copilot", new CopilotMCPAdapter()],
2611
- ["antigravity", new AntigravityMCPAdapter()]
2835
+ ["antigravity", new AntigravityMCPAdapter()],
2836
+ ["kiro", new KiroMCPAdapter()]
2612
2837
  ]);
2613
2838
  this.workflowSyncer = new WorkflowSyncer();
2614
2839
  this.rulesSyncer = new RulesSyncer(projectRoot);
@@ -2626,7 +2851,8 @@ var init_engine2 = __esm({
2626
2851
  codex: [],
2627
2852
  "claude-code": [],
2628
2853
  copilot: [],
2629
- antigravity: []
2854
+ antigravity: [],
2855
+ kiro: []
2630
2856
  };
2631
2857
  for (const [target, adapter] of this.adapters) {
2632
2858
  const configPath = adapter.getConfigPath(this.projectRoot);
@@ -2718,13 +2944,14 @@ var init_engine2 = __esm({
2718
2944
  windsurf: [".windsurf/skills"],
2719
2945
  "claude-code": [".claude/skills"],
2720
2946
  copilot: [".github/skills", ".copilot/skills"],
2721
- antigravity: [".agent/skills", ".gemini/skills", ".gemini/antigravity/skills"]
2947
+ antigravity: [".agent/skills", ".gemini/skills", ".gemini/antigravity/skills"],
2948
+ kiro: [".kiro/skills"]
2722
2949
  };
2723
2950
  /** Get the target skills directory for an agent (null if agent has no skills support) */
2724
2951
  getTargetSkillsDir(target) {
2725
2952
  const dirs = _WorkspaceSyncEngine.SKILLS_DIRS[target];
2726
2953
  if (!dirs || dirs.length === 0) return null;
2727
- return join8(this.projectRoot, dirs[0]);
2954
+ return join9(this.projectRoot, dirs[0]);
2728
2955
  }
2729
2956
  /**
2730
2957
  * Scan all agent skills directories and collect unique skills.
@@ -2733,12 +2960,12 @@ var init_engine2 = __esm({
2733
2960
  const skills = [];
2734
2961
  const conflicts = [];
2735
2962
  const seen = /* @__PURE__ */ new Map();
2736
- const home = homedir7();
2963
+ const home = homedir8();
2737
2964
  for (const [agent, dirs] of Object.entries(_WorkspaceSyncEngine.SKILLS_DIRS)) {
2738
2965
  for (const dir of dirs) {
2739
2966
  const paths = [
2740
- join8(this.projectRoot, dir),
2741
- join8(home, dir)
2967
+ join9(this.projectRoot, dir),
2968
+ join9(home, dir)
2742
2969
  ];
2743
2970
  for (const skillsRoot of paths) {
2744
2971
  if (!existsSync4(skillsRoot)) continue;
@@ -2746,7 +2973,7 @@ var init_engine2 = __esm({
2746
2973
  const entries = readdirSync(skillsRoot, { withFileTypes: true });
2747
2974
  for (const entry of entries) {
2748
2975
  if (!entry.isDirectory()) continue;
2749
- const skillMd = join8(skillsRoot, entry.name, "SKILL.md");
2976
+ const skillMd = join9(skillsRoot, entry.name, "SKILL.md");
2750
2977
  if (!existsSync4(skillMd)) continue;
2751
2978
  let description = "";
2752
2979
  try {
@@ -2758,7 +2985,7 @@ var init_engine2 = __esm({
2758
2985
  const newEntry = {
2759
2986
  name: entry.name,
2760
2987
  description,
2761
- sourcePath: join8(skillsRoot, entry.name),
2988
+ sourcePath: join9(skillsRoot, entry.name),
2762
2989
  sourceAgent: agent
2763
2990
  };
2764
2991
  const existing = seen.get(entry.name);
@@ -2795,7 +3022,7 @@ var init_engine2 = __esm({
2795
3022
  }
2796
3023
  for (const skill of skills) {
2797
3024
  if (skill.sourceAgent === target) continue;
2798
- const dest = join8(targetDir, skill.name);
3025
+ const dest = join9(targetDir, skill.name);
2799
3026
  if (existsSync4(dest)) {
2800
3027
  skipped.push(`${skill.name} (already exists in ${target})`);
2801
3028
  continue;
@@ -2811,13 +3038,13 @@ var init_engine2 = __esm({
2811
3038
  }
2812
3039
  scanWorkflows() {
2813
3040
  const workflows = [];
2814
- const wfDir = join8(this.projectRoot, ".windsurf", "workflows");
3041
+ const wfDir = join9(this.projectRoot, ".windsurf", "workflows");
2815
3042
  if (!existsSync4(wfDir)) return workflows;
2816
3043
  try {
2817
3044
  const files = readdirSync(wfDir).filter((f) => f.endsWith(".md"));
2818
3045
  for (const file of files) {
2819
3046
  try {
2820
- const content = readFileSync2(join8(wfDir, file), "utf-8");
3047
+ const content = readFileSync2(join9(wfDir, file), "utf-8");
2821
3048
  workflows.push(this.workflowSyncer.parseWindsurfWorkflow(file, content));
2822
3049
  } catch {
2823
3050
  }
@@ -2905,7 +3132,8 @@ var init_engine2 = __esm({
2905
3132
  codex: "codex",
2906
3133
  windsurf: "windsurf",
2907
3134
  copilot: "copilot",
2908
- antigravity: "antigravity"
3135
+ antigravity: "antigravity",
3136
+ kiro: "kiro"
2909
3137
  };
2910
3138
  return map[target] ?? null;
2911
3139
  }
@@ -3076,6 +3304,18 @@ async function detectInstalledAgents() {
3076
3304
  agents.push("kiro");
3077
3305
  } catch {
3078
3306
  }
3307
+ const codexDir = path5.join(home, ".codex");
3308
+ try {
3309
+ await fs3.access(codexDir);
3310
+ agents.push("codex");
3311
+ } catch {
3312
+ }
3313
+ const antigravityDir = path5.join(home, ".gemini", "antigravity");
3314
+ try {
3315
+ await fs3.access(antigravityDir);
3316
+ agents.push("antigravity");
3317
+ } catch {
3318
+ }
3079
3319
  return agents;
3080
3320
  }
3081
3321
  async function installHooks(agent, projectRoot, global = false) {
@@ -3152,15 +3392,34 @@ async function installAgentRules(agent, projectRoot) {
3152
3392
  case "copilot":
3153
3393
  rulesPath = path5.join(projectRoot, ".github", "copilot-instructions.md");
3154
3394
  break;
3395
+ case "codex":
3396
+ rulesPath = path5.join(projectRoot, "AGENTS.md");
3397
+ break;
3398
+ case "kiro":
3399
+ rulesPath = path5.join(projectRoot, ".kiro", "rules", "memorix.md");
3400
+ break;
3155
3401
  default:
3156
- return;
3402
+ rulesPath = path5.join(projectRoot, ".agent", "rules", "memorix.md");
3403
+ break;
3157
3404
  }
3158
3405
  try {
3159
3406
  await fs3.mkdir(path5.dirname(rulesPath), { recursive: true });
3160
- try {
3161
- await fs3.access(rulesPath);
3162
- } catch {
3163
- await fs3.writeFile(rulesPath, rulesContent, "utf-8");
3407
+ if (agent === "codex") {
3408
+ try {
3409
+ const existing = await fs3.readFile(rulesPath, "utf-8");
3410
+ if (existing.includes("Memorix")) {
3411
+ return;
3412
+ }
3413
+ await fs3.writeFile(rulesPath, existing + "\n\n" + rulesContent, "utf-8");
3414
+ } catch {
3415
+ await fs3.writeFile(rulesPath, rulesContent, "utf-8");
3416
+ }
3417
+ } else {
3418
+ try {
3419
+ await fs3.access(rulesPath);
3420
+ } catch {
3421
+ await fs3.writeFile(rulesPath, rulesContent, "utf-8");
3422
+ }
3164
3423
  }
3165
3424
  } catch {
3166
3425
  }
@@ -3174,22 +3433,50 @@ You have access to Memorix memory tools. Follow these rules to maintain persiste
3174
3433
 
3175
3434
  At the **beginning of every conversation**, before responding to the user:
3176
3435
 
3177
- 1. Call \`memorix_search\` with query related to the user's first message or the current project
3178
- 2. If results are found, use them to understand the current project state, recent decisions, and pending tasks
3179
- 3. Reference relevant memories naturally in your response
3436
+ 1. Call \`memorix_search\` with a query related to the user's first message or the current project
3437
+ 2. If results are found, use \`memorix_detail\` to fetch the most relevant ones
3438
+ 3. Reference relevant memories naturally in your response \u2014 the user should feel you "remember" them
3180
3439
 
3181
3440
  This ensures you already know the project context without the user re-explaining.
3182
3441
 
3183
3442
  ## During Session \u2014 Capture Important Context
3184
3443
 
3185
- Proactively call \`memorix_store\` when any of the following happen:
3444
+ **Proactively** call \`memorix_store\` whenever any of the following happen:
3445
+
3446
+ ### Architecture & Decisions
3447
+ - Technology choice, framework selection, or design pattern adopted
3448
+ - Trade-off discussion with a clear conclusion
3449
+ - API design, database schema, or project structure decisions
3450
+
3451
+ ### Bug Fixes & Problem Solving
3452
+ - A bug is identified and resolved \u2014 store root cause + fix
3453
+ - Workaround applied for a known issue
3454
+ - Performance issue diagnosed and optimized
3455
+
3456
+ ### Gotchas & Pitfalls
3457
+ - Something unexpected or tricky is discovered
3458
+ - A common mistake is identified and corrected
3459
+ - Platform-specific behavior that caused issues
3186
3460
 
3187
- - **Architecture decision**: You or the user decide on a technology, pattern, or approach
3188
- - **Bug fix**: A bug is identified and resolved \u2014 store the root cause and fix
3189
- - **Gotcha/pitfall**: Something unexpected or tricky is discovered
3190
- - **Configuration change**: Environment, port, path, or tooling changes
3461
+ ### Configuration & Environment
3462
+ - Environment variables, port numbers, paths changed
3463
+ - Docker, nginx, Caddy, or reverse proxy config modified
3464
+ - Package dependencies added, removed, or version-pinned
3191
3465
 
3192
- Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`.
3466
+ ### Deployment & Operations
3467
+ - Server deployment steps (Docker, VPS, cloud)
3468
+ - DNS, SSL/TLS certificate, domain configuration
3469
+ - CI/CD pipeline setup or changes
3470
+ - Database migration or data transfer procedures
3471
+ - Server topology (ports, services, reverse proxy chain)
3472
+ - SSH keys, access credentials setup (store pattern, NOT secrets)
3473
+
3474
+ ### Project Milestones
3475
+ - Feature completed or shipped
3476
+ - Version released or published to npm/PyPI/etc.
3477
+ - Repository made public, README updated, PR submitted
3478
+
3479
+ Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`, \`how-it-works\`.
3193
3480
 
3194
3481
  ## Session End \u2014 Store Summary
3195
3482
 
@@ -3197,18 +3484,21 @@ When the conversation is ending or the user says goodbye:
3197
3484
 
3198
3485
  1. Call \`memorix_store\` with type \`session-request\` to record:
3199
3486
  - What was accomplished in this session
3200
- - Current project state
3487
+ - Current project state and any blockers
3201
3488
  - Pending tasks or next steps
3202
- - Any unresolved issues
3489
+ - Key files modified
3203
3490
 
3204
- This creates a "handoff note" for the next session.
3491
+ This creates a "handoff note" for the next session (or for another AI agent).
3205
3492
 
3206
3493
  ## Guidelines
3207
3494
 
3208
- - **Don't store trivial information** (greetings, acknowledgments, simple file reads)
3495
+ - **Don't store trivial information** (greetings, acknowledgments, simple file reads, ls/dir output)
3209
3496
  - **Do store anything you'd want to know if you lost all context**
3210
- - **Use concise titles** and structured facts
3497
+ - **Do store anything a different AI agent would need to continue this work**
3498
+ - **Use concise titles** (~5-10 words) and structured facts
3211
3499
  - **Include file paths** in filesModified when relevant
3500
+ - **Include related concepts** for better searchability
3501
+ - **Prefer storing too much over too little** \u2014 the retention system will auto-decay stale memories
3212
3502
  `;
3213
3503
  }
3214
3504
  async function uninstallHooks(agent, projectRoot, global = false) {
@@ -3233,7 +3523,7 @@ async function uninstallHooks(agent, projectRoot, global = false) {
3233
3523
  }
3234
3524
  async function getHookStatus(projectRoot) {
3235
3525
  const results = [];
3236
- const agents = ["claude", "copilot", "windsurf", "cursor", "kiro", "codex"];
3526
+ const agents = ["claude", "copilot", "windsurf", "cursor", "kiro", "codex", "antigravity"];
3237
3527
  for (const agent of agents) {
3238
3528
  const projectPath = getProjectConfigPath(agent, projectRoot);
3239
3529
  const globalPath = getGlobalConfigPath(agent);
@@ -3398,31 +3688,65 @@ function sendError(res, message, status = 500) {
3398
3688
  function filterByProject(items, projectId) {
3399
3689
  return items.filter((item) => item.projectId === projectId);
3400
3690
  }
3401
- async function handleApi(req, res, dataDir, projectId, projectName) {
3691
+ async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
3402
3692
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
3403
3693
  const apiPath = url.pathname.replace("/api", "");
3694
+ const requestedProject = url.searchParams.get("project");
3695
+ let effectiveDataDir = dataDir;
3696
+ let effectiveProjectId = projectId;
3697
+ let effectiveProjectName = projectName;
3698
+ if (requestedProject && requestedProject !== projectId) {
3699
+ const sanitized = requestedProject.replace(/\//g, "--").replace(/[<>:"|?*\\]/g, "_");
3700
+ const candidateDir = path6.join(baseDir, sanitized);
3701
+ try {
3702
+ await fs4.access(candidateDir);
3703
+ effectiveDataDir = candidateDir;
3704
+ effectiveProjectId = requestedProject;
3705
+ effectiveProjectName = requestedProject.split("/").pop() || requestedProject;
3706
+ } catch {
3707
+ }
3708
+ }
3404
3709
  try {
3405
3710
  switch (apiPath) {
3711
+ case "/projects": {
3712
+ try {
3713
+ const entries = await fs4.readdir(baseDir, { withFileTypes: true });
3714
+ const projects = entries.filter((e) => e.isDirectory() && e.name.includes("--")).map((e) => {
3715
+ const dirName = e.name;
3716
+ const id = dirName.replace(/--/g, "/");
3717
+ return {
3718
+ id,
3719
+ name: id.split("/").pop() || id,
3720
+ dirName,
3721
+ isCurrent: id === projectId
3722
+ };
3723
+ });
3724
+ sendJson(res, projects);
3725
+ } catch {
3726
+ sendJson(res, []);
3727
+ }
3728
+ break;
3729
+ }
3406
3730
  case "/project": {
3407
- sendJson(res, { id: projectId, name: projectName });
3731
+ sendJson(res, { id: effectiveProjectId, name: effectiveProjectName });
3408
3732
  break;
3409
3733
  }
3410
3734
  case "/graph": {
3411
- const graph = await loadGraphJsonl(dataDir);
3735
+ const graph = await loadGraphJsonl(effectiveDataDir);
3412
3736
  sendJson(res, graph);
3413
3737
  break;
3414
3738
  }
3415
3739
  case "/observations": {
3416
- const allObs = await loadObservationsJson(dataDir);
3417
- const observations2 = filterByProject(allObs, projectId);
3740
+ const allObs = await loadObservationsJson(effectiveDataDir);
3741
+ const observations2 = filterByProject(allObs, effectiveProjectId);
3418
3742
  sendJson(res, observations2);
3419
3743
  break;
3420
3744
  }
3421
3745
  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);
3746
+ const graph = await loadGraphJsonl(effectiveDataDir);
3747
+ const allObs = await loadObservationsJson(effectiveDataDir);
3748
+ const observations2 = filterByProject(allObs, effectiveProjectId);
3749
+ const nextId2 = await loadIdCounter(effectiveDataDir);
3426
3750
  const typeCounts = {};
3427
3751
  for (const obs of observations2) {
3428
3752
  const t = obs.type || "unknown";
@@ -3440,8 +3764,8 @@ async function handleApi(req, res, dataDir, projectId, projectName) {
3440
3764
  break;
3441
3765
  }
3442
3766
  case "/retention": {
3443
- const allObs = await loadObservationsJson(dataDir);
3444
- const observations2 = filterByProject(allObs, projectId);
3767
+ const allObs = await loadObservationsJson(effectiveDataDir);
3768
+ const observations2 = filterByProject(allObs, effectiveProjectId);
3445
3769
  const now = Date.now();
3446
3770
  const scored = observations2.map((obs) => {
3447
3771
  const age = now - new Date(obs.createdAt || now).getTime();
@@ -3475,8 +3799,42 @@ async function handleApi(req, res, dataDir, projectId, projectName) {
3475
3799
  });
3476
3800
  break;
3477
3801
  }
3478
- default:
3802
+ default: {
3803
+ const deleteMatch = apiPath.match(/^\/observations\/(\d+)$/);
3804
+ if (deleteMatch && req.method === "DELETE") {
3805
+ const obsId = parseInt(deleteMatch[1], 10);
3806
+ const allObs = await loadObservationsJson(effectiveDataDir);
3807
+ const idx = allObs.findIndex((o) => o.id === obsId);
3808
+ if (idx === -1) {
3809
+ sendError(res, "Observation not found", 404);
3810
+ } else {
3811
+ allObs.splice(idx, 1);
3812
+ await saveObservationsJson(effectiveDataDir, allObs);
3813
+ sendJson(res, { ok: true, deleted: obsId });
3814
+ }
3815
+ break;
3816
+ }
3817
+ if (apiPath === "/export") {
3818
+ const graph = await loadGraphJsonl(effectiveDataDir);
3819
+ const allObs = await loadObservationsJson(effectiveDataDir);
3820
+ const observations2 = filterByProject(allObs, effectiveProjectId);
3821
+ const nextId2 = await loadIdCounter(effectiveDataDir);
3822
+ const exportData = {
3823
+ project: { id: effectiveProjectId, name: effectiveProjectName },
3824
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
3825
+ graph,
3826
+ observations: observations2,
3827
+ nextId: nextId2
3828
+ };
3829
+ res.writeHead(200, {
3830
+ "Content-Type": "application/json",
3831
+ "Content-Disposition": `attachment; filename="memorix-${effectiveProjectId.replace(/\//g, "-")}-export.json"`
3832
+ });
3833
+ res.end(JSON.stringify(exportData, null, 2));
3834
+ break;
3835
+ }
3479
3836
  sendError(res, "Not found", 404);
3837
+ }
3480
3838
  }
3481
3839
  } catch (err) {
3482
3840
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -3518,10 +3876,11 @@ function openBrowser(url) {
3518
3876
  }
3519
3877
  async function startDashboard(dataDir, port, staticDir, projectId, projectName, autoOpen = true) {
3520
3878
  const resolvedStaticDir = staticDir;
3879
+ const baseDir = getBaseDataDir();
3521
3880
  const server = createServer(async (req, res) => {
3522
3881
  const url = req.url || "/";
3523
3882
  if (url.startsWith("/api/")) {
3524
- await handleApi(req, res, dataDir, projectId, projectName);
3883
+ await handleApi(req, res, dataDir, projectId, projectName, baseDir);
3525
3884
  } else {
3526
3885
  await serveStatic(req, res, resolvedStaticDir);
3527
3886
  }
@@ -3579,6 +3938,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3579
3938
  import { z } from "zod";
3580
3939
  async function createMemorixServer(cwd) {
3581
3940
  const project = detectProject(cwd);
3941
+ try {
3942
+ const { migrateGlobalData: migrateGlobalData2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
3943
+ const migrated = await migrateGlobalData2(project.id);
3944
+ if (migrated) {
3945
+ console.error(`[memorix] Migrated legacy data to project directory: ${project.id}`);
3946
+ }
3947
+ } catch {
3948
+ }
3582
3949
  const projectDir2 = await getProjectDataDir(project.id);
3583
3950
  const graphManager = new KnowledgeGraphManager(projectDir2);
3584
3951
  await graphManager.init();
@@ -3593,15 +3960,14 @@ async function createMemorixServer(cwd) {
3593
3960
  const { getHookStatus: getHookStatus2, installHooks: installHooks2, detectInstalledAgents: detectInstalledAgents2 } = await Promise.resolve().then(() => (init_installers(), installers_exports));
3594
3961
  const workDir = cwd ?? process.cwd();
3595
3962
  const statuses = await getHookStatus2(workDir);
3596
- const anyInstalled = statuses.some((s) => s.installed);
3597
- if (!anyInstalled) {
3598
- const agents = await detectInstalledAgents2();
3599
- for (const agent of agents) {
3600
- try {
3601
- const config = await installHooks2(agent, workDir);
3602
- console.error(`[memorix] Auto-installed hooks for ${agent} \u2192 ${config.configPath}`);
3603
- } catch {
3604
- }
3963
+ const installedAgents = new Set(statuses.filter((s) => s.installed).map((s) => s.agent));
3964
+ const detectedAgents = await detectInstalledAgents2();
3965
+ for (const agent of detectedAgents) {
3966
+ if (installedAgents.has(agent)) continue;
3967
+ try {
3968
+ const config = await installHooks2(agent, workDir);
3969
+ console.error(`[memorix] Auto-installed hooks for ${agent} \u2192 ${config.configPath}`);
3970
+ } catch {
3605
3971
  }
3606
3972
  }
3607
3973
  } catch {
@@ -3738,15 +4104,20 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3738
4104
  query: z.string().describe("Search query (natural language or keywords)"),
3739
4105
  limit: z.number().optional().describe("Max results (default: 20)"),
3740
4106
  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)")
4107
+ maxTokens: z.number().optional().describe("Token budget \u2014 trim results to fit (0 = unlimited)"),
4108
+ scope: z.enum(["project", "global"]).optional().describe(
4109
+ 'Search scope: "project" (default) only searches current project, "global" searches all projects'
4110
+ )
3742
4111
  }
3743
4112
  },
3744
- async ({ query, limit, type, maxTokens }) => {
4113
+ async ({ query, limit, type, maxTokens, scope }) => {
3745
4114
  const result = await compactSearch({
3746
4115
  query,
3747
4116
  limit,
3748
4117
  type,
3749
- maxTokens
4118
+ maxTokens,
4119
+ // Default to current project scope; 'global' removes the project filter
4120
+ projectId: scope === "global" ? void 0 : project.id
3750
4121
  });
3751
4122
  let text = result.formatted;
3752
4123
  if (!syncAdvisoryShown && syncAdvisory) {
@@ -4325,7 +4696,29 @@ var init_serve = __esm({
4325
4696
  run: async ({ args }) => {
4326
4697
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
4327
4698
  const { createMemorixServer: createMemorixServer2 } = await Promise.resolve().then(() => (init_server2(), server_exports2));
4328
- const projectRoot = args.cwd || process.cwd();
4699
+ const { execSync: execSync2 } = await import("child_process");
4700
+ let projectRoot = args.cwd || process.env.INIT_CWD || process.cwd();
4701
+ try {
4702
+ execSync2("git rev-parse --show-toplevel", {
4703
+ cwd: projectRoot,
4704
+ encoding: "utf-8",
4705
+ stdio: ["pipe", "pipe", "pipe"]
4706
+ });
4707
+ } catch {
4708
+ const scriptDir = new URL(".", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
4709
+ try {
4710
+ const gitRoot = execSync2("git rev-parse --show-toplevel", {
4711
+ cwd: scriptDir,
4712
+ encoding: "utf-8",
4713
+ stdio: ["pipe", "pipe", "pipe"]
4714
+ }).trim();
4715
+ if (gitRoot) {
4716
+ projectRoot = gitRoot;
4717
+ console.error(`[memorix] CWD has no git, using script dir: ${projectRoot}`);
4718
+ }
4719
+ } catch {
4720
+ }
4721
+ }
4329
4722
  const { server, projectId } = await createMemorixServer2(projectRoot);
4330
4723
  const transport = new StdioServerTransport();
4331
4724
  await server.connect(transport);
@@ -4725,7 +5118,8 @@ function patternToObservationType(pattern) {
4725
5118
  gotcha: "gotcha",
4726
5119
  configuration: "what-changed",
4727
5120
  learning: "discovery",
4728
- implementation: "what-changed"
5121
+ implementation: "what-changed",
5122
+ deployment: "what-changed"
4729
5123
  };
4730
5124
  return map[pattern] ?? "discovery";
4731
5125
  }
@@ -4796,6 +5190,23 @@ var init_pattern_detector = __esm({
4796
5190
  ],
4797
5191
  minLength: 200,
4798
5192
  baseConfidence: 0.5
5193
+ },
5194
+ {
5195
+ type: "deployment",
5196
+ keywords: [
5197
+ /\b(deploy(ed|ing|ment)?|ship(ped|ping)?|releas(ed|ing)|publish(ed|ing)?)\b/i,
5198
+ /(部署|发布|上线|迁移|运维)/,
5199
+ /\b(docker|compose|container|kubernetes|k8s|helm)\b/i,
5200
+ /\b(VPS|server|host(ing)?|cloud|AWS|Azure|GCP|Cloudflare)\b/i,
5201
+ /\b(nginx|caddy|apache|reverse.?proxy|load.?balanc)\b/i,
5202
+ /\b(SSL|TLS|cert(ificate)?|HTTPS|Let'?s?.?Encrypt|ACME)\b/i,
5203
+ /\b(DNS|domain|A.?record|CNAME|nameserver|Cloudflare)\b/i,
5204
+ /\b(CI\/CD|pipeline|GitHub.?Actions|Jenkins|GitLab.?CI)\b/i,
5205
+ /\b(scp|rsync|ssh|sftp|systemd|systemctl|service)\b/i,
5206
+ /(服务器|域名|证书|反向代理|负载均衡|镜像|容器)/
5207
+ ],
5208
+ minLength: 80,
5209
+ baseConfidence: 0.75
4799
5210
  }
4800
5211
  ];
4801
5212
  }