@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.
Files changed (40) hide show
  1. package/README.md +11 -10
  2. package/package.json +2 -2
  3. package/src/api-types.ts +155 -0
  4. package/src/cli.ts +5 -3
  5. package/src/client.ts +157 -0
  6. package/src/commands/brain.ts +99 -113
  7. package/src/commands/compose.ts +17 -116
  8. package/src/commands/init.ts +66 -42
  9. package/src/commands/migrate.ts +61 -0
  10. package/src/commands/pack.ts +1 -5
  11. package/src/commands/persona.ts +97 -145
  12. package/src/commands/rules.ts +71 -174
  13. package/src/commands/server.ts +212 -0
  14. package/src/commands/shell.ts +55 -51
  15. package/src/commands/soul.ts +75 -110
  16. package/src/commands/status.ts +37 -78
  17. package/src/commands/sync.ts +0 -2
  18. package/src/config.ts +125 -0
  19. package/src/daemon.ts +404 -0
  20. package/src/errors.ts +172 -0
  21. package/src/migrate.ts +247 -0
  22. package/src/pack.ts +149 -428
  23. package/src/paths.ts +1 -8
  24. package/src/seeds.ts +62 -105
  25. package/src/state.ts +12 -397
  26. package/src/sync.ts +61 -102
  27. package/src/version-check.ts +137 -0
  28. package/src/brain.ts +0 -69
  29. package/src/commands/identity.ts +0 -276
  30. package/src/engines/bitwarden.ts +0 -105
  31. package/src/engines/index.ts +0 -12
  32. package/src/engines/types.ts +0 -12
  33. package/src/hooks.test.ts +0 -132
  34. package/src/pack.test.ts +0 -472
  35. package/src/seeds/templates/persona.md +0 -19
  36. package/src/seeds/templates/rule.md +0 -11
  37. package/src/seeds/templates/soul.md +0 -20
  38. /package/src/seeds/rules/{default/boundaries.md → boundaries.md} +0 -0
  39. /package/src/seeds/rules/{default/context-recovery.md → context-recovery.md} +0 -0
  40. /package/src/seeds/rules/{default/task-completion.md → task-completion.md} +0 -0
package/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  [![downloads](https://img.shields.io/npm/dm/@brainjar/cli)](https://www.npmjs.com/package/@brainjar/cli)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
7
 
8
- Shape how your AI thinks — identity, soul, persona, rules.
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 match them per project, per task, or per session.
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] [--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
- 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|--soul|--persona|--identity|--rules-add|--rules-remove]
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.2.3",
4
- "description": "Shape how your AI thinks — composable identity, soul, persona, and rules for AI agents",
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": {
@@ -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 — identity, soul, persona, rules',
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
+ }