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
@@ -1,28 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Riffer::Rig::Settings::Document
4
- # @rbs @model: String?
5
- # @rbs @reasoning: String?
6
- # @rbs @models: Hash[String, Riffer::Rig::Settings::Pricing]
7
- # @rbs @providers: Hash[String, Hash[String, String]]
8
-
9
- # @dynamic model, reasoning, models, providers
4
+ # @dynamic model, reasoning, models, providers, disabled
10
5
  attr_reader :model #: String?
11
6
  attr_reader :reasoning #: String?
12
7
  attr_reader :models #: Hash[String, Riffer::Rig::Settings::Pricing]
13
8
  attr_reader :providers #: Hash[String, Hash[String, String]]
9
+ attr_reader :disabled #: Array[String]
14
10
 
15
- # @rbs source: untyped
11
+ # @rbs source: Hash[Symbol, untyped]
16
12
  # @rbs return: void
17
13
  def initialize(source)
18
- @model = source['model'].is_a?(String) ? source['model'] : nil
19
- @reasoning = source['reasoning'].is_a?(String) ? source['reasoning'] : nil
20
- entries = source['models'].is_a?(Hash) ? source['models'] : {} #: Hash[String, untyped]
21
- @models = entries.filter_map do |name, entry|
22
- [name, Riffer::Rig::Settings::Pricing.from(entry)] if name.is_a?(String) && entry.is_a?(Hash)
23
- end.to_h
24
- blocks = source['providers'].is_a?(Hash) ? source['providers'] : {} #: Hash[String, untyped]
25
- @providers = blocks.select { |_identifier, fields| fields.is_a?(Hash) }
26
- .transform_values { |fields| fields.select { |_name, value| value.is_a?(String) } }
14
+ @model = source[:model].is_a?(String) ? source[:model] : nil
15
+ @reasoning = source[:reasoning].is_a?(String) ? source[:reasoning] : nil
16
+ @models = hash_or_empty(source[:models]).filter_map do |name, entry|
17
+ [name.to_s, Riffer::Rig::Settings::Pricing.from(entry)] if entry.is_a?(Hash)
18
+ end.to_h.freeze
19
+ @providers = hash_or_empty(source[:providers]).filter_map do |identifier, fields|
20
+ if fields.is_a?(Hash)
21
+ [identifier.to_s, fields.select do |_name, value|
22
+ value.is_a?(String)
23
+ end.transform_keys(&:to_s)]
24
+ end
25
+ end.to_h.freeze
26
+ disabled = hash_or_empty(source[:extensions])[:disabled]
27
+ names = disabled.is_a?(Array) ? disabled.grep(String) : [] #: Array[String]
28
+ @disabled = names.freeze
29
+ freeze
30
+ end
31
+
32
+ private
33
+
34
+ # @rbs value: untyped
35
+ # @rbs return: Hash[Symbol, untyped]
36
+ def hash_or_empty(value)
37
+ value.is_a?(Hash) ? value : {}
27
38
  end
28
39
  end
@@ -4,17 +4,28 @@ class Riffer::Rig::Settings::Pricing
4
4
  # @dynamic input, output, cache_write, cache_read
5
5
  attr_reader :input, :output, :cache_write, :cache_read #: Float
6
6
 
7
- # @rbs entry: Hash[String, untyped]
7
+ # @rbs entry: Hash[Symbol, untyped]
8
8
  # @rbs return: ::Riffer::Rig::Settings::Pricing
9
9
  def self.from(entry)
10
10
  new(
11
- input: entry.fetch('input', 0).to_f,
12
- output: entry.fetch('output', 0).to_f,
13
- cache_write: entry.fetch('cache_write', 0).to_f,
14
- cache_read: entry.fetch('cache_read', 0).to_f
11
+ input: entry.fetch(:input, 0).to_f,
12
+ output: entry.fetch(:output, 0).to_f,
13
+ cache_write: entry.fetch(:cache_write, 0).to_f,
14
+ cache_read: entry.fetch(:cache_read, 0).to_f
15
15
  )
