codex-sdk 0.1.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: 3f275364a09a5cc846fa53ffd2b0a670a304fc16f69bb9e0b57f1e0e93fe1238
4
+ data.tar.gz: 060edb785c62c1d28ac68cfd572d0be71e549a55390d6de8b950876e73ff2b8b
5
+ SHA512:
6
+ metadata.gz: 788c559e4e8a03ae43e3f774c9bb58f3553d3dbf10c8140d95977cf6918cdb827f70074fe6cb67af0428981467337d0260974c7cd22677cc0ad044c55abca34a
7
+ data.tar.gz: fdf5fba234c30116e18bad26cc4004a3c3cafdb9425635f2c42c4baf9062b94bce82cc5b33e45d191c7198fdf8bd1fcedb3c70ee05bff5bf4633f473028cf1b9
data/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-10-20
9
+
10
+ ### Added
11
+ - Initial release of **unofficial** Ruby SDK for Codex CLI
12
+ - ⚠️ This is a community-maintained SDK, not officially supported
13
+ - Core `Codex` class for creating and resuming threads
14
+ - `Thread` class with `run()` and `run_streamed()` methods
15
+ - `ProcessManager` for spawning and managing Codex CLI process
16
+ - Configuration classes: `CodexOptions`, `ThreadOptions`, `TurnOptions`
17
+ - Event and item type constants
18
+ - Support for structured output with JSON schemas
19
+ - Sandbox mode support (read-only, workspace-write, danger-full-access)
20
+ - Multi-turn conversation support
21
+ - Thread persistence and resumption
22
+ - Platform-specific binary detection (macOS, Linux, Windows)
23
+ - Comprehensive test suite with RSpec
24
+ - Full documentation and examples
25
+
26
+ ### Features
27
+ - Zero runtime dependencies (uses only Ruby stdlib)
28
+ - Streaming and buffered response modes
29
+ - Automatic thread ID management
30
+ - Environment variable configuration
31
+ - Custom working directory support
32
+ - Git repository check bypass option
data/LICENSE ADDED
@@ -0,0 +1,222 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tao Luo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Note: This Ruby SDK wrapper is licensed under MIT. The Codex CLI binary that this
26
+ SDK wraps may have its own separate license. Please refer to the Codex CLI
27
+ documentation for information about the CLI binary's licensing.
28
+
29
+ ---
30
+
31
+ For reference, Apache License 2.0 text:
32
+
33
+ Apache License
34
+ Version 2.0, January 2004
35
+ http://www.apache.org/licenses/
36
+
37
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
38
+
39
+ 1. Definitions.
40
+
41
+ "License" shall mean the terms and conditions for use, reproduction,
42
+ and distribution as defined by Sections 1 through 9 of this document.
43
+
44
+ "Licensor" shall mean the copyright owner or entity authorized by
45
+ the copyright owner that is granting the License.
46
+
47
+ "Legal Entity" shall mean the union of the acting entity and all
48
+ other entities that control, are controlled by, or are under common
49
+ control with that entity. For the purposes of this definition,
50
+ "control" means (i) the power, direct or indirect, to cause the
51
+ direction or management of such entity, whether by contract or
52
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
53
+ outstanding shares, or (iii) beneficial ownership of such entity.
54
+
55
+ "You" (or "Your") shall mean an individual or Legal Entity
56
+ exercising permissions granted by this License.
57
+
58
+ "Source" form shall mean the preferred form for making modifications,
59
+ including but not limited to software source code, documentation
60
+ source, and configuration files.
61
+
62
+ "Object" form shall mean any form resulting from mechanical
63
+ transformation or translation of a Source form, including but
64
+ not limited to compiled object code, generated documentation,
65
+ and conversions to other media types.
66
+
67
+ "Work" shall mean the work of authorship, whether in Source or
68
+ Object form, made available under the License, as indicated by a
69
+ copyright notice that is included in or attached to the work
70
+ (an example is provided in the Appendix below).
71
+
72
+ "Derivative Works" shall mean any work, whether in Source or Object
73
+ form, that is based on (or derived from) the Work and for which the
74
+ editorial revisions, annotations, elaborations, or other modifications
75
+ represent, as a whole, an original work of authorship. For the purposes
76
+ of this License, Derivative Works shall not include works that remain
77
+ separable from, or merely link (or bind by name) to the interfaces of,
78
+ the Work and Derivative Works thereof.
79
+
80
+ "Contribution" shall mean any work of authorship, including
81
+ the original version of the Work and any modifications or additions
82
+ to that Work or Derivative Works thereof, that is intentionally
83
+ submitted to Licensor for inclusion in the Work by the copyright owner
84
+ or by an individual or Legal Entity authorized to submit on behalf of
85
+ the copyright owner. For the purposes of this definition, "submitted"
86
+ means any form of electronic, verbal, or written communication sent
87
+ to the Licensor or its representatives, including but not limited to
88
+ communication on electronic mailing lists, source code control systems,
89
+ and issue tracking systems that are managed by, or on behalf of, the
90
+ Licensor for the purpose of discussing and improving the Work, but
91
+ excluding communication that is conspicuously marked or otherwise
92
+ designated in writing by the copyright owner as "Not a Contribution."
93
+
94
+ "Contributor" shall mean Licensor and any individual or Legal Entity
95
+ on behalf of whom a Contribution has been received by Licensor and
96
+ subsequently incorporated within the Work.
97
+
98
+ 2. Grant of Copyright License. Subject to the terms and conditions of
99
+ this License, each Contributor hereby grants to You a perpetual,
100
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
101
+ copyright license to reproduce, prepare Derivative Works of,
102
+ publicly display, publicly perform, sublicense, and distribute the
103
+ Work and such Derivative Works in Source or Object form.
104
+
105
+ 3. Grant of Patent License. Subject to the terms and conditions of
106
+ this License, each Contributor hereby grants to You a perpetual,
107
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
108
+ (except as stated in this section) patent license to make, have made,
109
+ use, offer to sell, sell, import, and otherwise transfer the Work,
110
+ where such license applies only to those patent claims licensable
111
+ by such Contributor that are necessarily infringed by their
112
+ Contribution(s) alone or by combination of their Contribution(s)
113
+ with the Work to which such Contribution(s) was submitted. If You
114
+ institute patent litigation against any entity (including a
115
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
116
+ or a Contribution incorporated within the Work constitutes direct
117
+ or contributory patent infringement, then any patent licenses
118
+ granted to You under this License for that Work shall terminate
119
+ as of the date such litigation is filed.
120
+
121
+ 4. Redistribution. You may reproduce and distribute copies of the
122
+ Work or Derivative Works thereof in any medium, with or without
123
+ modifications, and in Source or Object form, provided that You
124
+ meet the following conditions:
125
+
126
+ (a) You must give any other recipients of the Work or
127
+ Derivative Works a copy of this License; and
128
+
129
+ (b) You must cause any modified files to carry prominent notices
130
+ stating that You changed the files; and
131
+
132
+ (c) You must retain, in the Source form of any Derivative Works
133
+ that You distribute, all copyright, patent, trademark, and
134
+ attribution notices from the Source form of the Work,
135
+ excluding those notices that do not pertain to any part of
136
+ the Derivative Works; and
137
+
138
+ (d) If the Work includes a "NOTICE" text file as part of its
139
+ distribution, then any Derivative Works that You distribute must
140
+ include a readable copy of the attribution notices contained
141
+ within such NOTICE file, excluding those notices that do not
142
+ pertain to any part of the Derivative Works, in at least one
143
+ of the following places: within a NOTICE text file distributed
144
+ as part of the Derivative Works; within the Source form or
145
+ documentation, if provided along with the Derivative Works; or,
146
+ within a display generated by the Derivative Works, if and
147
+ wherever such third-party notices normally appear. The contents
148
+ of the NOTICE file are for informational purposes only and
149
+ do not modify the License. You may add Your own attribution
150
+ notices within Derivative Works that You distribute, alongside
151
+ or as an addendum to the NOTICE text from the Work, provided
152
+ that such additional attribution notices cannot be construed
153
+ as modifying the License.
154
+
155
+ You may add Your own copyright statement to Your modifications and
156
+ may provide additional or different license terms and conditions
157
+ for use, reproduction, or distribution of Your modifications, or
158
+ for any such Derivative Works as a whole, provided Your use,
159
+ reproduction, and distribution of the Work otherwise complies with
160
+ the conditions stated in this License.
161
+
162
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
163
+ any Contribution intentionally submitted for inclusion in the Work
164
+ by You to the Licensor shall be under the terms and conditions of
165
+ this License, without any additional terms or conditions.
166
+ Notwithstanding the above, nothing herein shall supersede or modify
167
+ the terms of any separate license agreement you may have executed
168
+ with Licensor regarding such Contributions.
169
+
170
+ 6. Trademarks. This License does not grant permission to use the trade
171
+ names, trademarks, service marks, or product names of the Licensor,
172
+ except as required for reasonable and customary use in describing the
173
+ origin of the Work and reproducing the content of the NOTICE file.
174
+
175
+ 7. Disclaimer of Warranty. Unless required by applicable law or
176
+ agreed to in writing, Licensor provides the Work (and each
177
+ Contributor provides its Contributions) on an "AS IS" BASIS,
178
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
179
+ implied, including, without limitation, any warranties or conditions
180
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
181
+ PARTICULAR PURPOSE. You are solely responsible for determining the
182
+ appropriateness of using or redistributing the Work and assume any
183
+ risks associated with Your exercise of permissions under this License.
184
+
185
+ 8. Limitation of Liability. In no event and under no legal theory,
186
+ whether in tort (including negligence), contract, or otherwise,
187
+ unless required by applicable law (such as deliberate and grossly
188
+ negligent acts) or agreed to in writing, shall any Contributor be
189
+ liable to You for damages, including any direct, indirect, special,
190
+ incidental, or consequential damages of any character arising as a
191
+ result of this License or out of the use or inability to use the
192
+ Work (including but not limited to damages for loss of goodwill,
193
+ work stoppage, computer failure or malfunction, or any and all
194
+ other commercial damages or losses), even if such Contributor
195
+ has been advised of the possibility of such damages.
196
+
197
+ 9. Accepting Warranty or Additional Support. While redistributing
198
+ the Work or Derivative Works thereof, You may choose to offer,
199
+ and charge a fee for, acceptance of support, warranty, indemnity,
200
+ or other liability obligations and/or rights consistent with this
201
+ License. However, in accepting such obligations, You may act only
202
+ on Your own behalf and on Your sole responsibility, not on behalf
203
+ of any other Contributor, and only if You agree to indemnify,
204
+ defend, and hold each Contributor harmless for any liability
205
+ incurred by, or claims asserted against, such Contributor by reason
206
+ of your accepting any such warranty or additional liability.
207
+
208
+ END OF TERMS AND CONDITIONS
209
+
210
+ Copyright 2025 OpenAI
211
+
212
+ Licensed under the Apache License, Version 2.0 (the "License");
213
+ you may not use this file except in compliance with the License.
214
+ You may obtain a copy of the License at
215
+
216
+ http://www.apache.org/licenses/LICENSE-2.0
217
+
218
+ Unless required by applicable law or agreed to in writing, software
219
+ distributed under the License is distributed on an "AS IS" BASIS,
220
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
221
+ See the License for the specific language governing permissions and
222
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,367 @@
1
+ # Codex SDK for Ruby (Unofficial)
2
+
3
+ **⚠️ This is an unofficial, community-maintained Ruby SDK for the Codex CLI.**
4
+
5
+ This SDK wraps the Codex CLI binary to enable embedding the Codex agent in your Ruby workflows and apps. It is based on the official TypeScript SDK architecture but is not officially maintained.
6
+
7
+ ## Important Notes
8
+
9
+ - **Unofficial SDK**: This is a community project. See [DISCLAIMER.md](DISCLAIMER.md) for details.
10
+ - **Requires Codex CLI**: You must have the Codex CLI binary installed.
11
+ - **Community Maintained**: Contributions welcome, but no official support.
12
+
13
+ ## Installation
14
+
15
+ **Prerequisites**: Install the Codex CLI binary and ensure it's available in your PATH or specify its location.
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'codex-sdk'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```bash
26
+ bundle install
27
+ ```
28
+
29
+ Or install it yourself as:
30
+
31
+ ```bash
32
+ gem install codex-sdk
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```ruby
38
+ require 'codex_sdk'
39
+
40
+ # Create a Codex instance
41
+ codex = CodexSdk.new(api_key: ENV['CODEX_API_KEY'])
42
+
43
+ # Start a new conversation thread
44
+ thread = codex.start_thread(
45
+ working_directory: '/path/to/your/project'
46
+ )
47
+
48
+ # Run a turn and get the response
49
+ result = thread.run("Diagnose the test failure in my project")
50
+
51
+ puts result[:final_response]
52
+ # => "I found the issue in your test suite..."
53
+
54
+ puts "Items: #{result[:items].size}"
55
+ puts "Usage: #{result[:usage]}"
56
+ ```
57
+
58
+ ## Features
59
+
60
+ - **Multi-turn conversations** - Maintain context across multiple interactions
61
+ - **Thread persistence** - Resume conversations by thread ID
62
+ - **Streaming support** - Stream events as they arrive
63
+ - **Structured output** - Use JSON schemas for validated responses
64
+ - **Sandbox modes** - Control file system access levels
65
+ - **Zero dependencies** - Uses only Ruby standard library
66
+
67
+ ## Usage
68
+
69
+ ### Basic Usage
70
+
71
+ ```ruby
72
+ # Create a Codex client
73
+ codex = CodexSdk.new
74
+
75
+ # Start a thread
76
+ thread = codex.start_thread
77
+
78
+ # Send a message
79
+ result = thread.run("What is the purpose of this codebase?")
80
+ puts result[:final_response]
81
+ ```
82
+
83
+ ### Streaming Events
84
+
85
+ Stream events as they arrive for real-time updates:
86
+
87
+ ```ruby
88
+ thread = codex.start_thread
89
+
90
+ thread.run_streamed("Fix the bug in user authentication").each do |event|
91
+ case event['type']
92
+ when 'item.started'
93
+ puts "Started: #{event['item']['type']}"
94
+ when 'item.completed'
95
+ item = event['item']
96
+ puts "Completed: #{item['type']}"
97
+ puts "Content: #{item['text']}" if item['text']
98
+ when 'turn.completed'
99
+ puts "Turn completed. Usage: #{event['usage']}"
100
+ end
101
+ end
102
+ ```
103
+
104
+ ### Multi-turn Conversations
105
+
106
+ ```ruby
107
+ thread = codex.start_thread
108
+
109
+ # First turn
110
+ result1 = thread.run("What files handle user authentication?")
111
+ puts result1[:final_response]
112
+
113
+ # Second turn - maintains context
114
+ result2 = thread.run("Add two-factor authentication to those files")
115
+ puts result2[:final_response]
116
+
117
+ # Save thread ID for later
118
+ thread_id = thread.id
119
+ ```
120
+
121
+ ### Resuming Threads
122
+
123
+ ```ruby
124
+ # Resume a previous conversation
125
+ thread = codex.resume_thread("thread-abc-123")
126
+
127
+ # Continue the conversation
128
+ result = thread.run("What was my last request?")
129
+ puts result[:final_response]
130
+ ```
131
+
132
+ ### Structured Output with JSON Schema
133
+
134
+ ```ruby
135
+ schema = {
136
+ type: "object",
137
+ properties: {
138
+ summary: { type: "string" },
139
+ priority: { type: "string", enum: ["high", "medium", "low"] },
140
+ files: {
141
+ type: "array",
142
+ items: { type: "string" }
143
+ }
144
+ },
145
+ required: ["summary", "priority"]
146
+ }
147
+
148
+ result = thread.run(
149
+ "Analyze the critical issues in this codebase",
150
+ output_schema: schema
151
+ )
152
+
153
+ # Parse the structured response
154
+ data = JSON.parse(result[:final_response])
155
+ puts "Summary: #{data['summary']}"
156
+ puts "Priority: #{data['priority']}"
157
+ puts "Files: #{data['files'].join(', ')}"
158
+ ```
159
+
160
+ ### Configuration Options
161
+
162
+ #### Codex Options
163
+
164
+ ```ruby
165
+ codex = CodexSdk.new(
166
+ api_key: "sk-...", # API key (default: ENV['CODEX_API_KEY'])
167
+ base_url: "https://api.custom.com", # Custom API endpoint
168
+ codex_path_override: "/path/to/codex" # Custom binary path (for testing)
169
+ )
170
+ ```
171
+
172
+ #### Thread Options
173
+
174
+ ```ruby
175
+ thread = codex.start_thread(
176
+ model: "gpt-4", # Model to use
177
+ working_directory: "/path/to/dir", # Working directory
178
+ sandbox_mode: "workspace-write", # Sandbox mode
179
+ skip_git_repo_check: false # Skip git repository check
180
+ )
181
+ ```
182
+
183
+ **Sandbox Modes:**
184
+ - `read-only` - Read-only access to files
185
+ - `workspace-write` - Can write within workspace
186
+ - `danger-full-access` - Full file system access
187
+
188
+ ### Error Handling
189
+
190
+ ```ruby
191
+ begin
192
+ result = thread.run("Perform a complex task")
193
+ puts result[:final_response]
194
+ rescue CodexSdk::ThreadError => e
195
+ puts "Thread error: #{e.message}"
196
+ rescue CodexSdk::ProcessError => e
197
+ puts "Process error: #{e.message}"
198
+ rescue CodexSdk::ValidationError => e
199
+ puts "Validation error: #{e.message}"
200
+ end
201
+ ```
202
+
203
+ ## Event Types
204
+
205
+ The SDK emits the following event types during streaming:
206
+
207
+ - `thread.started` - Thread has been created
208
+ - `turn.started` - A new turn has started
209
+ - `item.started` - A new item is being generated
210
+ - `item.updated` - An item is being updated (streaming)
211
+ - `item.completed` - An item has been completed
212
+ - `turn.completed` - Turn completed successfully
213
+ - `turn.failed` - Turn failed with an error
214
+ - `error` - An error occurred
215
+
216
+ ## Item Types
217
+
218
+ Items represent different types of content in the conversation:
219
+
220
+ - `agent_message` - Message from the agent
221
+ - `reasoning` - Agent's reasoning/thinking
222
+ - `command_execution` - Command execution (bash, etc.)
223
+ - `file_change` - File modifications
224
+ - `mcp_tool_call` - MCP tool invocations
225
+ - `web_search` - Web search results
226
+ - `todo_list` - Task list updates
227
+ - `error` - Error information
228
+
229
+ ## Examples
230
+
231
+ ### Example 1: Code Review
232
+
233
+ ```ruby
234
+ codex = CodexSdk.new
235
+ thread = codex.start_thread(
236
+ working_directory: '/path/to/project',
237
+ sandbox_mode: 'read-only'
238
+ )
239
+
240
+ result = thread.run("Review the code in src/main.rb and suggest improvements")
241
+
242
+ result[:items].each do |item|
243
+ case item['type']
244
+ when 'agent_message'
245
+ puts "Review: #{item['text']}"
246
+ when 'reasoning'
247
+ puts "Thinking: #{item['text']}"
248
+ end
249
+ end
250
+ ```
251
+
252
+ ### Example 2: Automated Testing
253
+
254
+ ```ruby
255
+ codex = CodexSdk.new
256
+ thread = codex.start_thread(
257
+ working_directory: '/path/to/project',
258
+ sandbox_mode: 'workspace-write'
259
+ )
260
+
261
+ # Run tests
262
+ result = thread.run("Run the test suite and fix any failures")
263
+
264
+ if result[:final_response].include?("All tests passed")
265
+ puts "✓ Success!"
266
+ else
267
+ puts "Some tests failed, running again..."
268
+ thread.run("Try fixing the remaining test failures")
269
+ end
270
+ ```
271
+
272
+ ### Example 3: Documentation Generation
273
+
274
+ ```ruby
275
+ schema = {
276
+ type: "object",
277
+ properties: {
278
+ summary: { type: "string" },
279
+ api_endpoints: {
280
+ type: "array",
281
+ items: {
282
+ type: "object",
283
+ properties: {
284
+ path: { type: "string" },
285
+ method: { type: "string" },
286
+ description: { type: "string" }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ result = thread.run(
294
+ "Analyze the API endpoints and generate documentation",
295
+ output_schema: schema
296
+ )
297
+
298
+ docs = JSON.parse(result[:final_response])
299
+ puts "API Summary: #{docs['summary']}"
300
+ docs['api_endpoints'].each do |endpoint|
301
+ puts "#{endpoint['method']} #{endpoint['path']} - #{endpoint['description']}"
302
+ end
303
+ ```
304
+
305
+ ## Development
306
+
307
+ After checking out the repo, run:
308
+
309
+ ```bash
310
+ bundle install
311
+ ```
312
+
313
+ Run tests:
314
+
315
+ ```bash
316
+ bundle exec rspec
317
+ ```
318
+
319
+ Run linting:
320
+
321
+ ```bash
322
+ bundle exec rubocop
323
+ ```
324
+
325
+ Run both tests and linting:
326
+
327
+ ```bash
328
+ bundle exec rake check
329
+ ```
330
+
331
+ ## Platform Support
332
+
333
+ The SDK supports the following platforms:
334
+
335
+ - **macOS**: x86_64, ARM64 (Apple Silicon)
336
+ - **Linux**: x86_64, ARM64
337
+ - **Windows**: x86_64, ARM64
338
+
339
+ Platform-specific binaries are automatically selected at runtime.
340
+
341
+ ## Requirements
342
+
343
+ - Ruby >= 2.7.0
344
+ - No external dependencies (uses Ruby stdlib only)
345
+
346
+ ## Contributing
347
+
348
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ya-luotao/codex-sdk-ruby.
349
+
350
+ This is an unofficial SDK. For issues with the Codex CLI binary itself, please refer to the official Codex documentation.
351
+
352
+ ## License
353
+
354
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
355
+
356
+ ## Resources
357
+
358
+ - [Official TypeScript SDK](https://github.com/anthropics/anthropic-sdk-typescript) (Reference)
359
+ - [This Repository](https://github.com/ya-luotao/codex-sdk-ruby)
360
+
361
+ ## Support
362
+
363
+ **Note**: This is an unofficial SDK. For questions about this Ruby SDK:
364
+ - Open an issue on [GitHub](https://github.com/ya-luotao/codex-sdk-ruby/issues)
365
+
366
+ For questions about the Codex CLI itself:
367
+ - Refer to the official Codex CLI documentation
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'thread'
4
+ require_relative 'process_manager'
5
+ require_relative 'options'
6
+
7
+ module CodexSdk
8
+ # Main entry point for the Codex SDK
9
+ # Factory for creating and resuming threads
10
+ class Codex
11
+ def initialize(options = {})
12
+ @process_manager = ProcessManager.new(options[:codex_path_override])
13
+ @options = CodexOptions.new(options)
14
+ end
15
+
16
+ # Creates a new conversation thread
17
+ #
18
+ # @param options [Hash] Thread configuration options
19
+ # @option options [String] :model The model to use
20
+ # @option options [String] :sandbox_mode Sandbox mode (read-only, workspace-write, danger-full-access)
21
+ # @option options [String] :working_directory Working directory for the thread
22
+ # @option options [Boolean] :skip_git_repo_check Whether to skip git repo check
23
+ # @return [Thread] A new thread instance
24
+ def start_thread(options = {})
25
+ thread_options = ThreadOptions.new(options)
26
+ Thread.new(@process_manager, @options, thread_options)
27
+ end
28
+
29
+ # Resumes an existing conversation thread by ID
30
+ #
31
+ # @param thread_id [String] The thread ID to resume
32
+ # @param options [Hash] Thread configuration options
33
+ # @option options [String] :model The model to use
34
+ # @option options [String] :sandbox_mode Sandbox mode
35
+ # @option options [String] :working_directory Working directory for the thread
36
+ # @option options [Boolean] :skip_git_repo_check Whether to skip git repo check
37
+ # @return [Thread] A thread instance for the resumed thread
38
+ def resume_thread(thread_id, options = {})
39
+ thread_options = ThreadOptions.new(options)
40
+ Thread.new(@process_manager, @options, thread_options, id: thread_id)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodexSdk
4
+ # Base error class for all CodexSdk errors
5
+ class Error < StandardError; end
6
+
7
+ # Error raised when thread operations fail
8
+ class ThreadError < Error; end
9
+
10
+ # Error raised when process management fails
11
+ class ProcessError < Error; end
12
+
13
+ # Error raised when invalid options are provided
14
+ class InvalidOptionsError < Error; end
15
+
16
+ # Error raised when validation fails
17
+ class ValidationError < Error; end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodexSdk
4
+ # Event types emitted by the Codex CLI
5
+ module EventTypes
6
+ THREAD_STARTED = "thread.started"
7
+ TURN_STARTED = "turn.started"
8
+ TURN_COMPLETED = "turn.completed"
9
+ TURN_FAILED = "turn.failed"
10
+ ITEM_STARTED = "item.started"
11
+ ITEM_UPDATED = "item.updated"
12
+ ITEM_COMPLETED = "item.completed"
13
+ ERROR = "error"
14
+
15
+ ALL_TYPES = [
16
+ THREAD_STARTED,
17
+ TURN_STARTED,
18
+ TURN_COMPLETED,
19
+ TURN_FAILED,
20
+ ITEM_STARTED,
21
+ ITEM_UPDATED,
22
+ ITEM_COMPLETED,
23
+ ERROR
24
+ ].freeze
25
+ end
26
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodexSdk
4
+ # Item types that can appear in Codex conversations
5
+ module ItemTypes
6
+ AGENT_MESSAGE = "agent_message"
7
+ REASONING = "reasoning"
8
+ COMMAND_EXECUTION = "command_execution"
9
+ FILE_CHANGE = "file_change"
10
+ MCP_TOOL_CALL = "mcp_tool_call"
11
+ WEB_SEARCH = "web_search"
12
+ TODO_LIST = "todo_list"
13
+ ERROR = "error"
14
+
15
+ ALL_TYPES = [
16
+ AGENT_MESSAGE,
17
+ REASONING,
18
+ COMMAND_EXECUTION,
19
+ FILE_CHANGE,
20
+ MCP_TOOL_CALL,
21
+ WEB_SEARCH,
22
+ TODO_LIST,
23
+ ERROR
24
+ ].freeze
25
+ end
26
+
27
+ # Status values for command execution items
28
+ module CommandExecutionStatus
29
+ IN_PROGRESS = "in_progress"
30
+ COMPLETED = "completed"
31
+ FAILED = "failed"
32
+ end
33
+
34
+ # Kinds of file changes in patches
35
+ module PatchChangeKind
36
+ ADD = "add"
37
+ DELETE = "delete"
38
+ UPDATE = "update"
39
+ end
40
+
41
+ # Status values for patch apply operations
42
+ module PatchApplyStatus
43
+ COMPLETED = "completed"
44
+ FAILED = "failed"
45
+ end
46
+
47
+ # Status values for MCP tool calls
48
+ module McpToolCallStatus
49
+ IN_PROGRESS = "in_progress"
50
+ COMPLETED = "completed"
51
+ FAILED = "failed"
52
+ end
53
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodexSdk
4
+ # Configuration options for the Codex SDK
5
+ class CodexOptions
6
+ attr_reader :codex_path_override, :base_url, :api_key
7
+
8
+ def initialize(options = {})
9
+ @codex_path_override = options[:codex_path_override]
10
+ @base_url = options[:base_url]
11
+ @api_key = options[:api_key]
12
+ end
13
+ end
14
+
15
+ # Configuration options for a Codex thread
16
+ class ThreadOptions
17
+ attr_reader :model, :sandbox_mode, :working_directory, :skip_git_repo_check
18
+
19
+ # Valid sandbox modes
20
+ VALID_SANDBOX_MODES = %w[read-only workspace-write danger-full-access].freeze
21
+
22
+ def initialize(options = {})
23
+ @model = options[:model]
24
+ @sandbox_mode = options[:sandbox_mode]
25
+ @working_directory = options[:working_directory]
26
+ @skip_git_repo_check = options[:skip_git_repo_check] || false
27
+
28
+ validate_sandbox_mode if @sandbox_mode
29
+ end
30
+
31
+ private
32
+
33
+ def validate_sandbox_mode
34
+ return if VALID_SANDBOX_MODES.include?(@sandbox_mode)
35
+
36
+ raise InvalidOptionsError, "Invalid sandbox_mode: #{@sandbox_mode}. " \
37
+ "Must be one of: #{VALID_SANDBOX_MODES.join(', ')}"
38
+ end
39
+ end
40
+
41
+ # Configuration options for a single turn in a thread
42
+ class TurnOptions
43
+ attr_reader :output_schema
44
+
45
+ def initialize(options = {})
46
+ @output_schema = options[:output_schema]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'json'
5
+ require 'rbconfig'
6
+
7
+ module CodexSdk
8
+ # Manages spawning and communicating with the Codex CLI process
9
+ class ProcessManager
10
+ ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
11
+ SDK_ORIGINATOR = "codex_sdk_ruby"
12
+
13
+ def initialize(executable_path = nil)
14
+ @executable_path = executable_path || find_codex_path
15
+ end
16
+
17
+ # Runs the Codex CLI and yields JSONL events
18
+ #
19
+ # @param input [String] The user input to send to the CLI
20
+ # @param base_url [String, nil] Optional API base URL
21
+ # @param api_key [String, nil] Optional API key
22
+ # @param thread_id [String, nil] Optional thread ID for resumption
23
+ # @param model [String, nil] Optional model name
24
+ # @param sandbox_mode [String, nil] Optional sandbox mode
25
+ # @param working_directory [String, nil] Optional working directory
26
+ # @param skip_git_repo_check [Boolean] Whether to skip git repo check
27
+ # @param output_schema_file [String, nil] Optional path to output schema file
28
+ # @return [Enumerator] An enumerator of JSONL event strings
29
+ def run(input:, base_url: nil, api_key: nil, thread_id: nil, model: nil,
30
+ sandbox_mode: nil, working_directory: nil, skip_git_repo_check: false,
31
+ output_schema_file: nil)
32
+
33
+ Enumerator.new do |yielder|
34
+ command_args = build_command_args(
35
+ thread_id: thread_id,
36
+ model: model,
37
+ sandbox_mode: sandbox_mode,
38
+ working_directory: working_directory,
39
+ skip_git_repo_check: skip_git_repo_check,
40
+ output_schema_file: output_schema_file
41
+ )
42
+
43
+ env = build_environment(base_url: base_url, api_key: api_key)
44
+
45
+ Open3.popen3(env, @executable_path, *command_args) do |stdin, stdout, stderr, wait_thr|
46
+ stdin.write(input)
47
+ stdin.close
48
+
49
+ stderr_chunks = []
50
+
51
+ # Read stderr in separate thread to avoid deadlock
52
+ stderr_thread = Thread.new do
53
+ stderr.each_line { |line| stderr_chunks << line }
54
+ end
55
+
56
+ # Stream lines from stdout
57
+ stdout.each_line do |line|
58
+ yielder << line.chomp
59
+ end
60
+
61
+ stderr_thread.join
62
+
63
+ exit_status = wait_thr.value
64
+ unless exit_status.success?
65
+ stderr_output = stderr_chunks.join("")
66
+ raise ProcessError, "Codex process exited with code #{exit_status.exitstatus}: #{stderr_output}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def build_command_args(thread_id: nil, model: nil, sandbox_mode: nil,
75
+ working_directory: nil, skip_git_repo_check: false,
76
+ output_schema_file: nil)
77
+ args = ["exec", "--experimental-json"]
78
+
79
+ args.push("--model", model) if model
80
+ args.push("--sandbox", sandbox_mode) if sandbox_mode
81
+ args.push("--cd", working_directory) if working_directory
82
+ args.push("--skip-git-repo-check") if skip_git_repo_check
83
+ args.push("--output-schema", output_schema_file) if output_schema_file
84
+ args.push("resume", thread_id) if thread_id
85
+
86
+ args
87
+ end
88
+
89
+ def build_environment(base_url: nil, api_key: nil)
90
+ env = ENV.to_h.dup
91
+
92
+ env[ORIGINATOR_ENV] = SDK_ORIGINATOR unless env[ORIGINATOR_ENV]
93
+ env["OPENAI_BASE_URL"] = base_url if base_url
94
+ env["CODEX_API_KEY"] = api_key if api_key
95
+
96
+ env
97
+ end
98
+
99
+ def find_codex_path
100
+ platform = RUBY_PLATFORM
101
+ arch = RbConfig::CONFIG['host_cpu']
102
+
103
+ target_triple = case platform
104
+ when /linux/
105
+ case arch
106
+ when /x86_64/, /x64/
107
+ "x86_64-unknown-linux-musl"
108
+ when /aarch64/, /arm64/
109
+ "aarch64-unknown-linux-musl"
110
+ end
111
+ when /darwin/, /mac/
112
+ case arch
113
+ when /x86_64/, /x64/
114
+ "x86_64-apple-darwin"
115
+ when /aarch64/, /arm64/
116
+ "aarch64-apple-darwin"
117
+ end
118
+ when /win32/, /cygwin/
119
+ case arch
120
+ when /x86_64/, /x64/
121
+ "x86_64-pc-windows-msvc"
122
+ when /aarch64/, /arm64/
123
+ "aarch64-pc-windows-msvc"
124
+ end
125
+ end
126
+
127
+ raise ProcessError, "Unsupported platform: #{platform} (#{arch})" unless target_triple
128
+
129
+ # Determine binary name
130
+ binary_name = if platform.include?("win32") || platform.include?("cygwin")
131
+ "codex.exe"
132
+ else
133
+ "codex"
134
+ end
135
+
136
+ # Build path to bundled binary
137
+ gem_root = File.expand_path("../..", __dir__)
138
+ vendor_root = File.join(gem_root, "vendor")
139
+ arch_root = File.join(vendor_root, target_triple)
140
+ File.join(arch_root, "codex", binary_name)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'tempfile'
5
+ require 'fileutils'
6
+
7
+ module CodexSdk
8
+ # Represents a conversation thread with the Codex agent
9
+ class Thread
10
+ attr_reader :id
11
+
12
+ def initialize(process_manager, codex_options, thread_options, id: nil)
13
+ @process_manager = process_manager
14
+ @codex_options = codex_options
15
+ @thread_options = thread_options
16
+ @id = id
17
+ end
18
+
19
+ # Runs a turn and returns the aggregated result
20
+ #
21
+ # @param input [String] The user input message
22
+ # @param turn_options [Hash] Options for this turn
23
+ # @option turn_options [Hash] :output_schema JSON schema for structured output
24
+ # @return [Hash] Result containing :items, :final_response, and :usage
25
+ def run(input, turn_options = {})
26
+ items = []
27
+ final_response = ""
28
+ usage = nil
29
+ turn_failure = nil
30
+
31
+ run_streamed(input, turn_options).each do |event|
32
+ case event['type']
33
+ when 'item.completed'
34
+ item = event['item']
35
+ final_response = item['text'] if item['type'] == 'agent_message'
36
+ items << item
37
+ when 'turn.completed'
38
+ usage = event['usage']
39
+ when 'turn.failed'
40
+ turn_failure = event['error']
41
+ break
42
+ end
43
+ end
44
+
45
+ raise ThreadError, turn_failure['message'] if turn_failure
46
+
47
+ {
48
+ items: items,
49
+ final_response: final_response,
50
+ usage: usage
51
+ }
52
+ end
53
+
54
+ # Runs a turn and yields events as they arrive
55
+ #
56
+ # @param input [String] The user input message
57
+ # @param turn_options [Hash] Options for this turn
58
+ # @option turn_options [Hash] :output_schema JSON schema for structured output
59
+ # @return [Enumerator] An enumerator of event hashes
60
+ def run_streamed(input, turn_options = {})
61
+ Enumerator.new do |yielder|
62
+ schema_path = nil
63
+ cleanup_proc = nil
64
+
65
+ begin
66
+ schema_path, cleanup_proc = create_output_schema(turn_options[:output_schema])
67
+
68
+ @process_manager.run(
69
+ input: input,
70
+ base_url: @codex_options.base_url,
71
+ api_key: @codex_options.api_key,
72
+ thread_id: @id,
73
+ model: @thread_options.model,
74
+ sandbox_mode: @thread_options.sandbox_mode,
75
+ working_directory: @thread_options.working_directory,
76
+ skip_git_repo_check: @thread_options.skip_git_repo_check,
77
+ output_schema_file: schema_path
78
+ ).each do |line|
79
+ event = JSON.parse(line)
80
+
81
+ @id = event['thread_id'] if event['type'] == 'thread.started'
82
+
83
+ yielder << event
84
+ end
85
+ ensure
86
+ cleanup_proc&.call
87
+ end
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def create_output_schema(schema)
94
+ return [nil, nil] unless schema
95
+
96
+ unless schema.is_a?(Hash)
97
+ raise ValidationError, "outputSchema must be a plain JSON object"
98
+ end
99
+
100
+ temp_dir = Dir.mktmpdir("codex-output-schema-")
101
+ schema_path = File.join(temp_dir, "schema.json")
102
+
103
+ File.write(schema_path, JSON.generate(schema))
104
+
105
+ cleanup_proc = lambda do
106
+ FileUtils.rm_rf(temp_dir, secure: true) if Dir.exist?(temp_dir)
107
+ end
108
+
109
+ [schema_path, cleanup_proc]
110
+ rescue StandardError => e
111
+ FileUtils.rm_rf(temp_dir, secure: true) if temp_dir && Dir.exist?(temp_dir)
112
+ raise e
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodexSdk
4
+ VERSION = "0.1.0"
5
+ end
data/lib/codex_sdk.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'codex_sdk/version'
4
+ require_relative 'codex_sdk/errors'
5
+ require_relative 'codex_sdk/events'
6
+ require_relative 'codex_sdk/items'
7
+ require_relative 'codex_sdk/options'
8
+ require_relative 'codex_sdk/process_manager'
9
+ require_relative 'codex_sdk/thread'
10
+ require_relative 'codex_sdk/codex'
11
+
12
+ # Unofficial Ruby SDK for Codex CLI
13
+ #
14
+ # This is an unofficial, community-maintained SDK that wraps the Codex CLI binary.
15
+ # It is not officially maintained or supported.
16
+ #
17
+ # Based on the official TypeScript SDK architecture.
18
+ module CodexSdk
19
+ class << self
20
+ # Creates a new Codex instance
21
+ #
22
+ # @param options [Hash] Configuration options
23
+ # @option options [String] :api_key API key for authentication
24
+ # @option options [String] :base_url Base URL for the API
25
+ # @option options [String] :codex_path_override Path to codex binary (for testing)
26
+ # @return [Codex] A new Codex instance
27
+ def new(options = {})
28
+ Codex.new(options)
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: codex-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tao Luo
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-10-20 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ description: Unofficial Ruby SDK that wraps the Codex CLI binary. Embed the Codex
69
+ agent in your Ruby workflows and apps.
70
+ email:
71
+ - your-email@example.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG.md
77
+ - LICENSE
78
+ - README.md
79
+ - lib/codex_sdk.rb
80
+ - lib/codex_sdk/codex.rb
81
+ - lib/codex_sdk/errors.rb
82
+ - lib/codex_sdk/events.rb
83
+ - lib/codex_sdk/items.rb
84
+ - lib/codex_sdk/options.rb
85
+ - lib/codex_sdk/process_manager.rb
86
+ - lib/codex_sdk/thread.rb
87
+ - lib/codex_sdk/version.rb
88
+ homepage: https://github.com/ya-luotao/codex-sdk-ruby
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ homepage_uri: https://github.com/ya-luotao/codex-sdk-ruby
93
+ source_code_uri: https://github.com/ya-luotao/codex-sdk-ruby
94
+ changelog_uri: https://github.com/ya-luotao/codex-sdk-ruby/blob/main/CHANGELOG.md
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 2.7.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.6.2
110
+ specification_version: 4
111
+ summary: Unofficial Ruby SDK for Codex CLI
112
+ test_files: []