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,12 +1,16 @@
1
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';
2
+ import { copyFile, cp, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { homedir, tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import YAML from 'yaml';
6
6
  import { ProfileError } from '../errors.js';
7
+ import { formatTimestamp } from './sync/agent-writer.js';
8
+ import { resolvePortableMcpCredentials } from './credential-resolution-service.js';
9
+ import { createMcpPreflightService } from './mcp-preflight-service.js';
7
10
  import { parseProfile } from './profile-service.js';
8
11
  const PROFILES_DIR = '.brainctl/profiles';
9
- export function createProfileImportService() {
12
+ export function createProfileImportService(deps = {}) {
13
+ const mcpPreflightService = deps.mcpPreflightService ?? createMcpPreflightService();
10
14
  return {
11
15
  async execute(options) {
12
16
  const cwd = options.cwd ?? process.cwd();
@@ -22,9 +26,13 @@ export function createProfileImportService() {
22
26
  execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
23
27
  stdio: 'pipe',
24
28
  });
29
+ const manifest = await readPortableManifest(extractDir);
25
30
  const profileSource = await readFile(path.join(extractDir, 'profile.yaml'), 'utf8');
26
31
  const profile = parseProfile(profileSource, 'imported');
27
32
  const profileName = profile.name;
33
+ if (manifest.profileName !== profileName) {
34
+ throw new ProfileError(`Portable profile manifest name "${manifest.profileName}" does not match profile name "${profileName}".`);
35
+ }
28
36
  const profilePath = path.join(cwd, PROFILES_DIR, `${profileName}.yaml`);
29
37
  if (!options.force) {
30
38
  try {
@@ -36,12 +44,27 @@ export function createProfileImportService() {
36
44
  throw err;
37
45
  }
38
46
  }
47
+ const missingCredentials = new Map();
48
+ for (const [name, mcp] of Object.entries(profile.mcps)) {
49
+ const resolution = resolvePortableMcpCredentials(mcp, {
50
+ credentials: options.credentials,
51
+ credentialSpecs: manifest.credentials,
52
+ environment: process.env,
53
+ });
54
+ profile.mcps[name] = resolution.resolved;
55
+ for (const credential of resolution.missing) {
56
+ missingCredentials.set(credential.key, credential.description ?? credential.key);
57
+ }
58
+ }
59
+ if (missingCredentials.size > 0) {
60
+ throw new ProfileError(`Missing required credentials: ${Array.from(missingCredentials.keys()).join(', ')}.`);
61
+ }
39
62
  const installedMcps = [];
40
63
  const mcpsBaseDir = path.join(cwd, PROFILES_DIR, profileName, 'mcps');
41
64
  for (const [name, mcp] of Object.entries(profile.mcps)) {
42
65
  if (!(mcp.kind === 'local' && mcp.source === 'bundled'))
43
66
  continue;
44
- const extractedMcpPath = path.join(extractDir, 'mcps', name);
67
+ const extractedMcpPath = resolveBundledArchivePath(extractDir, mcp.path);
45
68
  const destMcpPath = path.join(mcpsBaseDir, name);
46
69
  try {
47
70
  await stat(extractedMcpPath);
@@ -49,19 +72,44 @@ export function createProfileImportService() {
49
72
  catch {
50
73
  throw new ProfileError(`Bundled MCP "${name}" source not found in archive.`);
51
74
  }
75
+ await rm(destMcpPath, { recursive: true, force: true });
52
76
  await mkdir(destMcpPath, { recursive: true });
53
77
  await cp(extractedMcpPath, destMcpPath, { recursive: true });
54
- const installCmd = mcp.install ?? 'npm install';
55
- execSync(installCmd, {
56
- cwd: destMcpPath,
57
- stdio: 'pipe',
58
- });
78
+ const installCmd = mcp.install;
79
+ if (!installCmd) {
80
+ profile.mcps[name] = {
81
+ ...mcp,
82
+ path: destMcpPath,
83
+ };
84
+ installedMcps.push(name);
85
+ continue;
86
+ }
87
+ try {
88
+ execSync(installCmd, {
89
+ cwd: destMcpPath,
90
+ stdio: 'pipe',
91
+ });
92
+ }
93
+ catch (error) {
94
+ throw new ProfileError(`Bundled MCP "${name}" install failed: ${formatExecError(error)}`);
95
+ }
59
96
  profile.mcps[name] = {
60
97
  ...mcp,
61
98
  path: destMcpPath,
62
99
  };
63
100
  installedMcps.push(name);
64
101
  }
102
+ await validateImportedMcps(profile, cwd, mcpPreflightService);
103
+ const installedPlugins = [];
104
+ for (const plugin of manifest.plugins ?? []) {
105
+ await restorePlugin(extractDir, plugin);
106
+ installedPlugins.push(`${plugin.agent}:${plugin.name}`);
107
+ }
108
+ const installedUserSkills = [];
109
+ for (const skill of manifest.userSkills ?? []) {
110
+ await restoreUserSkill(extractDir, skill);
111
+ installedUserSkills.push(`${skill.agent}:${skill.name}`);
112
+ }
65
113
  const outputYaml = {
66
114
  name: profile.name,
67
115
  ...(profile.description ? { description: profile.description } : {}),
@@ -71,7 +119,7 @@ export function createProfileImportService() {
71
119
  };
72
120
  await mkdir(path.dirname(profilePath), { recursive: true });
73
121
  await writeFile(profilePath, YAML.stringify(outputYaml), 'utf8');
74
- return { profileName, installedMcps };
122
+ return { profileName, installedMcps, installedPlugins, installedUserSkills };
75
123
  }
76
124
  finally {
77
125
  await rm(extractDir, { recursive: true, force: true });
@@ -79,3 +127,210 @@ export function createProfileImportService() {
79
127
  },
80
128
  };
81
129
  }
130
+ async function validateImportedMcps(profile, cwd, mcpPreflightService) {
131
+ for (const [name, mcp] of Object.entries(profile.mcps)) {
132
+ if (mcp.kind === 'remote') {
133
+ validateRemoteMcp(name, mcp);
134
+ continue;
135
+ }
136
+ const validation = await mcpPreflightService.execute({
137
+ cwd: mcp.source === 'bundled' ? mcp.path : cwd,
138
+ agent: 'claude',
139
+ key: name,
140
+ entry: toAgentMcpEntry(mcp),
141
+ });
142
+ const firstError = validation.checks.find((check) => check.status === 'error');
143
+ if (firstError) {
144
+ throw new ProfileError(`Imported MCP "${name}" failed validation: ${firstError.message}`);
145
+ }
146
+ }
147
+ }
148
+ function toAgentMcpEntry(mcp) {
149
+ if (mcp.source === 'npm') {
150
+ return {
151
+ command: 'npx',
152
+ args: ['-y', mcp.package],
153
+ ...(mcp.env ? { env: mcp.env } : {}),
154
+ };
155
+ }
156
+ return {
157
+ command: mcp.command,
158
+ ...(mcp.args ? { args: mcp.args } : {}),
159
+ ...(mcp.env ? { env: mcp.env } : {}),
160
+ };
161
+ }
162
+ function validateRemoteMcp(name, mcp) {
163
+ let parsedUrl;
164
+ try {
165
+ parsedUrl = new URL(mcp.url);
166
+ }
167
+ catch {
168
+ throw new ProfileError(`Remote MCP "${name}" must include an absolute http(s) url.`);
169
+ }
170
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
171
+ throw new ProfileError(`Remote MCP "${name}" must include an absolute http(s) url.`);
172
+ }
173
+ }
174
+ function formatExecError(error) {
175
+ if (error && typeof error === 'object') {
176
+ const stderr = 'stderr' in error && typeof error.stderr === 'string'
177
+ ? error.stderr.trim()
178
+ : 'stderr' in error && Buffer.isBuffer(error.stderr)
179
+ ? error.stderr.toString('utf8').trim()
180
+ : '';
181
+ if (stderr.length > 0) {
182
+ return stderr;
183
+ }
184
+ if ('message' in error && typeof error.message === 'string') {
185
+ return error.message;
186
+ }
187
+ }
188
+ return 'Unknown install error.';
189
+ }
190
+ async function readPortableManifest(extractDir) {
191
+ let source;
192
+ try {
193
+ source = await readFile(path.join(extractDir, 'manifest.yaml'), 'utf8');
194
+ }
195
+ catch {
196
+ throw new ProfileError('Portable profile archive is missing manifest.yaml.');
197
+ }
198
+ let parsed;
199
+ try {
200
+ parsed = YAML.parse(source) ?? {};
201
+ }
202
+ catch {
203
+ throw new ProfileError('Portable profile manifest has invalid YAML.');
204
+ }
205
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
206
+ throw new ProfileError('Portable profile manifest has invalid structure.');
207
+ }
208
+ const manifest = parsed;
209
+ if (manifest.schemaVersion !== 1 && manifest.schemaVersion !== 2) {
210
+ throw new ProfileError(`Unsupported portable profile schema version: ${String(manifest.schemaVersion)}.`);
211
+ }
212
+ if (typeof manifest.profileName !== 'string' || manifest.profileName.trim().length === 0) {
213
+ throw new ProfileError('Portable profile manifest must include profileName.');
214
+ }
215
+ return manifest;
216
+ }
217
+ function resolveBundledArchivePath(extractDir, bundlePath) {
218
+ if (!bundlePath || path.isAbsolute(bundlePath)) {
219
+ throw new ProfileError('Bundled MCP path must be a relative archive path.');
220
+ }
221
+ const resolved = path.resolve(extractDir, bundlePath);
222
+ const relative = path.relative(extractDir, resolved);
223
+ if (relative.startsWith(`..${path.sep}`) ||
224
+ relative === '..' ||
225
+ path.isAbsolute(relative)) {
226
+ throw new ProfileError(`Bundled MCP path "${bundlePath}" escapes the archive root.`);
227
+ }
228
+ return resolved;
229
+ }
230
+ async function restorePlugin(extractDir, plugin) {
231
+ const sourceDir = resolveBundledArchivePath(extractDir, plugin.archivePath);
232
+ try {
233
+ await stat(sourceDir);
234
+ }
235
+ catch {
236
+ throw new ProfileError(`Bundled plugin "${plugin.name}" source missing in archive at ${plugin.archivePath}.`);
237
+ }
238
+ if (plugin.agent === 'gemini') {
239
+ // Gemini has no plugin cache concept. Treat as user-skill-equivalent no-op.
240
+ return;
241
+ }
242
+ const marketplace = plugin.marketplace ?? plugin.source;
243
+ const version = plugin.version ?? 'unknown';
244
+ const cacheRoot = path.join(homedir(), `.${plugin.agent}`, 'plugins', 'cache');
245
+ const targetDir = path.join(cacheRoot, marketplace, plugin.name, version);
246
+ await rm(targetDir, { recursive: true, force: true });
247
+ await mkdir(path.dirname(targetDir), { recursive: true });
248
+ await cp(sourceDir, targetDir, { recursive: true });
249
+ if (plugin.agent === 'claude') {
250
+ await registerClaudePlugin({
251
+ pluginKey: `${plugin.name}@${marketplace}`,
252
+ installPath: targetDir,
253
+ version,
254
+ });
255
+ return;
256
+ }
257
+ if (plugin.agent === 'codex') {
258
+ await registerCodexPlugin({
259
+ pluginKey: `${plugin.name}@${marketplace}`,
260
+ });
261
+ }
262
+ }
263
+ async function restoreUserSkill(extractDir, skill) {
264
+ const sourceDir = resolveBundledArchivePath(extractDir, skill.archivePath);
265
+ try {
266
+ await stat(sourceDir);
267
+ }
268
+ catch {
269
+ throw new ProfileError(`Bundled user skill "${skill.name}" source missing in archive at ${skill.archivePath}.`);
270
+ }
271
+ const targetDir = path.join(homedir(), `.${skill.agent}`, 'skills', skill.name);
272
+ await rm(targetDir, { recursive: true, force: true });
273
+ await mkdir(path.dirname(targetDir), { recursive: true });
274
+ await cp(sourceDir, targetDir, { recursive: true });
275
+ }
276
+ async function registerClaudePlugin(options) {
277
+ const filePath = path.join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
278
+ let existing = { version: 2, plugins: {} };
279
+ try {
280
+ const source = await readFile(filePath, 'utf8');
281
+ existing = JSON.parse(source);
282
+ await backupFile(filePath);
283
+ }
284
+ catch {
285
+ // fresh file
286
+ }
287
+ const plugins = (existing.plugins ?? {});
288
+ const now = new Date().toISOString();
289
+ const entry = {
290
+ scope: 'user',
291
+ installPath: options.installPath,
292
+ version: options.version,
293
+ installedAt: now,
294
+ lastUpdated: now,
295
+ };
296
+ plugins[options.pluginKey] = [entry];
297
+ existing.plugins = plugins;
298
+ if (typeof existing.version !== 'number')
299
+ existing.version = 2;
300
+ await mkdir(path.dirname(filePath), { recursive: true });
301
+ await atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
302
+ }
303
+ async function registerCodexPlugin(options) {
304
+ const filePath = path.join(homedir(), '.codex', 'config.toml');
305
+ let existing = '';
306
+ try {
307
+ existing = await readFile(filePath, 'utf8');
308
+ await backupFile(filePath);
309
+ }
310
+ catch {
311
+ existing = '';
312
+ }
313
+ const header = `[plugins."${options.pluginKey}"]`;
314
+ if (existing.includes(header))
315
+ return;
316
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
317
+ const separator = existing.length > 0 ? '\n' : '';
318
+ const block = `${header}\nenabled = true\n`;
319
+ const next = existing + prefix + separator + block;
320
+ await mkdir(path.dirname(filePath), { recursive: true });
321
+ await atomicWrite(filePath, next);
322
+ }
323
+ async function backupFile(filePath) {
324
+ const backupPath = `${filePath}.bak.${formatTimestamp()}`;
325
+ try {
326
+ await copyFile(filePath, backupPath);
327
+ }
328
+ catch {
329
+ // file may not exist
330
+ }
331
+ }
332
+ async function atomicWrite(filePath, content) {
333
+ const tmpPath = `${filePath}.tmp.${Date.now()}`;
334
+ await writeFile(tmpPath, content, 'utf8');
335
+ await rename(tmpPath, filePath);
336
+ }
@@ -2,6 +2,7 @@ import { readdir, readFile, writeFile, mkdir, stat, unlink } from 'node:fs/promi
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
4
  import { ProfileError, ProfileNotFoundError } from '../errors.js';
5
+ const VALID_RUNTIMES = new Set(['node', 'python', 'java', 'go', 'rust', 'binary']);
5
6
  const BRAINCTL_DIR = '.brainctl';
6
7
  const PROFILES_DIR = '.brainctl/profiles';
7
8
  const META_CONFIG = '.brainctl/config.yaml';
@@ -215,10 +216,12 @@ function normalizeMcps(value, profileName) {
215
216
  mcps[key] = {
216
217
  kind: 'local',
217
218
  source: 'bundled',
219
+ runtime: parseMcpRuntime(mcp.runtime),
218
220
  path: mcp.path,
219
221
  install: typeof mcp.install === 'string' ? mcp.install : undefined,
220
222
  command: mcp.command,
221
223
  args: parseStringArray(mcp.args),
224
+ ...(Array.isArray(mcp.exclude) ? { exclude: mcp.exclude.filter((v) => typeof v === 'string') } : {}),
222
225
  env: parseStringMap(mcp.env),
223
226
  };
224
227
  continue;
@@ -265,10 +268,12 @@ function normalizeMcps(value, profileName) {
265
268
  mcps[key] = {
266
269
  kind: 'local',
267
270
  source: 'bundled',
271
+ runtime: parseMcpRuntime(mcp.runtime),
268
272
  path: mcp.path,
269
273
  install: typeof mcp.install === 'string' ? mcp.install : undefined,
270
274
  command: mcp.command,
271
275
  args: parseStringArray(mcp.args),
276
+ ...(Array.isArray(mcp.exclude) ? { exclude: mcp.exclude.filter((v) => typeof v === 'string') } : {}),
272
277
  env: parseStringMap(mcp.env),
273
278
  };
274
279
  }
@@ -284,6 +289,12 @@ function parseStringMap(value) {
284
289
  }
285
290
  return Object.keys(result).length > 0 ? result : undefined;
286
291
  }
292
+ function parseMcpRuntime(value) {
293
+ if (typeof value === 'string' && VALID_RUNTIMES.has(value)) {
294
+ return value;
295
+ }
296
+ return 'node';
297
+ }
287
298
  function parseStringArray(value) {
288
299
  if (!Array.isArray(value)) {
289
300
  return undefined;
@@ -0,0 +1,9 @@
1
+ import type { McpRuntime } from '../types.js';
2
+ export declare function detectMcpRuntime(command: string): McpRuntime | null;
3
+ export declare function extractEntrypoint(command: string, args: string[]): string | null;
4
+ export declare function findProjectRoot(entrypointPath: string, runtime: McpRuntime): {
5
+ root: string;
6
+ marker: string | null;
7
+ };
8
+ export declare function getDefaultInstall(runtime: McpRuntime, marker: string | null, projectRoot: string, entrypoint?: string): string | undefined;
9
+ export declare function getDefaultExclude(runtime: McpRuntime, marker?: string | null): string[] | undefined;
@@ -0,0 +1,130 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ const COMMAND_RUNTIME_MAP = {
4
+ node: 'node',
5
+ nodejs: 'node',
6
+ python: 'python',
7
+ python3: 'python',
8
+ java: 'java',
9
+ go: 'go',
10
+ cargo: 'rust',
11
+ };
12
+ const RUNTIME_MARKERS = {
13
+ node: ['package.json'],
14
+ python: ['pyproject.toml', 'requirements.txt', 'setup.py'],
15
+ java: ['pom.xml', 'build.gradle'],
16
+ go: ['go.mod'],
17
+ rust: ['Cargo.toml'],
18
+ binary: [],
19
+ };
20
+ const MAX_WALK_DEPTH = 5;
21
+ export function detectMcpRuntime(command) {
22
+ const basename = path.basename(command);
23
+ const mapped = COMMAND_RUNTIME_MAP[basename];
24
+ if (mapped) {
25
+ return mapped;
26
+ }
27
+ if (command.startsWith('./') || command.startsWith('/') || command.startsWith('.\\')) {
28
+ return 'binary';
29
+ }
30
+ return null;
31
+ }
32
+ export function extractEntrypoint(command, args) {
33
+ const runtime = detectMcpRuntime(command);
34
+ if (runtime === 'binary') {
35
+ return command;
36
+ }
37
+ if (runtime === 'java') {
38
+ const jarIndex = args.indexOf('-jar');
39
+ if (jarIndex !== -1 && jarIndex + 1 < args.length) {
40
+ return args[jarIndex + 1];
41
+ }
42
+ return null;
43
+ }
44
+ if (runtime === 'go') {
45
+ const runIndex = args.indexOf('run');
46
+ if (runIndex !== -1 && runIndex + 1 < args.length) {
47
+ return args[runIndex + 1];
48
+ }
49
+ return null;
50
+ }
51
+ if (runtime === 'rust') {
52
+ return null;
53
+ }
54
+ // node, python: first non-flag arg
55
+ for (const arg of args) {
56
+ if (!arg.startsWith('-')) {
57
+ return arg;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ export function findProjectRoot(entrypointPath, runtime) {
63
+ const markers = RUNTIME_MARKERS[runtime];
64
+ if (markers.length === 0) {
65
+ return { root: path.dirname(entrypointPath), marker: null };
66
+ }
67
+ let current = path.dirname(entrypointPath);
68
+ for (let depth = 0; depth <= MAX_WALK_DEPTH; depth++) {
69
+ for (const marker of markers) {
70
+ if (existsSync(path.join(current, marker))) {
71
+ return { root: current, marker };
72
+ }
73
+ }
74
+ const parent = path.dirname(current);
75
+ if (parent === current) {
76
+ break;
77
+ }
78
+ current = parent;
79
+ }
80
+ return { root: path.dirname(entrypointPath), marker: null };
81
+ }
82
+ export function getDefaultInstall(runtime, marker, projectRoot, entrypoint) {
83
+ switch (runtime) {
84
+ case 'node':
85
+ return 'npm install';
86
+ case 'python':
87
+ if (marker === 'requirements.txt') {
88
+ return 'pip install -r requirements.txt';
89
+ }
90
+ if (marker === 'pyproject.toml') {
91
+ return existsSync(path.join(projectRoot, 'uv.lock')) ? 'uv sync' : 'pip install -e .';
92
+ }
93
+ return undefined;
94
+ case 'java':
95
+ if (entrypoint && entrypoint.endsWith('.jar')) {
96
+ return undefined;
97
+ }
98
+ if (marker === 'pom.xml')
99
+ return 'mvn package -q';
100
+ if (marker === 'build.gradle')
101
+ return 'gradle build';
102
+ return undefined;
103
+ case 'go':
104
+ return 'go build ./...';
105
+ case 'rust':
106
+ return 'cargo build --release';
107
+ case 'binary':
108
+ return undefined;
109
+ }
110
+ }
111
+ export function getDefaultExclude(runtime, marker) {
112
+ switch (runtime) {
113
+ case 'node':
114
+ return ['node_modules'];
115
+ case 'python':
116
+ return ['.venv', '__pycache__', '*.pyc'];
117
+ case 'rust':
118
+ return ['target'];
119
+ case 'java':
120
+ if (marker === 'build.gradle')
121
+ return ['build'];
122
+ if (marker === 'pom.xml')
123
+ return ['target'];
124
+ return undefined;
125
+ case 'go':
126
+ return undefined;
127
+ case 'binary':
128
+ return undefined;
129
+ }
130
+ }
@@ -1,2 +1,4 @@
1
1
  import type { AgentName } from '../types.js';
2
2
  export declare function getSkillDir(agent: AgentName, skillName: string): string;
3
+ export declare function getAgentFilePath(agent: AgentName, agentName: string): string;
4
+ export declare function getCommandFilePath(agent: AgentName, commandName: string): string;
@@ -10,3 +10,17 @@ export function getSkillDir(agent, skillName) {
10
10
  return path.join(homedir(), '.gemini', 'skills', safeName);
11
11
  throw new Error(`Skill management is not supported for ${agent}`);
12
12
  }
13
+ export function getAgentFilePath(agent, agentName) {
14
+ const safeName = path.basename(agentName);
15
+ if (agent === 'claude')
16
+ return path.join(homedir(), '.claude', 'agents', `${safeName}.md`);
17
+ if (agent === 'codex')
18
+ return path.join(homedir(), '.codex', 'agents', `${safeName}.toml`);
19
+ throw new Error(`Subagent management is not supported for ${agent}`);
20
+ }
21
+ export function getCommandFilePath(agent, commandName) {
22
+ const safeName = path.basename(commandName);
23
+ if (agent === 'claude')
24
+ return path.join(homedir(), '.claude', 'commands', `${safeName}.md`);
25
+ throw new Error(`Slash-command management is not supported for ${agent}`);
26
+ }
@@ -4,12 +4,20 @@ export interface AgentMcpEntry {
4
4
  args?: string[];
5
5
  env?: Record<string, string>;
6
6
  }
7
+ export interface PortableRemoteMcpMetadata {
8
+ transport: 'http' | 'sse';
9
+ url: string;
10
+ headers?: Record<string, string>;
11
+ env?: Record<string, string>;
12
+ }
7
13
  export interface AgentSkillEntry {
8
14
  name: string;
9
15
  source?: string;
10
16
  kind?: 'skill' | 'plugin';
11
17
  pluginSkills?: string[];
12
18
  pluginMcps?: string[];
19
+ pluginAgents?: string[];
20
+ pluginCommands?: string[];
13
21
  installPath?: string;
14
22
  managed?: boolean;
15
23
  }
@@ -18,6 +26,7 @@ export interface AgentLiveConfig {
18
26
  configPath: string;
19
27
  exists: boolean;
20
28
  mcpServers: Record<string, AgentMcpEntry>;
29
+ remoteMcpServers: Record<string, PortableRemoteMcpMetadata>;
21
30
  skills: AgentSkillEntry[];
22
31
  }
23
32
  export interface AgentConfigReader {