jules-ruby 0.0.67

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.jules/bolt.md +4 -0
  3. data/.rubocop.yml +51 -0
  4. data/AGENTS.md +250 -0
  5. data/CHANGELOG.md +20 -0
  6. data/CONTRIBUTING.md +82 -0
  7. data/LICENSE +21 -0
  8. data/README.md +330 -0
  9. data/Rakefile +70 -0
  10. data/SECURITY.md +41 -0
  11. data/assets/banner.png +0 -0
  12. data/bin/jules-ruby +7 -0
  13. data/jules-ruby.gemspec +43 -0
  14. data/lib/jules-ruby/cli/activities.rb +142 -0
  15. data/lib/jules-ruby/cli/banner.rb +113 -0
  16. data/lib/jules-ruby/cli/base.rb +38 -0
  17. data/lib/jules-ruby/cli/interactive/activity_renderer.rb +81 -0
  18. data/lib/jules-ruby/cli/interactive/session_creator.rb +112 -0
  19. data/lib/jules-ruby/cli/interactive/session_manager.rb +285 -0
  20. data/lib/jules-ruby/cli/interactive/source_manager.rb +65 -0
  21. data/lib/jules-ruby/cli/interactive.rb +48 -0
  22. data/lib/jules-ruby/cli/prompts.rb +184 -0
  23. data/lib/jules-ruby/cli/sessions.rb +185 -0
  24. data/lib/jules-ruby/cli/sources.rb +72 -0
  25. data/lib/jules-ruby/cli.rb +127 -0
  26. data/lib/jules-ruby/client.rb +130 -0
  27. data/lib/jules-ruby/configuration.rb +20 -0
  28. data/lib/jules-ruby/errors.rb +35 -0
  29. data/lib/jules-ruby/models/activity.rb +137 -0
  30. data/lib/jules-ruby/models/artifact.rb +78 -0
  31. data/lib/jules-ruby/models/github_branch.rb +17 -0
  32. data/lib/jules-ruby/models/github_repo.rb +31 -0
  33. data/lib/jules-ruby/models/plan.rb +23 -0
  34. data/lib/jules-ruby/models/plan_step.rb +25 -0
  35. data/lib/jules-ruby/models/pull_request.rb +23 -0
  36. data/lib/jules-ruby/models/session.rb +111 -0
  37. data/lib/jules-ruby/models/source.rb +23 -0
  38. data/lib/jules-ruby/models/source_context.rb +35 -0
  39. data/lib/jules-ruby/resources/activities.rb +76 -0
  40. data/lib/jules-ruby/resources/base.rb +27 -0
  41. data/lib/jules-ruby/resources/sessions.rb +125 -0
  42. data/lib/jules-ruby/resources/sources.rb +61 -0
  43. data/lib/jules-ruby/version.rb +5 -0
  44. data/lib/jules-ruby.rb +43 -0
  45. data/mise.toml +2 -0
  46. metadata +232 -0
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+ require 'async/http/internet'
5
+ require 'json'
6
+ require 'uri'
7
+
8
+ module JulesRuby
9
+ class Client
10
+ attr_reader :configuration
11
+
12
+ DEFAULT_HEADERS = {
13
+ 'Content-Type' => 'application/json',
14
+ 'Accept' => 'application/json'
15
+ }.freeze
16
+
17
+ def initialize(api_key: nil, base_url: nil, timeout: nil)
18
+ @configuration = JulesRuby.configuration&.dup || Configuration.new
19
+
20
+ @configuration.api_key = api_key if api_key
21
+ @configuration.base_url = base_url if base_url
22
+ @configuration.timeout = timeout if timeout
23
+
24
+ validate_configuration!
25
+ end
26
+
27
+ # Resource accessors
28
+ def sources
29
+ @sources ||= Resources::Sources.new(self)
30
+ end
31
+
32
+ def sessions
33
+ @sessions ||= Resources::Sessions.new(self)
34
+ end
35
+
36
+ def activities
37
+ @activities ||= Resources::Activities.new(self)
38
+ end
39
+
40
+ # HTTP methods
41
+ def get(path, params: {})
42
+ request(:get, path, params: params)
43
+ end
44
+
45
+ def post(path, body: {})
46
+ request(:post, path, body: body)
47
+ end
48
+
49
+ def delete(path)
50
+ request(:delete, path)
51
+ end
52
+
53
+ private
54
+
55
+ def validate_configuration!
56
+ return if configuration.valid?
57
+
58
+ raise ConfigurationError,
59
+ 'API key is required. Set JULES_API_KEY environment variable or pass api_key to Client.new'
60
+ end
61
+
62
+ def request(method, path, params: {}, body: nil)
63
+ url = build_url(path, params)
64
+
65
+ Async do
66
+ internet = Async::HTTP::Internet.new
67
+
68
+ begin
69
+ headers = build_headers
70
+
71
+ response = case method
72
+ when :get
73
+ internet.get(url, headers)
74
+ when :post
75
+ internet.post(url, headers, body ? JSON.generate(body) : nil)
76
+ when :delete
77
+ internet.delete(url, headers)
78
+ else
79
+ raise ArgumentError, "Unsupported HTTP method: #{method}"
80
+ end
81
+
82
+ handle_response(response)
83
+ ensure
84
+ internet.close
85
+ end
86
+ end.wait
87
+ end
88
+
89
+ def build_url(path, params)
90
+ # Ensure base_url ends without slash and path starts with slash
91
+ base = configuration.base_url.chomp('/')
92
+ path = "/#{path}" unless path.start_with?('/')
93
+
94
+ uri = URI.parse("#{base}#{path}")
95
+ uri.query = URI.encode_www_form(params.compact) unless params.empty?
96
+ uri.to_s
97
+ end
98
+
99
+ def build_headers
100
+ # Optimization: Reuse DEFAULT_HEADERS hash to avoid multiple array allocations per request
101
+ headers = DEFAULT_HEADERS.dup
102
+ headers['X-Goog-Api-Key'] = configuration.api_key
103
+ headers
104
+ end
105
+
106
+ def handle_response(response)
107
+ body = response.read
108
+ status = response.status
109
+
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)
125
+ else
126
+ raise Error.new("Unexpected response: #{status}", status_code: status, response: body)
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ class Configuration
5
+ attr_accessor :api_key, :base_url, :timeout
6
+
7
+ DEFAULT_BASE_URL = 'https://jules.googleapis.com/v1alpha'
8
+ DEFAULT_TIMEOUT = 30
9
+
10
+ def initialize
11
+ @api_key = ENV.fetch('JULES_API_KEY', nil)
12
+ @base_url = DEFAULT_BASE_URL
13
+ @timeout = DEFAULT_TIMEOUT
14
+ end
15
+
16
+ def valid?
17
+ !api_key.nil? && !api_key.empty?
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ # Base error class
5
+ class Error < StandardError
6
+ attr_reader :response, :status_code
7
+
8
+ def initialize(message = nil, response: nil, status_code: nil)
9
+ @response = response
10
+ @status_code = status_code
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ # 400 Bad Request
16
+ class BadRequestError < Error; end
17
+
18
+ # 401 Unauthorized
19
+ class AuthenticationError < Error; end
20
+
21
+ # 403 Forbidden
22
+ class ForbiddenError < Error; end
23
+
24
+ # 404 Not Found
25
+ class NotFoundError < Error; end
26
+
27
+ # 429 Too Many Requests
28
+ class RateLimitError < Error; end
29
+
30
+ # 5xx Server Errors
31
+ class ServerError < Error; end
32
+
33
+ # Configuration error
34
+ class ConfigurationError < Error; end
35
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class Activity
6
+ ORIGINATORS = %w[user agent system].freeze
7
+
8
+ attr_reader :name, :id, :description, :create_time, :originator, :artifacts,
9
+ :agent_messaged, :user_messaged, :plan_generated, :plan_approved,
10
+ :progress_updated, :session_completed, :session_failed
11
+
12
+ def initialize(data)
13
+ @name = data['name']
14
+ @id = data['id']
15
+ @description = data['description']
16
+ @create_time = data['createTime']
17
+ @originator = data['originator']
18
+ @artifacts = (data['artifacts'] || []).map { |a| Artifact.new(a) }
19
+
20
+ # Activity type (union field)
21
+ @agent_messaged = data['agentMessaged']
22
+ @user_messaged = data['userMessaged']
23
+ @plan_generated = parse_plan_generated(data['planGenerated'])
24
+ @plan_approved = data['planApproved']
25
+ @progress_updated = data['progressUpdated']
26
+ @session_completed = data['sessionCompleted']
27
+ @session_failed = data['sessionFailed']
28
+ end
29
+
30
+ def type
31
+ if agent_messaged
32
+ :agent_messaged
33
+ elsif user_messaged
34
+ :user_messaged
35
+ elsif plan_generated
36
+ :plan_generated
37
+ elsif plan_approved
38
+ :plan_approved
39
+ elsif progress_updated
40
+ :progress_updated
41
+ elsif session_completed
42
+ :session_completed
43
+ elsif session_failed
44
+ :session_failed
45
+ else
46
+ :unknown
47
+ end
48
+ end
49
+
50
+ # Type check helpers
51
+ def agent_message?
52
+ !agent_messaged.nil?
53
+ end
54
+
55
+ def user_message?
56
+ !user_messaged.nil?
57
+ end
58
+
59
+ def plan_generated?
60
+ !plan_generated.nil?
61
+ end
62
+
63
+ def plan_approved?
64
+ !plan_approved.nil?
65
+ end
66
+
67
+ def progress_update?
68
+ !progress_updated.nil?
69
+ end
70
+
71
+ def session_completed?
72
+ !session_completed.nil?
73
+ end
74
+
75
+ def session_failed?
76
+ !session_failed.nil?
77
+ end
78
+
79
+ def from_agent?
80
+ originator == 'agent'
81
+ end
82
+
83
+ def from_user?
84
+ originator == 'user'
85
+ end
86
+
87
+ def from_system?
88
+ originator == 'system'
89
+ end
90
+
91
+ # Content helpers
92
+ def message
93
+ agent_messaged&.dig('agentMessage') || user_messaged&.dig('userMessage')
94
+ end
95
+
96
+ def plan
97
+ plan_generated
98
+ end
99
+
100
+ def approved_plan_id
101
+ plan_approved&.dig('planId')
102
+ end
103
+
104
+ def progress_title
105
+ progress_updated&.dig('title')
106
+ end
107
+
108
+ def progress_description
109
+ progress_updated&.dig('description')
110
+ end
111
+
112
+ def failure_reason
113
+ session_failed&.dig('reason')
114
+ end
115
+
116
+ def to_h
117
+ {
118
+ name: name,
119
+ id: id,
120
+ description: description,
121
+ create_time: create_time,
122
+ originator: originator,
123
+ type: type,
124
+ artifacts: artifacts.map(&:to_h)
125
+ }
126
+ end
127
+
128
+ private
129
+
130
+ def parse_plan_generated(data)
131
+ return nil unless data
132
+
133
+ data['plan'] ? Plan.new(data['plan']) : nil
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class Artifact
6
+ attr_reader :change_set, :media, :bash_output
7
+
8
+ def initialize(data)
9
+ @change_set = data['changeSet']
10
+ @media = data['media']
11
+ @bash_output = data['bashOutput']
12
+ end
13
+
14
+ def type
15
+ if change_set
16
+ :change_set
17
+ elsif media
18
+ :media
19
+ elsif bash_output
20
+ :bash_output
21
+ else
22
+ :unknown
23
+ end
24
+ end
25
+
26
+ # ChangeSet helpers
27
+ def source
28
+ change_set&.dig('source')
29
+ end
30
+
31
+ def git_patch
32
+ change_set&.dig('gitPatch')
33
+ end
34
+
35
+ def unidiff_patch
36
+ git_patch&.dig('unidiffPatch')
37
+ end
38
+
39
+ def base_commit_id
40
+ git_patch&.dig('baseCommitId')
41
+ end
42
+
43
+ def suggested_commit_message
44
+ git_patch&.dig('suggestedCommitMessage')
45
+ end
46
+
47
+ # Media helpers
48
+ def media_data
49
+ media&.dig('data')
50
+ end
51
+
52
+ def media_mime_type
53
+ media&.dig('mimeType')
54
+ end
55
+
56
+ # BashOutput helpers
57
+ def bash_command
58
+ bash_output&.dig('command')
59
+ end
60
+
61
+ def bash_output_text
62
+ bash_output&.dig('output')
63
+ end
64
+
65
+ def bash_exit_code
66
+ bash_output&.dig('exitCode')
67
+ end
68
+
69
+ def to_h
70
+ {
71
+ change_set: change_set,
72
+ media: media,
73
+ bash_output: bash_output
74
+ }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class GitHubBranch
6
+ attr_reader :display_name
7
+
8
+ def initialize(data)
9
+ @display_name = data['displayName']
10
+ end
11
+
12
+ def to_h
13
+ { display_name: display_name }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class GitHubRepo
6
+ attr_reader :owner, :repo, :is_private, :default_branch, :branches
7
+
8
+ def initialize(data)
9
+ @owner = data['owner']
10
+ @repo = data['repo']
11
+ @is_private = data['isPrivate']
12
+ @default_branch = data['defaultBranch'] ? GitHubBranch.new(data['defaultBranch']) : nil
13
+ @branches = (data['branches'] || []).map { |b| GitHubBranch.new(b) }
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ owner: owner,
19
+ repo: repo,
20
+ is_private: is_private,
21
+ default_branch: default_branch&.to_h,
22
+ branches: branches.map(&:to_h)
23
+ }
24
+ end
25
+
26
+ def full_name
27
+ "#{owner}/#{repo}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class Plan
6
+ attr_reader :id, :steps, :create_time
7
+
8
+ def initialize(data)
9
+ @id = data['id']
10
+ @steps = (data['steps'] || []).map { |s| PlanStep.new(s) }
11
+ @create_time = data['createTime']
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ id: id,
17
+ steps: steps.map(&:to_h),
18
+ create_time: create_time
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class PlanStep
6
+ attr_reader :id, :title, :description, :index
7
+
8
+ def initialize(data)
9
+ @id = data['id']
10
+ @title = data['title']
11
+ @description = data['description']
12
+ @index = data['index']
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ id: id,
18
+ title: title,
19
+ description: description,
20
+ index: index
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class PullRequest
6
+ attr_reader :url, :title, :description
7
+
8
+ def initialize(data)
9
+ @url = data['url']
10
+ @title = data['title']
11
+ @description = data['description']
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ url: url,
17
+ title: title,
18
+ description: description
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class Session
6
+ # Session states
7
+ STATES = %w[
8
+ STATE_UNSPECIFIED
9
+ QUEUED
10
+ PLANNING
11
+ AWAITING_PLAN_APPROVAL
12
+ AWAITING_USER_FEEDBACK
13
+ IN_PROGRESS
14
+ PAUSED
15
+ FAILED
16
+ COMPLETED
17
+ ].freeze
18
+
19
+ # Automation modes
20
+ AUTOMATION_MODES = %w[
21
+ AUTOMATION_MODE_UNSPECIFIED
22
+ AUTO_CREATE_PR
23
+ ].freeze
24
+
25
+ attr_reader :name, :id, :prompt, :title, :source_context, :state,
26
+ :url, :outputs, :create_time, :update_time,
27
+ :require_plan_approval, :automation_mode
28
+
29
+ def initialize(data)
30
+ @name = data['name']
31
+ @id = data['id']
32
+ @prompt = data['prompt']
33
+ @title = data['title']
34
+ @source_context = data['sourceContext'] ? SourceContext.new(data['sourceContext']) : nil
35
+ @state = data['state']
36
+ @url = data['url']
37
+ @create_time = data['createTime']
38
+ @update_time = data['updateTime']
39
+ @require_plan_approval = data['requirePlanApproval']
40
+ @automation_mode = data['automationMode']
41
+ @outputs = parse_outputs(data['outputs'])
42
+ end
43
+
44
+ def to_h
45
+ {
46
+ name: name,
47
+ id: id,
48
+ prompt: prompt,
49
+ title: title,
50
+ source_context: source_context&.to_h,
51
+ state: state,
52
+ url: url,
53
+ outputs: outputs.map(&:to_h),
54
+ create_time: create_time,
55
+ update_time: update_time
56
+ }
57
+ end
58
+
59
+ # State check helpers
60
+ def queued?
61
+ state == 'QUEUED'
62
+ end
63
+
64
+ def planning?
65
+ state == 'PLANNING'
66
+ end
67
+
68
+ def awaiting_plan_approval?
69
+ state == 'AWAITING_PLAN_APPROVAL'
70
+ end
71
+
72
+ def awaiting_user_feedback?
73
+ state == 'AWAITING_USER_FEEDBACK'
74
+ end
75
+
76
+ def in_progress?
77
+ state == 'IN_PROGRESS'
78
+ end
79
+
80
+ def paused?
81
+ state == 'PAUSED'
82
+ end
83
+
84
+ def failed?
85
+ state == 'FAILED'
86
+ end
87
+
88
+ def completed?
89
+ state == 'COMPLETED'
90
+ end
91
+
92
+ def active?
93
+ %w[QUEUED PLANNING AWAITING_PLAN_APPROVAL AWAITING_USER_FEEDBACK IN_PROGRESS].include?(state)
94
+ end
95
+
96
+ private
97
+
98
+ def parse_outputs(outputs_data)
99
+ return [] unless outputs_data
100
+
101
+ outputs_data.map do |output|
102
+ if output['pullRequest']
103
+ PullRequest.new(output['pullRequest'])
104
+ else
105
+ output
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class Source
6
+ attr_reader :name, :id, :github_repo
7
+
8
+ def initialize(data)
9
+ @name = data['name']
10
+ @id = data['id']
11
+ @github_repo = data['githubRepo'] ? GitHubRepo.new(data['githubRepo']) : nil
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ name: name,
17
+ id: id,
18
+ github_repo: github_repo&.to_h
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JulesRuby
4
+ module Models
5
+ class SourceContext
6
+ attr_reader :source, :github_repo_context
7
+
8
+ def initialize(data)
9
+ @source = data['source']
10
+ @github_repo_context = data['githubRepoContext']
11
+ end
12
+
13
+ def starting_branch
14
+ github_repo_context&.dig('startingBranch')
15
+ end
16
+
17
+ def to_h
18
+ {
19
+ source: source,
20
+ github_repo_context: github_repo_context
21
+ }
22
+ end
23
+
24
+ # Build a SourceContext hash for API requests
25
+ def self.build(source:, starting_branch:)
26
+ {
27
+ 'source' => source,
28
+ 'githubRepoContext' => {
29
+ 'startingBranch' => starting_branch
30
+ }
31
+ }
32
+ end
33
+ end
34
+ end
35
+ end