brainctl 0.1.17 → 0.1.18

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 (72) hide show
  1. package/dist/cli.d.ts +4 -6
  2. package/dist/cli.js +11 -16
  3. package/dist/commands/profile.d.ts +4 -0
  4. package/dist/commands/profile.js +106 -16
  5. package/dist/commands/status.js +7 -7
  6. package/dist/mcp/server.js +48 -141
  7. package/dist/services/agent-asset-installer.d.ts +3 -0
  8. package/dist/services/agent-asset-installer.js +109 -0
  9. package/dist/services/agent-availability-service.d.ts +11 -0
  10. package/dist/services/agent-availability-service.js +32 -0
  11. package/dist/services/credential-redaction-service.d.ts +1 -0
  12. package/dist/services/credential-redaction-service.js +9 -3
  13. package/dist/services/doctor-service.d.ts +2 -2
  14. package/dist/services/doctor-service.js +7 -63
  15. package/dist/services/portable-profile-pack-service.d.ts +6 -0
  16. package/dist/services/portable-profile-pack-service.js +78 -4
  17. package/dist/services/profile-apply-service.d.ts +34 -0
  18. package/dist/services/profile-apply-service.js +102 -0
  19. package/dist/services/profile-export-service.d.ts +5 -1
  20. package/dist/services/profile-export-service.js +3 -1
  21. package/dist/services/profile-import-service.js +82 -127
  22. package/dist/services/profile-service.d.ts +3 -11
  23. package/dist/services/profile-service.js +57 -102
  24. package/dist/services/profile-snapshot-service.d.ts +12 -0
  25. package/dist/services/profile-snapshot-service.js +47 -0
  26. package/dist/services/status-service.d.ts +9 -7
  27. package/dist/services/status-service.js +14 -13
  28. package/dist/types.d.ts +2 -57
  29. package/dist/ui/routes.d.ts +0 -2
  30. package/dist/ui/routes.js +71 -120
  31. package/dist/web/assets/index-CGmTbSgk.js +63 -0
  32. package/dist/web/assets/index-EIVU5Woh.css +2 -0
  33. package/dist/web/index.html +2 -2
  34. package/package.json +1 -1
  35. package/dist/commands/init.d.ts +0 -3
  36. package/dist/commands/init.js +0 -27
  37. package/dist/commands/run.d.ts +0 -3
  38. package/dist/commands/run.js +0 -25
  39. package/dist/commands/sync.d.ts +0 -3
  40. package/dist/commands/sync.js +0 -31
  41. package/dist/config.d.ts +0 -14
  42. package/dist/config.js +0 -96
  43. package/dist/context/builder.d.ts +0 -6
  44. package/dist/context/builder.js +0 -13
  45. package/dist/context/memory.d.ts +0 -5
  46. package/dist/context/memory.js +0 -43
  47. package/dist/context/skills.d.ts +0 -2
  48. package/dist/context/skills.js +0 -8
  49. package/dist/executor/claude.d.ts +0 -12
  50. package/dist/executor/claude.js +0 -16
  51. package/dist/executor/codex.d.ts +0 -12
  52. package/dist/executor/codex.js +0 -16
  53. package/dist/executor/process.d.ts +0 -11
  54. package/dist/executor/process.js +0 -40
  55. package/dist/executor/resolver.d.ts +0 -13
  56. package/dist/executor/resolver.js +0 -60
  57. package/dist/executor/types.d.ts +0 -14
  58. package/dist/executor/types.js +0 -1
  59. package/dist/services/config-write-service.d.ts +0 -12
  60. package/dist/services/config-write-service.js +0 -70
  61. package/dist/services/init-service.d.ts +0 -14
  62. package/dist/services/init-service.js +0 -88
  63. package/dist/services/memory-write-service.d.ts +0 -12
  64. package/dist/services/memory-write-service.js +0 -56
  65. package/dist/services/run-service.d.ts +0 -15
  66. package/dist/services/run-service.js +0 -94
  67. package/dist/services/sync-service.d.ts +0 -15
  68. package/dist/services/sync-service.js +0 -69
  69. package/dist/ui/streaming.d.ts +0 -3
  70. package/dist/ui/streaming.js +0 -16
  71. package/dist/web/assets/index-Bbophmwh.css +0 -2
  72. package/dist/web/assets/index-DDG_ylui.js +0 -63
