ruby_coded 0.1.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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +76 -0
  3. data/.github/workflows/release.yml +24 -0
  4. data/.rubocop_todo.yml +122 -0
  5. data/CHANGELOG.md +9 -0
  6. data/CODE_OF_CONDUCT.md +10 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +140 -0
  9. data/Rakefile +12 -0
  10. data/exe/ruby_coded +6 -0
  11. data/lib/ruby_coded/auth/auth_manager.rb +145 -0
  12. data/lib/ruby_coded/auth/callback_servlet.rb +41 -0
  13. data/lib/ruby_coded/auth/credentials_store.rb +35 -0
  14. data/lib/ruby_coded/auth/oauth_callback_server.rb +38 -0
  15. data/lib/ruby_coded/auth/pkce.rb +19 -0
  16. data/lib/ruby_coded/auth/providers/anthropic.rb +32 -0
  17. data/lib/ruby_coded/auth/providers/openai.rb +55 -0
  18. data/lib/ruby_coded/chat/app/event_dispatch.rb +78 -0
  19. data/lib/ruby_coded/chat/app.rb +104 -0
  20. data/lib/ruby_coded/chat/command_handler/agent_commands.rb +53 -0
  21. data/lib/ruby_coded/chat/command_handler/history_commands.rb +38 -0
  22. data/lib/ruby_coded/chat/command_handler/model_commands.rb +91 -0
  23. data/lib/ruby_coded/chat/command_handler/plan_commands.rb +112 -0
  24. data/lib/ruby_coded/chat/command_handler/token_commands.rb +128 -0
  25. data/lib/ruby_coded/chat/command_handler/token_formatting.rb +26 -0
  26. data/lib/ruby_coded/chat/command_handler.rb +89 -0
  27. data/lib/ruby_coded/chat/help.txt +28 -0
  28. data/lib/ruby_coded/chat/input_handler/modal_inputs.rb +102 -0
  29. data/lib/ruby_coded/chat/input_handler/normal_mode_input.rb +116 -0
  30. data/lib/ruby_coded/chat/input_handler.rb +39 -0
  31. data/lib/ruby_coded/chat/llm_bridge/plan_mode.rb +73 -0
  32. data/lib/ruby_coded/chat/llm_bridge/streaming_retries.rb +86 -0
  33. data/lib/ruby_coded/chat/llm_bridge/tool_call_handling.rb +129 -0
  34. data/lib/ruby_coded/chat/llm_bridge.rb +131 -0
  35. data/lib/ruby_coded/chat/model_filter.rb +115 -0
  36. data/lib/ruby_coded/chat/plan_clarification_parser.rb +38 -0
  37. data/lib/ruby_coded/chat/renderer/chat_panel.rb +128 -0
  38. data/lib/ruby_coded/chat/renderer/chat_panel_input.rb +56 -0
  39. data/lib/ruby_coded/chat/renderer/chat_panel_thinking.rb +124 -0
  40. data/lib/ruby_coded/chat/renderer/model_selector.rb +96 -0
  41. data/lib/ruby_coded/chat/renderer/plan_clarifier.rb +112 -0
  42. data/lib/ruby_coded/chat/renderer/plan_clarifier_layout.rb +42 -0
  43. data/lib/ruby_coded/chat/renderer/status_bar.rb +47 -0
  44. data/lib/ruby_coded/chat/renderer.rb +64 -0
  45. data/lib/ruby_coded/chat/state/message_assistant.rb +77 -0
  46. data/lib/ruby_coded/chat/state/message_token_tracking.rb +57 -0
  47. data/lib/ruby_coded/chat/state/messages.rb +70 -0
  48. data/lib/ruby_coded/chat/state/model_selection.rb +79 -0
  49. data/lib/ruby_coded/chat/state/plan_tracking.rb +140 -0
  50. data/lib/ruby_coded/chat/state/scrollable.rb +42 -0
  51. data/lib/ruby_coded/chat/state/token_cost.rb +128 -0
  52. data/lib/ruby_coded/chat/state/tool_confirmation.rb +129 -0
  53. data/lib/ruby_coded/chat/state.rb +205 -0
  54. data/lib/ruby_coded/config/user_config.rb +110 -0
  55. data/lib/ruby_coded/errors/auth_error.rb +12 -0
  56. data/lib/ruby_coded/initializer/cover.rb +29 -0
  57. data/lib/ruby_coded/initializer.rb +52 -0
  58. data/lib/ruby_coded/plugins/base.rb +44 -0
  59. data/lib/ruby_coded/plugins/command_completion/input_extension.rb +30 -0
  60. data/lib/ruby_coded/plugins/command_completion/plugin.rb +27 -0
  61. data/lib/ruby_coded/plugins/command_completion/renderer_extension.rb +54 -0
  62. data/lib/ruby_coded/plugins/command_completion/state_extension.rb +90 -0
  63. data/lib/ruby_coded/plugins/registry.rb +88 -0
  64. data/lib/ruby_coded/plugins.rb +21 -0
  65. data/lib/ruby_coded/strategies/api_key_strategy.rb +39 -0
  66. data/lib/ruby_coded/strategies/base.rb +37 -0
  67. data/lib/ruby_coded/strategies/oauth_strategy.rb +106 -0
  68. data/lib/ruby_coded/tools/agent_cancelled_error.rb +7 -0
  69. data/lib/ruby_coded/tools/agent_iteration_limit_error.rb +7 -0
  70. data/lib/ruby_coded/tools/base_tool.rb +50 -0
  71. data/lib/ruby_coded/tools/create_directory_tool.rb +34 -0
  72. data/lib/ruby_coded/tools/delete_path_tool.rb +50 -0
  73. data/lib/ruby_coded/tools/edit_file_tool.rb +40 -0
  74. data/lib/ruby_coded/tools/list_directory_tool.rb +53 -0
  75. data/lib/ruby_coded/tools/plan_system_prompt.rb +72 -0
  76. data/lib/ruby_coded/tools/read_file_tool.rb +54 -0
  77. data/lib/ruby_coded/tools/registry.rb +66 -0
  78. data/lib/ruby_coded/tools/run_command_tool.rb +75 -0
  79. data/lib/ruby_coded/tools/system_prompt.rb +32 -0
  80. data/lib/ruby_coded/tools/tool_rejected_error.rb +7 -0
  81. data/lib/ruby_coded/tools/write_file_tool.rb +31 -0
  82. data/lib/ruby_coded/version.rb +10 -0
  83. data/lib/ruby_coded.rb +16 -0
  84. data/sig/ruby_coded.rbs +4 -0
  85. metadata +206 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f81632e5f552b3eb5e4e85d15ae4ecda0767c2f530606ccc083c9c1adcf9c92f
