antigravity-sdk 0.2.0 → 0.4.2

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,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Antigravity
6
+ # Resolves skill paths from various inputs: single skill dir, container dir,
7
+ # or GitHub URL. Returns expanded absolute paths to skill directories.
8
+ #
9
+ # Resolution logic:
10
+ # 1. Path contains SKILL.md -> single skill
11
+ # 2. Path has a skills/ subfolder -> discover children with SKILL.md
12
+ # 3. Path children have SKILL.md -> flat container discovery
13
+ # 4. GitHub URL -> clone/cache then resolve locally
14
+ #
15
+ # Spec compliance: https://agentskills.io/specification
16
+ class SkillResolver
17
+ GITHUB_URL_PATTERN = %r{\Ahttps?://github\.com/}i
18
+ CACHE_DIR = File.expand_path('~/.antigravity/cache/ruby-sdk/skills')
19
+
20
+ # Resolve a path or URL to an array of valid skill directory paths.
21
+ # @param path_or_url [String] local path or GitHub URL
22
+ # @return [Array<String>] expanded absolute paths to skill directories
23
+ def self.resolve(path_or_url)
24
+ path_or_url = path_or_url.to_s.strip
25
+
26
+ if github_url?(path_or_url)
27
+ resolve_github(path_or_url)
28
+ else
29
+ resolve_local(path_or_url)
30
+ end
31
+ end
32
+
33
+ # Discover all skill directories inside a container path.
34
+ # Checks for skills/ subfolder first, then scans children directly.
35
+ # @param container_path [String] path to scan for skills
36
+ # @return [Array<String>] sorted list of skill directory paths
37
+ def self.discover(container_path)
38
+ expanded = File.expand_path(container_path)
39
+ return [] unless File.directory?(expanded)
40
+
41
+ # Check for skills/ subfolder first (convention from agentskills.io)
42
+ skills_subdir = File.join(expanded, 'skills')
43
+ scan_dir = File.directory?(skills_subdir) ? skills_subdir : expanded
44
+
45
+ Dir.children(scan_dir)
46
+ .map { |child| File.join(scan_dir, child) }
47
+ .select { |child_path| skill_dir?(child_path) }
48
+ .sort
49
+ end
50
+
51
+ # Check if a directory is a valid skill (contains SKILL.md).
52
+ # @param path [String] directory path to check
53
+ # @return [Boolean]
54
+ def self.skill_dir?(path)
55
+ File.directory?(path) && File.exist?(File.join(path, 'SKILL.md'))
56
+ end
57
+
58
+ # Check if a string looks like a GitHub URL.
59
+ # @param str [String]
60
+ # @return [Boolean]
61
+ def self.github_url?(str)
62
+ str.match?(GITHUB_URL_PATTERN)
63
+ end
64
+
65
+ class << self
66
+ private
67
+
68
+ def resolve_local(path)
69
+ expanded = File.expand_path(path)
70
+ raise ArgumentError, "Skill path does not exist: #{expanded}" unless File.exist?(expanded)
71
+ raise ArgumentError, "Skill path is not a directory: #{expanded}" unless File.directory?(expanded)
72
+
73
+ # Case 1: Direct skill dir (has SKILL.md)
74
+ return [expanded] if skill_dir?(expanded)
75
+
76
+ # Case 2 & 3: Container dir -- discover children
77
+ discovered = discover(expanded)
78
+ return discovered unless discovered.empty?
79
+
80
+ raise ArgumentError, "No skills found at #{expanded}. Expected SKILL.md or a skills/ subfolder."
81
+ end
82
+
83
+ # Parse a GitHub URL into components.
84
+ # Supports:
85
+ # https://github.com/org/repo
86
+ # https://github.com/org/repo/tree/main/skills/my-skill
87
+ # https://github.com/org/repo/tree/main/skills
88
+ #
89
+ # Returns [org, repo, subpath_or_nil]
90
+ def parse_github_url(url)
91
+ # Strip trailing slash
92
+ url = url.chomp('/')
93
+
94
+ case url
95
+ when %r{github\.com/([^/]+)/([^/]+)/tree/[^/]+/(.+)}
96
+ # URL with tree/branch/subpath
97
+ [Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)]
98
+ when %r{github\.com/([^/]+)/([^/]+)(?:\.git)?/?$}
99
+ # Bare repo URL
100
+ [Regexp.last_match(1), Regexp.last_match(2), nil]
101
+ when %r{github\.com/([^/]+)/([^/]+)/(.+)}
102
+ # URL with subpath but no /tree/branch/ (e.g. from skill_name:)
103
+ [Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)]
104
+ else
105
+ raise ArgumentError, "Cannot parse GitHub URL: #{url}"
106
+ end
107
+ end
108
+
109
+ def resolve_github(url)
110
+ org, repo, subpath = parse_github_url(url)
111
+ local_repo = clone_or_update(org, repo)
112
+
113
+ # Resolve subpath within the cloned repo
114
+ target = subpath ? File.join(local_repo, subpath) : local_repo
115
+ resolve_local(target)
116
+ end
117
+
118
+ # Clone (or update) a GitHub repo into the cache.
119
+ # Returns the local path to the cloned repo.
120
+ def clone_or_update(org, repo)
121
+ cache_path = File.join(CACHE_DIR, org, repo)
122
+
123
+ if File.directory?(File.join(cache_path, '.git'))
124
+ # Already cloned -- pull latest
125
+ system('git', '-C', cache_path, 'pull', '--ff-only', '--quiet',
126
+ out: File::NULL, err: File::NULL)
127
+ else
128
+ # Fresh clone
129
+ FileUtils.mkdir_p(File.dirname(cache_path))
130
+ clone_url = "https://github.com/#{org}/#{repo}.git"
131
+ success = system('git', 'clone', '--depth=1', '--quiet', clone_url, cache_path,
132
+ out: File::NULL, err: File::NULL)
133
+ raise "Failed to clone #{clone_url}. Check the URL and your network." unless success
134
+ end
135
+
136
+ cache_path
137
+ end
138
+ end
139
+ end
140
+ end
@@ -1,18 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antigravity
4
- class Tool
5
- include Emojifiable
6
-
4
+ class Tool < Base
7
5
  class << self
