brainctl 0.1.7 → 0.1.9

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 (51) hide show
  1. package/README.md +210 -157
  2. package/dist/cli.js +40 -0
  3. package/dist/commands/mcp.js +35 -0
  4. package/dist/commands/profile.js +35 -2
  5. package/dist/mcp/server.js +51 -5
  6. package/dist/services/agent-config-service.d.ts +4 -2
  7. package/dist/services/agent-config-service.js +50 -15
  8. package/dist/services/agent-converter-service.d.ts +21 -0
  9. package/dist/services/agent-converter-service.js +182 -0
  10. package/dist/services/credential-redaction-service.d.ts +13 -0
  11. package/dist/services/credential-redaction-service.js +89 -0
  12. package/dist/services/credential-resolution-service.d.ts +11 -0
  13. package/dist/services/credential-resolution-service.js +69 -0
  14. package/dist/services/mcp-preflight-service.d.ts +3 -2
  15. package/dist/services/mcp-preflight-service.js +159 -5
  16. package/dist/services/plugin-install-service.d.ts +43 -0
  17. package/dist/services/plugin-install-service.js +379 -21
  18. package/dist/services/portable-mcp-classifier.d.ts +12 -0
  19. package/dist/services/portable-mcp-classifier.js +116 -0
  20. package/dist/services/portable-profile-pack-service.d.ts +26 -0
  21. package/dist/services/portable-profile-pack-service.js +264 -0
  22. package/dist/services/profile-export-service.d.ts +15 -3
  23. package/dist/services/profile-export-service.js +10 -57
  24. package/dist/services/profile-import-service.d.ts +9 -1
  25. package/dist/services/profile-import-service.js +265 -10
  26. package/dist/services/profile-service.js +11 -0
  27. package/dist/services/runtime-detector.d.ts +9 -0
  28. package/dist/services/runtime-detector.js +130 -0
  29. package/dist/services/skill-paths.d.ts +2 -0
  30. package/dist/services/skill-paths.js +14 -0
  31. package/dist/services/sync/agent-reader.d.ts +9 -0
  32. package/dist/services/sync/agent-reader.js +177 -35
  33. package/dist/services/sync/claude-writer.js +0 -6
  34. package/dist/services/sync/codex-writer.d.ts +1 -0
  35. package/dist/services/sync/codex-writer.js +21 -8
  36. package/dist/services/sync/gemini-writer.js +5 -7
  37. package/dist/services/sync/plugin-skill-reader.d.ts +5 -0
  38. package/dist/services/sync/plugin-skill-reader.js +142 -1
  39. package/dist/services/sync-service.js +1 -1
  40. package/dist/services/update-check-service.d.ts +33 -0
  41. package/dist/services/update-check-service.js +128 -0
  42. package/dist/types.d.ts +47 -0
  43. package/dist/ui/routes.js +35 -8
  44. package/dist/web/assets/index-Cdb5hbxM.css +1 -0
  45. package/dist/web/assets/index-gN83hZYA.js +65 -0
  46. package/dist/web/favicon-light.svg +13 -0
  47. package/dist/web/favicon.svg +13 -0
  48. package/dist/web/index.html +7 -2
  49. package/package.json +5 -1
  50. package/dist/web/assets/index-BCkorugl.css +0 -1
  51. package/dist/web/assets/index-sGnTMhkX.js +0 -16
