Packages
fnord
0.8.64
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/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"
@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, number of workers).
opts = set_globals(opts)
cmd_module = to_module_name(command)
# -------------------------------------------------------------------------
# HTTP Pools
# -------------------------------------------------------------------------
# Start dedicated pool for background indexer (half workers, at least 1)
:hackney_pool.start_pool(:ai_indexer,
max_connections:
Services.Globals.get_env(:fnord, :workers, Cmd.default_workers())
|> div(2)
|> max(1)
)
# Start a dedicated pool for research notes (just 1 worker, since it's
# restricted to a single genserver process and not concurrent).
:hackney_pool.start_pool(:ai_notes, max_connections: 3)
# Configure the :hackney_pool to limit the number of concurrent requests.
:hackney_pool.start_pool(:ai_api,
max_connections: Services.Globals.get_env(:fnord, :workers, Cmd.default_workers())
)
# 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.Replay,
Cmd.Search,
Cmd.Summary,
Cmd.Torch,
Cmd.Upgrade
]
[
name: "fnord",
description: @desc,
version: Util.get_running_version(),
subcommands: Enum.flat_map(subcommands, & &1.spec())
]
end
@spec parse_options([String.t()]) ::
{:ok, [atom()], map(), [String.t()]} | {:error, :no_subcommand}
defp parse_options(args) do
spec()
|> Optimus.new!()
|> Optimus.parse!(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 System.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
{:workers, workers} ->
Settings.set_workers(workers)
{: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, yes} ->
Settings.set_auto_approve(yes)
_ ->
: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