ruby_coded 0.2.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1980b8f52a5fefcd383aec24380904430c10654b4cffe7bef6288b1b55e1eed
4
- data.tar.gz: 1e54670c24db8f43ce9a0e4391889adf3d2d1c75f0923d43ff3260bc210e2e96
3
+ metadata.gz: 458d56c13426096bb1d553aad86f7d2502a94022cc53de44ca7c74ab48cf2b5a
4
+ data.tar.gz: 128c5c97ccf42e93a7671ad5641d85c3f0c80f425c74506f0f22f7a4d4e95652
5
5
  SHA512:
6
- metadata.gz: c4d75ef0c6c7d0b3d1da17287b08bbdec2d9ee29bcc12c8551409ae2dbf2276562a10fc96294a9685b55a41f36d4711e448a640e97bb2711a9da9e0b1231e5ad
7
- data.tar.gz: 75d2f43c84b9c8d027d17ac6d61b2f250fef6ce2dca88463b3ab580f74927bb16933b5a6f96eb7fed6b26e5138c621e9df8b56435f364dcbe3173c72f3a10114
6
+ metadata.gz: a244f39012eb45e2396f61a94fb2b14f23ffd1269c4b235a1ea7ce456763cd674f13e51c4cc3584f95ab4d15517ce63aaff1c113848b2ff467aae70606770726
7
+ data.tar.gz: b1ba816f5deb3e7051764602bb2bb3afcac031af219f6523eb1d54f021d77fc97844068952c914a2d7f1bd441f62968141678ec09b169bf6d855cbed3f1c0848
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-04-24
4
+
5
+ ### Added
6
+
7
+ - **Pro-only Codex model handling**: `gpt-5.3-codex-spark` and `gpt-5.2-codex` are tagged as `pro_only` in the Codex model catalog and render with a `(Pro only)` marker in the `/model` selector. Plus/free JWT plan claims now hide Pro-only models from the selector, while unknown plans keep the previous behavior to avoid breaking Pro users.
8
+ - **Automatic Codex model fallback**: When the Codex backend rejects a model with `not supported when using Codex with a ChatGPT account`, the request is transparently retried against `gpt-5.4` and the user is informed via a system message.
9
+ - **Provider metadata in `/model`**: `CodexModel` now exposes provider metadata so the selector shows `(openai)` instead of `(unknown)`.
10
+ - **Unified command catalog**: New `RubyCoded::Commands` module with a central `Catalog`, `CommandDefinition`, and provider abstraction (`CoreProvider`, `PluginProvider`, `MarkdownProvider`) consolidating all slash commands in a single source of truth, replacing the hand-rolled `help.txt` and ad-hoc registrations.
11
+ - **Markdown custom commands**: Users can now drop `*.md` files under `~/.ruby_coded/commands/` (or the project-local `.ruby_coded/commands/`) to define reusable slash commands with front-matter metadata, loaded via the new `MarkdownLoader` / `MarkdownProvider`.
12
+ - **Context window tracking**: New `State::ContextWindow` module plus status bar integration that shows live context usage per model, driven by per-turn token tracking.
13
+
14
+ ### Changed
15
+
16
+ - **Status bar polish**: The status bar now renders context window usage alongside the model/auth indicators and hides noisy fields when data is unavailable.
17
+ - **Command handler refactor**: `CommandHandler` is now powered by the unified catalog (core + plugin + markdown providers) and delegates custom command execution to the new `CustomCommands` mixin.
18
+
19
+ ### Fixed
20
+
21
+ - **Context window indicator for ChatGPT Plus/Pro users**: `CodexBridge` now parses the `response.completed` SSE event and feeds `input`/`output`/`reasoning`/`cached` token usage into `State`, mirroring the API path. `session_context_tokens_used` reflects the last turn's live prompt size via the new `State#last_turn_context_tokens` helper, eliminating the double-counting that happened because both bridges re-send the full history on each request.
22
+
3
23
  ## [0.2.2] - 2026-04-17
4
24
 
5
25
  ### Fixed
data/README.md CHANGED
@@ -28,7 +28,7 @@ An AI-powered terminal coding assistant built in Ruby. Chat with LLMs, let an ag
28
28
  - **Tool confirmation** — Write and dangerous operations require explicit approval; safe operations (read, list) run automatically
29
29
  - **Token & cost tracking** — Live status bar showing token usage, estimated session cost, and auth mode indicator
30
30
  - **Plugin system** — Extend the chat with custom state, input handlers, renderer overlays, and commands
