riffer-rig 0.6.0 → 0.7.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.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +37 -4
  4. data/exe/riffer +1 -1
  5. data/lib/riffer/rig/bundled/agents_md.rb +7 -0
  6. data/lib/riffer/rig/bundled/bash.rb +5 -0
  7. data/lib/riffer/rig/bundled/edit.rb +5 -0
  8. data/lib/riffer/rig/bundled/mcp.rb +9 -0
  9. data/lib/riffer/rig/bundled/read.rb +5 -0
  10. data/lib/riffer/rig/bundled/skills.rb +10 -0
  11. data/lib/riffer/rig/bundled/write.rb +5 -0
  12. data/lib/riffer/rig/bundled.rb +7 -0
  13. data/lib/riffer/rig/cli/flags.rb +77 -0
  14. data/lib/riffer/rig/cli.rb +69 -106
  15. data/lib/riffer/rig/command/context.rb +70 -0
  16. data/lib/riffer/rig/command.rb +29 -0
  17. data/lib/riffer/rig/commands/model.rb +54 -0
  18. data/lib/riffer/rig/commands/skill.rb +27 -0
  19. data/lib/riffer/rig/commands.rb +4 -0
  20. data/lib/riffer/rig/credentials.rb +2 -2
  21. data/lib/riffer/rig/directories.rb +20 -0
  22. data/lib/riffer/rig/env/invalid.rb +13 -0
  23. data/lib/riffer/rig/env.rb +12 -1
  24. data/lib/riffer/rig/events/after_response.rb +25 -0
  25. data/lib/riffer/rig/events/after_tool_call.rb +31 -0
  26. data/lib/riffer/rig/events/before_prompt.rb +25 -0
  27. data/lib/riffer/rig/events/before_request.rb +25 -0
  28. data/lib/riffer/rig/events/before_tool_call.rb +28 -0
  29. data/lib/riffer/rig/events/command_output.rb +3 -2
  30. data/lib/riffer/rig/events/notify.rb +3 -2
  31. data/lib/riffer/rig/events/session_end.rb +3 -2
  32. data/lib/riffer/rig/events/session_start.rb +3 -2
  33. data/lib/riffer/rig/events/skill_activated.rb +3 -2
  34. data/lib/riffer/rig/events/turn_end.rb +3 -2
  35. data/lib/riffer/rig/extension/failure.rb +16 -0
  36. data/lib/riffer/rig/extension.rb +16 -1
  37. data/lib/riffer/rig/hosts/mirror.rb +6 -7
  38. data/lib/riffer/rig/hosts/null.rb +1 -1
  39. data/lib/riffer/rig/loader.rb +163 -0
  40. data/lib/riffer/rig/mcp/declaration.rb +23 -0
  41. data/lib/riffer/rig/mcp/server.rb +16 -0
  42. data/lib/riffer/rig/mcp.rb +12 -0
  43. data/lib/riffer/rig/prompts/agents_md.rb +32 -0
  44. data/lib/riffer/rig/prompts.rb +4 -0
  45. data/lib/riffer/rig/registrar.rb +120 -5
  46. data/lib/riffer/rig/runtime/blocked.rb +13 -0
  47. data/lib/riffer/rig/runtime/hooks.rb +78 -0
  48. data/lib/riffer/rig/runtime/request_guardrail.rb +24 -0
  49. data/lib/riffer/rig/runtime/tool_runtime.rb +44 -0
  50. data/lib/riffer/rig/runtime.rb +506 -57
  51. data/lib/riffer/rig/settings/document.rb +27 -16
  52. data/lib/riffer/rig/settings/pricing.rb +16 -5
  53. data/lib/riffer/rig/settings.rb +73 -43
  54. data/lib/riffer/rig/skills/sources.rb +28 -0
  55. data/lib/riffer/rig/skills.rb +12 -0
  56. data/lib/riffer/rig/{events/event.rb → support/equatable.rb} +2 -11
  57. data/lib/riffer/rig/{ui → terminal}/animator.rb +4 -4
  58. data/lib/riffer/rig/{ui → terminal}/banner.rb +17 -21
  59. data/lib/riffer/rig/{ui → terminal}/cursor.rb +4 -4
  60. data/lib/riffer/rig/terminal/host.rb +85 -0
  61. data/lib/riffer/rig/{ui → terminal}/palette.rb +1 -1
  62. data/lib/riffer/rig/{ui → terminal}/renderer.rb +115 -66
  63. data/lib/riffer/rig/{ui → terminal}/smoother.rb +4 -4
  64. data/lib/riffer/rig/{ui → terminal}/theme.rb +12 -12
  65. data/lib/riffer/rig/terminal.rb +207 -0
  66. data/lib/riffer/rig/tools/bash.rb +6 -4
  67. data/lib/riffer/rig/tools/edit.rb +3 -2
  68. data/lib/riffer/rig/tools/read.rb +3 -2
  69. data/lib/riffer/rig/tools/write.rb +3 -2
  70. data/lib/riffer/rig/version.rb +1 -1
  71. data/lib/riffer/rig.rb +8 -8
  72. data/sig/generated/riffer/rig/bundled/agents_md.rbs +5 -0
  73. data/sig/generated/riffer/rig/bundled/bash.rbs +5 -0
  74. data/sig/generated/riffer/rig/bundled/edit.rbs +5 -0
  75. data/sig/generated/riffer/rig/bundled/mcp.rbs +5 -0
  76. data/sig/generated/riffer/rig/bundled/read.rbs +5 -0
  77. data/sig/generated/riffer/rig/bundled/skills.rbs +5 -0
  78. data/sig/generated/riffer/rig/bundled/write.rbs +5 -0
  79. data/sig/generated/riffer/rig/bundled.rbs +5 -0
  80. data/sig/generated/riffer/rig/cli/flags.rbs +40 -0
  81. data/sig/generated/riffer/rig/cli.rbs +30 -35
  82. data/sig/generated/riffer/rig/command/context.rbs +50 -0
  83. data/sig/generated/riffer/rig/command.rbs +23 -0
  84. data/sig/generated/riffer/rig/commands/model.rbs +27 -0
  85. data/sig/generated/riffer/rig/commands/skill.rbs +12 -0
  86. data/sig/generated/riffer/rig/commands.rbs +4 -0
  87. data/sig/generated/riffer/rig/credentials.rbs +4 -4
  88. data/sig/generated/riffer/rig/directories.rbs +11 -0
  89. data/sig/generated/riffer/rig/env/invalid.rbs +10 -0
  90. data/sig/generated/riffer/rig/env.rbs +7 -1
  91. data/sig/generated/riffer/rig/events/after_response.rbs +18 -0
  92. data/sig/generated/riffer/rig/events/after_tool_call.rbs +24 -0
  93. data/sig/generated/riffer/rig/events/before_prompt.rbs +18 -0
  94. data/sig/generated/riffer/rig/events/before_request.rbs +18 -0
  95. data/sig/generated/riffer/rig/events/before_tool_call.rbs +21 -0
  96. data/sig/generated/riffer/rig/events/command_output.rbs +3 -1
  97. data/sig/generated/riffer/rig/events/notify.rbs +3 -1
  98. data/sig/generated/riffer/rig/events/session_end.rbs +3 -1
  99. data/sig/generated/riffer/rig/events/session_start.rbs +3 -1
  100. data/sig/generated/riffer/rig/events/skill_activated.rbs +3 -1
  101. data/sig/generated/riffer/rig/events/turn_end.rbs +3 -1
  102. data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
  103. data/sig/generated/riffer/rig/extension.rbs +14 -1
  104. data/sig/generated/riffer/rig/hosts/mirror.rbs +9 -9
  105. data/sig/generated/riffer/rig/hosts/null.rbs +1 -1
  106. data/sig/generated/riffer/rig/loader.rbs +77 -0
  107. data/sig/generated/riffer/rig/mcp/declaration.rbs +18 -0
  108. data/sig/generated/riffer/rig/mcp/server.rbs +13 -0
  109. data/sig/generated/riffer/rig/mcp.rbs +8 -0
  110. data/sig/generated/riffer/rig/prompts/agents_md.rbs +19 -0
  111. data/sig/generated/riffer/rig/prompts.rbs +4 -0
  112. data/sig/generated/riffer/rig/registrar.rbs +78 -4
  113. data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
  114. data/sig/generated/riffer/rig/runtime/hooks.rbs +49 -0
  115. data/sig/generated/riffer/rig/runtime/request_guardrail.rbs +16 -0
  116. data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +23 -0
  117. data/sig/generated/riffer/rig/runtime.rbs +225 -22
  118. data/sig/generated/riffer/rig/settings/document.rbs +11 -11
  119. data/sig/generated/riffer/rig/settings/pricing.rbs +7 -2
  120. data/sig/generated/riffer/rig/settings.rbs +36 -23
  121. data/sig/generated/riffer/rig/skills/sources.rbs +18 -0
  122. data/sig/generated/riffer/rig/skills.rbs +7 -0
  123. data/sig/generated/riffer/rig/support/equatable.rbs +15 -0
  124. data/sig/generated/riffer/rig/{ui → terminal}/animator.rbs +5 -5
  125. data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
  126. data/sig/generated/riffer/rig/{ui → terminal}/cursor.rbs +5 -5
  127. data/sig/generated/riffer/rig/terminal/host.rbs +50 -0
  128. data/sig/generated/riffer/rig/{ui → terminal}/palette.rbs +2 -2
  129. data/sig/generated/riffer/rig/terminal/renderer.rbs +126 -0
  130. data/sig/generated/riffer/rig/{ui → terminal}/smoother.rbs +5 -5
  131. data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -5
  132. data/sig/generated/riffer/rig/terminal.rbs +85 -0
  133. data/sig/generated/riffer/rig/tools/bash.rbs +2 -1
  134. data/sig/generated/riffer/rig.rbs +4 -0
  135. data/sig/manual/riffer/rig/commands/model.rbs +6 -0
  136. data/sig/manual/riffer/rig/commands/skill.rbs +6 -0
  137. data/sig/manual/riffer/rig/directories.rbs +6 -0
  138. data/sig/manual/riffer/rig/events.rbs +6 -0
  139. data/sig/manual/riffer/rig/hosts.rbs +7 -0
  140. data/sig/manual/riffer/rig/mcp.rbs +11 -0
  141. data/sig/manual/riffer/rig/prompts/agents_md.rbs +6 -0
  142. data/sig/manual/riffer/rig/{ui/banner.rbs → skills.rbs} +2 -2
  143. data/sig/manual/riffer/rig/support.rbs +8 -0
  144. data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
  145. metadata +140 -27
  146. data/lib/riffer/rig/coding_agent.rb +0 -53
  147. data/lib/riffer/rig/hosts/base.rb +0 -36
  148. data/lib/riffer/rig/repl.rb +0 -188
  149. data/lib/riffer/rig/token_tally.rb +0 -54
  150. data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
  151. data/sig/generated/riffer/rig/events/event.rbs +0 -20
  152. data/sig/generated/riffer/rig/hosts/base.rbs +0 -26
  153. data/sig/generated/riffer/rig/repl.rbs +0 -69
  154. data/sig/generated/riffer/rig/token_tally.rbs +0 -42
  155. data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
  156. data/sig/generated/riffer/rig/ui/renderer.rbs +0 -94
  157. data/sig/manual/riffer/rig/ui.rbs +0 -5
