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,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'prompts'
4
+ require_relative 'interactive/session_creator'
5
+ require_relative 'interactive/session_manager'
6
+ require_relative 'interactive/source_manager'
7
+
8
+ module JulesRuby
9
+ # Interactive mode for jules-ruby CLI
10
+ class Interactive
11
+ def initialize
12
+ @client = JulesRuby::Client.new
13
+ @prompt = Prompts.prompt
14
+ end
15
+
16
+ def start
17
+ loop do
18
+ Prompts.clear_screen
19
+ Prompts.print_banner
20
+
21
+ choice = main_menu_selection
22
+
23
+ case choice
24
+ when :create_session
25
+ SessionCreator.new(@client, @prompt).run
26
+ when :view_sessions
27
+ SessionManager.new(@client, @prompt).run
28
+ when :browse_sources
29
+ SourceManager.new(@client, @prompt).run
30
+ when :exit
31
+ puts Prompts.rgb_color("\nGoodbye! 👋", :purple)
32
+ break
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def main_menu_selection
40
+ @prompt.select(Prompts.rgb_color('What would you like to do?', :lavender), cycle: true) do |menu|
41
+ menu.choice Prompts.rgb_color('Create new session', :purple), :create_session
42
+ menu.choice Prompts.rgb_color('View sessions', :purple), :view_sessions
43
+ menu.choice Prompts.rgb_color('Browse sources', :purple), :browse_sources
44
+ menu.choice Prompts.rgb_color('Exit', :purple), :exit
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+ require 'tty-prompt'
5
+ require 'tty-spinner'
6
+ require_relative 'banner'
7
+
8
+ module JulesRuby
9
+ # Helper methods for interactive prompts
10
+ module Prompts
11
+ # Custom true-color RGB theme matching Jules CLI design
12
+ PASTEL = Pastel.new
13
+
14
+ # RGB color values matching the reference CLI screenshot
15
+ COLORS = {
16
+ purple: [147, 112, 219], # Selection highlight #9370DB
17
+ lavender: [196, 181, 253], # Accent text #C4B5FD
18
+ muted: [139, 92, 246], # Muted purple #8B5CF6
19
+ dim: [107, 114, 128] # Dim gray #6B7280
20
+ }.freeze
21
+
22
+ # Define custom symbols for the prompt
23
+ PROMPT_SYMBOLS = {
24
+ marker: '❯',
25
+ radio_on: '◉',
26
+ radio_off: '◯'
27
+ }.freeze
28
+
29
+ # State emoji indicators
30
+ STATE_EMOJI = {
31
+ 'QUEUED' => '⏳',
32
+ 'PLANNING' => '🔵',
33
+ 'AWAITING_PLAN_APPROVAL' => '🟡',
34
+ 'AWAITING_USER_FEEDBACK' => '🟠',
35
+ 'IN_PROGRESS' => '🔵',
36
+ 'PAUSED' => '⏸️',
37
+ 'FAILED' => '🔴',
38
+ 'COMPLETED' => '🟢'
39
+ }.freeze
40
+
41
+ STATE_LABELS = {
42
+ 'QUEUED' => 'Queued',
43
+ 'PLANNING' => 'Planning',
44
+ 'AWAITING_PLAN_APPROVAL' => 'Needs Approval',
45
+ 'AWAITING_USER_FEEDBACK' => 'Needs Feedback',
46
+ 'IN_PROGRESS' => 'Working',
47
+ 'PAUSED' => 'Paused',
48
+ 'FAILED' => 'Failed',
49
+ 'COMPLETED' => 'Done'
50
+ }.freeze
51
+
52
+ class << self
53
+ # Apply true-color RGB to text using ANSI escape sequences
54
+ def rgb_color(text, color_name)
55
+ r, g, b = COLORS[color_name]
56
+ "\e[38;2;#{r};#{g};#{b}m#{text}\e[0m"
57
+ end
58
+
59
+ def prompt
60
+ @prompt ||= TTY::Prompt.new(
61
+ interrupt: :exit,
62
+ symbols: PROMPT_SYMBOLS,
63
+ active_color: :magenta,
64
+ help_color: :cyan
65
+ )
66
+ end
67
+
68
+ def spinner(message)
69
+ TTY::Spinner.new(
70
+ "[:spinner] #{rgb_color(message, :purple)}",
71
+ format: :dots,
72
+ success_mark: PASTEL.green('✓'),
73
+ error_mark: PASTEL.red('✗')
74
+ )
75
+ end
76
+
77
+ def with_spinner(message)
78
+ spin = spinner(message)
79
+ spin.auto_spin
80
+ result = yield
81
+ spin.success(PASTEL.green('done'))
82
+ result
83
+ rescue StandardError => e
84
+ spin.error(PASTEL.red('failed'))
85
+ raise e
86
+ end
87
+
88
+ def state_emoji(state)
89
+ # API returns nil for completed sessions
90
+ return '🟢' if state.nil?
91
+
92
+ STATE_EMOJI[state] || '❓'
93
+ end
94
+
95
+ def state_label(state)
96
+ # API returns nil for completed sessions
97
+ return 'Completed' if state.nil?
98
+
99
+ STATE_LABELS[state] || state
100
+ end
101
+
102
+ def format_session_choice(session)
103
+ emoji = state_emoji(session.state)
104
+ label = state_label(session.state)
105
+ title = session.title || session.prompt&.slice(0, 25) || 'Untitled'
106
+ title = "#{title[0..22]}..." if title.length > 25
107
+ time_ago = time_ago_in_words(session.update_time)
108
+ {
109
+ name: "#{emoji} #{rgb_color(title.ljust(27),
110
+ :purple)} #{rgb_color(label.ljust(15), :lavender)} #{rgb_color(time_ago, :dim)}",
111
+ value: session
112
+ }
113
+ end
114
+
115
+ def format_source_choice(source)
116
+ repo_name = source.github_repo&.full_name || source.name
117
+ {
118
+ name: rgb_color(repo_name, :purple),
119
+ value: source
120
+ }
121
+ end
122
+
123
+ def time_ago_in_words(time_string)
124
+ return 'N/A' unless time_string
125
+
126
+ time = Time.parse(time_string)
127
+ diff = Time.now - time
128
+ case diff
129
+ when 0..59
130
+ 'just now'
131
+ when 60..3599
132
+ "#{(diff / 60).to_i}m ago"
133
+ when 3600..86_399
134
+ "#{(diff / 3600).to_i}h ago"
135
+ else
136
+ "#{(diff / 86_400).to_i}d ago"
137
+ end
138
+ end
139
+
140
+ def format_datetime(time_string)
141
+ return 'N/A' unless time_string
142
+
143
+ time = Time.parse(time_string)
144
+ today = Time.now.to_date
145
+ date = time.to_date
146
+
147
+ if date == today
148
+ "Today #{time.strftime('%l:%M %p').strip}"
149
+ elsif date == today - 1
150
+ "Yesterday #{time.strftime('%l:%M %p').strip}"
151
+ else
152
+ time.strftime('%b %d, %Y %l:%M %p').strip
153
+ end
154
+ end
155
+
156
+ def clear_screen
157
+ print "\e[2J\e[H"
158
+ end
159
+
160
+ def header(title)
161
+ puts
162
+ puts " 🚀 #{rgb_color(title, :lavender)}"
163
+ puts " #{rgb_color('─' * 50, :muted)}"
164
+ puts
165
+ end
166
+
167
+ def divider
168
+ rgb_color('─' * 50, :muted)
169
+ end
170
+
171
+ def highlight(text)
172
+ rgb_color(text, :lavender)
173
+ end
174
+
175
+ def muted(text)
176
+ rgb_color(text, :dim)
177
+ end
178
+
179
+ def print_banner
180
+ Banner.print_banner
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module JulesRuby
6
+ module Commands
7
+ # Sessions subcommand
8
+ class Sessions < Base
9
+ desc 'list', 'List all sessions'
10
+ format_option
11
+ def list
12
+ sessions = client.sessions.all
13
+ if options[:format] == 'json'
14
+ puts JSON.pretty_generate(sessions.map(&:to_h))
15
+ else
16
+ print_sessions_table(sessions)
17
+ end
18
+ rescue JulesRuby::Error => e
19
+ error_exit(e)
20
+ end
21
+
22
+ desc 'show ID', 'Show details for a session'
23
+ format_option
24
+ def show(id)
25
+ session = client.sessions.find(id)
26
+ if options[:format] == 'json'
27
+ puts JSON.pretty_generate(session.to_h)
28
+ else
29
+ print_session_details(session)
30
+ end
31
+ rescue JulesRuby::Error => e
32
+ error_exit(e)
33
+ end
34
+
35
+ desc 'create', 'Create a new session'
36
+ long_desc <<~LONGDESC
37
+ Create a new Jules coding session.
38
+
39
+ You must provide a prompt either inline with --prompt or from a file with --prompt-file.
40
+ If both are provided, --prompt-file takes precedence.
41
+
42
+ Examples:
43
+ # Create with inline prompt
44
+ $ jules-ruby sessions create --source=sources/github/owner/repo --prompt="Fix the login bug"
45
+ # Create with prompt from file
46
+ $ jules-ruby sessions create --source=sources/github/owner/repo --prompt-file=./task-instructions.md
47
+ # Create with custom branch and auto-PR
48
+ $ jules-ruby sessions create --source=sources/github/owner/repo --branch=develop --prompt="Add tests" --auto-pr
49
+ LONGDESC
50
+ option :source, required: true, desc: 'Source name (e.g., sources/github/owner/repo)'
51
+ option :branch, default: 'main', desc: 'Starting branch'
52
+ option :prompt, desc: 'Task prompt (inline text)'
53
+ option :prompt_file, desc: 'Path to file containing task prompt'
54
+ option :title, desc: 'Session title'
55
+ option :auto_pr, type: :boolean, default: false, desc: 'Auto-create PR when done'
56
+ def create
57
+ prompt_text = resolve_prompt
58
+ raise Thor::Error, 'You must provide --prompt or --prompt-file' if prompt_text.nil? || prompt_text.strip.empty?
59
+
60
+ session = create_session(prompt_text)
61
+ print_creation_success(session)
62
+ rescue JulesRuby::Error => e
63
+ error_exit(e)
64
+ end
65
+
66
+ desc 'approve ID', 'Approve the plan for a session'
67
+ long_desc <<~LONGDESC
68
+ Approve the generated plan for a session.
69
+ Example:
70
+ $ jules-ruby sessions approve SESSION_ID
71
+ LONGDESC
72
+ def approve(id)
73
+ session = client.sessions.approve_plan(id)
74
+ puts "Plan approved for session: #{session.name}"
75
+ puts "State: #{session.state}"
76
+ rescue JulesRuby::Error => e
77
+ error_exit(e)
78
+ end
79
+
80
+ desc 'message ID', 'Send a message to a session'
81
+ long_desc <<~LONGDESC
82
+ Send a message to an existing session.
83
+ Examples:
84
+ $ jules-ruby sessions message SESSION_ID --prompt="Please also add unit tests"
85
+ LONGDESC
86
+ option :prompt, required: true, desc: 'Message to send'
87
+ def message(id)
88
+ session = client.sessions.send_message(id, prompt: options[:prompt])
89
+ puts "Message sent to session: #{session.name}"
90
+ puts "State: #{session.state}"
91
+ rescue JulesRuby::Error => e
92
+ error_exit(e)
93
+ end
94
+
95
+ desc 'delete ID', 'Delete a session'
96
+ def delete(id)
97
+ client.sessions.destroy(id)
98
+ puts "Session deleted: #{id}"
99
+ rescue JulesRuby::Error => e
100
+ error_exit(e)
101
+ end
102
+
103
+ private
104
+
105
+ def resolve_prompt
106
+ if options[:prompt_file]
107
+ file_path = File.expand_path(options[:prompt_file])
108
+ raise Thor::Error, "Prompt file not found: #{file_path}" unless File.exist?(file_path)
109
+
110
+ File.read(file_path)
111
+ else
112
+ options[:prompt]
113
+ end
114
+ end
115
+
116
+ def create_session(prompt_text)
117
+ params = build_create_params(prompt_text)
118
+ client.sessions.create(**params)
119
+ end
120
+
121
+ def build_create_params(prompt_text)
122
+ source_context = {
123
+ 'source' => options[:source],
124
+ 'githubRepoContext' => { 'startingBranch' => options[:branch] }
125
+ }
126
+
127
+ params = {
128
+ prompt: prompt_text,
129
+ source_context: source_context
130
+ }
131
+ params[:title] = options[:title] if options[:title]
132
+ params[:automation_mode] = 'AUTO_CREATE_PR' if options[:auto_pr]
133
+ params
134
+ end
135
+
136
+ def print_creation_success(session)
137
+ puts "Session created: #{session.name}"
138
+ puts "URL: #{session.url}"
139
+ puts "State: #{session.state}"
140
+ end
141
+
142
+ def print_sessions_table(sessions)
143
+ if sessions.empty?
144
+ puts 'No sessions found.'
145
+ return
146
+ end
147
+ puts 'ID TITLE STATE UPDATED '
148
+ puts '-' * 90
149
+ sessions.each do |s|
150
+ title = truncate(s.title || s.prompt, 28)
151
+ updated = s.update_time ? Time.parse(s.update_time).strftime('%Y-%m-%d %H:%M') : 'N/A'
152
+ puts format('%<id>-20s %<title>-30s %<state>-20s %<updated>-15s',
153
+ id: s.id, title: title, state: s.state, updated: updated)
154
+ end
155
+ end
156
+
157
+ def print_session_details(session)
158
+ print_session_basic_info(session)
159
+ print_session_outputs(session.outputs) if session.outputs&.any?
160
+ end
161
+
162
+ def print_session_basic_info(session)
163
+ puts "Name: #{session.name}"
164
+ puts "ID: #{session.id}"
165
+ puts "Title: #{session.title}" if session.title
166
+ puts "Prompt: #{session.prompt}"
167
+ puts "State: #{session.state}"
168
+ puts "URL: #{session.url}" if session.url
169
+ puts "Created: #{session.create_time}"
170
+ puts "Updated: #{session.update_time}"
171
+ end
172
+
173
+ def print_session_outputs(outputs)
174
+ puts "\nOutputs:"
175
+ outputs.each do |output|
176
+ if output.respond_to?(:url)
177
+ puts " - PR: #{output.url}"
178
+ else
179
+ puts " - #{output}"
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module JulesRuby
6
+ module Commands
7
+ # Sources subcommand
8
+ class Sources < Base
9
+ desc 'list', 'List all connected repositories'
10
+ long_desc <<~LONGDESC
11
+ List all GitHub repositories connected to your Jules account.
12
+
13
+ Examples:
14
+ $ jules-ruby sources list
15
+ $ jules-ruby sources list --format=json
16
+ LONGDESC
17
+ format_option
18
+ def list
19
+ sources = client.sources.all
20
+ if options[:format] == 'json'
21
+ puts JSON.pretty_generate(sources.map(&:to_h))
22
+ else
23
+ print_sources_table(sources)
24
+ end
25
+ rescue JulesRuby::Error => e
26
+ error_exit(e)
27
+ end
28
+
29
+ desc 'show NAME', 'Show details for a source'
30
+ long_desc <<~LONGDESC
31
+ Show details for a specific source.
32
+
33
+ Example:
34
+ $ jules-ruby sources show sources/github/owner/repo
35
+ LONGDESC
36
+ format_option
37
+ def show(name)
38
+ source = client.sources.find(name)
39
+ if options[:format] == 'json'
40
+ puts JSON.pretty_generate(source.to_h)
41
+ else
42
+ print_source_details(source)
43
+ end
44
+ rescue JulesRuby::Error => e
45
+ error_exit(e)
46
+ end
47
+
48
+ private
49
+
50
+ def print_sources_table(sources)
51
+ if sources.empty?
52
+ puts 'No sources found.'
53
+ return
54
+ end
55
+ puts 'NAME REPOSITORY '
56
+ puts '-' * 72
57
+ sources.each do |s|
58
+ puts format('%<name>-50s %<repo>-20s', name: s.name, repo: s.github_repo&.full_name || 'N/A')
59
+ end
60
+ end
61
+
62
+ def print_source_details(source)
63
+ puts "Name: #{source.name}"
64
+ puts "ID: #{source.id}"
65
+ return unless source.github_repo
66
+
67
+ puts "Repository: #{source.github_repo.full_name}"
68
+ puts "URL: #{source.github_repo.url}" if source.github_repo.respond_to?(:url)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'cli/interactive'
5
+ require_relative 'cli/prompts'
6
+ require_relative 'cli/sources'
7
+ require_relative 'cli/sessions'
8
+ require_relative 'cli/activities'
9
+
10
+ module JulesRuby
11
+ # Command-line interface for jules-ruby
12
+ class CLI < Thor
13
+ package_name 'jules-ruby'
14
+
15
+ def self.exit_on_failure?
16
+ true
17
+ end
18
+
19
+ def self.help(shell, subcommand = false)
20
+ Prompts.print_banner
21
+
22
+ shell.say <<~BANNER
23
+ QUICK START EXAMPLES:
24
+
25
+ # Start interactive mode (default)
26
+ $ jules-ruby
27
+
28
+ # List your connected repositories
29
+ $ jules-ruby sources list
30
+
31
+ # Create a new coding session
32
+ $ jules-ruby sessions create --source=sources/github/owner/repo --prompt="Fix the login bug"
33
+
34
+ # Create a session with prompt from file
35
+ $ jules-ruby sessions create --source=sources/github/owner/repo --prompt-file=./task.md
36
+
37
+ # List all sessions
38
+ $ jules-ruby sessions list
39
+
40
+ # View session activities
41
+ $ jules-ruby activities list SESSION_ID
42
+
43
+ # Approve a session's plan
44
+ $ jules-ruby sessions approve SESSION_ID
45
+
46
+ CONFIGURATION:
47
+
48
+ Set your API key via environment variable:
49
+ $ export JULES_API_KEY=your_api_key
50
+
51
+ BANNER
52
+ super
53
+ end
54
+
55
+ desc 'interactive', 'Start interactive mode'
56
+ def interactive
57
+ JulesRuby::Interactive.new.start
58
+ end
59
+
60
+ map %w[-i] => :interactive
61
+ default_command :interactive
62
+
63
+ desc 'sources SUBCOMMAND', 'Manage sources (connected repositories)'
64
+ long_desc <<~LONGDESC
65
+ Manage connected GitHub repositories (sources).
66
+
67
+ Examples:
68
+
69
+ # List all connected repositories
70
+ $ jules-ruby sources list
71
+
72
+ # Show details for a specific source
73
+ $ jules-ruby sources show sources/github/owner/repo
74
+
75
+ # Output as JSON
76
+ $ jules-ruby sources list --format=json
77
+ LONGDESC
78
+ subcommand 'sources', JulesRuby::Commands::Sources
79
+
80
+ desc 'sessions SUBCOMMAND', 'Manage coding sessions'
81
+ long_desc <<~LONGDESC
82
+ Manage Jules coding sessions.
83
+
84
+ Examples:
85
+
86
+ # List all sessions
87
+ $ jules-ruby sessions list
88
+
89
+ # Show session details
90
+ $ jules-ruby sessions show SESSION_ID
91
+
92
+ # Create a session with inline prompt
93
+ $ jules-ruby sessions create --source=sources/github/owner/repo --prompt="Fix the login bug"
94
+
95
+ # Create a session with prompt from file
96
+ $ jules-ruby sessions create --source=sources/github/owner/repo --prompt-file=./task.md
97
+
98
+ # Create a session with auto-PR
99
+ $ jules-ruby sessions create --source=sources/github/owner/repo --prompt="Add tests" --auto-pr
100
+ LONGDESC
101
+ subcommand 'sessions', JulesRuby::Commands::Sessions
102
+
103
+ desc 'activities SUBCOMMAND', 'View session activities'
104
+ long_desc <<~LONGDESC
105
+ View activities (messages, plans, progress) for Jules sessions.
106
+
107
+ Examples:
108
+
109
+ # List all activities for a session
110
+ $ jules-ruby activities list SESSION_ID
111
+
112
+ # Show details for a specific activity
113
+ $ jules-ruby activities show sessions/SESSION_ID/activities/ACTIVITY_ID
114
+
115
+ # Output as JSON
116
+ $ jules-ruby activities list SESSION_ID --format=json
117
+ LONGDESC
118
+ subcommand 'activities', JulesRuby::Commands::Activities
119
+
120
+ desc 'version', 'Show jules-ruby version'
121
+ def version
122
+ puts "jules-ruby #{JulesRuby::VERSION}"
123
+ end
124
+
125
+ map %w[-v --version] => :version
126
+ end
127
+ end