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
@@ -1,133 +1,115 @@
1
- import { readdir, readFile, writeFile, mkdir, stat, unlink } from 'node:fs/promises';
1
+ import { readdir, readFile, writeFile, mkdir, rename, rm, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
- import { ProfileError, ProfileNotFoundError } from '../errors.js';
4
+ import { ProfileError, ProfileNotFoundError } from '../../errors.js';
5
5
  const VALID_RUNTIMES = new Set(['node', 'python', 'java', 'go', 'rust', 'binary']);
6
- const BRAINCTL_DIR = '.brainctl';
7
6
  const PROFILES_DIR = '.brainctl/profiles';
8
- const META_CONFIG = '.brainctl/config.yaml';
7
+ const PROFILE_FILE = 'profile.yaml';
8
+ export function profileDir(cwd, name) {
9
+ return path.join(cwd, PROFILES_DIR, name);
10
+ }
11
+ export function profileFile(cwd, name) {
12
+ return path.join(profileDir(cwd, name), PROFILE_FILE);
13
+ }
14
+ function legacyProfileFile(cwd, name) {
15
+ return path.join(cwd, PROFILES_DIR, `${name}.yaml`);
16
+ }
17
+ async function migrateLegacyProfile(cwd, name) {
18
+ const legacy = legacyProfileFile(cwd, name);
19
+ const folder = profileDir(cwd, name);
20
+ const newFile = profileFile(cwd, name);
21
+ if (!(await pathExists(legacy)))
22
+ return;
23
+ if (await pathExists(newFile))
24
+ return;
25
+ await mkdir(folder, { recursive: true });
26
+ await rename(legacy, newFile);
27
+ }
9
28
  export function createProfileService() {
10
29
  return {
11
30
  async list(options = {}) {
12
31
  const cwd = options.cwd ?? process.cwd();
13
32
  const profilesDir = path.join(cwd, PROFILES_DIR);
14
- let files = [];
33
+ const names = new Set();
15
34
  try {
16
- const entries = await readdir(profilesDir);
17
- files = entries
18
- .filter((f) => f.endsWith('.yaml'))
19
- .map((f) => f.replace(/\.yaml$/, ''))
20
- .sort();
35
+ const entries = await readdir(profilesDir, { withFileTypes: true });
36
+ for (const entry of entries) {
37
+ if (entry.isDirectory()) {
38
+ if (await pathExists(path.join(profilesDir, entry.name, PROFILE_FILE))) {
39
+ names.add(entry.name);
40
+ }
41
+ }
42
+ else if (entry.isFile() && entry.name.endsWith('.yaml')) {
43
+ const bare = entry.name.replace(/\.yaml$/, '');
44
+ await migrateLegacyProfile(cwd, bare);
45
+ names.add(bare);
46
+ }
47
+ }
21
48
  }
22
49
  catch {
23
50
  // No profiles directory yet
24
51
  }
25
- const meta = await loadMetaConfig(cwd);
26
52
  return {
27
- profiles: files,
28
- activeProfile: meta.active_profile || null,
53
+ profiles: Array.from(names).sort(),
29
54
  };
30
55
  },
31
56
  async get(options) {
32
57
  const cwd = options.cwd ?? process.cwd();
33
- const profilePath = path.join(cwd, PROFILES_DIR, `${options.name}.yaml`);
58
+ await migrateLegacyProfile(cwd, options.name);
59
+ const filePath = profileFile(cwd, options.name);
34
60
  let source;
35
61
  try {
36
- source = await readFile(profilePath, 'utf8');
62
+ source = await readFile(filePath, 'utf8');
37
63
  }
38
64
  catch {
39
- throw new ProfileNotFoundError(`Profile "${options.name}" not found at ${profilePath}`);
65
+ throw new ProfileNotFoundError(`Profile "${options.name}" not found at ${filePath}`);
40
66
  }
41
67
  return parseProfile(source, options.name);
42
68
  },
43
69
  async create(options) {
44
70
  const cwd = options.cwd ?? process.cwd();
45
- const profilesDir = path.join(cwd, PROFILES_DIR);
46
- const profilePath = path.join(profilesDir, `${options.name}.yaml`);
47
- if (await pathExists(profilePath)) {
71
+ const folder = profileDir(cwd, options.name);
72
+ const filePath = profileFile(cwd, options.name);
73
+ if ((await pathExists(filePath)) ||
74
+ (await pathExists(legacyProfileFile(cwd, options.name)))) {
48
75
  throw new ProfileError(`Profile "${options.name}" already exists.`);
49
76
  }
50
77
  const scaffold = {
51
78
  name: options.name,
52
79
  description: options.description ?? '',
53
- skills: {
54
- example: {
55
- description: 'Example skill',
56
- prompt: 'Describe what this skill does...',
57
- },
58
- },
59
80
  mcps: {},
60
- memory: {
61
- paths: ['./memory'],
62
- },
63
81
  };
64
- await mkdir(profilesDir, { recursive: true });
65
- await writeFile(profilePath, YAML.stringify(scaffold), 'utf8');
66
- return { profilePath };
82
+ await mkdir(folder, { recursive: true });
83
+ await writeFile(filePath, YAML.stringify(scaffold), 'utf8');
84
+ return { profilePath: filePath };
67
85
  },
68
86
  async update(options) {
69
87
  const cwd = options.cwd ?? process.cwd();
70
- const profilePath = path.join(cwd, PROFILES_DIR, `${options.name}.yaml`);
71
- if (!(await pathExists(profilePath))) {
88
+ await migrateLegacyProfile(cwd, options.name);
89
+ const filePath = profileFile(cwd, options.name);
90
+ if (!(await pathExists(filePath))) {
72
91
  throw new ProfileNotFoundError(`Profile "${options.name}" not found.`);
73
92
  }
74
93
  const normalized = normalizeProfileConfig(options.config, options.name);
75
94
  const data = {
76
95
  name: normalized.name,
77
96
  ...(normalized.description ? { description: normalized.description } : {}),
78
- skills: normalized.skills,
79
97
  mcps: normalized.mcps,
80
- memory: normalized.memory,
81
98
  };
82
- await writeFile(profilePath, YAML.stringify(data), 'utf8');
99
+ await writeFile(filePath, YAML.stringify(data), 'utf8');
83
100
  },
84
101
  async delete(options) {
85
102
  const cwd = options.cwd ?? process.cwd();
86
- const profilePath = path.join(cwd, PROFILES_DIR, `${options.name}.yaml`);
87
- if (!(await pathExists(profilePath))) {
88
- throw new ProfileNotFoundError(`Profile "${options.name}" not found.`);
89
- }
90
- const meta = await loadMetaConfig(cwd);
91
- if (meta.active_profile === options.name) {
92
- throw new ProfileError('Cannot delete the active profile.');
93
- }
94
- await unlink(profilePath);
95
- },
96
- async use(options) {
97
- const cwd = options.cwd ?? process.cwd();
98
- // Validate profile exists
99
- const profilePath = path.join(cwd, PROFILES_DIR, `${options.name}.yaml`);
100
- if (!(await pathExists(profilePath))) {
103
+ await migrateLegacyProfile(cwd, options.name);
104
+ const folder = profileDir(cwd, options.name);
105
+ const filePath = profileFile(cwd, options.name);
106
+ if (!(await pathExists(filePath))) {
101
107
  throw new ProfileNotFoundError(`Profile "${options.name}" not found.`);
102
108
  }
103
- const meta = await loadMetaConfig(cwd);
104
- const previousProfile = meta.active_profile || null;
105
- meta.active_profile = options.name;
106
- const metaPath = path.join(cwd, META_CONFIG);
107
- await mkdir(path.dirname(metaPath), { recursive: true });
108
- await writeFile(metaPath, YAML.stringify(meta), 'utf8');
109
- return { previousProfile };
110
- },
111
- async getMetaConfig(options = {}) {
112
- const cwd = options.cwd ?? process.cwd();
113
- return loadMetaConfig(cwd);
109
+ await rm(folder, { recursive: true, force: true });
114
110
  },
115
111
  };
116
112
  }
117
- async function loadMetaConfig(cwd) {
118
- const metaPath = path.join(cwd, META_CONFIG);
119
- try {
120
- const source = await readFile(metaPath, 'utf8');
121
- const parsed = YAML.parse(source) ?? {};
122
- return {
123
- active_profile: typeof parsed.active_profile === 'string' ? parsed.active_profile : '',
124
- agents: Array.isArray(parsed.agents) ? parsed.agents : ['claude', 'codex'],
125
- };
126
- }
127
- catch {
128
- return { active_profile: '', agents: ['claude', 'codex', 'gemini'] };
129
- }
130
- }
131
113
  export function parseProfile(source, name) {
132
114
  let parsed;
133
115
  try {
@@ -146,38 +128,11 @@ export function normalizeProfileConfig(value, name) {
146
128
  throw new ProfileError(`Profile "${name}" has invalid structure.`);
147
129
  }
148
130
  const data = value;
149
- const skills = {};
150
- if (data.skills && typeof data.skills === 'object' && !Array.isArray(data.skills)) {
151
- for (const [key, value] of Object.entries(data.skills)) {
152
- if (value && typeof value === 'object' && !Array.isArray(value)) {
153
- const s = value;
154
- if (typeof s.prompt === 'string') {
155
- skills[key] = {
156
- prompt: s.prompt,
157
- description: typeof s.description === 'string' ? s.description : undefined,
158
- };
159
- }
160
- }
161
- }
162
- }
163
131
  const mcps = normalizeMcps(data.mcps, name);
164
- const memoryPaths = [];
165
- if (data.memory && typeof data.memory === 'object' && !Array.isArray(data.memory)) {
166
- const mem = data.memory;
167
- if (Array.isArray(mem.paths)) {
168
- for (const p of mem.paths) {
169
- if (typeof p === 'string') {
170
- memoryPaths.push(p);
171
- }
172
- }
173
- }
174
- }
175
132
  return {
176
133
  name: typeof data.name === 'string' ? data.name : name,
177
134
  description: typeof data.description === 'string' ? data.description : undefined,
178
- skills,
179
135
  mcps,
180
- memory: { paths: memoryPaths },
181
136
  };
182
137
  }
183
138
  function normalizeMcps(value, profileName) {
@@ -0,0 +1,12 @@
1
+ import type { AgentName } from '../../types.js';
2
+ export interface ProfileSnapshotService {
3
+ execute(options: {
4
+ cwd: string;
5
+ agent: AgentName;
6
+ profileName: string;
7
+ }): Promise<{
8
+ profilePath: string;
9
+ }>;
10
+ }
11
+ export declare function createProfileSnapshotService(): ProfileSnapshotService;
12
+ export declare function defaultBackupProfileName(agent: AgentName): string;
@@ -0,0 +1,47 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+ import { profileDir } from './profile-service.js';
5
+ import { createPortableProfilePackService } from './portable-profile-pack-service.js';
6
+ export function createProfileSnapshotService() {
7
+ const packService = createPortableProfilePackService();
8
+ return {
9
+ async execute(options) {
10
+ const outputPath = profileDir(options.cwd, options.profileName);
11
+ const result = await packService.execute({
12
+ cwd: options.cwd,
13
+ source: { source: 'agent', agent: options.agent, cwd: options.cwd },
14
+ outputPath,
15
+ format: 'folder',
16
+ credentialsMode: 'keep',
17
+ });
18
+ await renameInsideProfile(outputPath, options.profileName);
19
+ return { profilePath: result.archivePath };
20
+ },
21
+ };
22
+ }
23
+ async function renameInsideProfile(profilePath, name) {
24
+ const profileFile = path.join(profilePath, 'profile.yaml');
25
+ const manifestFile = path.join(profilePath, 'manifest.yaml');
26
+ for (const file of [profileFile, manifestFile]) {
27
+ try {
28
+ const source = await readFile(file, 'utf8');
29
+ const parsed = YAML.parse(source);
30
+ if (file === profileFile)
31
+ parsed.name = name;
32
+ else
33
+ parsed.profileName = name;
34
+ await writeFile(file, YAML.stringify(parsed), 'utf8');
35
+ }
36
+ catch {
37
+ // best-effort
38
+ }
39
+ }
40
+ }
41
+ export function defaultBackupProfileName(agent) {
42
+ const ts = new Date()
43
+ .toISOString()
44
+ .replace(/[-:T]/g, '')
45
+ .replace(/\..+$/, '');
46
+ return `backup-${agent}-${ts}`;
47
+ }
package/dist/types.d.ts CHANGED
@@ -1,55 +1,6 @@
1
1
  export type AgentName = 'claude' | 'codex' | 'gemini';
2
2
  export type ErrorCategory = 'user' | 'system';
3
3
  export type DiagnosticStatus = 'ok' | 'warn' | 'error';
4
- export interface SkillConfig {
5
- description?: string;
6
- prompt: string;
7
- }
8
- export interface BrainctlConfig {
9
- configPath: string;
10
- rootDir: string;
11
- memory: {
12
- paths: string[];
13
- };
14
- skills: Record<string, SkillConfig>;
15
- mcps: Record<string, unknown>;
16
- }
17
- export interface MemoryLoadResult {
18
- content: string;
19
- files: string[];
20
- count: number;
21
- entries: Array<{
22
- path: string;
23
- content: string;
24
- }>;
25
- }
26
- export interface RunRequest {
27
- cwd?: string;
28
- skill: string;
29
- inputFile: string;
30
- primaryAgent: AgentName;
31
- fallbackAgent?: AgentName;
32
- }
33
- export interface ExecutionStep {
34
- skill: string;
35
- inputFile: string;
36
- primaryAgent: AgentName;
37
- fallbackAgent?: AgentName;
38
- usePreviousOutput?: boolean;
39
- }
40
- export interface ExecutionStepResult {
41
- stepIndex: number;
42
- requestedAgent: AgentName;
43
- agent: AgentName;
44
- fallbackUsed: boolean;
45
- exitCode: number;
46
- output: string;
47
- }
48
- export interface ExecutionTrace {
49
- steps: ExecutionStepResult[];
50
- finalOutput: string;
51
- finalExitCode: number;
52
- }
53
4
  export interface DiagnosticCheck {
54
5
  label: string;
55
6
  status: DiagnosticStatus;
@@ -129,20 +80,14 @@ export type McpServerConfig = LocalMcpServerConfig | RemoteMcpServerConfig;
129
80
  export interface ProfileConfig {
130
81
  name: string;
131
82
  description?: string;
132
- skills: Record<string, SkillConfig>;
133
83
  mcps: Record<string, McpServerConfig>;
134
- memory: {
135
- paths: string[];
136
- };
137
- }
138
- export interface BrainctlMetaConfig {
139
- active_profile: string;
140
- agents: AgentName[];
141
84
  }
142
85
  export interface SyncAgentResult {
143
86
  agent: AgentName;
144
87
  configPath: string;
145
88
  backedUpTo: string | null;
146
89
  mcpCount: number;
90
+ pluginsInstalled?: string[];
91
+ userSkillsInstalled?: string[];
147
92
  }
148
93
  export type SyncResult = SyncAgentResult[];
@@ -1,10 +1,8 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import type { RunService } from '../services/run-service.js';
3
- import type { StatusService } from '../services/status-service.js';
2
+ import type { StatusService } from '../services/platform/status-service.js';
4
3
  export interface UiRouteDependencies {
5
4
  cwd: string;
6
5
  statusService?: StatusService;
7
- runService?: RunService;
8
6
  }
9
7
  export type UiRouteHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void>;
10
8
  export declare function createUiRouteHandler(dependencies: UiRouteDependencies): UiRouteHandler;
package/dist/ui/routes.js CHANGED
@@ -1,32 +1,26 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { loadConfig } from '../config.js';
4
- import { parseConfigPayload } from '../config.js';
5
3
  import { BrainctlError, ProfileError, ProfileNotFoundError, ValidationError } from '../errors.js';
6
- import { loadMemory } from '../context/memory.js';
7
- import { createAgentConfigService } from '../services/agent-config-service.js';
8
- import { createConfigWriteService } from '../services/config-write-service.js';
9
- import { createMcpPreflightService } from '../services/mcp-preflight-service.js';
10
- import { createPluginInstallService } from '../services/plugin-install-service.js';
11
- import { createProfileExportService } from '../services/profile-export-service.js';
12
- import { createProfileImportService } from '../services/profile-import-service.js';
13
- import { createProfileService } from '../services/profile-service.js';
14
- import { createRunService } from '../services/run-service.js';
15
- import { createSkillPreflightService } from '../services/skill-preflight-service.js';
16
- import { createStatusService } from '../services/status-service.js';
17
- import { createSyncService } from '../services/sync-service.js';
18
- import { startSseStream, writeSseEvent } from './streaming.js';
4
+ import { createAgentConfigService } from '../services/agent/agent-config-service.js';
5
+ import { createMcpPreflightService } from '../services/platform/mcp-preflight-service.js';
6
+ import { createPluginInstallService } from '../services/plugin/plugin-install-service.js';
7
+ import { createProfileExportService } from '../services/profile/profile-export-service.js';
8
+ import { createProfileImportService } from '../services/profile/profile-import-service.js';
9
+ import { createProfileService } from '../services/profile/profile-service.js';
10
+ import { createSkillPreflightService } from '../services/plugin/skill-preflight-service.js';
11
+ import { createStatusService } from '../services/platform/status-service.js';
12
+ import { createProfileApplyService, } from '../services/profile/profile-apply-service.js';
13
+ import { createProfileSnapshotService, defaultBackupProfileName, } from '../services/profile/profile-snapshot-service.js';
19
14
  import path from 'node:path';
20
15
  import { fileURLToPath } from 'node:url';
21
16
  const uiAssetRoot = resolveUiAssetRoot();
22
17
  export function createUiRouteHandler(dependencies) {
23
18
  const statusService = dependencies.statusService ?? createStatusService();
24
- const runService = dependencies.runService ?? createRunService();
25
- const configWriteService = createConfigWriteService();
26
19
  const profileService = createProfileService();
27
20
  const profileExportService = createProfileExportService({ profileService });
28
21
  const profileImportService = createProfileImportService();
29
- const syncService = createSyncService({ profileService });
22
+ const profileApplyService = createProfileApplyService({ profileService });
23
+ const profileSnapshotService = createProfileSnapshotService();
30
24
  const agentConfigService = createAgentConfigService();
31
25
  const mcpPreflightService = createMcpPreflightService();
32
26
  const pluginInstallService = createPluginInstallService();
@@ -41,71 +35,6 @@ export function createUiRouteHandler(dependencies) {
41
35
  const overview = await statusService.execute({ cwd: dependencies.cwd });
42
36
  return sendJson(response, 200, overview);
43
37
  }
44
- case '/api/run/stream': {
45
- if (request.method !== 'GET') {
46
- return sendJson(response, 405, { error: 'Method not allowed' });
47
- }
48
- const runRequest = parseRunRequest(url);
49
- if (runRequest === null) {
50
- return sendJson(response, 400, {
51
- error: 'Missing skill, inputFile, or primaryAgent'
52
- });
53
- }
54
- if ('error' in runRequest) {
55
- return sendJson(response, 400, {
56
- error: runRequest.error
57
- });
58
- }
59
- startSseStream(response);
60
- try {
61
- const trace = await runService.execute({
62
- ...runRequest.request,
63
- cwd: dependencies.cwd
64
- }, {
65
- onOutputChunk: (chunk) => {
66
- writeSseEvent(response, 'output', chunk);
67
- },
68
- streamOutput: false
69
- });
70
- writeSseEvent(response, 'result', trace);
71
- response.end();
72
- }
73
- catch (error) {
74
- writeSseEvent(response, 'run-error', {
75
- error: error instanceof Error ? error.message : 'Unexpected server error'
76
- });
77
- response.end();
78
- }
79
- return;
80
- }
81
- case '/api/config': {
82
- if (request.method === 'PUT') {
83
- const body = await readJsonBody(request);
84
- if (!body.ok) {
85
- return sendJson(response, 400, { error: 'Invalid JSON body' });
86
- }
87
- const config = parseConfigPayload(body.value);
88
- await configWriteService.execute({
89
- cwd: dependencies.cwd,
90
- config
91
- });
92
- const savedConfig = await loadConfig({ cwd: dependencies.cwd });
93
- return sendJson(response, 200, savedConfig);
94
- }
95
- if (request.method !== 'GET') {
96
- return sendJson(response, 405, { error: 'Method not allowed' });
97
- }
98
- const config = await loadConfig({ cwd: dependencies.cwd });
99
- return sendJson(response, 200, config);
100
- }
101
- case '/api/memory': {
102
- if (request.method !== 'GET') {
103
- return sendJson(response, 405, { error: 'Method not allowed' });
104
- }
105
- const config = await loadConfig({ cwd: dependencies.cwd });
106
- const memory = await loadMemory({ paths: config.memory.paths });
107
- return sendJson(response, 200, memory);
108
- }
109
38
  case '/api/agents': {
110
39
  if (request.method !== 'GET') {
111
40
  return sendJson(response, 405, { error: 'Method not allowed' });
@@ -207,21 +136,58 @@ export function createUiRouteHandler(dependencies) {
207
136
  return sendProfileError(response, error);
208
137
  }
209
138
  }
210
- case '/api/sync': {
139
+ case '/api/profiles/snapshot': {
211
140
  if (request.method !== 'POST') {
212
141
  return sendJson(response, 405, { error: 'Method not allowed' });
213
142
  }
143
+ const body = await readJsonBody(request);
144
+ if (!body.ok) {
145
+ return sendJson(response, 400, { error: 'Invalid JSON body' });
146
+ }
147
+ const data = (body.value ?? {});
148
+ if (data.agent !== 'claude' && data.agent !== 'codex' && data.agent !== 'gemini') {
149
+ return sendJson(response, 400, { error: 'Invalid agent' });
150
+ }
151
+ const profileName = data.as ?? defaultBackupProfileName(data.agent);
214
152
  try {
215
- const result = await syncService.execute({ cwd: dependencies.cwd });
216
- return sendJson(response, 200, result);
153
+ const result = await profileSnapshotService.execute({
154
+ cwd: dependencies.cwd,
155
+ agent: data.agent,
156
+ profileName,
157
+ });
158
+ return sendJson(response, 200, { profileName, ...result });
217
159
  }
218
160
  catch (error) {
219
- return sendJson(response, 500, {
220
- error: error instanceof Error ? error.message : 'Sync failed',
221
- });
161
+ return sendProfileError(response, error);
222
162
  }
223
163
  }
224
164
  default: {
165
+ // Profile apply: POST /api/profiles/:name/apply
166
+ const applyMatch = url.pathname.match(/^\/api\/profiles\/([^/]+)\/apply$/);
167
+ if (applyMatch) {
168
+ if (request.method !== 'POST') {
169
+ return sendJson(response, 405, { error: 'Method not allowed' });
170
+ }
171
+ const profileName = decodeURIComponent(applyMatch[1]);
172
+ const body = await readJsonBody(request);
173
+ if (!body.ok) {
174
+ return sendJson(response, 400, { error: 'Invalid JSON body' });
175
+ }
176
+ const data = (body.value ?? {});
177
+ try {
178
+ const result = await profileApplyService.execute({
179
+ cwd: dependencies.cwd,
180
+ profileName,
181
+ agents: data.agents ?? ['claude', 'codex', 'gemini'],
182
+ items: data.items,
183
+ backup: data.backup,
184
+ });
185
+ return sendJson(response, 200, result);
186
+ }
187
+ catch (error) {
188
+ return sendProfileError(response, error);
189
+ }
190
+ }
225
191
  // Agent MCP routes: /api/agents/:name/mcps(/:key)
226
192
  const agentMcpCheckMatch = url.pathname.match(/^\/api\/agents\/(claude|codex|gemini)\/mcps\/check$/);
227
193
  if (agentMcpCheckMatch) {
@@ -450,22 +416,34 @@ export function createUiRouteHandler(dependencies) {
450
416
  }
451
417
  return sendJson(response, 405, { error: 'Method not allowed' });
452
418
  }
453
- const profileMatch = url.pathname.match(/^\/api\/profiles\/([^/]+)(\/activate)?$/);
454
- if (profileMatch) {
455
- const name = decodeURIComponent(profileMatch[1]);
456
- const isActivate = profileMatch[2] === '/activate';
457
- if (isActivate) {
458
- if (request.method !== 'POST') {
459
- return sendJson(response, 405, { error: 'Method not allowed' });
460
- }
419
+ // Profile contents: GET /api/profiles/:name/contents
420
+ const contentsMatch = url.pathname.match(/^\/api\/profiles\/([^/]+)\/contents$/);
421
+ if (contentsMatch) {
422
+ if (request.method !== 'GET') {
423
+ return sendJson(response, 405, { error: 'Method not allowed' });
424
+ }
425
+ const profileName = decodeURIComponent(contentsMatch[1]);
426
+ try {
427
+ const profile = await profileService.get({ cwd: dependencies.cwd, name: profileName });
428
+ const manifestPath = path.join(dependencies.cwd, '.brainctl', 'profiles', profileName, 'manifest.yaml');
429
+ let manifest = null;
461
430
  try {
462
- const result = await profileService.use({ cwd: dependencies.cwd, name });
463
- return sendJson(response, 200, result);
431
+ const { readFile: readManifest } = await import('node:fs/promises');
432
+ const yamlMod = await import('yaml');
433
+ manifest = yamlMod.default.parse(await readManifest(manifestPath, 'utf8'));
464
434
  }
465
- catch (error) {
466
- return sendProfileError(response, error);
435
+ catch {
436
+ manifest = null;
467
437
  }
438
+ return sendJson(response, 200, { profile, manifest });
468
439
  }
440
+ catch (error) {
441
+ return sendProfileError(response, error);
442
+ }
443
+ }
444
+ const profileMatch = url.pathname.match(/^\/api\/profiles\/([^/]+)$/);
445
+ if (profileMatch) {
446
+ const name = decodeURIComponent(profileMatch[1]);
469
447
  if (request.method === 'GET') {
470
448
  try {
471
449
  const profile = await profileService.get({ cwd: dependencies.cwd, name });
@@ -548,33 +526,6 @@ async function readJsonBody(request) {
548
526
  return { ok: false };
549
527
  }
550
528
  }
551
- function parseRunRequest(url) {
552
- const skill = url.searchParams.get('skill');
553
- const inputFile = url.searchParams.get('inputFile');
554
- const primaryAgent = parseAgentName(url.searchParams.get('primaryAgent'));
555
- const fallbackAgentParam = url.searchParams.get('fallbackAgent');
556
- const fallbackAgent = fallbackAgentParam === null ? null : parseAgentName(fallbackAgentParam);
557
- if (!skill || !inputFile || !primaryAgent || fallbackAgentParam !== null && !fallbackAgent) {
558
- return null;
559
- }
560
- if (fallbackAgent !== null && fallbackAgent === primaryAgent) {
561
- return { error: 'fallbackAgent must differ from primaryAgent' };
562
- }
563
- return {
564
- request: {
565
- skill,
566
- inputFile,
567
- primaryAgent,
568
- fallbackAgent: fallbackAgent ?? undefined
569
- }
570
- };
571
- }
572
- function parseAgentName(value) {
573
- if (value === 'claude' || value === 'codex') {
574
- return value;
575
- }
576
- return null;
577
- }
578
529
  function sendProfileError(response, error) {
579
530
  if (error instanceof ProfileNotFoundError) {
580
531
  return sendJson(response, 404, { error: error.message });
@@ -1,5 +1,5 @@
1
1
  import { type Server } from 'node:http';
2
- import type { StatusService } from '../services/status-service.js';
2
+ import type { StatusService } from '../services/platform/status-service.js';
3
3
  export interface StartUiServerOptions {
4
4
  cwd?: string;
5
5
  host?: string;