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 +5 -3
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/installer.d.ts +1 -1
- package/dist/installer.js +229 -51
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/installer.ts +254 -53
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 (
|
|
219
|
+
### Option 1: The Magic Installer (Interactive)
|
|
220
220
|
|
|
221
|
-
Run this single command in your terminal. It will automatically detect your
|
|
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
|
-
|
|
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
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const SERVER_NAME = "engram-mcp-server";
|
|
2
|
-
export declare const SERVER_VERSION = "1.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.
|
|
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";
|
package/dist/installer.d.ts
CHANGED
|
@@ -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 —
|
|
87
|
-
config[key].engram
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
121
|
-
console.log(`\n ✅ ${
|
|
122
|
-
console.log(` Config: ${
|
|
123
|
-
|
|
124
|
-
|
|
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 ⚠️ ${
|
|
128
|
-
console.log(` Could not write to: ${
|
|
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 (
|
|
133
|
-
console.log("\n No
|
|
134
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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 —
|
|
98
|
-
config[key].engram
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
265
|
+
await performInstallationInteractive(idesToProcess);
|
|
266
|
+
}
|
|
134
267
|
|
|
135
|
-
|
|
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
|
-
|
|
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(
|
|
142
|
-
console.log(`\n ✅ ${
|
|
143
|
-
console.log(` Config: ${
|
|
144
|
-
|
|
145
|
-
|
|
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 ⚠️ ${
|
|
148
|
-
console.log(` Could not write to: ${
|
|
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 (
|
|
154
|
-
console.log("\n No
|
|
155
|
-
|
|
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
|
}
|