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
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# 🧠 brainctl
|
|
2
|
+
|
|
3
|
+
> Stop reconfiguring your AI tools.
|
|
4
|
+
|
|
5
|
+
`brainctl` is a CLI for managing a portable AI environment across tools like Claude Code and Codex.
|
|
6
|
+
|
|
7
|
+
Define your memory, skills, and execution flow once, then reuse them across different AI agents.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Why brainctl?
|
|
12
|
+
|
|
13
|
+
If you're using multiple AI tools, you've probably already hit the same problems:
|
|
14
|
+
|
|
15
|
+
- Rewriting the same prompt for different agents
|
|
16
|
+
- Losing context between tools
|
|
17
|
+
- Rebuilding your environment every time you switch
|
|
18
|
+
|
|
19
|
+
`brainctl` solves that with one core idea:
|
|
20
|
+
|
|
21
|
+
> **One AI setup. Multiple agents.**
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚀 Features
|
|
26
|
+
|
|
27
|
+
- 🧠 File-based memory from Markdown files
|
|
28
|
+
- 🧩 Reusable skills stored in `ai-stack.yaml`
|
|
29
|
+
- 🔌 Multi-agent execution with Claude and Codex
|
|
30
|
+
- ⚙️ Unified context builder
|
|
31
|
+
- 🛠 CLI-first workflow
|
|
32
|
+
- 🔍 `status` and `doctor` for visibility
|
|
33
|
+
- 🔁 Optional fallback agent support with `--fallback`
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
38
|
+
|
|
39
|
+
### Option 1: Install from npm
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g brainctl
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
brainctl --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Option 2: Local CLI install from source
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install
|
|
55
|
+
npm run build
|
|
56
|
+
npm link
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
brainctl --help
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Option 3: Run without linking
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install
|
|
69
|
+
npm run build
|
|
70
|
+
node dist/cli.js --help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`brainctl` does not bundle agent CLIs. You still need at least one supported agent installed separately and available on `PATH`, such as `claude` or `codex`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## ⚡ Quick Start
|
|
78
|
+
|
|
79
|
+
### 1. Initialize a project
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
brainctl init
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This creates:
|
|
86
|
+
|
|
87
|
+
- `ai-stack.yaml`
|
|
88
|
+
- `memory/`
|
|
89
|
+
- `memory/notes.md`
|
|
90
|
+
|
|
91
|
+
### 2. Inspect the setup
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
brainctl status
|
|
95
|
+
brainctl doctor
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. Run a task
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
brainctl run summarize ./memory/notes.md --with claude
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
brainctl run summarize ./memory/notes.md --with codex
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
With fallback:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
brainctl run summarize ./memory/notes.md --with claude --fallback codex
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🧠 Example `ai-stack.yaml`
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
memory:
|
|
122
|
+
paths:
|
|
123
|
+
- ./memory
|
|
124
|
+
|
|
125
|
+
skills:
|
|
126
|
+
summarize:
|
|
127
|
+
description: Summarize content
|
|
128
|
+
prompt: |
|
|
129
|
+
Summarize the following content into concise bullet points.
|
|
130
|
+
|
|
131
|
+
analyze:
|
|
132
|
+
description: Analyze content deeply
|
|
133
|
+
prompt: |
|
|
134
|
+
Analyze the following content and extract key insights.
|
|
135
|
+
|
|
136
|
+
mcps: {}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 🧩 How It Works
|
|
142
|
+
|
|
143
|
+
`brainctl` builds a unified context before calling an agent:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
--- MEMORY ---
|
|
147
|
+
[your markdown files]
|
|
148
|
+
|
|
149
|
+
--- SKILL ---
|
|
150
|
+
[prompt template]
|
|
151
|
+
|
|
152
|
+
--- INPUT ---
|
|
153
|
+
[your file]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
That context is then sent to the selected agent over stdin.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 🛠 Usage
|
|
161
|
+
|
|
162
|
+
### Commands
|
|
163
|
+
|
|
164
|
+
| Command | Purpose |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `brainctl init` | Initialize `ai-stack.yaml` and memory files |
|
|
167
|
+
| `brainctl status` | Show memory, skills, MCP count, and agent availability |
|
|
168
|
+
| `brainctl doctor` | Validate config, memory paths, skills, and installed agents |
|
|
169
|
+
| `brainctl run <skill> <file> --with <agent>` | Build context and execute with an agent |
|
|
170
|
+
|
|
171
|
+
### Examples
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
brainctl run summarize ./memory/notes.md --with claude
|
|
175
|
+
brainctl run analyze ./memory/notes.md --with codex
|
|
176
|
+
brainctl run summarize ./memory/notes.md --with claude --fallback codex
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 📂 Project Structure
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
brainctl/
|
|
185
|
+
├── src/
|
|
186
|
+
│ ├── cli.ts
|
|
187
|
+
│ ├── config.ts
|
|
188
|
+
│ ├── context/
|
|
189
|
+
│ ├── commands/
|
|
190
|
+
│ ├── executor/
|
|
191
|
+
│ └── services/
|
|
192
|
+
├── tests/
|
|
193
|
+
├── ai-stack.yaml
|
|
194
|
+
├── memory/
|
|
195
|
+
├── package.json
|
|
196
|
+
└── tsconfig.json
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 🧪 Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm install
|
|
205
|
+
npm test
|
|
206
|
+
npm run build
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 🧠 Philosophy
|
|
212
|
+
|
|
213
|
+
`brainctl` does not replace your AI tools.
|
|
214
|
+
|
|
215
|
+
It sits between you and them as a thin orchestration layer:
|
|
216
|
+
|
|
217
|
+
- You keep using Claude, Codex, and other agent CLIs
|
|
218
|
+
- `brainctl` keeps the environment consistent
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 🗺 Roadmap
|
|
223
|
+
|
|
224
|
+
- [ ] JSON output mode
|
|
225
|
+
- [ ] Multi-agent pipelines
|
|
226
|
+
- [ ] MCP runtime integration
|
|
227
|
+
- [ ] Better execution tracing and logs
|
|
228
|
+
- [ ] UI / dashboard
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 💡 Inspiration
|
|
233
|
+
|
|
234
|
+
AI tools are getting more powerful, but also more fragmented.
|
|
235
|
+
|
|
236
|
+
`brainctl` is an attempt to bring state, structure, and consistency to that workflow.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 📄 License
|
|
241
|
+
|
|
242
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { type DoctorService } from './services/doctor-service.js';
|
|
4
|
+
import { type InitService } from './services/init-service.js';
|
|
5
|
+
import { type RunService } from './services/run-service.js';
|
|
6
|
+
import { type StatusService } from './services/status-service.js';
|
|
7
|
+
export interface CliServices {
|
|
8
|
+
initService: InitService;
|
|
9
|
+
runService: RunService;
|
|
10
|
+
statusService: StatusService;
|
|
11
|
+
doctorService: DoctorService;
|
|
12
|
+
}
|
|
13
|
+
export declare function createProgram(overrides?: Partial<CliServices>): Command;
|
|
14
|
+
export declare function main(argv?: string[]): Promise<void>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { registerDoctorCommand } from './commands/doctor.js';
|
|
6
|
+
import { registerInitCommand } from './commands/init.js';
|
|
7
|
+
import { registerRunCommand } from './commands/run.js';
|
|
8
|
+
import { registerStatusCommand } from './commands/status.js';
|
|
9
|
+
import { printError } from './output.js';
|
|
10
|
+
import { createDoctorService } from './services/doctor-service.js';
|
|
11
|
+
import { createInitService } from './services/init-service.js';
|
|
12
|
+
import { createRunService } from './services/run-service.js';
|
|
13
|
+
import { createStatusService } from './services/status-service.js';
|
|
14
|
+
import { createExecutorResolver } from './executor/resolver.js';
|
|
15
|
+
export function createProgram(overrides = {}) {
|
|
16
|
+
const services = createDefaultServices(overrides);
|
|
17
|
+
const program = new Command();
|
|
18
|
+
program
|
|
19
|
+
.name('brainctl')
|
|
20
|
+
.description('Manage repeatable AI environments for local agent workflows')
|
|
21
|
+
.version('0.1.0');
|
|
22
|
+
registerInitCommand(program, services.initService);
|
|
23
|
+
registerStatusCommand(program, services.statusService);
|
|
24
|
+
registerRunCommand(program, services.runService);
|
|
25
|
+
registerDoctorCommand(program, services.doctorService);
|
|
26
|
+
return program;
|
|
27
|
+
}
|
|
28
|
+
export async function main(argv = process.argv) {
|
|
29
|
+
const program = createProgram();
|
|
30
|
+
try {
|
|
31
|
+
await program.parseAsync(argv);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
printError(error);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function createDefaultServices(overrides) {
|
|
39
|
+
const resolver = createExecutorResolver();
|
|
40
|
+
return {
|
|
41
|
+
initService: createInitService(),
|
|
42
|
+
runService: createRunService({ resolver }),
|
|
43
|
+
statusService: createStatusService({ resolver }),
|
|
44
|
+
doctorService: createDoctorService({ resolver }),
|
|
45
|
+
...overrides
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const entryPointPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
49
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
50
|
+
if (entryPointPath === currentFilePath) {
|
|
51
|
+
void main();
|
|
52
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { formatDiagnosticStatus } from '../output.js';
|
|
2
|
+
export function registerDoctorCommand(program, doctorService) {
|
|
3
|
+
program
|
|
4
|
+
.command('doctor')
|
|
5
|
+
.description('Validate the local brainctl setup')
|
|
6
|
+
.action(async () => {
|
|
7
|
+
const result = await doctorService.execute({ cwd: process.cwd() });
|
|
8
|
+
for (const check of result.checks) {
|
|
9
|
+
console.log(`${formatDiagnosticStatus(check.status)} ${check.label}: ${check.message}`);
|
|
10
|
+
}
|
|
11
|
+
process.exitCode = result.hasIssues ? 1 : 0;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export function registerInitCommand(program, initService) {
|
|
3
|
+
program
|
|
4
|
+
.command('init')
|
|
5
|
+
.description('Initialize brainctl in the current directory')
|
|
6
|
+
.option('--force', 'Overwrite existing scaffolded files')
|
|
7
|
+
.action(async (options) => {
|
|
8
|
+
const result = await initService.execute({
|
|
9
|
+
cwd: process.cwd(),
|
|
10
|
+
force: options.force
|
|
11
|
+
});
|
|
12
|
+
if (result.alreadyInitialized) {
|
|
13
|
+
console.log('brainctl is already initialized in this directory');
|
|
14
|
+
console.log('Use --force to overwrite existing files.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const item of result.created) {
|
|
18
|
+
console.log(pc.green(`created ${item}`));
|
|
19
|
+
}
|
|
20
|
+
for (const item of result.replaced) {
|
|
21
|
+
console.log(pc.yellow(`replaced ${item}`));
|
|
22
|
+
}
|
|
23
|
+
if (result.created.length === 0 && result.replaced.length === 0) {
|
|
24
|
+
console.log('No changes were required.');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function registerRunCommand(program, runService) {
|
|
2
|
+
program
|
|
3
|
+
.command('run')
|
|
4
|
+
.description('Run a file through a configured skill and AI agent')
|
|
5
|
+
.argument('<skill>', 'Skill name from ai-stack.yaml')
|
|
6
|
+
.argument('<file>', 'Input file to send to the agent')
|
|
7
|
+
.requiredOption('--with <agent>', 'Primary agent to run', validateAgentName)
|
|
8
|
+
.option('--fallback <agent>', 'Fallback agent if the primary agent is unavailable', validateAgentName)
|
|
9
|
+
.action(async (skill, inputFile, options) => {
|
|
10
|
+
const trace = await runService.execute({
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
skill,
|
|
13
|
+
inputFile,
|
|
14
|
+
primaryAgent: options.with,
|
|
15
|
+
fallbackAgent: options.fallback
|
|
16
|
+
});
|
|
17
|
+
process.exitCode = trace.finalExitCode;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function validateAgentName(value) {
|
|
21
|
+
if (value !== 'claude' && value !== 'codex') {
|
|
22
|
+
throw new Error(`Unsupported agent: ${value}`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export function registerStatusCommand(program, statusService) {
|
|
3
|
+
program
|
|
4
|
+
.command('status')
|
|
5
|
+
.description('Show current brainctl configuration status')
|
|
6
|
+
.action(async () => {
|
|
7
|
+
const status = await statusService.execute({ cwd: process.cwd() });
|
|
8
|
+
console.log(pc.bold('brainctl status'));
|
|
9
|
+
console.log(`Config: ${status.configPath}`);
|
|
10
|
+
console.log(`Memory files loaded: ${status.memory.count}`);
|
|
11
|
+
console.log(`Available skills: ${status.skills.length > 0 ? status.skills.join(', ') : 'none'}`);
|
|
12
|
+
console.log(`MCP count: ${status.mcpCount}`);
|
|
13
|
+
console.log('Available agents:');
|
|
14
|
+
for (const agent of Object.values(status.agents)) {
|
|
15
|
+
console.log(`- ${agent.agent}: ${agent.available ? pc.green('available') : pc.yellow('missing')}`);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { ConfigError } from './errors.js';
|
|
5
|
+
export async function loadConfig(options = {}) {
|
|
6
|
+
const cwd = options.cwd ?? process.cwd();
|
|
7
|
+
const configPath = path.join(cwd, 'ai-stack.yaml');
|
|
8
|
+
let source;
|
|
9
|
+
try {
|
|
10
|
+
source = await readFile(configPath, 'utf8');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new ConfigError(`Could not read ai-stack.yaml in ${cwd}.`);
|
|
14
|
+
}
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = (YAML.parse(source) ?? {});
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new ConfigError('ai-stack.yaml could not be parsed.');
|
|
21
|
+
}
|
|
22
|
+
if (!parsed.memory || !Array.isArray(parsed.memory.paths)) {
|
|
23
|
+
throw new ConfigError('ai-stack.yaml is missing the required "memory.paths" section.');
|
|
24
|
+
}
|
|
25
|
+
if (!parsed.skills || typeof parsed.skills !== 'object' || Array.isArray(parsed.skills)) {
|
|
26
|
+
throw new ConfigError('ai-stack.yaml is missing the required "skills" section.');
|
|
27
|
+
}
|
|
28
|
+
const skills = normalizeSkills(parsed.skills);
|
|
29
|
+
return {
|
|
30
|
+
configPath,
|
|
31
|
+
rootDir: cwd,
|
|
32
|
+
memory: {
|
|
33
|
+
paths: parsed.memory.paths.map((memoryPath) => {
|
|
34
|
+
if (typeof memoryPath !== 'string' || memoryPath.trim().length === 0) {
|
|
35
|
+
throw new ConfigError('ai-stack.yaml contains an invalid memory path.');
|
|
36
|
+
}
|
|
37
|
+
return path.resolve(cwd, memoryPath);
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
skills,
|
|
41
|
+
mcps: normalizeMcps(parsed.mcps)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function normalizeSkills(value) {
|
|
45
|
+
const entries = Object.entries(value);
|
|
46
|
+
if (entries.length === 0) {
|
|
47
|
+
throw new ConfigError('ai-stack.yaml must define at least one skill.');
|
|
48
|
+
}
|
|
49
|
+
return Object.fromEntries(entries.map(([name, skillValue]) => {
|
|
50
|
+
if (!skillValue || typeof skillValue !== 'object' || Array.isArray(skillValue)) {
|
|
51
|
+
throw new ConfigError(`Skill "${name}" must be an object with a prompt.`);
|
|
52
|
+
}
|
|
53
|
+
const prompt = skillValue.prompt;
|
|
54
|
+
const description = skillValue.description;
|
|
55
|
+
if (typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|
56
|
+
throw new ConfigError(`Skill "${name}" is missing a valid prompt.`);
|
|
57
|
+
}
|
|
58
|
+
if (description !== undefined && typeof description !== 'string') {
|
|
59
|
+
throw new ConfigError(`Skill "${name}" has an invalid description.`);
|
|
60
|
+
}
|
|
61
|
+
return [
|
|
62
|
+
name,
|
|
63
|
+
{
|
|
64
|
+
prompt,
|
|
65
|
+
description
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
function normalizeMcps(value) {
|
|
71
|
+
if (value === null || value === undefined) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
75
|
+
throw new ConfigError('The "mcps" section must be an object when present.');
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function buildContext({ memory, skill, input }) {
|
|
2
|
+
const sections = [memory, skill, input].map((value) => value.replace(/\n+$/, ''));
|
|
3
|
+
return [
|
|
4
|
+
'--- MEMORY ---',
|
|
5
|
+
sections[0],
|
|
6
|
+
'',
|
|
7
|
+
'--- SKILL ---',
|
|
8
|
+
sections[1],
|
|
9
|
+
'',
|
|
10
|
+
'--- INPUT ---',
|
|
11
|
+
sections[2]
|
|
12
|
+
].join('\n');
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { MemoryPathError } from '../errors.js';
|
|
4
|
+
export async function loadMemory(options) {
|
|
5
|
+
const markdownFiles = (await Promise.all(options.paths.map(async (memoryPath) => collectMarkdownFiles(memoryPath))))
|
|
6
|
+
.flat()
|
|
7
|
+
.sort((left, right) => left.localeCompare(right));
|
|
8
|
+
const contents = await Promise.all(markdownFiles.map(async (filePath) => (await readFile(filePath, 'utf8')).trim()));
|
|
9
|
+
return {
|
|
10
|
+
files: markdownFiles,
|
|
11
|
+
count: markdownFiles.length,
|
|
12
|
+
content: contents.filter((entry) => entry.length > 0).join('\n\n')
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async function collectMarkdownFiles(targetPath) {
|
|
16
|
+
let targetStats;
|
|
17
|
+
try {
|
|
18
|
+
targetStats = await stat(targetPath);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new MemoryPathError(`Memory path does not exist: ${targetPath}`);
|
|
22
|
+
}
|
|
23
|
+
if (!targetStats.isDirectory()) {
|
|
24
|
+
throw new MemoryPathError(`Memory path is not a directory: ${targetPath}`);
|
|
25
|
+
}
|
|
26
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
27
|
+
const nestedResults = await Promise.all(entries.map(async (entry) => {
|
|
28
|
+
const entryPath = path.join(targetPath, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
return collectMarkdownFiles(entryPath);
|
|
31
|
+
}
|
|
32
|
+
if (entry.isFile() && path.extname(entry.name).toLowerCase() === '.md') {
|
|
33
|
+
return [entryPath];
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}));
|
|
37
|
+
return nestedResults.flat();
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SkillNotFoundError } from '../errors.js';
|
|
2
|
+
export function resolveSkillPrompt(config, skillName) {
|
|
3
|
+
const skill = config.skills[skillName];
|
|
4
|
+
if (!skill) {
|
|
5
|
+
throw new SkillNotFoundError(`Skill "${skillName}" is not defined in ai-stack.yaml.`);
|
|
6
|
+
}
|
|
7
|
+
return skill.prompt;
|
|
8
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ErrorCategory } from './types.js';
|
|
2
|
+
export declare class BrainctlError extends Error {
|
|
3
|
+
readonly category: ErrorCategory;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
constructor(message: string, category: ErrorCategory, code: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class ConfigError extends BrainctlError {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class ValidationError extends BrainctlError {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class MemoryPathError extends BrainctlError {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class SkillNotFoundError extends BrainctlError {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class InputFileError extends BrainctlError {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
export declare class AgentNotAvailableError extends BrainctlError {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
export declare class ExecutionError extends BrainctlError {
|
|
26
|
+
constructor(message: string);
|
|
27
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class BrainctlError extends Error {
|
|
2
|
+
category;
|
|
3
|
+
code;
|
|
4
|
+
constructor(message, category, code) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = new.target.name;
|
|
7
|
+
this.category = category;
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class ConfigError extends BrainctlError {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message, 'user', 'CONFIG_ERROR');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class ValidationError extends BrainctlError {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message, 'user', 'VALIDATION_ERROR');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class MemoryPathError extends BrainctlError {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message, 'user', 'MEMORY_PATH_ERROR');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class SkillNotFoundError extends BrainctlError {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message, 'user', 'SKILL_NOT_FOUND');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class InputFileError extends BrainctlError {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message, 'user', 'INPUT_FILE_ERROR');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class AgentNotAvailableError extends BrainctlError {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message, 'user', 'AGENT_NOT_AVAILABLE');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class ExecutionError extends BrainctlError {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message, 'system', 'EXECUTION_ERROR');
|
|
44
|
+
}
|
|
45
|
+
}
|