nunki 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.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nunki
4
+ module Providers
5
+ class Anthropic < Provider
6
+ def self.headers(api_key)
7
+ api_key ? {"x-api-key" => Protocol.string(api_key, "api_key", empty: false, max: 8192)} : {}
8
+ end
9
+
10
+ def initialize(client:, model:, api_version: "2023-06-01", **options)
11
+ super(client: client, model: model, **options)
12
+ @api_version = Protocol.string(api_version, "api_version", empty: false, max: 64)
13
+ end
14
+
15
+ private
16
+
17
+ def perform_complete(prepared)
18
+ messages, tools, system, max_tokens = prepared
19
+ payload = {model: @model, messages: messages.map { |message| encode_message(message) }, max_tokens: max_tokens, stream: true}
20
+ payload[:system] = system if system
21
+ payload[:tools] = tools.map { |tool| encode_tool(tool) } unless tools.empty?
22
+ state = {parts: [], active: {}, usage: [0, 0], finish: nil}
23
+
24
+ result = @client.post_json(payload, headers: {
25
+ "Accept" => "text/event-stream, application/json",
26
+ "anthropic-version" => @api_version
27
+ }) do |event|
28
+ next if event.data.empty?
29
+ consume_stream(parse_event(event), state) { |part| yield part if block_given? }
30
+ end
31
+ return consume_response(result.body, messages) unless result.body.empty?
32
+
33
+ usage = state[:usage]
34
+ usage[0] = Nunki.estimate_tokens(messages) if usage[0].zero?
35
+ usage[1] = Nunki.estimate_tokens(state[:parts]) if usage[1].zero?
36
+ response(state[:parts], usage, state[:finish])
37
+ end
38
+
39
+ def consume_stream(message, state)
40
+ case message["type"]
41
+ when "message_start"
42
+ usage = Protocol.object(Protocol.object(message["message"], "message")["usage"], "usage")
43
+ state[:usage][0] = Protocol.uint(usage.fetch("input_tokens", 0), "input tokens")
44
+ when "content_block_start"
45
+ start_block(message, state) { |part| yield part }
46
+ when "content_block_delta"
47
+ consume_delta(message, state) { |part| yield part }
48
+ when "content_block_stop"
49
+ finish_block(message, state) { |part| yield part }
50
+ when "message_delta"
51
+ delta = Protocol.object(message["delta"], "message delta")
52
+ state[:finish] = Protocol.string(delta["stop_reason"], "stop reason") if delta["stop_reason"]
53
+ usage = Protocol.object(message.fetch("usage", {}), "usage")
54
+ state[:usage][1] = Protocol.uint(usage["output_tokens"], "output tokens") if usage["output_tokens"]
55
+ when "error"
56
+ error = Protocol.object(message["error"], "error")
57
+ raise Error, Protocol.string(error["message"], "error message")
58
+ end
59
+ end
60
+
61
+ def start_block(message, state)
62
+ index = Protocol.uint(message["index"], "content index")
63
+ block = Protocol.object(message["content_block"], "content block")
64
+ case block["type"]
65
+ when "text"
66
+ text = Protocol.string(block.fetch("text", ""), "text")
67
+ state[:active][index] = {type: :text, text: +text}
68
+ yield text_part(text) unless text.empty?
69
+ when "tool_use"
70
+ state[:active][index] = {
71
+ type: :tool_use,
72
+ id: Protocol.string(block["id"], "tool id", empty: false),
73
+ name: Protocol.string(block["name"], "tool name", empty: false),
74
+ input: block["input"], json: +""
75
+ }
76
+ end
77
+ end
78
+
79
+ def consume_delta(message, state)
80
+ index = Protocol.uint(message["index"], "content index")
81
+ block = state[:active][index]
82
+ return unless block
83
+ delta = Protocol.object(message["delta"], "content delta")
84
+ if delta["type"] == "text_delta"
85
+ text = Protocol.string(delta["text"], "text delta")
86
+ block[:text] << text
87
+ yield text_part(text)
88
+ elsif delta["type"] == "input_json_delta"
89
+ block[:json] << Protocol.string(delta["partial_json"], "tool input delta")
90
+ end
91
+ end
92
+
93
+ def finish_block(message, state)
94
+ block = state[:active].delete(Protocol.uint(message["index"], "content index"))
95
+ return unless block
96
+ if block[:type] == :text
97
+ state[:parts] << text_part(block[:text]) unless block[:text].empty?
98
+ else
99
+ input = block[:json].empty? ? Protocol.object(block[:input], "tool input") :
100
+ Protocol.object(Protocol.parse(block[:json], "tool input"), "tool input")
101
+ part = tool_part(block[:name], input, block[:id])
102
+ state[:parts] << part
103
+ yield part
104
+ end
105
+ end
106
+
107
+ def consume_response(body, messages)
108
+ value = Protocol.object(Protocol.parse(body), "response")
109
+ parts = Protocol.collection(value["content"], "content").map { |part| decode_part(part) }
110
+ usage = Protocol.object(value.fetch("usage", {}), "usage")
111
+ counts = [
112
+ usage["input_tokens"] ? Protocol.uint(usage["input_tokens"], "input tokens") : Nunki.estimate_tokens(messages),
113
+ usage["output_tokens"] ? Protocol.uint(usage["output_tokens"], "output tokens") : Nunki.estimate_tokens(parts)
114
+ ]
115
+ response(parts, counts, value["stop_reason"])
116
+ end
117
+
118
+ def decode_part(value)
119
+ value = Protocol.object(value, "content part")
120
+ return text_part(Protocol.string(value["text"], "text")) if value["type"] == "text"
121
+ raise ProtocolError, "unsupported content type" unless value["type"] == "tool_use"
122
+ tool_part(
123
+ Protocol.string(value["name"], "tool name", empty: false),
124
+ Protocol.object(value["input"], "tool input"),
125
+ Protocol.string(value["id"], "tool id", empty: false)
126
+ )
127
+ end
128
+
129
+ def encode_message(message)
130
+ {role: message.role.to_s, content: message.content.map { |part| encode_part(part) }}
131
+ end
132
+
133
+ def encode_part(part)
134
+ case part.type.to_s.to_sym
135
+ when :text then {type: "text", text: part.text}
136
+ when :tool_use then {type: "tool_use", id: part.tool_use_id, name: part.tool_name, input: part.tool_input}
137
+ when :tool_result then {type: "tool_result", tool_use_id: part.tool_use_id, content: part.text}
138
+ else raise ProtocolError, "unsupported part type: #{part.type}"
139
+ end
140
+ end
141
+
142
+ def encode_tool(tool)
143
+ {name: tool.name, description: tool.description, input_schema: tool.input_schema}
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nunki
4
+ module Providers
5
+ class OpenAI < Provider
6
+ def self.headers(api_key)
7
+ api_key ? {"Authorization" => "Bearer #{Protocol.string(api_key, "api_key", empty: false, max: 8192)}"} : {}
8
+ end
9
+
10
+ private
11
+
12
+ def perform_complete(prepared)
13
+ messages, tools, system, max_tokens = prepared
14
+ messages = [Message.new(role: :system, content: [text_part(system)])] + messages if system
15
+ payload = {
16
+ model: @model,
17
+ messages: messages.map { |message| encode_message(message) },
18
+ max_tokens: max_tokens,
19
+ stream: true
20
+ }
21
+ payload[:tools] = tools.map { |tool| encode_tool(tool) } unless tools.empty?
22
+
23
+ state = {text: +"", tools: {}, finish: nil, usage: nil}
24
+ result = @client.post_json(payload, headers: {"Accept" => "text/event-stream, application/json"}) do |event|
25
+ next if event.data.empty? || event.data == "[DONE]"
26
+ consume_stream(parse_event(event), state) { |part| yield part if block_given? }
27
+ end
28
+ return consume_response(result.body, messages) unless result.body.empty?
29
+
30
+ parts = []
31
+ parts << text_part(state[:text]) unless state[:text].empty?
32
+ state[:tools].sort.each do |_index, tool|
33
+ part = tool_part(tool[:name], parse_tool_input(tool[:arguments]), tool[:id])
34
+ parts << part
35
+ yield part if block_given?
36
+ end
37
+ usage = state[:usage] || [Nunki.estimate_tokens(messages), Nunki.estimate_tokens(parts)]
38
+ response(parts, usage, state[:finish])
39
+ end
40
+
41
+ def consume_stream(message, state)
42
+ usage = message["usage"]
43
+ usage = Protocol.object(usage, "usage") if usage
44
+ state[:usage] = [usage["prompt_tokens"], usage["completion_tokens"]].map { |v| Protocol.uint(v, "usage") } if usage
45
+
46
+ Protocol.collection(message.fetch("choices", []), "choices").each do |choice|
47
+ choice = Protocol.object(choice, "choice")
48
+ state[:finish] = Protocol.string(choice["finish_reason"], "finish reason") if choice["finish_reason"]
49
+ delta = Protocol.object(choice.fetch("delta", {}), "delta")
50
+ if delta["content"]
51
+ text = Protocol.string(delta["content"], "content delta")
52
+ state[:text] << text
53
+ yield text_part(text)
54
+ end
55
+ consume_tool_deltas(delta["tool_calls"], state) if delta["tool_calls"]
56
+ end
57
+ end
58
+
59
+ def consume_tool_deltas(calls, state)
60
+ Protocol.collection(calls, "tool calls").each do |call|
61
+ call = Protocol.object(call, "tool call")
62
+ index = Protocol.uint(call["index"], "tool call index")
63
+ tool = state[:tools][index] ||= {id: nil, name: +"", arguments: +""}
64
+ tool[:id] = Protocol.string(call["id"], "tool call id") if call["id"]
65
+ function = Protocol.object(call.fetch("function", {}), "tool function")
66
+ tool[:name] << Protocol.string(function["name"], "tool name") if function["name"]
67
+ tool[:arguments] << Protocol.string(function["arguments"], "tool arguments") if function["arguments"]
68
+ end
69
+ end
70
+
71
+ def consume_response(body, messages)
72
+ message = Protocol.object(Protocol.parse(body), "response")
73
+ choice = Protocol.object(Protocol.collection(message["choices"], "choices").first, "choice")
74
+ value = Protocol.object(choice["message"], "message")
75
+ parts = []
76
+ parts << text_part(Protocol.string(value["content"], "content")) if value["content"]
77
+ consume_nonstream_tools(value["tool_calls"], parts) if value["tool_calls"]
78
+ usage = message["usage"]
79
+ usage = Protocol.object(usage, "usage") if usage
80
+ counts = usage ? [usage["prompt_tokens"], usage["completion_tokens"]].map { |v| Protocol.uint(v, "usage") } :
81
+ [Nunki.estimate_tokens(messages), Nunki.estimate_tokens(parts)]
82
+ finish = choice["finish_reason"] && Protocol.string(choice["finish_reason"], "finish reason")
83
+ response(parts, counts, finish)
84
+ end
85
+
86
+ def consume_nonstream_tools(calls, parts)
87
+ Protocol.collection(calls, "tool calls").each do |call|
88
+ call = Protocol.object(call, "tool call")
89
+ function = Protocol.object(call["function"], "tool function")
90
+ parts << tool_part(
91
+ Protocol.string(function["name"], "tool name", empty: false),
92
+ parse_tool_input(Protocol.string(function["arguments"], "tool arguments")),
93
+ Protocol.string(call["id"], "tool id", empty: false)
94
+ )
95
+ end
96
+ end
97
+
98
+ def encode_message(message)
99
+ role = message.role.to_s
100
+ text = message.content.select { |part| part.type.to_s.to_sym == :text }.map(&:text).join
101
+ results = message.content.select { |part| part.type.to_s.to_sym == :tool_result }
102
+ if results.any?
103
+ raise ProtocolError, "OpenAI tool result messages must contain exactly one result" unless results.one? && text.empty?
104
+ return {role: "tool", tool_call_id: results.first.tool_use_id, content: results.first.text}
105
+ end
106
+
107
+ encoded = {role: role, content: text.empty? ? nil : text}
108
+ calls = message.content.select { |part| part.type.to_s.to_sym == :tool_use }
109
+ encoded[:tool_calls] = calls.map do |part|
110
+ {id: part.tool_use_id, type: "function", function: {name: part.tool_name, arguments: JSON.generate(part.tool_input)}}
111
+ end unless calls.empty?
112
+ encoded
113
+ end
114
+
115
+ def encode_tool(tool)
116
+ {type: "function", function: {name: tool.name, description: tool.description, parameters: tool.input_schema}}
117
+ end
118
+
119
+ def parse_tool_input(json)
120
+ Protocol.object(Protocol.parse(json, "tool input"), "tool input")
121
+ end
122
+ end
123
+ end
124
+ end
data/lib/nunki/sse.rb ADDED
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nunki
4
+ module SSE
5
+ Event = Value.define(:event, :data, :id, :retry)
6
+
7
+ class Parser
8
+ DEFAULT_MAX_EVENT_BYTES = 1_048_576
9
+
10
+ def initialize(max_event_bytes: DEFAULT_MAX_EVENT_BYTES)
11
+ @max_event_bytes = Protocol.uint(max_event_bytes, "max_event_bytes", positive: true)
12
+ @buffer = +""
13
+ reset_event
14
+ end
15
+
16
+ def feed(chunk)
17
+ raise ProtocolError, "SSE chunk must be a string" unless chunk.is_a?(String)
18
+ @buffer << chunk
19
+ events = []
20
+ while (newline = @buffer.index("\n"))
21
+ line = @buffer.slice!(0, newline + 1).delete_suffix("\n").delete_suffix("\r")
22
+ consume(line, events)
23
+ end
24
+ raise ProtocolError, "SSE event exceeds #{@max_event_bytes} bytes" if event_bytes > @max_event_bytes
25
+ events
26
+ end
27
+
28
+ def finish
29
+ events = []
30
+ consume(@buffer.delete_suffix("\r"), events) unless @buffer.empty?
31
+ dispatch(events) if @seen
32
+ @buffer.clear
33
+ events
34
+ end
35
+
36
+ private
37
+
38
+ def consume(line, events)
39
+ if line.empty?
40
+ dispatch(events)
41
+ return
42
+ end
43
+ return if line.start_with?(":")
44
+
45
+ field, value = line.split(":", 2)
46
+ value = value&.delete_prefix(" ") || ""
47
+ @seen = true
48
+ case field
49
+ when "event" then @event = value
50
+ when "data" then @data << value
51
+ when "id" then @id = value unless value.include?("\0")
52
+ when "retry" then @retry = Integer(value, exception: false) if value.match?(/\A\d+\z/)
53
+ end
54
+ end
55
+
56
+ def dispatch(events)
57
+ return reset_event unless @seen
58
+
59
+ events << Event.new(event: @event, data: @data.join("\n"), id: @id, retry: @retry)
60
+ reset_event
61
+ end
62
+
63
+ def reset_event
64
+ @event = nil
65
+ @data = []
66
+ @id = nil
67
+ @retry = nil
68
+ @seen = false
69
+ end
70
+
71
+ def event_bytes = @buffer.bytesize + @data.sum(&:bytesize)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nunki
4
+ VERSION = "0.1.0"
5
+ end
data/lib/nunki.rb ADDED
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "nunki/version"
4
+
5
+ module Nunki
6
+ class Error < StandardError; end
7
+ class ProtocolError < Error; end
8
+ class Timeout < Error; end
9
+ class Cancelled < Error; end
10
+
11
+ class HTTPError < Error
12
+ attr_reader :status, :body
13
+
14
+ def initialize(status, body = nil)
15
+ @status = status
16
+ @body = body
17
+ super("HTTP request failed with status #{status}")
18
+ end
19
+ end
20
+
21
+ class RateLimited < HTTPError
22
+ attr_reader :retry_after
23
+
24
+ def initialize(body = nil, retry_after: nil)
25
+ @retry_after = retry_after
26
+ super(429, body)
27
+ end
28
+ end
29
+
30
+ module Value
31
+ module_function
32
+
33
+ def define(*members)
34
+ return Data.define(*members) if defined?(Data) && Data.respond_to?(:define)
35
+
36
+ Struct.new(*members) do
37
+ members.each { |member| undef_method("#{member}=") }
38
+
39
+ define_method(:initialize) do |*values, **keywords|
40
+ if keywords.empty?
41
+ raise ArgumentError, "wrong number of arguments" unless values.length == self.class.members.length
42
+ super(*values)
43
+ else
44
+ raise ArgumentError, "cannot mix positional and keyword arguments" unless values.empty?
45
+
46
+ missing = self.class.members - keywords.keys
47
+ unknown = keywords.keys - self.class.members
48
+ raise ArgumentError, "missing keyword: #{missing.first.inspect}" unless missing.empty?
49
+ raise ArgumentError, "unknown keyword: #{unknown.first.inspect}" unless unknown.empty?
50
+
51
+ super(*self.class.members.map { |member| keywords.fetch(member) })
52
+ end
53
+ freeze
54
+ end
55
+
56
+ define_method(:with) do |**changes|
57
+ return self if changes.empty?
58
+ unknown = changes.keys - self.class.members
59
+ raise ArgumentError, "unknown keyword: #{unknown.first.inspect}" unless unknown.empty?
60
+ self.class.new(**to_h.merge(changes))
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ Message = Value.define(:role, :content)
67
+ Part = Value.define(:type, :text, :tool_name, :tool_input, :tool_use_id)
68
+ Tool = Value.define(:name, :description, :input_schema)
69
+ Usage = Value.define(:input_tokens, :output_tokens)
70
+ Response = Value.define(:message, :usage, :finish_reason)
71
+ private_constant :Value
72
+ end
73
+
74
+ require_relative "nunki/protocol"
75
+ require_relative "nunki/context"
76
+ require_relative "nunki/sse"
77
+ require_relative "nunki/http"
78
+ require_relative "nunki/provider"
79
+ require_relative "nunki/providers/open_ai"
80
+ require_relative "nunki/providers/anthropic"
81
+ require_relative "nunki/mcp"
data/sig/nunki.rbs ADDED
@@ -0,0 +1,132 @@
1
+ module Nunki
2
+ VERSION: String
3
+
4
+ type json = nil | bool | Integer | Float | String | Array[json] | Hash[String | Symbol, json]
5
+ type object = Hash[String | Symbol, json]
6
+
7
+ class Error < StandardError
8
+ end
9
+ class ProtocolError < Error
10
+ end
11
+ class Timeout < Error
12
+ end
13
+ class Cancelled < Error
14
+ end
15
+ class HTTPError < Error
16
+ attr_reader status: Integer
17
+ attr_reader body: String?
18
+ def initialize: (Integer status, ?String? body) -> void
19
+ end
20
+ class RateLimited < HTTPError
21
+ attr_reader retry_after: Float?
22
+ def initialize: (?String? body, ?retry_after: Float?) -> void
23
+ end
24
+
25
+ class Message
26
+ attr_reader role: String | Symbol
27
+ attr_reader content: Array[Part]
28
+ def self.new: (String | Symbol, Array[Part]) -> Message
29
+ | (role: String | Symbol, content: Array[Part]) -> Message
30
+ def with: (?role: String | Symbol, ?content: Array[Part]) -> Message
31
+ end
32
+ class Part
33
+ attr_reader type: String | Symbol
34
+ attr_reader text: String?
35
+ attr_reader tool_name: String?
36
+ attr_reader tool_input: object?
37
+ attr_reader tool_use_id: String?
38
+ def self.new: (String | Symbol, String?, String?, object?, String?) -> Part
39
+ | (type: String | Symbol, text: String?, tool_name: String?, tool_input: object?, tool_use_id: String?) -> Part
40
+ def with: (?type: String | Symbol, ?text: String?, ?tool_name: String?, ?tool_input: object?, ?tool_use_id: String?) -> Part
41
+ end
42
+ class Tool
43
+ attr_reader name: String
44
+ attr_reader description: String
45
+ attr_reader input_schema: object
46
+ def self.new: (String, String, object) -> Tool
47
+ | (name: String, description: String, input_schema: object) -> Tool
48
+ def with: (?name: String, ?description: String, ?input_schema: object) -> Tool
49
+ end
50
+ class Usage
51
+ attr_reader input_tokens: Integer
52
+ attr_reader output_tokens: Integer
53
+ def self.new: (Integer, Integer) -> Usage
54
+ | (input_tokens: Integer, output_tokens: Integer) -> Usage
55
+ def with: (?input_tokens: Integer, ?output_tokens: Integer) -> Usage
56
+ end
57
+ class Response
58
+ attr_reader message: Message
59
+ attr_reader usage: Usage
60
+ attr_reader finish_reason: String?
61
+ def self.new: (Message, Usage, String?) -> Response
62
+ | (message: Message, usage: Usage, finish_reason: String?) -> Response
63
+ def with: (?message: Message, ?usage: Usage, ?finish_reason: String?) -> Response
64
+ end
65
+
66
+ def self.estimate_tokens: (Message | Part | Array[Message | Part] | String value) -> Integer
67
+ def self.truncate: (Array[Message] messages, max_tokens: Integer) -> Array[Message]
68
+
69
+ class Provider
70
+ def self.build: (String | Symbol kind, endpoint: String, ?api_key: String?, model: String, **untyped options) -> Provider
71
+ def complete: (Array[Message] messages, ?tools: Array[Tool], ?system: String?, ?max_tokens: Integer) ?{ (Part) -> untyped } -> untyped
72
+ def cancel: () -> untyped
73
+ end
74
+
75
+ module SSE
76
+ class Event
77
+ attr_reader event: String?
78
+ attr_reader data: String
79
+ attr_reader id: String?
80
+ attr_reader retry: Integer?
81
+ def self.new: (String?, String, String?, Integer?) -> Event
82
+ | (event: String?, data: String, id: String?, retry: Integer?) -> Event
83
+ def with: (?event: String?, ?data: String, ?id: String?, ?retry: Integer?) -> Event
84
+ end
85
+ class Parser
86
+ def initialize: (?max_event_bytes: Integer) -> void
87
+ def feed: (String chunk) -> Array[Event]
88
+ def finish: () -> Array[Event]
89
+ end
90
+ end
91
+
92
+ module HTTP
93
+ class Result
94
+ attr_reader status: Integer
95
+ attr_reader headers: Hash[String, String]
96
+ attr_reader body: String
97
+ attr_reader events: Array[SSE::Event]
98
+ def self.new: (Integer, Hash[String, String], String, Array[SSE::Event]) -> Result
99
+ | (status: Integer, headers: Hash[String, String], body: String, events: Array[SSE::Event]) -> Result
100
+ def with: (?status: Integer, ?headers: Hash[String, String], ?body: String, ?events: Array[SSE::Event]) -> Result
101
+ end
102
+ class Client
103
+ attr_reader endpoint: untyped
104
+ def initialize: (endpoint: String, ?headers: Hash[String, String], ?open_timeout: Numeric, ?read_timeout: Numeric, ?retries: Integer, ?retry_base: Numeric, ?max_request_bytes: Integer, ?max_response_bytes: Integer) -> void
105
+ def post_json: (json payload, ?headers: Hash[String, String]) ?{ (SSE::Event, Hash[String, String]) -> untyped } -> Result
106
+ def delete: (?headers: Hash[String, String]) -> Result
107
+ def cancel: () -> untyped
108
+ end
109
+ end
110
+
111
+ module MCP
112
+ PROTOCOL_VERSION: String
113
+ class RemoteError < Error
114
+ attr_reader code: Integer
115
+ attr_reader data: json
116
+ end
117
+ class Client
118
+ attr_reader capabilities: object
119
+ attr_reader server_info: object
120
+ def self.stdio: (command: Array[String], ?env: Hash[String, String], ?timeout: Numeric, ?dispatch: untyped) -> Client
121
+ def self.http: (url: String, ?headers: Hash[String, String], ?timeout: Numeric, ?dispatch: untyped, **untyped options) -> Client
122
+ def start: () -> untyped
123
+ def tools: () -> untyped
124
+ def call_tool: (String name, object arguments) -> untyped
125
+ def resources: () -> untyped
126
+ def read_resource: (String uri) -> untyped
127
+ def prompts: () -> untyped
128
+ def get_prompt: (String name, ?object arguments) -> untyped
129
+ def close: () -> untyped
130
+ end
131
+ end
132
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nunki
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ email:
13
+ - t.yudai92@gmail.com
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - CHANGELOG.md
19
+ - LICENSE.txt
20
+ - README.md
21
+ - docs/adr/000-template.md
22
+ - docs/adr/001-explicit-network-boundary.md
23
+ - docs/adr/002-mcp-transports.md
24
+ - docs/adr/README.md
25
+ - lib/nunki.rb
26
+ - lib/nunki/context.rb
27
+ - lib/nunki/http.rb
28
+ - lib/nunki/mcp.rb
29
+ - lib/nunki/mcp/transports.rb
30
+ - lib/nunki/protocol.rb
31
+ - lib/nunki/provider.rb
32
+ - lib/nunki/providers/anthropic.rb
33
+ - lib/nunki/providers/open_ai.rb
34
+ - lib/nunki/sse.rb
35
+ - lib/nunki/version.rb
36
+ - sig/nunki.rbs
37
+ homepage: https://github.com/noxdea/nunki
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ source_code_uri: https://github.com/noxdea/nunki
42
+ changelog_uri: https://github.com/noxdea/nunki/blob/main/CHANGELOG.md
43
+ allowed_push_host: https://rubygems.org
44
+ rubygems_mfa_required: 'true'
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '3.1'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 4.0.16
60
+ specification_version: 4
61
+ summary: Provider-neutral LLM and MCP clients for Ruby
62
+ test_files: []