brainctl 0.1.16 → 0.1.18

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 (74) hide show
  1. package/dist/cli.d.ts +4 -6
  2. package/dist/cli.js +11 -16
  3. package/dist/commands/profile.d.ts +4 -0
  4. package/dist/commands/profile.js +106 -16
  5. package/dist/commands/status.js +7 -7
  6. package/dist/mcp/server.d.ts +5 -0
  7. package/dist/mcp/server.js +85 -154
  8. package/dist/services/agent-asset-installer.d.ts +3 -0
  9. package/dist/services/agent-asset-installer.js +109 -0
  10. package/dist/services/agent-availability-service.d.ts +11 -0
  11. package/dist/services/agent-availability-service.js +32 -0
  12. package/dist/services/credential-redaction-service.d.ts +1 -0
  13. package/dist/services/credential-redaction-service.js +9 -3
  14. package/dist/services/doctor-service.d.ts +2 -2
  15. package/dist/services/doctor-service.js +7 -63
  16. package/dist/services/portable-profile-pack-service.d.ts +6 -0
  17. package/dist/services/portable-profile-pack-service.js +78 -4
  18. package/dist/services/profile-apply-service.d.ts +34 -0
  19. package/dist/services/profile-apply-service.js +102 -0
  20. package/dist/services/profile-export-service.d.ts +5 -1
  21. package/dist/services/profile-export-service.js +3 -1
  22. package/dist/services/profile-import-service.js +82 -127
  23. package/dist/services/profile-service.d.ts +3 -11
  24. package/dist/services/profile-service.js +57 -102
  25. package/dist/services/profile-snapshot-service.d.ts +12 -0
  26. package/dist/services/profile-snapshot-service.js +47 -0
  27. package/dist/services/status-service.d.ts +9 -7
  28. package/dist/services/status-service.js +14 -13
  29. package/dist/types.d.ts +2 -57
  30. package/dist/ui/routes.d.ts +0 -2
  31. package/dist/ui/routes.js +71 -120
  32. package/dist/web/assets/index-CGmTbSgk.js +63 -0
  33. package/dist/web/assets/index-EIVU5Woh.css +2 -0
  34. package/dist/web/brainctl-mark.svg +13 -0
  35. package/dist/web/index.html +2 -5
  36. package/package.json +2 -1
  37. package/dist/commands/init.d.ts +0 -3
  38. package/dist/commands/init.js +0 -27
  39. package/dist/commands/run.d.ts +0 -3
  40. package/dist/commands/run.js +0 -25
  41. package/dist/commands/sync.d.ts +0 -3
  42. package/dist/commands/sync.js +0 -31
  43. package/dist/config.d.ts +0 -14
  44. package/dist/config.js +0 -96
  45. package/dist/context/builder.d.ts +0 -6
  46. package/dist/context/builder.js +0 -13
  47. package/dist/context/memory.d.ts +0 -5
  48. package/dist/context/memory.js +0 -43
  49. package/dist/context/skills.d.ts +0 -2
  50. package/dist/context/skills.js +0 -8
  51. package/dist/executor/claude.d.ts +0 -12
  52. package/dist/executor/claude.js +0 -16
  53. package/dist/executor/codex.d.ts +0 -12
  54. package/dist/executor/codex.js +0 -16
  55. package/dist/executor/process.d.ts +0 -11
  56. package/dist/executor/process.js +0 -40
  57. package/dist/executor/resolver.d.ts +0 -13
  58. package/dist/executor/resolver.js +0 -60
  59. package/dist/executor/types.d.ts +0 -14
  60. package/dist/executor/types.js +0 -1
  61. package/dist/services/config-write-service.d.ts +0 -12
  62. package/dist/services/config-write-service.js +0 -70
  63. package/dist/services/init-service.d.ts +0 -14
  64. package/dist/services/init-service.js +0 -88
  65. package/dist/services/memory-write-service.d.ts +0 -12
  66. package/dist/services/memory-write-service.js +0 -56
  67. package/dist/services/run-service.d.ts +0 -15
  68. package/dist/services/run-service.js +0 -94
  69. package/dist/services/sync-service.d.ts +0 -15
  70. package/dist/services/sync-service.js +0 -69
  71. package/dist/ui/streaming.d.ts +0 -3
  72. package/dist/ui/streaming.js +0 -16
  73. package/dist/web/assets/index-CuNIAQ7N.js +0 -65
  74. package/dist/web/assets/index-Ow6x3bQk.css +0 -2
