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,70 +0,0 @@
1
- import { lstat, mkdir, realpath, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import YAML from 'yaml';
4
- import { ConfigError } from '../errors.js';
5
- export function createConfigWriteService() {
6
- return {
7
- async execute(request) {
8
- const cwd = request.cwd ?? process.cwd();
9
- const configPath = path.join(cwd, 'ai-stack.yaml');
10
- const memoryPaths = await Promise.all(request.config.memory.paths.map((memoryPath) => normalizeMemoryPath(cwd, memoryPath)));
11
- const payload = {
12
- memory: {
13
- paths: memoryPaths
14
- },
15
- skills: request.config.skills,
16
- mcps: request.config.mcps
17
- };
18
- await mkdir(path.dirname(configPath), { recursive: true });
19
- await writeFile(configPath, `${YAML.stringify(payload)}`, 'utf8');
20
- return { configPath };
21
- }
22
- };
23
- }
24
- async function normalizeMemoryPath(cwd, filePath) {
25
- const workspaceRoot = await realpath(cwd);
26
- const resolvedPath = path.resolve(cwd, filePath);
27
- const realTargetPath = await resolvePathForWrite(resolvedPath);
28
- if (!isWithinDirectory(workspaceRoot, realTargetPath)) {
29
- throw new ConfigError('Memory paths must stay within the workspace root.');
30
- }
31
- const relativePath = path.relative(cwd, resolvedPath);
32
- return relativePath.length > 0 ? relativePath : '.';
33
- }
34
- async function resolvePathForWrite(targetPath) {
35
- const existingPath = await findNearestExistingPath(targetPath);
36
- const resolvedExistingPath = await realpath(existingPath);
37
- if (existingPath === targetPath) {
38
- return resolvedExistingPath;
39
- }
40
- return path.resolve(resolvedExistingPath, path.relative(existingPath, targetPath));
41
- }
42
- async function findNearestExistingPath(targetPath) {
43
- let currentPath = targetPath;
44
- while (true) {
45
- try {
46
- await lstat(currentPath);
47
- return currentPath;
48
- }
49
- catch (error) {
50
- if (!isMissingPathError(error)) {
51
- throw error;
52
- }
53
- }
54
- const parentPath = path.dirname(currentPath);
55
- if (parentPath === currentPath) {
56
- throw new ConfigError(`Could not resolve filesystem path for ${targetPath}.`);
57
- }
58
- currentPath = parentPath;
59
- }
60
- }
61
- function isWithinDirectory(parentDirectory, targetPath) {
62
- const relativePath = path.relative(parentDirectory, targetPath);
63
- if (relativePath === '') {
64
- return true;
65
- }
66
- return !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..' && !path.isAbsolute(relativePath);
67
- }
68
- function isMissingPathError(error) {
69
- return error instanceof Error && 'code' in error && error.code === 'ENOENT';
70
- }
@@ -1,14 +0,0 @@
1
- export interface InitServiceRequest {
2
- cwd?: string;
3
- force?: boolean;
4
- }
5
- export interface InitServiceResult {
6
- created: string[];
7
- replaced: string[];
8
- skipped: string[];
9
- alreadyInitialized: boolean;
10
- }
11
- export interface InitService {
12
- execute(request?: InitServiceRequest): Promise<InitServiceResult>;
13
- }
14
- export declare function createInitService(): InitService;
@@ -1,88 +0,0 @@
1
- import { mkdir, stat, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- const SAMPLE_CONFIG = `memory:
4
- paths:
5
- - ./memory
6
-
7
- skills:
8
- summarize:
9
- description: Summarize content
10
- prompt: |
11
- Summarize the following content into concise bullet points.
12
-
13
- analyze:
14
- description: Analyze content deeply
15
- prompt: |
16
- Analyze the following content and extract key insights.
17
-
18
- mcps: {}
19
- `;
20
- const SAMPLE_MEMORY = `# Team Notes
21
-
22
- - Track important project context here.
23
- - Keep prompts and references concise.
24
- `;
25
- export function createInitService() {
26
- return {
27
- async execute(request = {}) {
28
- const cwd = request.cwd ?? process.cwd();
29
- const force = request.force ?? false;
30
- const configPath = path.join(cwd, 'ai-stack.yaml');
31
- const memoryDir = path.join(cwd, 'memory');
32
- const notesPath = path.join(memoryDir, 'notes.md');
33
- const created = [];
34
- const replaced = [];
35
- const skipped = [];
36
- await writeManagedFile({
37
- targetPath: configPath,
38
- content: SAMPLE_CONFIG,
39
- force,
40
- created,
41
- replaced,
42
- skipped
43
- });
44
- const memoryDirExists = await pathExists(memoryDir);
45
- if (!memoryDirExists) {
46
- await mkdir(memoryDir, { recursive: true });
47
- created.push(memoryDir);
48
- }
49
- await writeManagedFile({
50
- targetPath: notesPath,
51
- content: SAMPLE_MEMORY,
52
- force,
53
- created,
54
- replaced,
55
- skipped
56
- });
57
- return {
58
- created,
59
- replaced,
60
- skipped,
61
- alreadyInitialized: created.length === 0 && replaced.length === 0
62
- };
63
- }
64
- };
65
- }
66
- async function writeManagedFile(options) {
67
- const exists = await pathExists(options.targetPath);
68
- if (exists && !options.force) {
69
- options.skipped.push(options.targetPath);
70
- return;
71
- }
72
- await mkdir(path.dirname(options.targetPath), { recursive: true });
73
- await writeFile(options.targetPath, options.content, 'utf8');
74
- if (exists) {
75
- options.replaced.push(options.targetPath);
76
- return;
77
- }
78
- options.created.push(options.targetPath);
79
- }
80
- async function pathExists(targetPath) {
81
- try {
82
- await stat(targetPath);
83
- return true;
84
- }
85
- catch {
86
- return false;
87
- }
88
- }
@@ -1,12 +0,0 @@
1
- export interface MemoryWriteRequest {
2
- cwd?: string;
3
- filePath: string;
4
- content: string;
5
- }
6
- export interface MemoryWriteResult {
7
- filePath: string;
8
- }
9
- export interface MemoryWriteService {
10
- execute(request: MemoryWriteRequest): Promise<MemoryWriteResult>;
11
- }
12
- export declare function createMemoryWriteService(): MemoryWriteService;
@@ -1,56 +0,0 @@
1
- import { lstat, mkdir, realpath, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { MemoryPathError } from '../errors.js';
4
- export function createMemoryWriteService() {
5
- return {
6
- async execute(request) {
7
- const cwd = request.cwd ?? process.cwd();
8
- const targetPath = path.resolve(cwd, request.filePath);
9
- const workspaceRoot = await realpath(cwd);
10
- const resolvedTargetPath = await resolvePathForWrite(targetPath);
11
- if (!isWithinDirectory(workspaceRoot, resolvedTargetPath)) {
12
- throw new MemoryPathError('Memory files must stay within the workspace root.');
13
- }
14
- await mkdir(path.dirname(targetPath), { recursive: true });
15
- await writeFile(targetPath, request.content, 'utf8');
16
- return { filePath: targetPath };
17
- }
18
- };
19
- }
20
- async function resolvePathForWrite(targetPath) {
21
- const existingPath = await findNearestExistingPath(targetPath);
22
- const resolvedExistingPath = await realpath(existingPath);
23
- if (existingPath === targetPath) {
24
- return resolvedExistingPath;
25
- }
26
- return path.resolve(resolvedExistingPath, path.relative(existingPath, targetPath));
27
- }
28
- async function findNearestExistingPath(targetPath) {
29
- let currentPath = targetPath;
30
- while (true) {
31
- try {
32
- await lstat(currentPath);
33
- return currentPath;
34
- }
35
- catch (error) {
36
- if (!isMissingPathError(error)) {
37
- throw error;
38
- }
39
- }
40
- const parentPath = path.dirname(currentPath);
41
- if (parentPath === currentPath) {
42
- throw new MemoryPathError(`Could not resolve filesystem path for ${targetPath}.`);
43
- }
44
- currentPath = parentPath;
45
- }
46
- }
47
- function isWithinDirectory(parentDirectory, targetPath) {
48
- const relativePath = path.relative(parentDirectory, targetPath);
49
- if (relativePath === '') {
50
- return true;
51
- }
52
- return !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..' && !path.isAbsolute(relativePath);
53
- }
54
- function isMissingPathError(error) {
55
- return error instanceof Error && 'code' in error && error.code === 'ENOENT';
56
- }
@@ -1,15 +0,0 @@
1
- import type { ExecutorResolver } from '../executor/resolver.js';
2
- import type { ExecutionStep, ExecutionTrace, RunRequest } from '../types.js';
3
- export interface RunService {
4
- execute(request: RunRequest, options?: RunServiceExecuteOptions): Promise<ExecutionTrace>;
5
- }
6
- export interface RunServiceExecuteOptions {
7
- onOutputChunk?: (chunk: string) => void;
8
- streamOutput?: boolean;
9
- }
10
- interface RunServiceDependencies {
11
- resolver?: ExecutorResolver;
12
- }
13
- export declare function createRunService(dependencies?: RunServiceDependencies): RunService;
14
- export declare function buildExecutionPlan(request: RunRequest): ExecutionStep[];
15
- export {};
@@ -1,94 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { AgentNotAvailableError, InputFileError } from '../errors.js';
4
- import { loadConfig } from '../config.js';
5
- import { buildContext } from '../context/builder.js';
6
- import { loadMemory } from '../context/memory.js';
7
- import { resolveSkillPrompt } from '../context/skills.js';
8
- import { createExecutorResolver } from '../executor/resolver.js';
9
- export function createRunService(dependencies = {}) {
10
- const resolver = dependencies.resolver ?? createExecutorResolver();
11
- return {
12
- async execute(request, options = {}) {
13
- const cwd = request.cwd ?? process.cwd();
14
- const config = await loadConfig({ cwd });
15
- const memory = await loadMemory({ paths: config.memory.paths });
16
- const steps = buildExecutionPlan(request);
17
- const results = [];
18
- let previousOutput;
19
- for (const [stepIndex, step] of steps.entries()) {
20
- const input = await resolveInput(step, cwd, previousOutput);
21
- const skill = resolveSkillPrompt(config, step.skill);
22
- const context = buildContext({
23
- memory: memory.content,
24
- skill,
25
- input
26
- });
27
- let executor = await resolvePrimaryExecutor(resolver, step);
28
- let fallbackUsed = false;
29
- if (executor.fallbackRequired) {
30
- fallbackUsed = true;
31
- }
32
- const result = await executor.instance.run(context, {
33
- streamOutput: options.streamOutput ?? true,
34
- onOutputChunk: options.onOutputChunk
35
- });
36
- previousOutput = result.output;
37
- results.push({
38
- stepIndex,
39
- requestedAgent: step.primaryAgent,
40
- agent: result.agent,
41
- fallbackUsed,
42
- exitCode: result.exitCode,
43
- output: result.output
44
- });
45
- }
46
- const finalResult = results.at(-1);
47
- return {
48
- steps: results,
49
- finalOutput: finalResult?.output ?? '',
50
- finalExitCode: finalResult?.exitCode ?? 0
51
- };
52
- }
53
- };
54
- }
55
- export function buildExecutionPlan(request) {
56
- return [
57
- {
58
- skill: request.skill,
59
- inputFile: request.inputFile,
60
- primaryAgent: request.primaryAgent,
61
- fallbackAgent: request.fallbackAgent,
62
- usePreviousOutput: false
63
- }
64
- ];
65
- }
66
- async function resolveInput(step, cwd, previousOutput) {
67
- if (step.usePreviousOutput && previousOutput !== undefined) {
68
- return previousOutput;
69
- }
70
- const inputPath = path.resolve(cwd, step.inputFile);
71
- try {
72
- return await readFile(inputPath, 'utf8');
73
- }
74
- catch (error) {
75
- throw new InputFileError(`Could not read input file: ${step.inputFile}`);
76
- }
77
- }
78
- async function resolvePrimaryExecutor(resolver, step) {
79
- try {
80
- return {
81
- instance: await resolver.resolveExecutor(step.primaryAgent),
82
- fallbackRequired: false
83
- };
84
- }
85
- catch (error) {
86
- if (!(error instanceof AgentNotAvailableError) || !step.fallbackAgent) {
87
- throw error;
88
- }
89
- return {
90
- instance: await resolver.resolveExecutor(step.fallbackAgent),
91
- fallbackRequired: true
92
- };
93
- }
94
- }
@@ -1,15 +0,0 @@
1
- import type { AgentName, SyncResult } from '../types.js';
2
- import type { AgentConfigWriter } from './sync/agent-writer.js';
3
- import { type ProfileService } from './profile-service.js';
4
- export interface SyncService {
5
- execute(options?: {
6
- cwd?: string;
7
- restore?: boolean;
8
- }): Promise<SyncResult>;
9
- }
10
- interface SyncServiceDependencies {
11
- profileService?: ProfileService;
12
- writers?: Partial<Record<AgentName, AgentConfigWriter>>;
13
- }
14
- export declare function createSyncService(dependencies?: SyncServiceDependencies): SyncService;
15
- export {};
@@ -1,69 +0,0 @@
1
- import { ProfileError } from '../errors.js';
2
- import { createClaudeWriter } from './sync/claude-writer.js';
3
- import { createCodexWriter } from './sync/codex-writer.js';
4
- import { createGeminiWriter } from './sync/gemini-writer.js';
5
- import { createProfileService } from './profile-service.js';
6
- export function createSyncService(dependencies = {}) {
7
- const profileService = dependencies.profileService ?? createProfileService();
8
- const defaultWriters = {
9
- claude: createClaudeWriter(),
10
- codex: createCodexWriter(),
11
- gemini: createGeminiWriter(),
12
- };
13
- const writers = { ...defaultWriters, ...dependencies.writers };
14
- return {
15
- async execute(options = {}) {
16
- const cwd = options.cwd ?? process.cwd();
17
- if (options.restore) {
18
- return restoreAll(writers, cwd);
19
- }
20
- const meta = await profileService.getMetaConfig({ cwd });
21
- if (!meta.active_profile) {
22
- throw new Error('No active profile set. Run "brainctl profile use <name>" first.');
23
- }
24
- const profile = await profileService.get({ cwd, name: meta.active_profile });
25
- const remoteMcpName = Object.entries(profile.mcps).find(([, config]) => config.kind === 'remote')?.[0];
26
- if (remoteMcpName) {
27
- throw new ProfileError(`Profile "${profile.name}" includes remote MCP "${remoteMcpName}". Remote MCP sync is not supported yet.`);
28
- }
29
- const results = [];
30
- for (const agent of meta.agents) {
31
- const writer = writers[agent];
32
- if (!writer) {
33
- continue;
34
- }
35
- const result = await writer.write({
36
- mcpServers: profile.mcps,
37
- cwd,
38
- });
39
- results.push({
40
- agent,
41
- configPath: result.configPath,
42
- backedUpTo: result.backedUpTo,
43
- mcpCount: Object.keys(profile.mcps).length,
44
- });
45
- }
46
- return results;
47
- },
48
- };
49
- }
50
- async function restoreAll(writers, cwd) {
51
- const results = [];
52
- for (const [agent, writer] of Object.entries(writers)) {
53
- if (!writer)
54
- continue;
55
- try {
56
- const { restoredFrom } = await writer.restore({ cwd });
57
- results.push({
58
- agent: agent,
59
- configPath: restoredFrom,
60
- backedUpTo: null,
61
- mcpCount: 0,
62
- });
63
- }
64
- catch {
65
- // Skip agents with no backup
66
- }
67
- }
68
- return results;
69
- }
@@ -1,3 +0,0 @@
1
- import type { ServerResponse } from 'node:http';
2
- export declare function startSseStream(response: ServerResponse): void;
3
- export declare function writeSseEvent(response: ServerResponse, event: string, data: unknown): void;
@@ -1,16 +0,0 @@
1
- export function startSseStream(response) {
2
- response.statusCode = 200;
3
- response.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
4
- response.setHeader('Cache-Control', 'no-cache, no-transform');
5
- response.setHeader('Connection', 'keep-alive');
6
- response.setHeader('X-Accel-Buffering', 'no');
7
- response.flushHeaders?.();
8
- }
9
- export function writeSseEvent(response, event, data) {
10
- response.write(`event: ${event}\n`);
11
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
12
- for (const line of payload.split(/\r?\n/)) {
13
- response.write(`data: ${line}\n`);
14
- }
15
- response.write('\n');
16
- }