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 +4 -4
- data/.jules/bolt.md +5 -0
- data/.jules/palette.md +4 -0
- data/.jules/sentinel.md +5 -0
- data/README.md +8 -6
- data/jules-ruby.gemspec +1 -1
- data/lib/jules-ruby/cli/interactive/session_manager.rb +20 -1
- data/lib/jules-ruby/client.rb +44 -16
- data/lib/jules-ruby/models/session.rb +3 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08f7c3871c4be4346aedc491e1071191d9ca0c3b6177ebcf48501b9128b5608b'
|
|
4
|
+
data.tar.gz: 3e9ba72098d344c68194a8a0fcccb202aa9b2c8b0a198c6fed0fa97288a4cace
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
data/.jules/sentinel.md
ADDED
|
@@ -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
|
-
|
|
1
|
+
[](https://badge.fury.io/rb/jules-ruby)
|
|
2
|
+
[](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="
|
|
7
|
+
<img src="assets/banner.png" alt="jules-ruby" width="600">
|
|
5
8
|
</p>
|
|
6
9
|
|
|
7
|
-
[
|
|
8
|
-
[](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
|
-
|
|
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
|
|
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
|
@@ -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
|
-
|
|
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)
|
data/lib/jules-ruby/client.rb
CHANGED
|
@@ -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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|