mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
feat: add Codex CLI, OpenClaw, and MCP-based IDE integrations
Codex CLI: transcript-based integration watching ~/.codex/sessions/, schema bumped to v0.3 with exec_command support, AGENTS.md context. OpenClaw: installer wires pre-built plugin to ~/.openclaw/extensions/, registers in openclaw.json with memory slot and sync config. MCP integrations (6 IDEs): Copilot CLI, Antigravity, Goose, Crush, Roo Code, and Warp — config writing + context injection. Goose uses string-based YAML manipulation (no parser dependency). All 13 IDE targets now supported in npx claude-mem install. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,8 +96,8 @@ export function detectInstalledIDEs(): IDEInfo[] {
|
|||||||
id: 'openclaw',
|
id: 'openclaw',
|
||||||
label: 'OpenClaw',
|
label: 'OpenClaw',
|
||||||
detected: existsSync(join(home, '.openclaw')),
|
detected: existsSync(join(home, '.openclaw')),
|
||||||
supported: false,
|
supported: true,
|
||||||
hint: 'coming soon',
|
hint: 'plugin-based integration',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'windsurf',
|
id: 'windsurf',
|
||||||
@@ -109,8 +109,8 @@ export function detectInstalledIDEs(): IDEInfo[] {
|
|||||||
id: 'codex-cli',
|
id: 'codex-cli',
|
||||||
label: 'Codex CLI',
|
label: 'Codex CLI',
|
||||||
detected: existsSync(join(home, '.codex')),
|
detected: existsSync(join(home, '.codex')),
|
||||||
supported: false,
|
supported: true,
|
||||||
hint: 'coming soon',
|
hint: 'transcript-based integration',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cursor',
|
id: 'cursor',
|
||||||
@@ -122,44 +122,44 @@ export function detectInstalledIDEs(): IDEInfo[] {
|
|||||||
id: 'copilot-cli',
|
id: 'copilot-cli',
|
||||||
label: 'Copilot CLI',
|
label: 'Copilot CLI',
|
||||||
detected: isCommandInPath('copilot'),
|
detected: isCommandInPath('copilot'),
|
||||||
supported: false,
|
supported: true,
|
||||||
hint: 'coming soon',
|
hint: 'MCP-based integration',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'antigravity',
|
id: 'antigravity',
|
||||||
label: 'Antigravity',
|
label: 'Antigravity',
|
||||||
detected: existsSync(join(home, '.gemini', 'antigravity')),
|
detected: existsSync(join(home, '.gemini', 'antigravity')),
|
||||||
supported: false,
|
supported: true,
|
||||||
hint: 'coming soon',
|
hint: 'MCP-based integration',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'goose',
|
id: 'goose',
|
||||||
label: 'Goose',
|
label: 'Goose',
|
||||||
detected:
|
detected:
|
||||||
existsSync(join(home, '.config', 'goose')) || isCommandInPath('goose'),
|
existsSync(join(home, '.config', 'goose')) || isCommandInPath('goose'),
|
||||||
supported: false,
|
supported: true,
|
||||||
hint: 'coming soon',
|
hint: 'MCP-based integration',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'crush',
|
id: 'crush',
|
||||||
label: 'Crush',
|
label: 'Crush',
|
||||||
detected: isCommandInPath('crush'),
|
detected: isCommandInPath('crush'),
|
||||||
supported: false,
|
supported: true,
|
||||||
hint: 'coming soon',
|
hint: 'MCP-based integration',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'roo-code',
|
id: 'roo-code',
|
||||||
label: 'Roo Code',
|
label: 'Roo Code',
|
||||||
detected: hasVscodeExtension('roo-code'),
|
detected: hasVscodeExtension('roo-code'),
|
||||||
supported: false,
|
supported: true,
|
||||||
hint: 'coming soon',
|
hint: 'MCP-based integration',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'warp',
|
id: 'warp',
|
||||||
label: 'Warp',
|
label: 'Warp',
|
||||||
detected: existsSync(join(home, '.warp')) || isCommandInPath('warp'),
|
detected: existsSync(join(home, '.warp')) || isCommandInPath('warp'),
|
||||||
supported: false,
|
supported: true,
|
||||||
hint: 'coming soon',
|
hint: 'MCP-based integration',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,50 @@ async function setupIDEs(selectedIDEs: string[]): Promise<void> {
|
|||||||
break;
|
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: {
|
default: {
|
||||||
const allIDEs = detectInstalledIDEs();
|
const allIDEs = detectInstalledIDEs();
|
||||||
const ide = allIDEs.find((i) => i.id === ideId);
|
const ide = allIDEs.find((i) => i.id === ideId);
|
||||||
|
|||||||
373
src/services/integrations/CodexCliInstaller.ts
Normal file
373
src/services/integrations/CodexCliInstaller.ts
Normal file
@@ -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 <claude-mem-context> 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',
|
||||||
|
'',
|
||||||
|
'<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->',
|
||||||
|
'',
|
||||||
|
'*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 <claude-mem-context> tags.
|
||||||
|
*/
|
||||||
|
function removeCodexAgentsMdContext(): void {
|
||||||
|
try {
|
||||||
|
if (!existsSync(CODEX_AGENTS_MD_PATH)) return;
|
||||||
|
|
||||||
|
const content = readFileSync(CODEX_AGENTS_MD_PATH, 'utf-8');
|
||||||
|
const startTag = '<claude-mem-context>';
|
||||||
|
const endTag = '</claude-mem-context>';
|
||||||
|
|
||||||
|
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<number> {
|
||||||
|
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('<claude-mem-context>')) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
610
src/services/integrations/McpIntegrations.ts
Normal file
610
src/services/integrations/McpIntegrations.ts
Normal file
@@ -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 = '<claude-mem-context>';
|
||||||
|
const CONTEXT_TAG_CLOSE = '</claude-mem-context>';
|
||||||
|
|
||||||
|
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<T>(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 <claude-mem-context> 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<Record<string, any>>(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<number> {
|
||||||
|
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<number> {
|
||||||
|
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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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<number> {
|
||||||
|
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<number> {
|
||||||
|
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<number> {
|
||||||
|
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<string, () => Promise<number>> = {
|
||||||
|
'copilot-cli': installCopilotCliMcpIntegration,
|
||||||
|
'antigravity': installAntigravityMcpIntegration,
|
||||||
|
'goose': installGooseMcpIntegration,
|
||||||
|
'crush': installCrushMcpIntegration,
|
||||||
|
'roo-code': installRooCodeMcpIntegration,
|
||||||
|
'warp': installWarpMcpIntegration,
|
||||||
|
};
|
||||||
430
src/services/integrations/OpenClawInstaller.ts
Normal file
430
src/services/integrations/OpenClawInstaller.ts
Normal file
@@ -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<string, any> {
|
||||||
|
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<string, any>): 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<number> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -7,3 +7,6 @@ export * from './CursorHooksInstaller.js';
|
|||||||
export * from './GeminiCliHooksInstaller.js';
|
export * from './GeminiCliHooksInstaller.js';
|
||||||
export * from './OpenCodeInstaller.js';
|
export * from './OpenCodeInstaller.js';
|
||||||
export * from './WindsurfHooksInstaller.js';
|
export * from './WindsurfHooksInstaller.js';
|
||||||
|
export * from './OpenClawInstaller.js';
|
||||||
|
export * from './CodexCliInstaller.js';
|
||||||
|
export * from './McpIntegrations.js';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const DEFAULT_STATE_PATH = join(homedir(), '.claude-mem', 'transcript-wat
|
|||||||
|
|
||||||
const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
|
const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
|
||||||
name: 'codex',
|
name: 'codex',
|
||||||
version: '0.2',
|
version: '0.3',
|
||||||
description: 'Schema for Codex session JSONL files under ~/.codex/sessions.',
|
description: 'Schema for Codex session JSONL files under ~/.codex/sessions.',
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
@@ -46,13 +46,14 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tool-use',
|
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',
|
action: 'tool_use',
|
||||||
fields: {
|
fields: {
|
||||||
toolId: 'payload.call_id',
|
toolId: 'payload.call_id',
|
||||||
toolName: {
|
toolName: {
|
||||||
coalesce: [
|
coalesce: [
|
||||||
'payload.name',
|
'payload.name',
|
||||||
|
'payload.type',
|
||||||
{ value: 'web_search' }
|
{ value: 'web_search' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -60,6 +61,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
|
|||||||
coalesce: [
|
coalesce: [
|
||||||
'payload.arguments',
|
'payload.arguments',
|
||||||
'payload.input',
|
'payload.input',
|
||||||
|
'payload.command',
|
||||||
'payload.action'
|
'payload.action'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -67,7 +69,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tool-result',
|
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',
|
action: 'tool_result',
|
||||||
fields: {
|
fields: {
|
||||||
toolId: 'payload.call_id',
|
toolId: 'payload.call_id',
|
||||||
@@ -76,7 +78,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'session-end',
|
name: 'session-end',
|
||||||
match: { path: 'payload.type', equals: 'turn_aborted' },
|
match: { path: 'payload.type', in: ['turn_aborted', 'turn_completed'] },
|
||||||
action: 'session_end'
|
action: 'session_end'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user