Skip to content

Commit 007347f

Browse files
Merge pull request #3 from gitmem-dev/feature/multi-client-distribution
feat: multi-client support + server-side enforcement
2 parents 81a42ee + 328ac88 commit 007347f

9 files changed

Lines changed: 741 additions & 40 deletions

File tree

README.md

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,27 @@
2121

2222
GitMem is an [MCP server](https://modelcontextprotocol.io/) that gives your AI coding agent **persistent memory across sessions**. It remembers mistakes (scars), successes (wins), and decisions — so your agent learns from experience instead of starting from scratch every time.
2323

24-
Works with **Claude Code**, **Claude Desktop**, **Cursor**, and any MCP-compatible client.
24+
Works with **Claude Code**, **Cursor**, **VS Code (Copilot)**, **Windsurf**, and any MCP-compatible client.
2525

2626
## Quick Start
2727

2828
```bash
2929
npx gitmem-mcp init
3030
```
3131

32-
One command. The wizard sets up everything:
33-
- `.gitmem/` directory with 3 starter scars
34-
- `.mcp.json` with gitmem server entry
35-
- `CLAUDE.md` with memory protocol instructions
36-
- `.claude/settings.json` with tool permissions
37-
- Lifecycle hooks for automatic session management
32+
One command. The wizard auto-detects your IDE and sets up everything:
33+
- `.gitmem/` directory with starter scars
34+
- MCP server config (`.mcp.json`, `.vscode/mcp.json`, `.cursor/mcp.json`, etc.)
35+
- Instructions file (`CLAUDE.md`, `.cursorrules`, `.windsurfrules`, `.github/copilot-instructions.md`)
36+
- Lifecycle hooks (where supported)
3837
- `.gitignore` updated
3938

4039
Already have existing config? The wizard merges without destroying anything. Re-running is safe.
4140

4241
```bash
43-
npx gitmem-mcp init --yes # Non-interactive
44-
npx gitmem-mcp init --dry-run # Preview changes
42+
npx gitmem-mcp init --yes # Non-interactive
43+
npx gitmem-mcp init --dry-run # Preview changes
44+
npx gitmem-mcp init --client vscode # Force specific client
4545
```
4646

4747
## How It Works
@@ -78,16 +78,22 @@ Every scar includes **counter-arguments** — reasons why someone might reasonab
7878

7979
## Supported Clients
8080

81-
| Client | Setup |
82-
|--------|-------|
83-
| **Claude Code** | `npx gitmem-mcp init` (auto-detected) |
84-
| **Claude Desktop** | `npx gitmem-mcp init` or add to `claude_desktop_config.json` |
85-
| **Cursor** | `npx gitmem-mcp init` or add to `.cursor/mcp.json` |
86-
| **Any MCP client** | Add `npx -y gitmem-mcp` as an MCP server |
81+
| Client | Setup | Hooks |
82+
|--------|-------|-------|
83+
| **Claude Code** | `npx gitmem-mcp init` | Full (session, recall, credential guard) |
84+
| **Cursor** | `npx gitmem-mcp init --client cursor` | Partial (session, recall) |
85+
| **VS Code (Copilot)** | `npx gitmem-mcp init --client vscode` | Instructions-based |
86+
| **Windsurf** | `npx gitmem-mcp init --client windsurf` | Instructions-based |
87+
| **Claude Desktop** | Add to `claude_desktop_config.json` | Manual |
88+
| **Any MCP client** | `npx gitmem-mcp init --client generic` | Instructions-based |
89+
90+
The wizard auto-detects your IDE. Use `--client` to override.
8791

8892
<details>
8993
<summary><strong>Manual MCP configuration</strong></summary>
9094

95+
Add this to your MCP client's config file:
96+
9197
```json
9298
{
9399
"mcpServers": {
@@ -99,13 +105,21 @@ Every scar includes **counter-arguments** — reasons why someone might reasonab
99105
}
100106
```
101107

108+
| Client | Config file |
109+
|--------|-------------|
110+
| Claude Code | `.mcp.json` |
111+
| Cursor | `.cursor/mcp.json` |
112+
| VS Code | `.vscode/mcp.json` |
113+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
114+
102115
</details>
103116

104117
## CLI Commands
105118

106119
| Command | Description |
107120
|---------|-------------|
108-
| `npx gitmem-mcp init` | Interactive setup wizard |
121+
| `npx gitmem-mcp init` | Interactive setup wizard (auto-detects IDE) |
122+
| `npx gitmem-mcp init --client <name>` | Setup for specific client (`claude`, `cursor`, `vscode`, `windsurf`, `generic`) |
109123
| `npx gitmem-mcp init --yes` | Non-interactive setup |
110124
| `npx gitmem-mcp init --dry-run` | Preview changes |
111125
| `npx gitmem-mcp uninstall` | Clean removal (preserves `.gitmem/` data) |

bin/init-wizard.js

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Interactive setup that detects existing config, prompts, and merges.
77
* Supports Claude Code and Cursor IDE.
88
*
9-
* Usage: npx gitmem-mcp init [--yes] [--dry-run] [--project <name>] [--client <claude|cursor>]
9+
* Usage: npx gitmem-mcp init [--yes] [--dry-run] [--project <name>] [--client <claude|cursor|vscode|windsurf|generic>]
1010
*/
1111

1212
import {
@@ -35,11 +35,15 @@ const clientFlag = clientIdx !== -1 ? args[clientIdx + 1]?.toLowerCase() : null;
3535

3636
// ── Client Configuration ──
3737

38+
// Resolve user home directory for clients that use user-level config
39+
const homeDir = process.env.HOME || process.env.USERPROFILE || "~";
40+
3841
const CLIENT_CONFIGS = {
3942
claude: {
4043
name: "Claude Code",
4144
mcpConfigPath: join(cwd, ".mcp.json"),
4245
mcpConfigName: ".mcp.json",
46+
mcpConfigScope: "project",
4347
instructionsFile: join(cwd, "CLAUDE.md"),
4448
instructionsName: "CLAUDE.md",
4549
templateFile: join(__dirname, "..", "CLAUDE.md.template"),
@@ -50,12 +54,14 @@ const CLIENT_CONFIGS = {
5054
settingsLocalFile: join(cwd, ".claude", "settings.local.json"),
5155
hasPermissions: true,
5256
hooksInSettings: true,
57+
hasHooks: true,
5358
completionMsg: "Setup complete! Start Claude Code \u2014 memory is active.",
5459
},
5560
cursor: {
5661
name: "Cursor",
5762
mcpConfigPath: join(cwd, ".cursor", "mcp.json"),
5863
mcpConfigName: ".cursor/mcp.json",
64+
mcpConfigScope: "project",
5965
instructionsFile: join(cwd, ".cursorrules"),
6066
instructionsName: ".cursorrules",
6167
templateFile: join(__dirname, "..", "cursorrules.template"),
@@ -66,10 +72,66 @@ const CLIENT_CONFIGS = {
6672
settingsLocalFile: null,
6773
hasPermissions: false,
6874
hooksInSettings: false,
75+
hasHooks: true,
6976
hooksFile: join(cwd, ".cursor", "hooks.json"),
7077
hooksFileName: ".cursor/hooks.json",
7178
completionMsg: "Setup complete! Open Cursor (Agent mode) \u2014 memory is active.",
7279
},
80+
vscode: {
81+
name: "VS Code (Copilot)",
82+
mcpConfigPath: join(cwd, ".vscode", "mcp.json"),
83+
mcpConfigName: ".vscode/mcp.json",
84+
mcpConfigScope: "project",
85+
instructionsFile: join(cwd, ".github", "copilot-instructions.md"),
86+
instructionsName: ".github/copilot-instructions.md",
87+
templateFile: join(__dirname, "..", "copilot-instructions.template"),
88+
startMarker: "<!-- gitmem:start -->",
89+
endMarker: "<!-- gitmem:end -->",
90+
configDir: join(cwd, ".vscode"),
91+
settingsFile: null,
92+
settingsLocalFile: null,
93+
hasPermissions: false,
94+
hooksInSettings: false,
95+
hasHooks: false,
96+
completionMsg: "Setup complete! Open VS Code \u2014 memory is active via Copilot.",
97+
},
98+
windsurf: {
99+
name: "Windsurf",
100+
mcpConfigPath: join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
101+
mcpConfigName: "~/.codeium/windsurf/mcp_config.json",
102+
mcpConfigScope: "user",
103+
instructionsFile: join(cwd, ".windsurfrules"),
104+
instructionsName: ".windsurfrules",
105+
templateFile: join(__dirname, "..", "windsurfrules.template"),
106+
startMarker: "# --- gitmem:start ---",
107+
endMarker: "# --- gitmem:end ---",
108+
configDir: null,
109+
settingsFile: null,
110+
settingsLocalFile: null,
111+
hasPermissions: false,
112+
hooksInSettings: false,
113+
hasHooks: false,
114+
completionMsg: "Setup complete! Open Windsurf \u2014 memory is active.",
115+
},
116+
generic: {
117+
name: "Generic MCP Client",
118+
mcpConfigPath: join(cwd, ".mcp.json"),
119+
mcpConfigName: ".mcp.json",
120+
mcpConfigScope: "project",
121+
instructionsFile: join(cwd, "CLAUDE.md"),
122+
instructionsName: "CLAUDE.md",
123+
templateFile: join(__dirname, "..", "CLAUDE.md.template"),
124+
startMarker: "<!-- gitmem:start -->",
125+
endMarker: "<!-- gitmem:end -->",
126+
configDir: null,
127+
settingsFile: null,
128+
settingsLocalFile: null,
129+
hasPermissions: false,
130+
hooksInSettings: false,
131+
hasHooks: false,
132+
completionMsg:
133+
"Setup complete! Configure your MCP client to use the gitmem server from .mcp.json.",
134+
},
73135
};
74136

75137
// Shared paths (client-agnostic)
@@ -84,33 +146,49 @@ let cc; // shorthand for CLIENT_CONFIGS[client]
84146

85147
// ── Client Detection ──
86148

149+
const VALID_CLIENTS = Object.keys(CLIENT_CONFIGS);
150+
87151
function detectClient() {
88152
// Explicit flag takes priority
89153
if (clientFlag) {
90-
if (clientFlag !== "claude" && clientFlag !== "cursor") {
91-
console.error(` Error: Unknown client "${clientFlag}". Use --client claude or --client cursor.`);
154+
if (!VALID_CLIENTS.includes(clientFlag)) {
155+
console.error(` Error: Unknown client "${clientFlag}". Use --client ${VALID_CLIENTS.join("|")}.`);
92156
process.exit(1);
93157
}
94158
return clientFlag;
95159
}
96160

97-
// Auto-detect based on directory presence
161+
// Auto-detect based on directory/file presence
98162
const hasCursorDir = existsSync(join(cwd, ".cursor"));
99163
const hasClaudeDir = existsSync(join(cwd, ".claude"));
100164
const hasMcpJson = existsSync(join(cwd, ".mcp.json"));
101165
const hasClaudeMd = existsSync(join(cwd, "CLAUDE.md"));
102166
const hasCursorRules = existsSync(join(cwd, ".cursorrules"));
103167
const hasCursorMcp = existsSync(join(cwd, ".cursor", "mcp.json"));
168+
const hasVscodeDir = existsSync(join(cwd, ".vscode"));
169+
const hasVscodeMcp = existsSync(join(cwd, ".vscode", "mcp.json"));
170+
const hasCopilotInstructions = existsSync(join(cwd, ".github", "copilot-instructions.md"));
171+
const hasWindsurfRules = existsSync(join(cwd, ".windsurfrules"));
172+
const hasWindsurfMcp = existsSync(
173+
join(homeDir, ".codeium", "windsurf", "mcp_config.json")
174+
);
104175

105176
// Strong Cursor signals
106177
if (hasCursorDir && !hasClaudeDir && !hasMcpJson && !hasClaudeMd) return "cursor";
107-
if (hasCursorRules && !hasClaudeMd) return "cursor";
108-
if (hasCursorMcp && !hasMcpJson) return "cursor";
178+
if (hasCursorRules && !hasClaudeMd && !hasCopilotInstructions) return "cursor";
179+
if (hasCursorMcp && !hasMcpJson && !hasVscodeMcp) return "cursor";
109180

110181
// Strong Claude signals
111-
if (hasClaudeDir && !hasCursorDir) return "claude";
112-
if (hasMcpJson && !hasCursorMcp) return "claude";
113-
if (hasClaudeMd && !hasCursorRules) return "claude";
182+
if (hasClaudeDir && !hasCursorDir && !hasVscodeDir) return "claude";
183+
if (hasMcpJson && !hasCursorMcp && !hasVscodeMcp) return "claude";
184+
if (hasClaudeMd && !hasCursorRules && !hasCopilotInstructions) return "claude";
185+
186+
// VS Code signals
187+
if (hasVscodeMcp && !hasMcpJson && !hasCursorMcp) return "vscode";
188+
if (hasCopilotInstructions && !hasClaudeMd && !hasCursorRules) return "vscode";
189+
190+
// Windsurf signals
191+
if (hasWindsurfRules && !hasClaudeMd && !hasCursorRules && !hasCopilotInstructions) return "windsurf";
114192

115193
// Default to Claude Code (most common)
116194
return "claude";
@@ -439,6 +517,7 @@ async function stepMemoryStore() {
439517
async function stepMcpServer() {
440518
const mcpPath = cc.mcpConfigPath;
441519
const mcpName = cc.mcpConfigName;
520+
const isUserLevel = cc.mcpConfigScope === "user";
442521

443522
const existing = readJson(mcpPath);
444523
const hasGitmem =
@@ -453,21 +532,22 @@ async function stepMcpServer() {
453532
? Object.keys(existing.mcpServers).length
454533
: 0;
455534
const tierLabel = process.env.SUPABASE_URL ? "pro" : "free";
535+
const scopeNote = isUserLevel ? " (user-level config)" : "";
456536
const prompt = existing
457-
? `Add gitmem to ${mcpName}? (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)`
458-
: `Create ${mcpName} with gitmem server?`;
537+
? `Add gitmem to ${mcpName}?${scopeNote} (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)`
538+
: `Create ${mcpName} with gitmem server?${scopeNote}`;
459539

460540
if (!(await confirm(prompt))) {
461541
console.log(" Skipped.");
462542
return;
463543
}
464544

465545
if (dryRun) {
466-
console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier)`);
546+
console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier${scopeNote})`);
467547
return;
468548
}
469549

470-
// Ensure parent directory exists (for .cursor/mcp.json)
550+
// Ensure parent directory exists (for .cursor/mcp.json, .vscode/mcp.json, ~/.codeium/windsurf/)
471551
const parentDir = dirname(mcpPath);
472552
if (!existsSync(parentDir)) {
473553
mkdirSync(parentDir, { recursive: true });
@@ -481,7 +561,8 @@ async function stepMcpServer() {
481561
console.log(
482562
` Added gitmem entry to ${mcpName} (${tierLabel} tier` +
483563
(process.env.SUPABASE_URL ? " \u2014 Supabase detected" : " \u2014 local storage") +
484-
")"
564+
")" +
565+
(isUserLevel ? " [user-level]" : "")
485566
);
486567
}
487568

@@ -525,6 +606,12 @@ async function stepInstructions() {
525606
block = `${cc.startMarker}\n${block}\n${cc.endMarker}`;
526607
}
527608

609+
// Ensure parent directory exists (for .github/copilot-instructions.md)
610+
const instrParentDir = dirname(instrPath);
611+
if (!existsSync(instrParentDir)) {
612+
mkdirSync(instrParentDir, { recursive: true });
613+
}
614+
528615
if (exists) {
529616
content = content.trimEnd() + "\n\n" + block + "\n";
530617
} else {
@@ -598,6 +685,11 @@ function copyHookScripts() {
598685
}
599686

600687
async function stepHooks() {
688+
if (!cc.hasHooks) {
689+
console.log(` ${cc.name} does not support lifecycle hooks. Skipping.`);
690+
console.log(" Enforcement relies on system prompt instructions instead.");
691+
return;
692+
}
601693
if (cc.hooksInSettings) {
602694
return stepHooksClaude();
603695
}
@@ -821,7 +913,7 @@ async function main() {
821913
);
822914
}
823915

824-
if (!cc.hooksInSettings && cc.hooksFile && existsSync(cc.hooksFile)) {
916+
if (!cc.hooksInSettings && cc.hasHooks && cc.hooksFile && existsSync(cc.hooksFile)) {
825917
const hooks = readJson(cc.hooksFile);
826918
const hookCount = hooks?.hooks
827919
? Object.values(hooks.hooks).flat().length
@@ -850,8 +942,10 @@ async function main() {
850942
);
851943
console.log("");
852944

853-
// Run steps — step count depends on client
854-
const stepCount = cc.hasPermissions ? 6 : 5;
945+
// Run steps — step count depends on client capabilities
946+
let stepCount = 4; // memory store + mcp server + instructions + gitignore
947+
if (cc.hasPermissions) stepCount++;
948+
if (cc.hasHooks) stepCount++;
855949
let step = 1;
856950

857951
console.log(` Step ${step}/${stepCount} \u2014 Memory Store`);
@@ -876,10 +970,12 @@ async function main() {
876970
step++;
877971
}
878972

879-
console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
880-
await stepHooks();
881-
console.log("");
882-
step++;
973+
if (cc.hasHooks) {
974+
console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
975+
await stepHooks();
976+
console.log("");
977+
step++;
978+
}
883979

884980
console.log(` Step ${step}/${stepCount} \u2014 Gitignore`);
885981
await stepGitignore();

0 commit comments

Comments
 (0)