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.
- package/dist/cli.d.ts +4 -6
- package/dist/cli.js +11 -16
- package/dist/commands/profile.d.ts +4 -0
- package/dist/commands/profile.js +106 -16
- package/dist/commands/status.js +7 -7
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +85 -154
- package/dist/services/agent-asset-installer.d.ts +3 -0
- package/dist/services/agent-asset-installer.js +109 -0
- package/dist/services/agent-availability-service.d.ts +11 -0
- package/dist/services/agent-availability-service.js +32 -0
- package/dist/services/credential-redaction-service.d.ts +1 -0
- package/dist/services/credential-redaction-service.js +9 -3
- package/dist/services/doctor-service.d.ts +2 -2
- package/dist/services/doctor-service.js +7 -63
- package/dist/services/portable-profile-pack-service.d.ts +6 -0
- package/dist/services/portable-profile-pack-service.js +78 -4
- package/dist/services/profile-apply-service.d.ts +34 -0
- package/dist/services/profile-apply-service.js +102 -0
- package/dist/services/profile-export-service.d.ts +5 -1
- package/dist/services/profile-export-service.js +3 -1
- package/dist/services/profile-import-service.js +82 -127
- package/dist/services/profile-service.d.ts +3 -11
- package/dist/services/profile-service.js +57 -102
- package/dist/services/profile-snapshot-service.d.ts +12 -0
- package/dist/services/profile-snapshot-service.js +47 -0
- package/dist/services/status-service.d.ts +9 -7
- package/dist/services/status-service.js +14 -13
- package/dist/types.d.ts +2 -57
- package/dist/ui/routes.d.ts +0 -2
- package/dist/ui/routes.js +71 -120
- package/dist/web/assets/index-CGmTbSgk.js +63 -0
- package/dist/web/assets/index-EIVU5Woh.css +2 -0
- package/dist/web/brainctl-mark.svg +13 -0
- package/dist/web/index.html +2 -5
- package/package.json +2 -1
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +0 -27
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -25
- package/dist/commands/sync.d.ts +0 -3
- package/dist/commands/sync.js +0 -31
- package/dist/config.d.ts +0 -14
- package/dist/config.js +0 -96
- package/dist/context/builder.d.ts +0 -6
- package/dist/context/builder.js +0 -13
- package/dist/context/memory.d.ts +0 -5
- package/dist/context/memory.js +0 -43
- package/dist/context/skills.d.ts +0 -2
- package/dist/context/skills.js +0 -8
- package/dist/executor/claude.d.ts +0 -12
- package/dist/executor/claude.js +0 -16
- package/dist/executor/codex.d.ts +0 -12
- package/dist/executor/codex.js +0 -16
- package/dist/executor/process.d.ts +0 -11
- package/dist/executor/process.js +0 -40
- package/dist/executor/resolver.d.ts +0 -13
- package/dist/executor/resolver.js +0 -60
- package/dist/executor/types.d.ts +0 -14
- package/dist/executor/types.js +0 -1
- package/dist/services/config-write-service.d.ts +0 -12
- package/dist/services/config-write-service.js +0 -70
- package/dist/services/init-service.d.ts +0 -14
- package/dist/services/init-service.js +0 -88
- package/dist/services/memory-write-service.d.ts +0 -12
- package/dist/services/memory-write-service.js +0 -56
- package/dist/services/run-service.d.ts +0 -15
- package/dist/services/run-service.js +0 -94
- package/dist/services/sync-service.d.ts +0 -15
- package/dist/services/sync-service.js +0 -69
- package/dist/ui/streaming.d.ts +0 -3
- package/dist/ui/streaming.js +0 -16
- package/dist/web/assets/index-CuNIAQ7N.js +0 -65
- 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,
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
|
34
|
+
const isFolderSource = archiveStats.isDirectory();
|
|
35
|
+
const extractDir = isFolderSource
|
|
36
|
+
? archivePath
|
|
37
|
+
: await mkdtemp(path.join(tmpdir(), 'brainctl-import-'));
|
|
25
38
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
-
|
|
33
|
+
const names = new Set();
|
|
15
34
|
try {
|
|
16
|
-
const entries = await readdir(profilesDir);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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:
|
|
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
|
-
|
|
58
|
+
await migrateLegacyProfile(cwd, options.name);
|
|
59
|
+
const filePath = profileFile(cwd, options.name);
|
|
34
60
|
let source;
|
|
35
61
|
try {
|
|
36
|
-
source = await readFile(
|
|
62
|
+
source = await readFile(filePath, 'utf8');
|
|
37
63
|
}
|
|
38
64
|
catch {
|
|
39
|
-
throw new ProfileNotFoundError(`Profile "${options.name}" not found at ${
|
|
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
|
|
46
|
-
const
|
|
47
|
-
if (await pathExists(
|
|
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(
|
|
65
|
-
await writeFile(
|
|
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
|
-
|
|
71
|
-
|
|
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(
|
|
99
|
+
await writeFile(filePath, YAML.stringify(data), 'utf8');
|
|
83
100
|
},
|
|
84
101
|
async delete(options) {
|
|
85
102
|
const cwd = options.cwd ?? process.cwd();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
import
|
|
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
|
-
|
|
17
|
+
availabilityService?: AgentAvailabilityService;
|
|
18
|
+
profileService?: ProfileService;
|
|
17
19
|
}): StatusService;
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const [agents, profileList] = await Promise.all([
|
|
10
|
+
availabilityService.getAll(),
|
|
11
|
+
profileService.list({ cwd }),
|
|
12
|
+
]);
|
|
12
13
|
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
agents,
|
|
15
|
+
profiles: {
|
|
16
|
+
count: profileList.profiles.length,
|
|
17
|
+
names: profileList.profiles,
|
|
18
|
+
},
|
|
18
19
|
};
|
|
19
|
-
}
|
|
20
|
+
},
|
|
20
21
|
};
|
|
21
22
|
}
|