4
+ data.tar.gz: 33759f15b1568c1f0e2c88fbc32d9e97ca3772662aeaa0112acceb1819ab7a9a
5
+ SHA512:
6
+ metadata.gz: d004abb1a5c5197cffc6b28fffb44172ed6b4cb22b62cf3438941d8c67b2869d7244df4d7c60cc63d1a28ce0dbd6dd20e5ab6fadff6b3e3f4bc89780d7c54d7a
7
+ data.tar.gz: ca3ff79da79a9fba4b2166deedb5eaa52626aaad0a3429dcb9ef75134fd831c852f4e1e3ff66d8d71cc234dc23e1a0c60b90f8dcab7166deaa5675e806052294
@@ -0,0 +1,76 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test (Ruby ${{ matrix.ruby }})
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby: ["3.3", "3.4"]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Remove Gemfile.lock for CI compatibility
20
+ run: rm -f Gemfile.lock
21
+ - uses: dtolnay/rust-toolchain@stable
22
+ - uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby }}
25
+ bundler-cache: true
26
+ - name: Run tests
27
+ run: bundle exec rake test
28
+
29
+ test-head:
30
+ name: Test (Ruby head)
31
+ runs-on: ubuntu-latest
32
+ continue-on-error: true
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - name: Remove Gemfile.lock for CI compatibility
36
+ run: rm -f Gemfile.lock
37
+ - uses: dtolnay/rust-toolchain@stable
38
+ - uses: ruby/setup-ruby@v1
39
+ with:
40
+ ruby-version: head
41
+ bundler-cache: true
42
+ - name: Run tests
43
+ run: bundle exec rake test
44
+
45
+ lint:
46
+ name: RuboCop
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ - name: Remove Gemfile.lock for CI compatibility
51
+ run: rm -f Gemfile.lock
52
+ - uses: dtolnay/rust-toolchain@stable
53
+ - uses: ruby/setup-ruby@v1
54
+ with:
55
+ ruby-version: "3.3"
56
+ bundler-cache: true
57
+ - name: Run RuboCop
58
+ run: bundle exec rake rubocop
59
+
60
+ build:
61
+ name: Build Gem
62
+ runs-on: ubuntu-latest
63
+ needs: [test, lint]
64
+ steps:
65
+ - uses: actions/checkout@v4
66
+ - name: Remove Gemfile.lock for CI compatibility
67
+ run: rm -f Gemfile.lock
68
+ - uses: dtolnay/rust-toolchain@stable
69
+ - uses: ruby/setup-ruby@v1
70
+ with:
71
+ ruby-version: "3.3"
72
+ bundler-cache: true
73
+ - name: Build gem
74
+ run: gem build ruby_coded.gemspec
75
+ - name: Verify gem was built
76
+ run: ls ruby_coded-*.gem
@@ -0,0 +1,24 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ name: Publish to RubyGems
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ ref: main
18
+ fetch-depth: 0
19
+ - uses: dtolnay/rust-toolchain@stable
20
+ - uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: "3.3"
23
+ bundler-cache: true
24
+ - uses: rubygems/release-gem@v1
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,122 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 999`
3
+ # on 2026-04-15 07:58:11 UTC using RuboCop version 1.86.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 2
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
12
+ Layout/EmptyLineBetweenDefs:
13
+ Exclude:
14
+ - 'test/test_agent_commands.rb'
15
+ - 'test/test_renderer_chat_panel.rb'
16
+
17
+ # Offense count: 5
18
+ # This cop supports safe autocorrection (--autocorrect).
19
+ Layout/EmptyLines:
20
+ Exclude:
21
+ - 'test/test_agent_commands.rb'
22
+ - 'test/test_model_commands.rb'
23
+ - 'test/test_renderer_chat_panel.rb'
24
+ - 'test/test_renderer_model_selector.rb'
25
+ - 'test/test_state_model_selection.rb'
26
+
27
+ # Offense count: 1
28
+ # This cop supports safe autocorrection (--autocorrect).
29
+ # Configuration parameters: EnforcedStyle.
30
+ # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only
31
+ Layout/EmptyLinesAroundClassBody:
32
+ Exclude:
33
+ - 'lib/ruby_coded/chat/app.rb'
34
+
35
+ # Offense count: 2
36
+ # This cop supports safe autocorrection (--autocorrect).
37
+ # Configuration parameters: EnforcedStyle.
38
+ # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines
39
+ Layout/EmptyLinesAroundModuleBody:
40
+ Exclude:
41
+ - 'lib/ruby_coded/chat/command_handler/token_commands.rb'
42
+ - 'lib/ruby_coded/chat/renderer/plan_clarifier.rb'
43
+
44
+ # Offense count: 1
45
+ # This cop supports safe autocorrection (--autocorrect).
46
+ # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
47
+ # URISchemes: http, https
48
+ Layout/LineLength:
49
+ Exclude:
50
+ - 'lib/ruby_coded/chat/llm_bridge/streaming_retries.rb'
51
+
52
+ # Offense count: 1
53
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
54
+ Metrics/AbcSize:
55
+ Exclude:
56
+ - 'lib/ruby_coded/chat/state.rb'
57
+
58
+ # Offense count: 1
59
+ # Configuration parameters: CountComments, Max, CountAsOne.
60
+ Metrics/ClassLength:
61
+ Exclude:
62
+ - 'lib/ruby_coded/chat/state.rb'
63
+
64
+ # Offense count: 2
65
+ # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
66
+ Metrics/MethodLength:
67
+ Exclude:
68
+ - 'lib/ruby_coded/chat/state.rb'
69
+ - 'lib/ruby_coded/strategies/oauth_strategy.rb'
70
+
71
+ # Offense count: 2
72
+ # This cop supports safe autocorrection (--autocorrect).
73
+ # Configuration parameters: EnforcedStyle, BlockForwardingName.
74
+ # SupportedStyles: anonymous, explicit
75
+ Naming/BlockForwarding:
76
+ Exclude:
77
+ - 'test/test_initializer.rb'
78
+
79
+ # Offense count: 2
80
+ # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
81
+ # AllowedMethods: call
82
+ # WaywardPredicates: infinite?, nonzero?
83
+ Naming/PredicateMethod:
84
+ Exclude:
85
+ - 'lib/ruby_coded/strategies/api_key_strategy.rb'
86
+ - 'lib/ruby_coded/strategies/oauth_strategy.rb'
87
+
88
+ # Offense count: 1
89
+ # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs.
90
+ # NamePrefix: is_, has_, have_, does_
91
+ # ForbiddenPrefixes: is_, has_, have_, does_
92
+ # AllowedMethods: is_a?
93
+ # MethodDefinitionMacros: define_method, define_singleton_method
94
+ Naming/PredicatePrefix:
95
+ Exclude:
96
+ - 'lib/ruby_coded/chat/state/plan_tracking.rb'
97
+
98
+ # Offense count: 2
99
+ # This cop supports safe autocorrection (--autocorrect).
100
+ # Configuration parameters: AllowOnlyRestArgument, UseAnonymousForwarding, RedundantRestArgumentNames, RedundantKeywordRestArgumentNames, RedundantBlockArgumentNames.
101
+ # RedundantRestArgumentNames: args, arguments
102
+ # RedundantKeywordRestArgumentNames: kwargs, options, opts
103
+ # RedundantBlockArgumentNames: blk, block, proc
104
+ Style/ArgumentsForwarding:
105
+ Exclude:
106
+ - 'test/test_initializer.rb'
107
+
108
+ # Offense count: 1
109
+ # This cop supports unsafe autocorrection (--autocorrect-all).
110
+ # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns.
111
+ # SupportedStyles: predicate, comparison
112
+ Style/NumericPredicate:
113
+ Exclude:
114
+ - 'lib/ruby_coded/chat/state.rb'
115
+
116
+ # Offense count: 3
117
+ # This cop supports safe autocorrection (--autocorrect).
118
+ Style/SuperArguments:
119
+ Exclude:
120
+ - 'lib/ruby_coded/errors/auth_error.rb'
121
+ - 'test/test_renderer_chat_panel.rb'
122
+ - 'test/test_renderer_status_bar.rb'
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-04-15
4
+
5
+ - Initial release
6
+
7
+ ## [0.1.1] - 2026-04-15
8
+
9
+ - Fix CI workflow
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "ruby_coded" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["cesar.rodriguez.lara54@gmail.com"](mailto:"cesar.rodriguez.lara54@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Cesar Rodriguez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # RubyCoded
2
+
3
+ ```
4
+ /\
5
+ / \
6
+ / \ ____ _ ____ _
7
+ /------\ | _ \ _ _| |__ _ _ / ___|___ __| | ___ __| |
8
+ / \ / \ | |_) | | | | '_ \| | | | | | / _ \ / _` |/ _ \ / _` |
9
+ / \/ \ | _ <| |_| | |_) | |_| | | |__| (_) | (_| | __/ (_| |
10
+ \ /\ / |_| \_\\__,_|_.__/ \__, | \____\___/ \__,_|\___| \__,_|
11
+ \ / \ / |___/
12
+ \/ \/
13
+ \ /
14
+ \ /
15
+ \/
16
+ ```
17
+
18
+ An AI-powered terminal coding assistant built in Ruby. Chat with LLMs, let an agent edit your project files, or plan tasks — all from your terminal.
19
+
20
+ ## Features
21
+
22
+ - **Chat mode** — Talk to an LLM directly in a full terminal UI (TUI) built with [ratatui](https://github.com/nicholasgasior/ratatui-ruby)
23
+ - **Agent mode** — The model can read, write, edit, and delete files in your project, create directories, and run shell commands with user confirmation
24
+ - **Plan mode** — Generate structured plans before implementing, with interactive clarification questions and auto-switch to agent mode when ready
25
+ - **Multi-provider support** — Works with OpenAI and Anthropic out of the box (OAuth and API key authentication)
26
+ - **Tool confirmation** — Write and dangerous operations require explicit approval; safe operations (read, list) run automatically
27
+ - **Token & cost tracking** — Live status bar showing token usage and estimated session cost
28
+ - **Plugin system** — Extend the chat with custom state, input handlers, renderer overlays, and commands
29
+ - **Slash commands** — `/agent`, `/plan`, `/model`, `/history`, `/tokens`, `/help`, and more
30
+
31
+ ## Requirements
32
+
33
+ - Ruby >= 3.3.0
34
+ - An OpenAI or Anthropic account (API key or OAuth)
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ gem install ruby_coded
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Navigate to any project directory and run:
45
+
46
+ ```bash
47
+ ruby_coded
48
+ ```
49
+
50
+ On first launch you'll be asked to authenticate with a provider. After that, you're dropped into chat mode.
51
+
52
+ ### Modes
53
+
54
+ | Command | Description |
55
+ |---|---|
56
+ | `/agent on` | Enable agent mode (file tools + shell access) |
57
+ | `/agent off` | Disable agent mode |
58
+ | `/plan on` | Enable plan mode (read-only tools, structured planning) |
59
+ | `/plan off` | Disable plan mode |
60
+ | `/plan save` | Save the current plan to a file |
61
+ | `/model` | Switch to a different model |
62
+ | `/tokens` | Show detailed token usage breakdown |
63
+ | `/history` | Show conversation history |
64
+ | `/clear` | Clear the conversation |
65
+ | `/help` | Show all available commands |
66
+
67
+ ### Agent mode
68
+
69
+ When agent mode is active, the model has access to these tools:
70
+
71
+ | Tool | Risk level | Description |
72
+ |---|---|---|
73
+ | `read_file` | Safe | Read file contents |
74
+ | `list_directory` | Safe | List directory contents |
75
+ | `write_file` | Confirm | Write a new file |
76
+ | `edit_file` | Confirm | Search and replace in a file |
77
+ | `create_directory` | Confirm | Create a new directory |
78
+ | `delete_path` | Dangerous | Delete a file or directory |
79
+ | `run_command` | Dangerous | Execute a shell command |
80
+
81
+ Safe tools run without asking. Confirm and dangerous tools show the operation details and wait for your approval (`y` to approve, `n` to reject, `a` to approve all remaining).
82
+
83
+ ### Plan mode
84
+
85
+ Plan mode restricts the model to read-only tools and a planning-oriented system prompt. The model will analyze your project and propose a structured plan. If the model needs clarification, it presents interactive options you can select or answer with custom text.
86
+
87
+ ## Keyboard shortcuts
88
+
89
+ | Key | Action |
90
+ |---|---|
91
+ | `Enter` | Send message |
92
+ | `Esc` | Cancel streaming / clear input |
93
+ | `Ctrl+C` | Quit |
94
+ | `Up/Down` | Scroll chat history |
95
+ | `Left/Right` | Move cursor in input |
96
+ | `Home/End` | Jump to start/end of input |
97
+ | `Tab` | Autocomplete commands |
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ git clone https://github.com/MrCesar107/ruby_code.git
103
+ cd ruby_code
104
+ bundle install
105
+ bundle exec rake test
106
+ ```
107
+
108
+ To run the application locally:
109
+
110
+ ```bash
111
+ bundle exec exe/ruby_coded
112
+ ```
113
+
114
+ ## What's next
115
+
116
+ - Find a way to update the autocomplete plugin when a new command is added []
117
+ - Display context window size (depending on the model) []
118
+ - UI element to indicate the AI is performing a task []
119
+ - Add the possibility to create custom commands []
120
+ - Skills implementation []
121
+ - Implement Google Auth for Gemini []
122
+ - Session recovery system by ID []
123
+
124
+ ## Contributing
125
+
126
+ Contributions are welcome! To get started:
127
+
128
+ 1. Fork the repository
129
+ 2. Create a new branch for your feature or fix (`git checkout -b my-feature`)
130
+ 3. Make your changes and add tests if applicable
131
+ 4. Make sure all tests pass (`bundle exec rake test`)
132
+ 5. Commit your changes (`git commit -m "Add my feature"`)
133
+ 6. Push to your fork (`git push origin my-feature`)
134
+ 7. Open a Pull Request against the `main` branch of this repository
135
+
136
+ Your PR will be reviewed and merged if everything looks good. If you're unsure about a change, feel free to open an issue first to discuss it.
137
+
138
+ ## License
139
+
140
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
data/exe/ruby_coded ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/ruby_coded"
5
+
6
+ RubyCoded.start
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "time"
5
+ require "ruby_llm"
6
+ require "tty-prompt"
7
+
8
+ require_relative "providers/openai"
9
+ require_relative "providers/anthropic"
10
+ require_relative "../strategies/oauth_strategy"
11
+ require_relative "../strategies/api_key_strategy"
12
+ require_relative "credentials_store"
13
+
14
+ module RubyCoded
15
+ module Auth
16
+ # This class is used to manage the authentication process for the different
17
+ # AI providers
18
+ class AuthManager
19
+ PROVIDERS = {
20
+ openai: Providers::OpenAI,
21
+ anthropic: Providers::Anthropic
22
+ }.freeze
23
+
24
+ def initialize(config_path: UserConfig::CONFIG_PATH)
25
+ @config_path = config_path
26
+ end
27
+
28
+ def login(provider_name)
29
+ provider = PROVIDERS.fetch(provider_name)
30
+ strategy = strategy_for(provider)
31
+ credentials = strategy.authenticate
32
+ credential_store.store(provider_name, credentials)
33
+ configure_ruby_llm!
34
+ print_api_credits_notice(provider)
35
+ credentials
36
+ end
37
+
38
+ def logout(provider_name)
39
+ credential_store.remove(provider_name)
40
+ configure_ruby_llm!
41
+ end
42
+
43
+ def configured_providers
44
+ PROVIDERS.keys
45
+ end
46
+
47
+ def authenticated_provider_names
48
+ PROVIDERS.keys.select { |name| credential_store.retrieve(name) }
49
+ end
50
+
51
+ def check_authentication
52
+ return if configured_providers.any? { |name| credential_store.retrieve(name) }
53
+
54
+ provider_name = choose_provider
55
+ login(provider_name)
56
+ end
57
+
58
+ def login_prompt
59
+ provider_name = choose_provider
60
+ login(provider_name)
61
+ end
62
+
63
+ def configure_ruby_llm!
64
+ RubyLLM.configure do |config|
65
+ config.max_retries = 1
66
+
67
+ PROVIDERS.each do |name, provider|
68
+ credentials = credential_store.retrieve(name)
69
+ next unless credentials
70
+
71
+ credentials = refresh_if_expired(name, provider, credentials)
72
+ key = extract_api_key(credentials)
73
+ config.send("#{provider.ruby_llm_key}=", key)
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def prompt
81
+ @prompt ||= TTY::Prompt.new
82
+ end
83
+
84
+ def choose_provider
85
+ prompt.select("Please select the AI provider you want to log in:",
86
+ configured_providers,
87
+ per_page: 10)
88
+ end
89
+
90
+ def strategy_for(provider)
91
+ method = choose_auth_method(provider)
92
+ case method
93
+ when :oauth then Strategies::OAuthStrategy.new(provider)
94
+ when :api_key then Strategies::APIKeyStrategy.new(provider)
95
+ else
96
+ raise "Invalid authentication method: #{method}"
97
+ end
98
+ end
99
+
100
+ def credential_store
101
+ @credential_store ||= CredentialsStore.new(config_path: @config_path)
102
+ end
103
+
104
+ def extract_api_key(credentials)
105
+ case credentials["auth_method"]
106
+ when "oauth" then credentials["access_token"]
107
+ when "api_key" then credentials["key"]
108
+ end
109
+ end
110
+
111
+ def refresh_if_expired(provider_name, provider, credentials)
112
+ return credentials unless credentials["auth_method"] == "oauth"
113
+ return credentials unless token_expired?(credentials)
114
+
115
+ strategy = Strategies::OAuthStrategy.new(provider)
116
+ refreshed = strategy.refresh(credentials)
117
+ credential_store.store(provider_name, refreshed)
118
+ refreshed
119
+ rescue StandardError
120
+ credentials
121
+ end
122
+
123
+ def token_expired?(credentials)
124
+ expires_at = credentials["expires_at"]
125
+ return false unless expires_at
126
+
127
+ Time.parse(expires_at) <= Time.now + 60
128
+ end
129
+
130
+ def print_api_credits_notice(provider)
131
+ console = provider.respond_to?(:console_url) ? provider.console_url : nil
132
+ billing_hint = console ? " Check your balance at #{console}." : ""
133
+ puts "\nNote: API usage consumes credits from your #{provider.display_name} account.#{billing_hint}\n\n"
134
+ end
135
+
136
+ def choose_auth_method(provider)
137
+ methods = provider.auth_methods
138
+ return methods.first[:key] if methods.size == 1
139
+
140
+ choices = methods.map { |m| { name: m[:label], value: m[:key] } }
141
+ prompt.select("How would you like to authenticate with #{provider.display_name}?", choices)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Auth
5
+ # This class creates a callback servlet for the OAuth authentication
6
+ class CallbackServlet < WEBrick::HTTPServlet::AbstractServlet
7
+ SUCCESS_HTML = <<~HTML
8
+ <html>
9
+ <body>
10
+ <h2>You are now logged</h2>
11
+ <p>You can close this window now.</p>
12
+ <script>window.close();</script>
13
+ </body>
14
+ </html>
15
+ HTML
16
+
17
+ def initialize(server, result_queue)
18
+ super(server)
19
+ @result_queue = result_queue
20
+ end
21
+
22
+ def do_GET(request, response) # rubocop:disable Naming/MethodName
23
+ process_callback(request)
24
+ response.status = 200
25
+ response.content_type = "text/html"
26
+ response.body = SUCCESS_HTML
27
+ end
28
+
29
+ private
30
+
31
+ def process_callback(request)
32
+ error = request.query["error"]
33
+ if error
34
+ @result_queue.push({ error: error })
35
+ else
36
+ @result_queue.push({ code: request.query["code"], state: request.query["state"] })
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ require_relative "../config/user_config"
6
+
7
+ module RubyCoded
8
+ module Auth
9
+ # This class is used to manage the credentials in the config file
10
+ class CredentialsStore
11
+ def initialize(config_path: UserConfig::CONFIG_PATH)
12
+ @config = UserConfig.new(config_path: config_path)
13
+ end
14
+
15
+ def store(provider_name, credentials)
16
+ cfg = @config.full_config
17
+ cfg["providers"] ||= {}
18
+ cfg["providers"][provider_name.to_s] = credentials
19
+ @config.save
20
+ end
21
+
22
+ def retrieve(provider_name)
23
+ @config.full_config.dig("providers", provider_name.to_s)
24
+ end
25
+
26
+ def remove(provider_name)
27
+ providers = @config.full_config["providers"]
28
+ return unless providers
29
+
30
+ providers.delete(provider_name.to_s)
31
+ @config.save
32
+ end
33
+ end
34
+ end
35
+ end