@@ -0,0 +1,26 @@
1
+ import type { AgentName } from '../types.js';
2
+ import { type AgentConfigService } from './agent-config-service.js';
3
+ import { type ProfileService } from './profile-service.js';
4
+ export type PortablePackSource = {
5
+ source: 'profile';
6
+ name: string;
7
+ } | {
8
+ source: 'agent';
9
+ agent: AgentName;
10
+ cwd: string;
11
+ };
12
+ export interface PortableProfilePackService {
13
+ execute(options: {
14
+ cwd?: string;
15
+ source: PortablePackSource;
16
+ outputPath?: string;
17
+ }): Promise<{
18
+ archivePath: string;
19
+ }>;
20
+ }
21
+ interface PortableProfilePackServiceDependencies {
22
+ profileService?: Pick<ProfileService, 'get'>;
23
+ agentConfigService?: Pick<AgentConfigService, 'readAll'>;
24
+ }
25
+ export declare function createPortableProfilePackService(deps?: PortableProfilePackServiceDependencies): PortableProfilePackService;
26
+ export {};
@@ -0,0 +1,264 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import YAML from 'yaml';
7
+ import { ProfileError } from '../errors.js';
8
+ import { createAgentConfigService } from './agent-config-service.js';
9
+ import { redactPortableMcpCredentials } from './credential-redaction-service.js';
10
+ import { createProfileService } from './profile-service.js';
11
+ import { classifyPortableMcp } from './portable-mcp-classifier.js';
12
+ import { findProjectRoot, getDefaultExclude, getDefaultInstall } from './runtime-detector.js';
13
+ const packageVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
14
+ export function createPortableProfilePackService(deps = {}) {
15
+ const profileService = deps.profileService ?? createProfileService();
16
+ const agentConfigService = deps.agentConfigService ?? createAgentConfigService();
17
+ return {
18
+ async execute(options) {
19
+ const cwd = options.source.source === 'agent' ? options.source.cwd : options.cwd ?? process.cwd();
20
+ const stagingDir = await mkdtemp(path.join(tmpdir(), 'brainctl-pack-'));
21
+ try {
22
+ const packed = await buildPackedProfile({
23
+ cwd,
24
+ source: options.source,
25
+ profileService,
26
+ agentConfigService,
27
+ });
28
+ await writeFile(path.join(stagingDir, 'manifest.yaml'), YAML.stringify(packed.manifest), 'utf8');
29
+ await writeFile(path.join(stagingDir, 'profile.yaml'), YAML.stringify(packed.profile), 'utf8');
30
+ for (const [key, sourcePath] of packed.bundledSources) {
31
+ const destPath = path.join(stagingDir, 'mcps', key);
32
+ await mkdir(destPath, { recursive: true });
33
+ const excludePatterns = getExcludePatternsForMcp(packed.profile.mcps[key]);
34
+ await cp(sourcePath, destPath, {
35
+ recursive: true,
36
+ filter: (src) => !matchesExcludePattern(src, excludePatterns),
37
+ });
38
+ }
39
+ for (const [archivePath, sourcePath] of packed.bundledPlugins) {
40
+ const destPath = path.join(stagingDir, archivePath);
41
+ await mkdir(path.dirname(destPath), { recursive: true });
42
+ await cp(sourcePath, destPath, {
43
+ recursive: true,
44
+ filter: (src) => !matchesPluginExclude(src),
45
+ });
46
+ }
47
+ for (const [archivePath, sourcePath] of packed.bundledUserSkills) {
48
+ const destPath = path.join(stagingDir, archivePath);
49
+ await mkdir(path.dirname(destPath), { recursive: true });
50
+ await cp(sourcePath, destPath, {
51
+ recursive: true,
52
+ filter: (src) => !matchesPluginExclude(src),
53
+ });
54
+ }
55
+ const outputPath = options.outputPath ?? path.join(cwd, `${packed.profile.name}.tar.gz`);
56
+ execSync(`tar -czf "${outputPath}" -C "${stagingDir}" .`, {
57
+ stdio: 'pipe',
58
+ });
59
+ return { archivePath: outputPath };
60
+ }
61
+ finally {
62
+ await rm(stagingDir, { recursive: true, force: true });
63
+ }
64
+ },
65
+ };
66
+ }
67
+ async function buildPackedProfile(options) {
68
+ if (options.source.source === 'profile') {
69
+ const profile = await options.profileService.get({
70
+ cwd: options.cwd,
71
+ name: options.source.name,
72
+ });
73
+ return redactAndNormalizeProfile(profile, options.cwd, {
74
+ kind: 'profile',
75
+ profileName: profile.name,
76
+ });
77
+ }
78
+ const agentSource = options.source;
79
+ const configs = await options.agentConfigService.readAll({ cwd: agentSource.cwd });
80
+ const agentConfig = configs.find((config) => config.agent === agentSource.agent);
81
+ if (!agentConfig?.exists) {
82
+ throw new ProfileError(`Agent "${agentSource.agent}" does not have a live config to pack.`);
83
+ }
84
+ const mcpKeys = new Set([
85
+ ...Object.keys(agentConfig.mcpServers),
86
+ ...Object.keys(agentConfig.remoteMcpServers),
87
+ ]);
88
+ const mcps = Object.fromEntries(Array.from(mcpKeys, (key) => {
89
+ const classified = classifyPortableMcp({
90
+ cwd: options.cwd,
91
+ key,
92
+ entry: agentConfig.mcpServers[key] ?? { command: '' },
93
+ remote: agentConfig.remoteMcpServers[key],
94
+ });
95
+ if (classified.kind === 'local' && classified.source === 'bundled') {
96
+ const entrypoint = agentConfig.mcpServers[key]?.args?.[0];
97
+ const entrypointPath = entrypoint
98
+ ? path.resolve(agentSource.cwd, entrypoint)
99
+ : classified.path;
100
+ const { marker } = findProjectRoot(entrypointPath, classified.runtime);
101
+ if (!classified.install) {
102
+ const defaultInstall = getDefaultInstall(classified.runtime, marker, classified.path, entrypoint);
103
+ if (defaultInstall) {
104
+ classified.install = defaultInstall;
105
+ }
106
+ }
107
+ if (!classified.exclude) {
108
+ const defaultExclude = getDefaultExclude(classified.runtime, marker);
109
+ if (defaultExclude) {
110
+ classified.exclude = defaultExclude;
111
+ }
112
+ }
113
+ }
114
+ return [key, classified];
115
+ }));
116
+ const profileName = `${sanitizePackName(path.basename(options.cwd) || 'workspace')}-${agentSource.agent}`;
117
+ const extras = collectAgentExtras(agentConfig);
118
+ return redactAndNormalizeProfile({
119
+ name: profileName,
120
+ skills: {},
121
+ mcps,
122
+ memory: { paths: [] },
123
+ }, agentSource.cwd, {
124
+ kind: 'agent',
125
+ agent: agentSource.agent,
126
+ }, extras);
127
+ }
128
+ function collectAgentExtras(agentConfig) {
129
+ const plugins = [];
130
+ const bundledPlugins = new Map();
131
+ const userSkills = [];
132
+ const bundledUserSkills = new Map();
133
+ for (const skill of agentConfig.skills) {
134
+ if (skill.kind === 'plugin') {
135
+ const installPath = skill.installPath;
136
+ const source = skill.source;
137
+ if (!installPath || !source)
138
+ continue;
139
+ const safeName = sanitizePackName(`${skill.name}--${source}`);
140
+ const archivePath = `plugins/${agentConfig.agent}/${safeName}`;
141
+ bundledPlugins.set(archivePath, installPath);
142
+ plugins.push({
143
+ agent: agentConfig.agent,
144
+ name: skill.name,
145
+ source,
146
+ marketplace: inferMarketplace(agentConfig.agent, installPath, source),
147
+ version: inferPluginVersion(agentConfig.agent, installPath),
148
+ archivePath,
149
+ ...(skill.managed ? { managed: true } : {}),
150
+ ...(skill.pluginSkills && skill.pluginSkills.length > 0 ? { pluginSkills: skill.pluginSkills } : {}),
151
+ ...(skill.pluginMcps && skill.pluginMcps.length > 0 ? { pluginMcps: skill.pluginMcps } : {}),
152
+ ...(skill.pluginAgents && skill.pluginAgents.length > 0 ? { pluginAgents: skill.pluginAgents } : {}),
153
+ ...(skill.pluginCommands && skill.pluginCommands.length > 0 ? { pluginCommands: skill.pluginCommands } : {}),
154
+ });
155
+ continue;
156
+ }
157
+ if (skill.kind === 'skill' && skill.source === 'local') {
158
+ const localSkillDir = path.join(homedir(), `.${agentConfig.agent}`, 'skills', skill.name);
159
+ const archivePath = `skills/${agentConfig.agent}/${skill.name}`;
160
+ bundledUserSkills.set(archivePath, localSkillDir);
161
+ userSkills.push({
162
+ agent: agentConfig.agent,
163
+ name: skill.name,
164
+ archivePath,
165
+ });
166
+ }
167
+ }
168
+ return { plugins, bundledPlugins, userSkills, bundledUserSkills };
169
+ }
170
+ function inferMarketplace(agent, installPath, source) {
171
+ const cacheSegment = path.join(homedir(), `.${agent}`, 'plugins', 'cache') + path.sep;
172
+ if (installPath.startsWith(cacheSegment)) {
173
+ const tail = installPath.slice(cacheSegment.length).split(path.sep);
174
+ if (tail.length > 0 && tail[0].length > 0)
175
+ return tail[0];
176
+ }
177
+ return source;
178
+ }
179
+ function inferPluginVersion(agent, installPath) {
180
+ const cacheSegment = path.join(homedir(), `.${agent}`, 'plugins', 'cache') + path.sep;
181
+ if (installPath.startsWith(cacheSegment)) {
182
+ const tail = installPath.slice(cacheSegment.length).split(path.sep);
183
+ if (tail.length >= 3)
184
+ return tail[2];
185
+ }
186
+ const base = path.basename(installPath);
187
+ return base.length > 0 ? base : undefined;
188
+ }
189
+ function redactAndNormalizeProfile(profile, cwd, source, extras) {
190
+ const bundledSources = new Map();
191
+ const credentials = new Map();
192
+ const mcps = Object.fromEntries(Object.entries(profile.mcps).map(([key, config]) => {
193
+ const result = redactPortableMcpCredentials(config);
194
+ for (const credential of result.credentials) {
195
+ credentials.set(credential.key, credential);
196
+ }
197
+ if (result.redacted.kind === 'local' && result.redacted.source === 'bundled') {
198
+ const sourcePath = path.isAbsolute(result.redacted.path)
199
+ ? result.redacted.path
200
+ : path.resolve(cwd, result.redacted.path);
201
+ bundledSources.set(key, sourcePath);
202
+ return [
203
+ key,
204
+ {
205
+ ...result.redacted,
206
+ path: `./mcps/${key}`,
207
+ },
208
+ ];
209
+ }
210
+ return [key, result.redacted];
211
+ }));
212
+ const hasExtras = extras !== undefined && (extras.plugins.length > 0 || extras.userSkills.length > 0);
213
+ return {
214
+ manifest: {
215
+ schemaVersion: hasExtras ? 2 : 1,
216
+ profileName: profile.name,
217
+ createdBy: {
218
+ tool: 'brainctl',
219
+ version: packageVersion.version,
220
+ },
221
+ ...(source ? { source } : {}),
222
+ ...(credentials.size > 0 ? { credentials: Array.from(credentials.values()) } : {}),
223
+ ...(extras && extras.plugins.length > 0 ? { plugins: extras.plugins } : {}),
224
+ ...(extras && extras.userSkills.length > 0 ? { userSkills: extras.userSkills } : {}),
225
+ },
226
+ profile: {
227
+ ...profile,
228
+ mcps,
229
+ },
230
+ bundledSources,
231
+ bundledPlugins: extras?.bundledPlugins ?? new Map(),
232
+ bundledUserSkills: extras?.bundledUserSkills ?? new Map(),
233
+ };
234
+ }
235
+ function sanitizePackName(value) {
236
+ return value
237
+ .trim()
238
+ .toLowerCase()
239
+ .replace(/[^a-z0-9]+/g, '-')
240
+ .replace(/^-+|-+$/g, '') || 'workspace';
241
+ }
242
+ function getExcludePatternsForMcp(mcp) {
243
+ if (mcp.kind === 'local' && mcp.source === 'bundled' && mcp.exclude) {
244
+ return mcp.exclude;
245
+ }
246
+ return ['node_modules'];
247
+ }
248
+ function matchesPluginExclude(filePath) {
249
+ const basename = path.basename(filePath);
250
+ return basename === '.git' || basename === '.DS_Store';
251
+ }
252
+ function matchesExcludePattern(filePath, patterns) {
253
+ const basename = path.basename(filePath);
254
+ for (const pattern of patterns) {
255
+ if (pattern.startsWith('*')) {
256
+ if (basename.endsWith(pattern.slice(1)))
257
+ return true;
258
+ }
259
+ else if (basename === pattern) {
260
+ return true;
261
+ }
262
+ }
263
+ return false;
264
+ }
@@ -1,15 +1,27 @@
1
- import { type ProfileService } from './profile-service.js';
1
+ import type { AgentName } from '../types.js';
2
+ import type { AgentConfigService } from './agent-config-service.js';
3
+ import { type PortableProfilePackService } from './portable-profile-pack-service.js';
4
+ import type { ProfileService } from './profile-service.js';
2
5
  export interface ProfileExportService {
3
6
  execute(options: {
4
7
  cwd?: string;
5
- name: string;
8
+ source: {
9
+ source: 'profile';
10
+ name: string;
11
+ } | {
12
+ source: 'agent';
13
+ agent: AgentName;
14
+ cwd: string;
15
+ };
6
16
  outputPath?: string;
7
17
  }): Promise<{
8
18
  archivePath: string;
9
19
  }>;
10
20
  }
