@brainjar/cli 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/LICENSE +21 -0
- package/README.md +309 -0
- package/package.json +55 -0
- package/src/cli.ts +30 -0
- package/src/commands/brain.ts +256 -0
- package/src/commands/compose.ts +156 -0
- package/src/commands/identity.ts +276 -0
- package/src/commands/init.ts +78 -0
- package/src/commands/persona.ts +259 -0
- package/src/commands/reset.ts +46 -0
- package/src/commands/rules.ts +269 -0
- package/src/commands/shell.ts +119 -0
- package/src/commands/soul.ts +207 -0
- package/src/commands/status.ts +131 -0
- package/src/engines/bitwarden.ts +105 -0
- package/src/engines/index.ts +12 -0
- package/src/engines/types.ts +12 -0
- package/src/paths.ts +48 -0
- package/src/seeds/personas/engineer.md +26 -0
- package/src/seeds/personas/planner.md +24 -0
- package/src/seeds/personas/reviewer.md +27 -0
- package/src/seeds/rules/default/boundaries.md +25 -0
- package/src/seeds/rules/default/context-recovery.md +17 -0
- package/src/seeds/rules/default/task-completion.md +31 -0
- package/src/seeds/rules/git-discipline.md +22 -0
- package/src/seeds/rules/security.md +26 -0
- package/src/seeds/souls/craftsman.md +24 -0
- package/src/seeds/templates/persona.md +19 -0
- package/src/seeds/templates/rule.md +11 -0
- package/src/seeds/templates/soul.md +20 -0
- package/src/seeds.ts +116 -0
- package/src/state.ts +414 -0
- package/src/sync.ts +190 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 brainjar contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# brainjar
|
|
2
|
+
|
|
3
|
+
[](https://github.com/brainjar-sh/brainjar-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/brainjar)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Shape how your AI thinks — identity, soul, persona, rules.
|
|
8
|
+
|
|
9
|
+
brainjar manages AI agent behavior through composable layers. Instead of one monolithic config file, you separate **what the agent sounds like** (soul), **how it works** (persona), and **what constraints it follows** (rules). Each layer is a markdown file. Mix and match them per project, per task, or per session.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Install
|
|
15
|
+
bun install -g brainjar
|
|
16
|
+
|
|
17
|
+
# Initialize with starter content
|
|
18
|
+
brainjar init --default
|
|
19
|
+
|
|
20
|
+
# See what's active
|
|
21
|
+
brainjar status
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That gives you a soul (craftsman), a persona (engineer), and rules (boundaries, git discipline, security) — all wired into your `CLAUDE.md` and ready to go.
|
|
25
|
+
|
|
26
|
+
## Concepts
|
|
27
|
+
|
|
28
|
+
brainjar has four core concepts. Understanding these makes everything else click.
|
|
29
|
+
|
|
30
|
+
### Soul — who the agent is
|
|
31
|
+
|
|
32
|
+
The soul defines personality: tone, character, standards. It's the constant across all tasks. You probably only have one or two. Think of it as the agent's voice.
|
|
33
|
+
|
|
34
|
+
### Persona — how the agent works
|
|
35
|
+
|
|
36
|
+
Personas define role and workflow. An engineer persona works differently than a reviewer or an architect. Switch personas based on what you're doing — they're the agent's job description.
|
|
37
|
+
|
|
38
|
+
Personas bundle their own rules via frontmatter:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
---
|
|
42
|
+
rules:
|
|
43
|
+
- default
|
|
44
|
+
- security
|
|
45
|
+
---
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Rules — what the agent must follow
|
|
49
|
+
|
|
50
|
+
Rules are behavioral constraints — guardrails that apply regardless of persona. Single files or multi-file packs (directories). Rules from a persona's frontmatter activate automatically when that persona is active.
|
|
51
|
+
|
|
52
|
+
### Brain — a saved configuration
|
|
53
|
+
|
|
54
|
+
A brain is a named snapshot of soul + persona + rules. Instead of switching three things separately, save your setup once and activate it in one shot. Useful for repeatable workflows like "code review" or "design session."
|
|
55
|
+
|
|
56
|
+
### How they compose
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
soul + persona + rules = full agent behavior
|
|
60
|
+
│ │
|
|
61
|
+
└── bundled rules ───┘ (from persona frontmatter)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A brain captures all three layers. `compose` assembles them into a single prompt for subagent dispatch.
|
|
65
|
+
|
|
66
|
+
## State cascade
|
|
67
|
+
|
|
68
|
+
State merges in three tiers. Each tier overrides the previous:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
global → local → env
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Tier | Storage | When to use |
|
|
75
|
+
|------|---------|-------------|
|
|
76
|
+
| **Global** | `~/.brainjar/state.yaml` | Default behavior across all projects |
|
|
77
|
+
| **Local** | `.brainjar/state.yaml` (in project) | Per-project overrides |
|
|
78
|
+
| **Env** | `BRAINJAR_*` environment variables | Per-session or CI overrides |
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Global (default for all projects)
|
|
82
|
+
brainjar persona use engineer
|
|
83
|
+
|
|
84
|
+
# Local (this project only)
|
|
85
|
+
brainjar persona use planner --local
|
|
86
|
+
|
|
87
|
+
# Env (this session only)
|
|
88
|
+
BRAINJAR_PERSONA=reviewer claude
|
|
89
|
+
|
|
90
|
+
# Or use a subshell for scoped sessions
|
|
91
|
+
brainjar shell --persona reviewer --rules-add security
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`brainjar status` shows where each setting comes from:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
soul craftsman (global)
|
|
98
|
+
persona planner (local)
|
|
99
|
+
rules default (global), security (+local)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Scope annotations
|
|
103
|
+
|
|
104
|
+
When you see scope labels in `status` and `rules list` output:
|
|
105
|
+
|
|
106
|
+
| Label | Meaning |
|
|
107
|
+
|-------|---------|
|
|
108
|
+
| `(global)` | Set in `~/.brainjar/state.yaml` |
|
|
109
|
+
| `(local)` | Overridden in `.brainjar/state.yaml` |
|
|
110
|
+
| `(+local)` | Added by local override (not in global) |
|
|
111
|
+
| `(-local)` | Removed by local override (active globally, suppressed here) |
|
|
112
|
+
| `(env)` | Overridden by `BRAINJAR_*` env var |
|
|
113
|
+
| `(+env)` | Added by env var |
|
|
114
|
+
| `(-env)` | Removed by env var |
|
|
115
|
+
|
|
116
|
+
### Environment variables
|
|
117
|
+
|
|
118
|
+
These env vars override all other state when set:
|
|
119
|
+
|
|
120
|
+
| Variable | Effect |
|
|
121
|
+
|----------|--------|
|
|
122
|
+
| `BRAINJAR_HOME` | Override `~/.brainjar/` location |
|
|
123
|
+
| `BRAINJAR_SOUL` | Override active soul |
|
|
124
|
+
| `BRAINJAR_PERSONA` | Override active persona |
|
|
125
|
+
| `BRAINJAR_IDENTITY` | Override active identity |
|
|
126
|
+
| `BRAINJAR_RULES_ADD` | Comma-separated rules to add |
|
|
127
|
+
| `BRAINJAR_RULES_REMOVE` | Comma-separated rules to remove |
|
|
128
|
+
|
|
129
|
+
Set to empty string to explicitly unset (e.g., `BRAINJAR_SOUL=""` removes the soul for that session).
|
|
130
|
+
|
|
131
|
+
## What it does
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
~/.brainjar/
|
|
135
|
+
souls/ # Voice and character — who the agent is
|
|
136
|
+
craftsman.md
|
|
137
|
+
personas/ # Role and workflow — how the agent works
|
|
138
|
+
engineer.md
|
|
139
|
+
planner.md
|
|
140
|
+
reviewer.md
|
|
141
|
+
rules/ # Constraints — what the agent must follow
|
|
142
|
+
default/ # Boundaries, context recovery, task completion
|
|
143
|
+
git-discipline.md
|
|
144
|
+
security.md
|
|
145
|
+
brains/ # Full-stack snapshots — soul + persona + rules
|
|
146
|
+
review.yaml
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
brainjar reads these markdown files, merges the active layers, and inlines them into your agent's config (`~/.claude/CLAUDE.md` or `.codex/AGENTS.md`) between `<!-- brainjar:start -->` / `<!-- brainjar:end -->` markers. Everything outside the markers is yours. Change a layer, and the agent's behavior changes on next sync.
|
|
150
|
+
|
|
151
|
+
## Layers
|
|
152
|
+
|
|
153
|
+
### Soul
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
brainjar soul create mysoul --description "Direct and rigorous"
|
|
157
|
+
brainjar soul use mysoul
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Persona
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
brainjar persona use planner # Design session
|
|
164
|
+
brainjar persona use engineer # Build session
|
|
165
|
+
brainjar persona use reviewer # Review session
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Rules
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
brainjar rules create no-delete --description "Never delete files without asking"
|
|
172
|
+
brainjar rules add no-delete
|
|
173
|
+
brainjar rules remove no-delete
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Brain
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
brainjar brain save review # Snapshot current state as a brain
|
|
180
|
+
brainjar brain use review # Activate soul + persona + rules in one shot
|
|
181
|
+
brainjar brain list # See available brains
|
|
182
|
+
brainjar brain show review # Inspect a brain's config
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
When to use a brain vs. switching layers individually:
|
|
186
|
+
- **Brain:** Repeatable workflow you do often (code review, design, debugging). Save once, activate in one command.
|
|
187
|
+
- **Individual layers:** Exploratory work, one-off overrides, or when you only need to change one thing.
|
|
188
|
+
|
|
189
|
+
## Subagent orchestration
|
|
190
|
+
|
|
191
|
+
Personas can spawn other personas as subagents. For example, a tech-lead persona can:
|
|
192
|
+
|
|
193
|
+
1. Spawn an **architect** subagent for design — produces a design doc
|
|
194
|
+
2. Get user approval
|
|
195
|
+
3. Implement the design itself
|
|
196
|
+
4. Spawn a **reviewer** subagent to verify — compares code against the spec
|
|
197
|
+
|
|
198
|
+
Each subagent gets the full brain context: soul + persona + rules. The `compose` command assembles the full prompt in a single call:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Primary path — brain drives everything
|
|
202
|
+
brainjar compose review --task "Review the changes in src/sync.ts"
|
|
203
|
+
|
|
204
|
+
# Ad-hoc — no saved brain, specify persona directly
|
|
205
|
+
brainjar compose --persona reviewer --task "Review the changes in src/sync.ts"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
For more granular control, use `brainjar persona show <name>` and `brainjar rules show <name>` to retrieve individual layers.
|
|
209
|
+
|
|
210
|
+
## Recipes
|
|
211
|
+
|
|
212
|
+
### Code review session
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# Save a review brain once
|
|
216
|
+
brainjar soul use craftsman
|
|
217
|
+
brainjar persona use reviewer
|
|
218
|
+
brainjar rules add default
|
|
219
|
+
brainjar rules add security
|
|
220
|
+
brainjar brain save review
|
|
221
|
+
|
|
222
|
+
# Then activate it anytime
|
|
223
|
+
brainjar brain use review
|
|
224
|
+
|
|
225
|
+
# Or scope it to a single session
|
|
226
|
+
brainjar shell --brain review
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### CI pipeline — enforce rules without a persona
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# In CI, use env vars to override behavior
|
|
233
|
+
BRAINJAR_PERSONA=auditor BRAINJAR_RULES_ADD=security,compliance brainjar status --sync
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Project-specific persona
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# In your project directory
|
|
240
|
+
brainjar persona use planner --local
|
|
241
|
+
brainjar rules add no-delete --local
|
|
242
|
+
|
|
243
|
+
# Global settings still apply — local just overrides what you specify
|
|
244
|
+
brainjar status
|
|
245
|
+
# soul craftsman (global)
|
|
246
|
+
# persona planner (local)
|
|
247
|
+
# rules default (global), no-delete (+local)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Commands
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
brainjar init [--default] [--obsidian] [--backend claude|codex]
|
|
254
|
+
brainjar status [--sync] [--global|--local] [--short]
|
|
255
|
+
brainjar compose <brain> [--task <text>]
|
|
256
|
+
brainjar compose --persona <name> [--task <text>]
|
|
257
|
+
|
|
258
|
+
brainjar brain save|use|list|show|drop
|
|
259
|
+
brainjar soul create|list|show|use|drop
|
|
260
|
+
brainjar persona create|list|show|use|drop
|
|
261
|
+
brainjar rules create|list|show|add|remove
|
|
262
|
+
|
|
263
|
+
brainjar identity create|list|show|use|drop|unlock|get|status|lock
|
|
264
|
+
brainjar shell [--brain|--soul|--persona|--identity|--rules-add|--rules-remove]
|
|
265
|
+
brainjar reset [--backend claude|codex]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`show` accepts an optional name to view any item, not just the active one. Use `--short` to get just the active name (useful in scripts and statuslines):
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
brainjar persona show reviewer # View a specific persona
|
|
272
|
+
brainjar soul show # View the active soul
|
|
273
|
+
brainjar rules show security # View a rule's content
|
|
274
|
+
brainjar status --short # One-liner: soul | persona | identity
|
|
275
|
+
brainjar soul show --short # Just the active soul name
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Obsidian support
|
|
279
|
+
|
|
280
|
+
`~/.brainjar/` is a folder of markdown files — it's already almost an Obsidian vault.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
brainjar init --obsidian
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Adds `.obsidian/` config that hides private files (state, identities) from the file explorer and includes templates for creating new souls, personas, and rules from within Obsidian.
|
|
287
|
+
|
|
288
|
+
## Backends
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
brainjar init --backend codex # writes ~/.codex/AGENTS.md
|
|
292
|
+
brainjar reset --backend codex
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Supported: `claude` (default), `codex`
|
|
296
|
+
|
|
297
|
+
## Backup & restore
|
|
298
|
+
|
|
299
|
+
On first sync, brainjar backs up any existing config file to `CLAUDE.md.pre-brainjar`. Running `brainjar reset` removes brainjar-managed config and restores the backup.
|
|
300
|
+
|
|
301
|
+
## Development
|
|
302
|
+
|
|
303
|
+
Built with [Bun](https://bun.sh) and [incur](https://github.com/bradjones/incur).
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
bun install
|
|
307
|
+
bun test
|
|
308
|
+
bun run src/cli.ts --help
|
|
309
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brainjar/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shape how your AI thinks — composable identity, soul, persona, and rules for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/brainjar-sh/brainjar-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/brainjar-sh/brainjar-cli",
|
|
12
|
+
"bugs": "https://github.com/brainjar-sh/brainjar-cli/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai",
|
|
15
|
+
"agent",
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"codex",
|
|
19
|
+
"prompt",
|
|
20
|
+
"persona",
|
|
21
|
+
"configuration",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"brainjar": "./src/cli.ts"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"provenance": true
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepare": "git config core.hooksPath .hooks",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"check": "tsc --noEmit && bun test",
|
|
41
|
+
"changeset": "changeset",
|
|
42
|
+
"changeset:version": "changeset version",
|
|
43
|
+
"changeset:tag": "changeset tag",
|
|
44
|
+
"changeset:publish": "changeset publish"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"incur": "^0.3.4",
|
|
48
|
+
"yaml": "^2.8.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@changesets/cli": "^2.30.0",
|
|
52
|
+
"@types/bun": "^1.3.10",
|
|
53
|
+
"typescript": "^5.9.3"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Cli } from 'incur'
|
|
3
|
+
import pkg from '../package.json'
|
|
4
|
+
import { init } from './commands/init.js'
|
|
5
|
+
import { identity } from './commands/identity.js'
|
|
6
|
+
import { soul } from './commands/soul.js'
|
|
7
|
+
import { persona } from './commands/persona.js'
|
|
8
|
+
import { rules } from './commands/rules.js'
|
|
9
|
+
import { brain } from './commands/brain.js'
|
|
10
|
+
import { status } from './commands/status.js'
|
|
11
|
+
import { reset } from './commands/reset.js'
|
|
12
|
+
import { shell } from './commands/shell.js'
|
|
13
|
+
import { compose } from './commands/compose.js'
|
|
14
|
+
|
|
15
|
+
Cli.create('brainjar', {
|
|
16
|
+
description: 'Shape how your AI thinks — identity, soul, persona, rules',
|
|
17
|
+
version: pkg.version,
|
|
18
|
+
sync: { depth: 0 },
|
|
19
|
+
})
|
|
20
|
+
.command(init)
|
|
21
|
+
.command(status)
|
|
22
|
+
.command(soul)
|
|
23
|
+
.command(persona)
|
|
24
|
+
.command(rules)
|
|
25
|
+
.command(brain)
|
|
26
|
+
.command(identity)
|
|
27
|
+
.command(reset)
|
|
28
|
+
.command(shell)
|
|
29
|
+
.command(compose)
|
|
30
|
+
.serve()
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { Cli, z, Errors } from 'incur'
|
|
2
|
+
|
|
3
|
+
const { IncurError } = Errors
|
|
4
|
+
import { readdir, readFile, writeFile, access, rm } from 'node:fs/promises'
|
|
5
|
+
import { join, basename } from 'node:path'
|
|
6
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
7
|
+
import { paths } from '../paths.js'
|
|
8
|
+
import {
|
|
9
|
+
readState,
|
|
10
|
+
writeState,
|
|
11
|
+
withStateLock,
|
|
12
|
+
readLocalState,
|
|
13
|
+
writeLocalState,
|
|
14
|
+
withLocalStateLock,
|
|
15
|
+
readEnvState,
|
|
16
|
+
mergeState,
|
|
17
|
+
requireBrainjarDir,
|
|
18
|
+
normalizeSlug,
|
|
19
|
+
} from '../state.js'
|
|
20
|
+
import { sync } from '../sync.js'
|
|
21
|
+
|
|
22
|
+
/** Brain YAML schema: soul + persona + rules */
|
|
23
|
+
export interface BrainConfig {
|
|
24
|
+
soul: string
|
|
25
|
+
persona: string
|
|
26
|
+
rules: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Read and validate a brain YAML file. */
|
|
30
|
+
export async function readBrain(name: string): Promise<BrainConfig> {
|
|
31
|
+
const slug = normalizeSlug(name, 'brain name')
|
|
32
|
+
const file = join(paths.brains, `${slug}.yaml`)
|
|
33
|
+
|
|
34
|
+
let raw: string
|
|
35
|
+
try {
|
|
36
|
+
raw = await readFile(file, 'utf-8')
|
|
37
|
+
} catch {
|
|
38
|
+
throw new IncurError({
|
|
39
|
+
code: 'BRAIN_NOT_FOUND',
|
|
40
|
+
message: `Brain "${slug}" not found.`,
|
|
41
|
+
hint: 'Run `brainjar brain list` to see available brains.',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let parsed: unknown
|
|
46
|
+
try {
|
|
47
|
+
parsed = parseYaml(raw)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
throw new IncurError({
|
|
50
|
+
code: 'BRAIN_CORRUPT',
|
|
51
|
+
message: `Brain "${slug}" has invalid YAML: ${(e as Error).message}`,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
56
|
+
throw new IncurError({
|
|
57
|
+
code: 'BRAIN_CORRUPT',
|
|
58
|
+
message: `Brain "${slug}" is empty or invalid.`,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const p = parsed as Record<string, unknown>
|
|
63
|
+
|
|
64
|
+
if (typeof p.soul !== 'string' || !p.soul) {
|
|
65
|
+
throw new IncurError({
|
|
66
|
+
code: 'BRAIN_INVALID',
|
|
67
|
+
message: `Brain "${slug}" is missing required field "soul".`,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof p.persona !== 'string' || !p.persona) {
|
|
72
|
+
throw new IncurError({
|
|
73
|
+
code: 'BRAIN_INVALID',
|
|
74
|
+
message: `Brain "${slug}" is missing required field "persona".`,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rules = Array.isArray(p.rules) ? p.rules.map(String) : []
|
|
79
|
+
|
|
80
|
+
return { soul: p.soul, persona: p.persona, rules }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const brain = Cli.create('brain', {
|
|
84
|
+
description: 'Manage brains — full-stack configuration snapshots (soul + persona + rules)',
|
|
85
|
+
})
|
|
86
|
+
.command('save', {
|
|
87
|
+
description: 'Snapshot current effective state as a named brain',
|
|
88
|
+
args: z.object({
|
|
89
|
+
name: z.string().describe('Brain name (will be used as filename)'),
|
|
90
|
+
}),
|
|
91
|
+
options: z.object({
|
|
92
|
+
overwrite: z.boolean().default(false).describe('Overwrite existing brain file'),
|
|
93
|
+
}),
|
|
94
|
+
async run(c) {
|
|
95
|
+
await requireBrainjarDir()
|
|
96
|
+
const name = normalizeSlug(c.args.name, 'brain name')
|
|
97
|
+
const dest = join(paths.brains, `${name}.yaml`)
|
|
98
|
+
|
|
99
|
+
// Check for existing brain
|
|
100
|
+
if (!c.options.overwrite) {
|
|
101
|
+
try {
|
|
102
|
+
await access(dest)
|
|
103
|
+
throw new IncurError({
|
|
104
|
+
code: 'BRAIN_EXISTS',
|
|
105
|
+
message: `Brain "${name}" already exists.`,
|
|
106
|
+
hint: 'Use --overwrite to replace it, or choose a different name.',
|
|
107
|
+
})
|
|
108
|
+
} catch (e) {
|
|
109
|
+
if (e instanceof IncurError) throw e
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Read effective state
|
|
114
|
+
const globalState = await readState()
|
|
115
|
+
const localState = await readLocalState()
|
|
116
|
+
const envState = readEnvState()
|
|
117
|
+
const effective = mergeState(globalState, localState, envState)
|
|
118
|
+
|
|
119
|
+
if (!effective.soul.value) {
|
|
120
|
+
throw new IncurError({
|
|
121
|
+
code: 'NO_ACTIVE_SOUL',
|
|
122
|
+
message: 'Cannot save brain: no active soul.',
|
|
123
|
+
hint: 'Activate a soul first with `brainjar soul use <name>`.',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!effective.persona.value) {
|
|
128
|
+
throw new IncurError({
|
|
129
|
+
code: 'NO_ACTIVE_PERSONA',
|
|
130
|
+
message: 'Cannot save brain: no active persona.',
|
|
131
|
+
hint: 'Activate a persona first with `brainjar persona use <name>`.',
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const activeRules = effective.rules
|
|
136
|
+
.filter(r => !r.scope.startsWith('-'))
|
|
137
|
+
.map(r => r.value)
|
|
138
|
+
|
|
139
|
+
const config: BrainConfig = {
|
|
140
|
+
soul: effective.soul.value,
|
|
141
|
+
persona: effective.persona.value,
|
|
142
|
+
rules: activeRules,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await writeFile(dest, stringifyYaml(config))
|
|
146
|
+
|
|
147
|
+
return { saved: name, ...config }
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
.command('use', {
|
|
151
|
+
description: 'Activate a brain — sets soul, persona, and rules in one shot',
|
|
152
|
+
args: z.object({
|
|
153
|
+
name: z.string().describe('Brain name to activate'),
|
|
154
|
+
}),
|
|
155
|
+
options: z.object({
|
|
156
|
+
local: z.boolean().default(false).describe('Apply brain at project scope'),
|
|
157
|
+
}),
|
|
158
|
+
async run(c) {
|
|
159
|
+
await requireBrainjarDir()
|
|
160
|
+
const name = normalizeSlug(c.args.name, 'brain name')
|
|
161
|
+
const config = await readBrain(name)
|
|
162
|
+
|
|
163
|
+
// Validate soul exists
|
|
164
|
+
try {
|
|
165
|
+
await readFile(join(paths.souls, `${config.soul}.md`), 'utf-8')
|
|
166
|
+
} catch {
|
|
167
|
+
throw new IncurError({
|
|
168
|
+
code: 'SOUL_NOT_FOUND',
|
|
169
|
+
message: `Brain "${name}" references soul "${config.soul}" which does not exist.`,
|
|
170
|
+
hint: 'Create the soul first or update the brain file.',
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Validate persona exists
|
|
175
|
+
try {
|
|
176
|
+
await readFile(join(paths.personas, `${config.persona}.md`), 'utf-8')
|
|
177
|
+
} catch {
|
|
178
|
+
throw new IncurError({
|
|
179
|
+
code: 'PERSONA_NOT_FOUND',
|
|
180
|
+
message: `Brain "${name}" references persona "${config.persona}" which does not exist.`,
|
|
181
|
+
hint: 'Create the persona first or update the brain file.',
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (c.options.local) {
|
|
186
|
+
await withLocalStateLock(async () => {
|
|
187
|
+
const local = await readLocalState()
|
|
188
|
+
local.soul = config.soul
|
|
189
|
+
local.persona = config.persona
|
|
190
|
+
// Replace rules entirely — brain is a complete snapshot
|
|
191
|
+
local.rules = { add: config.rules, remove: [] }
|
|
192
|
+
await writeLocalState(local)
|
|
193
|
+
await sync({ local: true })
|
|
194
|
+
})
|
|
195
|
+
} else {
|
|
196
|
+
await withStateLock(async () => {
|
|
197
|
+
const state = await readState()
|
|
198
|
+
state.soul = config.soul
|
|
199
|
+
state.persona = config.persona
|
|
200
|
+
state.rules = config.rules
|
|
201
|
+
await writeState(state)
|
|
202
|
+
await sync()
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { activated: name, local: c.options.local, ...config }
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
.command('list', {
|
|
210
|
+
description: 'List available brains',
|
|
211
|
+
async run() {
|
|
212
|
+
await requireBrainjarDir()
|
|
213
|
+
const entries = await readdir(paths.brains).catch(() => [])
|
|
214
|
+
const brains = entries
|
|
215
|
+
.filter(f => f.endsWith('.yaml'))
|
|
216
|
+
.map(f => basename(f, '.yaml'))
|
|
217
|
+
return { brains }
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
.command('show', {
|
|
221
|
+
description: 'Show a brain configuration',
|
|
222
|
+
args: z.object({
|
|
223
|
+
name: z.string().describe('Brain name to show'),
|
|
224
|
+
}),
|
|
225
|
+
async run(c) {
|
|
226
|
+
await requireBrainjarDir()
|
|
227
|
+
const name = normalizeSlug(c.args.name, 'brain name')
|
|
228
|
+
const config = await readBrain(name)
|
|
229
|
+
return { name, ...config }
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
.command('drop', {
|
|
233
|
+
description: 'Delete a brain',
|
|
234
|
+
args: z.object({
|
|
235
|
+
name: z.string().describe('Brain name to delete'),
|
|
236
|
+
}),
|
|
237
|
+
async run(c) {
|
|
238
|
+
await requireBrainjarDir()
|
|
239
|
+
const name = normalizeSlug(c.args.name, 'brain name')
|
|
240
|
+
const file = join(paths.brains, `${name}.yaml`)
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await access(file)
|
|
244
|
+
} catch {
|
|
245
|
+
throw new IncurError({
|
|
246
|
+
code: 'BRAIN_NOT_FOUND',
|
|
247
|
+
message: `Brain "${name}" not found.`,
|
|
248
|
+
hint: 'Run `brainjar brain list` to see available brains.',
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await rm(file)
|
|
253
|
+
|
|
254
|
+
return { dropped: name }
|
|
255
|
+
},
|
|
256
|
+
})
|