@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 CHANGED
@@ -36,20 +36,22 @@ brainjar status
36
36
  ## Commands
37
37
 
38
38
  ```
39
- brainjar init [--default] [--obsidian] [--backend claude|codex]
40
- brainjar status [--sync] [--global|--local] [--short]
39
+ brainjar init [--default] [--backend claude|codex]
40
+ brainjar status [--sync] [--workspace] [--project] [--short]
41
41
  brainjar sync [--quiet]
42
- brainjar compose <brain> [--task <text>]
42
+ brainjar compose <brain> [--persona <name>] [--task <text>]
43
43
 
44
44
  brainjar brain save|use|list|show|drop
45
- brainjar soul create|list|show|use|drop
46
- brainjar persona create|list|show|use|drop
47
- brainjar rules create|list|show|add|remove
45
+ brainjar soul create|use|show|list|drop
46
+ brainjar persona create|use|show|list|drop
47
+ brainjar rules create|add|remove|show|list
48
48
 
49
49
  brainjar pack export|import
50
50
  brainjar hooks install|remove|status [--local]
51
- brainjar shell [--brain|--soul|--persona|--rules-add|--rules-remove]
51
+ brainjar shell [--brain <name>] [--soul <name>] [--persona <name>]
52
52
  brainjar reset [--backend claude|codex]
53
+ brainjar server start|stop|status|logs|local|remote|upgrade
54
+ brainjar migrate [--dry-run] [--skip-backup]
53
55
  ```
54
56
 