11
21
  interface ProfileExportDependencies {
12
- profileService?: ProfileService;
22
+ portableProfilePackService?: PortableProfilePackService;
23
+ profileService?: Pick<ProfileService, 'get'>;
24
+ agentConfigService?: Pick<AgentConfigService, 'readAll'>;
13
25
  }
14
26
  export declare function createProfileExportService(deps?: ProfileExportDependencies): ProfileExportService;
15
27
  export {};
@@ -1,63 +1,16 @@
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';
1
+ import { createPortableProfilePackService } from './portable-profile-pack-service.js';
7
2
  export function createProfileExportService(deps = {}) {
8
- const profileService = deps.profileService ?? createProfileService();
3
+ const portableProfilePackService = deps.portableProfilePackService ?? createPortableProfilePackService({
4
+ profileService: deps.profileService,
5
+ agentConfigService: deps.agentConfigService,
6
+ });
9
7
  return {
10
8
  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.kind === 'local' && mcp.source === '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'),
9
+ return portableProfilePackService.execute({
10
+ cwd: options.cwd,
11
+ source: options.source,
12
+ outputPath: options.outputPath,
42
13
  });
43
- exportMcps[name] = {
44
- kind: 'local',
45
- source: 'bundled',
46
- path: `./mcps/${name}`,
47
- ...(mcp.install ? { install: mcp.install } : {}),
48
- command: mcp.command,
49
- ...(mcp.args ? { args: mcp.args } : {}),
50
- ...(mcp.env ? { env: mcp.env } : {}),
51
- };
52
- continue;
53
- }
54
- exportMcps[name] = mcp;
55
- }
56
- return {
57
- name: profile.name,
58
- ...(profile.description ? { description: profile.description } : {}),
59
- skills: profile.skills,
60
- mcps: exportMcps,
61
- memory: profile.memory,
14
+ },
62
15
  };
63
16
  }
@@ -1,11 +1,19 @@
1
+ import { type McpPreflightService } from './mcp-preflight-service.js';
1
2
  export interface ProfileImportService {
2
3
  execute(options: {
3
4
  cwd?: string;
4
5
  archivePath: string;
5
6
  force?: boolean;
7
+ credentials?: Record<string, string>;
6
8
  }): Promise<{
7
9
  profileName: string;
8
10
  installedMcps: string[];
11
+ installedPlugins: string[];
12
+ installedUserSkills: string[];
9
13
  }>;
10
14
  }
11
- export declare function createProfileImportService(): ProfileImportService;
15
+ interface ProfileImportServiceDependencies {
16
+ mcpPreflightService?: Pick<McpPreflightService, 'execute'>;
17
+ }
18
+ export declare function createProfileImportService(deps?: ProfileImportServiceDependencies): ProfileImportService;
19
+ export {};