| 1 |
|
defmodule Agent.Spawn do |
| 2 |
|
import Application, only: [get_env: 2] |
| 3 |
|
import Logger, only: [error: 1, warning: 1] |
| 4 |
|
import Port, only: [open: 2, close: 1] |
| 5 |
|
import String, only: [trim: 1] |
| 6 |
|
import System, only: [find_executable: 1] |
| 7 |
|
import File, only: [read: 1] |
| 8 |
|
|
| 9 |
:-( |
def run(message, folder, self \\ nil) do |
| 10 |
:-( |
cmd = {:spawn_executable, exe()} |
| 11 |
:-( |
open(cmd, setup(message, folder, self)) |> drain() |
| 12 |
|
end |
| 13 |
|
|
| 14 |
|
defp exe do |
| 15 |
:-( |
find_executable("claude") |> pick() |
| 16 |
|
end |
| 17 |
|
|
| 18 |
:-( |
defp pick(nil), do: raise("no claude") |
| 19 |
:-( |
defp pick(path), do: path |
| 20 |
|
|
| 21 |
:-( |
defp drain({:error, reason}) do |
| 22 |
:-( |
error("Failed to open Claude port: #{inspect(reason)}") |
| 23 |
|
"ERROR: Could not start Claude" |
| 24 |
|
end |
| 25 |
|
|
| 26 |
:-( |
defp drain(port) do |
| 27 |
:-( |
read(port, "") |
| 28 |
|
after |
| 29 |
:-( |
seal(port) |
| 30 |
|
end |
| 31 |
|
|
| 32 |
|
defp setup(message, folder, self) do |
| 33 |
:-( |
model = get_env(:elita, :claude_model) |
| 34 |
:-( |
args = ["-p", message, "--allowedTools", "", "--model", model] ++ prompt(self) |
| 35 |
:-( |
base = [{:args, args}, {:cd, to_charlist(folder)}] |
| 36 |
:-( |
base ++ [:binary, :exit_status, :use_stdio] |
| 37 |
|
end |
| 38 |
|
|
| 39 |
:-( |
defp prompt(nil), do: [] |
| 40 |
:-( |
defp prompt(path), do: inject(read(path), path) |
| 41 |
|
|
| 42 |
:-( |
defp inject({:ok, content}, _path), do: ["--append-system-prompt", content] |
| 43 |
:-( |
defp inject({:error, _}, _path), do: [] |
| 44 |
|
|
| 45 |
|
defp read(port, acc) do |
| 46 |
:-( |
listen(port, acc) |
| 47 |
|
end |
| 48 |
|
|
| 49 |
|
defp listen(port, acc) do |
| 50 |
:-( |
receive do |
| 51 |
:-( |
{^port, msg} -> recv(msg, port, acc) |
| 52 |
|
after |
| 53 |
:-( |
30000 -> stall(port, acc) |
| 54 |
|
end |
| 55 |
|
end |
| 56 |
|
|
| 57 |
:-( |
defp recv({:data, data}, port, acc), do: read(port, acc <> data) |
| 58 |
:-( |
defp recv({:exit_status, _}, _port, acc), do: trim(acc) |
| 59 |
|
|
| 60 |
|
defp stall(port, acc) do |
| 61 |
:-( |
slay(port) |
| 62 |
:-( |
warning("Claude port timeout") |
| 63 |
:-( |
acc |
| 64 |
|
end |
| 65 |
|
|
| 66 |
:-( |
defp slay(port) do |
| 67 |
:-( |
ref = :erlang.monitor(:port, port) |
| 68 |
:-( |
wait(ref, port) |
| 69 |
|
rescue |
| 70 |
:-( |
_ -> :ok |
| 71 |
|
end |
| 72 |
|
|
| 73 |
|
defp wait(ref, port) do |
| 74 |
:-( |
:erlang.port_close(port) |
| 75 |
:-( |
mark(ref, port) |
| 76 |
|
end |
| 77 |
|
|
| 78 |
|
defp mark(ref, port) do |
| 79 |
:-( |
receive do |
| 80 |
:-( |
{:DOWN, ^ref, :port, ^port, _} -> :ok |
| 81 |
|
after |
| 82 |
:-( |
8_000 -> :ok |
| 83 |
|
end |
| 84 |
|
end |
| 85 |
|
|
| 86 |
:-( |
defp seal(port) do |
| 87 |
:-( |
close(port) |
| 88 |
|
rescue |
| 89 |
:-( |
_ -> :ok |
| 90 |
|
end |
| 91 |
|
end |