16
16
  end
17
17
 
18
+ # @rbs table: Hash[String, ::Riffer::Rig::Settings::Pricing]
19
+ # @rbs registry: ::Riffer::Config::Pricing
20
+ # @rbs return: void
21
+ def self.register(table, registry)
22
+ table.each do |model, entry|
23
+ registry.set(
24
+ model, input: entry.input, output: entry.output, cache_read: entry.cache_read, cache_write: entry.cache_write
25
+ )
26
+ end
27
+ end
28
+
18
29
  # @rbs input: Float
19
30
  # @rbs output: Float
20
31
  # @rbs cache_write: Float
@@ -8,45 +8,85 @@ module Riffer::Rig::Settings
8
8
 
9
9
  PATH = File.expand_path('~/.riffer/settings.json') #: String
10
10
 
11
- DEFAULT_MODEL = 'anthropic/claude-sonnet-4-6'
12
-
13
11
  REASONING_LEVELS_BY_PROVIDER = {
14
12
  'anthropic' => %w[low medium high xhigh max].freeze,
15
13
  'openai' => %w[low medium high xhigh].freeze,
16
14
  'openrouter' => %w[low medium high xhigh].freeze
17
15
  }.freeze #: Hash[String, Array[String]]
18
16
 
17
+ MODEL_STRING = %r{\A(?<provider>[^/\s]+)/\S+\z} #: Regexp
18
+
19
+ # Upstream candidate: riffer's Repository keeps extension registrations
20
+ # private and lists no identifiers, so the list names riffer's built-ins.
21
+ PROVIDERS = (Riffer::Providers::Repository::REPO.keys - [:mock]).freeze #: Array[Symbol]
22
+
19
23
  # @rbs path: String
20
- # @rbs return: String
21
- def model(path: PATH)
22
- read(path).model || DEFAULT_MODEL
24
+ # @rbs return: Hash[Symbol, untyped]
25
+ def read(path)
26
+ return {} unless File.file?(path)
27
+
28
+ source = JSON.parse(File.read(path), symbolize_names: true)
29
+ source.is_a?(Hash) ? source : {}
30
+ rescue JSON::ParserError
31
+ {}
23
32
  end
24
33
 
25
- # @rbs path: String
34
+ # MCP servers merge by name, whole, so a home server's headers never reach a
35
+ # project server's url.
36
+ # @rbs home: Hash[Symbol, untyped]
37
+ # @rbs project: Hash[Symbol, untyped]
26
38
  # @rbs return: Hash[Symbol, untyped]
27
- def model_options(path: PATH)
28
- provider = provider_for(model(path:))
29
- base_options(provider).merge(reasoning_options(reasoning_for(path:, provider:), provider))
39
+ def merge(home, project)
40
+ merged = deep_merge(home, project)
41
+ home_mcp = home[:mcp]
42
+ project_mcp = project[:mcp]
43
+ if home_mcp.is_a?(Hash) && project_mcp.is_a?(Hash)
44
+ merged = merged.merge(mcp: Riffer::Rig::Mcp.merge(home_mcp, project_mcp))
45
+ end
46
+ disabled = [home, project].flat_map { |scope| Document.new(scope).disabled }.uniq
47
+ return merged if disabled.empty?
48
+
49
+ extensions = merged[:extensions].is_a?(Hash) ? merged[:extensions] : {} #: Hash[Symbol, untyped]
50
+ merged.merge(extensions: extensions.merge(disabled: disabled))
30
51
  end
31
52
 
32
- # @rbs model_string: String
53
+ # @rbs model: String
54
+ # @rbs return: String?
55
+ def provider_for(model)
56
+ model[MODEL_STRING, :provider]
57
+ end
58
+
59
+ # @rbs model: String
33
60
  # @rbs return: String?
