personality 0.1.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.
data/PLAN.md ADDED
@@ -0,0 +1,621 @@
1
+ # Plan: `psn init` Command
2
+
3
+ ## Overview
4
+
5
+ Interactive CLI command that bootstraps the personality runtime environment.
6
+ Checks for required dependencies, prompts the user before installing anything,
7
+ and initialises the local database.
8
+
9
+ ## Prerequisites
10
+
11
+ - Ruby >= 3.2
12
+ - `uv` (Python package manager) -- for piper-tts installation
13
+ - Internet access for model downloads
14
+
15
+ ## Steps
16
+
17
+ ### 1. Create sqlite-vec database
18
+
19
+ - Path: `~/.local/share/personality/main.db`
20
+ - Create parent directories if missing
21
+ - Initialise with sqlite-vec extension loaded
22
+ - Run schema migrations (embeddings table, metadata, etc.)
23
+ - Skip if database already exists and schema is current
24
+
25
+ ### 2. Check for Ollama
26
+
27
+ - Detect via `which ollama` or `ollama --version`
28
+ - If present: report version and continue
29
+ - If missing: prompt user to install
30
+ - macOS: `brew install ollama`
31
+ - Linux: `curl -fsSL https://ollama.com/install.sh | sh`
32
+ - Start the service if not running (`ollama serve` or systemd)
33
+
34
+ ### 3. Install nomic-embed-text model
35
+
36
+ - Check via `ollama list` for `nomic-embed-text`
37
+ - If Ollama was just installed in step 2: pull automatically without prompting
38
+ - If Ollama was already present: prompt before pulling
39
+ - Command: `ollama pull nomic-embed-text`
40
+
41
+ ### 4. Install piper-tts
42
+
43
+ - Detect via `which piper` or `piper --help`
44
+ - If present: report version and continue
45
+ - If missing: prompt user to install via `uv` (see step 4a)
46
+ - `uv tool install piper-tts --with pathvalidate`
47
+ (`pathvalidate` is a missing transitive dep in piper-tts 1.4.1)
48
+
49
+ ### 4a. Check for uv (prerequisite for piper-tts)
50
+
51
+ - Detect via `which uv` or `uv --version`
52
+ - If present: continue to piper install
53
+ - If missing: prompt user to install
54
+ - If `brew` is available: `brew install uv`
55
+ - Otherwise: `curl -LsSf https://astral.sh/uv/install.sh | sh`
56
+
57
+ ## UX
58
+
59
+ - Each step prints status with TTY spinners/colours (pastel + tty-spinner)
60
+ - Prompt before any install action; `--yes` flag to skip confirmations
61
+ - Idempotent: safe to re-run, skips already-completed steps
62
+ - Summary at the end listing what was installed/skipped
63
+
64
+ ## Command signature
65
+
66
+ ```
67
+ psn init [--yes]
68
+ ```
69
+
70
+ Registered as a Thor subcommand under the main `Personality::CLI`.
71
+
72
+ ---
73
+
74
+ # Plan: Vector DB Architecture
75
+
76
+ ## Overview
77
+
78
+ Port the Python psn vector DB capabilities (3 MCP servers) to the Ruby personality
79
+ gem using sqlite-vec instead of PostgreSQL/pgvector. Consolidate into a single SQLite
80
+ database with separate tables and vec0 virtual tables for each concern.
81
+
82
+ ## Database: `~/.local/share/personality/main.db`
83
+
84
+ ### Schema
85
+
86
+ sqlite-vec uses **vec0 virtual tables** for vector storage, linked to regular tables
87
+ via rowid. Each domain gets its own pair (data table + vec0 virtual table).
88
+
89
+ ```sql
90
+ -- === Personas (carts) ===
91
+ CREATE TABLE IF NOT EXISTS carts (
92
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
93
+ tag TEXT UNIQUE NOT NULL,
94
+ version TEXT,
95
+ name TEXT,
96
+ type TEXT,
97
+ tagline TEXT,
98
+ source TEXT,
99
+ created_at TEXT DEFAULT (datetime('now')),
100
+ updated_at TEXT DEFAULT (datetime('now'))
101
+ );
102
+
103
+ -- === Memory ===
104
+ CREATE TABLE IF NOT EXISTS memories (
105
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
106
+ cart_id INTEGER NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
107
+ subject TEXT NOT NULL,
108
+ content TEXT NOT NULL,
109
+ metadata TEXT DEFAULT '{}',
110
+ created_at TEXT DEFAULT (datetime('now')),
111
+ updated_at TEXT DEFAULT (datetime('now'))
112
+ );
113
+ CREATE INDEX IF NOT EXISTS idx_memories_cart_id ON memories(cart_id);
114
+ CREATE INDEX IF NOT EXISTS idx_memories_subject ON memories(subject);
115
+
116
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
117
+ memory_id INTEGER PRIMARY KEY,
118
+ embedding float[768]
119
+ );
120
+
121
+ -- === Code Index ===
122
+ CREATE TABLE IF NOT EXISTS code_chunks (
123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
124
+ path TEXT NOT NULL,
125
+ content TEXT NOT NULL,
126
+ language TEXT,
127
+ project TEXT,
128
+ chunk_index INTEGER DEFAULT 0,
129
+ indexed_at TEXT DEFAULT (datetime('now'))
130
+ );
131
+ CREATE INDEX IF NOT EXISTS idx_code_chunks_project ON code_chunks(project);
132
+
133
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_code USING vec0(
134
+ chunk_id INTEGER PRIMARY KEY,
135
+ embedding float[768]
136
+ );
137
+
138
+ -- === Doc Index ===
139
+ CREATE TABLE IF NOT EXISTS doc_chunks (
140
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
141
+ path TEXT NOT NULL,
142
+ content TEXT NOT NULL,
143
+ project TEXT,
144
+ chunk_index INTEGER DEFAULT 0,
145
+ indexed_at TEXT DEFAULT (datetime('now'))
146
+ );
147
+ CREATE INDEX IF NOT EXISTS idx_doc_chunks_project ON doc_chunks(project);
148
+
149
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_docs USING vec0(
150
+ chunk_id INTEGER PRIMARY KEY,
151
+ embedding float[768]
152
+ );
153
+
154
+ -- === Schema version ===
155
+ CREATE TABLE IF NOT EXISTS schema_version (
156
+ version INTEGER PRIMARY KEY,
157
+ applied_at TEXT DEFAULT (datetime('now'))
158
+ );
159
+ ```
160
+
161
+ ### Vector search pattern (sqlite-vec)
162
+
163
+ ```sql
164
+ -- Find similar memories (cosine distance via vec0)
165
+ SELECT m.id, m.subject, m.content, v.distance
166
+ FROM vec_memories v
167
+ INNER JOIN memories m ON m.id = v.memory_id
168
+ WHERE v.embedding MATCH ? -- ? = query embedding as JSON array
169
+ AND k = ? -- ? = limit
170
+ ORDER BY v.distance;
171
+ ```
172
+
173
+ ## Design Principle: Service Objects + Thin Interfaces
174
+
175
+ Business logic lives in **service objects** (plain Ruby classes). Both CLI commands
176
+ and MCP tool handlers are thin wrappers that delegate to the same services.
177
+ No logic in the interface layer — ever.
178
+
179
+ ```
180
+ ┌─────────────┐
181
+ │ Service │
182
+ │ Objects │
183
+ │ │
184
+ │ DB │
185
+ │ Embedding │
186
+ │ Chunker │
187
+ │ Memory │
188
+ │ Indexer │
189
+ │ Cart │
190
+ └──────┬──────┘
191
+
192
+ ┌───────────┼───────────┐
193
+ │ │
194
+ ┌───────▼───────┐ ┌───────▼───────┐
195
+ │ CLI Layer │ │ MCP Layer │
196
+ │ (Thor) │ │ (JSON-RPC) │
197
+ │ │ │ │
198
+ │ psn memory * │ │ memory/* │
199
+ │ psn index * │ │ index/* │
200
+ │ psn cart * │ │ cart/* │
201
+ └───────────────┘ └───────────────┘
202
+ ```
203
+
204
+ ## Module Architecture
205
+
206
+ ```
207
+ lib/personality/
208
+ # Core
209
+ version.rb # (existing)
210
+ db.rb # Database connection + schema + migrations
211
+ embedding.rb # Ollama HTTP client for embeddings
212
+ chunker.rb # Text chunking (2000 chars, 200 overlap)
213
+
214
+ # Service objects (all logic lives here)
215
+ memory.rb # Store/recall/search/forget (cart-scoped)
216
+ indexer.rb # Code + doc indexing with semantic search
217
+ cart.rb # Persona/cart management
218
+
219
+ # CLI layer (thin Thor wrappers)
220
+ cli.rb # Root CLI + init (existing)
221
+ cli/
222
+ memory.rb # psn memory subcommands
223
+ index.rb # psn index subcommands
224
+ cart.rb # psn cart subcommands
225
+
226
+ # MCP layer (thin JSON-RPC handlers)
227
+ mcp/
228
+ server.rb # MCP server bootstrap + stdio transport
229
+ memory_handler.rb # memory/* tool definitions + dispatch
230
+ index_handler.rb # index/* tool definitions + dispatch
231
+ cart_handler.rb # cart/* tool definitions + dispatch
232
+
233
+ # Init (existing)
234
+ init.rb
235
+ ```
236
+
237
+ ### Core: `db.rb` — Database layer
238
+
239
+ - Singleton connection to `main.db`
240
+ - Loads sqlite-vec extension
241
+ - Runs schema migrations (versioned)
242
+ - `Personality::DB.connection` accessor
243
+ - Transaction helpers
244
+
245
+ ### Core: `embedding.rb` — Ollama embeddings
246
+
247
+ - HTTP client using `net/http` (zero deps, Ollama is localhost)
248
+ - `Personality::Embedding.generate(text) -> Array[Float]`
249
+ - Configurable Ollama URL (default `http://localhost:11434`)
250
+ - Model: `nomic-embed-text` (768 dimensions)
251
+ - Truncates input to 8000 chars (token limit guard)
252
+
253
+ ### Core: `chunker.rb` — Text splitting
254
+
255
+ - `Personality::Chunker.split(text, size: 2000, overlap: 200) -> Array[String]`
256
+ - Overlapping window chunker matching psn's Python implementation
257
+ - Skips content < 10 chars
258
+
259
+ ### Service: `memory.rb` — Persistent memory
260
+
261
+ - Cart-scoped (memories belong to a persona)
262
+ - Returns plain hashes — no formatting, no output
263
+ - `store(subject:, content:, metadata: {})` → `{id:, subject:}`
264
+ - `recall(query:, limit: 5, subject: nil)` → `{memories: [...]}`
265
+ - `search(subject: nil, limit: 20)` → `{memories: [...]}`
266
+ - `forget(id:)` → `{deleted: true/false}`
267
+ - `list` → `{subjects: [{subject:, count:}, ...]}`
268
+
269
+ ### Service: `indexer.rb` — Code/doc indexing
270
+
271
+ - Returns plain hashes
272
+ - `index_code(path:, project: nil, extensions: nil)` → `{indexed:, project:, errors:}`
273
+ - `index_docs(path:, project: nil)` → `{indexed:, project:, errors:}`
274
+ - `search(query:, type: :all, project: nil, limit: 10)` → `{results: [...]}`
275
+ - `status(project: nil)` → `{code_index: [...], doc_index: [...]}`
276
+ - `clear(project: nil, type: :all)` → `{cleared:, project:}`
277
+ - Default code extensions: `.py .rs .rb .js .ts .go .java .c .cpp .h`
278
+ - Doc extensions: `.md .txt .rst .adoc`
279
+
280
+ ### Service: `cart.rb` — Persona management
281
+
282
+ - `Personality::Cart.find_or_create(tag)` → `{id:, tag:}`
283
+ - `Personality::Cart.active` → current cart from `ENV["PERSONALITY_CART"]` or "default"
284
+ - `Personality::Cart.list` → `[{id:, tag:, name:, ...}, ...]`
285
+ - `Personality::Cart.use(tag)` → sets active cart
286
+ - `Personality::Cart.create(tag, name: nil, type: nil)` → `{id:, tag:}`
287
+
288
+ ## CLI Layer
289
+
290
+ Thin Thor subcommands. Each method: parse args → call service → format output.
291
+
292
+ ```
293
+ psn init # (existing) bootstrap environment
294
+ psn memory store SUBJECT CONTENT # store a memory
295
+ psn memory recall QUERY # semantic recall
296
+ psn memory search [--subject X] # text search
297
+ psn memory forget ID # delete memory
298
+ psn memory list # list subjects
299
+ psn index code PATH [--project X] # index code files
300
+ psn index docs PATH [--project X] # index doc files
301
+ psn index search QUERY [--type X] # semantic search
302
+ psn index status [--project X] # show stats
303
+ psn index clear [--project X] # clear index
304
+ psn cart list # list personas
305
+ psn cart use TAG # switch active cart
306
+ psn cart create TAG [--name X] # create new persona
307
+ ```
308
+
309
+ CLI formatting uses pastel + tty-table + tty-spinner for human-readable output.
310
+
311
+ ## MCP Layer
312
+
313
+ JSON-RPC stdio server. Each handler: parse tool input → call service → return JSON.
314
+
315
+ ### Transport
316
+
317
+ - `exe/psn-mcp` — standalone MCP server binary (stdio transport)
318
+ - Also launchable via `psn mcp` CLI subcommand
319
+ - JSON-RPC 2.0 over stdin/stdout
320
+ - Implements MCP protocol: `initialize`, `tools/list`, `tools/call`
321
+
322
+ ### Tool Definitions
323
+
324
+ MCP tools mirror CLI commands 1:1. Tool names use `/` namespacing.
325
+
326
+ ```json
327
+ // memory_handler.rb tools
328
+ {"name": "memory/store", "inputSchema": {"subject": "string", "content": "string", "metadata": "object?"}}
329
+ {"name": "memory/recall", "inputSchema": {"query": "string", "limit": "integer?", "subject": "string?"}}
330
+ {"name": "memory/search", "inputSchema": {"subject": "string?", "limit": "integer?"}}
331
+ {"name": "memory/forget", "inputSchema": {"id": "integer"}}
332
+ {"name": "memory/list", "inputSchema": {}}
333
+
334
+ // index_handler.rb tools
335
+ {"name": "index/code", "inputSchema": {"path": "string", "project": "string?", "extensions": "string[]?"}}
336
+ {"name": "index/docs", "inputSchema": {"path": "string", "project": "string?"}}
337
+ {"name": "index/search", "inputSchema": {"query": "string", "type": "string?", "project": "string?", "limit": "integer?"}}
338
+ {"name": "index/status", "inputSchema": {"project": "string?"}}
339
+ {"name": "index/clear", "inputSchema": {"project": "string?", "type": "string?"}}
340
+
341
+ // cart_handler.rb tools
342
+ {"name": "cart/list", "inputSchema": {}}
343
+ {"name": "cart/use", "inputSchema": {"tag": "string"}}
344
+ {"name": "cart/create", "inputSchema": {"tag": "string", "name": "string?", "type": "string?"}}
345
+ ```
346
+
347
+ ### MCP Resources (read-only data exposed to clients)
348
+
349
+ ```
350
+ memory://subjects — all memory subjects with counts
351
+ memory://stats — total memories, subjects, date range
352
+ memory://recent — last 10 memories
353
+ memory://subject/{subject} — all memories for a subject
354
+ ```
355
+
356
+ ### Handler Pattern
357
+
358
+ Each handler follows the same pattern:
359
+
360
+ ```ruby
361
+ # lib/personality/mcp/memory_handler.rb
362
+ module Personality
363
+ module MCP
364
+ class MemoryHandler
365
+ def tools
366
+ # Return array of tool definition hashes
367
+ end
368
+
369
+ def call(name, arguments)
370
+ case name
371
+ when "memory/store"
372
+ Memory.new.store(**arguments.slice(:subject, :content, :metadata))
373
+ when "memory/recall"
374
+ Memory.new.recall(**arguments.slice(:query, :limit, :subject))
375
+ # ...
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
381
+ ```
382
+
383
+ ### Configuration for Claude Code
384
+
385
+ The MCP server is registered in `.mcp.json` or `settings.json`:
386
+
387
+ ```json
388
+ {
389
+ "mcpServers": {
390
+ "personality": {
391
+ "command": "psn-mcp",
392
+ "args": [],
393
+ "env": {
394
+ "PERSONALITY_CART": "bt7274"
395
+ }
396
+ }
397
+ }
398
+ }
399
+ ```
400
+
401
+ ## Executables
402
+
403
+ ```
404
+ exe/
405
+ psn # (existing) CLI entry point
406
+ psn-mcp # MCP server entry point (stdio)
407
+ ```
408
+
409
+ `psn-mcp` is a thin script:
410
+
411
+ ```ruby
412
+ #!/usr/bin/env ruby
413
+ require "personality"
414
+ Personality::MCP::Server.run
415
+ ```
416
+
417
+ ## Claude Code Hooks
418
+
419
+ Hooks are CLI commands that Claude Code invokes at lifecycle events. Each reads
420
+ JSON from stdin, performs side effects, and optionally prints JSON to stdout.
421
+
422
+ ### Hook Configuration (`hooks.json`)
423
+
424
+ Generated by `psn init` or `psn hooks install`. Registers `psn hooks <event>`
425
+ as the command for each Claude Code hook event.
426
+
427
+ ```json
428
+ {
429
+ "hooks": {
430
+ "PreToolUse": [{"hooks": [{"type": "command", "command": "psn hooks pre-tool-use", "timeout": 5000}]}],
431
+ "PostToolUse": [
432
+ {"matcher": "Read", "hooks": [{"type": "command", "command": "psn context track-read", "timeout": 5000}]},
433
+ {"matcher": "Write|Edit", "hooks": [{"type": "command", "command": "psn index hook", "timeout": 30000}]}
434
+ ],
435
+ "Stop": [{"hooks": [
436
+ {"type": "command", "command": "psn tts mark-natural-stop", "timeout": 1000},
437
+ {"type": "command", "command": "psn memory save", "timeout": 5000}
438
+ ]}],
439
+ "SubagentStop": [{"hooks": [{"type": "command", "command": "psn hooks subagent-stop", "timeout": 5000}]}],
440
+ "SessionStart": [{"hooks": [{"type": "command", "command": "psn hooks session-start", "timeout": 5000}]}],
441
+ "SessionEnd": [{"hooks": [
442
+ {"type": "command", "command": "psn hooks session-end", "timeout": 5000},
443
+ {"type": "command", "command": "psn tts stop", "timeout": 1000}
444
+ ]}],
445
+ "UserPromptSubmit": [{"hooks": [
446
+ {"type": "command", "command": "psn hooks user-prompt-submit", "timeout": 5000},
447
+ {"type": "command", "command": "psn tts interrupt-check", "timeout": 1000}
448
+ ]}],
449
+ "PreCompact": [{"hooks": [{"type": "command", "command": "psn memory save", "timeout": 5000}]}],
450
+ "Notification": [{"hooks": [{"type": "command", "command": "psn hooks notification", "timeout": 5000}]}]
451
+ }
452
+ }
453
+ ```
454
+
455
+ ### Hook Service: `hooks.rb`
456
+
457
+ Service object for hook logic. All hooks log to `~/.config/psn/hooks.jsonl`.
458
+
459
+ - `log(event, data)` — append JSONL entry with timestamp, session ID, truncated fields
460
+ - Configurable field truncation via `~/.config/psn/logging.toml`
461
+ - Preserves path fields (file_path, cwd, etc.) from truncation
462
+
463
+ ### Hook CLI: `cli/hooks.rb`
464
+
465
+ ```
466
+ psn hooks pre-tool-use # Log + allow (gate hook, can block)
467
+ psn hooks post-tool-use # Log only
468
+ psn hooks stop # Log only
469
+ psn hooks subagent-stop # Log only
470
+ psn hooks session-start # Log + output persona instructions + intro prompt
471
+ psn hooks session-end # Log only
472
+ psn hooks user-prompt-submit # Log + allow (gate hook, can block/modify)
473
+ psn hooks pre-compact # Log only
474
+ psn hooks notification # Log + speak via TTS
475
+ psn hooks install # Generate hooks.json in project/global settings
476
+ ```
477
+
478
+ ### Context Tracking: `context.rb` + `cli/context.rb`
479
+
480
+ Tracks which files Claude has read during a session (for require-read validation).
481
+
482
+ - Session-scoped file tracking in `/tmp/psn-context/{session_id}.json`
483
+ - Uses `CLAUDE_SESSION_ID` env var for session isolation
484
+
485
+ ```
486
+ psn context track-read # PostToolUse hook: record file read (stdin JSON)
487
+ psn context check FILE # Check if file is in session context
488
+ psn context list # List all files in current session context
489
+ psn context clear # Clear session context
490
+ ```
491
+
492
+ ### TTS Hooks: `tts.rb` + `cli/tts.rb`
493
+
494
+ Text-to-speech with piper, integrated into Claude Code lifecycle.
495
+
496
+ **Hook commands (called by hooks.json):**
497
+ ```
498
+ psn tts mark-natural-stop # Stop hook: set flag (agent completed naturally)
499
+ psn tts interrupt-check # UserPromptSubmit: kill TTS if user interrupted
500
+ psn tts stop # SessionEnd: kill any playing TTS
501
+ ```
502
+
503
+ **User-facing commands:**
504
+ ```
505
+ psn tts speak TEXT [--voice V] # Speak text with active persona's voice
506
+ psn tts voices # List installed voice models
507
+ psn tts download VOICE # Download piper voice from HuggingFace
508
+ psn tts test [--voice V] # Test voice with sample text
509
+ psn tts current # Show active persona's voice config
510
+ psn tts characters # List character voice models
511
+ ```
512
+
513
+ **TTS interrupt protocol:**
514
+ - Stop hook sets `data/tts_natural_stop` flag file
515
+ - UserPromptSubmit checks flag: present = natural stop (TTS continues),
516
+ absent = user interrupted (TTS killed)
517
+ - Works because Stop hooks only fire on natural completion, not user ESC/Ctrl+C
518
+
519
+ **Voice resolution:**
520
+ 1. Check `voices/` directory (character voices: BT7274, etc.)
521
+ 2. Check `~/.local/share/psn/voices/` (downloaded piper voices)
522
+ 3. Fall back to active cart's configured voice or `en_US-lessac-medium`
523
+
524
+ ### Memory Save Hook: `memory.rb`
525
+
526
+ Called on Stop and PreCompact events.
527
+
528
+ ```
529
+ psn memory save # Extract learnings from transcript, store to DB
530
+ psn memory hook-precompact # Deduplicate near-identical memories (similarity > 0.95)
531
+ ```
532
+
533
+ - Reads `transcript_path` from stdin JSON
534
+ - Extracts learnings from conversation transcript
535
+ - Stores each learning with subject, content, metadata, embedding
536
+ - PreCompact dedup finds pairs with >0.95 similarity and merges
537
+
538
+ ### Index Hook: `indexer.rb`
539
+
540
+ Called on PostToolUse for Write|Edit events.
541
+
542
+ ```
543
+ psn index hook # Re-index the written/edited file immediately
544
+ ```
545
+
546
+ - Reads `tool_input.file_path` and `cwd` from stdin JSON
547
+ - Skips non-code/non-doc extensions
548
+ - Generates embedding and upserts into code_chunks/doc_chunks + vec tables
549
+ - Project name derived from cwd directory name
550
+
551
+ ### Module Architecture (updated)
552
+
553
+ ```
554
+ lib/personality/
555
+ # Core
556
+ version.rb # (existing)
557
+ db.rb # Database connection + schema + migrations
558
+ embedding.rb # Ollama HTTP client for embeddings
559
+ chunker.rb # Text chunking (2000 chars, 200 overlap)
560
+
561
+ # Service objects (all logic lives here)
562
+ memory.rb # Store/recall/search/forget/save (cart-scoped)
563
+ indexer.rb # Code + doc indexing with semantic search
564
+ cart.rb # Persona/cart management
565
+ hooks.rb # Hook logging + event processing
566
+ context.rb # Session file-read tracking
567
+ tts.rb # TTS synthesis + playback + interrupt protocol
568
+
569
+ # CLI layer (thin Thor wrappers)
570
+ cli.rb # Root CLI + init (existing)
571
+ cli/
572
+ memory.rb # psn memory subcommands
573
+ index.rb # psn index subcommands
574
+ cart.rb # psn cart subcommands
575
+ hooks.rb # psn hooks subcommands
576
+ context.rb # psn context subcommands
577
+ tts.rb # psn tts subcommands
578
+
579
+ # MCP layer (thin JSON-RPC handlers)
580
+ mcp/
581
+ server.rb # MCP server bootstrap + stdio transport
582
+ memory_handler.rb # memory/* tool definitions + dispatch
583
+ index_handler.rb # index/* tool definitions + dispatch
584
+ cart_handler.rb # cart/* tool definitions + dispatch
585
+
586
+ # Init (existing)
587
+ init.rb
588
+ ```
589
+
590
+ ## Implementation Order
591
+
592
+ 1. `db.rb` + update `init.rb` schema — foundation
593
+ 2. `embedding.rb` — needed by everything else
594
+ 3. `chunker.rb` — simple, no deps
595
+ 4. `hooks.rb` service + `cli/hooks.rb` — logging backbone for all hooks
596
+ 5. `context.rb` service + `cli/context.rb` — file-read tracking
597
+ 6. `cart.rb` service + `cli/cart.rb` + `mcp/cart_handler.rb`
598
+ 7. `memory.rb` service + `cli/memory.rb` + `mcp/memory_handler.rb` (incl. save hook)
599
+ 8. `tts.rb` service + `cli/tts.rb` — TTS + interrupt protocol
600
+ 9. `indexer.rb` service + `cli/index.rb` + `mcp/index_handler.rb` (incl. index hook)
601
+ 10. `mcp/server.rb` — MCP transport + tool routing
602
+ 11. `exe/psn-mcp` — MCP binary
603
+ 12. `psn hooks install` — generate hooks.json
604
+ 13. Tests for each layer (service, CLI, MCP, hooks)
605
+
606
+ ## Key Differences from psn (Python)
607
+
608
+ | Aspect | psn (Python/PostgreSQL) | personality (Ruby/SQLite) |
609
+ |--------|------------------------|--------------------------|
610
+ | Vector storage | pgvector column type | vec0 virtual table (separate) |
611
+ | Vector search | `<=>` cosine operator | `MATCH` + `k` parameter |
612
+ | Similarity score | `1 - (a <=> b)` | `distance` (lower = closer) |
613
+ | IDs | UUID strings | INTEGER autoincrement |
614
+ | Metadata | JSONB column | TEXT (JSON string) |
615
+ | Connection | psycopg + config | sqlite3 gem, single file |
616
+ | Embedding | urllib (raw HTTP) | net/http (raw HTTP) |
617
+ | Architecture | 3 separate MCP servers | 1 gem: shared services, CLI + single MCP server |
618
+ | Interface | MCP only | CLI + MCP + hooks (same service objects) |
619
+ | Hooks | Python scripts + Typer CLI | Ruby service objects + Thor CLI |
620
+ | TTS | piper via Python import | piper via CLI subprocess |
621
+ | Context tracking | /tmp file per session | /tmp file per session (same pattern) |
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Personality
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/personality`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/personality.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]