brainctl 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +181 -131
  2. package/dist/executor/resolver.js +1 -38
  3. package/dist/mcp/server.js +183 -0
  4. package/dist/services/agent-config-service.d.ts +35 -0
  5. package/dist/services/agent-config-service.js +222 -0
  6. package/dist/services/mcp-preflight-service.d.ts +25 -0
  7. package/dist/services/mcp-preflight-service.js +84 -0
  8. package/dist/services/plugin-install-service.d.ts +92 -0
  9. package/dist/services/plugin-install-service.js +243 -0
  10. package/dist/services/profile-export-service.js +5 -5
  11. package/dist/services/profile-import-service.js +1 -1
  12. package/dist/services/profile-service.d.ts +10 -0
  13. package/dist/services/profile-service.js +140 -28
  14. package/dist/services/skill-paths.d.ts +2 -0
  15. package/dist/services/skill-paths.js +12 -0
  16. package/dist/services/skill-preflight-service.d.ts +23 -0
  17. package/dist/services/skill-preflight-service.js +40 -0
  18. package/dist/services/sync/agent-reader.d.ts +30 -0
  19. package/dist/services/sync/agent-reader.js +232 -0
  20. package/dist/services/sync/claude-writer.js +4 -1
  21. package/dist/services/sync/codex-writer.js +6 -2
  22. package/dist/services/sync/gemini-writer.js +4 -1
  23. package/dist/services/sync/managed-plugin-registry.d.ts +17 -0
  24. package/dist/services/sync/managed-plugin-registry.js +75 -0
  25. package/dist/services/sync/plugin-skill-reader.d.ts +2 -0
  26. package/dist/services/sync/plugin-skill-reader.js +33 -0
  27. package/dist/services/sync-service.js +5 -0
  28. package/dist/system/executables.d.ts +1 -0
  29. package/dist/system/executables.js +38 -0
  30. package/dist/types.d.ts +15 -5
  31. package/dist/ui/routes.js +423 -1
  32. package/dist/web/assets/index-BCkorugl.css +1 -0
  33. package/dist/web/assets/index-sGnTMhkX.js +16 -0
  34. package/dist/web/index.html +2 -2
  35. package/package.json +7 -1
  36. package/dist/web/assets/index-CRJ6cM0Q.css +0 -1
  37. package/dist/web/assets/index-Cr8gt3VF.js +0 -9
package/README.md CHANGED
@@ -1,121 +1,146 @@
1
1
  # 🧠 brainctl
2
2
 
3
- > Stop reconfiguring your AI tools.
3
+ > One AI setup. Multiple agents. Zero reconfiguration.
4
4
 
5
- `brainctl` is a CLI for managing a portable AI environment across tools like Claude Code and Codex.
5
+ **brainctl** is a cross-agent AI workflow manager that unifies your environment across Claude Code, Codex, and Gemini CLI — with a web dashboard, MCP server, and portable profiles.
6
6
 
