openclacky 0.5.1

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.clackyrules +80 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +8 -0
  5. data/CHANGELOG.md +74 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +272 -0
  9. data/Rakefile +38 -0
  10. data/bin/clacky +7 -0
  11. data/bin/openclacky +6 -0
  12. data/clacky-legacy/clacky.gemspec +24 -0
  13. data/clacky-legacy/clarky.gemspec +24 -0
  14. data/lib/clacky/agent.rb +1015 -0
  15. data/lib/clacky/agent_config.rb +47 -0
  16. data/lib/clacky/cli.rb +713 -0
  17. data/lib/clacky/client.rb +159 -0
  18. data/lib/clacky/config.rb +43 -0
  19. data/lib/clacky/conversation.rb +41 -0
  20. data/lib/clacky/hook_manager.rb +61 -0
  21. data/lib/clacky/progress_indicator.rb +53 -0
  22. data/lib/clacky/session_manager.rb +124 -0
  23. data/lib/clacky/thinking_verbs.rb +26 -0
  24. data/lib/clacky/tool_registry.rb +44 -0
  25. data/lib/clacky/tools/base.rb +64 -0
  26. data/lib/clacky/tools/edit.rb +100 -0
  27. data/lib/clacky/tools/file_reader.rb +79 -0
  28. data/lib/clacky/tools/glob.rb +93 -0
  29. data/lib/clacky/tools/grep.rb +169 -0
  30. data/lib/clacky/tools/run_project.rb +287 -0
  31. data/lib/clacky/tools/safe_shell.rb +396 -0
  32. data/lib/clacky/tools/shell.rb +305 -0
  33. data/lib/clacky/tools/todo_manager.rb +228 -0
  34. data/lib/clacky/tools/trash_manager.rb +371 -0
  35. data/lib/clacky/tools/web_fetch.rb +161 -0
  36. data/lib/clacky/tools/web_search.rb +138 -0
  37. data/lib/clacky/tools/write.rb +65 -0
  38. data/lib/clacky/trash_directory.rb +112 -0
  39. data/lib/clacky/utils/arguments_parser.rb +139 -0
  40. data/lib/clacky/utils/limit_stack.rb +80 -0
  41. data/lib/clacky/utils/path_helper.rb +15 -0
  42. data/lib/clacky/version.rb +5 -0
  43. data/lib/clacky.rb +38 -0
  44. data/sig/clacky.rbs +4 -0
  45. metadata +160 -0
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ module Tools
5
+ class Write < Base
6
+ self.tool_name = "write"
7
+ self.tool_description = "Write content to a file. Creates new files or overwrites existing ones."
8
+ self.tool_category = "file_system"
9
+ self.tool_parameters = {
10
+ type: "object",
11
+ properties: {
12
+ path: {
13
+ type: "string",
14
+ description: "The path of the file to write (absolute or relative)"
15
+ },
16
+ content: {
17
+ type: "string",
18
+ description: "The content to write to the file"
19
+ }
20
+ },
21
+ required: %w[path content]
22
+ }
23
+
24
+ def execute(path:, content:)
25
+ # Validate path
26
+ if path.nil? || path.strip.empty?
27
+ return { error: "Path cannot be empty" }
28
+ end
29
+
30
+ begin
31
+ # Ensure parent directory exists
32
+ dir = File.dirname(path)
33
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
34
+
35
+ # Write content to file
36
+ File.write(path, content)
37
+
38
+ {
39
+ path: File.expand_path(path),
40
+ bytes_written: content.bytesize,
41
+ error: nil
42
+ }
43
+ rescue Errno::EACCES => e
44
+ { error: "Permission denied: #{e.message}" }
45
+ rescue Errno::ENOSPC => e
46
+ { error: "No space left on device: #{e.message}" }
47
+ rescue StandardError => e
48
+ { error: "Failed to write file: #{e.message}" }
49
+ end
50
+ end
51
+
52
+ def format_call(args)
53
+ path = args[:path] || args['path']
54
+ "Write(#{Utils::PathHelper.safe_basename(path)})"
55
+ end
56
+
57
+ def format_result(result)
58
+ return result[:error] if result[:error]
59
+
60
+ bytes = result[:bytes_written] || result['bytes_written'] || 0
61
+ "Written #{bytes} bytes"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+
6
+ module Clacky
7
+ # Manages global trash directory at ~/.clacky/trash
8
+ # Organizes trash by project directory using path hash
9
+ class TrashDirectory
10
+ GLOBAL_TRASH_ROOT = File.join(Dir.home, ".clacky", "trash")
11
+
12
+ attr_reader :project_root, :trash_dir, :backup_dir
13
+
14
+ def initialize(project_root = Dir.pwd)
15
+ @project_root = File.expand_path(project_root)
16
+ @project_hash = generate_project_hash(@project_root)
17
+ @trash_dir = File.join(GLOBAL_TRASH_ROOT, @project_hash)
18
+ @backup_dir = File.join(@trash_dir, "backups")
19
+
20
+ setup_directories
21
+ end
22
+
23
+ # Generate a unique hash for project path
24
+ def generate_project_hash(path)
25
+ # Use MD5 hash of the absolute path, take first 16 chars for readability
26
+ hash = Digest::MD5.hexdigest(path)[0..15]
27
+ # Also include a readable suffix based on project name
28
+ project_name = File.basename(path).gsub(/[^a-zA-Z0-9_-]/, '_')[0..20]
29
+ "#{hash}_#{project_name}"
30
+ end
31
+
32
+ # Setup trash and backup directories with proper structure
33
+ def setup_directories
34
+ [@trash_dir, @backup_dir].each do |dir|
35
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
36
+
37
+ # Create .gitignore file to avoid trash files being committed
38
+ gitignore_path = File.join(dir, '.gitignore')
39
+ unless File.exist?(gitignore_path)
40
+ File.write(gitignore_path, "*\n!.gitignore\n")
41
+ end
42
+ end
43
+
44
+ # Create project metadata file
45
+ create_project_metadata
46
+ end
47
+
48
+ # Create or update metadata about this project
49
+ def create_project_metadata
50
+ metadata_file = File.join(@trash_dir, '.project_metadata.json')
51
+
52
+ metadata = {
53
+ project_root: @project_root,
54
+ project_name: File.basename(@project_root),
55
+ project_hash: @project_hash,
56
+ created_at: File.exist?(metadata_file) ? JSON.parse(File.read(metadata_file))['created_at'] : Time.now.iso8601,
57
+ last_accessed: Time.now.iso8601
58
+ }
59
+
60
+ File.write(metadata_file, JSON.pretty_generate(metadata))
61
+ rescue StandardError => e
62
+ # Log warning but don't block operation
63
+ warn "Warning: Could not create project metadata: #{e.message}"
64
+ end
65
+
66
+ # Get all project directories that have trash
67
+ def self.all_projects
68
+ return [] unless Dir.exist?(GLOBAL_TRASH_ROOT)
69
+
70
+ projects = []
71
+ Dir.glob(File.join(GLOBAL_TRASH_ROOT, "*", ".project_metadata.json")).each do |metadata_file|
72
+ begin
73
+ metadata = JSON.parse(File.read(metadata_file))
74
+ projects << {
75
+ project_root: metadata['project_root'],
76
+ project_name: metadata['project_name'],
77
+ project_hash: metadata['project_hash'],
78
+ trash_dir: File.dirname(metadata_file),
79
+ last_accessed: metadata['last_accessed']
80
+ }
81
+ rescue StandardError
82
+ # Skip corrupted metadata
83
+ end
84
+ end
85
+
86
+ projects.sort_by { |p| p[:last_accessed] }.reverse
87
+ end
88
+
89
+ # Get trash directory for a specific project
90
+ def self.for_project(project_root)
91
+ new(project_root)
92
+ end
93
+
94
+ # Clean up trash directories for non-existent projects
95
+ def self.cleanup_orphaned_projects
96
+ return 0 unless Dir.exist?(GLOBAL_TRASH_ROOT)
97
+
98
+ cleaned_count = 0
99
+ all_projects.each do |project|
100
+ unless Dir.exist?(project[:project_root])
101
+ # Project no longer exists, optionally remove trash
102
+ # For safety, we'll just mark it as orphaned
103
+ orphan_file = File.join(project[:trash_dir], '.orphaned')
104
+ File.write(orphan_file, "Original project path no longer exists: #{project[:project_root]}\n")
105
+ cleaned_count += 1
106
+ end
107
+ end
108
+
109
+ cleaned_count
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Clacky
6
+ module Utils
7
+ class ArgumentsParser
8
+ # Parse and validate tool call arguments with JSON repair capability
9
+ def self.parse_and_validate(call, tool_registry)
10
+ # 1. Try standard parsing
11
+ begin
12
+ args = JSON.parse(call[:arguments], symbolize_names: true)
13
+ return validate_required_params(call, args, tool_registry)
14
+ rescue JSON::ParserError => e
15
+ # Continue to repair
16
+ end
17
+
18
+ # 2. Try simple repair
19
+ repaired = repair_json(call[:arguments])
20
+
21
+ begin
22
+ args = JSON.parse(repaired, symbolize_names: true)
23
+ return validate_required_params(call, args, tool_registry)
24
+ rescue JSON::ParserError, MissingRequiredParamsError => e
25
+ # 3. Repair failed or missing params, return helpful error
26
+ raise_helpful_error(call, tool_registry, e)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # Simple JSON repair: complete brackets and quotes
33
+ def self.repair_json(json_str)
34
+ result = json_str.strip
35
+
36
+ # Complete unclosed strings
37
+ result += '"' if result.count('"').odd?
38
+
39
+ # Complete unclosed braces
40
+ depth = 0
41
+ result.each_char { |c| depth += 1 if c == '{'; depth -= 1 if c == '}' }
42
+ result += '}' * depth if depth > 0
43
+
44
+ result
45
+ end
46
+
47
+ # Validate required parameters
48
+ def self.validate_required_params(call, args, tool_registry)
49
+ tool = tool_registry.get(call[:name])
50
+ required = tool.parameters&.dig(:required) || []
51
+
52
+ missing = required.reject { |param|
53
+ args.key?(param.to_sym) || args.key?(param.to_s)
54
+ }
55
+
56
+ if missing.any?
57
+ raise MissingRequiredParamsError.new(call[:name], missing, args.keys)
58
+ end
59
+
60
+ args
61
+ end
62
+
63
+ # Generate error message with tool definition
64
+ def self.raise_helpful_error(call, tool_registry, original_error)
65
+ tool = tool_registry.get(call[:name])
66
+ error_msg = build_error_message(call, tool, original_error)
67
+ raise StandardError, error_msg
68
+ end
69
+
70
+ def self.build_error_message(call, tool, original_error)
71
+ # Extract tool information
72
+ required_params = tool.parameters&.dig(:required) || []
73
+
74
+ # Try to parse provided parameters from incomplete JSON
75
+ provided_params = extract_provided_params(call[:arguments])
76
+
77
+ # Build clear error message
78
+ msg = []
79
+ msg << "Failed to parse arguments for tool '#{call[:name]}'."
80
+ msg << ""
81
+ msg << "Error: #{original_error.message}"
82
+ msg << ""
83
+
84
+ if provided_params.any?
85
+ msg << "Provided parameters: #{provided_params.join(', ')}"
86
+ else
87
+ msg << "No valid parameters could be extracted."
88
+ end
89
+
90
+ msg << "Required parameters: #{required_params.join(', ')}"
91
+ msg << ""
92
+ msg << "Tool definition:"
93
+ msg << format_tool_definition(tool)
94
+ msg << ""
95
+ msg << "Suggestions:"
96
+ msg << "- If the parameter value is too large (e.g., large file content), consider breaking it into smaller operations"
97
+ msg << "- Ensure all required parameters are provided"
98
+ msg << "- Simplify complex parameter values"
99
+
100
+ msg.join("\n")
101
+ end
102
+
103
+ # Extract parameter names from incomplete JSON
104
+ def self.extract_provided_params(json_str)
105
+ # Simple extraction: find all "key": patterns
106
+ json_str.scan(/"(\w+)"\s*:/).flatten.uniq
107
+ end
108
+
109
+ # Format tool definition (concise version)
110
+ def self.format_tool_definition(tool)
111
+ lines = []
112
+ lines << " Name: #{tool.name}"
113
+ lines << " Description: #{tool.description}"
114
+
115
+ if tool.parameters[:properties]
116
+ lines << " Parameters:"
117
+ tool.parameters[:properties].each do |param, spec|
118
+ required_mark = tool.parameters[:required]&.include?(param.to_s) ? " (required)" : ""
119
+ lines << " - #{param}#{required_mark}: #{spec[:description]}"
120
+ end
121
+ end
122
+
123
+ lines.join("\n")
124
+ end
125
+ end
126
+
127
+ # Custom exception for missing required parameters
128
+ class MissingRequiredParamsError < StandardError
129
+ attr_reader :tool_name, :missing_params, :provided_params
130
+
131
+ def initialize(tool_name, missing_params, provided_params)
132
+ @tool_name = tool_name
133
+ @missing_params = missing_params
134
+ @provided_params = provided_params
135
+ super("Missing required parameters: #{missing_params.join(', ')}")
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ module Utils
5
+ # Auto-rolling fixed-size array
6
+ # Automatically discards oldest elements when size limit is exceeded
7
+ class LimitStack
8
+ attr_reader :max_size, :items
9
+
10
+ def initialize(max_size: 5000)
11
+ @max_size = max_size
12
+ @items = []
13
+ end
14
+
15
+ # Add elements (supports single or multiple)
16
+ def push(*elements)
17
+ elements.each do |element|
18
+ @items << element
19
+ trim_if_needed
20
+ end
21
+ self
22
+ end
23
+ alias_method :<<, :push
24
+
25
+ # Add multi-line text (split by lines and add)
26
+ def push_lines(text)
27
+ return self if text.nil? || text.empty?
28
+
29
+ lines = text.is_a?(Array) ? text : text.lines
30
+ lines.each { |line| push(line) }
31
+ self
32
+ end
33
+
34
+ # Get last N elements
35
+ def last(n = nil)
36
+ n ? @items.last(n) : @items.last
37
+ end
38
+
39
+ # Get all elements
40
+ def to_a
41
+ @items.dup
42
+ end
43
+
44
+ # Convert to string (for text content)
45
+ def to_s
46
+ @items.join
47
+ end
48
+
49
+ # Current size
50
+ def size
51
+ @items.size
52
+ end
53
+
54
+ # Check if empty
55
+ def empty?
56
+ @items.empty?
57
+ end
58
+
59
+ # Clear all elements
60
+ def clear
61
+ @items.clear
62
+ self
63
+ end
64
+
65
+ # Iterate over elements
66
+ def each(&block)
67
+ @items.each(&block)
68
+ end
69
+
70
+ private
71
+
72
+ def trim_if_needed
73
+ if @items.size > @max_size
74
+ # Remove oldest elements, keep only the latest max_size items
75
+ @items.shift(@items.size - @max_size)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ module Utils
5
+ module PathHelper
6
+ # Safely get basename from path, return placeholder if path is nil
7
+ def self.safe_basename(path, placeholder: "?")
8
+ return placeholder if path.nil? || path.to_s.empty?
9
+ File.basename(path.to_s)
10
+ rescue StandardError
11
+ placeholder
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ VERSION = "0.5.1"
5
+ end
data/lib/clacky.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "clacky/version"
4
+ require_relative "clacky/config"
5
+ require_relative "clacky/client"
6
+ require_relative "clacky/conversation"
7
+
8
+ # Agent system
9
+ require_relative "clacky/agent_config"
10
+ require_relative "clacky/hook_manager"
11
+ require_relative "clacky/tool_registry"
12
+ require_relative "clacky/thinking_verbs"
13
+ require_relative "clacky/progress_indicator"
14
+ require_relative "clacky/session_manager"
15
+ require_relative "clacky/utils/limit_stack"
16
+ require_relative "clacky/utils/path_helper"
17
+ require_relative "clacky/tools/base"
18
+
19
+ require_relative "clacky/tools/shell"
20
+ require_relative "clacky/tools/file_reader"
21
+ require_relative "clacky/tools/write"
22
+ require_relative "clacky/tools/edit"
23
+ require_relative "clacky/tools/glob"
24
+ require_relative "clacky/tools/grep"
25
+ require_relative "clacky/tools/web_search"
26
+ require_relative "clacky/tools/web_fetch"
27
+ require_relative "clacky/tools/todo_manager"
28
+ require_relative "clacky/tools/run_project"
29
+ require_relative "clacky/tools/safe_shell"
30
+ require_relative "clacky/tools/trash_manager"
31
+ require_relative "clacky/agent"
32
+
33
+ require_relative "clacky/cli"
34
+
35
+ module Clacky
36
+ class Error < StandardError; end
37
+ class AgentInterrupted < StandardError; end
38
+ end
data/sig/clacky.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Clacky
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openclacky
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
5
+ platform: ruby
6
+ authors:
7
+ - windy
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: thor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: tty-prompt
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.23'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.23'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tty-spinner
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.9'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.9'
68
+ - !ruby/object:Gem::Dependency
69
+ name: diffy
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.4'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.4'
82
+ description: OpenClacky is a Ruby CLI tool for interacting with AI models via OpenAI-compatible
83
+ APIs. It provides chat functionality and autonomous AI agent capabilities with tool
84
+ use.
85
+ email:
86
+ - yafei@dao42.com
87
+ executables:
88
+ - clacky
89
+ - openclacky
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - ".clackyrules"
94
+ - ".rspec"
95
+ - ".rubocop.yml"
96
+ - CHANGELOG.md
97
+ - CODE_OF_CONDUCT.md
98
+ - LICENSE.txt
99
+ - README.md
100
+ - Rakefile
101
+ - bin/clacky
102
+ - bin/openclacky
103
+ - clacky-legacy/clacky.gemspec
104
+ - clacky-legacy/clarky.gemspec
105
+ - lib/clacky.rb
106
+ - lib/clacky/agent.rb
107
+ - lib/clacky/agent_config.rb
108
+ - lib/clacky/cli.rb
109
+ - lib/clacky/client.rb
110
+ - lib/clacky/config.rb
111
+ - lib/clacky/conversation.rb
112
+ - lib/clacky/hook_manager.rb
113
+ - lib/clacky/progress_indicator.rb
114
+ - lib/clacky/session_manager.rb
115
+ - lib/clacky/thinking_verbs.rb
116
+ - lib/clacky/tool_registry.rb
117
+ - lib/clacky/tools/base.rb
118
+ - lib/clacky/tools/edit.rb
119
+ - lib/clacky/tools/file_reader.rb
120
+ - lib/clacky/tools/glob.rb
121
+ - lib/clacky/tools/grep.rb
122
+ - lib/clacky/tools/run_project.rb
123
+ - lib/clacky/tools/safe_shell.rb
124
+ - lib/clacky/tools/shell.rb
125
+ - lib/clacky/tools/todo_manager.rb
126
+ - lib/clacky/tools/trash_manager.rb
127
+ - lib/clacky/tools/web_fetch.rb
128
+ - lib/clacky/tools/web_search.rb
129
+ - lib/clacky/tools/write.rb
130
+ - lib/clacky/trash_directory.rb
131
+ - lib/clacky/utils/arguments_parser.rb
132
+ - lib/clacky/utils/limit_stack.rb
133
+ - lib/clacky/utils/path_helper.rb
134
+ - lib/clacky/version.rb
135
+ - sig/clacky.rbs
136
+ homepage: https://github.com/yafeilee/clacky
137
+ licenses:
138
+ - MIT
139
+ metadata:
140
+ homepage_uri: https://github.com/yafeilee/clacky
141
+ source_code_uri: https://github.com/yafeilee/clacky
142
+ changelog_uri: https://github.com/yafeilee/clacky/blob/main/CHANGELOG.md
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: 3.1.0
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 3.6.9
158
+ specification_version: 4
159
+ summary: A command-line interface for AI models (Claude, OpenAI, etc.)
160
+ test_files: []