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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +696 -0
- data/ai_sentinel.yml.example +111 -0
- data/exe/ai_sentinel +5 -0
- data/lib/ai_sentinel/actions/ai_prompt.rb +63 -0
- data/lib/ai_sentinel/actions/base.rb +44 -0
- data/lib/ai_sentinel/actions/http_get.rb +36 -0
- data/lib/ai_sentinel/actions/http_post.rb +47 -0
- data/lib/ai_sentinel/actions/shell_command.rb +51 -0
- data/lib/ai_sentinel/cli/context_display.rb +79 -0
- data/lib/ai_sentinel/cli/helpers.rb +88 -0
- data/lib/ai_sentinel/cli/prompt_change_handler.rb +67 -0
- data/lib/ai_sentinel/cli.rb +135 -0
- data/lib/ai_sentinel/condition_evaluator.rb +58 -0
- data/lib/ai_sentinel/config_loader.rb +164 -0
- data/lib/ai_sentinel/configuration.rb +113 -0
- data/lib/ai_sentinel/configuration_error.rb +5 -0
- data/lib/ai_sentinel/context.rb +34 -0
- data/lib/ai_sentinel/context_compactor.rb +130 -0
- data/lib/ai_sentinel/dsl.rb +27 -0
- data/lib/ai_sentinel/persistence/database.rb +116 -0
- data/lib/ai_sentinel/persistence/execution_log.rb +104 -0
- data/lib/ai_sentinel/prompt_change_detector.rb +75 -0
- data/lib/ai_sentinel/providers/anthropic.rb +151 -0
- data/lib/ai_sentinel/providers/base.rb +120 -0
- data/lib/ai_sentinel/providers/openai.rb +138 -0
- data/lib/ai_sentinel/runner.rb +77 -0
- data/lib/ai_sentinel/scheduler.rb +56 -0
- data/lib/ai_sentinel/step.rb +24 -0
- data/lib/ai_sentinel/tool_executor.rb +199 -0
- data/lib/ai_sentinel/tools/base.rb +44 -0
- data/lib/ai_sentinel/tools/shell_command.rb +40 -0
- data/lib/ai_sentinel/version.rb +5 -0
- data/lib/ai_sentinel/workflow.rb +17 -0
- data/lib/ai_sentinel.rb +57 -0
- 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,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
|
data/lib/ai_sentinel.rb
ADDED
|
@@ -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: []
|