31
- - **Slash commands** — `/agent`, `/plan`, `/model`, `/login`, `/history`, `/tokens`, `/help`, and more
31
+ - **Slash commands** — `/agent`, `/plan`, `/model`, `/login`, `/history`, `/tokens`, `/commands reload`, `/commands list`, `/help`, and more
32
32
 
33
33
  ## Requirements
34
34
 
@@ -62,6 +62,8 @@ On first launch you'll be asked to authenticate with a provider. After that, you
62
62
  | `/plan save` | Save the current plan to a file |
63
63
  | `/model` | Switch to a different model |
64
64
  | `/login` | Authenticate with a provider (OpenAI, Anthropic) |
65
+ | `/commands reload` | Reload custom markdown commands from the current project |
66
+ | `/commands list` | List currently loaded custom markdown commands |
65
67
  | `/tokens` | Show detailed token usage breakdown |
66
68
  | `/history` | Show conversation history |
67
69
  | `/clear` | Clear the conversation |
@@ -87,6 +89,86 @@ Safe tools run without asking. Confirm and dangerous tools show the operation de
87
89
 
88
90
  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.
89
91
 
92
+ ### Custom commands
93
+
94
+ You can define your own project-specific slash commands using Markdown files inside:
95
+
96
+ ```bash
97
+ .ruby_coded/commands/
98
+ ```
99
+
100
+ Each `.md` file represents one custom command. RubyCoded loads these commands into:
101
+
102
+ - the slash-command autocomplete list
103
+ - `/help`
104
+ - command execution
105
+
106
+ #### File format
107
+
108
+ Use YAML frontmatter followed by the command body:
109
+
110
+ ```md
111
+ ---
112
+ command: /review-auth
113
+ description: Review auth implementation
114
+ usage: /review-auth [file]
115
+ ---
116
+
117
+ Review the authentication implementation and suggest improvements.
118
+ Focus on risks, bugs, and refactoring opportunities.
119
+ ```
120
+
121
+ #### Required fields
122
+
123
+ - `command` — command name, must start with `/`
124
+ - `description` — short description shown in autocomplete and help
125
+
126
+ #### Optional fields
127
+
128
+ - `usage` — custom usage text shown in `/help`
129
+
130
+ #### How it works
131
+
132
+ The Markdown body is used as the prompt template for the command. For example:
133
+
134
+ ```bash
135
+ /review-auth lib/ruby_coded/chat/command_handler.rb
136
+ ```
137
+
138
+ RubyCoded will send the command body to the model and append the extra user input as additional context.
139
+
140
+ #### Managing commands
141
+
142
+ Custom commands are loaded from the current project. After adding, editing, or deleting command files, you can manage them without restarting the app.
143
+
144
+ Reload command definitions:
145
+
146
+ ```bash
147
+ /commands reload
148
+ ```
149
+
150
+ The reload command reports:
151
+
152
+ - how many custom commands were added
153
+ - how many were removed
154
+ - how many are currently available
155
+ - how many invalid files were ignored
156
+ - how many conflicting commands were ignored
157
+ - invalid file names
158
+ - conflicting command names
159
+
160
+ List currently loaded custom commands:
161
+
162
+ ```bash
163
+ /commands list
164
+ ```
165
+
166
+ #### Notes
167
+
168
+ - Core commands take precedence over custom commands.
169
+ - Plugin commands also take precedence over custom Markdown commands.
170
+ - Invalid Markdown command files are ignored during reload.
171
+
90
172
  ## Keyboard shortcuts
91
173
 
92
174
  | Key | Action |
@@ -116,10 +198,9 @@ bundle exec exe/ruby_coded
116
198
 
117
199
  ## What's next
118
200
 
119
- - Find a way to update the autocomplete plugin when a new command is added
120
201
  - Display context window size (depending on the model)
121
202
  - UI element to indicate the AI is performing a task
122
- - Add the possibility to create custom commands
203
+ - Improve custom commands further (validation, conflict reporting, management UX)
123
204
  - Skills implementation
124
205
  - Implement Google Auth for Gemini
125
206
  - Local LLM support
@@ -24,6 +24,20 @@ module RubyCoded
24
24
  payload = decode(token)
25
25
  payload&.dig(JWT_CLAIM_PATH, "chatgpt_account_id")
26
26
  end