@@ -1,31 +1,46 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { copyFile, cp, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
- import { homedir, tmpdir } from 'node:os';
2
+ import { copyFile, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { 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';
7
+ import { installPlugin, installUserSkill } from './agent-asset-installer.js';
8
8
  import { resolvePortableMcpCredentials } from './credential-resolution-service.js';
9
9
  import { createMcpPreflightService } from './mcp-preflight-service.js';
10
10
  import { parseProfile } from './profile-service.js';
11
11
  const PROFILES_DIR = '.brainctl/profiles';
12
+ async function pathExists(p) {
13
+ try {
14
+ await stat(p);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
12
21
  export function createProfileImportService(deps = {}) {
13
22
  const mcpPreflightService = deps.mcpPreflightService ?? createMcpPreflightService();
14
23
  return {
15
24
  async execute(options) {
16
25
  const cwd = options.cwd ?? process.cwd();
17
26
  const archivePath = path.resolve(cwd, options.archivePath);
27
+ let archiveStats;
18
28
  try {
19
- await stat(archivePath);
29
+ archiveStats = await stat(archivePath);
20
30
  }
21
31
  catch {
22
32
  throw new ProfileError(`Archive not found: ${archivePath}`);
23
33
  }
24
- const extractDir = await mkdtemp(path.join(tmpdir(), 'brainctl-import-'));
34
+ const isFolderSource = archiveStats.isDirectory();
35
+ const extractDir = isFolderSource
36
+ ? archivePath
37
+ : await mkdtemp(path.join(tmpdir(), 'brainctl-import-'));
25
38
  try {
26
- execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
27
- stdio: 'pipe',
28
- });
39
+ if (!isFolderSource) {
40
+ execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
41
+ stdio: 'pipe',
42
+ });
43
+ }
29
44
  const manifest = await readPortableManifest(extractDir);
30
45
  const profileSource = await readFile(path.join(extractDir, 'profile.yaml'), 'utf8');
31
46
  const profile = parseProfile(profileSource, 'imported');
@@ -33,21 +48,26 @@ export function createProfileImportService(deps = {}) {
33
48
  if (manifest.profileName !== profileName) {
34
49
  throw new ProfileError(`Portable profile manifest name "${manifest.profileName}" does not match profile name "${profileName}".`);
35
50
  }
36
- const profilePath = path.join(cwd, PROFILES_DIR, `${profileName}.yaml`);
51
+ const profileFolder = path.join(cwd, PROFILES_DIR, profileName);
52
+ const profilePath = path.join(profileFolder, 'profile.yaml');
53
+ const legacyProfilePath = path.join(cwd, PROFILES_DIR, `${profileName}.yaml`);
37
54
  if (!options.force) {
38
- try {
39
- await stat(profilePath);
55
+ if ((await pathExists(profilePath)) || (await pathExists(legacyProfilePath))) {
40
56
  throw new ProfileError(`Profile "${profileName}" already exists. Use --force to overwrite.`);
41
57
  }
42
- catch (err) {
43
- if (err instanceof ProfileError)
44
- throw err;
58
+ }
59
+ else {
60
+ // clean up legacy single-file profile so it doesn't shadow the new layout
61
+ if (await pathExists(legacyProfilePath)) {
62
+ await rm(legacyProfilePath, { force: true });
45
63
  }
46
64
  }
65
+ const dotEnvCreds = await readDotEnvCredentials(extractDir);
66
+ const combinedCreds = { ...dotEnvCreds, ...(options.credentials ?? {}) };
47
67
  const missingCredentials = new Map();
48
68
  for (const [name, mcp] of Object.entries(profile.mcps)) {
49
69
  const resolution = resolvePortableMcpCredentials(mcp, {
50
- credentials: options.credentials,
70
+ credentials: combinedCreds,
51
71
  credentialSpecs: manifest.credentials,
52
72
  environment: process.env,
53
73
  });
@@ -102,27 +122,44 @@ export function createProfileImportService(deps = {}) {
102
122
  await validateImportedMcps(profile, cwd, mcpPreflightService);
103
123
  const installedPlugins = [];
104
124
  for (const plugin of manifest.plugins ?? []) {
105
- await restorePlugin(extractDir, plugin);
125
+ const sourceDir = resolveBundledArchivePath(extractDir, plugin.archivePath);
126
+ const profileLocalDir = path.join(cwd, PROFILES_DIR, profileName, plugin.archivePath);
127
+ await rm(profileLocalDir, { recursive: true, force: true });
128
+ await mkdir(path.dirname(profileLocalDir), { recursive: true });
129
+ await cp(sourceDir, profileLocalDir, { recursive: true });
130
+ await installPlugin(profileLocalDir, plugin);
106
131
  installedPlugins.push(`${plugin.agent}:${plugin.name}`);
107
132
  }
108
133
  const installedUserSkills = [];
109
134
  for (const skill of manifest.userSkills ?? []) {
110
- await restoreUserSkill(extractDir, skill);
135
+ const sourceDir = resolveBundledArchivePath(extractDir, skill.archivePath);
136
+ const profileLocalDir = path.join(cwd, PROFILES_DIR, profileName, skill.archivePath);
137
+ await rm(profileLocalDir, { recursive: true, force: true });
138
+ await mkdir(path.dirname(profileLocalDir), { recursive: true });
139
+ await cp(sourceDir, profileLocalDir, { recursive: true });
140
+ await installUserSkill(profileLocalDir, skill);
111
141
  installedUserSkills.push(`${skill.agent}:${skill.name}`);
112
142
  }
143
+ // retain manifest in profile folder so sync can reapply assets
144
+ try {
145
+ await copyFile(path.join(extractDir, 'manifest.yaml'), path.join(cwd, PROFILES_DIR, profileName, 'manifest.yaml'));
146
+ }
147
+ catch {
148
+ // best-effort
149
+ }
113
150
  const outputYaml = {
114
151
  name: profile.name,
115
152
  ...(profile.description ? { description: profile.description } : {}),
116
- skills: profile.skills,
117
153
  mcps: profile.mcps,
118
- memory: profile.memory,
119
154
  };
120
155
  await mkdir(path.dirname(profilePath), { recursive: true });
121
156
  await writeFile(profilePath, YAML.stringify(outputYaml), 'utf8');
122
157
  return { profileName, installedMcps, installedPlugins, installedUserSkills };
123
158
  }
124
159
  finally {
125
- await rm(extractDir, { recursive: true, force: true });
160
+ if (!isFolderSource) {
161
+ await rm(extractDir, { recursive: true, force: true });
162
+ }
126
163
  }
127
164
  },
128
165
  };
@@ -187,6 +224,31 @@ function formatExecError(error) {
187
224
  }
188
225
  return 'Unknown install error.';
189
226
  }
227
+ async function readDotEnvCredentials(extractDir) {
228
+ try {
229
+ const content = await readFile(path.join(extractDir, '.env'), 'utf8');
230
+ const out = {};
231
+ for (const rawLine of content.split(/\r?\n/)) {
232
+ const line = rawLine.trim();
233
+ if (!line || line.startsWith('#'))
234
+ continue;
235
+ const eq = line.indexOf('=');
236
+ if (eq <= 0)
237
+ continue;
238
+ const key = line.slice(0, eq).trim();
239
+ let value = line.slice(eq + 1).trim();
240
+ if ((value.startsWith('"') && value.endsWith('"')) ||
241
+ (value.startsWith("'") && value.endsWith("'"))) {
242
+ value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
243
+ }
244
+ out[key.toLowerCase()] = value;
245
+ }
246
+ return out;
247
+ }
248
+ catch {
249
+ return {};
250
+ }
251
+ }
190
252
  async function readPortableManifest(extractDir) {
191
253
  let source;
192
254
  try {
@@ -227,110 +289,3 @@ function resolveBundledArchivePath(extractDir, bundlePath) {
227
289
  }
228
290
  return resolved;
229
291
  }
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
- }
@@ -1,10 +1,11 @@
1
- import type { BrainctlMetaConfig, ProfileConfig } from '../types.js';
1
+ import type { ProfileConfig } from '../types.js';
2
+ export declare function profileDir(cwd: string, name: string): string;
3
+ export declare function profileFile(cwd: string, name: string): string;
2
4
  export interface ProfileService {
3
5
  list(options?: {
4
6
  cwd?: string;
5
7
  }): Promise<{
6
8
  profiles: string[];
7
- activeProfile: string | null;
8
9
  }>;
9
10
  get(options: {
10
11
  cwd?: string;
@@ -26,15 +27,6 @@ export interface ProfileService {
26
27
  cwd?: string;
27
28
  name: string;
28
29
  }): Promise<void>;
29
- use(options: {
30
- cwd?: string;
31
- name: string;
32
- }): Promise<{
33
- previousProfile: string | null;
34
- }>;
35
- getMetaConfig(options?: {
36
- cwd?: string;
37
- }): Promise<BrainctlMetaConfig>;
38
30
  }
39
31
  export declare function createProfileService(): ProfileService;
40
32
  export declare function parseProfile(source: string, name: string): ProfileConfig;
@@ -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
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
+ }
@@ -1,11 +1,12 @@
1
- import type { AgentAvailability, ExecutorResolver } from '../executor/resolver.js';
2
- import type { AgentName, MemoryLoadResult } from '../types.js';
1
+ import { type AgentAvailability, type AgentAvailabilityService } from './agent-availability-service.js';
2
+ import { type ProfileService } from './profile-service.js';
3
+ import type { AgentName } from '../types.js';
3
4
  export interface StatusResult {
4
- configPath: string;
5
- memory: MemoryLoadResult;
6
- skills: string[];
7
- mcpCount: number;
8
5
  agents: Record<AgentName, AgentAvailability>;
6
+ profiles: {
7
+ count: number;
8
+ names: string[];
9
+ };
9
10
  }
10
11
  export interface StatusService {
11
12
  execute(options?: {
@@ -13,5 +14,6 @@ export interface StatusService {
13
14
  }): Promise<StatusResult>;
14
15
  }
15
16
  export declare function createStatusService(dependencies?: {
16
- resolver?: ExecutorResolver;
17
+ availabilityService?: AgentAvailabilityService;
18
+ profileService?: ProfileService;
17
19
  }): StatusService;
@@ -1,21 +1,22 @@
1
- import { loadConfig } from '../config.js';
2
- import { loadMemory } from '../context/memory.js';
3
- import { createExecutorResolver } from '../executor/resolver.js';
1
+ import { createAgentAvailabilityService, } from './agent-availability-service.js';
2
+ import { createProfileService } from './profile-service.js';
4
3
  export function createStatusService(dependencies = {}) {
5
- const resolver = dependencies.resolver ?? createExecutorResolver();
4
+ const availabilityService = dependencies.availabilityService ?? createAgentAvailabilityService();
5
+ const profileService = dependencies.profileService ?? createProfileService();
6
6
  return {
7
7
  async execute(options = {}) {
8
8
  const cwd = options.cwd ?? process.cwd();
9
- const config = await loadConfig({ cwd });
10
- const memory = await loadMemory({ paths: config.memory.paths });
11
- const agents = await resolver.getAgentAvailability();
9
+ const [agents, profileList] = await Promise.all([
10
+ availabilityService.getAll(),
11
+ profileService.list({ cwd }),
12
+ ]);
12
13
  return {
13
- configPath: config.configPath,
14
- memory,
15
- skills: Object.keys(config.skills).sort((left, right) => left.localeCompare(right)),
16
- mcpCount: Object.keys(config.mcps).length,
17
- agents
14
+ agents,
15
+ profiles: {
16
+ count: profileList.profiles.length,
17
+ names: profileList.profiles,
18
+ },
18
19
  };
19
- }
20
+ },
20
21
  };
21
22
  }