Packages

fnord

0.8.47

AI code archaeology

Current section

Files

Jump to
fnord lib ai notes external_docs.ex
Raw

lib/ai/notes/external_docs.ex

defmodule AI.Notes.ExternalDocs do
@moduledoc """
Discover and read external documentation files (README.md, CLAUDE.md, and
AGENTS.md).
This module searches for these files in the project root, current working
directory, and standard user directories, returning a list of `{type, path,
contents}` tuples.
"""
@type doc_type :: :claude | :agents | :readme
@type path :: String.t()
@type display_path :: String.t()
@type contents :: String.t()
@type doc_result :: {doc_type, path, display_path, contents}
@max_file_size 1_000_000
@spec get_docs() :: [doc_result]
def get_docs do
project_root = Path.expand("../../../", __DIR__)
cwd = Path.expand(File.cwd!())
user_home = System.get_env("HOME") || System.user_home!()
# Define sources in order of specificity: home files, project files, cwd
# files.
sources = [
# Home directory files (most general)
{:claude, Path.join(user_home, ".claude/CLAUDE.md"), "~/.claude/CLAUDE.md"},
{:claude, Path.join(user_home, ".config/claude/CLAUDE.md"), "~/.config/claude/CLAUDE.md"},
{:agents, Path.join(user_home, ".agents/AGENTS.md"), "~/.agents/AGENTS.md"},
{:agents, Path.join(user_home, ".config/agents/AGENTS.md"), "~/.config/agents/AGENTS.md"},
# Project root files (more specific)
{:claude, Path.join(project_root, "CLAUDE.md"),
get_relative_path(Path.join(project_root, "CLAUDE.md"), cwd)},
{:agents, Path.join(project_root, "AGENTS.md"),
get_relative_path(Path.join(project_root, "AGENTS.md"), cwd)},
# Current working directory files (most specific)
{:claude, Path.join(cwd, "CLAUDE.md"), "./CLAUDE.md"},
{:agents, Path.join(cwd, "AGENTS.md"), "./AGENTS.md"},
# README files
{:readme, Path.join(project_root, "README.md"),
get_relative_path(Path.join(project_root, "README.md"), cwd)},
{:readme, Path.join(cwd, "README.md"), "./README.md"}
]
for {type, path, display_path} <- sources,
result = read_doc(type, path, display_path),
result != nil,
do: result
end
defp read_doc(type, path, display_path) do
case File.stat(path) do
{:ok, %File.Stat{size: size}} when size > @max_file_size ->
UI.warn("Skipping large file", path)
nil
{:ok, _stat} ->
case File.read(path) do
{:ok, contents} ->
{type, path, display_path, contents}
{:error, reason} ->
UI.warn("Unable to read file", "#{path}: #{inspect(reason)}")
nil
end
{:error, :enoent} ->
nil
{:error, reason} ->
UI.warn("Unable to access file", "#{path}: #{inspect(reason)}")
nil
end
end
defp get_relative_path(target_path, from_path) do
Path.relative_to(target_path, from_path)
end
@hint_max_size 4_096
@spec format_hints([doc_result]) :: String.t()
def format_hints(docs) do
header = "# Hints\n\n"
docs
|> Enum.map(fn {type, _path, display, contents} ->
title = String.upcase(to_string(type))
snippet = String.slice(contents, 0, @hint_max_size)
"## #{title} (#{display})\n" <> snippet <> "\n\n"
end)
|> Enum.join()
|> then(&(header <> &1))
end
end