Packages
fnord
0.9.3
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/memory.ex
defmodule Cmd.Memory do
@min_match_threshold 0.2
alias Memory.Presentation
@behaviour Cmd
@impl Cmd
def requires_project?(), do: false
@impl Cmd
def spec() do
[
memory: [
name: "memory",
about: "List memories or perform a semantic search across memory scopes",
options: [
scope: [
value_name: "SCOPE",
long: "--scope",
short: "-s",
help: "Limit to scope(s): global, project (may be repeated)",
required: false,
multiple: true
],
query: [
value_name: "QUERY",
long: "--query",
short: "-q",
help: "Semantic search query. If omitted, all memories are shown.",
required: false
]
]
]
]
end
@impl Cmd
def run(opts, _subcommands, unknown) do
if unknown != [] do
UI.warn("Ignoring unknown arguments: #{Enum.join(unknown, " ")}")
end
scopes = parse_scopes(Map.get(opts, :scope))
markdown =
case Map.get(opts, :query) do
nil -> render_list(scopes)
query -> render_search(scopes, query)
end
markdown
|> UI.Formatter.format_output()
|> UI.puts()
end
# ----------------------------------------------------------------------------
# Listing
# ----------------------------------------------------------------------------
defp render_list(scopes) do
sections =
scopes
|> Enum.map(fn scope ->
case available?(scope) do
true ->
{:ok, titles} = list_titles(scope)
memories =
titles
|> Enum.sort()
|> Enum.map(fn title ->
{:ok, mem} = Memory.read(scope, title)
{mem, nil}
end)
render_scope_section(scope, memories, %{mode: :list})
false ->
render_unavailable_scope_section(scope)
end
end)
"""
# Memories
#{Enum.join(sections, "\n\n")}
"""
end
# ----------------------------------------------------------------------------
# Search
# ----------------------------------------------------------------------------
defp render_search(scopes, query) when is_binary(query) do
query = String.trim(query)
case Indexer.impl().get_embeddings(query) do
{:ok, needle} ->
sections =
scopes
|> Enum.map(fn scope ->
if available?(scope) do
{:ok, titles} = list_titles(scope)
{matches, stale_count} =
titles
|> Enum.reduce({[], 0}, fn title, {acc, stale} ->
case Memory.read(scope, title) do
{:ok, %Memory{embeddings: nil}} ->
{acc, stale + 1}
{:ok, %Memory{embeddings: embeddings} = mem} ->
score = AI.Util.cosine_similarity(needle, embeddings)
if score > @min_match_threshold do
{[{mem, score} | acc], stale}
else
{acc, stale}
end
{:error, _} ->
{acc, stale}
end
end)
matches = Enum.sort_by(matches, fn {_mem, score} -> score end, :desc)
render_scope_section(scope, matches, %{mode: :search, stale_count: stale_count})
else
render_unavailable_scope_section(scope)
end
end)
"""
# Memories
#{Enum.join(sections, "\n\n")}
"""
{:error, reason} ->
"""
# Memories
Failed to generate embeddings for query: #{inspect(reason)}
"""
end
end
# ----------------------------------------------------------------------------
# Markdown formatting
# ----------------------------------------------------------------------------
defp render_scope_section(scope, memories, %{mode: :list}) do
body =
case memories do
[] ->
"_No memories._"
_ ->
memories
|> Enum.map(&render_memory/1)
|> Enum.join("\n\n")
end
"""
## #{scope}
#{body}
"""
end
defp render_scope_section(scope, memories, %{mode: :search, stale_count: stale_count}) do
stale_note =
if stale_count > 0 do
"_Skipped #{stale_count} stale memorie(s) missing embeddings._"
else
""
end
body =
case memories do
[] ->
"_No matches._\n"
_ ->
memories
|> Enum.map(&render_memory/1)
|> Enum.join("\n\n")
end
"""
## #{scope}
#{stale_note}
#{body}
"""
end
defp render_unavailable_scope_section(scope) do
"""
## #{scope}
_Unavailable in the current context._
"""
end
defp render_memory({%Memory{} = mem, nil}) do
now = DateTime.utc_now()
age = Presentation.age_line(mem, now)
warning = Presentation.warning_line(mem, now)
"""
### [#{mem.scope}] #{mem.title}
_#{age}_
_#{warning}_
#{mem.content}
"""
end
defp render_memory({%Memory{} = mem, score}) when is_number(score) do
now = DateTime.utc_now()
age = Presentation.age_line(mem, now)
warning = Presentation.warning_line(mem, now)
"""
### #{mem.title}
_Score:_ #{Float.round(score, 4)}
_#{age}_
_#{warning}_
#{mem.content}
"""
end
# ----------------------------------------------------------------------------
# Scope helpers
# ----------------------------------------------------------------------------
defp parse_scopes(nil), do: [:global, :project]
defp parse_scopes([]), do: [:global, :project]
defp parse_scopes(scopes) when is_list(scopes) do
scopes
|> Enum.map(&parse_scope!/1)
|> Enum.uniq()
|> Enum.sort_by(&scope_order/1)
end
defp parse_scope!(s) when is_binary(s) do
case String.downcase(s) do
"global" ->
:global
"project" ->
:project
other ->
UI.fatal("Invalid --scope #{inspect(other)}; expected global, project")
exit({:shutdown, 1})
end
end
defp scope_order(:global), do: 1
defp scope_order(:project), do: 2
defp available?(:global), do: true
defp available?(:project), do: Memory.Project.is_available?()
defp list_titles(:global), do: Memory.Global.list()
defp list_titles(:project), do: Memory.Project.list()
end