riffer-rig 0.6.0 → 0.8.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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +38 -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 +240 -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 +29 -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/terminal/renderer.rb +313 -0
  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/wrapper.rb +103 -0
  66. data/lib/riffer/rig/terminal.rb +207 -0
  67. data/lib/riffer/rig/tools/bash.rb +6 -4
  68. data/lib/riffer/rig/tools/edit.rb +3 -2
  69. data/lib/riffer/rig/tools/read.rb +3 -2
  70. data/lib/riffer/rig/tools/write.rb +3 -2
  71. data/lib/riffer/rig/trust.rb +37 -0
  72. data/lib/riffer/rig/version.rb +1 -1
  73. data/lib/riffer/rig.rb +8 -8
  74. data/sig/generated/riffer/rig/bundled/agents_md.rbs +5 -0
  75. data/sig/generated/riffer/rig/bundled/bash.rbs +5 -0
  76. data/sig/generated/riffer/rig/bundled/edit.rbs +5 -0
  77. data/sig/generated/riffer/rig/bundled/mcp.rbs +5 -0
  78. data/sig/generated/riffer/rig/bundled/read.rbs +5 -0
  79. data/sig/generated/riffer/rig/bundled/skills.rbs +5 -0
  80. data/sig/generated/riffer/rig/bundled/write.rbs +5 -0
  81. data/sig/generated/riffer/rig/bundled.rbs +5 -0
  82. data/sig/generated/riffer/rig/cli/flags.rbs +40 -0
  83. data/sig/generated/riffer/rig/cli.rbs +30 -35
  84. data/sig/generated/riffer/rig/command/context.rbs +50 -0
  85. data/sig/generated/riffer/rig/command.rbs +23 -0
  86. data/sig/generated/riffer/rig/commands/model.rbs +27 -0
  87. data/sig/generated/riffer/rig/commands/skill.rbs +12 -0
  88. data/sig/generated/riffer/rig/commands.rbs +4 -0
  89. data/sig/generated/riffer/rig/credentials.rbs +4 -4
  90. data/sig/generated/riffer/rig/directories.rbs +11 -0
  91. data/sig/generated/riffer/rig/env/invalid.rbs +10 -0
  92. data/sig/generated/riffer/rig/env.rbs +7 -1
  93. data/sig/generated/riffer/rig/events/after_response.rbs +18 -0
  94. data/sig/generated/riffer/rig/events/after_tool_call.rbs +24 -0
  95. data/sig/generated/riffer/rig/events/before_prompt.rbs +18 -0
  96. data/sig/generated/riffer/rig/events/before_request.rbs +18 -0
  97. data/sig/generated/riffer/rig/events/before_tool_call.rbs +21 -0
  98. data/sig/generated/riffer/rig/events/command_output.rbs +3 -1
  99. data/sig/generated/riffer/rig/events/notify.rbs +3 -1
  100. data/sig/generated/riffer/rig/events/session_end.rbs +3 -1
  101. data/sig/generated/riffer/rig/events/session_start.rbs +3 -1
  102. data/sig/generated/riffer/rig/events/skill_activated.rbs +3 -1
  103. data/sig/generated/riffer/rig/events/turn_end.rbs +3 -1
  104. data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
  105. data/sig/generated/riffer/rig/extension.rbs +14 -1
  106. data/sig/generated/riffer/rig/hosts/mirror.rbs +9 -9
  107. data/sig/generated/riffer/rig/hosts/null.rbs +1 -1
  108. data/sig/generated/riffer/rig/loader.rbs +113 -0
  109. data/sig/generated/riffer/rig/mcp/declaration.rbs +18 -0
  110. data/sig/generated/riffer/rig/mcp/server.rbs +13 -0
  111. data/sig/generated/riffer/rig/mcp.rbs +8 -0
  112. data/sig/generated/riffer/rig/prompts/agents_md.rbs +19 -0
  113. data/sig/generated/riffer/rig/prompts.rbs +4 -0
  114. data/sig/generated/riffer/rig/registrar.rbs +78 -4
  115. data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
  116. data/sig/generated/riffer/rig/runtime/hooks.rbs +49 -0
  117. data/sig/generated/riffer/rig/runtime/request_guardrail.rbs +16 -0
  118. data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +23 -0
  119. data/sig/generated/riffer/rig/runtime.rbs +225 -22
  120. data/sig/generated/riffer/rig/settings/document.rbs +13 -11
  121. data/sig/generated/riffer/rig/settings/pricing.rbs +7 -2
  122. data/sig/generated/riffer/rig/settings.rbs +36 -23
  123. data/sig/generated/riffer/rig/skills/sources.rbs +18 -0
  124. data/sig/generated/riffer/rig/skills.rbs +7 -0
  125. data/sig/generated/riffer/rig/support/equatable.rbs +15 -0
  126. data/sig/generated/riffer/rig/{ui → terminal}/animator.rbs +5 -5
  127. data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
  128. data/sig/generated/riffer/rig/{ui → terminal}/cursor.rbs +5 -5
  129. data/sig/generated/riffer/rig/terminal/host.rbs +50 -0
  130. data/sig/generated/riffer/rig/{ui → terminal}/palette.rbs +2 -2
  131. data/sig/generated/riffer/rig/terminal/renderer.rbs +166 -0
  132. data/sig/generated/riffer/rig/{ui → terminal}/smoother.rbs +5 -5
  133. data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -5
  134. data/sig/generated/riffer/rig/terminal/wrapper.rbs +43 -0
  135. data/sig/generated/riffer/rig/terminal.rbs +85 -0
  136. data/sig/generated/riffer/rig/tools/bash.rbs +2 -1
  137. data/sig/generated/riffer/rig/trust.rbs +20 -0
  138. data/sig/generated/riffer/rig.rbs +4 -0
  139. data/sig/manual/riffer/rig/commands/model.rbs +6 -0
  140. data/sig/manual/riffer/rig/commands/skill.rbs +6 -0
  141. data/sig/manual/riffer/rig/directories.rbs +6 -0
  142. data/sig/manual/riffer/rig/events.rbs +6 -0
  143. data/sig/manual/riffer/rig/hosts.rbs +7 -0
  144. data/sig/manual/riffer/rig/mcp.rbs +11 -0
  145. data/sig/manual/riffer/rig/prompts/agents_md.rbs +6 -0
  146. data/sig/manual/riffer/rig/{ui/banner.rbs → skills.rbs} +2 -2
  147. data/sig/manual/riffer/rig/support.rbs +8 -0
  148. data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
  149. data/sig/manual/riffer/rig/trust.rbs +6 -0
  150. metadata +145 -27
  151. data/lib/riffer/rig/coding_agent.rb +0 -53
  152. data/lib/riffer/rig/hosts/base.rb +0 -36
  153. data/lib/riffer/rig/repl.rb +0 -188
  154. data/lib/riffer/rig/token_tally.rb +0 -54
  155. data/lib/riffer/rig/ui/renderer.rb +0 -191
  156. data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
  157. data/sig/generated/riffer/rig/events/event.rbs +0 -20
  158. data/sig/generated/riffer/rig/hosts/base.rbs +0 -26
  159. data/sig/generated/riffer/rig/repl.rbs +0 -69
  160. data/sig/generated/riffer/rig/token_tally.rbs +0 -42
  161. data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
  162. data/sig/generated/riffer/rig/ui/renderer.rbs +0 -94
  163. data/sig/manual/riffer/rig/ui.rbs +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b5a8d6de921c08c1ddb5caa12c5e22e0b3a68f7d26160b6fc732b647209b963