27
+
28
+ PLAN_CLAIM_KEYS = %w[chatgpt_plan_type planType plan_type].freeze
29
+
30
+ def self.extract_plan_type(token)
31
+ payload = decode(token)
32
+ return nil unless payload
33
+
34
+ auth_claims = payload[JWT_CLAIM_PATH].is_a?(Hash) ? payload[JWT_CLAIM_PATH] : {}
35
+ PLAN_CLAIM_KEYS.each do |key|
36
+ value = auth_claims[key] || payload[key]
37
+ return value.to_s.downcase if value
38
+ end
39
+ nil
40
+ end
27
41
  end
28
42
  end
29
43
  end
@@ -13,6 +13,7 @@ require_relative "command_handler"
13
13
  require_relative "llm_bridge"
14
14
  require_relative "codex_bridge"
15
15
  require_relative "codex_models"
16
+ require_relative "../commands/catalog"
16
17
  require_relative "../auth/credentials_store"
17
18
  require_relative "../auth/jwt_decoder"
18
19
  require_relative "../auth/pkce"
@@ -40,7 +41,12 @@ module RubyCoded
40
41
  end
41
42
 
42
43
  def build_components!
43
- @state = State.new(model: @model)
44
+ @command_catalog = RubyCoded::Commands::Catalog.new(
45
+ project_root: Dir.pwd,
46
+ plugin_registry: RubyCoded.plugin_registry
47
+ )
48
+
49
+ @state = State.new(model: @model, command_catalog: @command_catalog)
44
50
  @credentials_store = Auth::CredentialsStore.new(user_config: @user_config)
45
51
  @llm_bridge = create_bridge
46
52
  @input_handler = InputHandler.new(@state)
@@ -96,8 +102,14 @@ module RubyCoded
96
102
  end
97
103
 
98
104
  def build_command_handler
99
- CommandHandler.new(@state, llm_bridge: @llm_bridge, user_config: @user_config,
100
- credentials_store: @credentials_store, auth_manager: @auth_manager)
105
+ CommandHandler.new(
106
+ @state,
107
+ llm_bridge: @llm_bridge,
108
+ user_config: @user_config,
109
+ credentials_store: @credentials_store,
110
+ auth_manager: @auth_manager,
111
+ command_catalog: @command_catalog
112
+ )
101
113
  end
102
114
 
103
115
  def announce_model_fallback
@@ -150,6 +162,7 @@ module RubyCoded
150
162
  end
151
163
 
152
164
  def recreate_bridge!
165
+ @command_catalog.reload!
153
166
  agentic = @llm_bridge.agentic_mode
154
167
  plan = @llm_bridge.plan_mode
155
168
  @llm_bridge = create_bridge
@@ -157,7 +170,6 @@ module RubyCoded
157
170
  @llm_bridge.toggle_plan_mode!(plan) if plan
158
171
  @command_handler = build_command_handler
159
172
  end
160
-
161
173
  end
162
174
  end
163
175
  end
@@ -4,12 +4,15 @@ module RubyCoded
4
4
  module Chat
5
5
  class CodexBridge
6
6
  # Retry logic and error message formatting for the Codex API client.
7
+ # rubocop:disable Metrics/ModuleLength
7
8
  module ErrorHandling
8
9
  AGENT_SWITCH_PATTERN = /
9
10
  \b(implement|go[ ]ahead|proceed|execut|ejecutar?|comenz|
10
11
  comienz|hazlo|constru[iy]|adelante|dale|do[ ]it|build[ ]it)\b
11
12
  /ix
12
13
 
14
+ UNSUPPORTED_MODEL_PATTERN = /not supported when using Codex with a ChatGPT account/i
15
+
13
16
  private
14
17
 
15
18
  def build_connection
@@ -43,19 +46,56 @@ module RubyCoded
43
46
  "Plan mode disabled — switching to agent mode to implement the plan.")
44
47
  end
45
48
 
46
- def attempt_with_retries(input, retries = 0)
49
+ def attempt_with_retries(input, retries = 0, fallback_attempted: false)
47
50
  perform_codex_request(input)
48
51
  rescue Tools::AgentCancelledError, Tools::AgentIterationLimitError, Tools::ToolRejectedError => e
49
52
  @state.add_message(:system, e.message)
50
53
  rescue CodexAPIError => e
51
- @state.fail_last_assistant(e, friendly_message: codex_error_message(e))
54
+ handle_codex_api_error(e, input, retries, fallback_attempted)
52
55
  rescue Faraday::TooManyRequestsError => e
53
- retry if (retries = handle_rate_limit_retry(e, retries))
54
- @state.fail_last_assistant(e, friendly_message: rate_limit_message(e))
56
+ handle_rate_limit_error(e, retries, input, fallback_attempted)
55
57
  rescue StandardError => e
