ruby_claude 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 46dffd5cda4df76cdd6a657076af87f43c068929ce40b0973e2aa4dd26b4a7f1
4
+ data.tar.gz: 5c55b6f8448491d93d3716bbfe9694ea7f160bef7bd1c5410a3021778f5b362a
5
+ SHA512:
6
+ metadata.gz: 34dcef3922757a6b7580f196f0528268b9827ec6bdf0fea979ef768a7d12ecc0723c3f75dcec094b25f8dd45d7a20c682aef654e535f878775d4470bcd33a615
7
+ data.tar.gz: 0b2ecd60e8dbf74ef7d64b4b0eda9932b7b9dc5aad6a1c09aeb1dabaa159de5f0fdd1656b13cfc5944b7b681d269bc69668a967e2e8d825a92320244b86d14c5
data/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Kaíque Kandy Koga
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # Ruby Claude
2
+
3
+ A small, dependency-light, idiomatic Ruby SDK for talking to Claude — by
4
+ shelling out to the **Claude Code CLI** (`claude -p`) in headless mode and
5
+ authenticating with your **Claude Pro/Max subscription** instead of an
6
+ Anthropic API key.
7
+
8
+ > **Unofficial.** This is a community gem. It is *not* affiliated with or
9
+ > endorsed by Anthropic. It uses a documented, supported headless feature
10
+ > (`claude -p`) and stays within your subscription's normal rate limits. It
11
+ > does **not** extract or reuse OAuth tokens, and it makes **no** direct HTTP
12
+ > calls to the Anthropic API.
13
+
14
+ ## Why a subscription instead of an API key?
15
+
16
+ `claude -p "<prompt>"` runs Claude Code non-interactively and prints the
17
+ result, using whatever credentials the CLI is logged in with. If you logged in
18
+ with a **subscription** (`claude` → `/login` → subscription option), those
19
+ calls draw on your subscription — **no API billing**.
20
+
21
+ The one catch: if `ANTHROPIC_API_KEY` is present in the environment, Claude
22
+ Code may use it and bill the API. **Ruby Claude strips `ANTHROPIC_API_KEY`
23
+ from the child process environment by default** (`use_subscription = true`) so
24
+ the CLI falls back to your logged-in subscription credentials. Set
25
+ `use_subscription = false` only if you *want* API-key billing.
26
+
27
+ ## Prerequisites
28
+
29
+ This gem drives the `claude` binary; it does not install or replace it.
30
+
31
+ 1. Install Node.js and the Claude Code CLI, and make sure `claude` is on your `PATH`:
32
+ ```bash
33
+ npm install -g @anthropic-ai/claude-code
34
+ claude --version
35
+ ```
36
+ 2. Log in **once**, choosing the subscription option:
37
+ ```bash
38
+ claude # then run /login and pick "Claude account with subscription"
39
+ ```
40
+
41
+ ## Installation
42
+
43
+ Add it to your `Gemfile`:
44
+
45
+ ```ruby
46
+ gem "ruby_claude"
47
+ ```
48
+
49
+ Or install directly:
50
+
51
+ ```bash
52
+ gem install ruby_claude
53
+ ```
54
+
55
+ Ruby **3.2+** is required (the value objects use `Data.define`). The gem has
56
+ **zero runtime dependencies** — it only uses the standard library.
57
+
58
+ ## Quickstart
59
+
60
+ ```ruby
61
+ require "ruby_claude"
62
+
63
+ puts RubyClaude.query("Summarize lib/foo.rb in two sentences")
64
+ ```
65
+
66
+ That's it — if `claude` is installed and logged in, you get an answer back,
67
+ billed against your subscription.
68
+
69
+ ## Usage
70
+
71
+ ### 1. One-shot convenience
72
+
73
+ Delegates to a memoized, globally-configured default client.
74
+
75
+ ```ruby
76
+ puts RubyClaude.query("Summarize lib/foo.rb in two sentences")
77
+ ```
78
+
79
+ ### 2. A configured client
80
+
81
+ ```ruby
82
+ client = RubyClaude::Client.new(
83
+ model: "claude-sonnet-4-6",
84
+ cwd: "/path/to/project",
85
+ append_system_prompt: "Always answer concisely.",
86
+ allowed_tools: ["Read", "Grep"],
87
+ timeout: 180
88
+ )
89
+
90
+ res = client.query("What does this project do?")
91
+ res.text # => String, the final assistant result
92
+ res.session_id # => String
93
+ res.cost_usd # => Float (often 0.0 on a subscription)
94
+ res.usage # => Hash (token counts, when present)
95
+ res.num_turns # => Integer
96
+ res.duration_ms # => Integer
97
+ res.error? # => false
98
+ res.raw # => parsed Hash of the CLI's final result JSON
99
+ ```
100
+
101
+ `Response#to_s` returns `text`, so `puts client.query("...")` prints the answer.
102
+
103
+ ### 3. Streaming
104
+
105
+ `#stream` yields typed events as they arrive and returns the final `Response`.
106
+
107
+ ```ruby
108
+ client.stream("Write a haiku about Ruby") do |event|
109
+ case event.type
110
+ when :assistant then print event.text # assistant text for the turn
111
+ when :result then puts "\n[done in #{event.duration_ms}ms]"
112
+ end
113
+ end
114
+ ```
115
+
116
+ Each `Event` exposes `type` (`:system`, `:assistant`, `:user`, `:result`),
117
+ `text`, `session_id`, `cost_usd`, `duration_ms`, and `raw` (the full parsed
118
+ line). Streaming uses `--output-format stream-json --verbose` under the hood.
119
+
120
+ ### 4. Multi-turn session
121
+
122
+ A `Session` captures the underlying `session_id` from the first reply and
123
+ transparently resumes it on later calls.
124
+
125
+ ```ruby
126
+ session = client.session
127
+ session.query("My favorite number is 7.")
128
+ puts session.query("What's my favorite number?") # => "...7..."
129
+ session.id # => the session_id being resumed
130
+ ```
131
+
132
+ You can also resume a known session: `client.session(id: "…")`.
133
+
134
+ ### 5. Global configuration
135
+
136
+ ```ruby
137
+ RubyClaude.configure do |c|
138
+ c.model = "claude-sonnet-4-6"
139
+ c.timeout = 300
140
+ c.binary = "claude" # path/name of the CLI
141
+ c.cwd = Dir.pwd
142
+ c.use_subscription = true # strips ANTHROPIC_API_KEY from the child env
143
+ end
144
+ ```
145
+
146
+ These become the defaults for `RubyClaude.query` and for new `Client`
147
+ instances. Per-client options passed to `Client.new(**opts)` override them.
148
+
149
+ > **Note:** there is intentionally no `#send` method (it would shadow
150
+ > `Object#send`). Use `#query`, or its alias `#ask`.
151
+
152
+ ## Configuration options
153
+
154
+ | Option | Default | Maps to / effect |
155
+ |------------------------|----------------------|---------------------------------------------------------------------------|
156
+ | `binary` | `"claude"` | executable name/path |
157
+ | `model` | `nil` (CLI default) | `--model` |
158
+ | `cwd` | `Dir.pwd` | working directory for the subprocess |
159
+ | `timeout` | `300` | seconds before the child is killed |
160
+ | `use_subscription` | `true` | when true, delete `ANTHROPIC_API_KEY` from the child env |
161
+ | `append_system_prompt` | `nil` | `--append-system-prompt` |
162
+ | `allowed_tools` | `nil` | `--allowedTools` (array of tool/permission rules) |
163
+ | `disallowed_tools` | `nil` | `--disallowedTools` |
164
+ | `add_dirs` | `[]` | `--add-dir` (extra readable/writable directories) |
165
+ | `permission_mode` | `nil` | `--permission-mode` (`default` / `acceptEdits` / `plan` / `bypassPermissions`) |
166
+ | `max_turns` | `nil` | `--max-turns` |
167
+
168
+ Tool and directory lists are passed as separate CLI tokens, so permission-rule
169
+ patterns that contain spaces (e.g. `"Bash(git log *)"`) are preserved.
170
+
171
+ ## Errors
172
+
173
+ All errors inherit from `RubyClaude::Error`:
174
+
175
+ | Error | Raised when |
176
+ |----------------------------------|-----------------------------------------------------------------------------|
177
+ | `RubyClaude::BinaryNotFoundError`| `claude` is not on `PATH` / not executable (message explains how to install)|
178
+ | `RubyClaude::AuthenticationError`| output/exit indicates you are not logged in (suggests `claude` + `/login`) |
179
+ | `RubyClaude::TimeoutError` | the child exceeded `timeout`; the gem killed it |
180
+ | `RubyClaude::ExecutionError` | non-zero exit, or a result with `is_error: true` (carries `#status`, `#stderr`) |
181
+ | `RubyClaude::ParseError` | the CLI output couldn't be parsed as the expected JSON |
182
+
183
+ ```ruby
184
+ begin
185
+ RubyClaude.query("hello")
186
+ rescue RubyClaude::BinaryNotFoundError => e
187
+ warn e.message # install + /login instructions
188
+ rescue RubyClaude::AuthenticationError
189
+ warn "Run `claude` and `/login` with your subscription."
190
+ rescue RubyClaude::ExecutionError => e
191
+ warn "claude failed (status #{e.status}): #{e.stderr}"
192
+ end
193
+ ```
194
+
195
+ ## How it works
196
+
197
+ Ruby Claude is a thin, well-factored wrapper around `claude -p`:
198
+
199
+ - **`Command`** (pure, no I/O) turns your configuration + per-call options into
200
+ the argv array (`["claude", "-p", "--output-format", "json", …]`) and the
201
+ child-environment overrides (removing `ANTHROPIC_API_KEY` in subscription mode).
202
+ - **`Runner`** owns all subprocess concerns: it spawns `claude` via `Open3`
203
+ (always the array form — your prompt is **never** shell-interpolated), writes
204
+ the prompt to **stdin** (avoiding `ARG_MAX` and escaping issues), enforces the
205
+ timeout by killing the child, captures output, and — for streaming — reads
206
+ stdout line-by-line as newline-delimited JSON.
207
+ - **`Client`** composes the two and builds `Response` / `Event` objects.
208
+ - **`Session`** remembers the `session_id` and passes `--resume <id>`.
209
+
210
+ The runner is stateless and spawns one subprocess per call, so a `Client` is
211
+ safe to reuse and to call concurrently from multiple threads.
212
+
213
+ ## Development
214
+
215
+ ```bash
216
+ bundle install # install dev/test dependencies
217
+ rake test # run the test suite (hermetic — never spawns claude)
218
+ rake lint # rubocop
219
+ rake # test + lint
220
+ bin/console # IRB with the gem loaded
221
+ ```
222
+
223
+ Tests inject a fake runner at the `Client`'s runner boundary, so the suite is
224
+ fully hermetic: it never makes a network call and never invokes the real
225
+ `claude` binary. (A handful of `Runner` tests spawn a throwaway local `ruby`
226
+ process to exercise the subprocess plumbing.)
227
+
228
+ ## Building and publishing the gem
229
+
230
+ The version lives in [`lib/ruby_claude/version.rb`](lib/ruby_claude/version.rb).
231
+ Before a release, bump it following [SemVer](https://semver.org).
232
+
233
+ ### Build locally
234
+
235
+ ```bash
236
+ gem build ruby_claude.gemspec # => ruby_claude-<version>.gem
237
+ gem install ./ruby_claude-<version>.gem # try the built gem locally
238
+ ```
239
+
240
+ `spec.files` is derived from `git ls-files`, so only **tracked** files are
241
+ packaged — commit (or at least stage) your changes before building, or the gem
242
+ will be missing files. Bundler's gem tasks do the same and drop the artifact in
243
+ `pkg/`:
244
+
245
+ ```bash
246
+ rake build # build into pkg/
247
+ rake install # build and install locally
248
+ ```
249
+
250
+ ### Publish to RubyGems
251
+
252
+ 1. Create a [RubyGems.org](https://rubygems.org) account and sign in once
253
+ (credentials are stored in `~/.gem/credentials`):
254
+
255
+ ```bash
256
+ gem signin
257
+ ```
258
+
259
+ 2. Make sure the tree is green and committed:
260
+
261
+ ```bash
262
+ rake # tests + lint
263
+ git status # nothing uncommitted
264
+ ```
265
+
266
+ 3. Build and push:
267
+
268
+ ```bash
269
+ gem build ruby_claude.gemspec
270
+ gem push ruby_claude-<version>.gem
271
+ ```
272
+
273
+ The name `ruby_claude` is currently available on RubyGems. Releasing
274
+ `0.0.0` is unusual — bump to e.g. `0.1.0` for your first real publish.
275
+
276
+ Alternatively, do it all in one step with Bundler's release task, which builds
277
+ the gem, creates and pushes a `v<version>` git tag, and pushes to RubyGems
278
+ (requires a clean, committed tree):
279
+
280
+ ```bash
281
+ rake release
282
+ ```
283
+
284
+ > The gemspec sets `rubygems_mfa_required`, so enable MFA on your RubyGems
285
+ > account; pushes and yanks will then prompt for a one-time code.
286
+
287
+ ## License
288
+
289
+ BSD-3-Clause. See [LICENSE](LICENSE).
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module RubyClaude
6
+ # Composes a {Command} and a {Runner} to execute queries and build
7
+ # {Response} and {Event} objects.
8
+ #
9
+ # A client holds an immutable {Configuration} and a stateless runner, builds
10
+ # fresh argv/env per call, and never mutates shared state — so one instance
11
+ # is safe to reuse and to call concurrently from many threads.
12
+ class Client
13
+ # Heuristic patterns in stderr/result text that indicate an auth problem.
14
+ # Deliberately specific: bare "authentication" / "api key" / "credit
15
+ # balance" match too much benign text and would misclassify ordinary
16
+ # execution and billing failures as authentication errors.
17
+ AUTH_PATTERNS = Regexp.union(
18
+ /invalid api key/i,
19
+ /authentication[ _](?:failed|error|required)/i,
20
+ /unauthorized/i,
21
+ /not logged ?in/i,
22
+ %r{/login}i,
23
+ /oauth/i,
24
+ /log ?in to claude/i
25
+ ).freeze
26
+
27
+ # @return [Configuration] the effective configuration for this client
28
+ attr_reader :config
29
+
30
+ # @param runner [#run, #stream] subprocess runner (injectable for tests)
31
+ # @param overrides [Hash] per-instance {Configuration} overrides
32
+ # @raise [ArgumentError] on an unknown configuration option
33
+ def initialize(runner: Runner.new, **overrides)
34
+ @config = RubyClaude.configuration.merge(overrides)
35
+ @runner = runner
36
+ end
37
+
38
+ # Run a one-shot query and return its {Response}.
39
+ #
40
+ # @param prompt [String]
41
+ # @param resume [String, nil] a session id to resume
42
+ # @return [Response]
43
+ # @raise [AuthenticationError, ExecutionError, ParseError, TimeoutError,
44
+ # BinaryNotFoundError]
45
+ def query(prompt, resume: nil)
46
+ argv, env = Command.new(@config).build(stream: false, resume: resume)
47
+ result = @runner.run(**run_args(argv, env, prompt))
48
+ interpret(result)
49
+ end
50
+ alias ask query
51
+
52
+ # Stream a query, yielding {Event}s as they arrive.
53
+ #
54
+ # @param prompt [String]
55
+ # @param resume [String, nil] a session id to resume
56
+ # @yieldparam event [Event]
57
+ # @return [Response] the final result, built from the +result+ event
58
+ # @raise [AuthenticationError, ExecutionError, TimeoutError,
59
+ # BinaryNotFoundError]
60
+ def stream(prompt, resume: nil)
61
+ argv, env = Command.new(@config).build(stream: true, resume: resume)
62
+ final = nil
63
+ result = @runner.stream(**run_args(argv, env, prompt)) do |line|
64
+ data = try_parse(line)
65
+ next unless data
66
+
67
+ final = data if data["type"] == "result"
68
+ yield Event.from_hash(data) if block_given?
69
+ end
70
+ check_stream_result!(final, result)
71
+ Response.from_result(final)
72
+ end
73
+
74
+ # Start a multi-turn {Session} backed by this client.
75
+ #
76
+ # @param id [String, nil] an existing session id to resume
77
+ # @return [Session]
78
+ def session(id: nil)
79
+ Session.new(self, id: id)
80
+ end
81
+
82
+ private
83
+
84
+ def run_args(argv, env, prompt)
85
+ { argv: argv, env: env, cwd: @config.cwd, timeout: @config.timeout, stdin: prompt.to_s }
86
+ end
87
+
88
+ # Turn a one-shot {RunResult} into a {Response} or raise a typed error.
89
+ def interpret(result)
90
+ data = try_parse(result.stdout)
91
+ if data.is_a?(Hash) && data["type"] == "result"
92
+ raise_result_error!(data, result) if data["is_error"]
93
+ return Response.from_result(data)
94
+ end
95
+
96
+ raise failure_for(result) if failed?(result)
97
+
98
+ raise ParseError, "could not parse claude output as JSON: #{truncate(result.stdout)}"
99
+ end
100
+
101
+ def check_stream_result!(final, result)
102
+ raise_result_error!(final, result) if final && final["is_error"]
103
+ raise failure_for(result) if final.nil? && failed?(result)
104
+ end
105
+
106
+ def failed?(result)
107
+ status = result.exit_status
108
+ status.nil? || !status.zero?
109
+ end
110
+
111
+ def raise_result_error!(data, result)
112
+ detail = data["result"] || data["errors"]&.join("; ") || "subtype=#{data["subtype"]}"
113
+ raise AuthenticationError, auth_message(detail) if auth?(detail, result&.stderr)
114
+
115
+ raise ExecutionError.new(
116
+ "claude returned an error result: #{detail}",
117
+ status: result&.exit_status,
118
+ stderr: result&.stderr
119
+ )
120
+ end
121
+
122
+ def failure_for(result)
123
+ stderr = result.stderr.to_s
124
+ return AuthenticationError.new(auth_message(stderr.strip)) if auth?(stderr, result.stdout)
125
+
126
+ status = result.exit_status
127
+ ExecutionError.new(
128
+ "claude exited with status #{status || "signal"}: #{truncate(stderr)}",
129
+ status: status,
130
+ stderr: stderr
131
+ )
132
+ end
133
+
134
+ def auth?(*sources)
135
+ sources.compact.any? { |source| AUTH_PATTERNS.match?(source.to_s) }
136
+ end
137
+
138
+ def auth_message(detail)
139
+ base = "Claude authentication failed. Run `claude` and use `/login` to sign in with " \
140
+ "your Claude subscription (or set use_subscription = false to use ANTHROPIC_API_KEY)."
141
+ detail.nil? || detail.empty? ? base : "#{base}\n#{detail}"
142
+ end
143
+
144
+ def try_parse(string)
145
+ return nil if string.nil? || string.strip.empty?
146
+
147
+ JSON.parse(string)
148
+ rescue JSON::ParserError
149
+ nil
150
+ end
151
+
152
+ def truncate(string, max = 500)
153
+ stripped = string.to_s.strip
154
+ stripped.length > max ? "#{stripped[0, max]}..." : stripped
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyClaude
4
+ # Pure translation of a {Configuration} plus per-call options into the argv
5
+ # array and child-environment overrides for the +claude+ CLI.
6
+ #
7
+ # Performs no I/O, which makes flag mapping trivial to unit-test. The prompt
8
+ # is intentionally *never* part of argv — it is written to the child's stdin
9
+ # by the {Runner} to avoid +ARG_MAX+ limits and shell-escaping concerns.
10
+ class Command
11
+ # @param config [Configuration]
12
+ def initialize(config)
13
+ @config = config
14
+ end
15
+
16
+ # Build the argv array and child-environment overrides.
17
+ #
18
+ # @param stream [Boolean] use stream-json output (also adds +--verbose+,
19
+ # which the CLI requires for stream-json in print mode)
20
+ # @param resume [String, nil] a session id to resume via +--resume+
21
+ # @return [Array(Array<String>, Hash)] +[argv, env]+
22
+ def build(stream:, resume: nil)
23
+ argv = [@config.binary, "-p", "--output-format", stream ? "stream-json" : "json"]
24
+ argv << "--verbose" if stream
25
+ add_flag(argv, "--model", @config.model)
26
+ add_flag(argv, "--append-system-prompt", @config.append_system_prompt)
27
+ add_list(argv, "--allowedTools", @config.allowed_tools)
28
+ add_list(argv, "--disallowedTools", @config.disallowed_tools)
29
+ add_list(argv, "--add-dir", @config.add_dirs)
30
+ add_flag(argv, "--permission-mode", @config.permission_mode)
31
+ add_flag(argv, "--max-turns", @config.max_turns&.to_s)
32
+ add_flag(argv, "--resume", resume)
33
+ [argv, child_env]
34
+ end
35
+
36
+ # Environment overrides for the child process. In subscription mode,
37
+ # +ANTHROPIC_API_KEY+ is mapped to +nil+, which tells +Open3+/+spawn+ to
38
+ # remove it from the inherited environment so the CLI falls back to the
39
+ # logged-in subscription credentials.
40
+ #
41
+ # @return [Hash{String => String, nil}]
42
+ def child_env
43
+ return {} unless @config.use_subscription
44
+
45
+ { "ANTHROPIC_API_KEY" => nil }
46
+ end
47
+
48
+ private
49
+
50
+ # Append +flag value+ when +value+ is present.
51
+ def add_flag(argv, flag, value)
52
+ return if value.nil?
53
+
54
+ string = value.to_s
55
+ return if string.empty?
56
+
57
+ argv.push(flag, string)
58
+ end
59
+
60
+ # Append +flag item item ...+ (each list item as its own argv token, which
61
+ # matches the CLI's space-separated variadic options and preserves spaces
62
+ # inside permission-rule patterns such as +Bash(git log *)+).
63
+ def add_list(argv, flag, value)
64
+ items = Array(value).map(&:to_s).reject(&:empty?)
65
+ return if items.empty?
66
+
67
+ argv.push(flag, *items)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyClaude
4
+ # Holds every tunable option with sane defaults.
5
+ #
6
+ # Used as the global default (via {RubyClaude.configure}) and as the basis
7
+ # for per-{Client} overrides through {#merge}. A configuration is only ever
8
+ # read while a query runs, never mutated, which keeps {Client} thread-safe.
9
+ class Configuration
10
+ # @return [String] executable name or path of the CLI
11
+ attr_accessor :binary
12
+
13
+ # @return [String, nil] model for +--model+ (nil uses the CLI default)
14
+ attr_accessor :model
15
+
16
+ # @return [String, nil] working directory for the subprocess
17
+ attr_accessor :cwd
18
+
19
+ # @return [Integer] seconds before the child process is killed
20
+ attr_accessor :timeout
21
+
22
+ # @return [Boolean] when true, strip +ANTHROPIC_API_KEY+ from the child env
23
+ attr_accessor :use_subscription
24
+
25
+ # @return [String, nil] text for +--append-system-prompt+
26
+ attr_accessor :append_system_prompt
27
+
28
+ # @return [Array<String>, String, nil] tools for +--allowedTools+
29
+ attr_accessor :allowed_tools
30
+
31
+ # @return [Array<String>, String, nil] tools for +--disallowedTools+
32
+ attr_accessor :disallowed_tools
33
+
34
+ # @return [Array<String>] directories for repeated +--add-dir+
35
+ attr_accessor :add_dirs
36
+
37
+ # @return [String, nil] mode for +--permission-mode+
38
+ attr_accessor :permission_mode
39
+
40
+ # @return [Integer, nil] limit for +--max-turns+
41
+ attr_accessor :max_turns
42
+
43
+ def initialize
44
+ @binary = "claude"
45
+ @model = nil
46
+ @cwd = Dir.pwd
47
+ @timeout = 300
48
+ @use_subscription = true
49
+ @append_system_prompt = nil
50
+ @allowed_tools = nil
51
+ @disallowed_tools = nil
52
+ @add_dirs = []
53
+ @permission_mode = nil
54
+ @max_turns = nil
55
+ end
56
+
57
+ # Return a copy with the given overrides applied. The receiver is left
58
+ # untouched, so the global configuration is never mutated by a {Client}.
59
+ #
60
+ # @param overrides [Hash{Symbol => Object}]
61
+ # @return [Configuration]
62
+ # @raise [ArgumentError] when an option is not recognized
63
+ def merge(overrides)
64
+ dup.tap do |copy|
65
+ overrides.each do |key, value|
66
+ setter = "#{key}="
67
+ raise ArgumentError, "unknown configuration option: #{key}" unless copy.respond_to?(setter)
68
+
69
+ copy.public_send(setter, value)
70
+ end
71
+ end
72
+ end
73
+
74
+ # @return [Hash{Symbol => Object}] a plain-hash view of the configuration
75
+ def to_h
76
+ {
77
+ binary: binary, model: model, cwd: cwd, timeout: timeout,
78
+ use_subscription: use_subscription, append_system_prompt: append_system_prompt,
79
+ allowed_tools: allowed_tools, disallowed_tools: disallowed_tools,
80
+ add_dirs: add_dirs, permission_mode: permission_mode, max_turns: max_turns
81
+ }
82
+ end
83
+
84
+ private
85
+
86
+ # Deep-copy the mutable array options so a {Client} can never mutate the
87
+ # array held by the global configuration.
88
+ def initialize_copy(source)
89
+ super
90
+ @add_dirs = source.add_dirs.dup if source.add_dirs.is_a?(Array)
91
+ @allowed_tools = source.allowed_tools.dup if source.allowed_tools.is_a?(Array)
92
+ @disallowed_tools = source.disallowed_tools.dup if source.disallowed_tools.is_a?(Array)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyClaude
4
+ # Base class for every error raised by Ruby Claude.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the +claude+ binary cannot be found on PATH or executed.
8
+ #
9
+ # The message explains how to install Claude Code and reminds the user that
10
+ # they must run +claude+ and +/login+ at least once.
11
+ class BinaryNotFoundError < Error; end
12
+
13
+ # Raised when the CLI output or exit status indicates the user is not
14
+ # logged in or that authentication otherwise failed.
15
+ class AuthenticationError < Error; end
16
+
17
+ # Raised when the child process exceeds the configured timeout and the gem
18
+ # kills it.
19
+ class TimeoutError < Error; end
20
+
21
+ # Raised on a non-zero exit status, or on a result payload that reports
22
+ # +is_error: true+. Carries the exit status and captured stderr.
23
+ class ExecutionError < Error
24
+ # @return [Integer, nil] the child process exit status, when known
25
+ attr_reader :status
26
+
27
+ # @return [String, nil] captured standard error output, when available
28
+ attr_reader :stderr
29
+
30
+ # @param message [String, nil]
31
+ # @param status [Integer, nil]
32
+ # @param stderr [String, nil]
33
+ def initialize(message = nil, status: nil, stderr: nil)
34
+ @status = status
35
+ @stderr = stderr
36
+ super(message)
37
+ end
38
+ end
39
+
40
+ # Raised when the CLI output cannot be parsed as the expected JSON.
41
+ class ParseError < Error; end
42
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyClaude
4
+ # Immutable streaming event parsed from one line of +--output-format
5
+ # stream-json+ output. The {#type} mirrors the CLI's +type+ field as a
6
+ # Symbol (+:system+, +:assistant+, +:user+, +:result+, ...).
7
+ #
8
+ # @!attribute [r] type
9
+ # @return [Symbol] the event type
10
+ # @!attribute [r] text
11
+ # @return [String, nil] text extracted from assistant/user/result payloads
12
+ # @!attribute [r] session_id
13
+ # @return [String, nil] the session id, when present
14
+ # @!attribute [r] cost_usd
15
+ # @return [Float, nil] total cost, present on the result event
16
+ # @!attribute [r] duration_ms
17
+ # @return [Integer, nil] duration, present on the result event
18
+ # @!attribute [r] raw
19
+ # @return [Hash] the full parsed line
20
+ Event = Data.define(:type, :text, :session_id, :cost_usd, :duration_ms, :raw) do
21
+ # Build an Event from one parsed NDJSON line.
22
+ #
23
+ # @param data [Hash, nil] the parsed line
24
+ # @return [Event]
25
+ def self.from_hash(data)
26
+ data ||= {}
27
+ new(
28
+ type: (data["type"] || "unknown").to_sym,
29
+ text: extract_text(data),
30
+ session_id: data["session_id"],
31
+ cost_usd: data["total_cost_usd"],
32
+ duration_ms: data["duration_ms"],
33
+ raw: data
34
+ )
35
+ end
36
+
37
+ # Pull human-readable text out of a parsed line, if any.
38
+ #
39
+ # @param data [Hash]
40
+ # @return [String, nil]
41
+ def self.extract_text(data)
42
+ case data["type"]
43
+ when "assistant", "user"
44
+ message = data["message"] || data
45
+ text_from_content(message["content"])
46
+ when "result"
47
+ data["result"]
48
+ end
49
+ end
50
+
51
+ # Join the text from a content array (or pass a bare string through).
52
+ #
53
+ # @param content [String, Array, nil]
54
+ # @return [String, nil]
55
+ def self.text_from_content(content)
56
+ return content if content.is_a?(String)
57
+ return nil unless content.is_a?(Array)
58
+
59
+ texts = content
60
+ .select { |block| block.is_a?(Hash) && block["type"] == "text" }
61
+ .filter_map { |block| block["text"] }
62
+ texts.empty? ? nil : texts.join
63
+ end
64
+
65
+ # @return [Boolean] whether this is the final result event
66
+ def result? = type == :result
67
+
68
+ # @return [Boolean] whether this is an assistant message event
69
+ def assistant? = type == :assistant
70
+
71
+ # @return [Boolean] whether this is a system event
72
+ def system? = type == :system
73
+
74
+ # @return [Boolean] whether this is a user message event
75
+ def user? = type == :user
76
+ end
77
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyClaude
4
+ # Immutable value object describing the final result of a query.
5
+ #
6
+ # Built from the CLI's +--output-format json+ result object (or from the
7
+ # final +result+ line of a stream). Missing keys map to sensible defaults
8
+ # rather than raising.
9
+ #
10
+ # @!attribute [r] text
11
+ # @return [String] the assistant's final text result
12
+ # @!attribute [r] session_id
13
+ # @return [String, nil] the session id of this conversation
14
+ # @!attribute [r] cost_usd
15
+ # @return [Float] total cost in USD (often +0.0+ on a subscription)
16
+ # @!attribute [r] usage
17
+ # @return [Hash] token usage counts, when present
18
+ # @!attribute [r] num_turns
19
+ # @return [Integer] number of agentic turns
20
+ # @!attribute [r] duration_ms
21
+ # @return [Integer] wall-clock duration in milliseconds
22
+ # @!attribute [r] error
23
+ # @return [Boolean] whether the CLI reported an error
24
+ # @!attribute [r] raw
25
+ # @return [Hash] the full parsed result object
26
+ Response = Data.define(:text, :session_id, :cost_usd, :usage,
27
+ :num_turns, :duration_ms, :error, :raw) do
28
+ # Build a Response from a parsed CLI result hash.
29
+ #
30
+ # @param data [Hash, nil] the parsed result object
31
+ # @return [Response]
32
+ def self.from_result(data)
33
+ data ||= {}
34
+ new(
35
+ text: data["result"] || "",
36
+ session_id: data["session_id"],
37
+ cost_usd: (data["total_cost_usd"] || data["cost_usd"] || 0.0).to_f,
38
+ usage: data["usage"] || {},
39
+ num_turns: (data["num_turns"] || 0).to_i,
40
+ duration_ms: (data["duration_ms"] || 0).to_i,
41
+ error: data.fetch("is_error", false) ? true : false,
42
+ raw: data
43
+ )
44
+ end
45
+
46
+ # @return [Boolean] whether the result represents an error
47
+ def error? = !!error
48
+
49
+ # @return [Boolean] whether the result was successful
50
+ def success? = !error?
51
+
52
+ # Returns the assistant text, so +puts response+ prints the answer.
53
+ #
54
+ # @return [String]
55
+ def to_s = text
56
+ end
57
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module RubyClaude
6
+ # The captured result of a completed subprocess run.
7
+ #
8
+ # @!attribute [r] stdout
9
+ # @return [String, nil] captured stdout (nil when streaming)
10
+ # @!attribute [r] stderr
11
+ # @return [String] captured stderr
12
+ # @!attribute [r] exit_status
13
+ # @return [Integer, nil] exit code, or nil if the process was signalled
14
+ RunResult = Data.define(:stdout, :stderr, :exit_status)
15
+
16
+ # Owns every subprocess concern: spawning +claude+ via +Open3+, writing the
17
+ # prompt to stdin, enforcing the timeout by killing the child, capturing
18
+ # output, and translating spawn failures into {BinaryNotFoundError}.
19
+ #
20
+ # The runner is stateless, so a single instance is safe to share across
21
+ # threads. The {Client} accepts an injected runner so tests never spawn.
22
+ class Runner
23
+ # Seconds to wait after +SIGTERM+ before escalating to +SIGKILL+.
24
+ KILL_GRACE = 2
25
+
26
+ # Run the command to completion and capture its output.
27
+ #
28
+ # @param argv [Array<String>] the command and its arguments
29
+ # @param env [Hash] environment overrides (nil values unset a variable)
30
+ # @param cwd [String, nil] working directory
31
+ # @param timeout [Numeric] seconds before the child is killed
32
+ # @param stdin [String, nil] data to write to the child's stdin
33
+ # @return [RunResult]
34
+ # @raise [TimeoutError] if the child exceeds +timeout+
35
+ # @raise [BinaryNotFoundError] if the binary cannot be executed
36
+ def run(argv:, env:, cwd:, timeout:, stdin: nil)
37
+ spawn(argv, env, cwd) do |stdin_io, stdout_io, stderr_io, wait_thr|
38
+ out_reader = Thread.new { stdout_io.read }
39
+ err_reader = Thread.new { stderr_io.read }
40
+ write_stdin(stdin_io, stdin)
41
+
42
+ if wait_thr.join(timeout).nil?
43
+ terminate(wait_thr)
44
+ out_reader.kill
45
+ err_reader.kill
46
+ raise TimeoutError, "claude did not finish within #{timeout}s; the process was killed"
47
+ end
48
+
49
+ RunResult.new(
50
+ stdout: out_reader.value,
51
+ stderr: err_reader.value,
52
+ exit_status: wait_thr.value.exitstatus
53
+ )
54
+ end
55
+ end
56
+
57
+ # Run the command and yield each non-empty stdout line as it arrives.
58
+ #
59
+ # @param (see #run)
60
+ # @yieldparam line [String] one chomped, non-empty stdout line
61
+ # @return [RunResult] with +stdout+ nil (it was streamed, not captured)
62
+ # @raise [TimeoutError] if the child exceeds +timeout+
63
+ # @raise [BinaryNotFoundError] if the binary cannot be executed
64
+ def stream(argv:, env:, cwd:, timeout:, stdin: nil)
65
+ spawn(argv, env, cwd) do |stdin_io, stdout_io, stderr_io, wait_thr|
66
+ err_reader = Thread.new { stderr_io.read }
67
+ # Write stdin on its own thread so a prompt larger than the OS pipe
68
+ # buffer can't deadlock against stdout we haven't started reading yet.
69
+ writer = Thread.new { write_stdin(stdin_io, stdin) }
70
+ timed_out = false
71
+ watchdog = Thread.new do
72
+ sleep(timeout)
73
+ timed_out = true
74
+ terminate(wait_thr)
75
+ end
76
+
77
+ begin
78
+ stdout_io.each_line do |line|
79
+ chomped = line.chomp
80
+ yield chomped unless chomped.empty?
81
+ end
82
+ ensure
83
+ watchdog.kill
84
+ writer.join
85
+ end
86
+
87
+ raise TimeoutError, "claude streaming exceeded #{timeout}s; the process was killed" if timed_out
88
+
89
+ RunResult.new(stdout: nil, stderr: err_reader.value, exit_status: wait_thr.value.exitstatus)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def spawn(argv, env, cwd, &block)
96
+ validate_cwd!(cwd)
97
+ options = {}
98
+ options[:chdir] = cwd if cwd
99
+ Open3.popen3(env || {}, *argv, **options, &block)
100
+ rescue Errno::ENOENT
101
+ raise BinaryNotFoundError, binary_not_found_message(argv.first)
102
+ end
103
+
104
+ def validate_cwd!(cwd)
105
+ return if cwd.nil? || File.directory?(cwd)
106
+
107
+ raise Error, "working directory does not exist: #{cwd}"
108
+ end
109
+
110
+ def write_stdin(stdin_io, data)
111
+ stdin_io.write(data) if data
112
+ rescue Errno::EPIPE
113
+ # The child exited before reading stdin; the failure surfaces via status.
114
+ ensure
115
+ stdin_io.close unless stdin_io.closed?
116
+ end
117
+
118
+ # Send +SIGTERM+, wait up to {KILL_GRACE} for the child to exit, then
119
+ # escalate to +SIGKILL+ if it ignored the polite signal.
120
+ #
121
+ # This runs synchronously while holding +wait_thr+: the child's PID can't
122
+ # be reaped (and therefore can't be recycled by the OS) until we let go,
123
+ # so the +SIGKILL+ can never land on an unrelated, reused PID.
124
+ def terminate(wait_thr)
125
+ Process.kill("TERM", wait_thr.pid)
126
+ return if wait_thr.join(KILL_GRACE)
127
+
128
+ Process.kill("KILL", wait_thr.pid)
129
+ rescue Errno::ESRCH
130
+ # The process already exited.
131
+ end
132
+
133
+ def binary_not_found_message(binary)
134
+ "could not run #{binary.inspect}: is Claude Code installed and on your PATH?\n" \
135
+ "Install it with `npm install -g @anthropic-ai/claude-code`, then run `claude` " \
136
+ "and `/login` once to sign in with your Claude subscription."
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyClaude
4
+ # A multi-turn conversation.
5
+ #
6
+ # The first {#query} captures the +session_id+ from the reply; subsequent
7
+ # queries transparently pass +--resume <id>+ so the conversation continues.
8
+ class Session
9
+ # @return [String, nil] the session id being resumed (nil until the first
10
+ # reply, unless one was supplied to {Client#session})
11
+ attr_reader :id
12
+
13
+ # @param client [Client] the client used to run each turn
14
+ # @param id [String, nil] an existing session id to resume from the start
15
+ def initialize(client, id: nil)
16
+ @client = client
17
+ @id = id
18
+ @mutex = Mutex.new
19
+ end
20
+
21
+ # Ask a question within this conversation, resuming the captured session.
22
+ #
23
+ # @param prompt [String]
24
+ # @return [Response]
25
+ def query(prompt)
26
+ @mutex.synchronize do
27
+ response = @client.query(prompt, resume: @id)
28
+ @id = response.session_id || @id
29
+ response
30
+ end
31
+ end
32
+ alias ask query
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyClaude
4
+ # The released version of the gem, following Semantic Versioning.
5
+ VERSION = "0.0.0"
6
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruby_claude/version"
4
+ require_relative "ruby_claude/errors"
5
+ require_relative "ruby_claude/configuration"
6
+ require_relative "ruby_claude/response"
7
+ require_relative "ruby_claude/event"
8
+ require_relative "ruby_claude/command"
9
+ require_relative "ruby_claude/runner"
10
+ require_relative "ruby_claude/session"
11
+ require_relative "ruby_claude/client"
12
+
13
+ # Ruby Claude — a subscription-authenticated Ruby SDK that talks to Claude by
14
+ # shelling out to the Claude Code CLI (+claude -p+) in headless mode.
15
+ #
16
+ # It is an unofficial, community wrapper around a supported headless feature.
17
+ # By default it strips +ANTHROPIC_API_KEY+ from the child environment so calls
18
+ # draw on the logged-in Pro/Max subscription rather than API billing.
19
+ #
20
+ # @example One-shot
21
+ # puts RubyClaude.query("Summarize lib/foo.rb in two sentences")
22
+ #
23
+ # @example A configured client
24
+ # client = RubyClaude::Client.new(model: "claude-sonnet-4-6", timeout: 180)
25
+ # client.query("What does this project do?").text
26
+ module RubyClaude
27
+ class << self
28
+ # The global configuration used by {RubyClaude.query} and as the default
29
+ # for new {Client} instances.
30
+ #
31
+ # @return [Configuration]
32
+ def configuration
33
+ @configuration ||= Configuration.new
34
+ end
35
+
36
+ # Configure the global defaults.
37
+ #
38
+ # @yieldparam config [Configuration]
39
+ # @return [Configuration]
40
+ def configure
41
+ yield configuration if block_given?
42
+ @default_client = nil # rebuild with the new configuration on next use
43
+ configuration
44
+ end
45
+
46
+ # Reset all global state. Mainly useful in tests.
47
+ #
48
+ # @return [void]
49
+ def reset_configuration!
50
+ @configuration = Configuration.new
51
+ @default_client = nil
52
+ end
53
+
54
+ # One-shot convenience that delegates to a memoized default {Client}.
55
+ #
56
+ # @param prompt [String]
57
+ # @param options [Hash] forwarded to {Client#query} (e.g. +resume:+)
58
+ # @return [Response]
59
+ def query(prompt, **options)
60
+ default_client.query(prompt, **options)
61
+ end
62
+
63
+ # The memoized default {Client}, rebuilt whenever {configure} is called.
64
+ #
65
+ # @return [Client]
66
+ def default_client
67
+ @default_client ||= Client.new
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ruby_claude/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ruby_claude"
7
+ spec.version = RubyClaude::VERSION
8
+ spec.authors = ["Kaíque Kandy Koga"]
9
+ spec.email = ["kaique.koga@javln.com"]
10
+
11
+ spec.summary = "Subscription-authenticated Ruby SDK for Claude via the Claude Code CLI."
12
+ spec.description = <<~DESC
13
+ Ruby Claude is a small, dependency-light, idiomatic Ruby wrapper around the
14
+ Claude Code CLI in headless mode (claude -p). It lets Ruby programs talk to
15
+ Claude using a Claude Pro/Max subscription for authentication instead of an
16
+ Anthropic API key: by default it strips ANTHROPIC_API_KEY from the child
17
+ process environment so the CLI falls back to the logged-in subscription
18
+ credentials. Unofficial; uses a supported headless feature within the
19
+ subscription's rate limits.
20
+ DESC
21
+ spec.homepage = "https://github.com/kaiquekandykoga/ruby_claude"
22
+ spec.license = "BSD-3-Clause"
23
+ spec.required_ruby_version = ">= 3.2"
24
+
25
+ spec.metadata["source_code_uri"] = spec.homepage
26
+ spec.metadata["rubygems_mfa_required"] = "true"
27
+
28
+ spec.files = Dir.chdir(__dir__) do
29
+ `git ls-files -z`.split("\x0").select do |path|
30
+ path.start_with?("lib/") ||
31
+ %w[README.md LICENSE ruby_claude.gemspec].include?(path)
32
+ end
33
+ end
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_development_dependency "rake", "~> 13.0"
37
+ spec.add_development_dependency "rubocop", "~> 1.60"
38
+ spec.add_development_dependency "test-unit", "~> 3.6"
39
+ spec.add_development_dependency "yard", "~> 0.9"
40
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_claude
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaíque Kandy Koga
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.60'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.60'
40
+ - !ruby/object:Gem::Dependency
41
+ name: test-unit
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.6'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.6'
54
+ - !ruby/object:Gem::Dependency
55
+ name: yard
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.9'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.9'
68
+ description: |
69
+ Ruby Claude is a small, dependency-light, idiomatic Ruby wrapper around the
70
+ Claude Code CLI in headless mode (claude -p). It lets Ruby programs talk to
71
+ Claude using a Claude Pro/Max subscription for authentication instead of an
72
+ Anthropic API key: by default it strips ANTHROPIC_API_KEY from the child
73
+ process environment so the CLI falls back to the logged-in subscription
74
+ credentials. Unofficial; uses a supported headless feature within the
75
+ subscription's rate limits.
76
+ email:
77
+ - kaique.koga@javln.com
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - LICENSE
83
+ - README.md
84
+ - lib/ruby_claude.rb
85
+ - lib/ruby_claude/client.rb
86
+ - lib/ruby_claude/command.rb
87
+ - lib/ruby_claude/configuration.rb
88
+ - lib/ruby_claude/errors.rb
89
+ - lib/ruby_claude/event.rb
90
+ - lib/ruby_claude/response.rb
91
+ - lib/ruby_claude/runner.rb
92
+ - lib/ruby_claude/session.rb
93
+ - lib/ruby_claude/version.rb
94
+ - ruby_claude.gemspec
95
+ homepage: https://github.com/kaiquekandykoga/ruby_claude
96
+ licenses:
97
+ - BSD-3-Clause
98
+ metadata:
99
+ source_code_uri: https://github.com/kaiquekandykoga/ruby_claude
100
+ rubygems_mfa_required: 'true'
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '3.2'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 4.0.10
116
+ specification_version: 4
117
+ summary: Subscription-authenticated Ruby SDK for Claude via the Claude Code CLI.
118
+ test_files: []