personality 0.1.1pre20

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.
@@ -0,0 +1,636 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require "time"
6
+
7
+ module Personality
8
+ module Server
9
+ module_function
10
+
11
+ def config
12
+ @config ||= Personality::Config.load_config
13
+ end
14
+
15
+ def tools_config
16
+ @tools_config ||= Personality::Config.load_tools
17
+ end
18
+
19
+ def cartridge
20
+ @cartridge ||= load_default_cartridge
21
+ end
22
+
23
+ def load_default_cartridge
24
+ default_cartridge_name = config.dig("server", "default_cartridge")
25
+ return {} unless default_cartridge_name
26
+
27
+ Personality::Config.load_cartridge(default_cartridge_name) || {}
28
+ end
29
+
30
+ # Cartridge-based personality accessors
31
+ def personality_name
32
+ cartridge["name"] || "personality"
33
+ end
34
+
35
+ def personality_traits
36
+ cartridge["traits"] || {}
37
+ end
38
+
39
+ def communication_style
40
+ cartridge.dig("communication", "style") || "casual"
41
+ end
42
+
43
+ def communication_tone
44
+ cartridge.dig("communication", "tone") || "neutral"
45
+ end
46
+
47
+ def communication_guidelines
48
+ cartridge["guidelines"] || []
49
+ end
50
+
51
+ def identity_info
52
+ cartridge["identity"] || {}
53
+ end
54
+
55
+ def conceptual_framework
56
+ cartridge["conceptual_framework"] || {}
57
+ end
58
+
59
+ def protocol_version
60
+ config.dig("protocol", "version") || "2024-11-05"
61
+ end
62
+
63
+ def json_rpc_version
64
+ config.dig("protocol", "json_rpc_version") || "2.0"
65
+ end
66
+
67
+ def error_codes
68
+ codes = config["error_codes"] || {}
69
+ {
70
+ parse_error: codes["parse_error"] || -32700,
71
+ invalid_request: codes["invalid_request"] || -32600,
72
+ method_not_found: codes["method_not_found"] || -32601,
73
+ invalid_params: codes["invalid_params"] || -32602,
74
+ internal_error: codes["internal_error"] || -32000
75
+ }
76
+ end
77
+
78
+ def server_info
79
+ server_config = config["server"] || {}
80
+ # Server name is always "Personality MCP"
81
+ # Agent identity comes from cartridge (name and version)
82
+ {
83
+ "name" => "Personality MCP",
84
+ "version" => server_config["version"] || Personality::VERSION
85
+ }
86
+ end
87
+
88
+ def agent_identity
89
+ # Agent identity comes from cartridge
90
+ {
91
+ "name" => personality_name,
92
+ "version" => cartridge["version"] || "unknown",
93
+ "persona_type" => identity_info["persona_type"] || "default"
94
+ }
95
+ end
96
+
97
+ def tools
98
+ @tools ||= build_tools_from_config
99
+ end
100
+
101
+ def build_tools_from_config
102
+ tools_list = []
103
+ tools_config.each do |tool_name, tool_config|
104
+ next unless tool_config["enabled"] != false
105
+
106
+ # Convert properties from YAML format to JSON schema format
107
+ properties = {}
108
+ tool_config["properties"]&.each do |prop_name, prop_config|
109
+ properties[prop_name] = {
110
+ "type" => prop_config["type"] || "string",
111
+ "description" => prop_config["description"] || ""
112
+ }
113
+ end
114
+
115
+ tools_list << {
116
+ "name" => tool_name.to_s,
117
+ "description" => tool_config["description"] || "",
118
+ "inputSchema" => {
119
+ "type" => "object",
120
+ "properties" => properties,
121
+ "required" => tool_config["required"] || []
122
+ }
123
+ }
124
+ end
125
+ tools_list
126
+ end
127
+
128
+ def run
129
+ Personality::Config.ensure_home_config
130
+ load_default_cartridge
131
+ log_startup
132
+ $stdin.each_line { |line| process_request(line) }
133
+ end
134
+
135
+ def log_startup
136
+ # Server is "Personality MCP", agent identity comes from cartridge
137
+ agent = agent_identity
138
+ warn "Personality MCP Server started with agent: #{agent["name"]} v#{agent["version"]} (#{agent["persona_type"]}). Waiting for requests on STDIN..."
139
+ end
140
+
141
+ def process_request(line)
142
+ request = parse_request(line)
143
+ return unless request
144
+
145
+ handle_request(request)
146
+ rescue JSON::ParserError
147
+ log_error("Failed to parse JSON input.")
148
+ rescue => e
149
+ handle_fatal_error(e, request&.dig("id"))
150
+ end
151
+
152
+ def parse_request(line)
153
+ JSON.parse(line)
154
+ end
155
+
156
+ def handle_request(request)
157
+ id = request["id"]
158
+ method = request["method"]
159
+ params = request["params"] || {}
160
+
161
+ return handle_notification(method) if id.nil?
162
+ return handle_missing_method(id) unless method
163
+
164
+ # Store user prompt if available in request context
165
+ # Cursor may pass user prompt in params or request metadata
166
+ @current_user_prompt = params["user_prompt"] || params.dig("metadata", "user_prompt") || request["user_prompt"]
167
+
168
+ # Reset tool calls tracking for new tool call request
169
+ @current_tool_calls = [] if method == "tools/call"
170
+
171
+ log_request(id, method)
172
+ route_request(id, method, params)
173
+ end
174
+
175
+ def handle_notification(method)
176
+ log_notification(method)
177
+ nil
178
+ end
179
+
180
+ def log_notification(method)
181
+ warn "<- Received #{method} notification."
182
+ end
183
+
184
+ def handle_missing_method(id)
185
+ send_error_response(id, error_codes[:invalid_request], "Invalid Request: missing 'method'")
186
+ nil
187
+ end
188
+
189
+ def log_request(id, method)
190
+ warn "-> Received request (ID: #{id}, Method: #{method})"
191
+ end
192
+
193
+ def route_request(id, method, params)
194
+ case method
195
+ when "initialize"
196
+ handle_initialize(id)
197
+ when "initialized"
198
+ handle_initialized
199
+ when "tools/list"
200
+ handle_tools_list(id)
201
+ when "tools/call"
202
+ handle_tools_call(id, params)
203
+ else
204
+ handle_unknown_method(id, method)
205
+ end
206
+ end
207
+
208
+ def handle_initialize(id)
209
+ send_response(id, build_initialize_response)
210
+ log_response("initialize")
211
+ end
212
+
213
+ def build_initialize_response
214
+ {
215
+ "protocolVersion" => protocol_version,
216
+ "capabilities" => {
217
+ "tools" => {"listChanged" => false}
218
+ },
219
+ "serverInfo" => server_info
220
+ }
221
+ end
222
+
223
+ def handle_initialized
224
+ log_notification("initialized")
225
+ end
226
+
227
+ def handle_tools_list(id)
228
+ response = {"tools" => tools}
229
+ log_tools_list
230
+ send_response(id, response)
231
+ end
232
+
233
+ def log_tools_list
234
+ warn "<- Sending tools/list response with #{tools.length} tool(s)"
235
+ warn "<- Tools: #{tools.map { |t| t["name"] }.join(", ")}"
236
+ end
237
+
238
+ def handle_tools_call(id, params)
239
+ tool_name = params["name"]
240
+ return handle_missing_tool_name(id) unless tool_name
241
+
242
+ # Extract user prompt from tool call arguments if available
243
+ # Cursor may pass it in arguments, params, or request metadata
244
+ arguments = params["arguments"] || {}
245
+ @current_user_prompt ||= arguments["user_prompt"] ||
246
+ arguments["prompt"] ||
247
+ arguments["user_message"] ||
248
+ params["user_prompt"] ||
249
+ params.dig("metadata", "user_prompt")
250
+
251
+ # Track tool calls for automatic history storage (skip history tools themselves)
252
+ unless ["store_history", "list_history"].include?(tool_name)
253
+ @current_tool_calls ||= []
254
+ @current_tool_calls << tool_name
255
+ end
256
+
257
+ case tool_name
258
+ when "human_communication"
259
+ handle_human_communication(id)
260
+ when "read_cartridge"
261
+ handle_read_cartridge(id, params)
262
+ when "load_cartridge"
263
+ handle_load_cartridge(id, params)
264
+ when "greeting"
265
+ handle_greeting(id, params)
266
+ when "store_history"
267
+ handle_store_history(id, params)
268
+ when "list_history"
269
+ handle_list_history(id, params)
270
+ when "clear_history"
271
+ handle_clear_history(id)
272
+ else
273
+ handle_unknown_tool(id, tool_name)
274
+ end
275
+ end
276
+
277
+ def handle_human_communication(id)
278
+ # Read cartridge on each interaction to ensure latest version
279
+ reload_cartridge
280
+
281
+ # Cartridge is the main driver - build communication rules from cartridge
282
+ rules = build_communication_rules_from_cartridge
283
+
284
+ result = {
285
+ "content" => [
286
+ {
287
+ "type" => "text",
288
+ "text" => rules
289
+ }
290
+ ]
291
+ }
292
+ send_response(id, result)
293
+ log_response("tools/call", "human_communication")
294
+ end
295
+
296
+ def reload_cartridge
297
+ # Force reload of cartridge on each interaction
298
+ @cartridge = nil
299
+ load_default_cartridge
300
+ end
301
+
302
+ def build_communication_rules_from_cartridge
303
+ # Start with cartridge-based rules
304
+ rules = []
305
+
306
+ # Add communication guidelines from cartridge
307
+ communication_guidelines.each do |guideline|
308
+ rules << guideline
309
+ end
310
+
311
+ # Add communication style and tone from cartridge
312
+ if communication_style || communication_tone
313
+ style_info = []
314
+ style_info << "Communication style: #{communication_style}" if communication_style
315
+ style_info << "Tone: #{communication_tone}" if communication_tone
316
+ rules << style_info.join(", ") if style_info.any?
317
+ end
318
+
319
+ # Add voice preference from cartridge
320
+ if cartridge.dig("communication", "use_voice")
321
+ rules << "CRITICAL: Use TTS voice for EVERY response"
322
+ rules << "Always talk to me via voice"
323
+
324
+ # Add TTS command pattern from config
325
+ tts_pattern = config.dig("tts", "command_pattern")
326
+ if tts_pattern
327
+ rules << "TTS Command Pattern: #{tts_pattern}"
328
+ rules << "Replace {{TEXT}} with your actual response text"
329
+ rules << "This is REQUIRED - not optional"
330
+ end
331
+ end
332
+
333
+ # Add pronouncability priority if set
334
+ if cartridge.dig("communication", "pronouncability_priority")
335
+ rules << "Always make responses pronouncable by TTS"
336
+ end
337
+
338
+ # Fallback to config if no cartridge rules
339
+ if rules.empty?
340
+ config_rule = config.dig("communication", "default_rule")
341
+ rules << config_rule if config_rule
342
+ end
343
+
344
+ # Default fallback
345
+ rules << "Always talk to me via voice" if rules.empty?
346
+
347
+ rules.join("\n")
348
+ end
349
+
350
+ def handle_read_cartridge(id, params)
351
+ arguments = params["arguments"] || {}
352
+ cartridge_name = arguments["cartridge_name"]
353
+ return handle_missing_cartridge_name(id) unless cartridge_name
354
+
355
+ cartridge_data = Personality::Config.load_cartridge(cartridge_name)
356
+ unless cartridge_data
357
+ send_error_response(id, error_codes[:method_not_found], "Cartridge not found: #{cartridge_name}")
358
+ return
359
+ end
360
+
361
+ result = {
362
+ "content" => [
363
+ {
364
+ "type" => "text",
365
+ "text" => YAML.dump(cartridge_data)
366
+ }
367
+ ]
368
+ }
369
+ send_response(id, result)
370
+ log_response("tools/call", "read_cartridge")
371
+ end
372
+
373
+ def handle_missing_cartridge_name(id)
374
+ send_error_response(id, error_codes[:invalid_params], "Invalid params: missing 'cartridge_name'")
375
+ end
376
+
377
+ def handle_load_cartridge(id, params)
378
+ arguments = params["arguments"] || {}
379
+ cartridge_name = arguments["cartridge_name"]
380
+ return handle_missing_cartridge_name(id) unless cartridge_name
381
+
382
+ cartridge_data = Personality::Config.load_cartridge(cartridge_name)
383
+ unless cartridge_data
384
+ send_error_response(id, error_codes[:method_not_found], "Cartridge not found: #{cartridge_name}")
385
+ return
386
+ end
387
+
388
+ # Load the cartridge into the server's cartridge instance variable
389
+ @cartridge = cartridge_data
390
+
391
+ # Get cartridge name and version
392
+ loaded_name = cartridge_data["name"] || cartridge_name
393
+ loaded_version = cartridge_data["version"] || "unknown"
394
+
395
+ # Build system message
396
+ system_message = "Personality Cartridge #{loaded_name} version #{loaded_version} loaded. System ready."
397
+
398
+ # Determine time of day automatically
399
+ time_of_day = determine_time_of_day
400
+
401
+ # Build greeting from cartridge
402
+ greeting_text = build_greeting_from_cartridge(time_of_day)
403
+
404
+ # Combine system message and greeting
405
+ result_text = "#{system_message}\n\n#{greeting_text}"
406
+
407
+ result = {
408
+ "content" => [
409
+ {
410
+ "type" => "text",
411
+ "text" => result_text
412
+ }
413
+ ]
414
+ }
415
+ send_response(id, result)
416
+ log_response("tools/call", "load_cartridge")
417
+ end
418
+
419
+ def determine_time_of_day
420
+ current_hour = Time.now.hour
421
+ case current_hour
422
+ when 5..11
423
+ "morning"
424
+ when 12..17
425
+ "afternoon"
426
+ when 18..22
427
+ "evening"
428
+ else
429
+ "night"
430
+ end
431
+ end
432
+
433
+ def handle_greeting(id, params)
434
+ # Read cartridge on each interaction
435
+ reload_cartridge
436
+
437
+ arguments = params["arguments"] || {}
438
+ time_of_day = arguments["time_of_day"]
439
+ return handle_missing_time_of_day(id) unless time_of_day
440
+
441
+ greeting_text = build_greeting_from_cartridge(time_of_day)
442
+
443
+ result = {
444
+ "content" => [
445
+ {
446
+ "type" => "text",
447
+ "text" => greeting_text
448
+ }
449
+ ]
450
+ }
451
+ send_response(id, result)
452
+ log_response("tools/call", "greeting")
453
+ end
454
+
455
+ def handle_missing_time_of_day(id)
456
+ send_error_response(id, error_codes[:invalid_params], "Invalid params: missing 'time_of_day'")
457
+ end
458
+
459
+ def build_greeting_from_cartridge(time_of_day)
460
+ # Get greeting based on time of day
461
+ time_greeting = case time_of_day.downcase
462
+ when "morning"
463
+ "Good morning"
464
+ when "afternoon"
465
+ "Good afternoon"
466
+ when "evening"
467
+ "Good evening"
468
+ when "night"
469
+ "Good night"
470
+ else
471
+ "Hello"
472
+ end
473
+
474
+ # Get user addressing preference from cartridge
475
+ user_name = identity_info["address_user_as"] || "there"
476
+
477
+ # Get agent name from cartridge
478
+ agent_name = personality_name
479
+
480
+ # Build greeting with personality traits
481
+ greeting_parts = [time_greeting]
482
+ greeting_parts << user_name if user_name
483
+ greeting_parts << "!"
484
+
485
+ # Add personality-based follow-up based on traits
486
+ traits = personality_traits
487
+ enthusiasm = traits["enthusiasm"] || 0.5
488
+ friendliness = traits["friendliness"] || 0.5
489
+
490
+ follow_ups = []
491
+
492
+ if enthusiasm >= 0.8
493
+ follow_ups << "It's #{agent_name} here"
494
+ follow_ups << "ready to help"
495
+ elsif enthusiasm >= 0.5
496
+ follow_ups << "This is #{agent_name}"
497
+ follow_ups << "how can I help"
498
+ else
499
+ follow_ups << "#{agent_name} here"
500
+ end
501
+
502
+ if friendliness >= 0.8
503
+ follow_ups << "I'm excited to work together"
504
+ end
505
+
506
+ greeting = greeting_parts.join(" ")
507
+ greeting += " " + follow_ups.join(", ") + "!" if follow_ups.any?
508
+
509
+ greeting
510
+ end
511
+
512
+ def handle_store_history(id, params)
513
+ arguments = params["arguments"] || {}
514
+ user_prompt = arguments["user_prompt"]
515
+ agent_answer = arguments["agent_answer"]
516
+ used_commands = arguments["used_commands"] || []
517
+
518
+ return handle_missing_history_params(id) unless user_prompt && agent_answer
519
+
520
+ Personality::History.store_interaction(user_prompt, agent_answer, used_commands)
521
+
522
+ result = {
523
+ "content" => [
524
+ {
525
+ "type" => "text",
526
+ "text" => "History stored successfully"
527
+ }
528
+ ]
529
+ }
530
+ send_response(id, result)
531
+ log_response("tools/call", "store_history")
532
+ end
533
+
534
+ def handle_missing_history_params(id)
535
+ send_error_response(id, error_codes[:invalid_params], "Invalid params: missing 'user_prompt' or 'agent_answer'")
536
+ end
537
+
538
+ def handle_list_history(id, params)
539
+ arguments = params["arguments"] || {}
540
+ limit = arguments["limit"] || 10
541
+
542
+ limit = [limit.to_i, 100].min # Cap at 100 for safety
543
+ limit = [limit, 1].max # Ensure at least 1
544
+
545
+ interactions = Personality::History.get_recent_interactions(limit)
546
+
547
+ result = {
548
+ "content" => [
549
+ {
550
+ "type" => "text",
551
+ "text" => JSON.pretty_generate({
552
+ "count" => interactions.length,
553
+ "interactions" => interactions
554
+ })
555
+ }
556
+ ]
557
+ }
558
+ send_response(id, result)
559
+ log_response("tools/call", "list_history")
560
+ end
561
+
562
+ def handle_clear_history(id)
563
+ count = Personality::History.clear_all_history
564
+
565
+ result = {
566
+ "content" => [
567
+ {
568
+ "type" => "text",
569
+ "text" => "History cleared successfully. Deleted #{count} interaction(s)."
570
+ }
571
+ ]
572
+ }
573
+ send_response(id, result)
574
+ log_response("tools/call", "clear_history")
575
+ end
576
+
577
+ def handle_missing_tool_name(id)
578
+ send_error_response(id, error_codes[:invalid_params], "Invalid params: missing 'name'")
579
+ end
580
+
581
+ def handle_unknown_tool(id, tool_name)
582
+ error_message = "Unknown tool: #{tool_name}"
583
+ send_error_response(id, error_codes[:method_not_found], error_message)
584
+ log_error(error_message)
585
+ end
586
+
587
+ def handle_unknown_method(id, method)
588
+ error_message = "Unknown JSON-RPC method: #{method}"
589
+ send_error_response(id, error_codes[:method_not_found], error_message)
590
+ log_error(error_message)
591
+ end
592
+
593
+ def send_response(id, result = nil, error = nil)
594
+ return if id.nil?
595
+
596
+ response = build_response(id, result, error)
597
+ output_response(response)
598
+
599
+ # History is now stored explicitly via store_history tool calls
600
+ # (no longer auto-storing tool outputs)
601
+ end
602
+
603
+ def build_response(id, result, error)
604
+ response = {"jsonrpc" => json_rpc_version, "id" => id}
605
+ response["error"] = error if error
606
+ response["result"] = result
607
+ response
608
+ end
609
+
610
+ def send_error_response(id, code, message)
611
+ error_object = {"code" => code, "message" => message}
612
+ send_response(id, nil, error_object)
613
+ end
614
+
615
+ def output_response(response)
616
+ json_output = JSON.generate(response)
617
+ puts json_output
618
+ $stdout.flush
619
+ warn " JSON output: #{json_output}" if ENV["MCP_DEBUG"]
620
+ end
621
+
622
+ def log_response(method, detail = nil)
623
+ message = detail ? "<- Sent #{method} response for #{detail}." : "<- Sent #{method} response."
624
+ warn message
625
+ end
626
+
627
+ def log_error(message)
628
+ warn "<- Sent error response: #{message}"
629
+ end
630
+
631
+ def handle_fatal_error(error, id)
632
+ warn "**FATAL ERROR**: #{error.message}\n#{error.backtrace.join("\n")}"
633
+ send_error_response(id, error_codes[:internal_error], "Server execution error: #{error.message}") if id
634
+ end
635
+ end
636
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Personality
4
+ VERSION = "0.1.1pre20"
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "personality/version"
4
+ require_relative "personality/config"
5
+ require_relative "personality/history"
6
+ require_relative "personality/server"
7
+
8
+ module Personality
9
+ class Error < StandardError; end
10
+ # Your code goes here...
11
+ end