56
58
  @state.fail_last_assistant(e, friendly_message: "Codex API error: #{e.message}")
57
59
  end
58
60
 
61
+ def handle_codex_api_error(error, input, retries, fallback_attempted)
62
+ return fail_codex_request(error) unless should_fallback_to_default_model?(error, fallback_attempted)
63
+
64
+ switch_to_default_model!(error)
65
+ attempt_with_retries(input, retries, fallback_attempted: true)
66
+ end
67
+
68
+ def fail_codex_request(error)
69
+ @state.fail_last_assistant(error, friendly_message: codex_error_message(error))
70
+ end
71
+
72
+ def handle_rate_limit_error(error, retries, input, fallback_attempted)
73
+ next_retries = handle_rate_limit_retry(error, retries)
74
+ return attempt_with_retries(input, next_retries, fallback_attempted: fallback_attempted) if next_retries
75
+
76
+ @state.fail_last_assistant(error, friendly_message: rate_limit_message(error))
77
+ end
78
+
79
+ def should_fallback_to_default_model?(error, already_attempted)
80
+ return false if already_attempted
81
+ return false unless error.status == 400
82
+ return false unless error.message.match?(UNSUPPORTED_MODEL_PATTERN)
83
+
84
+ @model != DEFAULT_MODEL
85
+ end
86
+
87
+ def switch_to_default_model!(error)
88
+ previous_model = @model
89
+ @model = DEFAULT_MODEL
90
+ @state.model = DEFAULT_MODEL
91
+ @state.reset_last_assistant_content
92
+ @state.add_message(
93
+ :system,
94
+ "Model '#{previous_model}' requires ChatGPT Pro. " \
95
+ "Falling back to #{DEFAULT_MODEL} and retrying. Detail: #{error.message}"
96
+ )
97
+ end
98
+
59
99
  def handle_rate_limit_retry(error, retries)
60
100
  return unless retries < MAX_RATE_LIMIT_RETRIES && !@cancel_requested
61
101
 
@@ -69,18 +109,35 @@ module RubyCoded
69
109
  end
70
110
 
71
111
  def codex_error_message(error)
112
+ return unsupported_model_message(error) if unsupported_model_error?(error)
113
+
114
+ status_message(error) || "Codex API error: #{error.message}"
115
+ end
116
+
117
+ def unsupported_model_error?(error)
118
+ error.status == 400 && error.message.match?(UNSUPPORTED_MODEL_PATTERN)
119
+ end
120
+
121
+ def unsupported_model_message(error)
122
+ "The selected model requires ChatGPT Pro. " \
123
+ "Use /model to pick one without the 'Pro only' tag. (#{error.message})"
124
+ end
125
+
126
+ def status_message(error)
72
127
  case error.status
73
- when 401
74
- "Authentication failed. Your OAuth session may have expired. " \
75
- "Try /login to re-authenticate. (#{error.message})"
76
- when 403
77
- "Access denied. Your ChatGPT subscription may not include Codex access. (#{error.message})"
128
+ when 400 then "Codex API error: #{error.message}"
129
+ when 401 then authentication_error_message(error)
130
+ when 403 then "Access denied. Your ChatGPT subscription may not include Codex access. (#{error.message})"
78
131
  when 404 then "Codex endpoint not found. The API may have changed. (#{error.message})"
79
132
  when 429 then rate_limit_message(error)
80
- else "Codex API error: #{error.message}"
81
133
  end
82
134
  end
83
135
 
136
+ def authentication_error_message(error)
137
+ "Authentication failed. Your OAuth session may have expired. " \
138
+ "Try /login to re-authenticate. (#{error.message})"
139
+ end
140
+
84
141
  def rate_limit_message(error)
85
142
  <<~MSG.strip
86
143
  ChatGPT usage limit reached. This may be a 5-hour or weekly limit on your Plus/Pro subscription.
@@ -88,6 +145,7 @@ module RubyCoded
88
145
  MSG
89
146
  end
90
147
  end
148
+ # rubocop:enable Metrics/ModuleLength
91
149
  end
92
150
  end
93
151
  end
@@ -5,6 +5,7 @@ module RubyCoded
5
5
  class CodexBridge
6
6
  # Parses Server-Sent Events from the Codex streaming response and
7
7
  # dispatches content deltas, tool calls, and completion signals.
8
+ # rubocop:disable Metrics/ModuleLength
8
9
  module SSEParser
