@brainjar/cli 0.3.0 → 0.4.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/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 +152 -145
- package/src/commands/rules.ts +112 -174
- package/src/commands/server.ts +212 -0
- package/src/commands/shell.ts +53 -46
- package/src/commands/soul.ts +125 -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[] = []
|
|
@@ -41,29 +39,78 @@ export const soul = Cli.create('soul', {
|
|
|
41
39
|
lines.push(c.options.description)
|
|
42
40
|
lines.push('')
|
|
43
41
|
}
|
|
42
|
+
lines.push('## Voice')
|
|
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('')
|
|
44
51
|
|
|
45
52
|
const content = lines.join('\n')
|
|
46
|
-
await
|
|
53
|
+
await api.put<ApiSoul>(`/api/v1/souls/${name}`, { content })
|
|
47
54
|
|
|
48
55
|
if (c.agent || c.formatExplicit) {
|
|
49
|
-
return { created:
|
|
56
|
+
return { created: name, name, template: content }
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
return {
|
|
53
|
-
created:
|
|
60
|
+
created: name,
|
|
54
61
|
name,
|
|
55
62
|
template: `\n${content}`,
|
|
56
|
-
next: `
|
|
63
|
+
next: `Run \`brainjar soul show ${name}\` to view, then \`brainjar soul use ${name}\` to activate.`,
|
|
57
64
|
}
|
|
58
65
|
},
|
|
59
66
|
})
|
|
67
|
+
.command('update', {
|
|
68
|
+
description: 'Update a soul\'s content (reads from stdin)',
|
|
69
|
+
args: z.object({
|
|
70
|
+
name: z.string().describe('Soul name'),
|
|
71
|
+
}),
|
|
72
|
+
async run(c) {
|
|
73
|
+
const name = normalizeSlug(c.args.name, 'soul name')
|
|
74
|
+
const api = await getApi()
|
|
75
|
+
|
|
76
|
+
// Validate it exists
|
|
77
|
+
try {
|
|
78
|
+
await api.get<ApiSoul>(`/api/v1/souls/${name}`)
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
81
|
+
throw createError(ErrorCode.SOUL_NOT_FOUND, { params: [name] })
|
|
82
|
+
}
|
|
83
|
+
throw e
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const chunks: Uint8Array[] = []
|
|
87
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
88
|
+
chunks.push(chunk)
|
|
89
|
+
}
|
|
90
|
+
const content = Buffer.concat(chunks).toString().trim()
|
|
91
|
+
|
|
92
|
+
if (!content) {
|
|
93
|
+
throw createError(ErrorCode.MISSING_ARG, {
|
|
94
|
+
message: 'No content provided. Pipe content via stdin.',
|
|
95
|
+
hint: `echo "# ${name}\\n..." | brainjar soul update ${name}`,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await api.put<ApiSoul>(`/api/v1/souls/${name}`, { content })
|
|
100
|
+
|
|
101
|
+
// Sync if this soul is active
|
|
102
|
+
const state = await getEffectiveState(api)
|
|
103
|
+
if (state.soul === name) await sync({ api })
|
|
104
|
+
|
|
105
|
+
return { updated: name }
|
|
106
|
+
},
|
|
107
|
+
})
|
|
60
108
|
.command('list', {
|
|
61
109
|
description: 'List available souls',
|
|
62
110
|
async run() {
|
|
63
|
-
await
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
return { souls }
|
|
111
|
+
const api = await getApi()
|
|
112
|
+
const result = await api.get<ApiSoulList>('/api/v1/souls')
|
|
113
|
+
return { souls: result.souls.map(s => s.slug) }
|
|
67
114
|
},
|
|
68
115
|
})
|
|
69
116
|
.command('show', {
|
|
@@ -72,136 +119,104 @@ export const soul = Cli.create('soul', {
|
|
|
72
119
|
name: z.string().optional().describe('Soul name to show (defaults to active soul)'),
|
|
73
120
|
}),
|
|
74
121
|
options: z.object({
|
|
75
|
-
|
|
122
|
+
project: z.boolean().default(false).describe('Show project soul override (if any)'),
|
|
76
123
|
short: z.boolean().default(false).describe('Print only the active soul name'),
|
|
77
124
|
}),
|
|
78
125
|
async run(c) {
|
|
79
|
-
await
|
|
126
|
+
const api = await getApi()
|
|
80
127
|
|
|
81
128
|
if (c.options.short) {
|
|
82
129
|
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'
|
|
130
|
+
const state = await getEffectiveState(api)
|
|
131
|
+
return state.soul ?? 'none'
|
|
88
132
|
}
|
|
89
133
|
|
|
90
|
-
// If a specific name was given, show that soul directly
|
|
91
134
|
if (c.args.name) {
|
|
92
135
|
const name = normalizeSlug(c.args.name, 'soul name')
|
|
93
136
|
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
|
-
})
|
|
137
|
+
const soul = await api.get<ApiSoul>(`/api/v1/souls/${name}`)
|
|
138
|
+
return { name, title: soul.title, content: soul.content }
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
141
|
+
throw createError(ErrorCode.SOUL_NOT_FOUND, { params: [name] })
|
|
142
|
+
}
|
|
143
|
+
throw e
|
|
104
144
|
}
|
|
105
145
|
}
|
|
106
146
|
|
|
107
|
-
if (c.options.
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
if (c.options.project) {
|
|
148
|
+
const state = await api.get<import('../api-types.js').ApiStateOverride>('/api/v1/state/override', {
|
|
149
|
+
project: basename(process.cwd()),
|
|
150
|
+
})
|
|
151
|
+
if (state.soul_slug === undefined) return { active: false, scope: 'project', note: 'No project soul override (cascades from workspace)' }
|
|
152
|
+
if (state.soul_slug === null) return { active: false, scope: 'project', name: null, note: 'Explicitly unset at project scope' }
|
|
111
153
|
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 }
|
|
154
|
+
const soul = await api.get<ApiSoul>(`/api/v1/souls/${state.soul_slug}`)
|
|
155
|
+
return { active: true, scope: 'project', name: state.soul_slug, title: soul.title, content: soul.content }
|
|
116
156
|
} catch {
|
|
117
|
-
return { active: false, scope: '
|
|
157
|
+
return { active: false, scope: 'project', name: state.soul_slug, error: 'Not found on server' }
|
|
118
158
|
}
|
|
119
159
|
}
|
|
120
160
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const env = readEnvState()
|
|
124
|
-
const effective = mergeState(global, local, env)
|
|
125
|
-
if (!effective.soul.value) return { active: false }
|
|
161
|
+
const state = await getEffectiveState(api)
|
|
162
|
+
if (!state.soul) return { active: false }
|
|
126
163
|
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 }
|
|
164
|
+
const soul = await api.get<ApiSoul>(`/api/v1/souls/${state.soul}`)
|
|
165
|
+
return { active: true, name: state.soul, title: soul.title, content: soul.content }
|
|
131
166
|
} catch {
|
|
132
|
-
return { active: false, name:
|
|
167
|
+
return { active: false, name: state.soul, error: 'Not found on server' }
|
|
133
168
|
}
|
|
134
169
|
},
|
|
135
170
|
})
|
|
136
171
|
.command('use', {
|
|
137
172
|
description: 'Activate a soul',
|
|
138
173
|
args: z.object({
|
|
139
|
-
name: z.string().describe('Soul name
|
|
174
|
+
name: z.string().describe('Soul name'),
|
|
140
175
|
}),
|
|
141
176
|
options: z.object({
|
|
142
|
-
|
|
177
|
+
project: z.boolean().default(false).describe('Apply at project scope'),
|
|
143
178
|
}),
|
|
144
179
|
async run(c) {
|
|
145
|
-
await requireBrainjarDir()
|
|
146
180
|
const name = normalizeSlug(c.args.name, 'soul name')
|
|
147
|
-
const
|
|
181
|
+
const api = await getApi()
|
|
182
|
+
|
|
183
|
+
// Validate it exists on server
|
|
148
184
|
try {
|
|
149
|
-
await
|
|
150
|
-
} catch {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
})
|
|
185
|
+
await api.get<ApiSoul>(`/api/v1/souls/${name}`)
|
|
186
|
+
} catch (e) {
|
|
187
|
+
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
188
|
+
throw createError(ErrorCode.SOUL_NOT_FOUND, { params: [name] })
|
|
189
|
+
}
|
|
190
|
+
throw e
|
|
156
191
|
}
|
|
157
192
|
|
|
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
|
-
}
|
|
193
|
+
const mutationOpts = c.options.project
|
|
194
|
+
? { project: basename(process.cwd()) }
|
|
195
|
+
: undefined
|
|
196
|
+
await putState(api, { soul_slug: name }, mutationOpts)
|
|
197
|
+
|
|
198
|
+
await sync({ api })
|
|
199
|
+
if (c.options.project) await sync({ api, project: true })
|
|
173
200
|
|
|
174
|
-
return { activated: name,
|
|
201
|
+
return { activated: name, project: c.options.project }
|
|
175
202
|
},
|
|
176
203
|
})
|
|
177
204
|
.command('drop', {
|
|
178
205
|
description: 'Deactivate the current soul',
|
|
179
206
|
options: z.object({
|
|
180
|
-
|
|
207
|
+
project: z.boolean().default(false).describe('Remove project soul override or deactivate workspace soul'),
|
|
181
208
|
}),
|
|
182
209
|
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 }
|
|
210
|
+
const api = await getApi()
|
|
211
|
+
|
|
212
|
+
const mutationOpts = c.options.project
|
|
213
|
+
? { project: basename(process.cwd()) }
|
|
214
|
+
: undefined
|
|
215
|
+
await putState(api, { soul_slug: null }, mutationOpts)
|
|
216
|
+
|
|
217
|
+
await sync({ api })
|
|
218
|
+
if (c.options.project) await sync({ api, project: true })
|
|
219
|
+
|
|
220
|
+
return { deactivated: true, project: c.options.project }
|
|
206
221
|
},
|
|
207
222
|
})
|
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
|