Packages
fnord
0.8.47
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/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