2495f98496
- Phase 1: Replace 5 duplicate MCP installers with config-driven factory, extract shared context-injection and json-utils utilities, fix process.execPath usage - Phase 2: Add non-TTY fallback for @clack/prompts to prevent ENOENT in CI/Docker - Phase 3: Wire GeminiCliHooksInstaller through hook command framework with adapter - Phase 4: Auto-start transcript watchers on worker boot when config exists Net -107 lines via DRY consolidation of duplicated installer logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
359 lines
12 KiB
TypeScript
359 lines
12 KiB
TypeScript
/**
|
|
* McpIntegrations - MCP-based IDE integrations for claude-mem
|
|
*
|
|
* Handles MCP config writing and context injection for IDEs that support
|
|
* the Model Context Protocol. These are "MCP-only" integrations: they provide
|
|
* search tools and context injection but do NOT capture transcripts.
|
|
*
|
|
* Supported IDEs:
|
|
* - Copilot CLI
|
|
* - Antigravity (Gemini)
|
|
* - Goose
|
|
* - Crush
|
|
* - Roo Code
|
|
* - Warp
|
|
*
|
|
* All IDEs point to the same MCP server: plugin/scripts/mcp-server.cjs
|
|
*/
|
|
|
|
import path from 'path';
|
|
import { homedir } from 'os';
|
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
import { logger } from '../../utils/logger.js';
|
|
import { findMcpServerPath } from './CursorHooksInstaller.js';
|
|
import { readJsonSafe } from '../../utils/json-utils.js';
|
|
import { injectContextIntoMarkdownFile } from '../../utils/context-injection.js';
|
|
|
|
// ============================================================================
|
|
// Shared Constants
|
|
// ============================================================================
|
|
|
|
const PLACEHOLDER_CONTEXT = `# claude-mem: Cross-Session Memory
|
|
|
|
*No context yet. Complete your first session and context will appear here.*
|
|
|
|
Use claude-mem's MCP search tools for manual memory queries.`;
|
|
|
|
// ============================================================================
|
|
// Shared Utilities
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Build the standard MCP server entry that all IDEs use.
|
|
* Points to the same mcp-server.cjs script.
|
|
*/
|
|
function buildMcpServerEntry(mcpServerPath: string): { command: string; args: string[] } {
|
|
return {
|
|
command: process.execPath,
|
|
args: [mcpServerPath],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Write a standard MCP JSON config file, merging with existing config.
|
|
* Supports both { "mcpServers": { ... } } and { "servers": { ... } } formats.
|
|
*/
|
|
function writeMcpJsonConfig(
|
|
configFilePath: string,
|
|
mcpServerPath: string,
|
|
serversKeyName: string = 'mcpServers',
|
|
): void {
|
|
const parentDirectory = path.dirname(configFilePath);
|
|
mkdirSync(parentDirectory, { recursive: true });
|
|
|
|
const existingConfig = readJsonSafe<Record<string, any>>(configFilePath, {});
|
|
|
|
if (!existingConfig[serversKeyName]) {
|
|
existingConfig[serversKeyName] = {};
|
|
}
|
|
|
|
existingConfig[serversKeyName]['claude-mem'] = buildMcpServerEntry(mcpServerPath);
|
|
|
|
writeFileSync(configFilePath, JSON.stringify(existingConfig, null, 2) + '\n');
|
|
}
|
|
|
|
// ============================================================================
|
|
// MCP Installer Factory (Phase 1D)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Configuration for a JSON-based MCP IDE integration.
|
|
*/
|
|
interface McpInstallerConfig {
|
|
ideId: string;
|
|
ideLabel: string;
|
|
configPath: string;
|
|
configKey: 'servers' | 'mcpServers';
|
|
contextFile?: {
|
|
path: string;
|
|
isWorkspaceRelative: boolean;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Factory function that creates an MCP installer for any JSON-config-based IDE.
|
|
* Handles MCP config writing and optional context injection.
|
|
*/
|
|
function installMcpIntegration(config: McpInstallerConfig): () => Promise<number> {
|
|
return async (): Promise<number> => {
|
|
console.log(`\nInstalling Claude-Mem MCP integration for ${config.ideLabel}...\n`);
|
|
|
|
const mcpServerPath = findMcpServerPath();
|
|
if (!mcpServerPath) {
|
|
console.error('Could not find MCP server script');
|
|
console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs');
|
|
return 1;
|
|
}
|
|
|
|
try {
|
|
// Write MCP config
|
|
const configPath = config.configPath;
|
|
|
|
// Warp special case: skip config write if ~/.warp/ doesn't exist
|
|
if (config.ideId === 'warp' && !existsSync(path.dirname(configPath))) {
|
|
console.log(` Note: ~/.warp/ not found. MCP may need to be configured via Warp Drive UI.`);
|
|
} else {
|
|
writeMcpJsonConfig(configPath, mcpServerPath, config.configKey);
|
|
console.log(` MCP config written to: ${configPath}`);
|
|
}
|
|
|
|
// Inject context if configured
|
|
let contextPath: string | undefined;
|
|
if (config.contextFile) {
|
|
contextPath = config.contextFile.path;
|
|
injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT);
|
|
console.log(` Context placeholder written to: ${contextPath}`);
|
|
}
|
|
|
|
// Print summary
|
|
const summaryLines = [`\nInstallation complete!\n`];
|
|
summaryLines.push(`MCP config: ${configPath}`);
|
|
if (contextPath) {
|
|
summaryLines.push(`Context: ${contextPath}`);
|
|
}
|
|
summaryLines.push('');
|
|
summaryLines.push(`Note: This is an MCP-only integration providing search tools and context.`);
|
|
summaryLines.push(`Transcript capture is not available for ${config.ideLabel}.`);
|
|
if (config.ideId === 'warp') {
|
|
summaryLines.push('If MCP config via file is not supported, configure MCP through Warp Drive UI.');
|
|
}
|
|
summaryLines.push('');
|
|
summaryLines.push('Next steps:');
|
|
summaryLines.push(' 1. Start claude-mem worker: npx claude-mem start');
|
|
summaryLines.push(` 2. Restart ${config.ideLabel} to pick up the MCP server`);
|
|
summaryLines.push('');
|
|
console.log(summaryLines.join('\n'));
|
|
|
|
return 0;
|
|
} catch (error) {
|
|
console.error(`\nInstallation failed: ${(error as Error).message}`);
|
|
return 1;
|
|
}
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Factory Configs for JSON-based IDEs
|
|
// ============================================================================
|
|
|
|
const COPILOT_CLI_CONFIG: McpInstallerConfig = {
|
|
ideId: 'copilot-cli',
|
|
ideLabel: 'Copilot CLI',
|
|
configPath: path.join(homedir(), '.github', 'copilot', 'mcp.json'),
|
|
configKey: 'servers',
|
|
contextFile: {
|
|
path: path.join(process.cwd(), '.github', 'copilot-instructions.md'),
|
|
isWorkspaceRelative: true,
|
|
},
|
|
};
|
|
|
|
const ANTIGRAVITY_CONFIG: McpInstallerConfig = {
|
|
ideId: 'antigravity',
|
|
ideLabel: 'Antigravity',
|
|
configPath: path.join(homedir(), '.gemini', 'antigravity', 'mcp_config.json'),
|
|
configKey: 'mcpServers',
|
|
contextFile: {
|
|
path: path.join(process.cwd(), '.agent', 'rules', 'claude-mem-context.md'),
|
|
isWorkspaceRelative: true,
|
|
},
|
|
};
|
|
|
|
const CRUSH_CONFIG: McpInstallerConfig = {
|
|
ideId: 'crush',
|
|
ideLabel: 'Crush',
|
|
configPath: path.join(homedir(), '.config', 'crush', 'mcp.json'),
|
|
configKey: 'mcpServers',
|
|
};
|
|
|
|
const ROO_CODE_CONFIG: McpInstallerConfig = {
|
|
ideId: 'roo-code',
|
|
ideLabel: 'Roo Code',
|
|
configPath: path.join(process.cwd(), '.roo', 'mcp.json'),
|
|
configKey: 'mcpServers',
|
|
contextFile: {
|
|
path: path.join(process.cwd(), '.roo', 'rules', 'claude-mem-context.md'),
|
|
isWorkspaceRelative: true,
|
|
},
|
|
};
|
|
|
|
const WARP_CONFIG: McpInstallerConfig = {
|
|
ideId: 'warp',
|
|
ideLabel: 'Warp',
|
|
configPath: path.join(homedir(), '.warp', 'mcp.json'),
|
|
configKey: 'mcpServers',
|
|
contextFile: {
|
|
path: path.join(process.cwd(), 'WARP.md'),
|
|
isWorkspaceRelative: true,
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Goose (YAML-based — separate handler)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get the Goose config path.
|
|
* Goose stores its config at ~/.config/goose/config.yaml.
|
|
*/
|
|
function getGooseConfigPath(): string {
|
|
return path.join(homedir(), '.config', 'goose', 'config.yaml');
|
|
}
|
|
|
|
/**
|
|
* Check if a YAML string already has a claude-mem entry under mcpServers.
|
|
* Uses string matching to avoid needing a YAML parser.
|
|
*/
|
|
function gooseConfigHasClaudeMemEntry(yamlContent: string): boolean {
|
|
// Look for "claude-mem:" indented under mcpServers
|
|
return yamlContent.includes('claude-mem:') &&
|
|
yamlContent.includes('mcpServers:');
|
|
}
|
|
|
|
/**
|
|
* Build the Goose YAML MCP server block as a string.
|
|
* Produces properly indented YAML without needing a parser.
|
|
*/
|
|
function buildGooseMcpYamlBlock(mcpServerPath: string): string {
|
|
// Goose expects the mcpServers section at the top level
|
|
return [
|
|
'mcpServers:',
|
|
' claude-mem:',
|
|
` command: ${process.execPath}`,
|
|
' args:',
|
|
` - ${mcpServerPath}`,
|
|
].join('\n');
|
|
}
|
|
|
|
/**
|
|
* Build just the claude-mem server entry (for appending under existing mcpServers).
|
|
*/
|
|
function buildGooseClaudeMemEntryYaml(mcpServerPath: string): string {
|
|
return [
|
|
' claude-mem:',
|
|
` command: ${process.execPath}`,
|
|
' args:',
|
|
` - ${mcpServerPath}`,
|
|
].join('\n');
|
|
}
|
|
|
|
/**
|
|
* Install claude-mem MCP integration for Goose.
|
|
*
|
|
* - Writes/merges MCP config into ~/.config/goose/config.yaml
|
|
* - Uses string manipulation for YAML (no parser dependency)
|
|
*
|
|
* @returns 0 on success, 1 on failure
|
|
*/
|
|
export async function installGooseMcpIntegration(): Promise<number> {
|
|
console.log('\nInstalling Claude-Mem MCP integration for Goose...\n');
|
|
|
|
const mcpServerPath = findMcpServerPath();
|
|
if (!mcpServerPath) {
|
|
console.error('Could not find MCP server script');
|
|
console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs');
|
|
return 1;
|
|
}
|
|
|
|
try {
|
|
const configPath = getGooseConfigPath();
|
|
const configDirectory = path.dirname(configPath);
|
|
mkdirSync(configDirectory, { recursive: true });
|
|
|
|
if (existsSync(configPath)) {
|
|
let yamlContent = readFileSync(configPath, 'utf-8');
|
|
|
|
if (gooseConfigHasClaudeMemEntry(yamlContent)) {
|
|
// Already configured — replace the claude-mem block
|
|
// Find the claude-mem entry and replace it
|
|
const claudeMemPattern = /( {2}claude-mem:\n(?:.*\n)*?(?= {2}\S|\n\n|$))/;
|
|
const newEntry = buildGooseClaudeMemEntryYaml(mcpServerPath) + '\n';
|
|
|
|
if (claudeMemPattern.test(yamlContent)) {
|
|
yamlContent = yamlContent.replace(claudeMemPattern, newEntry);
|
|
}
|
|
writeFileSync(configPath, yamlContent);
|
|
console.log(` Updated existing claude-mem entry in: ${configPath}`);
|
|
} else if (yamlContent.includes('mcpServers:')) {
|
|
// mcpServers section exists but no claude-mem entry — append under it
|
|
const mcpServersIndex = yamlContent.indexOf('mcpServers:');
|
|
const insertionPoint = mcpServersIndex + 'mcpServers:'.length;
|
|
const newEntry = '\n' + buildGooseClaudeMemEntryYaml(mcpServerPath);
|
|
|
|
yamlContent =
|
|
yamlContent.slice(0, insertionPoint) +
|
|
newEntry +
|
|
yamlContent.slice(insertionPoint);
|
|
|
|
writeFileSync(configPath, yamlContent);
|
|
console.log(` Added claude-mem to existing mcpServers in: ${configPath}`);
|
|
} else {
|
|
// No mcpServers section — append the entire block
|
|
const mcpBlock = '\n' + buildGooseMcpYamlBlock(mcpServerPath) + '\n';
|
|
yamlContent = yamlContent.trimEnd() + '\n' + mcpBlock;
|
|
writeFileSync(configPath, yamlContent);
|
|
console.log(` Appended mcpServers section to: ${configPath}`);
|
|
}
|
|
} else {
|
|
// File doesn't exist — create from template
|
|
const templateContent = buildGooseMcpYamlBlock(mcpServerPath) + '\n';
|
|
writeFileSync(configPath, templateContent);
|
|
console.log(` Created config with MCP server: ${configPath}`);
|
|
}
|
|
|
|
console.log(`
|
|
Installation complete!
|
|
|
|
MCP config: ${configPath}
|
|
|
|
Note: This is an MCP-only integration providing search tools and context.
|
|
Transcript capture is not available for Goose.
|
|
|
|
Next steps:
|
|
1. Start claude-mem worker: npx claude-mem start
|
|
2. Restart Goose to pick up the MCP server
|
|
`);
|
|
|
|
return 0;
|
|
} catch (error) {
|
|
console.error(`\nInstallation failed: ${(error as Error).message}`);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unified Installer (used by npx install command)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Map of IDE identifiers to their install functions.
|
|
* Used by the install command to dispatch to the correct integration.
|
|
*/
|
|
export const MCP_IDE_INSTALLERS: Record<string, () => Promise<number>> = {
|
|
'copilot-cli': installMcpIntegration(COPILOT_CLI_CONFIG),
|
|
'antigravity': installMcpIntegration(ANTIGRAVITY_CONFIG),
|
|
'goose': installGooseMcpIntegration,
|
|
'crush': installMcpIntegration(CRUSH_CONFIG),
|
|
'roo-code': installMcpIntegration(ROO_CODE_CONFIG),
|
|
'warp': installMcpIntegration(WARP_CONFIG),
|
|
};
|