Packages

fnord

0.9.32

AI code archaeology

Current section

Files

Jump to
fnord lib fnord.ex
Raw

lib/fnord.ex

defmodule Fnord do
@moduledoc """
Fnord is a code search tool that uses OpenAI's embeddings API to index and
search code files.
"""
@desc "fnord - an AI code archaeologist"
# HTTP pool sizes are fixed based on the expected concurrency profile of
# each subsystem. The API pool handles the coordinator's LLM calls; the
# others handle background work that should not compete with the
# coordinator for connections.
@pool_api 12
@pool_indexer 6
@pool_memory 6
@pool_notes 6
@doc """
Main entry point for the application. Parses command line arguments and
dispatches to the appropriate subcommand.
"""
def main(args) do
configure_logger()
Services.start_all()
# Load all frob modules to prevent race conditions during concurrent operations
Frobs.load_all_modules()
# Parse command line arguments
with {:ok, [command | subcommands], opts, unknown} <- parse_options(args) do
# Certain options are common to most, if not all, subcommands, and control
# global state (e.g. project, verbosity).
opts = set_globals(opts)
cmd_module = to_module_name(command)
# -------------------------------------------------------------------------
# HTTP Pools
# -------------------------------------------------------------------------
:hackney_pool.start_pool(:ai_api, max_connections: @pool_api)
:hackney_pool.start_pool(:ai_indexer, max_connections: @pool_indexer)
:hackney_pool.start_pool(:ai_memory, max_connections: @pool_memory)
:hackney_pool.start_pool(:ai_notes, max_connections: @pool_notes)
# Start services that depend on CLI configuration now that it's available.
# These services depend on settings parsed from CLI arguments in set_globals().
Services.start_config_dependent_services(command)
# Require a project if the command needs it. As a rule, we make the
# project option optional in the Cmd implementation, and then manually
# require it here. That allows us to default to the project associated
# with the current directory, if possible.
if cmd_module.requires_project?() && !Settings.project_is_set?() do
UI.fatal("""
Error: The command `#{command}` must be called from an indexed project directory or with the --project option set to an indexed project.
""")
exit({:shutdown, 1})
end
# While the subcommand is working, we can check for a new version in the
# background (unless we're running the upgrade command itself).
maybe_start_version_check(command)
# Run the subcommand
Cmd.perform_command(cmd_module, opts, subcommands, unknown)
# Once the command has finished, we can check if a new version is
# available and prompt the user to upgrade if there is.
maybe_show_version_notification(command)
end
end
def spec() do
subcommands = [
Cmd.Ask,
Cmd.Config,
Cmd.Conversations,
Cmd.Files,
Cmd.Frobs,
Cmd.Index,
Cmd.Notes,
Cmd.Prime,
Cmd.Projects,
Cmd.Memory,
Cmd.Replay,
Cmd.Search,
Cmd.Skills,
Cmd.Summary,
Cmd.Torch,
Cmd.Upgrade,
Cmd.Worktrees
]
[
name: "fnord",
description: @desc,
version: Util.get_running_version(),
subcommands: Enum.flat_map(subcommands, & &1.spec())
]
end
# Optimus only recognizes `--help` as the sole argument or `help <subcommand>`
# as a prefix. It does NOT handle `<subcommand> --help` or `<subcommand> -h`.
# We normalize argv so that any occurrence of --help/-h is pulled out and
# rewritten into the `help <rest>` form that Optimus expects.
defp normalize_help(args) do
if Enum.any?(args, &(&1 in ["--help", "-h"])) do
rest = Enum.reject(args, &(&1 in ["--help", "-h"]))
case rest do
[] -> ["--help"]
_ -> ["help" | rest]
end
else
case args do
["help"] -> ["--help"]
_ -> args
end
end
end
@spec parse_options([String.t()]) ::
{:ok, [atom()], map(), [String.t()]} | {:error, :no_subcommand}
defp parse_options(args) do
spec()
|> Optimus.new!()
|> Optimus.parse!(normalize_help(args))
|> case do
{subcommand, %Optimus.ParseResult{} = result} ->
{:ok, subcommand, merge_options(result), result.unknown}
# If no subcommand is specified, fail.
_ ->
{:error, :no_subcommand}
end
end
defp merge_options(optimus_result) do
optimus_result.args
|> Map.merge(optimus_result.options)
|> Map.merge(optimus_result.flags)
end
def configure_logger do
{:ok, handler_config} = :logger.get_handler_config(:default)
updated_config = Map.update!(handler_config, :config, &Map.put(&1, :type, :standard_error))
:ok = :logger.remove_handler(:default)
:ok = :logger.add_handler(:default, :logger_std_h, updated_config)
:ok =
:logger.update_formatter_config(
:default,
:template,
["[", :level, "] ", :message, "\n"]
)
logger_level =
case Util.Env.get_env("LOGGER_LEVEL", "info") do
level when level in ~w[emergency alert critical error warning notice info debug] ->
String.to_existing_atom(level)
invalid ->
IO.warn("Invalid LOGGER_LEVEL '#{invalid}', defaulting to :info")
:info
end
:ok = :logger.set_primary_config(:level, logger_level)
end
defp to_module_name(subcommand) do
submodule =
subcommand
|> Atom.to_string()
|> Macro.camelize()
Module.concat("Cmd", submodule)
end
defp set_globals(args) do
args
|> Enum.each(fn
{:quiet, quiet} ->
Settings.set_quiet(quiet)
{:project, project} when is_binary(project) ->
Settings.set_project(project)
{:edit, edit_mode} ->
Settings.set_edit_mode(edit_mode)
{:yes, v} when is_integer(v) ->
Settings.set_auto_approve(v > 0)
Settings.set_yes_count(v)
{:yes, v} when is_boolean(v) ->
Settings.set_auto_approve(v)
Settings.set_yes_count(if v, do: 1, else: 0)
{:fonz, val} ->
Settings.set_fonz_mode(!!val)
_ ->
:ok
end)
# --------------------------------------------------------------------------
# If the user did not specify a project in ARGV, resolve it via ResolveProject.
# --------------------------------------------------------------------------
unless Settings.project_is_set?() do
case ResolveProject.resolve(nil) do
{:ok, project} ->
UI.info("Project not specified, but resolved to #{project}")
Settings.set_project(project)
{:error, :not_in_project} ->
:ok
end
end
# --------------------------------------------------------------------------
# When not connected to a TTY, the --quiet flag is automatically enabled,
# unless the user explicitly specifies it.
# --------------------------------------------------------------------------
cond do
args[:quiet] -> args
IO.ANSI.enabled?() -> args
true -> Map.put(args, :quiet, true)
end
end
# Start background version check unless running upgrade command
# Dialyzer can't infer that command is a dynamic atom from CLI args
@dialyzer {:no_match, maybe_start_version_check: 1}
@spec maybe_start_version_check(atom()) :: :ok
defp maybe_start_version_check(:upgrade), do: :ok
defp maybe_start_version_check(_command) do
task = Services.Globals.Spawn.async(fn -> Util.get_latest_version() end)
Process.put(:version_check_task, task)
:ok
end
# Show version notification if available
# Dialyzer can't infer that command is a dynamic atom from CLI args
@dialyzer {:no_match, maybe_show_version_notification: 1}
@spec maybe_show_version_notification(atom()) :: :ok
defp maybe_show_version_notification(:upgrade), do: :ok
defp maybe_show_version_notification(_command) do
case Process.get(:version_check_task) do
nil ->
:ok
task ->
with {:ok, {:ok, latest}} <- Task.yield(task, 1000) do
current = Util.get_running_version()
if Version.compare(current, latest) == :lt do
UI.info("""
A new version of fnord is available! To upgrade to v#{latest}:
fnord upgrade
""")
end
end
end
end
end