brainctl 0.1.4 → 0.1.6

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.
@@ -4,8 +4,11 @@ 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';
8
9
  import { createMemoryWriteService } from '../services/memory-write-service.js';
10
+ import { createProfileExportService } from '../services/profile-export-service.js';
11
+ import { createProfileImportService } from '../services/profile-import-service.js';
9
12
  import { createProfileService } from '../services/profile-service.js';
10
13
  import { createRunService } from '../services/run-service.js';
11
14
  import { createStatusService } from '../services/status-service.js';
@@ -175,6 +178,188 @@ export function createMcpServer(options = {}) {
175
178
  return JSON.stringify(result, null, 2);
176
179
  },
177
180
  });
181
+ server.addTool({
182
+ name: 'brainctl_get_profile',
183
+ description: 'Get the full config of a profile including all skills, MCPs, and memory paths.',
184
+ parameters: z.object({
185
+ name: z.string().describe('Profile name'),
186
+ }),
187
+ execute: async (args) => {
188
+ const profileService = createProfileService();
189
+ const profile = await profileService.get({ cwd, name: args.name });
190
+ return JSON.stringify(profile, null, 2);
191
+ },
192
+ });
193
+ server.addTool({
194
+ name: 'brainctl_create_profile',
195
+ description: 'Create a new profile with a default example skill.',
196
+ parameters: z.object({
197
+ name: z.string().describe('Profile name to create'),
198
+ description: z.string().optional().describe('Profile description'),
199
+ }),
200
+ execute: async (args) => {
201
+ const profileService = createProfileService();
202
+ const result = await profileService.create({
203
+ cwd,
204
+ name: args.name,
205
+ description: args.description,
206
+ });
207
+ return JSON.stringify(result, null, 2);
208
+ },
209
+ });
210
+ server.addTool({
211
+ name: 'brainctl_update_profile',
212
+ 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.',
213
+ parameters: z.object({
214
+ name: z.string().describe('Profile name to update'),
215
+ config: z.object({
216
+ name: z.string(),
217
+ description: z.string().optional(),
218
+ skills: z.record(z.string(), z.object({
219
+ description: z.string().optional(),
220
+ prompt: z.string(),
221
+ })),
222
+ mcps: z.record(z.string(), z.unknown()),
223
+ memory: z.object({
224
+ paths: z.array(z.string()),
225
+ }),
226
+ }).describe('Full profile config object'),
227
+ }),
228
+ execute: async (args) => {
229
+ const profileService = createProfileService();
230
+ await profileService.update({
231
+ cwd,
232
+ name: args.name,
233
+ config: args.config,
234
+ });
235
+ return JSON.stringify({ ok: true, updated: args.name });
236
+ },
237
+ });
238
+ server.addTool({
239
+ name: 'brainctl_delete_profile',
240
+ description: 'Delete a profile. Cannot delete the currently active profile.',
241
+ parameters: z.object({
242
+ name: z.string().describe('Profile name to delete'),
243
+ }),
244
+ execute: async (args) => {
245
+ const profileService = createProfileService();
246
+ await profileService.delete({ cwd, name: args.name });
247
+ return JSON.stringify({ ok: true, deleted: args.name });
248
+ },
249
+ });
250
+ server.addTool({
251
+ name: 'brainctl_copy_profile_items',
252
+ 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.',
253
+ parameters: z.object({
254
+ source: z.string().describe('Source profile name'),
255
+ target: z.string().describe('Target profile name'),
256
+ skills: z.array(z.string()).default([]).describe('Skill keys to copy'),
257
+ mcps: z.array(z.string()).default([]).describe('MCP keys to copy'),
258
+ }),
259
+ execute: async (args) => {
260
+ const profileService = createProfileService();
261
+ const sourceProfile = await profileService.get({ cwd, name: args.source });
262
+ const targetProfile = await profileService.get({ cwd, name: args.target });
263
+ const copiedSkills = [];
264
+ const copiedMcps = [];
265
+ for (const key of args.skills) {
266
+ if (sourceProfile.skills[key]) {
267
+ targetProfile.skills[key] = sourceProfile.skills[key];
268
+ copiedSkills.push(key);
269
+ }
270
+ }
271
+ for (const key of args.mcps) {
272
+ if (sourceProfile.mcps[key]) {
273
+ targetProfile.mcps[key] = sourceProfile.mcps[key];
274
+ copiedMcps.push(key);
275
+ }
276
+ }
277
+ await profileService.update({ cwd, name: args.target, config: targetProfile });
278
+ return JSON.stringify({
279
+ source: args.source,
280
+ target: args.target,
281
+ copiedSkills,
282
+ copiedMcps,
283
+ }, null, 2);
284
+ },
285
+ });
286
+ server.addTool({
287
+ name: 'brainctl_export_profile',
288
+ description: 'Export a profile as a portable tarball. Packages the profile config and bundled MCP source code for sharing.',
289
+ parameters: z.object({
290
+ name: z.string().describe('Profile name to export'),
291
+ output_path: z.string().optional().describe('Output file path (defaults to <name>.tar.gz in cwd)'),
292
+ }),
293
+ execute: async (args) => {
294
+ const exportService = createProfileExportService();
295
+ const result = await exportService.execute({
296
+ cwd,
297
+ name: args.name,
298
+ outputPath: args.output_path,
299
+ });
300
+ return JSON.stringify(result, null, 2);
301
+ },
302
+ });
303
+ server.addTool({
304
+ name: 'brainctl_import_profile',
305
+ description: 'Import a profile from a tarball. Extracts bundled MCP source, installs dependencies, and registers the profile.',
306
+ parameters: z.object({
307
+ archive_path: z.string().describe('Path to the profile tarball'),
308
+ force: z.boolean().default(false).describe('Overwrite existing profile if it exists'),
309
+ }),
310
+ execute: async (args) => {
311
+ const importService = createProfileImportService();
312
+ const result = await importService.execute({
313
+ cwd,
314
+ archivePath: args.archive_path,
315
+ force: args.force,
316
+ });
317
+ return JSON.stringify(result, null, 2);
318
+ },
319
+ });
320
+ server.addTool({
321
+ name: 'brainctl_read_agent_configs',
322
+ 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.',
323
+ parameters: z.object({}),
324
+ execute: async () => {
325
+ const agentConfigService = createAgentConfigService();
326
+ const configs = await agentConfigService.readAll({ cwd });
327
+ return JSON.stringify(configs, null, 2);
328
+ },
329
+ });
330
+ server.addTool({
331
+ name: 'brainctl_add_agent_mcp',
332
+ description: 'Add or overwrite an MCP server entry in a specific agent config. Writes directly to the agent config file (e.g., ~/.claude.json).',
333
+ parameters: z.object({
334
+ agent: z.enum(['claude', 'codex', 'gemini']).describe('Target agent'),
335
+ key: z.string().describe('MCP server name/key'),
336
+ command: z.string().describe('Command to run the MCP server'),
337
+ args: z.array(z.string()).default([]).describe('Arguments for the command'),
338
+ }),
339
+ execute: async (args) => {
340
+ const agentConfigService = createAgentConfigService();
341
+ await agentConfigService.addMcp({
342
+ cwd,
343
+ agent: args.agent,
344
+ key: args.key,
345
+ entry: { command: args.command, args: args.args },
346
+ });
347
+ return JSON.stringify({ ok: true, agent: args.agent, key: args.key });
348
+ },
349
+ });
350
+ server.addTool({
351
+ name: 'brainctl_remove_agent_mcp',
352
+ description: 'Remove an MCP server entry from a specific agent config.',
353
+ parameters: z.object({
354
+ agent: z.enum(['claude', 'codex', 'gemini']).describe('Target agent'),
355
+ key: z.string().describe('MCP server name/key to remove'),
356
+ }),
357
+ execute: async (args) => {
358
+ const agentConfigService = createAgentConfigService();
359
+ await agentConfigService.removeMcp({ cwd, agent: args.agent, key: args.key });
360
+ return JSON.stringify({ ok: true, agent: args.agent, removed: args.key });
361
+ },
362
+ });
178
363
  return server;
