brainctl 0.1.17 → 0.1.19

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 (111) hide show
  1. package/dist/cli.d.ts +9 -11
  2. package/dist/cli.js +17 -22
  3. package/dist/commands/doctor.d.ts +1 -1
  4. package/dist/commands/mcp.js +2 -2
  5. package/dist/commands/profile.d.ts +7 -3
  6. package/dist/commands/profile.js +106 -16
  7. package/dist/commands/status.d.ts +1 -1
  8. package/dist/commands/status.js +7 -7
  9. package/dist/{mcp/server.d.ts → mcp-server.d.ts} +1 -1
  10. package/dist/{mcp/server.js → mcp-server.js} +56 -149
  11. package/dist/services/agent/agent-asset-installer.d.ts +3 -0
  12. package/dist/services/agent/agent-asset-installer.js +109 -0
  13. package/dist/services/agent/agent-availability-service.d.ts +11 -0
  14. package/dist/services/agent/agent-availability-service.js +32 -0
  15. package/dist/services/{agent-config-service.d.ts → agent/agent-config-service.d.ts} +6 -6
  16. package/dist/services/{agent-config-service.js → agent/agent-config-service.js} +6 -6
  17. package/dist/services/{credential-redaction-service.d.ts → credential/credential-redaction-service.d.ts} +2 -1
  18. package/dist/services/{credential-redaction-service.js → credential/credential-redaction-service.js} +9 -3
  19. package/dist/services/{credential-resolution-service.d.ts → credential/credential-resolution-service.d.ts} +1 -1
  20. package/dist/services/{doctor-service.d.ts → platform/doctor-service.d.ts} +3 -3
  21. package/dist/services/platform/doctor-service.js +23 -0
  22. package/dist/services/{mcp-preflight-service.d.ts → platform/mcp-preflight-service.d.ts} +2 -2
  23. package/dist/services/{mcp-preflight-service.js → platform/mcp-preflight-service.js} +1 -1
  24. package/dist/services/{runtime-detector.d.ts → platform/runtime-detector.d.ts} +1 -1
  25. package/dist/services/platform/status-service.d.ts +19 -0
  26. package/dist/services/platform/status-service.js +22 -0
  27. package/dist/services/{update-check-service.js → platform/update-check-service.js} +1 -1
  28. package/dist/services/plugin/plugin-install-bundle.d.ts +20 -0
  29. package/dist/services/plugin/plugin-install-bundle.js +80 -0
  30. package/dist/services/plugin/plugin-install-compatibility.d.ts +15 -0
  31. package/dist/services/plugin/plugin-install-compatibility.js +91 -0
  32. package/dist/services/plugin/plugin-install-fs.d.ts +27 -0
  33. package/dist/services/plugin/plugin-install-fs.js +65 -0
  34. package/dist/services/{plugin-install-service.d.ts → plugin/plugin-install-service.d.ts} +4 -18
  35. package/dist/services/{plugin-install-service.js → plugin/plugin-install-service.js} +7 -308
  36. package/dist/services/plugin/plugin-install-uninstall.d.ts +12 -0
  37. package/dist/services/plugin/plugin-install-uninstall.js +76 -0
  38. package/dist/services/{skill-paths.d.ts → plugin/skill-paths.d.ts} +1 -1
  39. package/dist/services/{skill-preflight-service.d.ts → plugin/skill-preflight-service.d.ts} +1 -1
  40. package/dist/services/{portable-mcp-classifier.d.ts → profile/portable-mcp-classifier.d.ts} +3 -3
  41. package/dist/services/{portable-mcp-classifier.js → profile/portable-mcp-classifier.js} +2 -2
  42. package/dist/services/{portable-profile-pack-service.d.ts → profile/portable-profile-pack-service.d.ts} +8 -2
  43. package/dist/services/{portable-profile-pack-service.js → profile/portable-profile-pack-service.js} +83 -9
  44. package/dist/services/profile/profile-apply-service.d.ts +34 -0
  45. package/dist/services/profile/profile-apply-service.js +102 -0
  46. package/dist/services/{profile-export-service.d.ts → profile/profile-export-service.d.ts} +7 -3
  47. package/dist/services/{profile-export-service.js → profile/profile-export-service.js} +3 -1
  48. package/dist/services/{profile-import-service.d.ts → profile/profile-import-service.d.ts} +1 -1
  49. package/dist/services/{profile-import-service.js → profile/profile-import-service.js} +85 -130
  50. package/dist/services/{profile-service.d.ts → profile/profile-service.d.ts} +3 -11
  51. package/dist/services/{profile-service.js → profile/profile-service.js} +58 -103
  52. package/dist/services/profile/profile-snapshot-service.d.ts +12 -0
  53. package/dist/services/profile/profile-snapshot-service.js +47 -0
  54. package/dist/types.d.ts +2 -57
  55. package/dist/ui/routes.d.ts +1 -3
  56. package/dist/ui/routes.js +79 -128
  57. package/dist/ui/server.d.ts +1 -1
  58. package/dist/web/assets/index-CGmTbSgk.js +63 -0
  59. package/dist/web/assets/index-EIVU5Woh.css +2 -0
  60. package/dist/web/index.html +2 -2
  61. package/package.json +1 -1
  62. package/dist/commands/init.d.ts +0 -3
  63. package/dist/commands/init.js +0 -27
  64. package/dist/commands/run.d.ts +0 -3
  65. package/dist/commands/run.js +0 -25
  66. package/dist/commands/sync.d.ts +0 -3
  67. package/dist/commands/sync.js +0 -31
  68. package/dist/config.d.ts +0 -14
  69. package/dist/config.js +0 -96
  70. package/dist/context/builder.d.ts +0 -6
  71. package/dist/context/builder.js +0 -13
  72. package/dist/context/memory.d.ts +0 -5
  73. package/dist/context/memory.js +0 -43
  74. package/dist/context/skills.d.ts +0 -2
  75. package/dist/context/skills.js +0 -8
  76. package/dist/executor/claude.d.ts +0 -12
  77. package/dist/executor/claude.js +0 -16
  78. package/dist/executor/codex.d.ts +0 -12
  79. package/dist/executor/codex.js +0 -16
  80. package/dist/executor/process.d.ts +0 -11
  81. package/dist/executor/process.js +0 -40
  82. package/dist/executor/resolver.d.ts +0 -13
  83. package/dist/executor/resolver.js +0 -60
  84. package/dist/executor/types.d.ts +0 -14
  85. package/dist/executor/types.js +0 -1
  86. package/dist/services/config-write-service.d.ts +0 -12
  87. package/dist/services/config-write-service.js +0 -70
  88. package/dist/services/doctor-service.js +0 -79
  89. package/dist/services/init-service.d.ts +0 -14
  90. package/dist/services/init-service.js +0 -88
  91. package/dist/services/memory-write-service.d.ts +0 -12
  92. package/dist/services/memory-write-service.js +0 -56
  93. package/dist/services/run-service.d.ts +0 -15
  94. package/dist/services/run-service.js +0 -94
  95. package/dist/services/status-service.d.ts +0 -17
  96. package/dist/services/status-service.js +0 -21
  97. package/dist/services/sync-service.d.ts +0 -15
  98. package/dist/services/sync-service.js +0 -69
  99. package/dist/ui/streaming.d.ts +0 -3
  100. package/dist/ui/streaming.js +0 -16
  101. package/dist/web/assets/index-Bbophmwh.css +0 -2
  102. package/dist/web/assets/index-DDG_ylui.js +0 -63
  103. /package/dist/{system/executables.d.ts → executables.d.ts} +0 -0
  104. /package/dist/{system/executables.js → executables.js} +0 -0
  105. /package/dist/services/{agent-converter-service.d.ts → agent/agent-converter-service.d.ts} +0 -0
  106. /package/dist/services/{agent-converter-service.js → agent/agent-converter-service.js} +0 -0
  107. /package/dist/services/{credential-resolution-service.js → credential/credential-resolution-service.js} +0 -0
  108. /package/dist/services/{runtime-detector.js → platform/runtime-detector.js} +0 -0
  109. /package/dist/services/{update-check-service.d.ts → platform/update-check-service.d.ts} +0 -0
  110. /package/dist/services/{skill-paths.js → plugin/skill-paths.js} +0 -0
  111. /package/dist/services/{skill-preflight-service.js → plugin/skill-preflight-service.js} +0 -0