package/dist/cli.d.ts CHANGED
@@ -1,22 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { type DoctorService } from './services/doctor-service.js';
4
- import { type InitService } from './services/init-service.js';
4
+ import { type ProfileApplyService } from './services/profile-apply-service.js';
5
5
  import { type ProfileExportService } from './services/profile-export-service.js';
6
6
  import { type ProfileImportService } from './services/profile-import-service.js';
7
7
  import { type ProfileService } from './services/profile-service.js';
8
- import { type RunService } from './services/run-service.js';
8
+ import { type ProfileSnapshotService } from './services/profile-snapshot-service.js';
9
9
  import { type StatusService } from './services/status-service.js';
10
- import { type SyncService } from './services/sync-service.js';
11
10
  export interface CliServices {
12
- initService: InitService;
13
- runService: RunService;
14
11
  statusService: StatusService;
15
12
  doctorService: DoctorService;
16
13
  profileService: ProfileService;
17
14
  profileExportService: ProfileExportService;
18
15
  profileImportService: ProfileImportService;
19
- syncService: SyncService;
16
+ profileApplyService: ProfileApplyService;
17
+ profileSnapshotService: ProfileSnapshotService;
20
18
  }
21
19
  export declare function createProgram(overrides?: Partial<CliServices>): Command;
22
20
  export declare function main(argv?: string[]): Promise<void>;
package/dist/cli.js CHANGED
@@ -5,24 +5,20 @@ import { createInterface } from 'node:readline';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { Command } from 'commander';
7
7
  import { registerDoctorCommand } from './commands/doctor.js';
8
- import { registerInitCommand } from './commands/init.js';
9
8
  import { registerMcpCommand } from './commands/mcp.js';
10
9
  import { registerProfileCommand } from './commands/profile.js';
11
- import { registerRunCommand } from './commands/run.js';
12
10
  import { registerStatusCommand } from './commands/status.js';
13
- import { registerSyncCommand } from './commands/sync.js';
14
11
  import { registerUiCommand } from './commands/ui.js';
15
12
  import { printError } from './output.js';
16
13
  import { createUpdateCheckService } from './services/update-check-service.js';
17
14
  import { createDoctorService } from './services/doctor-service.js';
18
- import { createInitService } from './services/init-service.js';
15
+ import { createProfileApplyService } from './services/profile-apply-service.js';
19
16
  import { createProfileExportService } from './services/profile-export-service.js';
20
17
  import { createProfileImportService } from './services/profile-import-service.js';
21
18
  import { createProfileService } from './services/profile-service.js';
22
- import { createRunService } from './services/run-service.js';
19
+ import { createProfileSnapshotService } from './services/profile-snapshot-service.js';
23
20
  import { createStatusService } from './services/status-service.js';
24
- import { createSyncService } from './services/sync-service.js';
25
- import { createExecutorResolver } from './executor/resolver.js';
21
+ import { createAgentAvailabilityService } from './services/agent-availability-service.js';
26
22
  const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
