@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/migrate.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { readFile, readdir, stat, rename, access } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { parse as parseYaml } from 'yaml'
|
|
4
|
+
import type {
|
|
5
|
+
ContentBundle, BundleRule, BundleSoul, BundlePersona, BundleBrain, BundleState,
|
|
6
|
+
} from './api-types.js'
|
|
7
|
+
|
|
8
|
+
export interface MigrateCounts {
|
|
9
|
+
souls: number
|
|
10
|
+
personas: number
|
|
11
|
+
rules: number
|
|
12
|
+
brains: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
|
|
16
|
+
const trimmed = content.trimStart()
|
|
17
|
+
if (!trimmed.startsWith('---')) {
|
|
18
|
+
return { frontmatter: {}, body: content }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const endIdx = trimmed.indexOf('---', 3)
|
|
22
|
+
if (endIdx === -1) {
|
|
23
|
+
return { frontmatter: {}, body: content }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const yamlBlock = trimmed.slice(3, endIdx).trim()
|
|
27
|
+
const body = trimmed.slice(endIdx + 3).trimStart()
|
|
28
|
+
|
|
29
|
+
let frontmatter: Record<string, unknown> = {}
|
|
30
|
+
try {
|
|
31
|
+
const parsed = parseYaml(yamlBlock)
|
|
32
|
+
if (parsed && typeof parsed === 'object') {
|
|
33
|
+
frontmatter = parsed as Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Malformed frontmatter — treat as none
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { frontmatter, body }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function scanRules(rulesDir: string): Promise<{ rules: Record<string, BundleRule>; warnings: string[] }> {
|
|
43
|
+
const rules: Record<string, BundleRule> = {}
|
|
44
|
+
const warnings: string[] = []
|
|
45
|
+
|
|
46
|
+
let entries: string[]
|
|
47
|
+
try {
|
|
48
|
+
entries = await readdir(rulesDir)
|
|
49
|
+
} catch {
|
|
50
|
+
return { rules, warnings }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const entry of entries.sort()) {
|
|
54
|
+
const fullPath = join(rulesDir, entry)
|
|
55
|
+
const s = await stat(fullPath)
|
|
56
|
+
|
|
57
|
+
if (s.isDirectory()) {
|
|
58
|
+
try {
|
|
59
|
+
const files = await readdir(fullPath)
|
|
60
|
+
const mdFiles = files.filter(f => f.endsWith('.md')).sort()
|
|
61
|
+
const ruleEntries = await Promise.all(
|
|
62
|
+
mdFiles.map(async (file, i) => ({
|
|
63
|
+
sort_key: i,
|
|
64
|
+
content: await readFile(join(fullPath, file), 'utf-8'),
|
|
65
|
+
}))
|
|
66
|
+
)
|
|
67
|
+
if (ruleEntries.length > 0) {
|
|
68
|
+
rules[entry] = { entries: ruleEntries }
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
warnings.push(`Skipped rule directory "${entry}": ${(e as Error).message}`)
|
|
72
|
+
}
|
|
73
|
+
} else if (entry.endsWith('.md')) {
|
|
74
|
+
try {
|
|
75
|
+
const slug = entry.slice(0, -3)
|
|
76
|
+
const content = await readFile(fullPath, 'utf-8')
|
|
77
|
+
rules[slug] = { entries: [{ sort_key: 0, content }] }
|
|
78
|
+
} catch (e) {
|
|
79
|
+
warnings.push(`Skipped rule "${entry}": ${(e as Error).message}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { rules, warnings }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function scanSouls(soulsDir: string): Promise<{ souls: Record<string, BundleSoul>; warnings: string[] }> {
|
|
88
|
+
const souls: Record<string, BundleSoul> = {}
|
|
89
|
+
const warnings: string[] = []
|
|
90
|
+
|
|
91
|
+
let entries: string[]
|
|
92
|
+
try {
|
|
93
|
+
entries = await readdir(soulsDir)
|
|
94
|
+
} catch {
|
|
95
|
+
return { souls, warnings }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const entry of entries.sort()) {
|
|
99
|
+
if (!entry.endsWith('.md')) continue
|
|
100
|
+
try {
|
|
101
|
+
const slug = entry.slice(0, -3)
|
|
102
|
+
const content = await readFile(join(soulsDir, entry), 'utf-8')
|
|
103
|
+
souls[slug] = { content }
|
|
104
|
+
} catch (e) {
|
|
105
|
+
warnings.push(`Skipped soul "${entry}": ${(e as Error).message}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { souls, warnings }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function scanPersonas(personasDir: string): Promise<{ personas: Record<string, BundlePersona>; warnings: string[] }> {
|
|
113
|
+
const personas: Record<string, BundlePersona> = {}
|
|
114
|
+
const warnings: string[] = []
|
|
115
|
+
|
|
116
|
+
let entries: string[]
|
|
117
|
+
try {
|
|
118
|
+
entries = await readdir(personasDir)
|
|
119
|
+
} catch {
|
|
120
|
+
return { personas, warnings }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const entry of entries.sort()) {
|
|
124
|
+
if (!entry.endsWith('.md')) continue
|
|
125
|
+
try {
|
|
126
|
+
const slug = entry.slice(0, -3)
|
|
127
|
+
const raw = await readFile(join(personasDir, entry), 'utf-8')
|
|
128
|
+
const { frontmatter, body } = parseFrontmatter(raw)
|
|
129
|
+
|
|
130
|
+
let bundled_rules: string[] = []
|
|
131
|
+
if (Array.isArray(frontmatter.rules)) {
|
|
132
|
+
bundled_rules = frontmatter.rules.map(String)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
personas[slug] = { content: body, bundled_rules }
|
|
136
|
+
} catch (e) {
|
|
137
|
+
warnings.push(`Skipped persona "${entry}": ${(e as Error).message}`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { personas, warnings }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function scanBrains(brainsDir: string): Promise<{ brains: Record<string, BundleBrain>; warnings: string[] }> {
|
|
145
|
+
const brains: Record<string, BundleBrain> = {}
|
|
146
|
+
const warnings: string[] = []
|
|
147
|
+
|
|
148
|
+
let entries: string[]
|
|
149
|
+
try {
|
|
150
|
+
entries = await readdir(brainsDir)
|
|
151
|
+
} catch {
|
|
152
|
+
return { brains, warnings }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const entry of entries.sort()) {
|
|
156
|
+
if (!entry.endsWith('.yaml')) continue
|
|
157
|
+
try {
|
|
158
|
+
const slug = entry.slice(0, -5)
|
|
159
|
+
const raw = await readFile(join(brainsDir, entry), 'utf-8')
|
|
160
|
+
const parsed = parseYaml(raw) as Record<string, unknown>
|
|
161
|
+
|
|
162
|
+
brains[slug] = {
|
|
163
|
+
soul_slug: String(parsed.soul ?? ''),
|
|
164
|
+
persona_slug: String(parsed.persona ?? ''),
|
|
165
|
+
rule_slugs: Array.isArray(parsed.rules) ? parsed.rules.map(String) : [],
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
warnings.push(`Skipped brain "${entry}": ${(e as Error).message}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { brains, warnings }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function scanState(stateFile: string): Promise<BundleState | null> {
|
|
176
|
+
try {
|
|
177
|
+
const raw = await readFile(stateFile, 'utf-8')
|
|
178
|
+
const parsed = parseYaml(raw) as Record<string, unknown>
|
|
179
|
+
return {
|
|
180
|
+
soul: String(parsed.soul ?? ''),
|
|
181
|
+
persona: String(parsed.persona ?? ''),
|
|
182
|
+
rules: Array.isArray(parsed.rules) ? parsed.rules.map(String) : [],
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function buildMigrationBundle(brainjarDir: string): Promise<{
|
|
190
|
+
bundle: ContentBundle
|
|
191
|
+
state: BundleState | null
|
|
192
|
+
counts: MigrateCounts
|
|
193
|
+
warnings: string[]
|
|
194
|
+
}> {
|
|
195
|
+
const [rulesResult, soulsResult, personasResult, brainsResult, state] = await Promise.all([
|
|
196
|
+
scanRules(join(brainjarDir, 'rules')),
|
|
197
|
+
scanSouls(join(brainjarDir, 'souls')),
|
|
198
|
+
scanPersonas(join(brainjarDir, 'personas')),
|
|
199
|
+
scanBrains(join(brainjarDir, 'brains')),
|
|
200
|
+
scanState(join(brainjarDir, 'state.yaml')),
|
|
201
|
+
])
|
|
202
|
+
|
|
203
|
+
const warnings = [
|
|
204
|
+
...rulesResult.warnings,
|
|
205
|
+
...soulsResult.warnings,
|
|
206
|
+
...personasResult.warnings,
|
|
207
|
+
...brainsResult.warnings,
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
const bundle: ContentBundle = {}
|
|
211
|
+
if (Object.keys(soulsResult.souls).length > 0) bundle.souls = soulsResult.souls
|
|
212
|
+
if (Object.keys(personasResult.personas).length > 0) bundle.personas = personasResult.personas
|
|
213
|
+
if (Object.keys(rulesResult.rules).length > 0) bundle.rules = rulesResult.rules
|
|
214
|
+
if (Object.keys(brainsResult.brains).length > 0) bundle.brains = brainsResult.brains
|
|
215
|
+
if (state) bundle.state = state
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
bundle,
|
|
219
|
+
state,
|
|
220
|
+
counts: {
|
|
221
|
+
souls: Object.keys(soulsResult.souls).length,
|
|
222
|
+
personas: Object.keys(personasResult.personas).length,
|
|
223
|
+
rules: Object.keys(rulesResult.rules).length,
|
|
224
|
+
brains: Object.keys(brainsResult.brains).length,
|
|
225
|
+
},
|
|
226
|
+
warnings,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function backupContentDirs(brainjarDir: string): Promise<string[]> {
|
|
231
|
+
const dirs = ['souls', 'personas', 'rules', 'brains']
|
|
232
|
+
const backedUp: string[] = []
|
|
233
|
+
|
|
234
|
+
for (const dir of dirs) {
|
|
235
|
+
const src = join(brainjarDir, dir)
|
|
236
|
+
const dst = join(brainjarDir, `${dir}.bak`)
|
|
237
|
+
try {
|
|
238
|
+
await access(src)
|
|
239
|
+
await rename(src, dst)
|
|
240
|
+
backedUp.push(dir)
|
|
241
|
+
} catch {
|
|
242
|
+
// Directory doesn't exist — skip
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return backedUp
|
|
247
|
+
}
|