@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/shell.ts
CHANGED
|
@@ -1,99 +1,107 @@
|
|
|
1
1
|
import { Cli, z, Errors } from 'incur'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
2
3
|
|
|
3
4
|
const { IncurError } = Errors
|
|
5
|
+
import { ErrorCode, createError } from '../errors.js'
|
|
4
6
|
import { spawn } from 'node:child_process'
|
|
5
|
-
import {
|
|
6
|
-
import { requireBrainjarDir } from '../state.js'
|
|
7
|
+
import { putState } from '../state.js'
|
|
7
8
|
import { sync } from '../sync.js'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { getApi } from '../client.js'
|
|
10
|
+
import type { ApiBrain } from '../api-types.js'
|
|
10
11
|
|
|
11
12
|
export const shell = Cli.create('shell', {
|
|
12
|
-
description: 'Spawn a subshell with
|
|
13
|
+
description: 'Spawn a subshell with session-scoped state overrides',
|
|
13
14
|
options: z.object({
|
|
14
|
-
brain: z.string().optional().describe('Brain name — sets soul, persona, and rules from brain
|
|
15
|
+
brain: z.string().optional().describe('Brain name — sets soul, persona, and rules from brain'),
|
|
15
16
|
soul: z.string().optional().describe('Soul override for this session'),
|
|
16
17
|
persona: z.string().optional().describe('Persona override for this session'),
|
|
17
18
|
'rules-add': z.string().optional().describe('Comma-separated rules to add'),
|
|
18
19
|
'rules-remove': z.string().optional().describe('Comma-separated rules to remove'),
|
|
19
20
|
}),
|
|
20
21
|
async run(c) {
|
|
21
|
-
await requireBrainjarDir()
|
|
22
|
-
|
|
23
22
|
const individualFlags = c.options.soul || c.options.persona
|
|
24
23
|
|| c.options['rules-add'] || c.options['rules-remove']
|
|
25
24
|
|
|
26
25
|
if (c.options.brain && individualFlags) {
|
|
27
|
-
throw
|
|
28
|
-
code: 'MUTUALLY_EXCLUSIVE',
|
|
26
|
+
throw createError(ErrorCode.MUTUALLY_EXCLUSIVE, {
|
|
29
27
|
message: '--brain is mutually exclusive with --soul, --persona, --rules-add, --rules-remove.',
|
|
30
28
|
hint: 'Use --brain alone or individual flags, not both.',
|
|
31
29
|
})
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
if (!c.options.brain && !individualFlags) {
|
|
33
|
+
throw createError(ErrorCode.NO_OVERRIDES, {
|
|
34
|
+
message: 'No overrides specified.',
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create a unique session ID
|
|
39
|
+
const sessionId = randomUUID()
|
|
40
|
+
const api = await getApi({ session: sessionId })
|
|
41
|
+
|
|
42
|
+
// Build session-scoped state mutation
|
|
43
|
+
const mutation: Record<string, unknown> = {}
|
|
44
|
+
const labels: string[] = []
|
|
35
45
|
|
|
36
46
|
if (c.options.brain) {
|
|
37
|
-
const config = await
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
47
|
+
const config = await api.get<ApiBrain>(`/api/v1/brains/${c.options.brain}`)
|
|
48
|
+
mutation.soul_slug = config.soul_slug
|
|
49
|
+
mutation.persona_slug = config.persona_slug
|
|
50
|
+
mutation.rule_slugs = config.rule_slugs
|
|
51
|
+
labels.push(`brain: ${c.options.brain}`)
|
|
43
52
|
} else {
|
|
44
|
-
if (c.options.soul)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
if (c.options.soul) {
|
|
54
|
+
mutation.soul_slug = c.options.soul
|
|
55
|
+
labels.push(`soul: ${c.options.soul}`)
|
|
56
|
+
}
|
|
57
|
+
if (c.options.persona) {
|
|
58
|
+
mutation.persona_slug = c.options.persona
|
|
59
|
+
labels.push(`persona: ${c.options.persona}`)
|
|
60
|
+
}
|
|
61
|
+
if (c.options['rules-add']) {
|
|
62
|
+
mutation.rules_to_add = c.options['rules-add'].split(',').map(s => s.trim())
|
|
63
|
+
labels.push(`+rules: ${c.options['rules-add']}`)
|
|
64
|
+
}
|
|
65
|
+
if (c.options['rules-remove']) {
|
|
66
|
+
mutation.rules_to_remove = c.options['rules-remove'].split(',').map(s => s.trim())
|
|
67
|
+
labels.push(`-rules: ${c.options['rules-remove']}`)
|
|
68
|
+
}
|
|
48
69
|
}
|
|
49
70
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
code: 'NO_OVERRIDES',
|
|
53
|
-
message: 'No overrides specified.',
|
|
54
|
-
hint: 'Use --brain, --soul, --persona, --rules-add, or --rules-remove.',
|
|
55
|
-
})
|
|
56
|
-
}
|
|
71
|
+
// Apply session-scoped state on server
|
|
72
|
+
await putState(api, mutation)
|
|
57
73
|
|
|
58
|
-
// Sync with
|
|
59
|
-
|
|
60
|
-
await sync({ envOverrides })
|
|
61
|
-
if (hasLocal) await sync({ local: true, envOverrides })
|
|
74
|
+
// Sync CLAUDE.md with session state
|
|
75
|
+
await sync({ api })
|
|
62
76
|
|
|
63
77
|
// Print active config banner
|
|
64
|
-
const labels: string[] = []
|
|
65
|
-
if (envOverrides.BRAINJAR_SOUL) labels.push(`soul: ${envOverrides.BRAINJAR_SOUL}`)
|
|
66
|
-
if (envOverrides.BRAINJAR_PERSONA) labels.push(`persona: ${envOverrides.BRAINJAR_PERSONA}`)
|
|
67
|
-
if (envOverrides.BRAINJAR_RULES_ADD) labels.push(`+rules: ${envOverrides.BRAINJAR_RULES_ADD}`)
|
|
68
|
-
if (envOverrides.BRAINJAR_RULES_REMOVE) labels.push(`-rules: ${envOverrides.BRAINJAR_RULES_REMOVE}`)
|
|
69
|
-
|
|
70
78
|
if (!c.agent) {
|
|
71
79
|
const banner = `[brainjar] ${labels.join(' | ')}`
|
|
72
80
|
process.stderr.write(`${banner}\n`)
|
|
73
81
|
process.stderr.write(`${'─'.repeat(banner.length)}\n`)
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
// Spawn subshell with
|
|
84
|
+
// Spawn subshell with session ID so nested brainjar commands use the session
|
|
77
85
|
const userShell = process.env.SHELL || '/bin/sh'
|
|
78
86
|
const child = spawn(userShell, [], {
|
|
79
87
|
stdio: 'inherit',
|
|
80
|
-
env: { ...process.env,
|
|
88
|
+
env: { ...process.env, BRAINJAR_SESSION: sessionId },
|
|
81
89
|
})
|
|
82
90
|
|
|
83
91
|
return new Promise((resolve, reject) => {
|
|
84
92
|
child.on('exit', async (code) => {
|
|
85
|
-
//
|
|
93
|
+
// Clear session state and re-sync to restore
|
|
86
94
|
let syncWarning: string | undefined
|
|
87
95
|
try {
|
|
88
|
-
await
|
|
89
|
-
|
|
96
|
+
const cleanApi = await getApi()
|
|
97
|
+
await sync({ api: cleanApi })
|
|
90
98
|
} catch (err) {
|
|
91
99
|
syncWarning = `Re-sync on exit failed: ${(err as Error).message}`
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
const result = {
|
|
95
103
|
shell: userShell,
|
|
96
|
-
|
|
104
|
+
session: sessionId,
|
|
97
105
|
exitCode: code ?? 0,
|
|
98
106
|
...(syncWarning ? { warning: syncWarning } : {}),
|
|
99
107
|
}
|
|
@@ -106,8 +114,7 @@ export const shell = Cli.create('shell', {
|
|
|
106
114
|
}
|
|
107
115
|
})
|
|
108
116
|
child.on('error', (err) => {
|
|
109
|
-
reject(
|
|
110
|
-
code: 'SHELL_ERROR',
|
|
117
|
+
reject(createError(ErrorCode.SHELL_ERROR, {
|
|
111
118
|
message: `Failed to spawn shell: ${err.message}`,
|
|
112
119
|
}))
|
|
113
120
|
})
|
package/src/commands/soul.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 { readState, writeState, withStateLock, readLocalState, writeLocalState, withLocalStateLock, readEnvState, mergeState, requireBrainjarDir, stripFrontmatter, normalizeSlug } 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 { ApiSoul, ApiSoulList } from '../api-types.js'
|
|
9
10
|
|
|
10
11
|
export const soul = Cli.create('soul', {
|
|
11
12
|
description: 'Manage soul — personality and values for the agent',
|
|
@@ -13,25 +14,22 @@ export const soul = Cli.create('soul', {
|
|
|
13
14
|
.command('create', {
|
|
14
15
|
description: 'Create a new soul',
|
|
15
16
|
args: z.object({
|
|
16
|
-
name: z.string().describe('Soul name
|
|
17
|
+
name: z.string().describe('Soul name'),
|
|
17
18
|
}),
|
|
18
19
|
options: z.object({
|
|
19
20
|
description: z.string().optional().describe('One-line description of the soul'),
|
|
20
21
|
}),
|
|
21
22
|
async run(c) {
|
|
22
|
-
await requireBrainjarDir()
|
|
23
23
|
const name = normalizeSlug(c.args.name, 'soul name')
|
|
24
|
-
const
|
|
24
|
+
const api = await getApi()
|
|
25
25
|
|
|
26
|
+
// Check if it already exists
|
|
26
27
|
try {
|
|
27
|
-
await
|
|
28
|
-
throw
|
|
29
|
-
code: 'SOUL_EXISTS',
|
|
30
|
-
message: `Soul "${name}" already exists.`,
|
|
31
|
-
hint: 'Choose a different name or edit the existing file.',
|
|
32
|
-
})
|
|
28
|
+
await api.get<ApiSoul>(`/api/v1/souls/${name}`)
|
|
29
|
+
throw createError(ErrorCode.SOUL_EXISTS, { params: [name] })
|
|
33
30
|
} catch (e) {
|
|
34
|
-
if (e instanceof IncurError) throw e
|
|
31
|
+
if (e instanceof IncurError && e.code === ErrorCode.SOUL_EXISTS) throw e
|
|
32
|
+
if (e instanceof IncurError && e.code !== ErrorCode.NOT_FOUND) throw e
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
const lines: string[] = []
|
|
@@ -43,27 +41,26 @@ export const soul = Cli.create('soul', {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
const content = lines.join('\n')
|
|
46
|
-
await
|
|
44
|
+
await api.put<ApiSoul>(`/api/v1/souls/${name}`, { content })
|
|
47
45
|
|
|
48
46
|
if (c.agent || c.formatExplicit) {
|
|
49
|
-
return { created:
|
|
47
|
+
return { created: name, name, template: content }
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
return {
|
|
53
|
-
created:
|
|
51
|
+
created: name,
|
|
54
52
|
name,
|
|
55
53
|
template: `\n${content}`,
|
|
56
|
-
next: `
|
|
54
|
+
next: `Run \`brainjar soul show ${name}\` to view, then \`brainjar soul use ${name}\` to activate.`,
|
|
57
55
|
}
|
|
58
56
|
},
|
|
59
57
|
})
|
|
60
58
|
.command('list', {
|
|
61
59
|
description: 'List available souls',
|
|
62
60
|
async run() {
|
|
63
|
-
await
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
return { souls }
|
|
61
|
+
const api = await getApi()
|
|
62
|
+
const result = await api.get<ApiSoulList>('/api/v1/souls')
|
|
63
|
+
return { souls: result.souls.map(s => s.slug) }
|
|
67
64
|
},
|
|
68
65
|
})
|
|
69
66
|
.command('show', {
|
|
@@ -72,136 +69,104 @@ export const soul = Cli.create('soul', {
|
|
|
72
69
|
name: z.string().optional().describe('Soul name to show (defaults to active soul)'),
|
|
73
70
|
}),
|
|
74
71
|
options: z.object({
|
|
75
|
-
|
|
72
|
+
project: z.boolean().default(false).describe('Show project soul override (if any)'),
|
|
76
73
|
short: z.boolean().default(false).describe('Print only the active soul name'),
|
|
77
74
|
}),
|
|
78
75
|
async run(c) {
|
|
79
|
-
await
|
|
76
|
+
const api = await getApi()
|
|
80
77
|
|
|
81
78
|
if (c.options.short) {
|
|
82
79
|
if (c.args.name) return c.args.name
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const env = readEnvState()
|
|
86
|
-
const effective = mergeState(global, local, env)
|
|
87
|
-
return effective.soul.value ?? 'none'
|
|
80
|
+
const state = await getEffectiveState(api)
|
|
81
|
+
return state.soul ?? 'none'
|
|
88
82
|
}
|
|
89
83
|
|
|
90
|
-
// If a specific name was given, show that soul directly
|
|
91
84
|
if (c.args.name) {
|
|
92
85
|
const name = normalizeSlug(c.args.name, 'soul name')
|
|
93
86
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
message: `Soul "${name}" not found.`,
|
|
102
|
-
hint: 'Run `brainjar soul list` to see available souls.',
|
|
103
|
-
})
|
|
87
|
+
const soul = await api.get<ApiSoul>(`/api/v1/souls/${name}`)
|
|
88
|
+
return { name, title: soul.title, content: soul.content }
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
91
|
+
throw createError(ErrorCode.SOUL_NOT_FOUND, { params: [name] })
|
|
92
|
+
}
|
|
93
|
+
throw e
|
|
104
94
|
}
|
|
105
95
|
}
|
|
106
96
|
|
|
107
|
-
if (c.options.
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
if (c.options.project) {
|
|
98
|
+
const state = await api.get<import('../api-types.js').ApiStateOverride>('/api/v1/state/override', {
|
|
99
|
+
project: basename(process.cwd()),
|
|
100
|
+
})
|
|
101
|
+
if (state.soul_slug === undefined) return { active: false, scope: 'project', note: 'No project soul override (cascades from workspace)' }
|
|
102
|
+
if (state.soul_slug === null) return { active: false, scope: 'project', name: null, note: 'Explicitly unset at project scope' }
|
|
111
103
|
try {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const title = content.split('\n').find(l => l.startsWith('# '))?.replace('# ', '') ?? null
|
|
115
|
-
return { active: true, scope: 'local', name: local.soul, title, content }
|
|
104
|
+
const soul = await api.get<ApiSoul>(`/api/v1/souls/${state.soul_slug}`)
|
|
105
|
+
return { active: true, scope: 'project', name: state.soul_slug, title: soul.title, content: soul.content }
|
|
116
106
|
} catch {
|
|
117
|
-
return { active: false, scope: '
|
|
107
|
+
return { active: false, scope: 'project', name: state.soul_slug, error: 'Not found on server' }
|
|
118
108
|
}
|
|
119
109
|
}
|
|
120
110
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const env = readEnvState()
|
|
124
|
-
const effective = mergeState(global, local, env)
|
|
125
|
-
if (!effective.soul.value) return { active: false }
|
|
111
|
+
const state = await getEffectiveState(api)
|
|
112
|
+
if (!state.soul) return { active: false }
|
|
126
113
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const title = content.split('\n').find(l => l.startsWith('# '))?.replace('# ', '') ?? null
|
|
130
|
-
return { active: true, name: effective.soul.value, scope: effective.soul.scope, title, content }
|
|
114
|
+
const soul = await api.get<ApiSoul>(`/api/v1/souls/${state.soul}`)
|
|
115
|
+
return { active: true, name: state.soul, title: soul.title, content: soul.content }
|
|
131
116
|
} catch {
|
|
132
|
-
return { active: false, name:
|
|
117
|
+
return { active: false, name: state.soul, error: 'Not found on server' }
|
|
133
118
|
}
|
|
134
119
|
},
|
|
135
120
|
})
|
|
136
121
|
.command('use', {
|
|
137
122
|
description: 'Activate a soul',
|
|
138
123
|
args: z.object({
|
|
139
|
-
name: z.string().describe('Soul name
|
|
124
|
+
name: z.string().describe('Soul name'),
|
|
140
125
|
}),
|
|
141
126
|
options: z.object({
|
|
142
|
-
|
|
127
|
+
project: z.boolean().default(false).describe('Apply at project scope'),
|
|
143
128
|
}),
|
|
144
129
|
async run(c) {
|
|
145
|
-
await requireBrainjarDir()
|
|
146
130
|
const name = normalizeSlug(c.args.name, 'soul name')
|
|
147
|
-
const
|
|
131
|
+
const api = await getApi()
|
|
132
|
+
|
|
133
|
+
// Validate it exists on server
|
|
148
134
|
try {
|
|
149
|
-
await
|
|
150
|
-
} catch {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
})
|
|
135
|
+
await api.get<ApiSoul>(`/api/v1/souls/${name}`)
|
|
136
|
+
} catch (e) {
|
|
137
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
138
|
+
throw createError(ErrorCode.SOUL_NOT_FOUND, { params: [name] })
|
|
139
|
+
}
|
|
140
|
+
throw e
|
|
156
141
|
}
|
|
157
142
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} else {
|
|
166
|
-
await withStateLock(async () => {
|
|
167
|
-
const state = await readState()
|
|
168
|
-
state.soul = name
|
|
169
|
-
await writeState(state)
|
|
170
|
-
await sync()
|
|
171
|
-
})
|
|
172
|
-
}
|
|
143
|
+
const mutationOpts = c.options.project
|
|
144
|
+
? { project: basename(process.cwd()) }
|
|
145
|
+
: undefined
|
|
146
|
+
await putState(api, { soul_slug: name }, mutationOpts)
|
|
147
|
+
|
|
148
|
+
await sync({ api })
|
|
149
|
+
if (c.options.project) await sync({ api, project: true })
|
|
173
150
|
|
|
174
|
-
return { activated: name,
|
|
151
|
+
return { activated: name, project: c.options.project }
|
|
175
152
|
},
|
|
176
153
|
})
|
|
177
154
|
.command('drop', {
|
|
178
155
|
description: 'Deactivate the current soul',
|
|
179
156
|
options: z.object({
|
|
180
|
-
|
|
157
|
+
project: z.boolean().default(false).describe('Remove project soul override or deactivate workspace soul'),
|
|
181
158
|
}),
|
|
182
159
|
async run(c) {
|
|
183
|
-
await
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!state.soul) {
|
|
195
|
-
throw new IncurError({
|
|
196
|
-
code: 'NO_ACTIVE_SOUL',
|
|
197
|
-
message: 'No active soul to deactivate.',
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
state.soul = null
|
|
201
|
-
await writeState(state)
|
|
202
|
-
await sync()
|
|
203
|
-
})
|
|
204
|
-
}
|
|
205
|
-
return { deactivated: true, local: c.options.local }
|
|
160
|
+
const api = await getApi()
|
|
161
|
+
|
|
162
|
+
const mutationOpts = c.options.project
|
|
163
|
+
? { project: basename(process.cwd()) }
|
|
164
|
+
: undefined
|
|
165
|
+
await putState(api, { soul_slug: null }, mutationOpts)
|
|
166
|
+
|
|
167
|
+
await sync({ api })
|
|
168
|
+
if (c.options.project) await sync({ api, project: true })
|
|
169
|
+
|
|
170
|
+
return { deactivated: true, project: c.options.project }
|
|
206
171
|
},
|
|
207
172
|
})
|
package/src/commands/status.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { Cli, z } from 'incur'
|
|
2
|
-
import {
|
|
2
|
+
import { basename } from 'node:path'
|
|
3
|
+
import { getEffectiveState } from '../state.js'
|
|
3
4
|
import { sync } from '../sync.js'
|
|
5
|
+
import { getApi } from '../client.js'
|
|
6
|
+
import type { ApiStateOverride } from '../api-types.js'
|
|
4
7
|
|
|
5
8
|
export const status = Cli.create('status', {
|
|
6
9
|
description: 'Show active brain configuration',
|
|
7
10
|
options: z.object({
|
|
8
11
|
sync: z.boolean().default(false).describe('Regenerate config file from active layers'),
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
workspace: z.boolean().default(false).describe('Show only workspace state'),
|
|
13
|
+
project: z.boolean().default(false).describe('Show only project overrides'),
|
|
11
14
|
short: z.boolean().default(false).describe('One-line output: soul | persona'),
|
|
12
15
|
}),
|
|
13
16
|
async run(c) {
|
|
14
|
-
await
|
|
17
|
+
const api = await getApi()
|
|
15
18
|
|
|
16
19
|
// --short: compact one-liner for scripts/statuslines
|
|
17
20
|
if (c.options.short) {
|
|
18
|
-
const
|
|
19
|
-
const local = await readLocalState()
|
|
20
|
-
const env = readEnvState()
|
|
21
|
-
const effective = mergeState(global, local, env)
|
|
21
|
+
const state = await getEffectiveState(api)
|
|
22
22
|
const parts = [
|
|
23
|
-
`soul: ${
|
|
24
|
-
`persona: ${
|
|
23
|
+
`soul: ${state.soul ?? 'none'}`,
|
|
24
|
+
`persona: ${state.persona ?? 'none'}`,
|
|
25
25
|
]
|
|
26
26
|
return parts.join(' | ')
|
|
27
27
|
}
|
|
@@ -29,64 +29,59 @@ export const status = Cli.create('status', {
|
|
|
29
29
|
// Sync if requested
|
|
30
30
|
let synced: Record<string, unknown> | undefined
|
|
31
31
|
if (c.options.sync) {
|
|
32
|
-
const syncResult = await sync()
|
|
32
|
+
const syncResult = await sync({ api })
|
|
33
33
|
synced = { written: syncResult.written, warnings: syncResult.warnings }
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// --
|
|
37
|
-
if (c.options.
|
|
38
|
-
const
|
|
36
|
+
// --workspace: show only workspace-level override
|
|
37
|
+
if (c.options.workspace) {
|
|
38
|
+
const override = await api.get<ApiStateOverride>('/api/v1/state/override')
|
|
39
39
|
const result: Record<string, unknown> = {
|
|
40
|
-
soul:
|
|
41
|
-
persona:
|
|
42
|
-
rules:
|
|
40
|
+
soul: override.soul_slug ?? null,
|
|
41
|
+
persona: override.persona_slug ?? null,
|
|
42
|
+
rules: override.rule_slugs ?? [],
|
|
43
43
|
}
|
|
44
44
|
if (synced) result.synced = synced
|
|
45
45
|
return result
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// --
|
|
49
|
-
if (c.options.
|
|
50
|
-
const
|
|
48
|
+
// --project: show only project-level overrides
|
|
49
|
+
if (c.options.project) {
|
|
50
|
+
const override = await api.get<ApiStateOverride>('/api/v1/state/override', {
|
|
51
|
+
project: basename(process.cwd()),
|
|
52
|
+
})
|
|
51
53
|
const result: Record<string, unknown> = {}
|
|
52
|
-
if (
|
|
53
|
-
if (
|
|
54
|
-
if (
|
|
55
|
-
if (
|
|
54
|
+
if (override.soul_slug !== undefined) result.soul = override.soul_slug
|
|
55
|
+
if (override.persona_slug !== undefined) result.persona = override.persona_slug
|
|
56
|
+
if (override.rules_to_add?.length) result.rules_to_add = override.rules_to_add
|
|
57
|
+
if (override.rules_to_remove?.length) result.rules_to_remove = override.rules_to_remove
|
|
58
|
+
if (Object.keys(result).length === 0) result.note = 'No project overrides'
|
|
56
59
|
if (synced) result.synced = synced
|
|
57
60
|
return result
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
// Default: effective state with scope annotations
|
|
61
|
-
const
|
|
62
|
-
const local = await readLocalState()
|
|
63
|
-
const env = readEnvState()
|
|
64
|
-
const effective = mergeState(global, local, env)
|
|
64
|
+
const state = await getEffectiveState(api)
|
|
65
65
|
|
|
66
66
|
// Agents and explicit --format get full structured data
|
|
67
67
|
if (c.agent || c.formatExplicit) {
|
|
68
68
|
const result: Record<string, unknown> = {
|
|
69
|
-
soul:
|
|
70
|
-
persona:
|
|
71
|
-
rules:
|
|
69
|
+
soul: state.soul,
|
|
70
|
+
persona: state.persona,
|
|
71
|
+
rules: state.rules,
|
|
72
72
|
}
|
|
73
73
|
if (synced) result.synced = synced
|
|
74
74
|
return result
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Humans get a compact view
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const rulesLabel = effective.rules.length
|
|
81
|
-
? effective.rules
|
|
82
|
-
.filter(r => !r.scope.startsWith('-'))
|
|
83
|
-
.map(r => `${r.value} ${fmtScope(r.scope)}`)
|
|
84
|
-
.join(', ')
|
|
77
|
+
// Humans get a compact view
|
|
78
|
+
const rulesLabel = state.rules.length
|
|
79
|
+
? state.rules.join(', ')
|
|
85
80
|
: null
|
|
86
81
|
|
|
87
82
|
const result: Record<string, unknown> = {
|
|
88
|
-
soul:
|
|
89
|
-
persona:
|
|
83
|
+
soul: state.soul ?? null,
|
|
84
|
+
persona: state.persona ?? null,
|
|
90
85
|
rules: rulesLabel,
|
|
91
86
|
}
|
|
92
87
|
if (synced) result.synced = synced
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Cli, z } from 'incur'
|
|
2
2
|
import { sync as runSync } from '../sync.js'
|
|
3
|
-
import { requireBrainjarDir } from '../state.js'
|
|
4
3
|
|
|
5
4
|
export const sync = Cli.create('sync', {
|
|
6
5
|
description: 'Regenerate config file from active layers',
|
|
@@ -8,7 +7,6 @@ export const sync = Cli.create('sync', {
|
|
|
8
7
|
quiet: z.boolean().default(false).describe('Suppress output (for use in hooks)'),
|
|
9
8
|
}),
|
|
10
9
|
async run(c) {
|
|
11
|
-
await requireBrainjarDir()
|
|
12
10
|
const result = await runSync()
|
|
13
11
|
if (c.options.quiet) return
|
|
14
12
|
return result
|