simple-agent-extension 0.2.1 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5b57957c947b2f4c126dca54b41f69e67d8d2cbfc3a9447a31bea468b03cf76
4
- data.tar.gz: 2aafa730f3def389acfd18c2cec0807dfcd78aa1148199ca7a6129ab93fe7c49
3
+ metadata.gz: 404e535b96de550ec739358155e62a38ec0d9a07a2ec2f78e7607eb3a98930d7
4
+ data.tar.gz: e17a63f61c78d8b9f30631d52ff212ba890e71ccc0ab17e8a9e1a9b56d919060
5
5
  SHA512:
6
- metadata.gz: 6bcc677fcc9c6afe8f01f67e77b7a65e86fda1a45ea951a21a362eb88e8b3650ec34e90399292cb3b9b0dacdb082e8c8a5ea3bc0033dbf13f3476366ed4491f2
7
- data.tar.gz: f86cf67049dbc0bb7c16ed8c708ebb2e4b6c903240f1f260a2d88eb4be75e8a60c3922550ac1c99962393f5dcacee17c6c6c2ea72780a890f304b4e6d9c78bbd
6
+ metadata.gz: b9d622f95994d3f6abcfa23cb3bce94b92316250ad9c3a24ca0b83092be5f05628fa73a10ca5093b60b86cb27753e903566c4a38e248c200518a2d7bb99ef3d7
7
+ data.tar.gz: 963497aacd95ec19efa3157202dc547dacccd2c71567be762d7a8c8ba347efd6a65894c3501b1c0f5ab5794622ba3ff26db682eda2d73fe68cf1c3dbe4f54e63
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.2.3] 2026-10-12
2
+
3
+ - feat: add version option
4
+ - feat: add clean subcommand
5
+ - feat: support `disable-model-invocation' field for very heavy skills
6
+
7
+ ## [0.2.2] 2026-10-04
8
+
9
+ - fix: make the causes of errors easier to read
10
+ - chore: add gem package task
11
+ - docs: install from rubygems.org or run with rvx
12
+
1
13
  ## [0.2.1] 2026-10-02
2
14
 
3
15
  - feat: support Claude Code
data/README.md CHANGED
@@ -4,14 +4,14 @@
4
4
 
5
5
  ## Installation
6
6
 
7
- Requires Ruby 3.3 or later.
7
+ Requires Ruby 3.3 or later (not needed beforehand when running with `rvx`, which provides Ruby itself).
8
8
 
9
- The gem is not published to RubyGems.org yet, so install it from GitHub with Bundler:
9
+ Add the gem to your Gemfile:
10
10
 
11
11
  ```ruby
12
12
  # Gemfile
13
13
  group :development do
14
- gem "simple-agent-extension", github: "colorfulcompany/simple-agent-extension"
14
+ gem "simple-agent-extension"
15
15
  end
16
16
  ```
17
17
 
@@ -25,16 +25,12 @@ Then run the CLI through Bundler:
25
25
  bundle exec simple-agent-extension packages
26
26
  ```
27
27
 
