claude-brain 0.3.0 → 0.3.2

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.0",
3
+ "version": "0.3.2",
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,49 @@ 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
+ ['health', 'Run health checks'],
33
+ ['diagnose', 'Run diagnostics'],
34
+ ['version', 'Show version'],
35
+ ['help', 'Show this help message'],
36
+ ]
37
+
38
+ const cmdLines = commands
39
+ .map(([cmd, desc]) => ` ${theme.primary(cmd!.padEnd(12))} ${dimText(desc!)}`)
40
+ .join('\n')
41
+
42
+ const content = [
43
+ `${dimText('v' + version)} ${dimText('Local Development Assistant with Memory')}`,
44
+ '',
45
+ `${theme.bold('Usage:')} ${dimText('claude-brain [command]')}`,
46
+ '',
47
+ theme.bold('Commands:'),
48
+ cmdLines,
49
+ '',
50
+ theme.bold('Options:'),
51
+ ` ${theme.primary('-v'.padEnd(12))} ${dimText('Show version')}`,
52
+ ` ${theme.primary('-h'.padEnd(12))} ${dimText('Show help')}`,
53
+ '',
54
+ theme.bold('Examples:'),
55
+ ` ${dimText('claude-brain')} ${dimText('Start MCP server')}`,
56
+ ` ${dimText('claude-brain setup')} ${dimText('Configure Claude Brain')}`,
57
+ ` ${dimText('claude-brain install')} ${dimText('Register with Claude Code')}`,
58
+ ` ${dimText('claude-brain health')} ${dimText('Check system health')}`,
59
+ '',
60
+ theme.bold('Environment:'),
61
+ ` ${theme.primary('CLAUDE_BRAIN_HOME')} ${dimText('Override home directory (default: ~/.claude-brain/')}`,
62
+ ].join('\n')
63
+
64
+ console.log(content)
65
+ console.log()
49
66
  }
50
67
 
51
68
  async function main() {
@@ -97,7 +114,8 @@ async function main() {
97
114
  }
98
115
 
99
116
  default: {
100
- console.error(`Unknown command: ${command}\n`)
117
+ console.log()
118
+ console.log(errorText(`Unknown command: ${command}`))
101
119
  printHelp()
102
120
  process.exit(1)
103
121
  }
@@ -105,6 +123,7 @@ async function main() {
105
123
  }
106
124
 
107
125
  main().catch((error) => {
108
- console.error('Fatal error:', error)
126
+ console.log()
127
+ console.log(box(errorText(`Fatal error: ${error instanceof Error ? error.message : String(error)}`), 'Error'))
109
128
  process.exit(1)
110
129
  })
@@ -1,8 +1,13 @@
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 {
8
+ const cmd = process.platform === 'win32' ? 'where claude-brain' : 'which claude-brain'
4
9
  try {
5
- const result = execSync('which claude-brain', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
10
+ const result = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
6
11
  return result.trim().length > 0
7
12
  } catch {
8
13
  return false
@@ -10,41 +15,59 @@ function isGloballyInstalled(): boolean {
10
15
  }
11
16
 
12
17
  export async function runInstall() {
13
- 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()
14
23
 
15
- if (isGloballyInstalled()) {
16
- 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()
17
29
 
18
30
  try {
19
- execSync('claude mcp add claude-brain -- claude-brain serve', {
20
- encoding: 'utf-8',
21
- 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
+ })
22
36
  })
23
- console.log('\nClaude Brain registered as MCP server in Claude Code.')
24
- } catch (error) {
25
- console.error('\nFailed to register automatically. Run manually:')
26
- 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'))
27
47
  }
28
48
  } else {
29
- console.log('claude-brain is not globally installed.')
30
- console.log('You can either install globally or use bunx.\n')
31
-
32
- console.log('Option 1: Install globally, then register:')
33
- console.log(' bun install -g claude-brain')
34
- console.log(' claude mcp add claude-brain -- claude-brain serve\n')
35
-
36
- console.log('Option 2: Use bunx (zero-install):')
37
- 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()
38
51
 
39
- console.log('Option 3: Add to Claude Code config manually:')
40
- console.log(` {
41
- "mcpServers": {
42
- "claude-brain": {
43
- "command": "bunx",
44
- "args": ["claude-brain@latest"]
45
- }
46
- }
47
- }
48
- `)
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'))
49
71
  }
72
+ console.log()
50
73
  }
@@ -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
- }
130
+ // Step 5: Review
131
+ console.log(stepIndicator(5, 5, 'Review Configuration'))
132
+ await transition()
96
133
 
97
- const finalVaultPath = answers.vaultPath || answers.vaultPathChoice
98
-
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,
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)
105
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,25 +205,36 @@ 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
+ await withSpinner('Installing CLAUDE.md', async () => {
225
+ await this.installClaudeMd()
226
+ })
163
227
  }