@@ -2,31 +2,34 @@
2
2
 
3
3
  require 'json'
4
4
 
5
- class Riffer::Rig::UI::Renderer
5
+ class Riffer::Rig::Terminal::Renderer
6
6
  RESULT_PREVIEW_LIMIT = 200 #: Integer
7
7
 
8
8
  # @rbs @io: IO
9
- # @rbs @theme: Riffer::Rig::UI::Theme
10
- # @rbs @tally: Riffer::Rig::TokenTally?
11
- # @rbs @smoother: Riffer::Rig::UI::Smoother | PassThroughSmoother?
12
- # @rbs @deferred_usage: Riffer::Providers::TokenUsage?
9
+ # @rbs @theme: Riffer::Rig::Terminal::Theme
10
+ # @rbs @smoother: Riffer::Rig::Terminal::Smoother | PassThroughSmoother
11
+ # @rbs @cursor: Riffer::Rig::Terminal::Cursor
13
12
  # @rbs @prose_gap_pending: bool
14
13
 
15
14
  # @rbs io: IO
16
- # @rbs theme: Riffer::Rig::UI::Theme
17
- # @rbs tally: Riffer::Rig::TokenTally?
18
- # @rbs smoother: Riffer::Rig::UI::Smoother | PassThroughSmoother?
15
+ # @rbs theme: Riffer::Rig::Terminal::Theme
16
+ # @rbs smoother: Riffer::Rig::Terminal::Smoother | PassThroughSmoother
17
+ # @rbs cursor: Riffer::Rig::Terminal::Cursor
19
18
  # @rbs return: void
