claude-brain 0.3.1 → 0.3.3

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
@@ -22,55 +22,82 @@ A locally-running development assistant that bridges Obsidian knowledge vaults w
22
22
  - Runs completely locally with zero cloud dependencies
23
23
  - Compiles to single portable executable
24
24
 
25
- ## Prerequisites
26
-
27
- - [Bun](https://bun.sh) >= 1.0.0
28
- - An Obsidian vault (or any markdown folder)
29
-
30
- ## Installation
25
+ ## Quick Start
31
26
 
32
27
  ```bash
33
- # Clone the repository
34
- git clone <repo-url>
35
- cd claude-brain
28
+ # Install globally (requires Bun)
29
+ bun install -g claude-brain
36
30
 
37
- # Install dependencies
38
- bun install
31
+ # Interactive setup (vault path, log level, installs ~/CLAUDE.md)
32
+ claude-brain setup
39
33
 
40
- # Run setup to create directories and validate config
41
- bun run setup
34
+ # Register with Claude Code
35
+ claude mcp add claude-brain -- bunx claude-brain@latest
42
36
  ```
43
37
 
44
- ## Configuration
38
+ That's it. Every Claude Code session now has 25 brain tools available.
45
39
 
46
- Copy `.env.example` to `.env` and update with your settings:
40
+ ## Zero-Install Alternative
41
+
42
+ Skip the global install — just register with Claude Code directly:
47
43
 
48
44
  ```bash
49
- cp .env.example .env
45
+ claude mcp add claude-brain -- bunx claude-brain@latest
50
46
  ```
51
47
 
52
- Required configuration:
53
- - `VAULT_PATH`: Absolute path to your Obsidian vault
48
+ On first run, `~/.claude-brain/` is auto-created with default config.
49
+
50
+ ## Prerequisites
51
+
52
+ - [Bun](https://bun.sh) >= 1.0.0
53
+ - An Obsidian vault (or any markdown folder)
54
+
55
+ ## CLI Commands
56
+
57
+ | Command | Description |
58
+ |---------|-------------|
59
+ | `claude-brain` | Start MCP server (default) |
60
+ | `claude-brain setup` | Interactive setup wizard (run once per machine) |
61
+ | `claude-brain install` | Register as MCP server in Claude Code |
62
+ | `claude-brain health` | Run health checks |
63
+ | `claude-brain diagnose` | Run diagnostics |
64
+ | `claude-brain version` | Show version |
65
+ | `claude-brain help` | Show help |
66
+
67
+ ## Configuration
68
+
69
+ Configuration lives in `~/.claude-brain/.env`. Created automatically by `claude-brain setup`, or on first run with defaults.
70
+
71
+ | Variable | Description | Default |
72
+ |----------|-------------|---------|
73
+ | `VAULT_PATH` | Path to your Obsidian vault | `~/.claude-brain/vault` |
74
+ | `LOG_LEVEL` | Log level (debug/info/warn/error) | `info` |
75
+ | `NODE_ENV` | Environment | `production` |
76
+ | `CLAUDE_BRAIN_HOME` | Override home directory | `~/.claude-brain/` |
54
77
 
55
78
  ## Development
56
79
 
57
80
  ```bash
58
- # Start development server with hot reload
81
+ # Clone and install dependencies
82
+ git clone <repo-url>
83
+ cd claude-brain
84
+ bun install
85
+
86
+ # Start dev server (uses local ./data, ./logs)
59
87
  bun run dev
60
88
 
61
89
  # Run tests
62
- bun run test
90
+ bun test
63
91
 
64
92
  # Run tests in watch mode
65
- bun run test:watch
93
+ bun test --watch
66
94
  ```
67
95
 
96
+ The `dev` script sets `CLAUDE_BRAIN_HOME=.` so all data stays in the project directory.
97
+
68
98
  ## Building
69
99
 
70
100
  ```bash
71
- # Build for production (bundled JS)
72
- bun run build
73
-
74
101
  # Build standalone executable for current platform
75
102
  bun run build:binary
76
103
 
@@ -83,7 +110,15 @@ bun run build:all
83
110
  ```
84
111
  claude-brain/
85
112
  ├── src/
86
- │ ├── index.ts # Main entry point + orchestrator init
113
+ │ ├── index.ts # Entry point (thin wrapper)
114
+ │ ├── cli/
115
+ │ │ ├── bin.ts # CLI entry point (claude-brain command)
116
+ │ │ ├── auto-setup.ts # First-run home directory initialization
117
+ │ │ └── commands/ # serve, install-mcp
118
+ │ ├── config/
119
+ │ │ ├── home.ts # ~/.claude-brain/ path resolution
120
+ │ │ ├── loader.ts # Config loading (defaults → file → env)
121
+ │ │ └── schema.ts # Zod config schemas
87
122
  │ ├── server/ # MCP server code
88
123
  │ │ └── handlers/tools/ # 25 tool handlers with Zod validation
89
124
  │ ├── vault/ # Obsidian integration
@@ -94,27 +129,24 @@ claude-brain/
94
129
  │ ├── knowledge/ # Knowledge graph & entity extraction
95
130
  │ │ └── graph/ # In-memory graph with search & auto-population
96
131
  │ ├── retrieval/ # Hybrid search (BM25 + semantic + reranking)
97
- │ │ ├── bm25/ # BM25 keyword search engine
98
- │ │ ├── fusion/ # RRF/linear/max fusion strategies
99
- │ │ ├── reranker/ # Cross-encoder neural reranking
100
- │ │ ├── query/ # Intent classification & query expansion
101
- │ │ └── feedback/ # Feedback collection & adaptive learning
102
132
  │ ├── temporal/ # Timeline construction & trend detection
103
133
  │ ├── reasoning/ # Multi-hop chain retrieval & what-if analysis
104
134
  │ ├── prediction/ # Decision prediction & recommendations
105
135
  │ ├── cross-project/ # Cross-project pattern discovery & transfer
106
136
  │ ├── optimization/ # Semantic caching & precomputation
107
137
  │ ├── orchestrator/ # Event-driven coordination system
138
+ │ ├── setup/ # Interactive setup wizard
108
139
  │ ├── context/ # Context assembly
109
140
  │ ├── tools/ # MCP tool definitions & registry
110
- ├── config/ # Configuration management
111
- ├── utils/ # Shared utilities
112
- ├── types/ # TypeScript definitions
113
- │ └── scripts/ # CLI scripts
114
- ├── data/ # Local data storage
115
- ├── logs/ # Log files
141
+ └── utils/ # Shared utilities
142
+ ├── assets/
143
+ └── CLAUDE.md # Protocol file (shipped with npm package)
116
144
  ├── tests/ # 750+ tests
117
- └── dist/ # Build output
145
+ └── ~/.claude-brain/ # User data (created at runtime)
146
+ ├── .env # Configuration
147
+ ├── data/ # SQLite + ChromaDB
148
+ ├── logs/ # Log files
149
+ └── vault/ # Default vault location
118
150
  ```
119
151
 
120
152
  ## Architecture Highlights
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.3.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -57,6 +57,7 @@
57
57
  "@chroma-core/default-embed": "0.1.9",
58
58
  "@modelcontextprotocol/sdk": "^1.25.2",
59
59
  "@xenova/transformers": "2.17.2",
60
+ "chalk": "5.6.2",
60
61
  "chromadb": "3.2.2",
61
62
  "chromadb-default-embed": "2.14.0",
62
63
  "chrono-node": "2.9.0",
@@ -66,6 +67,7 @@
66
67
  "hono": "4.11.5",
67
68
  "lru-cache": "11.2.5",
68
69
  "minisearch": "^6.3.0",
70
+ "ora": "9.2.0",
69
71
  "pino": "^10.1.1",
70
72
  "pino-pretty": "^13.1.3",
71
73
  "prompts": "2.4.2",
package/src/cli/bin.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { readFileSync } from 'node:fs'
4
4
  import { resolve, dirname } from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
+ import { renderLogo, theme, dimText, box, errorText } from '@/cli/ui/index.js'
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url)
8
9
  const __dirname = dirname(__filename)
@@ -19,33 +20,50 @@ function getVersion(): string {
19
20
 
20
21
  function printHelp() {
21
22
  const version = getVersion()
22
- console.log(`
23
- claude-brain v${version} - Local Development Assistant with Memory
24
-
25
- Usage: claude-brain [command]
26
-
27
- Commands:
28
- serve Start the MCP server (default)
29
- setup Run interactive setup wizard
30
- install Register as MCP server in Claude Code
31
- health Run health checks
32
- diagnose Run diagnostics
33
- version Show version
34
- help Show this help message
35
-
36
- Options:
37
- -v Show version
38
- -h Show help
39
-
40
- Examples:
41
- claude-brain Start MCP server
42
- claude-brain setup Configure Claude Brain
43
- claude-brain install Register with Claude Code
44
- claude-brain health Check system health
45
-
46
- Environment:
47
- CLAUDE_BRAIN_HOME Override home directory (default: ~/.claude-brain/)
48
- `)
23
+
24
+ console.log()
25
+ console.log(renderLogo())
26
+ console.log()
27
+
28
+ const commands = [
29
+ ['serve', 'Start the MCP server (default)'],
30
+ ['setup', 'Run interactive setup wizard'],
31
+ ['install', 'Register as MCP server in Claude Code'],
32
+ ['uninstall', 'Remove MCP server from Claude Code'],
33
+ ['health', 'Run health checks'],
34
+ ['diagnose', 'Run diagnostics'],
35
+ ['version', 'Show version'],
36
+ ['help', 'Show this help message'],
37
+ ]
38
+
39
+ const cmdLines = commands
40
+ .map(([cmd, desc]) => ` ${theme.primary(cmd!.padEnd(12))} ${dimText(desc!)}`)
41
+ .join('\n')
42
+
43
+ const content = [
44
+ `${dimText('v' + version)} ${dimText('Local Development Assistant with Memory')}`,
45
+ '',
46
+ `${theme.bold('Usage:')} ${dimText('claude-brain [command]')}`,
47
+ '',
48
+ theme.bold('Commands:'),
49
+ cmdLines,
50
+ '',
51
+ theme.bold('Options:'),
52
+ ` ${theme.primary('-v'.padEnd(12))} ${dimText('Show version')}`,
53
+ ` ${theme.primary('-h'.padEnd(12))} ${dimText('Show help')}`,
54
+ '',
55
+ theme.bold('Examples:'),
56
+ ` ${dimText('claude-brain')} ${dimText('Start MCP server')}`,
57
+ ` ${dimText('claude-brain setup')} ${dimText('Configure Claude Brain')}`,
58
+ ` ${dimText('claude-brain install')} ${dimText('Register with Claude Code')}`,
59
+ ` ${dimText('claude-brain health')} ${dimText('Check system health')}`,
60
+ '',
61
+ theme.bold('Environment:'),
62
+ ` ${theme.primary('CLAUDE_BRAIN_HOME')} ${dimText('Override home directory (default: ~/.claude-brain/')}`,
63
+ ].join('\n')
64
+
65
+ console.log(content)
66
+ console.log()
49
67
  }
50
68
 
51
69
  async function main() {
@@ -70,6 +88,12 @@ async function main() {
70
88
  break
71
89
  }
72
90
 
91
+ case 'uninstall': {
92
+ const { runUninstall } = await import('./commands/uninstall-mcp')
93
+ await runUninstall()
94
+ break
95
+ }
96
+
73
97
  case 'health': {
74
98
  const { runHealthCheck } = await import('@/health')
75
99
  await runHealthCheck()
@@ -97,7 +121,8 @@ async function main() {
97
121
  }
98
122
 
99
123
  default: {
100
- console.error(`Unknown command: ${command}\n`)
124
+ console.log()
125
+ console.log(errorText(`Unknown command: ${command}`))
101
126
  printHelp()
102
127
  process.exit(1)
103
128
  }
@@ -105,6 +130,7 @@ async function main() {
105
130
  }
106
131
 
107
132
  main().catch((error) => {
108
- console.error('Fatal error:', error)
133
+ console.log()
134
+ console.log(box(errorText(`Fatal error: ${error instanceof Error ? error.message : String(error)}`), 'Error'))
109
135
  process.exit(1)
110
136
  })
@@ -1,4 +1,8 @@
1
1
  import { execSync } from 'node:child_process'
2
+ import {
3
+ renderLogo, theme, heading, successText, warningText, errorText, dimText,
4
+ box, withSpinner,
5
+ } from '@/cli/ui/index.js'
2
6
 
3
7
  function isGloballyInstalled(): boolean {
4
8
  const cmd = process.platform === 'win32' ? 'where claude-brain' : 'which claude-brain'
@@ -11,41 +15,59 @@ function isGloballyInstalled(): boolean {
11
15
  }
12
16
 
13
17
  export async function runInstall() {
14
- console.log('\nClaude Brain - MCP Installation\n')
18
+ console.log()
19
+ console.log(renderLogo())
20
+ console.log()
21
+ console.log(heading('MCP Installation'))
22
+ console.log()
15
23
 
16
- if (isGloballyInstalled()) {
17
- console.log('claude-brain is globally installed. Registering with Claude Code...\n')
24
+ const installed = isGloballyInstalled()
25
+
26
+ if (installed) {
27
+ console.log(successText('claude-brain is globally installed'))
28
+ console.log()
18
29
 
19
30
  try {
20
- execSync('claude mcp add claude-brain -- claude-brain serve', {
21
- encoding: 'utf-8',
22
- stdio: 'inherit'
31
+ await withSpinner('Registering with Claude Code', async () => {
32
+ execSync('claude mcp add claude-brain -- claude-brain serve', {
33
+ encoding: 'utf-8',
34
+ stdio: ['pipe', 'pipe', 'pipe']
35
+ })
23
36
  })
24
- console.log('\nClaude Brain registered as MCP server in Claude Code.')
25
- } catch (error) {
26
- console.error('\nFailed to register automatically. Run manually:')
27
- console.log('\n claude mcp add claude-brain -- claude-brain serve\n')
37
+ console.log()
38
+ console.log(box(successText('Claude Brain registered as MCP server in Claude Code.'), 'Success'))
39
+ } catch {
40
+ console.log()
41
+ console.log(box([
42
+ errorText('Failed to register automatically.'),
43
+ '',
44
+ dimText('Run manually:'),
45
+ ` ${theme.bold('claude mcp add claude-brain -- claude-brain serve')}`,
46
+ ].join('\n'), 'Error'))
28
47
  }
29
48
  } else {
30
- console.log('claude-brain is not globally installed.')
31
- console.log('You can either install globally or use bunx.\n')
32
-
33
- console.log('Option 1: Install globally, then register:')
34
- console.log(' bun install -g claude-brain')
35
- console.log(' claude mcp add claude-brain -- claude-brain serve\n')
36
-
37
- console.log('Option 2: Use bunx (zero-install):')
38
- console.log(' claude mcp add claude-brain -- bunx claude-brain@latest\n')
49
+ console.log(warningText('claude-brain is not globally installed'))
50
+ console.log()
39
51
 
40
- console.log('Option 3: Add to Claude Code config manually:')
41
- console.log(` {
42
- "mcpServers": {
43
- "claude-brain": {
44
- "command": "bunx",
45
- "args": ["claude-brain@latest"]
46
- }
47
- }
48
- }
49
- `)
52
+ console.log(box([
53
+ `${theme.primary('Option 1:')} Install globally, then register`,
54
+ ` ${theme.bold('bun install -g claude-brain')}`,
55
+ ` ${theme.bold('claude mcp add claude-brain -- claude-brain serve')}`,
56
+ '',
57
+ `${theme.primary('Option 2:')} Use bunx ${dimText('(zero-install)')}`,
58
+ ` ${theme.bold('claude mcp add claude-brain -- bunx claude-brain@latest')}`,
59
+ '',
60
+ `${theme.primary('Option 3:')} Add to Claude Code config manually`,
61
+ dimText(' Add to your Claude Code MCP settings:'),
62
+ ` ${theme.dim('{')}`,
63
+ ` ${theme.dim('"mcpServers": {')}`,
64
+ ` ${theme.dim('"claude-brain": {')}`,
65
+ ` ${theme.dim('"command": "bunx",')}`,
66
+ ` ${theme.dim('"args": ["claude-brain@latest"]')}`,
67
+ ` ${theme.dim('}')}`,
68
+ ` ${theme.dim('}')}`,
69
+ ` ${theme.dim('}')}`,
70
+ ].join('\n'), 'Install Options'))
50
71
  }
72
+ console.log()
51
73
  }
@@ -0,0 +1,41 @@
1
+ import { execSync } from 'node:child_process'
2
+ import {
3
+ renderLogo, theme, heading, successText, errorText, dimText,
4
+ box, withSpinner,
5
+ } from '@/cli/ui/index.js'
6
+
7
+ export async function runUninstall() {
8
+ console.log()
9
+ console.log(renderLogo())
10
+ console.log()
11
+ console.log(heading('MCP Uninstall'))
12
+ console.log()
13
+
14
+ try {
15
+ await withSpinner('Removing Claude Brain from Claude Code', async () => {
16
+ try {
17
+ execSync('claude mcp remove claude-brain', {
18
+ encoding: 'utf-8',
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ })
21
+ } catch {
22
+ // Falls back to scoped removal when registered in multiple scopes
23
+ execSync('claude mcp remove claude-brain -s local', {
24
+ encoding: 'utf-8',
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ })
27
+ }
28
+ })
29
+ console.log()
30
+ console.log(box(successText('Claude Brain MCP server removed from Claude Code.'), 'Success'))
31
+ } catch {
32
+ console.log()
33
+ console.log(box([
34
+ errorText('Failed to remove automatically.'),
35
+ '',
36
+ dimText('Run manually:'),
37
+ ` ${theme.bold('claude mcp remove claude-brain')}`,
38
+ ].join('\n'), 'Error'))
39
+ }
40
+ console.log()
41
+ }
@@ -0,0 +1,80 @@
1
+ import ora from 'ora'
2
+ import { theme } from './theme.js'
3
+
4
+ const isTTY = process.stdout.isTTY === true
5
+
6
+ // Async delay utility
7
+ export function delay(ms: number): Promise<void> {
8
+ return new Promise(resolve => setTimeout(resolve, ms))
9
+ }
10
+
11
+ // Character-by-character text output with configurable speed
12
+ export async function typewrite(text: string, speed = 30): Promise<void> {
13
+ if (!isTTY) {
14
+ process.stdout.write(text + '\n')
15
+ return
16
+ }
17
+ for (const char of text) {
18
+ process.stdout.write(char)
19
+ await delay(speed)
20
+ }
21
+ process.stdout.write('\n')
22
+ }
23
+
24
+ // Wraps async operations with ora spinner, auto succeed/fail
25
+ export async function withSpinner<T>(label: string, fn: () => Promise<T>): Promise<T> {
26
+ if (!isTTY) {
27
+ process.stdout.write(`${label}...`)
28
+ try {
29
+ const result = await fn()
30
+ process.stdout.write(' done\n')
31
+ return result
32
+ } catch (error) {
33
+ process.stdout.write(' failed\n')
34
+ throw error
35
+ }
36
+ }
37
+
38
+ const spinner = ora({
39
+ text: label,
40
+ color: 'magenta',
41
+ }).start()
42
+
43
+ try {
44
+ const result = await fn()
45
+ spinner.succeed(theme.success(label))
46
+ return result
47
+ } catch (error) {
48
+ spinner.fail(theme.error(label))
49
+ throw error
50
+ }
51
+ }
52
+
53
+ // Pause between sections for visual breathing room
54
+ export async function transition(ms = 300): Promise<void> {
55
+ if (!isTTY) return
56
+ await delay(ms)
57
+ }
58
+
59
+ // Progress bar that fills over time
60
+ export async function animateProgress(label: string, durationMs = 1000, width = 30): Promise<void> {
61
+ if (!isTTY) {
62
+ console.log(`${label}... done`)
63
+ return
64
+ }
65
+
66
+ const steps = 20
67
+ const stepDelay = durationMs / steps
68
+
69
+ for (let i = 0; i <= steps; i++) {
70
+ const percent = Math.round((i / steps) * 100)
71
+ const filled = Math.round((percent / 100) * width)
72
+ const empty = width - filled
73
+ const bar = theme.primary('='.repeat(Math.max(0, filled - 1)) + (filled > 0 ? '>' : '')) +
74
+ ' '.repeat(empty)
75
+ const pct = theme.dim(`${percent}%`)
76
+ process.stdout.write(`\r ${label} [${bar}] ${pct}`)
77
+ await delay(stepDelay)
78
+ }
79
+ process.stdout.write('\n')
80
+ }
@@ -0,0 +1,82 @@
1
+ import { theme, dimText, heading } from './theme.js'
2
+
3
+ const BOX_WIDTH = 60
4
+
5
+ // Unicode box-drawing panel with optional title
6
+ export function box(content: string, title?: string): string {
7
+ const innerWidth = BOX_WIDTH - 2
8
+ const lines = content.split('\n')
9
+
10
+ let top: string
11
+ if (title) {
12
+ const titleText = ` ${title} `
13
+ const remaining = innerWidth - titleText.length - 1
14
+ top = theme.secondary('╭─') + theme.primary.bold(titleText) + theme.secondary('─'.repeat(Math.max(0, remaining)) + '╮')
15
+ } else {
16
+ top = theme.secondary('╭' + '─'.repeat(innerWidth) + '╮')
17
+ }
18
+
19
+ const bottom = theme.secondary('╰' + '─'.repeat(innerWidth) + '╯')
20
+
21
+ const body = lines.map(line => {
22
+ // Strip ANSI codes to calculate visible length
23
+ const visible = stripAnsi(line)
24
+ const padding = Math.max(0, innerWidth - visible.length)
25
+ return theme.secondary('│') + ' ' + line + ' '.repeat(padding - 1) + theme.secondary('│')
26
+ })
27
+
28
+ return [top, ...body, bottom].join('\n')
29
+ }
30
+
31
+ // Step indicator: [Step 2/5] Configuring Vault
32
+ export function stepIndicator(current: number, total: number, label: string): string {
33
+ const badge = theme.secondary(`[Step ${current}/${total}]`)
34
+ const text = heading(label)
35
+ return `\n${badge} ${text}\n`
36
+ }
37
+
38
+ // Progress bar: [========> ] 40%
39
+ export function progressBar(percent: number, width = 30): string {
40
+ const filled = Math.round((percent / 100) * width)
41
+ const empty = width - filled
42
+ const bar = theme.primary('='.repeat(Math.max(0, filled - 1)) + (filled > 0 ? '>' : '')) +
43
+ dimText(' '.repeat(empty))
44
+ const pct = theme.dim(`${Math.round(percent)}%`)
45
+ return `[${bar}] ${pct}`
46
+ }
47
+
48
+ // Info panel: key-value list in a box
49
+ export function infoPanel(title: string, items: Record<string, string>): string {
50
+ const lines = Object.entries(items).map(([key, value]) => {
51
+ return `${theme.dim(key + ':')} ${theme.white(value)}`
52
+ })
53
+ return box(lines.join('\n'), title)
54
+ }
55
+
56
+ // Styled horizontal divider
57
+ export function divider(): string {
58
+ return theme.secondary('─'.repeat(BOX_WIDTH))
59
+ }
60
+
61
+ // Summary panel with status icons
62
+ export function summaryPanel(title: string, items: Array<{ label: string; value: string; status?: 'success' | 'warning' | 'error' | 'info' }>): string {
63
+ const statusIcons: Record<string, string> = {
64
+ success: theme.success('+'),
65
+ warning: theme.warning('!'),
66
+ error: theme.error('x'),
67
+ info: theme.primary('*'),
68
+ }
69
+
70
+ const lines = items.map(item => {
71
+ const icon = item.status ? statusIcons[item.status] ?? ' ' : ' '
72
+ return ` ${icon} ${theme.dim(item.label + ':')} ${theme.white(item.value)}`
73
+ })
74
+
75
+ return box(lines.join('\n'), title)
76
+ }
77
+
78
+ // Strip ANSI escape codes for length calculation
79
+ function stripAnsi(str: string): string {
80
+ // eslint-disable-next-line no-control-regex
81
+ return str.replace(/\u001B\[[0-9;]*m/g, '')
82
+ }
@@ -0,0 +1,4 @@
1
+ export * from './theme.js'
2
+ export * from './components.js'
3
+ export * from './logo.js'
4
+ export * from './animations.js'
@@ -0,0 +1,36 @@
1
+ import chalk from 'chalk'
2
+ import { box } from './components.js'
3
+ import { dimText } from './theme.js'
4
+
5
+ // 5-shade gradient from light purple to dark violet
6
+ const gradientColors = [
7
+ '#A78BFA', // light purple
8
+ '#8B5CF6', // medium purple
9
+ '#7C3AED', // deep purple (primary)
10
+ '#6D28D9', // dark purple
11
+ '#5B21B6', // dark violet (secondary)
12
+ ]
13
+
14
+ const logoLines = [
15
+ ' ██████╗ ██████╗ █████╗ ██╗███╗ ██╗ ',
16
+ ' ██╔══██╗██╔══██╗██╔══██╗██║████╗ ██║ ',
17
+ ' ██████╔╝██████╔╝███████║██║██╔██╗ ██║ ',
18
+ ' ██╔══██╗██╔══██╗██╔══██║██║██║╚██╗██║ ',
19
+ ' ██████╔╝██║ ██║██║ ██║██║██║ ╚████║ ',
20
+ ' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ',
21
+ ]
22
+
23
+ export function renderLogo(): string {
24
+ return logoLines
25
+ .map((line, i) => chalk.hex(gradientColors[i] ?? gradientColors[gradientColors.length - 1]!)(line))
26
+ .join('\n')
27
+ }
28
+
29
+ export function renderBanner(version: string): string {
30
+ const logo = renderLogo()
31
+ const tagline = dimText(' Local Development Assistant with Memory')
32
+ const ver = dimText(` v${version}`)
33
+
34
+ const content = [logo, '', tagline, ver].join('\n')
35
+ return box(content, 'Claude Brain')
36
+ }
@@ -0,0 +1,55 @@
1
+ import chalk from 'chalk'
2
+
3
+ // Color palette
4
+ export const colors = {
5
+ deepPurple: '#7C3AED',
6
+ darkViolet: '#5B21B6',
7
+ charcoal: '#1A1A1A',
8
+ nearBlack: '#0A0A0A',
9
+ darkRed: '#B91C1C',
10
+ green: '#22C55E',
11
+ amber: '#F59E0B',
12
+ white: '#FFFFFF',
13
+ gray: '#6B7280',
14
+ } as const
15
+
16
+ // Pre-built chalk instances
17
+ export const theme = {
18
+ primary: chalk.hex(colors.deepPurple),
19
+ secondary: chalk.hex(colors.darkViolet),
20
+ error: chalk.hex(colors.darkRed),
21
+ success: chalk.hex(colors.green),
22
+ warning: chalk.hex(colors.amber),
23
+ dim: chalk.hex(colors.gray),
24
+ bold: chalk.bold,
25
+ white: chalk.white,
26
+ }
27
+
28
+ // Styled text helpers
29
+ export function heading(text: string): string {
30
+ return theme.primary.bold(text)
31
+ }
32
+
33
+ export function successText(text: string): string {
34
+ return theme.success(` ${text}`)
35
+ }
36
+
37
+ export function errorText(text: string): string {
38
+ return theme.error(` ${text}`)
39
+ }
40
+
41
+ export function warningText(text: string): string {
42
+ return theme.warning(` ${text}`)
43
+ }
44
+
45
+ export function dimText(text: string): string {
46
+ return theme.dim(text)
47
+ }
48
+
49
+ export function highlight(text: string): string {
50
+ return theme.primary(text)
51
+ }
52
+
53
+ export function keyValue(key: string, value: string): string {
54
+ return `${theme.dim(key + ':')} ${theme.white(value)}`
55
+ }
@@ -2,12 +2,37 @@ import { createLogger } from '@/utils/logger'
2
2
  import { resolveHomePath } from '@/config/home'
3
3
  import { ensureHomeDirectory } from '@/cli/auto-setup'
4
4
  import { SetupWizard } from './wizard'
5
+ import { renderBanner, typewrite, transition, box, errorText, theme } from '@/cli/ui/index.js'
6
+ import { readFileSync } from 'node:fs'
7
+ import { resolve, dirname } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
5
9
 
6
10
  export * from './wizard'
7
11
 
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = dirname(__filename)
14
+ const PACKAGE_ROOT = resolve(__dirname, '..', '..')
15
+
16
+ function getVersion(): string {
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8'))
19
+ return pkg.version || 'unknown'
20
+ } catch {
21
+ return 'unknown'
22
+ }
23
+ }
24
+
8
25
  export async function runSetup() {
9
26
  ensureHomeDirectory()
10
27
 
28
+ const version = getVersion()
29
+ console.log()
30
+ console.log(renderBanner(version))
31
+ console.log()
32
+
33
+ await typewrite(theme.dim('Welcome! Let\'s configure Claude Brain for your system.'))
34
+ await transition(400)
35
+
11
36
  const logger = createLogger('info', resolveHomePath('./logs/setup.log'))
12
37
 
13
38
  try {
@@ -16,7 +41,8 @@ export async function runSetup() {
16
41
  await wizard.applyConfiguration(answers)
17
42
 
18
43
  } catch (error) {
19
- console.error('\nSetup failed:', error)
44
+ console.log()
45
+ console.log(box(errorText(`Setup failed: ${error instanceof Error ? error.message : String(error)}`), 'Error'))
20
46
  process.exit(1)
21
47
  }
22
48
  }
@@ -6,6 +6,11 @@ import os from 'os'
6
6
  import { fileURLToPath } from 'url'
7
7
  import type { Logger } from 'pino'
8
8
  import { getHomePaths } from '@/config/home'
9
+ import {
10
+ theme, heading, successText, errorText, warningText, dimText,
11
+ box, stepIndicator, summaryPanel,
12
+ withSpinner, transition,
13
+ } from '@/cli/ui/index.js'
9
14
 
10
15
  const __filename = fileURLToPath(import.meta.url)
11
16
  const __dirname = path.dirname(__filename)
@@ -28,12 +33,26 @@ export class SetupWizard {
28
33
  }
29
34
 
30
35
  async run(): Promise<SetupAnswers> {
31
- console.log('\nClaude Brain - Setup Wizard\n')
32
- console.log('Welcome! Let\'s configure Claude Brain for your system.\n')
36
+ // Step 1: Detect Vaults
37
+ console.log(stepIndicator(1, 5, 'Detecting Obsidian Vaults'))
38
+ await transition()
39
+
40
+ const suggestedPaths = await withSpinner('Scanning for Obsidian vaults', () =>
41
+ this.detectVaultLocations()
42
+ )
43
+
44
+ if (suggestedPaths.length > 0) {
45
+ console.log(successText(`Found ${suggestedPaths.length} vault${suggestedPaths.length > 1 ? 's' : ''}`))
46
+ } else {
47
+ console.log(warningText('No vaults auto-detected — enter path manually'))
48
+ }
49
+ console.log()
33
50
 
34
- const suggestedPaths = await this.detectVaultLocations()
51
+ // Step 2: Vault Configuration
52
+ console.log(stepIndicator(2, 5, 'Vault Configuration'))
53
+ await transition()
35
54
 
36
- const answers = await prompts([
55
+ const vaultAnswers = await prompts([
37
56
  {
38
57
  type: 'select',
39
58
  name: 'vaultPathChoice',
@@ -57,18 +76,37 @@ export class SetupWizard {
57
76
  }
58
77
  }
59
78
  },
60
- {
61
- type: 'select',
62
- name: 'logLevel',
63
- message: 'Select log level:',
64
- choices: [
65
- { title: 'Error (production)', value: 'error' },
66
- { title: 'Warn (recommended)', value: 'warn' },
67
- { title: 'Info', value: 'info' },
68
- { title: 'Debug (verbose)', value: 'debug' }
69
- ],
70
- initial: 1
71
- },
79
+ ])
80
+
81
+ if (!vaultAnswers.vaultPathChoice) {
82
+ console.log('\n' + errorText('Setup cancelled.'))
83
+ process.exit(0)
84
+ }
85
+
86
+ const finalVaultPath = vaultAnswers.vaultPath || vaultAnswers.vaultPathChoice
87
+
88
+ // Step 3: Logging
89
+ console.log(stepIndicator(3, 5, 'Logging Configuration'))
90
+ await transition()
91
+
92
+ const loggingAnswers = await prompts({
93
+ type: 'select',
94
+ name: 'logLevel',
95
+ message: 'Select log level:',
96
+ choices: [
97
+ { title: 'Error (production)', value: 'error' },
98
+ { title: 'Warn (recommended)', value: 'warn' },
99
+ { title: 'Info', value: 'info' },
100
+ { title: 'Debug (verbose)', value: 'debug' }
101
+ ],
102
+ initial: 1
103
+ })
104
+
105
+ // Step 4: Features
106
+ console.log(stepIndicator(4, 5, 'Feature Selection'))
107
+ await transition()
108
+
109
+ const featureAnswers = await prompts([
72
110
  {
73
111
  type: 'confirm',
74
112
  name: 'enableFileWatch',
@@ -89,20 +127,40 @@ export class SetupWizard {
89
127
  }
90
128
  ])
91
129
 
92
- if (!answers.vaultPathChoice) {
93
- console.log('\n Setup cancelled.')
94
- process.exit(0)
95
- }
96
-
97
- const finalVaultPath = answers.vaultPath || answers.vaultPathChoice
130
+ // Step 5: Review
131
+ console.log(stepIndicator(5, 5, 'Review Configuration'))
132
+ await transition()
98
133
 
99
- return {
134
+ const answers: SetupAnswers = {
100
135
  vaultPath: finalVaultPath,
101
- logLevel: answers.logLevel,
102
- enableFileWatch: answers.enableFileWatch,
103
- createSampleProject: answers.createSampleProject,
104
- installClaudeMd: answers.installClaudeMd ?? true
136
+ logLevel: loggingAnswers.logLevel ?? 'warn',
137
+ enableFileWatch: featureAnswers.enableFileWatch ?? true,
138
+ createSampleProject: featureAnswers.createSampleProject ?? true,
139
+ installClaudeMd: featureAnswers.installClaudeMd ?? true,
105
140
  }
141
+
142
+ console.log(summaryPanel('Configuration Summary', [
143
+ { label: 'Vault Path', value: answers.vaultPath, status: 'success' },
144
+ { label: 'Log Level', value: answers.logLevel, status: 'info' },
145
+ { label: 'File Watching', value: answers.enableFileWatch ? 'Enabled' : 'Disabled', status: answers.enableFileWatch ? 'success' : 'warning' },
146
+ { label: 'Sample Project', value: answers.createSampleProject ? 'Yes' : 'No', status: 'info' },
147
+ { label: 'Install CLAUDE.md', value: answers.installClaudeMd ? 'Yes' : 'No', status: 'info' },
148
+ ]))
149
+ console.log()
150
+
151
+ const { confirm } = await prompts({
152
+ type: 'confirm',
153
+ name: 'confirm',
154
+ message: 'Apply this configuration?',
155
+ initial: true
156
+ })
157
+
158
+ if (!confirm) {
159
+ console.log('\n' + errorText('Setup cancelled.'))
160
+ process.exit(0)
161
+ }
162
+
163
+ return answers
106
164
  }
107
165
 
108
166
  private async detectVaultLocations(): Promise<string[]> {
@@ -134,7 +192,7 @@ export class SetupWizard {
134
192
  }
135
193
 
136
194
  async applyConfiguration(answers: SetupAnswers): Promise<void> {
137
- console.log('\nApplying configuration...\n')
195
+ console.log('\n' + heading('Applying configuration...') + '\n')
138
196
 
139
197
  const homePaths = getHomePaths()
140
198
 
@@ -147,34 +205,48 @@ LOG_FILE_PATH=./logs/claude-brain.log
147
205
  SERVER_NAME=claude-brain
148
206
  `
149
207
 
150
- await fs.writeFile(homePaths.env, envContent, 'utf-8')
151
- console.log('Created .env configuration file')
208
+ await withSpinner('Writing .env configuration', async () => {
209
+ await fs.writeFile(homePaths.env, envContent, 'utf-8')
210
+ })
152
211
 
153
- await fs.mkdir(homePaths.data, { recursive: true })
154
- await fs.mkdir(homePaths.logs, { recursive: true })
155
- console.log('Created data and log directories')
212
+ await withSpinner('Creating data and log directories', async () => {
213
+ await fs.mkdir(homePaths.data, { recursive: true })
214
+ await fs.mkdir(homePaths.logs, { recursive: true })
215
+ })
156
216
 
157
217
  if (answers.createSampleProject) {
158
- await this.createSampleProject(answers.vaultPath)
218
+ await withSpinner('Creating sample project in vault', async () => {
219
+ await this.createSampleProject(answers.vaultPath)
220
+ })
159
221
  }
160
222
 
161
223
  if (answers.installClaudeMd) {
162
- await this.installClaudeMd()
224
+ const shouldInstall = await this.confirmClaudeMdInstall()
225
+ if (shouldInstall) {
226
+ await withSpinner('Installing CLAUDE.md', async () => {
227
+ await this.copyClaudeMd()
228
+ })
229
+ }
163
230
  }
164
231
 
165
- console.log('\nSetup complete!')
166
- console.log('\nNext steps:')
167
- console.log('1. Run: claude-brain install (register MCP with Claude Code)')
168
- console.log('2. Run: claude-brain health (verify everything works)')
232
+ console.log()
233
+ console.log(box([
234
+ heading('Setup complete!'),
235
+ '',
236
+ dimText('Next steps:'),
237
+ ` ${theme.primary('1.')} Run: ${theme.bold('claude-brain install')} ${dimText('Register MCP with Claude Code')}`,
238
+ ` ${theme.primary('2.')} Run: ${theme.bold('claude-brain health')} ${dimText('Verify everything works')}`,
239
+ ].join('\n'), 'Done'))
240
+ console.log()
169
241
  }
170
242
 
171
- private async installClaudeMd(): Promise<void> {
243
+ private async confirmClaudeMdInstall(): Promise<boolean> {
172
244
  const sourcePath = path.join(PACKAGE_ROOT, 'assets', 'CLAUDE.md')
173
245
  const destPath = path.join(os.homedir(), 'CLAUDE.md')
174
246
 
175
247
  if (!existsSync(sourcePath)) {
176
- console.log('CLAUDE.md asset not found, skipping')
177
- return
248
+ console.log(warningText('CLAUDE.md asset not found, skipping'))
249
+ return false
178
250
  }
179
251
 
180
252
  if (existsSync(destPath)) {
@@ -182,31 +254,31 @@ SERVER_NAME=claude-brain
182
254
  type: 'confirm',
183
255
  name: 'overwrite',
184
256
  message: '~/CLAUDE.md already exists. Overwrite?',
185
- initial: false
257
+ initial: false,
186
258
  })
187
259
  if (!overwrite) {
188
- console.log('Skipped CLAUDE.md installation')
189
- return
260
+ console.log(dimText(' Skipped CLAUDE.md installation'))
261
+ return false
190
262
  }
191
263
  }
192
264
 
193
- try {
194
- await fs.copyFile(sourcePath, destPath)
195
- console.log('Installed CLAUDE.md to ~/CLAUDE.md')
196
- } catch (error) {
197
- console.error('Failed to install CLAUDE.md:', error)
198
- }
265
+ return true
266
+ }
267
+
268
+ private async copyClaudeMd(): Promise<void> {
269
+ const sourcePath = path.join(PACKAGE_ROOT, 'assets', 'CLAUDE.md')
270
+ const destPath = path.join(os.homedir(), 'CLAUDE.md')
271
+ await fs.copyFile(sourcePath, destPath)
199
272
  }
