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>
74 lines
2.5 KiB
Python
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 ""
|