20
- def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io), tally: nil, smoother: nil)
19
+ def initialize(
20
+ io: $stdout,
21
+ theme: Riffer::Rig::Terminal::Theme.for(io),
22
+ smoother: PassThroughSmoother.new(io),
23
+ cursor: Riffer::Rig::Terminal::Cursor.new(io: io, theme: theme)
24
+ )
21
25
  @io = io
22
26
  @theme = theme
23
- @tally = tally
24
27
  @smoother = smoother
25
- @deferred_usage = nil
28
+ @cursor = cursor
26
29
  @prose_gap_pending = false
27
30
  end
28
31
 
29
- # @rbs event: Riffer::StreamEvents::Base
32
+ # @rbs event: Riffer::StreamEvents::TextDelta | Riffer::StreamEvents::ToolCallDone | Riffer::StreamEvents::Interrupt
30
33
  # @rbs return: void
31
34
  def render(event)
32
35
  case event
@@ -34,19 +37,45 @@ class Riffer::Rig::UI::Renderer
34
37
  render_prose(event.content)
35
38
  when Riffer::StreamEvents::ToolCallDone
36
39
  render_tool_activity(2) { @theme.cyan("⚙ #{event.name}(#{format_arguments(event.arguments)})") }
37
- when Riffer::StreamEvents::SkillActivation
38
- render_block(0) { @theme.magenta("✦ skill: #{event.name}") }
39
40
  when Riffer::StreamEvents::Interrupt
40
41
  render_block(0) { @theme.dim("[interrupted: #{event.reason}]") }
41
- when Riffer::StreamEvents::TokenUsageDone
42
- # Usage arrives per model call but tool results arrive via the session
43
- # callback after each call's stream ends, so rendering inline would print
44
- # the stats above the results they belong with.
45
- current = @deferred_usage
46
- @deferred_usage = current ? current + event.token_usage : event.token_usage
47
42
  end
48
43
  end
49
44
 
45
+ # @rbs name: String
46
+ # @rbs return: void
47
+ def skill(name)
48
+ render_block(0) { @theme.magenta("✦ skill: #{name}") }
49
+ end
50
+
51
+ # @rbs text: String
52
+ # @rbs return: void
53
+ def notice(text)
54
+ render_block(0) { @theme.grey(text) }
55
+ end
56
+
57
+ # @rbs text: String
58
+ # @rbs return: void
59
+ def error(text)
60
+ render_block(0) { @theme.red(text) }
61
+ end
62
+
63
+ # @rbs message: String
64
+ # @rbs level: Symbol
65
+ # @rbs return: void
66
+ def notify(message, level)
67
+ level == :error ? error(message) : notice(message)
68
+ end
69
+
70
+ # @rbs usage: Riffer::Providers::TokenUsage?
71
+ # @rbs session: Riffer::Providers::TokenUsage?
72
+ # @rbs return: void
73
+ def usage(usage, session)
74
+ return unless usage && session
75
+
76
+ render_block(0) { @theme.dim(usage_line(usage, session)) }
77
+ end
78
+
50
79
  # @rbs return: void