34
- def provider_for(model_string)
35
- model_string.split('/', 2).first if model_string.include?('/')
61
+ def rejection(model)
62
+ provider = provider_for(model)
63
+ return if provider && Riffer::Providers::Repository.find(provider)
64
+
65
+ "#{model} is not a provider/name model string; the provider is one of: #{PROVIDERS.join(', ')}"
66
+ end
67
+
68
+ # @rbs model: String
69
+ # @rbs reasoning: String?
70
+ # @rbs return: Hash[Symbol, untyped]
71
+ def model_options(model, reasoning)
72
+ provider = provider_for(model)
73
+ levels = (provider && REASONING_LEVELS_BY_PROVIDER[provider]) || []
74
+ level = reasoning if levels.include?(reasoning)
75
+ base_options(provider).merge(reasoning_options(level, provider))
36
76
  end
37
77
 
38
78
  # @rbs model: String
39
79
  # @rbs path: String
40
- # @rbs return: Pricing?
41
- def pricing_for(model, path: PATH)
42
- read(path).models[model]
80
+ # @rbs return: void
81
+ def store_model(model, path: PATH)
82
+ write(path, read(path).merge(model: model))
43
83
  end
44
84
 
45
85
  # @rbs identifier: String
46
86
  # @rbs path: String
47
87
  # @rbs return: Hash[String, String]
48
88
  def provider_fields(identifier, path: PATH)
49
- read(path).providers[identifier] || {}
89
+ Document.new(read(path)).providers[identifier] || {}
50
90
  end
51
91
 
52
92
  # @rbs identifier: String
@@ -68,17 +108,32 @@ module Riffer::Rig::Settings
68
108
 
69
109
  private
70
110
 
111
+ # @rbs home: Hash[Symbol, untyped]
112
+ # @rbs project: Hash[Symbol, untyped]
113
+ # @rbs return: Hash[Symbol, untyped]
114
+ def deep_merge(home, project)
115
+ home.merge(project) do |_key, earlier, later|
116
+ earlier.is_a?(Hash) && later.is_a?(Hash) ? deep_merge(earlier, later) : later
117
+ end
118
+ end
119
+
71
120
  # @rbs path: String
72
121
  # @rbs &: (Hash[String, Hash[String, String]]) -> Hash[String, Hash[String, String]]
73
122
  # @rbs return: void
74
123
  def update_providers(path)
75
- source = read_source(path)
124
+ source = read(path)
76
125
  current = Document.new(source).providers
77
126
  providers = yield current
78
127
  return if providers == current
79
128
 
129
+ write(path, providers.empty? ? source.except(:providers) : source.merge(providers: providers))
130
+ end
131
+
132
+ # @rbs path: String
133
+ # @rbs document: Hash[Symbol, untyped]
134
+ # @rbs return: void
135
+ def write(path, document)
80
136
  FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
81
- document = providers.empty? ? source.except('providers') : source.merge('providers' => providers)
82
137
  File.write(path, JSON.pretty_generate(document))
83
138
  end
84
139
 
@@ -90,15 +145,6 @@ module Riffer::Rig::Settings
90
145
  {}
91
146
  end
92
147
 
93
- # @rbs path: String
94
- # @rbs provider: String?
95
- # @rbs return: String?
96
- def reasoning_for(path: PATH, provider: nil)
97
- level = read(path).reasoning
98
- valid_levels = (provider && REASONING_LEVELS_BY_PROVIDER[provider]) || []
99
- valid_levels.include?(level) ? level : nil
100
- end
101
-
102
148
  # @rbs level: String?
103
149
  # @rbs provider: String?
104
150
  # @rbs return: Hash[Symbol, untyped]
@@ -114,20 +160,4 @@ module Riffer::Rig::Settings
114
160
  {}
115
161
  end
116
162
  end
