@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/shell.ts
CHANGED
|
@@ -1,102 +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
|
-
identity: z.string().optional().describe('Identity override for this session'),
|
|
18
18
|
'rules-add': z.string().optional().describe('Comma-separated rules to add'),
|
|
19
19
|
'rules-remove': z.string().optional().describe('Comma-separated rules to remove'),
|
|
20
20
|
}),
|
|
21
21
|
async run(c) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const individualFlags = c.options.soul || c.options.persona || c.options.identity
|
|
22
|
+
const individualFlags = c.options.soul || c.options.persona
|
|
25
23
|
|| c.options['rules-add'] || c.options['rules-remove']
|
|
26
24
|
|
|
27
25
|
if (c.options.brain && individualFlags) {
|
|
28
|
-
throw
|
|
29
|
-
|
|
30
|
-
message: '--brain is mutually exclusive with --soul, --persona, --identity, --rules-add, --rules-remove.',
|
|
26
|
+
throw createError(ErrorCode.MUTUALLY_EXCLUSIVE, {
|
|
27
|
+
message: '--brain is mutually exclusive with --soul, --persona, --rules-add, --rules-remove.',
|
|
31
28
|
hint: 'Use --brain alone or individual flags, not both.',
|
|
32
29
|
})
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
|
|
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[] = []
|
|
36
45
|
|
|
37
46
|
if (c.options.brain) {
|
|
38
|
-
const config = await
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
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}`)
|
|
44
52
|
} else {
|
|
45
|
-
if (c.options.soul)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (c.options
|
|
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
|
+
}
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
code: 'NO_OVERRIDES',
|
|
55
|
-
message: 'No overrides specified.',
|
|
56
|
-
hint: 'Use --brain, --soul, --persona, --identity, --rules-add, or --rules-remove.',
|
|
57
|
-
})
|
|
58
|
-
}
|
|
71
|
+
// Apply session-scoped state on server
|
|
72
|
+
await putState(api, mutation)
|
|
59
73
|
|
|
60
|
-
// Sync with
|
|
61
|
-
|
|
62
|
-
await sync({ envOverrides })
|
|
63
|
-
if (hasLocal) await sync({ local: true, envOverrides })
|
|
74
|
+
// Sync CLAUDE.md with session state
|
|
75
|
+
await sync({ api })
|
|
64
76
|
|
|
65
77
|
// Print active config banner
|
|
66
|
-
const labels: string[] = []
|
|
67
|
-
if (envOverrides.BRAINJAR_SOUL) labels.push(`soul: ${envOverrides.BRAINJAR_SOUL}`)
|
|
68
|
-
if (envOverrides.BRAINJAR_PERSONA) labels.push(`persona: ${envOverrides.BRAINJAR_PERSONA}`)
|
|
69
|
-
if (envOverrides.BRAINJAR_IDENTITY) labels.push(`identity: ${envOverrides.BRAINJAR_IDENTITY}`)
|
|
70
|
-
if (envOverrides.BRAINJAR_RULES_ADD) labels.push(`+rules: ${envOverrides.BRAINJAR_RULES_ADD}`)
|
|
71
|
-
if (envOverrides.BRAINJAR_RULES_REMOVE) labels.push(`-rules: ${envOverrides.BRAINJAR_RULES_REMOVE}`)
|
|
72
|
-
|
|
73
78
|
if (!c.agent) {
|
|
74
79
|
const banner = `[brainjar] ${labels.join(' | ')}`
|
|
75
80
|
process.stderr.write(`${banner}\n`)
|
|
76
81
|
process.stderr.write(`${'─'.repeat(banner.length)}\n`)
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
// Spawn subshell with
|
|
84
|
+
// Spawn subshell with session ID so nested brainjar commands use the session
|
|
80
85
|
const userShell = process.env.SHELL || '/bin/sh'
|
|
81
86
|
const child = spawn(userShell, [], {
|
|
82
87
|
stdio: 'inherit',
|
|
83
|
-
env: { ...process.env,
|
|
88
|
+
env: { ...process.env, BRAINJAR_SESSION: sessionId },
|
|
84
89
|
})
|
|
85
90
|
|
|
86
91
|
return new Promise((resolve, reject) => {
|
|
87
92
|
child.on('exit', async (code) => {
|
|
88
|
-
//
|
|
93
|
+
// Clear session state and re-sync to restore
|
|
89
94
|
let syncWarning: string | undefined
|
|
90
95
|
try {
|
|
91
|
-
await
|
|
92
|
-
|
|
96
|
+
const cleanApi = await getApi()
|
|
97
|
+
await sync({ api: cleanApi })
|
|
93
98
|
} catch (err) {
|
|
94
99
|
syncWarning = `Re-sync on exit failed: ${(err as Error).message}`
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
const result = {
|
|
98
103
|
shell: userShell,
|
|
99
|
-
|
|
104
|
+
session: sessionId,
|
|
100
105
|
exitCode: code ?? 0,
|
|
101
106
|
...(syncWarning ? { warning: syncWarning } : {}),
|
|
102
107
|
}
|
|
@@ -109,8 +114,7 @@ export const shell = Cli.create('shell', {
|
|
|
109
114
|
}
|
|
110
115
|
})
|
|
111
116
|
child.on('error', (err) => {
|
|
112
|
-
reject(
|
|
113
|
-
code: 'SHELL_ERROR',
|
|
117
|
+
reject(createError(ErrorCode.SHELL_ERROR, {
|
|
114
118
|
message: `Failed to spawn shell: ${err.message}`,
|
|
115
119
|
}))
|
|
116
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,31 +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
|
-
|
|
11
|
-
short: z.boolean().default(false).describe('One-line output: soul | persona
|
|
12
|
+
workspace: z.boolean().default(false).describe('Show only workspace state'),
|
|
13
|
+
project: z.boolean().default(false).describe('Show only project overrides'),
|
|
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)
|
|
22
|
-
const slug = effective.identity.value
|
|
23
|
-
? (await loadIdentity(effective.identity.value).catch(() => null))?.slug ?? effective.identity.value
|
|
24
|
-
: null
|
|
21
|
+
const state = await getEffectiveState(api)
|
|
25
22
|
const parts = [
|
|
26
|
-
`soul: ${
|
|
27
|
-
`persona: ${
|
|
28
|
-
`identity: ${slug ?? 'none'}`,
|
|
23
|
+
`soul: ${state.soul ?? 'none'}`,
|
|
24
|
+
`persona: ${state.persona ?? 'none'}`,
|
|
29
25
|
]
|
|
30
26
|
return parts.join(' | ')
|
|
31
27
|
}
|
|
@@ -33,97 +29,60 @@ export const status = Cli.create('status', {
|
|
|
33
29
|
// Sync if requested
|
|
34
30
|
let synced: Record<string, unknown> | undefined
|
|
35
31
|
if (c.options.sync) {
|
|
36
|
-
const syncResult = await sync()
|
|
32
|
+
const syncResult = await sync({ api })
|
|
37
33
|
synced = { written: syncResult.written, warnings: syncResult.warnings }
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
// --
|
|
41
|
-
if (c.options.
|
|
42
|
-
const
|
|
43
|
-
let identityFull: Record<string, unknown> | null = null
|
|
44
|
-
if (state.identity) {
|
|
45
|
-
try {
|
|
46
|
-
const { content: _, ...id } = await loadIdentity(state.identity)
|
|
47
|
-
identityFull = id
|
|
48
|
-
} catch {
|
|
49
|
-
identityFull = { slug: state.identity, error: 'File not found' }
|
|
50
|
-
}
|
|
51
|
-
}
|
|
36
|
+
// --workspace: show only workspace-level override
|
|
37
|
+
if (c.options.workspace) {
|
|
38
|
+
const override = await api.get<ApiStateOverride>('/api/v1/state/override')
|
|
52
39
|
const result: Record<string, unknown> = {
|
|
53
|
-
soul:
|
|
54
|
-
persona:
|
|
55
|
-
rules:
|
|
56
|
-
identity: identityFull,
|
|
40
|
+
soul: override.soul_slug ?? null,
|
|
41
|
+
persona: override.persona_slug ?? null,
|
|
42
|
+
rules: override.rule_slugs ?? [],
|
|
57
43
|
}
|
|
58
44
|
if (synced) result.synced = synced
|
|
59
45
|
return result
|
|
60
46
|
}
|
|
61
47
|
|
|
62
|
-
// --
|
|
63
|
-
if (c.options.
|
|
64
|
-
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
|
+
})
|
|
65
53
|
const result: Record<string, unknown> = {}
|
|
66
|
-
if (
|
|
67
|
-
if (
|
|
68
|
-
if (
|
|
69
|
-
if (
|
|
70
|
-
if (Object.keys(result).length === 0) result.note = 'No
|
|
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'
|
|
71
59
|
if (synced) result.synced = synced
|
|
72
60
|
return result
|
|
73
61
|
}
|
|
74
62
|
|
|
75
63
|
// Default: effective state with scope annotations
|
|
76
|
-
const
|
|
77
|
-
const local = await readLocalState()
|
|
78
|
-
const env = readEnvState()
|
|
79
|
-
const effective = mergeState(global, local, env)
|
|
80
|
-
|
|
81
|
-
// Resolve identity details
|
|
82
|
-
let identityFull: Record<string, unknown> | null = null
|
|
83
|
-
if (effective.identity.value) {
|
|
84
|
-
try {
|
|
85
|
-
const { content: _, ...id } = await loadIdentity(effective.identity.value)
|
|
86
|
-
identityFull = { ...id, scope: effective.identity.scope }
|
|
87
|
-
} catch {
|
|
88
|
-
identityFull = { slug: effective.identity.value, scope: effective.identity.scope, error: 'File not found' }
|
|
89
|
-
}
|
|
90
|
-
}
|
|
64
|
+
const state = await getEffectiveState(api)
|
|
91
65
|
|
|
92
66
|
// Agents and explicit --format get full structured data
|
|
93
67
|
if (c.agent || c.formatExplicit) {
|
|
94
68
|
const result: Record<string, unknown> = {
|
|
95
|
-
soul:
|
|
96
|
-
persona:
|
|
97
|
-
rules:
|
|
98
|
-
identity: identityFull,
|
|
69
|
+
soul: state.soul,
|
|
70
|
+
persona: state.persona,
|
|
71
|
+
rules: state.rules,
|
|
99
72
|
}
|
|
100
73
|
if (synced) result.synced = synced
|
|
101
74
|
return result
|
|
102
75
|
}
|
|
103
76
|
|
|
104
|
-
// Humans get a compact view
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const identityLabel = identityFull
|
|
108
|
-
? identityFull.error
|
|
109
|
-
? `${effective.identity.value} (not found)`
|
|
110
|
-
: identityFull.engine
|
|
111
|
-
? `${identityFull.slug} ${fmtScope(effective.identity.scope)} (${identityFull.engine})`
|
|
112
|
-
: `${identityFull.slug} ${fmtScope(effective.identity.scope)}`
|
|
113
|
-
: null
|
|
114
|
-
|
|
115
|
-
const rulesLabel = effective.rules.length
|
|
116
|
-
? effective.rules
|
|
117
|
-
.filter(r => !r.scope.startsWith('-'))
|
|
118
|
-
.map(r => `${r.value} ${fmtScope(r.scope)}`)
|
|
119
|
-
.join(', ')
|
|
77
|
+
// Humans get a compact view
|
|
78
|
+
const rulesLabel = state.rules.length
|
|
79
|
+
? state.rules.join(', ')
|
|
120
80
|
: null
|
|
121
81
|
|
|
122
82
|
const result: Record<string, unknown> = {
|
|
123
|
-
soul:
|
|
124
|
-
persona:
|
|
83
|
+
soul: state.soul ?? null,
|
|
84
|
+
persona: state.persona ?? null,
|
|
125
85
|
rules: rulesLabel,
|
|
126
|
-
identity: identityLabel,
|
|
127
86
|
}
|
|
128
87
|
if (synced) result.synced = synced
|
|
129
88
|
return result
|
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
|