179
364
  }
180
365
  export async function startMcpServer(options = {}) {
@@ -0,0 +1,20 @@
1
+ import type { AgentName } from '../types.js';
2
+ import { type AgentLiveConfig, type AgentMcpEntry } from './sync/agent-reader.js';
3
+ export type { AgentLiveConfig, AgentMcpEntry, AgentSkillEntry } from './sync/agent-reader.js';
4
+ export interface AgentConfigService {
5
+ readAll(options: {
6
+ cwd: string;
7
+ }): Promise<AgentLiveConfig[]>;
8
+ addMcp(options: {
9
+ cwd: string;
10
+ agent: AgentName;
11
+ key: string;
12
+ entry: AgentMcpEntry;
13
+ }): Promise<void>;
14
+ removeMcp(options: {
15
+ cwd: string;
16
+ agent: AgentName;
17
+ key: string;
18
+ }): Promise<void>;
19
+ }
20
+ export declare function createAgentConfigService(): AgentConfigService;
@@ -0,0 +1,189 @@
1
+ import { copyFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { createClaudeReader, createCodexReader, createGeminiReader, } from './sync/agent-reader.js';
5
+ import { formatTimestamp } from './sync/agent-writer.js';
6
+ const readers = {
7
+ claude: createClaudeReader(),
8
+ codex: createCodexReader(),
9
+ gemini: createGeminiReader(),
10
+ };
11
+ export function createAgentConfigService() {
12
+ return {
13
+ async readAll(options) {
14
+ const results = await Promise.all([
15
+ readers.claude.read(options),
16
+ readers.codex.read(options),
17
+ readers.gemini.read(options),
18
+ ]);
19
+ return results;
20
+ },
21
+ async addMcp(options) {
22
+ const { cwd, agent, key, entry } = options;
23
+ if (agent === 'claude') {
24
+ await mutateClaudeConfig(cwd, (servers) => {
25
+ servers[key] = toClaudeEntry(entry);
26
+ });
27
+ }
28
+ else if (agent === 'codex') {
29
+ await mutateCodexConfig((servers) => {
30
+ servers[key] = entry;
31
+ });
32
+ }
33
+ else if (agent === 'gemini') {
34
+ await mutateGeminiConfig(cwd, (servers) => {
35
+ servers[key] = toGeminiEntry(entry);
36
+ });
37
+ }
38
+ },
39
+ async removeMcp(options) {
40
+ const { cwd, agent, key } = options;
41
+ if (agent === 'claude') {
42
+ await mutateClaudeConfig(cwd, (servers) => {
43
+ delete servers[key];
44
+ });
45
+ }
46
+ else if (agent === 'codex') {
47
+ await mutateCodexConfig((servers) => {
48
+ delete servers[key];
49
+ });
50
+ }
51
+ else if (agent === 'gemini') {
52
+ await mutateGeminiConfig(cwd, (servers) => {
53
+ delete servers[key];
54
+ });
55
+ }
56
+ },
57
+ };
58
+ }
59
+ /* ---- Claude: JSON with projects[cwd].mcpServers ---- */
60
+ async function mutateClaudeConfig(cwd, mutate) {
61
+ const configPath = path.join(homedir(), '.claude.json');
62
+ let existing = {};
63
+ try {
64
+ existing = JSON.parse(await readFile(configPath, 'utf8'));
65
+ await backupFile(configPath);
66
+ }
67
+ catch {
68
+ // fresh config
69
+ }
70
+ const projects = (existing.projects ?? {});
71
+ const projectConfig = projects[cwd] ?? {};
72
+ const servers = (projectConfig.mcpServers ?? {});
73
+ mutate(servers);
74
+ projectConfig.mcpServers = servers;
75
+ projects[cwd] = projectConfig;
76
+ existing.projects = projects;
77
+ await atomicWriteJson(configPath, existing);
78
+ }
79
+ function toClaudeEntry(entry) {
80
+ return {
81
+ type: 'stdio',
82
+ command: entry.command,
83
+ args: entry.args ?? [],
84
+ ...(entry.env ? { env: entry.env } : {}),
85
+ };
86
+ }
87
+ /* ---- Codex: TOML with [mcp_servers.*] ---- */
88
+ async function mutateCodexConfig(mutate) {
89
+ const configPath = path.join(homedir(), '.codex', 'config.toml');
90
+ let existingContent = '';
91
+ try {
92
+ existingContent = await readFile(configPath, 'utf8');
93
+ await backupFile(configPath);
94
+ }
95
+ catch {
96
+ // fresh config
97
+ }
98
+ // Read current servers via reader
99
+ const current = await readers.codex.read({ cwd: '' });
100
+ const servers = { ...current.mcpServers };
101
+ mutate(servers);
102
+ // Rebuild: preserve non-mcp content + new mcp sections
103
+ const nonMcp = stripCodexMcpSections(existingContent).trim();
104
+ const mcpToml = buildCodexMcpToml(servers);
105
+ const final = nonMcp.length > 0 ? `${nonMcp}\n\n${mcpToml}` : mcpToml;
106
+ await mkdir(path.dirname(configPath), { recursive: true });
107
+ await atomicWrite(configPath, final + '\n');
108
+ }
109
+ function stripCodexMcpSections(content) {
110
+ const lines = content.split('\n');
111
+ const result = [];
112
+ let inMcp = false;
113
+ for (const line of lines) {
114
+ if (/^\[mcp_servers[\].]/.test(line)) {
115
+ inMcp = true;
116
+ continue;
117
+ }
118
+ if (inMcp && /^\[/.test(line) && !/^\[mcp_servers[\].]/.test(line)) {
119
+ inMcp = false;
120
+ }
121
+ if (!inMcp)
122
+ result.push(line);
123
+ }
124
+ return result.join('\n');
125
+ }
126
+ function buildCodexMcpToml(servers) {
127
+ const lines = [];
128
+ for (const [name, entry] of Object.entries(servers)) {
129
+ lines.push(`[mcp_servers.${name}]`);
130
+ lines.push(`command = ${tomlStr(entry.command)}`);
131
+ if (entry.args && entry.args.length > 0) {
132
+ lines.push(`args = [${entry.args.map(tomlStr).join(', ')}]`);
133
+ }
134
+ if (entry.env && Object.keys(entry.env).length > 0) {
135
+ lines.push('');
136
+ lines.push(`[mcp_servers.${name}.env]`);
137
+ for (const [k, v] of Object.entries(entry.env)) {
138
+ lines.push(`${k} = ${tomlStr(v)}`);
139
+ }
140
+ }
141
+ lines.push('');
142
+ }
143
+ return lines.join('\n').trim();
144
+ }
145
+ function tomlStr(value) {
146
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
147
+ }
148
+ /* ---- Gemini: JSON with mcpServers ---- */
149
+ async function mutateGeminiConfig(_cwd, mutate) {
150
+ const configPath = path.join(homedir(), '.gemini', 'settings.json');
151
+ let existing = {};
152
+ try {
153
+ existing = JSON.parse(await readFile(configPath, 'utf8'));
154
+ await backupFile(configPath);
155
+ }
156
+ catch {
157
+ // fresh config
158
+ }
159
+ const servers = (existing.mcpServers ?? {});
160
+ mutate(servers);
161
+ existing.mcpServers = servers;
162
+ await mkdir(path.dirname(configPath), { recursive: true });
163
+ await atomicWriteJson(configPath, existing);
164
+ }
165
+ function toGeminiEntry(entry) {
166
+ return {
167
+ command: entry.command,
168
+ args: entry.args ?? [],
169
+ ...(entry.env ? { env: entry.env } : {}),
170
+ };
171
+ }
172
+ /* ---- Shared helpers ---- */
173
+ async function backupFile(filePath) {
174
+ const backupPath = `${filePath}.bak.${formatTimestamp()}`;
175
+ try {
176
+ await copyFile(filePath, backupPath);
177
+ }
178
+ catch {
179
+ // File may not exist yet
180
+ }
181
+ }
182
+ async function atomicWriteJson(filePath, data) {
183
+ await atomicWrite(filePath, JSON.stringify(data, null, 2) + '\n');
184
+ }
185
+ async function atomicWrite(filePath, content) {
186
+ const tmpPath = `${filePath}.tmp.${Date.now()}`;
187
+ await writeFile(tmpPath, content, 'utf8');
188
+ await rename(tmpPath, filePath);
189
+ }
@@ -0,0 +1,15 @@
1
+ import { type ProfileService } from './profile-service.js';
2
+ export interface ProfileExportService {
3
+ execute(options: {
4
+ cwd?: string;
5
+ name: string;
6
+ outputPath?: string;
7
+ }): Promise<{
8
+ archivePath: string;
9
+ }>;
10
+ }
11
+ interface ProfileExportDependencies {
12
+ profileService?: ProfileService;
13
+ }
14
+ export declare function createProfileExportService(deps?: ProfileExportDependencies): ProfileExportService;
15
+ export {};
@@ -0,0 +1,63 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import YAML from 'yaml';
6
+ import { createProfileService } from './profile-service.js';
7
+ export function createProfileExportService(deps = {}) {
8
+ const profileService = deps.profileService ?? createProfileService();
9
+ return {
10
+ async execute(options) {
11
+ const cwd = options.cwd ?? process.cwd();
12
+ const profile = await profileService.get({ cwd, name: options.name });
13
+ const stagingDir = await mkdtemp(path.join(tmpdir(), 'brainctl-export-'));
14
+ try {
15
+ const exportProfile = await stageProfile(profile, cwd, stagingDir);
16
+ await writeFile(path.join(stagingDir, 'profile.yaml'), YAML.stringify(exportProfile), 'utf8');
17
+ const outputPath = options.outputPath ?? path.join(cwd, `${profile.name}.tar.gz`);
18
+ execSync(`tar -czf "${outputPath}" -C "${stagingDir}" .`, {
19
+ stdio: 'pipe',
20
+ });
21
+ return { archivePath: outputPath };
22
+ }
23
+ finally {
24
+ await rm(stagingDir, { recursive: true, force: true });
25
+ }
26
+ },
27
+ };
28
+ }
29
+ async function stageProfile(profile, cwd, stagingDir) {
30
+ const mcpsDir = path.join(stagingDir, 'mcps');
31
+ const exportMcps = {};
32
+ for (const [name, mcp] of Object.entries(profile.mcps)) {
33
+ if (mcp.type === 'bundled') {
34
+ const sourcePath = path.isAbsolute(mcp.path)
35
+ ? mcp.path
36
+ : path.resolve(cwd, mcp.path);
37
+ const destPath = path.join(mcpsDir, name);
38
+ await mkdir(destPath, { recursive: true });
39
+ await cp(sourcePath, destPath, {
40
+ recursive: true,
41
+ filter: (src) => !src.includes('node_modules'),
42
+ });
43
+ exportMcps[name] = {
44
+ type: 'bundled',
45
+ path: `./mcps/${name}`,
46
+ ...(mcp.install ? { install: mcp.install } : {}),
47
+ command: mcp.command,
48
+ ...(mcp.args ? { args: mcp.args } : {}),
49
+ ...(mcp.env ? { env: mcp.env } : {}),
50
+ };
51
+ }
52
+ else {
53
+ exportMcps[name] = mcp;
54
+ }
55
+ }
56
+ return {
57
+ name: profile.name,
58
+ ...(profile.description ? { description: profile.description } : {}),
59
+ skills: profile.skills,
60
+ mcps: exportMcps,
61
+ memory: profile.memory,
62
+ };
63
+ }
@@ -0,0 +1,11 @@
1
+ export interface ProfileImportService {
2
+ execute(options: {
3
+ cwd?: string;
4
+ archivePath: string;
5
+ force?: boolean;
6
+ }): Promise<{
7
+ profileName: string;
8
+ installedMcps: string[];
9
+ }>;
10
+ }
11
+ export declare function createProfileImportService(): ProfileImportService;
@@ -0,0 +1,81 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import YAML from 'yaml';
6
+ import { ProfileError } from '../errors.js';
7
+ import { parseProfile } from './profile-service.js';
8
+ const PROFILES_DIR = '.brainctl/profiles';
9
+ export function createProfileImportService() {
10
+ return {
11
+ async execute(options) {
12
+ const cwd = options.cwd ?? process.cwd();
13
+ const archivePath = path.resolve(cwd, options.archivePath);
14
+ try {
15
+ await stat(archivePath);
16
+ }
17
+ catch {
18
+ throw new ProfileError(`Archive not found: ${archivePath}`);
19
+ }
20
+ const extractDir = await mkdtemp(path.join(tmpdir(), 'brainctl-import-'));
21
+ try {
22
+ execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
23
+ stdio: 'pipe',
24
+ });
25
+ const profileSource = await readFile(path.join(extractDir, 'profile.yaml'), 'utf8');
26
+ const profile = parseProfile(profileSource, 'imported');
27
+ const profileName = profile.name;
28
+ const profilePath = path.join(cwd, PROFILES_DIR, `${profileName}.yaml`);
29
+ if (!options.force) {
30
+ try {
31
+ await stat(profilePath);
32
+ throw new ProfileError(`Profile "${profileName}" already exists. Use --force to overwrite.`);
33
+ }
34
+ catch (err) {
35
+ if (err instanceof ProfileError)
36
+ throw err;
37
+ }
38
+ }
39
+ const installedMcps = [];
40
+ const mcpsBaseDir = path.join(cwd, PROFILES_DIR, profileName, 'mcps');
41
+ for (const [name, mcp] of Object.entries(profile.mcps)) {
42
+ if (mcp.type !== 'bundled')
43
+ continue;
44
+ const extractedMcpPath = path.join(extractDir, 'mcps', name);
45
+ const destMcpPath = path.join(mcpsBaseDir, name);
46
+ try {
47
+ await stat(extractedMcpPath);
48
+ }
49
+ catch {
50
+ throw new ProfileError(`Bundled MCP "${name}" source not found in archive.`);
51
+ }
52
+ await mkdir(destMcpPath, { recursive: true });
53
+ await cp(extractedMcpPath, destMcpPath, { recursive: true });
54
+ const installCmd = mcp.install ?? 'npm install';
55
+ execSync(installCmd, {
56
+ cwd: destMcpPath,
57
+ stdio: 'pipe',
58
+ });
59
+ profile.mcps[name] = {
60
+ ...mcp,
61
+ path: destMcpPath,
62
+ };
63
+ installedMcps.push(name);
64
+ }
65
+ const outputYaml = {
66
+ name: profile.name,
67
+ ...(profile.description ? { description: profile.description } : {}),
68
+ skills: profile.skills,
69
+ mcps: profile.mcps,
70
+ memory: profile.memory,
71
+ };
72
+ await mkdir(path.dirname(profilePath), { recursive: true });
73
+ await writeFile(profilePath, YAML.stringify(outputYaml), 'utf8');
74
+ return { profileName, installedMcps };
75
+ }
76
+ finally {
77
+ await rm(extractDir, { recursive: true, force: true });
78
+ }
79
+ },
80
+ };
81
+ }
@@ -17,6 +17,15 @@ export interface ProfileService {
17
17
  }): Promise<{
18
18
  profilePath: string;
19
19
  }>;