164
228
 
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)')
229
+ console.log()
230
+ console.log(box([
231
+ heading('Setup complete!'),
232
+ '',
233
+ dimText('Next steps:'),
234
+ ` ${theme.primary('1.')} Run: ${theme.bold('claude-brain install')} ${dimText('Register MCP with Claude Code')}`,
235
+ ` ${theme.primary('2.')} Run: ${theme.bold('claude-brain health')} ${dimText('Verify everything works')}`,
236
+ ].join('\n'), 'Done'))
237
+ console.log()
169
238
  }
170
239
 
171
240
  private async installClaudeMd(): Promise<void> {
@@ -173,7 +242,7 @@ SERVER_NAME=claude-brain
173
242
  const destPath = path.join(os.homedir(), 'CLAUDE.md')
174
243
 
175
244
  if (!existsSync(sourcePath)) {
176
- console.log('CLAUDE.md asset not found, skipping')
245
+ console.log(warningText('CLAUDE.md asset not found, skipping'))
177
246
  return
178
247
  }
179
248
 
@@ -185,28 +254,22 @@ SERVER_NAME=claude-brain
185
254
  initial: false
186
255
  })
187
256
  if (!overwrite) {
188
- console.log('Skipped CLAUDE.md installation')
257
+ console.log(dimText(' Skipped CLAUDE.md installation'))
189
258
  return
190
259
  }
191
260
  }
192
261
 
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
- }
262
+ await fs.copyFile(sourcePath, destPath)
199
263
  }
200
264
 
201
265
  private async createSampleProject(vaultPath: string): Promise<void> {
202
266
  const projectPath = path.join(vaultPath, 'Projects', 'sample-project')
203
267
 
204
- try {
205
- await fs.mkdir(projectPath, { recursive: true })
268
+ await fs.mkdir(projectPath, { recursive: true })
206
269
 
207
- await fs.writeFile(
208
- path.join(projectPath, 'context.md'),
209
- `---
270
+ await fs.writeFile(
271
+ path.join(projectPath, 'context.md'),
272
+ `---
210
273
  type: project-context
211
274
  project: sample-project
212
275
  status: active
@@ -233,12 +296,12 @@ This is a sample project created by Claude Brain setup wizard.
233
296
  - Test Claude Brain integration
234
297
  - Learn the workflow
235
298
  `,
236
- 'utf-8'
237
- )
299
+ 'utf-8'
300
+ )
238
301
 
239
- await fs.writeFile(
240
- path.join(projectPath, 'progress.md'),
241
- `---
302
+ await fs.writeFile(
303
+ path.join(projectPath, 'progress.md'),
304
+ `---
242
305
  type: progress-tracker
243
306
  project: sample-project
244
307
  status: in-progress
@@ -253,12 +316,12 @@ completion_percentage: 0
253
316
  - [ ] Test MCP integration
254
317
  - [ ] Try all tools
255
318
  `,
256
- 'utf-8'
257
- )
319
+ 'utf-8'
320
+ )
258
321
 
259
- await fs.writeFile(
260
- path.join(projectPath, 'decisions.md'),
261
- `---
322
+ await fs.writeFile(
323
+ path.join(projectPath, 'decisions.md'),
324
+ `---
262
325
  type: decision-log
263
326
  project: sample-project
264
327
  decision_count: 0
@@ -268,12 +331,12 @@ decision_count: 0
268
331
 
269
332
  This file will track important decisions for the project.
270
333
  `,
271
- 'utf-8'
272
- )
334
+ 'utf-8'
335
+ )
273
336
 
274
- await fs.writeFile(
275
- path.join(projectPath, 'standards.md'),
276
- `---
337
+ await fs.writeFile(
338
+ path.join(projectPath, 'standards.md'),
339
+ `---
277
340
  type: coding-standards
278
341
  project: sample-project
279
342
  ---
@@ -284,19 +347,19 @@ project: sample-project
284
347
  - Use strict mode
285
348
  - Prefer const over let
286
349
  `,
287
- 'utf-8'
288
- )
350
+ 'utf-8'
351
+ )
289
352
 
290
- const globalPath = path.join(vaultPath, 'Global')
291
- await fs.mkdir(globalPath, { recursive: true })
353
+ const globalPath = path.join(vaultPath, 'Global')
354
+ await fs.mkdir(globalPath, { recursive: true })
292
355
 
293
- const globalStandardsPath = path.join(globalPath, 'standards.md')
294
- try {
295
- await fs.access(globalStandardsPath)
296
- } catch {
297
- await fs.writeFile(
298
- globalStandardsPath,
299
- `---
356
+ const globalStandardsPath = path.join(globalPath, 'standards.md')
357
+ try {
358
+ await fs.access(globalStandardsPath)
359
+ } catch {
360
+ await fs.writeFile(
361
+ globalStandardsPath,
362
+ `---
300
363
  type: global-standards
301
364
  last_updated: ${new Date().toISOString().split('T')[0]}
302
365
  ---
@@ -308,14 +371,8 @@ last_updated: ${new Date().toISOString().split('T')[0]}
308
371
  - Prefer const over let
309
372
  - Add JSDoc comments for public APIs
310
373
  `,
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)
374
+ 'utf-8'
375
+ )
319
376
  }
320
377
  }
321
378
  }