51
80
  def prompt
52
81
  # Closes any tool group the previous turn left open, so the next turn's
@@ -57,15 +86,42 @@ class Riffer::Rig::UI::Renderer
57
86
  @io.flush
58
87
  end
59
88
 
89
+ # @rbs question: String?
90
+ # @rbs options: Array[String]?
91
+ # @rbs secret: bool
60
92
  # @rbs return: void
61
- def flush_usage
62
- usage = @deferred_usage
63
- @deferred_usage = nil
64
- tally = @tally
65
- return unless usage && tally
93
+ def question(question, options, secret)
94
+ drain
95
+ @io.puts
96
+ @io.puts(question) if question
97
+ options&.each_with_index { |option, index| @io.puts(" #{@theme.magenta("#{index + 1}.")} #{option}") }
98
+ @io.print("#{@theme.pink('›')} #{@theme.grey('(hidden) ') if secret}")
99
+ @io.flush
100
+ end
66
101
 
67
- tally.add(usage)
68
- render_block(0) { @theme.dim(usage_line(usage, tally)) }
102
+ # @rbs return: void
103
+ def newline
104
+ @io.puts
105
+ @io.flush
106
+ end
107
+
108
+ # @rbs return: void
109
+ def begin_turn
110
+ # A hidden cursor can't flicker against the animator's erase-and-redraw
111
+ # churn.
112
+ @cursor.hide
113
+ @smoother.start
114
+ end
115
+
116
+ # @rbs return: void
117
+ def end_turn
118
+ @smoother.finish
119
+ @cursor.show
120
+ end
121
+
122
+ # @rbs return: void
123
+ def drain
124
+ @smoother.drain
69
125
  end
70
126
 
71
127
  # @rbs message: Riffer::Messages::Base
@@ -80,13 +136,38 @@ class Riffer::Rig::UI::Renderer
80
136
  @io.flush
81
137
  end
82
138
 
139
+ class PassThroughSmoother
140
+ # @rbs @io: IO
141
+
142
+ # @rbs io: IO
143
+ # @rbs return: void
144
+ def initialize(io) = @io = io
145
+
146
+ # @rbs content: String
147
+ # @rbs return: self
148
+ def <<(content)
149
+ @io.print(content)
150
+ @io.flush
151
+ self
152
+ end
153
+
154
+ # @rbs return: nil
155
+ def start = nil
156
+
157
+ # @rbs return: nil
158
+ def drain = nil
159
+
160
+ # @rbs return: nil
161
+ def finish = nil
162
+ end
163
+
83
164
  private
84
165
 
85
166
  # @rbs indent: Integer
86
167
  # @rbs &block: () -> String
87
168
  # @rbs return: void
88
169
  def render_block(indent, &)
89
- drain_smoother
170
+ drain
90
171
  @io.puts
91
172
  @prose_gap_pending = true
92
173
  @io.puts((' ' * indent) + yield)
@@ -102,7 +183,7 @@ class Riffer::Rig::UI::Renderer
102
183
  @prose_gap_pending = false
103
184
  @io.print("\n")
104
185
  end
105
- smoother << content
186
+ @smoother << content
106
187
  end
107
188
 
108
189
  # @rbs indent: Integer
@@ -120,57 +201,25 @@ class Riffer::Rig::UI::Renderer
120
201
  # group; the group stays open so the prose after it pays the closing gap.
121
202
  return if @prose_gap_pending
122
203
 
123
- drain_smoother
204
+ drain
124
205
  @io.puts
125
206
  @prose_gap_pending = true
126
207
  end
127
208
 
128
209
  # @rbs usage: Riffer::Providers::TokenUsage
129
- # @rbs tally: Riffer::Rig::TokenTally
210
+ # @rbs session: Riffer::Providers::TokenUsage
130
211
  # @rbs return: String
131
- def usage_line(usage, tally)
212
+ def usage_line(usage, session)
132
213
  parts = ["↑#{usage.input_tokens}", "↓#{usage.output_tokens}"]
133
214
  parts << "cache_write:#{usage.cache_write_tokens}" if usage.cache_write_tokens&.positive?
134
215
  parts << "cache_read:#{usage.cache_read_tokens}" if usage.cache_read_tokens&.positive?
135
- parts << "session #{tally.total_tokens} tok"
136
- cost = tally.estimated_cost
216
+ parts << "session #{session.total_tokens} tok"
217
+ cost = session.cost
137
218
  parts << format('~$%.4f', cost) if cost
138
219
 
139
220
  parts.join(' · ')
140
221
  end
141
222
 
142
- # @rbs return: Riffer::Rig::UI::Smoother | PassThroughSmoother
143
- def smoother
144
- @smoother || PassThroughSmoother.new(@io)
145
- end
146
-
147
- class PassThroughSmoother
148
- # @rbs @io: IO
149
-
150
- # @rbs io: IO
151
- # @rbs return: void
152
- def initialize(io) = @io = io
153
-
154
- # @rbs content: String
155
- # @rbs return: self
156
- def <<(content)
157
- @io.print(content)
158
- @io.flush
159
- self
160
- end
161
-
162
- # @rbs return: nil
163
- def drain = nil
164
-
165
- # @rbs return: nil
166
- def finish = nil
167
- end
168
-
169
- # @rbs return: void
170
- def drain_smoother
171
- smoother.drain
172
- end
173
-
174
223
  # @rbs arguments: String
175
224
  # @rbs return: String
176
225
  def format_arguments(arguments)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Riffer::Rig::UI::Smoother
3
+ class Riffer::Rig::Terminal::Smoother
4
4
  TICK_SECONDS = 1.0 / 60.0 #: Float
5
5
 
6
6
  # Releasing a fraction of the backlog, not a fixed rate, makes the reveal an
@@ -13,7 +13,7 @@ class Riffer::Rig::UI::Smoother
13
13
  MIN_CHARS_PER_TICK = Rational(1, 6) #: Rational
14
14
 
15
15
  # @rbs @io: IO
16
- # @rbs @theme: Riffer::Rig::UI::Theme
16
+ # @rbs @theme: Riffer::Rig::Terminal::Theme
17
17
  # @rbs @clock: singleton(Kernel)
18
18
  # @rbs @backlog: String
19
19
  # @rbs @carry: Rational
@@ -23,10 +23,10 @@ class Riffer::Rig::UI::Smoother
23
23
  # @rbs @newline_pending: bool
24
24
 
25
25
  # @rbs io: IO
26
- # @rbs theme: Riffer::Rig::UI::Theme
26
+ # @rbs theme: Riffer::Rig::Terminal::Theme
27
27
  # @rbs clock: singleton(Kernel)
28
28
  # @rbs return: void
29
- def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io), clock: Kernel)
29
+ def initialize(io: $stdout, theme: Riffer::Rig::Terminal::Theme.for(io), clock: Kernel)
30
30
  @io = io