117
-
118
- # @rbs path: String
119
- # @rbs return: Document
120
- def read(path)
121
- Document.new(read_source(path))
122
- end
123
-
124
- # @rbs path: String
125
- # @rbs return: Hash[String, untyped]
126
- def read_source(path)
127
- return {} unless File.file?(path)
128
-
129
- JSON.parse(File.read(path))
130
- rescue JSON::ParserError
131
- {}
132
- end
133
163
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Upstream candidate: riffer's Skills::Config takes a single backend, so the
4
+ # sources every extension registers are merged into one here.
5
+ class Riffer::Rig::Skills::Sources < Riffer::Skills::Backend
6
+ # @rbs @backends: Array[Riffer::Skills::Backend]
7
+
8
+ # @rbs backends: Array[Riffer::Skills::Backend]
9
+ # @rbs return: void
10
+ def initialize(backends)
11
+ super()
12
+ @backends = backends
13
+ end
14
+
15
+ # @rbs return: Array[Riffer::Skills::Frontmatter]
16
+ def list_skills
17
+ @backends.flat_map(&:list_skills).uniq(&:name)
18
+ end
19
+
20
+ # @rbs name: String
21
+ # @rbs return: String
22
+ def read_skill(name)
23
+ backend = @backends.find { |source| source.list_skills.any? { |skill| skill.name == name } }
24
+ raise Riffer::ArgumentError, "Skill not found: '#{name}'" unless backend
25
+
26
+ backend.read_skill(name)
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Skills
4
+ extend self
5
+
6
+ # @rbs cwd: String
7
+ # @rbs return: Array[String]
8
+ def directories(cwd)
9
+ project = Riffer::Rig::Directories.up_to_repo_root(cwd).map { |dir| File.join(dir, '.agents', 'skills') }
10
+ [*project, File.join(Dir.home, '.agents', 'skills')]
11
+ end
12
+ end
@@ -1,16 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Riffer::Rig::Events::Event
4
- # @rbs return: Symbol
5
- def type
6
- raise NotImplementedError, "#{self.class.name} must define type"
7
- end
8
-
9
- # @rbs return: Hash[Symbol, untyped]
10
- def to_h
11
- raise NotImplementedError, "#{self.class.name} must define to_h"
12
- end
13
-
3
+ # @rbs module-self Riffer::Rig::Support::_ToH
4
+ module Riffer::Rig::Support::Equatable
14
5
  # @rbs other: untyped
15
6
  # @rbs return: bool
16
7
  def ==(other)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Riffer::Rig::UI::Animator
3
+ class Riffer::Rig::Terminal::Animator
4
4
  REVEAL_FRAME_SECONDS = 0.05 #: Float
5
5
 
6
6
  SPINNER_FRAME_SECONDS = 0.12 #: Float
@@ -22,16 +22,16 @@ class Riffer::Rig::UI::Animator
22
22
  REASONING_TICK_RANGE = (1..5) #: Range[Integer]
23
23
 
24
24
  # @rbs @io: IO
25
- # @rbs @theme: Riffer::Rig::UI::Theme
25
+ # @rbs @theme: Riffer::Rig::Terminal::Theme
26
26
  # @rbs @thread: Thread?
27
27
  # @rbs @mode: Symbol
28
28
  # @rbs @phrase: String?
29
29
  # @rbs @stop: bool
30
30
 
31
31
  # @rbs io: IO
32
- # @rbs theme: Riffer::Rig::UI::Theme
32
+ # @rbs theme: Riffer::Rig::Terminal::Theme
33
33
  # @rbs return: void
34
- def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io))
34
+ def initialize(io: $stdout, theme: Riffer::Rig::Terminal::Theme.for(io))
35
35
  @io = io
36
36
  @theme = theme
37
37
  @thread = nil
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::Rig::UI::Banner
3
+ module Riffer::Rig::Terminal::Banner
4
4
  extend self
5
5
 
6
6
  WORDMARK = [
@@ -13,57 +13,53 @@ module Riffer::Rig::UI::Banner
13
13
  ].freeze #: Array[String]
14
14
 
