jules-ruby 0.0.67 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b8c57f38a3d3f7419ad7bd5a6a1f2690ba72b0106ccbe01ce4cdf28b115af7d
4
- data.tar.gz: 04cd808849c6993f215fd99a697e7b02553c3616ce308782881365c21f6f2026
3
+ metadata.gz: '08f7c3871c4be4346aedc491e1071191d9ca0c3b6177ebcf48501b9128b5608b'
4
+ data.tar.gz: 3e9ba72098d344c68194a8a0fcccb202aa9b2c8b0a198c6fed0fa97288a4cace
5
5
  SHA512:
6
- metadata.gz: bd7a74a418c3fbaaf2b5118433ec6268dd4da3bd1485dfd066193c380953f742a9e1ef00067f6d29acdc0146ab43ce12d7495ca3676222d027ccd5cdbc667bb8
7
- data.tar.gz: 31d50df4687c9fbfef15cab8299f3997cab960f5a5286f7bacd5db0e6094f7a42833747e14f23f1c4a70716e9c3821e114a1528842108889c4323666f78acca8
6
+ metadata.gz: 5cd91076f026b02f6a206681d84f35113ddd8394f9e9a7c9dcdbd0a79f843c3d0b1367e182e1e267772968ea9d14915ad9f0fa48260467e6a40b8a884da79ace
7
+ data.tar.gz: 91299b3412fdfe89aa2a4b852ecd7e9662d00cb36b34ec8f6e8268631d12c74b7ce757f51feec75429157e4468754283ff0024cf0a993a1ab4021c6cc1d496c2
data/.jules/bolt.md CHANGED
@@ -2,3 +2,8 @@
2
2
 
3
3
  **Learning:** `Async::HTTP::Internet.new` is instantiated for every request in `JulesRuby::Client#request`, which likely prevents connection keep-alive/pooling.
4
4
  **Action:** A significant future optimization would be to reuse the `Async::HTTP::Internet` instance, but this requires careful management of the `Async` reactor lifecycle since the current implementation wraps each request in its own `Async { ... }.wait` block.
