@brainjar/cli 0.3.0 → 0.4.1
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 +9 -7
- package/package.json +1 -1
- package/src/api-types.ts +155 -0
- package/src/cli.ts +4 -0
- package/src/client.ts +157 -0
- package/src/commands/brain.ts +99 -113
- package/src/commands/compose.ts +17 -116
- package/src/commands/init.ts +65 -40
- package/src/commands/migrate.ts +61 -0
- package/src/commands/pack.ts +1 -5
- package/src/commands/persona.ts +152 -145
- package/src/commands/rules.ts +112 -174
- package/src/commands/server.ts +212 -0
- package/src/commands/shell.ts +53 -46
- package/src/commands/soul.ts +125 -110
- package/src/commands/status.ts +36 -41
- package/src/commands/sync.ts +0 -2
- package/src/config.ts +125 -0
- package/src/daemon.ts +404 -0
- package/src/errors.ts +172 -0
- package/src/migrate.ts +247 -0
- package/src/pack.ts +149 -428
- package/src/paths.ts +1 -6
- package/src/seeds.ts +62 -103
- package/src/state.ts +12 -368
- package/src/sync.ts +60 -85
- package/src/version-check.ts +137 -0
- package/src/brain.ts +0 -69
- package/src/hooks.test.ts +0 -132
- package/src/pack.test.ts +0 -472
- package/src/seeds/templates/persona.md +0 -19
- package/src/seeds/templates/rule.md +0 -11
- package/src/seeds/templates/soul.md +0 -20
- /package/src/seeds/rules/{default/boundaries.md → boundaries.md} +0 -0
- /package/src/seeds/rules/{default/context-recovery.md → context-recovery.md} +0 -0
- /package/src/seeds/rules/{default/task-completion.md → task-completion.md} +0 -0
package/src/commands/persona.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Cli, z, Errors } from 'incur'
|
|
2
|
+
import { basename } from 'node:path'
|
|
2
3
|
|
|
3
4
|
const { IncurError } = Errors
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { paths } from '../paths.js'
|
|
7
|
-
import { type State, readState, writeState, withStateLock, readLocalState, writeLocalState, withLocalStateLock, readEnvState, mergeState, requireBrainjarDir, parseLayerFrontmatter, stripFrontmatter, normalizeSlug, listAvailableRules } from '../state.js'
|
|
5
|
+
import { ErrorCode, createError } from '../errors.js'
|
|
6
|
+
import { normalizeSlug, getEffectiveState, putState } from '../state.js'
|
|
8
7
|
import { sync } from '../sync.js'
|
|
8
|
+
import { getApi } from '../client.js'
|
|
9
|
+
import type { ApiPersona, ApiPersonaList, ApiRuleList } from '../api-types.js'
|
|
9
10
|
|
|
10
11
|
export const persona = Cli.create('persona', {
|
|
11
12
|
description: 'Manage personas — role behavior and workflow for the agent',
|
|
@@ -13,53 +14,43 @@ export const persona = Cli.create('persona', {
|
|
|
13
14
|
.command('create', {
|
|
14
15
|
description: 'Create a new persona',
|
|
15
16
|
args: z.object({
|
|
16
|
-
name: z.string().describe('Persona name
|
|
17
|
+
name: z.string().describe('Persona name'),
|
|
17
18
|
}),
|
|
18
19
|
options: z.object({
|
|
19
20
|
description: z.string().optional().describe('One-line description of the persona'),
|
|
20
21
|
rules: z.array(z.string()).optional().describe('Rules to bundle with this persona'),
|
|
21
22
|
}),
|
|
22
23
|
async run(c) {
|
|
23
|
-
await requireBrainjarDir()
|
|
24
24
|
const name = normalizeSlug(c.args.name, 'persona name')
|
|
25
|
-
const
|
|
25
|
+
const api = await getApi()
|
|
26
26
|
|
|
27
|
+
// Check if it already exists
|
|
27
28
|
try {
|
|
28
|
-
await
|
|
29
|
-
throw
|
|
30
|
-
code: 'PERSONA_EXISTS',
|
|
31
|
-
message: `Persona "${name}" already exists.`,
|
|
32
|
-
hint: 'Choose a different name or edit the existing file.',
|
|
33
|
-
})
|
|
29
|
+
await api.get<ApiPersona>(`/api/v1/personas/${name}`)
|
|
30
|
+
throw createError(ErrorCode.PERSONA_EXISTS, { params: [name] })
|
|
34
31
|
} catch (e) {
|
|
35
|
-
if (e instanceof IncurError) throw e
|
|
32
|
+
if (e instanceof IncurError && e.code === ErrorCode.PERSONA_EXISTS) throw e
|
|
33
|
+
if (e instanceof IncurError && e.code !== ErrorCode.NOT_FOUND) throw e
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
const rulesList = c.options.rules ?? []
|
|
39
37
|
|
|
40
|
-
// Validate rules exist
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
// Validate rules exist on server
|
|
39
|
+
if (rulesList.length > 0) {
|
|
40
|
+
const available = await api.get<ApiRuleList>('/api/v1/rules')
|
|
41
|
+
const availableSlugs = available.rules.map(r => r.slug)
|
|
42
|
+
const invalid = rulesList.filter(r => !availableSlugs.includes(r))
|
|
43
|
+
if (invalid.length > 0) {
|
|
44
|
+
throw createError(ErrorCode.RULES_NOT_FOUND, {
|
|
45
|
+
message: `Rules not found: ${invalid.join(', ')}`,
|
|
46
|
+
hint: `Available rules: ${availableSlugs.join(', ')}`,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
// Frontmatter — always write it (rules default to [default])
|
|
54
|
-
const effectiveRules = rulesList.length > 0 ? rulesList : ['default']
|
|
55
|
-
lines.push('---')
|
|
56
|
-
lines.push('rules:')
|
|
57
|
-
for (const rule of effectiveRules) {
|
|
58
|
-
lines.push(` - ${rule}`)
|
|
59
|
-
}
|
|
60
|
-
lines.push('---')
|
|
61
|
-
lines.push('')
|
|
51
|
+
const effectiveRules = rulesList
|
|
62
52
|
|
|
53
|
+
const lines: string[] = []
|
|
63
54
|
lines.push(`# ${name}`)
|
|
64
55
|
lines.push('')
|
|
65
56
|
if (c.options.description) {
|
|
@@ -77,33 +68,85 @@ export const persona = Cli.create('persona', {
|
|
|
77
68
|
lines.push('')
|
|
78
69
|
|
|
79
70
|
const content = lines.join('\n')
|
|
80
|
-
await
|
|
71
|
+
await api.put<ApiPersona>(`/api/v1/personas/${name}`, {
|
|
72
|
+
content,
|
|
73
|
+
bundled_rules: effectiveRules,
|
|
74
|
+
})
|
|
81
75
|
|
|
82
76
|
if (c.agent || c.formatExplicit) {
|
|
83
|
-
return {
|
|
84
|
-
created: dest,
|
|
85
|
-
name,
|
|
86
|
-
rules: effectiveRules,
|
|
87
|
-
template: content,
|
|
88
|
-
}
|
|
77
|
+
return { created: name, name, rules: effectiveRules, template: content }
|
|
89
78
|
}
|
|
90
79
|
|
|
91
80
|
return {
|
|
92
|
-
created:
|
|
81
|
+
created: name,
|
|
93
82
|
name,
|
|
94
83
|
rules: effectiveRules,
|
|
95
84
|
template: `\n${content}`,
|
|
96
|
-
next: `
|
|
85
|
+
next: `Run \`brainjar persona show ${name}\` to view, then \`brainjar persona use ${name}\` to activate.`,
|
|
97
86
|
}
|
|
98
87
|
},
|
|
99
88
|
})
|
|
89
|
+
.command('update', {
|
|
90
|
+
description: 'Update a persona\'s content (reads from stdin)',
|
|
91
|
+
args: z.object({
|
|
92
|
+
name: z.string().describe('Persona name'),
|
|
93
|
+
}),
|
|
94
|
+
options: z.object({
|
|
95
|
+
rules: z.array(z.string()).optional().describe('Update bundled rules'),
|
|
96
|
+
}),
|
|
97
|
+
async run(c) {
|
|
98
|
+
const name = normalizeSlug(c.args.name, 'persona name')
|
|
99
|
+
const api = await getApi()
|
|
100
|
+
|
|
101
|
+
// Validate it exists and get current data
|
|
102
|
+
let existing: ApiPersona
|
|
103
|
+
try {
|
|
104
|
+
existing = await api.get<ApiPersona>(`/api/v1/personas/${name}`)
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
107
|
+
throw createError(ErrorCode.PERSONA_NOT_FOUND, { params: [name] })
|
|
108
|
+
}
|
|
109
|
+
throw e
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const chunks: Uint8Array[] = []
|
|
113
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
114
|
+
chunks.push(chunk)
|
|
115
|
+
}
|
|
116
|
+
const content = Buffer.concat(chunks).toString().trim()
|
|
117
|
+
|
|
118
|
+
// Validate rules if provided
|
|
119
|
+
const rulesList = c.options.rules
|
|
120
|
+
if (rulesList && rulesList.length > 0) {
|
|
121
|
+
const available = await api.get<ApiRuleList>('/api/v1/rules')
|
|
122
|
+
const availableSlugs = available.rules.map(r => r.slug)
|
|
123
|
+
const invalid = rulesList.filter(r => !availableSlugs.includes(r))
|
|
124
|
+
if (invalid.length > 0) {
|
|
125
|
+
throw createError(ErrorCode.RULES_NOT_FOUND, {
|
|
126
|
+
message: `Rules not found: ${invalid.join(', ')}`,
|
|
127
|
+
hint: `Available rules: ${availableSlugs.join(', ')}`,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await api.put<ApiPersona>(`/api/v1/personas/${name}`, {
|
|
133
|
+
content: content || existing.content,
|
|
134
|
+
bundled_rules: rulesList ?? existing.bundled_rules,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Sync if this persona is active
|
|
138
|
+
const state = await getEffectiveState(api)
|
|
139
|
+
if (state.persona === name) await sync({ api })
|
|
140
|
+
|
|
141
|
+
return { updated: name, rules: rulesList ?? existing.bundled_rules }
|
|
142
|
+
},
|
|
143
|
+
})
|
|
100
144
|
.command('list', {
|
|
101
145
|
description: 'List available personas',
|
|
102
146
|
async run() {
|
|
103
|
-
await
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
return { personas }
|
|
147
|
+
const api = await getApi()
|
|
148
|
+
const result = await api.get<ApiPersonaList>('/api/v1/personas')
|
|
149
|
+
return { personas: result.personas.map(p => p.slug) }
|
|
107
150
|
},
|
|
108
151
|
})
|
|
109
152
|
.command('show', {
|
|
@@ -112,148 +155,112 @@ export const persona = Cli.create('persona', {
|
|
|
112
155
|
name: z.string().optional().describe('Persona name to show (defaults to active persona)'),
|
|
113
156
|
}),
|
|
114
157
|
options: z.object({
|
|
115
|
-
|
|
158
|
+
project: z.boolean().default(false).describe('Show project persona override (if any)'),
|
|
116
159
|
short: z.boolean().default(false).describe('Print only the active persona name'),
|
|
117
160
|
}),
|
|
118
161
|
async run(c) {
|
|
119
|
-
await
|
|
162
|
+
const api = await getApi()
|
|
120
163
|
|
|
121
164
|
if (c.options.short) {
|
|
122
165
|
if (c.args.name) return c.args.name
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
const env = readEnvState()
|
|
126
|
-
const effective = mergeState(global, local, env)
|
|
127
|
-
return effective.persona.value ?? 'none'
|
|
166
|
+
const state = await getEffectiveState(api)
|
|
167
|
+
return state.persona ?? 'none'
|
|
128
168
|
}
|
|
129
169
|
|
|
130
|
-
// If a specific name was given, show that persona directly
|
|
131
170
|
if (c.args.name) {
|
|
132
171
|
const name = normalizeSlug(c.args.name, 'persona name')
|
|
133
172
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
throw
|
|
141
|
-
code: 'PERSONA_NOT_FOUND',
|
|
142
|
-
message: `Persona "${name}" not found.`,
|
|
143
|
-
hint: 'Run `brainjar persona list` to see available personas.',
|
|
144
|
-
})
|
|
173
|
+
const p = await api.get<ApiPersona>(`/api/v1/personas/${name}`)
|
|
174
|
+
return { name, title: p.title, content: p.content, rules: p.bundled_rules }
|
|
175
|
+
} catch (e) {
|
|
176
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
177
|
+
throw createError(ErrorCode.PERSONA_NOT_FOUND, { params: [name] })
|
|
178
|
+
}
|
|
179
|
+
throw e
|
|
145
180
|
}
|
|
146
181
|
}
|
|
147
182
|
|
|
148
|
-
if (c.options.
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
183
|
+
if (c.options.project) {
|
|
184
|
+
const state = await api.get<import('../api-types.js').ApiStateOverride>('/api/v1/state/override', {
|
|
185
|
+
project: basename(process.cwd()),
|
|
186
|
+
})
|
|
187
|
+
if (state.persona_slug === undefined) return { active: false, scope: 'project', note: 'No project persona override (cascades from workspace)' }
|
|
188
|
+
if (state.persona_slug === null) return { active: false, scope: 'project', name: null, note: 'Explicitly unset at project scope' }
|
|
152
189
|
try {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const content = stripFrontmatter(raw)
|
|
156
|
-
const title = content.split('\n').find(l => l.startsWith('# '))?.replace('# ', '') ?? null
|
|
157
|
-
return { active: true, scope: 'local', name: local.persona, title, content, ...frontmatter }
|
|
190
|
+
const p = await api.get<ApiPersona>(`/api/v1/personas/${state.persona_slug}`)
|
|
191
|
+
return { active: true, scope: 'project', name: state.persona_slug, title: p.title, content: p.content, rules: p.bundled_rules }
|
|
158
192
|
} catch {
|
|
159
|
-
return { active: false, scope: '
|
|
193
|
+
return { active: false, scope: 'project', name: state.persona_slug, error: 'Not found on server' }
|
|
160
194
|
}
|
|
161
195
|
}
|
|
162
196
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const env = readEnvState()
|
|
166
|
-
const effective = mergeState(global, local, env)
|
|
167
|
-
if (!effective.persona.value) return { active: false }
|
|
197
|
+
const state = await getEffectiveState(api)
|
|
198
|
+
if (!state.persona) return { active: false }
|
|
168
199
|
try {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const content = stripFrontmatter(raw)
|
|
172
|
-
const title = content.split('\n').find(l => l.startsWith('# '))?.replace('# ', '') ?? null
|
|
173
|
-
return { active: true, name: effective.persona.value, scope: effective.persona.scope, title, content, ...frontmatter }
|
|
200
|
+
const p = await api.get<ApiPersona>(`/api/v1/personas/${state.persona}`)
|
|
201
|
+
return { active: true, name: state.persona, title: p.title, content: p.content, rules: p.bundled_rules }
|
|
174
202
|
} catch {
|
|
175
|
-
return { active: false, name:
|
|
203
|
+
return { active: false, name: state.persona, error: 'Not found on server' }
|
|
176
204
|
}
|
|
177
205
|
},
|
|
178
206
|
})
|
|
179
207
|
.command('use', {
|
|
180
208
|
description: 'Activate a persona',
|
|
181
209
|
args: z.object({
|
|
182
|
-
name: z.string().describe('Persona name
|
|
210
|
+
name: z.string().describe('Persona name'),
|
|
183
211
|
}),
|
|
184
212
|
options: z.object({
|
|
185
|
-
|
|
213
|
+
project: z.boolean().default(false).describe('Apply at project scope'),
|
|
186
214
|
}),
|
|
187
215
|
async run(c) {
|
|
188
|
-
await requireBrainjarDir()
|
|
189
216
|
const name = normalizeSlug(c.args.name, 'persona name')
|
|
190
|
-
const
|
|
191
|
-
|
|
217
|
+
const api = await getApi()
|
|
218
|
+
|
|
219
|
+
// Validate and get bundled rules
|
|
220
|
+
let personaData: ApiPersona
|
|
192
221
|
try {
|
|
193
|
-
|
|
194
|
-
} catch {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
})
|
|
222
|
+
personaData = await api.get<ApiPersona>(`/api/v1/personas/${name}`)
|
|
223
|
+
} catch (e) {
|
|
224
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
225
|
+
throw createError(ErrorCode.PERSONA_NOT_FOUND, { params: [name] })
|
|
226
|
+
}
|
|
227
|
+
throw e
|
|
200
228
|
}
|
|
201
229
|
|
|
202
|
-
const
|
|
230
|
+
const bundledRules = personaData.bundled_rules
|
|
203
231
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} else {
|
|
215
|
-
await withStateLock(async () => {
|
|
216
|
-
const state = await readState()
|
|
217
|
-
state.persona = name
|
|
218
|
-
if (frontmatter.rules.length > 0) state.rules = frontmatter.rules
|
|
219
|
-
await writeState(state)
|
|
220
|
-
await sync()
|
|
221
|
-
})
|
|
222
|
-
}
|
|
232
|
+
const mutationOpts = c.options.project
|
|
233
|
+
? { project: basename(process.cwd()) }
|
|
234
|
+
: undefined
|
|
235
|
+
await putState(api, {
|
|
236
|
+
persona_slug: name,
|
|
237
|
+
rule_slugs: bundledRules.length > 0 ? bundledRules : undefined,
|
|
238
|
+
}, mutationOpts)
|
|
239
|
+
|
|
240
|
+
await sync({ api })
|
|
241
|
+
if (c.options.project) await sync({ api, project: true })
|
|
223
242
|
|
|
224
|
-
const result: Record<string, unknown> = { activated: name,
|
|
225
|
-
if (
|
|
243
|
+
const result: Record<string, unknown> = { activated: name, project: c.options.project }
|
|
244
|
+
if (bundledRules.length > 0) result.rules = bundledRules
|
|
226
245
|
return result
|
|
227
246
|
},
|
|
228
247
|
})
|
|
229
248
|
.command('drop', {
|
|
230
249
|
description: 'Deactivate the current persona',
|
|
231
250
|
options: z.object({
|
|
232
|
-
|
|
251
|
+
project: z.boolean().default(false).describe('Remove project persona override or deactivate workspace persona'),
|
|
233
252
|
}),
|
|
234
253
|
async run(c) {
|
|
235
|
-
await
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!state.persona) {
|
|
247
|
-
throw new IncurError({
|
|
248
|
-
code: 'NO_ACTIVE_PERSONA',
|
|
249
|
-
message: 'No active persona to deactivate.',
|
|
250
|
-
})
|
|
251
|
-
}
|
|
252
|
-
state.persona = null
|
|
253
|
-
await writeState(state)
|
|
254
|
-
await sync()
|
|
255
|
-
})
|
|
256
|
-
}
|
|
257
|
-
return { deactivated: true, local: c.options.local }
|
|
254
|
+
const api = await getApi()
|
|
255
|
+
|
|
256
|
+
const mutationOpts = c.options.project
|
|
257
|
+
? { project: basename(process.cwd()) }
|
|
258
|
+
: undefined
|
|
259
|
+
await putState(api, { persona_slug: null }, mutationOpts)
|
|
260
|
+
|
|
261
|
+
await sync({ api })
|
|
262
|
+
if (c.options.project) await sync({ api, project: true })
|
|
263
|
+
|
|
264
|
+
return { deactivated: true, project: c.options.project }
|
|
258
265
|
},
|
|
259
266
|
})
|