@brainjar/cli 0.3.0 → 0.4.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 +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 +97 -145
- package/src/commands/rules.ts +71 -174
- package/src/commands/server.ts +212 -0
- package/src/commands/shell.ts +53 -46
- package/src/commands/soul.ts +75 -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,30 @@ 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
|
})
|
|
100
89
|
.command('list', {
|
|
101
90
|
description: 'List available personas',
|
|
102
91
|
async run() {
|
|
103
|
-
await
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
return { personas }
|
|
92
|
+
const api = await getApi()
|
|
93
|
+
const result = await api.get<ApiPersonaList>('/api/v1/personas')
|
|
94
|
+
return { personas: result.personas.map(p => p.slug) }
|
|
107
95
|
},
|
|
108
96
|
})
|
|
109
97
|
.command('show', {
|
|
@@ -112,148 +100,112 @@ export const persona = Cli.create('persona', {
|
|
|
112
100
|
name: z.string().optional().describe('Persona name to show (defaults to active persona)'),
|
|
113
101
|
}),
|
|
114
102
|
options: z.object({
|
|
115
|
-
|
|
103
|
+
project: z.boolean().default(false).describe('Show project persona override (if any)'),
|
|
116
104
|
short: z.boolean().default(false).describe('Print only the active persona name'),
|
|
117
105
|
}),
|
|
118
106
|
async run(c) {
|
|
119
|
-
await
|
|
107
|
+
const api = await getApi()
|
|
120
108
|
|
|
121
109
|
if (c.options.short) {
|
|
122
110
|
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'
|
|
111
|
+
const state = await getEffectiveState(api)
|
|
112
|
+
return state.persona ?? 'none'
|
|
128
113
|
}
|
|
129
114
|
|
|
130
|
-
// If a specific name was given, show that persona directly
|
|
131
115
|
if (c.args.name) {
|
|
132
116
|
const name = normalizeSlug(c.args.name, 'persona name')
|
|
133
117
|
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
|
-
})
|
|
118
|
+
const p = await api.get<ApiPersona>(`/api/v1/personas/${name}`)
|
|
119
|
+
return { name, title: p.title, content: p.content, rules: p.bundled_rules }
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
122
|
+
throw createError(ErrorCode.PERSONA_NOT_FOUND, { params: [name] })
|
|
123
|
+
}
|
|
124
|
+
throw e
|
|
145
125
|
}
|
|
146
126
|
}
|
|
147
127
|
|
|
148
|
-
if (c.options.
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
128
|
+
if (c.options.project) {
|
|
129
|
+
const state = await api.get<import('../api-types.js').ApiStateOverride>('/api/v1/state/override', {
|
|
130
|
+
project: basename(process.cwd()),
|
|
131
|
+
})
|
|
132
|
+
if (state.persona_slug === undefined) return { active: false, scope: 'project', note: 'No project persona override (cascades from workspace)' }
|
|
133
|
+
if (state.persona_slug === null) return { active: false, scope: 'project', name: null, note: 'Explicitly unset at project scope' }
|
|
152
134
|
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 }
|
|
135
|
+
const p = await api.get<ApiPersona>(`/api/v1/personas/${state.persona_slug}`)
|
|
136
|
+
return { active: true, scope: 'project', name: state.persona_slug, title: p.title, content: p.content, rules: p.bundled_rules }
|
|
158
137
|
} catch {
|
|
159
|
-
return { active: false, scope: '
|
|
138
|
+
return { active: false, scope: 'project', name: state.persona_slug, error: 'Not found on server' }
|
|
160
139
|
}
|
|
161
140
|
}
|
|
162
141
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const env = readEnvState()
|
|
166
|
-
const effective = mergeState(global, local, env)
|
|
167
|
-
if (!effective.persona.value) return { active: false }
|
|
142
|
+
const state = await getEffectiveState(api)
|
|
143
|
+
if (!state.persona) return { active: false }
|
|
168
144
|
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 }
|
|
145
|
+
const p = await api.get<ApiPersona>(`/api/v1/personas/${state.persona}`)
|
|
146
|
+
return { active: true, name: state.persona, title: p.title, content: p.content, rules: p.bundled_rules }
|
|
174
147
|
} catch {
|
|
175
|
-
return { active: false, name:
|
|
148
|
+
return { active: false, name: state.persona, error: 'Not found on server' }
|
|
176
149
|
}
|
|
177
150
|
},
|
|
178
151
|
})
|
|
179
152
|
.command('use', {
|
|
180
153
|
description: 'Activate a persona',
|
|
181
154
|
args: z.object({
|
|
182
|
-
name: z.string().describe('Persona name
|
|
155
|
+
name: z.string().describe('Persona name'),
|
|
183
156
|
}),
|
|
184
157
|
options: z.object({
|
|
185
|
-
|
|
158
|
+
project: z.boolean().default(false).describe('Apply at project scope'),
|
|
186
159
|
}),
|
|
187
160
|
async run(c) {
|
|
188
|
-
await requireBrainjarDir()
|
|
189
161
|
const name = normalizeSlug(c.args.name, 'persona name')
|
|
190
|
-
const
|
|
191
|
-
|
|
162
|
+
const api = await getApi()
|
|
163
|
+
|
|
164
|
+
// Validate and get bundled rules
|
|
165
|
+
let personaData: ApiPersona
|
|
192
166
|
try {
|
|
193
|
-
|
|
194
|
-
} catch {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
})
|
|
167
|
+
personaData = await api.get<ApiPersona>(`/api/v1/personas/${name}`)
|
|
168
|
+
} catch (e) {
|
|
169
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
170
|
+
throw createError(ErrorCode.PERSONA_NOT_FOUND, { params: [name] })
|
|
171
|
+
}
|
|
172
|
+
throw e
|
|
200
173
|
}
|
|
201
174
|
|
|
202
|
-
const
|
|
175
|
+
const bundledRules = personaData.bundled_rules
|
|
203
176
|
|
|
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
|
-
}
|
|
177
|
+
const mutationOpts = c.options.project
|
|
178
|
+
? { project: basename(process.cwd()) }
|
|
179
|
+
: undefined
|
|
180
|
+
await putState(api, {
|
|
181
|
+
persona_slug: name,
|
|
182
|
+
rule_slugs: bundledRules.length > 0 ? bundledRules : undefined,
|
|
183
|
+
}, mutationOpts)
|
|
184
|
+
|
|
185
|
+
await sync({ api })
|
|
186
|
+
if (c.options.project) await sync({ api, project: true })
|
|
223
187
|
|
|
224
|
-
const result: Record<string, unknown> = { activated: name,
|
|
225
|
-
if (
|
|
188
|
+
const result: Record<string, unknown> = { activated: name, project: c.options.project }
|
|
189
|
+
if (bundledRules.length > 0) result.rules = bundledRules
|
|
226
190
|
return result
|
|
227
191
|
},
|
|
228
192
|
})
|
|
229
193
|
.command('drop', {
|
|
230
194
|
description: 'Deactivate the current persona',
|
|
231
195
|
options: z.object({
|
|
232
|
-
|
|
196
|
+
project: z.boolean().default(false).describe('Remove project persona override or deactivate workspace persona'),
|
|
233
197
|
}),
|
|
234
198
|
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 }
|
|
199
|
+
const api = await getApi()
|
|
200
|
+
|
|
201
|
+
const mutationOpts = c.options.project
|
|
202
|
+
? { project: basename(process.cwd()) }
|
|
203
|
+
: undefined
|
|
204
|
+
await putState(api, { persona_slug: null }, mutationOpts)
|
|
205
|
+
|
|
206
|
+
await sync({ api })
|
|
207
|
+
if (c.options.project) await sync({ api, project: true })
|
|
208
|
+
|
|
209
|
+
return { deactivated: true, project: c.options.project }
|
|
258
210
|
},
|
|
259
211
|
})
|