Class: Pikuri::Command::Registry
- Inherits:
-
Object
- Object
- Pikuri::Command::Registry
- Defined in:
- lib/pikuri/command/registry.rb
Overview
On-disk registry of slash commands, over the same search bases as the
skill catalog: under each base it scans .pikuri/commands and
.claude/commands for .md files.
registry = Pikuri::Command::Registry.new(search_bases: [
Pikuri::Bundle::Base.new(dir: project_root, trusted: repo_sources_are_mine),
Pikuri::Bundle::Base.new(dir: Dir.home, trusted: home_bundles_are_mine)
])
text = registry.('wh', arguments: 'swingai') # nil if no such command
agent.run_loop(user_message: text) # it *is* the human's turn
Names are paths
A command's name is its file path under the scanned dir, .md dropped
and separators +:+-joined: commands/frontend/deploy.md is
/frontend:deploy. A frontmatter name: is ignored (warned about) —
for a skill the name is a label the model reads, but for a command it is
the address the human types, and an address its location doesn't predict
is not an address.
Precedence
Bases left to right, .pikuri/commands before .claude/commands; the
first command seen for a name wins and later ones are dropped with a
warning. Pass the project root first for "project beats global".
A host resolving a typed /name owes one more rule, since nothing here
sees the other two namespaces: host built-in > command > skill.
Match built-ins first or a cloned repo shipping
.claude/commands/plan.md captures a /plan that flips a safety gate.
Validation, and the two refusals
Nothing else is fatal. Frontmatter is optional (a bare .md is all
body); description falls back to the name; unknown keys are ignored.
A command that parses is a command the human can invoke — a menu entry
that silently vanished would be the worse failure, since the human is
looking straight at the list.
Two things are refused, at #get rather than at scan time, so typing
/foo explains itself instead of the entry being mysteriously absent:
allowed-tools:— a narrowing pikuri cannot honour, since an agent's toolset is fixed atconfiguretime; ignoring the key would widen what the author asked to fence.- a
!cmd</code> body directive — the shell command Claude Code runs before sending the prompt. pikuri never runs it.
Detection is over-eager on purpose and does not skip fenced blocks —
the inverse of what {Renderer} does to the same characters, which is not
an inconsistency; see there. Rationale for both refusals:
DECISIONS.md D_command_inlining_legs.
Trust
{#untrusted_level} unions {Pikuri::Bundle::Base#trusted} over the commands actually stored, exactly as the skill catalog does, and {Extension} declares the result. A refused command never counts: its bytes cannot reach the context.
Immutable.
Defined Under Namespace
Classes: Command
Instance Attribute Summary collapse
-
#list ⇒ Array<Command>
readonly
Commands in discovery order (which equals precedence order), refused ones included so a menu can show them.
-
#roots ⇒ Array<String>
readonly
Absolute paths of the command directories this registry covered, in scan order.
Instance Method Summary collapse
-
#empty? ⇒ Boolean
True when no base held a command; Extension then declares nothing.
-
#expand(name, arguments: '') ⇒ String?
#get plus Pikuri::Command::Renderer.expand, in one call.
-
#get(name) ⇒ Command?
Look a command up by the name the human typed.
- #initialize(search_bases:) ⇒ Registry constructor
-
#untrusted_level ⇒ Symbol
:nonewhen every runnable command came from atrustedbase, else:hard.
Constructor Details
#initialize(search_bases:) ⇒ Registry
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/pikuri/command/registry.rb', line 119 def initialize(search_bases:) @roots = [] @commands = {} @untrusted = false search_bases.each do |entry| base = entry.is_a?(Bundle::Base) ? entry : Bundle::Base.new(dir: entry) COMMAND_SUBDIRS.each do |sub| root = File.join(base.dir.to_s, sub) next unless File.directory?(root) @roots << root scan_root(root, trusted: base.trusted) end end @roots.freeze @commands.freeze @list = @commands.values.freeze freeze end |
Instance Attribute Details
#list ⇒ Array<Command> (readonly)
Returns commands in discovery order (which equals precedence order), refused ones included so a menu can show them.
143 144 145 |
# File 'lib/pikuri/command/registry.rb', line 143 def list @list end |
#roots ⇒ Array<String> (readonly)
Returns absolute paths of the command directories this
registry covered, in scan order. Hosts fold these into
+Pikuri::Workspace::Filesystem+'s readable: so the model can
read a file a command body points at.
149 150 151 |
# File 'lib/pikuri/command/registry.rb', line 149 def roots @roots end |
Instance Method Details
#empty? ⇒ Boolean
Returns true when no base held a command; Extension then declares nothing.
184 |
# File 'lib/pikuri/command/registry.rb', line 184 def empty? = @list.empty? |
#expand(name, arguments: '') ⇒ String?
#get plus Pikuri::Command::Renderer.expand, in one call. What comes back is the human's own user turn — see Pikuri::Command::Renderer for what that rules out.
175 176 177 178 179 180 |
# File 'lib/pikuri/command/registry.rb', line 175 def (name, arguments: '') command = get(name) return nil if command.nil? Renderer.(command, arguments: arguments) end |
#get(name) ⇒ Command?
Look a command up by the name the human typed.
Two outcomes beyond success, and they are different on purpose: absent is an ordinary thing a human causes by typo, while refused is a fact about a file on disk that retyping cannot fix.
160 161 162 163 164 165 166 |
# File 'lib/pikuri/command/registry.rb', line 160 def get(name) command = @commands[name] return nil if command.nil? raise RefusedError.new(command.location, command.refusal) if command.refused? command end |
#untrusted_level ⇒ Symbol
Returns :none when every runnable command came from a
trusted base, else :hard.
188 |
# File 'lib/pikuri/command/registry.rb', line 188 def untrusted_level = @untrusted ? :hard : :none |