@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.
@@ -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 { access } from 'node:fs/promises'
6
- import { requireBrainjarDir } from '../state.js'
7
+ import { putState } from '../state.js'
7
8
  import { sync } from '../sync.js'
8
- import { getLocalDir } from '../paths.js'
9
- import { readBrain } from '../brain.js'
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 BRAINJAR_* env vars set',
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 file'),
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 new IncurError({
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
- const envOverrides: Record<string, string> = {}
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 readBrain(c.options.brain)
38
- envOverrides.BRAINJAR_SOUL = config.soul
39
- envOverrides.BRAINJAR_PERSONA = config.persona
40
- if (config.rules.length > 0) {
41
- envOverrides.BRAINJAR_RULES_ADD = config.rules.join(',')
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) envOverrides.BRAINJAR_SOUL = c.options.soul
45
- if (c.options.persona) envOverrides.BRAINJAR_PERSONA = c.options.persona
46
- if (c.options['rules-add']) envOverrides.BRAINJAR_RULES_ADD = c.options['rules-add']
47
- if (c.options['rules-remove']) envOverrides.BRAINJAR_RULES_REMOVE = c.options['rules-remove']
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
- if (Object.keys(envOverrides).length === 0) {
51
- throw new IncurError({
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 env overrides passed explicitly (no process.env mutation)
59
- const hasLocal = await access(getLocalDir()).then(() => true, () => false)
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 overrides
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, ...envOverrides },
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
- // Re-sync without env overrides to restore config
93
+ // Clear session state and re-sync to restore
86
94
  let syncWarning: string | undefined
87
95
  try {
88
- await sync()
89
- if (hasLocal) await sync({ local: true })
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
- env: envOverrides,
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(new IncurError({
110
- code: 'SHELL_ERROR',
117
+ reject(createError(ErrorCode.SHELL_ERROR, {
111
118
  message: `Failed to spawn shell: ${err.message}`,
112
119
  }))
113
120
  })
@@ -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 { readdir, readFile, writeFile, access } from 'node:fs/promises'
5
- import { join, basename } from 'node:path'
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 (will be used as filename)'),
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 dest = join(paths.souls, `${name}.md`)
24
+ const api = await getApi()
25
25
 
26
+ // Check if it already exists
26
27
  try {
27
- await access(dest)
28
- throw new IncurError({
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 writeFile(dest, content)
44
+ await api.put<ApiSoul>(`/api/v1/souls/${name}`, { content })
47
45
 
48
46
  if (c.agent || c.formatExplicit) {
49
- return { created: dest, name, template: content }
47
+ return { created: name, name, template: content }
50
48
  }
51
49
 
52
50
  return {
53
- created: dest,
51
+ created: name,
54
52
  name,
55
53
  template: `\n${content}`,
56
- next: `Edit ${dest} to flesh out your soul, then run \`brainjar soul use ${name}\` to activate.`,
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 requireBrainjarDir()
64
- const entries = await readdir(paths.souls).catch(() => [])
65
- const souls = entries.filter(f => f.endsWith('.md')).map(f => basename(f, '.md'))
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
- local: z.boolean().default(false).describe('Show local soul override (if any)'),
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 requireBrainjarDir()
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 global = await readState()
84
- const local = await readLocalState()
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 raw = await readFile(join(paths.souls, `${name}.md`), 'utf-8')
95
- const content = stripFrontmatter(raw)
96
- const title = content.split('\n').find(l => l.startsWith('# '))?.replace('# ', '') ?? null
97
- return { name, title, content }
98
- } catch {
99
- throw new IncurError({
100
- code: 'SOUL_NOT_FOUND',
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.local) {
108
- const local = await readLocalState()
109
- if (!('soul' in local)) return { active: false, scope: 'local', note: 'No local soul override (cascades from global)' }
110
- if (local.soul === null) return { active: false, scope: 'local', name: null, note: 'Explicitly unset at local scope' }
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 raw = await readFile(join(paths.souls, `${local.soul}.md`), 'utf-8')
113
- const content = stripFrontmatter(raw)
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: 'local', name: local.soul, error: 'File not found' }
107
+ return { active: false, scope: 'project', name: state.soul_slug, error: 'Not found on server' }
118
108
  }
119
109
  }
120
110
 
121
- const global = await readState()
122
- const local = await readLocalState()
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 raw = await readFile(join(paths.souls, `${effective.soul.value}.md`), 'utf-8')
128
- const content = stripFrontmatter(raw)
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: effective.soul.value, error: 'File not found' }
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 (filename without .md in ~/.brainjar/souls/)'),
124
+ name: z.string().describe('Soul name'),
140
125
  }),
141
126
  options: z.object({
142
- local: z.boolean().default(false).describe('Write to local .claude/CLAUDE.md instead of global'),
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 source = join(paths.souls, `${name}.md`)
131
+ const api = await getApi()
132
+
133
+ // Validate it exists on server
148
134
  try {
149
- await readFile(source, 'utf-8')
150
- } catch {
151
- throw new IncurError({
152
- code: 'SOUL_NOT_FOUND',
153
- message: `Soul "${name}" not found.`,
154
- hint: 'Run `brainjar soul list` to see available souls.',
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
- if (c.options.local) {
159
- await withLocalStateLock(async () => {
160
- const local = await readLocalState()
161
- local.soul = name
162
- await writeLocalState(local)
163
- await sync({ local: true })
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, local: c.options.local }
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
- local: z.boolean().default(false).describe('Remove local soul override or deactivate global soul'),
157
+ project: z.boolean().default(false).describe('Remove project soul override or deactivate workspace soul'),
181
158
  }),
182
159
  async run(c) {
183
- await requireBrainjarDir()
184
- if (c.options.local) {
185
- await withLocalStateLock(async () => {
186
- const local = await readLocalState()
187
- delete local.soul
188
- await writeLocalState(local)
189
- await sync({ local: true })
190
- })
191
- } else {
192
- await withStateLock(async () => {
193
- const state = await readState()
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
  })
@@ -1,27 +1,27 @@
1
1
  import { Cli, z } from 'incur'
2
- import { readState, readLocalState, readEnvState, mergeState, requireBrainjarDir } from '../state.js'
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
- global: z.boolean().default(false).describe('Show only global state'),
10
- local: z.boolean().default(false).describe('Show only local overrides'),
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 requireBrainjarDir()
17
+ const api = await getApi()
15
18
 
16
19
  // --short: compact one-liner for scripts/statuslines
17
20
  if (c.options.short) {
18
- const global = await readState()
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: ${effective.soul.value ?? 'none'}`,
24
- `persona: ${effective.persona.value ?? 'none'}`,
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
- // --global: show only global state (v0.1 behavior)
37
- if (c.options.global) {
38
- const state = await readState()
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: state.soul ?? null,
41
- persona: state.persona ?? null,
42
- rules: state.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
- // --local: show only local overrides
49
- if (c.options.local) {
50
- const local = await readLocalState()
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 ('soul' in local) result.soul = local.soul
53
- if ('persona' in local) result.persona = local.persona
54
- if (local.rules) result.rules = local.rules
55
- if (Object.keys(result).length === 0) result.note = 'No local overrides'
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 global = await readState()
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: effective.soul,
70
- persona: effective.persona,
71
- rules: effective.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 with scope annotations
78
- const fmtScope = (scope: string) => `(${scope})`
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: effective.soul.value ? `${effective.soul.value} ${fmtScope(effective.soul.scope)}` : null,
89
- persona: effective.persona.value ? `${effective.persona.value} ${fmtScope(effective.persona.scope)}` : null,
83
+ soul: state.soul ?? null,
84
+ persona: state.persona ?? null,
90
85
  rules: rulesLabel,
91
86
  }
92
87
  if (synced) result.synced = synced
@@ -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