aihype 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/README.md +452 -0
- data/bin/aihype +10 -0
- data/bin/mock-claude +77 -0
- data/lib/aihype/ai_matcher.rb +144 -0
- data/lib/aihype/approval_prompt.rb +63 -0
- data/lib/aihype/blacklist.rb +65 -0
- data/lib/aihype/blacklist_rule.rb +43 -0
- data/lib/aihype/cli.rb +170 -0
- data/lib/aihype/core.rb +130 -0
- data/lib/aihype/env.rb +79 -0
- data/lib/aihype/log_entry.rb +43 -0
- data/lib/aihype/logger.rb +101 -0
- data/lib/aihype/memory.rb +131 -0
- data/lib/aihype/memory_file.rb +37 -0
- data/lib/aihype/model_selector.rb +66 -0
- data/lib/aihype/pty_controller.rb +180 -0
- data/lib/aihype/rate_limiter.rb +56 -0
- data/lib/aihype/version.rb +5 -0
- data/lib/aihype.rb +29 -0
- data/lib/defaults.rb +23 -0
- metadata +96 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'kramdown'
|
|
6
|
+
require_relative 'memory_file'
|
|
7
|
+
require_relative '../defaults'
|
|
8
|
+
|
|
9
|
+
module AIHype
|
|
10
|
+
class InvalidMemoryFileError < StandardError; end
|
|
11
|
+
|
|
12
|
+
class Memory
|
|
13
|
+
SEMVER_REGEX = /^\d+\.\d+\.\d+$/
|
|
14
|
+
|
|
15
|
+
def self.load(file_path)
|
|
16
|
+
content = File.read(file_path)
|
|
17
|
+
parse(content)
|
|
18
|
+
rescue Errno::ENOENT
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.parse(content)
|
|
23
|
+
parts = content.split(/^---$/)
|
|
24
|
+
raise InvalidMemoryFileError, 'Missing YAML frontmatter' if parts.length < 3
|
|
25
|
+
|
|
26
|
+
yaml_content = parts[1].strip
|
|
27
|
+
markdown_content = parts[2..].join('---').strip
|
|
28
|
+
|
|
29
|
+
frontmatter = YAML.safe_load(yaml_content, permitted_classes: [Time])
|
|
30
|
+
validate_frontmatter!(frontmatter)
|
|
31
|
+
|
|
32
|
+
default_rules, user_rules = extract_rules(markdown_content)
|
|
33
|
+
|
|
34
|
+
MemoryFile.new(
|
|
35
|
+
version: frontmatter['version'],
|
|
36
|
+
created: parse_datetime(frontmatter['created']),
|
|
37
|
+
updated: parse_datetime(frontmatter['updated']),
|
|
38
|
+
default_rules: default_rules,
|
|
39
|
+
user_rules: user_rules
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.create_default
|
|
44
|
+
now = Time.now.utc
|
|
45
|
+
safety_rules = Defaults::SAFETY_RULES.map { |rule| "- #{rule}" }.join("\n")
|
|
46
|
+
|
|
47
|
+
<<~MARKDOWN
|
|
48
|
+
---
|
|
49
|
+
version: #{Defaults::DEFAULT_VERSION}
|
|
50
|
+
created: #{now.iso8601}
|
|
51
|
+
updated: #{now.iso8601}
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# AIHype Blacklist Rules
|
|
55
|
+
|
|
56
|
+
## Default Safety Rules
|
|
57
|
+
#{safety_rules}
|
|
58
|
+
|
|
59
|
+
## User Rules
|
|
60
|
+
- #{Defaults::USER_RULES_PLACEHOLDER}
|
|
61
|
+
MARKDOWN
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.save(file_path, memory_file)
|
|
65
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
66
|
+
File.write(file_path, memory_file.to_markdown)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.validate_frontmatter!(frontmatter)
|
|
70
|
+
raise InvalidMemoryFileError, 'Missing version field' unless frontmatter['version']
|
|
71
|
+
|
|
72
|
+
version = frontmatter['version'].to_s
|
|
73
|
+
raise InvalidMemoryFileError, 'Invalid semver version' unless version =~ SEMVER_REGEX
|
|
74
|
+
|
|
75
|
+
# Parse or validate datetime fields (they might already be Time objects from YAML)
|
|
76
|
+
parse_datetime(frontmatter['created'])
|
|
77
|
+
parse_datetime(frontmatter['updated'])
|
|
78
|
+
rescue ArgumentError => e
|
|
79
|
+
raise InvalidMemoryFileError, "Invalid datetime: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.parse_datetime(value)
|
|
83
|
+
return value if value.is_a?(Time)
|
|
84
|
+
Time.parse(value)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.extract_rules(markdown_content)
|
|
88
|
+
raise InvalidMemoryFileError, 'Missing "Default Safety Rules" section' unless markdown_content.include?('## Default Safety Rules')
|
|
89
|
+
|
|
90
|
+
sections = markdown_content.split(/^## /)
|
|
91
|
+
default_section = sections.find { |s| s.start_with?('Default Safety Rules') }
|
|
92
|
+
user_section = sections.find { |s| s.start_with?('User Rules') }
|
|
93
|
+
|
|
94
|
+
default_rules = extract_list_items(default_section)
|
|
95
|
+
user_rules = extract_list_items(user_section)
|
|
96
|
+
|
|
97
|
+
validate_rules!(default_rules + user_rules)
|
|
98
|
+
|
|
99
|
+
min_rules = Defaults::MIN_SAFETY_RULES
|
|
100
|
+
raise InvalidMemoryFileError, "Default Safety Rules must contain at least #{min_rules} rules" if default_rules.length < min_rules
|
|
101
|
+
|
|
102
|
+
[default_rules, user_rules]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.extract_list_items(section)
|
|
106
|
+
return [] unless section
|
|
107
|
+
|
|
108
|
+
items = section.split("\n").select { |line| line.strip.start_with?('-') }.map do |line|
|
|
109
|
+
line.sub(/^-\s*/, '').strip
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Validate items before filtering placeholders
|
|
113
|
+
items.each do |item|
|
|
114
|
+
# Skip placeholder validation
|
|
115
|
+
next if item == Defaults::USER_RULES_PLACEHOLDER
|
|
116
|
+
|
|
117
|
+
raise InvalidMemoryFileError, 'Rule cannot be empty' if item.strip.empty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Filter out empty items and placeholders
|
|
121
|
+
items.reject { |item| item.empty? || item == Defaults::USER_RULES_PLACEHOLDER }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.validate_rules!(rules)
|
|
125
|
+
rules.each do |rule|
|
|
126
|
+
raise InvalidMemoryFileError, 'Rule cannot be empty' if rule.strip.empty?
|
|
127
|
+
raise InvalidMemoryFileError, 'Rule exceeds 500 characters' if rule.length > 500
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AIHype
|
|
4
|
+
class MemoryFile
|
|
5
|
+
attr_reader :version, :created, :updated, :default_rules, :user_rules
|
|
6
|
+
|
|
7
|
+
def initialize(version:, created:, updated:, default_rules:, user_rules:)
|
|
8
|
+
@version = version
|
|
9
|
+
@created = created
|
|
10
|
+
@updated = updated
|
|
11
|
+
@default_rules = default_rules
|
|
12
|
+
@user_rules = user_rules
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all_rules
|
|
16
|
+
default_rules + user_rules
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_markdown
|
|
20
|
+
<<~MARKDOWN
|
|
21
|
+
---
|
|
22
|
+
version: #{version}
|
|
23
|
+
created: #{created.iso8601}
|
|
24
|
+
updated: #{updated.iso8601}
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# AIHype Blacklist Rules
|
|
28
|
+
|
|
29
|
+
## Default Safety Rules
|
|
30
|
+
#{default_rules.map { |r| "- #{r}" }.join("\n")}
|
|
31
|
+
|
|
32
|
+
## User Rules
|
|
33
|
+
#{user_rules.empty? ? '- (no custom rules)' : user_rules.map { |r| "- #{r}" }.join("\n")}
|
|
34
|
+
MARKDOWN
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'env'
|
|
6
|
+
|
|
7
|
+
module AIHype
|
|
8
|
+
class ModelSelector
|
|
9
|
+
MODELS_API_URL = 'https://api.anthropic.com/v1/models'
|
|
10
|
+
|
|
11
|
+
# Model selection preferences for different use cases
|
|
12
|
+
PREFERENCES = {
|
|
13
|
+
fast: ->(display_name) { display_name.downcase.include?('haiku') },
|
|
14
|
+
cheap: ->(display_name) { display_name.downcase.include?('haiku') },
|
|
15
|
+
balanced: ->(display_name) { display_name.downcase.include?('sonnet') },
|
|
16
|
+
powerful: ->(display_name) { display_name.downcase.include?('opus') }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(api_key:, preference: :cheap)
|
|
20
|
+
@api_key = api_key
|
|
21
|
+
@preference = preference
|
|
22
|
+
@cache = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def select_model
|
|
26
|
+
# Fetch and cache models
|
|
27
|
+
models = fetch_models
|
|
28
|
+
return nil if models.empty?
|
|
29
|
+
|
|
30
|
+
# Apply preference filter
|
|
31
|
+
preference_filter = PREFERENCES[@preference]
|
|
32
|
+
preferred_models = models.select { |m| preference_filter.call(m['display_name']) }
|
|
33
|
+
|
|
34
|
+
# Return most recent preferred model, or most recent model if no preference match
|
|
35
|
+
(preferred_models.first || models.first)['id']
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
# Fallback to a known stable model if API fetch fails
|
|
38
|
+
warn "Warning: Failed to fetch models (#{e.message}), using fallback"
|
|
39
|
+
'claude-3-5-haiku-20241022'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def fetch_models
|
|
45
|
+
return @cache if @cache
|
|
46
|
+
|
|
47
|
+
conn = Faraday.new(url: MODELS_API_URL) do |f|
|
|
48
|
+
f.response :json
|
|
49
|
+
f.adapter Faraday.default_adapter
|
|
50
|
+
f.options.timeout = 5
|
|
51
|
+
f.options.open_timeout = 5
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
response = conn.get do |req|
|
|
55
|
+
req.headers['x-api-key'] = @api_key
|
|
56
|
+
req.headers['anthropic-version'] = '2023-06-01'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return [] unless response.status == 200
|
|
60
|
+
return [] unless response.body['data']
|
|
61
|
+
|
|
62
|
+
@cache = response.body['data']
|
|
63
|
+
@cache
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pty'
|
|
4
|
+
require 'io/console'
|
|
5
|
+
require_relative 'core'
|
|
6
|
+
require_relative 'blacklist'
|
|
7
|
+
require_relative 'ai_matcher'
|
|
8
|
+
require_relative 'logger'
|
|
9
|
+
|
|
10
|
+
module AIHype
|
|
11
|
+
class PTYController
|
|
12
|
+
attr_reader :command, :args, :blacklist, :ai_matcher, :logger
|
|
13
|
+
|
|
14
|
+
PROMPT_TIMEOUT = 0.5
|
|
15
|
+
BUFFER_LINES = 20
|
|
16
|
+
|
|
17
|
+
def initialize(command:, args: [], config_path:, log_path:, verbose: false)
|
|
18
|
+
@command = command
|
|
19
|
+
@args = args
|
|
20
|
+
@config_path = config_path
|
|
21
|
+
@log_path = log_path
|
|
22
|
+
@verbose = verbose
|
|
23
|
+
@line_buffer = String.new
|
|
24
|
+
@recent_lines = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
# Load components
|
|
29
|
+
@blacklist = Blacklist.from_file(@config_path)
|
|
30
|
+
@ai_matcher = AIMatcher.new
|
|
31
|
+
@logger = Logger.new(log_file_path: @log_path, verbose: @verbose)
|
|
32
|
+
@core = Core.new(blacklist: @blacklist, ai_matcher: @ai_matcher, logger: @logger)
|
|
33
|
+
|
|
34
|
+
@logger.log_startup(@blacklist.enabled_rules.length, @config_path)
|
|
35
|
+
|
|
36
|
+
# Spawn child process via PTY
|
|
37
|
+
PTY.spawn(@command, *@args) do |pty_out, pty_in, pid|
|
|
38
|
+
exit_code = process_pty_io(pty_out, pty_in, pid)
|
|
39
|
+
return exit_code
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
0
|
|
43
|
+
rescue InvalidMemoryFileError, Errno::ENOENT => e
|
|
44
|
+
$stderr.puts "Error: Invalid or missing config file: #{e.message}"
|
|
45
|
+
2
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
$stderr.puts "Error: #{e.message}"
|
|
48
|
+
$stderr.puts e.backtrace.join("\n") if @verbose
|
|
49
|
+
1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def process_pty_io(pty_out, pty_in, pid)
|
|
55
|
+
# Set stdin to raw mode for transparent pass-through (only if truly interactive)
|
|
56
|
+
old_stdin_mode = nil
|
|
57
|
+
stdin_closed = false
|
|
58
|
+
|
|
59
|
+
if $stdin.tty? && !$stdin.closed?
|
|
60
|
+
begin
|
|
61
|
+
old_stdin_mode = $stdin.raw!
|
|
62
|
+
rescue => e
|
|
63
|
+
@logger.log_ai_failure(nil, "Failed to set raw mode: #{e.message}") if @verbose
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
loop do
|
|
68
|
+
# Monitor BOTH child output AND user input (unless stdin closed)
|
|
69
|
+
read_streams = [pty_out]
|
|
70
|
+
read_streams << $stdin unless stdin_closed || $stdin.closed?
|
|
71
|
+
|
|
72
|
+
ready = IO.select(read_streams, nil, nil, PROMPT_TIMEOUT)
|
|
73
|
+
|
|
74
|
+
if ready
|
|
75
|
+
# Handle user input (forward to child)
|
|
76
|
+
if !stdin_closed && ready[0].include?($stdin)
|
|
77
|
+
begin
|
|
78
|
+
user_input = $stdin.read_nonblock(4096)
|
|
79
|
+
@logger.log_user_input(user_input) if @verbose
|
|
80
|
+
pty_in.write(user_input)
|
|
81
|
+
pty_in.flush
|
|
82
|
+
rescue IO::WaitReadable
|
|
83
|
+
# No input available
|
|
84
|
+
rescue EOFError
|
|
85
|
+
# stdin closed - stop monitoring it
|
|
86
|
+
stdin_closed = true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Handle child output (check for prompts)
|
|
91
|
+
if ready[0].include?(pty_out)
|
|
92
|
+
begin
|
|
93
|
+
chunk = pty_out.read_nonblock(4096)
|
|
94
|
+
$stdout.print chunk
|
|
95
|
+
$stdout.flush
|
|
96
|
+
@logger.log_child_output(chunk) if @verbose
|
|
97
|
+
@line_buffer << chunk
|
|
98
|
+
# Check for prompts immediately after receiving output
|
|
99
|
+
check_for_prompt(pty_in)
|
|
100
|
+
rescue IO::WaitReadable
|
|
101
|
+
check_for_prompt(pty_in)
|
|
102
|
+
rescue EOFError, Errno::EIO
|
|
103
|
+
break
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
# Timeout - check if we have a waiting prompt
|
|
108
|
+
check_for_prompt(pty_in)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Process.wait(pid)
|
|
113
|
+
$?.exitstatus || 0
|
|
114
|
+
rescue Errno::ECHILD
|
|
115
|
+
0
|
|
116
|
+
ensure
|
|
117
|
+
# Restore terminal mode
|
|
118
|
+
$stdin.cooked! if old_stdin_mode
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def check_for_prompt(pty_in)
|
|
122
|
+
return if @line_buffer.empty?
|
|
123
|
+
|
|
124
|
+
lines = @line_buffer.split("\n")
|
|
125
|
+
@recent_lines = (@recent_lines + lines).last(BUFFER_LINES)
|
|
126
|
+
|
|
127
|
+
# Check multi-line first
|
|
128
|
+
if @recent_lines.length > 1
|
|
129
|
+
combined = @recent_lines.last(10).join("\n")
|
|
130
|
+
if @core.approval_prompt?(combined)
|
|
131
|
+
handle_detected_prompt(combined, pty_in)
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check last line
|
|
137
|
+
last_line = lines.last || ""
|
|
138
|
+
test_line = last_line.strip
|
|
139
|
+
|
|
140
|
+
if !test_line.empty? && @core.approval_prompt?(test_line)
|
|
141
|
+
unless possibly_incomplete_menu?(test_line)
|
|
142
|
+
handle_detected_prompt(test_line, pty_in)
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@line_buffer = @line_buffer[-1000..-1] if @line_buffer.length > 10000
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def possibly_incomplete_menu?(text)
|
|
151
|
+
text.match?(/do you want|would you like|should i/i) && !text.match?(/1\.\s*yes/i)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def handle_detected_prompt(prompt_text, pty_in)
|
|
155
|
+
result = @core.process_line(prompt_text)
|
|
156
|
+
|
|
157
|
+
if result[:is_prompt]
|
|
158
|
+
response = result[:response]
|
|
159
|
+
|
|
160
|
+
$stderr.puts "[DEBUG] Writing: #{response.inspect}" if @verbose
|
|
161
|
+
|
|
162
|
+
# Write response - handle keyboard sequences with delays
|
|
163
|
+
write_response_with_delays(pty_in, response)
|
|
164
|
+
|
|
165
|
+
# Clear buffers
|
|
166
|
+
@line_buffer.clear
|
|
167
|
+
@recent_lines.clear
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def write_response_with_delays(pty_in, response)
|
|
172
|
+
# Wait briefly to ensure Ink framework is ready for input
|
|
173
|
+
sleep 0.3
|
|
174
|
+
|
|
175
|
+
# Write response
|
|
176
|
+
pty_in.write(response)
|
|
177
|
+
pty_in.flush
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thread'
|
|
4
|
+
|
|
5
|
+
module AIHype
|
|
6
|
+
class RateLimiter
|
|
7
|
+
attr_reader :requests_per_minute, :window_size
|
|
8
|
+
|
|
9
|
+
def initialize(requests_per_minute: 50, window_size: 60)
|
|
10
|
+
@requests_per_minute = requests_per_minute
|
|
11
|
+
@window_size = window_size # seconds
|
|
12
|
+
@requests = []
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Wait if necessary to respect rate limits, then record request
|
|
17
|
+
def throttle
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
now = Time.now
|
|
20
|
+
|
|
21
|
+
# Remove requests outside the current window
|
|
22
|
+
@requests.reject! { |time| now - time > @window_size }
|
|
23
|
+
|
|
24
|
+
# If at limit, wait until oldest request expires
|
|
25
|
+
if @requests.length >= @requests_per_minute
|
|
26
|
+
oldest_request = @requests.first
|
|
27
|
+
sleep_time = @window_size - (now - oldest_request) + 0.1 # Add small buffer
|
|
28
|
+
sleep(sleep_time) if sleep_time > 0
|
|
29
|
+
|
|
30
|
+
# Clean up again after sleeping
|
|
31
|
+
now = Time.now
|
|
32
|
+
@requests.reject! { |time| now - time > @window_size }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Record this request
|
|
36
|
+
@requests << now
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get current request count in window
|
|
41
|
+
def current_count
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
now = Time.now
|
|
44
|
+
@requests.reject! { |time| now - time > @window_size }
|
|
45
|
+
@requests.length
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Reset the rate limiter
|
|
50
|
+
def reset
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
@requests.clear
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/aihype.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'aihype/version'
|
|
4
|
+
require_relative 'defaults'
|
|
5
|
+
|
|
6
|
+
# Configuration
|
|
7
|
+
require_relative 'aihype/env'
|
|
8
|
+
|
|
9
|
+
# Entity classes
|
|
10
|
+
require_relative 'aihype/blacklist_rule'
|
|
11
|
+
require_relative 'aihype/approval_prompt'
|
|
12
|
+
require_relative 'aihype/memory_file'
|
|
13
|
+
require_relative 'aihype/log_entry'
|
|
14
|
+
|
|
15
|
+
# Service modules
|
|
16
|
+
require_relative 'aihype/memory'
|
|
17
|
+
require_relative 'aihype/blacklist'
|
|
18
|
+
require_relative 'aihype/rate_limiter'
|
|
19
|
+
require_relative 'aihype/model_selector'
|
|
20
|
+
require_relative 'aihype/ai_matcher'
|
|
21
|
+
require_relative 'aihype/logger'
|
|
22
|
+
require_relative 'aihype/core'
|
|
23
|
+
|
|
24
|
+
# CLI interface
|
|
25
|
+
require_relative 'aihype/cli'
|
|
26
|
+
|
|
27
|
+
module AIHype
|
|
28
|
+
class Error < StandardError; end
|
|
29
|
+
end
|
data/lib/defaults.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AIHype
|
|
4
|
+
module Defaults
|
|
5
|
+
# Default safety rules to prevent dangerous operations
|
|
6
|
+
# These rules are included in every new memory/aihype.md configuration
|
|
7
|
+
SAFETY_RULES = [
|
|
8
|
+
'Never execute rm -rf / or similar destructive filesystem operations',
|
|
9
|
+
'Never modify critical system files (/etc/passwd, /boot/, /etc/shadow)',
|
|
10
|
+
'Never disable security features or firewalls',
|
|
11
|
+
'Never brick the machine by corrupting bootloader or kernel'
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
# Placeholder text for user rules section
|
|
15
|
+
USER_RULES_PLACEHOLDER = '(add your custom rules here)'
|
|
16
|
+
|
|
17
|
+
# Default version for new configurations
|
|
18
|
+
DEFAULT_VERSION = '1.0.0'
|
|
19
|
+
|
|
20
|
+
# Minimum required safety rules
|
|
21
|
+
MIN_SAFETY_RULES = 3
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: aihype
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- AIHype Contributors
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.7'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.7'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: kramdown
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.4'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.4'
|
|
41
|
+
description: Like "yes | ai" - wraps command-line tools via PTY, auto-answers interactive
|
|
42
|
+
prompts with AI-powered blacklist security
|
|
43
|
+
email:
|
|
44
|
+
- noreply@example.com
|
|
45
|
+
executables:
|
|
46
|
+
- aihype
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- README.md
|
|
51
|
+
- bin/aihype
|
|
52
|
+
- bin/mock-claude
|
|
53
|
+
- lib/aihype.rb
|
|
54
|
+
- lib/aihype/ai_matcher.rb
|
|
55
|
+
- lib/aihype/approval_prompt.rb
|
|
56
|
+
- lib/aihype/blacklist.rb
|
|
57
|
+
- lib/aihype/blacklist_rule.rb
|
|
58
|
+
- lib/aihype/cli.rb
|
|
59
|
+
- lib/aihype/core.rb
|
|
60
|
+
- lib/aihype/env.rb
|
|
61
|
+
- lib/aihype/log_entry.rb
|
|
62
|
+
- lib/aihype/logger.rb
|
|
63
|
+
- lib/aihype/memory.rb
|
|
64
|
+
- lib/aihype/memory_file.rb
|
|
65
|
+
- lib/aihype/model_selector.rb
|
|
66
|
+
- lib/aihype/pty_controller.rb
|
|
67
|
+
- lib/aihype/rate_limiter.rb
|
|
68
|
+
- lib/aihype/version.rb
|
|
69
|
+
- lib/defaults.rb
|
|
70
|
+
homepage: https://github.com/ahoward/aihype
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
homepage_uri: https://github.com/ahoward/aihype
|
|
75
|
+
source_code_uri: https://github.com/ahoward/aihype
|
|
76
|
+
changelog_uri: https://github.com/ahoward/aihype/blob/main/CHANGELOG.md
|
|
77
|
+
post_install_message:
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: 3.0.0
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements: []
|
|
92
|
+
rubygems_version: 3.5.22
|
|
93
|
+
signing_key:
|
|
94
|
+
specification_version: 4
|
|
95
|
+
summary: Auto-approve interactive prompts with blacklist protection
|
|
96
|
+
test_files: []
|