9
10
  StreamContext = Struct.new(
10
11
  :assistant_text, :pending_tool_calls, :buffer, :raw_body, :status, keyword_init: true
@@ -70,6 +71,7 @@ module RubyCoded
70
71
  when "response.function_call_arguments.delta" then handle_function_args_delta(event, pending_tool_calls)
71
72
  when "response.function_call_arguments.done" then handle_function_call_done(event, pending_tool_calls)
72
73
  when "response.output_item.added" then handle_output_item_added(event, pending_tool_calls)
74
+ when "response.completed" then handle_response_completed(event)
73
75
  end
74
76
  end
75
77
 
@@ -107,6 +109,23 @@ module RubyCoded
107
109
  tc[:arguments] = event["arguments"] || tc[:arguments] if tc
108
110
  end
109
111
 
112
+ # The Responses API emits a `response.completed` event at the end of the
113
+ # stream carrying the `usage` block. We mirror what `LLMBridge` does and
114
+ # feed those counters into `State` so that the status bar and the
115
+ # context-window indicator work for ChatGPT Plus/Pro users too.
116
+ def handle_response_completed(event)
117
+ usage = event.dig("response", "usage") || event["usage"]
118
+ return unless usage.is_a?(Hash)
119
+
120
+ @state.update_last_message_tokens(
121
+ model: @model,
122
+ input_tokens: usage["input_tokens"],
123
+ output_tokens: usage["output_tokens"],
124
+ thinking_tokens: usage.dig("output_tokens_details", "reasoning_tokens"),
125
+ cached_tokens: usage.dig("input_tokens_details", "cached_tokens")
126
+ )
127
+ end
128
+
110
129
  def finalize_response(assistant_text)
111
130
  @conversation_history << { role: "assistant", content: assistant_text } unless assistant_text.empty?
112
131
  end
@@ -131,6 +150,7 @@ module RubyCoded
131
150
  nil
132
151
  end
133
152
  end
153
+ # rubocop:enable Metrics/ModuleLength
134
154
  end
135
155
  end
136
156
  end
@@ -6,23 +6,34 @@ module RubyCoded
6
6
  # These models are not listed in RubyLLM.models because they use a
7
7
  # different API (Responses API via chatgpt.com/backend-api).
8
8
  module CodexModels
9
- CodexModel = Struct.new(:id, :display_name, :context_window, :max_output, keyword_init: true) do
9
+ PROVIDER = :openai
10
+
11
+ CodexModel = Struct.new(:id, :display_name, :context_window, :max_output, :pro_only,
12
+ keyword_init: true) do
10
13
  def to_s
11
14
  id
12
15
  end
16
+
17
+ def provider
18
+ PROVIDER
19
+ end
20
+
21
+ def pro_only?
22
+ pro_only == true
23
+ end
13
24
  end
14
25
 
15
26
  MODELS = [
16
27
  CodexModel.new(id: "gpt-5.4", display_name: "GPT 5.4 (Recommended)",
17
- context_window: 272_000, max_output: 128_000),
28
+ context_window: 272_000, max_output: 128_000, pro_only: false),
18
29
  CodexModel.new(id: "gpt-5.4-mini", display_name: "GPT 5.4 Mini",
19
- context_window: 272_000, max_output: 128_000),
20
- CodexModel.new(id: "gpt-5.3-codex-spark", display_name: "GPT 5.3 Codex Spark (Pro only)",
21
- context_window: 272_000, max_output: 128_000),
30
+ context_window: 272_000, max_output: 128_000, pro_only: false),
31
+ CodexModel.new(id: "gpt-5.3-codex-spark", display_name: "GPT 5.3 Codex Spark",
32
+ context_window: 272_000, max_output: 128_000, pro_only: true),
22
33
  CodexModel.new(id: "gpt-5.2-codex", display_name: "GPT 5.2 Codex",
23
- context_window: 272_000, max_output: 128_000),
34
+ context_window: 272_000, max_output: 128_000, pro_only: true),
24
35
  CodexModel.new(id: "gpt-5.2", display_name: "GPT 5.2",
25
- context_window: 272_000, max_output: 128_000)
36
+ context_window: 272_000, max_output: 128_000, pro_only: false)
26
37
  ].freeze
27
38
 
28
39
  def self.all
@@ -36,6 +47,24 @@ module RubyCoded
36
47
  def self.codex_model?(id)
37
48
  MODELS.any? { |m| m.id == id }
38
49
  end