27
23
  export function createProgram(overrides = {}) {
28
24
  const services = createDefaultServices(overrides);
@@ -31,16 +27,15 @@ export function createProgram(overrides = {}) {
31
27
  .name('brainctl')
32
28
  .description('Manage repeatable AI environments for local agent workflows')
33
29
  .version(packageVersion.version);
34
- registerInitCommand(program, services.initService);
35
30
  registerStatusCommand(program, services.statusService);
36
- registerRunCommand(program, services.runService);
37
31
  registerDoctorCommand(program, services.doctorService);
38
32
  registerProfileCommand(program, {
39
33
  profileService: services.profileService,
40
34
  profileExportService: services.profileExportService,
41
35
  profileImportService: services.profileImportService,
36
+ profileApplyService: services.profileApplyService,
37
+ profileSnapshotService: services.profileSnapshotService,
42
38
  });
43
- registerSyncCommand(program, services.syncService);
44
39
  registerUiCommand(program);
45
40
  registerMcpCommand(program);
46
41
  return program;
@@ -100,17 +95,17 @@ export function shouldRunMain(entryPointPath, moduleUrl) {
100
95
  return resolveRealPath(entryPointPath) === resolveRealPath(fileURLToPath(moduleUrl));
101
96
  }
102
97
  function createDefaultServices(overrides) {
103
- const resolver = createExecutorResolver();
98
+ const availabilityService = createAgentAvailabilityService();
104
99
  const profileService = createProfileService();
100
+ const profileSnapshotService = createProfileSnapshotService();
105
101
  return {
106
- initService: createInitService(),
107
- runService: createRunService({ resolver }),
108
- statusService: createStatusService({ resolver }),
109
- doctorService: createDoctorService({ resolver }),
102
+ statusService: createStatusService({ availabilityService }),
103
+ doctorService: createDoctorService({ availabilityService }),
110
104
  profileService,
111
105
  profileExportService: createProfileExportService({ profileService }),
112
106
  profileImportService: createProfileImportService(),
113
- syncService: createSyncService({ profileService }),
107
+ profileApplyService: createProfileApplyService({ profileService, snapshotService: profileSnapshotService }),
108
+ profileSnapshotService,
114
109
  ...overrides
115
110
  };
116
111
  }
@@ -1,10 +1,14 @@
1
1
  import type { Command } from 'commander';
2
+ import type { ProfileApplyService } from '../services/profile-apply-service.js';
2
3
  import type { ProfileExportService } from '../services/profile-export-service.js';
3
4
  import type { ProfileImportService } from '../services/profile-import-service.js';
4
5
  import type { ProfileService } from '../services/profile-service.js';
6
+ import type { ProfileSnapshotService } from '../services/profile-snapshot-service.js';
5
7
  export interface ProfileCommandServices {
6
8
  profileService: ProfileService;
7
9
  profileExportService: ProfileExportService;
8
10
  profileImportService: ProfileImportService;
11
+ profileApplyService: ProfileApplyService;
12
+ profileSnapshotService: ProfileSnapshotService;
9
13
  }
10
14
  export declare function registerProfileCommand(program: Command, services: ProfileCommandServices): void;
@@ -1,6 +1,7 @@
1
1
  import pc from 'picocolors';
2
+ const ALL_AGENTS = ['claude', 'codex', 'gemini'];
2
3
  export function registerProfileCommand(program, services) {
3
- const { profileService, profileExportService, profileImportService } = services;
4
+ const { profileService, profileExportService, profileImportService, profileApplyService, profileSnapshotService, } = services;
4
5
  const profileCmd = program
5
6
  .command('profile')
6
7
  .description('Manage brainctl profiles');
@@ -8,15 +9,14 @@ export function registerProfileCommand(program, services) {
8
9
  .command('list')
9
10
  .description('List available profiles')
10
11
  .action(async () => {
11
- const { profiles, activeProfile } = await profileService.list({ cwd: process.cwd() });
12
+ const { profiles } = await profileService.list({ cwd: process.cwd() });
12
13
  if (profiles.length === 0) {
13
14
  console.log('No profiles found. Run "brainctl profile create <name>" to create one.');
14
15
  return;
15
16
  }
16
17
  console.log(pc.bold('Profiles:'));
17
18
  for (const name of profiles) {
18
- const marker = name === activeProfile ? pc.green(' (active)') : '';
19
- console.log(` ${name}${marker}`);
19
+ console.log(` ${name}`);
20
20
  }
21
21
  });
22
22
  profileCmd
@@ -32,21 +32,14 @@ export function registerProfileCommand(program, services) {
32
32
  });
33
33
  console.log(`Created profile at ${result.profilePath}`);
34
34
  });
35
- profileCmd
36
- .command('use')
37
- .argument('<name>', 'Profile name to activate')
38
- .description('Switch the active profile')
39
- .action(async (name) => {
40
- const result = await profileService.use({ cwd: process.cwd(), name });
41
- const prev = result.previousProfile ? ` (was "${result.previousProfile}")` : '';
42
- console.log(`Switched to profile "${name}"${prev}`);
43
- });
44
35
  profileCmd
45
36
  .command('export')
46
37
  .argument('[name]', 'Profile name to export')
47
38
  .option('-a, --agent <name>', 'Pack a live agent config instead (claude, codex, gemini)')
48
- .option('-o, --output <path>', 'Output file path')
49
- .description('Export a profile as a portable tarball')
39
+ .option('-o, --output <path>', 'Output file or directory path')
40
+ .option('-f, --format <format>', 'Output format: tarball (default) or folder', 'tarball')
41
+ .option('--credentials <mode>', 'How to handle secrets: redact (default, public-safe) or keep (writes .env with real values for self-sync)', 'redact')
42
+ .description('Export a profile as a portable tarball or folder')
50
43
  .action(async (name, options) => {
51
44
  const agent = options.agent === 'claude' || options.agent === 'codex' || options.agent === 'gemini'
52
45
  ? options.agent
@@ -54,14 +47,28 @@ export function registerProfileCommand(program, services) {
54
47
  if (!agent && !name) {
55
48
  throw new Error('Provide a profile name or --agent <name>.');
56
49
  }
50
+ if (options.format && options.format !== 'tarball' && options.format !== 'folder') {
51
+ throw new Error(`Invalid --format "${options.format}". Use "tarball" or "folder".`);
52
+ }
53
+ if (options.credentials &&
54
+ options.credentials !== 'redact' &&
55
+ options.credentials !== 'keep') {
56
+ throw new Error(`Invalid --credentials "${options.credentials}". Use "redact" or "keep".`);
57
+ }
57
58
  const result = await profileExportService.execute({
58
59
  cwd: process.cwd(),
59
60
  source: agent
60
61
  ? { source: 'agent', agent, cwd: process.cwd() }
61
62
  : { source: 'profile', name: name },
62
63
  outputPath: options.output,
64
+ format: options.format ?? 'tarball',
65
+ credentialsMode: options.credentials ?? 'redact',
63
66
  });
64
- console.log(`Exported profile to ${result.archivePath}`);
67
+ for (const warning of result.warnings) {
68
+ console.warn(pc.yellow(`warning: ${warning}`));
69
+ }
70
+ const label = result.format === 'folder' ? 'profile folder' : 'profile tarball';
71
+ console.log(`Exported ${label} to ${result.archivePath}`);
65
72
  });
66
73
  profileCmd
67
74
  .command('import')
@@ -81,6 +88,89 @@ export function registerProfileCommand(program, services) {
81
88
  console.log(`Installed bundled MCPs: ${result.installedMcps.join(', ')}`);
82
89
  }
83
90
  });
