engram-mcp-server 1.2.2 → 1.2.4

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
@@ -216,15 +216,17 @@ Engram is published to the npm registry. **You do not need to download or compil
216
216
 
217
217
  As long as you have Node.js installed, your IDE will download and run the latest version of Engram automatically using `npx`.
218
218
 
219
- ### Option 1: The Magic Installer (Zero Config)
219
+ ### Option 1: The Magic Installer (Interactive)
220
220
 
221
- Run this single command in your terminal. It will automatically detect your IDEs (Cursor, VS Code, Visual Studio, Cline, Windsurf, Antigravity) and inject the correct configuration for you:
221
+ Run this single command in your terminal. It will automatically detect your IDE (Cursor, VS Code, Visual Studio, Cline, Windsurf, Antigravity) and ask for confirmation before injecting the configuration:
222
222
 
223
223
  ```bash
224
224
  npx -y engram-mcp-server --install
225
225
  ```
226
226
 
227
- *(You can also run `npx -y engram-mcp-server --list` to see what IDEs it detects before installing)*
227
+ If it cannot detect your IDE automatically, it will present a numbered menu where you can choose your IDE from a list, or even **provide a custom path** to any `mcp.json` file on your system.
228
+
229
+ *(You can also run `npx -y engram-mcp-server --list` to preview which IDEs are detected on your machine!)*
228
230
 
229
231
  Restart your IDE, and Engram is ready!
230
232
 
@@ -1,5 +1,5 @@
1
1
  export declare const SERVER_NAME = "engram-mcp-server";
2
- export declare const SERVER_VERSION = "1.2.2";
2
+ export declare const SERVER_VERSION = "1.2.4";
3
3
  export declare const TOOL_PREFIX = "engram";
4
4
  export declare const DB_DIR_NAME = ".engram";
5
5
  export declare const DB_FILE_NAME = "memory.db";
package/dist/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Engram MCP Server — Constants
3
3
  // ============================================================================
4
4
  export const SERVER_NAME = "engram-mcp-server";
5
- export const SERVER_VERSION = "1.2.2";
5
+ export const SERVER_VERSION = "1.2.4";
6
6
  export const TOOL_PREFIX = "engram";
7
7
  // Database
8
8
  export const DB_DIR_NAME = ".engram";
@@ -1,2 +1,2 @@
1
- export declare function runInstaller(args: string[]): void;
1
+ export declare function runInstaller(args: string[]): Promise<void>;
2
2
  //# sourceMappingURL=installer.d.ts.map
package/dist/installer.js CHANGED
@@ -1,55 +1,66 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import os from "os";
4
+ import readline from "readline";
4
5
  const HOME = os.homedir();
5
6
  const APPDATA = process.env.APPDATA || path.join(HOME, ".config");
