@brainjar/cli 0.2.2 → 0.3.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 CHANGED
@@ -5,9 +5,11 @@
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
+
12
+ **[Documentation](https://brainjar.sh)** · **[Getting Started](https://brainjar.sh/getting-started/)**
11
13
 
12
14
  ## Quick start
13
15
 
@@ -22,272 +24,14 @@ brainjar init --default
22
24
  brainjar status
23
25
  ```
24
26
 
25
- That gives you a soul (craftsman), a persona (engineer), and rules (boundaries, git discipline, security) — all wired into your `CLAUDE.md` and ready to go.
26
-
27
27
  ## Concepts
28
28
 
29
- brainjar has four core concepts. Understanding these makes everything else click.
30
-
31
- ### Soul — who the agent is
32
-
33
- The soul defines personality: tone, character, standards. It's the constant across all tasks. You probably only have one or two. Think of it as the agent's voice.
34
-
35
- ### Persona — how the agent works
36
-
37
- Personas define role and workflow. An engineer persona works differently than a reviewer or an architect. Switch personas based on what you're doing — they're the agent's job description.
38
-
39
- Personas bundle their own rules via frontmatter:
40
-
41
- ```yaml
42
- ---
43
- rules:
44
- - default
45
- - security
46
- ---
47
- ```
48
-
49
- ### Rules — what the agent must follow
50
-
51
- Rules are behavioral constraints — guardrails that apply regardless of persona. Single files or multi-file packs (directories). Rules from a persona's frontmatter activate automatically when that persona is active.
52
-
53
- ### Brain — a saved configuration
54
-
55
- A brain is a named snapshot of soul + persona + rules. Instead of switching three things separately, save your setup once and activate it in one shot. Useful for repeatable workflows like "code review" or "design session."
56
-
57
- ### How they compose
58
-
59
- ```
60
- soul + persona + rules = full agent behavior
61
- │ │
62
- └── bundled rules ───┘ (from persona frontmatter)
63
- ```
64
-
65
- A brain captures all three layers. `compose` assembles them into a single prompt for subagent dispatch.
66
-
67
- ## State cascade
68
-
69
- State merges in three tiers. Each tier overrides the previous:
70
-
71
- ```
72
- global → local → env
73
- ```
74
-
75
- | Tier | Storage | When to use |
76
- |------|---------|-------------|
77
- | **Global** | `~/.brainjar/state.yaml` | Default behavior across all projects |
78
- | **Local** | `.brainjar/state.yaml` (in project) | Per-project overrides |
79
- | **Env** | `BRAINJAR_*` environment variables | Per-session or CI overrides |
80
-
81
- ```bash
82
- # Global (default for all projects)
83
- brainjar persona use engineer
84
-
85
- # Local (this project only)
86
- brainjar persona use planner --local
87
-
88
- # Env (this session only)
89
- BRAINJAR_PERSONA=reviewer claude
90
-
91
- # Or use a subshell for scoped sessions
92
- brainjar shell --persona reviewer --rules-add security
93
- ```
94
-
95
- `brainjar status` shows where each setting comes from:
96
-
97
- ```
98
- soul craftsman (global)
99
- persona planner (local)
100
- rules default (global), security (+local)
101
- ```
102
-
103
- ### Scope annotations
104
-
105
- When you see scope labels in `status` and `rules list` output:
106
-
107
- | Label | Meaning |
29
+ | Layer | Purpose |
108
30
  |-------|---------|
109
- | `(global)` | Set in `~/.brainjar/state.yaml` |
110
- | `(local)` | Overridden in `.brainjar/state.yaml` |
111
- | `(+local)` | Added by local override (not in global) |
112
- | `(-local)` | Removed by local override (active globally, suppressed here) |
113
- | `(env)` | Overridden by `BRAINJAR_*` env var |
114
- | `(+env)` | Added by env var |
115
- | `(-env)` | Removed by env var |
116
-
117
- ### Environment variables
118
-
119
- These env vars override all other state when set:
120
-
121
- | Variable | Effect |
122
- |----------|--------|
123
- | `BRAINJAR_HOME` | Override `~/.brainjar/` location |
124
- | `BRAINJAR_SOUL` | Override active soul |
125
- | `BRAINJAR_PERSONA` | Override active persona |
126
- | `BRAINJAR_IDENTITY` | Override active identity |
127
- | `BRAINJAR_RULES_ADD` | Comma-separated rules to add |
128
- | `BRAINJAR_RULES_REMOVE` | Comma-separated rules to remove |
129
-
130
- Set to empty string to explicitly unset (e.g., `BRAINJAR_SOUL=""` removes the soul for that session).
131
-
132
- ## What it does
133
-
134
- ```
135
- ~/.brainjar/
136
- souls/ # Voice and character — who the agent is
137
- craftsman.md
138
- personas/ # Role and workflow — how the agent works
139
- engineer.md
140
- planner.md
141
- reviewer.md
142
- rules/ # Constraints — what the agent must follow
143
- default/ # Boundaries, context recovery, task completion
144
- git-discipline.md
145
- security.md
146
- brains/ # Full-stack snapshots — soul + persona + rules
147
- review.yaml
148
- ```
149
-
150
- brainjar reads these markdown files, merges the active layers, and inlines them into your agent's config (`~/.claude/CLAUDE.md` or `.codex/AGENTS.md`) between `<!-- brainjar:start -->` / `<!-- brainjar:end -->` markers. Everything outside the markers is yours. Change a layer, and the agent's behavior changes on next sync.
151
-
152
- ## Layers
153
-
154
- ### Soul
155
-
156
- ```bash
157
- brainjar soul create mysoul --description "Direct and rigorous"
158
- brainjar soul use mysoul
159
- ```
160
-
161
- ### Persona
162
-
163
- ```bash
164
- brainjar persona use planner # Design session
165
- brainjar persona use engineer # Build session
166
- brainjar persona use reviewer # Review session
167
- ```
168
-
169
- ### Rules
170
-
171
- ```bash
172
- brainjar rules create no-delete --description "Never delete files without asking"
173
- brainjar rules add no-delete
174
- brainjar rules remove no-delete
175
- ```
176
-
177
- ### Brain
178
-
179
- ```bash
180
- brainjar brain save review # Snapshot current state as a brain
181
- brainjar brain use review # Activate soul + persona + rules in one shot
182
- brainjar brain list # See available brains
183
- brainjar brain show review # Inspect a brain's config
184
- ```
185
-
186
- When to use a brain vs. switching layers individually:
187
- - **Brain:** Repeatable workflow you do often (code review, design, debugging). Save once, activate in one command.
188
- - **Individual layers:** Exploratory work, one-off overrides, or when you only need to change one thing.
189
-
190
- ## Subagent orchestration
191
-
192
- Personas can spawn other personas as subagents. For example, a tech-lead persona can:
193
-
194
- 1. Spawn an **architect** subagent for design — produces a design doc
195
- 2. Get user approval
196
- 3. Implement the design itself
197
- 4. Spawn a **reviewer** subagent to verify — compares code against the spec
198
-
199
- Each subagent gets the full brain context: soul + persona + rules. The `compose` command assembles the full prompt in a single call:
200
-
201
- ```bash
202
- # Primary path — brain drives everything
203
- brainjar compose review --task "Review the changes in src/sync.ts"
204
-
205
- # Ad-hoc — no saved brain, specify persona directly
206
- brainjar compose --persona reviewer --task "Review the changes in src/sync.ts"
207
- ```
208
-
209
- For more granular control, use `brainjar persona show <name>` and `brainjar rules show <name>` to retrieve individual layers.
210
-
211
- ## Recipes
212
-
213
- ### Code review session
214
-
215
- ```bash
216
- # Save a review brain once
217
- brainjar soul use craftsman
218
- brainjar persona use reviewer
219
- brainjar rules add default
220
- brainjar rules add security
221
- brainjar brain save review
222
-
223
- # Then activate it anytime
224
- brainjar brain use review
225
-
226
- # Or scope it to a single session
227
- brainjar shell --brain review
228
- ```
229
-
230
- ### CI pipeline — enforce rules without a persona
231
-
232
- ```bash
233
- # In CI, use env vars to override behavior
234
- BRAINJAR_PERSONA=auditor BRAINJAR_RULES_ADD=security,compliance brainjar status --sync
235
- ```
236
-
237
- ### Project-specific persona
238
-
239
- ```bash
240
- # In your project directory
241
- brainjar persona use planner --local
242
- brainjar rules add no-delete --local
243
-
244
- # Global settings still apply — local just overrides what you specify
245
- brainjar status
246
- # soul craftsman (global)
247
- # persona planner (local)
248
- # rules default (global), no-delete (+local)
249
- ```
250
-
251
- ## Pack
252
-
253
- Packs are self-contained, shareable bundles of a brain and all its layers — soul, persona, and rules. Export a brain as a pack directory, hand it to a teammate, and they import it in one command.
254
-
255
- A pack mirrors the `~/.brainjar/` structure with a `pack.yaml` manifest at the root. No tarballs, no magic — just files you can inspect with `ls` and `cat`.
256
-
257
- ```bash
258
- # Export a brain as a pack
259
- brainjar pack export review # creates ./review/
260
- brainjar pack export review --out /tmp # creates /tmp/review/
261
- brainjar pack export review --name my-review # override pack name
262
- brainjar pack export review --version 1.0.0 # set version (default: 0.1.0)
263
- brainjar pack export review --author frank # set author field
264
-
265
- # Import a pack
266
- brainjar pack import ./review # import into ~/.brainjar/
267
- brainjar pack import ./review --force # overwrite conflicts
268
- brainjar pack import ./review --merge # rename conflicts as <name>-from-<packname>
269
- brainjar pack import ./review --activate # activate the brain after import
270
- ```
271
-
272
- On conflict (a file already exists with different content), import fails by default and lists the conflicts. Use `--force` to overwrite or `--merge` to keep both versions. Identical files are silently skipped.
273
-
274
- ## Hooks
275
-
276
- brainjar integrates with Claude Code's hook system for automatic context injection. When hooks are installed, brainjar syncs your config on every session start — no manual `brainjar sync` needed.
277
-
278
- ```bash
279
- # Install hooks (writes to ~/.claude/settings.json)
280
- brainjar hooks install
281
-
282
- # Install for this project only
283
- brainjar hooks install --local
284
-
285
- # Check hook status
286
- brainjar hooks status
287
-
288
- # Remove hooks
289
- brainjar hooks remove
290
- ```
31
+ | **Soul** | Who the agent is — voice, character, standards |
32
+ | **Persona** | How the agent works — role, workflow, bundled rules |
33
+ | **Rules** | Behavioral constraints guardrails that apply regardless of persona |
34
+ | **Brain** | Saved snapshot of soul + persona + rules — activate in one shot |
291
35
 
292
36
  ## Commands
293
37
 
@@ -296,52 +40,19 @@ brainjar init [--default] [--obsidian] [--backend claude|codex]
296
40
  brainjar status [--sync] [--global|--local] [--short]
297
41
  brainjar sync [--quiet]
298
42
  brainjar compose <brain> [--task <text>]
299
- brainjar compose --persona <name> [--task <text>]
300
43
 
301
44
  brainjar brain save|use|list|show|drop
302
45
  brainjar soul create|list|show|use|drop
303
46
  brainjar persona create|list|show|use|drop
304
47
  brainjar rules create|list|show|add|remove
305
48
 
306
- brainjar identity create|list|show|use|drop|unlock|get|status|lock
307
49
  brainjar pack export|import
308
50
  brainjar hooks install|remove|status [--local]
309
- brainjar shell [--brain|--soul|--persona|--identity|--rules-add|--rules-remove]
51
+ brainjar shell [--brain|--soul|--persona|--rules-add|--rules-remove]
310
52
  brainjar reset [--backend claude|codex]
311
53
  ```
312
54
 
313
- `show` accepts an optional name to view any item, not just the active one. Use `--short` to get just the active name (useful in scripts and statuslines):
314
-
315
- ```bash
316
- brainjar persona show reviewer # View a specific persona
317
- brainjar soul show # View the active soul
318
- brainjar rules show security # View a rule's content
319
- brainjar status --short # One-liner: soul | persona | identity
320
- brainjar soul show --short # Just the active soul name
321
- ```
322
-
323
- ## Obsidian support
324
-
325
- `~/.brainjar/` is a folder of markdown files — it's already almost an Obsidian vault.
326
-
327
- ```bash
328
- brainjar init --obsidian
329
- ```
330
-
331
- Adds `.obsidian/` config that hides private files (state, identities) from the file explorer and includes templates for creating new souls, personas, and rules from within Obsidian.
332
-
333
- ## Backends
334
-
335
- ```bash
336
- brainjar init --backend codex # writes ~/.codex/AGENTS.md
337
- brainjar reset --backend codex
338
- ```
339
-
340
- Supported: `claude` (default), `codex`
341
-
342
- ## Backup & restore
343
-
344
- On first sync, brainjar backs up any existing config file to `CLAUDE.md.pre-brainjar`. Running `brainjar reset` removes brainjar-managed config and restores the backup.
55
+ See the [CLI reference](https://brainjar.sh/reference/cli/) for full details.
345
56
 
346
57
  ## Development
347
58
 
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@brainjar/cli",
3
- "version": "0.2.2",
4
- "description": "Shape how your AI thinks — composable identity, soul, persona, and rules for AI agents",
3
+ "version": "0.3.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": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/brainjar-sh/brainjar-cli.git"
10
10
  },
11
- "homepage": "https://github.com/brainjar-sh/brainjar-cli",
11
+ "homepage": "https://brainjar.sh",
12
12
  "bugs": "https://github.com/brainjar-sh/brainjar-cli/issues",
13
13
  "keywords": [
14
14
  "ai",
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'
@@ -16,7 +15,7 @@ import { hooks } from './commands/hooks.js'
16
15
  import { pack } from './commands/pack.js'
17
16
 
18
17
  Cli.create('brainjar', {
19
- description: 'Shape how your AI thinks — identity, soul, persona, rules',
18
+ description: 'Shape how your AI thinks — soul, persona, rules',
20
19
  version: pkg.version,
21
20
  sync: { depth: 0 },
22
21
  })
@@ -26,7 +25,6 @@ Cli.create('brainjar', {
26
25
  .command(persona)
27
26
  .command(rules)
28
27
  .command(brain)
29
- .command(identity)
30
28
  .command(reset)
31
29
  .command(shell)
32
30
  .command(compose)
@@ -21,14 +21,13 @@ export const init = Cli.create('init', {
21
21
  mkdir(paths.personas, { recursive: true }),
22
22
  mkdir(paths.rules, { recursive: true }),
23
23
  mkdir(paths.brains, { recursive: true }),
24
- mkdir(paths.identities, { recursive: true }),
25
24
  ])
26
25
 
27
26
  // Seed the default rule pack
28
27
  await seedDefaultRule(paths.rules)
29
28
 
30
29
  // Build .gitignore — always exclude private files, add .obsidian if vault enabled
31
- const gitignoreLines = ['identities/', '.session', 'state.yaml']
30
+ const gitignoreLines = ['state.yaml']
32
31
  if (c.options.obsidian) {
33
32
  gitignoreLines.push('.obsidian/', 'templates/')
34
33
  }
@@ -53,7 +52,7 @@ export const init = Cli.create('init', {
53
52
  const result: Record<string, unknown> = {
54
53
  created: brainjarDir,
55
54
  backend: c.options.backend,
56
- directories: ['souls/', 'personas/', 'rules/', 'brains/', 'identities/'],
55
+ directories: ['souls/', 'personas/', 'rules/', 'brains/'],
57
56
  }
58
57
 
59
58
  if (c.options.default) {
@@ -63,7 +62,7 @@ export const init = Cli.create('init', {
63
62
  result.personas = ['engineer', 'planner', 'reviewer']
64
63
  result.next = 'Ready to go. Run `brainjar status` to see your config.'
65
64
  } else {
66
- result.next = 'Run `brainjar identity create <slug> --name <name> --email <email>` to set up your first identity.'
65
+ result.next = 'Run `brainjar soul create <name>` to create your first soul.'
67
66
  }
68
67
 
69
68
  if (c.options.obsidian) {
@@ -14,20 +14,19 @@ export const shell = Cli.create('shell', {
14
14
  brain: z.string().optional().describe('Brain name — sets soul, persona, and rules from brain file'),
15
15
  soul: z.string().optional().describe('Soul override for this session'),
16
16
  persona: z.string().optional().describe('Persona override for this session'),
17
- identity: z.string().optional().describe('Identity override for this session'),
18
17
  'rules-add': z.string().optional().describe('Comma-separated rules to add'),
19
18
  'rules-remove': z.string().optional().describe('Comma-separated rules to remove'),
20
19
  }),
21
20
  async run(c) {
22
21
  await requireBrainjarDir()
23
22
 
24
- const individualFlags = c.options.soul || c.options.persona || c.options.identity
23
+ const individualFlags = c.options.soul || c.options.persona
25
24
  || c.options['rules-add'] || c.options['rules-remove']
26
25
 
27
26
  if (c.options.brain && individualFlags) {
28
27
  throw new IncurError({
29
28
  code: 'MUTUALLY_EXCLUSIVE',
30
- message: '--brain is mutually exclusive with --soul, --persona, --identity, --rules-add, --rules-remove.',
29
+ message: '--brain is mutually exclusive with --soul, --persona, --rules-add, --rules-remove.',
31
30
  hint: 'Use --brain alone or individual flags, not both.',
32
31
  })
33
32
  }
@@ -44,7 +43,6 @@ export const shell = Cli.create('shell', {
44
43
  } else {
45
44
  if (c.options.soul) envOverrides.BRAINJAR_SOUL = c.options.soul
46
45
  if (c.options.persona) envOverrides.BRAINJAR_PERSONA = c.options.persona
47
- if (c.options.identity) envOverrides.BRAINJAR_IDENTITY = c.options.identity
48
46
  if (c.options['rules-add']) envOverrides.BRAINJAR_RULES_ADD = c.options['rules-add']
49
47
  if (c.options['rules-remove']) envOverrides.BRAINJAR_RULES_REMOVE = c.options['rules-remove']
50
48
  }
@@ -53,7 +51,7 @@ export const shell = Cli.create('shell', {
53
51
  throw new IncurError({
54
52
  code: 'NO_OVERRIDES',
55
53
  message: 'No overrides specified.',
56
- hint: 'Use --brain, --soul, --persona, --identity, --rules-add, or --rules-remove.',
54
+ hint: 'Use --brain, --soul, --persona, --rules-add, or --rules-remove.',
57
55
  })
58
56
  }
59
57
 
@@ -66,7 +64,6 @@ export const shell = Cli.create('shell', {
66
64
  const labels: string[] = []
67
65
  if (envOverrides.BRAINJAR_SOUL) labels.push(`soul: ${envOverrides.BRAINJAR_SOUL}`)
68
66
  if (envOverrides.BRAINJAR_PERSONA) labels.push(`persona: ${envOverrides.BRAINJAR_PERSONA}`)
69
- if (envOverrides.BRAINJAR_IDENTITY) labels.push(`identity: ${envOverrides.BRAINJAR_IDENTITY}`)
70
67
  if (envOverrides.BRAINJAR_RULES_ADD) labels.push(`+rules: ${envOverrides.BRAINJAR_RULES_ADD}`)
71
68
  if (envOverrides.BRAINJAR_RULES_REMOVE) labels.push(`-rules: ${envOverrides.BRAINJAR_RULES_REMOVE}`)
72
69
 
@@ -1,5 +1,5 @@
1
1
  import { Cli, z } from 'incur'
2
- import { readState, readLocalState, readEnvState, mergeState, loadIdentity, requireBrainjarDir } from '../state.js'
2
+ import { readState, readLocalState, readEnvState, mergeState, requireBrainjarDir } from '../state.js'
3
3
  import { sync } from '../sync.js'
4
4
 
5
5
  export const status = Cli.create('status', {
@@ -8,7 +8,7 @@ export const status = Cli.create('status', {
8
8
  sync: z.boolean().default(false).describe('Regenerate config file from active layers'),
9
9
  global: z.boolean().default(false).describe('Show only global state'),
10
10
  local: z.boolean().default(false).describe('Show only local overrides'),
11
- short: z.boolean().default(false).describe('One-line output: soul | persona | identity'),
11
+ short: z.boolean().default(false).describe('One-line output: soul | persona'),
12
12
  }),
13
13
  async run(c) {
14
14
  await requireBrainjarDir()
@@ -19,13 +19,9 @@ export const status = Cli.create('status', {
19
19
  const local = await readLocalState()
20
20
  const env = readEnvState()
21
21
  const effective = mergeState(global, local, env)
22
- const slug = effective.identity.value
23
- ? (await loadIdentity(effective.identity.value).catch(() => null))?.slug ?? effective.identity.value
24
- : null
25
22
  const parts = [
26
23
  `soul: ${effective.soul.value ?? 'none'}`,
27
24
  `persona: ${effective.persona.value ?? 'none'}`,
28
- `identity: ${slug ?? 'none'}`,
29
25
  ]
30
26
  return parts.join(' | ')
31
27
  }
@@ -40,20 +36,10 @@ export const status = Cli.create('status', {
40
36
  // --global: show only global state (v0.1 behavior)
41
37
  if (c.options.global) {
42
38
  const state = await readState()
43
- let identityFull: Record<string, unknown> | null = null
44
- if (state.identity) {
45
- try {
46
- const { content: _, ...id } = await loadIdentity(state.identity)
47
- identityFull = id
48
- } catch {
49
- identityFull = { slug: state.identity, error: 'File not found' }
50
- }
51
- }
52
39
  const result: Record<string, unknown> = {
53
40
  soul: state.soul ?? null,
54
41
  persona: state.persona ?? null,
55
42
  rules: state.rules,
56
- identity: identityFull,
57
43
  }
58
44
  if (synced) result.synced = synced
59
45
  return result
@@ -66,7 +52,6 @@ export const status = Cli.create('status', {
66
52
  if ('soul' in local) result.soul = local.soul
67
53
  if ('persona' in local) result.persona = local.persona
68
54
  if (local.rules) result.rules = local.rules
69
- if ('identity' in local) result.identity = local.identity
70
55
  if (Object.keys(result).length === 0) result.note = 'No local overrides'
71
56
  if (synced) result.synced = synced
72
57
  return result
@@ -78,24 +63,12 @@ export const status = Cli.create('status', {
78
63
  const env = readEnvState()
79
64
  const effective = mergeState(global, local, env)
80
65
 
81
- // Resolve identity details
82
- let identityFull: Record<string, unknown> | null = null
83
- if (effective.identity.value) {
84
- try {
85
- const { content: _, ...id } = await loadIdentity(effective.identity.value)
86
- identityFull = { ...id, scope: effective.identity.scope }
87
- } catch {
88
- identityFull = { slug: effective.identity.value, scope: effective.identity.scope, error: 'File not found' }
89
- }
90
- }
91
-
92
66
  // Agents and explicit --format get full structured data
93
67
  if (c.agent || c.formatExplicit) {
94
68
  const result: Record<string, unknown> = {
95
69
  soul: effective.soul,
96
70
  persona: effective.persona,
97
71
  rules: effective.rules,
98
- identity: identityFull,
99
72
  }
100
73
  if (synced) result.synced = synced
101
74
  return result
@@ -104,14 +77,6 @@ export const status = Cli.create('status', {
104
77
  // Humans get a compact view with scope annotations
105
78
  const fmtScope = (scope: string) => `(${scope})`
106
79
 
107
- const identityLabel = identityFull
108
- ? identityFull.error
109
- ? `${effective.identity.value} (not found)`
110
- : identityFull.engine
111
- ? `${identityFull.slug} ${fmtScope(effective.identity.scope)} (${identityFull.engine})`
112
- : `${identityFull.slug} ${fmtScope(effective.identity.scope)}`
113
- : null
114
-
115
80
  const rulesLabel = effective.rules.length
116
81
  ? effective.rules
117
82
  .filter(r => !r.scope.startsWith('-'))
@@ -123,7 +88,6 @@ export const status = Cli.create('status', {
123
88
  soul: effective.soul.value ? `${effective.soul.value} ${fmtScope(effective.soul.scope)}` : null,
124
89
  persona: effective.persona.value ? `${effective.persona.value} ${fmtScope(effective.persona.scope)}` : null,
125
90
  rules: rulesLabel,
126
- identity: identityLabel,
127
91
  }
128
92
  if (synced) result.synced = synced
129
93
  return result
package/src/paths.ts CHANGED
@@ -41,8 +41,6 @@ export const paths = {
41
41
  get personas() { return join(getBrainjarDir(), 'personas') },
42
42
  get rules() { return join(getBrainjarDir(), 'rules') },
43
43
  get brains() { return join(getBrainjarDir(), 'brains') },
44
- get identities() { return join(getBrainjarDir(), 'identities') },
45
- get session() { return join(getBrainjarDir(), '.session') },
46
44
  get state() { return join(getBrainjarDir(), 'state.yaml') },
47
45
  get localState() { return join(getLocalDir(), 'state.yaml') },
48
46
  }
package/src/seeds.ts CHANGED
@@ -26,9 +26,7 @@ function obsidianAppConfigWithExclusions() {
26
26
  useMarkdownLinks: false,
27
27
  alwaysUpdateLinks: true,
28
28
  userIgnoreFilters: [
29
- 'identities/',
30
29
  'state.yaml',
31
- '.session',
32
30
  '.gitignore',
33
31
  ],
34
32
  }, null, 2)
package/src/state.ts CHANGED
@@ -73,13 +73,12 @@ export async function listAvailableRules(): Promise<string[]> {
73
73
 
74
74
  export interface State {
75
75
  backend: string | null
76
- identity: string | null
77
76
  soul: string | null
78
77
  persona: string | null
79
78
  rules: string[]
80
79
  }
81
80
 
82
- const DEFAULT_STATE: State = { backend: null, identity: null, soul: null, persona: null, rules: [] }
81
+ const DEFAULT_STATE: State = { backend: null, soul: null, persona: null, rules: [] }
83
82
 
84
83
  export async function requireBrainjarDir(): Promise<void> {
85
84
  try {
@@ -111,20 +110,6 @@ export function stripFrontmatter(content: string): string {
111
110
  return content.replace(/\r\n/g, '\n').replace(/^---\n[\s\S]*?\n---\n*/, '').trim()
112
111
  }
113
112
 
114
- export function parseIdentity(content: string) {
115
- const parsed = parseYaml(content)
116
- return {
117
- name: parsed?.name as string | undefined,
118
- email: parsed?.email as string | undefined,
119
- engine: parsed?.engine as string | undefined,
120
- }
121
- }
122
-
123
- export async function loadIdentity(slug: string) {
124
- const content = await readFile(join(paths.identities, `${slug}.yaml`), 'utf-8')
125
- return { slug, content, ...parseIdentity(content) }
126
- }
127
-
128
113
  /** Return a valid slug or null. Prevents path traversal from state.yaml. */
129
114
  function safeName(value: unknown): string | null {
130
115
  if (typeof value !== 'string' || !value) return null
@@ -151,7 +136,6 @@ export async function readState(): Promise<State> {
151
136
 
152
137
  return {
153
138
  backend: ((parsed as any).backend === 'claude' || (parsed as any).backend === 'codex') ? (parsed as any).backend : null,
154
- identity: safeName((parsed as any).identity),
155
139
  soul: safeName((parsed as any).soul),
156
140
  persona: safeName((parsed as any).persona),
157
141
  rules: Array.isArray((parsed as any).rules)
@@ -209,7 +193,6 @@ export async function withStateLock<T>(fn: () => Promise<T>): Promise<T> {
209
193
  export async function writeState(state: State): Promise<void> {
210
194
  const doc = {
211
195
  backend: state.backend ?? null,
212
- identity: state.identity ?? null,
213
196
  soul: state.soul ?? null,
214
197
  persona: state.persona ?? null,
215
198
  rules: state.rules,
@@ -223,7 +206,6 @@ export async function writeState(state: State): Promise<void> {
223
206
 
224
207
  /** Local state only stores overrides. undefined = cascade, null = explicit unset. */
225
208
  export interface LocalState {
226
- identity?: string | null
227
209
  soul?: string | null
228
210
  persona?: string | null
229
211
  rules?: {
@@ -240,7 +222,6 @@ export type Scope = 'global' | 'local' | '+local' | '-local' | 'env' | '+env' |
240
222
  /** Effective state after merging global + local + env, with scope annotations. */
241
223
  export interface EffectiveState {
242
224
  backend: string | null
243
- identity: { value: string | null; scope: Scope }
244
225
  soul: { value: string | null; scope: Scope }
245
226
  persona: { value: string | null; scope: Scope }
246
227
  rules: { value: string; scope: Scope }[]
@@ -268,7 +249,6 @@ export async function readLocalState(): Promise<LocalState> {
268
249
  const p = parsed as Record<string, unknown>
269
250
 
270
251
  // For each layer: if key is present, include it (even if null)
271
- if ('identity' in p) result.identity = p.identity === null ? null : safeName(p.identity)
272
252
  if ('soul' in p) result.soul = p.soul === null ? null : safeName(p.soul)
273
253
  if ('persona' in p) result.persona = p.persona === null ? null : safeName(p.persona)
274
254
 
@@ -292,7 +272,6 @@ export async function writeLocalState(local: LocalState): Promise<void> {
292
272
 
293
273
  // Build a clean doc — only include keys that are present in local
294
274
  const doc: Record<string, unknown> = {}
295
- if ('identity' in local) doc.identity = local.identity ?? null
296
275
  if ('soul' in local) doc.soul = local.soul ?? null
297
276
  if ('persona' in local) doc.persona = local.persona ?? null
298
277
  if (local.rules) {
@@ -313,9 +292,6 @@ export function readEnvState(extraEnv?: Record<string, string>): EnvState {
313
292
  const env = extraEnv ? { ...process.env, ...extraEnv } : process.env
314
293
  const result: EnvState = {}
315
294
 
316
- if (env.BRAINJAR_IDENTITY !== undefined) {
317
- result.identity = env.BRAINJAR_IDENTITY === '' ? null : safeName(env.BRAINJAR_IDENTITY)
318
- }
319
295
  if (env.BRAINJAR_SOUL !== undefined) {
320
296
  result.soul = env.BRAINJAR_SOUL === '' ? null : safeName(env.BRAINJAR_SOUL)
321
297
  }
@@ -353,10 +329,6 @@ function applyOverrides(
353
329
  const plusScope = `+${scope}` as Scope
354
330
  const minusScope = `-${scope}` as Scope
355
331
 
356
- const identity = 'identity' in overrides
357
- ? { value: overrides.identity ?? null, scope: scope as Scope }
358
- : base.identity
359
-
360
332
  const soul = 'soul' in overrides
361
333
  ? { value: overrides.soul ?? null, scope: scope as Scope }
362
334
  : base.soul
@@ -395,7 +367,7 @@ function applyOverrides(
395
367
  }
396
368
  }
397
369
 
398
- return { backend: base.backend, identity, soul, persona, rules }
370
+ return { backend: base.backend, soul, persona, rules }
399
371
  }
400
372
 
401
373
  /** Pure merge: global → local → env, each scope overrides the previous. */
@@ -403,7 +375,6 @@ export function mergeState(global: State, local: LocalState, env?: EnvState): Ef
403
375
  // Start with global as the base effective state
404
376
  const base: EffectiveState = {
405
377
  backend: global.backend,
406
- identity: { value: global.identity, scope: 'global' },
407
378
  soul: { value: global.soul, scope: 'global' },
408
379
  persona: { value: global.persona, scope: 'global' },
409
380
  rules: global.rules.map(r => ({ value: r, scope: 'global' as Scope })),
package/src/sync.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile, copyFile, mkdir, access } from 'node:fs/promises'
1
+ import { readFile, writeFile, copyFile, mkdir } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import { type Backend, getBackendConfig, paths } from './paths.js'
4
4
  import { type State, readState, readLocalState, readEnvState, mergeState, requireBrainjarDir, stripFrontmatter, resolveRuleContent } from './state.js'
@@ -40,17 +40,6 @@ async function inlineRules(rules: string[], sections: string[], warnings: string
40
40
  }
41
41
  }
42
42
 
43
- async function inlineIdentity(name: string, sections: string[]) {
44
- try {
45
- await access(join(paths.identities, `${name}.yaml`))
46
- sections.push('')
47
- sections.push('## Identity')
48
- sections.push('')
49
- sections.push(`See ~/.brainjar/identities/${name}.yaml for active identity.`)
50
- sections.push('Manage with `brainjar identity [list|use|show]`.')
51
- } catch {}
52
- }
53
-
54
43
  /** Extract content before, inside, and after brainjar markers. */
55
44
  function parseMarkers(content: string): { before: string; after: string } | null {
56
45
  const startIdx = content.indexOf(MARKER_START)
@@ -115,21 +104,16 @@ export async function sync(options?: Backend | SyncOptions) {
115
104
  // But only write rules section if local state has rules overrides
116
105
  await inlineRules(activeRules, sections, warnings)
117
106
  }
118
- if ('identity' in localState && effective.identity.value) {
119
- await inlineIdentity(effective.identity.value, sections)
120
- }
121
107
  } else {
122
108
  // Global mode: apply env overrides on top of global state, write all layers
123
109
  const effective = mergeState(globalState, {}, envState)
124
110
  const effectiveSoul = effective.soul.value
125
111
  const effectivePersona = effective.persona.value
126
112
  const effectiveRules = effective.rules.filter(r => !r.scope.startsWith('-')).map(r => r.value)
127
- const effectiveIdentity = effective.identity.value
128
113
 
129
114
  if (effectiveSoul) await inlineSoul(effectiveSoul, sections)
130
115
  if (effectivePersona) await inlinePersona(effectivePersona, sections)
131
116
  await inlineRules(effectiveRules, sections, warnings)
132
- if (effectiveIdentity) await inlineIdentity(effectiveIdentity, sections)
133
117
 
134
118
  // Local Overrides note (only for global config)
135
119
  sections.push('')
@@ -1,276 +0,0 @@
1
- import { Cli, z, Errors } from 'incur'
2
- import { stringify as stringifyYaml } from 'yaml'
3
-
4
- const { IncurError } = Errors
5
- import { readdir, readFile, writeFile, mkdir, rm } from 'node:fs/promises'
6
- import { join, basename } from 'node:path'
7
- import { paths } from '../paths.js'
8
- import { readState, writeState, withStateLock, readLocalState, writeLocalState, withLocalStateLock, readEnvState, mergeState, loadIdentity, parseIdentity, requireBrainjarDir, normalizeSlug } from '../state.js'
9
- import { getEngine } from '../engines/index.js'
10
- import { sync } from '../sync.js'
11
-
12
- function redactSession(status: Record<string, unknown>) {
13
- const { session: _, ...safe } = status as any
14
- return safe
15
- }
16
-
17
- async function requireActiveIdentity() {
18
- await requireBrainjarDir()
19
- const state = await readState()
20
- if (!state.identity) {
21
- throw new IncurError({
22
- code: 'NO_ACTIVE_IDENTITY',
23
- message: 'No active identity.',
24
- hint: 'Run `brainjar identity use <slug>` to activate one.',
25
- })
26
- }
27
- return loadIdentity(state.identity)
28
- }
29
-
30
- function requireEngine(engineName: string | undefined) {
31
- if (!engineName) {
32
- throw new IncurError({
33
- code: 'NO_ENGINE',
34
- message: 'Active identity has no engine configured.',
35
- })
36
- }
37
- const engine = getEngine(engineName)
38
- if (!engine) {
39
- throw new IncurError({
40
- code: 'UNKNOWN_ENGINE',
41
- message: `Unknown engine: ${engineName}`,
42
- hint: 'Supported engines: bitwarden',
43
- })
44
- }
45
- return engine
46
- }
47
-
48
- export const identity = Cli.create('identity', {
49
- description: 'Manage digital identity — one active at a time',
50
- })
51
- .command('create', {
52
- description: 'Create a new identity',
53
- args: z.object({
54
- slug: z.string().describe('Identity slug (e.g. personal, work)'),
55
- }),
56
- options: z.object({
57
- name: z.string().describe('Full display name'),
58
- email: z.string().describe('Email address'),
59
- engine: z.literal('bitwarden').default('bitwarden').describe('Credential engine'),
60
- }),
61
- async run(c) {
62
- await requireBrainjarDir()
63
- const slug = normalizeSlug(c.args.slug, 'identity slug')
64
- await mkdir(paths.identities, { recursive: true })
65
-
66
- const content = stringifyYaml({ name: c.options.name, email: c.options.email, engine: c.options.engine })
67
-
68
- const filePath = join(paths.identities, `${slug}.yaml`)
69
- await writeFile(filePath, content)
70
-
71
- return {
72
- created: filePath,
73
- identity: { slug, name: c.options.name, email: c.options.email, engine: c.options.engine },
74
- next: `Run \`brainjar identity use ${slug}\` to activate.`,
75
- }
76
- },
77
- })
78
- .command('list', {
79
- description: 'List available identities',
80
- async run() {
81
- const entries = await readdir(paths.identities).catch(() => [])
82
- const identities = []
83
-
84
- for (const file of entries.filter(f => f.endsWith('.yaml'))) {
85
- const slug = basename(file, '.yaml')
86
- const content = await readFile(join(paths.identities, file), 'utf-8')
87
- identities.push({ slug, ...parseIdentity(content) })
88
- }
89
-
90
- return { identities }
91
- },
92
- })
93
- .command('show', {
94
- description: 'Show the active identity',
95
- options: z.object({
96
- local: z.boolean().default(false).describe('Show local identity override (if any)'),
97
- short: z.boolean().default(false).describe('Print only the active identity slug'),
98
- }),
99
- async run(c) {
100
- if (c.options.short) {
101
- const global = await readState()
102
- const local = await readLocalState()
103
- const env = readEnvState()
104
- const effective = mergeState(global, local, env)
105
- return effective.identity.value ?? 'none'
106
- }
107
-
108
- if (c.options.local) {
109
- const local = await readLocalState()
110
- if (!('identity' in local)) return { active: false, scope: 'local', note: 'No local identity override (cascades from global)' }
111
- if (local.identity === null) return { active: false, scope: 'local', slug: null, note: 'Explicitly unset at local scope' }
112
- try {
113
- const content = await readFile(join(paths.identities, `${local.identity}.yaml`), 'utf-8')
114
- return { active: true, scope: 'local', slug: local.identity, ...parseIdentity(content) }
115
- } catch {
116
- return { active: false, scope: 'local', slug: local.identity, error: 'File not found' }
117
- }
118
- }
119
-
120
- const global = await readState()
121
- const local = await readLocalState()
122
- const env = readEnvState()
123
- const effective = mergeState(global, local, env)
124
- if (!effective.identity.value) return { active: false }
125
- try {
126
- const content = await readFile(join(paths.identities, `${effective.identity.value}.yaml`), 'utf-8')
127
- return { active: true, slug: effective.identity.value, scope: effective.identity.scope, ...parseIdentity(content) }
128
- } catch {
129
- return { active: false, slug: effective.identity.value, error: 'File not found' }
130
- }
131
- },
132
- })
133
- .command('use', {
134
- description: 'Activate an identity',
135
- args: z.object({
136
- slug: z.string().describe('Identity slug to activate'),
137
- }),
138
- options: z.object({
139
- local: z.boolean().default(false).describe('Write to local .claude/CLAUDE.md instead of global'),
140
- }),
141
- async run(c) {
142
- await requireBrainjarDir()
143
- const slug = normalizeSlug(c.args.slug, 'identity slug')
144
- const source = join(paths.identities, `${slug}.yaml`)
145
- try {
146
- await readFile(source, 'utf-8')
147
- } catch {
148
- throw new IncurError({
149
- code: 'IDENTITY_NOT_FOUND',
150
- message: `Identity "${slug}" not found.`,
151
- hint: 'Run `brainjar identity list` to see available identities.',
152
- })
153
- }
154
-
155
- if (c.options.local) {
156
- await withLocalStateLock(async () => {
157
- const local = await readLocalState()
158
- local.identity = slug
159
- await writeLocalState(local)
160
- await sync({ local: true })
161
- })
162
- } else {
163
- await withStateLock(async () => {
164
- const state = await readState()
165
- state.identity = slug
166
- await writeState(state)
167
- await sync()
168
- })
169
- }
170
-
171
- return { activated: slug, local: c.options.local }
172
- },
173
- })
174
- .command('drop', {
175
- description: 'Deactivate the current identity',
176
- options: z.object({
177
- local: z.boolean().default(false).describe('Remove local identity override or deactivate global identity'),
178
- }),
179
- async run(c) {
180
- await requireBrainjarDir()
181
- if (c.options.local) {
182
- await withLocalStateLock(async () => {
183
- const local = await readLocalState()
184
- delete local.identity
185
- await writeLocalState(local)
186
- await sync({ local: true })
187
- })
188
- } else {
189
- await withStateLock(async () => {
190
- const state = await readState()
191
- if (!state.identity) {
192
- throw new IncurError({
193
- code: 'NO_ACTIVE_IDENTITY',
194
- message: 'No active identity to deactivate.',
195
- })
196
- }
197
- state.identity = null
198
- await writeState(state)
199
- await sync()
200
- })
201
- }
202
- return { deactivated: true, local: c.options.local }
203
- },
204
- })
205
- .command('unlock', {
206
- description: 'Store the credential engine session token',
207
- args: z.object({
208
- session: z.string().optional().describe('Session token (reads from stdin if omitted)'),
209
- }),
210
- async run(c) {
211
- let session = c.args.session
212
- if (!session) {
213
- if (process.stdin.isTTY) {
214
- throw new IncurError({
215
- code: 'NO_SESSION',
216
- message: 'No session token provided.',
217
- hint: 'Pipe it in: bw unlock --raw | brainjar identity unlock',
218
- })
219
- }
220
- let data = ''
221
- for await (const chunk of process.stdin) {
222
- data += typeof chunk === 'string' ? chunk : chunk.toString('utf-8')
223
- }
224
- session = data.trim()
225
- }
226
- if (!session) {
227
- throw new IncurError({
228
- code: 'EMPTY_SESSION',
229
- message: 'Session token is empty.',
230
- })
231
- }
232
- await writeFile(paths.session, session, { mode: 0o600 })
233
- return { unlocked: true, stored: paths.session }
234
- },
235
- })
236
- .command('get', {
237
- description: 'Retrieve a credential from the active identity engine',
238
- args: z.object({
239
- item: z.string().describe('Item name or ID to retrieve from the vault'),
240
- }),
241
- async run(c) {
242
- const { engine: engineName } = await requireActiveIdentity()
243
- const engine = requireEngine(engineName)
244
-
245
- const status = await engine.status()
246
- if (status.state !== 'unlocked') {
247
- throw new IncurError({
248
- code: 'ENGINE_LOCKED',
249
- message: 'Credential engine is not unlocked.',
250
- hint: 'operator_action' in status ? status.operator_action : undefined,
251
- retryable: true,
252
- })
253
- }
254
-
255
- return engine.get(c.args.item, status.session)
256
- },
257
- })
258
- .command('status', {
259
- description: 'Check if the credential engine session is active',
260
- async run() {
261
- const { name, email, engine: engineName } = await requireActiveIdentity()
262
- const engine = requireEngine(engineName)
263
- const engineStatus = await engine.status()
264
- return { identity: { name, email, engine: engineName }, ...redactSession(engineStatus) }
265
- },
266
- })
267
- .command('lock', {
268
- description: 'Lock the credential engine session',
269
- async run() {
270
- const { engine: engineName } = await requireActiveIdentity()
271
- const engine = requireEngine(engineName)
272
- await engine.lock()
273
- await rm(paths.session, { force: true })
274
- return { locked: true }
275
- },
276
- })
@@ -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
- }