5
+
6
+ ## 2025-12-18 - Failed Optimization: Connection Reuse
7
+
8
+ **Learning:** Attempted to reuse `Async::HTTP::Internet` in `JulesRuby::Client` to enable connection pooling. This failed because `JulesRuby::Client#request` wraps each call in a new `Async` reactor (`Async { ... }.wait`). Sockets are tied to the reactor they were created in; reusing them in a subsequent transient reactor causes errors or fails to work. Additionally, reusing the client instance makes `JulesRuby::Client` thread-unsafe.
9
+ **Action:** Do not attempt connection pooling unless the entire application architecture changes to use a persistent reactor or `JulesRuby::Client` is refactored to be purely async (returning Tasks instead of blocking).
data/.jules/palette.md ADDED
@@ -0,0 +1,4 @@
1
+ ## 2025-12-18 - Better API Error Messages
2
+
3
+ **Learning:** Users were receiving generic "Bad request" or "Server error" messages even when the API provided specific error details in the JSON body. This forced users to inspect exception objects to find the root cause.
4
+ **Action:** Updated `Client#handle_response` to parse and bubble up specific error messages from the API response (e.g., `error.message` in JSON), significantly improving debuggability.
@@ -0,0 +1,5 @@
1
+ ## 2025-12-18 - [Security] Validate Session URLs
2
+
3
+ **Vulnerability:** The interactive CLI opened URLs from API responses using `system('open', url)` without validation, allowing execution of potentially unsafe schemes (e.g., `file://`) or arbitrary commands if the URL was maliciously crafted.
4
+ **Learning:** `system('open', ...)` on macOS treats arguments as files or URLs, and can open applications. Trusting external input (even from our own API) for system commands carries risk.
5
+ **Prevention:** Always validate URL schemes (allowlist `http`, `https`) before passing them to system commands.
data/README.md CHANGED
@@ -1,13 +1,15 @@
1
- # Jules Ruby
1
+ [![Gem Version](https://badge.fury.io/rb/jules-ruby.svg)](https://badge.fury.io/rb/jules-ruby)
2
+ [![CI](https://github.com/tweibley/jules-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/tweibley/jules-ruby/actions/workflows/ci.yml)
3
+
4
+ # jules-ruby
2
5
 
3
6
  <p align="center">
4
- <img src="assets/banner.png" alt="Jules Ruby" width="600">
7
+ <img src="assets/banner.png" alt="jules-ruby" width="600">
5
8
  </p>
6
9
 
7
- [![Gem Version](https://badge.fury.io/rb/jules-ruby.svg)](https://badge.fury.io/rb/jules-ruby)
8
- [![CI](https://github.com/tweibley/jules-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/tweibley/jules-ruby/actions/workflows/ci.yml)
10
+ A Ruby gem for interacting with the [jules API](https://developers.google.com/jules/api) to programmatically create and manage asynchronous coding tasks.
9
11
 
10
- A Ruby gem for interacting with the [Jules API](https://developers.google.com/jules/api) to programmatically create and manage asynchronous coding tasks.
12
+ > 🚀 **New!** Use jules-ruby directly from [Gemini CLI](https://github.com/google-gemini/gemini-cli) with our extension: [jules-ruby-gemini-cli-extension](https://github.com/tweibley/jules-ruby-gemini-cli-extension)
11
13
 
12
14
  ## Installation
13
15
 
@@ -60,7 +62,7 @@ client = JulesRuby::Client.new(api_key: 'different_api_key')
60
62
 
61
63
  ## Command-Line Interface
62
64
 
63
- The gem includes a CLI for interacting with the Jules API from your terminal.
65
+ The gem includes a CLI for interacting with the jules API from your terminal.
64
66
 
65
67
  ### Installation
66
68
 
data/jules-ruby.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "jules-ruby"
5
- spec.version = "0.0.67"
5
+ spec.version = "0.1.0"
6
6
  spec.authors = ["Taylor Weibley"]
7
7
  spec.email = ["tweibley@gmail.com"]
8
8
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
3
4
  require_relative '../prompts'
4
5
  require_relative 'activity_renderer'
5
6
  require_relative 'session_creator'
@@ -193,7 +194,7 @@ module JulesRuby
193
194
  view_activities(session)
194
195
  :refresh
195
196
  when :open_url
196
- system('open', session.url) if session.url
197
+ open_session_url(session)
197
198
  nil
198
199
  when :delete then delete_session?(session) ? :deleted : nil
199
200
  when :refresh then refresh_session(session)
@@ -252,6 +253,24 @@ module JulesRuby
252
253
  true
253
254
  end
254
255
 
256
+ def open_session_url(session)
257
+ return unless session.url
258
+
259
+ if safe_url?(session.url)
260
+ system('open', session.url)
261
+ else
262
+ @prompt.warn(Prompts.rgb_color("Invalid URL scheme: #{session.url}", :purple))
263
+ @prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
264
+ end
265
+ end
266
+
267
+ def safe_url?(url)
268
+ uri = URI.parse(url)
269
+ %w[http https].include?(uri.scheme)
270
+ rescue URI::InvalidURIError
271
+ false
272
+ end
273
+
255
274
  def refresh_session(session)
256
275
  Prompts.with_spinner('Refreshing...') do
257
276
  @client.sessions.find(session.name)
@@ -14,6 +14,14 @@ module JulesRuby
14
14
  'Accept' => 'application/json'
15
15
  }.freeze
16
16
 
17
+ ERROR_MAPPING = {
18
+ 400 => [BadRequestError, 'Bad request'],
19
+ 401 => [AuthenticationError, 'Invalid API key'],
20
+ 403 => [ForbiddenError, 'Access forbidden'],
21
+ 404 => [NotFoundError, 'Resource not found'],
22
+ 429 => [RateLimitError, 'Rate limit exceeded']
23
+ }.freeze
24
+
17
25
  def initialize(api_key: nil, base_url: nil, timeout: nil)
18
26
  @configuration = JulesRuby.configuration&.dup || Configuration.new
19
27
 
@@ -107,24 +115,44 @@ module JulesRuby
107
115
  body = response.read
108
116
  status = response.status
109
117
 
110
- case status
111
- when 200..299
112
- body.nil? || body.empty? ? {} : JSON.parse(body)
113
- when 400
114
- raise BadRequestError.new('Bad request', status_code: status, response: body)
115
- when 401
116
- raise AuthenticationError.new('Invalid API key', status_code: status, response: body)
117
- when 403
118
- raise ForbiddenError.new('Access forbidden', status_code: status, response: body)
119
- when 404
120
- raise NotFoundError.new('Resource not found', status_code: status, response: body)
121
- when 429
122
- raise RateLimitError.new('Rate limit exceeded', status_code: status, response: body)
123
- when 500..599
124
- raise ServerError.new('Server error', status_code: status, response: body)
118
+ return parse_success_response(body) if (200..299).cover?(status)
119
+
120
+ handle_error_response(status, body)
121
+ end
122
+
123
+ def parse_success_response(body)
124
+ body.nil? || body.empty? ? {} : JSON.parse(body)
125
+ end
126
+
127
+ def handle_error_response(status, body)
128
+ if (klass, default_msg = ERROR_MAPPING[status])
129
+ raise klass.new(extract_error_message(body, default_msg), status_code: status, response: body)
130
+ end
131
+
132
+ if (500..599).cover?(status)
133
+ raise ServerError.new(extract_error_message(body, 'Server error'), status_code: status, response: body)
134
+ end
135
+
136
+ raise Error.new("Unexpected response: #{status}", status_code: status, response: body)
137
+ end
138
+
139
+ def extract_error_message(body, default)
140
+ return default if body.nil? || body.empty?
141
+
142
+ data = JSON.parse(body)
143
+ return default unless data.is_a?(Hash)
144
+
145
+ if data['error'].is_a?(Hash)
146
+ data.dig('error', 'message') || default
147
+ elsif data['error'].is_a?(String)
148
+ data['error']
149
+ elsif data['message'].is_a?(String)
150
+ data['message']
125
151
  else
126
- raise Error.new("Unexpected response: #{status}", status_code: status, response: body)
152
+ default
127
153
  end
154
+ rescue JSON::ParserError
155
+ default
128
156
  end
129
157
  end
130
158
  end
@@ -89,8 +89,10 @@ module JulesRuby
89
89
  state == 'COMPLETED'
90
90
  end
91
91
 
92
+ ACTIVE_STATES = %w[QUEUED PLANNING AWAITING_PLAN_APPROVAL AWAITING_USER_FEEDBACK IN_PROGRESS].freeze
93
+
92
94
  def active?
93
- %w[QUEUED PLANNING AWAITING_PLAN_APPROVAL AWAITING_USER_FEEDBACK IN_PROGRESS].include?(state)
95
+ ACTIVE_STATES.include?(state)
94
96
  end
95
97
 
96
98
  private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jules-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.67
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taylor Weibley
@@ -159,6 +159,8 @@ extensions: []
159
159
  extra_rdoc_files: []
160
160
  files:
161
161
  - ".jules/bolt.md"
162
+ - ".jules/palette.md"
163
+ - ".jules/sentinel.md"
162
164
  - ".rubocop.yml"
163
165
  - AGENTS.md
164
166
  - CHANGELOG.md