riffer-rig 0.5.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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/README.md +77 -7
  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 +70 -102
  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/resolution.rb +16 -0
  21. data/lib/riffer/rig/credentials.rb +151 -60
  22. data/lib/riffer/rig/directories.rb +20 -0
  23. data/lib/riffer/rig/env/invalid.rb +13 -0
  24. data/lib/riffer/rig/env.rb +40 -0
  25. data/lib/riffer/rig/events/after_response.rb +25 -0
  26. data/lib/riffer/rig/events/after_tool_call.rb +31 -0
  27. data/lib/riffer/rig/events/before_prompt.rb +25 -0
  28. data/lib/riffer/rig/events/before_request.rb +25 -0
  29. data/lib/riffer/rig/events/before_tool_call.rb +28 -0
  30. data/lib/riffer/rig/events/command_output.rb +28 -0
  31. data/lib/riffer/rig/events/notify.rb +28 -0
  32. data/lib/riffer/rig/events/session_end.rb +25 -0
  33. data/lib/riffer/rig/events/session_start.rb +28 -0
  34. data/lib/riffer/rig/events/skill_activated.rb +25 -0
  35. data/lib/riffer/rig/events/turn_end.rb +33 -0
  36. data/lib/riffer/rig/events.rb +4 -0
  37. data/lib/riffer/rig/extension/failure.rb +16 -0
  38. data/lib/riffer/rig/extension.rb +16 -5
  39. data/lib/riffer/rig/hosts/mirror.rb +59 -0
  40. data/lib/riffer/rig/hosts/null.rb +36 -0
  41. data/lib/riffer/rig/loader.rb +163 -0
  42. data/lib/riffer/rig/mcp/declaration.rb +23 -0
  43. data/lib/riffer/rig/mcp/server.rb +16 -0
  44. data/lib/riffer/rig/mcp.rb +12 -0
  45. data/lib/riffer/rig/prompts/agents_md.rb +32 -0
  46. data/lib/riffer/rig/prompts.rb +4 -0
  47. data/lib/riffer/rig/provider_setup/field.rb +24 -0
  48. data/lib/riffer/rig/provider_setup.rb +84 -0
  49. data/lib/riffer/rig/registrar.rb +134 -7
  50. data/lib/riffer/rig/runtime/blocked.rb +13 -0
  51. data/lib/riffer/rig/runtime/cancel_flag.rb +27 -0
  52. data/lib/riffer/rig/runtime/hooks.rb +78 -0
  53. data/lib/riffer/rig/runtime/request_guardrail.rb +24 -0
  54. data/lib/riffer/rig/runtime/tool_runtime.rb +44 -0
  55. data/lib/riffer/rig/runtime.rb +582 -70
  56. data/lib/riffer/rig/settings/document.rb +28 -12
  57. data/lib/riffer/rig/settings/pricing.rb +16 -6
  58. data/lib/riffer/rig/settings.rb +108 -81
  59. data/lib/riffer/rig/skills/sources.rb +28 -0
  60. data/lib/riffer/rig/skills.rb +12 -0
  61. data/lib/riffer/rig/support/equatable.rb +21 -0
  62. data/lib/riffer/rig/{ui → terminal}/animator.rb +6 -13
  63. data/lib/riffer/rig/{ui → terminal}/banner.rb +17 -21
  64. data/lib/riffer/rig/{ui → terminal}/cursor.rb +4 -6
  65. data/lib/riffer/rig/terminal/host.rb +85 -0
  66. data/lib/riffer/rig/{ui → terminal}/palette.rb +1 -1
  67. data/lib/riffer/rig/{ui → terminal}/renderer.rb +121 -87
  68. data/lib/riffer/rig/{ui → terminal}/smoother.rb +15 -23
  69. data/lib/riffer/rig/{ui → terminal}/theme.rb +12 -14
  70. data/lib/riffer/rig/terminal.rb +207 -0
  71. data/lib/riffer/rig/tools/bash.rb +36 -13
  72. data/lib/riffer/rig/tools/edit.rb +3 -2
  73. data/lib/riffer/rig/tools/read.rb +3 -2
  74. data/lib/riffer/rig/tools/write.rb +3 -2
  75. data/lib/riffer/rig/version.rb +1 -1
  76. data/lib/riffer/rig.rb +10 -15
  77. data/sig/generated/riffer/rig/bundled/agents_md.rbs +5 -0
  78. data/sig/generated/riffer/rig/bundled/bash.rbs +5 -0
  79. data/sig/generated/riffer/rig/bundled/edit.rbs +5 -0
  80. data/sig/generated/riffer/rig/bundled/mcp.rbs +5 -0
  81. data/sig/generated/riffer/rig/bundled/read.rbs +5 -0
  82. data/sig/generated/riffer/rig/bundled/skills.rbs +5 -0
  83. data/sig/generated/riffer/rig/bundled/write.rbs +5 -0
  84. data/sig/generated/riffer/rig/bundled.rbs +5 -0
  85. data/sig/generated/riffer/rig/cli/flags.rbs +40 -0
  86. data/sig/generated/riffer/rig/cli.rbs +30 -27
  87. data/sig/generated/riffer/rig/command/context.rbs +50 -0
  88. data/sig/generated/riffer/rig/command.rbs +23 -0
  89. data/sig/generated/riffer/rig/commands/model.rbs +27 -0
  90. data/sig/generated/riffer/rig/commands/skill.rbs +12 -0
  91. data/sig/generated/riffer/rig/commands.rbs +4 -0
  92. data/sig/generated/riffer/rig/credentials/resolution.rbs +13 -0
  93. data/sig/generated/riffer/rig/credentials.rbs +70 -41
  94. data/sig/generated/riffer/rig/directories.rbs +11 -0
  95. data/sig/generated/riffer/rig/env/invalid.rbs +10 -0
  96. data/sig/generated/riffer/rig/env.rbs +26 -0
  97. data/sig/generated/riffer/rig/events/after_response.rbs +18 -0
  98. data/sig/generated/riffer/rig/events/after_tool_call.rbs +24 -0
  99. data/sig/generated/riffer/rig/events/before_prompt.rbs +18 -0
  100. data/sig/generated/riffer/rig/events/before_request.rbs +18 -0
  101. data/sig/generated/riffer/rig/events/before_tool_call.rbs +21 -0
  102. data/sig/generated/riffer/rig/events/command_output.rbs +21 -0
  103. data/sig/generated/riffer/rig/events/notify.rbs +21 -0
  104. data/sig/generated/riffer/rig/events/session_end.rbs +18 -0
  105. data/sig/generated/riffer/rig/events/session_start.rbs +21 -0
  106. data/sig/generated/riffer/rig/events/skill_activated.rbs +18 -0
  107. data/sig/generated/riffer/rig/events/turn_end.rbs +24 -0
  108. data/sig/generated/riffer/rig/events.rbs +4 -0
  109. data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
  110. data/sig/generated/riffer/rig/extension.rbs +14 -5
  111. data/sig/generated/riffer/rig/hosts/mirror.rbs +41 -0
  112. data/sig/generated/riffer/rig/hosts/null.rbs +26 -0
  113. data/sig/generated/riffer/rig/loader.rbs +77 -0
  114. data/sig/generated/riffer/rig/mcp/declaration.rbs +18 -0
  115. data/sig/generated/riffer/rig/mcp/server.rbs +13 -0
  116. data/sig/generated/riffer/rig/mcp.rbs +8 -0
  117. data/sig/generated/riffer/rig/prompts/agents_md.rbs +19 -0
  118. data/sig/generated/riffer/rig/prompts.rbs +4 -0
  119. data/sig/generated/riffer/rig/provider_setup/field.rbs +22 -0
  120. data/sig/generated/riffer/rig/provider_setup.rbs +30 -0
  121. data/sig/generated/riffer/rig/registrar.rbs +88 -6
  122. data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
  123. data/sig/generated/riffer/rig/runtime/cancel_flag.rbs +19 -0
  124. data/sig/generated/riffer/rig/runtime/hooks.rbs +49 -0
  125. data/sig/generated/riffer/rig/runtime/request_guardrail.rbs +16 -0
  126. data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +23 -0
  127. data/sig/generated/riffer/rig/runtime.rbs +262 -40
  128. data/sig/generated/riffer/rig/settings/document.rbs +13 -9
  129. data/sig/generated/riffer/rig/settings/pricing.rbs +7 -3
  130. data/sig/generated/riffer/rig/settings.rbs +56 -64
  131. data/sig/generated/riffer/rig/skills/sources.rbs +18 -0
  132. data/sig/generated/riffer/rig/skills.rbs +7 -0
  133. data/sig/generated/riffer/rig/support/equatable.rbs +15 -0
  134. data/sig/generated/riffer/rig/{ui → terminal}/animator.rbs +5 -13
  135. data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
  136. data/sig/generated/riffer/rig/terminal/cursor.rbs +31 -0
  137. data/sig/generated/riffer/rig/terminal/host.rbs +50 -0
  138. data/sig/generated/riffer/rig/{ui → terminal}/palette.rbs +2 -2
  139. data/sig/generated/riffer/rig/terminal/renderer.rbs +126 -0
  140. data/sig/generated/riffer/rig/terminal/smoother.rbs +59 -0
  141. data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -7
  142. data/sig/generated/riffer/rig/terminal.rbs +85 -0
  143. data/sig/generated/riffer/rig/tools/bash.rbs +14 -1
  144. data/sig/generated/riffer/rig.rbs +4 -7
  145. data/sig/manual/riffer/rig/cli.rbs +2 -4
  146. data/sig/manual/riffer/rig/commands/model.rbs +6 -0
  147. data/sig/manual/riffer/rig/commands/skill.rbs +6 -0
  148. data/sig/manual/riffer/rig/credentials.rbs +2 -4
  149. data/sig/manual/riffer/rig/directories.rbs +6 -0
  150. data/sig/manual/riffer/rig/events.rbs +6 -0
  151. data/sig/manual/riffer/rig/hosts.rbs +12 -0
  152. data/sig/manual/riffer/rig/mcp.rbs +11 -0
  153. data/sig/manual/riffer/rig/prompts/agents_md.rbs +6 -0
  154. data/sig/manual/riffer/rig/settings.rbs +2 -4
  155. data/sig/manual/riffer/rig/skills.rbs +6 -0
  156. data/sig/manual/riffer/rig/support.rbs +8 -0
  157. data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
  158. metadata +173 -35
  159. data/lib/riffer/rig/coding_agent.rb +0 -53
  160. data/lib/riffer/rig/host.rb +0 -52
  161. data/lib/riffer/rig/repl.rb +0 -189
  162. data/lib/riffer/rig/token_tally.rb +0 -74
  163. data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
  164. data/sig/generated/riffer/rig/host.rbs +0 -42
  165. data/sig/generated/riffer/rig/repl.rbs +0 -77
  166. data/sig/generated/riffer/rig/token_tally.rbs +0 -61
  167. data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
  168. data/sig/generated/riffer/rig/ui/cursor.rbs +0 -33
  169. data/sig/generated/riffer/rig/ui/renderer.rbs +0 -118
  170. data/sig/generated/riffer/rig/ui/smoother.rbs +0 -72
  171. data/sig/manual/riffer/rig/ui/banner.rbs +0 -8
  172. data/sig/manual/riffer/rig/ui.rbs +0 -5
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Commands::Model
4
+ extend self
5
+
6
+ HINT = 'Use /model provider/name, with a provider from: ' \
7
+ "#{Riffer::Rig::Settings::PROVIDERS.join(', ')}".freeze #: String
8
+
9
+ SAVE_UNAVAILABLE = '/model --save is not available in this host'
10
+
11
+ # @rbs return: Riffer::Rig::Command
12
+ def command
13
+ Riffer::Rig::Command.new(
14
+ 'model',
15
+ description: 'Switch the model for this session: /model provider/name',
16
+ extension: 'core'
17
+ ) { |ctx| run(ctx) }
18
+ end
19
+
20
+ # @rbs ctx: Riffer::Rig::Command::Context
21
+ # @rbs return: void
22
+ def run(ctx)
23
+ model = ctx.args.strip
24
+ return ctx.host.notify(SAVE_UNAVAILABLE, level: :error) if model.split.include?('--save')
25
+ return ctx.say("Model: #{ctx.runtime.model}") if model.empty?
26
+
27
+ provider = Riffer::Rig::Settings.provider_for(model)
28
+ return ctx.say(HINT) unless provider && Riffer::Providers::Repository.find(provider)
29
+
30
+ missing = missing_credentials(provider, ctx.runtime.credentials)
31
+ return ctx.host.notify(refusal(model, provider, missing), level: :error) unless missing.empty?
32
+
33
+ ctx.runtime.model = model
34
+ ctx.say("Model: #{model}")
35
+ end
36
+
37
+ private
38
+
39
+ # @rbs model: String
40
+ # @rbs provider: String
41
+ # @rbs missing: Array[Symbol]
42
+ # @rbs return: String
43
+ def refusal(model, provider, missing)
44
+ "Not switching to #{model}: #{provider} has no #{missing.join(', ')} in credentials"
45
+ end
46
+
47
+ # @rbs provider: String
48
+ # @rbs credentials: Hash[Symbol, Hash[Symbol, String]]
49
+ # @rbs return: Array[Symbol]
50
+ def missing_credentials(provider, credentials)
51
+ required = Riffer::Rig::ProviderSetup[provider]&.fields&.select(&:required) || []
52
+ required.map(&:name).reject { |name| credentials.dig(provider.to_sym, name) }
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Commands::Skill
4
+ extend self
5
+
6
+ # @rbs skill: Riffer::Skills::Frontmatter
7
+ # @rbs return: Riffer::Rig::Command
8
+ def command(skill)
9
+ Riffer::Rig::Command.new("skill:#{skill.name}", description: skill.description, extension: 'core') do |ctx|
10
+ run(ctx, skill)
11
+ end
12
+ end
13
+
14
+ # @rbs ctx: Riffer::Rig::Command::Context
15
+ # @rbs skill: Riffer::Skills::Frontmatter
16
+ # @rbs return: void
17
+ def run(ctx, skill)
18
+ skills = ctx.runtime.agent.context.skills
19
+ return ctx.host.notify("Skill #{skill.name} is no longer available", level: :error) unless skills
20
+
21
+ # Context#read, not #activate: a skill the user runs stays in the model's
22
+ # catalog, and only the model's own activations are recorded.
23
+ content = skills.adapter.render_activation(skill, skills.read(skill.name))
24
+ ctx.emit(Riffer::Rig::Events::SkillActivated.new(skill.name))
25
+ ctx.prompt([content, ctx.args.strip].reject(&:empty?).join("\n\n"))
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Commands
4
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Credentials::Resolution
4
+ # @dynamic values, missing
5
+ attr_reader :values #: Hash[Symbol, String]
6
+ attr_reader :missing #: Array[Symbol]
7
+
8
+ # @rbs values: Hash[Symbol, String]
9
+ # @rbs missing: Array[Symbol]
10
+ # @rbs return: void
11
+ def initialize(values:, missing:)
12
+ @values = values.freeze
13
+ @missing = missing.freeze
14
+ freeze
15
+ end
16
+ end
@@ -2,88 +2,179 @@
2
2
 
