Files
ai-agent/backend/files/discovery.py
Gabriel Vidal 0ff9e40242 refactor(backend): split main.py into domain packages with app.state injection
main.py (5146 lines, 106 routes) becomes an assembly only: one package per
domain — core, files, settings, dashboards, conversations, diff,
notifications, forms, runs, accounts, workers, models, cron, agents,
projects, services, goals, memories, plans, templates — each exposing an
APIRouter; the flat domain modules move into their package behind a barrel
that keeps the old `import conversations` / `import projects` spellings.

The shared singletons (store, meta_store, hub, indexer, …) are built once by
core.state.build_state() and attached to app.state.ai; routes take them as
the `deps: State` dependency and helpers as an explicit `deps: AppState`.
conversations/pricing.py carries the per-model rates out of the parser.

Verified: route table and OpenAPI byte-identical; 90 read endpoints
golden-diffed against the monolith on a copy of the live data (identical);
write routes smoke-tested; 66 backend tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 23:55:47 +02:00

74 lines
2.5 KiB
Python

"""Which workspace files the editor exposes (CLAUDE.md, .claude/, data/)."""
import os
import pathlib
from core.config import DATA_TEXT_EXTS, IGNORE_DIRS, MD_EXTS, WORKSPACE
# ── file discovery ──────────────────────────────────────────────────────────
def _is_data_file(rel: pathlib.PurePosixPath) -> bool:
"""A text file under the repo's root `data/` tree (plans/logs/notes/etc.).
Surfaced read-only for visibility; binaries and the ignored `data/tmp/`
(scratch + secrets) are filtered out via DATA_TEXT_EXTS + IGNORE_DIRS."""
return (
len(rel.parts) > 1
and rel.parts[0] == "data"
and rel.suffix.lower() in DATA_TEXT_EXTS
)
def _is_context_file(rel: pathlib.PurePosixPath) -> bool:
"""A file we expose: any CLAUDE.md, anything under a `.claude/` tree, or a
text file under the repo's root `data/` tree."""
if rel.name == "CLAUDE.md":
return True
if ".claude" in rel.parts:
return True
return _is_data_file(rel)
def _is_editable(rel: pathlib.PurePosixPath) -> bool:
"""Text files we allow editing. Binary/lock files stay read-only, and the
`data/` tree is read-only (mounted :ro — surfaced for viewing only)."""
if _is_data_file(rel):
return False
return rel.suffix.lower() in {
".md", ".markdown", ".sh", ".py", ".js", ".mjs", ".ts", ".tsx",
".json", ".yml", ".yaml", ".txt", ".toml", ".cfg", ".ini", "",
}
def _kind(rel: pathlib.PurePosixPath) -> str:
name = rel.name
if name == "CLAUDE.md":
return "claude-md"
if name == "SKILL.md":
return "skill-md"
if rel.suffix in MD_EXTS:
return "markdown"
if rel.suffix in {".sh", ".py", ".js", ".mjs", ".ts", ".tsx"}:
return "script"
if rel.suffix in {".json", ".yml", ".yaml", ".toml", ".ini", ".cfg"}:
return "config"
return "other"
def _iter_files():
"""Yield (abs_path, rel_posix) for every exposed context file."""
for dirpath, dirnames, filenames in os.walk(WORKSPACE):
dirnames[:] = [d for d in dirnames if d not in IGNORE_DIRS]
for fn in filenames:
ap = pathlib.Path(dirpath) / fn
rel = pathlib.PurePosixPath(ap.relative_to(WORKSPACE).as_posix())
if _is_context_file(rel):
yield ap, rel
def _read_text(ap: pathlib.Path) -> str:
try:
return ap.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
return ""