55
57
  See the [CLI reference](https://brainjar.sh/reference/cli/) for full details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainjar/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Shape how your AI thinks — composable soul, persona, and rules for AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,155 @@
1
+ /** Soul as returned by the server. */
2
+ export interface ApiSoul {
3
+ slug: string
4
+ title: string | null
5
+ content: string
6
+ }
7
+
8
+ /** Persona as returned by the server. */
9
+ export interface ApiPersona {
10
+ slug: string
11
+ title: string | null
12
+ content: string
13
+ bundled_rules: string[]
14
+ }
15
+
16
+ /** Single entry within a rule. */
17
+ export interface ApiRuleEntry {
18
+ name: string
19
+ content: string
20
+ }
21
+
22
+ /** Rule as returned by the server. */
23
+ export interface ApiRule {
24
+ slug: string
25
+ entries: ApiRuleEntry[]
26
+ }
27
+
28
+ /** Rule summary for list responses. */
29
+ export interface ApiRuleSummary {
30
+ slug: string
31
+ entry_count: number
32
+ }
33
+
34
+ /** Brain as returned by the server. */
35
+ export interface ApiBrain {
36
+ slug: string
37
+ soul_slug: string
38
+ persona_slug: string
39
+ rule_slugs: string[]
40
+ }
41
+
42
+ /** List response wrappers. */
43
+ export interface ApiSoulList {
44
+ souls: Array<{ slug: string; title: string | null }>
45
+ }
46
+
47
+ export interface ApiPersonaList {
48
+ personas: Array<{ slug: string; title: string | null }>
49
+ }
50
+
51
+ export interface ApiRuleList {
52
+ rules: ApiRuleSummary[]
53
+ }
54
+
55
+ export interface ApiBrainList {
56
+ brains: ApiBrain[]
57
+ }
58
+
59
+ /** Effective state as returned by GET /api/v1/state. */
60
+ export interface ApiEffectiveState {
61
+ soul: string | null
62
+ persona: string | null
63
+ rules: string[]
64
+ }
65
+
66
+ /** State override at a single scope, returned by GET /api/v1/state/override. */
67
+ export interface ApiStateOverride {
68
+ soul_slug?: string | null
69
+ persona_slug?: string | null
70
+ rule_slugs?: string[]
71
+ rules_to_add?: string[]
72
+ rules_to_remove?: string[]
73
+ }
74
+
75
+ /** Body for PUT /api/v1/state — partial update. */
76
+ export interface ApiStateMutation {
77
+ soul_slug?: string | null
78
+ persona_slug?: string | null
79
+ rule_slugs?: string[]
80
+ rules_to_add?: string[]
81
+ rules_to_remove?: string[]
82
+ }
83
+
84
+ /** Token estimate breakdown from compose. */
85
+ export interface ApiTokenEstimate {
86
+ soul: number
87
+ persona: number
88
+ rules: number
89
+ task: number
90
+ total: number
91
+ }
92
+
93
+ /** Response from POST /api/v1/compose. */
94
+ export interface ApiComposeResult {
95
+ prompt: string
96
+ soul: string | null
97
+ persona: string
98
+ brain?: string
99
+ rules: string[]
100
+ token_estimate?: ApiTokenEstimate
101
+ warnings?: string[]
102
+ }
103
+
104
+ // --- Content bundle types (export/import) ---
105
+
106
+ export interface BundleSoul {
107
+ content: string
108
+ }
109
+
110
+ export interface BundlePersona {
111
+ content: string
112
+ bundled_rules: string[]
113
+ }
114
+
115
+ export interface BundleRuleEntry {
116
+ sort_key: number
117
+ content: string
118
+ }
119
+
120
+ export interface BundleRule {
121
+ entries: BundleRuleEntry[]
122
+ }
123
+
124
+ export interface BundleBrain {
125
+ soul_slug: string
126
+ persona_slug: string
127
+ rule_slugs: string[]
128
+ }
129
+
130
+ export interface BundleState {
131
+ soul: string
132
+ persona: string
133
+ rules: string[]
134
+ }
135
+
136
+ export interface ContentBundle {
137
+ souls?: Record<string, BundleSoul>
138
+ personas?: Record<string, BundlePersona>
139
+ rules?: Record<string, BundleRule>
140
+ brains?: Record<string, BundleBrain>
141
+ state?: BundleState
142
+ }
143
+
144
+ export interface ImportCounts {
145
+ souls: number
146
+ personas: number
147
+ rules: number
148
+ brains: number
149
+ state: boolean
150
+ }
151
+
152
+ export interface ApiImportResult {
153
+ imported: ImportCounts
154
+ warnings: string[]
155
+ }
package/src/cli.ts CHANGED
@@ -13,6 +13,8 @@ import { compose } from './commands/compose.js'
13
13
  import { sync } from './commands/sync.js'
14
14
  import { hooks } from './commands/hooks.js'
15
15
  import { pack } from './commands/pack.js'
16
+ import { server } from './commands/server.js'
17
+ import { migrate } from './commands/migrate.js'
16
18
 
17
19
  Cli.create('brainjar', {
18
20
  description: 'Shape how your AI thinks — soul, persona, rules',
@@ -31,4 +33,6 @@ Cli.create('brainjar', {
31
33
  .command(sync)
32
34
  .command(hooks)
33
35
  .command(pack)
36
+ .command(server)
37
+ .command(migrate)
34
38
  .serve()
package/src/client.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { Errors } from 'incur'
2
+ import { basename } from 'node:path'
3
+ import { readConfig } from './config.js'
4
+ import { getLocalDir } from './paths.js'
5
+ import { access } from 'node:fs/promises'
6
+ import { ensureRunning } from './daemon.js'
7
+ import { ErrorCode, createError } from './errors.js'
8
+
9
+ const { IncurError } = Errors
10
+
11
+ export interface ClientOptions {
12
+ serverUrl?: string
13
+ workspace?: string
14
+ project?: string
15
+ session?: string
16
+ timeout?: number
17
+ }
18
+
19
+ export interface RequestOptions {
20
+ timeout?: number
21
+ headers?: Record<string, string>
22
+ project?: string
23
+ }
24
+
25
+ export interface BrainjarClient {
26
+ get<T>(path: string, options?: RequestOptions): Promise<T>
27
+ post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>
28
+ put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>
29
+ delete<T>(path: string, options?: RequestOptions): Promise<T>
30
+ }
31
+
32
+ const ERROR_MAP: Record<number, { code: ErrorCode; hint?: string }> = {
33
+ 400: { code: ErrorCode.BAD_REQUEST },
34
+ 401: { code: ErrorCode.UNAUTHORIZED, hint: 'Check your server configuration.' },
35
+ 404: { code: ErrorCode.NOT_FOUND },
36
+ 409: { code: ErrorCode.CONFLICT },
37
+ 422: { code: ErrorCode.VALIDATION_ERROR },
38
+ 500: { code: ErrorCode.SERVER_ERROR, hint: 'Check server logs at ~/.brainjar/server.log' },
39
+ 502: { code: ErrorCode.SERVER_ERROR, hint: 'Server may be starting up. Try again.' },
40
+ 503: { code: ErrorCode.SERVER_UNAVAILABLE, hint: 'Server is not ready. Try again in a moment.' },
41
+ }
42
+
43
+ async function detectProject(explicit?: string): Promise<string | null> {
44
+ if (explicit) return explicit
45
+ try {
46
+ await access(getLocalDir())
47
+ return basename(process.cwd())
48
+ } catch {
49
+ return null
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Create a client instance bound to the current config.
55
+ */
56
+ export async function createClient(options?: ClientOptions): Promise<BrainjarClient> {
57
+ const config = await readConfig()
58
+ const serverUrl = (options?.serverUrl ?? config.server.url).replace(/\/$/, '')
59
+ const workspace = options?.workspace ?? config.workspace
60
+ const session = options?.session ?? process.env.BRAINJAR_SESSION ?? null
61
+ const defaultTimeout = options?.timeout ?? 10_000
62
+ const mode = config.server.mode
63
+
64
+ async function request<T>(method: string, path: string, body?: unknown, reqOpts?: RequestOptions): Promise<T> {
65
+ const url = `${serverUrl}${path}`
66
+ const timeout = reqOpts?.timeout ?? defaultTimeout
67
+
68
+ const headers: Record<string, string> = {
69
+ 'Accept': 'application/json',
70
+ 'X-Brainjar-Workspace': workspace,
71
+ ...(reqOpts?.headers ?? {}),
72
+ }
73
+
74
+ const project = await detectProject(reqOpts?.project ?? options?.project)
75
+ if (project) headers['X-Brainjar-Project'] = project
76
+ if (session) headers['X-Brainjar-Session'] = session
77
+
78
+ if (body !== undefined) {
79
+ headers['Content-Type'] = 'application/json'
80
+ }
81
+
82
+ let response: Response
83
+ try {
84
+ response = await fetch(url, {
85
+ method,
86
+ headers,
87
+ body: body !== undefined ? JSON.stringify(body) : undefined,
88
+ signal: AbortSignal.timeout(timeout),
89
+ })
90
+ } catch (e) {
91
+ if (e instanceof DOMException && e.name === 'TimeoutError') {
92
+ throw createError(ErrorCode.TIMEOUT, {
93
+ message: `Request timed out after ${timeout}ms`,
94
+ })
95
+ }
96
+ const hint = mode === 'local'
97
+ ? "Run 'brainjar server start' or 'brainjar init'."
98
+ : `Check the URL or run 'brainjar server remote <url>'.`
99
+ throw createError(ErrorCode.SERVER_UNREACHABLE, {
100
+ params: [serverUrl],
101
+ hint,
102
+ })
103
+ }
104
+
105
+ if (!response.ok) {
106
+ let serverError: { error?: string | { code?: string; message?: string }; code?: string; message?: string } | null = null
107
+ try {
108
+ serverError = await response.json()
109
+ } catch {}
110
+
111
+ const mapped = ERROR_MAP[response.status]
112
+
113
+ // Handle both flat { error: "msg", code: "X" } and nested { error: { code: "X", message: "msg" } }
114
+ let code: string
115
+ let message: string
116
+ if (serverError?.error && typeof serverError.error === 'object') {
117
+ code = serverError.error.code ?? mapped?.code ?? ErrorCode.API_ERROR
118
+ message = serverError.error.message ?? `Server returned ${response.status}`
119
+ } else {
120
+ code = serverError?.code ?? mapped?.code ?? ErrorCode.API_ERROR
121
+ message = (typeof serverError?.error === 'string' ? serverError.error : null)
122
+ ?? serverError?.message
123
+ ?? `Server returned ${response.status}`
124
+ }
125
+ const hint = mapped?.hint
126
+
127
+ throw new IncurError({ code, message, hint })
128
+ }
129
+
130
+ return response.json() as Promise<T>
131
+ }
132
+
133
+ return {
134
+ get<T>(path: string, options?: RequestOptions) {
135
+ return request<T>('GET', path, undefined, options)
136
+ },
137
+ post<T>(path: string, body?: unknown, options?: RequestOptions) {
138
+ return request<T>('POST', path, body, options)
139
+ },
140
+ put<T>(path: string, body?: unknown, options?: RequestOptions) {
141
+ return request<T>('PUT', path, body, options)
142
+ },
143
+ delete<T>(path: string, options?: RequestOptions) {
144
+ return request<T>('DELETE', path, undefined, options)
145
+ },
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Ensure the server is running and return a connected client.
151
+ * Convenience wrapper — commands should use this instead of calling
152
+ * ensureRunning() + createClient() separately.
153
+ */
154
+ export async function getApi(options?: ClientOptions): Promise<BrainjarClient> {
155
+ await ensureRunning()
156
+ return createClient(options)
157
+ }
@@ -1,24 +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, rm } from 'node:fs/promises'
5
- import { join, basename } from 'node:path'
6
- import { stringify as stringifyYaml } from 'yaml'
7
- import { paths } from '../paths.js'
8
- import {
9
- readState,
10
- writeState,
11
- withStateLock,
12
- readLocalState,
13
- writeLocalState,
14
- withLocalStateLock,
15
- readEnvState,
16
- mergeState,
17
- requireBrainjarDir,
18
- normalizeSlug,
19
- } from '../state.js'
20
- import { readBrain, type BrainConfig } from '../brain.js'
5
+ import { ErrorCode, createError } from '../errors.js'
6
+ import { normalizeSlug, getEffectiveState, putState } from '../state.js'
21
7
  import { sync } from '../sync.js'
8
+ import { getApi } from '../client.js'
9
+ import type { ApiBrain, ApiBrainList, ApiSoul, ApiPersona } from '../api-types.js'
22
10
 
23
11
  export const brain = Cli.create('brain', {
24
12
  description: 'Manage brains — full-stack configuration snapshots (soul + persona + rules)',
@@ -26,65 +14,50 @@ export const brain = Cli.create('brain', {
26
14
  .command('save', {
27
15
  description: 'Snapshot current effective state as a named brain',
28
16
  args: z.object({
29
- name: z.string().describe('Brain name (will be used as filename)'),
17
+ name: z.string().describe('Brain name'),
30
18
  }),
31
19
  options: z.object({
32
- overwrite: z.boolean().default(false).describe('Overwrite existing brain file'),
20
+ overwrite: z.boolean().default(false).describe('Overwrite existing brain'),
33
21
  }),
34
22
  async run(c) {
35
- await requireBrainjarDir()
36
23
  const name = normalizeSlug(c.args.name, 'brain name')
37
- const dest = join(paths.brains, `${name}.yaml`)
24
+ const api = await getApi()
38
25
 
39
26
  // Check for existing brain
40
27
  if (!c.options.overwrite) {
41
28
  try {
42
- await access(dest)
43
- throw new IncurError({
44
- code: 'BRAIN_EXISTS',
45
- message: `Brain "${name}" already exists.`,
46
- hint: 'Use --overwrite to replace it, or choose a different name.',
47
- })
29
+ await api.get<ApiBrain>(`/api/v1/brains/${name}`)
30
+ throw createError(ErrorCode.BRAIN_EXISTS, { params: [name] })
48
31
  } catch (e) {
49
- if (e instanceof IncurError) throw e
32
+ if (e instanceof IncurError && e.code === ErrorCode.BRAIN_EXISTS) throw e
33
+ if (e instanceof IncurError && e.code !== ErrorCode.NOT_FOUND) throw e
50
34
  }
51
35
  }
52
36
 
53
- // Read effective state
54
- const globalState = await readState()
55
- const localState = await readLocalState()
56
- const envState = readEnvState()
57
- const effective = mergeState(globalState, localState, envState)
58
-
59
- if (!effective.soul.value) {
60
- throw new IncurError({
61
- code: 'NO_ACTIVE_SOUL',
62
- message: 'Cannot save brain: no active soul.',
63
- hint: 'Activate a soul first with `brainjar soul use <name>`.',
64
- })
37
+ const effective = await getEffectiveState(api)
38
+
39
+ if (!effective.soul) {
40
+ throw createError(ErrorCode.NO_ACTIVE_SOUL)
65
41
  }
66
42
 
67
- if (!effective.persona.value) {
68
- throw new IncurError({
69
- code: 'NO_ACTIVE_PERSONA',
70
- message: 'Cannot save brain: no active persona.',
71
- hint: 'Activate a persona first with `brainjar persona use <name>`.',
72
- })
43
+ if (!effective.persona) {
44
+ throw createError(ErrorCode.NO_ACTIVE_PERSONA)
73
45
  }
74
46
 
75
47
  const activeRules = effective.rules
76
- .filter(r => !r.scope.startsWith('-'))
77
- .map(r => r.value)
78
48
 
79
- const config: BrainConfig = {
80
- soul: effective.soul.value,
81
- persona: effective.persona.value,
49
+ await api.put<ApiBrain>(`/api/v1/brains/${name}`, {
50
+ soul_slug: effective.soul,
51
+ persona_slug: effective.persona,
52
+ rule_slugs: activeRules,
53
+ })
54
+
55
+ return {
56
+ saved: name,
57
+ soul: effective.soul,
58
+ persona: effective.persona,
82
59
  rules: activeRules,
83
60
  }
84
-
85
- await writeFile(dest, stringifyYaml(config))
86
-
87
- return { saved: name, ...config }
88
61
  },
89
62
  })
90
63
  .command('use', {
@@ -93,68 +66,77 @@ export const brain = Cli.create('brain', {
93
66
  name: z.string().describe('Brain name to activate'),
94
67
  }),
95
68
  options: z.object({
96
- local: z.boolean().default(false).describe('Apply brain at project scope'),
69
+ project: z.boolean().default(false).describe('Apply brain at project scope'),
97
70
  }),
98
71
  async run(c) {
99
- await requireBrainjarDir()
100
72
  const name = normalizeSlug(c.args.name, 'brain name')
101
- const config = await readBrain(name)
73
+ const api = await getApi()
74
+
75
+ let config: ApiBrain
76
+ try {
77
+ config = await api.get<ApiBrain>(`/api/v1/brains/${name}`)
78
+ } catch (e) {
79
+ if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
80
+ throw createError(ErrorCode.BRAIN_NOT_FOUND, { params: [name] })
81
+ }
82
+ throw e
83
+ }
102
84
 
103
85
  // Validate soul exists
104
86
  try {
105
- await readFile(join(paths.souls, `${config.soul}.md`), 'utf-8')
106
- } catch {
107
- throw new IncurError({
108
- code: 'SOUL_NOT_FOUND',
109
- message: `Brain "${name}" references soul "${config.soul}" which does not exist.`,
110
- hint: 'Create the soul first or update the brain file.',
111
- })
87
+ await api.get<ApiSoul>(`/api/v1/souls/${config.soul_slug}`)
88
+ } catch (e) {
89
+ if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
90
+ throw createError(ErrorCode.SOUL_NOT_FOUND, {
91
+ params: [config.soul_slug],
92
+ message: `Brain "${name}" references soul "${config.soul_slug}" which does not exist.`,
93
+ hint: 'Create the soul first or update the brain.',
94
+ })
95
+ }
96
+ throw e
112
97
  }
113
98
 
114
99
  // Validate persona exists
115
100
  try {
116
- await readFile(join(paths.personas, `${config.persona}.md`), 'utf-8')
117
- } catch {
118
- throw new IncurError({
119
- code: 'PERSONA_NOT_FOUND',
120
- message: `Brain "${name}" references persona "${config.persona}" which does not exist.`,
121
- hint: 'Create the persona first or update the brain file.',
122
- })
101
+ await api.get<ApiPersona>(`/api/v1/personas/${config.persona_slug}`)
102
+ } catch (e) {
103
+ if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
104
+ throw createError(ErrorCode.PERSONA_NOT_FOUND, {
105
+ params: [config.persona_slug],
106
+ message: `Brain "${name}" references persona "${config.persona_slug}" which does not exist.`,
107
+ hint: 'Create the persona first or update the brain.',
108
+ })
109
+ }
110
+ throw e
123
111
  }
124
112
 
125
- if (c.options.local) {
126
- await withLocalStateLock(async () => {
127
- const local = await readLocalState()
128
- local.soul = config.soul
129
- local.persona = config.persona
130
- // Replace rules entirely — brain is a complete snapshot
131
- local.rules = { add: config.rules, remove: [] }
132
- await writeLocalState(local)
133
- await sync({ local: true })
134
- })
135
- } else {
136
- await withStateLock(async () => {
137
- const state = await readState()
138
- state.soul = config.soul
139
- state.persona = config.persona
140
- state.rules = config.rules
141
- await writeState(state)
142
- await sync()
143
- })
113
+ const mutationOpts = c.options.project
114
+ ? { project: basename(process.cwd()) }
115
+ : undefined
116
+ await putState(api, {
117
+ soul_slug: config.soul_slug,
118
+ persona_slug: config.persona_slug,
119
+ rule_slugs: config.rule_slugs,
120
+ }, mutationOpts)
121
+
122
+ await sync({ api })
123
+ if (c.options.project) await sync({ api, project: true })
124
+
125
+ return {
126
+ activated: name,
127
+ project: c.options.project,
128
+ soul: config.soul_slug,
129
+ persona: config.persona_slug,
130
+ rules: config.rule_slugs,
144
131
  }
145
-
146
- return { activated: name, local: c.options.local, ...config }
147
132
  },
148
133
  })
149
134
  .command('list', {
150
135
  description: 'List available brains',
151
136
  async run() {
152
- await requireBrainjarDir()
153
- const entries = await readdir(paths.brains).catch(() => [])
154
- const brains = entries
155
- .filter(f => f.endsWith('.yaml'))
156
- .map(f => basename(f, '.yaml'))
157
- return { brains }
137
+ const api = await getApi()
138
+ const result = await api.get<ApiBrainList>('/api/v1/brains')
139
+ return { brains: result.brains.map(b => b.slug) }
158
140
  },
159
141
  })
160
142
  .command('show', {
@@ -163,10 +145,18 @@ export const brain = Cli.create('brain', {
163
145
  name: z.string().describe('Brain name to show'),
164
146
  }),
165
147
  async run(c) {
166
- await requireBrainjarDir()
167
148
  const name = normalizeSlug(c.args.name, 'brain name')
168
- const config = await readBrain(name)
169
- return { name, ...config }
149
+ const api = await getApi()
150
+
151
+ try {
152
+ const config = await api.get<ApiBrain>(`/api/v1/brains/${name}`)
153
+ return { name, soul: config.soul_slug, persona: config.persona_slug, rules: config.rule_slugs }
154
+ } catch (e) {
155
+ if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
156
+ throw createError(ErrorCode.BRAIN_NOT_FOUND, { params: [name] })
157
+ }
158
+ throw e
159
+ }
170
160
  },
171
161
  })
172
162
  .command('drop', {
@@ -175,22 +165,18 @@ export const brain = Cli.create('brain', {
175
165
  name: z.string().describe('Brain name to delete'),
176
166
  }),
177
167
  async run(c) {
178
- await requireBrainjarDir()
179
168
  const name = normalizeSlug(c.args.name, 'brain name')
180
- const file = join(paths.brains, `${name}.yaml`)
169
+ const api = await getApi()
181
170
 
182
171
  try {
183
- await access(file)
184
- } catch {
185
- throw new IncurError({
186
- code: 'BRAIN_NOT_FOUND',
187
- message: `Brain "${name}" not found.`,
188
- hint: 'Run `brainjar brain list` to see available brains.',
189
- })
172
+ await api.delete(`/api/v1/brains/${name}`)
173
+ } catch (e) {
174
+ if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
175
+ throw createError(ErrorCode.BRAIN_NOT_FOUND, { params: [name] })
176
+ }
177
+ throw e
190
178
  }
191
179
 
192
- await rm(file)
193
-
194
180
  return { dropped: name }
195
181
  },
196
182
  })