diff --git a/src/npx-cli/commands/ide-detection.ts b/src/npx-cli/commands/ide-detection.ts index ef49b684..a97c21c1 100644 --- a/src/npx-cli/commands/ide-detection.ts +++ b/src/npx-cli/commands/ide-detection.ts @@ -96,8 +96,8 @@ export function detectInstalledIDEs(): IDEInfo[] { id: 'openclaw', label: 'OpenClaw', detected: existsSync(join(home, '.openclaw')), - supported: false, - hint: 'coming soon', + supported: true, + hint: 'plugin-based integration', }, { id: 'windsurf', @@ -109,8 +109,8 @@ export function detectInstalledIDEs(): IDEInfo[] { id: 'codex-cli', label: 'Codex CLI', detected: existsSync(join(home, '.codex')), - supported: false, - hint: 'coming soon', + supported: true, + hint: 'transcript-based integration', }, { id: 'cursor', @@ -122,44 +122,44 @@ export function detectInstalledIDEs(): IDEInfo[] { id: 'copilot-cli', label: 'Copilot CLI', detected: isCommandInPath('copilot'), - supported: false, - hint: 'coming soon', + supported: true, + hint: 'MCP-based integration', }, { id: 'antigravity', label: 'Antigravity', detected: existsSync(join(home, '.gemini', 'antigravity')), - supported: false, - hint: 'coming soon', + supported: true, + hint: 'MCP-based integration', }, { id: 'goose', label: 'Goose', detected: existsSync(join(home, '.config', 'goose')) || isCommandInPath('goose'), - supported: false, - hint: 'coming soon', + supported: true, + hint: 'MCP-based integration', }, { id: 'crush', label: 'Crush', detected: isCommandInPath('crush'), - supported: false, - hint: 'coming soon', + supported: true, + hint: 'MCP-based integration', }, { id: 'roo-code', label: 'Roo Code', detected: hasVscodeExtension('roo-code'), - supported: false, - hint: 'coming soon', + supported: true, + hint: 'MCP-based integration', }, { id: 'warp', label: 'Warp', detected: existsSync(join(home, '.warp')) || isCommandInPath('warp'), - supported: false, - hint: 'coming soon', + supported: true, + hint: 'MCP-based integration', }, ]; } diff --git a/src/npx-cli/commands/install.ts b/src/npx-cli/commands/install.ts index d95e5f3d..8b0405de 100644 --- a/src/npx-cli/commands/install.ts +++ b/src/npx-cli/commands/install.ts @@ -132,6 +132,50 @@ async function setupIDEs(selectedIDEs: string[]): Promise { break; } + case 'openclaw': { + const { installOpenClawIntegration } = await import('../../services/integrations/OpenClawInstaller.js'); + const openClawResult = await installOpenClawIntegration(); + if (openClawResult === 0) { + p.log.success('OpenClaw: plugin installed.'); + } else { + p.log.error('OpenClaw: plugin installation failed.'); + } + break; + } + + case 'codex-cli': { + const { installCodexCli } = await import('../../services/integrations/CodexCliInstaller.js'); + const codexResult = await installCodexCli(); + if (codexResult === 0) { + p.log.success('Codex CLI: transcript watching configured.'); + } else { + p.log.error('Codex CLI: integration setup failed.'); + } + break; + } + + case 'copilot-cli': + case 'antigravity': + case 'goose': + case 'crush': + case 'roo-code': + case 'warp': { + const { MCP_IDE_INSTALLERS } = await import('../../services/integrations/McpIntegrations.js'); + const mcpInstaller = MCP_IDE_INSTALLERS[ideId]; + if (mcpInstaller) { + const mcpResult = await mcpInstaller(); + const allIDEs = detectInstalledIDEs(); + const ideInfo = allIDEs.find((i) => i.id === ideId); + const ideLabel = ideInfo?.label ?? ideId; + if (mcpResult === 0) { + p.log.success(`${ideLabel}: MCP integration installed.`); + } else { + p.log.error(`${ideLabel}: MCP integration failed.`); + } + } + break; + } + default: { const allIDEs = detectInstalledIDEs(); const ide = allIDEs.find((i) => i.id === ideId); diff --git a/src/services/integrations/CodexCliInstaller.ts b/src/services/integrations/CodexCliInstaller.ts new file mode 100644 index 00000000..9645b2bc --- /dev/null +++ b/src/services/integrations/CodexCliInstaller.ts @@ -0,0 +1,373 @@ +/** + * CodexCliInstaller - Codex CLI integration for claude-mem + * + * Uses transcript-only watching (no notify hook). The watcher infrastructure + * already exists in src/services/transcripts/. This installer: + * + * 1. Writes/merges transcript-watch config to ~/.claude-mem/transcript-watch.json + * 2. Sets up watch for ~/.codex/sessions/**\/*.jsonl using existing watcher + * 3. Injects context via ~/.codex/AGENTS.md (Codex reads this natively) + * + * Anti-patterns: + * - Does NOT add notify hooks -- transcript watching is sufficient + * - Does NOT modify existing transcript watcher infrastructure + * - Does NOT overwrite existing transcript-watch.json -- merges only + */ + +import path from 'path'; +import { homedir } from 'os'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { logger } from '../../utils/logger.js'; +import { replaceTaggedContent } from '../../utils/claude-md-utils.js'; +import { + DEFAULT_CONFIG_PATH, + DEFAULT_STATE_PATH, + SAMPLE_CONFIG, +} from '../transcripts/config.js'; +import type { TranscriptWatchConfig, WatchTarget } from '../transcripts/types.js'; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const CODEX_DIR = path.join(homedir(), '.codex'); +const CODEX_AGENTS_MD_PATH = path.join(CODEX_DIR, 'AGENTS.md'); +const CLAUDE_MEM_DIR = path.join(homedir(), '.claude-mem'); + +/** + * The watch name used to identify the Codex CLI entry in transcript-watch.json. + * Must match the name in SAMPLE_CONFIG for merging to work correctly. + */ +const CODEX_WATCH_NAME = 'codex'; + +// --------------------------------------------------------------------------- +// Transcript Watch Config Merging +// --------------------------------------------------------------------------- + +/** + * Load existing transcript-watch.json, or return an empty config scaffold. + * Never throws -- returns a valid empty config on any parse error. + */ +function loadExistingTranscriptWatchConfig(): TranscriptWatchConfig { + const configPath = DEFAULT_CONFIG_PATH; + + if (!existsSync(configPath)) { + return { version: 1, schemas: {}, watches: [], stateFile: DEFAULT_STATE_PATH }; + } + + try { + const raw = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(raw) as TranscriptWatchConfig; + + // Ensure required fields exist + if (!parsed.version) parsed.version = 1; + if (!parsed.watches) parsed.watches = []; + if (!parsed.schemas) parsed.schemas = {}; + if (!parsed.stateFile) parsed.stateFile = DEFAULT_STATE_PATH; + + return parsed; + } catch (parseError) { + logger.error('CODEX', 'Corrupt transcript-watch.json, creating backup', { path: configPath }, parseError as Error); + + // Back up corrupt file + const backupPath = `${configPath}.backup.${Date.now()}`; + writeFileSync(backupPath, readFileSync(configPath)); + console.warn(` Backed up corrupt transcript-watch.json to ${backupPath}`); + + return { version: 1, schemas: {}, watches: [], stateFile: DEFAULT_STATE_PATH }; + } +} + +/** + * Merge Codex watch configuration into existing transcript-watch.json. + * + * - If a watch with name 'codex' already exists, it is replaced in-place. + * - If the 'codex' schema already exists, it is replaced in-place. + * - All other watches and schemas are preserved untouched. + */ +function mergeCodexWatchConfig(existingConfig: TranscriptWatchConfig): TranscriptWatchConfig { + const merged = { ...existingConfig }; + + // Merge schemas: add/replace the codex schema + merged.schemas = { ...merged.schemas }; + const codexSchema = SAMPLE_CONFIG.schemas?.[CODEX_WATCH_NAME]; + if (codexSchema) { + merged.schemas[CODEX_WATCH_NAME] = codexSchema; + } + + // Merge watches: add/replace the codex watch entry + const codexWatchFromSample = SAMPLE_CONFIG.watches.find( + (w: WatchTarget) => w.name === CODEX_WATCH_NAME, + ); + + if (codexWatchFromSample) { + const existingWatchIndex = merged.watches.findIndex( + (w: WatchTarget) => w.name === CODEX_WATCH_NAME, + ); + + if (existingWatchIndex !== -1) { + // Replace existing codex watch in-place + merged.watches[existingWatchIndex] = codexWatchFromSample; + } else { + // Append new codex watch + merged.watches.push(codexWatchFromSample); + } + } + + return merged; +} + +/** + * Write the merged transcript-watch.json config atomically. + */ +function writeTranscriptWatchConfig(config: TranscriptWatchConfig): void { + mkdirSync(CLAUDE_MEM_DIR, { recursive: true }); + writeFileSync(DEFAULT_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n'); +} + +// --------------------------------------------------------------------------- +// Context Injection (AGENTS.md) +// --------------------------------------------------------------------------- + +/** + * Inject claude-mem context section into ~/.codex/AGENTS.md. + * Uses the same tag pattern as CLAUDE.md and GEMINI.md. + * Preserves any existing user content outside the tags. + */ +function injectCodexAgentsMdContext(): void { + try { + mkdirSync(CODEX_DIR, { recursive: true }); + + let existingContent = ''; + if (existsSync(CODEX_AGENTS_MD_PATH)) { + existingContent = readFileSync(CODEX_AGENTS_MD_PATH, 'utf-8'); + } + + // Initial placeholder content -- will be populated after first session + const contextContent = [ + '# Recent Activity', + '', + '', + '', + '*No context yet. Complete your first session and context will appear here.*', + ].join('\n'); + + const finalContent = replaceTaggedContent(existingContent, contextContent); + writeFileSync(CODEX_AGENTS_MD_PATH, finalContent); + console.log(` Injected context placeholder into ${CODEX_AGENTS_MD_PATH}`); + } catch (error) { + // Non-fatal -- transcript watching still works without context injection + logger.warn('CODEX', 'Failed to inject AGENTS.md context', { error: (error as Error).message }); + console.warn(` Warning: Could not inject context into AGENTS.md: ${(error as Error).message}`); + } +} + +/** + * Remove claude-mem context section from AGENTS.md. + * Preserves user content outside the tags. + */ +function removeCodexAgentsMdContext(): void { + try { + if (!existsSync(CODEX_AGENTS_MD_PATH)) return; + + const content = readFileSync(CODEX_AGENTS_MD_PATH, 'utf-8'); + const startTag = ''; + const endTag = ''; + + const startIdx = content.indexOf(startTag); + const endIdx = content.indexOf(endTag); + + if (startIdx === -1 || endIdx === -1) return; + + // Remove the tagged section and any surrounding blank lines + const before = content.substring(0, startIdx).replace(/\n+$/, ''); + const after = content.substring(endIdx + endTag.length).replace(/^\n+/, ''); + const finalContent = (before + (after ? '\n\n' + after : '')).trim(); + + if (finalContent) { + writeFileSync(CODEX_AGENTS_MD_PATH, finalContent + '\n'); + } else { + // File would be empty -- leave it empty rather than deleting + // (user may have other tooling that expects it to exist) + writeFileSync(CODEX_AGENTS_MD_PATH, ''); + } + + console.log(` Removed context section from ${CODEX_AGENTS_MD_PATH}`); + } catch (error) { + logger.warn('CODEX', 'Failed to clean AGENTS.md context', { error: (error as Error).message }); + } +} + +// --------------------------------------------------------------------------- +// Public API: Install +// --------------------------------------------------------------------------- + +/** + * Install Codex CLI integration for claude-mem. + * + * 1. Merges Codex transcript-watch config into ~/.claude-mem/transcript-watch.json + * 2. Injects context placeholder into ~/.codex/AGENTS.md + * + * @returns 0 on success, 1 on failure + */ +export async function installCodexCli(): Promise { + console.log('\nInstalling Claude-Mem for Codex CLI (transcript watching)...\n'); + + try { + // Step 1: Merge transcript-watch config + const existingConfig = loadExistingTranscriptWatchConfig(); + const mergedConfig = mergeCodexWatchConfig(existingConfig); + writeTranscriptWatchConfig(mergedConfig); + console.log(` Updated ${DEFAULT_CONFIG_PATH}`); + console.log(` Watch path: ~/.codex/sessions/**/*.jsonl`); + console.log(` Schema: codex (v${SAMPLE_CONFIG.schemas?.codex?.version ?? '?'})`); + + // Step 2: Inject context into AGENTS.md + injectCodexAgentsMdContext(); + + console.log(` +Installation complete! + +Transcript watch config: ${DEFAULT_CONFIG_PATH} +Context file: ${CODEX_AGENTS_MD_PATH} + +How it works: + - claude-mem watches Codex session JSONL files for new activity + - No hooks needed -- transcript watching is fully automatic + - Context from past sessions is injected via ${CODEX_AGENTS_MD_PATH} + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Use Codex CLI as usual -- memory capture is automatic! +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// --------------------------------------------------------------------------- +// Public API: Uninstall +// --------------------------------------------------------------------------- + +/** + * Remove Codex CLI integration from claude-mem. + * + * 1. Removes the codex watch and schema from transcript-watch.json (preserves others) + * 2. Removes context section from AGENTS.md (preserves user content) + * + * @returns 0 on success, 1 on failure + */ +export function uninstallCodexCli(): number { + console.log('\nUninstalling Claude-Mem Codex CLI integration...\n'); + + try { + // Step 1: Remove codex watch from transcript-watch.json + if (existsSync(DEFAULT_CONFIG_PATH)) { + const config = loadExistingTranscriptWatchConfig(); + + // Remove codex watch + config.watches = config.watches.filter( + (w: WatchTarget) => w.name !== CODEX_WATCH_NAME, + ); + + // Remove codex schema + if (config.schemas) { + delete config.schemas[CODEX_WATCH_NAME]; + } + + writeTranscriptWatchConfig(config); + console.log(` Removed codex watch from ${DEFAULT_CONFIG_PATH}`); + } else { + console.log(' No transcript-watch.json found -- nothing to remove.'); + } + + // Step 2: Remove context section from AGENTS.md + removeCodexAgentsMdContext(); + + console.log('\nUninstallation complete!'); + console.log('Restart claude-mem worker to apply changes.\n'); + + return 0; + } catch (error) { + console.error(`\nUninstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// --------------------------------------------------------------------------- +// Public API: Status Check +// --------------------------------------------------------------------------- + +/** + * Check Codex CLI integration status. + * + * @returns 0 always (informational) + */ +export function checkCodexCliStatus(): number { + console.log('\nClaude-Mem Codex CLI Integration Status\n'); + + // Check transcript-watch.json + if (!existsSync(DEFAULT_CONFIG_PATH)) { + console.log('Status: Not installed'); + console.log(` No transcript watch config at ${DEFAULT_CONFIG_PATH}`); + console.log('\nRun: npx claude-mem install --ide codex-cli\n'); + return 0; + } + + try { + const config = loadExistingTranscriptWatchConfig(); + const codexWatch = config.watches.find( + (w: WatchTarget) => w.name === CODEX_WATCH_NAME, + ); + const codexSchema = config.schemas?.[CODEX_WATCH_NAME]; + + if (!codexWatch) { + console.log('Status: Not installed'); + console.log(' transcript-watch.json exists but no codex watch configured.'); + console.log('\nRun: npx claude-mem install --ide codex-cli\n'); + return 0; + } + + console.log('Status: Installed'); + console.log(` Config: ${DEFAULT_CONFIG_PATH}`); + console.log(` Watch path: ${codexWatch.path}`); + console.log(` Schema: ${codexSchema ? `codex (v${codexSchema.version ?? '?'})` : 'missing'}`); + console.log(` Start at end: ${codexWatch.startAtEnd ?? false}`); + + // Check context config + if (codexWatch.context) { + console.log(` Context mode: ${codexWatch.context.mode}`); + console.log(` Context path: ${codexWatch.context.path ?? 'default'}`); + console.log(` Context updates on: ${codexWatch.context.updateOn?.join(', ') ?? 'none'}`); + } + + // Check AGENTS.md + if (existsSync(CODEX_AGENTS_MD_PATH)) { + const mdContent = readFileSync(CODEX_AGENTS_MD_PATH, 'utf-8'); + if (mdContent.includes('')) { + console.log(` Context: Active (${CODEX_AGENTS_MD_PATH})`); + } else { + console.log(` Context: AGENTS.md exists but no context tags`); + } + } else { + console.log(` Context: No AGENTS.md file`); + } + + // Check if ~/.codex/sessions exists (indicates Codex has been used) + const sessionsDir = path.join(CODEX_DIR, 'sessions'); + if (existsSync(sessionsDir)) { + console.log(` Sessions directory: exists`); + } else { + console.log(` Sessions directory: not yet created (use Codex CLI to generate sessions)`); + } + } catch { + console.log('Status: Unknown'); + console.log(' Could not parse transcript-watch.json.'); + } + + console.log(''); + return 0; +} diff --git a/src/services/integrations/McpIntegrations.ts b/src/services/integrations/McpIntegrations.ts new file mode 100644 index 00000000..75a6f347 --- /dev/null +++ b/src/services/integrations/McpIntegrations.ts @@ -0,0 +1,610 @@ +/** + * 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'; + +// ============================================================================ +// Shared Constants +// ============================================================================ + +const CONTEXT_TAG_OPEN = ''; +const CONTEXT_TAG_CLOSE = ''; + +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: 'node', + args: [mcpServerPath], + }; +} + +/** + * Read a JSON file safely, returning a default value if it doesn't exist or is corrupt. + */ +function readJsonSafe(filePath: string, defaultValue: T): T { + if (!existsSync(filePath)) return defaultValue; + try { + return JSON.parse(readFileSync(filePath, 'utf-8')); + } catch (error) { + logger.error('MCP', `Corrupt JSON file, using default`, { path: filePath }, error as Error); + return defaultValue; + } +} + +/** + * Inject or update a section in a markdown file. + * Creates the file if it doesn't exist. Preserves content outside the tags. + */ +function injectContextIntoMarkdownFile(filePath: string, contextContent: string): void { + const parentDirectory = path.dirname(filePath); + mkdirSync(parentDirectory, { recursive: true }); + + const wrappedContent = `${CONTEXT_TAG_OPEN}\n${contextContent}\n${CONTEXT_TAG_CLOSE}`; + + if (existsSync(filePath)) { + let existingContent = readFileSync(filePath, 'utf-8'); + + const tagStartIndex = existingContent.indexOf(CONTEXT_TAG_OPEN); + const tagEndIndex = existingContent.indexOf(CONTEXT_TAG_CLOSE); + + if (tagStartIndex !== -1 && tagEndIndex !== -1) { + // Replace existing section + existingContent = + existingContent.slice(0, tagStartIndex) + + wrappedContent + + existingContent.slice(tagEndIndex + CONTEXT_TAG_CLOSE.length); + } else { + // Append section + existingContent = existingContent.trimEnd() + '\n\n' + wrappedContent + '\n'; + } + + writeFileSync(filePath, existingContent, 'utf-8'); + } else { + writeFileSync(filePath, wrappedContent + '\n', 'utf-8'); + } +} + +/** + * 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>(configFilePath, {}); + + if (!existingConfig[serversKeyName]) { + existingConfig[serversKeyName] = {}; + } + + existingConfig[serversKeyName]['claude-mem'] = buildMcpServerEntry(mcpServerPath); + + writeFileSync(configFilePath, JSON.stringify(existingConfig, null, 2) + '\n'); +} + +// ============================================================================ +// Copilot CLI +// ============================================================================ + +/** + * Get the Copilot CLI MCP config path. + * Copilot CLI uses ~/.github/copilot/mcp.json for user-level MCP config. + */ +function getCopilotCliMcpConfigPath(): string { + return path.join(homedir(), '.github', 'copilot', 'mcp.json'); +} + +/** + * Get the Copilot CLI context injection path for the current workspace. + * Copilot reads instructions from .github/copilot-instructions.md in the workspace. + */ +function getCopilotCliContextPath(): string { + return path.join(process.cwd(), '.github', 'copilot-instructions.md'); +} + +/** + * Install claude-mem MCP integration for Copilot CLI. + * + * - Writes MCP config to ~/.github/copilot/mcp.json + * - Injects context into .github/copilot-instructions.md in the workspace + * + * @returns 0 on success, 1 on failure + */ +export async function installCopilotCliMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Copilot CLI...\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 — Copilot CLI uses { "servers": { ... } } format + const configPath = getCopilotCliMcpConfigPath(); + writeMcpJsonConfig(configPath, mcpServerPath, 'servers'); + console.log(` MCP config written to: ${configPath}`); + + // Inject context into workspace instructions + const contextPath = getCopilotCliContextPath(); + injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); + console.log(` Context placeholder written to: ${contextPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} +Context: ${contextPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Copilot CLI. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Copilot CLI to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Antigravity +// ============================================================================ + +/** + * Get the Antigravity MCP config path. + * Antigravity stores MCP config at ~/.gemini/antigravity/mcp_config.json. + */ +function getAntigravityMcpConfigPath(): string { + return path.join(homedir(), '.gemini', 'antigravity', 'mcp_config.json'); +} + +/** + * Get the Antigravity context injection path for the current workspace. + * Antigravity reads agent rules from .agent/rules/ in the workspace. + */ +function getAntigravityContextPath(): string { + return path.join(process.cwd(), '.agent', 'rules', 'claude-mem-context.md'); +} + +/** + * Install claude-mem MCP integration for Antigravity. + * + * - Writes MCP config to ~/.gemini/antigravity/mcp_config.json + * - Injects context into .agent/rules/claude-mem-context.md in the workspace + * + * @returns 0 on success, 1 on failure + */ +export async function installAntigravityMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Antigravity...\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 = getAntigravityMcpConfigPath(); + writeMcpJsonConfig(configPath, mcpServerPath); + console.log(` MCP config written to: ${configPath}`); + + // Inject context into workspace rules + const contextPath = getAntigravityContextPath(); + injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); + console.log(` Context placeholder written to: ${contextPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} +Context: ${contextPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Antigravity. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Antigravity to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Goose +// ============================================================================ + +/** + * 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: node', + ' 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: node', + ' 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 { + 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; + } +} + +// ============================================================================ +// Crush +// ============================================================================ + +/** + * Get the Crush MCP config path. + * Crush stores MCP config at ~/.config/crush/mcp.json. + */ +function getCrushMcpConfigPath(): string { + return path.join(homedir(), '.config', 'crush', 'mcp.json'); +} + +/** + * Install claude-mem MCP integration for Crush. + * + * - Writes MCP config to ~/.config/crush/mcp.json + * + * @returns 0 on success, 1 on failure + */ +export async function installCrushMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Crush...\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 = getCrushMcpConfigPath(); + writeMcpJsonConfig(configPath, mcpServerPath); + console.log(` MCP config written to: ${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 Crush. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Crush to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Roo Code +// ============================================================================ + +/** + * Get the Roo Code MCP config path for the current workspace. + * Roo Code reads MCP config from .roo/mcp.json in the workspace. + */ +function getRooCodeMcpConfigPath(): string { + return path.join(process.cwd(), '.roo', 'mcp.json'); +} + +/** + * Get the Roo Code context injection path for the current workspace. + * Roo Code reads rules from .roo/rules/ in the workspace. + */ +function getRooCodeContextPath(): string { + return path.join(process.cwd(), '.roo', 'rules', 'claude-mem-context.md'); +} + +/** + * Install claude-mem MCP integration for Roo Code. + * + * - Writes MCP config to .roo/mcp.json in the workspace + * - Injects context into .roo/rules/claude-mem-context.md in the workspace + * + * @returns 0 on success, 1 on failure + */ +export async function installRooCodeMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Roo Code...\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 to workspace + const configPath = getRooCodeMcpConfigPath(); + writeMcpJsonConfig(configPath, mcpServerPath); + console.log(` MCP config written to: ${configPath}`); + + // Inject context into workspace rules + const contextPath = getRooCodeContextPath(); + injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); + console.log(` Context placeholder written to: ${contextPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} +Context: ${contextPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Roo Code. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Roo Code to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Warp +// ============================================================================ + +/** + * Get the Warp context injection path for the current workspace. + * Warp reads project-level instructions from WARP.md in the project root. + */ +function getWarpContextPath(): string { + return path.join(process.cwd(), 'WARP.md'); +} + +/** + * Get the Warp MCP config path. + * Warp stores MCP config at ~/.warp/mcp.json when supported. + */ +function getWarpMcpConfigPath(): string { + return path.join(homedir(), '.warp', 'mcp.json'); +} + +/** + * Install claude-mem MCP integration for Warp. + * + * - Writes MCP config to ~/.warp/mcp.json + * - Injects context into WARP.md in the project root + * + * @returns 0 on success, 1 on failure + */ +export async function installWarpMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Warp...\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 — Warp may also support configuring MCP via Warp Drive UI + const configPath = getWarpMcpConfigPath(); + if (existsSync(path.dirname(configPath))) { + writeMcpJsonConfig(configPath, mcpServerPath); + console.log(` MCP config written to: ${configPath}`); + } else { + console.log(` Note: ~/.warp/ not found. MCP may need to be configured via Warp Drive UI.`); + } + + // Inject context into project-level WARP.md + const contextPath = getWarpContextPath(); + injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); + console.log(` Context placeholder written to: ${contextPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} +Context: ${contextPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Warp. +If MCP config via file is not supported, configure MCP through Warp Drive UI. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Warp 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 Promise> = { + 'copilot-cli': installCopilotCliMcpIntegration, + 'antigravity': installAntigravityMcpIntegration, + 'goose': installGooseMcpIntegration, + 'crush': installCrushMcpIntegration, + 'roo-code': installRooCodeMcpIntegration, + 'warp': installWarpMcpIntegration, +}; diff --git a/src/services/integrations/OpenClawInstaller.ts b/src/services/integrations/OpenClawInstaller.ts new file mode 100644 index 00000000..0867f794 --- /dev/null +++ b/src/services/integrations/OpenClawInstaller.ts @@ -0,0 +1,430 @@ +/** + * OpenClawInstaller - OpenClaw gateway integration installer for claude-mem + * + * Installs the pre-built claude-mem plugin into OpenClaw's extension directory + * and registers it in ~/.openclaw/openclaw.json. + * + * Install strategy: File-based + * - Copies the pre-built plugin from the npm package's openclaw/dist/ directory + * to ~/.openclaw/extensions/claude-mem/dist/ + * - Registers the plugin in openclaw.json under plugins.entries.claude-mem + * - Sets the memory slot to claude-mem + * + * Important: The OpenClaw plugin ships pre-built from the npm package. + * It must NOT be rebuilt at install time. + */ + +import path from 'path'; +import { homedir } from 'os'; +import { + existsSync, + readFileSync, + writeFileSync, + mkdirSync, + cpSync, + rmSync, + unlinkSync, +} from 'fs'; +import { logger } from '../../utils/logger.js'; + +// ============================================================================ +// Path Resolution +// ============================================================================ + +/** + * Resolve the OpenClaw config directory (~/.openclaw). + */ +export function getOpenClawConfigDirectory(): string { + return path.join(homedir(), '.openclaw'); +} + +/** + * Resolve the OpenClaw extensions directory where plugins are installed. + */ +export function getOpenClawExtensionsDirectory(): string { + return path.join(getOpenClawConfigDirectory(), 'extensions'); +} + +/** + * Resolve the claude-mem extension install directory. + */ +export function getOpenClawClaudeMemExtensionDirectory(): string { + return path.join(getOpenClawExtensionsDirectory(), 'claude-mem'); +} + +/** + * Resolve the path to openclaw.json config file. + */ +export function getOpenClawConfigFilePath(): string { + return path.join(getOpenClawConfigDirectory(), 'openclaw.json'); +} + +// ============================================================================ +// Pre-built Plugin Location +// ============================================================================ + +/** + * Find the pre-built OpenClaw plugin bundle in the npm package. + * Searches in: openclaw/dist/index.js relative to package root, + * then the marketplace install location. + */ +export function findPreBuiltPluginDirectory(): string | null { + const possibleRoots = [ + // Marketplace install location (production — after `npx claude-mem install`) + path.join( + process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), '.claude'), + 'plugins', 'marketplaces', 'thedotmack', + ), + // Development location (relative to project root) + process.cwd(), + ]; + + for (const root of possibleRoots) { + const openclawDistDirectory = path.join(root, 'openclaw', 'dist'); + const pluginEntryPoint = path.join(openclawDistDirectory, 'index.js'); + if (existsSync(pluginEntryPoint)) { + return openclawDistDirectory; + } + } + + return null; +} + +/** + * Find the openclaw.plugin.json file for copying alongside the plugin. + */ +export function findPluginManifestPath(): string | null { + const possibleRoots = [ + path.join( + process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), '.claude'), + 'plugins', 'marketplaces', 'thedotmack', + ), + process.cwd(), + ]; + + for (const root of possibleRoots) { + const manifestPath = path.join(root, 'openclaw', 'openclaw.plugin.json'); + if (existsSync(manifestPath)) { + return manifestPath; + } + } + + return null; +} + +/** + * Find the openclaw skills directory for copying alongside the plugin. + */ +export function findPluginSkillsDirectory(): string | null { + const possibleRoots = [ + path.join( + process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), '.claude'), + 'plugins', 'marketplaces', 'thedotmack', + ), + process.cwd(), + ]; + + for (const root of possibleRoots) { + const skillsDirectory = path.join(root, 'openclaw', 'skills'); + if (existsSync(skillsDirectory)) { + return skillsDirectory; + } + } + + return null; +} + +// ============================================================================ +// OpenClaw Config (openclaw.json) Management +// ============================================================================ + +/** + * Read openclaw.json safely, returning an empty object if missing or invalid. + */ +function readOpenClawConfig(): Record { + const configFilePath = getOpenClawConfigFilePath(); + if (!existsSync(configFilePath)) return {}; + try { + return JSON.parse(readFileSync(configFilePath, 'utf-8')); + } catch { + return {}; + } +} + +/** + * Write openclaw.json atomically, creating the directory if needed. + */ +function writeOpenClawConfig(config: Record): void { + const configDirectory = getOpenClawConfigDirectory(); + mkdirSync(configDirectory, { recursive: true }); + writeFileSync(getOpenClawConfigFilePath(), JSON.stringify(config, null, 2) + '\n', 'utf-8'); +} + +/** + * Register claude-mem in openclaw.json by merging into the existing config. + * Does NOT overwrite the entire file -- only touches the claude-mem entry + * and the memory slot. + */ +function registerPluginInOpenClawConfig( + workerPort: number = 37777, + project: string = 'openclaw', + syncMemoryFile: boolean = true, +): void { + const config = readOpenClawConfig(); + + // Ensure the plugins structure exists + if (!config.plugins) config.plugins = {}; + if (!config.plugins.slots) config.plugins.slots = {}; + if (!config.plugins.entries) config.plugins.entries = {}; + + // Set the memory slot to claude-mem + config.plugins.slots.memory = 'claude-mem'; + + // Create or update the claude-mem plugin entry + if (!config.plugins.entries['claude-mem']) { + config.plugins.entries['claude-mem'] = { + enabled: true, + config: { + workerPort, + project, + syncMemoryFile, + }, + }; + } else { + // Merge: enable and update config without losing existing user settings + config.plugins.entries['claude-mem'].enabled = true; + if (!config.plugins.entries['claude-mem'].config) { + config.plugins.entries['claude-mem'].config = {}; + } + const existingPluginConfig = config.plugins.entries['claude-mem'].config; + // Only set defaults if not already configured + if (existingPluginConfig.workerPort === undefined) existingPluginConfig.workerPort = workerPort; + if (existingPluginConfig.project === undefined) existingPluginConfig.project = project; + if (existingPluginConfig.syncMemoryFile === undefined) existingPluginConfig.syncMemoryFile = syncMemoryFile; + } + + writeOpenClawConfig(config); +} + +/** + * Remove claude-mem from openclaw.json without deleting other config. + */ +function unregisterPluginFromOpenClawConfig(): void { + const configFilePath = getOpenClawConfigFilePath(); + if (!existsSync(configFilePath)) return; + + const config = readOpenClawConfig(); + + // Remove claude-mem entry + if (config.plugins?.entries?.['claude-mem']) { + delete config.plugins.entries['claude-mem']; + } + + // Clear memory slot if it points to claude-mem + if (config.plugins?.slots?.memory === 'claude-mem') { + delete config.plugins.slots.memory; + } + + writeOpenClawConfig(config); +} + +// ============================================================================ +// Plugin Installation +// ============================================================================ + +/** + * Install the claude-mem plugin into OpenClaw's extensions directory. + * Copies the pre-built plugin bundle and registers it in openclaw.json. + * + * @returns 0 on success, 1 on failure + */ +export function installOpenClawPlugin(): number { + const preBuiltDistDirectory = findPreBuiltPluginDirectory(); + if (!preBuiltDistDirectory) { + console.error('Could not find pre-built OpenClaw plugin bundle.'); + console.error(' Expected at: openclaw/dist/index.js'); + console.error(' Ensure the npm package includes the openclaw directory.'); + return 1; + } + + const extensionDirectory = getOpenClawClaudeMemExtensionDirectory(); + const destinationDistDirectory = path.join(extensionDirectory, 'dist'); + + try { + // Create the extension directory structure + mkdirSync(destinationDistDirectory, { recursive: true }); + + // Copy pre-built dist files + cpSync(preBuiltDistDirectory, destinationDistDirectory, { recursive: true, force: true }); + console.log(` Plugin dist copied to: ${destinationDistDirectory}`); + + // Copy openclaw.plugin.json if available + const manifestPath = findPluginManifestPath(); + if (manifestPath) { + const destinationManifest = path.join(extensionDirectory, 'openclaw.plugin.json'); + cpSync(manifestPath, destinationManifest, { force: true }); + console.log(` Plugin manifest copied to: ${destinationManifest}`); + } + + // Copy skills directory if available + const skillsDirectory = findPluginSkillsDirectory(); + if (skillsDirectory) { + const destinationSkills = path.join(extensionDirectory, 'skills'); + cpSync(skillsDirectory, destinationSkills, { recursive: true, force: true }); + console.log(` Skills copied to: ${destinationSkills}`); + } + + // Create a minimal package.json for the extension (OpenClaw expects this) + const extensionPackageJson = { + name: 'claude-mem', + version: '1.0.0', + type: 'module', + main: 'dist/index.js', + openclaw: { extensions: ['./dist/index.js'] }, + }; + writeFileSync( + path.join(extensionDirectory, 'package.json'), + JSON.stringify(extensionPackageJson, null, 2) + '\n', + 'utf-8', + ); + + // Register in openclaw.json (merge, not overwrite) + registerPluginInOpenClawConfig(); + console.log(` Registered in openclaw.json`); + + logger.info('OPENCLAW', 'Plugin installed', { destination: extensionDirectory }); + return 0; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Failed to install OpenClaw plugin: ${message}`); + return 1; + } +} + +// ============================================================================ +// Uninstallation +// ============================================================================ + +/** + * Remove the claude-mem plugin from OpenClaw. + * Removes extension files and unregisters from openclaw.json. + * + * @returns 0 on success, 1 on failure + */ +export function uninstallOpenClawPlugin(): number { + let hasErrors = false; + + // Remove extension directory + const extensionDirectory = getOpenClawClaudeMemExtensionDirectory(); + if (existsSync(extensionDirectory)) { + try { + rmSync(extensionDirectory, { recursive: true, force: true }); + console.log(` Removed extension: ${extensionDirectory}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(` Failed to remove extension directory: ${message}`); + hasErrors = true; + } + } + + // Unregister from openclaw.json + try { + unregisterPluginFromOpenClawConfig(); + console.log(` Unregistered from openclaw.json`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(` Failed to update openclaw.json: ${message}`); + hasErrors = true; + } + + return hasErrors ? 1 : 0; +} + +// ============================================================================ +// Status Check +// ============================================================================ + +/** + * Check OpenClaw integration status. + * + * @returns 0 always (informational only) + */ +export function checkOpenClawStatus(): number { + console.log('\nClaude-Mem OpenClaw Integration Status\n'); + + const configDirectory = getOpenClawConfigDirectory(); + const extensionDirectory = getOpenClawClaudeMemExtensionDirectory(); + const configFilePath = getOpenClawConfigFilePath(); + const pluginEntryPoint = path.join(extensionDirectory, 'dist', 'index.js'); + + console.log(`Config directory: ${configDirectory}`); + console.log(` Exists: ${existsSync(configDirectory) ? 'yes' : 'no'}`); + console.log(''); + + console.log(`Extension directory: ${extensionDirectory}`); + console.log(` Exists: ${existsSync(extensionDirectory) ? 'yes' : 'no'}`); + console.log(` Plugin entry: ${existsSync(pluginEntryPoint) ? 'yes' : 'no'}`); + console.log(''); + + console.log(`Config (openclaw.json): ${configFilePath}`); + if (existsSync(configFilePath)) { + const config = readOpenClawConfig(); + const isRegistered = config.plugins?.entries?.['claude-mem'] !== undefined; + const isEnabled = config.plugins?.entries?.['claude-mem']?.enabled === true; + const isMemorySlot = config.plugins?.slots?.memory === 'claude-mem'; + + console.log(` Exists: yes`); + console.log(` Registered: ${isRegistered ? 'yes' : 'no'}`); + console.log(` Enabled: ${isEnabled ? 'yes' : 'no'}`); + console.log(` Memory slot: ${isMemorySlot ? 'yes' : 'no'}`); + + if (isRegistered) { + const pluginConfig = config.plugins.entries['claude-mem'].config; + if (pluginConfig) { + console.log(` Worker port: ${pluginConfig.workerPort ?? 'default'}`); + console.log(` Project: ${pluginConfig.project ?? 'default'}`); + console.log(` Sync MEMORY.md: ${pluginConfig.syncMemoryFile ?? 'default'}`); + } + } + } else { + console.log(` Exists: no`); + } + + console.log(''); + return 0; +} + +// ============================================================================ +// Full Install Flow (used by npx install command) +// ============================================================================ + +/** + * Run the full OpenClaw installation: copy plugin + register in config. + * + * @returns 0 on success, 1 on failure + */ +export async function installOpenClawIntegration(): Promise { + console.log('\nInstalling Claude-Mem for OpenClaw...\n'); + + // Step 1: Install plugin files and register in config + const pluginResult = installOpenClawPlugin(); + if (pluginResult !== 0) { + return pluginResult; + } + + const extensionDirectory = getOpenClawClaudeMemExtensionDirectory(); + + console.log(` +Installation complete! + +Plugin installed to: ${extensionDirectory} +Config updated: ${getOpenClawConfigFilePath()} + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart OpenClaw to load the plugin + 3. Memory capture is automatic from then on +`); + + return 0; +} diff --git a/src/services/integrations/index.ts b/src/services/integrations/index.ts index 67676836..a985957d 100644 --- a/src/services/integrations/index.ts +++ b/src/services/integrations/index.ts @@ -7,3 +7,6 @@ export * from './CursorHooksInstaller.js'; export * from './GeminiCliHooksInstaller.js'; export * from './OpenCodeInstaller.js'; export * from './WindsurfHooksInstaller.js'; +export * from './OpenClawInstaller.js'; +export * from './CodexCliInstaller.js'; +export * from './McpIntegrations.js'; diff --git a/src/services/transcripts/config.ts b/src/services/transcripts/config.ts index 7390e7ff..78c8e82d 100644 --- a/src/services/transcripts/config.ts +++ b/src/services/transcripts/config.ts @@ -8,7 +8,7 @@ export const DEFAULT_STATE_PATH = join(homedir(), '.claude-mem', 'transcript-wat const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { name: 'codex', - version: '0.2', + version: '0.3', description: 'Schema for Codex session JSONL files under ~/.codex/sessions.', events: [ { @@ -46,13 +46,14 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { }, { name: 'tool-use', - match: { path: 'payload.type', in: ['function_call', 'custom_tool_call', 'web_search_call'] }, + match: { path: 'payload.type', in: ['function_call', 'custom_tool_call', 'web_search_call', 'exec_command'] }, action: 'tool_use', fields: { toolId: 'payload.call_id', toolName: { coalesce: [ 'payload.name', + 'payload.type', { value: 'web_search' } ] }, @@ -60,6 +61,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { coalesce: [ 'payload.arguments', 'payload.input', + 'payload.command', 'payload.action' ] } @@ -67,7 +69,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { }, { name: 'tool-result', - match: { path: 'payload.type', in: ['function_call_output', 'custom_tool_call_output'] }, + match: { path: 'payload.type', in: ['function_call_output', 'custom_tool_call_output', 'exec_command_output'] }, action: 'tool_result', fields: { toolId: 'payload.call_id', @@ -76,7 +78,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { }, { name: 'session-end', - match: { path: 'payload.type', equals: 'turn_aborted' }, + match: { path: 'payload.type', in: ['turn_aborted', 'turn_completed'] }, action: 'session_end' } ]