4
- data.tar.gz: a37ce48ed7e46d9bca9a00db9a46411ecc0187550d58627879e9d97d36789650
3
+ metadata.gz: 9d560ab0c2be62a87e8468ba302f443985cd5d92d4872de2073c5c77b672d83f
4
+ data.tar.gz: b75fe6a3d1a7f0c718405f619f2f56c20fe85282ddb94aff9c2beb853344c119
5
5
  SHA512:
6
- metadata.gz: b2b6f88978aa64314fb2b420047f97332090c3f8f699caa01564cd1ebf78dd5d862a22dca60e8e0734db647e2506aafbc5bd4de7a1ee1d800ee6a6edf1688b74
7
- data.tar.gz: 3d6a59d2a2fa892430ddec4682c29fcc934d8568a3827ecf99546ea064b9ec246a1c5dbd5e96a0848328017408d17be91944bd2e84fd1eaf65dcc7707cf74389
6
+ metadata.gz: a2ca46037680a034a31b977101a9c5b442fc21f56842cb3055f2163493d42eef2d92ccdbc53aa9bf5fd2749af6fcfd52fb6d74aecbea0da907a5bac3ecee7498
7
+ data.tar.gz: 4e531d3fee7fa1107e7d6a7c5d1fa064c4bbdf9e46763353332d4b9d9b57b6a37436d54326fe06e21248c147d99e15ddaac4a18b6ee8ffd34be4e29d733444ae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0](https://github.com/bottrall/riffer-rig/compare/v0.7.0...v0.8.0) (2026-10-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * discover rig.rb files with project trust and optional gem autoload ([#208](https://github.com/bottrall/riffer-rig/issues/208)) ([6ed818f](https://github.com/bottrall/riffer-rig/commit/6ed818f5b5d382b3a9c7d48ce667892b10190985))
9
+ * truncate tool output and fit lines to the terminal width ([#209](https://github.com/bottrall/riffer-rig/issues/209)) ([3765de1](https://github.com/bottrall/riffer-rig/commit/3765de1f4392d903939c1458e843c2ca1e70112e))
10
+
11
+ ## [0.7.0](https://github.com/bottrall/riffer-rig/compare/v0.6.0...v0.7.0) (2026-09-30)
12
+
13
+
14
+ ### ⚠ BREAKING CHANGES
15
+
16
+ * Riffer::Rig::CodingAgent, REPL, TokenTally and the UI namespace are removed, and the old CLI is replaced by the dispatcher. Embed a Runtime through Riffer::Rig::Loader.runtime instead of CodingAgent; the terminal UI classes now live under Riffer::Rig::Terminal. The bash tool now errors when its context has no :cwd.
17
+ * DEFAULT_MODEL is gone; with no model the Loader asks the host to onboard and the null host's refusal is a configuration error. The old Settings module API changes shape.
18
+ * Riffer::Rig::Hosts::Base and Riffer::Rig::Events::Event are removed. A host is any object implementing the five host methods (Riffer::Rig::Hosts::_Host); drop the superclass. A custom event drops the superclass and super(), and includes Riffer::Rig::Events::Value for equality.
19
+
20
+ ### Features
21
+
22
+ * /model switches the model for the current Runtime ([#193](https://github.com/bottrall/riffer-rig/issues/193)) ([f888972](https://github.com/bottrall/riffer-rig/commit/f8889722f62499f97ee34158826e9d00dd3d92ef))
23
+ * bundle Agent Skills with /skill: commands over the skills seam ([#201](https://github.com/bottrall/riffer-rig/issues/201)) ([e14844d](https://github.com/bottrall/riffer-rig/commit/e14844d9c7c22dd85b3d4158892ec53bae736c6b))
24
+ * bundle AGENTS.md as the agents_md prompt section ([#198](https://github.com/bottrall/riffer-rig/issues/198)) ([e2e9141](https://github.com/bottrall/riffer-rig/commit/e2e914195f7c15c75ee2ae6bb55d772d754a2f37))
25
+ * bundle read, write, edit and bash as extensions ([#195](https://github.com/bottrall/riffer-rig/issues/195)) ([d5c9ffe](https://github.com/bottrall/riffer-rig/commit/d5c9ffe692abf5e5cab16af0686f96b040a6cbaa))
26
+ * bundle the MCP client over the mcp seam ([#202](https://github.com/bottrall/riffer-rig/issues/202)) ([c2fc42f](https://github.com/bottrall/riffer-rig/commit/c2fc42ff873af8205f3a8042069a3dd5f27420e6))
27
+ * isolate extension load errors and check requires ([#192](https://github.com/bottrall/riffer-rig/issues/192)) ([0aac3bf](https://github.com/bottrall/riffer-rig/commit/0aac3bf7d3b6ada84a4f38a40abc181f7d3f57c7)), closes [#114](https://github.com/bottrall/riffer-rig/issues/114)
28
+ * lifecycle, vetoable and observe hooks plus the stream passthrough ([#194](https://github.com/bottrall/riffer-rig/issues/194)) ([206d349](https://github.com/bottrall/riffer-rig/commit/206d349a4967081961372d1692b0946663f32a10))
29
+ * Loader builds a Runtime from settings, credentials and the bundle ([#205](https://github.com/bottrall/riffer-rig/issues/205)) ([01170fe](https://github.com/bottrall/riffer-rig/commit/01170fee282deae87aba1b8797cfe5cedc892deb)), closes [#123](https://github.com/bottrall/riffer-rig/issues/123)
30
+ * namespaced extension settings ([#196](https://github.com/bottrall/riffer-rig/issues/196)) ([dc16a84](https://github.com/bottrall/riffer-rig/commit/dc16a8424ea00ea0b39323db97309c9c94224443)), closes [#117](https://github.com/bottrall/riffer-rig/issues/117)
31
+ * rebuild the registrar in place ([#199](https://github.com/bottrall/riffer-rig/issues/199)) ([f4b742b](https://github.com/bottrall/riffer-rig/commit/f4b742b3f45a28f3c69d75a355c3a2880938de1b))
32
+ * runtime commands with run_command and the command context ([#190](https://github.com/bottrall/riffer-rig/issues/190)) ([159d007](https://github.com/bottrall/riffer-rig/commit/159d0077b34773fadb2cc6f5f66622b12e4b7a0e)), closes [#111](https://github.com/bottrall/riffer-rig/issues/111)
33
+ * snapshot and restore a Runtime ([#200](https://github.com/bottrall/riffer-rig/issues/200)) ([aaad579](https://github.com/bottrall/riffer-rig/commit/aaad5797e5e626b6f560b96a5f4cf1bf5bc379a3))
34
+ * Terminal drives a Loader-built Runtime ([#206](https://github.com/bottrall/riffer-rig/issues/206)) ([f805db6](https://github.com/bottrall/riffer-rig/commit/f805db625e69f90c2dd343bbb7ca016aba7cecf0))
35
+ * token tally and pricing behind the Runtime ([#189](https://github.com/bottrall/riffer-rig/issues/189)) ([1ccde0d](https://github.com/bottrall/riffer-rig/commit/1ccde0d1670feaa2a9b9c957bbe79fa80ca47d4f))
36
+
37
+
38
+ ### Bug Fixes
39
+
40
+ * run steep check without attaching to a language server ([#197](https://github.com/bottrall/riffer-rig/issues/197)) ([4f0b92e](https://github.com/bottrall/riffer-rig/commit/4f0b92eeafcb4e4fb6d054c29d707fcfb39766d5))
41
+
42
+
43
+ ### Code Refactoring
44
+
45
+ * interfaces over inheritance, POROs over hashes, new cops ([#204](https://github.com/bottrall/riffer-rig/issues/204)) ([7fda3e3](https://github.com/bottrall/riffer-rig/commit/7fda3e3ec79ea97479008127f0c153ce84b888d5))
46
+
3
47
  ## [0.6.0](https://github.com/bottrall/riffer-rig/compare/v0.5.0...v0.6.0) (2026-09-27)
4
48
 
5
49
 
data/README.md CHANGED
@@ -26,9 +26,36 @@ Run the agent from any project directory:
26
26
  riffer
27
27
  ```
28
28
 
29
+ The first run asks which model to use, as `provider/name` (for example `anthropic/claude-sonnet-4-6`), and saves the answer as `model` in `~/.riffer/settings.json`; it then asks for any missing credential ([Authentication](#authentication)). After that, `riffer` opens straight into the prompt. [Getting started](docs/GETTING_STARTED.md) walks through a first session.
30
+
31
+ The model is the first of these that is set: `--model`, `RIFFER_MODEL`, `model` in `<cwd>/.riffer/settings.json`, then in `~/.riffer/settings.json` ([Configuration](docs/CONFIGURATION.md#model)).
32
+
33
+ ### Flags
34
+
35
+ | Flag | Effect |
36
+ | ----------------------- | -------------------------------------------------------------------------- |
37
+ | `--model provider/name` | the model for this session, over `RIFFER_MODEL` and the settings |
38
+ | `--no-extensions` | skips the [`rig.rb` files](docs/EXTENSIONS.md#the-rigrb-files) and gem autoload; the bundle still loads |
39
+ | `--no-skills` | leaves out [Agent Skills](docs/SKILLS.md) |
40
+ | `--no-agents-md` | leaves out the [AGENTS.md](docs/INSTRUCTIONS.md#agentsmd) instructions |
41
+ | `--tools read,bash` | only these tools, by identifier ([Tools](docs/TOOLS.md)) |
42
+ | `--max-steps N` | stops a turn after `N` model calls |
43
+ | `-h`, `--help` | prints the usage |
44
+
45
+ `riffer -p` (one prompt, no terminal) and `riffer acp` (an editor's agent over stdio) are reserved: for now each prints the usage and exits with status 2.
46
+
47
+ ### In the session
48
+
49
+ Type a prompt and press Enter; the reply streams in, with each tool call and the first line of its result, and every turn ends with a token line (`↑in · ↓out · session N tok`, plus the session cost when the model is [priced](docs/CONFIGURATION.md#models)).
50
+
51
+ - Ctrl-C during a turn cancels it and returns to the prompt. Ctrl-C at the prompt asks for a second one, which exits.
52
+ - `/exit` or `/quit` ends the session, as does Ctrl-D.
53
+ - `/model provider/name` switches the model ([below](#switching-the-model)), and `/skill:<name> [text]` runs a skill ([Skills](docs/SKILLS.md)).
54
+ - Any other `/name args` runs the Runtime command of that name, such as one an extension registers ([Extensions](docs/EXTENSIONS.md)); an unknown name is reported and nothing is sent to the model.
55
+
29
56
  ### Authentication
30
57
 
31
- `riffer-rig` talks to the provider named by the model's prefix — `anthropic/claude-sonnet-4-6`, the default, means Anthropic. Provide that provider's credentials in either of two ways:
58
+ `riffer-rig` talks to the provider named by the model's prefix — `anthropic/claude-sonnet-4-6` means Anthropic. Provide that provider's credentials in either of two ways:
32
59
 
33
60
  - Set the provider's environment variables, or
34
61
  - Run `riffer` and paste each missing value when prompted on first launch. Secrets are saved to `~/.riffer/auth.json` (file permissions `600`); plain values such as an endpoint or region are saved to the `providers` block in `~/.riffer/settings.json`.
@@ -55,10 +82,16 @@ Each value resolves on its own: environment variable first, then the stored valu
55
82
 
56
83
  The flat `{"anthropic": "sk-…"}` shape earlier versions wrote is no longer read; paste the key again when prompted. [Providers](docs/PROVIDERS.md) has the full format.
57
84
 
85
+ ### Switching the model
86
+
87
+ `/model provider/name` switches the model for the current session only, keeping the conversation so far — `/model openai/gpt-5`, say. The provider prefix is required: a bare name such as `/model sonnet` is rejected with the list of providers. `/model` alone shows the model in use. The switch is refused when the session has no credentials for the new provider, and it never changes `settings.json`; set `model` there to change the model new sessions start with ([Configuration](docs/CONFIGURATION.md#model)).
88
+
58
89
  ### Configuration
59
90
 
60
- - `AGENTS.md` — if present, an `AGENTS.md` in the current working directory and/or a global `~/.riffer/AGENTS.md` is loaded as additional context.
61
- - `~/.riffer/settings.json` — optional user settings. Every key is optional:
91
+ - `AGENTS.md` — `~/.riffer/AGENTS.md` and an `AGENTS.md` in the current working directory or any directory above it, whichever exist, are re-read every turn as instructions that take precedence over the default norms ([Instructions](docs/INSTRUCTIONS.md#agentsmd)).
92
+ - Skills — Agent Skills in `.agents/skills/` from the current working directory up to the repository root, and in `~/.agents/skills/`, are offered to the model, and each can be run with `/skill:<name>` ([Skills](docs/SKILLS.md)).
93
+ - `RIFFER_MODEL` — the model for this run as `provider/name`, winning over the `model` setting. A bare name such as `sonnet` is rejected with the list of providers.
94
+ - `~/.riffer/settings.json` — optional user settings, and `<cwd>/.riffer/settings.json` for one project, merged key by key with the project winning. Every key is optional:
62
95
 
63
96
  ```json
64
97
  {
@@ -75,7 +108,7 @@ The flat `{"anthropic": "sk-…"}` shape earlier versions wrote is no longer rea
75
108
  }
76
109
  ```
77
110
 
78
- `models` holds pricing in USD per million tokens; a model without an entry shows no cost. `reasoning` is translated to the provider's own parameter — Anthropic accepts `low`, `medium`, `high`, `xhigh` and `max`; OpenAI and OpenRouter accept `low`, `medium`, `high` and `xhigh`. Omitting it, or supplying an unrecognised value, leaves the model's default reasoning behaviour unchanged.
111
+ `model` is the model new sessions start with; a host built on the [Loader](docs/EMBEDDING.md#building-a-runtime-with-the-loader) asks for one when none is set and writes the answer here. `extensions.disabled` (for example `["mcp"]`) leaves bundled extensions out. `models` prices each model in USD per million tokens; [Configuration](docs/CONFIGURATION.md) has every key. `reasoning` is translated to the provider's own parameter — Anthropic accepts `low`, `medium`, `high`, `xhigh` and `max`; OpenAI and OpenRouter accept `low`, `medium`, `high` and `xhigh`. Omitting it, or supplying an unrecognised value, leaves the model's default reasoning behaviour unchanged.
79
112
 
80
113
  ## Development
81
114
 
@@ -91,6 +124,7 @@ Every project chore is a script in `bin/`. The Rakefile behind them is an implem
91
124
  | `bin/rbs-watch` | Regenerate `sig/generated` whenever `lib/` changes |
92
125
  | `bin/ci` | Run everything CI runs, serially. Use before pushing |
93
126
  | `bin/build` | Build the gem into `pkg/`; the publish workflow runs this before `gem push` |
127
+ | `bin/docs` | Build the docs site and API reference into `_site/`; `bin/docs serve` serves it at http://localhost:8000 |
94
128
  | `bin/plans` | Serve the building plans in `plans/` at http://localhost:8001 |
95
129
 
96
130
  ## Releasing
data/exe/riffer CHANGED
@@ -5,4 +5,4 @@ require "bundler/setup" if File.exist?(File.expand_path("../Gemfile", __dir__))
5
5
 
6
6
  require_relative "../lib/riffer/rig"
7
7
 
8
- exit(Riffer::Rig::CLI.start)
8
+ exit(Riffer::Rig::CLI.start(ARGV))
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Bundled
4
+ AgentsMd = Riffer::Rig::Extension.new('agents_md') do |rig|
5
+ rig.prompt(:agents_md) { |runtime| Riffer::Rig::Prompts::AgentsMd.section(runtime.cwd) }
6
+ end #: Riffer::Rig::Extension
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Bundled
4
+ Bash = Riffer::Rig::Extension.new('bash') { |rig| rig.tool Riffer::Rig::Tools::Bash } #: Riffer::Rig::Extension
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Bundled
4
+ Edit = Riffer::Rig::Extension.new('edit') { |rig| rig.tool Riffer::Rig::Tools::Edit } #: Riffer::Rig::Extension
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Bundled
4
+ Mcp = Riffer::Rig::Extension.new('mcp') do |rig|
5
+ rig.settings[:servers].to_h.each do |name, server|
6
+ rig.mcp(name.to_s, url: server.fetch(:url), headers: server[:headers].to_h.transform_keys(&:to_s))
7
+ end
8
+ end #: Riffer::Rig::Extension
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Bundled
4
+ Read = Riffer::Rig::Extension.new('read') { |rig| rig.tool Riffer::Rig::Tools::Read } #: Riffer::Rig::Extension
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Bundled
4
+ Skills = Riffer::Rig::Extension.new('skills') do |rig|
5
+ rig.skills do |runtime|
6
+ Riffer::Skills::FilesystemBackend.new(*Riffer::Rig::Skills.directories(runtime.cwd))
7
+ end
8
+ rig.prompt(:skills) { nil }
9
+ end #: Riffer::Rig::Extension
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Bundled
4
+ Write = Riffer::Rig::Extension.new('write') { |rig| rig.tool Riffer::Rig::Tools::Write } #: Riffer::Rig::Extension
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Bundled
4
+ BY_NAME = {
5
+ read: Read, write: Write, edit: Edit, bash: Bash, agents_md: AgentsMd, skills: Skills, mcp: Mcp
6
+ }.freeze #: Hash[Symbol, Riffer::Rig::Extension]
7
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ class Riffer::Rig::CLI::Flags
6
+ BANNER = <<~TEXT.chomp #: String
7
+ Usage: riffer [options]
8
+ riffer -p [prompt] (not available yet)
9
+ riffer acp (not available yet)
10
+ TEXT
11
+
12
+ # @dynamic model, extensions, skills, agents_md, tools, max_steps, help
13
+ attr_reader :model #: String?
14
+ attr_reader :extensions, :skills, :agents_md, :help #: bool
15
+ attr_reader :tools #: Array[String]?
16
+ attr_reader :max_steps #: Integer?
17
+
18
+ # @rbs argv: Array[String]
19
+ # @rbs return: Riffer::Rig::CLI::Flags | String
20
+ def self.parse(argv)
21
+ values = {} #: Hash[Symbol, untyped]
22
+ rest = parser.parse(argv, into: values)
23
+ return "unexpected argument: #{rest.join(' ')}" unless rest.empty?
24
+
25
+ new(
26
+ model: values[:model],
27
+ extensions: !values.key?(:'no-extensions'),
28
+ skills: !values.key?(:'no-skills'),
29
+ agents_md: !values.key?(:'no-agents-md'),
30
+ tools: values[:tools],
31
+ max_steps: values[:'max-steps'],
32
+ help: values.key?(:help)
33
+ )
34
+ rescue OptionParser::ParseError => e
35
+ e.message
36
+ end
37
+
38
+ # @rbs return: String
39
+ def self.usage
40
+ parser.help
41
+ end
42
+
43
+ # @rbs return: OptionParser
44
+ def self.parser
45
+ OptionParser.new do |parser|
46
+ parser.banner = BANNER
47
+ parser.separator('')
48
+ parser.on('--model PROVIDER/NAME', String, 'Model for this session, over RIFFER_MODEL and settings')
49
+ parser.on('--no-extensions', 'Skip extension files and autoload; the bundle still loads')
50
+ parser.on('--no-skills', 'Leave out Agent Skills')
51
+ parser.on('--no-agents-md', 'Leave out AGENTS.md instructions')
52
+ parser.on('--tools NAME,NAME', Array, 'Only these tools, by identifier')
53
+ parser.on('--max-steps N', Integer, 'Stop a turn after N model calls')
54
+ parser.on('-h', '--help', 'Show this help')
55
+ end
56
+ end
57
+ private_class_method :parser
58
+
59
+ # @rbs model: String?
60
+ # @rbs extensions: bool
61
+ # @rbs skills: bool
62
+ # @rbs agents_md: bool
63
+ # @rbs tools: Array[String]?
64
+ # @rbs max_steps: Integer?
65
+ # @rbs help: bool
66
+ # @rbs return: void
67
+ def initialize(model: nil, extensions: true, skills: true, agents_md: true, tools: nil, max_steps: nil, help: false)
68
+ @model = model
69
+ @extensions = extensions
70
+ @skills = skills
71
+ @agents_md = agents_md
72
+ @tools = tools&.freeze
73
+ @max_steps = max_steps
74
+ @help = help
75
+ freeze
76
+ end
77
+ end
@@ -1,129 +1,92 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'io/console'
4
-
5
3
  module Riffer::Rig::CLI
6
4
  extend self
7
5
 
8
- # @rbs output: IO
9
- # @rbs input: IO
10
- # @rbs model: String
11
- # @rbs env: Riffer::Rig::Env
12
- # @rbs return: Integer
13
- def start(output: $stdout, input: $stdin, model: Riffer::Rig::Settings.model, env: Riffer::Rig::Env.new)
14
- theme = Riffer::Rig::UI::Theme.for(output, env:)
6
+ PRINT_FLAGS = %w[-p --print].freeze #: Array[String]
15
7
 
16
- provider = Riffer::Rig::Settings.provider_for(model)
8
+ ACP_COMMAND = 'acp'
17
9
 
18
- if provider
19
- values = credentials_for(provider, theme, output:, input:, env:)
20
- return 1 if values.nil?
10
+ USAGE_ERROR = 2
21
11
 
22
- Riffer::Rig::Credentials.apply(provider, values)
23
- end
24
-
25
- agent = Riffer::Rig::CodingAgent.new
26
- animator = Riffer::Rig::UI::Animator.new(io: output, theme:)
27
-
28
- reveal_banner(theme, animator, model)
29
-
30
- tally = Riffer::Rig::TokenTally.new(pricing: Riffer::Rig::Settings.pricing_for(model))
31
- smoother = Riffer::Rig::UI::Smoother.new(io: output, theme:)
32
- renderer = Riffer::Rig::UI::Renderer.new(io: output, theme:, tally:, smoother:)
33
- Riffer::Rig::REPL.new(agent:, renderer:, animator:, theme:, smoother:, input:, output:).run
34
- 0
12
+ # @rbs argv: Array[String]
13
+ # @rbs input: IO
14
+ # @rbs output: IO
15
+ # @rbs error: IO
16
+ # @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
17
+ # @rbs cwd: String
18
+ # @rbs home: String
19
+ # @rbs return: Integer
20
+ def start(
21
+ argv = ARGV,
22
+ input: $stdin,
23
+ output: $stdout,
24
+ error: $stderr,
25
+ env: Riffer::Rig::Env.load,
26
+ cwd: Dir.pwd,
27
+ home: Dir.home
28
+ )
29
+ return unavailable("riffer #{ACP_COMMAND}", error) if argv.first == ACP_COMMAND
30
+ return unavailable('riffer -p', error) if argv.intersect?(PRINT_FLAGS)
31
+
32
+ flags = Flags.parse(argv)
33
+ return refuse(flags, error) if flags.is_a?(String)
34
+ return help(output) if flags.help
35
+
36
+ terminal(flags, input:, output:, env:, cwd:, home:)
35
37
  end
36
38
 
37
39
  private
38
40
 
39
- # @rbs provider: String
40
- # @rbs theme: Riffer::Rig::UI::Theme
41
- # @rbs output: IO
41
+ # @rbs flags: Riffer::Rig::CLI::Flags
42
42
  # @rbs input: IO
43
- # @rbs env: Riffer::Rig::Env
44
- # @rbs return: Hash[Symbol, String]?
45
- def credentials_for(provider, theme, output:, input:, env:)
46
- resolution = Riffer::Rig::Credentials.resolve(provider, host: Riffer::Rig::Hosts::Null.new, env:)
47
- return resolution.values if resolution.missing.empty?
48
-
49
- onboard(provider, resolution, theme, output:, input:)
50
- end
51
-
52
- # @rbs provider: String
53
- # @rbs resolution: Riffer::Rig::Credentials::Resolution
54
- # @rbs theme: Riffer::Rig::UI::Theme
55
43
  # @rbs output: IO
56
- # @rbs input: IO
57
- # @rbs return: Hash[Symbol, String]?
58
- def onboard(provider, resolution, theme, output:, input:)
59
- setup = Riffer::Rig::ProviderSetup.for(provider)
60
- missing = setup.fields.select { |field| resolution.missing.include?(field.name) }
61
-
62
- output.puts(theme.cyan('♪ welcome to riffer-rig ♪'))
63
- output.puts(theme.grey("No #{provider} credentials found. Create them at #{setup.url || 'your provider'}"))
64
-
65
- answers = missing.to_h { |field| [field.name, prompt(provider, field, theme, output:, input:)] }
66
- if answers.values.any?(&:empty?)
67
- env_vars = missing.flat_map(&:env).join(', ')
68
- output.puts(theme.grey("Nothing entered. Set #{env_vars} or re-run riffer to try again."))
69
- return nil
44
+ # @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
45
+ # @rbs cwd: String
46
+ # @rbs home: String
47
+ # @rbs return: Integer
48
+ def terminal(flags, input:, output:, env:, cwd:, home:)
49
+ Riffer::Rig::Terminal.for(
50
+ input: input,
51
+ output: output,
52
+ version: Riffer::Rig::VERSION,
53
+ no_color: env.is_a?(Riffer::Rig::Env) && env.no_color
54
+ ).run do |host|
55
+ Riffer::Rig::Loader.runtime(
56
+ cwd: cwd,
57
+ host: host,
58
+ env: env,
59
+ home: home,
60
+ model: flags.model,
61
+ extensions: flags.extensions,
62
+ skills: flags.skills,
63
+ agents_md: flags.agents_md,
64
+ tools: flags.tools,
65
+ max_steps: flags.max_steps
66
+ )
70
67
  end
71
-
72
- Riffer::Rig::Credentials.store(provider, answers)
73
- output.puts(theme.grey("Saved under #{File.dirname(Riffer::Rig::Credentials::PATH)} (auth.json permissions 600)."))
74
- resolution.values.merge(answers)
75
- end
76
-
77
- # @rbs provider: String
78
- # @rbs field: Riffer::Rig::ProviderSetup::Field
79
- # @rbs theme: Riffer::Rig::UI::Theme
80
- # @rbs output: IO
81
- # @rbs input: IO
82
- # @rbs return: String
83
- def prompt(provider, field, theme, output:, input:)
84
- hint = field.secret ? " #{theme.grey('(hidden)')}" : ''
85
- output.print("#{theme.pink('›')} Paste your #{provider} #{field.name}#{hint}: ")
86
-
87
- answer = (field.secret ? read_secret(input) : input.gets).to_s.strip
88
- output.puts
89
- answer
90
68
  end
91
69
 
92
- # @rbs input: IO
93
- # @rbs return: String?
94
- def read_secret(input)
95
- return input.noecho(&:gets) if input.respond_to?(:noecho) && input.tty?
96
-
97
- input.gets
70
+ # @rbs name: String
71
+ # @rbs error: IO
72
+ # @rbs return: Integer
73
+ def unavailable(name, error)
74
+ refuse("#{name} is not available yet", error)
98
75
  end
99
76
 
100
- # @rbs theme: Riffer::Rig::UI::Theme
101
- # @rbs animator: Riffer::Rig::UI::Animator
102
- # @rbs model: String
103
- # @rbs return: void
104
- def reveal_banner(theme, animator, model)
105
- loaded = [Riffer::Rig::CodingAgent::GLOBAL_AGENTS_FILE, File.join(Dir.pwd, 'AGENTS.md')].select { |path| File.file?(path) }
106
- context = loaded.empty? ? 'none' : loaded.join(', ')
107
-
108
- animator.reveal(
109
- [Riffer::Rig::UI::Banner.lines(
110
- theme,
111
- model: model,
112
- cwd: Dir.pwd,
113
- context: context,
114
- skills: count_skills,
115
- version: Riffer::Rig::VERSION
116
- )]
117
- )
77
+ # @rbs message: String
78
+ # @rbs error: IO
79
+ # @rbs return: Integer
80
+ def refuse(message, error)
81
+ error.puts("riffer: #{message}")
82
+ error.puts(Flags.usage)
83
+ USAGE_ERROR
118
84
  end
119
85
 
120
- # @rbs return: String
121
- def count_skills
122
- dirs = [Riffer::Rig::CodingAgent::GLOBAL_SKILLS_DIR, Riffer::Rig::CodingAgent::PROJECT_SKILLS_DIR.call]
123
- backend = Riffer::Skills::FilesystemBackend.new(*dirs)
124
- count = backend.list_skills.length
125
- count.zero? ? 'none' : count.to_s
126
- rescue StandardError
127
- 'none'
86
+ # @rbs output: IO
87
+ # @rbs return: Integer
88
+ def help(output)
89
+ output.puts(Flags.usage)
90
+ 0
128
91
  end
129
92
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Command::Context
4
+ # @rbs @command: String
5
+ # @rbs @emit: ^(Riffer::Rig::Events::_Event) -> void
6
+ # @rbs @turn: ^(String) -> void
7
+
8
+ # @dynamic args, runtime, host, settings
9
+ attr_reader :args #: String
10
+ attr_reader :runtime #: Riffer::Rig::Runtime
11
+ attr_reader :host #: Riffer::Rig::Hosts::_Host
12
+ attr_reader :settings #: Hash[Symbol, untyped]
13
+
14
+ # @rbs command: String
15
+ # @rbs args: String
16
+ # @rbs runtime: Riffer::Rig::Runtime
17
+ # @rbs host: Riffer::Rig::Hosts::_Host
18
+ # @rbs settings: Hash[Symbol, untyped]
19
+ # @rbs emit: ^(Riffer::Rig::Events::_Event) -> void
20
+ # @rbs turn: ^(String) -> void
21
+ # @rbs return: void
22
+ def initialize(command, args, runtime:, host:, settings:, emit:, turn:)
23
+ @command = command
24
+ @args = args
25
+ @runtime = runtime
26
+ @host = host
27
+ @settings = settings
28
+ @emit = emit
29
+ @turn = turn
30
+ freeze
31
+ end
32
+
33
+ # @rbs text: String
34
+ # @rbs return: nil
35
+ def say(text)
36
+ emit(Riffer::Rig::Events::CommandOutput.new(@command, text))
37
+ end
38
+
39
+ # @rbs event: Riffer::Rig::Events::_Event
40
+ # @rbs return: nil
41
+ def emit(event)
42
+ @emit.call(event)
43
+ nil
44
+ end
45
+
46
+ # @rbs text: String
47
+ # @rbs return: nil
48
+ def prompt(text)
49
+ @turn.call(text)
50
+ nil
51
+ end
52
+
53
+ # @rbs question: String?
54
+ # @rbs options: Array[String]?
55
+ # @rbs secret: bool
56
+ # @rbs return: String?
57
+ def ask(question = nil, options: nil, secret: false)
58
+ return unless host.capabilities.include?(:ask)
59
+
60
+ host.ask(question, options: options, secret: secret)
61
+ end
62
+
63
+ # @rbs question: String?
64
+ # @rbs return: bool
65
+ def confirm(question = nil)
66
+ return false unless host.capabilities.include?(:confirm)
67
+
68
+ host.confirm(question)
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Command
4
+ # @rbs @block: ^(Riffer::Rig::Command::Context) -> void
5
+
6
+ # @dynamic name, description, extension
7
+ attr_reader :name #: String
8
+ attr_reader :description #: String
9
+ attr_reader :extension #: String
10
+
11
+ # @rbs name: String
12
+ # @rbs description: String
13
+ # @rbs extension: String
14
+ # @rbs &block: (Riffer::Rig::Command::Context) -> void
15
+ # @rbs return: void
16
+ def initialize(name, description:, extension:, &block)
17
+ @name = name
18
+ @description = description
19
+ @extension = extension
20
+ @block = block
21
+ freeze
22
+ end
23
+
24
+ # @rbs ctx: Riffer::Rig::Command::Context
25
+ # @rbs return: void
26
+ def call(ctx)
27
+ @block.call(ctx)
28
+ end
29
+ end
@@ -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