91
+ profileCmd
92
+ .command('apply')
93
+ .argument('<name>', 'Profile name to apply')
94
+ .option('-a, --agent <list>', 'Comma-separated agents to target (claude, codex, gemini, or all)', 'all')
95
+ .option('-i, --items <list>', 'Comma-separated items to apply (e.g. mcp:github,plugin:demo,skill:reviewer). Default: everything matching.')
96
+ .option('--no-backup', 'Skip auto-backup of live agent state before applying')
97
+ .description('Apply a profile (MCPs + plugins + skills) to selected agents')
98
+ .action(async (name, options) => {
99
+ const agents = parseAgentList(options.agent);
100
+ const items = options.items ? parseItemList(options.items) : undefined;
101
+ const { backups, applied } = await profileApplyService.execute({
102
+ cwd: process.cwd(),
103
+ profileName: name,
104
+ agents,
105
+ items,
106
+ backup: options.backup,
107
+ });
108
+ if (backups.length > 0) {
109
+ console.log(pc.bold('Backups:'));
110
+ for (const b of backups) {
111
+ console.log(` ${b.agent} -> ${b.profileName}`);
112
+ }
113
+ }
114
+ console.log(pc.bold(`Applied "${name}" to:`));
115
+ for (const r of applied) {
116
+ const extras = [`${r.mcpCount} MCPs`];
117
+ if (r.pluginsInstalled?.length)
118
+ extras.push(`plugins: ${r.pluginsInstalled.join(',')}`);
119
+ if (r.userSkillsInstalled?.length)
120
+ extras.push(`skills: ${r.userSkillsInstalled.join(',')}`);
121
+ console.log(` ${r.agent}: ${extras.join(' | ')}`);
122
+ }
123
+ });
124
+ profileCmd
125
+ .command('snapshot')
126
+ .option('-a, --agent <name>', 'Agent to snapshot (claude, codex, gemini)')
127
+ .option('--as <name>', 'Profile name to write into (default: backup-<agent>-<timestamp>)')
128
+ .description("Snapshot a live agent's MCPs+plugins+skills into a new profile folder")
129
+ .action(async (options) => {
130
+ if (!options.agent || !ALL_AGENTS.includes(options.agent)) {
131
+ throw new Error('Provide --agent <claude|codex|gemini>.');
132
+ }
133
+ const agent = options.agent;
134
+ const { defaultBackupProfileName } = await import('../services/profile-snapshot-service.js');
135
+ const profileName = options.as ?? defaultBackupProfileName(agent);
136
+ const result = await profileSnapshotService.execute({
137
+ cwd: process.cwd(),
138
+ agent,
139
+ profileName,
140
+ });
141
+ console.log(`Snapshotted ${agent} into ${result.profilePath}`);
142
+ });
143
+ }
144
+ function parseAgentList(value) {
145
+ if (value === 'all')
146
+ return [...ALL_AGENTS];
147
+ const parts = value.split(',').map((s) => s.trim()).filter(Boolean);
148
+ for (const p of parts) {
149
+ if (!ALL_AGENTS.includes(p)) {
150
+ throw new Error(`Invalid agent "${p}". Use claude, codex, gemini, or all.`);
151
+ }
152
+ }
153
+ return parts;
154
+ }
155
+ function parseItemList(value) {
156
+ return value
157
+ .split(',')
158
+ .map((s) => s.trim())
159
+ .filter(Boolean)
160
+ .map((entry) => {
161
+ const colonIdx = entry.indexOf(':');
162
+ if (colonIdx <= 0) {
163
+ throw new Error(`Invalid item "${entry}". Use type:name (e.g. mcp:github).`);
164
+ }
165
+ const type = entry.slice(0, colonIdx);
166
+ const name = entry.slice(colonIdx + 1);
167
+ if (type !== 'mcp' && type !== 'plugin' && type !== 'skill') {
168
+ throw new Error(`Invalid item type "${type}". Use mcp, plugin, or skill.`);
169
+ }
170
+ if (!name)
171
+ throw new Error(`Item "${entry}" missing name.`);
172
+ return { type, name };
173
+ });
84
174
  }
