agent_skills_configurations 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 +7 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +16 -0
- data/CODE_OF_CONDUCT.md +11 -0
- data/LICENSE.txt +13 -0
- data/README.md +289 -0
- data/Rakefile +17 -0
- data/doc/AgentSkillsConfigurations/Agent.md +112 -0
- data/doc/AgentSkillsConfigurations/Error.md +10 -0
- data/doc/AgentSkillsConfigurations/Registry.md +278 -0
- data/doc/AgentSkillsConfigurations.md +297 -0
- data/doc/index.csv +18 -0
- data/lib/agent_skills_configurations/agent.rb +74 -0
- data/lib/agent_skills_configurations/agents.yml +415 -0
- data/lib/agent_skills_configurations/registry.rb +589 -0
- data/lib/agent_skills_configurations/version.rb +8 -0
- data/lib/agent_skills_configurations.rb +285 -0
- data/sig/agent_skills_configurations.rbs +4 -0
- metadata +79 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module AgentSkillsConfigurations
|
|
6
|
+
# Loads agent configurations, resolves paths, and exposes query helpers.
|
|
7
|
+
#
|
|
8
|
+
# The Registry is the internal implementation class that handles:
|
|
9
|
+
#
|
|
10
|
+
# * Loading and parsing the YAML configuration file
|
|
11
|
+
# * Resolving environment variables to absolute paths
|
|
12
|
+
# * Handling fallback paths for missing directories
|
|
13
|
+
# * Detecting which agents have their paths present
|
|
14
|
+
# * Caching results for performance
|
|
15
|
+
#
|
|
16
|
+
# This class is typically used through the public API methods in the
|
|
17
|
+
# {AgentSkillsConfigurations} module, but can also be used directly for
|
|
18
|
+
# advanced use cases.
|
|
19
|
+
#
|
|
20
|
+
# == Path Resolution
|
|
21
|
+
#
|
|
22
|
+
# The Registry uses a sophisticated path resolution system that respects
|
|
23
|
+
# environment variables and provides fallback locations. The resolution
|
|
24
|
+
# process works as follows:
|
|
25
|
+
#
|
|
26
|
+
# 1. Check if the configured environment variable is set and non-empty
|
|
27
|
+
# 2. If set, use that value as the base path
|
|
28
|
+
# 3. If not set, use the configured fallback path (often relative to home)
|
|
29
|
+
# 4. Expand relative paths using the user's home directory
|
|
30
|
+
#
|
|
31
|
+
# Example resolution flow for XDG_CONFIG_HOME:
|
|
32
|
+
#
|
|
33
|
+
# # Configuration in YAML:
|
|
34
|
+
# base_paths:
|
|
35
|
+
# xdg_config:
|
|
36
|
+
# env_var: XDG_CONFIG_HOME
|
|
37
|
+
# fallback: ".config"
|
|
38
|
+
#
|
|
39
|
+
# # With environment variable set:
|
|
40
|
+
# ENV["XDG_CONFIG_HOME"] = "/custom/xdg"
|
|
41
|
+
# # => resolves to "/custom/xdg"
|
|
42
|
+
#
|
|
43
|
+
# # Without environment variable:
|
|
44
|
+
# ENV["XDG_CONFIG_HOME"] = nil
|
|
45
|
+
# # => resolves to "/Users/username/.config"
|
|
46
|
+
#
|
|
47
|
+
# # Empty environment variable treated as unset:
|
|
48
|
+
# ENV["XDG_CONFIG_HOME"] = ""
|
|
49
|
+
# # => resolves to "/Users/username/.config"
|
|
50
|
+
#
|
|
51
|
+
# == Global Skills Path Resolution
|
|
52
|
+
#
|
|
53
|
+
# Global skills paths are resolved relative to the agent's base path
|
|
54
|
+
# and support multiple fallbacks. The Registry checks each candidate path
|
|
55
|
+
# in order and returns the first one that exists:
|
|
56
|
+
#
|
|
57
|
+
# # Configuration in YAML:
|
|
58
|
+
# agents:
|
|
59
|
+
# - name: moltbot
|
|
60
|
+
# base_path: home
|
|
61
|
+
# global_skills_path: ".moltbot/skills"
|
|
62
|
+
# global_skills_path_fallbacks:
|
|
63
|
+
# - ".clawdbot/skills"
|
|
64
|
+
# - ".moltbot/skills"
|
|
65
|
+
#
|
|
66
|
+
# # Resolution order:
|
|
67
|
+
# # 1. Check ~/.moltbot/skills
|
|
68
|
+
# # 2. Check ~/.clawdbot/skills
|
|
69
|
+
# # 3. Check ~/.moltbot/skills (fallback)
|
|
70
|
+
# # 4. Return first existing path, or primary path if none exist
|
|
71
|
+
#
|
|
72
|
+
# == Agent Detection
|
|
73
|
+
#
|
|
74
|
+
# The Registry determines whether an agent's paths are present by checking the
|
|
75
|
+
# paths configured in the agent's +detect_paths+ array. Each path spec
|
|
76
|
+
# can be one of several types:
|
|
77
|
+
#
|
|
78
|
+
# * *String*: Check if the path exists relative to the user's home directory
|
|
79
|
+
# * *Hash with +cwd+*: Check if the path exists relative to the current working directory
|
|
80
|
+
# * *Hash with +base+ and +path+*: Check if the path exists relative to a configured base path
|
|
81
|
+
# * *Hash with +absolute+*: Check if the absolute path exists
|
|
82
|
+
#
|
|
83
|
+
# An agent is considered detected if <b>any</b> of its detect paths exists.
|
|
84
|
+
#
|
|
85
|
+
# Examples of detect paths:
|
|
86
|
+
#
|
|
87
|
+
# agents:
|
|
88
|
+
# - name: cursor
|
|
89
|
+
# detect_paths:
|
|
90
|
+
# - ".cursor" # Check ~/.cursor exists
|
|
91
|
+
#
|
|
92
|
+
# - name: antigravity
|
|
93
|
+
# detect_paths:
|
|
94
|
+
# - { cwd: ".agent" } # Check .agent exists in current dir
|
|
95
|
+
# - { base: home, path: ".gemini/antigravity" } # Check ~/.gemini/antigravity exists
|
|
96
|
+
#
|
|
97
|
+
# - name: codex
|
|
98
|
+
# detect_paths:
|
|
99
|
+
# - "" # Always detected (empty string matches)
|
|
100
|
+
# - { absolute: "/etc/codex" } # Check /etc/codex exists
|
|
101
|
+
#
|
|
102
|
+
# == Caching
|
|
103
|
+
#
|
|
104
|
+
# The Registry caches the results of {all} and {detected} to avoid
|
|
105
|
+
# repeatedly parsing the YAML file and checking file system paths.
|
|
106
|
+
# The cache can be cleared using {reset}.
|
|
107
|
+
#
|
|
108
|
+
# Use {reset} when:
|
|
109
|
+
#
|
|
110
|
+
# * Environment variables that affect path resolution have changed
|
|
111
|
+
# * Agent paths have been created or removed
|
|
112
|
+
# * The YAML configuration file has been modified
|
|
113
|
+
#
|
|
114
|
+
# @see AgentSkillsConfigurations Public API module that uses this class
|
|
115
|
+
# @see YAML_PATH Path to the configuration file
|
|
116
|
+
class Registry
|
|
117
|
+
# Absolute path to the configuration file.
|
|
118
|
+
#
|
|
119
|
+
# The configuration file contains agent definitions, base path configurations,
|
|
120
|
+
# and detection rules. This path is resolved relative to the gem's lib directory.
|
|
121
|
+
#
|
|
122
|
+
# @return [String] absolute path to agents.yml
|
|
123
|
+
YAML_PATH = File.expand_path("agents.yml", __dir__)
|
|
124
|
+
|
|
125
|
+
# Create a registry from the YAML configuration.
|
|
126
|
+
#
|
|
127
|
+
# Loads the agents.yml file and parses it into a data structure that
|
|
128
|
+
# can be queried for agent information. The YAML is loaded safely
|
|
129
|
+
# with permitted classes for security.
|
|
130
|
+
#
|
|
131
|
+
# @return [Registry] a new registry instance with loaded configuration
|
|
132
|
+
# @raise [Psych::SyntaxError] when the YAML is invalid or malformed
|
|
133
|
+
#
|
|
134
|
+
# @example Create a registry
|
|
135
|
+
# registry = AgentSkillsConfigurations::Registry.new
|
|
136
|
+
# registry.find("cursor").name # => "cursor"
|
|
137
|
+
def initialize
|
|
138
|
+
@data = YAML.safe_load_file(YAML_PATH, permitted_classes: [Hash], aliases: true)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Find an agent by name.
|
|
142
|
+
#
|
|
143
|
+
# Looks up an agent configuration by its canonical name and returns an
|
|
144
|
+
# {Agent} value object with resolved paths. This method performs path
|
|
145
|
+
# resolution each time it's called, so it reflects the current
|
|
146
|
+
# environment variables and file system state.
|
|
147
|
+
#
|
|
148
|
+
# @param name [String] the canonical agent name from agents.yml
|
|
149
|
+
# @return [Agent] the resolved agent configuration with absolute paths
|
|
150
|
+
# @raise [Error] when the agent name is unknown
|
|
151
|
+
#
|
|
152
|
+
# @example Find a specific agent
|
|
153
|
+
# registry = AgentSkillsConfigurations::Registry.new
|
|
154
|
+
# agent = registry.find("cursor")
|
|
155
|
+
# agent.name # => "cursor"
|
|
156
|
+
# agent.global_skills_dir # => "/Users/username/.cursor/skills"
|
|
157
|
+
#
|
|
158
|
+
# @example Error for unknown agent
|
|
159
|
+
# registry.find("unknown-agent")
|
|
160
|
+
# # => raises AgentSkillsConfigurations::Error: Unknown agent: unknown-agent
|
|
161
|
+
def find(name)
|
|
162
|
+
entry = @data["agents"].find { |a| a["name"] == name }
|
|
163
|
+
raise Error, "Unknown agent: #{name}" unless entry
|
|
164
|
+
|
|
165
|
+
build_agent(entry)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Return all configured agents.
|
|
169
|
+
#
|
|
170
|
+
# Returns a frozen array of all {Agent} objects defined in the configuration.
|
|
171
|
+
# The result is cached on first call for performance. Path resolution happens
|
|
172
|
+
# once during caching and the results are reused on subsequent calls.
|
|
173
|
+
#
|
|
174
|
+
# Use {reset} to clear the cache and force re-resolution when needed.
|
|
175
|
+
#
|
|
176
|
+
# @return [Array<Agent>] frozen array of all agents with resolved paths
|
|
177
|
+
# @raise [Psych::SyntaxError] when the YAML is invalid (only on first call)
|
|
178
|
+
#
|
|
179
|
+
# @example Get all agents
|
|
180
|
+
# registry = AgentSkillsConfigurations::Registry.new
|
|
181
|
+
# all = registry.all
|
|
182
|
+
# all.map(&:name)
|
|
183
|
+
# # => ["amp", "claude-code", "cursor", "codex", ...]
|
|
184
|
+
#
|
|
185
|
+
# @example Verify array is frozen and cached
|
|
186
|
+
# registry = AgentSkillsConfigurations::Registry.new
|
|
187
|
+
# first_call = registry.all
|
|
188
|
+
# second_call = registry.all
|
|
189
|
+
# first_call.equal?(second_call) # => true (same object)
|
|
190
|
+
# first_call.frozen? # => true
|
|
191
|
+
#
|
|
192
|
+
# @see #reset Clear the cache
|
|
193
|
+
# @see #detected Get only detected agents
|
|
194
|
+
def all
|
|
195
|
+
@all ||= @data["agents"].map { |entry| build_agent(entry) }.freeze
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Return agents detected on this machine.
|
|
199
|
+
#
|
|
200
|
+
# Filters the list of all agents to those that have their detect paths present.
|
|
201
|
+
# Detection uses the paths configured in each agent's
|
|
202
|
+
# <tt>detect_paths</tt> configuration. An agent is considered detected
|
|
203
|
+
# if <b>any</b> of its detect paths exists.
|
|
204
|
+
#
|
|
205
|
+
# Detection strategies:
|
|
206
|
+
#
|
|
207
|
+
# * String: Check if path exists relative to user's home directory
|
|
208
|
+
# * Hash with +cwd+: Check relative to current working directory
|
|
209
|
+
# * Hash with +base+ and +path+: Check relative to configured base path
|
|
210
|
+
# * Hash with +absolute+: Check absolute path directly
|
|
211
|
+
#
|
|
212
|
+
# The result is cached on first call. Use {reset} to clear the cache.
|
|
213
|
+
#
|
|
214
|
+
# @return [Array<Agent>] frozen array of detected agents with resolved paths
|
|
215
|
+
# @raise [Psych::SyntaxError] when the YAML is invalid (only on first call)
|
|
216
|
+
#
|
|
217
|
+
# @example Get detected agents
|
|
218
|
+
# registry = AgentSkillsConfigurations::Registry.new
|
|
219
|
+
# detected = registry.detected
|
|
220
|
+
# detected.map(&:name)
|
|
221
|
+
# # => ["cursor", "claude-code"]
|
|
222
|
+
#
|
|
223
|
+
# @example Check if specific agent is detected
|
|
224
|
+
# registry = AgentSkillsConfigurations::Registry.new
|
|
225
|
+
# detected_names = registry.detected.map(&:name)
|
|
226
|
+
# detected_names.include?("cursor") # => true (if detected)
|
|
227
|
+
# detected_names.include?("unknown") # => false
|
|
228
|
+
#
|
|
229
|
+
# @see #all Get all configured agents
|
|
230
|
+
# @see #reset Clear the cache
|
|
231
|
+
def detected
|
|
232
|
+
@detected ||= all.select { |agent| detected?(agent) }.freeze
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Clear cached agent lists.
|
|
236
|
+
#
|
|
237
|
+
# Clears the internal caches for {all} and {detected} results. This
|
|
238
|
+
# forces path resolution to be re-executed on the next call, which is
|
|
239
|
+
# useful when:
|
|
240
|
+
#
|
|
241
|
+
# * Environment variables affecting path resolution have changed
|
|
242
|
+
# * Agent paths have been created or removed
|
|
243
|
+
# * The YAML configuration file has been modified
|
|
244
|
+
#
|
|
245
|
+
# @return [void]
|
|
246
|
+
#
|
|
247
|
+
# @example Reset after changing environment variable
|
|
248
|
+
# registry = AgentSkillsConfigurations::Registry.new
|
|
249
|
+
# ENV["XDG_CONFIG_HOME"] = "/new/path"
|
|
250
|
+
# registry.reset
|
|
251
|
+
# agent = registry.find("amp")
|
|
252
|
+
# agent.global_skills_dir # => "/new/path/agents/skills"
|
|
253
|
+
#
|
|
254
|
+
# @example Reset for fresh detection
|
|
255
|
+
# registry = AgentSkillsConfigurations::Registry.new
|
|
256
|
+
# # Create agent paths...
|
|
257
|
+
# registry.reset
|
|
258
|
+
# registry.detected # => includes newly detected agent
|
|
259
|
+
#
|
|
260
|
+
# @see #all Get all agents
|
|
261
|
+
# @see #detected Get detected agents
|
|
262
|
+
def reset
|
|
263
|
+
@all = nil
|
|
264
|
+
@detected = nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
private
|
|
268
|
+
|
|
269
|
+
# Build an agent value object from a YAML entry.
|
|
270
|
+
#
|
|
271
|
+
# Creates an {Agent} object by resolving the base path and global skills
|
|
272
|
+
# path from the YAML configuration. The base path is resolved first,
|
|
273
|
+
# then used as the context for resolving the global skills path (including
|
|
274
|
+
# any fallbacks).
|
|
275
|
+
#
|
|
276
|
+
# @param entry [Hash] the agent entry from the YAML configuration
|
|
277
|
+
# @return [Agent] a new Agent instance with resolved absolute paths
|
|
278
|
+
#
|
|
279
|
+
# @example Build an agent from YAML entry
|
|
280
|
+
# entry = {
|
|
281
|
+
# "name" => "cursor",
|
|
282
|
+
# "display_name" => "Cursor",
|
|
283
|
+
# "skills_dir" => ".cursor/skills",
|
|
284
|
+
# "base_path" => "home",
|
|
285
|
+
# "global_skills_path" => ".cursor/skills"
|
|
286
|
+
# }
|
|
287
|
+
# build_agent(entry)
|
|
288
|
+
# # => #<Agent name="cursor" display_name="Cursor" ...>
|
|
289
|
+
def build_agent(entry)
|
|
290
|
+
base = resolve_base_path(entry["base_path"])
|
|
291
|
+
global_path = resolve_global_skills_path(entry, base)
|
|
292
|
+
|
|
293
|
+
Agent.new(
|
|
294
|
+
name: entry["name"],
|
|
295
|
+
display_name: entry["display_name"],
|
|
296
|
+
skills_dir: entry["skills_dir"],
|
|
297
|
+
global_skills_dir: global_path
|
|
298
|
+
)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Resolve a base path key into an absolute path.
|
|
302
|
+
#
|
|
303
|
+
# Looks up a base path configuration by key and resolves it to an absolute
|
|
304
|
+
# path using the following priority:
|
|
305
|
+
#
|
|
306
|
+
# 1. If env_var is "~" or "", return the home directory directly
|
|
307
|
+
# 2. If env_var is set and non-empty, use that value as the path
|
|
308
|
+
# 3. Otherwise, use the configured fallback path (relative to home)
|
|
309
|
+
#
|
|
310
|
+
# This method ensures that empty environment variables are treated the same
|
|
311
|
+
# as unset variables, preventing unexpected behavior.
|
|
312
|
+
#
|
|
313
|
+
# @param key [String] the base path key from the YAML configuration
|
|
314
|
+
# @return [String] an absolute path
|
|
315
|
+
#
|
|
316
|
+
# @example Resolve XDG_CONFIG_HOME with environment variable
|
|
317
|
+
# # YAML: base_paths.xdg_config.env_var = "XDG_CONFIG_HOME"
|
|
318
|
+
# # YAML: base_paths.xdg_config.fallback = ".config"
|
|
319
|
+
# ENV["XDG_CONFIG_HOME"] = "/custom/xdg"
|
|
320
|
+
# resolve_base_path("xdg_config") # => "/custom/xdg"
|
|
321
|
+
#
|
|
322
|
+
# @example Resolve XDG_CONFIG_HOME without environment variable
|
|
323
|
+
# ENV["XDG_CONFIG_HOME"] = nil
|
|
324
|
+
# resolve_base_path("xdg_config") # => "/Users/username/.config"
|
|
325
|
+
#
|
|
326
|
+
# @example Resolve with empty environment variable
|
|
327
|
+
# ENV["XDG_CONFIG_HOME"] = ""
|
|
328
|
+
# resolve_base_path("xdg_config") # => "/Users/username/.config"
|
|
329
|
+
def resolve_base_path(key)
|
|
330
|
+
config = @data["base_paths"][key]
|
|
331
|
+
env_var = config["env_var"]
|
|
332
|
+
home = Dir.home || "/tmp"
|
|
333
|
+
|
|
334
|
+
return home if ["~", ""].include?(env_var)
|
|
335
|
+
return ENV.fetch(env_var, nil) if env_var_set?(env_var)
|
|
336
|
+
|
|
337
|
+
resolve_fallback(config["fallback"], home)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# True when an environment variable is set and non-empty.
|
|
341
|
+
#
|
|
342
|
+
# Checks if an environment variable exists and contains a non-empty value.
|
|
343
|
+
# This distinction is important because an empty string should be treated
|
|
344
|
+
# the same as an unset variable for path resolution purposes.
|
|
345
|
+
#
|
|
346
|
+
# @param env_var [String, nil] the environment variable name
|
|
347
|
+
# @return [Boolean] true if the variable is set and non-empty
|
|
348
|
+
#
|
|
349
|
+
# @example Set environment variable
|
|
350
|
+
# ENV["VAR"] = "value"
|
|
351
|
+
# env_var_set?("VAR") # => true
|
|
352
|
+
#
|
|
353
|
+
# @example Unset environment variable
|
|
354
|
+
# ENV.delete("VAR")
|
|
355
|
+
# env_var_set?("VAR") # => false
|
|
356
|
+
#
|
|
357
|
+
# @example Empty environment variable
|
|
358
|
+
# ENV["VAR"] = ""
|
|
359
|
+
# env_var_set?("VAR") # => false
|
|
360
|
+
#
|
|
361
|
+
# @example Nil environment variable name
|
|
362
|
+
# env_var_set?(nil) # => false
|
|
363
|
+
def env_var_set?(env_var)
|
|
364
|
+
env_var && !ENV[env_var].nil? && !ENV[env_var].empty?
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Resolve a fallback path relative to the user's home directory.
|
|
368
|
+
#
|
|
369
|
+
# Joins the fallback path with the home directory, unless the fallback is
|
|
370
|
+
# "~" or an empty string, in which case the home directory is returned
|
|
371
|
+
# directly.
|
|
372
|
+
#
|
|
373
|
+
# @param fallback [String] the fallback path from YAML configuration
|
|
374
|
+
# @param home [String] the user's home directory
|
|
375
|
+
# @return [String] an absolute path
|
|
376
|
+
#
|
|
377
|
+
# @example Normal fallback
|
|
378
|
+
# resolve_fallback(".config", "/Users/username")
|
|
379
|
+
# # => "/Users/username/.config"
|
|
380
|
+
#
|
|
381
|
+
# @example Tilde fallback (use home directly)
|
|
382
|
+
# resolve_fallback("~", "/Users/username")
|
|
383
|
+
# # => "/Users/username"
|
|
384
|
+
#
|
|
385
|
+
# @example Empty fallback (use home directly)
|
|
386
|
+
# resolve_fallback("", "/Users/username")
|
|
387
|
+
# # => "/Users/username"
|
|
388
|
+
def resolve_fallback(fallback, home)
|
|
389
|
+
return home if ["~", ""].include?(fallback)
|
|
390
|
+
|
|
391
|
+
File.join(home, fallback)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Resolve the global skills path, using fallbacks when needed.
|
|
395
|
+
#
|
|
396
|
+
# Resolves the primary global skills path relative to the base path, checking
|
|
397
|
+
# if it exists. If it doesn't exist, tries each fallback path in order.
|
|
398
|
+
# Returns the first existing path, or the primary path if none exist.
|
|
399
|
+
#
|
|
400
|
+
# This ensures that agents can gracefully handle missing directories by
|
|
401
|
+
# trying alternative locations before falling back to the primary path.
|
|
402
|
+
#
|
|
403
|
+
# @param entry [Hash] the agent entry from YAML configuration
|
|
404
|
+
# @param base_path [String] the resolved base path for this agent
|
|
405
|
+
# @return [String] an absolute path to the global skills directory
|
|
406
|
+
#
|
|
407
|
+
# @example Primary path exists
|
|
408
|
+
# # ~/.cursor/skills exists
|
|
409
|
+
# resolve_global_skills_path({ "global_skills_path" => ".cursor/skills" }, "/Users/username")
|
|
410
|
+
# # => "/Users/username/.cursor/skills"
|
|
411
|
+
#
|
|
412
|
+
# @example Use fallback when primary doesn't exist
|
|
413
|
+
# # ~/.moltbot/skills doesn't exist, ~/.clawdbot/skills exists
|
|
414
|
+
# entry = {
|
|
415
|
+
# "global_skills_path" => ".moltbot/skills",
|
|
416
|
+
# "global_skills_path_fallbacks" => [".clawdbot/skills", ".moltbot/skills"]
|
|
417
|
+
# }
|
|
418
|
+
# resolve_global_skills_path(entry, "/Users/username")
|
|
419
|
+
# # => "/Users/username/.clawdbot/skills"
|
|
420
|
+
#
|
|
421
|
+
# @example Return primary path if none exist
|
|
422
|
+
# # No paths exist
|
|
423
|
+
# resolve_global_skills_path({ "global_skills_path" => ".unknown/skills" }, "/Users/username")
|
|
424
|
+
# # => "/Users/username/.unknown/skills"
|
|
425
|
+
def resolve_global_skills_path(entry, base_path)
|
|
426
|
+
primary = entry["global_skills_path"]
|
|
427
|
+
fallbacks = entry["global_skills_path_fallbacks"] || []
|
|
428
|
+
|
|
429
|
+
candidates = [primary] + fallbacks
|
|
430
|
+
candidates.each do |path|
|
|
431
|
+
resolved = File.expand_path(path, base_path)
|
|
432
|
+
return resolved if Dir.exist?(resolved)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
File.expand_path(primary, base_path)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Detect whether an agent's paths are present based on configured paths.
|
|
439
|
+
#
|
|
440
|
+
# Checks each of the agent's configured detection paths. The agent is
|
|
441
|
+
# considered detected if <b>any</b> of the paths exists. Detection paths
|
|
442
|
+
# can be strings or hashes with different resolution strategies.
|
|
443
|
+
#
|
|
444
|
+
# @param agent [Agent] the agent to check for detection
|
|
445
|
+
# @return [Boolean] true if any detection path exists
|
|
446
|
+
#
|
|
447
|
+
# @example Agent with detect paths
|
|
448
|
+
# # YAML: detect_paths: [".cursor"]
|
|
449
|
+
# agent = registry.find("cursor")
|
|
450
|
+
# detected?(agent) # => true if ~/.cursor exists
|
|
451
|
+
#
|
|
452
|
+
# @example Agent with no detect paths (not detected)
|
|
453
|
+
# # YAML: detect_paths: []
|
|
454
|
+
# agent = registry.find("some-agent")
|
|
455
|
+
# detected?(agent) # => false
|
|
456
|
+
#
|
|
457
|
+
# @see #detect_path Individual path detection logic
|
|
458
|
+
def detected?(agent)
|
|
459
|
+
entry = @data["agents"].find { |a| a["name"] == agent.name }
|
|
460
|
+
return false unless entry
|
|
461
|
+
|
|
462
|
+
detect_paths = entry["detect_paths"] || []
|
|
463
|
+
|
|
464
|
+
detect_paths.any? do |detect_spec|
|
|
465
|
+
path?(detect_spec)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Detect a path using the configured spec.
|
|
470
|
+
#
|
|
471
|
+
# Dispatches to the appropriate detection method based on the type
|
|
472
|
+
# of the detection specification:
|
|
473
|
+
#
|
|
474
|
+
# * String: Check relative to home directory
|
|
475
|
+
# * Hash: Check based on hash keys (cwd, base, absolute)
|
|
476
|
+
# * Other: Return false
|
|
477
|
+
#
|
|
478
|
+
# @param spec [String, Hash] the detection specification from YAML
|
|
479
|
+
# @return [Boolean] true if the path exists
|
|
480
|
+
#
|
|
481
|
+
# @example String detection
|
|
482
|
+
# path?(".cursor") # => true if ~/.cursor exists
|
|
483
|
+
#
|
|
484
|
+
# @example Hash detection with cwd
|
|
485
|
+
# path?({ cwd: ".agent" }) # => true if ./.agent exists
|
|
486
|
+
#
|
|
487
|
+
# @example Hash detection with absolute
|
|
488
|
+
# path?({ absolute: "/etc/codex" }) # => true if /etc/codex exists
|
|
489
|
+
#
|
|
490
|
+
# @see #string_path? String detection logic
|
|
491
|
+
# @see #hash_path? Hash detection logic
|
|
492
|
+
def path?(spec)
|
|
493
|
+
case spec
|
|
494
|
+
when String
|
|
495
|
+
string_path?(spec)
|
|
496
|
+
when Hash
|
|
497
|
+
hash_path?(spec)
|
|
498
|
+
else
|
|
499
|
+
false
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Detect a string spec relative to the user's home directory.
|
|
504
|
+
#
|
|
505
|
+
# For non-empty strings, checks if the path exists relative to the user's
|
|
506
|
+
# home directory. An empty string is treated as always true, which can
|
|
507
|
+
# be used to mark an agent as always detected.
|
|
508
|
+
#
|
|
509
|
+
# @param spec [String] a path relative to the home directory
|
|
510
|
+
# @return [Boolean] true if the path exists or spec is empty
|
|
511
|
+
#
|
|
512
|
+
# @example Normal string detection
|
|
513
|
+
# string_path?(".cursor") # => true if ~/.cursor exists
|
|
514
|
+
#
|
|
515
|
+
# @example Empty string (always detected)
|
|
516
|
+
# string_path?("") # => true
|
|
517
|
+
#
|
|
518
|
+
# @example Non-existent path
|
|
519
|
+
# string_path?(".does-not-exist") # => false
|
|
520
|
+
def string_path?(spec)
|
|
521
|
+
spec.empty? || File.exist?(File.join(Dir.home, spec))
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Detect a hash spec with absolute/cwd/base rules.
|
|
525
|
+
#
|
|
526
|
+
# Handles hash-based detection specifications with different strategies:
|
|
527
|
+
#
|
|
528
|
+
# * +absolute+: Check the absolute path directly
|
|
529
|
+
# * +cwd+: Check relative to current working directory
|
|
530
|
+
# * +base+ and +path+: Check relative to a configured base path
|
|
531
|
+
#
|
|
532
|
+
# Only one strategy is applied per hash, in the order above.
|
|
533
|
+
#
|
|
534
|
+
# @param spec [Hash] a hash with detection strategy and path
|
|
535
|
+
# @return [Boolean] true if the path exists
|
|
536
|
+
#
|
|
537
|
+
# @example Absolute path detection
|
|
538
|
+
# hash_path?({ absolute: "/etc/codex" })
|
|
539
|
+
# # => true if /etc/codex exists
|
|
540
|
+
#
|
|
541
|
+
# @example Current working directory detection
|
|
542
|
+
# hash_path?({ cwd: ".agent" })
|
|
543
|
+
# # => true if ./.agent exists (relative to Dir.pwd)
|
|
544
|
+
#
|
|
545
|
+
# @example Base path detection
|
|
546
|
+
# # Assuming home base path resolves to /Users/username
|
|
547
|
+
# hash_path?({ base: "home", path: ".codex" })
|
|
548
|
+
# # => true if /Users/username/.codex exists
|
|
549
|
+
#
|
|
550
|
+
# @see #base_path? Base path resolution logic
|
|
551
|
+
def hash_path?(spec)
|
|
552
|
+
return File.exist?(spec[:absolute]) if spec.key?(:absolute)
|
|
553
|
+
return File.exist?(File.join(Dir.pwd, spec[:cwd])) if spec.key?(:cwd)
|
|
554
|
+
return base_path?(spec) if spec.key?(:base) && spec.key?(:path)
|
|
555
|
+
|
|
556
|
+
false
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Detect a path relative to a configured base path.
|
|
560
|
+
#
|
|
561
|
+
# Resolves a base path configuration and checks if the specified path
|
|
562
|
+
# exists relative to it. If the path is empty, checks if the base path
|
|
563
|
+
# directory itself exists.
|
|
564
|
+
#
|
|
565
|
+
# @param spec [Hash] a hash with +base+ key and optional +path+ key
|
|
566
|
+
# @return [Boolean] true if the path exists
|
|
567
|
+
#
|
|
568
|
+
# @example Non-empty path
|
|
569
|
+
# # home base path resolves to /Users/username
|
|
570
|
+
# base_path?({ base: "home", path: ".codex" })
|
|
571
|
+
# # => true if /Users/username/.codex exists
|
|
572
|
+
#
|
|
573
|
+
# @example Empty path (check base directory)
|
|
574
|
+
# base_path?({ base: "home", path: "" })
|
|
575
|
+
# # => true if /Users/username exists (home directory)
|
|
576
|
+
#
|
|
577
|
+
# @see #resolve_base_path Base path resolution logic
|
|
578
|
+
def base_path?(spec)
|
|
579
|
+
base = resolve_base_path(spec[:base])
|
|
580
|
+
path = spec[:path]
|
|
581
|
+
|
|
582
|
+
if path.empty?
|
|
583
|
+
Dir.exist?(base)
|
|
584
|
+
else
|
|
585
|
+
File.exist?(File.join(base, path))
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
end
|