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
@@ -1,13 +1,47 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { findExecutable } from '../system/executables.js';
4
- const SCRIPT_RUNNERS = new Set(['node', 'nodejs', 'python', 'python3', 'bash', 'sh', 'zsh', 'deno', 'bun']);
4
+ const ENTRYPOINT_RUNNERS = new Set(['node', 'nodejs', 'python', 'python3', 'bash', 'sh', 'zsh', 'deno', 'bun']);
5
+ const SUPPORTED_LOCAL_LAUNCHERS = ['npx', 'uvx', 'node', 'python', 'python3', 'java -jar', 'go run', 'cargo run', 'and local binaries/scripts'];
5
6
  export function createMcpPreflightService(dependencies = {}) {
6
7
  const resolveExecutable = dependencies.resolveExecutable ?? findExecutable;
7
8
  const pathExists = dependencies.pathExists ?? defaultPathExists;
8
9
  return {
9
10
  async execute(options) {
11
+ if (options.remoteEntry) {
12
+ return validateRemoteMcp(options.agent, options.remoteEntry);
13
+ }
10
14
  const checks = [];
15
+ if (!options.entry) {
16
+ checks.push({
17
+ label: 'Launcher',
18
+ status: 'error',
19
+ message: 'Missing local MCP command metadata.',
20
+ });
21
+ return { ok: false, checks };
22
+ }
23
+ const launcherCheck = validateLocalLauncher(options.entry);
24
+ checks.push(launcherCheck);
25
+ if (launcherCheck.status === 'error') {
26
+ return { ok: false, checks };
27
+ }
28
+ if (isLocalBinaryPath(options.entry.command)) {
29
+ const commandPath = path.isAbsolute(options.entry.command)
30
+ ? options.entry.command
31
+ : path.resolve(options.cwd, options.entry.command);
32
+ const exists = await pathExists(commandPath);
33
+ checks.push({
34
+ label: 'Command',
35
+ status: exists ? 'ok' : 'error',
36
+ message: exists
37
+ ? `Local MCP command was found: ${commandPath}`
38
+ : `Local MCP command was not found: ${commandPath}`,
39
+ });
40
+ return {
41
+ ok: checks.every((check) => check.status !== 'error'),
42
+ checks,
43
+ };
44
+ }
11
45
  const resolvedCommand = await resolveExecutable(options.entry.command);
12
46
  if (!resolvedCommand) {
13
47
  checks.push({
@@ -22,20 +56,20 @@ export function createMcpPreflightService(dependencies = {}) {
22
56
  status: 'ok',
23
57
  message: `Command "${options.entry.command}" resolved to ${resolvedCommand}.`,
24
58
  });
25
- if (options.entry.command === 'npx') {
59
+ if (options.entry.command === 'npx' || options.entry.command === 'uvx') {
26
60
  const nonFlagArg = options.entry.args?.find((arg) => !arg.startsWith('-'));
27
61
  if (!nonFlagArg) {
28
62
  checks.push({
29
63
  label: 'Package',
30
64
  status: 'error',
31
- message: 'npx-based MCP entries must include a package or executable argument.',
65
+ message: `${options.entry.command}-based MCP entries must include a package or executable argument.`,
32
66
  });
33
67
  }
34
68
  else {
35
69
  checks.push({
36
70
  label: 'Package',
37
71
  status: 'ok',
38
- message: `npx will attempt to launch ${nonFlagArg}.`,
72
+ message: `${options.entry.command} will attempt to launch ${nonFlagArg}.`,
39
73
  });
40
74
  }
41
75
  }
@@ -58,7 +92,21 @@ export function createMcpPreflightService(dependencies = {}) {
58
92
  };
59
93
  }
60
94
  function resolveEntrypointPath(cwd, entry) {
61
- if (!SCRIPT_RUNNERS.has(entry.command)) {
95
+ if (entry.command === 'java') {
96
+ const jarIndex = entry.args?.indexOf('-jar') ?? -1;
97
+ const jarPath = jarIndex >= 0 ? entry.args?.[jarIndex + 1] : undefined;
98
+ if (!jarPath || !looksLikeLocalPath(jarPath))
99
+ return null;
100
+ return path.isAbsolute(jarPath) ? jarPath : path.resolve(cwd, jarPath);
101
+ }
102
+ if (entry.command === 'go') {
103
+ const runIndex = entry.args?.indexOf('run') ?? -1;
104
+ const runPath = runIndex >= 0 ? entry.args?.[runIndex + 1] : undefined;
105
+ if (!runPath || !looksLikeLocalPath(runPath))
106
+ return null;
107
+ return path.isAbsolute(runPath) ? runPath : path.resolve(cwd, runPath);
108
+ }
109
+ if (!ENTRYPOINT_RUNNERS.has(entry.command)) {
62
110
  return null;
63
111
  }
64
112
  const firstArg = entry.args?.[0];
@@ -73,6 +121,112 @@ function looksLikeLocalPath(value) {
73
121
  value.includes(path.sep) ||
74
122
  /\.(cjs|cts|js|json|jsx|mjs|mts|py|sh|ts|tsx)$/i.test(value));
75
123
  }
124
+ function validateLocalLauncher(entry) {
125
+ if (isLocalBinaryPath(entry.command)) {
126
+ return {
127
+ label: 'Launcher',
128
+ status: 'ok',
129
+ message: `Launcher "${entry.command}" is supported for exact MCP copy.`,
130
+ };
131
+ }
132
+ if (entry.command === 'npx' || entry.command === 'uvx' || entry.command === 'node' || entry.command === 'nodejs' || entry.command === 'python' || entry.command === 'python3') {
133
+ return {
134
+ label: 'Launcher',
135
+ status: 'ok',
136
+ message: `Launcher "${entry.command}" is supported for exact MCP copy.`,
137
+ };
138
+ }
139
+ if (entry.command === 'java') {
140
+ const jarIndex = entry.args?.indexOf('-jar') ?? -1;
141
+ const jarPath = jarIndex >= 0 ? entry.args?.[jarIndex + 1] : undefined;
142
+ return jarPath
143
+ ? {
144
+ label: 'Launcher',
145
+ status: 'ok',
146
+ message: 'Launcher "java -jar" is supported for exact MCP copy.',
147
+ }
148
+ : {
149
+ label: 'Launcher',
150
+ status: 'error',
151
+ message: 'Launcher "java" is only supported for exact MCP copy when used as "java -jar <path>".',
152
+ };
153
+ }
154
+ if (entry.command === 'go') {
155
+ const runIndex = entry.args?.indexOf('run') ?? -1;
156
+ const runPath = runIndex >= 0 ? entry.args?.[runIndex + 1] : undefined;
157
+ return runPath
158
+ ? {
159
+ label: 'Launcher',
160
+ status: 'ok',
161
+ message: 'Launcher "go run" is supported for exact MCP copy.',
162
+ }
163
+ : {
164
+ label: 'Launcher',
165
+ status: 'error',
166
+ message: 'Launcher "go" is only supported for exact MCP copy when used as "go run <path>".',
167
+ };
168
+ }
169
+ if (entry.command === 'cargo') {
170
+ return entry.args?.[0] === 'run'
171
+ ? {
172
+ label: 'Launcher',
173
+ status: 'ok',
174
+ message: 'Launcher "cargo run" is supported for exact MCP copy.',
175
+ }
176
+ : {
177
+ label: 'Launcher',
178
+ status: 'error',
179
+ message: 'Launcher "cargo" is only supported for exact MCP copy when used as "cargo run".',
180
+ };
181
+ }
182
+ return {
183
+ label: 'Launcher',
184
+ status: 'error',
185
+ message: `Launcher "${entry.command}" is not supported for exact MCP copy. Supported launchers: ${SUPPORTED_LOCAL_LAUNCHERS.join(', ')}.`,
186
+ };
187
+ }
188
+ function isLocalBinaryPath(command) {
189
+ return command.startsWith('./') || command.startsWith('/') || command.startsWith('.\\');
190
+ }
191
+ function validateRemoteMcp(agent, remoteEntry) {
192
+ const checks = [];
193
+ let parsedUrl;
194
+ try {
195
+ parsedUrl = new URL(remoteEntry.url);
196
+ }
197
+ catch {
198
+ checks.push({
199
+ label: 'Remote URL',
200
+ status: 'error',
201
+ message: `Remote MCP URL is invalid: ${remoteEntry.url}`,
202
+ });
203
+ return { ok: false, checks };
204
+ }
205
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
206
+ checks.push({
207
+ label: 'Remote URL',
208
+ status: 'error',
209
+ message: `Remote MCP URL must use http or https: ${remoteEntry.url}`,
210
+ });
211
+ return { ok: false, checks };
212
+ }
213
+ checks.push({
214
+ label: 'Remote URL',
215
+ status: 'ok',
216
+ message: `Remote MCP URL is valid: ${remoteEntry.url}`,
217
+ });
218
+ if (agent === 'codex' && (remoteEntry.transport !== 'http' || remoteEntry.headers || remoteEntry.env)) {
219
+ checks.push({
220
+ label: 'Remote support',
221
+ status: 'error',
222
+ message: 'Codex remote MCP entries only support a plain HTTP url today; headers and env cannot be preserved exactly.',
223
+ });
224
+ }
225
+ return {
226
+ ok: checks.every((check) => check.status !== 'error'),
227
+ checks,
228
+ };
229
+ }
76
230
  async function defaultPathExists(targetPath) {
77
231
  try {
78
232
  await stat(targetPath);
@@ -5,25 +5,42 @@ export interface PluginInstallCheck {
5
5
  status: 'ok' | 'warn' | 'error';
6
6
  message: string;
7
7
  }
8
+ export interface PluginBundleAgent {
9
+ name: string;
10
+ sourceFormat: 'claude-md' | 'codex-toml';
11
+ content: string;
12
+ }
13
+ export interface PluginBundleCommand {
14
+ name: string;
15
+ content: string;
16
+ }
8
17
  export interface PluginInstallPlan {
9
18
  ok: boolean;
10
19
  checks: PluginInstallCheck[];
11
20
  skills: string[];
12
21
  mcps: Record<string, AgentMcpEntry>;
22
+ agents: string[];
23
+ commands: string[];
13
24
  }
14
25
  export interface PluginInstallResult {
15
26
  installedSkills: string[];
16
27
  installedMcps: string[];
28
+ installedAgents: string[];
29
+ installedCommands: string[];
17
30
  }
18
31
  export interface PluginUninstallPlan {
19
32
  ok: boolean;
20
33
  checks: PluginInstallCheck[];
21
34
  skills: string[];
22
35
  mcps: string[];
36
+ agents: string[];
37
+ commands: string[];
23
38
  }
24
39
  export interface PluginUninstallResult {
25
40
  removedSkills: string[];
26
41
  removedMcps: string[];
42
+ removedAgents: string[];
43
+ removedCommands: string[];
27
44
  }
28
45
  export interface PluginInstallService {
29
46
  plan(options: {
@@ -52,6 +69,8 @@ export interface PluginInstallService {
52
69
  interface PluginBundle {
53
70
  skills: string[];
54
71
  mcps: Record<string, AgentMcpEntry>;
72
+ agents: PluginBundleAgent[];
73
+ commands: PluginBundleCommand[];
55
74
  }
56
75
  interface PluginInstallDependencies {
57
76
  readInstalledPluginBundle?: (installPath: string) => Promise<PluginBundle>;
@@ -64,6 +83,14 @@ interface PluginInstallDependencies {
64
83
  skillName: string;
65
84
  targetAgent: AgentName;
66
85
  }) => Promise<void>;
86
+ installAgent?: (options: {
87
+ targetAgent: AgentName;
88
+ agent: PluginBundleAgent;
89
+ }) => Promise<void>;
90
+ installCommand?: (options: {
91
+ targetAgent: AgentName;
92
+ command: PluginBundleCommand;
93
+ }) => Promise<void>;
67
94
  addMcpEntry?: (options: {
68
95
  cwd: string;
69
96
  agent: AgentName;
@@ -78,6 +105,14 @@ interface PluginInstallDependencies {
78
105
  targetAgent: AgentName;
79
106
  skillName: string;
80
107
  }) => Promise<void>;
108
+ removeAgentFile?: (options: {
109
+ targetAgent: AgentName;
110
+ agentName: string;
111
+ }) => Promise<void>;
112
+ removeCommandFile?: (options: {
113
+ targetAgent: AgentName;
114
+ commandName: string;
115
+ }) => Promise<void>;
81
116
  removeMcpEntry?: (options: {
82
117
  cwd: string;
83
118
  agent: AgentName;
@@ -87,6 +122,14 @@ interface PluginInstallDependencies {
87
122
  agent: AgentName;
88
123
  pluginName: string;
89
124
  }) => Promise<void>;
125
+ uninstallCodexPlugin?: (options: {
126
+ pluginKey: string;
127
+ installPath: string;
128
+ }) => Promise<void>;
129
+ uninstallClaudePlugin?: (options: {
130
+ pluginKey: string;
131
+ installPath: string;
132
+ }) => Promise<void>;
90
133
  }
91
134
  export declare function createPluginInstallService(dependencies?: PluginInstallDependencies): PluginInstallService;
92
135
  export {};