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/TODO.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## Phase 1: Foundation
|
|
4
|
+
|
|
5
|
+
- [x] `db.rb` — singleton connection, sqlite-vec loading, migration runner
|
|
6
|
+
- [x] Schema v2 — carts, memories, code_chunks, doc_chunks, vec0 virtual tables
|
|
7
|
+
- [x] Update `init.rb` to use `db.rb` for schema creation (remove inline SQL)
|
|
8
|
+
- [x] `embedding.rb` — Ollama HTTP client, `generate(text)`, 8000 char truncation
|
|
9
|
+
- [x] `chunker.rb` — overlapping window splitter (2000/200)
|
|
10
|
+
- [x] Tests for db, embedding, chunker
|
|
11
|
+
|
|
12
|
+
## Phase 2: Hooks & Context
|
|
13
|
+
|
|
14
|
+
- [x] `hooks.rb` service — JSONL logging, field truncation, config via `logging.toml`
|
|
15
|
+
- [x] `cli/hooks.rb` — all 9 hook event subcommands (pre-tool-use, post-tool-use, stop, subagent-stop, session-start, session-end, user-prompt-submit, pre-compact, notification)
|
|
16
|
+
- [x] `psn hooks install` — generate `hooks.json` for Claude Code settings
|
|
17
|
+
- [x] `context.rb` service — session file-read tracking (`/tmp/psn-context/`)
|
|
18
|
+
- [x] `cli/context.rb` — track-read, check, list, clear subcommands
|
|
19
|
+
- [x] Tests for hooks and context
|
|
20
|
+
|
|
21
|
+
## Phase 3: Cart & Memory
|
|
22
|
+
|
|
23
|
+
- [x] `cart.rb` service — find_or_create, active, list, use, create
|
|
24
|
+
- [x] `cli/cart.rb` — list, use, create subcommands
|
|
25
|
+
- [x] `memory.rb` service — store, recall, search, forget, list (cart-scoped)
|
|
26
|
+
- [x] `memory.rb` save hook — extract learnings from transcript (stub endpoint, ready for future impl)
|
|
27
|
+
- [ ] `memory.rb` precompact hook — deduplicate memories (>0.95 similarity) [deferred: needs real usage data]
|
|
28
|
+
- [x] `cli/memory.rb` — store, recall, search, forget, list, save subcommands
|
|
29
|
+
- [x] Tests for cart and memory
|
|
30
|
+
|
|
31
|
+
## Phase 4: TTS
|
|
32
|
+
|
|
33
|
+
- [x] `tts.rb` service — piper synthesis, playback, PID tracking, voice resolution
|
|
34
|
+
- [x] TTS interrupt protocol — natural stop flag, interrupt-check logic
|
|
35
|
+
- [x] `cli/tts.rb` — speak, stop, mark-natural-stop, interrupt-check, voices, download, test, current
|
|
36
|
+
- [x] Voice download from HuggingFace (piper-voices repo)
|
|
37
|
+
- [x] Tests for TTS service
|
|
38
|
+
|
|
39
|
+
## Phase 5: Indexer
|
|
40
|
+
|
|
41
|
+
- [x] `indexer.rb` service — index_code, index_docs, search, status, clear, index_single_file
|
|
42
|
+
- [x] `indexer.rb` hook — re-index on Write/Edit (PostToolUse)
|
|
43
|
+
- [x] `cli/index.rb` — code, docs, search, status, clear, hook subcommands
|
|
44
|
+
- [x] Tests for indexer
|
|
45
|
+
|
|
46
|
+
## Phase 6: MCP Server
|
|
47
|
+
|
|
48
|
+
- [x] `mcp/server.rb` — official `mcp` gem (0.9.1), stdio transport, define_tool API
|
|
49
|
+
- [x] Memory tools — memory.store, memory.recall, memory.search, memory.forget, memory.list
|
|
50
|
+
- [x] Index tools — index.code, index.docs, index.search, index.status, index.clear
|
|
51
|
+
- [x] Cart tools — cart.list, cart.use, cart.create
|
|
52
|
+
- [x] MCP resources — memory://subjects, memory://stats, memory://recent
|
|
53
|
+
- [x] `exe/psn-mcp` — standalone MCP binary
|
|
54
|
+
- [ ] `.mcp.json` template generation [deferred: trivial, can add to hooks install]
|
|
55
|
+
- [x] Tests for MCP server (15 specs)
|
|
56
|
+
|
|
57
|
+
## Phase 7: Integration & Polish
|
|
58
|
+
|
|
59
|
+
- [x] `psn hooks session-start` — load persona instructions + intro prompt
|
|
60
|
+
- [x] `psn hooks notification` — speak notifications via TTS
|
|
61
|
+
- [x] End-to-end test: init → store memory → recall → verify
|
|
62
|
+
- [x] End-to-end test: index code → search → verify results
|
|
63
|
+
- [x] End-to-end test: MCP server protocol → tools/call → resources/read
|
|
64
|
+
- [ ] CLI help text and `--help` output review
|
|
65
|
+
- [ ] README update with usage examples
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: https://github.com/modelcontextprotocol/ruby-sdk
|
|
3
|
+
fetched: 2026-03-26
|
|
4
|
+
gem: mcp (0.9.1)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MCP Ruby SDK
|
|
8
|
+
|
|
9
|
+
The official Ruby SDK for Model Context Protocol servers and clients.
|
|
10
|
+
|
|
11
|
+
## Building an MCP Server
|
|
12
|
+
|
|
13
|
+
The `MCP::Server` class handles JSON-RPC requests and responses implementing the MCP specification.
|
|
14
|
+
|
|
15
|
+
### Key Features
|
|
16
|
+
|
|
17
|
+
- JSON-RPC 2.0 message handling
|
|
18
|
+
- Protocol initialization and capability negotiation
|
|
19
|
+
- Tool registration and invocation
|
|
20
|
+
- Prompt registration and execution
|
|
21
|
+
- Resource registration and retrieval
|
|
22
|
+
- Stdio & Streamable HTTP transports
|
|
23
|
+
- Notifications for list changes (tools, prompts, resources)
|
|
24
|
+
|
|
25
|
+
### Supported Methods
|
|
26
|
+
|
|
27
|
+
- `initialize` - Protocol init, returns server capabilities
|
|
28
|
+
- `ping` - Health check
|
|
29
|
+
- `tools/list` - Lists registered tools and schemas
|
|
30
|
+
- `tools/call` - Invokes a tool with arguments
|
|
31
|
+
- `prompts/list` - Lists registered prompts
|
|
32
|
+
- `prompts/get` - Retrieves a prompt by name
|
|
33
|
+
- `resources/list` - Lists registered resources
|
|
34
|
+
- `resources/read` - Retrieves a resource by URI
|
|
35
|
+
- `resources/templates/list` - Lists resource templates
|
|
36
|
+
|
|
37
|
+
## Defining Tools
|
|
38
|
+
|
|
39
|
+
Three ways to define tools:
|
|
40
|
+
|
|
41
|
+
### 1. Class definition
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class MyTool < MCP::Tool
|
|
45
|
+
tool_name "my_tool"
|
|
46
|
+
description "Does something"
|
|
47
|
+
input_schema(
|
|
48
|
+
properties: {
|
|
49
|
+
message: { type: "string" },
|
|
50
|
+
},
|
|
51
|
+
required: ["message"]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def self.call(message:, server_context:)
|
|
55
|
+
MCP::Tool::Response.new([{ type: "text", text: "OK" }])
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Tool.define
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
tool = MCP::Tool.define(
|
|
64
|
+
name: "my_tool",
|
|
65
|
+
description: "Does something",
|
|
66
|
+
) do |args, server_context:|
|
|
67
|
+
MCP::Tool::Response.new([{ type: "text", text: "OK" }])
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Server#define_tool
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
server.define_tool(
|
|
75
|
+
name: "my_tool",
|
|
76
|
+
description: "Does something",
|
|
77
|
+
input_schema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: { msg: { type: "string" } },
|
|
80
|
+
required: ["msg"]
|
|
81
|
+
}
|
|
82
|
+
) do |msg:, server_context:|
|
|
83
|
+
MCP::Tool::Response.new([{ type: "text", text: msg }])
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Important:** When using `define_tool`, arguments are passed as **keyword args** (splatted from the arguments hash). The `server_context:` keyword is always passed.
|
|
88
|
+
|
|
89
|
+
### Tool Names
|
|
90
|
+
|
|
91
|
+
Tool names only allow: `A-Z`, `a-z`, `0-9`, `_`, `-`, `.`
|
|
92
|
+
|
|
93
|
+
**No `/` allowed.** Use dots for namespacing: `memory.store`, `index.search`.
|
|
94
|
+
|
|
95
|
+
### Tool Responses
|
|
96
|
+
|
|
97
|
+
Tools must return `MCP::Tool::Response`:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
MCP::Tool::Response.new([{ type: "text", text: "result" }])
|
|
101
|
+
MCP::Tool::Response.new([{ type: "text", text: "error" }], error: true)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Resources
|
|
105
|
+
|
|
106
|
+
Register resources with the server:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
resource = MCP::Resource.new(
|
|
110
|
+
uri: "memory://subjects",
|
|
111
|
+
name: "memory-subjects",
|
|
112
|
+
description: "All subjects",
|
|
113
|
+
mime_type: "application/json",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
server = MCP::Server.new(name: "my_server", resources: [resource])
|
|
117
|
+
# or: server.resources = [resource]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Handle reads:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
server.resources_read_handler do |params|
|
|
124
|
+
[{
|
|
125
|
+
uri: params[:uri],
|
|
126
|
+
mimeType: "application/json",
|
|
127
|
+
text: JSON.generate({ data: "value" })
|
|
128
|
+
}]
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Server Handle API
|
|
133
|
+
|
|
134
|
+
Two methods for processing requests:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Hash in, Hash out (symbol keys)
|
|
138
|
+
response = server.handle({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} })
|
|
139
|
+
# => { jsonrpc: "2.0", id: 1, result: { tools: [...] } }
|
|
140
|
+
|
|
141
|
+
# JSON string in, JSON string out
|
|
142
|
+
response = server.handle_json('{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}')
|
|
143
|
+
# => '{"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Must call `initialize` before other methods** — the server requires protocol handshake first.
|
|
147
|
+
|
|
148
|
+
## Stdio Transport
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
server = MCP::Server.new(name: "my_server", version: "1.0")
|
|
152
|
+
# ... define tools, resources ...
|
|
153
|
+
|
|
154
|
+
transport = MCP::Transports::StdioTransport.new(server)
|
|
155
|
+
transport.open # Blocks, reads stdin, writes stdout
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The transport class is at `MCP::Server::Transports::StdioTransport` (require `mcp/transports/stdio`).
|
|
159
|
+
|
|
160
|
+
## Client (for connecting to MCP servers)
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
stdio_transport = MCP::Client::Stdio.new(
|
|
164
|
+
command: "bundle",
|
|
165
|
+
args: ["exec", "ruby", "path/to/server.rb"],
|
|
166
|
+
env: { "API_KEY" => "secret" },
|
|
167
|
+
read_timeout: 30
|
|
168
|
+
)
|
|
169
|
+
client = MCP::Client.new(transport: stdio_transport)
|
|
170
|
+
|
|
171
|
+
tools = client.tools
|
|
172
|
+
response = client.call_tool(tool: tools.first, arguments: { message: "Hello" })
|
|
173
|
+
stdio_transport.close
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Notifications
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
server.notify_tools_list_changed
|
|
180
|
+
server.notify_resources_list_changed
|
|
181
|
+
server.notify_log_message(data: { message: "Hello" }, level: "info")
|
|
182
|
+
server.notify_progress(progress_token: "token", progress: 50, total: 100)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Configuration
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# Set server context (passed to tool blocks)
|
|
189
|
+
server.server_context = { user_id: 123 }
|
|
190
|
+
|
|
191
|
+
# Protocol version
|
|
192
|
+
server.configuration.protocol_version # "2024-11-05"
|
|
193
|
+
```
|
data/exe/psn
ADDED
data/exe/psn-mcp
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "db"
|
|
4
|
+
|
|
5
|
+
module Personality
|
|
6
|
+
class Cart
|
|
7
|
+
DEFAULT_TAG = "default"
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def find_or_create(tag)
|
|
11
|
+
db = DB.connection
|
|
12
|
+
row = db.execute("SELECT * FROM carts WHERE tag = ?", [tag]).first
|
|
13
|
+
|
|
14
|
+
if row
|
|
15
|
+
row_to_hash(row)
|
|
16
|
+
else
|
|
17
|
+
db.execute(
|
|
18
|
+
"INSERT INTO carts (tag) VALUES (?)", [tag]
|
|
19
|
+
)
|
|
20
|
+
id = db.last_insert_row_id
|
|
21
|
+
{id: id, tag: tag}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def active
|
|
26
|
+
tag = ENV.fetch("PERSONALITY_CART", DEFAULT_TAG)
|
|
27
|
+
find_or_create(tag)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def list
|
|
31
|
+
db = DB.connection
|
|
32
|
+
db.execute("SELECT * FROM carts ORDER BY tag").map { |row| row_to_hash(row) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def use(tag)
|
|
36
|
+
find_or_create(tag)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create(tag, name: nil, type: nil, tagline: nil)
|
|
40
|
+
db = DB.connection
|
|
41
|
+
existing = db.execute("SELECT id FROM carts WHERE tag = ?", [tag]).first
|
|
42
|
+
return find_or_create(tag) if existing
|
|
43
|
+
|
|
44
|
+
db.execute(
|
|
45
|
+
"INSERT INTO carts (tag, name, type, tagline) VALUES (?, ?, ?, ?)",
|
|
46
|
+
[tag, name, type, tagline]
|
|
47
|
+
)
|
|
48
|
+
id = db.last_insert_row_id
|
|
49
|
+
{id: id, tag: tag, name: name, type: type, tagline: tagline}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find(tag)
|
|
53
|
+
db = DB.connection
|
|
54
|
+
row = db.execute("SELECT * FROM carts WHERE tag = ?", [tag]).first
|
|
55
|
+
row ? row_to_hash(row) : nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def row_to_hash(row)
|
|
61
|
+
{
|
|
62
|
+
id: row["id"],
|
|
63
|
+
tag: row["tag"],
|
|
64
|
+
version: row["version"],
|
|
65
|
+
name: row["name"],
|
|
66
|
+
type: row["type"],
|
|
67
|
+
tagline: row["tagline"],
|
|
68
|
+
source: row["source"],
|
|
69
|
+
created_at: row["created_at"],
|
|
70
|
+
updated_at: row["updated_at"]
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Personality
|
|
4
|
+
module Chunker
|
|
5
|
+
MIN_LENGTH = 10
|
|
6
|
+
DEFAULT_SIZE = 2000
|
|
7
|
+
DEFAULT_OVERLAP = 200
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def split(text, size: DEFAULT_SIZE, overlap: DEFAULT_OVERLAP)
|
|
11
|
+
return [] if text.nil? || text.length < MIN_LENGTH
|
|
12
|
+
|
|
13
|
+
return [text] if text.length <= size
|
|
14
|
+
|
|
15
|
+
chunks = []
|
|
16
|
+
start = 0
|
|
17
|
+
while start < text.length
|
|
18
|
+
chunk = text[start, size]
|
|
19
|
+
chunks << chunk
|
|
20
|
+
start += size - overlap
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
chunks
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Personality
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
class Cart < Thor
|
|
8
|
+
desc "list", "List all personas"
|
|
9
|
+
def list
|
|
10
|
+
require_relative "../cart"
|
|
11
|
+
require_relative "../db"
|
|
12
|
+
require "pastel"
|
|
13
|
+
require "tty-table"
|
|
14
|
+
|
|
15
|
+
DB.migrate!
|
|
16
|
+
pastel = Pastel.new
|
|
17
|
+
carts = Personality::Cart.list
|
|
18
|
+
|
|
19
|
+
if carts.empty?
|
|
20
|
+
puts pastel.dim("No personas found")
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
table = TTY::Table.new(
|
|
25
|
+
header: %w[ID Tag Name Type],
|
|
26
|
+
rows: carts.map { |c| [c[:id], c[:tag], c[:name] || "-", c[:type] || "-"] }
|
|
27
|
+
)
|
|
28
|
+
puts table.render(:unicode, padding: [0, 1])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "use TAG", "Switch active persona"
|
|
32
|
+
def use(tag)
|
|
33
|
+
require_relative "../cart"
|
|
34
|
+
require_relative "../db"
|
|
35
|
+
require "pastel"
|
|
36
|
+
|
|
37
|
+
DB.migrate!
|
|
38
|
+
cart = Personality::Cart.use(tag)
|
|
39
|
+
puts "#{Pastel.new.green("Active:")} #{cart[:tag]} (id: #{cart[:id]})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "create TAG", "Create a new persona"
|
|
43
|
+
option :name, type: :string, desc: "Display name"
|
|
44
|
+
option :type, type: :string, desc: "Persona type"
|
|
45
|
+
option :tagline, type: :string, desc: "Short description"
|
|
46
|
+
def create(tag)
|
|
47
|
+
require_relative "../cart"
|
|
48
|
+
require_relative "../db"
|
|
49
|
+
require "pastel"
|
|
50
|
+
|
|
51
|
+
DB.migrate!
|
|
52
|
+
cart = Personality::Cart.create(tag, name: options[:name], type: options[:type], tagline: options[:tagline])
|
|
53
|
+
puts "#{Pastel.new.green("Created:")} #{cart[:tag]} (id: #{cart[:id]})"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.exit_on_failure?
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Personality
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
class Context < Thor
|
|
8
|
+
desc "track-read", "Track a file read (PostToolUse hook, reads JSON from stdin)"
|
|
9
|
+
def track_read
|
|
10
|
+
require_relative "../context"
|
|
11
|
+
require_relative "../hooks"
|
|
12
|
+
|
|
13
|
+
data = Personality::Hooks.read_stdin_json
|
|
14
|
+
return unless data
|
|
15
|
+
|
|
16
|
+
file_path = data.dig("tool_input", "file_path")
|
|
17
|
+
return unless file_path
|
|
18
|
+
|
|
19
|
+
session_id = data["session_id"]
|
|
20
|
+
Personality::Context.track_read(file_path, session_id: session_id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "check FILE", "Check if a file is in session context"
|
|
24
|
+
def check(file_path)
|
|
25
|
+
require_relative "../context"
|
|
26
|
+
require "pastel"
|
|
27
|
+
|
|
28
|
+
pastel = Pastel.new
|
|
29
|
+
if Personality::Context.check(file_path)
|
|
30
|
+
puts "#{pastel.green("✓")} #{file_path} is in context"
|
|
31
|
+
else
|
|
32
|
+
puts "#{pastel.dim("✗")} #{file_path} not in context"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "list", "List all files in current session context"
|
|
38
|
+
def list
|
|
39
|
+
require_relative "../context"
|
|
40
|
+
require "pastel"
|
|
41
|
+
|
|
42
|
+
pastel = Pastel.new
|
|
43
|
+
files = Personality::Context.list
|
|
44
|
+
|
|
45
|
+
if files.empty?
|
|
46
|
+
puts pastel.dim("No files in context")
|
|
47
|
+
else
|
|
48
|
+
puts "#{pastel.bold("Files in context")} (#{files.length})"
|
|
49
|
+
files.each { |f| puts " #{f}" }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "clear", "Clear session context"
|
|
54
|
+
def clear
|
|
55
|
+
require_relative "../context"
|
|
56
|
+
require "pastel"
|
|
57
|
+
|
|
58
|
+
Personality::Context.clear
|
|
59
|
+
puts Pastel.new.green("Context cleared")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.exit_on_failure?
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Personality
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
class Hooks < Thor
|
|
8
|
+
desc "pre-tool-use", "PreToolUse hook — log and allow"
|
|
9
|
+
def pre_tool_use
|
|
10
|
+
require_relative "../hooks"
|
|
11
|
+
data = Personality::Hooks.read_stdin_json
|
|
12
|
+
Personality::Hooks.log("PreToolUse", data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "post-tool-use", "PostToolUse hook — log"
|
|
16
|
+
def post_tool_use
|
|
17
|
+
require_relative "../hooks"
|
|
18
|
+
data = Personality::Hooks.read_stdin_json
|
|
19
|
+
Personality::Hooks.log("PostToolUse", data)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "stop", "Stop hook — log"
|
|
23
|
+
def stop
|
|
24
|
+
require_relative "../hooks"
|
|
25
|
+
data = Personality::Hooks.read_stdin_json
|
|
26
|
+
Personality::Hooks.log("Stop", data)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "subagent-stop", "SubagentStop hook — log"
|
|
30
|
+
def subagent_stop
|
|
31
|
+
require_relative "../hooks"
|
|
32
|
+
data = Personality::Hooks.read_stdin_json
|
|
33
|
+
Personality::Hooks.log("SubagentStop", data)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "session-start", "SessionStart hook — log, load persona, output intro"
|
|
37
|
+
def session_start
|
|
38
|
+
require_relative "../hooks"
|
|
39
|
+
require_relative "../cart"
|
|
40
|
+
require_relative "../db"
|
|
41
|
+
|
|
42
|
+
data = Personality::Hooks.read_stdin_json
|
|
43
|
+
Personality::Hooks.log("SessionStart", data)
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
Personality::DB.migrate!
|
|
47
|
+
cart = Personality::Cart.active
|
|
48
|
+
|
|
49
|
+
if cart[:name] || cart[:tagline]
|
|
50
|
+
name = cart[:name] || cart[:tag]
|
|
51
|
+
puts "**Active Persona:** #{name}"
|
|
52
|
+
puts cart[:tagline] if cart[:tagline]
|
|
53
|
+
puts
|
|
54
|
+
end
|
|
55
|
+
rescue
|
|
56
|
+
# Silently continue if cart loading fails
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
desc "session-end", "SessionEnd hook — log"
|
|
61
|
+
def session_end
|
|
62
|
+
require_relative "../hooks"
|
|
63
|
+
data = Personality::Hooks.read_stdin_json
|
|
64
|
+
Personality::Hooks.log("SessionEnd", data)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "user-prompt-submit", "UserPromptSubmit hook — log and allow"
|
|
68
|
+
def user_prompt_submit
|
|
69
|
+
require_relative "../hooks"
|
|
70
|
+
data = Personality::Hooks.read_stdin_json
|
|
71
|
+
Personality::Hooks.log("UserPromptSubmit", data)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc "pre-compact", "PreCompact hook — log"
|
|
75
|
+
def pre_compact
|
|
76
|
+
require_relative "../hooks"
|
|
77
|
+
data = Personality::Hooks.read_stdin_json
|
|
78
|
+
Personality::Hooks.log("PreCompact", data)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
desc "notification", "Notification hook — log and speak via TTS"
|
|
82
|
+
def notification
|
|
83
|
+
require_relative "../hooks"
|
|
84
|
+
require_relative "../tts"
|
|
85
|
+
|
|
86
|
+
data = Personality::Hooks.read_stdin_json
|
|
87
|
+
Personality::Hooks.log("Notification", data)
|
|
88
|
+
|
|
89
|
+
return unless data
|
|
90
|
+
|
|
91
|
+
message = data["message"]
|
|
92
|
+
return if message.nil? || message.empty?
|
|
93
|
+
|
|
94
|
+
# Prepend project name for context
|
|
95
|
+
cwd = data["cwd"] || Dir.pwd
|
|
96
|
+
project = File.basename(cwd)
|
|
97
|
+
speech = "#{project}: #{message}"
|
|
98
|
+
|
|
99
|
+
Personality::TTS.stop_current
|
|
100
|
+
Personality::TTS.speak(speech)
|
|
101
|
+
rescue
|
|
102
|
+
# Silently continue if TTS fails
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
desc "install", "Generate hooks.json for Claude Code"
|
|
106
|
+
option :output, type: :string, aliases: "-o", default: "hooks.json",
|
|
107
|
+
desc: "Output file path"
|
|
108
|
+
def install
|
|
109
|
+
require_relative "../hooks"
|
|
110
|
+
output = options[:output]
|
|
111
|
+
File.write(output, Personality::Hooks.generate_hooks_json)
|
|
112
|
+
puts "Generated #{output}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.exit_on_failure?
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|