20
+ update(options: {
21
+ cwd?: string;
22
+ name: string;
23
+ config: ProfileConfig;
24
+ }): Promise<void>;
25
+ delete(options: {
26
+ cwd?: string;
27
+ name: string;
28
+ }): Promise<void>;
20
29
  use(options: {
21
30
  cwd?: string;
22
31
  name: string;
@@ -28,3 +37,4 @@ export interface ProfileService {
28
37
  }): Promise<BrainctlMetaConfig>;
29
38
  }
30
39
  export declare function createProfileService(): ProfileService;
40
+ export declare function parseProfile(source: string, name: string): ProfileConfig;
@@ -1,4 +1,4 @@
1
- import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
1
+ import { readdir, readFile, writeFile, mkdir, stat, unlink } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
4
  import { ProfileError, ProfileNotFoundError } from '../errors.js';
@@ -64,6 +64,33 @@ export function createProfileService() {
64
64
  await writeFile(profilePath, YAML.stringify(scaffold), 'utf8');
65
65
  return { profilePath };
66
66
  },
67
+ async update(options) {
68
+ const cwd = options.cwd ?? process.cwd();
69
+ const profilePath = path.join(cwd, PROFILES_DIR, `${options.name}.yaml`);
70
+ if (!(await pathExists(profilePath))) {
71
+ throw new ProfileNotFoundError(`Profile "${options.name}" not found.`);
72
+ }
73
+ const data = {
74
+ name: options.config.name,
75
+ ...(options.config.description ? { description: options.config.description } : {}),
76
+ skills: options.config.skills,
77
+ mcps: options.config.mcps,
78
+ memory: options.config.memory,
79
+ };
80
+ await writeFile(profilePath, YAML.stringify(data), 'utf8');
81
+ },
82
+ async delete(options) {
83
+ const cwd = options.cwd ?? process.cwd();
84
+ const profilePath = path.join(cwd, PROFILES_DIR, `${options.name}.yaml`);
85
+ if (!(await pathExists(profilePath))) {
86
+ throw new ProfileNotFoundError(`Profile "${options.name}" not found.`);
87
+ }
88
+ const meta = await loadMetaConfig(cwd);
89
+ if (meta.active_profile === options.name) {
90
+ throw new ProfileError('Cannot delete the active profile.');
91
+ }
92
+ await unlink(profilePath);
93
+ },
67
94
  async use(options) {
68
95
  const cwd = options.cwd ?? process.cwd();
69
96
  // Validate profile exists
@@ -99,7 +126,7 @@ async function loadMetaConfig(cwd) {
99
126
  return { active_profile: '', agents: ['claude', 'codex', 'gemini'] };
100
127
  }
101
128
  }
102
- function parseProfile(source, name) {
129
+ export function parseProfile(source, name) {
103
130
  let parsed;
104
131
  try {
105
132
  parsed = YAML.parse(source) ?? {};
@@ -0,0 +1,25 @@
1
+ import type { AgentName } from '../../types.js';
2
+ export interface AgentMcpEntry {
3
+ command: string;
4
+ args?: string[];
5
+ env?: Record<string, string>;
6
+ }
7
+ export interface AgentSkillEntry {
8
+ name: string;
9
+ source?: string;
10
+ }
11
+ export interface AgentLiveConfig {
12
+ agent: AgentName;
13
+ configPath: string;
14
+ exists: boolean;
15
+ mcpServers: Record<string, AgentMcpEntry>;
16
+ skills: AgentSkillEntry[];
17
+ }
18
+ export interface AgentConfigReader {
19
+ read(options: {
20
+ cwd: string;
21
+ }): Promise<AgentLiveConfig>;
22
+ }
23
+ export declare function createClaudeReader(): AgentConfigReader;
24
+ export declare function createCodexReader(): AgentConfigReader;
25
+ export declare function createGeminiReader(): AgentConfigReader;