@@ -0,0 +1,109 @@
1
+ import { copyFile, cp, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { ProfileError } from '../../errors.js';
5
+ import { formatTimestamp } from '../sync/agent-writer.js';
6
+ export async function installPlugin(sourceDir, plugin) {
7
+ try {
8
+ await stat(sourceDir);
9
+ }
10
+ catch {
11
+ throw new ProfileError(`Bundled plugin "${plugin.name}" source missing at ${sourceDir}.`);
12
+ }
13
+ if (plugin.agent === 'gemini') {
14
+ return;
15
+ }
16
+ const marketplace = plugin.marketplace ?? plugin.source;
17
+ const version = plugin.version ?? 'unknown';
18
+ const cacheRoot = path.join(homedir(), `.${plugin.agent}`, 'plugins', 'cache');
19
+ const targetDir = path.join(cacheRoot, marketplace, plugin.name, version);
20
+ await rm(targetDir, { recursive: true, force: true });
21
+ await mkdir(path.dirname(targetDir), { recursive: true });
22
+ await cp(sourceDir, targetDir, { recursive: true });
23
+ if (plugin.agent === 'claude') {
24
+ await registerClaudePlugin({
25
+ pluginKey: `${plugin.name}@${marketplace}`,
26
+ installPath: targetDir,
27
+ version,
28
+ });
29
+ return;
30
+ }
31
+ if (plugin.agent === 'codex') {
32
+ await registerCodexPlugin({
33
+ pluginKey: `${plugin.name}@${marketplace}`,
34
+ });
35
+ }
36
+ }
37
+ export async function installUserSkill(sourceDir, skill) {
38
+ try {
39
+ await stat(sourceDir);
40
+ }
41
+ catch {
42
+ throw new ProfileError(`Bundled user skill "${skill.name}" source missing at ${sourceDir}.`);
43
+ }
44
+ const targetDir = path.join(homedir(), `.${skill.agent}`, 'skills', skill.name);
45
+ await rm(targetDir, { recursive: true, force: true });
46
+ await mkdir(path.dirname(targetDir), { recursive: true });
47
+ await cp(sourceDir, targetDir, { recursive: true });
48
+ }
49
+ async function registerClaudePlugin(options) {
50
+ const filePath = path.join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
51
+ let existing = { version: 2, plugins: {} };
52
+ try {
53
+ const source = await readFile(filePath, 'utf8');
54
+ existing = JSON.parse(source);
55
+ await backupFile(filePath);
56
+ }
57
+ catch {
58
+ // fresh file
59
+ }
60
+ const plugins = (existing.plugins ?? {});
61
+ const now = new Date().toISOString();
62
+ const entry = {
63
+ scope: 'user',
64
+ installPath: options.installPath,
65
+ version: options.version,
66
+ installedAt: now,
67
+ lastUpdated: now,
68
+ };
69
+ plugins[options.pluginKey] = [entry];
70
+ existing.plugins = plugins;
71
+ if (typeof existing.version !== 'number')
72
+ existing.version = 2;
73
+ await mkdir(path.dirname(filePath), { recursive: true });
74
+ await atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
75
+ }
76
+ async function registerCodexPlugin(options) {
77
+ const filePath = path.join(homedir(), '.codex', 'config.toml');
78
+ let existing = '';
79
+ try {
80
+ existing = await readFile(filePath, 'utf8');
81
+ await backupFile(filePath);
82
+ }
83
+ catch {
84
+ existing = '';
85
+ }
86
+ const header = `[plugins."${options.pluginKey}"]`;
87
+ if (existing.includes(header))
88
+ return;
89
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
90
+ const separator = existing.length > 0 ? '\n' : '';
91
+ const block = `${header}\nenabled = true\n`;
92
+ const next = existing + prefix + separator + block;
93
+ await mkdir(path.dirname(filePath), { recursive: true });
94
+ await atomicWrite(filePath, next);
95
+ }
96
+ async function backupFile(filePath) {
97
+ const backupPath = `${filePath}.bak.${formatTimestamp()}`;
98
+ try {
99
+ await copyFile(filePath, backupPath);
100
+ }
101
+ catch {
102
+ // file may not exist
103
+ }
104
+ }
105
+ async function atomicWrite(filePath, content) {
106
+ const tmpPath = `${filePath}.tmp.${Date.now()}`;
107
+ await writeFile(tmpPath, content, 'utf8');
108
+ await rename(tmpPath, filePath);
109
+ }
@@ -0,0 +1,11 @@
1
+ import type { AgentName } from '../../types.js';
2
+ export interface AgentAvailability {
3
+ agent: AgentName;
4
+ available: boolean;
5
+ command: string;
6
+ resolvedPath?: string;
7
+ }
8
+ export interface AgentAvailabilityService {
9
+ getAll(): Promise<Record<AgentName, AgentAvailability>>;
10
+ }
11
+ export declare function createAgentAvailabilityService(): AgentAvailabilityService;
@@ -0,0 +1,32 @@
1
+ import { findExecutable } from '../../executables.js';
2
+ const SUPPORTED_AGENTS = ['claude', 'codex', 'gemini'];
3
+ const AGENT_COMMANDS = {
4
+ claude: 'claude',
5
+ codex: 'codex',
6
+ gemini: 'gemini',
7
+ };
8
+ export function createAgentAvailabilityService() {
9
+ const cache = new Map();
10
+ const check = (agent) => {
11
+ if (!cache.has(agent)) {
12
+ cache.set(agent, checkAvailability(agent));
13
+ }
14
+ return cache.get(agent);
15
+ };
16
+ return {
17
+ async getAll() {
18
+ const entries = await Promise.all(SUPPORTED_AGENTS.map(async (agent) => [agent, await check(agent)]));
19
+ return Object.fromEntries(entries);
20
+ },
21
+ };
22
+ }
23
+ async function checkAvailability(agent) {
24
+ const command = AGENT_COMMANDS[agent];
25
+ const resolvedPath = await findExecutable(command);
26
+ return {
27
+ agent,
28
+ command,
29
+ available: resolvedPath !== null,
30
+ resolvedPath: resolvedPath ?? undefined,
31
+ };
32
+ }
@@ -1,9 +1,9 @@
1
- import type { AgentName } from '../types.js';
2
- import { type AgentLiveConfig, type AgentMcpEntry, type PortableRemoteMcpMetadata } from './sync/agent-reader.js';
3
- import { type McpPreflightService } from './mcp-preflight-service.js';
4
- import { type SkillPreflightService } from './skill-preflight-service.js';
5
- export type { AgentLiveConfig, AgentMcpEntry, AgentSkillEntry } from './sync/agent-reader.js';
6
- export type { PortableRemoteMcpMetadata } from './sync/agent-reader.js';
1
+ import type { AgentName } from '../../types.js';
2
+ import { type AgentLiveConfig, type AgentMcpEntry, type PortableRemoteMcpMetadata } from '../sync/agent-reader.js';
3
+ import { type McpPreflightService } from '../platform/mcp-preflight-service.js';
4
+ import { type SkillPreflightService } from '../plugin/skill-preflight-service.js';
5
+ export type { AgentLiveConfig, AgentMcpEntry, AgentSkillEntry } from '../sync/agent-reader.js';
6
+ export type { PortableRemoteMcpMetadata } from '../sync/agent-reader.js';
7
7
  export interface AgentConfigService {
8
8
  readAll(options: {
9
9
  cwd: string;
@@ -1,12 +1,12 @@
1
1
  import { copyFile, cp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import path from 'node:path';
4
- import { ValidationError } from '../errors.js';
5
- import { createClaudeReader, createCodexReader, createGeminiReader, } from './sync/agent-reader.js';
6
- import { formatTimestamp } from './sync/agent-writer.js';
7
- import { createMcpPreflightService } from './mcp-preflight-service.js';
8
- import { createSkillPreflightService } from './skill-preflight-service.js';
9
- import { getSkillDir } from './skill-paths.js';
4
+ import { ValidationError } from '../../errors.js';
5
+ import { createClaudeReader, createCodexReader, createGeminiReader, } from '../sync/agent-reader.js';
6
+ import { formatTimestamp } from '../sync/agent-writer.js';
7
+ import { createMcpPreflightService } from '../platform/mcp-preflight-service.js';
8
+ import { createSkillPreflightService } from '../plugin/skill-preflight-service.js';
9
+ import { getSkillDir } from '../plugin/skill-paths.js';
10
10
  const readers = {
11
11
  claude: createClaudeReader(),
12
12
  codex: createCodexReader(),
@@ -1,7 +1,8 @@
1
- import type { McpServerConfig, PortableCredentialSpec } from '../types.js';
1
+ import type { McpServerConfig, PortableCredentialSpec } from '../../types.js';
2
2
  export interface CredentialRedactionResult<T extends McpServerConfig> {
3
3
  redacted: T;
4
4
  credentials: PortableCredentialSpec[];
5
+ rawValues: Record<string, string>;
5
6
  }
6
7
  export declare function redactPortableMcpCredentials<T extends McpServerConfig>(config: T): CredentialRedactionResult<T>;
7
8
  interface CredentialAccumulator {
@@ -1,8 +1,9 @@
1
1
  export function redactPortableMcpCredentials(config) {
2
2
  const credentialsByKey = new Map();
3
- const redactedEnv = redactStringMap(config.env, 'env', credentialsByKey);
3
+ const rawValues = {};
4
+ const redactedEnv = redactStringMap(config.env, 'env', credentialsByKey, rawValues);
4
5
  if (config.kind === 'remote') {
5
- const redactedHeaders = redactStringMap(config.headers, 'header', credentialsByKey);
6
+ const redactedHeaders = redactStringMap(config.headers, 'header', credentialsByKey, rawValues);
6
7
  return {
7
8
  redacted: {
8
9
  ...config,
@@ -10,6 +11,7 @@ export function redactPortableMcpCredentials(config) {
10
11
  ...(redactedHeaders ? { headers: redactedHeaders } : {}),
11
12
  },
12
13
  credentials: finalizePortableCredentialSpecs(credentialsByKey),
14
+ rawValues,
13
15
  };
14
16
  }
15
17
  return {
@@ -18,9 +20,10 @@ export function redactPortableMcpCredentials(config) {
18
20
  ...(redactedEnv ? { env: redactedEnv } : {}),
19
21
  },
20
22
  credentials: finalizePortableCredentialSpecs(credentialsByKey),
23
+ rawValues,
21
24
  };
22
25
  }
23
- function redactStringMap(values, source, credentialsByKey) {
26
+ function redactStringMap(values, source, credentialsByKey, rawValues) {
24
27
  if (!values) {
25
28
  return undefined;
26
29
  }
@@ -32,6 +35,9 @@ function redactStringMap(values, source, credentialsByKey) {
32
35
  }
33
36
  const credentialKey = normalizeCredentialKey(key);
34
37
  addCredentialSpec(credentialsByKey, credentialKey, source, key);
38
+ if (!isCredentialPlaceholder(value)) {
39
+ rawValues[credentialKey] = value;
40
+ }
35
41
  redacted[key] = isCredentialPlaceholder(value)
36
42
  ? value
37
43
  : `\${credentials.${credentialKey}}`;
@@ -1,4 +1,4 @@
1
- import type { McpServerConfig, PortableCredentialPlaceholder, PortableCredentialSpec } from '../types.js';
1
+ import type { McpServerConfig, PortableCredentialPlaceholder, PortableCredentialSpec } from '../../types.js';
2
2
  export interface CredentialResolutionResult<T extends McpServerConfig> {
3
3
  resolved: T;
4
4
  missing: PortableCredentialSpec[];
@@ -1,5 +1,5 @@
1
- import type { ExecutorResolver } from '../executor/resolver.js';
2
- import type { DiagnosticCheck } from '../types.js';
1
+ import { type AgentAvailabilityService } from '../agent/agent-availability-service.js';
2
+ import type { DiagnosticCheck } from '../../types.js';
3
3
  export interface DoctorResult {
4
4
  checks: DiagnosticCheck[];
5
5
  hasIssues: boolean;
@@ -10,5 +10,5 @@ export interface DoctorService {
10
10
  }): Promise<DoctorResult>;
11
11
  }
12
12
  export declare function createDoctorService(dependencies?: {
13
- resolver?: ExecutorResolver;
13
+ availabilityService?: AgentAvailabilityService;
14
14
  }): DoctorService;
@@ -0,0 +1,23 @@
1
+ import { createAgentAvailabilityService, } from '../agent/agent-availability-service.js';
2
+ export function createDoctorService(dependencies = {}) {
3
+ const availabilityService = dependencies.availabilityService ?? createAgentAvailabilityService();
4
+ return {
5
+ async execute() {
6
+ const checks = [];
7
+ const availability = await availabilityService.getAll();
8
+ for (const agent of Object.values(availability)) {
9
+ checks.push({
10
+ label: 'Agent',
11
+ status: agent.available ? 'ok' : 'warn',
12
+ message: agent.available
13
+ ? `${agent.agent} is available`
14
+ : `${agent.agent} is not available on PATH`,
15
+ });
16
+ }
17
+ return {
18
+ checks,
19
+ hasIssues: checks.some((c) => c.status !== 'ok'),
20
+ };
21
+ },
22
+ };
23
+ }
@@ -1,5 +1,5 @@
1
- import type { AgentName } from '../types.js';
2
- import type { AgentMcpEntry, PortableRemoteMcpMetadata } from './agent-config-service.js';
1
+ import type { AgentName } from '../../types.js';
2
+ import type { AgentMcpEntry, PortableRemoteMcpMetadata } from '../agent/agent-config-service.js';
3
3
  export interface McpPreflightCheck {
4
4
  label: string;
5
5
  status: 'ok' | 'warn' | 'error';
@@ -1,6 +1,6 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { findExecutable } from '../system/executables.js';
3
+ import { findExecutable } from '../../executables.js';
4
4
  const ENTRYPOINT_RUNNERS = new Set(['node', 'nodejs', 'python', 'python3', 'bash', 'sh', 'zsh', 'deno', 'bun']);
5
5
  const SUPPORTED_LOCAL_LAUNCHERS = ['npx', 'uvx', 'node', 'python', 'python3', 'java -jar', 'go run', 'cargo run', 'and local binaries/scripts'];
6
6
  export function createMcpPreflightService(dependencies = {}) {
@@ -1,4 +1,4 @@
1
- import type { McpRuntime } from '../types.js';
1
+ import type { McpRuntime } from '../../types.js';
2
2
  export declare function detectMcpRuntime(command: string): McpRuntime | null;
3
3
  export declare function extractEntrypoint(command: string, args: string[]): string | null;
4
4
  export declare function findProjectRoot(entrypointPath: string, runtime: McpRuntime): {
@@ -0,0 +1,19 @@
1
+ import { type AgentAvailability, type AgentAvailabilityService } from '../agent/agent-availability-service.js';
2
+ import { type ProfileService } from '../profile/profile-service.js';
3
+ import type { AgentName } from '../../types.js';
4
+ export interface StatusResult {
5
+ agents: Record<AgentName, AgentAvailability>;
6
+ profiles: {
7
+ count: number;
8
+ names: string[];
9
+ };
10
+ }
11
+ export interface StatusService {
12
+ execute(options?: {
13
+ cwd?: string;
14
+ }): Promise<StatusResult>;
15
+ }
16
+ export declare function createStatusService(dependencies?: {
17
+ availabilityService?: AgentAvailabilityService;
18
+ profileService?: ProfileService;
19
+ }): StatusService;
@@ -0,0 +1,22 @@
1
+ import { createAgentAvailabilityService, } from '../agent/agent-availability-service.js';
2
+ import { createProfileService } from '../profile/profile-service.js';
3
+ export function createStatusService(dependencies = {}) {
4
+ const availabilityService = dependencies.availabilityService ?? createAgentAvailabilityService();
5
+ const profileService = dependencies.profileService ?? createProfileService();
6
+ return {
7
+ async execute(options = {}) {
8
+ const cwd = options.cwd ?? process.cwd();
9
+ const [agents, profileList] = await Promise.all([
10
+ availabilityService.getAll(),
11
+ profileService.list({ cwd }),
12
+ ]);
13
+ return {
14
+ agents,
15
+ profiles: {
16
+ count: profileList.profiles.length,
17
+ names: profileList.profiles,
18
+ },
19
+ };
20
+ },
21
+ };
22
+ }
@@ -9,7 +9,7 @@ const execFileAsync = promisify(execFile);
9
9
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
10
10
  const CACHE_DIR = path.join(homedir(), '.brainctl');
11
11
  const CACHE_PATH = path.join(CACHE_DIR, 'update-check.json');
12
- const packageVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
12
+ const packageVersion = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'));
13
13
  export function createUpdateCheckService(dependencies = {}) {
14
14
  const currentVersion = dependencies.currentVersion ?? packageVersion.version;
15
15
  const fetchLatestVersion = dependencies.fetchLatestVersion ?? fetchFromRegistry;
@@ -0,0 +1,20 @@
1
+ import type { AgentName } from '../../types.js';
2
+ import type { AgentMcpEntry } from '../agent/agent-config-service.js';
3
+ export interface PluginBundleAgent {
4
+ name: string;
5
+ sourceFormat: 'claude-md' | 'codex-toml';
6
+ content: string;
7
+ }
8
+ export interface PluginBundleCommand {
9
+ name: string;
10
+ content: string;
11
+ }
12
+ export interface PluginBundle {
13
+ skills: string[];
14
+ mcps: Record<string, AgentMcpEntry>;
15
+ agents: PluginBundleAgent[];
16
+ commands: PluginBundleCommand[];
17
+ }
18
+ export declare function isAgentInstallableOnTarget(target: AgentName): boolean;
19
+ export declare function isCommandInstallableOnTarget(target: AgentName): boolean;
20
+ export declare function defaultReadInstalledPluginBundle(installPath: string): Promise<PluginBundle>;
@@ -0,0 +1,80 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export function isAgentInstallableOnTarget(target) {
4
+ return target === 'claude' || target === 'codex';
5
+ }
6
+ export function isCommandInstallableOnTarget(target) {
7
+ return target === 'claude' || target === 'codex';
8
+ }
9
+ export async function defaultReadInstalledPluginBundle(installPath) {
10
+ const skillsDir = path.join(installPath, 'skills');
11
+ let skills = [];
12
+ try {
13
+ const entries = await readdir(skillsDir, { withFileTypes: true });
14
+ skills = entries
15
+ .filter((entry) => !entry.name.startsWith('.') && entry.isDirectory())
16
+ .map((entry) => entry.name)
17
+ .sort((left, right) => left.localeCompare(right));
18
+ }
19
+ catch {
20
+ skills = [];
21
+ }
22
+ let mcps = {};
23
+ try {
24
+ const mcpSource = await readFile(path.join(installPath, '.mcp.json'), 'utf8');
25
+ const parsed = JSON.parse(mcpSource);
26
+ mcps = Object.fromEntries(Object.entries(parsed)
27
+ .filter(([, value]) => typeof value?.command === 'string')
28
+ .map(([key, value]) => [
29
+ key,
30
+ {
31
+ command: String(value.command),
32
+ args: Array.isArray(value.args) ? value.args.map(String) : undefined,
33
+ env: value.env && typeof value.env === 'object' && !Array.isArray(value.env)
34
+ ? Object.fromEntries(Object.entries(value.env).map(([envKey, envValue]) => [
35
+ envKey,
36
+ String(envValue),
37
+ ]))
38
+ : undefined,
39
+ },
40
+ ]));
41
+ }
42
+ catch {
43
+ mcps = {};
44
+ }
45
+ const agents = [];
46
+ try {
47
+ const entries = await readdir(path.join(installPath, 'agents'), { withFileTypes: true });
48
+ for (const entry of entries) {
49
+ if (!entry.isFile())
50
+ continue;
51
+ if (entry.name.endsWith('.md')) {
52
+ const content = await readFile(path.join(installPath, 'agents', entry.name), 'utf8');
53
+ agents.push({ name: entry.name.replace(/\.md$/, ''), sourceFormat: 'claude-md', content });
54
+ }
55
+ else if (entry.name.endsWith('.toml')) {
56
+ const content = await readFile(path.join(installPath, 'agents', entry.name), 'utf8');
57
+ agents.push({ name: entry.name.replace(/\.toml$/, ''), sourceFormat: 'codex-toml', content });
58
+ }
59
+ }
60
+ agents.sort((left, right) => left.name.localeCompare(right.name));
61
+ }
62
+ catch {
63
+ // no agents dir
64
+ }
65
+ const commands = [];
66
+ try {
67
+ const entries = await readdir(path.join(installPath, 'commands'), { withFileTypes: true });
68
+ for (const entry of entries) {
69
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
70
+ continue;
71
+ const content = await readFile(path.join(installPath, 'commands', entry.name), 'utf8');
72
+ commands.push({ name: entry.name.replace(/\.md$/, ''), content });
73
+ }
74
+ commands.sort((left, right) => left.name.localeCompare(right.name));
75
+ }
76
+ catch {
77
+ // no commands dir
78
+ }
79
+ return { skills, mcps, agents, commands };
80
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentName } from '../../types.js';
2
+ import type { PluginInstallCheck } from './plugin-install-service.js';
3
+ export interface IncompatibleArtifacts {
4
+ hasAppConnector: boolean;
5
+ hasHooks: boolean;
6
+ hasCommands: boolean;
7
+ codexAgentSkills: string[];
8
+ claudeAgents: string[];
9
+ }
10
+ export declare function pathExists(target: string): Promise<boolean>;
11
+ export declare function detectIncompatibleArtifacts(installPath: string): Promise<IncompatibleArtifacts>;
12
+ export declare function formatCompatibilityWarnings(artifacts: IncompatibleArtifacts, context: {
13
+ sourceAgent: AgentName;
14
+ targetAgent: AgentName;
15
+ }): PluginInstallCheck[];
@@ -0,0 +1,91 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export async function pathExists(target) {
4
+ try {
5
+ await stat(target);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export async function detectIncompatibleArtifacts(installPath) {
13
+ const [hasAppConnector, hasHooks, hasCommands, codexAgentSkills, claudeAgents] = await Promise.all([
14
+ pathExists(path.join(installPath, '.app.json')),
15
+ pathExists(path.join(installPath, 'hooks')),
16
+ pathExists(path.join(installPath, 'commands')),
17
+ listCodexAgentSkills(installPath),
18
+ listClaudeAgentFiles(installPath),
19
+ ]);
20
+ return { hasAppConnector, hasHooks, hasCommands, codexAgentSkills, claudeAgents };
21
+ }
22
+ async function listCodexAgentSkills(installPath) {
23
+ const skillsDir = path.join(installPath, 'skills');
24
+ try {
25
+ const entries = await readdir(skillsDir, { withFileTypes: true });
26
+ const matches = [];
27
+ for (const entry of entries) {
28
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
29
+ continue;
30
+ if (await pathExists(path.join(skillsDir, entry.name, 'agents'))) {
31
+ matches.push(entry.name);
32
+ }
33
+ }
34
+ return matches.sort((left, right) => left.localeCompare(right));
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ }
40
+ async function listClaudeAgentFiles(installPath) {
41
+ const agentsDir = path.join(installPath, 'agents');
42
+ try {
43
+ const entries = await readdir(agentsDir, { withFileTypes: true });
44
+ return entries
45
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
46
+ .map((entry) => entry.name.replace(/\.md$/, ''))
47
+ .sort((left, right) => left.localeCompare(right));
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ export function formatCompatibilityWarnings(artifacts, context) {
54
+ const warnings = [];
55
+ if (artifacts.hasAppConnector && context.targetAgent !== 'codex') {
56
+ warnings.push({
57
+ label: 'App connector',
58
+ status: 'warn',
59
+ message: `Plugin ships a Codex app connector (.app.json) that will NOT transfer. Skill instructions will copy over but the backing integration will not work on ${context.targetAgent}.`,
60
+ });
61
+ }
62
+ if (artifacts.codexAgentSkills.length > 0 && context.targetAgent !== 'codex') {
63
+ warnings.push({
64
+ label: 'Codex agent YAML',
65
+ status: 'warn',
66
+ message: `Skills ${artifacts.codexAgentSkills.join(', ')} include Codex-specific agent YAML that will not transfer to ${context.targetAgent}.`,
67
+ });
68
+ }
69
+ if (artifacts.hasHooks && context.targetAgent !== 'claude') {
70
+ warnings.push({
71
+ label: 'Claude hooks',
72
+ status: 'warn',
73
+ message: `Plugin ships session hooks that only work on Claude and will NOT transfer to ${context.targetAgent}.`,
74
+ });
75
+ }
76
+ if (context.targetAgent === 'gemini' && artifacts.claudeAgents.length > 0) {
77
+ warnings.push({
78
+ label: 'Subagents',
79
+ status: 'warn',
80
+ message: `Plugin ships subagent definitions (${artifacts.claudeAgents.join(', ')}) that cannot be converted to ${context.targetAgent}.`,
81
+ });
82
+ }
83
+ if (context.targetAgent === 'gemini' && artifacts.hasCommands) {
84
+ warnings.push({
85
+ label: 'Slash commands',
86
+ status: 'warn',
87
+ message: `Plugin ships slash commands that cannot be converted to ${context.targetAgent}.`,
88
+ });
89
+ }
90
+ return warnings;
91
+ }
@@ -0,0 +1,27 @@
1
+ import type { AgentName } from '../../types.js';
2
+ import type { PluginBundleAgent, PluginBundleCommand } from './plugin-install-bundle.js';
3
+ export declare function defaultInstallAgent(options: {
4
+ targetAgent: AgentName;
5
+ agent: PluginBundleAgent;
6
+ }): Promise<void>;
7
+ export declare function defaultInstallCommand(options: {
8
+ targetAgent: AgentName;
9
+ command: PluginBundleCommand;
10
+ }): Promise<void>;
11
+ export declare function defaultRemoveAgentFile(options: {
12
+ targetAgent: AgentName;
13
+ agentName: string;
14
+ }): Promise<void>;
15
+ export declare function defaultRemoveCommandFile(options: {
16
+ targetAgent: AgentName;
17
+ commandName: string;
18
+ }): Promise<void>;
19
+ export declare function defaultCopySkillDirectory(options: {
20
+ sourceInstallPath: string;
21
+ skillName: string;
22
+ targetAgent: AgentName;
23
+ }): Promise<void>;
24
+ export declare function defaultRemoveSkillDirectory(options: {
25
+ targetAgent: AgentName;
26
+ skillName: string;
27
+ }): Promise<void>;