brainctl 0.1.4 → 0.1.5
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 -1
- package/dist/commands/profile.d.ts +8 -1
- package/dist/commands/profile.js +31 -1
- package/dist/mcp/server.js +36 -0
- package/dist/services/profile-export-service.d.ts +15 -0
- package/dist/services/profile-export-service.js +63 -0
- package/dist/services/profile-import-service.d.ts +11 -0
- package/dist/services/profile-import-service.js +81 -0
- package/dist/services/profile-service.d.ts +1 -0
- package/dist/services/profile-service.js +1 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
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 ProfileExportService } from './services/profile-export-service.js';
|
|
6
|
+
import { type ProfileImportService } from './services/profile-import-service.js';
|
|
5
7
|
import { type ProfileService } from './services/profile-service.js';
|
|
6
8
|
import { type RunService } from './services/run-service.js';
|
|
7
9
|
import { type StatusService } from './services/status-service.js';
|
|
@@ -12,6 +14,8 @@ export interface CliServices {
|
|
|
12
14
|
statusService: StatusService;
|
|
13
15
|
doctorService: DoctorService;
|
|
14
16
|
profileService: ProfileService;
|
|
17
|
+
profileExportService: ProfileExportService;
|
|
18
|
+
profileImportService: ProfileImportService;
|
|
15
19
|
syncService: SyncService;
|
|
16
20
|
}
|
|
17
21
|
export declare function createProgram(overrides?: Partial<CliServices>): Command;
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,8 @@ import { registerUiCommand } from './commands/ui.js';
|
|
|
14
14
|
import { printError } from './output.js';
|
|
15
15
|
import { createDoctorService } from './services/doctor-service.js';
|
|
16
16
|
import { createInitService } from './services/init-service.js';
|
|
17
|
+
import { createProfileExportService } from './services/profile-export-service.js';
|
|
18
|
+
import { createProfileImportService } from './services/profile-import-service.js';
|
|
17
19
|
import { createProfileService } from './services/profile-service.js';
|
|
18
20
|
import { createRunService } from './services/run-service.js';
|
|
19
21
|
import { createStatusService } from './services/status-service.js';
|
|
@@ -31,7 +33,11 @@ export function createProgram(overrides = {}) {
|
|
|
31
33
|
registerStatusCommand(program, services.statusService);
|
|
32
34
|
registerRunCommand(program, services.runService);
|
|
33
35
|
registerDoctorCommand(program, services.doctorService);
|
|
34
|
-
registerProfileCommand(program,
|
|
36
|
+
registerProfileCommand(program, {
|
|
37
|
+
profileService: services.profileService,
|
|
38
|
+
profileExportService: services.profileExportService,
|
|
39
|
+
profileImportService: services.profileImportService,
|
|
40
|
+
});
|
|
35
41
|
registerSyncCommand(program, services.syncService);
|
|
36
42
|
registerUiCommand(program);
|
|
37
43
|
registerMcpCommand(program);
|
|
@@ -62,6 +68,8 @@ function createDefaultServices(overrides) {
|
|
|
62
68
|
statusService: createStatusService({ resolver }),
|
|
63
69
|
doctorService: createDoctorService({ resolver }),
|
|
64
70
|
profileService,
|
|
71
|
+
profileExportService: createProfileExportService({ profileService }),
|
|
72
|
+
profileImportService: createProfileImportService(),
|
|
65
73
|
syncService: createSyncService({ profileService }),
|
|
66
74
|
...overrides
|
|
67
75
|
};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import type { ProfileExportService } from '../services/profile-export-service.js';
|
|
3
|
+
import type { ProfileImportService } from '../services/profile-import-service.js';
|
|
2
4
|
import type { ProfileService } from '../services/profile-service.js';
|
|
3
|
-
export
|
|
5
|
+
export interface ProfileCommandServices {
|
|
6
|
+
profileService: ProfileService;
|
|
7
|
+
profileExportService: ProfileExportService;
|
|
8
|
+
profileImportService: ProfileImportService;
|
|
9
|
+
}
|
|
10
|
+
export declare function registerProfileCommand(program: Command, services: ProfileCommandServices): void;
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
export function registerProfileCommand(program,
|
|
2
|
+
export function registerProfileCommand(program, services) {
|
|
3
|
+
const { profileService, profileExportService, profileImportService } = services;
|
|
3
4
|
const profileCmd = program
|
|
4
5
|
.command('profile')
|
|
5
6
|
.description('Manage brainctl profiles');
|
|
@@ -40,4 +41,33 @@ export function registerProfileCommand(program, profileService) {
|
|
|
40
41
|
const prev = result.previousProfile ? ` (was "${result.previousProfile}")` : '';
|
|
41
42
|
console.log(`Switched to profile "${name}"${prev}`);
|
|
42
43
|
});
|
|
44
|
+
profileCmd
|
|
45
|
+
.command('export')
|
|
46
|
+
.argument('<name>', 'Profile name to export')
|
|
47
|
+
.option('-o, --output <path>', 'Output file path')
|
|
48
|
+
.description('Export a profile as a portable tarball')
|
|
49
|
+
.action(async (name, options) => {
|
|
50
|
+
const result = await profileExportService.execute({
|
|
51
|
+
cwd: process.cwd(),
|
|
52
|
+
name,
|
|
53
|
+
outputPath: options.output,
|
|
54
|
+
});
|
|
55
|
+
console.log(`Exported profile to ${result.archivePath}`);
|
|
56
|
+
});
|
|
57
|
+
profileCmd
|
|
58
|
+
.command('import')
|
|
59
|
+
.argument('<archive>', 'Path to profile tarball')
|
|
60
|
+
.option('--force', 'Overwrite existing profile', false)
|
|
61
|
+
.description('Import a profile from a tarball')
|
|
62
|
+
.action(async (archive, options) => {
|
|
63
|
+
const result = await profileImportService.execute({
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
archivePath: archive,
|
|
66
|
+
force: options.force,
|
|
67
|
+
});
|
|
68
|
+
console.log(`Imported profile "${result.profileName}"`);
|
|
69
|
+
if (result.installedMcps.length > 0) {
|
|
70
|
+
console.log(`Installed bundled MCPs: ${result.installedMcps.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
43
73
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -6,6 +6,8 @@ import { loadConfig } from '../config.js';
|
|
|
6
6
|
import { loadMemory } from '../context/memory.js';
|
|
7
7
|
import { createDoctorService } from '../services/doctor-service.js';
|
|
8
8
|
import { createMemoryWriteService } from '../services/memory-write-service.js';
|
|
9
|
+
import { createProfileExportService } from '../services/profile-export-service.js';
|
|
10
|
+
import { createProfileImportService } from '../services/profile-import-service.js';
|
|
9
11
|
import { createProfileService } from '../services/profile-service.js';
|
|
10
12
|
import { createRunService } from '../services/run-service.js';
|
|
11
13
|
import { createStatusService } from '../services/status-service.js';
|
|
@@ -175,6 +177,40 @@ export function createMcpServer(options = {}) {
|
|
|
175
177
|
return JSON.stringify(result, null, 2);
|
|
176
178
|
},
|
|
177
179
|
});
|
|
180
|
+
server.addTool({
|
|
181
|
+
name: 'brainctl_export_profile',
|
|
182
|
+
description: 'Export a profile as a portable tarball. Packages the profile config and bundled MCP source code for sharing.',
|
|
183
|
+
parameters: z.object({
|
|
184
|
+
name: z.string().describe('Profile name to export'),
|
|
185
|
+
output_path: z.string().optional().describe('Output file path (defaults to <name>.tar.gz in cwd)'),
|
|
186
|
+
}),
|
|
187
|
+
execute: async (args) => {
|
|
188
|
+
const exportService = createProfileExportService();
|
|
189
|
+
const result = await exportService.execute({
|
|
190
|
+
cwd,
|
|
191
|
+
name: args.name,
|
|
192
|
+
outputPath: args.output_path,
|
|
193
|
+
});
|
|
194
|
+
return JSON.stringify(result, null, 2);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
server.addTool({
|
|
198
|
+
name: 'brainctl_import_profile',
|
|
199
|
+
description: 'Import a profile from a tarball. Extracts bundled MCP source, installs dependencies, and registers the profile.',
|
|
200
|
+
parameters: z.object({
|
|
201
|
+
archive_path: z.string().describe('Path to the profile tarball'),
|
|
202
|
+
force: z.boolean().default(false).describe('Overwrite existing profile if it exists'),
|
|
203
|
+
}),
|
|
204
|
+
execute: async (args) => {
|
|
205
|
+
const importService = createProfileImportService();
|
|
206
|
+
const result = await importService.execute({
|
|
207
|
+
cwd,
|
|
208
|
+
archivePath: args.archive_path,
|
|
209
|
+
force: args.force,
|
|
210
|
+
});
|
|
211
|
+
return JSON.stringify(result, null, 2);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
178
214
|
return server;
|
|
179
215
|
}
|
|
180
216
|
export async function startMcpServer(options = {}) {
|
|
@@ -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
|
+
}
|
|
@@ -99,7 +99,7 @@ async function loadMetaConfig(cwd) {
|
|
|
99
99
|
return { active_profile: '', agents: ['claude', 'codex', 'gemini'] };
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
function parseProfile(source, name) {
|
|
102
|
+
export function parseProfile(source, name) {
|
|
103
103
|
let parsed;
|
|
104
104
|
try {
|
|
105
105
|
parsed = YAML.parse(source) ?? {};
|