@brainjar/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/package.json +1 -1
- package/src/api-types.ts +155 -0
- package/src/cli.ts +4 -0
- package/src/client.ts +157 -0
- package/src/commands/brain.ts +99 -113
- package/src/commands/compose.ts +17 -116
- package/src/commands/init.ts +65 -40
- package/src/commands/migrate.ts +61 -0
- package/src/commands/pack.ts +1 -5
- package/src/commands/persona.ts +97 -145
- package/src/commands/rules.ts +71 -174
- package/src/commands/server.ts +212 -0
- package/src/commands/shell.ts +53 -46
- package/src/commands/soul.ts +75 -110
- package/src/commands/status.ts +36 -41
- package/src/commands/sync.ts +0 -2
- package/src/config.ts +125 -0
- package/src/daemon.ts +404 -0
- package/src/errors.ts +172 -0
- package/src/migrate.ts +247 -0
- package/src/pack.ts +149 -428
- package/src/paths.ts +1 -6
- package/src/seeds.ts +62 -103
- package/src/state.ts +12 -368
- package/src/sync.ts +60 -85
- package/src/version-check.ts +137 -0
- package/src/brain.ts +0 -69
- package/src/hooks.test.ts +0 -132
- package/src/pack.test.ts +0 -472
- package/src/seeds/templates/persona.md +0 -19
- package/src/seeds/templates/rule.md +0 -11
- package/src/seeds/templates/soul.md +0 -20
- /package/src/seeds/rules/{default/boundaries.md → boundaries.md} +0 -0
- /package/src/seeds/rules/{default/context-recovery.md → context-recovery.md} +0 -0
- /package/src/seeds/rules/{default/task-completion.md → task-completion.md} +0 -0
package/README.md
CHANGED
|
@@ -36,20 +36,22 @@ brainjar status
|
|
|
36
36
|
## Commands
|
|
37
37
|
|
|
38
38
|
```
|
|
39
|
-
brainjar init [--default] [--
|
|
40
|
-
brainjar status [--sync] [--
|
|
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|
|
|
46
|
-
brainjar persona create|
|
|
47
|
-
brainjar rules create|
|
|
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
|
|
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
package/src/api-types.ts
ADDED
|
@@ -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
|
+
}
|
package/src/commands/brain.ts
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
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
|
|
17
|
+
name: z.string().describe('Brain name'),
|
|
30
18
|
}),
|
|
31
19
|
options: z.object({
|
|
32
|
-
overwrite: z.boolean().default(false).describe('Overwrite existing brain
|
|
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
|
|
24
|
+
const api = await getApi()
|
|
38
25
|
|
|
39
26
|
// Check for existing brain
|
|
40
27
|
if (!c.options.overwrite) {
|
|
41
28
|
try {
|
|
42
|
-
await
|
|
43
|
-
throw
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
68
|
-
throw
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
106
|
-
} catch {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
117
|
-
} catch {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
153
|
-
const
|
|
154
|
-
|
|
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
|
|
169
|
-
|
|
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
|
|
169
|
+
const api = await getApi()
|
|
181
170
|
|
|
182
171
|
try {
|
|
183
|
-
await
|
|
184
|
-
} catch {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
})
|