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