@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
@@ -1,105 +0,0 @@
1
- import { $ } from 'bun'
2
- import { readFile } from 'node:fs/promises'
3
- import { paths } from '../paths.js'
4
- import type { CredentialEngine, EngineStatus } from './types.js'
5
-
6
- export async function loadSession(): Promise<string | null> {
7
- // Env var takes precedence, then session file
8
- if (process.env.BW_SESSION) return process.env.BW_SESSION
9
- try {
10
- const session = (await readFile(paths.session, 'utf-8')).trim()
11
- return session || null
12
- } catch {
13
- return null
14
- }
15
- }
16
-
17
- /** Thin shell wrapper — extracted so tests can replace it via spyOn. */
18
- export const bw = {
19
- async whichBw(): Promise<void> {
20
- await $`which bw`.quiet()
21
- },
22
-
23
- async status(session: string | null): Promise<any> {
24
- return session
25
- ? $`bw status`.env({ ...process.env, BW_SESSION: session }).json()
26
- : $`bw status`.json()
27
- },
28
-
29
- async getItem(item: string, session: string): Promise<any> {
30
- return $`bw get item ${item}`.env({ ...process.env, BW_SESSION: session }).json()
31
- },
32
-
33
- async lock(): Promise<void> {
34
- await $`bw lock`.quiet()
35
- },
36
- }
37
-
38
- export const bitwarden: CredentialEngine = {
39
- name: 'bitwarden',
40
-
41
- async status(): Promise<EngineStatus> {
42
- try {
43
- await bw.whichBw()
44
- } catch {
45
- return { state: 'not_installed', install: 'npm install -g @bitwarden/cli' }
46
- }
47
-
48
- try {
49
- const session = await loadSession()
50
- const result = await bw.status(session)
51
-
52
- if (result.status === 'unauthenticated') {
53
- return {
54
- state: 'unauthenticated',
55
- operator_action: `bw login ${result.userEmail ?? '<email>'}`,
56
- }
57
- }
58
-
59
- if (result.status === 'unlocked' && session) {
60
- return { state: 'unlocked', session }
61
- }
62
-
63
- return {
64
- state: 'locked',
65
- operator_action: 'Run `bw unlock` and then `brainjar identity unlock <session>`',
66
- }
67
- } catch {
68
- return {
69
- state: 'locked',
70
- operator_action: 'Could not determine vault status. Run `bw unlock` and then `brainjar identity unlock <session>`',
71
- }
72
- }
73
- },
74
-
75
- async get(item: string, session: string) {
76
- if (!item || item.length > 256 || /[\x00-\x1f]/.test(item)) {
77
- return { error: `Invalid item name: "${item}"` }
78
- }
79
- try {
80
- const result = await bw.getItem(item, session)
81
-
82
- if (result.login?.password) {
83
- return { value: result.login.password }
84
- }
85
- if (result.notes) {
86
- return { value: result.notes }
87
- }
88
-
89
- return { error: `Item "${item}" found but has no password or notes.` }
90
- } catch (e) {
91
- const stderr = (e as any)?.stderr?.toString?.()?.trim?.()
92
- const message = stderr || (e as Error).message || 'unknown error'
93
- return { error: `Could not retrieve "${item}": ${message}` }
94
- }
95
- },
96
-
97
- async lock() {
98
- try {
99
- await bw.lock()
100
- } catch (e) {
101
- const stderr = (e as any)?.stderr?.toString?.()?.trim?.()
102
- throw new Error(`Failed to lock vault: ${stderr || (e as Error).message}`)
103
- }
104
- },
105
- }
@@ -1,12 +0,0 @@
1
- import type { CredentialEngine } from './types.js'
2
- import { bitwarden } from './bitwarden.js'
3
-
4
- const engines: Record<string, CredentialEngine> = {
5
- bitwarden,
6
- }
7
-
8
- export function getEngine(name: string): CredentialEngine | null {
9
- return engines[name] ?? null
10
- }
11
-
12
- export type { CredentialEngine }
@@ -1,12 +0,0 @@
1
- export type EngineStatus =
2
- | { state: 'not_installed'; install: string }
3
- | { state: 'unauthenticated'; operator_action: string }
4
- | { state: 'locked'; operator_action: string }
5
- | { state: 'unlocked'; session: string }
6
-
7
- export interface CredentialEngine {
8
- name: string
9
- status(): Promise<EngineStatus>
10
- get(item: string, session: string): Promise<{ value: string } | { error: string }>
11
- lock(): Promise<void>
12
- }
package/src/hooks.test.ts DELETED
@@ -1,132 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2
- import { readFile, rm, mkdir, writeFile } from 'node:fs/promises'
3
- import { join } from 'node:path'
4
- import { installHooks, removeHooks, getHooksStatus } from './hooks.js'
5
-
6
- const TEST_HOME = join(import.meta.dir, '..', '.test-home-hooks')
7
- const SETTINGS_PATH = join(TEST_HOME, '.claude', 'settings.json')
8
-
9
- beforeEach(async () => {
10
- process.env.BRAINJAR_TEST_HOME = TEST_HOME
11
- await mkdir(join(TEST_HOME, '.claude'), { recursive: true })
12
- })
13
-
14
- afterEach(async () => {
15
- delete process.env.BRAINJAR_TEST_HOME
16
- await rm(TEST_HOME, { recursive: true, force: true })
17
- })
18
-
19
- describe('hooks install', () => {
20
- test('creates hooks in empty settings', async () => {
21
- const result = await installHooks()
22
- expect(result.installed).toContain('SessionStart')
23
-
24
- const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
25
- expect(settings.hooks.SessionStart).toHaveLength(1)
26
- expect(settings.hooks.SessionStart[0].matcher).toBe('startup')
27
- expect(settings.hooks.SessionStart[0].hooks[0].command).toBe('brainjar sync --quiet')
28
- expect(settings.hooks.SessionStart[0].hooks[0]._brainjar).toBe(true)
29
- })
30
-
31
- test('preserves existing settings', async () => {
32
- await writeFile(SETTINGS_PATH, JSON.stringify({
33
- statusLine: { type: 'command', command: 'echo hi' },
34
- enabledPlugins: { foo: true },
35
- }))
36
-
37
- await installHooks()
38
-
39
- const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
40
- expect(settings.statusLine.command).toBe('echo hi')
41
- expect(settings.enabledPlugins.foo).toBe(true)
42
- expect(settings.hooks.SessionStart).toHaveLength(1)
43
- })
44
-
45
- test('preserves existing non-brainjar hooks', async () => {
46
- await writeFile(SETTINGS_PATH, JSON.stringify({
47
- hooks: {
48
- SessionStart: [
49
- { matcher: 'startup', hooks: [{ type: 'command', command: 'echo hello' }] },
50
- ],
51
- PreToolUse: [
52
- { matcher: 'Edit', hooks: [{ type: 'command', command: 'lint.sh' }] },
53
- ],
54
- },
55
- }))
56
-
57
- await installHooks()
58
-
59
- const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
60
- // Existing non-brainjar SessionStart hook preserved
61
- expect(settings.hooks.SessionStart).toHaveLength(2)
62
- // PreToolUse untouched
63
- expect(settings.hooks.PreToolUse).toHaveLength(1)
64
- expect(settings.hooks.PreToolUse[0].hooks[0].command).toBe('lint.sh')
65
- })
66
-
67
- test('is idempotent — no duplicates on re-install', async () => {
68
- await installHooks()
69
- await installHooks()
70
-
71
- const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
72
- const brainjarEntries = settings.hooks.SessionStart.filter(
73
- (m: any) => m.hooks.some((h: any) => h._brainjar)
74
- )
75
- expect(brainjarEntries).toHaveLength(1)
76
- })
77
- })
78
-
79
- describe('hooks remove', () => {
80
- test('removes brainjar hooks', async () => {
81
- await installHooks()
82
- const result = await removeHooks()
83
-
84
- expect(result.removed).toContain('SessionStart')
85
-
86
- const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
87
- expect(settings.hooks).toBeUndefined()
88
- })
89
-
90
- test('preserves non-brainjar hooks', async () => {
91
- await writeFile(SETTINGS_PATH, JSON.stringify({
92
- hooks: {
93
- SessionStart: [
94
- { matcher: 'startup', hooks: [{ type: 'command', command: 'echo hello' }] },
95
- ],
96
- },
97
- }))
98
-
99
- await installHooks()
100
- await removeHooks()
101
-
102
- const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
103
- expect(settings.hooks.SessionStart).toHaveLength(1)
104
- expect(settings.hooks.SessionStart[0].hooks[0].command).toBe('echo hello')
105
- })
106
-
107
- test('no-op when no brainjar hooks exist', async () => {
108
- await writeFile(SETTINGS_PATH, JSON.stringify({ statusLine: { command: 'echo' } }))
109
-
110
- const result = await removeHooks()
111
- expect(result.removed).toHaveLength(0)
112
- })
113
- })
114
-
115
- describe('hooks status', () => {
116
- test('reports not installed when no hooks', async () => {
117
- await writeFile(SETTINGS_PATH, JSON.stringify({}))
118
- const result = await getHooksStatus()
119
- expect(Object.keys(result.hooks)).toHaveLength(0)
120
- })
121
-
122
- test('reports installed hooks', async () => {
123
- await installHooks()
124
- const result = await getHooksStatus()
125
- expect(result.hooks.SessionStart).toBe('brainjar sync --quiet')
126
- })
127
-
128
- test('handles missing settings file', async () => {
129
- const result = await getHooksStatus()
130
- expect(Object.keys(result.hooks)).toHaveLength(0)
131
- })
132
- })