28
- Pin a release with `tag:` so the checkout does not move:
28
+ Or run it without a Gemfile with [rv](https://github.com/spinel-coop/rv)'s `rvx`, which installs the gem on first use:
29
29
 
30
- ```ruby
31
- gem "simple-agent-extension",
32
- github: "colorfulcompany/simple-agent-extension",
33
- tag: "v0.1.0"
30
+ ```sh
31
+ rvx simple-agent-extension packages
34
32
  ```
35
33
 
36
- `branch:` and `ref:` are also available; note that `branch:` follows new commits rather than pinning.
37
-
38
34
  ## Usage
39
35
 
40
36
  ### Defining a package
@@ -119,9 +115,16 @@ entrypoint frontmatter
119
115
  < static.agents.<agent>
120
116
  ```
121
117
 
122
- #### Adaptation across Agents
118
+ #### Supported adaptive fields
119
+
120
+ | Field | Applies to | Value | Meaning |
121
+ |---|---|---|---|
122
+ | `permissions` | agent, skill | map of tool name to `allow`, `ask`, or `deny` | Which tools the extension may use, and how. |
123
+ | `disable-model-invocation` | skill | boolean | `true` keeps the skill out of model auto-selection; `/name` still works. |
123
124
 
124
- One `adaptive` fragment becomes different artifact metadata per Agent. The `permissions` fragment above compiles to:
125
+ Each Agent decides how a field appears in its artifact; see the `MetadataTranslator` in `lib/simple_agent_extension/agents/`. A field an Agent has no rule for, listed here or not, passes through unchanged, so it takes effect only on an Agent that recognizes it.
126
+
127
+ The `permissions` fragment above (`read: allow`, `edit: ask`) compiles to:
125
128
 
126
129
  | Agent | Artifact metadata |
127
130
  |---|---|
@@ -131,6 +134,8 @@ One `adaptive` fragment becomes different artifact metadata per Agent. The `perm
131
134
 
132
135
  `claude` maps known lowercase tool names such as `read` and `webfetch` to Claude Code's names (`Read`, `WebFetch`) and passes any other name through unchanged. `deny` becomes `disallowedTools` on an agent and `disallowed-tools` on a skill. An agent's `tools` lists both `allow` and `ask` tools, because Claude Code's `tools` limits which tools exist rather than pre-approving them.
133
136
 
137
+ `disable-model-invocation` is not part of the Agent Skills specification, but Claude Code, Copilot, and OpenCode v2 ( 2.0.23+ ) read it as written. Older OpenCode versions read only their own `metadata` keys, so `opencode` translates the field for them.
138
+
134
139
  A skill whose metadata is left unchanged by adaptation and carries no Agent-specific static fragments is written once to the shared skill directory. Otherwise it is written into that Agent's own configuration tree ( written with Ruby ). An agent extension is always Agent-specific. `claude` does not read the shared skill directory, so its skills are always written to `~/.claude/skills`.
135
140
 
136
141
  ### Commands
@@ -141,11 +146,14 @@ snapshot as below:
141
146
 
142
147
  ```sh
143
148
  simple-agent-extension packages
149
+ simple-agent-extension clean
144
150
  simple-agent-extension build
145
151
  simple-agent-extension deploy
146
152
  simple-agent-extension agents
147
153
  ```
148
154
 
155
+ `build` (and therefore `deploy`) runs `clean` first, so an artifact whose source package was removed is not deployed again. `clean` removes `<build-root>/<agent>/` for each targeted Agent only. It does not remove anything already deployed.
156
+
149
157
  All commands resolve `--source-root` from `./packages` and `--build-root` from `./build` by default. Either root may be outside the repository.
150
158
 
151
159
  ```sh
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleAgentExtension
4
- class InvalidAgentDirectory < Error; end
4
+ class AgentDirectoryNotFound < Error; end
5
+ class AgentDirectoryNotDirectory < Error; end
6
+ class AgentDirectoryNotReadable < Error; end
5
7
  class MissingAgentRegistration < Error; end
6
8
  class UnloadableAgentRegistration < Error; end
7
9
 
@@ -28,12 +30,15 @@ module SimpleAgentExtension
28
30
  @directories.map { |directory| canonical_directory(directory) }.uniq
29
31
  end
30
32
 
33
+ # The error class names the condition, so the message carries the path
34
+ # alone.
31
35
  def canonical_directory(directory)
32
36
  path = File.expand_path(directory)
33
- raise InvalidAgentDirectory, "invalid agent directory: #{path}" unless File.directory?(path)
37
+ raise AgentDirectoryNotFound, path unless File.exist?(path)
38
+ raise AgentDirectoryNotDirectory, path unless File.directory?(path)
34
39
 
35
40
  path = File.realpath(path)
36
- raise InvalidAgentDirectory, "invalid agent directory: #{path}" unless File.readable?(path)
41
+ raise AgentDirectoryNotReadable, path unless File.readable?(path)
37
42
 
38
43
  path
39
44
  end
@@ -10,6 +10,22 @@ module SimpleAgentExtension
10
10
  {"permission" => permissions}
11
11
  end
12
12
 
13
+ # For OpenCode before 2.0.23, which reads only these `metadata` keys.
14
+ # 2.0.23+ reads top-level `disable-model-invocation` itself and ignores
15
+ # `opencode/slash`, so this rule can go once older versions are unsupported.
16
+ define_translation type: :skill, field: "disable-model-invocation" do |disabled|
17
+ if disabled == true
18
+ {
19
+ "metadata" => {
20
+ "opencode/autoinvoke" => "false",
21
+ "opencode/slash" => "true"
22
+ }
23
+ }
24
+ else # standard:disable Style/EmptyElse
25
+ nil
26
+ end
27
+ end
28
+
13
29
  define_translation type: :agent, field: "name" do |_name|
14
30
  nil
15
31
  end
@@ -6,9 +6,17 @@ require "simple_agent_extension"
6
6
  module SimpleAgentExtension
7
7
  # Command-line adapter for public package compilation and deployment.
8
8
  class CLI
9
+ # The command line itself was written wrong. The message describes the
10
+ # mistake in full, so where it was raised tells the caller nothing.
11
+ class UsageError < Error; end
12
+
13
+ # No command was resolved, so the caller still needs the vocabulary.
14
+ class CommandError < UsageError; end
15
+
9
16
  COMMANDS = {
10
17
  "packages" => "List package names under the source root",
11
- "build" => "Compile artifacts into the build root",
18
+ "clean" => "Remove artifacts from the build root",
19
+ "build" => "Clean, then compile artifacts into the build root",
12
20
  "deploy" => "Build artifacts, then deploy them to Agents",
13
21
  "agents" => "List Agent names that can be targeted"
14
22
  }.freeze
@@ -22,6 +30,12 @@ module SimpleAgentExtension
22
30
  @error = error
23
31
  end
24
32
 
33
+ # Only errors this CLI defines, and the one OptionParser raises for a
34
+ # misspelled option, are reduced to a message. Everything else passes
35
+ # through with its class, origin, and backtrace intact.
36
+ #
37
+ # CommandError has to be listed first: it is a UsageError, so the wider
38
+ # clause would take it otherwise.
25
39
  def run(arguments)
26
40
  arguments = arguments.dup
27
41
  options = default_options
@@ -29,15 +43,17 @@ module SimpleAgentExtension
29
43
  parser.parse!(arguments)
30
44
 
31
45
  return display_help(parser) if options[:help]
46
+ return display_version(parser) if options[:version]
32
47
 
33
48
  command = command(arguments)
34
49
  validate_options(command, options)
35
50
  AgentDirectoryLoader.new(options[:agent_directories]).load
36
51
  run_command(command, options)
37
52
  0
38
- rescue OptionParser::ParseError, Error, ArgumentError, KeyError => error
39
- @error.puts error.message
40
- 1
53
+ rescue CommandError => error
54
+ report_failure(error, usage: parser)
55
+ rescue OptionParser::ParseError, UsageError => error
56
+ report_failure(error)
41
57
  end
42
58
 
43
59
  # Aligns the name column alone. Columns an Agent does not declare are
@@ -55,6 +71,16 @@ module SimpleAgentExtension
55
71
 
56
72
  private
57
73
 
74
+ # @param [Exception] error
75
+ # @param [OptionParser, nil] usage printed when the caller still needs the
76
+ # command vocabulary
77
+ # @return [Integer] exit status the run reports as a failure
78
+ def report_failure(error, usage: nil)
79
+ @error.puts error.message
80
+ @error.puts usage if usage
81
+ 1
82
+ end
83
+
58
84
  def default_options
59
85
  {
60
86
  source_root: File.expand_path("packages"),
@@ -62,13 +88,16 @@ module SimpleAgentExtension
62
88
  agents: [],
63
89
  agent_directories: [],
64
90
  force: false,
65
- help: false
91
+ help: false,
92
+ version: false
66
93
  }
67
94
  end
68
95
 
69
96
  def option_parser(options)
70
97
  OptionParser.new do |parser|
71
98
  parser.banner = banner
99
+ parser.program_name = "simple-agent-extension"
100
+ parser.version = VERSION
72
101
 
73
102
  parser.on("--source-root DIRECTORY", "Source package root (default: ./packages)") do |directory|
74
103
  options[:source_root] = File.expand_path(directory)
@@ -76,7 +105,7 @@ module SimpleAgentExtension
76
105
  parser.on("--build-root DIRECTORY", "Build artifact root (default: ./build)") do |directory|
77
106
  options[:build_root] = File.expand_path(directory)
78
107
  end
79
- parser.on("--agent NAME", "Build or deploy only NAME (repeatable)") do |name|
108
+ parser.on("--agent NAME", "Clean, build, or deploy only NAME (repeatable)") do |name|
80
109
  options[:agents] << name
81
110
  end
82
111
  parser.on("--agent-dir DIRECTORY", "Load direct *.rb Agent registrations from DIRECTORY") do |directory|
@@ -88,6 +117,9 @@ module SimpleAgentExtension
88
117
  parser.on("-h", "--help", "Show this help") do
89
118
  options[:help] = true
90
119
  end
120
+ parser.on("-V", "--version", "Show version") do
121
+ options[:version] = true
122
+ end
91
123
  end
92
124
  end
93
125
 
@@ -99,15 +131,20 @@ module SimpleAgentExtension
99
131
  end
100
132
 
101
133
  def display_help(parser)
102
- @output.puts parser
134
+ @output.puts parser.ver, "", parser
135
+ 0
136
+ end
137
+
138
+ def display_version(parser)
139
+ @output.puts parser.ver
103
140
  0
104
141
  end
105
142
 
106
143
  def command(arguments)
107
144
  command = arguments.shift
108
- raise ArgumentError, "missing command" unless command
109
- raise ArgumentError, "unknown command: #{command}" unless COMMANDS.key?(command)
110
- raise ArgumentError, "unexpected arguments: #{arguments.join(" ")}" unless arguments.empty?
145
+ raise CommandError, "missing command" unless command
146
+ raise CommandError, "unknown command: #{command}" unless COMMANDS.key?(command)
147
+ raise CommandError, "unexpected arguments: #{arguments.join(" ")}" unless arguments.empty?
111
148
 
112
149
  command
113
150
  end
@@ -115,7 +152,7 @@ module SimpleAgentExtension
115
152
  def validate_options(command, options)
116
153
  return unless options[:force] && command != "deploy"
117
154
 
118
- raise ArgumentError, "--force is only valid for deploy"
155
+ raise UsageError, "--force is only valid for deploy"
119
156
  end
120
157
 
121
158
  def run_command(command, options)
@@ -130,6 +167,8 @@ module SimpleAgentExtension
130
167
  @output.puts runner.packages
131
168
  when "agents"
132
169
  @output.puts agent_lines(runner.agents)
170
+ when "clean"
171
+ @output.puts runner.clean(agents: options[:agents])
133
172
  when "build"
134
173
  @output.puts runner.build(agents: options[:agents])
135
174
  when "deploy"
@@ -1,5 +1,8 @@
1
1
  module SimpleAgentExtension
2
2
  module Extensions
3
+ class EntrypointDocumentNotFound < Error; end
4
+ class InvalidEntrypointFrontmatter < Error; end
5
+
3
6
  EntrypointDocument = Data.define(:metadata, :body)
4
7
 
5
8
  # An extension that owns loading metadata from its directory.
@@ -48,6 +51,12 @@ module SimpleAgentExtension
48
51
  metadata, body = Frontmatter.parse(File.read(entrypoint_path))
49
52
 
50
53
  EntrypointDocument.new(metadata, body)
54
+ rescue Errno::ENOENT => e
55
+ not_found = EntrypointDocumentNotFound.new("entrypoint of package `#{@package}' / type `#{type}', #{entrypoint_path} not found.")
56
+ not_found.set_backtrace(e.backtrace)
57
+ raise not_found, cause: nil
58
+ rescue Psych::SyntaxError => e
59
+ raise InvalidEntrypointFrontmatter, "#{entrypoint_path}: #{[e.problem, e.context].compact.join(" ")} at line #{e.line + Frontmatter::METADATA_START_LINE_OFFSET} column #{e.column}", cause: nil
51
60
  end
52
61
 
53
62
  # @return [MetadataLoader] source YAML reader
@@ -13,6 +13,8 @@ module SimpleAgentExtension
13
13
  module Frontmatter
14
14
  DELIMITER = "---".freeze
15
15
  PATTERN = /\A---\r?\n(.*?)^---[ \t]*\r?\n?/m
16
+ # Metadata starts after the opening delimiter line.
17
+ METADATA_START_LINE_OFFSET = 1
16
18
 
17
19
  class InvalidFrontmatter < Error; end
18
20
 
@@ -8,15 +8,23 @@ module SimpleAgentExtension
8
8
  # reader exposes adaptive, common static, and Agent-specific static fragment
9
9
  # collections without defining their field vocabulary.
10
10
  class MetadataLoader
11
+ class InvalidMetadata < Error; end
12
+
11
13
  FILENAME = "metadata.yaml".freeze
12
14
 
15
+ # Psych does not know which file it parsed, so the path is attached here.
16
+ # Its backtrace points into Psych itself, so it is dropped.
17
+ #
13
18
  # @param [String] dir
14
19
  # @return [MetadataLoader] empty when +metadata.yaml+ is absent, never nil
20
+ # @raise [InvalidMetadata] when +metadata.yaml+ has a YAML syntax error
15
21
  def self.load(dir)
16
22
  path = File.join(dir, FILENAME)
17
23
  return new(Metadata.new) unless File.exist?(path)
18
24
 
19
25
  new(Metadata.from(YAML.safe_load(File.read(path)) || {})) # rubocop:disable Style/YAMLFileRead
26
+ rescue Psych::SyntaxError => e
27
+ raise InvalidMetadata, "#{path}: #{[e.problem, e.context].compact.join(" ")} at line #{e.line} column #{e.column}", cause: nil
20
28
  end
21
29
 
22
30
  # @param [Metadata] metadata source YAML mapping
@@ -1,3 +1,5 @@
1
+ require "fileutils"
2
+
1
3
  module SimpleAgentExtension
2
4
  # Coordinates source collection, compilation, and deployment for one
3
5
  # source and build roots. Entry points configure these boundaries explicitly.
@@ -25,12 +27,29 @@ module SimpleAgentExtension
25
27
  end
26
28
  end
27
29
 
30
+ # Removes each selected Agent's whole artifact subtree, so artifacts whose
31
+ # source package is gone cannot survive into the next deploy. Other
32
+ # Agents' subtrees are left alone.
33
+ #
34
+ # @param [Array<String>] agents target names; all targets when empty
35
+ # @return [Array<String>] Agent directories that existed and were removed
36
+ def clean(agents: [])
37
+ selected_agents(agents)
38
+ .map { |agent| File.join(@build_root, agent.name) }
39
+ .select { |dir| File.exist?(dir) }
40
+ .each { |dir| FileUtils.rm_rf(dir) }
41
+ end
42
+
43
+ # Cleans the selected Agents first, so the build tree mirrors the current
44
+ # source tree instead of accumulating every artifact ever compiled.
45
+ #
28
46
  # @param [Array<String>] agents target names; all targets when empty
29
47
  # @return [Array<String>] directories written to the build tree
30
48
  def build(agents: [])
31
49
  targets = selected_agents(agents)
32
50
  extensions = source_extensions
33
51
  warn_unknown_deploy_to_names(extensions)
52
+ clean(agents: agents)
34
53
 
35
54
  targets.flat_map { |agent|
36
55
  compiler = Compiler.new(agent: agent, build_root: @build_root)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleAgentExtension
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple-agent-extension
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-02 00:00:00.000000000 Z
11
+ date: 2026-10-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'Coding Agent Extension Utility with Simple DSL and Rule Translating
14
14
  and Deploying '