Packages
fnord
0.8.27
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/agent/shell_cmd_parser.ex
defmodule AI.Agent.ShellCmdParser do
@behaviour AI.Agent
@max_attempts 3
@model AI.Model.balanced()
@prompt """
You are an expert shell parser.
Your job is to analyze a given shell command string, extract its structural components, and output a strict JSON object as specified below.
Instructions:
- Read the command as a user would type it in a shell, e.g., `git -C path rev-parse --show-toplevel`.
- Parse out the **base command** (the executable), and group any "subcommand" or "verb" that acts as a command selector.
- For commands like `git rev-parse`, the subcommand is `rev-parse`.
- The "cmd" field must be the main executable (e.g., "git").
- The "args" field must be an array of all following tokens, preserving their order.
- The "approval_bits" array must contain the executable and any subcommands that define a *unique operation*.
- For example, for `git -C path rev-parse --show-toplevel`, the approval_bits are ["git", "rev-parse"] (ignore flags and options).
- For `find . -type f`, approval_bits is just ["find"].
- For `docker run -it ubuntu`, approval_bits is ["docker", "run"].
- If the input cannot be confidently parsed, provide an error message explaining why.
Special rules
- Do **not** attempt to guess meanings—split strictly on whitespace, respecting quoted arguments.
- Treat the first token as "cmd", everything else as "args".
- "approval_bits" should only include the executable and literal subcommands (verbs), not options or arguments.
- If the command is ambiguous or doesn't fit the above (e.g., missing executable), respond with an error.
- If the command is obviously malformed or incomplete (e.g., missing or duplicated command or arguments, or contains only flags/options without a command), respond with an error.
- If the command includes any of the following constructs, the "cmd" is "sh", and "args" is ["-c", "<entire_command>"]:
- redirection (`>`, `>>`, `<`, `2>`)
- pipes (`|`)
- subshells or command substitution (`$()`, `` ` ``)
- logical operators (`&&`, `||`)
- semicolons (`;`)
- backgrounding (`&`)
- process substitution (`<(...)`, `>(...)`)
"""
@response_format %{
type: "json_schema",
json_schema: %{
name: "shell_command_parse",
description: """
A JSON object containing the parsed components of a shell command,
or an error message if the command cannot be parsed.
""",
schema: %{
type: "object",
properties: %{
cmd: %{
type: "string",
minLength: 1,
description: "The main executable command"
},
args: %{
type: "array",
items: %{type: "string"},
description: "Array of all arguments following the command"
},
approval_bits: %{
type: "array",
items: %{type: "string"},
minItems: 1,
description:
"Array containing the executable and any subcommands that define a unique operation"
},
error: %{
type: "string",
description: "Error message explaining why the command could not be parsed"
}
},
additionalProperties: false
}
}
}
@impl AI.Agent
def get_response(opts) do
with {:ok, shell_cmd} <- Map.fetch(opts, :shell_cmd) do
shell_cmd
|> String.trim()
|> do_stuff()
end
end
defp do_stuff(shell_cmd, attempt \\ 1)
defp do_stuff(shell_cmd, attempt) when attempt <= @max_attempts do
shell_cmd
|> get_completion()
|> validate_response()
|> case do
{:ok, result} ->
{:ok, result}
{:rejected, reason} ->
{:error, "Command rejected: #{reason}"}
{:invalid_format, reason} ->
UI.warn("[shell-cmd-parser]", "Input: #{inspect(shell_cmd)}, Error: #{inspect(reason)}")
do_stuff(shell_cmd, attempt + 1)
end
end
defp do_stuff(_shell_cmd, _attempt) do
{:error, "Failed to parse command after #{@max_attempts} attempts."}
end
defp get_completion(shell_cmd) do
AI.Completion.get(
log_msgs: false,
log_tool_calls: false,
model: @model,
response_format: @response_format,
messages: [
AI.Util.system_msg(@prompt),
AI.Util.user_msg("""
Please parse the following shell command:
```sh
#{shell_cmd}
```
""")
]
)
|> case do
{:ok, %{response: response}} -> {:ok, response}
{:error, reason} -> {:error, reason}
end
end
defp validate_response({:error, reason}), do: {:error, reason}
defp validate_response({:ok, response}) do
# JSON parsing and structure validation is now guaranteed by response_format
case Jason.decode!(response) do
%{"error" => error} ->
{:rejected, error}
%{"cmd" => cmd, "args" => args, "approval_bits" => approval_bits} = parsed ->
# Basic sanity checks (though schema should prevent these)
cond do
not is_binary(cmd) or cmd == "" ->
{:invalid_format, "The 'cmd' field must be a non-empty string."}
not is_list(args) ->
{:invalid_format, "The 'args' field must be an array."}
not is_list(approval_bits) or length(approval_bits) == 0 ->
{:invalid_format, "The 'approval_bits' field must be a non-empty array."}
true ->
{:ok, parsed}
end
result ->
# Handle case where neither proper fields nor error is provided
if Map.has_key?(result, "cmd") || Map.has_key?(result, "args") ||
Map.has_key?(result, "approval_bits") do
{:invalid_format, "Incomplete command parsing result"}
else
{:invalid_format, "No valid command parse or error provided"}
end
end
end
end