15
15
  ROW_COLOURS = [
16
- Riffer::Rig::UI::Palette::PINK,
17
- Riffer::Rig::UI::Palette::MAGENTA,
18
- Riffer::Rig::UI::Palette::PURPLE,
19
- Riffer::Rig::UI::Palette::BLUE,
20
- Riffer::Rig::UI::Palette::CYAN,
21
- Riffer::Rig::UI::Palette::CYAN
16
+ Riffer::Rig::Terminal::Palette::PINK,
17
+ Riffer::Rig::Terminal::Palette::MAGENTA,
18
+ Riffer::Rig::Terminal::Palette::PURPLE,
19
+ Riffer::Rig::Terminal::Palette::BLUE,
20
+ Riffer::Rig::Terminal::Palette::CYAN,
21
+ Riffer::Rig::Terminal::Palette::CYAN
22
22
  ].freeze #: Array[Array[Integer]]
23
23
 
24
24
  INFO_LABEL_WIDTH = 8 #: Integer
25
25
 
26
26
  INDENT = ' ' #: String
27
27
 
28
- # @rbs theme: Riffer::Rig::UI::Theme
28
+ # @rbs theme: Riffer::Rig::Terminal::Theme
29
29
  # @rbs model: String
30
30
  # @rbs cwd: String
31
- # @rbs context: String
32
31
  # @rbs skills: String
33
32
  # @rbs version: String
34
33
  # @rbs return: String
35
- def call(theme, model:, cwd:, context:, skills:, version:)
36
- lines(theme, model: model, cwd: cwd, context: context, skills: skills, version: version).join("\n")
34
+ def call(theme, model:, cwd:, skills:, version:)
35
+ lines(theme, model: model, cwd: cwd, skills: skills, version: version).join("\n")
37
36
  end
38
37
 
39
- # @rbs theme: Riffer::Rig::UI::Theme
38
+ # @rbs theme: Riffer::Rig::Terminal::Theme
40
39
  # @rbs model: String
41
40
  # @rbs cwd: String
42
- # @rbs context: String
43
41
  # @rbs skills: String
44
42
  # @rbs version: String
45
43
  # @rbs return: Array[String]
46
- def lines(theme, model:, cwd:, context:, skills:, version:)
47
- [''] + art(theme) + [''] +
48
- info(theme, model: model, cwd: cwd, context: context, skills: skills, version: version) + ['']
44
+ def lines(theme, model:, cwd:, skills:, version:)
45
+ [''] + art(theme) + [''] + info(theme, model: model, cwd: cwd, skills: skills, version: version) + ['']
49
46
  end
50
47
 
51
- # @rbs theme: Riffer::Rig::UI::Theme
48
+ # @rbs theme: Riffer::Rig::Terminal::Theme
52
49
  # @rbs return: Array[String]
53
50
  def art(theme)
54
51
  art = WORDMARK.each_index.map { |i| "#{INDENT}#{theme.paint(WORDMARK[i], ROW_COLOURS[i])}" }
55
52
  art + ["#{INDENT}#{theme.cyan("♪ let's riff ♪")}"]
56
53
  end
57
54
 
58
- # @rbs theme: Riffer::Rig::UI::Theme
55
+ # @rbs theme: Riffer::Rig::Terminal::Theme
59
56
  # @rbs model: String
60
57
  # @rbs cwd: String
61
- # @rbs context: String
62
58
  # @rbs skills: String
63
59
  # @rbs version: String
64
60
  # @rbs return: Array[String]
65
- def info(theme, model:, cwd:, context:, skills:, version:)
66
- rows = { model: model, cwd: cwd, context: context, skills: skills, version: version }.map do |label, value|
61
+ def info(theme, model:, cwd:, skills:, version:)
62
+ rows = { model: model, cwd: cwd, skills: skills, version: version }.map do |label, value|
67
63
  " #{theme.magenta('▸')} #{theme.grey(label.to_s.ljust(INFO_LABEL_WIDTH))}#{value}"
68
64
  end
69
65
  rows + ['', " #{theme.dim('type /exit to quit')}"]
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Riffer::Rig::UI::Cursor
3
+ class Riffer::Rig::Terminal::Cursor
4
4
  HIDE = "\e[?25l" #: String
5
5
 
6
6
  SHOW = "\e[?25h" #: String
7
7
 
8
8
  # @rbs @io: IO
9
- # @rbs @theme: Riffer::Rig::UI::Theme
9
+ # @rbs @theme: Riffer::Rig::Terminal::Theme
10
10
 
11
11
  # @rbs io: IO
12
- # @rbs theme: Riffer::Rig::UI::Theme
12
+ # @rbs theme: Riffer::Rig::Terminal::Theme
13
13
  # @rbs return: void
14
- def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io))
14
+ def initialize(io: $stdout, theme: Riffer::Rig::Terminal::Theme.for(io))
15
15
  @io = io
