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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +37 -4
- data/exe/riffer +1 -1
- data/lib/riffer/rig/bundled/agents_md.rb +7 -0
- data/lib/riffer/rig/bundled/bash.rb +5 -0
- data/lib/riffer/rig/bundled/edit.rb +5 -0
- data/lib/riffer/rig/bundled/mcp.rb +9 -0
- data/lib/riffer/rig/bundled/read.rb +5 -0
- data/lib/riffer/rig/bundled/skills.rb +10 -0
- data/lib/riffer/rig/bundled/write.rb +5 -0
- data/lib/riffer/rig/bundled.rb +7 -0
- data/lib/riffer/rig/cli/flags.rb +77 -0
- data/lib/riffer/rig/cli.rb +69 -106
- data/lib/riffer/rig/command/context.rb +70 -0
- data/lib/riffer/rig/command.rb +29 -0
- data/lib/riffer/rig/commands/model.rb +54 -0
- data/lib/riffer/rig/commands/skill.rb +27 -0
- data/lib/riffer/rig/commands.rb +4 -0
- data/lib/riffer/rig/credentials.rb +2 -2
- data/lib/riffer/rig/directories.rb +20 -0
- data/lib/riffer/rig/env/invalid.rb +13 -0
- data/lib/riffer/rig/env.rb +12 -1
- data/lib/riffer/rig/events/after_response.rb +25 -0
- data/lib/riffer/rig/events/after_tool_call.rb +31 -0
- data/lib/riffer/rig/events/before_prompt.rb +25 -0
- data/lib/riffer/rig/events/before_request.rb +25 -0
- data/lib/riffer/rig/events/before_tool_call.rb +28 -0
- data/lib/riffer/rig/events/command_output.rb +3 -2
- data/lib/riffer/rig/events/notify.rb +3 -2
- data/lib/riffer/rig/events/session_end.rb +3 -2
- data/lib/riffer/rig/events/session_start.rb +3 -2
- data/lib/riffer/rig/events/skill_activated.rb +3 -2
- data/lib/riffer/rig/events/turn_end.rb +3 -2
- data/lib/riffer/rig/extension/failure.rb +16 -0
- data/lib/riffer/rig/extension.rb +16 -1
- data/lib/riffer/rig/hosts/mirror.rb +6 -7
- data/lib/riffer/rig/hosts/null.rb +1 -1
- data/lib/riffer/rig/loader.rb +163 -0
- data/lib/riffer/rig/mcp/declaration.rb +23 -0
- data/lib/riffer/rig/mcp/server.rb +16 -0
- data/lib/riffer/rig/mcp.rb +12 -0
- data/lib/riffer/rig/prompts/agents_md.rb +32 -0
- data/lib/riffer/rig/prompts.rb +4 -0
- data/lib/riffer/rig/registrar.rb +120 -5
- data/lib/riffer/rig/runtime/blocked.rb +13 -0
- data/lib/riffer/rig/runtime/hooks.rb +78 -0
- data/lib/riffer/rig/runtime/request_guardrail.rb +24 -0
- data/lib/riffer/rig/runtime/tool_runtime.rb +44 -0
- data/lib/riffer/rig/runtime.rb +506 -57
- data/lib/riffer/rig/settings/document.rb +27 -16
- data/lib/riffer/rig/settings/pricing.rb +16 -5
- data/lib/riffer/rig/settings.rb +73 -43
- data/lib/riffer/rig/skills/sources.rb +28 -0
- data/lib/riffer/rig/skills.rb +12 -0
- data/lib/riffer/rig/{events/event.rb → support/equatable.rb} +2 -11
- data/lib/riffer/rig/{ui → terminal}/animator.rb +4 -4
- data/lib/riffer/rig/{ui → terminal}/banner.rb +17 -21
- data/lib/riffer/rig/{ui → terminal}/cursor.rb +4 -4
- data/lib/riffer/rig/terminal/host.rb +85 -0
- data/lib/riffer/rig/{ui → terminal}/palette.rb +1 -1
- data/lib/riffer/rig/{ui → terminal}/renderer.rb +115 -66
- data/lib/riffer/rig/{ui → terminal}/smoother.rb +4 -4
- data/lib/riffer/rig/{ui → terminal}/theme.rb +12 -12
- data/lib/riffer/rig/terminal.rb +207 -0
- data/lib/riffer/rig/tools/bash.rb +6 -4
- data/lib/riffer/rig/tools/edit.rb +3 -2
- data/lib/riffer/rig/tools/read.rb +3 -2
- data/lib/riffer/rig/tools/write.rb +3 -2
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +8 -8
- data/sig/generated/riffer/rig/bundled/agents_md.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/bash.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/edit.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/mcp.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/read.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/skills.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/write.rbs +5 -0
- data/sig/generated/riffer/rig/bundled.rbs +5 -0
- data/sig/generated/riffer/rig/cli/flags.rbs +40 -0
- data/sig/generated/riffer/rig/cli.rbs +30 -35
- data/sig/generated/riffer/rig/command/context.rbs +50 -0
- data/sig/generated/riffer/rig/command.rbs +23 -0
- data/sig/generated/riffer/rig/commands/model.rbs +27 -0
- data/sig/generated/riffer/rig/commands/skill.rbs +12 -0
- data/sig/generated/riffer/rig/commands.rbs +4 -0
- data/sig/generated/riffer/rig/credentials.rbs +4 -4
- data/sig/generated/riffer/rig/directories.rbs +11 -0
- data/sig/generated/riffer/rig/env/invalid.rbs +10 -0
- data/sig/generated/riffer/rig/env.rbs +7 -1
- data/sig/generated/riffer/rig/events/after_response.rbs +18 -0
- data/sig/generated/riffer/rig/events/after_tool_call.rbs +24 -0
- data/sig/generated/riffer/rig/events/before_prompt.rbs +18 -0
- data/sig/generated/riffer/rig/events/before_request.rbs +18 -0
- data/sig/generated/riffer/rig/events/before_tool_call.rbs +21 -0
- data/sig/generated/riffer/rig/events/command_output.rbs +3 -1
- data/sig/generated/riffer/rig/events/notify.rbs +3 -1
- data/sig/generated/riffer/rig/events/session_end.rbs +3 -1
- data/sig/generated/riffer/rig/events/session_start.rbs +3 -1
- data/sig/generated/riffer/rig/events/skill_activated.rbs +3 -1
- data/sig/generated/riffer/rig/events/turn_end.rbs +3 -1
- data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
- data/sig/generated/riffer/rig/extension.rbs +14 -1
- data/sig/generated/riffer/rig/hosts/mirror.rbs +9 -9
- data/sig/generated/riffer/rig/hosts/null.rbs +1 -1
- data/sig/generated/riffer/rig/loader.rbs +77 -0
- data/sig/generated/riffer/rig/mcp/declaration.rbs +18 -0
- data/sig/generated/riffer/rig/mcp/server.rbs +13 -0
- data/sig/generated/riffer/rig/mcp.rbs +8 -0
- data/sig/generated/riffer/rig/prompts/agents_md.rbs +19 -0
- data/sig/generated/riffer/rig/prompts.rbs +4 -0
- data/sig/generated/riffer/rig/registrar.rbs +78 -4
- data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
- data/sig/generated/riffer/rig/runtime/hooks.rbs +49 -0
- data/sig/generated/riffer/rig/runtime/request_guardrail.rbs +16 -0
- data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +23 -0
- data/sig/generated/riffer/rig/runtime.rbs +225 -22
- data/sig/generated/riffer/rig/settings/document.rbs +11 -11
- data/sig/generated/riffer/rig/settings/pricing.rbs +7 -2
- data/sig/generated/riffer/rig/settings.rbs +36 -23
- data/sig/generated/riffer/rig/skills/sources.rbs +18 -0
- data/sig/generated/riffer/rig/skills.rbs +7 -0
- data/sig/generated/riffer/rig/support/equatable.rbs +15 -0
- data/sig/generated/riffer/rig/{ui → terminal}/animator.rbs +5 -5
- data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
- data/sig/generated/riffer/rig/{ui → terminal}/cursor.rbs +5 -5
- data/sig/generated/riffer/rig/terminal/host.rbs +50 -0
- data/sig/generated/riffer/rig/{ui → terminal}/palette.rbs +2 -2
- data/sig/generated/riffer/rig/terminal/renderer.rbs +126 -0
- data/sig/generated/riffer/rig/{ui → terminal}/smoother.rbs +5 -5
- data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -5
- data/sig/generated/riffer/rig/terminal.rbs +85 -0
- data/sig/generated/riffer/rig/tools/bash.rbs +2 -1
- data/sig/generated/riffer/rig.rbs +4 -0
- data/sig/manual/riffer/rig/commands/model.rbs +6 -0
- data/sig/manual/riffer/rig/commands/skill.rbs +6 -0
- data/sig/manual/riffer/rig/directories.rbs +6 -0
- data/sig/manual/riffer/rig/events.rbs +6 -0
- data/sig/manual/riffer/rig/hosts.rbs +7 -0
- data/sig/manual/riffer/rig/mcp.rbs +11 -0
- data/sig/manual/riffer/rig/prompts/agents_md.rbs +6 -0
- data/sig/manual/riffer/rig/{ui/banner.rbs → skills.rbs} +2 -2
- data/sig/manual/riffer/rig/support.rbs +8 -0
- data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
- metadata +140 -27
- data/lib/riffer/rig/coding_agent.rb +0 -53
- data/lib/riffer/rig/hosts/base.rb +0 -36
- data/lib/riffer/rig/repl.rb +0 -188
- data/lib/riffer/rig/token_tally.rb +0 -54
- data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
- data/sig/generated/riffer/rig/events/event.rbs +0 -20
- data/sig/generated/riffer/rig/hosts/base.rbs +0 -26
- data/sig/generated/riffer/rig/repl.rbs +0 -69
- data/sig/generated/riffer/rig/token_tally.rbs +0 -42
- data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
- data/sig/generated/riffer/rig/ui/renderer.rbs +0 -94
- 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::
|
|
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::
|
|
10
|
-
# @rbs @
|
|
11
|
-
# @rbs @
|
|
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::
|
|
17
|
-
# @rbs
|
|
18
|
-
# @rbs
|
|
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(
|
|
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
|
-
@
|
|
28
|
+
@cursor = cursor
|
|
26
29
|
@prose_gap_pending = false
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
# @rbs event: Riffer::StreamEvents::
|
|
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
|
|
62
|
-
|
|
63
|
-
@
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
210
|
+
# @rbs session: Riffer::Providers::TokenUsage
|
|
130
211
|
# @rbs return: String
|
|
131
|
-
def usage_line(usage,
|
|
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 #{
|
|
136
|
-
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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
|
|
17
|
-
# @rbs return: Riffer::Rig::
|
|
18
|
-
def self.for(io,
|
|
19
|
-
new(enabled: io.tty? && !
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
20
|
+
resolved = File.expand_path(path, context&.[](:cwd))
|
|
20
21
|
FileUtils.mkdir_p(File.dirname(resolved))
|
|
21
22
|
File.write(resolved, content)
|
|
22
23
|
|
data/lib/riffer/rig/version.rb
CHANGED
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'
|
|
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] =
|
|
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
|