personality 0.1.3 → 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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
- require "mcp/transports/stdio"
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
- def self.run
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: "core",
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: "memory.store",
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: "memory.recall",
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: "memory.search",
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: "memory.forget",
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: "memory.list",
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: "index.code",
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: "index.docs",
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: "index.search",
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: "index.status",
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: "index.clear",
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: "cart.list",
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: "cart.use",
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: "cart.create",
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: "cart.teach",
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: "cart.show",
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: "cart.instructions",
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: "cart.carts",
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: "resource.read",
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
- register_memory_tools
434
- register_index_tools
435
- register_cart_tools
436
- register_persona_tools
437
- register_resource_tools
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 = Personality::TTS.speak(text, voice: opts[:voice])
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
- ::MCP::Tool::Response.new([{type: "text", text: JSON.generate({voice: voice, installed: installed})}])
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