16
16
  @theme = theme
17
17
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ class Riffer::Rig::Terminal::Host
6
+ CAPABILITIES = Set[:ask, :confirm, :notify, :progress].freeze #: Set[Symbol]
7
+
8
+ # @rbs @input: IO
9
+ # @rbs @renderer: Riffer::Rig::Terminal::Renderer
10
+ # @rbs @animator: Riffer::Rig::Terminal::Animator
11
+
12
+ # @rbs input: IO
13
+ # @rbs renderer: Riffer::Rig::Terminal::Renderer
14
+ # @rbs animator: Riffer::Rig::Terminal::Animator
15
+ # @rbs return: void
16
+ def initialize(input:, renderer:, animator:)
17
+ @input = input
18
+ @renderer = renderer
19
+ @animator = animator
20
+ end
21
+
22
+ # @rbs return: Set[Symbol]
23
+ def capabilities
24
+ CAPABILITIES
25
+ end
26
+
27
+ # @rbs question: String?
28
+ # @rbs options: Array[String]?
29
+ # @rbs secret: bool
30
+ # @rbs return: String?
31
+ def ask(question = nil, options: nil, secret: false)
32
+ @animator.stop
33
+ @renderer.question(question, options, secret)
34
+ answer = secret ? read_secret : @input.gets
35
+ return nil if answer.nil?
36
+
37
+ choice(answer.strip, options)
38
+ end
39
+
40
+ # @rbs question: String?
41
+ # @rbs return: bool
42
+ def confirm(question = nil)
43
+ ask("#{question} [y/N]").to_s.downcase.start_with?('y')
44
+ end
45
+
46
+ # @rbs message: String?
47
+ # @rbs level: Symbol
48
+ # @rbs return: void
49
+ def notify(message = nil, level: :info)
50
+ @animator.stop
51
+ @renderer.notify(message.to_s, level)
52
+ end
53
+
54
+ # @rbs label: String?
55
+ # @rbs &block: ? () -> void
56
+ # @rbs return: void
57
+ def progress(label = nil, &block)
58
+ @renderer.notice(label) if label
59
+ @animator.start
60
+ block&.call
61
+ ensure
62
+ @animator.stop
63
+ end
64
+
65
+ private
66
+
67
+ # @rbs return: String?
68
+ def read_secret
69
+ return @input.gets unless @input.tty?
70
+
71
+ answer = @input.noecho(&:gets)
72
+ # noecho swallows the newline the user typed along with the secret.
73
+ @renderer.newline
74
+ answer
75
+ end
76
+
77
+ # @rbs answer: String
78
+ # @rbs options: Array[String]?
79
+ # @rbs return: String
80
+ def choice(answer, options)
81
+ return answer unless options && answer.match?(/\A[1-9]\d*\z/)
82
+
83
+ options[answer.to_i - 1] || answer
84
+ end
85
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::Rig::UI::Palette
3
+ module Riffer::Rig::Terminal::Palette
4
4
  PINK = [255, 106, 193].freeze #: Array[Integer]
5
5
 
6
6
  MAGENTA = [192, 76, 253].freeze #: Array[Integer]