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.
- checksums.yaml +7 -0
- data/CLAUDE.md +88 -0
- data/PLAN.md +621 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/TODO.md +65 -0
- data/docs/mcp-ruby-sdk.md +193 -0
- data/exe/psn +6 -0
- data/exe/psn-mcp +7 -0
- data/lib/personality/cart.rb +75 -0
- data/lib/personality/chunker.rb +27 -0
- data/lib/personality/cli/cart.rb +61 -0
- data/lib/personality/cli/context.rb +67 -0
- data/lib/personality/cli/hooks.rb +120 -0
- data/lib/personality/cli/index.rb +147 -0
- data/lib/personality/cli/memory.rb +130 -0
- data/lib/personality/cli/tts.rb +140 -0
- data/lib/personality/cli.rb +54 -0
- data/lib/personality/context.rb +73 -0
- data/lib/personality/db.rb +148 -0
- data/lib/personality/embedding.rb +44 -0
- data/lib/personality/hooks.rb +143 -0
- data/lib/personality/indexer.rb +211 -0
- data/lib/personality/init.rb +257 -0
- data/lib/personality/mcp/server.rb +314 -0
- data/lib/personality/memory.rb +125 -0
- data/lib/personality/tts.rb +191 -0
- data/lib/personality/version.rb +5 -0
- data/lib/personality.rb +17 -0
- metadata +269 -0
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.
|