brainctl 0.1.5 → 0.1.7
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/README.md +181 -131
- package/dist/executor/resolver.js +1 -38
- package/dist/mcp/server.js +183 -0
- package/dist/services/agent-config-service.d.ts +35 -0
- package/dist/services/agent-config-service.js +222 -0
- package/dist/services/mcp-preflight-service.d.ts +25 -0
- package/dist/services/mcp-preflight-service.js +84 -0
- package/dist/services/plugin-install-service.d.ts +92 -0
- package/dist/services/plugin-install-service.js +243 -0
- package/dist/services/profile-export-service.js +5 -5
- package/dist/services/profile-import-service.js +1 -1
- package/dist/services/profile-service.d.ts +10 -0
- package/dist/services/profile-service.js +140 -28
- package/dist/services/skill-paths.d.ts +2 -0
- package/dist/services/skill-paths.js +12 -0
- package/dist/services/skill-preflight-service.d.ts +23 -0
- package/dist/services/skill-preflight-service.js +40 -0
- package/dist/services/sync/agent-reader.d.ts +30 -0
- package/dist/services/sync/agent-reader.js +232 -0
- package/dist/services/sync/claude-writer.js +4 -1
- package/dist/services/sync/codex-writer.js +6 -2
- package/dist/services/sync/gemini-writer.js +4 -1
- package/dist/services/sync/managed-plugin-registry.d.ts +17 -0
- package/dist/services/sync/managed-plugin-registry.js +75 -0
- package/dist/services/sync/plugin-skill-reader.d.ts +2 -0
- package/dist/services/sync/plugin-skill-reader.js +33 -0
- package/dist/services/sync-service.js +5 -0
- package/dist/system/executables.d.ts +1 -0
- package/dist/system/executables.js +38 -0
- package/dist/types.d.ts +15 -5
- package/dist/ui/routes.js +423 -1
- package/dist/web/assets/index-BCkorugl.css +1 -0
- package/dist/web/assets/index-sGnTMhkX.js +16 -0
- package/dist/web/index.html +2 -2
- package/package.json +7 -1
- package/dist/web/assets/index-CRJ6cM0Q.css +0 -1
- package/dist/web/assets/index-Cr8gt3VF.js +0 -9
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { mergeManagedPluginsIntoSkills, readManagedPlugins, } from './managed-plugin-registry.js';
|
|
5
|
+
import { readInstalledPlugins } from './plugin-skill-reader.js';
|
|
6
|
+
export function createClaudeReader() {
|
|
7
|
+
return {
|
|
8
|
+
async read(options) {
|
|
9
|
+
const configPath = path.join(homedir(), '.claude.json');
|
|
10
|
+
try {
|
|
11
|
+
const source = await readFile(configPath, 'utf8');
|
|
12
|
+
const data = JSON.parse(source);
|
|
13
|
+
const projects = (data.projects ?? {});
|
|
14
|
+
const projectConfig = projects[options.cwd] ?? {};
|
|
15
|
+
const rawServers = (projectConfig.mcpServers ?? {});
|
|
16
|
+
const mcpServers = {};
|
|
17
|
+
for (const [name, entry] of Object.entries(rawServers)) {
|
|
18
|
+
mcpServers[name] = {
|
|
19
|
+
command: String(entry.command ?? ''),
|
|
20
|
+
args: Array.isArray(entry.args) ? entry.args.map(String) : undefined,
|
|
21
|
+
env: parseEnvObject(entry.env),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const skills = await readClaudePlugins();
|
|
25
|
+
return { agent: 'claude', configPath, exists: true, mcpServers, skills };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
const skills = await readClaudePlugins();
|
|
29
|
+
return { agent: 'claude', configPath, exists: false, mcpServers: {}, skills };
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function createCodexReader() {
|
|
35
|
+
return {
|
|
36
|
+
async read() {
|
|
37
|
+
const configPath = path.join(homedir(), '.codex', 'config.toml');
|
|
38
|
+
try {
|
|
39
|
+
const source = await readFile(configPath, 'utf8');
|
|
40
|
+
const mcpServers = parseCodexToml(source);
|
|
41
|
+
const skills = await readCodexSkills();
|
|
42
|
+
return { agent: 'codex', configPath, exists: true, mcpServers, skills };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
const skills = await readCodexSkills();
|
|
46
|
+
return { agent: 'codex', configPath, exists: false, mcpServers: {}, skills };
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createGeminiReader() {
|
|
52
|
+
return {
|
|
53
|
+
async read() {
|
|
54
|
+
const configPath = path.join(homedir(), '.gemini', 'settings.json');
|
|
55
|
+
try {
|
|
56
|
+
const source = await readFile(configPath, 'utf8');
|
|
57
|
+
const data = JSON.parse(source);
|
|
58
|
+
const rawServers = (data.mcpServers ?? {});
|
|
59
|
+
const mcpServers = {};
|
|
60
|
+
for (const [name, entry] of Object.entries(rawServers)) {
|
|
61
|
+
mcpServers[name] = {
|
|
62
|
+
command: String(entry.command ?? ''),
|
|
63
|
+
args: Array.isArray(entry.args) ? entry.args.map(String) : undefined,
|
|
64
|
+
env: parseEnvObject(entry.env),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const skills = await readGeminiSkills();
|
|
68
|
+
return { agent: 'gemini', configPath, exists: true, mcpServers, skills };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
const skills = await readGeminiSkills();
|
|
72
|
+
return { agent: 'gemini', configPath, exists: false, mcpServers: {}, skills };
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function parseEnvObject(value) {
|
|
78
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
79
|
+
return undefined;
|
|
80
|
+
const result = {};
|
|
81
|
+
for (const [k, v] of Object.entries(value)) {
|
|
82
|
+
result[k] = String(v);
|
|
83
|
+
}
|
|
84
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
85
|
+
}
|
|
86
|
+
function parseCodexToml(source) {
|
|
87
|
+
const servers = {};
|
|
88
|
+
const lines = source.split('\n');
|
|
89
|
+
let currentServer = null;
|
|
90
|
+
let inEnv = false;
|
|
91
|
+
let currentEntry = { command: '' };
|
|
92
|
+
let currentEnv = {};
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const trimmed = line.trim();
|
|
95
|
+
// Match [mcp_servers.name.env]
|
|
96
|
+
const envMatch = trimmed.match(/^\[mcp_servers\.([^.]+)\.env\]$/);
|
|
97
|
+
if (envMatch) {
|
|
98
|
+
inEnv = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Match [mcp_servers.name]
|
|
102
|
+
const serverMatch = trimmed.match(/^\[mcp_servers\.([^\].]+)\]$/);
|
|
103
|
+
if (serverMatch) {
|
|
104
|
+
// Save previous server
|
|
105
|
+
if (currentServer) {
|
|
106
|
+
if (Object.keys(currentEnv).length > 0)
|
|
107
|
+
currentEntry.env = currentEnv;
|
|
108
|
+
servers[currentServer] = currentEntry;
|
|
109
|
+
}
|
|
110
|
+
currentServer = serverMatch[1];
|
|
111
|
+
currentEntry = { command: '' };
|
|
112
|
+
currentEnv = {};
|
|
113
|
+
inEnv = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// New non-mcp section — flush current server
|
|
117
|
+
if (/^\[/.test(trimmed) && !/^\[mcp_servers/.test(trimmed)) {
|
|
118
|
+
if (currentServer) {
|
|
119
|
+
if (Object.keys(currentEnv).length > 0)
|
|
120
|
+
currentEntry.env = currentEnv;
|
|
121
|
+
servers[currentServer] = currentEntry;
|
|
122
|
+
currentServer = null;
|
|
123
|
+
currentEntry = { command: '' };
|
|
124
|
+
currentEnv = {};
|
|
125
|
+
}
|
|
126
|
+
inEnv = false;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Key-value pair
|
|
130
|
+
const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
131
|
+
if (!kvMatch || !currentServer)
|
|
132
|
+
continue;
|
|
133
|
+
const [, key, rawValue] = kvMatch;
|
|
134
|
+
if (inEnv) {
|
|
135
|
+
currentEnv[key] = parseTomlValue(rawValue);
|
|
136
|
+
}
|
|
137
|
+
else if (key === 'command') {
|
|
138
|
+
currentEntry.command = parseTomlValue(rawValue);
|
|
139
|
+
}
|
|
140
|
+
else if (key === 'args') {
|
|
141
|
+
currentEntry.args = parseTomlArray(rawValue);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Flush last server
|
|
145
|
+
if (currentServer) {
|
|
146
|
+
if (Object.keys(currentEnv).length > 0)
|
|
147
|
+
currentEntry.env = currentEnv;
|
|
148
|
+
servers[currentServer] = currentEntry;
|
|
149
|
+
}
|
|
150
|
+
return servers;
|
|
151
|
+
}
|
|
152
|
+
function parseTomlValue(raw) {
|
|
153
|
+
const trimmed = raw.trim();
|
|
154
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
155
|
+
return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
156
|
+
}
|
|
157
|
+
return trimmed;
|
|
158
|
+
}
|
|
159
|
+
function parseTomlArray(raw) {
|
|
160
|
+
const trimmed = raw.trim();
|
|
161
|
+
if (!trimmed.startsWith('[') || !trimmed.endsWith(']'))
|
|
162
|
+
return [];
|
|
163
|
+
const inner = trimmed.slice(1, -1);
|
|
164
|
+
const result = [];
|
|
165
|
+
const parts = inner.split(',');
|
|
166
|
+
for (const part of parts) {
|
|
167
|
+
const val = parseTomlValue(part.trim());
|
|
168
|
+
if (val.length > 0)
|
|
169
|
+
result.push(val);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
/* ---- Skill readers ---- */
|
|
174
|
+
async function readClaudePlugins() {
|
|
175
|
+
const results = [];
|
|
176
|
+
// Read marketplace plugins
|
|
177
|
+
try {
|
|
178
|
+
const pluginsPath = path.join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
179
|
+
results.push(...await readInstalledPlugins(pluginsPath));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// no plugins file
|
|
183
|
+
}
|
|
184
|
+
// Read local skills from ~/.claude/skills/
|
|
185
|
+
try {
|
|
186
|
+
const skillsDir = path.join(homedir(), '.claude', 'skills');
|
|
187
|
+
const localSkills = await readSkillDirs(skillsDir);
|
|
188
|
+
results.push(...localSkills);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// no skills dir
|
|
192
|
+
}
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
async function readCodexSkills() {
|
|
196
|
+
try {
|
|
197
|
+
const skillsDir = path.join(homedir(), '.codex', 'skills');
|
|
198
|
+
const localSkills = await readSkillDirs(skillsDir);
|
|
199
|
+
const managedPlugins = await readManagedPlugins({ agent: 'codex' });
|
|
200
|
+
return mergeManagedPluginsIntoSkills(localSkills, managedPlugins);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return await readManagedPlugins({ agent: 'codex' });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function readGeminiSkills() {
|
|
207
|
+
try {
|
|
208
|
+
const skillsDir = path.join(homedir(), '.gemini', 'skills');
|
|
209
|
+
const localSkills = await readSkillDirs(skillsDir);
|
|
210
|
+
const managedPlugins = await readManagedPlugins({ agent: 'gemini' });
|
|
211
|
+
return mergeManagedPluginsIntoSkills(localSkills, managedPlugins);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return await readManagedPlugins({ agent: 'gemini' });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Shared: read skill directories (Codex and Gemini use the same SKILL.md convention) */
|
|
218
|
+
async function readSkillDirs(skillsDir) {
|
|
219
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
220
|
+
const skills = [];
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
if (entry.name.startsWith('.'))
|
|
223
|
+
continue;
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
skills.push({ name: entry.name, source: 'local', kind: 'skill' });
|
|
226
|
+
}
|
|
227
|
+
else if (entry.isSymbolicLink()) {
|
|
228
|
+
skills.push({ name: entry.name, source: 'linked', kind: 'skill' });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return skills;
|
|
232
|
+
}
|
|
@@ -65,7 +65,10 @@ export function createClaudeWriter() {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
function toClaudeFormat(config) {
|
|
68
|
-
if (config.
|
|
68
|
+
if (config.kind === 'remote') {
|
|
69
|
+
throw new SyncError('Remote MCP servers are not supported in Claude sync.');
|
|
70
|
+
}
|
|
71
|
+
if (config.source === 'npm') {
|
|
69
72
|
return {
|
|
70
73
|
type: 'stdio',
|
|
71
74
|
command: 'npx',
|
|
@@ -27,7 +27,8 @@ export function createCodexWriter() {
|
|
|
27
27
|
const allServers = { ...options.mcpServers };
|
|
28
28
|
// Always include brainctl itself
|
|
29
29
|
allServers['brainctl'] = {
|
|
30
|
-
|
|
30
|
+
kind: 'local',
|
|
31
|
+
source: 'npm',
|
|
31
32
|
package: 'brainctl',
|
|
32
33
|
};
|
|
33
34
|
const mcpToml = buildMcpToml(allServers);
|
|
@@ -71,7 +72,10 @@ function buildMcpToml(servers) {
|
|
|
71
72
|
const lines = [];
|
|
72
73
|
for (const [name, config] of Object.entries(servers)) {
|
|
73
74
|
lines.push(`[mcp_servers.${name}]`);
|
|
74
|
-
if (config.
|
|
75
|
+
if (config.kind === 'remote') {
|
|
76
|
+
throw new SyncError('Remote MCP servers are not supported in Codex sync.');
|
|
77
|
+
}
|
|
78
|
+
if (config.source === 'npm') {
|
|
75
79
|
lines.push(`command = "npx"`);
|
|
76
80
|
lines.push(`args = ["-y", ${tomlString(config.package)}]`);
|
|
77
81
|
}
|
|
@@ -67,7 +67,10 @@ export function createGeminiWriter() {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
function toGeminiFormat(config) {
|
|
70
|
-
if (config.
|
|
70
|
+
if (config.kind === 'remote') {
|
|
71
|
+
throw new SyncError('Remote MCP servers are not supported in Gemini sync.');
|
|
72
|
+
}
|
|
73
|
+
if (config.source === 'npm') {
|
|
71
74
|
return {
|
|
72
75
|
command: 'npx',
|
|
73
76
|
args: ['-y', config.package],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentName } from '../../types.js';
|
|
2
|
+
import type { AgentSkillEntry } from './agent-reader.js';
|
|
3
|
+
export declare function readManagedPlugins(options: {
|
|
4
|
+
homeDir?: string;
|
|
5
|
+
agent: AgentName;
|
|
6
|
+
}): Promise<AgentSkillEntry[]>;
|
|
7
|
+
export declare function writeManagedPluginInstall(options: {
|
|
8
|
+
homeDir?: string;
|
|
9
|
+
agent: AgentName;
|
|
10
|
+
plugin: AgentSkillEntry;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
export declare function removeManagedPluginInstall(options: {
|
|
13
|
+
homeDir?: string;
|
|
14
|
+
agent: AgentName;
|
|
15
|
+
pluginName: string;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
export declare function mergeManagedPluginsIntoSkills(localSkills: AgentSkillEntry[], managedPlugins: AgentSkillEntry[]): AgentSkillEntry[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function getRegistryPath(homeDir) {
|
|
5
|
+
return path.join(homeDir, '.brainctl', 'managed-plugins.json');
|
|
6
|
+
}
|
|
7
|
+
export async function readManagedPlugins(options) {
|
|
8
|
+
const homeDir = options.homeDir ?? homedir();
|
|
9
|
+
const registryPath = getRegistryPath(homeDir);
|
|
10
|
+
try {
|
|
11
|
+
const source = await readFile(registryPath, 'utf8');
|
|
12
|
+
const parsed = JSON.parse(source);
|
|
13
|
+
return (parsed.agents?.[options.agent] ?? []).map((entry) => ({
|
|
14
|
+
...entry,
|
|
15
|
+
kind: 'plugin',
|
|
16
|
+
managed: true,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function writeManagedPluginInstall(options) {
|
|
24
|
+
const homeDir = options.homeDir ?? homedir();
|
|
25
|
+
const registryPath = getRegistryPath(homeDir);
|
|
26
|
+
const existing = await readRegistryFile(homeDir);
|
|
27
|
+
const currentEntries = existing.agents?.[options.agent] ?? [];
|
|
28
|
+
const nextEntries = [
|
|
29
|
+
...currentEntries.filter((entry) => entry.name !== options.plugin.name),
|
|
30
|
+
{
|
|
31
|
+
...options.plugin,
|
|
32
|
+
kind: 'plugin',
|
|
33
|
+
managed: true,
|
|
34
|
+
},
|
|
35
|
+
].sort((left, right) => left.name.localeCompare(right.name));
|
|
36
|
+
const next = {
|
|
37
|
+
version: 1,
|
|
38
|
+
agents: {
|
|
39
|
+
...existing.agents,
|
|
40
|
+
[options.agent]: nextEntries,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
await mkdir(path.dirname(registryPath), { recursive: true });
|
|
44
|
+
await writeFile(registryPath, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
45
|
+
}
|
|
46
|
+
export async function removeManagedPluginInstall(options) {
|
|
47
|
+
const homeDir = options.homeDir ?? homedir();
|
|
48
|
+
const registryPath = getRegistryPath(homeDir);
|
|
49
|
+
const existing = await readRegistryFile(homeDir);
|
|
50
|
+
const currentEntries = existing.agents?.[options.agent] ?? [];
|
|
51
|
+
const nextEntries = currentEntries.filter((entry) => entry.name !== options.pluginName);
|
|
52
|
+
const next = {
|
|
53
|
+
version: 1,
|
|
54
|
+
agents: {
|
|
55
|
+
...existing.agents,
|
|
56
|
+
[options.agent]: nextEntries,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
await mkdir(path.dirname(registryPath), { recursive: true });
|
|
60
|
+
await writeFile(registryPath, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
61
|
+
}
|
|
62
|
+
export function mergeManagedPluginsIntoSkills(localSkills, managedPlugins) {
|
|
63
|
+
const pluginOwnedSkills = new Set(managedPlugins.flatMap((plugin) => plugin.pluginSkills ?? []));
|
|
64
|
+
const filteredLocalSkills = localSkills.filter((skill) => !pluginOwnedSkills.has(skill.name));
|
|
65
|
+
return [...managedPlugins, ...filteredLocalSkills];
|
|
66
|
+
}
|
|
67
|
+
async function readRegistryFile(homeDir) {
|
|
68
|
+
try {
|
|
69
|
+
const source = await readFile(getRegistryPath(homeDir), 'utf8');
|
|
70
|
+
return JSON.parse(source);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { version: 1, agents: {} };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function readInstalledPlugins(installedPluginsPath) {
|
|
4
|
+
const source = await readFile(installedPluginsPath, 'utf8');
|
|
5
|
+
const data = JSON.parse(source);
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const [key, records] of Object.entries(data.plugins ?? {})) {
|
|
8
|
+
const [name, pluginSource] = key.split('@');
|
|
9
|
+
const installPath = records[0]?.installPath;
|
|
10
|
+
const pluginSkills = installPath ? await readPluginSkills(installPath) : [];
|
|
11
|
+
results.push({
|
|
12
|
+
name,
|
|
13
|
+
source: pluginSource,
|
|
14
|
+
kind: 'plugin',
|
|
15
|
+
installPath,
|
|
16
|
+
pluginSkills,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return results;
|
|
20
|
+
}
|
|
21
|
+
async function readPluginSkills(installPath) {
|
|
22
|
+
const skillsDir = path.join(installPath, 'skills');
|
|
23
|
+
try {
|
|
24
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
25
|
+
return entries
|
|
26
|
+
.filter((entry) => !entry.name.startsWith('.') && entry.isDirectory())
|
|
27
|
+
.map((entry) => entry.name)
|
|
28
|
+
.sort((left, right) => left.localeCompare(right));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProfileError } from '../errors.js';
|
|
1
2
|
import { createClaudeWriter } from './sync/claude-writer.js';
|
|
2
3
|
import { createCodexWriter } from './sync/codex-writer.js';
|
|
3
4
|
import { createGeminiWriter } from './sync/gemini-writer.js';
|
|
@@ -21,6 +22,10 @@ export function createSyncService(dependencies = {}) {
|
|
|
21
22
|
throw new Error('No active profile set. Run "brainctl profile use <name>" first.');
|
|
22
23
|
}
|
|
23
24
|
const profile = await profileService.get({ cwd, name: meta.active_profile });
|
|
25
|
+
const remoteMcpName = Object.entries(profile.mcps).find(([, config]) => config.kind === 'remote')?.[0];
|
|
26
|
+
if (remoteMcpName) {
|
|
27
|
+
throw new ProfileError(`Profile "${profile.name}" includes remote MCP "${remoteMcpName}". Remote MCP sync is not supported yet.`);
|
|
28
|
+
}
|
|
24
29
|
const results = [];
|
|
25
30
|
for (const agent of meta.agents) {
|
|
26
31
|
const writer = writers[agent];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findExecutable(command: string): Promise<string | null>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export async function findExecutable(command) {
|
|
5
|
+
if (command.includes(path.sep)) {
|
|
6
|
+
return (await isExecutable(command)) ? command : null;
|
|
7
|
+
}
|
|
8
|
+
const pathEntries = (process.env.PATH ?? '')
|
|
9
|
+
.split(path.delimiter)
|
|
10
|
+
.filter((entry) => entry.length > 0);
|
|
11
|
+
const extensions = process.platform === 'win32'
|
|
12
|
+
? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM')
|
|
13
|
+
.split(';')
|
|
14
|
+
.filter((entry) => entry.length > 0)
|
|
15
|
+
: [''];
|
|
16
|
+
for (const pathEntry of pathEntries) {
|
|
17
|
+
for (const extension of extensions) {
|
|
18
|
+
const candidate = process.platform === 'win32' &&
|
|
19
|
+
extension.length > 0 &&
|
|
20
|
+
!command.toLowerCase().endsWith(extension.toLowerCase())
|
|
21
|
+
? path.join(pathEntry, `${command}${extension}`)
|
|
22
|
+
: path.join(pathEntry, command);
|
|
23
|
+
if (await isExecutable(candidate)) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
async function isExecutable(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
await access(filePath, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -55,20 +55,30 @@ export interface DiagnosticCheck {
|
|
|
55
55
|
status: DiagnosticStatus;
|
|
56
56
|
message: string;
|
|
57
57
|
}
|
|
58
|
-
export interface
|
|
59
|
-
|
|
58
|
+
export interface LocalNpmMcpServerConfig {
|
|
59
|
+
kind: 'local';
|
|
60
|
+
source: 'npm';
|
|
60
61
|
package: string;
|
|
61
62
|
env?: Record<string, string>;
|
|
62
63
|
}
|
|
63
|
-
export interface
|
|
64
|
-
|
|
64
|
+
export interface LocalBundledMcpServerConfig {
|
|
65
|
+
kind: 'local';
|
|
66
|
+
source: 'bundled';
|
|
65
67
|
path: string;
|
|
66
68
|
install?: string;
|
|
67
69
|
command: string;
|
|
68
70
|
args?: string[];
|
|
69
71
|
env?: Record<string, string>;
|
|
70
72
|
}
|
|
71
|
-
export
|
|
73
|
+
export interface RemoteMcpServerConfig {
|
|
74
|
+
kind: 'remote';
|
|
75
|
+
transport: 'http' | 'sse';
|
|
76
|
+
url: string;
|
|
77
|
+
headers?: Record<string, string>;
|
|
78
|
+
env?: Record<string, string>;
|
|
79
|
+
}
|
|
80
|
+
export type LocalMcpServerConfig = LocalNpmMcpServerConfig | LocalBundledMcpServerConfig;
|
|
81
|
+
export type McpServerConfig = LocalMcpServerConfig | RemoteMcpServerConfig;
|
|
72
82
|
export interface ProfileConfig {
|
|
73
83
|
name: string;
|
|
74
84
|
description?: string;
|