8
6
  def inherited(subclass)
7
+ super
9
8
  subclass.extend(ClassMethods)
10
9
  end
10
+
11
+ # Factory method: define a tool inline with a block.
12
+ # Tool.define(:name, desc: '...', params: { city: { type: :string } }) { |city:| ... }
13
+ def define(name, desc: '', params: {}, &block)
14
+ Dynamic.new(name, description: desc, params: params, &block)
15
+ end
11
16
  end
12
17
 
13
18
  module ClassMethods
14
- include Emojifiable::ClassMethods
15
-
16
19
  def name(val = nil, desc: nil)
17
20
  if val
18
21
  @tool_name = val.to_s
@@ -75,12 +78,13 @@ module Antigravity
75
78
 
76
79
  # Factory for dynamic Proc-based tools
77
80
  class Dynamic < Tool
78
- attr_reader :block
81
+ attr_reader :block, :params
79
82
 
80
- def initialize(name, description: "", &block)
83
+ def initialize(name, description: '', params: {}, &block)
81
84
  super()
82
85
  @dynamic_name = name.to_s
83
86
  @dynamic_description = description
87
+ @params = params
84
88
  @block = block
85
89
  end
86
90
 
@@ -88,17 +92,37 @@ module Antigravity
88
92
  @dynamic_name
89
93
  end
90
94
 
91
- def call(params)
92
- @block.call(params)
95
+ def call(params = nil, **kwargs)
96
+ return nil unless @block
97
+
98
+ if params && kwargs.empty?
99
+ # Old-style: tool.call({key: value})
100
+ @block.call(params)
101
+ else
102
+ # New-style: tool.call(key: value)
103
+ @block.call(**kwargs)
104
+ end
93
105
  end
94
106
 
95
107
  def to_json_schema
108
+ properties = {}
109
+ required_params = []
110
+
111
+ @params.each do |param_name, spec|
112
+ properties[param_name] = {
113
+ type: (spec[:type] || :string).to_s,
114
+ description: spec[:description] || spec[:desc] || ''
115
+ }
116
+ required_params << param_name.to_s unless spec[:required] == false
117
+ end
118
+
96
119
  {
97
120
  name: @dynamic_name,
98
121
  description: @dynamic_description,
99
122
  parameters: {
100
- type: "object",
101
- properties: {}
123
+ type: 'object',
124
+ properties: properties,
125
+ required: required_params
102
126
  }
103
127
  }