31
31
  @theme = theme
32
32
  @clock = clock
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Riffer::Rig::UI::Theme
3
+ class Riffer::Rig::Terminal::Theme
4
4
  # @rbs @enabled: bool
5
5
 
6
6
  # @dynamic enabled
@@ -13,10 +13,10 @@ class Riffer::Rig::UI::Theme
13
13
  end
14
14
 
15
15
  # @rbs io: IO
16
- # @rbs env: Riffer::Rig::Env
17
- # @rbs return: Riffer::Rig::UI::Theme
18
- def self.for(io, env: Riffer::Rig::Env.new)
19
- new(enabled: io.tty? && !env.no_color)
16
+ # @rbs no_color: bool
17
+ # @rbs return: Riffer::Rig::Terminal::Theme
18
+ def self.for(io, no_color: false)
19
+ new(enabled: io.tty? && !no_color)
20
20
  end
21
21
 
22
22
  # @rbs text: String
@@ -30,31 +30,31 @@ class Riffer::Rig::UI::Theme
30
30
 
31
31
  # @rbs text: String
32
32
  # @rbs return: String
33
- def pink(text) = paint(text, Riffer::Rig::UI::Palette::PINK)
33
+ def pink(text) = paint(text, Riffer::Rig::Terminal::Palette::PINK)
34
34
 
35
35
  # @rbs text: String
36
36
  # @rbs return: String
37
- def magenta(text) = paint(text, Riffer::Rig::UI::Palette::MAGENTA)
37
+ def magenta(text) = paint(text, Riffer::Rig::Terminal::Palette::MAGENTA)
38
38
 
39
39
  # @rbs text: String
40
40
  # @rbs return: String
41
- def purple(text) = paint(text, Riffer::Rig::UI::Palette::PURPLE)
41
+ def purple(text) = paint(text, Riffer::Rig::Terminal::Palette::PURPLE)
42
42
 
43
43
  # @rbs text: String
44
44
  # @rbs return: String
45
- def blue(text) = paint(text, Riffer::Rig::UI::Palette::BLUE)
45
+ def blue(text) = paint(text, Riffer::Rig::Terminal::Palette::BLUE)
46
46
 
47
47
  # @rbs text: String
48
48
  # @rbs return: String
49
- def cyan(text) = paint(text, Riffer::Rig::UI::Palette::CYAN)
49
+ def cyan(text) = paint(text, Riffer::Rig::Terminal::Palette::CYAN)
50
50
 
51
51
  # @rbs text: String
52
52
  # @rbs return: String
53
- def grey(text) = paint(text, Riffer::Rig::UI::Palette::GREY)
53
+ def grey(text) = paint(text, Riffer::Rig::Terminal::Palette::GREY)
54
54
 
55
55
  # @rbs text: String
56
56
  # @rbs return: String
57
- def red(text) = paint(text, Riffer::Rig::UI::Palette::RED)
57
+ def red(text) = paint(text, Riffer::Rig::Terminal::Palette::RED)
58
58
 
59
59
  # @rbs text: String
