ruby-mana 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: d264315170eee3756db9dc603810cb524ce0a933f15b8897d36e7306c20d25b1
4
+ data.tar.gz: fc2f36ef0eceec87a7ff9911384db6ed9c6f004eee8c279fc379d3d2c36756fb
5
+ SHA512:
6
+ metadata.gz: be603cd3ddfcc6e457b656df9caff5ba8a9de4252dbb74416879daead34c64d7b4c50ad6ea422eaa21b76b92acc780e93b54425250e369e3f14223c0c82fee03
7
+ data.tar.gz: ae6ea7cb12475dded7d5f6cf1c1f73acfb2bc16b592741cd935b9406d118516e1530651095310bc791d436f0039991936e8074da053f629240f0ec5b2c396143
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - Unreleased
4
+
5
+ - Initial release
6
+ - `~"..."` syntax for embedding LLM prompts in Ruby
7
+ - Effect system: read_var, write_var, read_attr, write_attr, call_func
8
+ - Anthropic Claude backend
9
+ - Variable binding via `<var>` convention
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 carlnoah6
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.
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # ruby-mana 🔮
2
+
3
+ Embed LLM as native Ruby. Write natural language, it just runs.
4
+
5
+ ```ruby
6
+ require "mana"
7
+
8
+ numbers = [1, "2", "three", "cuatro", "五"]
9
+ ~"compute the semantic average of <numbers> and store in <result>"
10
+ puts result # => 3.0
11
+ ```
12
+
13
+ ## What is this?
14
+
15
+ Mana turns LLM into a Ruby co-processor. Your natural language strings can read and write Ruby variables, call Ruby functions, manipulate objects, and control program flow — all from a single `~"..."`.
16
+
17
+ Not an API wrapper. Not prompt formatting. Mana weaves LLM into your Ruby code as a first-class construct.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ gem install ruby-mana
23
+ ```
24
+
25
+ Or in your Gemfile:
26
+
27
+ ```ruby
28
+ gem "ruby-mana"
29
+ ```
30
+
31
+ Requires Ruby 3.3+ and an Anthropic API key:
32
+
33
+ ```bash
34
+ export ANTHROPIC_API_KEY=your_key_here
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ Prefix any string with `~` to make it an LLM prompt:
40
+
41
+ ```ruby
42
+ require "mana"
43
+
44
+ numbers = [1, 2, 3, 4, 5]
45
+ ~"compute the average of <numbers> and store in <result>"
46
+ puts result
47
+ ```
48
+
49
+ ### Variables
50
+
51
+ Use `<var>` to reference variables. Mana figures out read vs write:
52
+
53
+ - Variable exists in scope → Mana reads it and passes to LLM
54
+ - Variable doesn't exist → LLM creates it via `write_var`
55
+
56
+ ```ruby
57
+ name = "Alice"
58
+ scores = [85, 92, 78, 95, 88]
59
+
60
+ ~"analyze <scores> for <name>, store the mean in <average>, the highest in <best>, and a short comment in <comment>"
61
+
62
+ puts average # => 87.6
63
+ puts best # => 95
64
+ puts comment # => "Excellent and consistent performance"
65
+ ```
66
+
67
+ ### Object manipulation
68
+
69
+ LLM can read and write object attributes:
70
+
71
+ ```ruby
72
+ class Email
73
+ attr_accessor :subject, :body, :category, :priority
74
+ end
75
+
76
+ email = Email.new
77
+ email.subject = "URGENT: Server down"
78
+ email.body = "Database connection pool exhausted..."
79
+
80
+ ~"read <email> subject and body, then set its category and priority"
81
+
82
+ puts email.category # => "urgent"
83
+ puts email.priority # => "high"
84
+ ```
85
+
86
+ ### Calling Ruby functions
87
+
88
+ LLM can call functions in your scope:
89
+
90
+ ```ruby
91
+ def fetch_price(symbol)
92
+ { "AAPL" => 189.5, "GOOG" => 141.2, "TSLA" => 248.9 }[symbol] || 0
93
+ end
94
+
95
+ def send_alert(msg)
96
+ puts "[ALERT] #{msg}"
97
+ end
98
+
99
+ portfolio = ["AAPL", "GOOG", "TSLA", "MSFT"]
100
+
101
+ ~"iterate <portfolio>, call fetch_price for each, send_alert if price > 200, store the sum in <total>"
102
+ puts total # => 579.6
103
+ ```
104
+
105
+ ### Mixed control flow
106
+
107
+ Ruby handles the structure, LLM handles the decisions:
108
+
109
+ ```ruby
110
+ player_hp = 100
111
+ enemy_hp = 80
112
+ inventory = ["sword", "potion", "shield"]
113
+
114
+ while player_hp > 0 && enemy_hp > 0
115
+ ~"player HP=<player_hp>, enemy HP=<enemy_hp>, inventory=<inventory>, choose an action and store in <action>"
116
+
117
+ case action
118
+ when "attack" then enemy_hp -= rand(15..25)
119
+ when "defend" then nil
120
+ when "use_item"
121
+ ~"pick a healing item from <inventory> and store its name in <item_name>"
122
+ inventory.delete(item_name)
123
+ player_hp += 25
124
+ end
125
+
126
+ player_hp -= action == "defend" ? rand(5..10) : rand(10..20)
127
+ end
128
+ ```
129
+
130
+ ## Configuration
131
+
132
+ ```ruby
133
+ Mana.configure do |c|
134
+ c.model = "claude-sonnet-4-20250514"
135
+ c.temperature = 0
136
+ c.api_key = ENV["ANTHROPIC_API_KEY"]
137
+ c.max_iterations = 50
138
+ end
139
+
140
+ # Or shorthand
141
+ Mana.model = "claude-sonnet-4-20250514"
142
+ ```
143
+
144
+ ## How it works
145
+
146
+ 1. `~"..."` calls `String#~@`, which captures the caller's `Binding`
147
+ 2. Mana parses `<var>` references and reads existing variables as context
148
+ 3. The prompt + context is sent to the LLM with tools: `read_var`, `write_var`, `read_attr`, `write_attr`, `call_func`, `done`
149
+ 4. LLM responds with tool calls → Mana executes them against the live Ruby binding → sends results back
150
+ 5. Loop until LLM calls `done` or returns without tool calls
151
+
152
+ ## Safety
153
+
154
+ ⚠️ Mana executes LLM-generated operations against your live Ruby state. Use with the same caution as `eval`.
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ class Config
5
+ attr_accessor :model, :temperature, :api_key, :max_iterations, :base_url
6
+
7
+ def initialize
8
+ @model = "claude-sonnet-4-20250514"
9
+ @temperature = 0
10
+ @api_key = ENV["ANTHROPIC_API_KEY"]
11
+ @max_iterations = 50
12
+ @base_url = "https://api.anthropic.com"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ module Effects
5
+ ReadVar = Struct.new(:name)
6
+ WriteVar = Struct.new(:name, :value)
7
+ ReadAttr = Struct.new(:obj_name, :attr)
8
+ WriteAttr = Struct.new(:obj_name, :attr, :value)
9
+ CallFunc = Struct.new(:name, :args)
10
+ Done = Struct.new(:result)
11
+ end
12
+ end
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Mana
8
+ class Engine
9
+ TOOLS = [
10
+ {
11
+ name: "read_var",
12
+ description: "Read a variable value from the Ruby scope.",
13
+ input_schema: {
14
+ type: "object",
15
+ properties: { name: { type: "string", description: "Variable name" } },
16
+ required: ["name"]
17
+ }
18
+ },
19
+ {
20
+ name: "write_var",
21
+ description: "Write a value to a variable in the Ruby scope. Creates the variable if it doesn't exist.",
22
+ input_schema: {
23
+ type: "object",
24
+ properties: {
25
+ name: { type: "string", description: "Variable name" },
26
+ value: { description: "Value to assign (any JSON type)" }
27
+ },
28
+ required: %w[name value]
29
+ }
30
+ },
31
+ {
32
+ name: "read_attr",
33
+ description: "Read an attribute from a Ruby object.",
34
+ input_schema: {
35
+ type: "object",
36
+ properties: {
37
+ obj: { type: "string", description: "Variable name holding the object" },
38
+ attr: { type: "string", description: "Attribute name to read" }
39
+ },
40
+ required: %w[obj attr]
41
+ }
42
+ },
43
+ {
44
+ name: "write_attr",
45
+ description: "Set an attribute on a Ruby object.",
46
+ input_schema: {
47
+ type: "object",
48
+ properties: {
49
+ obj: { type: "string", description: "Variable name holding the object" },
50
+ attr: { type: "string", description: "Attribute name to set" },
51
+ value: { description: "Value to assign" }
52
+ },
53
+ required: %w[obj attr value]
54
+ }
55
+ },
56
+ {
57
+ name: "call_func",
58
+ description: "Call a Ruby method/function available in the current scope.",
59
+ input_schema: {
60
+ type: "object",
61
+ properties: {
62
+ name: { type: "string", description: "Function/method name" },
63
+ args: { type: "array", description: "Arguments to pass", items: {} }
64
+ },
65
+ required: ["name"]
66
+ }
67
+ },
68
+ {
69
+ name: "done",
70
+ description: "Signal that the task is complete.",
71
+ input_schema: {
72
+ type: "object",
73
+ properties: {
74
+ result: { description: "Optional return value" }
75
+ }
76
+ }
77
+ }
78
+ ].freeze
79
+
80
+ class << self
81
+ def run(prompt, caller_binding)
82
+ new(prompt, caller_binding).execute
83
+ end
84
+
85
+ def handler_stack
86
+ Thread.current[:mana_handlers] ||= []
87
+ end
88
+
89
+ def with_handler(handler = nil, **opts, &block)
90
+ handler_stack.push(handler)
91
+ block.call
92
+ ensure
93
+ handler_stack.pop
94
+ end
95
+ end
96
+
97
+ def initialize(prompt, caller_binding)
98
+ @prompt = prompt
99
+ @binding = caller_binding
100
+ @config = Mana.config
101
+ end
102
+
103
+ def execute
104
+ context = build_context(@prompt)
105
+ system_prompt = build_system_prompt(context)
106
+ messages = [{ role: "user", content: @prompt }]
107
+
108
+ iterations = 0
109
+ done_result = nil
110
+
111
+ loop do
112
+ iterations += 1
113
+ raise MaxIterationsError, "exceeded #{@config.max_iterations} iterations" if iterations > @config.max_iterations
114
+
115
+ response = llm_call(system_prompt, messages)
116
+ tool_uses = extract_tool_uses(response)
117
+
118
+ break if tool_uses.empty?
119
+
120
+ # Append assistant message
121
+ messages << { role: "assistant", content: response }
122
+
123
+ # Process each tool use
124
+ tool_results = tool_uses.map do |tu|
125
+ result = handle_effect(tu)
126
+ done_result = tu[:input]["result"] if tu[:name] == "done"
127
+ { type: "tool_result", tool_use_id: tu[:id], content: result.to_s }
128
+ end
129
+
130
+ messages << { role: "user", content: tool_results }
131
+
132
+ break if tool_uses.any? { |t| t[:name] == "done" }
133
+ end
134
+
135
+ done_result
136
+ end
137
+
138
+ private
139
+
140
+ # --- Context Building ---
141
+
142
+ def build_context(prompt)
143
+ var_names = prompt.scan(/<(\w+)>/).flatten.uniq
144
+ ctx = {}
145
+ var_names.each do |name|
146
+ val = resolve(name)
147
+ ctx[name] = serialize_value(val)
148
+ rescue NameError
149
+ # Variable doesn't exist yet — will be created by LLM
150
+ end
151
+ ctx
152
+ end
153
+
154
+ def build_system_prompt(context)
155
+ parts = [
156
+ "You are embedded inside a Ruby program. You interact with the program's live state using the provided tools.",
157
+ "",
158
+ "Rules:",
159
+ "- Use read_var / read_attr to inspect variables and objects.",
160
+ "- Use write_var to create or update variables in the Ruby scope.",
161
+ "- Use write_attr to set attributes on Ruby objects.",
162
+ "- Use call_func to call Ruby methods available in scope.",
163
+ "- Call done when the task is complete.",
164
+ "- When the user references <var>, that's a variable in scope.",
165
+ "- If a referenced variable doesn't exist yet, the user expects you to create it with write_var.",
166
+ "- Be precise with types: use numbers for numeric values, arrays for lists, strings for text."
167
+ ]
168
+
169
+ unless context.empty?
170
+ parts << ""
171
+ parts << "Current variable values:"
172
+ context.each { |k, v| parts << " #{k} = #{v}" }
173
+ end
174
+
175
+ parts.join("\n")
176
+ end
177
+
178
+ # --- Effect Handling ---
179
+
180
+ def handle_effect(tool_use)
181
+ name = tool_use[:name]
182
+ input = tool_use[:input] || {}
183
+ # Normalize keys to strings for consistent access
184
+ input = input.transform_keys(&:to_s) if input.is_a?(Hash)
185
+
186
+ # Check handler stack first
187
+ handler = self.class.handler_stack.last
188
+ return handler.call(name, input) if handler && handler.respond_to?(:call)
189
+
190
+ case name
191
+ when "read_var"
192
+ serialize_value(resolve(input["name"]))
193
+
194
+ when "write_var"
195
+ var_name = input["name"]
196
+ value = input["value"]
197
+ write_local(var_name, value)
198
+ "ok: #{var_name} = #{value.inspect}"
199
+
200
+ when "read_attr"
201
+ obj = resolve(input["obj"])
202
+ validate_name!(input["attr"])
203
+ serialize_value(obj.public_send(input["attr"]))
204
+
205
+ when "write_attr"
206
+ obj = resolve(input["obj"])
207
+ validate_name!(input["attr"])
208
+ obj.public_send("#{input['attr']}=", input["value"])
209
+ "ok: #{input['obj']}.#{input['attr']} = #{input['value'].inspect}"
210
+
211
+ when "call_func"
212
+ func = input["name"]
213
+ validate_name!(func)
214
+ args = input["args"] || []
215
+ result = @binding.receiver.method(func.to_sym).call(*args)
216
+ serialize_value(result)
217
+
218
+ when "done"
219
+ input["result"].to_s
220
+
221
+ else
222
+ "error: unknown tool #{name}"
223
+ end
224
+ rescue => e
225
+ "error: #{e.class}: #{e.message}"
226
+ end
227
+
228
+ # --- Binding Helpers ---
229
+
230
+ VALID_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
231
+
232
+ def validate_name!(name)
233
+ raise Mana::Error, "invalid identifier: #{name.inspect}" unless name.match?(VALID_IDENTIFIER)
234
+ end
235
+
236
+ def resolve(name)
237
+ validate_name!(name)
238
+ if @binding.local_variable_defined?(name.to_sym)
239
+ @binding.local_variable_get(name.to_sym)
240
+ elsif @binding.receiver.respond_to?(name.to_sym, true)
241
+ @binding.receiver.send(name.to_sym)
242
+ else
243
+ raise NameError, "undefined variable or method '#{name}'"
244
+ end
245
+ end
246
+
247
+ def write_local(name, value)
248
+ validate_name!(name)
249
+ @binding.local_variable_set(name.to_sym, value)
250
+ end
251
+
252
+ def serialize_value(val)
253
+ case val
254
+ when String, Integer, Float, TrueClass, FalseClass, NilClass
255
+ val.inspect
256
+ when Symbol
257
+ val.to_s.inspect
258
+ when Array
259
+ "[#{val.map { |v| serialize_value(v) }.join(', ')}]"
260
+ when Hash
261
+ pairs = val.map { |k, v| "#{serialize_value(k)} => #{serialize_value(v)}" }
262
+ "{#{pairs.join(', ')}}"
263
+ else
264
+ ivars = val.instance_variables
265
+ obj_repr = ivars.map do |ivar|
266
+ attr_name = ivar.to_s.delete_prefix("@")
267
+ "#{attr_name}: #{val.instance_variable_get(ivar).inspect}" rescue nil
268
+ end.compact.join(", ")
269
+ "#<#{val.class} #{obj_repr}>"
270
+ end
271
+ end
272
+
273
+ # --- LLM Client ---
274
+
275
+ def llm_call(system, messages)
276
+ uri = URI("#{@config.base_url}/v1/messages")
277
+ body = {
278
+ model: @config.model,
279
+ max_tokens: 4096,
280
+ system: system,
281
+ tools: TOOLS,
282
+ messages: messages
283
+ }
284
+
285
+ http = Net::HTTP.new(uri.host, uri.port)
286
+ http.use_ssl = uri.scheme == "https"
287
+ http.read_timeout = 120
288
+
289
+ req = Net::HTTP::Post.new(uri)
290
+ req["Content-Type"] = "application/json"
291
+ req["x-api-key"] = @config.api_key
292
+ req["anthropic-version"] = "2023-06-01"
293
+ req.body = JSON.generate(body)
294
+
295
+ res = http.request(req)
296
+ raise LLMError, "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
297
+
298
+ parsed = JSON.parse(res.body, symbolize_names: true)
299
+ parsed[:content] || []
300
+ end
301
+
302
+ def extract_tool_uses(content)
303
+ return [] unless content.is_a?(Array)
304
+
305
+ content
306
+ .select { |block| block[:type] == "tool_use" }
307
+ .map { |block| { id: block[:id], name: block[:name], input: block[:input] || {} } }
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module Mana
8
+ module LLM
9
+ class Anthropic < Base
10
+ API_URL = "https://api.anthropic.com/v1/messages"
11
+ API_VERSION = "2023-06-01"
12
+
13
+ def initialize(config = Mana.config)
14
+ super(config)
15
+ @api_key = config.api_key
16
+ @model = config.model
17
+ end
18
+
19
+ def chat(system:, messages:, tools:)
20
+ raise Mana::Error, "Anthropic API key not set" unless @api_key
21
+
22
+ body = {
23
+ model: @model,
24
+ max_tokens: 4096,
25
+ temperature: @config.temperature,
26
+ system: system,
27
+ messages: messages,
28
+ tools: tools
29
+ }
30
+
31
+ response = post(body)
32
+
33
+ unless response.is_a?(Net::HTTPSuccess)
34
+ parsed = JSON.parse(response.body) rescue nil
35
+ error_msg = parsed&.dig("error", "message") || response.body
36
+ raise Mana::Error, "Anthropic API error (#{response.code}): #{error_msg}"
37
+ end
38
+
39
+ parsed = JSON.parse(response.body)
40
+ parsed["content"].map { |block| symbolize_keys(block) }
41
+ end
42
+
43
+ private
44
+
45
+ def post(body)
46
+ uri = URI(API_URL)
47
+ http = Net::HTTP.new(uri.host, uri.port)
48
+ http.use_ssl = true
49
+ http.open_timeout = 30
50
+ http.read_timeout = 120
51
+ http.write_timeout = 30
52
+
53
+ request = Net::HTTP::Post.new(uri)
54
+ request["x-api-key"] = @api_key
55
+ request["anthropic-version"] = API_VERSION
56
+ request["content-type"] = "application/json"
57
+ request.body = JSON.generate(body)
58
+
59
+ http.request(request)
60
+ end
61
+
62
+ def symbolize_keys(hash)
63
+ hash.each_with_object({}) do |(k, v), acc|
64
+ acc[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ module LLM
5
+ # Base interface for LLM clients.
6
+ # Subclass and implement #chat to add new providers.
7
+ class Base
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ # Send a chat request with tools.
13
+ # Returns an array of content blocks (tool_use / text).
14
+ def chat(system:, messages:, tools:)
15
+ raise NotImplementedError, "#{self.class}#chat not implemented"
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/mana/mixin.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ # Include in classes to use ~"..." in instance methods.
5
+ # binding_of_caller handles scope automatically, so this
6
+ # is mainly a semantic marker + future extension point.
7
+ module Mixin
8
+ def self.included(base)
9
+ # Reserved for future: auto-expose methods, etc.
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "binding_of_caller"
4
+
5
+ class String
6
+ # ~"natural language prompt" → execute via Mana engine
7
+ def ~@
8
+ Mana::Engine.run(self, binding.of_caller(1))
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ VERSION = "0.1.0"
5
+ end
data/lib/mana.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mana/version"
4
+ require_relative "mana/config"
5
+ require_relative "mana/effects"
6
+ require_relative "mana/engine"
7
+ require_relative "mana/string_ext"
8
+ require_relative "mana/mixin"
9
+
10
+ module Mana
11
+ class Error < StandardError; end
12
+ class MaxIterationsError < Error; end
13
+ class LLMError < Error; end
14
+
15
+ class << self
16
+ def config
17
+ @config ||= Config.new
18
+ end
19
+
20
+ def configure
21
+ yield(config) if block_given?
22
+ config
23
+ end
24
+
25
+ def model=(model)
26
+ config.model = model
27
+ end
28
+
29
+ def handle(handler = nil, **opts, &block)
30
+ Engine.with_handler(handler, **opts, &block)
31
+ end
32
+
33
+ def reset!
34
+ @config = Config.new
35
+ end
36
+ end
37
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-mana
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Carl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: binding_of_caller
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ description: |
28
+ Mana lets you write natural language strings in Ruby that execute via LLM
29
+ with full access to your program's live state. Read/write variables, call
30
+ functions, manipulate objects — all from a simple ~"..." syntax.
31
+ email:
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - CHANGELOG.md
37
+ - LICENSE
38
+ - README.md
39
+ - lib/mana.rb
40
+ - lib/mana/config.rb
41
+ - lib/mana/effects.rb
42
+ - lib/mana/engine.rb
43
+ - lib/mana/llm/anthropic.rb
44
+ - lib/mana/llm/base.rb
45
+ - lib/mana/mixin.rb
46
+ - lib/mana/string_ext.rb
47
+ - lib/mana/version.rb
48
+ homepage: https://github.com/carlnoah6/ruby-mana
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/carlnoah6/ruby-mana
53
+ source_code_uri: https://github.com/carlnoah6/ruby-mana
54
+ changelog_uri: https://github.com/carlnoah6/ruby-mana/blob/main/CHANGELOG.md
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.3.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.4.20
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Embed LLM as native Ruby — write natural language, it just runs
74
+ test_files: []