Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,27 @@

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.

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

## Quick Start

```bash
npx gitmem-mcp init
```

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

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

```bash
npx gitmem-mcp init --yes # Non-interactive
npx gitmem-mcp init --dry-run # Preview changes
npx gitmem-mcp init --yes # Non-interactive
npx gitmem-mcp init --dry-run # Preview changes
npx gitmem-mcp init --client vscode # Force specific client
```

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

## Supported Clients

| Client | Setup |
|--------|-------|
| **Claude Code** | `npx gitmem-mcp init` (auto-detected) |
| **Claude Desktop** | `npx gitmem-mcp init` or add to `claude_desktop_config.json` |
| **Cursor** | `npx gitmem-mcp init` or add to `.cursor/mcp.json` |
| **Any MCP client** | Add `npx -y gitmem-mcp` as an MCP server |
| Client | Setup | Hooks |
|--------|-------|-------|
| **Claude Code** | `npx gitmem-mcp init` | Full (session, recall, credential guard) |
| **Cursor** | `npx gitmem-mcp init --client cursor` | Partial (session, recall) |
| **VS Code (Copilot)** | `npx gitmem-mcp init --client vscode` | Instructions-based |
| **Windsurf** | `npx gitmem-mcp init --client windsurf` | Instructions-based |
| **Claude Desktop** | Add to `claude_desktop_config.json` | Manual |
| **Any MCP client** | `npx gitmem-mcp init --client generic` | Instructions-based |

The wizard auto-detects your IDE. Use `--client` to override.

<details>
<summary><strong>Manual MCP configuration</strong></summary>

Add this to your MCP client's config file:

```json
{
"mcpServers": {
Expand All @@ -99,13 +105,21 @@ Every scar includes **counter-arguments** — reasons why someone might reasonab
}
```

| Client | Config file |
|--------|-------------|
| Claude Code | `.mcp.json` |
| Cursor | `.cursor/mcp.json` |
| VS Code | `.vscode/mcp.json` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |

</details>

## CLI Commands

| Command | Description |
|---------|-------------|
| `npx gitmem-mcp init` | Interactive setup wizard |
| `npx gitmem-mcp init` | Interactive setup wizard (auto-detects IDE) |
| `npx gitmem-mcp init --client <name>` | Setup for specific client (`claude`, `cursor`, `vscode`, `windsurf`, `generic`) |
| `npx gitmem-mcp init --yes` | Non-interactive setup |
| `npx gitmem-mcp init --dry-run` | Preview changes |
| `npx gitmem-mcp uninstall` | Clean removal (preserves `.gitmem/` data) |
Expand Down
138 changes: 117 additions & 21 deletions bin/init-wizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Interactive setup that detects existing config, prompts, and merges.
* Supports Claude Code and Cursor IDE.
*
* Usage: npx gitmem-mcp init [--yes] [--dry-run] [--project <name>] [--client <claude|cursor>]
* Usage: npx gitmem-mcp init [--yes] [--dry-run] [--project <name>] [--client <claude|cursor|vscode|windsurf|generic>]
*/

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

// ── Client Configuration ──

// Resolve user home directory for clients that use user-level config
const homeDir = process.env.HOME || process.env.USERPROFILE || "~";

