repl: user-defined skills loader (closes #2)
PHASE0.md §5.2 froze the meta-command set at compile time. Skills let
the user package repeatable workflows (project queries, prompt
templates, audit routines) without forking aish.
Discovery: scan ~/.config/aish/skills/*.lua at startup (or whatever
$AISH_SKILLS_DIR points at — used both by users with non-XDG layouts
and by CI). Each module exports:
return {
name = "<meta-cmd-name>", -- must match [%w_-]+
description = "<one-line>", -- shown by :skills
run = function(args, h) ... end,
}
Helpers passed to run():
h.ask(text) — same path as :ask (with @path expansion)
h.status(s) — emit "[aish] s"
h.exec(cmd) — run a shell command (subject to plan_mode, hooks)
h.model() — current active model name
h.ctx — raw Context object (advanced)
h.config — the loaded config table
Validation rejects modules that miss name/run, use whitespace in the
name, or collide with an existing meta command (built-in or earlier
skill). Each rejection emits a status line so the user sees why a
skill didn't appear.
New meta command :skills lists what's loaded (sorted, with description).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,7 @@ Meta commands:
|
||||
:route classes show current class → model mapping
|
||||
:route check <text> report which class <text> would route to (debug)
|
||||
:fallback on/off toggle cloud retry when local transport fails
|
||||
:skills list user-defined skills loaded from ~/.config/aish/skills/
|
||||
:help this message
|
||||
]]
|
||||
|
||||
@@ -1340,6 +1341,73 @@ function M.run(config)
|
||||
help = function() io.write(HELP) end,
|
||||
}
|
||||
|
||||
-- Issue #2: user-defined skills loader. Scan ~/.config/aish/skills/
|
||||
-- (or $AISH_SKILLS_DIR) for *.lua modules. Each module returns
|
||||
-- { name = "<meta-cmd-name>", description = "...", run = function(args, h) end }
|
||||
-- and gets registered as a meta command :<name>. Helpers passed to run():
|
||||
-- h.ask(text) -- send text as an ai-kind prompt (same path as :ask)
|
||||
-- h.status(s) -- emit a [aish] status line
|
||||
-- h.exec(cmd) -- run a shell command (subject to plan/hooks)
|
||||
-- h.model() -- current active model name
|
||||
-- h.ctx -- raw context object (advanced)
|
||||
-- h.config -- raw config table
|
||||
local skills = {} -- { [name] = {description=, run=} }
|
||||
local function load_skills()
|
||||
local dir = os.getenv("AISH_SKILLS_DIR")
|
||||
or ((os.getenv("HOME") or ".") .. "/.config/aish/skills")
|
||||
local pipe = io.popen(("ls -1 %q/*.lua 2>/dev/null"):format(dir))
|
||||
if not pipe then return end
|
||||
for path in pipe:lines() do
|
||||
local ok, mod = pcall(dofile, path)
|
||||
if not ok then
|
||||
renderer.status(("skill load failed: %s: %s")
|
||||
:format(path, tostring(mod)))
|
||||
elseif type(mod) ~= "table"
|
||||
or type(mod.name) ~= "string"
|
||||
or type(mod.run) ~= "function"
|
||||
or not mod.name:match("^[%w_-]+$")
|
||||
then
|
||||
renderer.status(("skill %s: invalid module (need {name, run})")
|
||||
:format(path))
|
||||
elseif meta[mod.name] or skills[mod.name] then
|
||||
renderer.status(("skill %s: name '%s' already in use")
|
||||
:format(path, mod.name))
|
||||
else
|
||||
skills[mod.name] = {
|
||||
description = mod.description or "",
|
||||
run = mod.run,
|
||||
}
|
||||
local helpers = {
|
||||
ask = function(t) ask_ai(expand_mentions(t or "", renderer.status)) end,
|
||||
status = renderer.status,
|
||||
exec = run_shell,
|
||||
model = function() return active_name end,
|
||||
ctx = ctx,
|
||||
config = config,
|
||||
}
|
||||
meta[mod.name] = function(args)
|
||||
local sk_ok, sk_err = pcall(mod.run, args or "", helpers)
|
||||
if not sk_ok then
|
||||
renderer.status(("skill %s failed: %s")
|
||||
:format(mod.name, tostring(sk_err)))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
pipe:close()
|
||||
end
|
||||
meta.skills = function()
|
||||
local names = {}
|
||||
for n, _ in pairs(skills) do names[#names + 1] = n end
|
||||
table.sort(names)
|
||||
if #names == 0 then renderer.status("(no skills loaded)"); return end
|
||||
renderer.status(("skills (%d):"):format(#names))
|
||||
for _, n in ipairs(names) do
|
||||
io.write((" :%-16s %s\n"):format(n, skills[n].description))
|
||||
end
|
||||
end
|
||||
load_skills()
|
||||
|
||||
-- Main loop.
|
||||
while true do
|
||||
local line = rl.readline(prompt())
|
||||
|
||||
Reference in New Issue
Block a user