ai_sentinel 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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +696 -0
  4. data/ai_sentinel.yml.example +111 -0
  5. data/exe/ai_sentinel +5 -0
  6. data/lib/ai_sentinel/actions/ai_prompt.rb +63 -0
  7. data/lib/ai_sentinel/actions/base.rb +44 -0
  8. data/lib/ai_sentinel/actions/http_get.rb +36 -0
  9. data/lib/ai_sentinel/actions/http_post.rb +47 -0
  10. data/lib/ai_sentinel/actions/shell_command.rb +51 -0
  11. data/lib/ai_sentinel/cli/context_display.rb +79 -0
  12. data/lib/ai_sentinel/cli/helpers.rb +88 -0
  13. data/lib/ai_sentinel/cli/prompt_change_handler.rb +67 -0
  14. data/lib/ai_sentinel/cli.rb +135 -0
  15. data/lib/ai_sentinel/condition_evaluator.rb +58 -0
  16. data/lib/ai_sentinel/config_loader.rb +164 -0
  17. data/lib/ai_sentinel/configuration.rb +113 -0
  18. data/lib/ai_sentinel/configuration_error.rb +5 -0
  19. data/lib/ai_sentinel/context.rb +34 -0
  20. data/lib/ai_sentinel/context_compactor.rb +130 -0
  21. data/lib/ai_sentinel/dsl.rb +27 -0
  22. data/lib/ai_sentinel/persistence/database.rb +116 -0
  23. data/lib/ai_sentinel/persistence/execution_log.rb +104 -0
  24. data/lib/ai_sentinel/prompt_change_detector.rb +75 -0
  25. data/lib/ai_sentinel/providers/anthropic.rb +151 -0
  26. data/lib/ai_sentinel/providers/base.rb +120 -0
  27. data/lib/ai_sentinel/providers/openai.rb +138 -0
  28. data/lib/ai_sentinel/runner.rb +77 -0
  29. data/lib/ai_sentinel/scheduler.rb +56 -0
  30. data/lib/ai_sentinel/step.rb +24 -0
  31. data/lib/ai_sentinel/tool_executor.rb +199 -0
  32. data/lib/ai_sentinel/tools/base.rb +44 -0
  33. data/lib/ai_sentinel/tools/shell_command.rb +40 -0
  34. data/lib/ai_sentinel/version.rb +5 -0
  35. data/lib/ai_sentinel/workflow.rb +17 -0
  36. data/lib/ai_sentinel.rb +57 -0
  37. metadata +180 -0
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AiSentinel
4
+ class Runner
5
+ ACTION_MAP = {
6
+ http_get: Actions::HttpGet,
7
+ http_post: Actions::HttpPost,
8
+ ai_prompt: Actions::AiPrompt,
9
+ shell_command: Actions::ShellCommand
10
+ }.freeze
11
+
12
+ attr_reader :workflow, :configuration
13
+
14
+ def initialize(workflow:, configuration:)
15
+ @workflow = workflow
16
+ @configuration = configuration
17
+ end
18
+
19
+ def execute
20
+ execution_id = Persistence::ExecutionLog.create(workflow_name: workflow.name)
21
+ context = Context.new(workflow_name: workflow.name, execution_id: execution_id)
22
+
23
+ AiSentinel.logger.info("Starting workflow '#{workflow.name}'")
24
+
25
+ workflow.steps.each do |step|
26
+ execute_step(step, context)
27
+ end
28
+
29
+ Persistence::ExecutionLog.complete(execution_id)
30
+ AiSentinel.logger.info("Workflow '#{workflow.name}' completed successfully")
31
+ context
32
+ rescue StandardError => e
33
+ Persistence::ExecutionLog.fail(execution_id, e.message)
34
+ AiSentinel.logger.error("Workflow '#{workflow.name}' failed: #{e.message}")
35
+ raise
36
+ end
37
+
38
+ private
39
+
40
+ def execute_step(step, context)
41
+ if step.skip?(context)
42
+ AiSentinel.logger.info(" Skipping step '#{step.name}' (condition not met)")
43
+ return
44
+ end
45
+
46
+ AiSentinel.logger.info(" Running step '#{step.name}' (#{step.action})")
47
+ started_at = Time.now
48
+ result = run_action(step, context)
49
+ context.set(step.name, result)
50
+
51
+ log_step_result(context, step, 'completed', started_at, result_data: result)
52
+ AiSentinel.logger.info(" Step '#{step.name}' completed")
53
+ rescue StandardError => e
54
+ log_step_result(context, step, 'failed', started_at || Time.now, error_message: e.message)
55
+ raise
56
+ end
57
+
58
+ def run_action(step, context)
59
+ action_class = ACTION_MAP[step.action]
60
+ raise Error, "Unknown action: #{step.action}" unless action_class
61
+
62
+ action_class.new(step: step, context: context, configuration: configuration).call
63
+ end
64
+
65
+ def log_step_result(context, step, status, started_at, result_data: nil, error_message: nil)
66
+ Persistence::ExecutionLog.log_step(
67
+ execution_id: context.execution_id,
68
+ step_name: step.name,
69
+ action: step.action,
70
+ status: status,
71
+ result_data: result_data,
72
+ error_message: error_message,
73
+ started_at: started_at
74
+ )
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rufus-scheduler'
4
+
5
+ module AiSentinel
6
+ class Scheduler
7
+ attr_reader :registry, :configuration, :rufus
8
+
9
+ def initialize(registry, configuration)
10
+ @registry = registry
11
+ @configuration = configuration
12
+ @rufus = Rufus::Scheduler.new
13
+ end
14
+
15
+ def start(daemonize: false)
16
+ register_workflows
17
+
18
+ if daemonize
19
+ AiSentinel.logger.info("AiSentinel started in background (#{registry.size} workflow(s))")
20
+ else
21
+ AiSentinel.logger.info("AiSentinel started (#{registry.size} workflow(s)). Press Ctrl+C to stop.")
22
+ trap('INT') { Thread.new { stop } }
23
+ trap('TERM') { Thread.new { stop } }
24
+ @rufus.join
25
+ AiSentinel.logger.info('AiSentinel stopped')
26
+ end
27
+ end
28
+
29
+ def stop
30
+ @rufus.shutdown
31
+ end
32
+
33
+ def trigger(workflow_name)
34
+ workflow = registry[workflow_name.to_s] || registry[workflow_name.to_sym]
35
+ raise Error, "Unknown workflow: #{workflow_name}" unless workflow
36
+
37
+ runner = Runner.new(workflow: workflow, configuration: configuration)
38
+ runner.execute
39
+ end
40
+
41
+ private
42
+
43
+ def register_workflows
44
+ registry.each do |name, workflow|
45
+ @rufus.cron(workflow.schedule_expression) do
46
+ runner = Runner.new(workflow: workflow, configuration: configuration)
47
+ runner.execute
48
+ rescue StandardError => e
49
+ AiSentinel.logger.error("Workflow '#{name}' failed: #{e.message}")
50
+ end
51
+
52
+ AiSentinel.logger.info("Registered workflow '#{name}' with schedule '#{workflow.schedule_expression}'")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AiSentinel
4
+ class Step
5
+ attr_reader :name, :action, :params, :condition
6
+
7
+ def initialize(name:, action:, condition: nil, **params)
8
+ @name = name.to_sym
9
+ @action = action.to_sym
10
+ @params = params
11
+ @condition = condition
12
+ end
13
+
14
+ def skip?(context)
15
+ return false if condition.nil?
16
+
17
+ if condition.respond_to?(:call)
18
+ !condition.call(context)
19
+ else
20
+ !ConditionEvaluator.evaluate(condition.to_s, context)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'shellwords'
5
+ require 'timeout'
6
+
7
+ module AiSentinel
8
+ class ToolExecutor
9
+ SUBSHELL_PATTERN = /\$\(|`/
10
+ DEFAULT_TIMEOUT = 30
11
+ DEFAULT_MAX_OUTPUT_BYTES = 10_240
12
+ DEFAULT_MAX_TOOL_ROUNDS = 10
13
+
14
+ attr_reader :tools, :configuration
15
+
16
+ def initialize(tools:, configuration:)
17
+ @tools = tools.to_h { |t| [t.name, t] }
18
+ @configuration = configuration
19
+ end
20
+
21
+ def tool_definitions_for(provider)
22
+ @tools.values.map do |tool|
23
+ case provider
24
+ when :anthropic then tool.to_anthropic_schema
25
+ when :openai then tool.to_openai_schema
26
+ else raise Error, "Unknown provider for tool schema: #{provider}"
27
+ end
28
+ end
29
+ end
30
+
31
+ def execute(tool_name, input)
32
+ tool = @tools[tool_name]
33
+ raise Error, "Unknown tool: #{tool_name}" unless tool
34
+
35
+ case tool_name
36
+ when 'shell_command'
37
+ execute_shell_command(input)
38
+ else
39
+ tool.execute(input)
40
+ end
41
+ end
42
+
43
+ def max_tool_rounds
44
+ configuration.respond_to?(:max_tool_rounds) ? configuration.max_tool_rounds : DEFAULT_MAX_TOOL_ROUNDS
45
+ end
46
+
47
+ private
48
+
49
+ def execute_shell_command(input)
50
+ command = input['command'] || input[:command]
51
+ raise Error, 'Missing required parameter: command' unless command
52
+ raise Error, 'Command must be a non-empty string' if command.to_s.strip.empty?
53
+
54
+ validate_command!(command)
55
+
56
+ timeout = tool_timeout
57
+ max_bytes = max_output_bytes
58
+
59
+ stdout, stderr, status = run_with_timeout(command, timeout)
60
+
61
+ stdout = truncate_output(stdout, max_bytes)
62
+ stderr = truncate_output(stderr, max_bytes)
63
+
64
+ JSON.generate(stdout: stdout, stderr: stderr, exit_code: status.exitstatus)
65
+ rescue Timeout::Error
66
+ JSON.generate(stdout: '', stderr: "Command timed out after #{timeout}s", exit_code: -1)
67
+ end
68
+
69
+ def validate_command!(command)
70
+ validate_no_subshells!(command)
71
+ binaries = extract_binaries(command)
72
+ validate_allowlist!(binaries)
73
+ end
74
+
75
+ def validate_no_subshells!(command)
76
+ return unless command.match?(SUBSHELL_PATTERN)
77
+
78
+ raise Error, "Command contains subshell execution ($() or backticks) which is not allowed: #{command}"
79
+ end
80
+
81
+ def extract_binaries(command)
82
+ segments = split_on_operators(command)
83
+
84
+ segments.map do |segment|
85
+ segment = segment.strip
86
+
87
+ segment = segment.sub(/\A\s*(?:\w+=\S*\s+)*/, '')
88
+
89
+ tokens = Shellwords.split(segment)
90
+ next nil if tokens.empty?
91
+
92
+ binary = tokens.first
93
+ File.basename(binary)
94
+ rescue ArgumentError
95
+ raise Error, "Malformed command segment: #{segment}"
96
+ end.compact
97
+ end
98
+
99
+ def split_on_operators(command)
100
+ segments = []
101
+ current = +''
102
+ chars = command.chars
103
+ i = 0
104
+ quote = nil
105
+
106
+ while i < chars.length
107
+ char = chars[i]
108
+ quote = toggle_quote(char, quote) if quote_boundary?(char, quote)
109
+
110
+ if !quote && (skip = operator_length(chars, i))
111
+ segments << current
112
+ current = +''
113
+ i += skip
114
+ else
115
+ current << char
116
+ i += 1
117
+ end
118
+ end
119
+
120
+ segments << current unless current.strip.empty?
121
+ segments
122
+ end
123
+
124
+ def quote_boundary?(char, quote)
125
+ ["'", '"'].include?(char) && (quote.nil? || quote == char)
126
+ end
127
+
128
+ def toggle_quote(char, quote)
129
+ quote == char ? nil : char
130
+ end
131
+
132
+ def operator_length(chars, index)
133
+ two_char = "#{chars[index]}#{chars[index + 1]}"
134
+ return 2 if ['&&', '||'].include?(two_char)
135
+ return 1 if [';', '|'].include?(chars[index])
136
+
137
+ nil
138
+ end
139
+
140
+ def validate_allowlist!(binaries)
141
+ allowed = allowed_commands
142
+ return if allowed.empty?
143
+
144
+ binaries.each do |binary|
145
+ next if allowed.include?(binary)
146
+
147
+ raise Error,
148
+ "Command '#{binary}' is not in the allowed commands list. " \
149
+ "Allowed: #{allowed.join(', ')}"
150
+ end
151
+ end
152
+
153
+ def allowed_commands
154
+ safety = configuration.tool_safety
155
+ return [] unless safety
156
+
157
+ safety[:allowed_commands] || safety['allowed_commands'] || []
158
+ end
159
+
160
+ def tool_timeout
161
+ safety = configuration.tool_safety
162
+ return DEFAULT_TIMEOUT unless safety
163
+
164
+ safety[:tool_timeout] || safety['tool_timeout'] || DEFAULT_TIMEOUT
165
+ end
166
+
167
+ def max_output_bytes
168
+ safety = configuration.tool_safety
169
+ return DEFAULT_MAX_OUTPUT_BYTES unless safety
170
+
171
+ safety[:max_output_bytes] || safety['max_output_bytes'] || DEFAULT_MAX_OUTPUT_BYTES
172
+ end
173
+
174
+ def working_directory
175
+ safety = configuration.tool_safety
176
+ return nil unless safety
177
+
178
+ dir = safety[:working_directory] || safety['working_directory']
179
+ dir ? File.expand_path(dir) : nil
180
+ end
181
+
182
+ def run_with_timeout(command, timeout)
183
+ opts = {}
184
+ dir = working_directory
185
+ opts[:chdir] = dir if dir
186
+
187
+ Timeout.timeout(timeout) do
188
+ Open3.capture3(command, **opts)
189
+ end
190
+ end
191
+
192
+ def truncate_output(output, max_bytes)
193
+ return output if output.bytesize <= max_bytes
194
+
195
+ truncated = output.byteslice(0, max_bytes)
196
+ "#{truncated}\n... [truncated at #{max_bytes} bytes]"
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AiSentinel
4
+ module Tools
5
+ REGISTRY = {}.freeze
6
+
7
+ class Base
8
+ def name
9
+ raise NotImplementedError, "#{self.class}#name must be implemented"
10
+ end
11
+
12
+ def description
13
+ raise NotImplementedError, "#{self.class}#description must be implemented"
14
+ end
15
+
16
+ def input_schema
17
+ raise NotImplementedError, "#{self.class}#input_schema must be implemented"
18
+ end
19
+
20
+ def execute(input)
21
+ raise NotImplementedError, "#{self.class}#execute must be implemented"
22
+ end
23
+
24
+ def to_anthropic_schema
25
+ {
26
+ name: name,
27
+ description: description,
28
+ input_schema: input_schema
29
+ }
30
+ end
31
+
32
+ def to_openai_schema
33
+ {
34
+ type: 'function',
35
+ function: {
36
+ name: name,
37
+ description: description,
38
+ parameters: input_schema
39
+ }
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'timeout'
5
+
6
+ module AiSentinel
7
+ module Tools
8
+ class ShellCommand < Base
9
+ def name
10
+ 'shell_command'
11
+ end
12
+
13
+ def description
14
+ 'Execute a shell command on the local machine. Returns stdout, stderr, and the exit code. ' \
15
+ 'Use this to run CLI tools, inspect files, manage processes, or perform any system operation.'
16
+ end
17
+
18
+ def input_schema
19
+ {
20
+ type: 'object',
21
+ properties: {
22
+ command: {
23
+ type: 'string',
24
+ description: 'The shell command to execute (e.g. "ls -la", "git status", "cat file.txt")'
25
+ }
26
+ },
27
+ required: ['command']
28
+ }
29
+ end
30
+
31
+ def execute(input)
32
+ command = input['command'] || input[:command]
33
+ raise Error, 'Missing required parameter: command' unless command
34
+ raise Error, 'Command must be a non-empty string' if command.strip.empty?
35
+
36
+ { stdout: '', stderr: '', exit_code: -1 }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AiSentinel
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AiSentinel
4
+ class Workflow
5
+ attr_reader :name, :schedule_expression, :steps
6
+
7
+ def initialize(name:, schedule_expression:, steps: [])
8
+ @name = name
9
+ @schedule_expression = schedule_expression
10
+ @steps = steps
11
+ end
12
+
13
+ def add_step(step)
14
+ @steps << step
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ require 'dotenv/load'
5
+ require 'logger'
6
+
7
+ require_relative 'ai_sentinel/version'
8
+
9
+ loader = Zeitwerk::Loader.for_gem
10
+ loader.inflector.inflect(
11
+ 'cli' => 'CLI',
12
+ 'dsl' => 'DSL'
13
+ )
14
+ loader.ignore("#{__dir__}/ai_sentinel/version.rb")
15
+ loader.setup
16
+
17
+ module AiSentinel
18
+ class Error < StandardError; end
19
+
20
+ class << self
21
+ def configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+
25
+ def configure
26
+ yield(configuration)
27
+ resolve_api_key
28
+ configuration
29
+ end
30
+
31
+ def registry
32
+ @registry ||= {}
33
+ end
34
+
35
+ def start(daemonize: false)
36
+ resolve_api_key
37
+ configuration.validate!
38
+ Persistence::Database.setup(configuration.database_path)
39
+ scheduler = Scheduler.new(registry, configuration)
40
+ scheduler.start(daemonize: daemonize)
41
+ scheduler
42
+ end
43
+
44
+ def reset!
45
+ @configuration = nil
46
+ @registry = {}
47
+ end
48
+
49
+ def logger
50
+ configuration.logger
51
+ end
52
+
53
+ def resolve_api_key
54
+ configuration.api_key ||= ENV.fetch(configuration.env_key_name, nil)
55
+ end
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ai_sentinel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mario Celi
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dotenv
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rufus-scheduler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.9'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.9'
54
+ - !ruby/object:Gem::Dependency
55
+ name: sequel
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: sqlite3
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: thor
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: zeitwerk
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.6'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.6'
110
+ description: Schedule AI-driven tasks to run at specified times, process results through
111
+ LLMs, and take conditional actions based on the output. Designed to be lightweight
112
+ and self-hostable.
113
+ email:
114
+ - mcelicalderon@gmail.com
115
+ executables:
116
+ - ai_sentinel
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - LICENSE
121
+ - README.md
122
+ - ai_sentinel.yml.example
123
+ - exe/ai_sentinel
124
+ - lib/ai_sentinel.rb
125
+ - lib/ai_sentinel/actions/ai_prompt.rb
126
+ - lib/ai_sentinel/actions/base.rb
127
+ - lib/ai_sentinel/actions/http_get.rb
128
+ - lib/ai_sentinel/actions/http_post.rb
129
+ - lib/ai_sentinel/actions/shell_command.rb
130
+ - lib/ai_sentinel/cli.rb
131
+ - lib/ai_sentinel/cli/context_display.rb
132
+ - lib/ai_sentinel/cli/helpers.rb
133
+ - lib/ai_sentinel/cli/prompt_change_handler.rb
134
+ - lib/ai_sentinel/condition_evaluator.rb
135
+ - lib/ai_sentinel/config_loader.rb
136
+ - lib/ai_sentinel/configuration.rb
137
+ - lib/ai_sentinel/configuration_error.rb
138
+ - lib/ai_sentinel/context.rb
139
+ - lib/ai_sentinel/context_compactor.rb
140
+ - lib/ai_sentinel/dsl.rb
141
+ - lib/ai_sentinel/persistence/database.rb
142
+ - lib/ai_sentinel/persistence/execution_log.rb
143
+ - lib/ai_sentinel/prompt_change_detector.rb
144
+ - lib/ai_sentinel/providers/anthropic.rb
145
+ - lib/ai_sentinel/providers/base.rb
146
+ - lib/ai_sentinel/providers/openai.rb
147
+ - lib/ai_sentinel/runner.rb
148
+ - lib/ai_sentinel/scheduler.rb
149
+ - lib/ai_sentinel/step.rb
150
+ - lib/ai_sentinel/tool_executor.rb
151
+ - lib/ai_sentinel/tools/base.rb
152
+ - lib/ai_sentinel/tools/shell_command.rb
153
+ - lib/ai_sentinel/version.rb
154
+ - lib/ai_sentinel/workflow.rb
155
+ homepage: https://github.com/mcelicalderon/ai_sentinel
156
+ licenses:
157
+ - MIT
158
+ metadata:
159
+ homepage_uri: https://github.com/mcelicalderon/ai_sentinel
160
+ source_code_uri: https://github.com/mcelicalderon/ai_sentinel
161
+ changelog_uri: https://github.com/mcelicalderon/ai_sentinel/blob/main/CHANGELOG.md
162
+ rubygems_mfa_required: 'true'
163
+ rdoc_options: []
164
+ require_paths:
165
+ - lib
166
+ required_ruby_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: 3.1.0
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ requirements: []
177
+ rubygems_version: 3.6.7
178
+ specification_version: 4
179
+ summary: Lightweight AI task scheduler with conditional actions
180
+ test_files: []