const CLIENT_CONFIGS = {
claude: {
name: "Claude Code",
mcpConfigPath: join(cwd, ".mcp.json"),
mcpConfigName: ".mcp.json",
mcpConfigScope: "project",
instructionsFile: join(cwd, "CLAUDE.md"),
instructionsName: "CLAUDE.md",
templateFile: join(__dirname, "..", "CLAUDE.md.template"),
Expand All @@ -50,12 +54,14 @@ const CLIENT_CONFIGS = {
settingsLocalFile: join(cwd, ".claude", "settings.local.json"),
hasPermissions: true,
hooksInSettings: true,
hasHooks: true,
completionMsg: "Setup complete! Start Claude Code \u2014 memory is active.",
},
cursor: {
name: "Cursor",
mcpConfigPath: join(cwd, ".cursor", "mcp.json"),
mcpConfigName: ".cursor/mcp.json",
mcpConfigScope: "project",
instructionsFile: join(cwd, ".cursorrules"),
instructionsName: ".cursorrules",
templateFile: join(__dirname, "..", "cursorrules.template"),
Expand All @@ -66,10 +72,66 @@ const CLIENT_CONFIGS = {
settingsLocalFile: null,
hasPermissions: false,
hooksInSettings: false,
hasHooks: true,
hooksFile: join(cwd, ".cursor", "hooks.json"),
hooksFileName: ".cursor/hooks.json",
completionMsg: "Setup complete! Open Cursor (Agent mode) \u2014 memory is active.",
},
vscode: {
name: "VS Code (Copilot)",
mcpConfigPath: join(cwd, ".vscode", "mcp.json"),
mcpConfigName: ".vscode/mcp.json",
mcpConfigScope: "project",
instructionsFile: join(cwd, ".github", "copilot-instructions.md"),
instructionsName: ".github/copilot-instructions.md",
templateFile: join(__dirname, "..", "copilot-instructions.template"),
startMarker: "<!-- gitmem:start -->",
endMarker: "<!-- gitmem:end -->",
configDir: join(cwd, ".vscode"),
settingsFile: null,
settingsLocalFile: null,
hasPermissions: false,
hooksInSettings: false,
hasHooks: false,
completionMsg: "Setup complete! Open VS Code \u2014 memory is active via Copilot.",
},
windsurf: {
name: "Windsurf",
mcpConfigPath: join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
mcpConfigName: "~/.codeium/windsurf/mcp_config.json",
mcpConfigScope: "user",
instructionsFile: join(cwd, ".windsurfrules"),
instructionsName: ".windsurfrules",
templateFile: join(__dirname, "..", "windsurfrules.template"),
startMarker: "# --- gitmem:start ---",
endMarker: "# --- gitmem:end ---",
configDir: null,
settingsFile: null,
settingsLocalFile: null,
hasPermissions: false,
hooksInSettings: false,
hasHooks: false,
completionMsg: "Setup complete! Open Windsurf \u2014 memory is active.",
},
generic: {
name: "Generic MCP Client",
mcpConfigPath: join(cwd, ".mcp.json"),
mcpConfigName: ".mcp.json",
mcpConfigScope: "project",
instructionsFile: join(cwd, "CLAUDE.md"),
instructionsName: "CLAUDE.md",
templateFile: join(__dirname, "..", "CLAUDE.md.template"),
startMarker: "<!-- gitmem:start -->",
endMarker: "<!-- gitmem:end -->",
configDir: null,
settingsFile: null,
settingsLocalFile: null,
hasPermissions: false,
hooksInSettings: false,
hasHooks: false,
completionMsg:
"Setup complete! Configure your MCP client to use the gitmem server from .mcp.json.",
},
};

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

// ── Client Detection ──

const VALID_CLIENTS = Object.keys(CLIENT_CONFIGS);

function detectClient() {
// Explicit flag takes priority
if (clientFlag) {
if (clientFlag !== "claude" && clientFlag !== "cursor") {
console.error(` Error: Unknown client "${clientFlag}". Use --client claude or --client cursor.`);
if (!VALID_CLIENTS.includes(clientFlag)) {
console.error(` Error: Unknown client "${clientFlag}". Use --client ${VALID_CLIENTS.join("|")}.`);
process.exit(1);
}
return clientFlag;
}

// Auto-detect based on directory presence
// Auto-detect based on directory/file presence
const hasCursorDir = existsSync(join(cwd, ".cursor"));
const hasClaudeDir = existsSync(join(cwd, ".claude"));
const hasMcpJson = existsSync(join(cwd, ".mcp.json"));
const hasClaudeMd = existsSync(join(cwd, "CLAUDE.md"));
const hasCursorRules = existsSync(join(cwd, ".cursorrules"));
const hasCursorMcp = existsSync(join(cwd, ".cursor", "mcp.json"));
const hasVscodeDir = existsSync(join(cwd, ".vscode"));
const hasVscodeMcp = existsSync(join(cwd, ".vscode", "mcp.json"));
const hasCopilotInstructions = existsSync(join(cwd, ".github", "copilot-instructions.md"));
const hasWindsurfRules = existsSync(join(cwd, ".windsurfrules"));
const hasWindsurfMcp = existsSync(
join(homeDir, ".codeium", "windsurf", "mcp_config.json")
);

// Strong Cursor signals
if (hasCursorDir && !hasClaudeDir && !hasMcpJson && !hasClaudeMd) return "cursor";
if (hasCursorRules && !hasClaudeMd) return "cursor";
if (hasCursorMcp && !hasMcpJson) return "cursor";
if (hasCursorRules && !hasClaudeMd && !hasCopilotInstructions) return "cursor";
if (hasCursorMcp && !hasMcpJson && !hasVscodeMcp) return "cursor";

// Strong Claude signals
if (hasClaudeDir && !hasCursorDir) return "claude";
if (hasMcpJson && !hasCursorMcp) return "claude";
if (hasClaudeMd && !hasCursorRules) return "claude";
if (hasClaudeDir && !hasCursorDir && !hasVscodeDir) return "claude";
if (hasMcpJson && !hasCursorMcp && !hasVscodeMcp) return "claude";
if (hasClaudeMd && !hasCursorRules && !hasCopilotInstructions) return "claude";

// VS Code signals
if (hasVscodeMcp && !hasMcpJson && !hasCursorMcp) return "vscode";
if (hasCopilotInstructions && !hasClaudeMd && !hasCursorRules) return "vscode";

// Windsurf signals
if (hasWindsurfRules && !hasClaudeMd && !hasCursorRules && !hasCopilotInstructions) return "windsurf";

// Default to Claude Code (most common)
return "claude";
Expand Down Expand Up @@ -439,6 +517,7 @@ async function stepMemoryStore() {
async function stepMcpServer() {
const mcpPath = cc.mcpConfigPath;
const mcpName = cc.mcpConfigName;
const isUserLevel = cc.mcpConfigScope === "user";

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

if (!(await confirm(prompt))) {
console.log(" Skipped.");
return;
}

if (dryRun) {
console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier)`);
console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier${scopeNote})`);
return;
}

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

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

// Ensure parent directory exists (for .github/copilot-instructions.md)
const instrParentDir = dirname(instrPath);
if (!existsSync(instrParentDir)) {
mkdirSync(instrParentDir, { recursive: true });
}

if (exists) {
content = content.trimEnd() + "\n\n" + block + "\n";
} else {
Expand Down Expand Up @@ -598,6 +685,11 @@ function copyHookScripts() {
}

async function stepHooks() {
if (!cc.hasHooks) {
console.log(` ${cc.name} does not support lifecycle hooks. Skipping.`);
console.log(" Enforcement relies on system prompt instructions instead.");
return;
}
if (cc.hooksInSettings) {
return stepHooksClaude();
}
Expand Down Expand Up @@ -821,7 +913,7 @@ async function main() {
);
}

if (!cc.hooksInSettings && cc.hooksFile && existsSync(cc.hooksFile)) {
if (!cc.hooksInSettings && cc.hasHooks && cc.hooksFile && existsSync(cc.hooksFile)) {
const hooks = readJson(cc.hooksFile);
const hookCount = hooks?.hooks
? Object.values(hooks.hooks).flat().length
Expand Down Expand Up @@ -850,8 +942,10 @@ async function main() {
);
console.log("");

// Run steps — step count depends on client
const stepCount = cc.hasPermissions ? 6 : 5;
// Run steps — step count depends on client capabilities
let stepCount = 4; // memory store + mcp server + instructions + gitignore
if (cc.hasPermissions) stepCount++;
if (cc.hasHooks) stepCount++;
let step = 1;

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

console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
await stepHooks();
console.log("");
step++;
if (cc.hasHooks) {
console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
await stepHooks();
console.log("");
step++;
}

console.log(` Step ${step}/${stepCount} \u2014 Gitignore`);
await stepGitignore();
Expand Down
Loading