6
- // ─── IDE Config Locations ────────────────────────────────────────────
7
7
  const IDE_CONFIGS = {
8
8
  antigravity: {
9
9
  name: "Antigravity IDE",
10
- paths: [
11
- path.join(HOME, ".gemini", "antigravity", "mcp_config.json"),
12
- ],
13
10
  format: "mcpServers",
11
+ scopes: {
12
+ global: [path.join(HOME, ".gemini", "antigravity", "mcp_config.json")],
13
+ },
14
14
  },
15
15
  cursor: {
16
16
  name: "Cursor",
17
- paths: [
18
- path.join(HOME, ".cursor", "mcp.json"),
19
- path.join(APPDATA, "Cursor", "mcp.json"),
20
- ],
21
17
  format: "mcpServers",
18
+ scopes: {
19
+ global: [
20
+ path.join(HOME, ".cursor", "mcp.json"),
21
+ path.join(APPDATA, "Cursor", "mcp.json"),
22
+ ],
23
+ localDirs: [".cursor"],
24
+ },
22
25
  },
23
26
  vscode: {
24
27
  name: "VS Code (Copilot)",
25
- paths: [
26
- path.join(APPDATA, "Code", "User", "mcp.json"),
27
- path.join(HOME, ".vscode", "mcp.json"),
28
- ],
29
28
  format: "servers",
29
+ scopes: {
30
+ global: [
31
+ path.join(APPDATA, "Code", "User", "mcp.json"),
32
+ path.join(HOME, ".vscode", "mcp.json"),
33
+ ],
34
+ localDirs: [".vscode"],
35
+ },
30
36
  },
31
37
  cline: {
32
38
  name: "Cline / Roo Code",
33
- paths: [
34
- path.join(APPDATA, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
35
- path.join(HOME, ".cline", "mcp_settings.json"),
36
- ],
37
39
  format: "mcpServers",
40
+ scopes: {
41
+ global: [
42
+ path.join(APPDATA, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
43
+ path.join(HOME, ".cline", "mcp_settings.json"),
44
+ ],
45
+ },
38
46
  },
39
47
  windsurf: {
40
48
  name: "Windsurf",
41
- paths: [
42
- path.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
43
- path.join(APPDATA, "Windsurf", "mcp.json"),
44
- ],
45
49
  format: "mcpServers",
50
+ scopes: {
51
+ global: [
52
+ path.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
53
+ path.join(APPDATA, "Windsurf", "mcp.json"),
54
+ ],
55
+ },
46
56
  },
47
57
  visualstudio: {
48
- name: "Visual Studio 2022",
49
- paths: [
50
- path.join(HOME, ".mcp.json"), // Global config for Visual Studio
51
- ],
58
+ name: "Visual Studio 2022/2026",
52
59
  format: "servers",
60
+ scopes: {
61
+ global: [path.join(HOME, ".mcp.json")],
62
+ localDirs: [".vs", ""], // "" means root of solution (e.g. SolutionDir/.mcp.json)
63
+ },
53
64
  },
54
65
  };
55
66
  // ─── Engram Entry ────────────────────────────────────────────────────
@@ -82,59 +93,226 @@ function addToConfig(configPath, format) {
82
93
  const key = format; // "mcpServers" or "servers"
83
94
  if (!config[key])
84
95
  config[key] = {};
96
+ const newEntry = makeEngramEntry(format);
85
97
  if (config[key].engram) {
86
- // Already exists — update to use npx
87
- config[key].engram = makeEngramEntry(format);
98
+ // Already exists — check if it's identical
99
+ if (JSON.stringify(config[key].engram) === JSON.stringify(newEntry)) {
100
+ return "exists";
101
+ }
102
+ // Exists but different (e.g. old local path) — update to use npx
103
+ config[key].engram = newEntry;
88
104
  writeJson(configPath, config);
89
105
  return "updated";
90
106
  }
91
- config[key].engram = makeEngramEntry(format);
107
+ config[key].engram = newEntry;
92
108
  writeJson(configPath, config);
93
109
  return "added";
94
110
  }
111
+ // ─── Environment Detection ──────────────────────────────────────────
112
+ function detectCurrentIde() {
113
+ const env = process.env;
114
+ // Explicit hints usually present in extension hosts or integrated terminals
115
+ if (env.ANTIGRAVITY_EDITOR_APP_ROOT)
116
+ return "antigravity";
117
+ if (env.WINDSURF_PROFILE)
118
+ return "windsurf";
119
+ // VS Code forks share TERM_PROGRAM="vscode", but we can distinguish them by checking VSCODE_CWD or path
120
+ if (env.TERM_PROGRAM === "vscode" || env.VSCODE_IPC_HOOK || env.VSCODE_CWD) {
121
+ const cwdLower = (env.VSCODE_CWD || "").toLowerCase();
122
+ if (cwdLower.includes("antigravity"))
123
+ return "antigravity";
124
+ if (cwdLower.includes("cursor"))
125
+ return "cursor";
126
+ if (cwdLower.includes("windsurf"))
127
+ return "windsurf";
128
+ // Final fallback: check PATH but ONLY for the specific IDE execution paths, not generically
129
+ const pathLower = (env.PATH || "").toLowerCase();
130
+ if (pathLower.includes("antigravity"))
131
+ return "antigravity";
132
+ if (pathLower.includes("cursor\\cli"))
133
+ return "cursor"; // more specific to avoid false positives
134
+ if (pathLower.includes("windsurf"))
135
+ return "windsurf";
136
+ return "vscode";
137
+ }
138
+ return null;
139
+ }
95
140
  // ─── Main ────────────────────────────────────────────────────────────
96
- export function runInstaller(args) {
141
+ async function askQuestion(query) {
142
+ const rl = readline.createInterface({
143
+ input: process.stdin,
144
+ output: process.stdout,
145
+ });
146
+ return new Promise(resolve => rl.question(query, ans => {
147
+ rl.close();
148
+ resolve(ans);
149
+ }));
150
+ }
151
+ export async function runInstaller(args) {
97
152
  if (args.includes("--list")) {
98
153
  console.log("\nEngram can be auto-installed into these IDEs:\n");
99
154
  for (const [id, ide] of Object.entries(IDE_CONFIGS)) {
100
- const found = ide.paths.find((p) => fs.existsSync(p) || fs.existsSync(path.dirname(p)));
101
- console.log(` ${id.padEnd(15)} ${ide.name} ${found ? "✅ detected" : "❌ not found"}`);
155
+ let found = false;
156
+ if (ide.scopes.global) {
157
+ found = !!ide.scopes.global.find(p => fs.existsSync(p) || fs.existsSync(path.dirname(p)));
158
+ }
159
+ console.log(` ${id.padEnd(15)} ${ide.name}${ide.scopes.localDirs ? " (Global / Local)" : " (Global)"} ${found ? "✅ global path detected" : "❌ global not found"}`);
102
160
  }
103
161
  process.exit(0);
104
162
  }
105
- // Specific IDE requested?
163
+ // Specific IDE requested via CLI flag?
106
164
  const ideFlagIdx = args.indexOf("--ide");
107
- const targetIde = ideFlagIdx >= 0 ? args[ideFlagIdx + 1] : null;
108
- const idesToProcess = targetIde
109
- ? (IDE_CONFIGS[targetIde] ? { [targetIde]: IDE_CONFIGS[targetIde] } : null)
110
- : IDE_CONFIGS;
111
- if (!idesToProcess) {
112
- console.error(`Unknown IDE: "${targetIde}". Options: ${Object.keys(IDE_CONFIGS).join(", ")}`);
113
- process.exit(1);
165
+ if (ideFlagIdx >= 0 && args[ideFlagIdx + 1]) {
166
+ const targetIde = args[ideFlagIdx + 1];
167
+ if (!IDE_CONFIGS[targetIde]) {
168
+ console.error(`Unknown IDE: "${targetIde}". Options: ${Object.keys(IDE_CONFIGS).join(", ")}`);
169
+ process.exit(1);
170
+ }
171
+ await performInstallationInteractive({ [targetIde]: IDE_CONFIGS[targetIde] });
172
+ return;
114
173
  }
115
174
  console.log("\n🧠 Engram MCP Installer\n");
116
- let installed = 0;
175
+ // Auto-detect environment if it's run without specific args
176
+ const currentIde = detectCurrentIde();
177
+ if (currentIde && IDE_CONFIGS[currentIde]) {
178
+ console.log(`🔍 Detected environment: ${IDE_CONFIGS[currentIde].name}`);
179
+ const ans = await askQuestion(" Install Engram for this IDE? [Y/n]: ");
180
+ if (ans.trim().toLowerCase() !== 'n') {
181
+ await performInstallationInteractive({ [currentIde]: IDE_CONFIGS[currentIde] });
182
+ return;
183
+ }
184
+ console.log(""); // Skip to menu
185
+ }
186
+ // Interactive Menu
187
+ console.log("Where would you like to configure the Engram MCP server?\n");
188
+ const ideKeys = Object.keys(IDE_CONFIGS);
189
+ ideKeys.forEach((key, index) => {
190
+ console.log(` ${index + 1}. ${IDE_CONFIGS[key].name}`);
191
+ });
192
+ const allOpt = ideKeys.length + 1;
193
+ const customOpt = ideKeys.length + 2;
194
+ console.log(` ${allOpt}. Install to ALL detected IDEs`);
195
+ console.log(` ${customOpt}. Custom IDE config path...`);
196
+ console.log(` 0. Cancel`);
197
+ const answer = await askQuestion(`\nSelect an option [0-${customOpt}]: `);
198
+ const choice = parseInt(answer.trim(), 10);
199
+ if (isNaN(choice) || choice === 0) {
200
+ console.log("Installation cancelled.");
201
+ process.exit(0);
202
+ }
203
+ let idesToProcess = {};
204
+ if (choice === allOpt) {
205
+ idesToProcess = IDE_CONFIGS; // All
206
+ }
207
+ else if (choice === customOpt) {
208
+ const customPath = await askQuestion("Enter the absolute path to your MCP config JSON file: ");
209
+ if (!customPath.trim()) {
210
+ console.log("No path provided. Exiting.");
211
+ process.exit(1);
212
+ }
213
+ idesToProcess = {
214
+ custom: {
215
+ name: "Custom Path",
216
+ paths: [customPath.trim()],
217
+ format: "mcpServers" // Safe default for unknown IDEs
218
+ }
219
+ };
220
+ }
221
+ else if (choice >= 1 && choice <= ideKeys.length) {
222
+ const selectedKey = ideKeys[choice - 1];
223
+ idesToProcess = { [selectedKey]: IDE_CONFIGS[selectedKey] };
224
+ }
225
+ else {
226
+ console.log("\nInvalid selection. Exiting.");
227
+ process.exit(1);
228
+ }
229
+ await performInstallationInteractive(idesToProcess);
230
+ }
231
+ // Interactive wizard to resolve specific config paths for the selected IDEs
232
+ async function performInstallationInteractive(idesToProcess) {
233
+ let resolvedConfigs = [];
117
234
  for (const [id, ide] of Object.entries(idesToProcess)) {
118
- const configPath = ide.paths.find((p) => fs.existsSync(p)) || ide.paths[0];
235
+ if (id === "custom") {
236
+ resolvedConfigs.push({ name: ide.name, path: ide.paths[0], format: ide.format });
237
+ continue;
238
+ }
239
+ const supportsLocal = ide.scopes?.localDirs && ide.scopes.localDirs.length > 0;
240
+ const supportsGlobal = ide.scopes?.global && ide.scopes.global.length > 0;
241
+ let targetScope = "global";
242
+ if (supportsLocal && supportsGlobal) {
243
+ console.log(`\n${ide.name} supports multiple installation scopes.`);
244
+ console.log(` 1. Global (Applies to all projects)`);
245
+ console.log(` 2. Local (Applies to a specific Solution/Workspace)`);
246
+ const scopeAns = await askQuestion("Select scope [1-2] (default 1): ");
247
+ if (scopeAns.trim() === "2") {
248
+ targetScope = "local";
249
+ }
250
+ }
251
+ else if (supportsLocal && !supportsGlobal) {
252
+ targetScope = "local";
253
+ }
254
+ if (targetScope === "global") {
255
+ const configPath = ide.scopes.global.find((p) => fs.existsSync(p)) || ide.scopes.global[0];
256
+ resolvedConfigs.push({ name: `${ide.name} (Global)`, path: configPath, format: ide.format });
257
+ }
258
+ else if (targetScope === "local") {
259
+ const solutionDir = await askQuestion(`Enter the absolute path to your ${ide.name} Solution/Workspace directory:\n> `);
260
+ if (!solutionDir.trim()) {
261
+ console.log(`Skipping ${ide.name} local installation (no path provided).`);
262
+ continue;
263
+ }
264
+ // For Visual studio, it's .vs/mcp.json or .mcp.json at root. We'll use the first defined localDir.
265
+ const localDirPrefix = ide.scopes.localDirs[0];
266
+ let configFileName = "mcp.json";
267
+ // Visual Studio special case: if localDir is "" (root), the file is usually .mcp.json
268
+ if (id === "visualstudio" && localDirPrefix === "") {
269
+ configFileName = ".mcp.json";
270
+ }
271
+ else if (id === "visualstudio" && localDirPrefix === ".vs") {
272
+ configFileName = "mcp.json";
273
+ }
274
+ // If they use `.mcp.json` vs `mcp.json` varies slightly, but standard is mcp.json inside `.vscode` or `.vs`
275
+ const configPath = path.join(solutionDir.trim(), localDirPrefix, configFileName);
276
+ resolvedConfigs.push({ name: `${ide.name} (Local: ${path.basename(solutionDir)})`, path: configPath, format: ide.format });
277
+ }
278
+ }
279
+ await performInstallation(resolvedConfigs);
280
+ }
281
+ async function performInstallation(configs) {
282
+ let installed = 0;
283
+ for (const config of configs) {
119
284
  try {
120
- const result = addToConfig(configPath, ide.format);
121
- console.log(`\n ✅ ${ide.name}`);
122
- console.log(` Config: ${configPath}`);
123
- console.log(` Status: ${result === "added" ? "Engram added" : "Engram config updated to use npx"}`);
124
- installed++;
285
+ const result = addToConfig(config.path, config.format);
286
+ console.log(`\n ✅ ${config.name}`);
287
+ console.log(` Config: ${config.path}`);
288
+ let statusText = "";
289
+ if (result === "added")
290
+ statusText = "Engram added";
291
+ else if (result === "updated")
292
+ statusText = "Engram config updated to use npx";
293
+ else if (result === "exists")
294
+ statusText = "Engram is already installed and up to date";
295
+ console.log(` Status: ${statusText}`);
296
+ if (result !== "exists") {
297
+ installed++;
298
+ }
125
299
  }
126
300
  catch (e) {
127
- console.log(`\n ⚠️ ${ide.name}`);
128
- console.log(` Could not write to: ${configPath}`);
301
+ console.log(`\n ⚠️ ${config.name}`);
302
+ console.log(` Could not write to: ${config.path}`);
129
303
  console.log(` Reason: ${e.message}`);
304
+ console.log(`\n For manual instructions, visit: https://github.com/keggan-std/Engram`);
130
305
  }
131
306
  }
132
- if (installed === 0) {
133
- console.log("\n No supported IDEs were found on this machine.");
134
- console.log(" Run 'npx -y engram-mcp-server --list' to see what was detected.\n");
307
+ if (configs.length === 0) {
308
+ console.log("\n No target configurations resolved.");
309
+ }
310
+ else if (installed === 0) {
311
+ // Technically they could all just be "already exists"
312
+ console.log("\n✅ Done! No new changes were needed.");
135
313
  }
136
314
  else {
137
- console.log(`\n✅ Done! Engram configured in ${installed} IDE(s).`);
315
+ console.log(`\n✅ Done! Engram configured in ${installed} IDE scope(s).`);
138
316
  console.log(" Restart your IDE(s) to load Engram.\n");
139
317
  }
140
318
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engram-mcp-server",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Engram — Persistent Memory Cortex for AI coding agents. Gives agents session continuity, change tracking, decision logging, and project intelligence across sessions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/constants.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // ============================================================================
4
4
 
5
5
  export const SERVER_NAME = "engram-mcp-server";
6
- export const SERVER_VERSION = "1.2.2";
6
+ export const SERVER_VERSION = "1.2.4";
7
7
  export const TOOL_PREFIX = "engram";
8
8
 
9
9
  // Database
package/src/installer.ts CHANGED
@@ -1,58 +1,77 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import os from "os";
4
+ import readline from "readline";
4
5
 
5
6
  const HOME = os.homedir();
6
7
  const APPDATA = process.env.APPDATA || path.join(HOME, ".config");
7
8
 
8
- // ─── IDE Config Locations ────────────────────────────────────────────
9
+ interface IdeDefinition {
10
+ name: string;
11
+ format: "mcpServers" | "servers";
12
+ scopes: {
13
+ global?: string[];
14
+ localDirs?: string[]; // e.g. [".vs", ".vscode"] - appended to a user-provided solution path
15
+ };
16
+ }
9
17
 
10
- const IDE_CONFIGS: Record<string, any> = {
18
+ const IDE_CONFIGS: Record<string, IdeDefinition> = {
11
19
  antigravity: {
12
20
  name: "Antigravity IDE",
13
- paths: [
14
- path.join(HOME, ".gemini", "antigravity", "mcp_config.json"),
15
- ],
16
21
  format: "mcpServers",
22
+ scopes: {
23
+ global: [path.join(HOME, ".gemini", "antigravity", "mcp_config.json")],
24
+ },
17
25
  },
18
26
  cursor: {
19
27
  name: "Cursor",
20
- paths: [
21
- path.join(HOME, ".cursor", "mcp.json"),
22
- path.join(APPDATA, "Cursor", "mcp.json"),
23
- ],
24
28
  format: "mcpServers",
29
+ scopes: {
30
+ global: [
31
+ path.join(HOME, ".cursor", "mcp.json"),
32
+ path.join(APPDATA, "Cursor", "mcp.json"),
33
+ ],
34
+ localDirs: [".cursor"],
35
+ },
25
36
  },
26
37
  vscode: {
27
38
  name: "VS Code (Copilot)",
28
- paths: [
29
- path.join(APPDATA, "Code", "User", "mcp.json"),
30
- path.join(HOME, ".vscode", "mcp.json"),
31
- ],
32
39
  format: "servers",
40
+ scopes: {
41
+ global: [
42
+ path.join(APPDATA, "Code", "User", "mcp.json"),
43
+ path.join(HOME, ".vscode", "mcp.json"),
44
+ ],
45
+ localDirs: [".vscode"],
46
+ },
33
47
  },
34
48
  cline: {
35
49
  name: "Cline / Roo Code",
36
- paths: [
37
- path.join(APPDATA, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
38
- path.join(HOME, ".cline", "mcp_settings.json"),
39
- ],
40
50
  format: "mcpServers",
51
+ scopes: {
52
+ global: [
53
+ path.join(APPDATA, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
54
+ path.join(HOME, ".cline", "mcp_settings.json"),
55
+ ],
56
+ },
41
57
  },
42
58
  windsurf: {
43
59
  name: "Windsurf",
44
- paths: [
45
- path.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
46
- path.join(APPDATA, "Windsurf", "mcp.json"),
47
- ],
48
60
  format: "mcpServers",
61
+ scopes: {
62
+ global: [
63
+ path.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
64
+ path.join(APPDATA, "Windsurf", "mcp.json"),
65
+ ],
66
+ },
49
67
  },
50
68
  visualstudio: {
51
- name: "Visual Studio 2022",
52
- paths: [
53
- path.join(HOME, ".mcp.json"), // Global config for Visual Studio
54
- ],
69
+ name: "Visual Studio 2022/2026",
55
70
  format: "servers",
71
+ scopes: {
72
+ global: [path.join(HOME, ".mcp.json")],
73
+ localDirs: [".vs", ""], // "" means root of solution (e.g. SolutionDir/.mcp.json)
74
+ },
56
75
  },
57
76
  };
58
77
 
@@ -88,73 +107,255 @@ function writeJson(filePath: string, data: any) {
88
107
  }
89
108
 
90
109
  function addToConfig(configPath: string, format: string) {
91
- let config = readJson(configPath) || {};
110
+ let config: Record<string, any> = readJson(configPath) || {};
92
111
  const key = format; // "mcpServers" or "servers"
93
112
 
94
113
  if (!config[key]) config[key] = {};
95
114
 
115
+ const newEntry = makeEngramEntry(format);
116
+
96
117
  if (config[key].engram) {
97
- // Already exists — update to use npx
98
- config[key].engram = makeEngramEntry(format);
118
+ // Already exists — check if it's identical
119
+ if (JSON.stringify(config[key].engram) === JSON.stringify(newEntry)) {
120
+ return "exists";
121
+ }
122
+
123
+ // Exists but different (e.g. old local path) — update to use npx
124
+ config[key].engram = newEntry;
99
125
  writeJson(configPath, config);
100
126
  return "updated";
101
127
  }
102
128
 
103
- config[key].engram = makeEngramEntry(format);
129
+ config[key].engram = newEntry;
104
130
  writeJson(configPath, config);
105
131
  return "added";
106
132
  }
107
133
 
134
+ // ─── Environment Detection ──────────────────────────────────────────
135
+
136
+ function detectCurrentIde(): string | null {
137
+ const env = process.env;
138
+
139
+ // Explicit hints usually present in extension hosts or integrated terminals
140
+ if (env.ANTIGRAVITY_EDITOR_APP_ROOT) return "antigravity";
141
+ if (env.WINDSURF_PROFILE) return "windsurf";
142
+
143
+ // VS Code forks share TERM_PROGRAM="vscode", but we can distinguish them by checking VSCODE_CWD or path
144
+ if (env.TERM_PROGRAM === "vscode" || env.VSCODE_IPC_HOOK || env.VSCODE_CWD) {
145
+ const cwdLower = (env.VSCODE_CWD || "").toLowerCase();
146
+ if (cwdLower.includes("antigravity")) return "antigravity";
147
+ if (cwdLower.includes("cursor")) return "cursor";
148
+ if (cwdLower.includes("windsurf")) return "windsurf";
149
+
150
+ // Final fallback: check PATH but ONLY for the specific IDE execution paths, not generically
151
+ const pathLower = (env.PATH || "").toLowerCase();
152
+ if (pathLower.includes("antigravity")) return "antigravity";
153
+ if (pathLower.includes("cursor\\cli")) return "cursor"; // more specific to avoid false positives
154
+ if (pathLower.includes("windsurf")) return "windsurf";
155
+
156
+ return "vscode";
157
+ }
158
+
159
+ return null;
160
+ }
161
+
108
162
  // ─── Main ────────────────────────────────────────────────────────────
109
163
 
110
- export function runInstaller(args: string[]) {
164
+ async function askQuestion(query: string): Promise<string> {
165
+ const rl = readline.createInterface({
166
+ input: process.stdin,
167
+ output: process.stdout,
168
+ });
169
+
170
+ return new Promise(resolve => rl.question(query, ans => {
171
+ rl.close();
172
+ resolve(ans);
173
+ }));
174
+ }
175
+
176
+ export async function runInstaller(args: string[]) {
111
177
  if (args.includes("--list")) {
112
178
  console.log("\nEngram can be auto-installed into these IDEs:\n");
113
179
  for (const [id, ide] of Object.entries(IDE_CONFIGS)) {
114
- const found = ide.paths.find((p: string) => fs.existsSync(p) || fs.existsSync(path.dirname(p)));
115
- console.log(` ${id.padEnd(15)} ${ide.name} ${found ? "✅ detected" : "❌ not found"}`);
180
+ let found = false;
181
+ if (ide.scopes.global) {
182
+ found = !!ide.scopes.global.find(p => fs.existsSync(p) || fs.existsSync(path.dirname(p)));
183
+ }
184
+ console.log(` ${id.padEnd(15)} ${ide.name}${ide.scopes.localDirs ? " (Global / Local)" : " (Global)"} ${found ? "✅ global path detected" : "❌ global not found"}`);
116
185
  }
117
186
  process.exit(0);
118
187
  }
119
188
 
120
- // Specific IDE requested?
189
+ // Specific IDE requested via CLI flag?
121
190
  const ideFlagIdx = args.indexOf("--ide");
122
- const targetIde = ideFlagIdx >= 0 ? args[ideFlagIdx + 1] : null;
191
+ if (ideFlagIdx >= 0 && args[ideFlagIdx + 1]) {
192
+ const targetIde = args[ideFlagIdx + 1];
193
+ if (!IDE_CONFIGS[targetIde]) {
194
+ console.error(`Unknown IDE: "${targetIde}". Options: ${Object.keys(IDE_CONFIGS).join(", ")}`);
195
+ process.exit(1);
196
+ }
197
+ await performInstallationInteractive({ [targetIde]: IDE_CONFIGS[targetIde] });
198
+ return;
199
+ }
200
+
201
+ console.log("\n🧠 Engram MCP Installer\n");
202
+
203
+ // Auto-detect environment if it's run without specific args
204
+ const currentIde = detectCurrentIde();
205
+
206
+ if (currentIde && IDE_CONFIGS[currentIde]) {
207
+ console.log(`🔍 Detected environment: ${IDE_CONFIGS[currentIde].name}`);
208
+ const ans = await askQuestion(" Install Engram for this IDE? [Y/n]: ");
209
+
210
+ if (ans.trim().toLowerCase() !== 'n') {
211
+ await performInstallationInteractive({ [currentIde]: IDE_CONFIGS[currentIde] });
212
+ return;
213
+ }
214
+ console.log(""); // Skip to menu
215
+ }
216
+
217
+ // Interactive Menu
218
+ console.log("Where would you like to configure the Engram MCP server?\n");
219
+
220
+ const ideKeys = Object.keys(IDE_CONFIGS);
221
+ ideKeys.forEach((key, index) => {
222
+ console.log(` ${index + 1}. ${IDE_CONFIGS[key].name}`);
223
+ });
224
+
225
+ const allOpt = ideKeys.length + 1;
226
+ const customOpt = ideKeys.length + 2;
123
227
 
124
- const idesToProcess = targetIde
125
- ? (IDE_CONFIGS[targetIde] ? { [targetIde]: IDE_CONFIGS[targetIde] } : null)
126
- : IDE_CONFIGS;
228
+ console.log(` ${allOpt}. Install to ALL detected IDEs`);
229
+ console.log(` ${customOpt}. Custom IDE config path...`);
230
+ console.log(` 0. Cancel`);
127
231
 
128
- if (!idesToProcess) {
129
- console.error(`Unknown IDE: "${targetIde}". Options: ${Object.keys(IDE_CONFIGS).join(", ")}`);
232
+ const answer = await askQuestion(`\nSelect an option [0-${customOpt}]: `);
233
+ const choice = parseInt(answer.trim(), 10);
234
+
235
+ if (isNaN(choice) || choice === 0) {
236
+ console.log("Installation cancelled.");
237
+ process.exit(0);
238
+ }
239
+
240
+ let idesToProcess: Record<string, any> = {};
241
+
242
+ if (choice === allOpt) {
243
+ idesToProcess = IDE_CONFIGS; // All
244
+ } else if (choice === customOpt) {
245
+ const customPath = await askQuestion("Enter the absolute path to your MCP config JSON file: ");
246
+ if (!customPath.trim()) {
247
+ console.log("No path provided. Exiting.");
248
+ process.exit(1);
249
+ }
250
+ idesToProcess = {
251
+ custom: {
252
+ name: "Custom Path",
253
+ paths: [customPath.trim()],
254
+ format: "mcpServers" // Safe default for unknown IDEs
255
+ }
256
+ };
257
+ } else if (choice >= 1 && choice <= ideKeys.length) {
258
+ const selectedKey = ideKeys[choice - 1];
259
+ idesToProcess = { [selectedKey]: IDE_CONFIGS[selectedKey] };
260
+ } else {
261
+ console.log("\nInvalid selection. Exiting.");
130
262
  process.exit(1);
131
263
  }
132
264
 
133
- console.log("\n🧠 Engram MCP Installer\n");
265
+ await performInstallationInteractive(idesToProcess);
266
+ }
134
267
 
135
- let installed = 0;
268
+ // Interactive wizard to resolve specific config paths for the selected IDEs
269
+ async function performInstallationInteractive(idesToProcess: Record<string, IdeDefinition | any>) {
270
+ let resolvedConfigs: { name: string; path: string; format: string }[] = [];
136
271
 
137
272
  for (const [id, ide] of Object.entries(idesToProcess)) {
138
- const configPath = ide.paths.find((p: string) => fs.existsSync(p)) || ide.paths[0];
273
+ if (id === "custom") {
274
+ resolvedConfigs.push({ name: ide.name, path: ide.paths[0], format: ide.format });
275
+ continue;
276
+ }
277
+
278
+ const supportsLocal = ide.scopes?.localDirs && ide.scopes.localDirs.length > 0;
279
+ const supportsGlobal = ide.scopes?.global && ide.scopes.global.length > 0;
280
+
281
+ let targetScope = "global";
282
+
283
+ if (supportsLocal && supportsGlobal) {
284
+ console.log(`\n${ide.name} supports multiple installation scopes.`);
285
+ console.log(` 1. Global (Applies to all projects)`);
286
+ console.log(` 2. Local (Applies to a specific Solution/Workspace)`);
287
+ const scopeAns = await askQuestion("Select scope [1-2] (default 1): ");
288
+ if (scopeAns.trim() === "2") {
289
+ targetScope = "local";
290
+ }
291
+ } else if (supportsLocal && !supportsGlobal) {
292
+ targetScope = "local";
293
+ }
294
+
295
+ if (targetScope === "global") {
296
+ const configPath = ide.scopes.global!.find((p: string) => fs.existsSync(p)) || ide.scopes.global![0];
297
+ resolvedConfigs.push({ name: `${ide.name} (Global)`, path: configPath, format: ide.format });
298
+ } else if (targetScope === "local") {
299
+ const solutionDir = await askQuestion(`Enter the absolute path to your ${ide.name} Solution/Workspace directory:\n> `);
300
+ if (!solutionDir.trim()) {
301
+ console.log(`Skipping ${ide.name} local installation (no path provided).`);
302
+ continue;
303
+ }
304
+
305
+ // For Visual studio, it's .vs/mcp.json or .mcp.json at root. We'll use the first defined localDir.
306
+ const localDirPrefix = ide.scopes.localDirs![0];
307
+
308
+ let configFileName = "mcp.json";
309
+ // Visual Studio special case: if localDir is "" (root), the file is usually .mcp.json
310
+ if (id === "visualstudio" && localDirPrefix === "") {
311
+ configFileName = ".mcp.json";
312
+ } else if (id === "visualstudio" && localDirPrefix === ".vs") {
313
+ configFileName = "mcp.json";
314
+ }
315
+ // If they use `.mcp.json` vs `mcp.json` varies slightly, but standard is mcp.json inside `.vscode` or `.vs`
316
+
317
+ const configPath = path.join(solutionDir.trim(), localDirPrefix, configFileName);
318
+ resolvedConfigs.push({ name: `${ide.name} (Local: ${path.basename(solutionDir)})`, path: configPath, format: ide.format });
319
+ }
320
+ }
139
321
 
322
+ await performInstallation(resolvedConfigs);
323
+ }
324
+
325
+ async function performInstallation(configs: { name: string; path: string; format: string }[]) {
326
+ let installed = 0;
327
+
328
+ for (const config of configs) {
140
329
  try {
141
- const result = addToConfig(configPath, ide.format);
142
- console.log(`\n ✅ ${ide.name}`);
143
- console.log(` Config: ${configPath}`);
144
- console.log(` Status: ${result === "added" ? "Engram added" : "Engram config updated to use npx"}`);
145
- installed++;
330
+ const result = addToConfig(config.path, config.format);
331
+ console.log(`\n ✅ ${config.name}`);
332
+ console.log(` Config: ${config.path}`);
333
+
334
+ let statusText = "";
335
+ if (result === "added") statusText = "Engram added";
336
+ else if (result === "updated") statusText = "Engram config updated to use npx";
337
+ else if (result === "exists") statusText = "Engram is already installed and up to date";
338
+
339
+ console.log(` Status: ${statusText}`);
340
+
341
+ if (result !== "exists") {
342
+ installed++;
343
+ }
146
344
  } catch (e: any) {
147
- console.log(`\n ⚠️ ${ide.name}`);
148
- console.log(` Could not write to: ${configPath}`);
345
+ console.log(`\n ⚠️ ${config.name}`);
346
+ console.log(` Could not write to: ${config.path}`);
149
347
  console.log(` Reason: ${e.message}`);
348
+ console.log(`\n For manual instructions, visit: https://github.com/keggan-std/Engram`);
150
349
  }
151
350
  }
152
351
 
153
- if (installed === 0) {
154
- console.log("\n No supported IDEs were found on this machine.");
155
- console.log(" Run 'npx -y engram-mcp-server --list' to see what was detected.\n");
352
+ if (configs.length === 0) {
353
+ console.log("\n No target configurations resolved.");
354
+ } else if (installed === 0) {
355
+ // Technically they could all just be "already exists"
356
+ console.log("\n✅ Done! No new changes were needed.");
156
357
  } else {
157
- console.log(`\n✅ Done! Engram configured in ${installed} IDE(s).`);
358
+ console.log(`\n✅ Done! Engram configured in ${installed} IDE scope(s).`);
158
359
  console.log(" Restart your IDE(s) to load Engram.\n");
159
360
  }
160
361
  }