brainctl 0.1.3 → 0.1.4
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.
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +9 -0
- package/dist/commands/profile.d.ts +3 -0
- package/dist/commands/profile.js +43 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.js +31 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +15 -0
- package/dist/executor/resolver.js +5 -2
- package/dist/mcp/server.js +95 -0
- package/dist/services/profile-service.d.ts +30 -0
- package/dist/services/profile-service.js +190 -0
- package/dist/services/sync/agent-writer.d.ts +18 -0
- package/dist/services/sync/agent-writer.js +5 -0
- package/dist/services/sync/claude-writer.d.ts +2 -0
- package/dist/services/sync/claude-writer.js +83 -0
- package/dist/services/sync/codex-writer.d.ts +2 -0
- package/dist/services/sync/codex-writer.js +116 -0
- package/dist/services/sync/gemini-writer.d.ts +2 -0
- package/dist/services/sync/gemini-writer.js +83 -0
- package/dist/services/sync-service.d.ts +15 -0
- package/dist/services/sync-service.js +64 -0
- package/dist/types.d.ts +35 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { type DoctorService } from './services/doctor-service.js';
|
|
4
4
|
import { type InitService } from './services/init-service.js';
|
|
5
|
+
import { type ProfileService } from './services/profile-service.js';
|
|
5
6
|
import { type RunService } from './services/run-service.js';
|
|
6
7
|
import { type StatusService } from './services/status-service.js';
|
|
8
|
+
import { type SyncService } from './services/sync-service.js';
|
|
7
9
|
export interface CliServices {
|
|
8
10
|
initService: InitService;
|
|
9
11
|
runService: RunService;
|
|
10
12
|
statusService: StatusService;
|
|
11
13
|
doctorService: DoctorService;
|
|
14
|
+
profileService: ProfileService;
|
|
15
|
+
syncService: SyncService;
|
|
12
16
|
}
|
|
13
17
|
export declare function createProgram(overrides?: Partial<CliServices>): Command;
|
|
14
18
|
export declare function main(argv?: string[]): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -6,14 +6,18 @@ import { Command } from 'commander';
|
|
|
6
6
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
7
7
|
import { registerInitCommand } from './commands/init.js';
|
|
8
8
|
import { registerMcpCommand } from './commands/mcp.js';
|
|
9
|
+
import { registerProfileCommand } from './commands/profile.js';
|
|
9
10
|
import { registerRunCommand } from './commands/run.js';
|
|
10
11
|
import { registerStatusCommand } from './commands/status.js';
|
|
12
|
+
import { registerSyncCommand } from './commands/sync.js';
|
|
11
13
|
import { registerUiCommand } from './commands/ui.js';
|
|
12
14
|
import { printError } from './output.js';
|
|
13
15
|
import { createDoctorService } from './services/doctor-service.js';
|
|
14
16
|
import { createInitService } from './services/init-service.js';
|
|
17
|
+
import { createProfileService } from './services/profile-service.js';
|
|
15
18
|
import { createRunService } from './services/run-service.js';
|
|
16
19
|
import { createStatusService } from './services/status-service.js';
|
|
20
|
+
import { createSyncService } from './services/sync-service.js';
|
|
17
21
|
import { createExecutorResolver } from './executor/resolver.js';
|
|
18
22
|
const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
19
23
|
export function createProgram(overrides = {}) {
|
|
@@ -27,6 +31,8 @@ export function createProgram(overrides = {}) {
|
|
|
27
31
|
registerStatusCommand(program, services.statusService);
|
|
28
32
|
registerRunCommand(program, services.runService);
|
|
29
33
|
registerDoctorCommand(program, services.doctorService);
|
|
34
|
+
registerProfileCommand(program, services.profileService);
|
|
35
|
+
registerSyncCommand(program, services.syncService);
|
|
30
36
|
registerUiCommand(program);
|
|
31
37
|
registerMcpCommand(program);
|
|
32
38
|
return program;
|
|
@@ -49,11 +55,14 @@ export function shouldRunMain(entryPointPath, moduleUrl) {
|
|
|
49
55
|
}
|
|
50
56
|
function createDefaultServices(overrides) {
|
|
51
57
|
const resolver = createExecutorResolver();
|
|
58
|
+
const profileService = createProfileService();
|
|
52
59
|
return {
|
|
53
60
|
initService: createInitService(),
|
|
54
61
|
runService: createRunService({ resolver }),
|
|
55
62
|
statusService: createStatusService({ resolver }),
|
|
56
63
|
doctorService: createDoctorService({ resolver }),
|
|
64
|
+
profileService,
|
|
65
|
+
syncService: createSyncService({ profileService }),
|
|
57
66
|
...overrides
|
|
58
67
|
};
|
|
59
68
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export function registerProfileCommand(program, profileService) {
|
|
3
|
+
const profileCmd = program
|
|
4
|
+
.command('profile')
|
|
5
|
+
.description('Manage brainctl profiles');
|
|
6
|
+
profileCmd
|
|
7
|
+
.command('list')
|
|
8
|
+
.description('List available profiles')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const { profiles, activeProfile } = await profileService.list({ cwd: process.cwd() });
|
|
11
|
+
if (profiles.length === 0) {
|
|
12
|
+
console.log('No profiles found. Run "brainctl profile create <name>" to create one.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
console.log(pc.bold('Profiles:'));
|
|
16
|
+
for (const name of profiles) {
|
|
17
|
+
const marker = name === activeProfile ? pc.green(' (active)') : '';
|
|
18
|
+
console.log(` ${name}${marker}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
profileCmd
|
|
22
|
+
.command('create')
|
|
23
|
+
.argument('<name>', 'Profile name')
|
|
24
|
+
.option('-d, --description <text>', 'Profile description')
|
|
25
|
+
.description('Create a new profile')
|
|
26
|
+
.action(async (name, options) => {
|
|
27
|
+
const result = await profileService.create({
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
name,
|
|
30
|
+
description: options.description,
|
|
31
|
+
});
|
|
32
|
+
console.log(`Created profile at ${result.profilePath}`);
|
|
33
|
+
});
|
|
34
|
+
profileCmd
|
|
35
|
+
.command('use')
|
|
36
|
+
.argument('<name>', 'Profile name to activate')
|
|
37
|
+
.description('Switch the active profile')
|
|
38
|
+
.action(async (name) => {
|
|
39
|
+
const result = await profileService.use({ cwd: process.cwd(), name });
|
|
40
|
+
const prev = result.previousProfile ? ` (was "${result.previousProfile}")` : '';
|
|
41
|
+
console.log(`Switched to profile "${name}"${prev}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export function registerSyncCommand(program, syncService) {
|
|
3
|
+
program
|
|
4
|
+
.command('sync')
|
|
5
|
+
.description('Sync active profile to agent configs')
|
|
6
|
+
.option('--restore', 'Restore agent configs from most recent backup')
|
|
7
|
+
.action(async (options) => {
|
|
8
|
+
const results = await syncService.execute({
|
|
9
|
+
cwd: process.cwd(),
|
|
10
|
+
restore: options.restore,
|
|
11
|
+
});
|
|
12
|
+
if (results.length === 0) {
|
|
13
|
+
console.log('No agents to sync.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (options.restore) {
|
|
17
|
+
console.log(pc.bold('Restored agent configs:'));
|
|
18
|
+
for (const result of results) {
|
|
19
|
+
console.log(` ${result.agent}: restored from ${result.configPath}`);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(pc.bold('Synced profile to agents:'));
|
|
24
|
+
for (const result of results) {
|
|
25
|
+
console.log(` ${result.agent}: ${result.configPath} (${result.mcpCount} MCPs)`);
|
|
26
|
+
if (result.backedUpTo) {
|
|
27
|
+
console.log(` backed up to ${result.backedUpTo}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -25,3 +25,12 @@ export declare class AgentNotAvailableError extends BrainctlError {
|
|
|
25
25
|
export declare class ExecutionError extends BrainctlError {
|
|
26
26
|
constructor(message: string);
|
|
27
27
|
}
|
|
28
|
+
export declare class ProfileError extends BrainctlError {
|
|
29
|
+
constructor(message: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class ProfileNotFoundError extends BrainctlError {
|
|
32
|
+
constructor(message: string);
|
|
33
|
+
}
|
|
34
|
+
export declare class SyncError extends BrainctlError {
|
|
35
|
+
constructor(message: string);
|
|
36
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -43,3 +43,18 @@ export class ExecutionError extends BrainctlError {
|
|
|
43
43
|
super(message, 'system', 'EXECUTION_ERROR');
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
export class ProfileError extends BrainctlError {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message, 'user', 'PROFILE_ERROR');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class ProfileNotFoundError extends BrainctlError {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message, 'user', 'PROFILE_NOT_FOUND');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export class SyncError extends BrainctlError {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message, 'system', 'SYNC_ERROR');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -4,10 +4,11 @@ import path from 'node:path';
|
|
|
4
4
|
import { AgentNotAvailableError } from '../errors.js';
|
|
5
5
|
import { ClaudeExecutor } from './claude.js';
|
|
6
6
|
import { CodexExecutor } from './codex.js';
|
|
7
|
-
const SUPPORTED_AGENTS = ['claude', 'codex'];
|
|
7
|
+
const SUPPORTED_AGENTS = ['claude', 'codex', 'gemini'];
|
|
8
8
|
const AGENT_COMMANDS = {
|
|
9
9
|
claude: 'claude',
|
|
10
|
-
codex: 'codex'
|
|
10
|
+
codex: 'codex',
|
|
11
|
+
gemini: 'gemini'
|
|
11
12
|
};
|
|
12
13
|
class DefaultExecutorResolver {
|
|
13
14
|
availabilityCache = new Map();
|
|
@@ -45,6 +46,8 @@ function createExecutor(agentName) {
|
|
|
45
46
|
return new ClaudeExecutor();
|
|
46
47
|
case 'codex':
|
|
47
48
|
return new CodexExecutor();
|
|
49
|
+
default:
|
|
50
|
+
throw new AgentNotAvailableError(`Agent "${agentName}" does not have an executor implementation.`);
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
async function checkAvailability(agentName) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -3,9 +3,13 @@ import path from 'node:path';
|
|
|
3
3
|
import { FastMCP } from 'fastmcp';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { loadConfig } from '../config.js';
|
|
6
|
+
import { loadMemory } from '../context/memory.js';
|
|
6
7
|
import { createDoctorService } from '../services/doctor-service.js';
|
|
8
|
+
import { createMemoryWriteService } from '../services/memory-write-service.js';
|
|
9
|
+
import { createProfileService } from '../services/profile-service.js';
|
|
7
10
|
import { createRunService } from '../services/run-service.js';
|
|
8
11
|
import { createStatusService } from '../services/status-service.js';
|
|
12
|
+
import { createSyncService } from '../services/sync-service.js';
|
|
9
13
|
const packageVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
10
14
|
export function createMcpServer(options = {}) {
|
|
11
15
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -80,6 +84,97 @@ export function createMcpServer(options = {}) {
|
|
|
80
84
|
return JSON.stringify(result, null, 2);
|
|
81
85
|
},
|
|
82
86
|
});
|
|
87
|
+
server.addTool({
|
|
88
|
+
name: 'brainctl_read_memory',
|
|
89
|
+
description: 'Read all shared memory files. Returns every markdown file from configured memory paths with file names and content. Use this to understand context left by other agents.',
|
|
90
|
+
parameters: z.object({}),
|
|
91
|
+
execute: async () => {
|
|
92
|
+
const config = await loadConfig({ cwd });
|
|
93
|
+
const memory = await loadMemory({ paths: config.memory.paths });
|
|
94
|
+
const result = {
|
|
95
|
+
count: memory.count,
|
|
96
|
+
files: memory.entries.map((entry) => ({
|
|
97
|
+
path: entry.path,
|
|
98
|
+
content: entry.content,
|
|
99
|
+
})),
|
|
100
|
+
};
|
|
101
|
+
return JSON.stringify(result, null, 2);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
server.addTool({
|
|
105
|
+
name: 'brainctl_write_memory',
|
|
106
|
+
description: 'Write or update a shared memory file. Use this to leave notes, decisions, or context for other agents. The file must be within a configured memory path.',
|
|
107
|
+
parameters: z.object({
|
|
108
|
+
file_path: z.string().describe('Relative path for the memory file (e.g., "memory/notes.md")'),
|
|
109
|
+
content: z.string().describe('Markdown content to write'),
|
|
110
|
+
}),
|
|
111
|
+
execute: async (args) => {
|
|
112
|
+
const memoryWriteService = createMemoryWriteService();
|
|
113
|
+
const result = await memoryWriteService.execute({
|
|
114
|
+
cwd,
|
|
115
|
+
filePath: args.file_path,
|
|
116
|
+
content: args.content,
|
|
117
|
+
});
|
|
118
|
+
return JSON.stringify({ written: result.filePath });
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
server.addTool({
|
|
122
|
+
name: 'brainctl_get_skill',
|
|
123
|
+
description: 'Get the full details of a specific skill including its prompt text and description. Use this to understand what a skill does before running it.',
|
|
124
|
+
parameters: z.object({
|
|
125
|
+
skill: z.string().describe('Skill name as defined in ai-stack.yaml'),
|
|
126
|
+
}),
|
|
127
|
+
execute: async (args) => {
|
|
128
|
+
const config = await loadConfig({ cwd });
|
|
129
|
+
const skillConfig = config.skills[args.skill];
|
|
130
|
+
if (!skillConfig) {
|
|
131
|
+
throw new Error(`Skill "${args.skill}" is not defined in ai-stack.yaml.`);
|
|
132
|
+
}
|
|
133
|
+
return JSON.stringify({
|
|
134
|
+
name: args.skill,
|
|
135
|
+
description: skillConfig.description ?? null,
|
|
136
|
+
prompt: skillConfig.prompt,
|
|
137
|
+
}, null, 2);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
server.addTool({
|
|
141
|
+
name: 'brainctl_list_profiles',
|
|
142
|
+
description: 'List available profiles and show which one is active.',
|
|
143
|
+
parameters: z.object({}),
|
|
144
|
+
execute: async () => {
|
|
145
|
+
const profileService = createProfileService();
|
|
146
|
+
const result = await profileService.list({ cwd });
|
|
147
|
+
return JSON.stringify(result, null, 2);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
server.addTool({
|
|
151
|
+
name: 'brainctl_switch_profile',
|
|
152
|
+
description: 'Switch the active profile and sync it to all configured agents. Combines profile switch + sync in one step.',
|
|
153
|
+
parameters: z.object({
|
|
154
|
+
name: z.string().describe('Profile name to activate'),
|
|
155
|
+
}),
|
|
156
|
+
execute: async (args) => {
|
|
157
|
+
const profileService = createProfileService();
|
|
158
|
+
const switchResult = await profileService.use({ cwd, name: args.name });
|
|
159
|
+
const syncService = createSyncService({ profileService });
|
|
160
|
+
const syncResult = await syncService.execute({ cwd });
|
|
161
|
+
return JSON.stringify({
|
|
162
|
+
previousProfile: switchResult.previousProfile,
|
|
163
|
+
activeProfile: args.name,
|
|
164
|
+
synced: syncResult,
|
|
165
|
+
}, null, 2);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
server.addTool({
|
|
169
|
+
name: 'brainctl_sync',
|
|
170
|
+
description: 'Sync the active profile to all configured agent configs (Claude, Codex). Creates backups before overwriting.',
|
|
171
|
+
parameters: z.object({}),
|
|
172
|
+
execute: async () => {
|
|
173
|
+
const syncService = createSyncService();
|
|
174
|
+
const result = await syncService.execute({ cwd });
|
|
175
|
+
return JSON.stringify(result, null, 2);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
83
178
|
return server;
|
|
84
179
|
}
|
|
85
180
|
export async function startMcpServer(options = {}) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BrainctlMetaConfig, ProfileConfig } from '../types.js';
|
|
2
|
+
export interface ProfileService {
|
|
3
|
+
list(options?: {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
profiles: string[];
|
|
7
|
+
activeProfile: string | null;
|
|
8
|
+
}>;
|
|
9
|
+
get(options: {
|
|
10
|
+
cwd?: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}): Promise<ProfileConfig>;
|
|
13
|
+
create(options: {
|
|
14
|
+
cwd?: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
profilePath: string;
|
|
19
|
+
}>;
|
|
20
|
+
use(options: {
|
|
21
|
+
cwd?: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
previousProfile: string | null;
|
|
25
|
+
}>;
|
|
26
|
+
getMetaConfig(options?: {
|
|
27
|
+
cwd?: string;
|
|
28
|
+
}): Promise<BrainctlMetaConfig>;
|
|
29
|
+
}
|
|
30
|
+
export declare function createProfileService(): ProfileService;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { ProfileError, ProfileNotFoundError } from '../errors.js';
|
|
5
|
+
const BRAINCTL_DIR = '.brainctl';
|
|
6
|
+
const PROFILES_DIR = '.brainctl/profiles';
|
|
7
|
+
const META_CONFIG = '.brainctl/config.yaml';
|
|
8
|
+
export function createProfileService() {
|
|
9
|
+
return {
|
|
10
|
+
async list(options = {}) {
|
|
11
|
+
const cwd = options.cwd ?? process.cwd();
|
|
12
|
+
const profilesDir = path.join(cwd, PROFILES_DIR);
|
|
13
|
+
let files = [];
|
|
14
|
+
try {
|
|
15
|
+
const entries = await readdir(profilesDir);
|
|
16
|
+
files = entries
|
|
17
|
+
.filter((f) => f.endsWith('.yaml'))
|
|
18
|
+
.map((f) => f.replace(/\.yaml$/, ''))
|
|
19
|
+
.sort();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// No profiles directory yet
|
|
23
|
+
}
|
|
24
|
+
const meta = await loadMetaConfig(cwd);
|
|
25
|
+
return {
|
|
26
|
+
profiles: files,
|
|
27
|
+
activeProfile: meta.active_profile || null,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
async get(options) {
|
|
31
|
+
const cwd = options.cwd ?? process.cwd();
|
|
32
|
+
const profilePath = path.join(cwd, PROFILES_DIR, `${options.name}.yaml`);
|
|
33
|
+
let source;
|
|
34
|
+
try {
|
|
35
|
+
source = await readFile(profilePath, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new ProfileNotFoundError(`Profile "${options.name}" not found at ${profilePath}`);
|
|
39
|
+
}
|
|
40
|
+
return parseProfile(source, options.name);
|
|
41
|
+
},
|
|
42
|
+
async create(options) {
|
|
43
|
+
const cwd = options.cwd ?? process.cwd();
|
|
44
|
+
const profilesDir = path.join(cwd, PROFILES_DIR);
|
|
45
|
+
const profilePath = path.join(profilesDir, `${options.name}.yaml`);
|
|
46
|
+
if (await pathExists(profilePath)) {
|
|
47
|
+
throw new ProfileError(`Profile "${options.name}" already exists.`);
|
|
48
|
+
}
|
|
49
|
+
const scaffold = {
|
|
50
|
+
name: options.name,
|
|
51
|
+
description: options.description ?? '',
|
|
52
|
+
skills: {
|
|
53
|
+
example: {
|
|
54
|
+
description: 'Example skill',
|
|
55
|
+
prompt: 'Describe what this skill does...',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
mcps: {},
|
|
59
|
+
memory: {
|
|
60
|
+
paths: ['./memory'],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
await mkdir(profilesDir, { recursive: true });
|
|
64
|
+
await writeFile(profilePath, YAML.stringify(scaffold), 'utf8');
|
|
65
|
+
return { profilePath };
|
|
66
|
+
},
|
|
67
|
+
async use(options) {
|
|
68
|
+
const cwd = options.cwd ?? process.cwd();
|
|
69
|
+
// Validate profile exists
|
|
70
|
+
const profilePath = path.join(cwd, PROFILES_DIR, `${options.name}.yaml`);
|
|
71
|
+
if (!(await pathExists(profilePath))) {
|
|
72
|
+
throw new ProfileNotFoundError(`Profile "${options.name}" not found.`);
|
|
73
|
+
}
|
|
74
|
+
const meta = await loadMetaConfig(cwd);
|
|
75
|
+
const previousProfile = meta.active_profile || null;
|
|
76
|
+
meta.active_profile = options.name;
|
|
77
|
+
const metaPath = path.join(cwd, META_CONFIG);
|
|
78
|
+
await mkdir(path.dirname(metaPath), { recursive: true });
|
|
79
|
+
await writeFile(metaPath, YAML.stringify(meta), 'utf8');
|
|
80
|
+
return { previousProfile };
|
|
81
|
+
},
|
|
82
|
+
async getMetaConfig(options = {}) {
|
|
83
|
+
const cwd = options.cwd ?? process.cwd();
|
|
84
|
+
return loadMetaConfig(cwd);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function loadMetaConfig(cwd) {
|
|
89
|
+
const metaPath = path.join(cwd, META_CONFIG);
|
|
90
|
+
try {
|
|
91
|
+
const source = await readFile(metaPath, 'utf8');
|
|
92
|
+
const parsed = YAML.parse(source) ?? {};
|
|
93
|
+
return {
|
|
94
|
+
active_profile: typeof parsed.active_profile === 'string' ? parsed.active_profile : '',
|
|
95
|
+
agents: Array.isArray(parsed.agents) ? parsed.agents : ['claude', 'codex'],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { active_profile: '', agents: ['claude', 'codex', 'gemini'] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function parseProfile(source, name) {
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = YAML.parse(source) ?? {};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new ProfileError(`Profile "${name}" has invalid YAML.`);
|
|
109
|
+
}
|
|
110
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
111
|
+
throw new ProfileError(`Profile "${name}" has invalid structure.`);
|
|
112
|
+
}
|
|
113
|
+
const data = parsed;
|
|
114
|
+
const skills = {};
|
|
115
|
+
if (data.skills && typeof data.skills === 'object' && !Array.isArray(data.skills)) {
|
|
116
|
+
for (const [key, value] of Object.entries(data.skills)) {
|
|
117
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
118
|
+
const s = value;
|
|
119
|
+
if (typeof s.prompt === 'string') {
|
|
120
|
+
skills[key] = {
|
|
121
|
+
prompt: s.prompt,
|
|
122
|
+
description: typeof s.description === 'string' ? s.description : undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const mcps = {};
|
|
129
|
+
if (data.mcps && typeof data.mcps === 'object' && !Array.isArray(data.mcps)) {
|
|
130
|
+
for (const [key, value] of Object.entries(data.mcps)) {
|
|
131
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
132
|
+
const m = value;
|
|
133
|
+
if (m.type === 'npm' && typeof m.package === 'string') {
|
|
134
|
+
mcps[key] = {
|
|
135
|
+
type: 'npm',
|
|
136
|
+
package: m.package,
|
|
137
|
+
env: parseEnv(m.env),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
else if (m.type === 'bundled' && typeof m.command === 'string') {
|
|
141
|
+
mcps[key] = {
|
|
142
|
+
type: 'bundled',
|
|
143
|
+
path: typeof m.path === 'string' ? m.path : '.',
|
|
144
|
+
install: typeof m.install === 'string' ? m.install : undefined,
|
|
145
|
+
command: m.command,
|
|
146
|
+
args: Array.isArray(m.args) ? m.args.map(String) : undefined,
|
|
147
|
+
env: parseEnv(m.env),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const memoryPaths = [];
|
|
154
|
+
if (data.memory && typeof data.memory === 'object' && !Array.isArray(data.memory)) {
|
|
155
|
+
const mem = data.memory;
|
|
156
|
+
if (Array.isArray(mem.paths)) {
|
|
157
|
+
for (const p of mem.paths) {
|
|
158
|
+
if (typeof p === 'string') {
|
|
159
|
+
memoryPaths.push(p);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
name: typeof data.name === 'string' ? data.name : name,
|
|
166
|
+
description: typeof data.description === 'string' ? data.description : undefined,
|
|
167
|
+
skills,
|
|
168
|
+
mcps,
|
|
169
|
+
memory: { paths: memoryPaths },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function parseEnv(value) {
|
|
173
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const result = {};
|
|
177
|
+
for (const [k, v] of Object.entries(value)) {
|
|
178
|
+
result[k] = String(v);
|
|
179
|
+
}
|
|
180
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
181
|
+
}
|
|
182
|
+
async function pathExists(targetPath) {
|
|
183
|
+
try {
|
|
184
|
+
await stat(targetPath);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { McpServerConfig } from '../../types.js';
|
|
2
|
+
export interface AgentWriteOptions {
|
|
3
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
4
|
+
cwd: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AgentWriteResult {
|
|
7
|
+
configPath: string;
|
|
8
|
+
backedUpTo: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface AgentConfigWriter {
|
|
11
|
+
write(options: AgentWriteOptions): Promise<AgentWriteResult>;
|
|
12
|
+
restore(options: {
|
|
13
|
+
cwd: string;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
restoredFrom: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export declare function formatTimestamp(): string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { copyFile, readdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { SyncError } from '../../errors.js';
|
|
5
|
+
import { formatTimestamp } from './agent-writer.js';
|
|
6
|
+
export function createClaudeWriter() {
|
|
7
|
+
return {
|
|
8
|
+
async write(options) {
|
|
9
|
+
const configPath = path.join(homedir(), '.claude.json');
|
|
10
|
+
let existing = {};
|
|
11
|
+
let backedUpTo = null;
|
|
12
|
+
// Read existing config
|
|
13
|
+
try {
|
|
14
|
+
const source = await readFile(configPath, 'utf8');
|
|
15
|
+
existing = JSON.parse(source);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// No existing config, start fresh
|
|
19
|
+
}
|
|
20
|
+
// Backup if file exists
|
|
21
|
+
if (Object.keys(existing).length > 0) {
|
|
22
|
+
const backupPath = `${configPath}.bak.${formatTimestamp()}`;
|
|
23
|
+
await copyFile(configPath, backupPath);
|
|
24
|
+
backedUpTo = backupPath;
|
|
25
|
+
}
|
|
26
|
+
// Build mcpServers for this project
|
|
27
|
+
const mcpServers = {};
|
|
28
|
+
for (const [name, config] of Object.entries(options.mcpServers)) {
|
|
29
|
+
mcpServers[name] = toClaudeFormat(config);
|
|
30
|
+
}
|
|
31
|
+
// Always include brainctl itself
|
|
32
|
+
mcpServers['brainctl'] = {
|
|
33
|
+
type: 'stdio',
|
|
34
|
+
command: 'npx',
|
|
35
|
+
args: ['-y', 'brainctl', 'mcp'],
|
|
36
|
+
};
|
|
37
|
+
// Merge into existing config (preserve other projects)
|
|
38
|
+
const projects = (existing.projects ?? {});
|
|
39
|
+
const projectConfig = projects[options.cwd] ?? {};
|
|
40
|
+
projectConfig.mcpServers = mcpServers;
|
|
41
|
+
projects[options.cwd] = projectConfig;
|
|
42
|
+
existing.projects = projects;
|
|
43
|
+
// Atomic write: write to temp, then rename
|
|
44
|
+
const tmpPath = `${configPath}.tmp.${Date.now()}`;
|
|
45
|
+
await writeFile(tmpPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
46
|
+
await rename(tmpPath, configPath);
|
|
47
|
+
return { configPath, backedUpTo };
|
|
48
|
+
},
|
|
49
|
+
async restore(options) {
|
|
50
|
+
const configPath = path.join(homedir(), '.claude.json');
|
|
51
|
+
const dir = path.dirname(configPath);
|
|
52
|
+
const base = path.basename(configPath);
|
|
53
|
+
const entries = await readdir(dir);
|
|
54
|
+
const backups = entries
|
|
55
|
+
.filter((f) => f.startsWith(`${base}.bak.`))
|
|
56
|
+
.sort()
|
|
57
|
+
.reverse();
|
|
58
|
+
if (backups.length === 0) {
|
|
59
|
+
throw new SyncError('No Claude config backup found.');
|
|
60
|
+
}
|
|
61
|
+
const latestBackup = path.join(dir, backups[0]);
|
|
62
|
+
await copyFile(latestBackup, configPath);
|
|
63
|
+
return { restoredFrom: latestBackup };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function toClaudeFormat(config) {
|
|
68
|
+
if (config.type === 'npm') {
|
|
69
|
+
return {
|
|
70
|
+
type: 'stdio',
|
|
71
|
+
command: 'npx',
|
|
72
|
+
args: ['-y', config.package],
|
|
73
|
+
...(config.env ? { env: config.env } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// bundled
|
|
77
|
+
return {
|
|
78
|
+
type: 'stdio',
|
|
79
|
+
command: config.command,
|
|
80
|
+
args: config.args ?? [],
|
|
81
|
+
...(config.env ? { env: config.env } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { SyncError } from '../../errors.js';
|
|
5
|
+
import { formatTimestamp } from './agent-writer.js';
|
|
6
|
+
export function createCodexWriter() {
|
|
7
|
+
return {
|
|
8
|
+
async write(options) {
|
|
9
|
+
const configDir = path.join(homedir(), '.codex');
|
|
10
|
+
const configPath = path.join(configDir, 'config.toml');
|
|
11
|
+
let existingContent = '';
|
|
12
|
+
let backedUpTo = null;
|
|
13
|
+
// Read existing config
|
|
14
|
+
try {
|
|
15
|
+
existingContent = await readFile(configPath, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// No existing config
|
|
19
|
+
}
|
|
20
|
+
// Backup if file exists
|
|
21
|
+
if (existingContent.length > 0) {
|
|
22
|
+
const backupPath = `${configPath}.bak.${formatTimestamp()}`;
|
|
23
|
+
await copyFile(configPath, backupPath);
|
|
24
|
+
backedUpTo = backupPath;
|
|
25
|
+
}
|
|
26
|
+
// Build MCP servers section
|
|
27
|
+
const allServers = { ...options.mcpServers };
|
|
28
|
+
// Always include brainctl itself
|
|
29
|
+
allServers['brainctl'] = {
|
|
30
|
+
type: 'npm',
|
|
31
|
+
package: 'brainctl',
|
|
32
|
+
};
|
|
33
|
+
const mcpToml = buildMcpToml(allServers);
|
|
34
|
+
// Preserve non-mcp_servers content from existing config
|
|
35
|
+
const existingNonMcp = stripMcpSections(existingContent);
|
|
36
|
+
const finalContent = existingNonMcp.trim().length > 0
|
|
37
|
+
? `${existingNonMcp.trim()}\n\n${mcpToml}`
|
|
38
|
+
: mcpToml;
|
|
39
|
+
// Atomic write
|
|
40
|
+
await mkdir(configDir, { recursive: true });
|
|
41
|
+
const tmpPath = `${configPath}.tmp.${Date.now()}`;
|
|
42
|
+
await writeFile(tmpPath, finalContent + '\n', 'utf8');
|
|
43
|
+
await rename(tmpPath, configPath);
|
|
44
|
+
return { configPath, backedUpTo };
|
|
45
|
+
},
|
|
46
|
+
async restore(options) {
|
|
47
|
+
const configPath = path.join(homedir(), '.codex', 'config.toml');
|
|
48
|
+
const dir = path.dirname(configPath);
|
|
49
|
+
const base = path.basename(configPath);
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(dir);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new SyncError('No Codex config directory found.');
|
|
56
|
+
}
|
|
57
|
+
const backups = entries
|
|
58
|
+
.filter((f) => f.startsWith(`${base}.bak.`))
|
|
59
|
+
.sort()
|
|
60
|
+
.reverse();
|
|
61
|
+
if (backups.length === 0) {
|
|
62
|
+
throw new SyncError('No Codex config backup found.');
|
|
63
|
+
}
|
|
64
|
+
const latestBackup = path.join(dir, backups[0]);
|
|
65
|
+
await copyFile(latestBackup, configPath);
|
|
66
|
+
return { restoredFrom: latestBackup };
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function buildMcpToml(servers) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
73
|
+
lines.push(`[mcp_servers.${name}]`);
|
|
74
|
+
if (config.type === 'npm') {
|
|
75
|
+
lines.push(`command = "npx"`);
|
|
76
|
+
lines.push(`args = ["-y", ${tomlString(config.package)}]`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
lines.push(`command = ${tomlString(config.command)}`);
|
|
80
|
+
if (config.args && config.args.length > 0) {
|
|
81
|
+
const argsStr = config.args.map(tomlString).join(', ');
|
|
82
|
+
lines.push(`args = [${argsStr}]`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (config.env && Object.keys(config.env).length > 0) {
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(`[mcp_servers.${name}.env]`);
|
|
88
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
89
|
+
lines.push(`${key} = ${tomlString(value)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
return lines.join('\n').trim();
|
|
95
|
+
}
|
|
96
|
+
function tomlString(value) {
|
|
97
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
98
|
+
}
|
|
99
|
+
function stripMcpSections(content) {
|
|
100
|
+
const lines = content.split('\n');
|
|
101
|
+
const result = [];
|
|
102
|
+
let inMcpSection = false;
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
if (/^\[mcp_servers[\].]/.test(line)) {
|
|
105
|
+
inMcpSection = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (inMcpSection && /^\[/.test(line) && !/^\[mcp_servers[\].]/.test(line)) {
|
|
109
|
+
inMcpSection = false;
|
|
110
|
+
}
|
|
111
|
+
if (!inMcpSection) {
|
|
112
|
+
result.push(line);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result.join('\n');
|
|
116
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { SyncError } from '../../errors.js';
|
|
4
|
+
import { formatTimestamp } from './agent-writer.js';
|
|
5
|
+
export function createGeminiWriter() {
|
|
6
|
+
return {
|
|
7
|
+
async write(options) {
|
|
8
|
+
const geminiDir = path.join(options.cwd, '.gemini');
|
|
9
|
+
const configPath = path.join(geminiDir, 'settings.json');
|
|
10
|
+
let existing = {};
|
|
11
|
+
let backedUpTo = null;
|
|
12
|
+
// Read existing config
|
|
13
|
+
try {
|
|
14
|
+
const source = await readFile(configPath, 'utf8');
|
|
15
|
+
existing = JSON.parse(source);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// No existing config, start fresh
|
|
19
|
+
}
|
|
20
|
+
// Backup if file exists with content
|
|
21
|
+
if (Object.keys(existing).length > 0) {
|
|
22
|
+
const backupPath = `${configPath}.bak.${formatTimestamp()}`;
|
|
23
|
+
await copyFile(configPath, backupPath);
|
|
24
|
+
backedUpTo = backupPath;
|
|
25
|
+
}
|
|
26
|
+
// Build mcpServers
|
|
27
|
+
const mcpServers = {};
|
|
28
|
+
for (const [name, config] of Object.entries(options.mcpServers)) {
|
|
29
|
+
mcpServers[name] = toGeminiFormat(config);
|
|
30
|
+
}
|
|
31
|
+
// Always include brainctl itself
|
|
32
|
+
mcpServers['brainctl'] = {
|
|
33
|
+
command: 'npx',
|
|
34
|
+
args: ['-y', 'brainctl', 'mcp'],
|
|
35
|
+
};
|
|
36
|
+
// Merge into existing config (preserve other settings)
|
|
37
|
+
existing.mcpServers = mcpServers;
|
|
38
|
+
// Atomic write
|
|
39
|
+
await mkdir(geminiDir, { recursive: true });
|
|
40
|
+
const tmpPath = `${configPath}.tmp.${Date.now()}`;
|
|
41
|
+
await writeFile(tmpPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
42
|
+
await rename(tmpPath, configPath);
|
|
43
|
+
return { configPath, backedUpTo };
|
|
44
|
+
},
|
|
45
|
+
async restore(options) {
|
|
46
|
+
const configPath = path.join(options.cwd, '.gemini', 'settings.json');
|
|
47
|
+
const dir = path.dirname(configPath);
|
|
48
|
+
const base = path.basename(configPath);
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await readdir(dir);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new SyncError('No Gemini config directory found.');
|
|
55
|
+
}
|
|
56
|
+
const backups = entries
|
|
57
|
+
.filter((f) => f.startsWith(`${base}.bak.`))
|
|
58
|
+
.sort()
|
|
59
|
+
.reverse();
|
|
60
|
+
if (backups.length === 0) {
|
|
61
|
+
throw new SyncError('No Gemini config backup found.');
|
|
62
|
+
}
|
|
63
|
+
const latestBackup = path.join(dir, backups[0]);
|
|
64
|
+
await copyFile(latestBackup, configPath);
|
|
65
|
+
return { restoredFrom: latestBackup };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function toGeminiFormat(config) {
|
|
70
|
+
if (config.type === 'npm') {
|
|
71
|
+
return {
|
|
72
|
+
command: 'npx',
|
|
73
|
+
args: ['-y', config.package],
|
|
74
|
+
...(config.env ? { env: config.env } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// bundled
|
|
78
|
+
return {
|
|
79
|
+
command: config.command,
|
|
80
|
+
args: config.args ?? [],
|
|
81
|
+
...(config.env ? { env: config.env } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentName, SyncResult } from '../types.js';
|
|
2
|
+
import type { AgentConfigWriter } from './sync/agent-writer.js';
|
|
3
|
+
import { type ProfileService } from './profile-service.js';
|
|
4
|
+
export interface SyncService {
|
|
5
|
+
execute(options?: {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
restore?: boolean;
|
|
8
|
+
}): Promise<SyncResult>;
|
|
9
|
+
}
|
|
10
|
+
interface SyncServiceDependencies {
|
|
11
|
+
profileService?: ProfileService;
|
|
12
|
+
writers?: Partial<Record<AgentName, AgentConfigWriter>>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createSyncService(dependencies?: SyncServiceDependencies): SyncService;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createClaudeWriter } from './sync/claude-writer.js';
|
|
2
|
+
import { createCodexWriter } from './sync/codex-writer.js';
|
|
3
|
+
import { createGeminiWriter } from './sync/gemini-writer.js';
|
|
4
|
+
import { createProfileService } from './profile-service.js';
|
|
5
|
+
export function createSyncService(dependencies = {}) {
|
|
6
|
+
const profileService = dependencies.profileService ?? createProfileService();
|
|
7
|
+
const defaultWriters = {
|
|
8
|
+
claude: createClaudeWriter(),
|
|
9
|
+
codex: createCodexWriter(),
|
|
10
|
+
gemini: createGeminiWriter(),
|
|
11
|
+
};
|
|
12
|
+
const writers = { ...defaultWriters, ...dependencies.writers };
|
|
13
|
+
return {
|
|
14
|
+
async execute(options = {}) {
|
|
15
|
+
const cwd = options.cwd ?? process.cwd();
|
|
16
|
+
if (options.restore) {
|
|
17
|
+
return restoreAll(writers, cwd);
|
|
18
|
+
}
|
|
19
|
+
const meta = await profileService.getMetaConfig({ cwd });
|
|
20
|
+
if (!meta.active_profile) {
|
|
21
|
+
throw new Error('No active profile set. Run "brainctl profile use <name>" first.');
|
|
22
|
+
}
|
|
23
|
+
const profile = await profileService.get({ cwd, name: meta.active_profile });
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const agent of meta.agents) {
|
|
26
|
+
const writer = writers[agent];
|
|
27
|
+
if (!writer) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const result = await writer.write({
|
|
31
|
+
mcpServers: profile.mcps,
|
|
32
|
+
cwd,
|
|
33
|
+
});
|
|
34
|
+
results.push({
|
|
35
|
+
agent,
|
|
36
|
+
configPath: result.configPath,
|
|
37
|
+
backedUpTo: result.backedUpTo,
|
|
38
|
+
mcpCount: Object.keys(profile.mcps).length + 1, // +1 for brainctl itself
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function restoreAll(writers, cwd) {
|
|
46
|
+
const results = [];
|
|
47
|
+
for (const [agent, writer] of Object.entries(writers)) {
|
|
48
|
+
if (!writer)
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
const { restoredFrom } = await writer.restore({ cwd });
|
|
52
|
+
results.push({
|
|
53
|
+
agent: agent,
|
|
54
|
+
configPath: restoredFrom,
|
|
55
|
+
backedUpTo: null,
|
|
56
|
+
mcpCount: 0,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Skip agents with no backup
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AgentName = 'claude' | 'codex';
|
|
1
|
+
export type AgentName = 'claude' | 'codex' | 'gemini';
|
|
2
2
|
export type ErrorCategory = 'user' | 'system';
|
|
3
3
|
export type DiagnosticStatus = 'ok' | 'warn' | 'error';
|
|
4
4
|
export interface SkillConfig {
|
|
@@ -55,3 +55,37 @@ export interface DiagnosticCheck {
|
|
|
55
55
|
status: DiagnosticStatus;
|
|
56
56
|
message: string;
|
|
57
57
|
}
|
|
58
|
+
export interface NpmMcpServerConfig {
|
|
59
|
+
type: 'npm';
|
|
60
|
+
package: string;
|
|
61
|
+
env?: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
export interface BundledMcpServerConfig {
|
|
64
|
+
type: 'bundled';
|
|
65
|
+
path: string;
|
|
66
|
+
install?: string;
|
|
67
|
+
command: string;
|
|
68
|
+
args?: string[];
|
|
69
|
+
env?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
export type McpServerConfig = NpmMcpServerConfig | BundledMcpServerConfig;
|
|
72
|
+
export interface ProfileConfig {
|
|
73
|
+
name: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
skills: Record<string, SkillConfig>;
|
|
76
|
+
mcps: Record<string, McpServerConfig>;
|
|
77
|
+
memory: {
|
|
78
|
+
paths: string[];
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export interface BrainctlMetaConfig {
|
|
82
|
+
active_profile: string;
|
|
83
|
+
agents: AgentName[];
|
|
84
|
+
}
|
|
85
|
+
export interface SyncAgentResult {
|
|
86
|
+
agent: AgentName;
|
|
87
|
+
configPath: string;
|
|
88
|
+
backedUpTo: string | null;
|
|
89
|
+
mcpCount: number;
|
|
90
|
+
}
|
|
91
|
+
export type SyncResult = SyncAgentResult[];
|