brainctl 0.1.0
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/README.md +242 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +52 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +13 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +27 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +25 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +18 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +78 -0
- package/dist/context/builder.d.ts +6 -0
- package/dist/context/builder.js +13 -0
- package/dist/context/memory.d.ts +5 -0
- package/dist/context/memory.js +38 -0
- package/dist/context/skills.d.ts +2 -0
- package/dist/context/skills.js +8 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +45 -0
- package/dist/executor/claude.d.ts +5 -0
- package/dist/executor/claude.js +12 -0
- package/dist/executor/codex.d.ts +5 -0
- package/dist/executor/codex.js +12 -0
- package/dist/executor/process.d.ts +10 -0
- package/dist/executor/process.js +38 -0
- package/dist/executor/resolver.d.ts +13 -0
- package/dist/executor/resolver.js +94 -0
- package/dist/executor/types.d.ts +13 -0
- package/dist/executor/types.js +1 -0
- package/dist/output.d.ts +4 -0
- package/dist/output.js +25 -0
- package/dist/services/doctor-service.d.ts +14 -0
- package/dist/services/doctor-service.js +79 -0
- package/dist/services/init-service.d.ts +14 -0
- package/dist/services/init-service.js +88 -0
- package/dist/services/run-service.d.ts +11 -0
- package/dist/services/run-service.js +93 -0
- package/dist/services/status-service.d.ts +17 -0
- package/dist/services/status-service.js +21 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentName } from '../types.js';
|
|
2
|
+
import type { ExecutorResult, ExecutorRunOptions } from './types.js';
|
|
3
|
+
interface RunAgentProcessOptions {
|
|
4
|
+
command: string;
|
|
5
|
+
agent: AgentName;
|
|
6
|
+
context: string;
|
|
7
|
+
runOptions?: ExecutorRunOptions;
|
|
8
|
+
}
|
|
9
|
+
export declare function runAgentProcess(options: RunAgentProcessOptions): Promise<ExecutorResult>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { ExecutionError } from '../errors.js';
|
|
3
|
+
export async function runAgentProcess(options) {
|
|
4
|
+
return await new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(options.command, [], {
|
|
6
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
7
|
+
});
|
|
8
|
+
let output = '';
|
|
9
|
+
child.stdout.on('data', (chunk) => {
|
|
10
|
+
const text = chunk.toString();
|
|
11
|
+
output += text;
|
|
12
|
+
if (options.runOptions?.streamOutput !== false) {
|
|
13
|
+
process.stdout.write(chunk);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
child.stderr.on('data', (chunk) => {
|
|
17
|
+
const text = chunk.toString();
|
|
18
|
+
output += text;
|
|
19
|
+
if (options.runOptions?.streamOutput !== false) {
|
|
20
|
+
process.stderr.write(chunk);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
child.on('error', (error) => {
|
|
24
|
+
reject(new ExecutionError(`Failed to start ${options.agent}: ${error.message}`));
|
|
25
|
+
});
|
|
26
|
+
child.on('close', (code) => {
|
|
27
|
+
resolve({
|
|
28
|
+
output,
|
|
29
|
+
exitCode: code ?? 1,
|
|
30
|
+
agent: options.agent
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
child.stdin.on('error', () => {
|
|
34
|
+
// Ignore broken-pipe behavior if the child exits before stdin completes.
|
|
35
|
+
});
|
|
36
|
+
child.stdin.end(options.context);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentName } from '../types.js';
|
|
2
|
+
import type { Executor } from './types.js';
|
|
3
|
+
export interface AgentAvailability {
|
|
4
|
+
agent: AgentName;
|
|
5
|
+
available: boolean;
|
|
6
|
+
command: string;
|
|
7
|
+
resolvedPath?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ExecutorResolver {
|
|
10
|
+
resolveExecutor(agentName: AgentName): Promise<Executor>;
|
|
11
|
+
getAgentAvailability(): Promise<Record<AgentName, AgentAvailability>>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createExecutorResolver(): ExecutorResolver;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { AgentNotAvailableError } from '../errors.js';
|
|
5
|
+
import { ClaudeExecutor } from './claude.js';
|
|
6
|
+
import { CodexExecutor } from './codex.js';
|
|
7
|
+
const SUPPORTED_AGENTS = ['claude', 'codex'];
|
|
8
|
+
const AGENT_COMMANDS = {
|
|
9
|
+
claude: 'claude',
|
|
10
|
+
codex: 'codex'
|
|
11
|
+
};
|
|
12
|
+
class DefaultExecutorResolver {
|
|
13
|
+
availabilityCache = new Map();
|
|
14
|
+
executorCache = new Map();
|
|
15
|
+
async resolveExecutor(agentName) {
|
|
16
|
+
const availability = await this.getAvailabilityForAgent(agentName);
|
|
17
|
+
if (!availability.available) {
|
|
18
|
+
throw new AgentNotAvailableError(`Agent "${agentName}" is not available on PATH.`);
|
|
19
|
+
}
|
|
20
|
+
if (!this.executorCache.has(agentName)) {
|
|
21
|
+
this.executorCache.set(agentName, createExecutor(agentName));
|
|
22
|
+
}
|
|
23
|
+
return this.executorCache.get(agentName);
|
|
24
|
+
}
|
|
25
|
+
async getAgentAvailability() {
|
|
26
|
+
const checks = await Promise.all(SUPPORTED_AGENTS.map(async (agentName) => [
|
|
27
|
+
agentName,
|
|
28
|
+
await this.getAvailabilityForAgent(agentName)
|
|
29
|
+
]));
|
|
30
|
+
return Object.fromEntries(checks);
|
|
31
|
+
}
|
|
32
|
+
async getAvailabilityForAgent(agentName) {
|
|
33
|
+
if (!this.availabilityCache.has(agentName)) {
|
|
34
|
+
this.availabilityCache.set(agentName, checkAvailability(agentName));
|
|
35
|
+
}
|
|
36
|
+
return await this.availabilityCache.get(agentName);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function createExecutorResolver() {
|
|
40
|
+
return new DefaultExecutorResolver();
|
|
41
|
+
}
|
|
42
|
+
function createExecutor(agentName) {
|
|
43
|
+
switch (agentName) {
|
|
44
|
+
case 'claude':
|
|
45
|
+
return new ClaudeExecutor();
|
|
46
|
+
case 'codex':
|
|
47
|
+
return new CodexExecutor();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function checkAvailability(agentName) {
|
|
51
|
+
const command = AGENT_COMMANDS[agentName];
|
|
52
|
+
const resolvedPath = await findExecutable(command);
|
|
53
|
+
return {
|
|
54
|
+
agent: agentName,
|
|
55
|
+
command,
|
|
56
|
+
available: resolvedPath !== null,
|
|
57
|
+
resolvedPath: resolvedPath ?? undefined
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function findExecutable(command) {
|
|
61
|
+
if (command.includes(path.sep)) {
|
|
62
|
+
return (await isExecutable(command)) ? command : null;
|
|
63
|
+
}
|
|
64
|
+
const pathEntries = (process.env.PATH ?? '')
|
|
65
|
+
.split(path.delimiter)
|
|
66
|
+
.filter((entry) => entry.length > 0);
|
|
67
|
+
const extensions = process.platform === 'win32'
|
|
68
|
+
? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM')
|
|
69
|
+
.split(';')
|
|
70
|
+
.filter((entry) => entry.length > 0)
|
|
71
|
+
: [''];
|
|
72
|
+
for (const pathEntry of pathEntries) {
|
|
73
|
+
for (const extension of extensions) {
|
|
74
|
+
const candidate = process.platform === 'win32' &&
|
|
75
|
+
extension.length > 0 &&
|
|
76
|
+
!command.toLowerCase().endsWith(extension.toLowerCase())
|
|
77
|
+
? path.join(pathEntry, `${command}${extension}`)
|
|
78
|
+
: path.join(pathEntry, command);
|
|
79
|
+
if (await isExecutable(candidate)) {
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
async function isExecutable(filePath) {
|
|
87
|
+
try {
|
|
88
|
+
await access(filePath, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentName } from '../types.js';
|
|
2
|
+
export interface ExecutorRunOptions {
|
|
3
|
+
streamOutput?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface ExecutorResult {
|
|
6
|
+
output: string;
|
|
7
|
+
exitCode: number;
|
|
8
|
+
agent: AgentName;
|
|
9
|
+
}
|
|
10
|
+
export interface Executor {
|
|
11
|
+
readonly agent: AgentName;
|
|
12
|
+
run(context: string, options?: ExecutorRunOptions): Promise<ExecutorResult>;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/output.d.ts
ADDED
package/dist/output.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { BrainctlError } from './errors.js';
|
|
3
|
+
export function printError(error) {
|
|
4
|
+
console.error(formatError(error));
|
|
5
|
+
}
|
|
6
|
+
export function formatError(error) {
|
|
7
|
+
if (error instanceof BrainctlError) {
|
|
8
|
+
const prefix = error.category === 'user' ? 'Error' : 'System error';
|
|
9
|
+
return pc.red(`${prefix}: ${error.message}`);
|
|
10
|
+
}
|
|
11
|
+
if (error instanceof Error) {
|
|
12
|
+
return pc.red(`System error: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
return pc.red('System error: An unknown failure occurred.');
|
|
15
|
+
}
|
|
16
|
+
export function formatDiagnosticStatus(status) {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'ok':
|
|
19
|
+
return pc.green('OK');
|
|
20
|
+
case 'warn':
|
|
21
|
+
return pc.yellow('WARN');
|
|
22
|
+
case 'error':
|
|
23
|
+
return pc.red('ERROR');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExecutorResolver } from '../executor/resolver.js';
|
|
2
|
+
import type { DiagnosticCheck } from '../types.js';
|
|
3
|
+
export interface DoctorResult {
|
|
4
|
+
checks: DiagnosticCheck[];
|
|
5
|
+
hasIssues: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface DoctorService {
|
|
8
|
+
execute(options?: {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
}): Promise<DoctorResult>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createDoctorService(dependencies?: {
|
|
13
|
+
resolver?: ExecutorResolver;
|
|
14
|
+
}): DoctorService;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { createExecutorResolver } from '../executor/resolver.js';
|
|
5
|
+
export function createDoctorService(dependencies = {}) {
|
|
6
|
+
const resolver = dependencies.resolver ?? createExecutorResolver();
|
|
7
|
+
return {
|
|
8
|
+
async execute(options = {}) {
|
|
9
|
+
const cwd = options.cwd ?? process.cwd();
|
|
10
|
+
const checks = [];
|
|
11
|
+
const configPath = path.join(cwd, 'ai-stack.yaml');
|
|
12
|
+
const configExists = await pathExists(configPath);
|
|
13
|
+
if (!configExists) {
|
|
14
|
+
checks.push({
|
|
15
|
+
label: 'Config',
|
|
16
|
+
status: 'error',
|
|
17
|
+
message: 'ai-stack.yaml was not found.'
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (configExists) {
|
|
21
|
+
try {
|
|
22
|
+
const config = await loadConfig({ cwd });
|
|
23
|
+
checks.push({
|
|
24
|
+
label: 'Config',
|
|
25
|
+
status: 'ok',
|
|
26
|
+
message: `Loaded ${config.configPath}`
|
|
27
|
+
});
|
|
28
|
+
for (const memoryPath of config.memory.paths) {
|
|
29
|
+
const exists = await pathExists(memoryPath);
|
|
30
|
+
checks.push({
|
|
31
|
+
label: 'Memory',
|
|
32
|
+
status: exists ? 'ok' : 'error',
|
|
33
|
+
message: exists
|
|
34
|
+
? `Memory path is available: ${memoryPath}`
|
|
35
|
+
: `Memory path is missing: ${memoryPath}`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
checks.push({
|
|
39
|
+
label: 'Skills',
|
|
40
|
+
status: Object.keys(config.skills).length > 0 ? 'ok' : 'error',
|
|
41
|
+
message: Object.keys(config.skills).length > 0
|
|
42
|
+
? `${Object.keys(config.skills).length} skills configured`
|
|
43
|
+
: 'No skills are configured.'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
checks.push({
|
|
48
|
+
label: 'Config',
|
|
49
|
+
status: 'error',
|
|
50
|
+
message: error instanceof Error ? error.message : 'Config validation failed.'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const availability = await resolver.getAgentAvailability();
|
|
55
|
+
for (const agent of Object.values(availability)) {
|
|
56
|
+
checks.push({
|
|
57
|
+
label: 'Agent',
|
|
58
|
+
status: agent.available ? 'ok' : 'warn',
|
|
59
|
+
message: agent.available
|
|
60
|
+
? `${agent.agent} is available`
|
|
61
|
+
: `${agent.agent} is not available on PATH`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
checks,
|
|
66
|
+
hasIssues: checks.some((check) => check.status !== 'ok')
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function pathExists(targetPath) {
|
|
72
|
+
try {
|
|
73
|
+
await stat(targetPath);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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;
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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): Promise<ExecutionTrace>;
|
|
5
|
+
}
|
|
6
|
+
interface RunServiceDependencies {
|
|
7
|
+
resolver?: ExecutorResolver;
|
|
8
|
+
}
|
|
9
|
+
export declare function createRunService(dependencies?: RunServiceDependencies): RunService;
|
|
10
|
+
export declare function buildExecutionPlan(request: RunRequest): ExecutionStep[];
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
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) {
|
|
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: true
|
|
34
|
+
});
|
|
35
|
+
previousOutput = result.output;
|
|
36
|
+
results.push({
|
|
37
|
+
stepIndex,
|
|
38
|
+
requestedAgent: step.primaryAgent,
|
|
39
|
+
agent: result.agent,
|
|
40
|
+
fallbackUsed,
|
|
41
|
+
exitCode: result.exitCode,
|
|
42
|
+
output: result.output
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const finalResult = results.at(-1);
|
|
46
|
+
return {
|
|
47
|
+
steps: results,
|
|
48
|
+
finalOutput: finalResult?.output ?? '',
|
|
49
|
+
finalExitCode: finalResult?.exitCode ?? 0
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function buildExecutionPlan(request) {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
skill: request.skill,
|
|
58
|
+
inputFile: request.inputFile,
|
|
59
|
+
primaryAgent: request.primaryAgent,
|
|
60
|
+
fallbackAgent: request.fallbackAgent,
|
|
61
|
+
usePreviousOutput: false
|
|
62
|
+
}
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
async function resolveInput(step, cwd, previousOutput) {
|
|
66
|
+
if (step.usePreviousOutput && previousOutput !== undefined) {
|
|
67
|
+
return previousOutput;
|
|
68
|
+
}
|
|
69
|
+
const inputPath = path.resolve(cwd, step.inputFile);
|
|
70
|
+
try {
|
|
71
|
+
return await readFile(inputPath, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new InputFileError(`Could not read input file: ${step.inputFile}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function resolvePrimaryExecutor(resolver, step) {
|
|
78
|
+
try {
|
|
79
|
+
return {
|
|
80
|
+
instance: await resolver.resolveExecutor(step.primaryAgent),
|
|
81
|
+
fallbackRequired: false
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (!(error instanceof AgentNotAvailableError) || !step.fallbackAgent) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
instance: await resolver.resolveExecutor(step.fallbackAgent),
|
|
90
|
+
fallbackRequired: true
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentAvailability, ExecutorResolver } from '../executor/resolver.js';
|
|
2
|
+
import type { AgentName, MemoryLoadResult } from '../types.js';
|
|
3
|
+
export interface StatusResult {
|
|
4
|
+
configPath: string;
|
|
5
|
+
memory: MemoryLoadResult;
|
|
6
|
+
skills: string[];
|
|
7
|
+
mcpCount: number;
|
|
8
|
+
agents: Record<AgentName, AgentAvailability>;
|
|
9
|
+
}
|
|
10
|
+
export interface StatusService {
|
|
11
|
+
execute(options?: {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}): Promise<StatusResult>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createStatusService(dependencies?: {
|
|
16
|
+
resolver?: ExecutorResolver;
|
|
17
|
+
}): StatusService;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadConfig } from '../config.js';
|
|
2
|
+
import { loadMemory } from '../context/memory.js';
|
|
3
|
+
import { createExecutorResolver } from '../executor/resolver.js';
|
|
4
|
+
export function createStatusService(dependencies = {}) {
|
|
5
|
+
const resolver = dependencies.resolver ?? createExecutorResolver();
|
|
6
|
+
return {
|
|
7
|
+
async execute(options = {}) {
|
|
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();
|
|
12
|
+
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
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type AgentName = 'claude' | 'codex';
|
|
2
|
+
export type ErrorCategory = 'user' | 'system';
|
|
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
|
+
}
|
|
22
|
+
export interface RunRequest {
|
|
23
|
+
cwd?: string;
|
|
24
|
+
skill: string;
|
|
25
|
+
inputFile: string;
|
|
26
|
+
primaryAgent: AgentName;
|
|
27
|
+
fallbackAgent?: AgentName;
|
|
28
|
+
}
|
|
29
|
+
export interface ExecutionStep {
|
|
30
|
+
skill: string;
|
|
31
|
+
inputFile: string;
|
|
32
|
+
primaryAgent: AgentName;
|
|
33
|
+
fallbackAgent?: AgentName;
|
|
34
|
+
usePreviousOutput?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ExecutionStepResult {
|
|
37
|
+
stepIndex: number;
|
|
38
|
+
requestedAgent: AgentName;
|
|
39
|
+
agent: AgentName;
|
|
40
|
+
fallbackUsed: boolean;
|
|
41
|
+
exitCode: number;
|
|
42
|
+
output: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ExecutionTrace {
|
|
45
|
+
steps: ExecutionStepResult[];
|
|
46
|
+
finalOutput: string;
|
|
47
|
+
finalExitCode: number;
|
|
48
|
+
}
|
|
49
|
+
export interface DiagnosticCheck {
|
|
50
|
+
label: string;
|
|
51
|
+
status: DiagnosticStatus;
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "brainctl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI environment manager for consistent AI workflows",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Rorogogogo/brainctl.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Rorogogogo/brainctl#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Rorogogogo/brainctl/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"brainctl": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.18.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"dev": "tsx src/cli.ts",
|
|
26
|
+
"prepublishOnly": "npm test && npm run build",
|
|
27
|
+
"test": "vitest run"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"cli",
|
|
31
|
+
"ai",
|
|
32
|
+
"developer-tools"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^14.0.1",
|
|
37
|
+
"picocolors": "^1.1.1",
|
|
38
|
+
"yaml": "^2.8.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^24.6.1",
|
|
42
|
+
"tsx": "^4.20.6",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^3.2.4"
|
|
45
|
+
}
|
|
46
|
+
}
|