104
128
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Antigravity
6
+ # Registers, resolves, and executes custom tools for agent callbacks.
7
+ # Tools are invoked by the harness when the model decides to call them.
8
+ class ToolRunner < Base
9
+ def initialize
10
+ @tools = {}
11
+ end
12
+
13
+ # Register a tool (Tool::Dynamic or any Tool subclass)
14
+ def register(tool)
15
+ name = tool.respond_to?(:tool_name) ? tool.tool_name : tool.to_s
16
+ raise ToolError, "Tool '#{name}' is already registered" if @tools.key?(name)
17
+
18
+ @tools[name] = tool
19
+ end
20
+
21
+ # Check if a tool is registered
22
+ def registered?(name)
23
+ @tools.key?(name.to_s)
24
+ end
25
+
26
+ # Execute a tool by name with keyword args.
27
+ # Returns the result or a { error: "..." } hash on failure.
28
+ def execute(name, **kwargs)
29
+ tool = @tools[name.to_s]
30
+ raise ToolNotFoundError, "Tool '#{name}' not found" unless tool
31
+
32
+ tool.call(**kwargs)
33
+ rescue ToolNotFoundError
34
+ raise
35
+ rescue => e
36
+ { error: e.message }
37
+ end
38
+
39
+ # Generate harness-compatible tool definitions for HarnessConfig
40
+ def to_harness_tools
41
+ @tools.values.map do |tool|
42
+ schema = tool.to_json_schema
43
+ {
44
+ name: schema[:name],
45
+ description: schema[:description],
46
+ parametersJsonSchema: JSON.generate(schema[:parameters])
47
+ }
48
+ end
49
+ end
50
+
51
+ def size
52
+ @tools.size
53
+ end
54
+
55
+ def empty?
56
+ @tools.empty?
57
+ end
58
+ end
59
+ end
data/lib/antigravity.rb CHANGED
@@ -8,15 +8,24 @@ end
8
8
 
9
9
  require_relative "antigravity/version"
10
10
  require_relative "antigravity/emojis"
11
+ require_relative "antigravity/base"
12
+ require_relative "antigravity/errors"
11
13
  require_relative "antigravity/config"
12
14
  require_relative "antigravity/message"
15
+ require_relative "antigravity/protocol"
13
16
  require_relative "antigravity/harness"
14
17
  require_relative "antigravity/hooks"
15
18
  require_relative "antigravity/guards"
16
19
  require_relative "antigravity/sidecar"
17
20
  require_relative "antigravity/tool"
21
+ require_relative "antigravity/tool_runner"
22
+ require_relative "antigravity/skill_resolver"
18
23
  require_relative "antigravity/skill"
19
24
  require_relative "antigravity/client"
25
+ require_relative "antigravity/connection/binary_fetcher"
26
+ require_relative "antigravity/connection/websocket_client"
27
+ require_relative "antigravity/connection/local_connection"
28
+ require_relative "antigravity/conversation"
20
29
  require_relative "antigravity/agent"
21
30
 
22
31
  module Antigravity
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antigravity-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Riccardo Carlesso
@@ -10,19 +10,19 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: faye-websocket
13
+ name: websocket
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.11'
18
+ version: '1.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.11'
25
+ version: '1.2'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: json
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -61,16 +61,25 @@ extra_rdoc_files: []
61
61
  files:
62
62
  - lib/antigravity.rb
63
63
  - lib/antigravity/agent.rb
64
+ - lib/antigravity/base.rb
64
65
  - lib/antigravity/client.rb
65
66
  - lib/antigravity/config.rb
67
+ - lib/antigravity/connection/binary_fetcher.rb
68
+ - lib/antigravity/connection/local_connection.rb
69
+ - lib/antigravity/connection/websocket_client.rb
70
+ - lib/antigravity/conversation.rb
66
71
  - lib/antigravity/emojis.rb
72
+ - lib/antigravity/errors.rb
67
73
  - lib/antigravity/guards.rb
68
74
  - lib/antigravity/harness.rb
69
75
  - lib/antigravity/hooks.rb
70
76
  - lib/antigravity/message.rb
77
+ - lib/antigravity/protocol.rb
71
78
  - lib/antigravity/sidecar.rb
72
79
  - lib/antigravity/skill.rb
80
+ - lib/antigravity/skill_resolver.rb
73
81
  - lib/antigravity/tool.rb
82
+ - lib/antigravity/tool_runner.rb
74
83
  - lib/antigravity/version.rb
75
84
  homepage: https://github.com/palladius/antigravity-ruby-sdk
76
85
  licenses: