personality 0.1.4 → 0.1.5
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 +4 -4
- data/.claude/settings.local.json +9 -0
- data/README.md +92 -13
- data/exe/psn-http +28 -0
- data/exe/psn-mcp +15 -1
- data/exe/psn-voice +7 -0
- data/lib/personality/cart.rb +12 -1
- data/lib/personality/cart_manager.rb +1 -1
- data/lib/personality/cli/index.rb +50 -14
- data/lib/personality/cli/tts.rb +27 -2
- data/lib/personality/indexer.rb +27 -9
- data/lib/personality/mcp/oauth.rb +238 -0
- data/lib/personality/mcp/rack_app.rb +155 -0
- data/lib/personality/mcp/server.rb +183 -30
- data/lib/personality/mcp/tts_server.rb +11 -4
- data/lib/personality/mcp/voice_server.rb +412 -0
- data/lib/personality/tts.rb +168 -35
- data/lib/personality/version.rb +1 -1
- metadata +51 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "mcp"
|
|
4
|
-
require "mcp/transports/
|
|
4
|
+
require "mcp/server/transports/stdio_transport"
|
|
5
5
|
require "json"
|
|
6
6
|
require_relative "../db"
|
|
7
7
|
require_relative "../memory"
|
|
@@ -11,23 +11,36 @@ require_relative "../cart"
|
|
|
11
11
|
module Personality
|
|
12
12
|
module MCP
|
|
13
13
|
class Server
|
|
14
|
-
|
|
14
|
+
# Modes:
|
|
15
|
+
# :all - All tools (default)
|
|
16
|
+
# :indexer - Only index_* tools (for local file indexing)
|
|
17
|
+
# :core - Only memory/cart tools (for remote/centralized)
|
|
18
|
+
VALID_MODES = %i[all indexer core].freeze
|
|
19
|
+
|
|
20
|
+
def self.run(mode: :all)
|
|
15
21
|
DB.migrate!
|
|
16
|
-
new.start
|
|
22
|
+
new(mode: mode).start
|
|
17
23
|
end
|
|
18
24
|
|
|
19
|
-
def initialize
|
|
25
|
+
def initialize(mode: :all)
|
|
26
|
+
@mode = VALID_MODES.include?(mode) ? mode : :all
|
|
27
|
+
server_name = case @mode
|
|
28
|
+
when :indexer then "indexer"
|
|
29
|
+
when :core then "core"
|
|
30
|
+
else "personality"
|
|
31
|
+
end
|
|
32
|
+
|
|
20
33
|
@server = ::MCP::Server.new(
|
|
21
|
-
name:
|
|
34
|
+
name: server_name,
|
|
22
35
|
version: Personality::VERSION
|
|
23
36
|
)
|
|
24
37
|
@server.server_context = {}
|
|
25
38
|
register_tools
|
|
26
|
-
register_resources
|
|
39
|
+
register_resources unless @mode == :indexer
|
|
27
40
|
end
|
|
28
41
|
|
|
29
42
|
def start
|
|
30
|
-
transport = ::MCP::Transports::StdioTransport.new(@server)
|
|
43
|
+
transport = ::MCP::Server::Transports::StdioTransport.new(@server)
|
|
31
44
|
transport.open
|
|
32
45
|
end
|
|
33
46
|
|
|
@@ -41,7 +54,7 @@ module Personality
|
|
|
41
54
|
|
|
42
55
|
def register_memory_tools
|
|
43
56
|
@server.define_tool(
|
|
44
|
-
name: "
|
|
57
|
+
name: "memory_store",
|
|
45
58
|
description: "Store a memory with subject and content. Automatically generates embedding.",
|
|
46
59
|
input_schema: {
|
|
47
60
|
type: "object",
|
|
@@ -58,7 +71,7 @@ module Personality
|
|
|
58
71
|
end
|
|
59
72
|
|
|
60
73
|
@server.define_tool(
|
|
61
|
-
name: "
|
|
74
|
+
name: "memory_recall",
|
|
62
75
|
description: "Recall memories by semantic similarity to a query.",
|
|
63
76
|
input_schema: {
|
|
64
77
|
type: "object",
|
|
@@ -75,7 +88,7 @@ module Personality
|
|
|
75
88
|
end
|
|
76
89
|
|
|
77
90
|
@server.define_tool(
|
|
78
|
-
name: "
|
|
91
|
+
name: "memory_search",
|
|
79
92
|
description: "Search memories by subject or metadata.",
|
|
80
93
|
input_schema: {
|
|
81
94
|
type: "object",
|
|
@@ -90,7 +103,7 @@ module Personality
|
|
|
90
103
|
end
|
|
91
104
|
|
|
92
105
|
@server.define_tool(
|
|
93
|
-
name: "
|
|
106
|
+
name: "memory_forget",
|
|
94
107
|
description: "Delete a memory by ID.",
|
|
95
108
|
input_schema: {
|
|
96
109
|
type: "object",
|
|
@@ -105,7 +118,7 @@ module Personality
|
|
|
105
118
|
end
|
|
106
119
|
|
|
107
120
|
@server.define_tool(
|
|
108
|
-
name: "
|
|
121
|
+
name: "memory_list",
|
|
109
122
|
description: "List all memory subjects and counts.",
|
|
110
123
|
input_schema: {type: "object", properties: {}}
|
|
111
124
|
) do |server_context:, **|
|
|
@@ -118,7 +131,7 @@ module Personality
|
|
|
118
131
|
|
|
119
132
|
def register_index_tools
|
|
120
133
|
@server.define_tool(
|
|
121
|
-
name: "
|
|
134
|
+
name: "index_code",
|
|
122
135
|
description: "Index code files in a directory for semantic search.",
|
|
123
136
|
input_schema: {
|
|
124
137
|
type: "object",
|
|
@@ -135,7 +148,7 @@ module Personality
|
|
|
135
148
|
end
|
|
136
149
|
|
|
137
150
|
@server.define_tool(
|
|
138
|
-
name: "
|
|
151
|
+
name: "index_docs",
|
|
139
152
|
description: "Index documentation files for semantic search.",
|
|
140
153
|
input_schema: {
|
|
141
154
|
type: "object",
|
|
@@ -151,7 +164,7 @@ module Personality
|
|
|
151
164
|
end
|
|
152
165
|
|
|
153
166
|
@server.define_tool(
|
|
154
|
-
name: "
|
|
167
|
+
name: "index_search",
|
|
155
168
|
description: "Search indexed code and docs by semantic similarity.",
|
|
156
169
|
input_schema: {
|
|
157
170
|
type: "object",
|
|
@@ -174,7 +187,7 @@ module Personality
|
|
|
174
187
|
end
|
|
175
188
|
|
|
176
189
|
@server.define_tool(
|
|
177
|
-
name: "
|
|
190
|
+
name: "index_status",
|
|
178
191
|
description: "Show indexing status and statistics.",
|
|
179
192
|
input_schema: {
|
|
180
193
|
type: "object",
|
|
@@ -188,7 +201,7 @@ module Personality
|
|
|
188
201
|
end
|
|
189
202
|
|
|
190
203
|
@server.define_tool(
|
|
191
|
-
name: "
|
|
204
|
+
name: "index_clear",
|
|
192
205
|
description: "Clear index for a project or all.",
|
|
193
206
|
input_schema: {
|
|
194
207
|
type: "object",
|
|
@@ -207,7 +220,7 @@ module Personality
|
|
|
207
220
|
|
|
208
221
|
def register_cart_tools
|
|
209
222
|
@server.define_tool(
|
|
210
|
-
name: "
|
|
223
|
+
name: "cart_list",
|
|
211
224
|
description: "List all personas.",
|
|
212
225
|
input_schema: {type: "object", properties: {}}
|
|
213
226
|
) do |server_context:, **|
|
|
@@ -216,7 +229,7 @@ module Personality
|
|
|
216
229
|
end
|
|
217
230
|
|
|
218
231
|
@server.define_tool(
|
|
219
|
-
name: "
|
|
232
|
+
name: "cart_use",
|
|
220
233
|
description: "Switch active persona.",
|
|
221
234
|
input_schema: {
|
|
222
235
|
type: "object",
|
|
@@ -231,7 +244,7 @@ module Personality
|
|
|
231
244
|
end
|
|
232
245
|
|
|
233
246
|
@server.define_tool(
|
|
234
|
-
name: "
|
|
247
|
+
name: "cart_create",
|
|
235
248
|
description: "Create a new persona.",
|
|
236
249
|
input_schema: {
|
|
237
250
|
type: "object",
|
|
@@ -252,7 +265,7 @@ module Personality
|
|
|
252
265
|
|
|
253
266
|
def register_persona_tools
|
|
254
267
|
@server.define_tool(
|
|
255
|
-
name: "
|
|
268
|
+
name: "cart_teach",
|
|
256
269
|
description: "Learn a persona from a training YAML file. Creates a .pcart cartridge and imports memories into the database.",
|
|
257
270
|
input_schema: {
|
|
258
271
|
type: "object",
|
|
@@ -278,7 +291,7 @@ module Personality
|
|
|
278
291
|
end
|
|
279
292
|
|
|
280
293
|
@server.define_tool(
|
|
281
|
-
name: "
|
|
294
|
+
name: "cart_show",
|
|
282
295
|
description: "Show persona details and LLM instructions for a cartridge.",
|
|
283
296
|
input_schema: {
|
|
284
297
|
type: "object",
|
|
@@ -320,7 +333,7 @@ module Personality
|
|
|
320
333
|
end
|
|
321
334
|
|
|
322
335
|
@server.define_tool(
|
|
323
|
-
name: "
|
|
336
|
+
name: "cart_instructions",
|
|
324
337
|
description: "Get the LLM persona instructions for the active or specified cart. Returns markdown formatted character instructions.",
|
|
325
338
|
input_schema: {
|
|
326
339
|
type: "object",
|
|
@@ -351,7 +364,7 @@ module Personality
|
|
|
351
364
|
end
|
|
352
365
|
|
|
353
366
|
@server.define_tool(
|
|
354
|
-
name: "
|
|
367
|
+
name: "cart_carts",
|
|
355
368
|
description: "List available .pcart cartridge files with their metadata.",
|
|
356
369
|
input_schema: {type: "object", properties: {}}
|
|
357
370
|
) do |server_context:, **|
|
|
@@ -396,7 +409,7 @@ module Personality
|
|
|
396
409
|
|
|
397
410
|
def register_resource_tools
|
|
398
411
|
@server.define_tool(
|
|
399
|
-
name: "
|
|
412
|
+
name: "resource_read",
|
|
400
413
|
description: "Read an MCP resource by URI. Available resources: memory://subjects (subjects with counts), memory://stats (total memories, date range), memory://recent (last 10 memories).",
|
|
401
414
|
input_schema: {
|
|
402
415
|
type: "object",
|
|
@@ -430,11 +443,151 @@ module Personality
|
|
|
430
443
|
end
|
|
431
444
|
|
|
432
445
|
def register_tools
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
446
|
+
case @mode
|
|
447
|
+
when :indexer
|
|
448
|
+
# Local indexing only - no memory/cart/messaging
|
|
449
|
+
register_index_tools
|
|
450
|
+
when :core
|
|
451
|
+
# Centralized services - no local file indexing
|
|
452
|
+
register_memory_tools
|
|
453
|
+
register_cart_tools
|
|
454
|
+
register_persona_tools
|
|
455
|
+
register_resource_tools
|
|
456
|
+
register_messaging_tools
|
|
457
|
+
register_shell_tools
|
|
458
|
+
else # :all
|
|
459
|
+
register_memory_tools
|
|
460
|
+
register_index_tools
|
|
461
|
+
register_cart_tools
|
|
462
|
+
register_persona_tools
|
|
463
|
+
register_resource_tools
|
|
464
|
+
register_messaging_tools
|
|
465
|
+
register_shell_tools
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# === Shell Tools ===
|
|
470
|
+
|
|
471
|
+
def register_shell_tools
|
|
472
|
+
@server.define_tool(
|
|
473
|
+
name: "shell_exec",
|
|
474
|
+
description: "Execute a shell command on junkpile (Ubuntu x86_64). Returns stdout, stderr, and exit code.",
|
|
475
|
+
input_schema: {
|
|
476
|
+
type: "object",
|
|
477
|
+
properties: {
|
|
478
|
+
command: {type: "string", description: "Shell command to execute"},
|
|
479
|
+
timeout: {type: "integer", description: "Timeout in seconds (default: 60, max: 300)"},
|
|
480
|
+
cwd: {type: "string", description: "Working directory (default: home)"}
|
|
481
|
+
},
|
|
482
|
+
required: %w[command]
|
|
483
|
+
}
|
|
484
|
+
) do |command:, server_context:, **opts|
|
|
485
|
+
require "open3"
|
|
486
|
+
require "shellwords"
|
|
487
|
+
|
|
488
|
+
timeout = [opts[:timeout] || 60, 300].min
|
|
489
|
+
cwd = opts[:cwd] || ENV["HOME"]
|
|
490
|
+
|
|
491
|
+
# Wrap with timeout
|
|
492
|
+
full_cmd = "timeout #{timeout} bash -c #{Shellwords.escape(command)}"
|
|
493
|
+
|
|
494
|
+
stdout, stderr, status = Open3.capture3(full_cmd, chdir: cwd)
|
|
495
|
+
|
|
496
|
+
result = {
|
|
497
|
+
command: command,
|
|
498
|
+
cwd: cwd,
|
|
499
|
+
exit_code: status.exitstatus,
|
|
500
|
+
stdout: stdout,
|
|
501
|
+
stderr: stderr,
|
|
502
|
+
timed_out: status.exitstatus == 124
|
|
503
|
+
}
|
|
504
|
+
::MCP::Tool::Response.new([{type: "text", text: JSON.generate(result)}])
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# === Messaging Tools ===
|
|
509
|
+
|
|
510
|
+
SIGNAL_ACCOUNT = "+48600965497" # Moto G52 - BT's comm array
|
|
511
|
+
PILOT_NUMBER = "+48535329895" # Adam's number
|
|
512
|
+
SIGNAL_CLI = "/home/linuxbrew/.linuxbrew/bin/signal-cli"
|
|
513
|
+
|
|
514
|
+
def register_messaging_tools
|
|
515
|
+
@server.define_tool(
|
|
516
|
+
name: "signal_send",
|
|
517
|
+
description: "Send a Signal message. Default sends from BT's comm array (+48600965497) to Pilot (+48535329895).",
|
|
518
|
+
input_schema: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
message: {type: "string", description: "Message text to send"},
|
|
522
|
+
to: {type: "string", description: "Recipient phone number (default: Pilot's number)"},
|
|
523
|
+
from: {type: "string", description: "Sender account (default: BT's comm array)"}
|
|
524
|
+
},
|
|
525
|
+
required: %w[message]
|
|
526
|
+
}
|
|
527
|
+
) do |message:, server_context:, **opts|
|
|
528
|
+
from = opts[:from] || SIGNAL_ACCOUNT
|
|
529
|
+
to = opts[:to] || PILOT_NUMBER
|
|
530
|
+
|
|
531
|
+
# Escape message for shell
|
|
532
|
+
escaped_message = message.gsub("'", "'\\''")
|
|
533
|
+
cmd = "#{SIGNAL_CLI} -a #{from} send -m '#{escaped_message}' #{to}"
|
|
534
|
+
|
|
535
|
+
output = `#{cmd} 2>&1`
|
|
536
|
+
success = $?.success?
|
|
537
|
+
|
|
538
|
+
result = {
|
|
539
|
+
success: success,
|
|
540
|
+
from: from,
|
|
541
|
+
to: to,
|
|
542
|
+
message: message,
|
|
543
|
+
output: output.strip
|
|
544
|
+
}
|
|
545
|
+
::MCP::Tool::Response.new([{type: "text", text: JSON.generate(result)}])
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
@server.define_tool(
|
|
549
|
+
name: "signal_receive",
|
|
550
|
+
description: "Check for incoming Signal messages on BT's comm array.",
|
|
551
|
+
input_schema: {
|
|
552
|
+
type: "object",
|
|
553
|
+
properties: {
|
|
554
|
+
account: {type: "string", description: "Account to check (default: BT's comm array)"},
|
|
555
|
+
timeout: {type: "integer", description: "Timeout in seconds (default: 5)"}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
) do |server_context:, **opts|
|
|
559
|
+
account = opts[:account] || SIGNAL_ACCOUNT
|
|
560
|
+
timeout = opts[:timeout] || 5
|
|
561
|
+
|
|
562
|
+
cmd = "timeout #{timeout} #{SIGNAL_CLI} -a #{account} receive --json 2>&1"
|
|
563
|
+
output = `#{cmd}`
|
|
564
|
+
|
|
565
|
+
messages = []
|
|
566
|
+
output.each_line do |line|
|
|
567
|
+
next if line.strip.empty?
|
|
568
|
+
begin
|
|
569
|
+
msg = JSON.parse(line)
|
|
570
|
+
if msg["envelope"] && msg["envelope"]["dataMessage"]
|
|
571
|
+
data = msg["envelope"]["dataMessage"]
|
|
572
|
+
messages << {
|
|
573
|
+
from: msg["envelope"]["source"],
|
|
574
|
+
timestamp: msg["envelope"]["timestamp"],
|
|
575
|
+
message: data["message"],
|
|
576
|
+
group: data["groupInfo"]&.dig("groupId")
|
|
577
|
+
}
|
|
578
|
+
end
|
|
579
|
+
rescue JSON::ParserError
|
|
580
|
+
# Skip non-JSON lines
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
result = {
|
|
585
|
+
account: account,
|
|
586
|
+
message_count: messages.length,
|
|
587
|
+
messages: messages
|
|
588
|
+
}
|
|
589
|
+
::MCP::Tool::Response.new([{type: "text", text: JSON.generate(result)}])
|
|
590
|
+
end
|
|
438
591
|
end
|
|
439
592
|
|
|
440
593
|
def read_memory_resource(uri)
|
|
@@ -40,17 +40,23 @@ module Personality
|
|
|
40
40
|
def register_speak
|
|
41
41
|
@server.define_tool(
|
|
42
42
|
name: "speak",
|
|
43
|
-
description: "Speak text aloud using TTS. Synthesizes and plays audio in the background.",
|
|
43
|
+
description: "Speak text aloud using TTS. Synthesizes and plays audio in the background unless wait=true.",
|
|
44
44
|
input_schema: {
|
|
45
45
|
type: "object",
|
|
46
46
|
properties: {
|
|
47
47
|
text: {type: "string", description: "Text to speak aloud"},
|
|
48
|
-
voice: {type: "string", description: "Voice model name (optional, uses active voice if omitted)"}
|
|
48
|
+
voice: {type: "string", description: "Voice model name (optional, uses active voice if omitted)"},
|
|
49
|
+
language: {type: "string", description: "Language code (e.g. 'en', 'pl'). Auto-detected if omitted."},
|
|
50
|
+
wait: {type: "boolean", description: "Wait for playback to complete before returning (default: false)"}
|
|
49
51
|
},
|
|
50
52
|
required: %w[text]
|
|
51
53
|
}
|
|
52
54
|
) do |text:, server_context:, **opts|
|
|
53
|
-
result =
|
|
55
|
+
result = if opts[:wait]
|
|
56
|
+
Personality::TTS.speak_and_wait(text, voice: opts[:voice], language: opts[:language])
|
|
57
|
+
else
|
|
58
|
+
Personality::TTS.speak(text, voice: opts[:voice], language: opts[:language])
|
|
59
|
+
end
|
|
54
60
|
::MCP::Tool::Response.new([{type: "text", text: JSON.generate(result)}])
|
|
55
61
|
end
|
|
56
62
|
end
|
|
@@ -85,7 +91,8 @@ module Personality
|
|
|
85
91
|
) do |server_context:, **|
|
|
86
92
|
voice = Personality::TTS.active_voice
|
|
87
93
|
installed = !Personality::TTS.find_voice(voice).nil?
|
|
88
|
-
|
|
94
|
+
backend = Personality::TTS.backend
|
|
95
|
+
::MCP::Tool::Response.new([{type: "text", text: JSON.generate({voice: voice, installed: installed, backend: backend})}])
|
|
89
96
|
end
|
|
90
97
|
end
|
|
91
98
|
|