@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/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@brainjar/cli)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
Shape how your AI thinks —
|
|
8
|
+
Shape how your AI thinks — soul, persona, rules.
|
|
9
9
|
|
|
10
|
-
brainjar manages AI agent behavior through composable layers. Instead of one monolithic config file, you separate **what the agent sounds like** (soul), **how it works** (persona), and **what constraints it follows** (rules). Each layer is a markdown file. Mix and
|
|
10
|
+
brainjar manages AI agent behavior through composable layers. Instead of one monolithic config file, you separate **what the agent sounds like** (soul), **how it works** (persona), and **what constraints it follows** (rules). Each layer is a markdown file. Mix, match, and switch them per project, per task, or per session.
|
|
11
11
|
|
|
12
12
|
**[Documentation](https://brainjar.sh)** · **[Getting Started](https://brainjar.sh/getting-started/)**
|
|
13
13
|
|
|
@@ -36,21 +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
|
-
brainjar identity create|list|show|use|drop|unlock|get|status|lock
|
|
50
49
|
brainjar pack export|import
|
|
51
50
|
brainjar hooks install|remove|status [--local]
|
|
52
|
-
brainjar shell [--brain
|
|
51
|
+
brainjar shell [--brain <name>] [--soul <name>] [--persona <name>]
|
|
53
52
|
brainjar reset [--backend claude|codex]
|
|
53
|
+
brainjar server start|stop|status|logs|local|remote|upgrade
|
|
54
|
+
brainjar migrate [--dry-run] [--skip-backup]
|
|
54
55
|
```
|
|
55
56
|
|
|
56
57
|
See the [CLI reference](https://brainjar.sh/reference/cli/) for full details.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainjar/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shape how your AI thinks — composable
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Shape how your AI thinks — composable soul, persona, and rules for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
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
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { Cli } from 'incur'
|
|
3
3
|
import pkg from '../package.json'
|
|
4
4
|
import { init } from './commands/init.js'
|
|
5
|
-
import { identity } from './commands/identity.js'
|
|
6
5
|
import { soul } from './commands/soul.js'
|
|
7
6
|
import { persona } from './commands/persona.js'
|
|
8
7
|
import { rules } from './commands/rules.js'
|
|
@@ -14,9 +13,11 @@ import { compose } from './commands/compose.js'
|
|
|
14
13
|
import { sync } from './commands/sync.js'
|
|
15
14
|
import { hooks } from './commands/hooks.js'
|
|
16
15
|
import { pack } from './commands/pack.js'
|
|
16
|
+
import { server } from './commands/server.js'
|
|
17
|
+
import { migrate } from './commands/migrate.js'
|
|
17
18
|
|
|
18
19
|
Cli.create('brainjar', {
|
|
19
|
-
description: 'Shape how your AI thinks —
|
|
20
|
+
description: 'Shape how your AI thinks — soul, persona, rules',
|
|
20
21
|
version: pkg.version,
|
|
21
22
|
sync: { depth: 0 },
|
|
22
23
|
})
|
|
@@ -26,11 +27,12 @@ Cli.create('brainjar', {
|
|
|
26
27
|
.command(persona)
|
|
27
28
|
.command(rules)
|
|
28
29
|
.command(brain)
|
|
29
|
-
.command(identity)
|
|
30
30
|
.command(reset)
|
|
31
31
|
.command(shell)
|
|
32
32
|
.command(compose)
|
|
33
33
|
.command(sync)
|
|
34
34
|
.command(hooks)
|
|
35
35
|
.command(pack)
|
|
36
|
+
.command(server)
|
|
37
|
+
.command(migrate)
|
|
36
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
|
+
}
|