diff --git a/API.md b/API.md index d44f730..ee50ba1 100644 --- a/API.md +++ b/API.md @@ -15,6 +15,40 @@ code --install-extension your-publisher.agentmemory --- +## Agent Configuration API + +### `configureAgents(agents: string[])` + +Select which AI coding agents should receive memory bank sync. Only selected agents will have memory bank directories created and files synced. + +**Example:** +```typescript +// Select KiloCode and OpenCode only +await api.configureAgents(['kilocode', 'opencode']); + +// Get current selection +const current = await api.getActiveAgents(); +console.log('Active agents:', current); // ['kilocode', 'opencode'] +``` + +| Agent Key | Full Name | Type | +|-----------|-----------|------| +| `kilocode` | KiloCode | VS Code Extension | +| `cline` | Cline | VS Code Extension | +| `roocode` | RooCode | VS Code Extension | +| `opencode` | OpenCode | Terminal TUI | + +Configuration is persisted in `.agentMemory/agents.json`: +```json +{ + "selectedAgents": ["kilocode", "opencode"], + "createdAt": "2025-01-15T10:00:00Z", + "updatedAt": "2025-01-15T10:00:00Z" +} +``` + +--- + ## Getting the API ```typescript @@ -218,7 +252,7 @@ unsubscribe(); Get statistics about memory usage. -**Returns:** `Promise` - Statistics object with memory counts, cache info, etc. +**Returns:** `Promise` - Statistics object with memory counts, cache info, agent sync status, etc. **Example:** ```typescript @@ -227,6 +261,8 @@ const stats = await api.getStats(); console.log(`Total memories: ${stats.totalMemories}`); console.log(`By type:`, stats.byType); console.log(`Cache size: ${stats.cache.size}`); +console.log(`Synced agents:`, stats.sync.agents); +console.log(`Config exists:`, stats.sync.configExists); ``` --- @@ -280,6 +316,12 @@ interface MemoryEvent { agent: string; timestamp: number; } + +interface AgentConfigData { + selectedAgents: string[]; + createdAt: string; + updatedAt: string; +} ``` --- @@ -299,6 +341,9 @@ export async function activate(context: vscode.ExtensionContext) { const memoryAPI = await agentMemoryExt.activate(); + // Configure active agents (only KiloCode and OpenCode) + await memoryAPI.configureAgents(['kilocode', 'opencode']); + // Example: Store architecture decision const storeDecision = vscode.commands.registerCommand('myext.storeDecision', async () => { const key = await vscode.window.showInputBox({ prompt: 'Decision key' }); @@ -364,6 +409,9 @@ Query memories to help new developers understand the codebase. ### 5. Testing Framework Store test patterns and retrieve them when generating new tests. +### 6. Agent Configuration Manager +Programmatically switch which agents sync with the memory bank based on team preferences. + --- ## Best Practices @@ -374,6 +422,7 @@ Store test patterns and retrieve them when generating new tests. 4. **Set createdBy**: Identify your extension in metadata for analytics 5. **Handle Errors**: Always check for null returns from `read()` and `update()` 6. **Unsubscribe**: Clean up event subscriptions when no longer needed +7. **Configure Agents Early**: Call `configureAgents()` during extension activation to set up the right agents for your workspace --- diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6b7042d..e906373 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,7 @@ ## System Overview -agentMemory is a **hybrid memory system** that enhances the built-in memory banks of KiloCode, Cline, and RooCode with powerful search, analytics, and automation capabilities while maintaining full compatibility with their markdown-based documentation. +agentMemory is a **hybrid memory system** that enhances the built-in memory banks of KiloCode, Cline, RooCode, and OpenCode with powerful search, analytics, and automation capabilities while maintaining full compatibility with their markdown-based documentation. ## Design Philosophy @@ -14,8 +14,30 @@ Rather than replacing existing memory bank systems, agentMemory **enhances** the 2. **Syncing** bi-directionally to keep both systems in harmony 3. **Adding** capabilities they lack (search, analytics, automation) 4. **Maintaining** git-friendly, human-readable files +5. **Letting the user choose** which agents to sync with, preventing directory clutter -### Why Hybrid? +### Why Agent Selection? + +Previously, agentMemory automatically configured **all** agents (KiloCode, Cline, RooCode, OpenCode) regardless of which ones the user actually uses. This created unnecessary files and directories in projects, causing confusion and clutter. + +**Solution:** The user explicitly selects which agents to sync with. This selection is persisted in `.agentMemory/agents.json` and respected by: +- The VS Code Extension setup wizard +- The CLI server startup (`--agents=...` flag) +- The `configure_agents` MCP tool +- The bi-directional sync engine + +### Agent Configuration Storage + +``` +.agentMemory/agents.json +{ + "selectedAgents": ["kilocode", "opencode"], + "createdAt": "2025-01-15T10:00:00Z", + "updatedAt": "2025-01-15T12:30:00Z" +} +``` + +This file is the **single source of truth** for agent selection. All components read from it. **Their Systems (Markdown)** - ✅ Human-readable @@ -125,9 +147,13 @@ Rather than replacing existing memory bank systems, agentMemory **enhances** the - Bi-directional sync engine - Parses markdown files from agents' memory banks - Exports MCP memories to markdown format -- Supports KiloCode, Cline, and RooCode +- Supports KiloCode, Cline, RooCode, and OpenCode +- **Respects agent selection** from `.agentMemory/agents.json` +- **Only syncs to selected agents**, prevents directory clutter -**File Mapping:** +**Agent Configuration:** +All agent definitions (name, memory bank path, file mapping) live in `src/mcp-server/agent-config.ts` (`ALL_AGENTS`). +The sync engine reads `agents.json` to determine which agents to sync with. ```typescript KiloCode: .kilocode/rules/memory-bank/ brief.md → architecture @@ -152,22 +178,47 @@ RooCode: .roo/memory-bank/ techContext.md → decision progress.md → feature decisionLog.md → decision + +OpenCode: .opencode/memory-bank/ + architecture.md → architecture + patterns.md → pattern + decisions.md → decision + features.md → feature + +OpenCode also syncs to AGENTS.md (project root) for session-start context. ``` ### 3. Configuration Management (`src/config.ts`) **Responsibilities:** - Detect installed AI coding agents +- **Prompt user to select which agents to sync with** (via `showQuickPick` multi-select) - Write MCP server config to `.vscode/settings.json` +- Persist agent selection to `.agentMemory/agents.json` - Configure socket paths for each project +**Agent Selection Flow:** +``` +1. Detect installed extensions (KiloCode, Cline, RooCode) +2. Detect OpenCode by file system (opencode.json, .opencode/) +3. Show QuickPick with all agents, pre-selecting installed ones +4. User picks which agents to sync with +5. Save selection to .agentMemory/agents.json +6. Only configure MCP settings for selected agents +``` + **Agent Detection:** ```typescript detectInstalledAgents(): string[] { - // Checks for: + // Checks for VS Code extensions: // - saoudrizwan.claude-dev (Cline) // - kilocode.kilo-code (KiloCode) - // - rooveterinaryinc.roo-cline (RooCode) + // - roo-code.roo-code (RooCode) + // - Continue.continue (Continue) + + // Also checks for OpenCode by file system: + // - opencode.json in workspace root + // - .opencode/ directory in workspace } ``` diff --git a/README.md b/README.md index ba7af4f..7e3df3a 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,39 @@ **Hybrid Memory System for AI Coding Agents** -Seamlessly integrate with KiloCode, Cline, and RooCode's built-in memory banks while providing powerful search, analytics, and automation. +Seamlessly integrate with KiloCode, Cline, RooCode, and OpenCode's built-in memory banks while providing powerful search, analytics, and automation. [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/webzler.agentmemory?label=VS%20Code%20Marketplace)](https://marketplace.visualstudio.com/) [![Installs](https://img.shields.io/visual-studio-marketplace/i/webzler.agentmemory)](https://marketplace.visualstudio.com/) --- +## 📢 Latest Release Highlights + +### v0.2.0 — Agent Selector & OpenCode Support + +**🎯 Agent Selection (New in v0.2.0)** +- You can now **choose which coding agents** to sync with instead of having all agents configured at once. +- Memory bank files are only created for the agents you actively use, keeping your project directory clean and uncluttered. +- Supports selection via: + - **VS Code Extension** — `showQuickPick` multi-select during setup + - **SKILL.md / Terminal** — Interactive readline prompt or `--agents` CLI flag + - **MCP Tool** — `configure_agents({ agents: "kilocode,opencode" })` anytime + +**🤖 OpenCode Support (Added in v0.2.0)** +- Full compatibility with the **OpenCode** terminal-based agent. +- Syncs to `.opencode/memory-bank/` and maintains `AGENTS.md` + `.opencode/commands/`. + +--- + +## 🚀 Now Available as an Antigravity Skill! + +agentMemory is now fully compatible with **Antigravity**. Use it as a skill to give your agents persistent, searchable memory that syncs with your project documentation. + +See [SKILL.md](SKILL.md) for usage instructions. + +--- + ## 🚀 Now Available as an Antigravity Skill! @@ -153,13 +179,22 @@ memory_search({ ### 🤖 Multi-Agent Support -| Agent | Memory Bank Location | Sync Status | -|-------|---------------------|-------------| -| **KiloCode** | `.kilocode/rules/memory-bank/` | ✅ Full sync | -| **Cline** | `.clinerules/memory-bank/` | ✅ Full sync | -| **RooCode** | `.roo/memory-bank/` | ✅ Full sync | +| Agent | Type | Memory Bank Location | Sync Status | +|-------|------|---------------------|-------------| +| **KiloCode** | VS Code Extension | `.kilocode/rules/memory-bank/` | ✅ Selectable | +| **Cline** | VS Code Extension | `.clinerules/memory-bank/` | ✅ Selectable | +| **RooCode** | VS Code Extension | `.roo/memory-bank/` | ✅ Selectable | +| **OpenCode** | Terminal TUI | `AGENTS.md` + `.opencode/commands/` | ✅ Selectable | + +**You choose which agents to sync.** Only selected agents receive: +- Memory bank directories +- MCP settings +- Synced markdown files +- `opencode.json` (OpenCode only) -**Files Synced:** +Use `configure_agents({ agents: "kilocode,opencode" })` to change your selection anytime. + +**Files Synced (for selected agents only):** - `projectBrief.md` / `brief.md` - `architecture.md` / `systemPatterns.md` - `productContext.md` / `product.md` @@ -180,9 +215,10 @@ memory_search({ 4. Reload VS Code **That's it!** The extension will: +- ✅ Ask which agents to sync with (via QuickPick) - ✅ Create MCP server configuration -- ✅ Inject memory-first instructions into memory banks -- ✅ Start bi-directional sync +- ✅ Inject memory-first instructions into selected memory banks +- ✅ Start bi-directional sync with **only** selected agents - ✅ Enable dashboard ### Manual Installation @@ -243,40 +279,53 @@ Agents treat this as **project architecture** and follow it automatically. | `memory_list` | List by type | Show all architecture decisions | | `memory_update` | Modify existing | Append to existing pattern | | `memory_stats` | View analytics | Usage statistics | +| `configure_agents` | Change agent selection | `configure_agents({ agents: "kilocode,opencode" })` | --- ## � Project Structure -After installation, your project will have: +After installation, your project will have (only for **selected** agents): ``` your-project/ ├── .vscode/ │ └── settings.json # MCP server config (auto-created) │ -├── .agentMemory/ # Our structured storage +├── .agentMemory/ # Our structured storage + config │ ├── uuid-001.json # Memory: OAuth architecture │ ├── uuid-002.json # Memory: API patterns -│ └── ... +│ └── agents.json # Active agent configuration ⭐ NEW +│ +├── AGENTS.md # OpenCode rules (auto-created if OpenCode selected) +│ +├── opencode.json # OpenCode MCP config (auto-created if selected) │ -├── .kilocode/rules/memory-bank/ # KiloCode memory bank +├── .opencode/ # ONLY if OpenCode is selected +│ └── commands/ # OpenCode custom commands +│ ├── memory-search.md +│ ├── memory-write.md +│ └── memory-review.md +│ +├── .kilocode/rules/memory-bank/ # ONLY if KiloCode is selected │ ├── brief.md # ⬍ Synced with our database │ ├── architecture.md # ⬍ Auto-updated │ ├── product.md # ⬍ Auto-updated │ └── tech.md # ⬍ Auto-updated │ -├── .clinerules/memory-bank/ # Cline memory bank +├── .clinerules/memory-bank/ # ONLY if Cline is selected │ ├── projectBrief.md # ⬍ Synced │ ├── systemPatterns.md # ⬍ Synced │ └── ... # ⬍ Synced │ -└── .roo/memory-bank/ # RooCode memory bank +└── .roo/memory-bank/ # ONLY if RooCode is selected ├── projectBrief.md # ⬍ Synced ├── decisionLog.md # ⬍ Synced └── ... # ⬍ Synced ``` +**Only the agents you select get directories and files created!** This keeps your project clean. + **All markdown files stay human-readable and git-friendly!** --- @@ -323,7 +372,7 @@ your-project/ | **Search** | ✅ Fast indexed | ❌ No | ✅ Yes | | **Analytics** | ✅ Dashboard | ❌ No | ❌ No | | **Automation** | ✅ Auto-sync | ❌ Manual | ⚠️ Partial | -| **Multi-Agent** | ✅ All 3 | ✅ Per-agent | ✅ All | +| **Multi-Agent** | ✅ All 4 | ✅ Per-agent | ✅ All | | **Git-Friendly** | ✅ Yes | ✅ Yes | ⚠️ Depends | | **Cross-Project** | ✅ Yes | ❌ No | ❌ No | diff --git a/SKILL.md b/SKILL.md index d9f22eb..f1b1caf 100644 --- a/SKILL.md +++ b/SKILL.md @@ -17,22 +17,138 @@ This skill extends your capabilities by providing a persistent, searchable memor ## Setup -1. **Install Dependencies**: - ```bash - npm install - ``` - -2. **Build the Project**: - ```bash - npm run compile - ``` - -3. **Start the Memory Server**: - You need to run the MCP server to interact with the memory bank. - ```bash - npm run start-server - ``` - *Note: This skill typically runs as a background process or via an mcp-server configuration. ensuring it is running is key.* +### 1. Choose Your Agents (Required!) + +Before initializing the memory bank, you MUST ask the user which AI coding agents they plan to use in this project. + +**Supported agents:** + +| # | Agent | Type | Memory Bank Path | +|---|-------|------|------------------| +| 1 | **KiloCode** | VS Code Extension | `.kilocode/rules/memory-bank/` | +| 2 | **Cline** | VS Code Extension | `.clinerules/memory-bank/` | +| 3 | **RooCode** | VS Code Extension | `.roo/memory-bank/` | +| 4 | **OpenCode** | Terminal TUI | `.opencode/memory-bank/` | + +**Ask the user:** +``` +Which AI coding agents will you use for this project? +You can select multiple: +[ ] KiloCode +[ ] Cline +[ ] RooCode +[ ] OpenCode +``` + +Then configure them using the MCP tool: +``` +configure_agents({ agents: "kilocode,opencode" }) +``` + +### 2. Build the MCP Server + +Once agents are configured, install dependencies and compile: +```bash +cd ~/.agents/skills/agent-memory && npm install && npm run compile +``` + +### 3. Start the Memory Server + +#### For VS Code Agents (Cline, RooCode, KiloCode) + +```bash +npm run start-server +``` + +#### For OpenCode (Terminal-based Agent) + +```bash +# Include the agents you configured: +node ~/.agents/skills/agent-memory/out/mcp-server/server.js --agents=kilocode,opencode +``` + +Or add to your `opencode.json` in the project root: +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "agentmemory": { + "type": "local", + "command": [ + "node", + "/Users/YOUR_USER/.agents/skills/agent-memory/out/mcp-server/server.js", + "PROJECT_ID", + "/ABSOLUTE/PATH/TO/WORKSPACE", + "--agents=kilocode,opencode" + ], + "enabled": true + } + } +} +``` +Replace `YOUR_USER`, `PROJECT_ID`, `/ABSOLUTE/PATH/TO/WORKSPACE`, and the agents list with actual values. + +### 4. Add Memory Instructions to `AGENTS.md` + +Create or update `AGENTS.md` in the project root: +```markdown +## agentMemory System (REQUIRED) + +This project uses agentMemory for persistent knowledge management. + +### Required Workflow + +**EVERY task MUST follow this sequence:** + +1. **Before ANY work:** Call `memory_search()` to check existing knowledge +2. **After ANY significant work:** Call `memory_write()` to document what was done + +### Available MCP Tools + +- `agentmemory_memory_search` — Search for memories by query, type, or tags +- `agentmemory_memory_write` — Save new memory +- `agentmemory_memory_read` — Retrieve specific memory by key +- `agentmemory_memory_list` — List memories by type +- `agentmemory_memory_update` — Update existing memory +- `agentmemory_memory_stats` — View memory statistics +- `agentmemory_configure_agents` — Change which agents to sync with + +**Failure to use memory tools = Incomplete work** +``` + +### 5. Optionally Add Custom Commands + +To `.opencode/commands/memory-search.md`: +```markdown +--- +description: Search project memories +--- +Search the agentMemory system for relevant context about: $ARGUMENTS + +Use the agentmemory_memory_search tool with query "$ARGUMENTS". +If results are found, summarize them clearly. If no results, suggest creating a new memory. +``` + +## Changing Agents Later + +If the user wants to add or remove agents after initial setup, use: +``` +configure_agents({ agents: "kilocode,roocode" }) +``` + +Or run interactively (only in TTY): +``` +configure_agents({ interactive: true }) +``` + +The configuration is stored in `.agentMemory/agents.json`: +```json +{ + "selectedAgents": ["kilocode", "opencode"], + "createdAt": "2025-01-15T10:00:00Z", + "updatedAt": "2025-01-15T10:00:00Z" +} +``` ## Capabilities (MCP Tools) @@ -54,14 +170,43 @@ Retrieve specific memory content by key. - **Usage**: "Get the auth design" -> `memory_read({ key: "auth-v1" })` ### `memory_stats` -View analytics on memory usage. +View analytics on memory usage, including which agents are active. - **Usage**: "Show memory statistics" -> `memory_stats({})` +### `configure_agents` +Switch which agents receive memory bank sync. +- **Args**: `agents` (string — comma-separated) +- **Usage**: `configure_agents({ agents: "kilocode,opencode" })` + +## Supported Agents + +| Agent | Type | Config Location | Memory Bank Path | +|-------|------|-----------------|------------------| +| KiloCode | VS Code Extension | VS Code MCP settings | `.kilocode/rules/memory-bank/` | +| Cline | VS Code Extension | VS Code MCP settings | `.clinerules/memory-bank/` | +| RooCode | VS Code Extension | VS Code MCP settings | `.roo/memory-bank/` | +| **OpenCode** | Terminal TUI | `opencode.json` | `.opencode/memory-bank/` | + ## Workflow -1. **Initialization**: The first time you run this in a project, it may attempt to import existing markdown memory banks from `.kilocode/`, `.clinerules/`, or `.roo/`. +1. **Initialization**: The first time you run this in a project, ask which agents to use and call `configure_agents()`. 2. **Development Loop**: - **Before Task**: Search memory for relevant context. - **During Task**: Use read/search to answer questions. - **After Task**: Write new findings to memory. -3. **Sync**: Your writes are automatically synced to standard markdown files in the project. +3. **Sync**: Your writes are automatically synced to standard markdown files **only for the selected agents**. + +## Why Agent Selection Matters + +Previously, agentMemory automatically created configuration and files for **all agents** (KiloCode, Cline, RooCode, OpenCode) simultaneously, which cluttered the project directory with files the user never intended to use. + +Now, only the agents you explicitly choose will have: +- Memory bank directories created +- MCP settings configured +- Memory files synced + +This keeps your project clean and focused. + +Base directory for this skill: file:///Users/maksim/.agents/skills/agent-memory +Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. +Note: file list is sampled. diff --git a/src/config.ts b/src/config.ts index f306444..5670401 100644 --- a/src/config.ts +++ b/src/config.ts @@ -22,18 +22,44 @@ export class ConfigManager { args: [serverPath, projectId, workspacePath] }; - // Detect installed agents - const installedAgents = await this.detectInstalledAgents(); - - if (installedAgents.length === 0) { - this.outputChannel.appendLine('⚠️ No AI coding agents detected. Skipping MCP configuration.'); + // --- Agent Selection Dialog --- + // Ask user which agents to sync with before writing any config files + const selectedAgents = await this.promptAgentSelection(); + if (!selectedAgents || selectedAgents.length === 0) { + this.outputChannel.appendLine('⚠️ No agents selected. Skipping MCP configuration.'); return; } - this.outputChannel.appendLine(`📡 Detected agents: ${installedAgents.join(', ')}`); + this.outputChannel.appendLine(`📡 Selected agents: ${selectedAgents.join(', ')}`); + + // Write the agent selection to .agentMemory/agents.json so the MCP server and sync engine respect it + try { + const { AgentConfig } = require('./mcp-server/agent-config'); + const agentCfg = new AgentConfig(workspacePath); + agentCfg.write(selectedAgents); + this.outputChannel.appendLine(`💾 Saved agent selection to .agentMemory/agents.json`); + } catch (err: any) { + this.outputChannel.appendLine(`⚠️ Failed to save agent selection: ${err.message}`); + } + // --- + + // Detect installed agents to know WHICH ones can be configured + const installedAgents = await this.detectInstalledAgents(); + + // Configure each SELECTED agent's settings file + for (const agent of selectedAgents) { + // OpenCode uses opencode.json (handled by InterceptorManager), skip VS Code settings + if (agent === 'opencode') { + this.outputChannel.appendLine(` ℹ️ opencode: configured via opencode.json (handled by interceptor)`); + continue; + } + + // Only configure if the agent is actually installed (has extension) + if (!installedAgents.includes(agent)) { + this.outputChannel.appendLine(` ⚠️ ${agent}: not installed, skipping MCP settings`); + continue; + } - // Configure each agent's settings file - for (const agent of installedAgents) { const settingsPath = this.getAgentSettingsPath(agent); if (!settingsPath) { this.outputChannel.appendLine(`⚠️ Unknown settings path for ${agent}`); @@ -44,6 +70,41 @@ export class ConfigManager { } } + /** + * Show a VS Code QuickPick to let the user select which agents to sync with. + * Returns the selected agent keys. + */ + private async promptAgentSelection(): Promise { + const allAgents = [ + { label: 'KiloCode', description: 'VS Code Extension', key: 'kilocode', picked: true }, + { label: 'Cline', description: 'VS Code Extension', key: 'cline', picked: true }, + { label: 'RooCode', description: 'VS Code Extension', key: 'roocode', picked: true }, + { label: 'OpenCode', description: 'Terminal TUI Agent', key: 'opencode', picked: false } + ]; + + const installed = await this.detectInstalledAgents(); + + // Default-pick agents that are actually installed + const items = allAgents.map(agent => ({ + label: `${agent.label}${installed.includes(agent.key) ? ' (installed)' : ''}`, + description: agent.description, + key: agent.key, + picked: installed.includes(agent.key) + })); + + const selected = await vscode.window.showQuickPick(items, { + canPickMany: true, + placeHolder: 'Select which AI coding agents to sync memory bank with (multi-select)', + ignoreFocusOut: true + }); + + if (!selected) { + return undefined; // User cancelled + } + + return selected.map(s => s.key); + } + /** * Get the path to an agent's MCP settings file */ @@ -117,6 +178,23 @@ export class ConfigManager { } } + // Detect OpenCode by checking for opencode.json or .opencode/ directory + const workspacePath = this.workspaceFolder.uri.fsPath; + + try { + await fs.access(path.join(workspacePath, 'opencode.json')); + installed.push('opencode'); + this.outputChannel.appendLine(' ✅ opencode: opencode.json found'); + } catch { + try { + await fs.access(path.join(workspacePath, '.opencode')); + installed.push('opencode'); + this.outputChannel.appendLine(' ✅ opencode: .opencode/ directory found'); + } catch { + // OpenCode not detected + } + } + return installed; } } diff --git a/src/interceptor.ts b/src/interceptor.ts index 9a3fa52..cf089f6 100644 --- a/src/interceptor.ts +++ b/src/interceptor.ts @@ -37,6 +37,11 @@ export class InterceptorManager { if (installedAgents.includes('continue')) { promises.push(this.injectContinueConfig(workspacePath)); } + if (installedAgents.includes('opencode')) { + promises.push(this.injectOpenCodeRules(workspacePath)); + promises.push(this.injectOpenCodeCommands(workspacePath)); + promises.push(this.injectOpenCodeMCPConfig(workspacePath)); + } await Promise.all(promises); @@ -63,6 +68,25 @@ export class InterceptorManager { } } + // Detect OpenCode by checking for opencode.json or .opencode/ directory + const workspacePath = this.workspaceFolder.uri.fsPath; + const opencodeConfigPath = path.join(workspacePath, 'opencode.json'); + const opencodeDirPath = path.join(workspacePath, '.opencode'); + + try { + await fs.access(opencodeConfigPath); + installed.push('opencode'); + this.outputChannel.appendLine(` ✅ opencode: opencode.json found`); + } catch { + try { + await fs.access(opencodeDirPath); + installed.push('opencode'); + this.outputChannel.appendLine(` ✅ opencode: .opencode/ directory found`); + } catch { + // OpenCode not detected + } + } + return installed; } @@ -392,6 +416,175 @@ This project uses agentMemory for persistent knowledge management. } } + /** + * Inject memory instructions into AGENTS.md for OpenCode. + * OpenCode reads AGENTS.md at session start for project rules. + * Keeps AGENTS.md clean — only instructions, no operational data. + */ + private async injectOpenCodeRules(workspacePath: string): Promise { + const agentsMdPath = path.join(workspacePath, 'AGENTS.md'); + const memorySection = `## agentMemory + +This project uses agentMemory for persistent knowledge management. + +### Episodic Memory + +Project decisions, patterns, and context are stored in \`.opencode/memory-context.md\`. + +**Before starting any task:** Read \`.opencode/memory-context.md\` to load recent project context. +**After significant work:** Use \`agentmemory_memory_write\` to persist new knowledge. Entries sync to the context file automatically. + +The context file is a sliding window of the 25 most recent memories (newest first). Older entries are pruned automatically — full history remains searchable via \`agentmemory_memory_search\`. + +### Available Tools (MCP server: agentmemory) + +- \`agentmemory_memory_search\` — Search all memories by query, type, or tags +- \`agentmemory_memory_write\` — Save new memory (key, type, content, tags) +- \`agentmemory_memory_read\` — Retrieve specific memory by key +- \`agentmemory_memory_list\` — List memories by type +- \`agentmemory_memory_update\` — Update existing memory +- \`agentmemory_memory_stats\` — View memory usage statistics + +`; + + try { + let existing = ''; + try { + existing = await fs.readFile(agentsMdPath, 'utf-8'); + } catch { + // File doesn't exist yet + } + + if (existing.includes('## agentMemory')) { + this.outputChannel.appendLine(` ℹ️ AGENTS.md already contains agentMemory section`); + return; + } + + if (existing.trim()) { + // Append to existing AGENTS.md + await fs.writeFile(agentsMdPath, existing.trimEnd() + '\n\n' + memorySection, 'utf-8'); + this.outputChannel.appendLine(` 📄 Updated: AGENTS.md with agentMemory rules`); + } else { + // Create new AGENTS.md + const header = `# AGENTS.md\n\nInstructions for AI coding agents working on this project.\n\n`; + await fs.writeFile(agentsMdPath, header + memorySection, 'utf-8'); + this.outputChannel.appendLine(` 📄 Created: AGENTS.md with agentMemory rules`); + } + } catch (error) { + this.outputChannel.appendLine(` ❌ Failed to create/update AGENTS.md`); + } + } + + /** + * Create OpenCode custom commands for memory operations. + * These appear as /memory-search and /memory-write in the TUI. + */ + private async injectOpenCodeCommands(workspacePath: string): Promise { + const commandsDir = path.join(workspacePath, '.opencode', 'commands'); + + const memorySearchCommand = `--- +description: Search project memories for relevant context +--- +Search the agentMemory system for context relevant to: $ARGUMENTS + +Use the \`agentmemory_memory_search\` tool with the query "$ARGUMENTS". + +If results are found: +1. Summarize each memory's key information +2. Note the memory type and tags +3. Suggest how the findings relate to the current task + +If no results are found: +- Suggest alternative search terms +- Consider if this is new knowledge that should be documented +`; + + const memoryWriteCommand = `--- +description: Save findings to project memory +--- +Document the following in the agentMemory system: $ARGUMENTS + +Use the \`agentmemory_memory_write\` tool with: +- \`key\`: A unique kebab-case identifier derived from the topic +- \`type\`: Choose from architecture, pattern, feature, api, bug, decision +- \`content\`: Detailed markdown documentation of the finding +- \`tags\`: Relevant keywords for searchability (e.g., ["auth", "security", "backend"]) + +After writing, confirm the memory was saved successfully. +`; + + const memoryReviewCommand = `--- +description: Review all project memories and summarize patterns +--- +List all memories in the project using \`agentmemory_memory_stats\` and \`agentmemory_memory_list\`. + +Then summarize: +1. Total number of memories by type +2. Most frequently accessed memories +3. Recent additions +4. Coverage gaps (areas with no memories) + +This helps identify knowledge gaps in the project documentation. +`; + + try { + await fs.mkdir(commandsDir, { recursive: true }); + await fs.writeFile(path.join(commandsDir, 'memory-search.md'), memorySearchCommand, 'utf-8'); + await fs.writeFile(path.join(commandsDir, 'memory-write.md'), memoryWriteCommand, 'utf-8'); + await fs.writeFile(path.join(commandsDir, 'memory-review.md'), memoryReviewCommand, 'utf-8'); + this.outputChannel.appendLine(` 📄 Created: .opencode/commands/memory-search.md`); + this.outputChannel.appendLine(` 📄 Created: .opencode/commands/memory-write.md`); + this.outputChannel.appendLine(` 📄 Created: .opencode/commands/memory-review.md`); + } catch (error) { + this.outputChannel.appendLine(` ❌ Failed to create OpenCode commands`); + } + } + + /** + * Inject agentMemory MCP server config into opencode.json. + * Adds the local MCP server so OpenCode can use memory tools. + */ + private async injectOpenCodeMCPConfig(workspacePath: string): Promise { + const configPath = path.join(workspacePath, 'opencode.json'); + + const serverPath = path.join( + this.workspaceFolder.uri.fsPath, + '..', '..', '.agents', 'skills', 'agent-memory', 'out', 'mcp-server', 'server.js' + ); + + const projectId = path.basename(workspacePath); + + try { + let config: any = {}; + try { + const content = await fs.readFile(configPath, 'utf-8'); + config = JSON.parse(content); + } catch { + // File doesn't exist, start fresh + } + + if (!config.mcp) { + config.mcp = {}; + } + + if (config.mcp.agentmemory) { + this.outputChannel.appendLine(` ℹ️ opencode.json already has agentmemory MCP config`); + return; + } + + config.mcp.agentmemory = { + type: 'local', + command: ['node', serverPath, projectId, workspacePath], + enabled: true + }; + + await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); + this.outputChannel.appendLine(` 📄 Updated: opencode.json with agentmemory MCP server`); + } catch (error) { + this.outputChannel.appendLine(` ❌ Failed to update opencode.json`); + } + } + /** * Inject context provider config for Continue */ diff --git a/src/mcp-server/agent-config.ts b/src/mcp-server/agent-config.ts new file mode 100644 index 0000000..5e1a348 --- /dev/null +++ b/src/mcp-server/agent-config.ts @@ -0,0 +1,200 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export const ALL_AGENTS: Record }> = { + kilocode: { + name: 'kilocode', + fullName: 'KiloCode', + description: 'KiloCode VS Code Extension', + memoryBankPath: '.kilocode/rules/memory-bank', + fileMapping: { + 'brief.md': { type: 'architecture', tags: ['overview', 'project'] }, + 'product.md': { type: 'feature', tags: ['product', 'features'] }, + 'context.md': { type: 'bug', tags: ['context', 'issues'] }, + 'architecture.md': { type: 'architecture', tags: ['design', 'system'] }, + 'tech.md': { type: 'decision', tags: ['technology', 'stack'] } + } + }, + cline: { + name: 'cline', + fullName: 'Cline', + description: 'Cline VS Code Extension', + memoryBankPath: '.clinerules/memory-bank', + fileMapping: { + 'projectBrief.md': { type: 'architecture', tags: ['overview', 'project'] }, + 'productContext.md': { type: 'feature', tags: ['product', 'goals'] }, + 'activeContext.md': { type: 'pattern', tags: ['current', 'focus'] }, + 'systemPatterns.md': { type: 'pattern', tags: ['patterns', 'design'] }, + 'techContext.md': { type: 'decision', tags: ['technology', 'decisions'] }, + 'progress.md': { type: 'feature', tags: ['progress', 'status'] } + } + }, + roocode: { + name: 'roocode', + fullName: 'RooCode', + description: 'RooCode VS Code Extension', + memoryBankPath: '.roo/memory-bank', + fileMapping: { + 'projectBrief.md': { type: 'architecture', tags: ['overview', 'project'] }, + 'productContext.md': { type: 'feature', tags: ['product', 'vision'] }, + 'activeContext.md': { type: 'pattern', tags: ['current', 'work'] }, + 'systemPatterns.md': { type: 'pattern', tags: ['patterns', 'architecture'] }, + 'techContext.md': { type: 'decision', tags: ['technology', 'stack'] }, + 'progress.md': { type: 'feature', tags: ['progress', 'tracking'] }, + 'decisionLog.md': { type: 'decision', tags: ['decisions', 'log'] } + } + }, + opencode: { + name: 'opencode', + fullName: 'OpenCode', + description: 'OpenCode Terminal TUI Agent', + memoryBankPath: '.opencode/memory-bank', + fileMapping: { + 'architecture.md': { type: 'architecture', tags: ['design', 'system', 'opencode'] }, + 'patterns.md': { type: 'pattern', tags: ['patterns', 'design', 'opencode'] }, + 'decisions.md': { type: 'decision', tags: ['decisions', 'tech', 'opencode'] }, + 'features.md': { type: 'feature', tags: ['features', 'product', 'opencode'] } + } + } +}; + +export type AgentName = keyof typeof ALL_AGENTS; + +export interface AgentConfigData { + selectedAgents: AgentName[]; + createdAt: string; + updatedAt: string; +} + +export class AgentConfig { + private configPath: string; + + constructor(private workspacePath: string) { + this.configPath = path.join(workspacePath, '.agentMemory', 'agents.json'); + } + + /** + * Detect which agents are "present" in the workspace by checking for their + * configuration files or directories. + */ + detectPresentAgents(): AgentName[] { + const present: AgentName[] = []; + for (const [key, agent] of Object.entries(ALL_AGENTS)) { + const agentPath = path.join(this.workspacePath, agent.memoryBankPath); + try { + fs.accessSync(agentPath); + present.push(key as AgentName); + } catch { + // Also check for opencode-specific files + if (key === 'opencode') { + try { + fs.accessSync(path.join(this.workspacePath, 'opencode.json')); + present.push('opencode'); + } catch { + try { + fs.accessSync(path.join(this.workspacePath, '.opencode')); + present.push('opencode'); + } catch { + // not present + } + } + } + } + } + return present; + } + + /** + * Check if agents.json already exists + */ + exists(): boolean { + return fs.existsSync(this.configPath); + } + + /** + * Read selected agents from agents.json. + * Returns null if no config exists. + */ + read(): AgentConfigData | null { + try { + if (!fs.existsSync(this.configPath)) { + return null; + } + const content = fs.readFileSync(this.configPath, 'utf-8'); + const data = JSON.parse(content) as AgentConfigData; + // Validate agent names + data.selectedAgents = data.selectedAgents.filter(a => ALL_AGENTS[a as AgentName] !== undefined); + return data; + } catch (error) { + console.error(`[AgentConfig] Failed to read config: ${error}`); + return null; + } + } + + /** + * Write selected agents to agents.json + */ + write(selectedAgents: AgentName[]): AgentConfigData { + const data: AgentConfigData = { + selectedAgents: [...new Set(selectedAgents)], // deduplicate + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + try { + const dir = path.dirname(this.configPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(this.configPath, JSON.stringify(data, null, 2), 'utf-8'); + console.error(`[AgentConfig] Saved config: ${data.selectedAgents.join(', ')}`); + } catch (error) { + console.error(`[AgentConfig] Failed to write config: ${error}`); + } + return data; + } + + /** + * Update only selected agents, preserving createdAt + */ + updateAgents(selectedAgents: AgentName[]): AgentConfigData { + const existing = this.read(); + const data: AgentConfigData = { + selectedAgents: [...new Set(selectedAgents)], + createdAt: existing?.createdAt || new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + try { + fs.writeFileSync(this.configPath, JSON.stringify(data, null, 2), 'utf-8'); + console.error(`[AgentConfig] Updated config: ${data.selectedAgents.join(', ')}`); + } catch (error) { + console.error(`[AgentConfig] Failed to update config: ${error}`); + } + return data; + } + + /** + * Get the list of agents that should be active. + * This reads the config. If no config exists, returns all present agents or all agents as fallback. + */ + getActiveAgents(): AgentName[] { + const config = this.read(); + if (config && config.selectedAgents.length > 0) { + return config.selectedAgents; + } + const present = this.detectPresentAgents(); + if (present.length > 0) { + return present; + } + // Fallback: none active until configured + return []; + } + + /** + * Returns agent configuration objects for active agents + */ + getActiveAgentConfigs(): Array { + const active = this.getActiveAgents(); + return active.map(name => ALL_AGENTS[name]).filter(Boolean); + } +} diff --git a/src/mcp-server/memory-bank-sync.ts b/src/mcp-server/memory-bank-sync.ts index a115073..3b9797f 100644 --- a/src/mcp-server/memory-bank-sync.ts +++ b/src/mcp-server/memory-bank-sync.ts @@ -2,6 +2,7 @@ import * as fs from 'fs/promises'; import * as fsCallback from 'fs'; import * as path from 'path'; import { v4 as uuidv4 } from 'uuid'; +import { AgentConfig, ALL_AGENTS, AgentName } from './agent-config'; interface Memory { id: string; @@ -23,72 +24,55 @@ interface Memory { updatedAt: number; } -interface AgentConfig { +interface AgentFileMappingConfig { name: string; memoryBankPath: string; fileMapping: Record; } /** - * Bi-directional sync between agent memory bank files and MCP storage + * Bi-directional sync between agent memory bank files and MCP storage, + * respecting the user's agent selection in .agentMemory/agents.json. */ export class MemoryBankSync { private workspacePath: string; private mcpDataPath: string; - - // Multi-agent configuration - private agents: AgentConfig[] = [ - { - name: 'kilocode', - memoryBankPath: '.kilocode/rules/memory-bank', - fileMapping: { - 'brief.md': { type: 'architecture', tags: ['overview', 'project'] }, - 'product.md': { type: 'feature', tags: ['product', 'features'] }, - 'context.md': { type: 'bug', tags: ['context', 'issues'] }, - 'architecture.md': { type: 'architecture', tags: ['design', 'system'] }, - 'tech.md': { type: 'decision', tags: ['technology', 'stack'] } - } - }, - { - name: 'cline', - memoryBankPath: '.clinerules/memory-bank', - fileMapping: { - 'projectBrief.md': { type: 'architecture', tags: ['overview', 'project'] }, - 'productContext.md': { type: 'feature', tags: ['product', 'goals'] }, - 'activeContext.md': { type: 'pattern', tags: ['current', 'focus'] }, - 'systemPatterns.md': { type: 'pattern', tags: ['patterns', 'design'] }, - 'techContext.md': { type: 'decision', tags: ['technology', 'decisions'] }, - 'progress.md': { type: 'feature', tags: ['progress', 'status'] } - } - }, - { - name: 'roocode', - memoryBankPath: '.roo/memory-bank', - fileMapping: { - 'projectBrief.md': { type: 'architecture', tags: ['overview', 'project'] }, - 'productContext.md': { type: 'feature', tags: ['product', 'vision'] }, - 'activeContext.md': { type: 'pattern', tags: ['current', 'work'] }, - 'systemPatterns.md': { type: 'pattern', tags: ['patterns', 'architecture'] }, - 'techContext.md': { type: 'decision', tags: ['technology', 'stack'] }, - 'progress.md': { type: 'feature', tags: ['progress', 'tracking'] }, - 'decisionLog.md': { type: 'decision', tags: ['decisions', 'log'] } - } - } - ]; + private agentConfig: AgentConfig; constructor(workspacePath: string, mcpDataPath: string = '.agentMemory') { this.workspacePath = workspacePath; this.mcpDataPath = path.join(workspacePath, mcpDataPath); + this.agentConfig = new AgentConfig(workspacePath); + } + + private getActiveAgents(): AgentFileMappingConfig[] { + const activeNames = this.agentConfig.getActiveAgents(); + if (activeNames.length === 0) { + console.error('[MemoryBankSync] No agents configured. Use configure_agents tool or select agents during setup.'); + return []; + } + return activeNames.map(name => ({ + name: ALL_AGENTS[name].name, + memoryBankPath: ALL_AGENTS[name].memoryBankPath, + fileMapping: ALL_AGENTS[name].fileMapping + })); } /** - * Import all memory bank files from all agents into MCP storage + * Import all memory bank files from selected agents into MCP storage */ async importAll(): Promise { - console.error('[MemoryBankSync] Starting import from all agents...'); + const agents = this.getActiveAgents(); + if (agents.length === 0) { + console.error('[MemoryBankSync] No active agents to import from. Run configure_agents to select agents.'); + await this.initializeProject(); + return; + } + + console.error('[MemoryBankSync] Starting import from selected agents...'); let totalImported = 0; - for (const agent of this.agents) { + for (const agent of agents) { totalImported += await this.importFromAgent(agent); } @@ -104,7 +88,7 @@ export class MemoryBankSync { * Import memory bank files from a specific agent * Returns the number of memories imported */ - private async importFromAgent(agent: AgentConfig): Promise { + private async importFromAgent(agent: AgentFileMappingConfig): Promise { const memoryBankDir = path.join(this.workspacePath, agent.memoryBankPath); try { @@ -151,6 +135,8 @@ export class MemoryBankSync { console.error('[MemoryBankSync] Creating default project memory...'); const projectName = path.basename(this.workspacePath); + const activeAgents = this.agentConfig.getActiveAgents(); + const defaultMemory: Memory = { id: uuidv4(), projectId: projectName, @@ -158,7 +144,7 @@ export class MemoryBankSync { type: 'architecture', content: `# ${projectName} - Project Overview -This project uses **agentMemory** for persistent knowledge management. +This project uses **agentMemory** for persistent knowledge management.${activeAgents.length > 0 ? `\n\n**Active Agents:** ${activeAgents.map(a => ALL_AGENTS[a]?.fullName || a).join(', ')}` : ''} ## How It Works @@ -199,7 +185,7 @@ As you work on this project, document: // Save to MCP storage await this.saveMCPMemory(defaultMemory); - // Export to all agent markdown files + // Export to all selected agent markdown files await this.exportToAgents(defaultMemory); console.error('[MemoryBankSync] ✅ Default project memory created and synced'); @@ -323,10 +309,12 @@ As you work on this project, document: } /** - * Export MCP memory to appropriate agent markdown files + * Export MCP memory to appropriate agent markdown files (only selected agents) */ async exportToAgents(memory: Memory): Promise { - for (const agent of this.agents) { + const agents = this.getActiveAgents(); + + for (const agent of agents) { // Find which file this memory type maps to const targetFile = this.getTargetFile(memory.type, agent); @@ -334,12 +322,167 @@ As you work on this project, document: await this.appendToMarkdown(memory, agent, targetFile); } + + // Sync to episodic memory file + ensure AGENTS.md has instructions + await this.syncToMemoryContext(memory); + await this.ensureAgentsMDInstructions(); + } + + /** + * Maximum number of episodic memory entries to keep in memory-context.md + */ + private static readonly MAX_MEMORY_CONTEXT_ENTRIES = 25; + + /** + * Write memory summary to .opencode/memory-context.md (episodic memory). + * Prunes to MAX_MEMORY_CONTEXT_ENTRIES by keeping the most recent entries. + */ + private async syncToMemoryContext(memory: Memory): Promise { + const memoryContextDir = path.join(this.workspacePath, '.opencode'); + const memoryContextPath = path.join(memoryContextDir, 'memory-context.md'); + + const entry = `### ${memory.key}\n- **Type:** ${memory.type}\n- **Tags:** ${memory.tags.join(', ')}\n- **Created:** ${new Date(memory.createdAt).toISOString()}\n- **Summary:** ${memory.content.substring(0, 200).replace(/\n/g, ' ').trim()}...\n`; + + try { + await fs.mkdir(memoryContextDir, { recursive: true }); + + let existing = ''; + try { + existing = await fs.readFile(memoryContextPath, 'utf-8'); + } catch { + // File doesn't exist yet + } + + // Check if this memory entry already exists + if (existing.includes(`### ${memory.key}\n`)) { + return; + } + + const header = `# Episodic Memory\n\nProject knowledge base synced by agentMemory.\nNewest entries first. Max ${MemoryBankSync.MAX_MEMORY_CONTEXT_ENTRIES} entries.\n\n`; + + // Build new content + let content: string; + if (existing.trim()) { + // Insert new entry right after the header, before existing entries + const headerEnd = existing.indexOf('### '); + if (headerEnd === -1) { + content = header + entry + '\n' + existing; + } else { + content = existing.substring(0, headerEnd) + entry + '\n' + existing.substring(headerEnd); + } + } else { + content = header + entry; + } + + // Prune: keep only the first MAX_MEMORY_CONTEXT_ENTRIES + const lines = content.split('\n'); + const entryHeaders: number[] = []; + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('### ')) { + entryHeaders.push(i); + } + } + + if (entryHeaders.length > MemoryBankSync.MAX_MEMORY_CONTEXT_ENTRIES) { + // Find where the 25th entry ends + const keepUntil = entryHeaders[MemoryBankSync.MAX_MEMORY_CONTEXT_ENTRIES]; + // Find the end of the last kept entry (next ### or end of file) + let endOfLastKept = lines.length; + for (let i = keepUntil; i < lines.length; i++) { + if (lines[i].startsWith('### ') && i > keepUntil) { + endOfLastKept = i; + break; + } + } + content = lines.slice(0, endOfLastKept).join('\n').trimEnd() + '\n'; + console.error(`[MemoryBankSync] Pruned episodic memory to ${MemoryBankSync.MAX_MEMORY_CONTEXT_ENTRIES} entries`); + } + + await fs.writeFile(memoryContextPath, content, 'utf-8'); + console.error(`[MemoryBankSync] ✓ Synced ${memory.key} to .opencode/memory-context.md`); + } catch (error) { + console.error(`[MemoryBankSync] Failed to sync memory context: ${error}`); + } + } + + /** + * Ensure AGENTS.md has a reference to .opencode/memory-context.md. + * AGENTS.md stays clean — only instructions, no operational data. + */ + private async ensureAgentsMDInstructions(): Promise { + const agentsMdPath = path.join(this.workspacePath, 'AGENTS.md'); + const activeAgents = this.agentConfig.getActiveAgents(); + const activeAgentsText = activeAgents.length > 0 + ? activeAgents.map(a => `| ${ALL_AGENTS[a]?.fullName || a} | ${ALL_AGENTS[a]?.memoryBankPath || 'N/A'} |`).join('\n') + : '| (none configured) | Run `configure_agents` to enable |'; + + const memorySection = `## agentMemory + +This project uses agentMemory for persistent knowledge management. + +### Active Agents + +| Agent | Memory Bank Path | +|-------|-----------------| +${activeAgentsText} + +### Episodic Memory + +Project decisions, patterns, and context are stored in \`.opencode/memory-context.md\`. + +**Before starting any task:** Read \`.opencode/memory-context.md\` to load recent project context. +**After significant work:** Use \`agentmemory_memory_write\` to persist new knowledge. Entries sync to the context file automatically. + +The context file is a sliding window of the 25 most recent memories (newest first). Older entries are pruned automatically — full history remains searchable via \`agentmemory_memory_search\`. + +### Available Tools (MCP server: agentmemory) + +- \`agentmemory_memory_search\` — Search all memories by query, type, or tags +- \`agentmemory_memory_write\` — Save new memory (key, type, content, tags) +- \`agentmemory_memory_read\` — Retrieve specific memory by key +- \`agentmemory_memory_list\` — List memories by type +- \`agentmemory_memory_update\` — Update existing memory +- \`agentmemory_memory_stats\` — View memory usage statistics +- \`agentmemory_configure_agents\` — Configure which coding agents to sync with + +`; + + try { + let existing = ''; + try { + existing = await fs.readFile(agentsMdPath, 'utf-8'); + } catch { + // File doesn't exist yet + } + + // If the section already exists, replace it to keep agent list up to date + if (existing.includes('## agentMemory')) { + const regex = /## agentMemory[\s\S]*?(?=\n#{1,2} .|$)/; + const updated = existing.replace(regex, memorySection.trimEnd()); + if (updated !== existing) { + await fs.writeFile(agentsMdPath, updated.trimEnd() + '\n', 'utf-8'); + console.error(`[MemoryBankSync] ✓ Updated AGENTS.md with current active agents`); + } + return; + } + + if (existing.trim()) { + await fs.writeFile(agentsMdPath, existing.trimEnd() + '\n\n' + memorySection, 'utf-8'); + console.error(`[MemoryBankSync] ✓ Updated AGENTS.md with agentMemory instructions`); + } else { + const header = `# AGENTS.md\n\nInstructions for AI coding agents working on this project.\n\n`; + await fs.writeFile(agentsMdPath, header + memorySection, 'utf-8'); + console.error(`[MemoryBankSync] ✓ Created AGENTS.md with agentMemory instructions`); + } + } catch (error) { + console.error(`[MemoryBankSync] Failed to update AGENTS.md: ${error}`); + } } /** * Get target markdown file for a memory type */ - private getTargetFile(type: Memory['type'], agent: AgentConfig): string | null { + private getTargetFile(type: Memory['type'], agent: AgentFileMappingConfig): string | null { for (const [filename, config] of Object.entries(agent.fileMapping)) { if (config.type === type) { return filename; @@ -353,7 +496,7 @@ As you work on this project, document: */ private async appendToMarkdown( memory: Memory, - agent: AgentConfig, + agent: AgentFileMappingConfig, filename: string ): Promise { const memoryBankDir = path.join(this.workspacePath, agent.memoryBankPath); @@ -436,8 +579,14 @@ ${memory.content} async startWatching(): Promise { console.error('[MemoryBankSync] Starting file watching for memory bank sync...'); + const agents = this.getActiveAgents(); + if (agents.length === 0) { + console.error('[MemoryBankSync] No active agents to watch. Configure agents to enable file watching.'); + return; + } + // Watch each agent's memory bank directory - for (const agent of this.agents) { + for (const agent of agents) { const memoryBankPath = path.join(this.workspacePath, agent.memoryBankPath); try { diff --git a/src/mcp-server/server.ts b/src/mcp-server/server.ts index 0c22eb1..3b9acbe 100644 --- a/src/mcp-server/server.ts +++ b/src/mcp-server/server.ts @@ -5,6 +5,7 @@ import { CacheManager } from './cache'; import { MCPTools } from './tools'; import { SocketBridge } from './socket-bridge'; import { MemoryBankSync } from './memory-bank-sync'; +import { AgentConfig } from './agent-config'; import { StandaloneDashboard } from '../standalone-dashboard'; interface MCPRequest { @@ -25,6 +26,45 @@ interface MCPResponse { }; } +/** + * Parse CLI arguments. + * Supported: + * node server.js [--agents=kilocode,opencode] + */ +function parseArgs(): { projectId: string; workspacePath: string; agentsArg?: string } { + const args = process.argv.slice(2); + let projectId = 'default-project'; + let workspacePath = process.cwd(); + let agentsArg: string | undefined; + + for (const arg of args) { + if (arg.startsWith('--agents=')) { + agentsArg = arg.split('=')[1]; + } else if (arg.startsWith('--agents')) { + // Handle --agents val (next arg) could be done but keep simple + const eqIdx = arg.indexOf('='); + if (eqIdx !== -1) { + agentsArg = arg.substring(eqIdx + 1); + } + } else if (!projectId || projectId === 'default-project') { + // First positional = projectId + if (projectId === 'default-project' && arg !== workspacePath) { + projectId = arg; + } + } else { + // Second positional = workspacePath + workspacePath = arg; + } + } + + // More robust positional parsing + const positional = args.filter(a => !a.startsWith('--')); + if (positional.length >= 1) projectId = positional[0]; + if (positional.length >= 2) workspacePath = positional[1]; + + return { projectId, workspacePath, agentsArg }; +} + /** * Simple MCP Server using stdio transport * This server implements the Model Context Protocol for memory tools @@ -35,9 +75,11 @@ class MCPServer { private tools: MCPTools; private projectId: string; private syncEngine: MemoryBankSync; + private workspacePath: string; constructor(projectId: string, workspacePath: string) { this.projectId = projectId; + this.workspacePath = workspacePath; // Use absolute path based on workspace const storagePath = workspacePath + '/.agentMemory'; @@ -50,13 +92,20 @@ class MCPServer { // Initialize sync engine this.syncEngine = new MemoryBankSync(workspacePath); - this.tools = new MCPTools(this.storage, this.cache, this.syncEngine); + this.tools = new MCPTools(this.storage, this.cache, this.syncEngine, workspacePath); console.error(`[MCP Server] Initialized for project: ${projectId}`); console.error(`[MCP Server] Workspace path: ${workspacePath}`); console.error(`[MCP Server] Storage path: ${storagePath}`); } + /** + * Public accessor for the tools instance (used for initialization with agents arg) + */ + public getTools(): MCPTools { + return this.tools; + } + /** * Handle incoming MCP request (public for socket bridge) */ @@ -107,6 +156,9 @@ class MCPServer { case 'project_init': result = await this.tools.project_init(toolArgs); break; + case 'configure_agents': + result = await this.tools.configure_agents(toolArgs); + break; case 'memory_stats': result = await this.tools.memory_stats(toolArgs); break; @@ -224,10 +276,22 @@ class MCPServer { } // Main entry point -const projectId = process.argv[2] || 'default-project'; -const workspacePath = process.argv[3] || process.cwd(); +const { projectId, workspacePath, agentsArg } = parseArgs(); const server = new MCPServer(projectId, workspacePath); + +// If --agents was passed, write to config immediately before starting +if (agentsArg) { + const agentConfig = new AgentConfig(workspacePath); + if (!agentConfig.exists()) { + server.getTools().configure_agents({ projectId, agents: agentsArg }).then(() => { + console.error(`[MCP Server] Pre-configured agents from CLI: ${agentsArg}`); + }).catch(err => { + console.error(`[MCP Server] Failed to pre-configure agents: ${err}`); + }); + } +} + server.start(); // Also start Unix socket bridge for KiloCode diff --git a/src/mcp-server/tools.ts b/src/mcp-server/tools.ts index 63609c5..07773c8 100644 --- a/src/mcp-server/tools.ts +++ b/src/mcp-server/tools.ts @@ -1,9 +1,11 @@ import { StorageManager } from './storage'; import { CacheManager } from './cache'; import { MemoryBankSync } from './memory-bank-sync'; +import { AgentConfig, AgentConfigData, ALL_AGENTS, AgentName } from './agent-config'; import { v4 as uuidv4 } from 'uuid'; import * as fs from 'fs'; import * as path from 'path'; +import * as readline from 'readline'; interface Memory { id: string; @@ -34,11 +36,17 @@ export class MCPTools { private storage: StorageManager; private cache: CacheManager; private syncEngine?: MemoryBankSync; + private agentConfig?: AgentConfig; + private workspacePath: string; - constructor(storage: StorageManager, cache: CacheManager, syncEngine?: MemoryBankSync) { + constructor(storage: StorageManager, cache: CacheManager, syncEngine?: MemoryBankSync, workspacePath?: string) { this.storage = storage; this.cache = cache; this.syncEngine = syncEngine; + this.workspacePath = workspacePath || process.cwd(); + if (this.workspacePath) { + this.agentConfig = new AgentConfig(this.workspacePath); + } } /** @@ -150,29 +158,61 @@ export class MCPTools { } /** - * Tool 6: project_init - Auto-detect workspace (10μs target) + * Tool 6: project_init - Auto-detect workspace and setup agents + * + * Supports a comma-separated `agents` argument for non-interactive + * configuration: e.g., { agents: "kilocode,opencode" }. + * If omitted and no agents.json exists, interactive readline prompts + * the user (only in a TTY environment). */ - async project_init(params: ToolCallParams): Promise<{ success: boolean; projectId: string }> { - const { projectId } = params; + async project_init(params: ToolCallParams): Promise<{ success: boolean; projectId: string; configuredAgents: string[] }> { + const { projectId, agents } = params; await this.storage.initProject(projectId); - // Auto-create .agent structure if it doesn't exist (Antigravity support) - // We need to resolve the workspace path. Since we don't have it passed explicitly in params, - // we'll rely on the storage manager's base path or try to infer it. - // Ideally, we should pass workspacePath to project_init. - // For now, let's assume storage manager knows where the root is. - // Or better yet, let's update call args to include workspacePath if possible, - // but for safety, we'll try to use the parent of .agentMemory if available. + let configuredAgents: string[] = []; + + if (this.agentConfig) { + const existing = this.agentConfig.read(); + if (existing && existing.selectedAgents.length > 0) { + configuredAgents = existing.selectedAgents; + console.error(`[project_init] Using existing agent config: ${configuredAgents.join(', ')}`); + } else if (typeof agents === 'string' && agents.trim()) { + const requestedAgents = agents.split(',').map(a => a.trim().toLowerCase() as AgentName); + const validAgents = requestedAgents.filter(a => ALL_AGENTS[a] !== undefined); + if (validAgents.length > 0) { + this.agentConfig.write(validAgents); + configuredAgents = validAgents; + console.error(`[project_init] Agents configured from CLI args: ${configuredAgents.join(', ')}`); + } + } else if (process.stdin.isTTY) { + try { + configuredAgents = await this.interactiveAgentPrompt(); + if (configuredAgents.length > 0) { + this.agentConfig.write(configuredAgents); + console.error(`[project_init] Agents configured interactively: ${configuredAgents.join(', ')}`); + } + } catch (error) { + console.error(`[project_init] Interactive prompt failed: ${error}`); + } + } else { + console.error('[project_init] No TTY available, skipping interactive agent selection. Pass --agents or configure later.'); + } + if (configuredAgents.length === 0) { + const detected = this.agentConfig.detectPresentAgents(); + if (detected.length > 0) { + this.agentConfig.write(detected); + configuredAgents = detected; + console.error(`[project_init] Auto-detected present agents: ${configuredAgents.join(', ')}`); + } + } + } + // Auto-create .agent structure if it doesn't exist (Antigravity support) try { // @ts-ignore const storagePath = this.storage.baseDir; - console.error('[project_init] Storage path:', storagePath); - if (storagePath) { - const projectRoot = path.dirname(storagePath); // Parent of .agentMemory - console.error('[project_init] Project root:', projectRoot); - + const projectRoot = path.dirname(storagePath); const agentDir = path.join(projectRoot, '.agent'); const workflowsDir = path.join(agentDir, 'workflows'); @@ -182,6 +222,7 @@ export class MCPTools { const workflowFile = path.join(workflowsDir, 'update-memory.md'); if (!fs.existsSync(workflowFile)) { + const activeAgents = configuredAgents.length > 0 ? configuredAgents.map(a => ALL_AGENTS[a]?.fullName || a).join(', ') : 'All'; const workflowContent = `--- description: How to update the project memory bank with new findings --- @@ -190,6 +231,8 @@ description: How to update the project memory bank with new findings Follow this workflow to document important architectural decisions, patterns, or features. +**Active Agents:** ${activeAgents} + 1. **Search First**: Check if a similar memory already exists. \`\`\`bash # Use the memory_search tool @@ -222,10 +265,99 @@ Follow this workflow to document important architectural decisions, patterns, or } } catch (error) { console.error('[project_init] Failed to scaffold .agent directory:', error); - // Don't fail the init, just log the error } - return { success: true, projectId }; + return { success: true, projectId, configuredAgents }; + } + + /** + * Tool 6.5: configure_agents - Select which agents to enable/disable for syncing + */ + async configure_agents(params: ToolCallParams): Promise<{ success: boolean; selectedAgents: string[]; previousAgents: string[] }> { + const { agents, interactive } = params; + let selectedAgents: string[] = []; + let previousAgents: string[] = []; + + if (!this.agentConfig) { + return { success: false, selectedAgents: [], previousAgents: [] }; + } + + const existing = this.agentConfig.read(); + if (existing) { + previousAgents = [...existing.selectedAgents]; + } + + if (typeof agents === 'string' && agents.trim()) { + const requestedAgents = agents.split(',').map(a => a.trim().toLowerCase() as AgentName); + selectedAgents = requestedAgents.filter(a => ALL_AGENTS[a] !== undefined); + } else if (interactive !== false && process.stdin.isTTY) { + try { + selectedAgents = await this.interactiveAgentPrompt(previousAgents); + } catch (error) { + console.error(`[configure_agents] Interactive prompt failed: ${error}`); + } + } + + if (selectedAgents.length === 0) { + // Keep previous selection or fall back to detected + if (previousAgents.length > 0) { + selectedAgents = previousAgents; + } else { + selectedAgents = this.agentConfig.detectPresentAgents(); + } + } + + if (selectedAgents.length > 0) { + this.agentConfig.write(selectedAgents); + console.error(`[configure_agents] Agents updated: ${selectedAgents.join(', ')}`); + } + + return { success: true, selectedAgents, previousAgents }; + } + + /** + * Interactive readline prompt for selecting agents + */ + private async interactiveAgentPrompt(defaultSelection: string[] = []): Promise { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const choices = Object.entries(ALL_AGENTS).map(([key, agent]) => { + const index = Object.keys(ALL_AGENTS).indexOf(key) + 1; + const checked = defaultSelection.includes(key) ? '✅' : '⬜'; + return `${index}. ${checked} ${agent.fullName} - ${agent.description}`; + }).join('\n'); + + console.error('\n╔════════════════════════════════════════════════════╗'); + console.error('║ Select AI Coding Agents for Memory Bank Sync ║'); + console.error('╠════════════════════════════════════════════════════╣'); + console.error(choices); + console.error('╠════════════════════════════════════════════════════╣'); + console.error('║ Enter numbers separated by commas (e.g., 1,3,4) ║'); + console.error('║ Or type "all" to select every agent ║'); + console.error('╚════════════════════════════════════════════════════╝\n'); + + rl.question('Your selection: ', (answer) => { + rl.close(); + const trimmed = answer.trim().toLowerCase(); + + if (trimmed === 'all') { + resolve(Object.keys(ALL_AGENTS) as AgentName[]); + return; + } + + const indices = trimmed.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n)); + const keys = Object.keys(ALL_AGENTS) as AgentName[]; + const selected = indices + .map(idx => keys[idx - 1]) + .filter((k): k is AgentName => k !== undefined && ALL_AGENTS[k] !== undefined); + + resolve(selected); + }); + }); } /** @@ -236,10 +368,25 @@ Follow this workflow to document important architectural decisions, patterns, or const stats = await this.storage.getStats(projectId); const cacheStats = this.cache.getStats(); - // Add sync status if available + // Show active agent configuration + let activeAgents: string[] = []; + let configExists = false; + if (this.agentConfig) { + const config = this.agentConfig.read(); + if (config) { + activeAgents = config.selectedAgents; + configExists = true; + } + if (activeAgents.length === 0) { + activeAgents = this.agentConfig.detectPresentAgents(); + } + } + const syncStatus = this.syncEngine ? { enabled: true, - agents: ['kilocode', 'cline', 'roocode'] + agents: activeAgents, + configExists, + allSupportedAgents: Object.keys(ALL_AGENTS) } : { enabled: false }; return { @@ -326,18 +473,31 @@ Follow this workflow to document important architectural decisions, patterns, or }, { name: 'project_init', - description: 'Initialize project storage', + description: 'Initialize project storage and optionally configure agents (pass agents: "kilocode,opencode" or use interactive mode)', inputSchema: { type: 'object', properties: { - projectId: { type: 'string' } + projectId: { type: 'string' }, + agents: { type: 'string', description: 'Comma-separated list of agents to sync with (e.g., \"kilocode,opencode\")' } }, required: ['projectId'] } }, + { + name: 'configure_agents', + description: 'Configure which coding agents to sync memory bank files with. Pass agents: "kilocode,opencode" or use interactive mode.', + inputSchema: { + type: 'object', + properties: { + agents: { type: 'string', description: 'Comma-separated list of agents to enable (e.g., \"kilocode,opencode\")' }, + interactive: { type: 'boolean', description: 'If true and TTY available, prompt interactively. Set to false for non-interactive.' } + }, + required: [] + } + }, { name: 'memory_stats', - description: 'Get storage and cache statistics', + description: 'Get storage, cache, and sync statistics', inputSchema: { type: 'object', properties: {