3
3
  require 'json'
4
4
  require 'fileutils'
5
+ require 'open3'
5
6
 
6
- # Reads and writes API keys for each supported provider in
7
- # <tt>~/.riffer/auth.json</tt>.
8
- #
9
- # Keys are stored under the provider name, e.g.:
10
- #
11
- # {
12
- # "anthropic": "sk-ant-...",
13
- # "openai": "sk-...",
14
- # "gemini": "...",
15
- # "openrouter": "sk-or-..."
16
- # }
17
- #
18
- # The environment variable checked per provider:
19
- #
20
- # anthropic → ANTHROPIC_API_KEY
21
- # openai → OPENAI_API_KEY
22
- # gemini → GEMINI_API_KEY
23
- # openrouter → OPENROUTER_API_KEY
24
- #
25
7
  module Riffer::Rig::Credentials
26
8
  extend self
27
9
 
28
10
  PATH = File.expand_path('~/.riffer/auth.json') #: String
29
11
 
30
- PROVIDER_ENV_VARS = {
31
- 'anthropic' => 'ANTHROPIC_API_KEY',
32
- 'openai' => 'OPENAI_API_KEY',
33
- 'gemini' => 'GEMINI_API_KEY',
34
- 'openrouter' => 'OPENROUTER_API_KEY'
35
- }.freeze #: Hash[String, String]
36
-
37
- # Returns the API key for +provider+, checking the environment variable first,
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?
43
- def api_key_for(provider, path: PATH)
44
- env_var = provider && PROVIDER_ENV_VARS[provider]
45
- key = env_var && ENV.fetch(env_var, nil).then { |v| v unless v.nil? || v.strip.empty? }
46
- key || key_from_file(provider, path)
12
+ ENTRY_TYPE = 'api_key'
13
+
14
+ # @rbs identifier: String | Symbol
15
+ # @rbs host: Riffer::Rig::Hosts::_Host
16
+ # @rbs setup: Riffer::Rig::ProviderSetup
17
+ # @rbs env: Riffer::Rig::Env
18
+ # @rbs auth_path: String
19
+ # @rbs settings_path: String
20
+ # @rbs return: Resolution
21
+ def resolve(
22
+ identifier,
23
+ host:,
24
+ setup: Riffer::Rig::ProviderSetup.for(identifier),
25
+ env: Riffer::Rig::Env.new,
26
+ auth_path: PATH,
27
+ settings_path: Riffer::Rig::Settings::PATH
28
+ )
29
+ secrets = stored_secrets(identifier, auth_path)
30
+ plain = Riffer::Rig::Settings.provider_fields(identifier.to_s, path: settings_path)
31
+ found = setup.fields.to_h do |field|
32
+ [field.name, env.value_for(field) || stored_value(field, secrets, plain, env) || field.fallback&.call]
33
+ end.compact
34
+ required = setup.fields.select(&:required)
35
+ answers = ask_for(required.reject { |field| found.key?(field.name) }, identifier, host)
36
+ store(identifier, answers, setup:, auth_path:, settings_path:)
37
+
38
+ values = found.merge(answers)
39
+ Resolution.new(values:, missing: required.map(&:name) - values.keys)
47
40
  end