7
- Define your memory, skills, and execution flow once, then reuse them across different AI agents.
7
+ [![npm version](https://img.shields.io/npm/v/brainctl)](https://www.npmjs.com/package/brainctl)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+ [![Build](https://img.shields.io/github/actions/workflow/status/Rorogogogo/brainctl/deploy.yml?branch=main)](https://github.com/Rorogogogo/brainctl/actions)
8
10
 
9
11
  ---
10
12
 
11
- ## ✨ Why brainctl?
13
+ ## ✨ Features
12
14
 
13
- If you're using multiple AI tools, you've probably already hit the same problems:
15
+ - 🔀 **Multi-agent support** Claude Code, Codex, and Gemini CLI from one config
16
+ - 🖥️ **Web dashboard** — Visual drag-and-drop MCP management across agents
17
+ - 🔌 **MCP server** — 20 tools exposable to any MCP-compatible agent
18
+ - 📦 **Portable profiles** — Export/import skill + MCP bundles as tarballs
19
+ - 🧠 **Shared memory** — Markdown-based context files shared across agents
20
+ - 🧩 **Reusable skills** — Prompt templates stored in `ai-stack.yaml`
21
+ - 🔄 **Profile sync** — Push configs to all agents in one command
22
+ - 🩺 **Health checks** — `status` and `doctor` commands for visibility
23
+ - 🔁 **Fallback agents** — Automatic failover if primary agent is unavailable
14
24
 
15
- - Rewriting the same prompt for different agents
16
- - Losing context between tools
17
- - Rebuilding your environment every time you switch
18
-
19
- `brainctl` solves that with one core idea:
25
+ ---
20
26
 
21
- > **One AI setup. Multiple agents.**
27
+ ## 📸 Demo
22
28
 
23
- ---
29
+ ### Web Dashboard — Drag MCPs between agents
24
30
 
25
- ## 🚀 Features
31
+ ```
32
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
33
+ │ Claude │ │ Codex │ │ Gemini │
34
+ │ ┌──────────┐ │ │ ┌──────────┐ │ │ │
35
+ │ │ github │◄├──┤►│ github │ │ │ Drop here │
36
+ │ │ brainctl │ │ │ │ brainctl │ │ │ to copy │
37
+ │ └──────────┘ │ │ └──────────┘ │ │ │
38
+ │ Skills: 11 │ │ Skills: 3 │ │ Skills: 0 │
39
+ └──────────────┘ └──────────────┘ └──────────────┘
40
+ ```
26
41
 
27
- - 🧠 File-based memory from Markdown files
28
- - 🧩 Reusable skills stored in `ai-stack.yaml`
29
- - 🔌 Multi-agent execution with Claude and Codex
30
- - ⚙️ Unified context builder
31
- - 🛠 CLI-first workflow
32
- - 🔍 `status` and `doctor` for visibility
33
- - 🔁 Optional fallback agent support with `--fallback`
42
+ The dashboard reads **live config files** from each agent (`~/.claude.json`, `~/.codex/config.toml`, `~/.gemini/settings.json`) and lets you drag MCPs between them. Changes are staged and only applied on confirm.
34
43
 
35
44
  ---
36
45
 
37
46
  ## 📦 Installation
38
47
 
39
- ### Option 1: Install from npm
40
-
41
48
  ```bash
42
49
  npm install -g brainctl
43
50
  ```
44
51
 
45
- Then:
52
+ Or from source:
46
53
 
47
54
  ```bash
48
- brainctl --help
55
+ git clone https://github.com/Rorogogogo/brainctl.git
56
+ cd brainctl
57
+ npm install && npm run build && npm link
49
58
  ```
50
59
 
51
- ### Option 2: Local CLI install from source
60
+ > **Prerequisite:** At least one supported agent CLI must be installed and on your `PATH` (`claude`, `codex`, or `gemini`).
52
61
 
53
- ```bash
54
- npm install
55
- npm run build
56
- npm link
57
- ```
62
+ ---
58
63
 
59
- Then:
64
+ ## 🚀 Quick Start
60
65
 
61
66
  ```bash
62
- brainctl --help
63
- ```
67
+ # 1. Initialize a project
68
+ brainctl init
64
69
 
65
- ### Option 3: Run without linking
70
+ # 2. Check your setup
71
+ brainctl status
72
+ brainctl doctor
66
73
 
67
- ```bash
68
- npm install
69
- npm run build
70
- node dist/cli.js --help
71
- ```
74
+ # 3. Run a skill
75
+ brainctl run summarize ./memory/notes.md --with claude
72
76
 
73
- `brainctl` does not bundle agent CLIs. You still need at least one supported agent installed separately and available on `PATH`, such as `claude` or `codex`.
77
+ # 4. Launch the web dashboard
78
+ brainctl ui
79
+ # → http://127.0.0.1:3333
80
+ ```
74
81
 
75
82
  ---
76
83
 
77
- ## Quick Start
84
+ ## 📖 Usage
78
85
 
79
- ### 1. Initialize a project
80
-
81
- ```bash
82
- brainctl init
83
- ```
86
+ ### CLI Commands
84
87
 
85
- This creates:
88
+ | Command | Description |
89
+ |---------|-------------|
90
+ | `brainctl init` | Scaffold `ai-stack.yaml` and `memory/` directory |
91
+ | `brainctl status` | Show memory, skills, MCPs, and agent availability |
92
+ | `brainctl doctor` | Validate config, paths, and installed agents |
93
+ | `brainctl run <skill> <file> --with <agent>` | Execute a skill through an agent |
94
+ | `brainctl profile list` | List available profiles |
95
+ | `brainctl profile create <name>` | Create a new profile |
96
+ | `brainctl profile use <name>` | Switch active profile |
97
+ | `brainctl profile export <name>` | Export profile as portable tarball |
98
+ | `brainctl profile import <archive>` | Import profile from tarball |
99
+ | `brainctl sync` | Sync active profile to all agent configs |
100
+ | `brainctl ui` | Start the web dashboard |
86
101
 
87
- - `ai-stack.yaml`
88
- - `memory/`
89
- - `memory/notes.md`
90
-
91
- ### 2. Inspect the setup
102
+ ### Run Examples
92
103
 
93
104
  ```bash
94
- brainctl status
95
- brainctl doctor
96
- ```
105
+ # Basic execution
106
+ brainctl run summarize ./notes.md --with claude
97
107
 
98
- ### 3. Run a task
108
+ # With fallback agent
109
+ brainctl run analyze ./report.md --with codex --fallback claude
99
110
 
100
- ```bash
101
- brainctl run summarize ./memory/notes.md --with claude
111
+ # Using Gemini
112
+ brainctl run review ./code.md --with gemini
102
113
  ```
103
114
 
104
- Or:
115
+ ### Profile MCP Format
105
116
 
106
- ```bash
107
- brainctl run summarize ./memory/notes.md --with codex
117
+ Packed/published profiles should classify every MCP as either `local` or `remote`.
118
+
119
+ ```yaml
120
+ mcps:
121
+ github:
122
+ kind: local
123
+ source: npm
124
+ package: "@modelcontextprotocol/server-github"
125
+
126
+ internal-docs:
127
+ kind: remote
128
+ transport: http
129
+ url: "https://mcp.example.com"
108
130
  ```
109
131
 
110
- With fallback:
132
+ Rules:
111
133
 
112
- ```bash
113
- brainctl run summarize ./memory/notes.md --with claude --fallback codex
114
- ```
134
+ - local profile files may still use the older `type: npm` / `type: bundled` MCP shape
135
+ - `brainctl profile export` writes the packed profile using the explicit format below
136
+ - `local` MCPs must declare `source: npm` or `source: bundled`
137
+ - `remote` MCPs must declare `transport` and `url`
138
+ - bundled local MCPs must declare `path` and `command`
139
+ - `brainctl sync` currently supports local MCPs only; remote MCPs remain in the profile package but are not written into agent configs yet
115
140
 
116
141
  ---
117
142
 
118
- ## 🧠 Example `ai-stack.yaml`
143
+ ## 🧠 Config: `ai-stack.yaml`
119
144
 
120
145
  ```yaml
121
146
  memory:
@@ -124,119 +149,144 @@ memory:
124
149
 
125
150
  skills:
126
151
  summarize:
127
- description: Summarize content
152
+ description: Summarize content into bullet points
128
153
  prompt: |
129
154
  Summarize the following content into concise bullet points.
130
155
 
131
- analyze:
132
- description: Analyze content deeply
156
+ review:
157
+ description: Code review with actionable feedback
133
158
  prompt: |
134
- Analyze the following content and extract key insights.
159
+ Review the following code and provide actionable feedback.
135
160
 
136
161
  mcps: {}
137
162
  ```
138
163
 
139
- ---
164
+ ### How Context Assembly Works
140
165
 
141
- ## 🧩 How It Works
166
+ ```
167
+ ┌─────────────┐
168
+ │ MEMORY │ ← Markdown files from configured paths
169
+ ├─────────────┤
170
+ │ SKILL │ ← Prompt template from ai-stack.yaml
171
+ ├─────────────┤
172
+ │ INPUT │ ← Your file
173
+ └─────────────┘
174
+
175
+ Agent CLI (claude / codex / gemini)
176
+ ```
142
177
 
143
- `brainctl` builds a unified context before calling an agent:
178
+ ---
144
179
 
145
- ```text
146
- --- MEMORY ---
147
- [your markdown files]
180
+ ## 🔌 MCP Server
148
181
 
149
- --- SKILL ---
150
- [prompt template]
182
+ brainctl exposes **20 MCP tools** that any compatible agent can call:
151
183
 
152
- --- INPUT ---
153
- [your file]
184
+ ```bash
185
+ # Add brainctl as an MCP server in your agent config
186
+ # Claude (~/.claude.json):
187
+ {
188
+ "mcpServers": {
189
+ "brainctl": {
190
+ "type": "stdio",
191
+ "command": "npx",
192
+ "args": ["-y", "brainctl", "mcp"]
193
+ }
194
+ }
195
+ }
154
196
  ```
155
197
 
156
- That context is then sent to the selected agent over stdin.
157
-
158
- ---
159
-
160
- ## 🛠 Usage
198
+ **Available tools:**
161
199
 
162
- ### Commands
200
+ | Category | Tools |
201
+ |----------|-------|
202
+ | **Skills** | `list_skills`, `get_skill`, `run` |
203
+ | **Memory** | `read_memory`, `write_memory` |
204
+ | **Profiles** | `list_profiles`, `get_profile`, `create_profile`, `update_profile`, `delete_profile`, `switch_profile`, `copy_profile_items`, `export_profile`, `import_profile` |
205
+ | **Agent Configs** | `read_agent_configs`, `add_agent_mcp`, `remove_agent_mcp` |
206
+ | **System** | `status`, `doctor`, `sync` |
163
207
 
164
- | Command | Purpose |
165
- | --- | --- |
166
- | `brainctl init` | Initialize `ai-stack.yaml` and memory files |
167
- | `brainctl status` | Show memory, skills, MCP count, and agent availability |
168
- | `brainctl doctor` | Validate config, memory paths, skills, and installed agents |
169
- | `brainctl run <skill> <file> --with <agent>` | Build context and execute with an agent |
208
+ ---
170
209
 
171
- ### Examples
210
+ ## 🖥️ Web Dashboard
172
211
 
173
212
  ```bash
174
- brainctl run summarize ./memory/notes.md --with claude
175
- brainctl run analyze ./memory/notes.md --with codex
176
- brainctl run summarize ./memory/notes.md --with claude --fallback codex
213
+ brainctl ui
177
214
  ```
178
215
 
216
+ Opens a local dashboard at `http://127.0.0.1:3333` with:
217
+
218
+ - **Agent Profiles** — See live MCPs and skills for Claude, Codex, and Gemini side-by-side
219
+ - **Drag & Drop** — Copy MCPs between agents by dragging cards
220
+ - **Staged Changes** — Preview adds/removes before applying, with undo support
221
+ - **Skills Editor** — Edit skill prompts with live preview
222
+ - **MCP Manager** — View and edit MCP configurations
223
+ - **Memory Viewer** — Browse shared markdown memory files
224
+ - **Run Console** — Execute skills with real-time streaming output
225
+
179
226
  ---
180
227
 
181
- ## 📂 Project Structure
228
+ ## 🏗️ Architecture
182
229
 
183
- ```text
230
+ ```
184
231
  brainctl/
185
232
  ├── src/
186
- │ ├── cli.ts
187
- │ ├── config.ts
188
- │ ├── context/
189
- ├── commands/
190
- │ ├── executor/
191
- └── services/
192
- ├── tests/
193
- ├── ai-stack.yaml
194
- ├── memory/
195
- ├── package.json
196
- └── tsconfig.json
233
+ │ ├── cli.ts # CLI entry point (Commander)
234
+ │ ├── commands/ # 8 command handlers
235
+ │ ├── services/ # 11 business logic services
236
+ │ └── sync/ # Agent config readers/writers
237
+ │ ├── context/ # Memory loader, skill resolver, context builder
238
+ ├── executor/ # Agent spawning (Claude, Codex, Gemini)
239
+ ├── mcp/ # FastMCP server (20 tools)
240
+ │ └── ui/ # HTTP server with SSE streaming
241
+ ├── web/src/ # React dashboard (Vite + dnd-kit)
242
+ ├── tests/ # Vitest test suite
243
+ └── ai-stack.yaml # Project config
197
244
  ```
198
245
 
246
+ ### Agent Config Locations
247
+
248
+ | Agent | Config Path | MCP Location |
249
+ |-------|-------------|-------------|
250
+ | Claude | `~/.claude.json` | `projects[cwd].mcpServers` |
251
+ | Codex | `~/.codex/config.toml` | `[mcp_servers.*]` |
252
+ | Gemini | `~/.gemini/settings.json` | `mcpServers` |
253
+
199
254
  ---
200
255
 
201
256
  ## 🧪 Development
202
257
 
203
258
  ```bash
204
259
  npm install
205
- npm test
206
- npm run build
260
+ npm test # Run all tests (Vitest)
261
+ npm run build # Build server (tsc) + web (Vite)
262
+ npm run dev -- <args> # Run CLI via tsx
207
263
  ```
208
264
 
209
265
  ---
210
266
 
211
- ## 🧠 Philosophy
212
-
213
- `brainctl` does not replace your AI tools.
214
-
215
- It sits between you and them as a thin orchestration layer:
267
+ ## 🤝 Contributing
216
268
 
217
- - You keep using Claude, Codex, and other agent CLIs
218
- - `brainctl` keeps the environment consistent
269
+ Contributions are welcome! Please open an issue or submit a pull request.
219
270
 
220
- ---
221
-
222
- ## 🗺 Roadmap
223
-
224
- - [ ] JSON output mode
225
- - [ ] Multi-agent pipelines
226
- - [ ] MCP runtime integration
227
- - [ ] Better execution tracing and logs
228
- - [ ] UI / dashboard
271
+ ```bash
272
+ git clone https://github.com/Rorogogogo/brainctl.git
273
+ cd brainctl
274
+ npm install
275
+ npm test
276
+ ```
229
277
 
230
278
  ---
231
279
 
232
- ## 💡 Inspiration
280
+ ## 💡 Philosophy
233
281
 
234
- AI tools are getting more powerful, but also more fragmented.
282
+ brainctl doesn't replace your AI tools. It sits between you and them as a thin orchestration layer:
235
283
 
236
- `brainctl` is an attempt to bring state, structure, and consistency to that workflow.
284
+ - **You keep using** Claude Code, Codex, and Gemini CLI directly
285
+ - **brainctl keeps** the environment consistent across all of them
286
+ - **Profiles make it portable** — share your setup with your team
237
287
 
238
288
  ---
239
289
 
240
290
  ## 📄 License
241
291
 
242
- MIT
292
+ [MIT](https://opensource.org/licenses/MIT) — use it however you want.
@@ -1,9 +1,7 @@
1
- import { access } from 'node:fs/promises';
2
- import { constants } from 'node:fs';
3
- import path from 'node:path';
4
1
  import { AgentNotAvailableError } from '../errors.js';
5
2
  import { ClaudeExecutor } from './claude.js';
6
3
  import { CodexExecutor } from './codex.js';
4
+ import { findExecutable } from '../system/executables.js';
7
5
  const SUPPORTED_AGENTS = ['claude', 'codex', 'gemini'];
8
6
  const AGENT_COMMANDS = {
9
7
  claude: 'claude',
@@ -60,38 +58,3 @@ async function checkAvailability(agentName) {
60
58
  resolvedPath: resolvedPath ?? undefined
61
59
  };
62
60
  }
63
- async function findExecutable(command) {
64
- if (command.includes(path.sep)) {
65
- return (await isExecutable(command)) ? command : null;
66
- }
67
- const pathEntries = (process.env.PATH ?? '')
68
- .split(path.delimiter)
69
- .filter((entry) => entry.length > 0);
70
- const extensions = process.platform === 'win32'
71
- ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM')
72
- .split(';')
73
- .filter((entry) => entry.length > 0)
74
- : [''];
75
- for (const pathEntry of pathEntries) {
76
- for (const extension of extensions) {
77
- const candidate = process.platform === 'win32' &&
78
- extension.length > 0 &&
79
- !command.toLowerCase().endsWith(extension.toLowerCase())
80
- ? path.join(pathEntry, `${command}${extension}`)
81
- : path.join(pathEntry, command);
82
- if (await isExecutable(candidate)) {
83
- return candidate;
84
- }
85
- }
86
- }
87
- return null;
88
- }
89
- async function isExecutable(filePath) {
90
- try {
91
- await access(filePath, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
92
- return true;
93
- }
94
- catch {
95
- return false;
96
- }
97
- }
@@ -4,7 +4,9 @@ import { FastMCP } from 'fastmcp';
4
4
  import { z } from 'zod';
5
5
  import { loadConfig } from '../config.js';
6
6
  import { loadMemory } from '../context/memory.js';
7
+ import { createAgentConfigService } from '../services/agent-config-service.js';
7
8
  import { createDoctorService } from '../services/doctor-service.js';
9
+ import { startUiServer } from '../ui/server.js';
8
10
  import { createMemoryWriteService } from '../services/memory-write-service.js';
9
11
  import { createProfileExportService } from '../services/profile-export-service.js';
10
12
  import { createProfileImportService } from '../services/profile-import-service.js';
@@ -177,6 +179,111 @@ export function createMcpServer(options = {}) {
177
179
  return JSON.stringify(result, null, 2);
178
180
  },
179
181
  });
182
+ server.addTool({
183
+ name: 'brainctl_get_profile',
184
+ description: 'Get the full config of a profile including all skills, MCPs, and memory paths.',
185
+ parameters: z.object({
186
+ name: z.string().describe('Profile name'),
187
+ }),
188
+ execute: async (args) => {
189
+ const profileService = createProfileService();
190
+ const profile = await profileService.get({ cwd, name: args.name });
191
+ return JSON.stringify(profile, null, 2);
192
+ },
193
+ });
194
+ server.addTool({
195
+ name: 'brainctl_create_profile',
196
+ description: 'Create a new profile with a default example skill.',
197
+ parameters: z.object({
198
+ name: z.string().describe('Profile name to create'),
199
+ description: z.string().optional().describe('Profile description'),
200
+ }),
201
+ execute: async (args) => {
202
+ const profileService = createProfileService();
203
+ const result = await profileService.create({
204
+ cwd,
205
+ name: args.name,
206
+ description: args.description,
207
+ });
208
+ return JSON.stringify(result, null, 2);
209
+ },
210
+ });
211
+ server.addTool({
212
+ name: 'brainctl_update_profile',
213
+ description: 'Update a profile config. Pass the full profile object with skills, mcps, and memory fields. Use this to add, remove, or modify skills and MCPs within a profile.',
214
+ parameters: z.object({
215
+ name: z.string().describe('Profile name to update'),
216
+ config: z.object({
217
+ name: z.string(),
218
+ description: z.string().optional(),
219
+ skills: z.record(z.string(), z.object({
220
+ description: z.string().optional(),
221
+ prompt: z.string(),
222
+ })),
223
+ mcps: z.record(z.string(), z.unknown()),
224
+ memory: z.object({
225
+ paths: z.array(z.string()),
226
+ }),
227
+ }).describe('Full profile config object'),
228
+ }),
229
+ execute: async (args) => {
230
+ const profileService = createProfileService();
231
+ await profileService.update({
232
+ cwd,
233
+ name: args.name,
234
+ config: args.config,
235
+ });
236
+ return JSON.stringify({ ok: true, updated: args.name });
237
+ },
238
+ });
239
+ server.addTool({
240
+ name: 'brainctl_delete_profile',
241
+ description: 'Delete a profile. Cannot delete the currently active profile.',
242
+ parameters: z.object({
243
+ name: z.string().describe('Profile name to delete'),
244
+ }),
245
+ execute: async (args) => {
246
+ const profileService = createProfileService();
247
+ await profileService.delete({ cwd, name: args.name });
248
+ return JSON.stringify({ ok: true, deleted: args.name });
249
+ },
250
+ });
251
+ server.addTool({
252
+ name: 'brainctl_copy_profile_items',
253
+ description: 'Copy skills and/or MCPs from one profile to another. Specify which skill and MCP keys to copy. Existing items with the same key in the target are overwritten.',
254
+ parameters: z.object({
255
+ source: z.string().describe('Source profile name'),
256
+ target: z.string().describe('Target profile name'),
257
+ skills: z.array(z.string()).default([]).describe('Skill keys to copy'),
258
+ mcps: z.array(z.string()).default([]).describe('MCP keys to copy'),
259
+ }),
260
+ execute: async (args) => {
261
+ const profileService = createProfileService();
262
+ const sourceProfile = await profileService.get({ cwd, name: args.source });
263
+ const targetProfile = await profileService.get({ cwd, name: args.target });
264
+ const copiedSkills = [];
265
+ const copiedMcps = [];
266
+ for (const key of args.skills) {
267
+ if (sourceProfile.skills[key]) {
268
+ targetProfile.skills[key] = sourceProfile.skills[key];
269
+ copiedSkills.push(key);
270
+ }
271
+ }
272
+ for (const key of args.mcps) {
273
+ if (sourceProfile.mcps[key]) {
274
+ targetProfile.mcps[key] = sourceProfile.mcps[key];
275
+ copiedMcps.push(key);
276
+ }
277
+ }
278
+ await profileService.update({ cwd, name: args.target, config: targetProfile });
279
+ return JSON.stringify({
280
+ source: args.source,
281
+ target: args.target,
282
+ copiedSkills,
283
+ copiedMcps,
284
+ }, null, 2);
285
+ },
286
+ });
180
287
  server.addTool({
181
288
  name: 'brainctl_export_profile',
182
289
  description: 'Export a profile as a portable tarball. Packages the profile config and bundled MCP source code for sharing.',
@@ -211,6 +318,82 @@ export function createMcpServer(options = {}) {
211
318
  return JSON.stringify(result, null, 2);
212
319
  },
213
320
  });
321
+ let uiServerInstance = null;
322
+ server.addTool({
323
+ name: 'brainctl_open_ui',
324
+ description: 'Start the brainctl web dashboard. Returns the URL to open in a browser. If already running, returns the existing URL.',
325
+ parameters: z.object({
326
+ port: z.number().default(3333).describe('Port number for the UI server'),
327
+ }),
328
+ execute: async (args) => {
329
+ if (uiServerInstance) {
330
+ return JSON.stringify({ url: uiServerInstance.url, status: 'already_running' });
331
+ }
332
+ try {
333
+ uiServerInstance = await startUiServer({ cwd, port: args.port });
334
+ return JSON.stringify({ url: uiServerInstance.url, status: 'started' });
335
+ }
336
+ catch (err) {
337
+ return JSON.stringify({ error: err.message, status: 'failed' });
338
+ }
339
+ },
340
+ });
341
+ server.addTool({
342
+ name: 'brainctl_close_ui',
343
+ description: 'Stop the brainctl web dashboard if it is running.',
344
+ parameters: z.object({}),
345
+ execute: async () => {
346
+ if (!uiServerInstance) {
347
+ return JSON.stringify({ status: 'not_running' });
348
+ }
349
+ await uiServerInstance.close();
350
+ uiServerInstance = null;
351
+ return JSON.stringify({ status: 'stopped' });
352
+ },
353
+ });
354
+ server.addTool({
355
+ name: 'brainctl_read_agent_configs',
356
+ description: 'Read the live MCP configs from all agents (Claude, Codex, Gemini). Shows what is actually configured in each agent right now, by reading their real config files.',
357
+ parameters: z.object({}),
358
+ execute: async () => {
359
+ const agentConfigService = createAgentConfigService();
360
+ const configs = await agentConfigService.readAll({ cwd });
361
+ return JSON.stringify(configs, null, 2);
362
+ },
363
+ });
364
+ server.addTool({
365
+ name: 'brainctl_add_agent_mcp',
366
+ description: 'Add or overwrite an MCP server entry in a specific agent config. Writes directly to the agent config file (e.g., ~/.claude.json).',
367
+ parameters: z.object({
368
+ agent: z.enum(['claude', 'codex', 'gemini']).describe('Target agent'),
369
+ key: z.string().describe('MCP server name/key'),
370
+ command: z.string().describe('Command to run the MCP server'),
371
+ args: z.array(z.string()).default([]).describe('Arguments for the command'),
372
+ }),
373
+ execute: async (args) => {
374
+ const agentConfigService = createAgentConfigService();
375
+ await agentConfigService.addMcp({
376
+ cwd,
377
+ agent: args.agent,
378
+ key: args.key,
379
+ entry: { command: args.command, args: args.args },
380
+ });
381
+ return JSON.stringify({ ok: true, agent: args.agent, key: args.key });
382
+ },
383
+ });
384
+ server.addTool({
385
+ name: 'brainctl_remove_agent_mcp',
386
+ description: 'Remove an MCP server entry from a specific agent config.',
387
+ parameters: z.object({
388
+ agent: z.enum(['claude', 'codex', 'gemini']).describe('Target agent'),
389
+ key: z.string().describe('MCP server name/key to remove'),
390
+ }),
391
+ execute: async (args) => {
392
+ const agentConfigService = createAgentConfigService();
393
+ await agentConfigService.removeMcp({ cwd, agent: args.agent, key: args.key });
394
+ return JSON.stringify({ ok: true, agent: args.agent, removed: args.key });
395
+ },
396
+ });
214
397
  return server;
215
398
  }
216
399
  export async function startMcpServer(options = {}) {