@brainjar/cli 0.2.3 → 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 +11 -10
- package/package.json +2 -2
- package/src/api-types.ts +155 -0
- package/src/cli.ts +5 -3
- 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 +66 -42
- 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 +55 -51
- package/src/commands/soul.ts +75 -110
- package/src/commands/status.ts +37 -78
- 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 -8
- package/src/seeds.ts +62 -105
- package/src/state.ts +12 -397
- package/src/sync.ts +61 -102
- package/src/version-check.ts +137 -0
- package/src/brain.ts +0 -69
- package/src/commands/identity.ts +0 -276
- package/src/engines/bitwarden.ts +0 -105
- package/src/engines/index.ts +0 -12
- package/src/engines/types.ts +0 -12
- 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/brain.ts
CHANGED
|
@@ -1,24 +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 { 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 { readBrain, type BrainConfig } from '../brain.js'
|
|
5
|
+
import { ErrorCode, createError } from '../errors.js'
|
|
6
|
+
import { normalizeSlug, getEffectiveState, putState } from '../state.js'
|
|
21
7
|
import { sync } from '../sync.js'
|
|
8
|
+
import { getApi } from '../client.js'
|
|
9
|
+
import type { ApiBrain, ApiBrainList, ApiSoul, ApiPersona } from '../api-types.js'
|
|
22
10
|
|
|
23
11
|
export const brain = Cli.create('brain', {
|
|
24
12
|
description: 'Manage brains — full-stack configuration snapshots (soul + persona + rules)',
|
|
@@ -26,65 +14,50 @@ export const brain = Cli.create('brain', {
|
|
|
26
14
|
.command('save', {
|
|
27
15
|
description: 'Snapshot current effective state as a named brain',
|
|
28
16
|
args: z.object({
|
|
29
|
-
name: z.string().describe('Brain name
|
|
17
|
+
name: z.string().describe('Brain name'),
|
|
30
18
|
}),
|
|
31
19
|
options: z.object({
|
|
32
|
-
overwrite: z.boolean().default(false).describe('Overwrite existing brain
|
|
20
|
+
overwrite: z.boolean().default(false).describe('Overwrite existing brain'),
|
|
33
21
|
}),
|
|
34
22
|
async run(c) {
|
|
35
|
-
await requireBrainjarDir()
|
|
36
23
|
const name = normalizeSlug(c.args.name, 'brain name')
|
|
37
|
-
const
|
|
24
|
+
const api = await getApi()
|
|
38
25
|
|
|
39
26
|
// Check for existing brain
|
|
40
27
|
if (!c.options.overwrite) {
|
|
41
28
|
try {
|
|
42
|
-
await
|
|
43
|
-
throw
|
|
44
|
-
code: 'BRAIN_EXISTS',
|
|
45
|
-
message: `Brain "${name}" already exists.`,
|
|
46
|
-
hint: 'Use --overwrite to replace it, or choose a different name.',
|
|
47
|
-
})
|
|
29
|
+
await api.get<ApiBrain>(`/api/v1/brains/${name}`)
|
|
30
|
+
throw createError(ErrorCode.BRAIN_EXISTS, { params: [name] })
|
|
48
31
|
} catch (e) {
|
|
49
|
-
if (e instanceof IncurError) throw e
|
|
32
|
+
if (e instanceof IncurError && e.code === ErrorCode.BRAIN_EXISTS) throw e
|
|
33
|
+
if (e instanceof IncurError && e.code !== ErrorCode.NOT_FOUND) throw e
|
|
50
34
|
}
|
|
51
35
|
}
|
|
52
36
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const effective = mergeState(globalState, localState, envState)
|
|
58
|
-
|
|
59
|
-
if (!effective.soul.value) {
|
|
60
|
-
throw new IncurError({
|
|
61
|
-
code: 'NO_ACTIVE_SOUL',
|
|
62
|
-
message: 'Cannot save brain: no active soul.',
|
|
63
|
-
hint: 'Activate a soul first with `brainjar soul use <name>`.',
|
|
64
|
-
})
|
|
37
|
+
const effective = await getEffectiveState(api)
|
|
38
|
+
|
|
39
|
+
if (!effective.soul) {
|
|
40
|
+
throw createError(ErrorCode.NO_ACTIVE_SOUL)
|
|
65
41
|
}
|
|
66
42
|
|
|
67
|
-
if (!effective.persona
|
|
68
|
-
throw
|
|
69
|
-
code: 'NO_ACTIVE_PERSONA',
|
|
70
|
-
message: 'Cannot save brain: no active persona.',
|
|
71
|
-
hint: 'Activate a persona first with `brainjar persona use <name>`.',
|
|
72
|
-
})
|
|
43
|
+
if (!effective.persona) {
|
|
44
|
+
throw createError(ErrorCode.NO_ACTIVE_PERSONA)
|
|
73
45
|
}
|
|
74
46
|
|
|
75
47
|
const activeRules = effective.rules
|
|
76
|
-
.filter(r => !r.scope.startsWith('-'))
|
|
77
|
-
.map(r => r.value)
|
|
78
48
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
49
|
+
await api.put<ApiBrain>(`/api/v1/brains/${name}`, {
|
|
50
|
+
soul_slug: effective.soul,
|
|
51
|
+
persona_slug: effective.persona,
|
|
52
|
+
rule_slugs: activeRules,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
saved: name,
|
|
57
|
+
soul: effective.soul,
|
|
58
|
+
persona: effective.persona,
|
|
82
59
|
rules: activeRules,
|
|
83
60
|
}
|
|
84
|
-
|
|
85
|
-
await writeFile(dest, stringifyYaml(config))
|
|
86
|
-
|
|
87
|
-
return { saved: name, ...config }
|
|
88
61
|
},
|
|
89
62
|
})
|
|
90
63
|
.command('use', {
|
|
@@ -93,68 +66,77 @@ export const brain = Cli.create('brain', {
|
|
|
93
66
|
name: z.string().describe('Brain name to activate'),
|
|
94
67
|
}),
|
|
95
68
|
options: z.object({
|
|
96
|
-
|
|
69
|
+
project: z.boolean().default(false).describe('Apply brain at project scope'),
|
|
97
70
|
}),
|
|
98
71
|
async run(c) {
|
|
99
|
-
await requireBrainjarDir()
|
|
100
72
|
const name = normalizeSlug(c.args.name, 'brain name')
|
|
101
|
-
const
|
|
73
|
+
const api = await getApi()
|
|
74
|
+
|
|
75
|
+
let config: ApiBrain
|
|
76
|
+
try {
|
|
77
|
+
config = await api.get<ApiBrain>(`/api/v1/brains/${name}`)
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
80
|
+
throw createError(ErrorCode.BRAIN_NOT_FOUND, { params: [name] })
|
|
81
|
+
}
|
|
82
|
+
throw e
|
|
83
|
+
}
|
|
102
84
|
|
|
103
85
|
// Validate soul exists
|
|
104
86
|
try {
|
|
105
|
-
await
|
|
106
|
-
} catch {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
87
|
+
await api.get<ApiSoul>(`/api/v1/souls/${config.soul_slug}`)
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
90
|
+
throw createError(ErrorCode.SOUL_NOT_FOUND, {
|
|
91
|
+
params: [config.soul_slug],
|
|
92
|
+
message: `Brain "${name}" references soul "${config.soul_slug}" which does not exist.`,
|
|
93
|
+
hint: 'Create the soul first or update the brain.',
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
throw e
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
// Validate persona exists
|
|
115
100
|
try {
|
|
116
|
-
await
|
|
117
|
-
} catch {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
101
|
+
await api.get<ApiPersona>(`/api/v1/personas/${config.persona_slug}`)
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
104
|
+
throw createError(ErrorCode.PERSONA_NOT_FOUND, {
|
|
105
|
+
params: [config.persona_slug],
|
|
106
|
+
message: `Brain "${name}" references persona "${config.persona_slug}" which does not exist.`,
|
|
107
|
+
hint: 'Create the persona first or update the brain.',
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
throw e
|
|
123
111
|
}
|
|
124
112
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
})
|
|
113
|
+
const mutationOpts = c.options.project
|
|
114
|
+
? { project: basename(process.cwd()) }
|
|
115
|
+
: undefined
|
|
116
|
+
await putState(api, {
|
|
117
|
+
soul_slug: config.soul_slug,
|
|
118
|
+
persona_slug: config.persona_slug,
|
|
119
|
+
rule_slugs: config.rule_slugs,
|
|
120
|
+
}, mutationOpts)
|
|
121
|
+
|
|
122
|
+
await sync({ api })
|
|
123
|
+
if (c.options.project) await sync({ api, project: true })
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
activated: name,
|
|
127
|
+
project: c.options.project,
|
|
128
|
+
soul: config.soul_slug,
|
|
129
|
+
persona: config.persona_slug,
|
|
130
|
+
rules: config.rule_slugs,
|
|
144
131
|
}
|
|
145
|
-
|
|
146
|
-
return { activated: name, local: c.options.local, ...config }
|
|
147
132
|
},
|
|
148
133
|
})
|
|
149
134
|
.command('list', {
|
|
150
135
|
description: 'List available brains',
|
|
151
136
|
async run() {
|
|
152
|
-
await
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
.filter(f => f.endsWith('.yaml'))
|
|
156
|
-
.map(f => basename(f, '.yaml'))
|
|
157
|
-
return { brains }
|
|
137
|
+
const api = await getApi()
|
|
138
|
+
const result = await api.get<ApiBrainList>('/api/v1/brains')
|
|
139
|
+
return { brains: result.brains.map(b => b.slug) }
|
|
158
140
|
},
|
|
159
141
|
})
|
|
160
142
|
.command('show', {
|
|
@@ -163,10 +145,18 @@ export const brain = Cli.create('brain', {
|
|
|
163
145
|
name: z.string().describe('Brain name to show'),
|
|
164
146
|
}),
|
|
165
147
|
async run(c) {
|
|
166
|
-
await requireBrainjarDir()
|
|
167
148
|
const name = normalizeSlug(c.args.name, 'brain name')
|
|
168
|
-
const
|
|
169
|
-
|
|
149
|
+
const api = await getApi()
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const config = await api.get<ApiBrain>(`/api/v1/brains/${name}`)
|
|
153
|
+
return { name, soul: config.soul_slug, persona: config.persona_slug, rules: config.rule_slugs }
|
|
154
|
+
} catch (e) {
|
|
155
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
156
|
+
throw createError(ErrorCode.BRAIN_NOT_FOUND, { params: [name] })
|
|
157
|
+
}
|
|
158
|
+
throw e
|
|
159
|
+
}
|
|
170
160
|
},
|
|
171
161
|
})
|
|
172
162
|
.command('drop', {
|
|
@@ -175,22 +165,18 @@ export const brain = Cli.create('brain', {
|
|
|
175
165
|
name: z.string().describe('Brain name to delete'),
|
|
176
166
|
}),
|
|
177
167
|
async run(c) {
|
|
178
|
-
await requireBrainjarDir()
|
|
179
168
|
const name = normalizeSlug(c.args.name, 'brain name')
|
|
180
|
-
const
|
|
169
|
+
const api = await getApi()
|
|
181
170
|
|
|
182
171
|
try {
|
|
183
|
-
await
|
|
184
|
-
} catch {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
})
|
|
172
|
+
await api.delete(`/api/v1/brains/${name}`)
|
|
173
|
+
} catch (e) {
|
|
174
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
175
|
+
throw createError(ErrorCode.BRAIN_NOT_FOUND, { params: [name] })
|
|
176
|
+
}
|
|
177
|
+
throw e
|
|
190
178
|
}
|
|
191
179
|
|
|
192
|
-
await rm(file)
|
|
193
|
-
|
|
194
180
|
return { dropped: name }
|
|
195
181
|
},
|
|
196
182
|
})
|
package/src/commands/compose.ts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import { Cli, z, Errors } from 'incur'
|
|
2
2
|
|
|
3
3
|
const { IncurError } = Errors
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
readState,
|
|
9
|
-
readLocalState,
|
|
10
|
-
readEnvState,
|
|
11
|
-
mergeState,
|
|
12
|
-
requireBrainjarDir,
|
|
13
|
-
normalizeSlug,
|
|
14
|
-
parseLayerFrontmatter,
|
|
15
|
-
stripFrontmatter,
|
|
16
|
-
resolveRuleContent,
|
|
17
|
-
} from '../state.js'
|
|
18
|
-
import { readBrain } from '../brain.js'
|
|
4
|
+
import { ErrorCode, createError } from '../errors.js'
|
|
5
|
+
import { getApi } from '../client.js'
|
|
6
|
+
import type { ApiComposeResult } from '../api-types.js'
|
|
19
7
|
|
|
20
8
|
export const compose = Cli.create('compose', {
|
|
21
9
|
description: 'Assemble a full subagent prompt from a brain or ad-hoc persona',
|
|
@@ -27,129 +15,42 @@ export const compose = Cli.create('compose', {
|
|
|
27
15
|
task: z.string().optional().describe('Task description to append to the prompt'),
|
|
28
16
|
}),
|
|
29
17
|
async run(c) {
|
|
30
|
-
await requireBrainjarDir()
|
|
31
|
-
|
|
32
18
|
const brainName = c.args.brain
|
|
33
19
|
const personaFlag = c.options.persona
|
|
34
20
|
|
|
35
21
|
// Mutual exclusivity
|
|
36
22
|
if (brainName && personaFlag) {
|
|
37
|
-
throw
|
|
38
|
-
code: 'MUTUALLY_EXCLUSIVE',
|
|
23
|
+
throw createError(ErrorCode.MUTUALLY_EXCLUSIVE, {
|
|
39
24
|
message: 'Cannot specify both a brain name and --persona.',
|
|
40
25
|
hint: 'Use `brainjar compose <brain>` or `brainjar compose --persona <name>`, not both.',
|
|
41
26
|
})
|
|
42
27
|
}
|
|
43
28
|
|
|
44
29
|
if (!brainName && !personaFlag) {
|
|
45
|
-
throw
|
|
46
|
-
code: 'MISSING_ARG',
|
|
30
|
+
throw createError(ErrorCode.MISSING_ARG, {
|
|
47
31
|
message: 'Provide a brain name or --persona.',
|
|
48
32
|
hint: 'Usage: `brainjar compose <brain>` or `brainjar compose --persona <name>`.',
|
|
49
33
|
})
|
|
50
34
|
}
|
|
51
35
|
|
|
52
|
-
const
|
|
53
|
-
const warnings: string[] = []
|
|
54
|
-
let soulName: string | null = null
|
|
55
|
-
let personaName: string
|
|
56
|
-
let rulesList: string[]
|
|
57
|
-
|
|
58
|
-
if (brainName) {
|
|
59
|
-
// === Primary path: brain-driven ===
|
|
60
|
-
const config = await readBrain(brainName)
|
|
61
|
-
soulName = config.soul
|
|
62
|
-
personaName = config.persona
|
|
63
|
-
rulesList = config.rules
|
|
64
|
-
|
|
65
|
-
// Soul — from brain
|
|
66
|
-
try {
|
|
67
|
-
const raw = await readFile(join(paths.souls, `${soulName}.md`), 'utf-8')
|
|
68
|
-
sections.push(stripFrontmatter(raw))
|
|
69
|
-
} catch {
|
|
70
|
-
warnings.push(`Soul "${soulName}" not found — skipped`)
|
|
71
|
-
soulName = null
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Persona — from brain
|
|
75
|
-
let personaRaw: string
|
|
76
|
-
try {
|
|
77
|
-
personaRaw = await readFile(join(paths.personas, `${personaName}.md`), 'utf-8')
|
|
78
|
-
} catch {
|
|
79
|
-
throw new IncurError({
|
|
80
|
-
code: 'PERSONA_NOT_FOUND',
|
|
81
|
-
message: `Brain "${brainName}" references persona "${personaName}" which does not exist.`,
|
|
82
|
-
hint: 'Create the persona first or update the brain file.',
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
sections.push(stripFrontmatter(personaRaw))
|
|
86
|
-
|
|
87
|
-
// Rules — from brain (overrides persona frontmatter)
|
|
88
|
-
for (const rule of rulesList) {
|
|
89
|
-
const resolved = await resolveRuleContent(rule, warnings)
|
|
90
|
-
sections.push(...resolved)
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
// === Ad-hoc path: --persona flag ===
|
|
94
|
-
const personaSlug = normalizeSlug(personaFlag!, 'persona name')
|
|
95
|
-
personaName = personaSlug
|
|
96
|
-
|
|
97
|
-
let personaRaw: string
|
|
98
|
-
try {
|
|
99
|
-
personaRaw = await readFile(join(paths.personas, `${personaSlug}.md`), 'utf-8')
|
|
100
|
-
} catch {
|
|
101
|
-
throw new IncurError({
|
|
102
|
-
code: 'PERSONA_NOT_FOUND',
|
|
103
|
-
message: `Persona "${personaSlug}" not found.`,
|
|
104
|
-
hint: 'Run `brainjar persona list` to see available personas.',
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const frontmatter = parseLayerFrontmatter(personaRaw)
|
|
109
|
-
rulesList = frontmatter.rules
|
|
36
|
+
const api = await getApi()
|
|
110
37
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const effective = mergeState(globalState, localState, envState)
|
|
116
|
-
|
|
117
|
-
if (effective.soul.value) {
|
|
118
|
-
soulName = effective.soul.value
|
|
119
|
-
try {
|
|
120
|
-
const raw = await readFile(join(paths.souls, `${soulName}.md`), 'utf-8')
|
|
121
|
-
sections.push(stripFrontmatter(raw))
|
|
122
|
-
} catch {
|
|
123
|
-
warnings.push(`Soul "${soulName}" not found — skipped`)
|
|
124
|
-
soulName = null
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Persona content
|
|
129
|
-
sections.push(stripFrontmatter(personaRaw))
|
|
130
|
-
|
|
131
|
-
// Rules — from persona frontmatter
|
|
132
|
-
for (const rule of rulesList) {
|
|
133
|
-
const resolved = await resolveRuleContent(rule, warnings)
|
|
134
|
-
sections.push(...resolved)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Task section
|
|
139
|
-
if (c.options.task) {
|
|
140
|
-
sections.push(`# Task\n\n${c.options.task}`)
|
|
141
|
-
}
|
|
38
|
+
const body: Record<string, unknown> = {}
|
|
39
|
+
if (brainName) body.brain = brainName
|
|
40
|
+
if (personaFlag) body.persona = personaFlag
|
|
41
|
+
if (c.options.task) body.task = c.options.task
|
|
142
42
|
|
|
143
|
-
const
|
|
43
|
+
const composed = await api.post<ApiComposeResult>('/api/v1/compose', body)
|
|
144
44
|
|
|
145
45
|
const result: Record<string, unknown> = {
|
|
146
|
-
persona:
|
|
147
|
-
rules:
|
|
148
|
-
prompt,
|
|
46
|
+
persona: composed.persona,
|
|
47
|
+
rules: composed.rules,
|
|
48
|
+
prompt: composed.prompt,
|
|
149
49
|
}
|
|
150
50
|
if (brainName) result.brain = brainName
|
|
151
|
-
if (
|
|
152
|
-
if (
|
|
51
|
+
if (composed.soul) result.soul = composed.soul
|
|
52
|
+
if (composed.token_estimate) result.token_estimate = composed.token_estimate
|
|
53
|
+
if (composed.warnings?.length) result.warnings = composed.warnings
|
|
153
54
|
|
|
154
55
|
return result
|
|
155
56
|
},
|
package/src/commands/init.ts
CHANGED
|
@@ -1,76 +1,100 @@
|
|
|
1
1
|
import { Cli, z } from 'incur'
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import { join } from 'node:path'
|
|
2
|
+
import { mkdir, access } from 'node:fs/promises'
|
|
4
3
|
import { getBrainjarDir, paths, type Backend } from '../paths.js'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { buildSeedBundle } from '../seeds.js'
|
|
5
|
+
import { putState } from '../state.js'
|
|
7
6
|
import { sync } from '../sync.js'
|
|
7
|
+
import { getApi } from '../client.js'
|
|
8
|
+
import { readConfig, writeConfig, type Config } from '../config.js'
|
|
9
|
+
import { ensureBinary, upgradeServer } from '../daemon.js'
|
|
10
|
+
import type { ApiImportResult } from '../api-types.js'
|
|
8
11
|
|
|
9
12
|
export const init = Cli.create('init', {
|
|
10
|
-
description: '
|
|
13
|
+
description: 'Initialize brainjar: config, server, and optional seed content',
|
|
11
14
|
options: z.object({
|
|
12
15
|
backend: z.enum(['claude', 'codex']).default('claude').describe('Agent backend to target'),
|
|
13
16
|
default: z.boolean().default(false).describe('Seed starter soul, personas, and rules'),
|
|
14
|
-
obsidian: z.boolean().default(false).describe('Set up ~/.brainjar/ as an Obsidian vault'),
|
|
15
17
|
}),
|
|
16
18
|
async run(c) {
|
|
17
19
|
const brainjarDir = getBrainjarDir()
|
|
20
|
+
const binDir = `${brainjarDir}/bin`
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
mkdir(paths.rules, { recursive: true }),
|
|
23
|
-
mkdir(paths.brains, { recursive: true }),
|
|
24
|
-
mkdir(paths.identities, { recursive: true }),
|
|
25
|
-
])
|
|
22
|
+
// 1. Create directories
|
|
23
|
+
await mkdir(brainjarDir, { recursive: true })
|
|
24
|
+
await mkdir(binDir, { recursive: true })
|
|
26
25
|
|
|
27
|
-
//
|
|
28
|
-
|
|
26
|
+
// 2. Write config.yaml if missing
|
|
27
|
+
let configExists = false
|
|
28
|
+
try {
|
|
29
|
+
await access(paths.config)
|
|
30
|
+
configExists = true
|
|
31
|
+
} catch {}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
if (!configExists) {
|
|
34
|
+
const config: Config = {
|
|
35
|
+
server: {
|
|
36
|
+
url: 'http://localhost:7742',
|
|
37
|
+
mode: 'local',
|
|
38
|
+
bin: `${brainjarDir}/bin/brainjar-server`,
|
|
39
|
+
pid_file: `${brainjarDir}/server.pid`,
|
|
40
|
+
log_file: `${brainjarDir}/server.log`,
|
|
41
|
+
},
|
|
42
|
+
workspace: 'default',
|
|
43
|
+
backend: c.options.backend as Backend,
|
|
44
|
+
}
|
|
45
|
+
await writeConfig(config)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Ensure server binary exists and is up to date
|
|
49
|
+
await ensureBinary()
|
|
50
|
+
const config = await readConfig()
|
|
51
|
+
if (config.server.mode === 'local') {
|
|
52
|
+
// Only upgrade if server isn't already running (can't overwrite a running binary)
|
|
53
|
+
const health = await (await import('../daemon.js')).healthCheck({ timeout: 1000, url: config.server.url })
|
|
54
|
+
if (!health.healthy) {
|
|
55
|
+
await upgradeServer()
|
|
56
|
+
}
|
|
34
57
|
}
|
|
35
|
-
await writeFile(join(brainjarDir, '.gitignore'), gitignoreLines.join('\n') + '\n')
|
|
36
58
|
|
|
59
|
+
// 4. Start server and get API client
|
|
60
|
+
const api = await getApi()
|
|
61
|
+
|
|
62
|
+
// 5. Ensure workspace exists (ignore conflict if already created)
|
|
63
|
+
try {
|
|
64
|
+
await api.post('/api/v1/workspaces', { name: config.workspace }, { headers: { 'X-Brainjar-Workspace': '' } })
|
|
65
|
+
} catch (e: any) {
|
|
66
|
+
if (e.code !== 'CONFLICT') throw e
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 6. Seed defaults if requested
|
|
37
70
|
if (c.options.default) {
|
|
38
|
-
await
|
|
71
|
+
const bundle = await buildSeedBundle()
|
|
72
|
+
await api.post<ApiImportResult>('/api/v1/import', bundle)
|
|
73
|
+
|
|
74
|
+
await putState(api, {
|
|
75
|
+
soul_slug: 'craftsman',
|
|
76
|
+
persona_slug: 'engineer',
|
|
77
|
+
rule_slugs: ['boundaries', 'context-recovery', 'task-completion', 'git-discipline', 'security'],
|
|
78
|
+
})
|
|
39
79
|
}
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
state.backend = c.options.backend
|
|
44
|
-
if (c.options.default) {
|
|
45
|
-
state.soul = 'craftsman'
|
|
46
|
-
state.persona = 'engineer'
|
|
47
|
-
state.rules = ['default', 'git-discipline', 'security']
|
|
48
|
-
}
|
|
49
|
-
await writeState(state)
|
|
50
|
-
await sync(c.options.backend as Backend)
|
|
51
|
-
})
|
|
81
|
+
// 7. Sync to write CLAUDE.md / AGENTS.md
|
|
82
|
+
await sync({ api, backend: c.options.backend as Backend })
|
|
52
83
|
|
|
84
|
+
// 8. Build result
|
|
53
85
|
const result: Record<string, unknown> = {
|
|
54
86
|
created: brainjarDir,
|
|
55
87
|
backend: c.options.backend,
|
|
56
|
-
directories: ['souls/', 'personas/', 'rules/', 'brains/', 'identities/'],
|
|
57
88
|
}
|
|
58
89
|
|
|
59
90
|
if (c.options.default) {
|
|
60
91
|
result.soul = 'craftsman'
|
|
61
92
|
result.persona = 'engineer'
|
|
62
|
-
result.rules = ['
|
|
93
|
+
result.rules = ['boundaries', 'context-recovery', 'task-completion', 'git-discipline', 'security']
|
|
63
94
|
result.personas = ['engineer', 'planner', 'reviewer']
|
|
64
95
|
result.next = 'Ready to go. Run `brainjar status` to see your config.'
|
|
65
96
|
} else {
|
|
66
|
-
result.next = 'Run `brainjar
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (c.options.obsidian) {
|
|
70
|
-
await initObsidian(brainjarDir)
|
|
71
|
-
result.obsidian = true
|
|
72
|
-
result.vault = brainjarDir
|
|
73
|
-
result.hint = `Open "${brainjarDir}" as a vault in Obsidian.`
|
|
97
|
+
result.next = 'Run `brainjar soul create <name>` to create your first soul.'
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
return result
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Cli, z } from 'incur'
|
|
2
|
+
import { getBrainjarDir } from '../paths.js'
|
|
3
|
+
import { buildMigrationBundle, backupContentDirs } from '../migrate.js'
|
|
4
|
+
import { getApi } from '../client.js'
|
|
5
|
+
import { putState } from '../state.js'
|
|
6
|
+
import { sync } from '../sync.js'
|
|
7
|
+
import type { ApiImportResult } from '../api-types.js'
|
|
8
|
+
|
|
9
|
+
export const migrate = Cli.create('migrate', {
|
|
10
|
+
description: 'Import file-based content into the server',
|
|
11
|
+
options: z.object({
|
|
12
|
+
dryRun: z.boolean().default(false).describe('Preview what would be imported without making changes'),
|
|
13
|
+
skipBackup: z.boolean().default(false).describe('Skip renaming source directories to .bak'),
|
|
14
|
+
}),
|
|
15
|
+
async run(c) {
|
|
16
|
+
const brainjarDir = getBrainjarDir()
|
|
17
|
+
const { bundle, state, counts, warnings: scanWarnings } = await buildMigrationBundle(brainjarDir)
|
|
18
|
+
|
|
19
|
+
const total = counts.souls + counts.personas + counts.rules + counts.brains
|
|
20
|
+
if (total === 0) {
|
|
21
|
+
return { migrated: false, reason: 'No file-based content found to migrate.' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (c.options.dryRun) {
|
|
25
|
+
return {
|
|
26
|
+
dry_run: true,
|
|
27
|
+
would_import: counts,
|
|
28
|
+
would_restore_state: state !== null,
|
|
29
|
+
warnings: scanWarnings,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const api = await getApi()
|
|
34
|
+
const result = await api.post<ApiImportResult>('/api/v1/import', bundle)
|
|
35
|
+
|
|
36
|
+
let stateRestored = false
|
|
37
|
+
if (state && (state.soul || state.persona || state.rules.length > 0)) {
|
|
38
|
+
await putState(api, {
|
|
39
|
+
soul_slug: state.soul || undefined,
|
|
40
|
+
persona_slug: state.persona || undefined,
|
|
41
|
+
rule_slugs: state.rules.length > 0 ? state.rules : undefined,
|
|
42
|
+
})
|
|
43
|
+
stateRestored = true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await sync({ api })
|
|
47
|
+
|
|
48
|
+
let backedUp: string[] = []
|
|
49
|
+
if (!c.options.skipBackup) {
|
|
50
|
+
backedUp = await backupContentDirs(brainjarDir)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
migrated: true,
|
|
55
|
+
imported: result.imported,
|
|
56
|
+
state_restored: stateRestored,
|
|
57
|
+
backed_up: backedUp,
|
|
58
|
+
warnings: [...scanWarnings, ...result.warnings],
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
})
|