@brainjar/cli 0.5.2 → 0.6.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/package.json +1 -1
- package/src/api-types.ts +21 -0
- package/src/commands/brain.ts +21 -2
- package/src/commands/persona.ts +102 -24
- package/src/commands/rules.ts +99 -18
- package/src/commands/soul.ts +102 -23
- package/src/daemon.ts +8 -1
- package/src/pack.ts +8 -2
- package/src/upgrade.ts +10 -1
package/package.json
CHANGED
package/src/api-types.ts
CHANGED
|
@@ -107,6 +107,27 @@ export interface ApiComposeResult {
|
|
|
107
107
|
warnings?: string[]
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// --- Content version types ---
|
|
111
|
+
|
|
112
|
+
export interface ApiVersionSummary {
|
|
113
|
+
version: number
|
|
114
|
+
created_at: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ApiVersionList {
|
|
118
|
+
versions: ApiVersionSummary[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ApiContentVersion {
|
|
122
|
+
id: string
|
|
123
|
+
content_type: string
|
|
124
|
+
slug: string
|
|
125
|
+
version: number
|
|
126
|
+
content: string | null
|
|
127
|
+
metadata: Record<string, unknown> | null
|
|
128
|
+
created_at: string
|
|
129
|
+
}
|
|
130
|
+
|
|
110
131
|
// --- Content bundle types (export/import) ---
|
|
111
132
|
|
|
112
133
|
export interface BundleSoul {
|
package/src/commands/brain.ts
CHANGED
|
@@ -160,7 +160,26 @@ export const brain = Cli.create('brain', {
|
|
|
160
160
|
},
|
|
161
161
|
})
|
|
162
162
|
.command('drop', {
|
|
163
|
-
description: '
|
|
163
|
+
description: 'Deactivate the current brain — clears soul, persona, and rules',
|
|
164
|
+
options: z.object({
|
|
165
|
+
project: z.boolean().default(false).describe('Remove project brain override or deactivate workspace brain'),
|
|
166
|
+
}),
|
|
167
|
+
async run(c) {
|
|
168
|
+
const api = await getApi()
|
|
169
|
+
|
|
170
|
+
const mutationOpts = c.options.project
|
|
171
|
+
? { project: basename(process.cwd()) }
|
|
172
|
+
: undefined
|
|
173
|
+
await putState(api, { soul_slug: '', persona_slug: '', rule_slugs: [] }, mutationOpts)
|
|
174
|
+
|
|
175
|
+
await sync({ api })
|
|
176
|
+
if (c.options.project) await sync({ api, project: true })
|
|
177
|
+
|
|
178
|
+
return { deactivated: true, project: c.options.project }
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
.command('delete', {
|
|
182
|
+
description: 'Delete a brain permanently',
|
|
164
183
|
args: z.object({
|
|
165
184
|
name: z.string().describe('Brain name to delete'),
|
|
166
185
|
}),
|
|
@@ -177,6 +196,6 @@ export const brain = Cli.create('brain', {
|
|
|
177
196
|
throw e
|
|
178
197
|
}
|
|
179
198
|
|
|
180
|
-
return {
|
|
199
|
+
return { deleted: name }
|
|
181
200
|
},
|
|
182
201
|
})
|
package/src/commands/persona.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ErrorCode, createError } from '../errors.js'
|
|
|
6
6
|
import { normalizeSlug, getEffectiveState, getStateOverride, putState } from '../state.js'
|
|
7
7
|
import { sync } from '../sync.js'
|
|
8
8
|
import { getApi } from '../client.js'
|
|
9
|
-
import type { ApiPersona, ApiPersonaList, ApiRuleList } from '../api-types.js'
|
|
9
|
+
import type { ApiPersona, ApiPersonaList, ApiRuleList, ApiVersionList, ApiContentVersion } from '../api-types.js'
|
|
10
10
|
|
|
11
11
|
export const persona = Cli.create('persona', {
|
|
12
12
|
description: 'Manage personas — role behavior and workflow for the agent',
|
|
@@ -17,6 +17,7 @@ export const persona = Cli.create('persona', {
|
|
|
17
17
|
name: z.string().describe('Persona name'),
|
|
18
18
|
}),
|
|
19
19
|
options: z.object({
|
|
20
|
+
content: z.string().optional().describe('Persona content (if omitted, creates with a starter template you can edit)'),
|
|
20
21
|
description: z.string().optional().describe('One-line description of the persona'),
|
|
21
22
|
rules: z.array(z.string()).optional().describe('Rules to bundle with this persona'),
|
|
22
23
|
}),
|
|
@@ -50,24 +51,29 @@ export const persona = Cli.create('persona', {
|
|
|
50
51
|
|
|
51
52
|
const effectiveRules = rulesList
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
lines
|
|
54
|
+
let content: string
|
|
55
|
+
if (c.options.content) {
|
|
56
|
+
content = c.options.content.trim()
|
|
57
|
+
} else {
|
|
58
|
+
const lines: string[] = []
|
|
59
|
+
lines.push(`# ${name}`)
|
|
60
|
+
lines.push('')
|
|
61
|
+
if (c.options.description) {
|
|
62
|
+
lines.push(c.options.description)
|
|
63
|
+
}
|
|
64
|
+
lines.push('')
|
|
65
|
+
lines.push('## Direct mode')
|
|
66
|
+
lines.push('- ')
|
|
67
|
+
lines.push('')
|
|
68
|
+
lines.push('## Subagent mode')
|
|
69
|
+
lines.push('- ')
|
|
70
|
+
lines.push('')
|
|
71
|
+
lines.push('## Always')
|
|
72
|
+
lines.push('- ')
|
|
73
|
+
lines.push('')
|
|
74
|
+
content = lines.join('\n')
|
|
58
75
|
}
|
|
59
|
-
|
|
60
|
-
lines.push('## Direct mode')
|
|
61
|
-
lines.push('- ')
|
|
62
|
-
lines.push('')
|
|
63
|
-
lines.push('## Subagent mode')
|
|
64
|
-
lines.push('- ')
|
|
65
|
-
lines.push('')
|
|
66
|
-
lines.push('## Always')
|
|
67
|
-
lines.push('- ')
|
|
68
|
-
lines.push('')
|
|
69
|
-
|
|
70
|
-
const content = lines.join('\n')
|
|
76
|
+
|
|
71
77
|
await api.put<ApiPersona>(`/api/v1/personas/${name}`, {
|
|
72
78
|
content,
|
|
73
79
|
bundled_rules: effectiveRules,
|
|
@@ -87,11 +93,12 @@ export const persona = Cli.create('persona', {
|
|
|
87
93
|
},
|
|
88
94
|
})
|
|
89
95
|
.command('update', {
|
|
90
|
-
description: 'Update a persona\'s content (reads from stdin)',
|
|
96
|
+
description: 'Update a persona\'s content (reads from stdin or --content)',
|
|
91
97
|
args: z.object({
|
|
92
98
|
name: z.string().describe('Persona name'),
|
|
93
99
|
}),
|
|
94
100
|
options: z.object({
|
|
101
|
+
content: z.string().optional().describe('Persona content (reads from stdin if omitted)'),
|
|
95
102
|
rules: z.array(z.string()).optional().describe('Update bundled rules'),
|
|
96
103
|
}),
|
|
97
104
|
async run(c) {
|
|
@@ -109,11 +116,14 @@ export const persona = Cli.create('persona', {
|
|
|
109
116
|
throw e
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
chunks
|
|
119
|
+
let content = c.options.content?.trim()
|
|
120
|
+
if (!content) {
|
|
121
|
+
const chunks: Uint8Array[] = []
|
|
122
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
123
|
+
chunks.push(chunk)
|
|
124
|
+
}
|
|
125
|
+
content = Buffer.concat(chunks).toString().trim()
|
|
115
126
|
}
|
|
116
|
-
const content = Buffer.concat(chunks).toString().trim()
|
|
117
127
|
|
|
118
128
|
// Validate rules if provided
|
|
119
129
|
const rulesList = c.options.rules
|
|
@@ -157,6 +167,7 @@ export const persona = Cli.create('persona', {
|
|
|
157
167
|
options: z.object({
|
|
158
168
|
project: z.boolean().default(false).describe('Show project persona override (if any)'),
|
|
159
169
|
short: z.boolean().default(false).describe('Print only the active persona name'),
|
|
170
|
+
version: z.number().optional().describe('Show a specific version from history'),
|
|
160
171
|
}),
|
|
161
172
|
async run(c) {
|
|
162
173
|
const api = await getApi()
|
|
@@ -167,6 +178,14 @@ export const persona = Cli.create('persona', {
|
|
|
167
178
|
return state.persona ?? 'none'
|
|
168
179
|
}
|
|
169
180
|
|
|
181
|
+
if (c.options.version) {
|
|
182
|
+
const name = c.args.name
|
|
183
|
+
if (!name) throw createError(ErrorCode.MISSING_ARG, { message: 'Name is required when using --version' })
|
|
184
|
+
const slug = normalizeSlug(name, 'persona name')
|
|
185
|
+
const v = await api.get<ApiContentVersion>(`/api/v1/personas/${slug}/versions/${c.options.version}`)
|
|
186
|
+
return { name: slug, version: v.version, content: v.content, metadata: v.metadata, created_at: v.created_at }
|
|
187
|
+
}
|
|
188
|
+
|
|
170
189
|
if (c.args.name) {
|
|
171
190
|
const name = normalizeSlug(c.args.name, 'persona name')
|
|
172
191
|
try {
|
|
@@ -204,6 +223,40 @@ export const persona = Cli.create('persona', {
|
|
|
204
223
|
}
|
|
205
224
|
},
|
|
206
225
|
})
|
|
226
|
+
.command('history', {
|
|
227
|
+
description: 'List version history for a persona',
|
|
228
|
+
args: z.object({
|
|
229
|
+
name: z.string().describe('Persona name'),
|
|
230
|
+
}),
|
|
231
|
+
async run(c) {
|
|
232
|
+
const name = normalizeSlug(c.args.name, 'persona name')
|
|
233
|
+
const api = await getApi()
|
|
234
|
+
const result = await api.get<ApiVersionList>(`/api/v1/personas/${name}/versions`)
|
|
235
|
+
return { name, versions: result.versions }
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
.command('revert', {
|
|
239
|
+
description: 'Restore a persona to a previous version',
|
|
240
|
+
args: z.object({
|
|
241
|
+
name: z.string().describe('Persona name'),
|
|
242
|
+
}),
|
|
243
|
+
options: z.object({
|
|
244
|
+
to: z.number().describe('Version number to restore'),
|
|
245
|
+
}),
|
|
246
|
+
async run(c) {
|
|
247
|
+
const name = normalizeSlug(c.args.name, 'persona name')
|
|
248
|
+
const api = await getApi()
|
|
249
|
+
const v = await api.get<ApiContentVersion>(`/api/v1/personas/${name}/versions/${c.options.to}`)
|
|
250
|
+
if (!v.content) throw createError(ErrorCode.BAD_REQUEST, { message: 'Version has no content to restore' })
|
|
251
|
+
const bundledRules = (v.metadata as { bundled_rules?: string[] })?.bundled_rules ?? []
|
|
252
|
+
await api.put<ApiPersona>(`/api/v1/personas/${name}`, { content: v.content, bundled_rules: bundledRules })
|
|
253
|
+
|
|
254
|
+
const state = await getEffectiveState(api)
|
|
255
|
+
if (state.persona === name) await sync({ api })
|
|
256
|
+
|
|
257
|
+
return { reverted: name, to_version: c.options.to }
|
|
258
|
+
},
|
|
259
|
+
})
|
|
207
260
|
.command('use', {
|
|
208
261
|
description: 'Activate a persona',
|
|
209
262
|
args: z.object({
|
|
@@ -245,6 +298,31 @@ export const persona = Cli.create('persona', {
|
|
|
245
298
|
return result
|
|
246
299
|
},
|
|
247
300
|
})
|
|
301
|
+
.command('delete', {
|
|
302
|
+
description: 'Delete a persona permanently',
|
|
303
|
+
args: z.object({
|
|
304
|
+
name: z.string().describe('Persona name to delete'),
|
|
305
|
+
}),
|
|
306
|
+
async run(c) {
|
|
307
|
+
const name = normalizeSlug(c.args.name, 'persona name')
|
|
308
|
+
const api = await getApi()
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
await api.delete(`/api/v1/personas/${name}`)
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
314
|
+
throw createError(ErrorCode.PERSONA_NOT_FOUND, { params: [name] })
|
|
315
|
+
}
|
|
316
|
+
throw e
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If this persona was active, sync to reflect removal
|
|
320
|
+
const state = await getEffectiveState(api)
|
|
321
|
+
if (state.persona === name) await sync({ api })
|
|
322
|
+
|
|
323
|
+
return { deleted: name }
|
|
324
|
+
},
|
|
325
|
+
})
|
|
248
326
|
.command('drop', {
|
|
249
327
|
description: 'Deactivate the current persona',
|
|
250
328
|
options: z.object({
|
|
@@ -256,7 +334,7 @@ export const persona = Cli.create('persona', {
|
|
|
256
334
|
const mutationOpts = c.options.project
|
|
257
335
|
? { project: basename(process.cwd()) }
|
|
258
336
|
: undefined
|
|
259
|
-
await putState(api, { persona_slug:
|
|
337
|
+
await putState(api, { persona_slug: '' }, mutationOpts)
|
|
260
338
|
|
|
261
339
|
await sync({ api })
|
|
262
340
|
if (c.options.project) await sync({ api, project: true })
|
package/src/commands/rules.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ErrorCode, createError } from '../errors.js'
|
|
|
6
6
|
import { normalizeSlug, getEffectiveState, getStateOverride, putState } from '../state.js'
|
|
7
7
|
import { sync } from '../sync.js'
|
|
8
8
|
import { getApi } from '../client.js'
|
|
9
|
-
import type { ApiRule, ApiRuleList } from '../api-types.js'
|
|
9
|
+
import type { ApiRule, ApiRuleList, ApiVersionList, ApiContentVersion } from '../api-types.js'
|
|
10
10
|
|
|
11
11
|
export const rules = Cli.create('rules', {
|
|
12
12
|
description: 'Manage rules — behavioral constraints for the agent',
|
|
@@ -17,6 +17,7 @@ export const rules = Cli.create('rules', {
|
|
|
17
17
|
name: z.string().describe('Rule name'),
|
|
18
18
|
}),
|
|
19
19
|
options: z.object({
|
|
20
|
+
content: z.string().optional().describe('Rule content (if omitted, creates with a starter template you can edit)'),
|
|
20
21
|
description: z.string().optional().describe('One-line description of the rule'),
|
|
21
22
|
}),
|
|
22
23
|
async run(c) {
|
|
@@ -32,15 +33,20 @@ export const rules = Cli.create('rules', {
|
|
|
32
33
|
if (e instanceof IncurError && e.code !== ErrorCode.NOT_FOUND) throw e
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
let scaffold: string
|
|
37
|
+
if (c.options.content) {
|
|
38
|
+
scaffold = c.options.content.trim()
|
|
39
|
+
} else {
|
|
40
|
+
scaffold = [
|
|
41
|
+
`# ${name}`,
|
|
42
|
+
'',
|
|
43
|
+
c.options.description ?? 'Describe what this rule enforces and why.',
|
|
44
|
+
'',
|
|
45
|
+
'## Constraints',
|
|
46
|
+
'- ',
|
|
47
|
+
'',
|
|
48
|
+
].join('\n')
|
|
49
|
+
}
|
|
44
50
|
|
|
45
51
|
await api.put<ApiRule>(`/api/v1/rules/${name}`, {
|
|
46
52
|
entries: [{ name: `${name}.md`, content: scaffold }],
|
|
@@ -59,10 +65,13 @@ export const rules = Cli.create('rules', {
|
|
|
59
65
|
},
|
|
60
66
|
})
|
|
61
67
|
.command('update', {
|
|
62
|
-
description: 'Update a rule\'s content (reads from stdin)',
|
|
68
|
+
description: 'Update a rule\'s content (reads from stdin or --content)',
|
|
63
69
|
args: z.object({
|
|
64
70
|
name: z.string().describe('Rule name'),
|
|
65
71
|
}),
|
|
72
|
+
options: z.object({
|
|
73
|
+
content: z.string().optional().describe('Rule content (reads from stdin if omitted)'),
|
|
74
|
+
}),
|
|
66
75
|
async run(c) {
|
|
67
76
|
const name = normalizeSlug(c.args.name, 'rule name')
|
|
68
77
|
const api = await getApi()
|
|
@@ -77,11 +86,14 @@ export const rules = Cli.create('rules', {
|
|
|
77
86
|
throw e
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
chunks
|
|
89
|
+
let content = c.options.content?.trim()
|
|
90
|
+
if (!content) {
|
|
91
|
+
const chunks: Uint8Array[] = []
|
|
92
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
93
|
+
chunks.push(chunk)
|
|
94
|
+
}
|
|
95
|
+
content = Buffer.concat(chunks).toString().trim()
|
|
83
96
|
}
|
|
84
|
-
const content = Buffer.concat(chunks).toString().trim()
|
|
85
97
|
|
|
86
98
|
if (!content) {
|
|
87
99
|
throw createError(ErrorCode.MISSING_ARG, {
|
|
@@ -133,10 +145,20 @@ export const rules = Cli.create('rules', {
|
|
|
133
145
|
args: z.object({
|
|
134
146
|
name: z.string().describe('Rule name to show'),
|
|
135
147
|
}),
|
|
148
|
+
options: z.object({
|
|
149
|
+
version: z.number().optional().describe('Show a specific version from history'),
|
|
150
|
+
}),
|
|
136
151
|
async run(c) {
|
|
137
152
|
const name = normalizeSlug(c.args.name, 'rule name')
|
|
138
153
|
const api = await getApi()
|
|
139
154
|
|
|
155
|
+
if (c.options.version) {
|
|
156
|
+
const v = await api.get<ApiContentVersion>(`/api/v1/rules/${name}/versions/${c.options.version}`)
|
|
157
|
+
const entries = (v.metadata as { entries?: Array<{ sort_key: number; content: string }> })?.entries ?? []
|
|
158
|
+
const content = entries.map(e => e.content.trim()).join('\n\n')
|
|
159
|
+
return { name, version: v.version, content, created_at: v.created_at }
|
|
160
|
+
}
|
|
161
|
+
|
|
140
162
|
try {
|
|
141
163
|
const rule = await api.get<ApiRule>(`/api/v1/rules/${name}`)
|
|
142
164
|
const content = rule.entries.map(e => e.content.trim()).join('\n\n')
|
|
@@ -149,6 +171,40 @@ export const rules = Cli.create('rules', {
|
|
|
149
171
|
}
|
|
150
172
|
},
|
|
151
173
|
})
|
|
174
|
+
.command('history', {
|
|
175
|
+
description: 'List version history for a rule',
|
|
176
|
+
args: z.object({
|
|
177
|
+
name: z.string().describe('Rule name'),
|
|
178
|
+
}),
|
|
179
|
+
async run(c) {
|
|
180
|
+
const name = normalizeSlug(c.args.name, 'rule name')
|
|
181
|
+
const api = await getApi()
|
|
182
|
+
const result = await api.get<ApiVersionList>(`/api/v1/rules/${name}/versions`)
|
|
183
|
+
return { name, versions: result.versions }
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
.command('revert', {
|
|
187
|
+
description: 'Restore a rule to a previous version',
|
|
188
|
+
args: z.object({
|
|
189
|
+
name: z.string().describe('Rule name'),
|
|
190
|
+
}),
|
|
191
|
+
options: z.object({
|
|
192
|
+
to: z.number().describe('Version number to restore'),
|
|
193
|
+
}),
|
|
194
|
+
async run(c) {
|
|
195
|
+
const name = normalizeSlug(c.args.name, 'rule name')
|
|
196
|
+
const api = await getApi()
|
|
197
|
+
const v = await api.get<ApiContentVersion>(`/api/v1/rules/${name}/versions/${c.options.to}`)
|
|
198
|
+
const entries = (v.metadata as { entries?: Array<{ sort_key: number; content: string }> })?.entries
|
|
199
|
+
if (!entries) throw createError(ErrorCode.BAD_REQUEST, { message: 'Version has no entries to restore' })
|
|
200
|
+
await api.put<ApiRule>(`/api/v1/rules/${name}`, { entries: entries.map(e => ({ name: `${name}.md`, content: e.content })) })
|
|
201
|
+
|
|
202
|
+
const state = await getEffectiveState(api)
|
|
203
|
+
if (state.rules.includes(name)) await sync({ api })
|
|
204
|
+
|
|
205
|
+
return { reverted: name, to_version: c.options.to }
|
|
206
|
+
},
|
|
207
|
+
})
|
|
152
208
|
.command('add', {
|
|
153
209
|
description: 'Activate a rule or rule pack',
|
|
154
210
|
args: z.object({
|
|
@@ -182,13 +238,38 @@ export const rules = Cli.create('rules', {
|
|
|
182
238
|
return { activated: name, project: c.options.project }
|
|
183
239
|
},
|
|
184
240
|
})
|
|
185
|
-
.command('
|
|
241
|
+
.command('delete', {
|
|
242
|
+
description: 'Delete a rule permanently',
|
|
243
|
+
args: z.object({
|
|
244
|
+
name: z.string().describe('Rule name to delete'),
|
|
245
|
+
}),
|
|
246
|
+
async run(c) {
|
|
247
|
+
const name = normalizeSlug(c.args.name, 'rule name')
|
|
248
|
+
const api = await getApi()
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await api.delete(`/api/v1/rules/${name}`)
|
|
252
|
+
} catch (e) {
|
|
253
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
254
|
+
throw createError(ErrorCode.RULE_NOT_FOUND, { params: [name] })
|
|
255
|
+
}
|
|
256
|
+
throw e
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// If this rule was active, sync to reflect removal
|
|
260
|
+
const state = await getEffectiveState(api)
|
|
261
|
+
if (state.rules.includes(name)) await sync({ api })
|
|
262
|
+
|
|
263
|
+
return { deleted: name }
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
.command('drop', {
|
|
186
267
|
description: 'Deactivate a rule',
|
|
187
268
|
args: z.object({
|
|
188
|
-
name: z.string().describe('Rule name to
|
|
269
|
+
name: z.string().describe('Rule name to deactivate'),
|
|
189
270
|
}),
|
|
190
271
|
options: z.object({
|
|
191
|
-
project: z.boolean().default(false).describe('
|
|
272
|
+
project: z.boolean().default(false).describe('Deactivate rule at project scope'),
|
|
192
273
|
}),
|
|
193
274
|
async run(c) {
|
|
194
275
|
const name = normalizeSlug(c.args.name, 'rule name')
|
package/src/commands/soul.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ErrorCode, createError } from '../errors.js'
|
|
|
6
6
|
import { normalizeSlug, getEffectiveState, getStateOverride, putState } from '../state.js'
|
|
7
7
|
import { sync } from '../sync.js'
|
|
8
8
|
import { getApi } from '../client.js'
|
|
9
|
-
import type { ApiSoul, ApiSoulList } from '../api-types.js'
|
|
9
|
+
import type { ApiSoul, ApiSoulList, ApiVersionList, ApiContentVersion } from '../api-types.js'
|
|
10
10
|
|
|
11
11
|
export const soul = Cli.create('soul', {
|
|
12
12
|
description: 'Manage soul — personality and values for the agent',
|
|
@@ -17,6 +17,7 @@ export const soul = Cli.create('soul', {
|
|
|
17
17
|
name: z.string().describe('Soul name'),
|
|
18
18
|
}),
|
|
19
19
|
options: z.object({
|
|
20
|
+
content: z.string().optional().describe('Soul content (if omitted, creates with a starter template you can edit)'),
|
|
20
21
|
description: z.string().optional().describe('One-line description of the soul'),
|
|
21
22
|
}),
|
|
22
23
|
async run(c) {
|
|
@@ -32,24 +33,29 @@ export const soul = Cli.create('soul', {
|
|
|
32
33
|
if (e instanceof IncurError && e.code !== ErrorCode.NOT_FOUND) throw e
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
lines
|
|
36
|
+
let content: string
|
|
37
|
+
if (c.options.content) {
|
|
38
|
+
content = c.options.content.trim()
|
|
39
|
+
} else {
|
|
40
|
+
const lines: string[] = []
|
|
41
|
+
lines.push(`# ${name}`)
|
|
40
42
|
lines.push('')
|
|
43
|
+
if (c.options.description) {
|
|
44
|
+
lines.push(c.options.description)
|
|
45
|
+
lines.push('')
|
|
46
|
+
}
|
|
47
|
+
lines.push('## Voice')
|
|
48
|
+
lines.push('- ')
|
|
49
|
+
lines.push('')
|
|
50
|
+
lines.push('## Character')
|
|
51
|
+
lines.push('- ')
|
|
52
|
+
lines.push('')
|
|
53
|
+
lines.push('## Standards')
|
|
54
|
+
lines.push('- ')
|
|
55
|
+
lines.push('')
|
|
56
|
+
content = lines.join('\n')
|
|
41
57
|
}
|
|
42
|
-
|
|
43
|
-
lines.push('- ')
|
|
44
|
-
lines.push('')
|
|
45
|
-
lines.push('## Character')
|
|
46
|
-
lines.push('- ')
|
|
47
|
-
lines.push('')
|
|
48
|
-
lines.push('## Standards')
|
|
49
|
-
lines.push('- ')
|
|
50
|
-
lines.push('')
|
|
51
|
-
|
|
52
|
-
const content = lines.join('\n')
|
|
58
|
+
|
|
53
59
|
await api.put<ApiSoul>(`/api/v1/souls/${name}`, { content })
|
|
54
60
|
|
|
55
61
|
if (c.agent || c.formatExplicit) {
|
|
@@ -65,10 +71,13 @@ export const soul = Cli.create('soul', {
|
|
|
65
71
|
},
|
|
66
72
|
})
|
|
67
73
|
.command('update', {
|
|
68
|
-
description: 'Update a soul\'s content (reads from stdin)',
|
|
74
|
+
description: 'Update a soul\'s content (reads from stdin or --content)',
|
|
69
75
|
args: z.object({
|
|
70
76
|
name: z.string().describe('Soul name'),
|
|
71
77
|
}),
|
|
78
|
+
options: z.object({
|
|
79
|
+
content: z.string().optional().describe('Soul content (reads from stdin if omitted)'),
|
|
80
|
+
}),
|
|
72
81
|
async run(c) {
|
|
73
82
|
const name = normalizeSlug(c.args.name, 'soul name')
|
|
74
83
|
const api = await getApi()
|
|
@@ -83,11 +92,14 @@ export const soul = Cli.create('soul', {
|
|
|
83
92
|
throw e
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
chunks
|
|
95
|
+
let content = c.options.content?.trim()
|
|
96
|
+
if (!content) {
|
|
97
|
+
const chunks: Uint8Array[] = []
|
|
98
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
99
|
+
chunks.push(chunk)
|
|
100
|
+
}
|
|
101
|
+
content = Buffer.concat(chunks).toString().trim()
|
|
89
102
|
}
|
|
90
|
-
const content = Buffer.concat(chunks).toString().trim()
|
|
91
103
|
|
|
92
104
|
if (!content) {
|
|
93
105
|
throw createError(ErrorCode.MISSING_ARG, {
|
|
@@ -121,6 +133,7 @@ export const soul = Cli.create('soul', {
|
|
|
121
133
|
options: z.object({
|
|
122
134
|
project: z.boolean().default(false).describe('Show project soul override (if any)'),
|
|
123
135
|
short: z.boolean().default(false).describe('Print only the active soul name'),
|
|
136
|
+
version: z.number().optional().describe('Show a specific version from history'),
|
|
124
137
|
}),
|
|
125
138
|
async run(c) {
|
|
126
139
|
const api = await getApi()
|
|
@@ -131,6 +144,14 @@ export const soul = Cli.create('soul', {
|
|
|
131
144
|
return state.soul ?? 'none'
|
|
132
145
|
}
|
|
133
146
|
|
|
147
|
+
if (c.options.version) {
|
|
148
|
+
const name = c.args.name
|
|
149
|
+
if (!name) throw createError(ErrorCode.MISSING_ARG, { message: 'Name is required when using --version' })
|
|
150
|
+
const slug = normalizeSlug(name, 'soul name')
|
|
151
|
+
const v = await api.get<ApiContentVersion>(`/api/v1/souls/${slug}/versions/${c.options.version}`)
|
|
152
|
+
return { name: slug, version: v.version, content: v.content, created_at: v.created_at }
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
if (c.args.name) {
|
|
135
156
|
const name = normalizeSlug(c.args.name, 'soul name')
|
|
136
157
|
try {
|
|
@@ -168,6 +189,39 @@ export const soul = Cli.create('soul', {
|
|
|
168
189
|
}
|
|
169
190
|
},
|
|
170
191
|
})
|
|
192
|
+
.command('history', {
|
|
193
|
+
description: 'List version history for a soul',
|
|
194
|
+
args: z.object({
|
|
195
|
+
name: z.string().describe('Soul name'),
|
|
196
|
+
}),
|
|
197
|
+
async run(c) {
|
|
198
|
+
const name = normalizeSlug(c.args.name, 'soul name')
|
|
199
|
+
const api = await getApi()
|
|
200
|
+
const result = await api.get<ApiVersionList>(`/api/v1/souls/${name}/versions`)
|
|
201
|
+
return { name, versions: result.versions }
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
.command('revert', {
|
|
205
|
+
description: 'Restore a soul to a previous version',
|
|
206
|
+
args: z.object({
|
|
207
|
+
name: z.string().describe('Soul name'),
|
|
208
|
+
}),
|
|
209
|
+
options: z.object({
|
|
210
|
+
to: z.number().describe('Version number to restore'),
|
|
211
|
+
}),
|
|
212
|
+
async run(c) {
|
|
213
|
+
const name = normalizeSlug(c.args.name, 'soul name')
|
|
214
|
+
const api = await getApi()
|
|
215
|
+
const v = await api.get<ApiContentVersion>(`/api/v1/souls/${name}/versions/${c.options.to}`)
|
|
216
|
+
if (!v.content) throw createError(ErrorCode.BAD_REQUEST, { message: 'Version has no content to restore' })
|
|
217
|
+
await api.put<ApiSoul>(`/api/v1/souls/${name}`, { content: v.content })
|
|
218
|
+
|
|
219
|
+
const state = await getEffectiveState(api)
|
|
220
|
+
if (state.soul === name) await sync({ api })
|
|
221
|
+
|
|
222
|
+
return { reverted: name, to_version: c.options.to }
|
|
223
|
+
},
|
|
224
|
+
})
|
|
171
225
|
.command('use', {
|
|
172
226
|
description: 'Activate a soul',
|
|
173
227
|
args: z.object({
|
|
@@ -201,6 +255,31 @@ export const soul = Cli.create('soul', {
|
|
|
201
255
|
return { activated: name, project: c.options.project }
|
|
202
256
|
},
|
|
203
257
|
})
|
|
258
|
+
.command('delete', {
|
|
259
|
+
description: 'Delete a soul permanently',
|
|
260
|
+
args: z.object({
|
|
261
|
+
name: z.string().describe('Soul name to delete'),
|
|
262
|
+
}),
|
|
263
|
+
async run(c) {
|
|
264
|
+
const name = normalizeSlug(c.args.name, 'soul name')
|
|
265
|
+
const api = await getApi()
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await api.delete(`/api/v1/souls/${name}`)
|
|
269
|
+
} catch (e) {
|
|
270
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
271
|
+
throw createError(ErrorCode.SOUL_NOT_FOUND, { params: [name] })
|
|
272
|
+
}
|
|
273
|
+
throw e
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If this soul was active, sync to reflect removal
|
|
277
|
+
const state = await getEffectiveState(api)
|
|
278
|
+
if (state.soul === name) await sync({ api })
|
|
279
|
+
|
|
280
|
+
return { deleted: name }
|
|
281
|
+
},
|
|
282
|
+
})
|
|
204
283
|
.command('drop', {
|
|
205
284
|
description: 'Deactivate the current soul',
|
|
206
285
|
options: z.object({
|
|
@@ -212,7 +291,7 @@ export const soul = Cli.create('soul', {
|
|
|
212
291
|
const mutationOpts = c.options.project
|
|
213
292
|
? { project: basename(process.cwd()) }
|
|
214
293
|
: undefined
|
|
215
|
-
await putState(api, { soul_slug:
|
|
294
|
+
await putState(api, { soul_slug: '' }, mutationOpts)
|
|
216
295
|
|
|
217
296
|
await sync({ api })
|
|
218
297
|
if (c.options.project) await sync({ api, project: true })
|
package/src/daemon.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { readConfig, activeContext, localContext } from './config.js'
|
|
|
8
8
|
import { ErrorCode, createError } from './errors.js'
|
|
9
9
|
|
|
10
10
|
export const DIST_BASE = 'https://get.brainjar.sh/brainjar-server'
|
|
11
|
+
export const SEMVER_RE = /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?(\+[a-zA-Z0-9.]+)?$/
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Compare two semver strings. Returns -1, 0, or 1.
|
|
@@ -156,7 +157,13 @@ export async function fetchLatestVersion(distBase: string = DIST_BASE): Promise<
|
|
|
156
157
|
hint: 'Check your network connection or try again later.',
|
|
157
158
|
})
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
+
const version = (await response.text()).trim()
|
|
161
|
+
if (!SEMVER_RE.test(version)) {
|
|
162
|
+
throw createError(ErrorCode.VALIDATION_ERROR, {
|
|
163
|
+
message: `Invalid server version string from distribution: "${version}"`,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
return version
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
/**
|
package/src/pack.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, readdir, writeFile, access, mkdir, stat } from 'node:fs/promises'
|
|
2
|
-
import { join, dirname } from 'node:path'
|
|
2
|
+
import { join, dirname, basename } from 'node:path'
|
|
3
3
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
4
4
|
import { Errors } from 'incur'
|
|
5
5
|
import { ErrorCode, createError } from './errors.js'
|
|
@@ -127,7 +127,13 @@ export async function exportPack(brainName: string, options: ExportOptions = {})
|
|
|
127
127
|
const ruleDir = join(packDir, 'rules', ruleSlug)
|
|
128
128
|
await mkdir(ruleDir, { recursive: true })
|
|
129
129
|
for (const entry of rule.entries) {
|
|
130
|
-
|
|
130
|
+
const safeName = basename(entry.name)
|
|
131
|
+
if (!safeName || safeName !== entry.name) {
|
|
132
|
+
throw createError(ErrorCode.PACK_INVALID_MANIFEST, {
|
|
133
|
+
message: `Rule entry name contains invalid path: "${entry.name}"`,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
await writeFile(join(ruleDir, safeName), entry.content)
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
}
|
package/src/upgrade.ts
CHANGED
|
@@ -2,14 +2,22 @@ import { execFile } from 'node:child_process'
|
|
|
2
2
|
import {
|
|
3
3
|
healthCheck,
|
|
4
4
|
start,
|
|
5
|
-
stop,
|
|
6
5
|
status as daemonStatus,
|
|
7
6
|
upgradeServer,
|
|
7
|
+
SEMVER_RE,
|
|
8
8
|
} from './daemon.js'
|
|
9
9
|
import { checkForUpdates } from './version-check.js'
|
|
10
10
|
import { ErrorCode, createError } from './errors.js'
|
|
11
11
|
import pkg from '../package.json'
|
|
12
12
|
|
|
13
|
+
function validateVersion(v: string, label: string): void {
|
|
14
|
+
if (!SEMVER_RE.test(v)) {
|
|
15
|
+
throw createError(ErrorCode.VALIDATION_ERROR, {
|
|
16
|
+
message: `Invalid ${label} version string: "${v}"`,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
export interface ComponentResult {
|
|
14
22
|
upgraded: boolean
|
|
15
23
|
from: string
|
|
@@ -59,6 +67,7 @@ export async function upgradeCli(): Promise<ComponentResult> {
|
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
const latestVersion = updates.cli.latest
|
|
70
|
+
validateVersion(latestVersion, 'CLI')
|
|
62
71
|
const pm = detectPackageManager()
|
|
63
72
|
|
|
64
73
|
try {
|