60
60
  # @rbs return: String
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Terminal
4
+ EXIT_COMMANDS = %w[/exit /quit].freeze #: Array[String]
5
+
6
+ SLASH_COMMAND = %r{\A/(\S+)\s*(.*)\z}m #: Regexp
7
+
8
+ SKILL_COMMAND_PREFIX = 'skill:'
9
+
10
+ EXIT_HINT = 'Press Ctrl-C again to exit.'
11
+
12
+ FAREWELL = 'see you on the next riff.'
13
+
14
+ # @rbs @input: IO
15
+ # @rbs @host: Riffer::Rig::Hosts::_Host
16
+ # @rbs @renderer: Riffer::Rig::Terminal::Renderer
17
+ # @rbs @animator: Riffer::Rig::Terminal::Animator
18
+ # @rbs @theme: Riffer::Rig::Terminal::Theme
19
+ # @rbs @version: String
20
+
21
+ # @rbs input: IO
22
+ # @rbs output: IO
23
+ # @rbs version: String
24
+ # @rbs no_color: bool
25
+ # @rbs return: Riffer::Rig::Terminal
26
+ def self.for(input:, output:, version:, no_color: false)
27
+ theme = Theme.for(output, no_color: no_color)
28
+ smoother = Smoother.new(io: output, theme: theme)
29
+ renderer = Renderer.new(io: output, theme: theme, smoother: smoother, cursor: Cursor.new(io: output, theme: theme))
30
+ animator = Animator.new(io: output, theme: theme)
31
+ host = Riffer::Rig::Terminal::Host.new(input: input, renderer: renderer, animator: animator)
32
+ new(input: input, host: host, renderer: renderer, animator: animator, theme: theme, version: version)
33
+ end
34
+
35
+ # @rbs input: IO
36
+ # @rbs host: Riffer::Rig::Hosts::_Host
37
+ # @rbs renderer: Riffer::Rig::Terminal::Renderer
38
+ # @rbs animator: Riffer::Rig::Terminal::Animator
39
+ # @rbs theme: Riffer::Rig::Terminal::Theme
40
+ # @rbs version: String
41
+ # @rbs return: void
42
+ def initialize(input:, host:, renderer:, animator:, theme:, version:)
43
+ @input = input
44
+ @host = host
45
+ @renderer = renderer
46
+ @animator = animator
47
+ @theme = theme
48
+ @version = version
49
+ end
50
+
51
+ # @rbs &build: (Riffer::Rig::Hosts::_Host) -> Riffer::Rig::Runtime
52
+ # @rbs return: Integer
53
+ def run(&build)
54
+ runtime = load(build)
55
+ return 1 unless runtime
56
+
57
+ begin
58
+ runtime.on_message { |message| render_tool_result(message) }
59
+ reveal_banner(runtime)
60
+ repl(runtime)
61
+ @renderer.notice(FAREWELL)
62
+ 0
63
+ ensure
64
+ runtime.close
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # @rbs build: ^(Riffer::Rig::Hosts::_Host) -> Riffer::Rig::Runtime
71
+ # @rbs return: Riffer::Rig::Runtime?
72
+ def load(build)
73
+ build.call(@host)
74
+ rescue Riffer::Rig::Loader::ConfigurationError => e
75
+ @renderer.error(e.message)
76
+ nil
77
+ rescue Interrupt
78
+ @renderer.newline
79
+ nil
80
+ end
81
+
82
+ # @rbs runtime: Riffer::Rig::Runtime
83
+ # @rbs return: void
84
+ def reveal_banner(runtime)
85
+ skills = runtime.commands.count { |command| command.name.start_with?(SKILL_COMMAND_PREFIX) }
86
+ lines = Banner.lines(
87
+ @theme,
88
+ model: runtime.model,
89
+ cwd: runtime.cwd,
90
+ skills: skills.zero? ? 'none' : skills.to_s,
91
+ version: @version
92
+ )
93
+ @animator.reveal([lines])
94
+ end
95
+
96
+ # @rbs runtime: Riffer::Rig::Runtime
97
+ # @rbs return: void
98
+ def repl(runtime)
99
+ armed = false
100
+ loop do
101
+ case (line = read_line)
102
+ when nil then break
103
+ when :interrupt
104
+ break if armed
105
+
106
+ armed = true
107
+ @renderer.notice(EXIT_HINT)
108
+ else
109
+ armed = false
110
+ text = line.strip
111
+ break if EXIT_COMMANDS.include?(text)
112
+
113
+ dispatch(runtime, text) unless text.empty?
114
+ end
115
+ end
116
+ end
117
+
118
+ # @rbs return: String | :interrupt | nil
119
+ def read_line
120
+ @renderer.prompt
121
+ @input.gets
122
+ rescue Interrupt
123
+ :interrupt
124
+ end
125
+
126
+ # @rbs runtime: Riffer::Rig::Runtime
127
+ # @rbs text: String
128
+ # @rbs return: void
129
+ def dispatch(runtime, text)
130
+ command = SLASH_COMMAND.match(text)
131
+ turn(runtime) do |emit|
132
+ if command
133
+ runtime.run_command(command[1].to_s, command[2].to_s, &emit)
134
+ else
135
+ runtime.prompt(text, &emit)
136
+ end
137
+ end
138
+ end
139
+
140
+ # @rbs runtime: Riffer::Rig::Runtime
141
+ # @rbs &block: (^(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void) -> void
142
+ # @rbs return: void
143
+ def turn(runtime)
144
+ # Runtime#cancel takes a Mutex, which Ruby refuses inside a trap handler,
145
+ # so the handler hands the cancel to a thread.
146
+ previous = Signal.trap('INT') { Thread.new { runtime.cancel } }
147
+ begin
148
+ @renderer.begin_turn
149
+ start_animator
150
+ yield(->(event) { render(runtime, event) })
151
+ rescue StandardError => e
152
+ @animator.stop
153
+ @renderer.error("Error: #{e.message}")
154
+ ensure
155
+ Signal.trap('INT', previous)
156
+ @animator.stop
157
+ @renderer.end_turn
158
+ end
159
+ end
160
+
161
+ # @rbs runtime: Riffer::Rig::Runtime
162
+ # @rbs event: ::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event
163
+ # @rbs return: void
164
+ def render(runtime, event)
165
+ case event
166
+ when Riffer::StreamEvents::ReasoningDelta
167
+ start_animator(:reasoning)
168
+ when Riffer::StreamEvents::ReasoningDone, Riffer::StreamEvents::TokenUsageDone
169
+ # Tool execution and the next model call emit no events, so the spinner
170
+ # comes straight back on to cover the silent stretch.
171
+ start_animator
172
+ when Riffer::StreamEvents::TextDelta, Riffer::StreamEvents::ToolCallDone, Riffer::StreamEvents::Interrupt
173
+ @animator.stop
174
+ @renderer.render(event)
175
+ when Riffer::StreamEvents::SkillActivation, Riffer::Rig::Events::SkillActivated
176
+ @animator.stop
177
+ @renderer.skill(event.name)
178
+ start_animator
179
+ when Riffer::Rig::Events::CommandOutput
180
+ @animator.stop
181
+ @renderer.notice(event.text)
182
+ when Riffer::Rig::Events::TurnEnd
183
+ @animator.stop
184
+ @renderer.usage(event.usage, runtime.tally)
185
+ end
186
+ end
187
+
188
+ # @rbs message: Riffer::Messages::Base
189
+ # @rbs return: void
190
+ def render_tool_result(message)
191
+ return unless message.is_a?(Riffer::Messages::Tool)
192
+
193
+ @animator.stop
194
+ @renderer.render_tool_result(message)
195
+ start_animator
196
+ end
197
+
198
+ # @rbs mode: Symbol
199
+ # @rbs return: void
200
+ def start_animator(mode = :neutral)
201
+ # The smoother's backlog may still be trickling out from streamed prose,
202
+ # and spinner frames drawn mid-drain would carve `\r…\e[K` through a
203
+ # half-printed sentence.
204
+ @renderer.drain
205
+ @animator.start(mode)
206
+ end
207
+ end
@@ -5,7 +5,7 @@ require 'open3'
5
5
  class Riffer::Rig::Tools::Bash < Riffer::Tool
6
6
  identifier 'bash'
7
7
  description 'Run a shell command in the working directory and return its combined stdout/stderr and exit status. ' \
8
- 'Use this for listing, searching (rg/grep/find), running tests, git, etc.'
8
+ 'Use this for exploring and running things: ls, rg or grep, find, tests, git, package managers.'
9
9
 
10
10
  timeout 600
11
11
 
@@ -29,7 +29,8 @@ class Riffer::Rig::Tools::Bash < Riffer::Tool
29
29
  # @rbs return: Riffer::Tools::Response
30
30
  def call(context:, command:, timeout_ms: DEFAULT_TIMEOUT_MS)
31
31
  cancel_flag = context&.[](:cancel_flag) #: Riffer::Rig::Runtime::CancelFlag?
32
- output, status = run(command, timeout_ms / 1000.0, cancel_flag)
32
+ cwd = context&.[](:cwd) #: String
33
+ output, status = run(command, cwd, timeout_ms / 1000.0, cancel_flag)
33
34
  output = truncate(output.rstrip)
34
35
 
35
36
  return error("Command exited with status #{status}\n#{output}", type: :command_failed) unless status.zero?
@@ -40,11 +41,12 @@ class Riffer::Rig::Tools::Bash < Riffer::Tool
40
41
  private
41
42
 
42
43
  # @rbs command: String
44
+ # @rbs cwd: String
43
45
  # @rbs timeout_seconds: Float
44
46
  # @rbs cancel_flag: Riffer::Rig::Runtime::CancelFlag?
45
47
  # @rbs return: [String, Integer]
46
- def run(command, timeout_seconds, cancel_flag)
47
- stdin, stdout_and_stderr, wait_thread = Open3.popen2e(command, chdir: Dir.pwd, pgroup: true)
48
+ def run(command, cwd, timeout_seconds, cancel_flag)
49
+ stdin, stdout_and_stderr, wait_thread = Open3.popen2e(command, chdir: cwd, pgroup: true)
48
50
  stdin.close
49
51
 
50
52
  ending = await(wait_thread, monotonic_now + timeout_seconds, cancel_flag)
@@ -3,7 +3,8 @@
3
3
  class Riffer::Rig::Tools::Edit < Riffer::Tool
4
4
  identifier 'edit'
