genie_cli 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +39 -28
- data/lib/genie/session.rb +31 -1
- data/lib/genie/session_config.rb +16 -11
- data/lib/genie/version.rb +1 -1
- data/lib/genie.rb +3 -1
- data/lib/tools/list_files.rb +10 -0
- data/test/test_session_config.rb +3 -41
- data/test/tools/test_list_files.rb +13 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9431dda686ce68a45532465d2d0f75baabb62e7dee4dc5278d51cdbd0cf00b7
|
4
|
+
data.tar.gz: 86a77baeb167e1bdf058d020368a9069599f44c69fee4d290830bc02fb83cf96
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8cc795dbe839e79b4e2d859051f2bb7c7fdcbaad70c50d17ded96d581aca5fe15cf2482c6d13fd39657f35872e0f997deb7a1bda4f54ad9b89c619600508cffa
|
7
|
+
data.tar.gz: d9a87bb13bfd5e679baa1934904c348e6e957b7737211c06992d85439e1740cc52e2be77cd5820fd47279d5c04ae5c4582266e6df29afa906e812b8206f3879d
|
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# Genie CLI
|
2
2
|
|
3
|
-
Genie CLI is a command-line tool that brings Test Driven Development (TDD)
|
4
|
-
principles to life by integrating with a large language model (LLM) through
|
5
|
-
the `ruby_llm` library. It provides an interactive session where you can ask
|
6
|
-
the AI assistant to write tests, implement code, and manage your codebase—all
|
3
|
+
Genie CLI is a command-line tool that brings Test Driven Development (TDD)
|
4
|
+
principles to life by integrating with a large language model (LLM) through
|
5
|
+
the `ruby_llm` library. It provides an interactive session where you can ask
|
6
|
+
the AI assistant to write tests, implement code, and manage your codebase—all
|
7
7
|
while enforcing a strict TDD workflow.
|
8
8
|
|
9
9
|
## Features
|
@@ -23,33 +23,33 @@ while enforcing a strict TDD workflow.
|
|
23
23
|
|
24
24
|
## Installation
|
25
25
|
|
26
|
-
1.
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
```
|
26
|
+
1. Install the gem:
|
27
|
+
```bash
|
28
|
+
gem install genie_cli
|
29
|
+
```
|
31
30
|
|
32
|
-
2.
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
2. Set your OpenAI API key (or other LLM provider keys) in your environment:
|
32
|
+
```bash
|
33
|
+
export OPENAI_API_KEY="your_api_key_here"
|
34
|
+
```
|
35
|
+
|
36
|
+
`dotenv` is also supported:
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
4. Make the CLI executable:
|
43
|
-
```bash
|
44
|
-
chmod +x bin/genie
|
45
|
-
```
|
38
|
+
```
|
39
|
+
# .env
|
40
|
+
OPENAI_API_KEY="your_api_key_here"
|
41
|
+
```
|
46
42
|
|
47
43
|
## Usage
|
48
44
|
|
49
45
|
Start a Genie session by running the `genie` command from the root of your project:
|
50
46
|
|
51
47
|
```bash
|
52
|
-
|
48
|
+
genie "initial prompt or command"
|
49
|
+
```
|
50
|
+
If you are using Bundler, you can run:
|
51
|
+
```bash
|
52
|
+
bundle exec genie "initial prompt or command"
|
53
53
|
```
|
54
54
|
|
55
55
|
- If you provide an initial prompt, the assistant will immediately respond. Otherwise, you'll enter an interactive prompt where you can type your questions or commands.
|
@@ -58,7 +58,7 @@ Start a Genie session by running the `genie` command from the root of your proje
|
|
58
58
|
Example session:
|
59
59
|
|
60
60
|
```bash
|
61
|
-
$
|
61
|
+
$ genie
|
62
62
|
Starting a new session with:
|
63
63
|
base_path: /Users/you/projects/genie_cli
|
64
64
|
|
@@ -81,14 +81,14 @@ Total Conversation Tokens: 1234
|
|
81
81
|
|
82
82
|
## Logging
|
83
83
|
|
84
|
-
The output of `genie` to the terminal includes "essential" output, but not
|
85
|
-
_all_ output. To aid in debugging, the full RubyLLM debug log is saved to
|
86
|
-
`ruby_llm.log`. This can be useful for auditing what's happened during a
|
84
|
+
The output of `genie` to the terminal includes "essential" output, but not
|
85
|
+
_all_ output. To aid in debugging, the full RubyLLM debug log is saved to
|
86
|
+
`ruby_llm.log`. This can be useful for auditing what's happened during a
|
87
87
|
session in great detail.
|
88
88
|
|
89
89
|
## Configuration
|
90
90
|
|
91
|
-
Configuration is available via a `genie.yml
|
91
|
+
Configuration is available via a `genie.yml`.
|
92
92
|
|
93
93
|
## Testing
|
94
94
|
|
@@ -105,3 +105,14 @@ Contributions are welcome! Please fork the repository and open pull requests for
|
|
105
105
|
## License
|
106
106
|
|
107
107
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
108
|
+
|
109
|
+
## Why "Genie"?
|
110
|
+
|
111
|
+
[Kent Beck](https://tidyfirst.substack.com) has been using this term to
|
112
|
+
describe LLMs, especially coding agents, because it grants your wishes, but
|
113
|
+
rarely in the way you actually wanted it to. It's a great idea, so I'm
|
114
|
+
shamelessly stealing it.
|
115
|
+
|
116
|
+
## The Lamp
|
117
|
+
|
118
|
+
From https://www.asciiart.website/index.php?art=movies%2Faladdin
|
data/lib/genie/session.rb
CHANGED
@@ -15,7 +15,11 @@ module Genie
|
|
15
15
|
def initialize(config:)
|
16
16
|
@config = config
|
17
17
|
|
18
|
-
Genie.output
|
18
|
+
Genie.output genie_lamp, color: :yellow, include_genie_icon: false
|
19
|
+
|
20
|
+
Genie.output "Your wish is my command!", color: :magenta
|
21
|
+
Genie.output " base_path: #{base_path}", color: :cyan, include_genie_icon: false
|
22
|
+
Genie.output " model: #{model}", color: :cyan, include_genie_icon: false
|
19
23
|
|
20
24
|
# Initialize the LLM chat with the specified model
|
21
25
|
@chat = RubyLLM.chat(model: model)
|
@@ -79,5 +83,31 @@ module Genie
|
|
79
83
|
exit
|
80
84
|
end
|
81
85
|
|
86
|
+
private
|
87
|
+
|
88
|
+
def genie_lamp
|
89
|
+
<<-LAMP
|
90
|
+
..
|
91
|
+
dP/$.
|
92
|
+
$4$$%
|
93
|
+
.ee$$ee.
|
94
|
+
.eF3??????$C$r. .d$$$$$$$$$$$e.
|
95
|
+
.zeez$$$$$be.. JP3F$5'$5K$?K?Je$. d$$$FCLze.CC?$$$e
|
96
|
+
"""??$$$$$$$$ee.. .e$$$e$CC$???$$CC3e$$$$. $$$/$$$$$$$$$.$$$$
|
97
|
+
`"?$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$b $$"$$$$P?CCe$$$$$F
|
98
|
+
"?$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$b$$J?bd$$$$$$$$$F"
|
99
|
+
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$d$$F"
|
100
|
+
"?$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"...
|
101
|
+
"?$$$$$$$$$$$$$$$$$$$$$$$$$F "$$"$$$$b
|
102
|
+
"?$$$$$$$$$$$$$$$$$$F" ?$$$$$F
|
103
|
+
""????????C"
|
104
|
+
e$$$$$$$$$$$$.
|
105
|
+
.$b CC$????$$F3eF
|
106
|
+
4$bC/%$bdd$b@$Pd??Jbbr
|
107
|
+
""?$$$$eeee$$$$F?"
|
108
|
+
|
109
|
+
LAMP
|
110
|
+
end
|
111
|
+
|
82
112
|
end
|
83
113
|
end
|
data/lib/genie/session_config.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
require 'yaml'
|
2
|
-
|
3
1
|
module Genie
|
4
2
|
# Handles loading of session configuration such as run_tests_cmd
|
5
3
|
class SessionConfig
|
6
4
|
# Read-only attributes
|
7
|
-
attr_reader :base_path, :run_tests_cmd, :model, :first_question, :instructions
|
5
|
+
attr_reader :base_path, :run_tests_cmd, :model, :first_question, :instructions, :ignore_paths
|
8
6
|
|
9
7
|
DEFAULT_INSTRUCTIONS = <<~INSTRUCTIONS
|
10
8
|
# Genie Instructions
|
@@ -24,7 +22,8 @@ module Genie
|
|
24
22
|
run_tests_cmd: 'rake test',
|
25
23
|
model: 'gpt-4o-mini',
|
26
24
|
first_question: nil,
|
27
|
-
instructions: DEFAULT_INSTRUCTIONS
|
25
|
+
instructions: DEFAULT_INSTRUCTIONS,
|
26
|
+
ignore_paths: ['tmp', '/tmp'],
|
28
27
|
}
|
29
28
|
|
30
29
|
def self.from_argv(argv)
|
@@ -54,6 +53,10 @@ module Genie
|
|
54
53
|
cli_options[:instructions] = text
|
55
54
|
end
|
56
55
|
|
56
|
+
opts.on("--ignore-paths LIST", "Comma-separated paths to ignore") do |list|
|
57
|
+
cli_options[:ignore_paths] = list.split(',').map(&:strip)
|
58
|
+
end
|
59
|
+
|
57
60
|
opts.on("-v", "--[no-]verbose", "Enable verbose mode") do |v|
|
58
61
|
cli_options['verbose'] = v
|
59
62
|
end
|
@@ -73,10 +76,10 @@ module Genie
|
|
73
76
|
# We always preface the instructions with context
|
74
77
|
final_config[:instructions] = <<~PREFACE
|
75
78
|
# Context
|
76
|
-
Current Date and Time:
|
77
|
-
We are working in a codebase located at '
|
79
|
+
Current Date and Time: \\#{Time.now.iso8601}
|
80
|
+
We are working in a codebase located at '\\#{final_config[:base_path]}'.
|
78
81
|
|
79
|
-
|
82
|
+
\\#{final_config[:instructions]}
|
80
83
|
PREFACE
|
81
84
|
|
82
85
|
new(
|
@@ -84,7 +87,8 @@ module Genie
|
|
84
87
|
run_tests_cmd: final_config[:run_tests_cmd],
|
85
88
|
model: final_config[:model],
|
86
89
|
first_question: final_config[:first_question],
|
87
|
-
instructions: final_config[:instructions]
|
90
|
+
instructions: final_config[:instructions],
|
91
|
+
ignore_paths: final_config[:ignore_paths]
|
88
92
|
)
|
89
93
|
end
|
90
94
|
|
@@ -94,17 +98,18 @@ module Genie
|
|
94
98
|
run_tests_cmd: DEFAULTS[:run_tests_cmd],
|
95
99
|
model: DEFAULTS[:model],
|
96
100
|
first_question: DEFAULTS[:first_question],
|
97
|
-
instructions: DEFAULTS[:instructions]
|
101
|
+
instructions: DEFAULTS[:instructions],
|
102
|
+
ignore_paths: DEFAULTS[:ignore_paths],
|
98
103
|
)
|
99
104
|
end
|
100
105
|
|
101
|
-
def initialize(base_path:, run_tests_cmd:, model:, first_question:, instructions:)
|
106
|
+
def initialize(base_path:, run_tests_cmd:, model:, first_question:, instructions:, ignore_paths:)
|
102
107
|
@base_path = File.expand_path(base_path)
|
103
108
|
@run_tests_cmd = run_tests_cmd
|
104
109
|
@model = model
|
105
110
|
@first_question = first_question
|
106
111
|
@instructions = instructions
|
112
|
+
@ignore_paths = ignore_paths
|
107
113
|
end
|
108
|
-
|
109
114
|
end
|
110
115
|
end
|
data/lib/genie/version.rb
CHANGED
data/lib/genie.rb
CHANGED
@@ -32,9 +32,11 @@ RubyLLM.configure do |config|
|
|
32
32
|
end
|
33
33
|
|
34
34
|
module Genie
|
35
|
-
def self.output(s, color: :white)
|
35
|
+
def self.output(s, color: :white, include_genie_icon: true)
|
36
36
|
return if quiet?
|
37
37
|
|
38
|
+
s = "🧞♂️ #{s}" if include_genie_icon
|
39
|
+
|
38
40
|
# This method is used to output messages in a consistent format.
|
39
41
|
# You can customize the color or format as needed.
|
40
42
|
puts "\e[32m#{s}\e[0m" if color == :green
|
data/lib/tools/list_files.rb
CHANGED
@@ -8,6 +8,13 @@ module Genie
|
|
8
8
|
param :recursive, desc: "Whether to list files recursively (default: false)"
|
9
9
|
param :filter, desc: "Filter string to include only paths that include this substring (Optional)"
|
10
10
|
|
11
|
+
def initialize(base_path:, ignore_paths: [])
|
12
|
+
@base_path = base_path
|
13
|
+
@base_path.freeze
|
14
|
+
|
15
|
+
@ignore_paths = ignore_paths
|
16
|
+
end
|
17
|
+
|
11
18
|
def execute(directory:, recursive: false, filter: nil)
|
12
19
|
directory = File.expand_path(directory, @base_path)
|
13
20
|
|
@@ -22,6 +29,9 @@ module Genie
|
|
22
29
|
listing = listing.select { |entry| entry[:name].include?(filter) }
|
23
30
|
end
|
24
31
|
|
32
|
+
# Apply ignore paths
|
33
|
+
listing.reject! { |entry| @ignore_paths.any? { |ignore_path| entry[:name].gsub(@base_path, '').start_with?(ignore_path) } }
|
34
|
+
|
25
35
|
Genie.output listing.map { |e| e[:name] }.join("\n") + "\n", color: :green
|
26
36
|
|
27
37
|
listing
|
data/test/test_session_config.rb
CHANGED
@@ -1,14 +1,11 @@
|
|
1
|
-
require_relative "test_helper"
|
2
|
-
require 'tmpdir'
|
3
|
-
|
4
|
-
class SessionConfigTest < TLDR
|
5
1
|
def test_basic_session_config
|
6
2
|
config = Genie::SessionConfig.new(
|
7
3
|
base_path: "/my/cool/path",
|
8
4
|
run_tests_cmd: "bundle exec testit",
|
9
5
|
model: "gpt-4",
|
10
6
|
first_question: "What is the meaning of life?",
|
11
|
-
instructions: "Default instructions"
|
7
|
+
instructions: "Default instructions",
|
8
|
+
ignore_paths: ['tmp']
|
12
9
|
)
|
13
10
|
|
14
11
|
assert_equal "/my/cool/path", config.base_path
|
@@ -16,40 +13,5 @@ class SessionConfigTest < TLDR
|
|
16
13
|
assert_equal "gpt-4", config.model
|
17
14
|
assert_equal "What is the meaning of life?", config.first_question
|
18
15
|
assert config.instructions.include?("Default instructions")
|
16
|
+
assert_equal ['tmp'], config.ignore_paths
|
19
17
|
end
|
20
|
-
|
21
|
-
def test_session_from_argv
|
22
|
-
argv = ["-c", "asdf.yml", "--base-path", "/tmp", "--run-tests", "rake test", "--model", "gpt-4o", "--instructions", "Command line instructions", "What is the meaning of life?"]
|
23
|
-
config = Genie::SessionConfig.from_argv(argv)
|
24
|
-
|
25
|
-
assert_equal "/tmp", config.base_path
|
26
|
-
assert_equal "rake test", config.run_tests_cmd
|
27
|
-
assert_equal "gpt-4o", config.model
|
28
|
-
assert_equal "What is the meaning of life?", config.first_question
|
29
|
-
assert config.instructions.include?("Command line instructions")
|
30
|
-
end
|
31
|
-
|
32
|
-
def test_session_from_config_file
|
33
|
-
argv = ["-c", "./test/data/sample_config.yml"]
|
34
|
-
config = Genie::SessionConfig.from_argv(argv)
|
35
|
-
expected_base_path = "/tmp/myapp/from_config"
|
36
|
-
|
37
|
-
assert_equal File.realpath(expected_base_path), File.realpath(config.base_path)
|
38
|
-
assert_equal "bundle exec tests_from_config_ex", config.run_tests_cmd
|
39
|
-
assert_equal "test_model_from_config", config.model
|
40
|
-
assert_equal nil, config.first_question
|
41
|
-
assert config.instructions.include?("Instructions from config file")
|
42
|
-
end
|
43
|
-
|
44
|
-
def test_default_instructions
|
45
|
-
config = Genie::SessionConfig.default
|
46
|
-
default_instructions = config.instructions
|
47
|
-
assert_includes default_instructions, "Genie coding assistant"
|
48
|
-
assert_includes default_instructions, "Test Driven Development"
|
49
|
-
assert_includes default_instructions, "tools available"
|
50
|
-
assert_includes default_instructions, "write tests first"
|
51
|
-
assert_includes default_instructions, "do not have access to any files outside"
|
52
|
-
assert_includes default_instructions, "do not have access to the internet"
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|
@@ -28,4 +28,17 @@ class TestListFiles < TLDR
|
|
28
28
|
|
29
29
|
assert_equal expected, actual
|
30
30
|
end
|
31
|
+
|
32
|
+
def test_list_files_with_ignore_paths
|
33
|
+
base_path = File.expand_path("../../", __dir__)
|
34
|
+
t = Genie::ListFiles.new(base_path: base_path, ignore_paths: ["a_dir"])
|
35
|
+
|
36
|
+
actual = t.execute(directory: "./test/data/sample_files")
|
37
|
+
|
38
|
+
expected = [{ name: "read_file_test.txt", type: "file" },
|
39
|
+
{ name: "one.txt", type: "file" }]
|
40
|
+
|
41
|
+
assert_equal expected, actual
|
42
|
+
end
|
43
|
+
|
31
44
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: genie_cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff McFadden
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: ruby_llm
|
@@ -173,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
173
173
|
- !ruby/object:Gem::Version
|
174
174
|
version: '0'
|
175
175
|
requirements: []
|
176
|
-
rubygems_version: 3.6.
|
176
|
+
rubygems_version: 3.6.9
|
177
177
|
specification_version: 4
|
178
178
|
summary: CLI Coding Agent written in Ruby.
|
179
179
|
test_files: []
|