Packages
fnord
0.9.19
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"
# 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
]
[
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