Packages
fnord
0.8.43
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/cmd/ask.ex
defmodule Cmd.Ask do
@default_rounds 1
# Universal default: auto-deny after 180 seconds if no explicit auto flag
@default_auto_policy {:deny, 180_000}
@behaviour Cmd
@impl Cmd
def requires_project?(), do: true
@impl Cmd
def spec() do
[
ask: [
name: "ask",
about: "Ask the AI a question about the project",
options: [
project: Cmd.project_arg(),
workers: Cmd.workers_arg(),
question: [
value_name: "QUESTION",
long: "--question",
short: "-q",
help: "The prompt to ask the AI",
required: true
],
auto_approve_after: [
value_name: "SECONDS",
long: "--auto-approve-after",
short: "-A",
help:
"After notification, auto-APPROVE in SECONDS if no input. Default: auto-DENY after 180 seconds.",
parser: :integer,
required: false
],
auto_deny_after: [
value_name: "SECONDS",
long: "--auto-deny-after",
short: "-D",
help:
"After notification, auto-DENY in SECONDS if no input. Default: auto-DENY after 180 seconds.",
parser: :integer,
required: false
],
rounds: [
value_name: "ROUNDS",
long: "--rounds",
short: "-R",
help:
"The number of research rounds to perform. Additional rounds generally result in more thorough research.",
parser: :integer,
default: @default_rounds,
required: false
],
worktree: [
value_name: "WORKTREE",
long: "--worktree",
short: "-W",
help: "Override project source root for this run",
parser: :string,
required: false
],
follow: [
value_name: "UUID",
long: "--follow",
short: "-f",
help: "Continue an existing conversation by UUID",
required: false
],
fork: [
value_name: "UUID",
long: "--fork",
short: "-F",
help: "Fork (branch) an existing conversation by UUID",
required: false
]
],
flags: [
replay: [
long: "--replay",
short: "-r",
help: "Replay a conversation (with --follow)",
default: false
],
edit: [
long: "--edit",
short: "-e",
help: "Permit the AI to edit files in the project",
default: false
],
yes: [
long: "--yes",
short: "-y",
help: "Automatically approve edit/manage prompts (requires --edit)",
default: false
]
]
]
]
end
@impl Cmd
def run(opts, _subcommands, _unknown) do
# Start a new performance tracking session
Services.ModelPerformanceTracker.start_session()
opts =
if opts[:edit] do
UI.warning_banner("EDITING MODE ENABLED! THE AI CAN MODIFY FILES. YOU MUST BE NUTS.")
UI.warn("You can work from a git worktree with --worktree <dir>.")
opts
else
Map.put(opts, :edit, false)
end
# Handle --yes auto-approval flag
if opts[:yes] do
if opts[:edit] do
UI.warning_banner("AUTO-CONFIRMATION ENABLED FOR CODE EDIT PROMPTS.")
UI.warning_banner("ALL YOU'VE *REALLY* AUTO-CONFIRMED IS THAT YOU ARE INDEED NUTS.")
else
UI.warn("--yes has no effect unless you also pass --edit; ignoring")
end
end
start_time = System.monotonic_time(:second)
try do
with {:ok, opts} <- validate(opts),
:ok <- set_auto_policy(opts),
:ok <- set_worktree(opts),
{:ok, opts} <- maybe_fork_conversation(opts),
{:ok, pid} <- Services.Conversation.start_link(opts[:follow]),
{:ok, usage, context, response} <- get_response(opts, pid),
{:ok, conversation_id} <- save_conversation(pid) do
end_time = System.monotonic_time(:second)
print_result(start_time, end_time, response, usage, context, conversation_id)
Clipboard.copy(conversation_id)
:ok
else
{:error, :testing} ->
:ok
{:error, :invalid_worktree} ->
UI.error("--worktree must be an existing directory")
{:error, :invalid_worktree}
{:error, :auto_approval_mutually_exclusive} ->
UI.error("--auto-approve-after and --auto-deny-after are mutually exclusive")
{:error, :auto_approval_mutually_exclusive}
{:error, :invalid_auto_approve_after} ->
UI.error("--auto-approve-after must be a positive integer")
{:error, :invalid_auto_approve_after}
{:error, :invalid_auto_deny_after} ->
UI.error("--auto-deny-after must be a positive integer")
{:error, :invalid_auto_deny_after}
{:error, :invalid_rounds} ->
UI.error("--rounds expects a positive integer")
{:error, :invalid_rounds}
{:error, :conversation_not_found} ->
UI.error("Conversation ID #{opts[:conversation]} not found")
{:error, :conversation_not_found}
{:error, other} ->
UI.error("An error occurred while generating the response:\n\n#{other}")
{:error, other}
end
after
Services.BackupFile.offer_cleanup()
Services.Notes.join()
end
end
# ----------------------------------------------------------------------------
# Validation
# ----------------------------------------------------------------------------
defp validate(opts) do
with :ok <- validate_conversation(opts),
:ok <- validate_rounds(opts),
:ok <- validate_auto(opts),
:ok <- validate_worktree(opts) do
{:ok, opts}
end
end
defp validate_rounds(%{rounds: rounds}) when is_integer(rounds) and rounds > 0, do: :ok
defp validate_rounds(_opts), do: {:error, :invalid_rounds}
# Validate mutual exclusion and positivity of auto flags
@spec validate_auto(map) :: :ok | {:error, atom | binary}
def validate_auto(opts) do
case {opts[:auto_approve_after], opts[:auto_deny_after]} do
{a, d} when not is_nil(a) and not is_nil(d) ->
{:error, :auto_approval_mutually_exclusive}
{a, _} when not is_nil(a) and a <= 0 ->
{:error, :invalid_auto_approve_after}
{_, d} when not is_nil(d) and d <= 0 ->
{:error, :invalid_auto_deny_after}
_ ->
:ok
end
end
@spec validate_worktree(map) :: :ok | {:error, :invalid_worktree}
defp validate_worktree(%{worktree: dir}) when is_binary(dir) do
path = Path.expand(dir)
if File.dir?(path) do
:ok
else
{:error, :invalid_worktree}
end
end
defp validate_worktree(_), do: :ok
@spec validate_conversation(map) :: :ok | {:error, :conversation_not_found}
defp validate_conversation(%{follow: id}) when is_binary(id) do
id
|> Store.Project.Conversation.new()
|> Store.Project.Conversation.exists?()
|> case do
true -> :ok
false -> {:error, :conversation_not_found}
end
end
defp validate_conversation(_opts), do: :ok
# -----------------------------------------------------------------------------
# Worktree setting
# -----------------------------------------------------------------------------
defp set_worktree(%{worktree: dir}) do
Settings.set_project_root_override(dir)
end
# No override was set in edit mode, but if the user is in a git worktree, we
# should check whether the user _intended_ to set the worktree to cwd instead
# of defaulting to the project root.
defp set_worktree(%{edit: true}) do
with {:ok, project} <- Store.get_project() do
wt_root = GitCli.worktree_root()
if GitCli.is_worktree?() && wt_root != project.source_root do
choice =
UI.confirm(
"""
You are working with project "#{project.name}", located at #{project.source_root}.
However, your current working directory is within a git worktree at #{wt_root}.
Would you like to set the project source root to #{wt_root} for this run?
""",
false
)
if choice do
Settings.set_project_root_override(wt_root)
else
:ok
end
end
else
_ -> :ok
end
end
defp set_worktree(_opts), do: :ok
# -----------------------------------------------------------------------------
# Auto-approval policy
# -----------------------------------------------------------------------------
def set_auto_policy(%{auto_approve_after: seconds}) when is_integer(seconds) do
Settings.set_auto_policy({:approve, seconds * 1_000})
end
def set_auto_policy(%{auto_deny_after: seconds}) when is_integer(seconds) do
Settings.set_auto_policy({:deny, seconds * 1_000})
end
def set_auto_policy(_opts) do
# No explicit auto flags: apply universal default auto-deny policy
Settings.set_auto_policy(@default_auto_policy)
end
# -----------------------------------------------------------------------------
# Forking a conversation
# -----------------------------------------------------------------------------
defp maybe_fork_conversation(%{fork: fork_id} = opts) when is_binary(fork_id) do
fork_conv = Store.Project.Conversation.new(fork_id)
if Store.Project.Conversation.exists?(fork_conv) do
with {:ok, new_conv} <- Store.Project.Conversation.fork(fork_conv) do
UI.info("Conversation #{fork_id} forked as #{new_conv.id}")
{:ok, Map.put(opts, :follow, new_conv.id)}
end
else
{:error, :conversation_not_found}
end
end
defp maybe_fork_conversation(opts), do: {:ok, opts}
# ----------------------------------------------------------------------------
# Agent response
# ----------------------------------------------------------------------------
@spec get_response(map, pid) ::
{:ok, non_neg_integer, non_neg_integer, binary}
| {:error, any}
defp get_response(opts, conversation_server) do
opts
|> get_agent_response(conversation_server)
|> case do
{:ok, %{usage: usage, context: context, last_response: res}} ->
{:ok, usage, context, res}
{:error, reason} ->
{:error, reason}
end
end
@spec get_agent_response(map, pid) :: {:ok, map} | {:error, any}
defp get_agent_response(opts, conversation_server) do
Services.Conversation.get_response(conversation_server,
edit: opts.edit,
rounds: opts.rounds,
question: opts.question,
replay: Map.get(opts, :replay, false),
yes: Map.get(opts, :yes, false)
)
end
# ----------------------------------------------------------------------------
# Output
# ----------------------------------------------------------------------------
defp print_result(start_time, end_time, response, usage, context, conversation_id) do
time_taken = end_time - start_time
pct_context_used =
if usage > 0 and context > 0 do
Float.round(usage / context * 100, 2)
else
0.0
end
usage_str = Util.format_number(usage)
context_str = Util.format_number(context)
{:ok, project} = Store.get_project()
%{new: new, stale: stale, deleted: deleted} = Store.Project.index_status(project)
UI.say("""
#{response}
-----
### Response Summary:
- Response generated in #{time_taken} seconds
- Tokens used: #{usage_str} | #{pct_context_used}% of context window (#{context_str})
- Conversation saved with ID #{conversation_id} (_copied to clipboard_)
### Project Search Index Status:
- Stale: #{Enum.count(stale)}
- New: #{Enum.count(new)}
- Deleted: #{Enum.count(deleted)}
_Run `fnord index` to update the index._
""")
UI.flush()
end
defp save_conversation(pid) do
with {:ok, conversation} <- Services.Conversation.save(pid) do
UI.debug("Conversation saved to file", conversation.store_path)
UI.report_step("Conversation saved", conversation.id)
{:ok, conversation.id}
end
end
end