Packages
fnord
0.9.18
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/ai/tools/save_skill.ex
defmodule AI.Tools.SaveSkill do
@moduledoc """
Save a new skill definition into the current project's skills directory.
The coordinator can use this tool to persist skill TOML files under:
`~/.fnord/projects/<project>/skills/<name>.toml`
Safety rules:
- The tool is synchronous.
- It requires explicit user confirmation via `UI.confirm/2`.
- It refuses to create a project skill if a user-defined skill with the same
name already exists in `~/fnord/skills` (because user definitions override
project definitions).
"""
@behaviour AI.Tools
@tool_name "save_skill"
@impl AI.Tools
def async?, do: false
@impl AI.Tools
def is_available?, do: true
@impl AI.Tools
def ui_note_on_request(%{"name" => name}) do
{"Save skill", name}
end
@impl AI.Tools
def ui_note_on_result(%{"name" => name}, _result) do
{"Saved skill", name}
end
@impl AI.Tools
def tool_call_failure_message(_args, _reason), do: :default
@impl AI.Tools
def read_args(%{} = args) do
with {:ok, name} <- get_string(args, "name"),
{:ok, description} <- get_string(args, "description"),
{:ok, model} <- get_string(args, "model"),
{:ok, tools} <- get_string_list(args, "tools"),
{:ok, system_prompt} <- get_string(args, "system_prompt"),
{:ok, response_format} <- get_optional_map(args, "response_format") do
{:ok,
%{
"name" => name,
"description" => description,
"model" => model,
"tools" => tools,
"system_prompt" => system_prompt,
"response_format" => response_format
}}
end
end
defp get_string(args, key) do
case Map.fetch(args, key) do
{:ok, value} when is_binary(value) and byte_size(value) > 0 -> {:ok, value}
_ -> {:error, "Missing or empty required argument: #{key}"}
end
end
defp get_string_list(args, key) do
case Map.fetch(args, key) do
{:ok, list} when is_list(list) ->
list
|> Enum.filter(&is_binary/1)
|> case do
[] -> {:error, "Missing or empty required argument: #{key}"}
strings -> {:ok, strings}
end
_ ->
{:error, "Missing or empty required argument: #{key}"}
end
end
defp get_optional_map(args, key) do
case Map.fetch(args, key) do
:error -> {:ok, nil}
{:ok, nil} -> {:ok, nil}
{:ok, value} when is_map(value) -> {:ok, value}
_ -> {:error, {:invalid_arg, key}}
end
end
@impl AI.Tools
def spec() do
%{
type: "function",
function: %{
name: @tool_name,
description:
"Save a skill into the current project's skills directory (~/.fnord/projects/<project>/skills).",
parameters: %{
type: "object",
additionalProperties: false,
properties: %{
name: %{type: "string", description: "Skill name"},
description: %{type: "string", description: "Brief description"},
model: %{
type: "string",
description: "Model preset (smart/balanced/fast/web/large_context...)"
},
tools: %{type: "array", items: %{type: "string"}, description: "Tool tags"},
system_prompt: %{type: "string", description: "Base system prompt"},
response_format: %{type: "object", description: "Optional response_format map"}
},
required: ["name", "description", "model", "tools", "system_prompt"]
}
}
}
end
@valid_name_pattern ~r/^[a-z0-9][a-z0-9_-]*$/
@impl AI.Tools
def call(%{
"name" => name,
"description" => description,
"model" => model,
"tools" => tools,
"system_prompt" => system_prompt,
"response_format" => response_format
}) do
with :ok <- validate_name(name),
{:ok, project_dir} <- Skills.project_skills_dir(),
:ok <- check_user_collision(name),
{:ok, _model_preset} <- Skills.Runtime.model_from_string(model),
{:ok, _toolbox} <- Skills.Runtime.toolbox_from_tags(tools),
{:ok, _rf} <- Skills.Runtime.validate_response_format(response_format),
{:ok, toml} <-
Skills.Toml.encode_skill(%Skills.Skill{
name: name,
description: description,
model: model,
tools: tools,
system_prompt: system_prompt,
response_format: response_format
}),
{:ok, path} <- skill_path(project_dir, name),
:ok <- confirm_write(path),
:ok <- write_skill_file(path, toml) do
{:ok, "Saved skill #{name} to #{path}"}
end
end
defp validate_name(name) do
if Regex.match?(@valid_name_pattern, name) do
:ok
else
{:error, "Invalid skill name '#{name}'. Names must match [a-z0-9][a-z0-9_-]*."}
end
end
defp check_user_collision(name) do
user_dir = Skills.user_skills_dir()
# Check only whether a file with this name exists in the user dir, rather
# than loading the entire directory (which can fail on unrelated issues
# like duplicate names among other skills).
path = Path.join(user_dir, "#{name}.toml")
if File.exists?(path) do
{:error,
"A user-defined skill named '#{name}' already exists at #{path}. " <>
"User skills override project skills, so this save would be shadowed."}
else
:ok
end
end
defp skill_path(project_dir, name) do
File.mkdir_p(project_dir)
|> case do
:ok -> {:ok, Path.join(project_dir, "#{name}.toml")}
{:error, reason} -> {:error, reason}
end
end
defp confirm_write(path) do
if UI.confirm("Write skill to #{path}?", false) do
:ok
else
{:error, :aborted}
end
end
defp write_skill_file(path, toml) do
lock_path = path <> ".lock"
FileLock.with_lock(lock_path, fn ->
Settings.write_atomic!(path, toml)
:ok
end)
|> case do
{:ok, :ok} -> :ok
{:error, reason} -> {:error, reason}
end
rescue
e -> {:error, Exception.message(e)}
end
end