activematrix 0.0.5 → 0.0.8
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/README.md +96 -28
- data/app/jobs/active_matrix/application_job.rb +11 -0
- data/app/models/active_matrix/agent/jobs/memory_reaper.rb +87 -0
- data/app/models/active_matrix/agent.rb +166 -0
- data/app/models/active_matrix/agent_store.rb +80 -0
- data/app/models/active_matrix/application_record.rb +15 -0
- data/app/models/active_matrix/chat_session.rb +105 -0
- data/app/models/active_matrix/knowledge_base.rb +100 -0
- data/exe/activematrix +7 -0
- data/lib/active_matrix/agent_manager.rb +160 -121
- data/lib/active_matrix/agent_registry.rb +25 -21
- data/lib/active_matrix/api.rb +8 -2
- data/lib/active_matrix/async_query.rb +58 -0
- data/lib/active_matrix/bot/base.rb +3 -3
- data/lib/active_matrix/bot/builtin_commands.rb +188 -0
- data/lib/active_matrix/bot/command_parser.rb +175 -0
- data/lib/active_matrix/cli.rb +273 -0
- data/lib/active_matrix/client.rb +21 -6
- data/lib/active_matrix/client_pool.rb +38 -27
- data/lib/active_matrix/daemon/probe_server.rb +118 -0
- data/lib/active_matrix/daemon/signal_handler.rb +156 -0
- data/lib/active_matrix/daemon/worker.rb +109 -0
- data/lib/active_matrix/daemon.rb +236 -0
- data/lib/active_matrix/engine.rb +18 -0
- data/lib/active_matrix/errors.rb +1 -1
- data/lib/active_matrix/event_router.rb +61 -49
- data/lib/active_matrix/events.rb +1 -0
- data/lib/active_matrix/instrumentation.rb +148 -0
- data/lib/active_matrix/memory/agent_memory.rb +7 -21
- data/lib/active_matrix/memory/conversation_memory.rb +4 -20
- data/lib/active_matrix/memory/global_memory.rb +15 -30
- data/lib/active_matrix/message_dispatcher.rb +197 -0
- data/lib/active_matrix/metrics.rb +424 -0
- data/lib/active_matrix/presence_manager.rb +181 -0
- data/lib/active_matrix/railtie.rb +8 -0
- data/lib/active_matrix/telemetry.rb +134 -0
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +18 -11
- data/lib/generators/active_matrix/install/install_generator.rb +3 -22
- data/lib/generators/active_matrix/install/templates/README +5 -2
- metadata +191 -31
- data/lib/generators/active_matrix/install/templates/agent_memory.rb +0 -47
- data/lib/generators/active_matrix/install/templates/conversation_context.rb +0 -72
- data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +0 -17
- data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
- data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
- data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +0 -26
- data/lib/generators/active_matrix/install/templates/global_memory.rb +0 -70
- data/lib/generators/active_matrix/install/templates/matrix_agent.rb +0 -127
data/lib/active_matrix/api.rb
CHANGED
|
@@ -10,7 +10,7 @@ module ActiveMatrix
|
|
|
10
10
|
extend ActiveMatrix::Extensions
|
|
11
11
|
include ActiveMatrix::Logging
|
|
12
12
|
|
|
13
|
-
USER_AGENT = "
|
|
13
|
+
USER_AGENT = "ActiveMatrix v#{ActiveMatrix::VERSION}".freeze
|
|
14
14
|
DEFAULT_HEADERS = {
|
|
15
15
|
'accept' => 'application/json',
|
|
16
16
|
'user-agent' => USER_AGENT
|
|
@@ -287,7 +287,7 @@ module ActiveMatrix
|
|
|
287
287
|
end
|
|
288
288
|
|
|
289
289
|
failures = 0
|
|
290
|
-
loop do
|
|
290
|
+
loop do # rubocop:disable Metrics/BlockLength
|
|
291
291
|
raise MatrixConnectionError, "Server still too busy to handle request after #{failures} attempts, try again later" if failures >= 10
|
|
292
292
|
|
|
293
293
|
req_id = ('A'..'Z').to_a.sample(4).join
|
|
@@ -343,6 +343,12 @@ module ActiveMatrix
|
|
|
343
343
|
end
|
|
344
344
|
raise MatrixRequestError.new_by_code(data, response.code) if data
|
|
345
345
|
|
|
346
|
+
# For 4xx errors without JSON body, construct a synthetic error
|
|
347
|
+
if response.code.to_i >= 400 && response.code.to_i < 500
|
|
348
|
+
synthetic_error = { errcode: 'M_UNKNOWN', error: "HTTP #{response.code} #{response.message}" }
|
|
349
|
+
raise MatrixRequestError.new_by_code(synthetic_error, response.code)
|
|
350
|
+
end
|
|
351
|
+
|
|
346
352
|
raise MatrixConnectionError.class_by_code(response.code), response
|
|
347
353
|
end
|
|
348
354
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveMatrix
|
|
4
|
+
# Helper methods for async ActiveRecord queries (Rails 8.0+)
|
|
5
|
+
module AsyncQuery
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Load records asynchronously
|
|
9
|
+
# @param relation [ActiveRecord::Relation] The relation to load
|
|
10
|
+
# @return [Array] The loaded records
|
|
11
|
+
def load_async(relation)
|
|
12
|
+
relation.load_async.to_a
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Count records asynchronously
|
|
16
|
+
# @param relation [ActiveRecord::Relation] The relation to count
|
|
17
|
+
# @return [Integer] The count
|
|
18
|
+
def async_count(relation)
|
|
19
|
+
relation.async_count.value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Sum column asynchronously
|
|
23
|
+
# @param relation [ActiveRecord::Relation] The relation
|
|
24
|
+
# @param column [Symbol] The column to sum
|
|
25
|
+
# @return [Numeric] The sum
|
|
26
|
+
def async_sum(relation, column)
|
|
27
|
+
relation.async_sum(column).value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Pluck columns asynchronously
|
|
31
|
+
# @param relation [ActiveRecord::Relation] The relation
|
|
32
|
+
# @param columns [Array<Symbol>] The columns to pluck
|
|
33
|
+
# @return [Array] The plucked values
|
|
34
|
+
def async_pluck(relation, *columns)
|
|
35
|
+
relation.async_pluck(*columns).value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check existence asynchronously
|
|
39
|
+
# @param relation [ActiveRecord::Relation] The relation
|
|
40
|
+
# @return [Boolean] Whether records exist
|
|
41
|
+
def async_exists?(relation)
|
|
42
|
+
relation.async_count.value.positive?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Execute multiple async queries in parallel and wait for all results
|
|
46
|
+
# @param queries [Hash<Symbol, Proc>] Named queries to execute
|
|
47
|
+
# @return [Hash<Symbol, Object>] Results keyed by query name
|
|
48
|
+
# @example
|
|
49
|
+
# results = AsyncQuery.parallel(
|
|
50
|
+
# agents: -> { MatrixAgent.where(state: :online).load_async },
|
|
51
|
+
# count: -> { MatrixAgent.async_count }
|
|
52
|
+
# )
|
|
53
|
+
def parallel(**queries)
|
|
54
|
+
promises = queries.transform_values(&:call)
|
|
55
|
+
promises.transform_values { |result| result.respond_to?(:value) ? result.value : result }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -377,7 +377,7 @@ module ActiveMatrix::Bot
|
|
|
377
377
|
if settings.store_sync_token
|
|
378
378
|
begin
|
|
379
379
|
active_bot.client.api.set_account_data(
|
|
380
|
-
active_bot.client.mxid, "
|
|
380
|
+
active_bot.client.mxid, "com.seuros.active_matrix.#{settings.bot_name}",
|
|
381
381
|
{ sync_token: active_bot.client.sync_token }
|
|
382
382
|
)
|
|
383
383
|
rescue StandardError => e
|
|
@@ -466,7 +466,7 @@ module ActiveMatrix::Bot
|
|
|
466
466
|
bot.client.instance_variable_set(:@next_batch, settings.sync_token)
|
|
467
467
|
elsif settings.store_sync_token?
|
|
468
468
|
begin
|
|
469
|
-
data = bot.client.api.get_account_data(bot.client.mxid, "
|
|
469
|
+
data = bot.client.api.get_account_data(bot.client.mxid, "com.seuros.active_matrix.#{bot_name}")
|
|
470
470
|
bot.client.sync_token = data[:sync_token]
|
|
471
471
|
rescue ActiveMatrix::MatrixNotFoundError
|
|
472
472
|
# Valid
|
|
@@ -479,7 +479,7 @@ module ActiveMatrix::Bot
|
|
|
479
479
|
|
|
480
480
|
bot.client.start_listener_thread
|
|
481
481
|
|
|
482
|
-
bot.client.
|
|
482
|
+
bot.client.sync_thread&.join
|
|
483
483
|
rescue Interrupt
|
|
484
484
|
# Happens when killed
|
|
485
485
|
rescue StandardError => e
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveMatrix
|
|
4
|
+
module Bot
|
|
5
|
+
# Built-in commands that can be included in bot classes
|
|
6
|
+
#
|
|
7
|
+
# @example Include in a bot
|
|
8
|
+
# class MyBot < ActiveMatrix::Bot::Base
|
|
9
|
+
# include ActiveMatrix::Bot::BuiltinCommands
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
module BuiltinCommands
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.extend(ClassMethods)
|
|
15
|
+
base.register_builtin_commands
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ClassMethods
|
|
19
|
+
def register_builtin_commands
|
|
20
|
+
# Ping command - connectivity test
|
|
21
|
+
command(
|
|
22
|
+
:ping,
|
|
23
|
+
desc: 'Test bot connectivity and response time'
|
|
24
|
+
) do
|
|
25
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
26
|
+
|
|
27
|
+
response_parts = [
|
|
28
|
+
'**Pong!**',
|
|
29
|
+
'',
|
|
30
|
+
'Bot is online and responding',
|
|
31
|
+
"Server time: #{Time.current.strftime('%Y-%m-%d %H:%M:%S %Z')}"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
35
|
+
response_time_ms = ((end_time - start_time) * 1000).round(2)
|
|
36
|
+
response_parts.insert(2, "Response time: #{response_time_ms}ms")
|
|
37
|
+
|
|
38
|
+
room.send_notice(response_parts.join("\n"))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Version command - show bot version
|
|
42
|
+
command(
|
|
43
|
+
:version,
|
|
44
|
+
desc: 'Show bot version information'
|
|
45
|
+
) do
|
|
46
|
+
response_parts = [
|
|
47
|
+
'**Version Information**',
|
|
48
|
+
'',
|
|
49
|
+
"ActiveMatrix: #{ActiveMatrix::VERSION}",
|
|
50
|
+
"Ruby: #{RUBY_VERSION}",
|
|
51
|
+
"Platform: #{RUBY_PLATFORM}"
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Add application version if available
|
|
55
|
+
response_parts << "Application: #{Rails.application.version}" if defined?(Rails) && Rails.application.respond_to?(:version)
|
|
56
|
+
|
|
57
|
+
room.send_notice(response_parts.join("\n"))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Status command - show bot status
|
|
61
|
+
command(
|
|
62
|
+
:status,
|
|
63
|
+
desc: 'Show bot status and health information'
|
|
64
|
+
) do
|
|
65
|
+
response_parts = [
|
|
66
|
+
'**Bot Status**',
|
|
67
|
+
'',
|
|
68
|
+
'State: Online',
|
|
69
|
+
"Uptime: #{format_uptime}",
|
|
70
|
+
"User ID: #{client.mxid}",
|
|
71
|
+
"Homeserver: #{client.api.homeserver}"
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Add room count if available
|
|
75
|
+
if client.respond_to?(:rooms)
|
|
76
|
+
room_count = client.rooms.size
|
|
77
|
+
response_parts << "Joined rooms: #{room_count}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Add metrics if available
|
|
81
|
+
if defined?(ActiveMatrix::Metrics)
|
|
82
|
+
metrics = ActiveMatrix::Metrics.instance.get_health_summary
|
|
83
|
+
if metrics[:total_operations].positive?
|
|
84
|
+
response_parts += [
|
|
85
|
+
'',
|
|
86
|
+
'**Metrics**',
|
|
87
|
+
"Total operations: #{metrics[:total_operations]}",
|
|
88
|
+
"Success rate: #{metrics[:overall_success_rate]}%"
|
|
89
|
+
]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
room.send_notice(response_parts.join("\n"))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Time command - show current time
|
|
97
|
+
command(
|
|
98
|
+
:time,
|
|
99
|
+
desc: 'Show current time in specified timezone',
|
|
100
|
+
notes: 'Usage: !time [TIMEZONE]. Examples: !time UTC, !time America/New_York'
|
|
101
|
+
) do |timezone = nil|
|
|
102
|
+
time = if timezone && defined?(ActiveSupport::TimeZone)
|
|
103
|
+
tz = ActiveSupport::TimeZone[timezone]
|
|
104
|
+
if tz
|
|
105
|
+
tz.now
|
|
106
|
+
else
|
|
107
|
+
room.send_notice("Unknown timezone: #{timezone}. Using server time.")
|
|
108
|
+
Time.current
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
Time.current
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
formatted = time.strftime('%Y-%m-%d %H:%M:%S %Z')
|
|
115
|
+
unix_timestamp = time.to_i
|
|
116
|
+
|
|
117
|
+
response_parts = [
|
|
118
|
+
'**Current Time**',
|
|
119
|
+
'',
|
|
120
|
+
formatted,
|
|
121
|
+
"Unix timestamp: #{unix_timestamp}"
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
room.send_notice(response_parts.join("\n"))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Echo command - echo back message
|
|
128
|
+
command(
|
|
129
|
+
:echo,
|
|
130
|
+
desc: 'Echo back the provided message'
|
|
131
|
+
) do |message = nil|
|
|
132
|
+
if message.nil? || message.strip.empty?
|
|
133
|
+
room.send_notice('Nothing to echo. Usage: !echo <message>')
|
|
134
|
+
else
|
|
135
|
+
room.send_text(message)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Rooms command - list joined rooms (admin only)
|
|
140
|
+
command(
|
|
141
|
+
:rooms,
|
|
142
|
+
desc: 'List joined rooms',
|
|
143
|
+
only: :admin
|
|
144
|
+
) do
|
|
145
|
+
rooms_list = client.rooms.map do |r|
|
|
146
|
+
name = r.display_name || r.id
|
|
147
|
+
"- #{name}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if rooms_list.empty?
|
|
151
|
+
room.send_notice('Not joined to any rooms.')
|
|
152
|
+
else
|
|
153
|
+
response = [
|
|
154
|
+
"**Joined Rooms** (#{rooms_list.size})",
|
|
155
|
+
'',
|
|
156
|
+
*rooms_list.first(20)
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
response << "... and #{rooms_list.size - 20} more" if rooms_list.size > 20
|
|
160
|
+
|
|
161
|
+
room.send_notice(response.join("\n"))
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def format_uptime
|
|
170
|
+
return 'Unknown' unless defined?(@start_time)
|
|
171
|
+
|
|
172
|
+
seconds = (Time.current - @start_time).to_i
|
|
173
|
+
days = seconds / 86_400
|
|
174
|
+
hours = (seconds % 86_400) / 3600
|
|
175
|
+
minutes = (seconds % 3600) / 60
|
|
176
|
+
secs = seconds % 60
|
|
177
|
+
|
|
178
|
+
parts = []
|
|
179
|
+
parts << "#{days}d" if days.positive?
|
|
180
|
+
parts << "#{hours}h" if hours.positive?
|
|
181
|
+
parts << "#{minutes}m" if minutes.positive?
|
|
182
|
+
parts << "#{secs}s" if secs.positive? || parts.empty?
|
|
183
|
+
|
|
184
|
+
parts.join(' ')
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveMatrix
|
|
4
|
+
module Bot
|
|
5
|
+
# Parses command arguments with support for flags and quoted strings
|
|
6
|
+
#
|
|
7
|
+
# @example Basic parsing
|
|
8
|
+
# parser = CommandParser.new('search "hello world" --verbose')
|
|
9
|
+
# parser.args # => ["search", "hello world"]
|
|
10
|
+
# parser.flags # => { "verbose" => true }
|
|
11
|
+
#
|
|
12
|
+
# @example With key-value flags
|
|
13
|
+
# parser = CommandParser.new('greet --name=Alice --formal')
|
|
14
|
+
# parser.positional_args # => []
|
|
15
|
+
# parser.flags # => { "name" => "Alice", "formal" => true }
|
|
16
|
+
#
|
|
17
|
+
class CommandParser
|
|
18
|
+
attr_reader :raw_input, :command_name, :args, :raw_args
|
|
19
|
+
|
|
20
|
+
# Command prefixes recognized by the parser
|
|
21
|
+
COMMAND_PREFIXES = ['/', '!'].freeze
|
|
22
|
+
|
|
23
|
+
# @param input [String] Raw command input
|
|
24
|
+
# @param prefixes [Array<String>] Command prefixes to recognize
|
|
25
|
+
def initialize(input, prefixes: COMMAND_PREFIXES)
|
|
26
|
+
@raw_input = input.to_s.strip
|
|
27
|
+
@prefixes = prefixes
|
|
28
|
+
@command_name = nil
|
|
29
|
+
@args = []
|
|
30
|
+
@raw_args = ''
|
|
31
|
+
@parsed_flags = nil
|
|
32
|
+
parse!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if input is a valid command (has prefix and command name)
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def command?
|
|
39
|
+
!@command_name.nil? && @prefixes.any? { |prefix| @raw_input.start_with?(prefix) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get the prefix used in the command
|
|
43
|
+
#
|
|
44
|
+
# @return [String, nil]
|
|
45
|
+
def prefix
|
|
46
|
+
@prefixes.find { |p| @raw_input.start_with?(p) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Parse flags and positional arguments
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] Hash with :flags and :args keys
|
|
52
|
+
def parse_flags
|
|
53
|
+
@parse_flags ||= begin
|
|
54
|
+
flags = {}
|
|
55
|
+
positional = []
|
|
56
|
+
|
|
57
|
+
@args.each do |arg|
|
|
58
|
+
case arg
|
|
59
|
+
when /\A--([^=]+)=(.+)\z/
|
|
60
|
+
# --key=value format
|
|
61
|
+
flags[Regexp.last_match(1)] = Regexp.last_match(2)
|
|
62
|
+
when /\A--(.+)\z/
|
|
63
|
+
# --flag format (boolean)
|
|
64
|
+
flags[Regexp.last_match(1)] = true
|
|
65
|
+
when /\A-([a-zA-Z]+)\z/
|
|
66
|
+
# Short flags like -v, -abc (multiple)
|
|
67
|
+
Regexp.last_match(1).chars.each { |c| flags[c] = true }
|
|
68
|
+
else
|
|
69
|
+
positional << arg
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
{ flags: flags, args: positional }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get only positional arguments (no flags)
|
|
78
|
+
#
|
|
79
|
+
# @return [Array<String>]
|
|
80
|
+
def positional_args
|
|
81
|
+
parse_flags[:args]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get only flag arguments
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash<String, Object>]
|
|
87
|
+
def flags
|
|
88
|
+
parse_flags[:flags]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if a specific flag is set
|
|
92
|
+
#
|
|
93
|
+
# @param name [String] Flag name (without dashes)
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def flag?(name)
|
|
96
|
+
flags.key?(name.to_s)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get a flag value
|
|
100
|
+
#
|
|
101
|
+
# @param name [String] Flag name
|
|
102
|
+
# @param default [Object] Default value if flag not set
|
|
103
|
+
# @return [Object]
|
|
104
|
+
def flag(name, default = nil)
|
|
105
|
+
flags.fetch(name.to_s, default)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Formatted command string
|
|
109
|
+
#
|
|
110
|
+
# @return [String]
|
|
111
|
+
def formatted_command
|
|
112
|
+
return '' unless command?
|
|
113
|
+
|
|
114
|
+
[@command_name, *@args].join(' ')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def parse!
|
|
120
|
+
# Check if input starts with a valid prefix
|
|
121
|
+
found_prefix = @prefixes.find { |p| @raw_input.start_with?(p) }
|
|
122
|
+
return unless found_prefix
|
|
123
|
+
|
|
124
|
+
# Remove prefix
|
|
125
|
+
content = @raw_input[found_prefix.length..].strip
|
|
126
|
+
return if content.empty?
|
|
127
|
+
|
|
128
|
+
# Parse respecting quoted strings
|
|
129
|
+
parts = parse_with_quotes(content)
|
|
130
|
+
return if parts.empty?
|
|
131
|
+
|
|
132
|
+
@command_name = parts.first.downcase
|
|
133
|
+
@args = parts[1..] || []
|
|
134
|
+
@raw_args = @args.join(' ')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_with_quotes(input)
|
|
138
|
+
parts = []
|
|
139
|
+
current = +''
|
|
140
|
+
in_quotes = false
|
|
141
|
+
quote_char = nil
|
|
142
|
+
|
|
143
|
+
input.each_char do |char|
|
|
144
|
+
case char
|
|
145
|
+
when '"', "'"
|
|
146
|
+
if in_quotes && char == quote_char
|
|
147
|
+
# End of quoted section
|
|
148
|
+
in_quotes = false
|
|
149
|
+
quote_char = nil
|
|
150
|
+
elsif !in_quotes
|
|
151
|
+
# Start of quoted section
|
|
152
|
+
in_quotes = true
|
|
153
|
+
quote_char = char
|
|
154
|
+
else
|
|
155
|
+
# Different quote inside quotes, treat as literal
|
|
156
|
+
current << char
|
|
157
|
+
end
|
|
158
|
+
when ' ', "\t"
|
|
159
|
+
if in_quotes
|
|
160
|
+
current << char
|
|
161
|
+
elsif current.length.positive?
|
|
162
|
+
parts << current
|
|
163
|
+
current = +''
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
current << char
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
parts << current if current.length.positive?
|
|
171
|
+
parts
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|