riffer-rig 0.2.1 → 0.4.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 +24 -0
- data/README.md +2 -2
- data/lib/riffer/rig/cli.rb +31 -2
- data/lib/riffer/rig/coding_agent.rb +35 -31
- data/lib/riffer/rig/credentials.rb +24 -4
- data/lib/riffer/rig/repl.rb +75 -22
- data/lib/riffer/rig/settings/pricing.rb +19 -0
- data/lib/riffer/rig/settings.rb +33 -8
- data/lib/riffer/rig/token_tally.rb +22 -6
- data/lib/riffer/rig/tools/bash.rb +23 -5
- data/lib/riffer/rig/tools/edit.rb +16 -2
- data/lib/riffer/rig/tools/read.rb +6 -1
- data/lib/riffer/rig/tools/write.rb +4 -0
- data/lib/riffer/rig/ui/animator.rb +51 -11
- data/lib/riffer/rig/ui/banner.rb +28 -4
- data/lib/riffer/rig/ui/cursor.rb +14 -2
- data/lib/riffer/rig/ui/palette.rb +13 -7
- data/lib/riffer/rig/ui/renderer.rb +137 -29
- data/lib/riffer/rig/ui/smoother.rb +56 -14
- data/lib/riffer/rig/ui/theme.rb +38 -7
- data/lib/riffer/rig/version.rb +1 -1
- data/sig/generated/riffer/rig/cli.rbs +41 -0
- data/sig/generated/riffer/rig/coding_agent.rbs +9 -0
- data/sig/generated/riffer/rig/credentials.rbs +55 -0
- data/sig/generated/riffer/rig/repl.rbs +68 -0
- data/sig/generated/riffer/rig/settings/pricing.rbs +23 -0
- data/sig/generated/riffer/rig/settings.rbs +92 -0
- data/sig/generated/riffer/rig/token_tally.rbs +61 -0
- data/sig/generated/riffer/rig/tools/bash.rbs +28 -0
- data/sig/generated/riffer/rig/tools/edit.rbs +11 -0
- data/sig/generated/riffer/rig/tools/read.rbs +12 -0
- data/sig/generated/riffer/rig/tools/write.rbs +9 -0
- data/sig/generated/riffer/rig/ui/animator.rbs +76 -0
- data/sig/generated/riffer/rig/ui/banner.rbs +42 -0
- data/sig/generated/riffer/rig/ui/cursor.rbs +33 -0
- data/sig/generated/riffer/rig/ui/palette.rbs +17 -0
- data/sig/generated/riffer/rig/ui/renderer.rbs +116 -0
- data/sig/generated/riffer/rig/ui/smoother.rbs +72 -0
- data/sig/generated/riffer/rig/ui/theme.rbs +60 -0
- data/sig/generated/riffer/rig/version.rbs +7 -0
- data/sig/generated/riffer/rig.rbs +2 -0
- data/sig/manual/riffer/rig/cli.rbs +8 -0
- data/sig/manual/riffer/rig/credentials.rbs +8 -0
- data/sig/manual/riffer/rig/settings.rbs +8 -0
- data/sig/manual/riffer/rig/tools.rbs +5 -0
- data/sig/manual/riffer/rig/ui/banner.rbs +8 -0
- data/sig/manual/riffer/rig/ui.rbs +5 -0
- metadata +31 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e833f2a4dfe0b87598e2be9d585f3cb772562ab39a2dd528684a38dca87f63ab
|
|
4
|
+
data.tar.gz: f469b328103a49e52773c0073e02d8c2fbd06d8cf6779daa50c09b49ab2eba8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 696e8b92a7c1b7a3b71e1f628894e89aed725fbe072dba712d94df277fc5730b06d74def094445b02842d0849409b7900038aedf5d65f29f5416d1450a5b5121
|
|
7
|
+
data.tar.gz: 3dc42ac6e625d292462e00119bd7b3c7a8d1f23ad3c4f66763fff96cbfc3328f962dca67bdc4613e884ba81a6e3f61d9af86f1ba1d8bbd16e0f3caa307ca453c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/bottrall/riffer-rig/compare/v0.3.0...v0.4.0) (2026-09-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **ui:** one blank line between blocks, prompt-owned gaps ([#170](https://github.com/bottrall/riffer-rig/issues/170)) ([636e4d8](https://github.com/bottrall/riffer-rig/commit/636e4d82a069f158775043037a43e2aa494f56dc))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **lint:** adjust rubocop rules and reformat to match ([#168](https://github.com/bottrall/riffer-rig/issues/168)) ([56b81aa](https://github.com/bottrall/riffer-rig/commit/56b81aa904609f246340c8b8135ee7e57791abdd))
|
|
14
|
+
|
|
15
|
+
## [0.3.0](https://github.com/bottrall/riffer-rig/compare/v0.2.1...v0.3.0) (2026-09-10)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **types:** strict rbs-inline typing with collection-managed third-party RBS ([#164](https://github.com/bottrall/riffer-rig/issues/164)) ([be59fb9](https://github.com/bottrall/riffer-rig/commit/be59fb9102d9c6f9bae2a5f456650cc68131924f))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* print tool call lines after the round's stats line ([#166](https://github.com/bottrall/riffer-rig/issues/166)) ([dcc4e80](https://github.com/bottrall/riffer-rig/commit/dcc4e8093ffaec0bc93bb3eaafb47acd72292710))
|
|
26
|
+
|
|
3
27
|
## [0.2.1](https://github.com/bottrall/riffer-rig/compare/v0.2.0...v0.2.1) (2026-09-08)
|
|
4
28
|
|
|
5
29
|
|
data/README.md
CHANGED
|
@@ -46,10 +46,10 @@ Every project chore is a script in `bin/`. The Rakefile behind them is an implem
|
|
|
46
46
|
|
|
47
47
|
| Script | What it does |
|
|
48
48
|
| --------------- | ---------------------------------------------------------------------------------------------- |
|
|
49
|
-
| `bin/setup` | Install dependencies on a fresh checkout
|
|
49
|
+
| `bin/setup` | Install dependencies on a fresh checkout (gems + rbs collection) |
|
|
50
50
|
| `bin/test` | Run the test suite. Pass files and/or Minitest flags: `bin/test test/foo_test.rb -n /pattern/` |
|
|
51
51
|
| `bin/lint` | Run RuboCop. Arguments are forwarded, e.g. `bin/lint -a` |
|
|
52
|
-
| `bin/typecheck` | Check `sig/generated`
|
|
52
|
+
| `bin/typecheck` | Check the rbs collection lockfile and `sig/generated` are current, then type-check with Steep |
|
|
53
53
|
| `bin/rbs` | Regenerate `sig/generated` from the inline annotations in `lib/` |
|
|
54
54
|
| `bin/rbs-watch` | Regenerate `sig/generated` whenever `lib/` changes |
|
|
55
55
|
| `bin/ci` | Run everything CI runs, serially. Use before pushing |
|
data/lib/riffer/rig/cli.rb
CHANGED
|
@@ -10,8 +10,11 @@ module Riffer::Rig::CLI
|
|
|
10
10
|
'openai' => 'https://platform.openai.com/api-keys',
|
|
11
11
|
'gemini' => 'https://aistudio.google.com/app/apikey',
|
|
12
12
|
'openrouter' => 'https://openrouter.ai/keys'
|
|
13
|
-
}.freeze
|
|
13
|
+
}.freeze #: Hash[String, String]
|
|
14
14
|
|
|
15
|
+
# @rbs output: untyped
|
|
16
|
+
# @rbs input: untyped
|
|
17
|
+
# @rbs return: Integer
|
|
15
18
|
def start(output: $stdout, input: $stdin)
|
|
16
19
|
theme = Riffer::Rig::UI::Theme.for(output)
|
|
17
20
|
|
|
@@ -38,6 +41,9 @@ module Riffer::Rig::CLI
|
|
|
38
41
|
|
|
39
42
|
private
|
|
40
43
|
|
|
44
|
+
# @rbs provider: String?
|
|
45
|
+
# @rbs api_key: String
|
|
46
|
+
# @rbs return: void
|
|
41
47
|
def configure_provider(provider, api_key)
|
|
42
48
|
case provider
|
|
43
49
|
when 'anthropic' then Riffer.configure { |c| c.anthropic.api_key = api_key }
|
|
@@ -47,6 +53,11 @@ module Riffer::Rig::CLI
|
|
|
47
53
|
end
|
|
48
54
|
end
|
|
49
55
|
|
|
56
|
+
# @rbs provider: String?
|
|
57
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
58
|
+
# @rbs output: untyped
|
|
59
|
+
# @rbs input: untyped
|
|
60
|
+
# @rbs return: String?
|
|
50
61
|
def onboard(provider, theme, output:, input:)
|
|
51
62
|
url = (provider && PROVIDER_URLS[provider]) || 'your provider'
|
|
52
63
|
name = provider ? provider.capitalize : 'provider'
|
|
@@ -68,23 +79,41 @@ module Riffer::Rig::CLI
|
|
|
68
79
|
key
|
|
69
80
|
end
|
|
70
81
|
|
|
82
|
+
# @rbs provider: String?
|
|
83
|
+
# @rbs return: String
|
|
71
84
|
def env_var_for(provider)
|
|
72
85
|
(provider && Riffer::Rig::Credentials::PROVIDER_ENV_VARS[provider]) || 'the appropriate API key env var'
|
|
73
86
|
end
|
|
74
87
|
|
|
88
|
+
# @rbs input: untyped
|
|
89
|
+
# @rbs return: String?
|
|
75
90
|
def read_secret(input)
|
|
76
91
|
return input.noecho(&:gets) if input.respond_to?(:noecho) && input.tty?
|
|
77
92
|
|
|
78
93
|
input.gets
|
|
79
94
|
end
|
|
80
95
|
|
|
96
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
97
|
+
# @rbs animator: Riffer::Rig::UI::Animator
|
|
98
|
+
# @rbs model: String
|
|
99
|
+
# @rbs return: void
|
|
81
100
|
def reveal_banner(theme, animator, model)
|
|
82
101
|
loaded = [Riffer::Rig::CodingAgent::GLOBAL_AGENTS_FILE, File.join(Dir.pwd, 'AGENTS.md')].select { |path| File.file?(path) }
|
|
83
102
|
context = loaded.empty? ? 'none' : loaded.join(', ')
|
|
84
103
|
|
|
85
|
-
animator.reveal(
|
|
104
|
+
animator.reveal(
|
|
105
|
+
[Riffer::Rig::UI::Banner.lines(
|
|
106
|
+
theme,
|
|
107
|
+
model: model,
|
|
108
|
+
cwd: Dir.pwd,
|
|
109
|
+
context: context,
|
|
110
|
+
skills: count_skills,
|
|
111
|
+
version: Riffer::Rig::VERSION
|
|
112
|
+
)]
|
|
113
|
+
)
|
|
86
114
|
end
|
|
87
115
|
|
|
116
|
+
# @rbs return: String
|
|
88
117
|
def count_skills
|
|
89
118
|
dirs = [Riffer::Rig::CodingAgent::GLOBAL_SKILLS_DIR, Riffer::Rig::CodingAgent::PROJECT_SKILLS_DIR.call]
|
|
90
119
|
backend = Riffer::Skills::FilesystemBackend.new(*dirs)
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
require 'date'
|
|
4
4
|
|
|
5
5
|
class Riffer::Rig::CodingAgent < Riffer::Agent
|
|
6
|
-
GLOBAL_AGENTS_FILE = File.expand_path('~/.riffer/AGENTS.md')
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
GLOBAL_AGENTS_FILE = File.expand_path('~/.riffer/AGENTS.md') #: String
|
|
7
|
+
|
|
8
|
+
GLOBAL_SKILLS_DIR = File.expand_path('~/.riffer/skills') #: String
|
|
9
|
+
|
|
10
|
+
PROJECT_SKILLS_DIR = -> { File.join(Dir.pwd, '.skills') } #: ^() -> String
|
|
9
11
|
|
|
10
12
|
model Riffer::Rig::Settings.model
|
|
11
13
|
model_options Riffer::Rig::Settings.model_options
|
|
@@ -23,32 +25,34 @@ class Riffer::Rig::CodingAgent < Riffer::Agent
|
|
|
23
25
|
|
|
24
26
|
max_steps nil
|
|
25
27
|
|
|
26
|
-
instructions(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
28
|
+
instructions(
|
|
29
|
+
lambda do
|
|
30
|
+
context_files = [GLOBAL_AGENTS_FILE, File.join(Dir.pwd, 'AGENTS.md')]
|
|
31
|
+
.select { |path| File.file?(path) }
|
|
32
|
+
.map { |path| "<project_instructions path=\"#{path}\">\n#{File.read(path)}\n</project_instructions>" }
|
|
33
|
+
|
|
34
|
+
project_context =
|
|
35
|
+
if context_files.empty?
|
|
36
|
+
''
|
|
37
|
+
else
|
|
38
|
+
"\n\n<project_context>\n#{context_files.join("\n\n")}\n</project_context>"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
<<~PROMPT.chomp + project_context + "\n\nCurrent date: #{Date.today}\nCurrent working directory: #{Dir.pwd}"
|
|
42
|
+
You are an expert coding assistant running inside riffer-rig, a terminal coding agent. You help the user by reading files, running shell commands, editing code, and writing new files.
|
|
43
|
+
|
|
44
|
+
Available tools:
|
|
45
|
+
- read: read a file's contents
|
|
46
|
+
- write: write content to a file
|
|
47
|
+
- edit: replace an exact string in a file
|
|
48
|
+
- bash: run a shell command in the working directory
|
|
49
|
+
|
|
50
|
+
Guidelines:
|
|
51
|
+
- Be concise and direct in your responses.
|
|
52
|
+
- Show file paths clearly when working with files.
|
|
53
|
+
- Use bash for file exploration (ls, rg, find) rather than guessing.
|
|
54
|
+
- Prefer editing existing files over creating new ones.
|
|
55
|
+
PROMPT
|
|
56
|
+
end
|
|
57
|
+
)
|
|
54
58
|
end
|
|
@@ -25,24 +25,33 @@ require 'fileutils'
|
|
|
25
25
|
module Riffer::Rig::Credentials
|
|
26
26
|
extend self
|
|
27
27
|
|
|
28
|
-
PATH = File.expand_path('~/.riffer/auth.json')
|
|
28
|
+
PATH = File.expand_path('~/.riffer/auth.json') #: String
|
|
29
29
|
|
|
30
30
|
PROVIDER_ENV_VARS = {
|
|
31
31
|
'anthropic' => 'ANTHROPIC_API_KEY',
|
|
32
32
|
'openai' => 'OPENAI_API_KEY',
|
|
33
33
|
'gemini' => 'GEMINI_API_KEY',
|
|
34
34
|
'openrouter' => 'OPENROUTER_API_KEY'
|
|
35
|
-
}.freeze
|
|
35
|
+
}.freeze #: Hash[String, String]
|
|
36
36
|
|
|
37
37
|
# Returns the API key for +provider+, checking the environment variable first,
|
|
38
38
|
# then the stored file. Returns +nil+ if no key is available.
|
|
39
|
+
#
|
|
40
|
+
# @rbs provider: String?
|
|
41
|
+
# @rbs path: String
|
|
42
|
+
# @rbs return: String?
|
|
39
43
|
def api_key_for(provider, path: PATH)
|
|
40
|
-
env_var = PROVIDER_ENV_VARS[provider]
|
|
44
|
+
env_var = provider && PROVIDER_ENV_VARS[provider]
|
|
41
45
|
key = env_var && ENV.fetch(env_var, nil).then { |v| v unless v.nil? || v.strip.empty? }
|
|
42
46
|
key || key_from_file(provider, path)
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
# Saves an API key for +provider+ to the auth file with 0600 permissions.
|
|
50
|
+
#
|
|
51
|
+
# @rbs provider: String
|
|
52
|
+
# @rbs key: String
|
|
53
|
+
# @rbs path: String
|
|
54
|
+
# @rbs return: String
|
|
46
55
|
def save_api_key(provider, key, path: PATH)
|
|
47
56
|
FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
48
57
|
data = read(path).tap { |h| h[provider] = key }
|
|
@@ -54,11 +63,22 @@ module Riffer::Rig::Credentials
|
|
|
54
63
|
|
|
55
64
|
private
|
|
56
65
|
|
|
66
|
+
# @rbs provider: String?
|
|
67
|
+
# @rbs path: String
|
|
68
|
+
# @rbs return: String?
|
|
57
69
|
def key_from_file(provider, path)
|
|
58
70
|
key = read(path)[provider]
|
|
59
|
-
|
|
71
|
+
return nil unless key.is_a?(String)
|
|
72
|
+
return key unless key.strip.empty?
|
|
73
|
+
|
|
74
|
+
nil
|
|
60
75
|
end
|
|
61
76
|
|
|
77
|
+
# The parsed auth file, or an empty hash when the file is absent or
|
|
78
|
+
# malformed.
|
|
79
|
+
#
|
|
80
|
+
# @rbs path: String
|
|
81
|
+
# @rbs return: Hash[String, untyped]
|
|
62
82
|
def read(path)
|
|
63
83
|
return {} unless File.file?(path)
|
|
64
84
|
|
data/lib/riffer/rig/repl.rb
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Riffer::Rig::REPL
|
|
4
|
-
EXIT_COMMANDS = ['/exit', '/quit'].freeze
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
EXIT_COMMANDS = ['/exit', '/quit'].freeze #: Array[String]
|
|
5
|
+
|
|
6
|
+
SKILL_COMMAND = %r{\A/skill:([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+(.*))?\z}m #: Regexp
|
|
7
|
+
|
|
8
|
+
# @rbs @agent: Riffer::Agent
|
|
9
|
+
# @rbs @renderer: Riffer::Rig::UI::Renderer
|
|
10
|
+
# @rbs @animator: Riffer::Rig::UI::Animator
|
|
11
|
+
# @rbs @smoother: Riffer::Rig::UI::Smoother
|
|
12
|
+
# @rbs @cursor: Riffer::Rig::UI::Cursor
|
|
13
|
+
# @rbs @theme: Riffer::Rig::UI::Theme
|
|
14
|
+
# @rbs @input: untyped
|
|
15
|
+
# @rbs @output: untyped
|
|
16
|
+
|
|
17
|
+
# @rbs ?agent: Riffer::Agent
|
|
18
|
+
# @rbs ?renderer: Riffer::Rig::UI::Renderer
|
|
19
|
+
# @rbs input: untyped
|
|
20
|
+
# @rbs output: untyped
|
|
21
|
+
# @rbs ?theme: Riffer::Rig::UI::Theme
|
|
22
|
+
# @rbs ?animator: Riffer::Rig::UI::Animator
|
|
23
|
+
# @rbs ?smoother: Riffer::Rig::UI::Smoother
|
|
24
|
+
# @rbs ?cursor: Riffer::Rig::UI::Cursor
|
|
25
|
+
# @rbs return: void
|
|
26
|
+
def initialize(agent:, renderer:, input: $stdin, output: $stdout, theme: Riffer::Rig::UI::Theme.for(output),
|
|
27
|
+
animator: Riffer::Rig::UI::Animator.new(
|
|
28
|
+
io: output,
|
|
29
|
+
theme:
|
|
30
|
+
), smoother: Riffer::Rig::UI::Smoother.new(
|
|
31
|
+
io: output,
|
|
32
|
+
theme:
|
|
33
|
+
), cursor: Riffer::Rig::UI::Cursor.new(
|
|
34
|
+
io: output, theme:
|
|
35
|
+
))
|
|
8
36
|
@agent = agent
|
|
9
37
|
@renderer = renderer
|
|
10
38
|
@animator = animator
|
|
@@ -16,9 +44,10 @@ class Riffer::Rig::REPL
|
|
|
16
44
|
@agent.session.on_message { |message| render_tool_result(message) }
|
|
17
45
|
end
|
|
18
46
|
|
|
47
|
+
# @rbs return: Symbol
|
|
19
48
|
def run
|
|
20
49
|
loop do
|
|
21
|
-
@
|
|
50
|
+
@renderer.prompt
|
|
22
51
|
line = @input.gets
|
|
23
52
|
break if line.nil?
|
|
24
53
|
|
|
@@ -35,11 +64,14 @@ class Riffer::Rig::REPL
|
|
|
35
64
|
end
|
|
36
65
|
end
|
|
37
66
|
|
|
38
|
-
|
|
67
|
+
print_block { @theme.grey('see you on the next riff.') }
|
|
68
|
+
:done
|
|
39
69
|
end
|
|
40
70
|
|
|
41
71
|
private
|
|
42
72
|
|
|
73
|
+
# @rbs prompt: String
|
|
74
|
+
# @rbs return: void
|
|
43
75
|
def run_turn(prompt)
|
|
44
76
|
@cursor.hide
|
|
45
77
|
@animator.start
|
|
@@ -51,17 +83,20 @@ class Riffer::Rig::REPL
|
|
|
51
83
|
when Riffer::StreamEvents::ReasoningDone
|
|
52
84
|
@animator.start
|
|
53
85
|
when Riffer::StreamEvents::ToolCallDelta, Riffer::StreamEvents::FinishReasonDone
|
|
54
|
-
# Round bookkeeping: nothing renders, so the indicator just carries on
|
|
55
|
-
# (or stays parked while the smoother finishes typing earlier text).
|
|
56
86
|
next
|
|
57
|
-
when Riffer::StreamEvents::
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
# the render, then bring it straight back to cover the silent stretch.
|
|
87
|
+
when Riffer::StreamEvents::SkillActivation
|
|
88
|
+
# Tool execution and the next model invocation emit no events, so the
|
|
89
|
+
# spinner comes straight back on to cover the silent stretch.
|
|
61
90
|
@animator.stop
|
|
62
91
|
@renderer.render(event)
|
|
63
92
|
@animator.start
|
|
64
93
|
next
|
|
94
|
+
when Riffer::StreamEvents::TokenUsageDone
|
|
95
|
+
# Usage renders nothing inline — it flushes below the turn's output.
|
|
96
|
+
# Restarting here covers the silent tool-execution stretch that follows.
|
|
97
|
+
@renderer.render(event)
|
|
98
|
+
@animator.start
|
|
99
|
+
next
|
|
65
100
|
else
|
|
66
101
|
@animator.stop
|
|
67
102
|
end
|
|
@@ -69,15 +104,18 @@ class Riffer::Rig::REPL
|
|
|
69
104
|
end
|
|
70
105
|
@animator.stop
|
|
71
106
|
@smoother.finish
|
|
72
|
-
@
|
|
107
|
+
@renderer.flush_usage
|
|
73
108
|
rescue StandardError => e
|
|
74
|
-
@output.puts("\nError: #{e.message}")
|
|
75
|
-
ensure
|
|
76
109
|
@smoother.finish
|
|
110
|
+
print_block { @theme.red("Error: #{e.message}") }
|
|
111
|
+
ensure
|
|
77
112
|
@animator.stop
|
|
78
113
|
@cursor.show
|
|
79
114
|
end
|
|
80
115
|
|
|
116
|
+
# @rbs name: String
|
|
117
|
+
# @rbs args: String
|
|
118
|
+
# @rbs return: void
|
|
81
119
|
def run_skill_command(name, args)
|
|
82
120
|
block = activate_skill(name)
|
|
83
121
|
return if block.nil?
|
|
@@ -85,11 +123,13 @@ class Riffer::Rig::REPL
|
|
|
85
123
|
run_turn([block, args].reject(&:empty?).join("\n\n"))
|
|
86
124
|
end
|
|
87
125
|
|
|
126
|
+
# @rbs name: String
|
|
127
|
+
# @rbs return: String?
|
|
88
128
|
def activate_skill(name)
|
|
89
129
|
skills = @agent.context.skills
|
|
90
130
|
|
|
91
131
|
unless skills
|
|
92
|
-
@
|
|
132
|
+
print_block { @theme.grey('No skills configured.') }
|
|
93
133
|
return
|
|
94
134
|
end
|
|
95
135
|
|
|
@@ -97,23 +137,36 @@ class Riffer::Rig::REPL
|
|
|
97
137
|
# a non-mutating Context#read. `activate` marks the skill model-activated as
|
|
98
138
|
# a side effect, which drops it from the model's catalog after manual use.
|
|
99
139
|
body = skills.activate(name)
|
|
100
|
-
@
|
|
140
|
+
print_block { @theme.magenta("✦ skill: #{name}") }
|
|
101
141
|
skill_block(name, body)
|
|
102
142
|
rescue Riffer::ArgumentError
|
|
103
|
-
@
|
|
143
|
+
print_block { @theme.red("Unknown skill: #{name}") }
|
|
104
144
|
nil
|
|
105
145
|
rescue StandardError => e
|
|
106
|
-
@
|
|
146
|
+
print_block { @theme.red("Error activating skill: #{e.message}") }
|
|
107
147
|
nil
|
|
108
148
|
end
|
|
109
149
|
|
|
150
|
+
# @rbs name: String
|
|
151
|
+
# @rbs body: String
|
|
152
|
+
# @rbs return: String
|
|
110
153
|
def skill_block(name, body)
|
|
111
154
|
"<skill name=\"#{name}\">\n#{body}\n</skill>"
|
|
112
155
|
end
|
|
113
156
|
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
157
|
+
# Chrome lines that never pass through the renderer (errors, exit line) end
|
|
158
|
+
# their line with a newline but leave the renderer's tool-group state alone —
|
|
159
|
+
# they can only follow prose, never mid-group.
|
|
160
|
+
#
|
|
161
|
+
# @rbs return: void
|
|
162
|
+
def print_block(&)
|
|
163
|
+
@output.puts
|
|
164
|
+
@output.puts(yield)
|
|
165
|
+
@output.flush
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @rbs message: Riffer::Messages::Base
|
|
169
|
+
# @rbs return: void
|
|
117
170
|
def render_tool_result(message)
|
|
118
171
|
return unless message.is_a?(Riffer::Messages::Tool)
|
|
119
172
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# USD per million tokens, parsed from a model's entry in the settings file.
|
|
4
|
+
class Riffer::Rig::Settings::Pricing
|
|
5
|
+
# @dynamic input, output, cache_write, cache_read
|
|
6
|
+
attr_reader :input, :output, :cache_write, :cache_read #: Float
|
|
7
|
+
|
|
8
|
+
# @rbs input: Float
|
|
9
|
+
# @rbs output: Float
|
|
10
|
+
# @rbs cache_write: Float
|
|
11
|
+
# @rbs cache_read: Float
|
|
12
|
+
# @rbs return: void
|
|
13
|
+
def initialize(input:, output:, cache_write:, cache_read:)
|
|
14
|
+
@input = input
|
|
15
|
+
@output = output
|
|
16
|
+
@cache_write = cache_write
|
|
17
|
+
@cache_read = cache_read
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/riffer/rig/settings.rb
CHANGED
|
@@ -36,22 +36,29 @@ require 'fileutils'
|
|
|
36
36
|
module Riffer::Rig::Settings
|
|
37
37
|
extend self
|
|
38
38
|
|
|
39
|
-
PATH = File.expand_path('~/.riffer/settings.json')
|
|
39
|
+
PATH = File.expand_path('~/.riffer/settings.json') #: String
|
|
40
|
+
|
|
40
41
|
DEFAULT_MODEL = 'anthropic/claude-sonnet-4-6'
|
|
41
42
|
|
|
42
43
|
REASONING_LEVELS_BY_PROVIDER = {
|
|
43
44
|
'anthropic' => %w[low medium high xhigh max].freeze,
|
|
44
45
|
'openai' => %w[low medium high xhigh].freeze,
|
|
45
46
|
'openrouter' => %w[low medium high xhigh].freeze
|
|
46
|
-
}.freeze
|
|
47
|
+
}.freeze #: Hash[String, Array[String]]
|
|
47
48
|
|
|
48
49
|
# Returns the configured model string, or +DEFAULT_MODEL+ if not set.
|
|
50
|
+
#
|
|
51
|
+
# @rbs path: String
|
|
52
|
+
# @rbs return: String
|
|
49
53
|
def model(path: PATH)
|
|
50
54
|
read(path).fetch('model', DEFAULT_MODEL)
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
# Returns model options for the configured model and reasoning level, ready
|
|
54
58
|
# to pass directly to the Riffer agent's +model_options+.
|
|
59
|
+
#
|
|
60
|
+
# @rbs path: String
|
|
61
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
55
62
|
def model_options(path: PATH)
|
|
56
63
|
provider = provider_for(model(path:))
|
|
57
64
|
base_options(provider).merge(reasoning_options(reasoning_for(path:, provider:), provider))
|
|
@@ -60,34 +67,43 @@ module Riffer::Rig::Settings
|
|
|
60
67
|
# Returns the provider prefix for +model_string+, e.g. <tt>"anthropic"</tt>
|
|
61
68
|
# for <tt>"anthropic/claude-sonnet-4-6"</tt>. Returns +nil+ if the model
|
|
62
69
|
# string contains no slash.
|
|
70
|
+
#
|
|
71
|
+
# @rbs model_string: String
|
|
72
|
+
# @rbs return: String?
|
|
63
73
|
def provider_for(model_string)
|
|
64
74
|
model_string.split('/', 2).first if model_string.include?('/')
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
# Returns the pricing
|
|
77
|
+
# Returns the pricing for +model+, or +nil+ if not configured.
|
|
68
78
|
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
79
|
+
# @rbs model: String
|
|
80
|
+
# @rbs path: String
|
|
81
|
+
# @rbs return: Pricing?
|
|
71
82
|
def pricing_for(model, path: PATH)
|
|
72
83
|
entry = read(path).dig('models', model)
|
|
73
|
-
return nil unless entry
|
|
84
|
+
return nil unless entry.is_a?(Hash)
|
|
74
85
|
|
|
75
|
-
|
|
86
|
+
Pricing.new(
|
|
76
87
|
input: entry.fetch('input', 0).to_f,
|
|
77
88
|
output: entry.fetch('output', 0).to_f,
|
|
78
89
|
cache_write: entry.fetch('cache_write', 0).to_f,
|
|
79
90
|
cache_read: entry.fetch('cache_read', 0).to_f
|
|
80
|
-
|
|
91
|
+
)
|
|
81
92
|
end
|
|
82
93
|
|
|
83
94
|
private
|
|
84
95
|
|
|
96
|
+
# @rbs provider: String?
|
|
97
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
85
98
|
def base_options(provider)
|
|
86
99
|
return { cache_control: { type: :ephemeral } } if provider == 'anthropic'
|
|
87
100
|
|
|
88
101
|
{}
|
|
89
102
|
end
|
|
90
103
|
|
|
104
|
+
# @rbs path: String
|
|
105
|
+
# @rbs ?provider: String?
|
|
106
|
+
# @rbs return: String?
|
|
91
107
|
def reasoning_for(path: PATH, provider: nil)
|
|
92
108
|
level = read(path)['reasoning']
|
|
93
109
|
valid_levels = (provider && REASONING_LEVELS_BY_PROVIDER[provider]) || []
|
|
@@ -96,6 +112,10 @@ module Riffer::Rig::Settings
|
|
|
96
112
|
|
|
97
113
|
# Maps a reasoning level to the provider-specific model option hash expected
|
|
98
114
|
# by Riffer. Returns an empty hash when +level+ is +nil+.
|
|
115
|
+
#
|
|
116
|
+
# @rbs level: String?
|
|
117
|
+
# @rbs provider: String?
|
|
118
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
99
119
|
def reasoning_options(level, provider)
|
|
100
120
|
return {} unless level
|
|
101
121
|
|
|
@@ -109,6 +129,11 @@ module Riffer::Rig::Settings
|
|
|
109
129
|
end
|
|
110
130
|
end
|
|
111
131
|
|
|
132
|
+
# The parsed settings file, or an empty hash when the file is absent or
|
|
133
|
+
# malformed.
|
|
134
|
+
#
|
|
135
|
+
# @rbs path: String
|
|
136
|
+
# @rbs return: Hash[String, untyped]
|
|
112
137
|
def read(path)
|
|
113
138
|
return {} unless File.file?(path)
|
|
114
139
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Accumulates token usage across multiple turns in a session and computes an
|
|
4
|
-
# estimated cost based on a pricing
|
|
4
|
+
# estimated cost based on a pricing object sourced from
|
|
5
|
+
# +Riffer::Rig::Settings+.
|
|
5
6
|
#
|
|
6
7
|
# Instantiate once per REPL session, call +add+ after each turn, then read
|
|
7
8
|
# +total_tokens+ and +estimated_cost+ for display.
|
|
@@ -12,8 +13,17 @@
|
|
|
12
13
|
# tally.estimated_cost # => 0.000042 (USD), or nil if pricing is nil
|
|
13
14
|
#
|
|
14
15
|
class Riffer::Rig::TokenTally
|
|
16
|
+
# @rbs @input_tokens: Integer
|
|
17
|
+
# @rbs @output_tokens: Integer
|
|
18
|
+
# @rbs @cache_write_tokens: Integer
|
|
19
|
+
# @rbs @cache_read_tokens: Integer
|
|
20
|
+
# @rbs @pricing: Riffer::Rig::Settings::Pricing?
|
|
21
|
+
|
|
22
|
+
# @dynamic input_tokens, output_tokens, cache_write_tokens, cache_read_tokens
|
|
15
23
|
attr_reader :input_tokens, :output_tokens, :cache_write_tokens, :cache_read_tokens
|
|
16
24
|
|
|
25
|
+
# @rbs ?pricing: Riffer::Rig::Settings::Pricing?
|
|
26
|
+
# @rbs return: void
|
|
17
27
|
def initialize(pricing: nil)
|
|
18
28
|
@pricing = pricing
|
|
19
29
|
@input_tokens = 0
|
|
@@ -24,6 +34,8 @@ class Riffer::Rig::TokenTally
|
|
|
24
34
|
|
|
25
35
|
# Accumulates token counts from a +Riffer::Providers::TokenUsage+ object.
|
|
26
36
|
#
|
|
37
|
+
# @rbs usage: Riffer::Providers::TokenUsage
|
|
38
|
+
# @rbs return: void
|
|
27
39
|
def add(usage)
|
|
28
40
|
@input_tokens += usage.input_tokens
|
|
29
41
|
@output_tokens += usage.output_tokens
|
|
@@ -33,26 +45,30 @@ class Riffer::Rig::TokenTally
|
|
|
33
45
|
|
|
34
46
|
# Returns the total token count across all categories.
|
|
35
47
|
#
|
|
48
|
+
# @rbs return: Integer
|
|
36
49
|
def total_tokens
|
|
37
50
|
@input_tokens + @output_tokens + @cache_write_tokens + @cache_read_tokens
|
|
38
51
|
end
|
|
39
52
|
|
|
40
53
|
# Returns +true+ if any tokens have been counted.
|
|
41
54
|
#
|
|
55
|
+
# @rbs return: bool
|
|
42
56
|
def any?
|
|
43
57
|
total_tokens.positive?
|
|
44
58
|
end
|
|
45
59
|
|
|
46
60
|
# Returns the estimated cost in USD, or +nil+ if no pricing was provided.
|
|
47
61
|
#
|
|
62
|
+
# @rbs return: Float?
|
|
48
63
|
def estimated_cost
|
|
49
|
-
|
|
64
|
+
pricing = @pricing
|
|
65
|
+
return nil unless pricing
|
|
50
66
|
|
|
51
67
|
(
|
|
52
|
-
(@input_tokens *
|
|
53
|
-
(@output_tokens *
|
|
54
|
-
(@cache_write_tokens *
|
|
55
|
-
(@cache_read_tokens *
|
|
68
|
+
(@input_tokens * pricing.input) +
|
|
69
|
+
(@output_tokens * pricing.output) +
|
|
70
|
+
(@cache_write_tokens * pricing.cache_write) +
|
|
71
|
+
(@cache_read_tokens * pricing.cache_read)
|
|
56
72
|
) / 1_000_000.0
|
|
57
73
|
end
|
|
58
74
|
end
|