48
41
 
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
55
- def save_api_key(provider, key, path: PATH)
56
- FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
57
- data = read(path).merge(provider => key)
58
- File.open(path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
59
- file.write(JSON.pretty_generate(data))
42
+ # @rbs identifier: String | Symbol
43
+ # @rbs values: Hash[Symbol, String]
44
+ # @rbs config: Riffer::Config
45
+ # @rbs return: void
46
+ def apply(identifier, values, config: Riffer.config)
47
+ return unless Riffer::Rig::ProviderSetup[identifier]
48
+
49
+ provider_config = config.public_send(identifier)
50
+ values.each { |name, value| provider_config.public_send(:"#{name}=", value) }
51
+ end
52
+
53
+ # @rbs identifier: String | Symbol
54
+ # @rbs values: Hash[Symbol, String]
55
+ # @rbs setup: Riffer::Rig::ProviderSetup
56
+ # @rbs auth_path: String
57
+ # @rbs settings_path: String
58
+ # @rbs return: void
59
+ def store(
60
+ identifier,
61
+ values,
62
+ setup: Riffer::Rig::ProviderSetup.for(identifier),
63
+ auth_path: PATH,
64
+ settings_path: Riffer::Rig::Settings::PATH
65
+ )
66
+ secret_names = setup.fields.select(&:secret).map { |field| field.name.to_s }
67
+ fields = values.transform_keys(&:to_s)
68
+ secrets = fields.slice(*secret_names)
69
+ plain = fields.except(*secret_names)
70
+
71
+ unless secrets.empty?
72
+ entries = read_auth(auth_path).merge(identifier.to_s => secrets) do |_identifier, stored, given|
73
+ stored.merge(given)
74
+ end
75
+ write_auth(auth_path, entries)
60
76
  end
61
- key
77
+ Riffer::Rig::Settings.store_provider(identifier.to_s, plain, path: settings_path) unless plain.empty?
78
+ end
79
+
80
+ # @rbs identifier: String | Symbol
81
+ # @rbs auth_path: String
82
+ # @rbs settings_path: String
83
+ # @rbs return: void
84
+ def remove(identifier, auth_path: PATH, settings_path: Riffer::Rig::Settings::PATH)
85
+ entries = read_auth(auth_path)
86
+ write_auth(auth_path, entries.except(identifier.to_s)) if entries.key?(identifier.to_s)
87
+ Riffer::Rig::Settings.remove_provider(identifier.to_s, path: settings_path)
88
+ end
89
+
90
+ # @rbs identifier: String | Symbol
91
+ # @rbs setup: Riffer::Rig::ProviderSetup
92
+ # @rbs env: Riffer::Rig::Env
93
+ # @rbs auth_path: String
94
+ # @rbs return: :env | :stored | :chain | :missing
95
+ def status(identifier, setup: Riffer::Rig::ProviderSetup.for(identifier), env: Riffer::Rig::Env.new, auth_path: PATH)
96
+ secret_fields = setup.fields.select(&:secret)
97
+ return :env if secret_fields.any? { |field| env.value_for(field) }
98
+
99
+ stored = stored_secrets(identifier, auth_path)
100
+ return :stored if secret_fields.any? { |field| stored.key?(field.name.to_s) }
101
+
102
+ setup.chain ? :chain : :missing
62
103
  end
63
104
 
64
105
  private
65
106
 
66
- # @rbs provider: String?
67
- # @rbs path: String
107
+ # @rbs fields: Array[Riffer::Rig::ProviderSetup::Field]
108
+ # @rbs identifier: String | Symbol
109
+ # @rbs host: Riffer::Rig::Hosts::_Host
110
+ # @rbs return: Hash[Symbol, String]
111
+ def ask_for(fields, identifier, host)
112
+ fields.to_h do |field|
113
+ [field.name, presence(host.ask("#{identifier} #{field.name}", secret: field.secret))]
114
+ end.compact
115
+ end
116
+
117
+ # @rbs field: Riffer::Rig::ProviderSetup::Field
118
+ # @rbs secrets: Hash[String, String]
119
+ # @rbs plain: Hash[String, String]
120
+ # @rbs env: Riffer::Rig::Env
121
+ # @rbs return: String?
122
+ def stored_value(field, secrets, plain, env)
123
+ field.secret ? expand(secrets[field.name.to_s], env) : plain[field.name.to_s]
124
+ end
125
+
126
+ # @rbs value: String?
127
+ # @rbs env: Riffer::Rig::Env
128
+ # @rbs return: String?
129
+ def expand(value, env)
130
+ return nil if value.nil?
131
+ return env[value.delete_prefix('$')] if value.start_with?('$')
132
+ return presence(run(value.delete_prefix('!'))) if value.start_with?('!')
133
+
134
+ value
135
+ end
136
+
137
+ # @rbs command: String
68
138
  # @rbs return: String?
69
- def key_from_file(provider, path)
70
- key = read(path)[provider]
71
- return nil if key.nil? || key.strip.empty?
139
+ def run(command)
140
+ stdout, status = Open3.capture2(command)
141
+ stdout if status.success?
142
+ end
72
143
 
73
- key
144
+ # @rbs value: String?
145
+ # @rbs return: String?
146
+ def presence(value)
147
+ stripped = value.to_s.strip
148
+ stripped unless stripped.empty?
74
149
  end
75
150
 
76
- # The parsed auth file, or an empty hash when the file is absent or
77
- # malformed. Non-string values can't be keys, so they're dropped here rather
78
- # than re-checked by every reader.
79
- #
151
+ # @rbs identifier: String | Symbol
80
152
  # @rbs path: String
81
153
  # @rbs return: Hash[String, String]
82
- def read(path)
154
+ def stored_secrets(identifier, path)
155
+ read_auth(path)[identifier.to_s] || {}
156
+ end
157
+
158
+ # @rbs path: String
159
+ # @rbs return: Hash[String, Hash[String, String]]
160
+ def read_auth(path)
83
161
  return {} unless File.file?(path)
84
162
 
85
- JSON.parse(File.read(path)).select { |_provider, key| key.is_a?(String) }
163
+ JSON.parse(File.read(path))
164
+ .select { |_identifier, entry| entry.is_a?(Hash) && entry['type'] == ENTRY_TYPE }
165
+ .transform_values { |entry| entry.except('type').select { |_name, value| value.is_a?(String) } }
86
166
  rescue JSON::ParserError
87
167
  {}
88
168
  end
169
+
170
+ # @rbs path: String
171
+ # @rbs entries: Hash[String, Hash[String, String]]
172
+ # @rbs return: void
173
+ def write_auth(path, entries)
174
+ FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
175
+ data = entries.transform_values { |fields| { 'type' => ENTRY_TYPE }.merge(fields) }
176
+ File.open(path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
177
+ file.write(JSON.pretty_generate(data))
178
+ end
179
+ end
89
180
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Directories
4
+ extend self
5
+
6
+ # @rbs dir: String
7
+ # @rbs return: Array[String]
8
+ def ancestors(dir)
9
+ parent = File.dirname(dir)
10
+ parent == dir ? [dir] : [dir, *ancestors(parent)]
11
+ end
12
+
13
+ # @rbs cwd: String
14
+ # @rbs return: Array[String]
15
+ def up_to_repo_root(cwd)
16
+ dirs = ancestors(File.expand_path(cwd))
17
+ root = dirs.index { |dir| File.exist?(File.join(dir, '.git')) } || 0
18
+ dirs.take(root + 1)
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Env::Invalid
4
+ # @dynamic message
5
+ attr_reader :message #: String
6
+
7
+ # @rbs message: String
8
+ # @rbs return: void
9
+ def initialize(message)
10
+ @message = message
11
+ freeze
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Env
4
+ # @rbs @source: Hash[String, String]
5
+
6
+ # @dynamic no_color, model
7
+ attr_reader :no_color #: bool
8
+ attr_reader :model #: String?
9
+
10
+ # @rbs source: Hash[String, String]
11
+ # @rbs return: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
12
+ def self.load(source = ENV.to_h)
13
+ env = new(source)
14
+ model = env.model
15
+ rejection = model && Riffer::Rig::Settings.rejection(model)
16
+ rejection ? Invalid.new("RIFFER_MODEL: #{rejection}") : env
17
+ end
18
+
19
+ # @rbs source: Hash[String, String]
20
+ # @rbs return: void
21
+ def initialize(source = ENV.to_h)
22
+ @source = source.freeze
23
+ @no_color = @source.key?('NO_COLOR')
24
+ @model = self['RIFFER_MODEL']
25
+ freeze
26
+ end
27
+
28
+ # @rbs name: String
29
+ # @rbs return: String?
30
+ def [](name)
31
+ stripped = @source[name].to_s.strip
32
+ stripped unless stripped.empty?
33
+ end
34
+
35
+ # @rbs field: Riffer::Rig::ProviderSetup::Field
36
+ # @rbs return: String?
37
+ def value_for(field)
38
+ field.env.filter_map { |name| self[name] }.first
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::AfterResponse
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic message
7
+ attr_reader :message #: ::Riffer::Messages::Assistant
8
+
9
+ # @rbs message: ::Riffer::Messages::Assistant
10
+ # @rbs return: void
11
+ def initialize(message)
12
+ @message = message
13
+ freeze
14
+ end
15
+
16
+ # @rbs return: Symbol
17
+ def type
18
+ :after_response
19
+ end
20
+
21
+ # @rbs return: Hash[Symbol, untyped]
22
+ def to_h
23
+ { type: type, message: message }
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::AfterToolCall
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic tool, args, result
7
+ attr_reader :tool #: String
8
+ attr_reader :args #: Hash[Symbol, untyped]
9
+ attr_reader :result #: ::Riffer::Tools::Response
10
+
11
+ # @rbs tool: String
12
+ # @rbs args: Hash[Symbol, untyped]
13
+ # @rbs result: ::Riffer::Tools::Response
14
+ # @rbs return: void
15
+ def initialize(tool, args, result)
16
+ @tool = tool
17
+ @args = args.dup.freeze
18
+ @result = result
19
+ freeze
20
+ end
21
+
22
+ # @rbs return: Symbol
23
+ def type
24
+ :after_tool_call
25
+ end
26
+
27
+ # @rbs return: Hash[Symbol, untyped]
28
+ def to_h
29
+ { type: type, tool: tool, args: args, result: result }
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::BeforePrompt
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic text
7
+ attr_reader :text #: String
8
+
9
+ # @rbs text: String
10
+ # @rbs return: void
11
+ def initialize(text)
12
+ @text = text
13
+ freeze
14
+ end
15
+
16
+ # @rbs return: Symbol
17
+ def type
18
+ :before_prompt
19
+ end
20
+
21
+ # @rbs return: Hash[Symbol, untyped]
22
+ def to_h
23
+ { type: type, text: text }
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::BeforeRequest
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic messages
7
+ attr_reader :messages #: Array[::Riffer::Messages::Base]
8
+
9
+ # @rbs messages: Array[::Riffer::Messages::Base]
10
+ # @rbs return: void
11
+ def initialize(messages)
12
+ @messages = messages.dup.freeze
13
+ freeze
14
+ end
15
+
16
+ # @rbs return: Symbol
17
+ def type
18
+ :before_request
19
+ end
20
+
21
+ # @rbs return: Hash[Symbol, untyped]
22
+ def to_h
23
+ { type: type, messages: messages }
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::BeforeToolCall
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic tool, args
7
+ attr_reader :tool #: String
8
+ attr_reader :args #: Hash[Symbol, untyped]
9
+
10
+ # @rbs tool: String
11
+ # @rbs args: Hash[Symbol, untyped]
12
+ # @rbs return: void
13
+ def initialize(tool, args)
14
+ @tool = tool
15
+ @args = args.dup.freeze
16
+ freeze
17
+ end
18
+
19
+ # @rbs return: Symbol
20
+ def type
21
+ :before_tool_call
22
+ end
23
+
24
+ # @rbs return: Hash[Symbol, untyped]
25
+ def to_h
26
+ { type: type, tool: tool, args: args }
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::CommandOutput
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic command, text
7
+ attr_reader :command #: String
8
+ attr_reader :text #: String
9
+
10
+ # @rbs command: String
11
+ # @rbs text: String
12
+ # @rbs return: void
13
+ def initialize(command, text)
14
+ @command = command
15
+ @text = text
16
+ freeze
17
+ end
18
+
19
+ # @rbs return: Symbol
20
+ def type
21
+ :command_output
22
+ end
23
+
24
+ # @rbs return: Hash[Symbol, untyped]
25
+ def to_h
26
+ { type: type, command: command, text: text }
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::Notify
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic message, level
7
+ attr_reader :message #: String?
8
+ attr_reader :level #: Symbol
9
+
10
+ # @rbs message: String?
11
+ # @rbs level: Symbol
12
+ # @rbs return: void
13
+ def initialize(message, level)
14
+ @message = message
15
+ @level = level
16
+ freeze
17
+ end
18
+
19
+ # @rbs return: Symbol
20
+ def type
21
+ :notify
22
+ end
23
+
24
+ # @rbs return: Hash[Symbol, untyped]
25
+ def to_h
26
+ { type: type, message: message, level: level }
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::SessionEnd
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic reason
7
+ attr_reader :reason #: Symbol
8
+
9
+ # @rbs reason: Symbol
10
+ # @rbs return: void
11
+ def initialize(reason)
12
+ @reason = reason
13
+ freeze
14
+ end
15
+
16
+ # @rbs return: Symbol
17
+ def type
18
+ :session_end
19
+ end
20
+
21
+ # @rbs return: Hash[Symbol, untyped]
22
+ def to_h
23
+ { type: type, reason: reason }
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::SessionStart
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic id, reason
7
+ attr_reader :id #: String
8
+ attr_reader :reason #: Symbol
9
+
10
+ # @rbs id: String
11
+ # @rbs reason: Symbol
12
+ # @rbs return: void
13
+ def initialize(id, reason)
14
+ @id = id
15
+ @reason = reason
16
+ freeze
17
+ end
18
+
19
+ # @rbs return: Symbol
20
+ def type
21
+ :session_start
22
+ end
23
+
24
+ # @rbs return: Hash[Symbol, untyped]
25
+ def to_h
26
+ { type: type, id: id, reason: reason }
27
+ end
28
+ end