85
175
  function collectCredentialOption(value, previous) {
86
176
  return [...previous, value];
@@ -2,17 +2,17 @@ import pc from 'picocolors';
2
2
  export function registerStatusCommand(program, statusService) {
3
3
  program
4
4
  .command('status')
5
- .description('Show current brainctl configuration status')
5
+ .description('Show agent availability and profile inventory')
6
6
  .action(async () => {
7
7
  const status = await statusService.execute({ cwd: process.cwd() });
8
8
  console.log(pc.bold('brainctl status'));
9
- console.log(`Config: ${status.configPath}`);
10
- console.log(`Memory files loaded: ${status.memory.count}`);
11
- console.log(`Available skills: ${status.skills.length > 0 ? status.skills.join(', ') : 'none'}`);
12
- console.log(`MCP count: ${status.mcpCount}`);
13
- console.log('Available agents:');
9
+ console.log(`Profiles: ${status.profiles.count}`);
10
+ for (const name of status.profiles.names) {
11
+ console.log(` ${name}`);
12
+ }
13
+ console.log('Agents:');
14
14
  for (const agent of Object.values(status.agents)) {
15
- console.log(`- ${agent.agent}: ${agent.available ? pc.green('available') : pc.yellow('missing')}`);
15
+ console.log(` ${agent.agent}: ${agent.available ? pc.green('available') : pc.yellow('missing')}`);
16
16
  }
17
17
  });
18
18
  }
@@ -1,20 +1,17 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
- import path from 'node:path';
4
3
  import { FastMCP } from 'fastmcp';
5
4
  import { z } from 'zod';
6
- import { loadConfig } from '../config.js';
7
- import { loadMemory } from '../context/memory.js';
8
5
  import { createAgentConfigService } from '../services/agent-config-service.js';
9
6
  import { createDoctorService } from '../services/doctor-service.js';
10
7
  import { startUiServer } from '../ui/server.js';
11
- import { createMemoryWriteService } from '../services/memory-write-service.js';
12
8
  import { createProfileExportService } from '../services/profile-export-service.js';
13
9
  import { createProfileImportService } from '../services/profile-import-service.js';
10
+ import { createProfileApplyService } from '../services/profile-apply-service.js';
14
11
  import { createProfileService } from '../services/profile-service.js';
15
- import { createRunService } from '../services/run-service.js';
12
+ import { createProfileSnapshotService, defaultBackupProfileName, } from '../services/profile-snapshot-service.js';
16
13
  import { createStatusService } from '../services/status-service.js';
17
- import { createSyncService } from '../services/sync-service.js';
14
+ const ALL_AGENTS = ['claude', 'codex', 'gemini'];
18
15
  const packageVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
19
16
  export function createMcpServer(options = {}) {
20
17
  const cwd = options.cwd ?? process.cwd();
@@ -23,53 +20,6 @@ export function createMcpServer(options = {}) {
23
20
  name: 'brainctl',
24
21
  version: packageVersion.version,
25
22
  });
26
- server.addTool({
27
- name: 'brainctl_list_skills',
28
- description: 'List available skills from the ai-stack.yaml config',
29
- parameters: z.object({}),
30
- execute: async () => {
31
- const config = await loadConfig({ cwd });
32
- const skills = Object.entries(config.skills).map(([name, skill]) => ({
33
- name,
34
- description: skill.description ?? null,
35
- }));
36
- return JSON.stringify(skills, null, 2);
37
- },
38
- });
39
- server.addTool({
40
- name: 'brainctl_run',
41
- description: 'Execute a skill with input text. Runs the skill through the configured agent and returns the output.',
42
- parameters: z.object({
43
- skill: z.string().describe('Skill name as defined in ai-stack.yaml'),
44
- input: z.string().describe('Input text to pass to the skill'),
45
- agent: z.enum(['claude', 'codex']).default('claude').describe('Agent to use for execution'),
46
- fallback_agent: z.enum(['claude', 'codex']).optional().describe('Fallback agent if primary is unavailable'),
47
- }),
48
- execute: async (args) => {
49
- const inputPath = path.join(cwd, `.brainctl-mcp-input-${Date.now()}.tmp`);
50
- const { writeFile: writeFileAsync, unlink } = await import('node:fs/promises');
51
- try {
52
- await writeFileAsync(inputPath, args.input, 'utf8');
53
- const runService = createRunService();
54
- const trace = await runService.execute({
55
- cwd,
56
- skill: args.skill,
57
- inputFile: path.basename(inputPath),
58
- primaryAgent: args.agent,
59
- fallbackAgent: args.fallback_agent,
60
- });
61
- return trace.finalOutput;
62
- }
63
- finally {
64
- try {
65
- await unlink(inputPath);
66
- }
67
- catch {
68
- // temp file cleanup is best-effort
69
- }
70
- }
71
- },
72
- });
73
23
  server.addTool({
74
24
  name: 'brainctl_status',
75
25
  description: 'Show project status: config path, memory files, available skills, and agent availability',
@@ -91,94 +41,67 @@ export function createMcpServer(options = {}) {
91
41
  },
92
42
  });
93
43
  server.addTool({
94
- name: 'brainctl_read_memory',
95
- 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.',
44
+ name: 'brainctl_list_profiles',
45
+ description: 'List available profiles and show which one is active.',
96
46
  parameters: z.object({}),
97
47
  execute: async () => {
98
- const config = await loadConfig({ cwd });
99
- const memory = await loadMemory({ paths: config.memory.paths });
100
- const result = {
101
- count: memory.count,
102
- files: memory.entries.map((entry) => ({
103
- path: entry.path,
104
- content: entry.content,
105
- })),
106
- };
48
+ const profileService = createProfileService();
49
+ const result = await profileService.list({ cwd });
107
50
  return JSON.stringify(result, null, 2);
108
51
  },
109
52
  });
110
53
  server.addTool({
111
- name: 'brainctl_write_memory',
112
- 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.',
54
+ name: 'brainctl_apply_profile',
55
+ description: 'Apply a profile (MCPs, plugins, user skills) to the specified agents. Selective by --agents and --items. Auto-backs up live agent state before a full apply unless backup=false.',
113
56
  parameters: z.object({
114
- file_path: z.string().describe('Relative path for the memory file (e.g., "memory/notes.md")'),
115
- content: z.string().describe('Markdown content to write'),
57
+ name: z.string().describe('Profile name to apply'),
58
+ agents: z
59
+ .array(z.enum(['claude', 'codex', 'gemini']))
60
+ .optional()
61
+ .describe('Agents to target (default: all three)'),
62
+ items: z
63
+ .array(z.object({
64
+ type: z.enum(['mcp', 'plugin', 'skill']),
65
+ name: z.string(),
66
+ }))
67
+ .optional()
68
+ .describe('Specific items to apply (default: everything matching)'),
69
+ backup: z
70
+ .boolean()
71
+ .optional()
72
+ .describe('Force backup on/off (default: on for full apply, off for partial)'),
116
73
  }),
117
74
  execute: async (args) => {
118
- const memoryWriteService = createMemoryWriteService();
119
- const result = await memoryWriteService.execute({
75
+ const applyService = createProfileApplyService();
76
+ const result = await applyService.execute({
120
77
  cwd,
121
- filePath: args.file_path,
122
- content: args.content,
78
+ profileName: args.name,
79
+ agents: args.agents ?? ALL_AGENTS,
80
+ items: args.items,
81
+ backup: args.backup,
123
82
  });
124
- return JSON.stringify({ written: result.filePath });
125
- },
126
- });
127
- server.addTool({
128
- name: 'brainctl_get_skill',
129
- 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.',
130
- parameters: z.object({
131
- skill: z.string().describe('Skill name as defined in ai-stack.yaml'),
132
- }),
133
- execute: async (args) => {
134
- const config = await loadConfig({ cwd });
135
- const skillConfig = config.skills[args.skill];
136
- if (!skillConfig) {
137
- throw new Error(`Skill "${args.skill}" is not defined in ai-stack.yaml.`);
138
- }
139
- return JSON.stringify({
140
- name: args.skill,
141
- description: skillConfig.description ?? null,
142
- prompt: skillConfig.prompt,
143
- }, null, 2);
144
- },
145
- });
146
- server.addTool({
147
- name: 'brainctl_list_profiles',
148
- description: 'List available profiles and show which one is active.',
149
- parameters: z.object({}),
150
- execute: async () => {
151
- const profileService = createProfileService();
152
- const result = await profileService.list({ cwd });
153
83
  return JSON.stringify(result, null, 2);
154
84
  },
155
85
  });
156
86
  server.addTool({
157
- name: 'brainctl_switch_profile',
158
- description: 'Switch the active profile and sync it to all configured agents. Combines profile switch + sync in one step.',
87
+ name: 'brainctl_snapshot_agent',
88
+ description: "Snapshot a live agent's MCPs+plugins+skills into a new profile folder. Useful for backups or capturing your current setup as a shareable profile.",
159
89
  parameters: z.object({
160
- name: z.string().describe('Profile name to activate'),
90
+ agent: z.enum(['claude', 'codex', 'gemini']),
91
+ as: z
92
+ .string()
93
+ .optional()
94
+ .describe('Profile name to write into (default: backup-<agent>-<timestamp>)'),
161
95
  }),
162
96
  execute: async (args) => {
163
- const profileService = createProfileService();
164
- const switchResult = await profileService.use({ cwd, name: args.name });
165
- const syncService = createSyncService({ profileService });
166
- const syncResult = await syncService.execute({ cwd });
167
- return JSON.stringify({
168
- previousProfile: switchResult.previousProfile,
169
- activeProfile: args.name,
170
- synced: syncResult,
171
- }, null, 2);
172
- },
173
- });
174
- server.addTool({
175
- name: 'brainctl_sync',
176
- description: 'Sync the active profile to all configured agent configs (Claude, Codex). Creates backups before overwriting.',
177
- parameters: z.object({}),
178
- execute: async () => {
179
- const syncService = createSyncService();
180
- const result = await syncService.execute({ cwd });
181
- return JSON.stringify(result, null, 2);
97
+ const snapshotService = createProfileSnapshotService();
98
+ const profileName = args.as ?? defaultBackupProfileName(args.agent);
99
+ const result = await snapshotService.execute({
100
+ cwd,
101
+ agent: args.agent,
102
+ profileName,
103
+ });
104
+ return JSON.stringify({ profileName, ...result }, null, 2);
182
105
  },
183
106
  });
184
107
  server.addTool({
@@ -212,20 +135,13 @@ export function createMcpServer(options = {}) {
212
135
  });
213
136
  server.addTool({
214
137
  name: 'brainctl_update_profile',
215
- 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.',
138
+ description: 'Update a profile config. Pass the full profile object with name, optional description, and mcps map.',
216
139
  parameters: z.object({
217
140
  name: z.string().describe('Profile name to update'),
218
141
  config: z.object({
219
142
  name: z.string(),
220
143
  description: z.string().optional(),
221
- skills: z.record(z.string(), z.object({
222
- description: z.string().optional(),
223
- prompt: z.string(),
224
- })),
225
144
  mcps: z.record(z.string(), z.unknown()),
226
- memory: z.object({
227
- paths: z.array(z.string()),
228
- }),
229
145
  }).describe('Full profile config object'),
230
146
  }),
231
147
  execute: async (args) => {
@@ -252,25 +168,17 @@ export function createMcpServer(options = {}) {
252
168
  });
253
169
  server.addTool({
254
170
  name: 'brainctl_copy_profile_items',
255
- 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.',
171
+ description: 'Copy MCPs from one profile to another. Existing MCPs with the same key in the target are overwritten.',
256
172
  parameters: z.object({
257
173
  source: z.string().describe('Source profile name'),
258
174
  target: z.string().describe('Target profile name'),
259
- skills: z.array(z.string()).default([]).describe('Skill keys to copy'),
260
175
  mcps: z.array(z.string()).default([]).describe('MCP keys to copy'),
261
176
  }),
262
177
  execute: async (args) => {
263
178
  const profileService = createProfileService();
264
179
  const sourceProfile = await profileService.get({ cwd, name: args.source });
265
180
  const targetProfile = await profileService.get({ cwd, name: args.target });
266
- const copiedSkills = [];
267
181
  const copiedMcps = [];
268
- for (const key of args.skills) {
269
- if (sourceProfile.skills[key]) {
270
- targetProfile.skills[key] = sourceProfile.skills[key];
271
- copiedSkills.push(key);
272
- }
273
- }
274
182
  for (const key of args.mcps) {
275
183
  if (sourceProfile.mcps[key]) {
276
184
  targetProfile.mcps[key] = sourceProfile.mcps[key];
@@ -281,7 +189,6 @@ export function createMcpServer(options = {}) {
281
189
  return JSON.stringify({
282
190
  source: args.source,
283
191
  target: args.target,
284
- copiedSkills,
285
192
  copiedMcps,
286
193
  }, null, 2);
287
194
  },
@@ -0,0 +1,3 @@
1
+ import type { PortablePluginSnapshot, PortableUserSkillSnapshot } from '../types.js';
2
+ export declare function installPlugin(sourceDir: string, plugin: PortablePluginSnapshot): Promise<void>;
3
+ export declare function installUserSkill(sourceDir: string, skill: PortableUserSkillSnapshot): Promise<void>;