5
5
  description 'Replace an exact string in a file. By default old_string must occur exactly once; ' \
6
- 'set replace_all to replace every occurrence.'
6
+ 'set replace_all to replace every occurrence. ' \
7
+ 'Prefer this over write for existing files; pass enough surrounding text to make old_string unique.'
7
8
 
8
9
  params do
9
10
  required :path, String, description: 'Path to the file (absolute, or relative to the working directory)'
@@ -22,7 +23,7 @@ class Riffer::Rig::Tools::Edit < Riffer::Tool
22
23
  # @rbs replace_all: bool
23
24
  # @rbs return: Riffer::Tools::Response
24
25
  def call(context:, path:, old_string:, new_string:, replace_all: false)
25
- resolved = File.expand_path(path, Dir.pwd)
26
+ resolved = File.expand_path(path, context&.[](:cwd))
26
27
  return error("File not found: #{path}", type: :not_found) unless File.file?(resolved)
27
28
 
28
29
  content = File.read(resolved)
@@ -3,7 +3,8 @@
3
3
  class Riffer::Rig::Tools::Read < Riffer::Tool
4
4
  identifier 'read'
5
5
  description 'Read a file from the filesystem. Returns its contents with line numbers. ' \
6
- 'Use offset/limit to read a slice of a large file.'
6
+ 'Use offset/limit to read a slice of a large file. ' \
7
+ 'Read a file before editing it, and read rather than guess what a file contains.'
7
8
 
8
9
  DEFAULT_LIMIT = 2000 #: Integer
9
10
 
@@ -19,7 +20,7 @@ class Riffer::Rig::Tools::Read < Riffer::Tool
19
20
  # @rbs limit: Integer
20
21
  # @rbs return: Riffer::Tools::Response
21
22
  def call(context:, path:, offset: nil, limit: DEFAULT_LIMIT)
22
- resolved = File.expand_path(path, Dir.pwd)
23
+ resolved = File.expand_path(path, context&.[](:cwd))
23
24
  return error("File not found: #{path}", type: :not_found) unless File.file?(resolved)
24
25
  return error('offset must be >= 1', type: :invalid_argument) if offset && offset < 1
25
26
 
@@ -4,7 +4,8 @@ require 'fileutils'
4
4
 
5
5
  class Riffer::Rig::Tools::Write < Riffer::Tool
6
6
  identifier 'write'
7
- description 'Write content to a file, creating it (and any parent directories) or overwriting it.'
7
+ description 'Write content to a file, creating it (and any parent directories) or overwriting it. ' \
8
+ 'Use this for new files or full rewrites; prefer edit for changes to an existing file.'
8
9
 
9
10
  params do
10
11
  required :path, String, description: 'Path to the file (absolute, or relative to the working directory)'
@@ -16,7 +17,7 @@ class Riffer::Rig::Tools::Write < Riffer::Tool
16
17
  # @rbs content: String
17
18
  # @rbs return: Riffer::Tools::Response
18
19
  def call(context:, path:, content:)
19
- resolved = File.expand_path(path, Dir.pwd)
20
+ resolved = File.expand_path(path, context&.[](:cwd))
20
21
  FileUtils.mkdir_p(File.dirname(resolved))
21
22
  File.write(resolved, content)
22
23
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Riffer
4
4
  module Rig
5
- VERSION = '0.6.0'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  end
data/lib/riffer/rig.rb CHANGED
@@ -6,7 +6,7 @@ require_relative 'rig/version'
6
6
 
7
7
  loader = Zeitwerk::Loader.for_gem_extension(Riffer)
8
8
  loader.ignore("#{__dir__}/rig/version.rb")
9
- loader.inflector.inflect('cli' => 'CLI', 'repl' => 'REPL', 'ui' => 'UI')
9
+ loader.inflector.inflect('cli' => 'CLI')
10
10
  loader.setup
11
11
 
12
12
  module Riffer::Rig
@@ -18,19 +18,19 @@ module Riffer::Rig
18
18
  # @rbs &block: (::Riffer::Rig::Registrar) -> void
19
19
  # @rbs return: Riffer::Rig::Extension
20
20
  def self.extension(name, requires: nil, &)
21
- ext = Extension.new(name, requires: requires, &)
22
- if ext.requires && !ext.requires.satisfied_by?(Gem::Version.new(Riffer::Rig::VERSION))
23
- raise Riffer::ArgumentError,
24
- "extension #{name} requires riffer-rig #{ext.requires}, found #{Riffer::Rig::VERSION}"
25
- end
26
-
27
21
  # Keyed by name so reloading a file replaces its block rather than
28
22
  # duplicating it.
29
- @extensions[name] = ext
23
+ @extensions[name] = Extension.new(name, requires: requires, &)
30
24
  end
31
25
 
32
26
  # @rbs return: Hash[String, Riffer::Rig::Extension]
33
27
  def self.extensions
34
28
  @extensions.dup
35
29
  end
30
+
31
+ # @rbs name: Symbol?
32
+ # @rbs return: Riffer::Rig::Extension | Array[Riffer::Rig::Extension]
33
+ def self.bundled(name = nil)
34
+ name ? Bundled::BY_NAME.fetch(name) : Bundled::BY_NAME.values
35
+ end
36
36
  end