50
+
51
+ def self.pro_only?(id)
52
+ model = find(id)
53
+ model ? model.pro_only? : false
54
+ end
55
+
56
+ PRO_TIER_PLANS = %w[pro team enterprise business edu].freeze
57
+
58
+ def self.available_for_plan(plan)
59
+ normalized = plan.to_s.downcase
60
+ return MODELS if normalized.empty? || PRO_TIER_PLANS.any? { |p| normalized.include?(p) }
61
+
62
+ if normalized.include?("plus") || normalized.include?("free")
63
+ MODELS.reject(&:pro_only?)
64
+ else
65
+ MODELS
66
+ end
67
+ end
39
68
  end
40
69
  end
41
70
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ class CommandHandler
6
+ # Slash commands for managing custom markdown commands.
7
+ module CustomCommands
8
+ private
9
+
10
+ def cmd_commands(rest)
11
+ case rest&.strip&.downcase
12
+ when "reload"
13
+ reload_commands
14
+ when "list"
15
+ list_commands
16
+ else
17
+ @state.add_message(:system, "Usage: /commands [reload|list]")
18
+ end
19
+ end
20
+
21
+ def reload_commands
22
+ return missing_command_catalog unless @command_catalog
23
+
24
+ report = @command_catalog.reload!
25
+ @commands = build_command_map
26
+ @state.add_message(:system, format_reload_message(report))
27
+ end
28
+
29
+ def list_commands
30
+ return missing_command_catalog unless @command_catalog
31
+
32
+ commands = @command_catalog.definitions_for_source(:markdown)
33
+ return show_empty_custom_commands if commands.empty?
34
+
35
+ @state.add_message(:system, formatted_custom_commands(commands))
36
+ end
37
+
38
+ def missing_command_catalog
39
+ @state.add_message(:system, "Command catalog is not available.")
40
+ end
41
+
42
+ def show_empty_custom_commands
43
+ @state.add_message(
44
+ :system,
45
+ "No custom commands loaded. Add markdown files under .ruby_coded/commands " \
46
+ "and run /commands reload."
47
+ )
48
+ end
49
+
50
+ def formatted_custom_commands(commands)
51
+ lines = ["Custom commands:"]
52
+ commands.sort_by { |definition| definition.name.downcase }.each do |definition|
53
+ lines << formatted_command_line(definition)
54
+ end
55
+ lines.join("\n")
56
+ end
57
+
58
+ def format_reload_message(report)
59
+ message = reload_summary(report)
60
+ details = reload_details(report)
61
+ return message if details.empty?
62
+
63
+ "#{message}\n#{details.join("\n")}"
64
+ end
65
+
66
+ def reload_summary(report)
67
+ "Commands reloaded. " \
68
+ "Added: #{report[:added]}, removed: #{report[:removed]}, " \
69
+ "total custom commands: #{report[:total]}, " \
70
+ "invalid files ignored: #{report[:invalid]}, " \
71
+ "conflicts ignored: #{report[:conflicts]}."
72
+ end
73
+
74
+ def reload_details(report)
75
+ details = []
76
+ invalid_files = Array(report[:invalid_files])
77
+ conflict_commands = Array(report[:conflict_commands])
78
+
79
+ details << "Invalid files: #{invalid_files.join(", ")}" if invalid_files.any?
80
+ details << "Conflicting commands: #{conflict_commands.join(", ")}" if conflict_commands.any?
81
+ details
82
+ end
83
+
84
+ def formatted_command_line(definition)
85
+ usage = definition.usage || definition.name
86
+ " #{usage.ljust(28)} #{definition.description}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../model_filter"
4
4
  require_relative "../codex_models"
5
+ require_relative "../../auth/jwt_decoder"
5
6
 
6
7
  module RubyCoded
7
8
  module Chat
@@ -74,12 +75,18 @@ module RubyCoded
74
75
 
75
76
  def models_for_provider(name, creds)
76
77
  if name == :openai && creds["auth_method"] == "oauth"
77
- CodexModels.all
78
+ codex_models_for_plan(creds)
78
79
  else
79
80
  RubyLLM.models.by_provider(name).chat_models.to_a
80
81
  end
81
82
  end
82
83
 
84
+ def codex_models_for_plan(creds)
85
+ token = creds["access_token"]
86
+ plan = token ? Auth::JWTDecoder.extract_plan_type(token) : nil
87
+ CodexModels.available_for_plan(plan)
88
+ end
89
+
83
90
  def fetch_chat_models
84
91
  RubyLLM.models.chat_models.to_a
85
92
  rescue StandardError