200
273
 
201
274
  private async createSampleProject(vaultPath: string): Promise<void> {
202
275
  const projectPath = path.join(vaultPath, 'Projects', 'sample-project')
203
276
 
204
- try {
205
- await fs.mkdir(projectPath, { recursive: true })
277
+ await fs.mkdir(projectPath, { recursive: true })
206
278
 
207
- await fs.writeFile(
208
- path.join(projectPath, 'context.md'),
209
- `---
279
+ await fs.writeFile(
280
+ path.join(projectPath, 'context.md'),
281
+ `---
210
282
  type: project-context
211
283
  project: sample-project
212
284
  status: active
@@ -233,12 +305,12 @@ This is a sample project created by Claude Brain setup wizard.
233
305
  - Test Claude Brain integration
234
306
  - Learn the workflow
235
307
  `,
236
- 'utf-8'
237
- )
308
+ 'utf-8'
309
+ )
238
310
 
239
- await fs.writeFile(
240
- path.join(projectPath, 'progress.md'),
241
- `---
311
+ await fs.writeFile(
312
+ path.join(projectPath, 'progress.md'),
313
+ `---
242
314
  type: progress-tracker
243
315
  project: sample-project
244
316
  status: in-progress
@@ -253,12 +325,12 @@ completion_percentage: 0
253
325
  - [ ] Test MCP integration
254
326
  - [ ] Try all tools
255
327
  `,
256
- 'utf-8'
257
- )
328
+ 'utf-8'
329
+ )
258
330
 
259
- await fs.writeFile(
260
- path.join(projectPath, 'decisions.md'),
261
- `---
331
+ await fs.writeFile(
332
+ path.join(projectPath, 'decisions.md'),
333
+ `---
262
334
  type: decision-log
263
335
  project: sample-project
264
336
  decision_count: 0
@@ -268,12 +340,12 @@ decision_count: 0
268
340
 
269
341
  This file will track important decisions for the project.
270
342
  `,
271
- 'utf-8'
272
- )
343
+ 'utf-8'
344
+ )
273
345
 
274
- await fs.writeFile(
275
- path.join(projectPath, 'standards.md'),
276
- `---
346
+ await fs.writeFile(
347
+ path.join(projectPath, 'standards.md'),
348
+ `---
277
349
  type: coding-standards
278
350
  project: sample-project
279
351
  ---
@@ -284,19 +356,19 @@ project: sample-project
284
356
  - Use strict mode
285
357
  - Prefer const over let
286
358
  `,
287
- 'utf-8'
288
- )
359
+ 'utf-8'
360
+ )
289
361
 
290
- const globalPath = path.join(vaultPath, 'Global')
291
- await fs.mkdir(globalPath, { recursive: true })
362
+ const globalPath = path.join(vaultPath, 'Global')
363
+ await fs.mkdir(globalPath, { recursive: true })
292
364
 
293
- const globalStandardsPath = path.join(globalPath, 'standards.md')
294
- try {
295
- await fs.access(globalStandardsPath)
296
- } catch {
297
- await fs.writeFile(
298
- globalStandardsPath,
299
- `---
365
+ const globalStandardsPath = path.join(globalPath, 'standards.md')
366
+ try {
367
+ await fs.access(globalStandardsPath)
368
+ } catch {
369
+ await fs.writeFile(
370
+ globalStandardsPath,
371
+ `---
300
372
  type: global-standards
301
373
  last_updated: ${new Date().toISOString().split('T')[0]}
302
374
  ---
@@ -308,14 +380,8 @@ last_updated: ${new Date().toISOString().split('T')[0]}
308
380
  - Prefer const over let
309
381
  - Add JSDoc comments for public APIs
310
382
  `,
311
- 'utf-8'
312
- )
313
- }
314
-
315
- console.log('Created sample project in vault')
316
-
317
- } catch (error) {
318
- console.error('Failed to create sample project:', error)
383
+ 'utf-8'
384
+ )
319
385
  }
320
386
  }
321
387
  }