core: init + config schema + plugin discovery
This commit is contained in:
@@ -11,3 +11,6 @@ Docs:
|
||||
- `docs/ROADMAP.md`
|
||||
- `docs/REPO_FACTS.json`
|
||||
- `docs/NARRATIVE.md`
|
||||
|
||||
Quickstart:
|
||||
- `python -m afs init --context-root ~/path/to/context --workspace-name trunk`
|
||||
|
||||
7
src/afs/__main__.py
Normal file
7
src/afs/__main__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Entry point for python -m afs."""
|
||||
|
||||
from .cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
163
src/afs/cli.py
Normal file
163
src/afs/cli.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""AFS command-line entry points."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
from .config import load_config_model
|
||||
from .plugins import discover_plugins, load_plugins
|
||||
from .schema import AFSConfig, GeneralConfig, WorkspaceDirectory
|
||||
|
||||
|
||||
AFS_DIRS = [
|
||||
"memory",
|
||||
"knowledge",
|
||||
"history",
|
||||
"scratchpad",
|
||||
"tools",
|
||||
"hivemind",
|
||||
"global",
|
||||
"items",
|
||||
]
|
||||
|
||||
|
||||
def _ensure_context_root(root: Path) -> None:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
for name in AFS_DIRS:
|
||||
(root / name).mkdir(parents=True, exist_ok=True)
|
||||
(root / "workspaces").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _write_config(path: Path, config: AFSConfig) -> None:
|
||||
general = config.general
|
||||
lines: list[str] = [
|
||||
"[general]",
|
||||
f"context_root = \"{general.context_root}\"",
|
||||
f"agent_workspaces_dir = \"{general.agent_workspaces_dir}\"",
|
||||
]
|
||||
|
||||
if general.workspace_directories:
|
||||
for ws in general.workspace_directories:
|
||||
lines.append("")
|
||||
lines.append("[[general.workspace_directories]]")
|
||||
lines.append(f"path = \"{ws.path}\"")
|
||||
if ws.description:
|
||||
lines.append(f"description = \"{ws.description}\"")
|
||||
|
||||
lines.append("")
|
||||
lines.append("[cognitive]")
|
||||
lines.append(f"enabled = {str(config.cognitive.enabled).lower()}")
|
||||
lines.append(f"record_emotions = {str(config.cognitive.record_emotions).lower()}")
|
||||
lines.append(
|
||||
f"record_metacognition = {str(config.cognitive.record_metacognition).lower()}"
|
||||
)
|
||||
lines.append(f"record_goals = {str(config.cognitive.record_goals).lower()}")
|
||||
lines.append(f"record_epistemic = {str(config.cognitive.record_epistemic).lower()}")
|
||||
lines.append("")
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _build_config(
|
||||
context_root: Path,
|
||||
workspace_path: Path | None,
|
||||
workspace_name: str | None,
|
||||
) -> AFSConfig:
|
||||
general = GeneralConfig()
|
||||
general.context_root = context_root
|
||||
general.agent_workspaces_dir = context_root / "workspaces"
|
||||
if workspace_path:
|
||||
general.workspace_directories = [
|
||||
WorkspaceDirectory(path=workspace_path, description=workspace_name)
|
||||
]
|
||||
return AFSConfig(general=general)
|
||||
|
||||
|
||||
def _init_command(args: argparse.Namespace) -> int:
|
||||
config_path = Path(args.config) if args.config else Path.cwd() / "afs.toml"
|
||||
if args.no_config:
|
||||
config_path = None
|
||||
|
||||
workspace_path = None
|
||||
if args.workspace_path or args.workspace_name:
|
||||
workspace_path = Path(args.workspace_path) if args.workspace_path else Path.cwd()
|
||||
|
||||
existing_config = None
|
||||
if config_path and config_path.exists() and not args.force:
|
||||
existing_config = load_config_model(config_path=config_path, merge_user=False)
|
||||
|
||||
if args.context_root:
|
||||
context_root = Path(args.context_root).expanduser().resolve()
|
||||
elif existing_config:
|
||||
context_root = existing_config.general.context_root
|
||||
else:
|
||||
context_root = GeneralConfig().context_root
|
||||
|
||||
_ensure_context_root(context_root)
|
||||
|
||||
if args.link_context:
|
||||
link_path = Path.cwd() / ".context"
|
||||
if not link_path.exists():
|
||||
link_path.symlink_to(context_root)
|
||||
|
||||
if config_path:
|
||||
if config_path.exists() and not args.force:
|
||||
print(f"Config exists, not modified: {config_path}")
|
||||
else:
|
||||
config = _build_config(context_root, workspace_path, args.workspace_name)
|
||||
_write_config(config_path, config)
|
||||
print(f"Wrote config: {config_path}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _plugins_command(args: argparse.Namespace) -> int:
|
||||
config_path = Path(args.config) if args.config else None
|
||||
config = load_config_model(config_path=config_path, merge_user=True)
|
||||
plugin_names = discover_plugins(config)
|
||||
if args.load:
|
||||
loaded = load_plugins(plugin_names, config.plugins.plugin_dirs)
|
||||
for name in plugin_names:
|
||||
status = "ok" if name in loaded else "failed"
|
||||
print(f"{name}\t{status}")
|
||||
else:
|
||||
for name in plugin_names:
|
||||
print(name)
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="afs")
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
init_parser = subparsers.add_parser("init", help="Initialize AFS context/root.")
|
||||
init_parser.add_argument("--context-root", help="Context root path.")
|
||||
init_parser.add_argument("--config", help="Path to write afs.toml.")
|
||||
init_parser.add_argument("--no-config", action="store_true", help="Do not write config.")
|
||||
init_parser.add_argument("--force", action="store_true", help="Overwrite config if it exists.")
|
||||
init_parser.add_argument("--workspace-path", help="Workspace path to register.")
|
||||
init_parser.add_argument("--workspace-name", help="Workspace label/description.")
|
||||
init_parser.add_argument("--link-context", action="store_true", help="Symlink .context to context root.")
|
||||
init_parser.set_defaults(func=_init_command)
|
||||
|
||||
plugins_parser = subparsers.add_parser("plugins", help="List or load plugins.")
|
||||
plugins_parser.add_argument("--config", help="Config path for plugin discovery.")
|
||||
plugins_parser.add_argument("--load", action="store_true", help="Attempt to import plugins.")
|
||||
plugins_parser.set_defaults(func=_plugins_command)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Iterable[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
if not getattr(args, "command", None):
|
||||
parser.print_help()
|
||||
return 1
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -62,7 +62,7 @@ class PluginsConfig:
|
||||
plugin_dirs: list[Path] = field(default_factory=list)
|
||||
auto_discover: bool = True
|
||||
auto_discover_prefixes: list[str] = field(
|
||||
default_factory=lambda: ["afs_plugin"]
|
||||
default_factory=lambda: ["afs_plugin", "afs_scawful"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -89,14 +89,35 @@ class PluginsConfig:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CognitiveConfig:
|
||||
enabled: bool = False
|
||||
record_emotions: bool = False
|
||||
record_metacognition: bool = False
|
||||
record_goals: bool = False
|
||||
record_epistemic: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "CognitiveConfig":
|
||||
return cls(
|
||||
enabled=bool(data.get("enabled", False)),
|
||||
record_emotions=bool(data.get("record_emotions", False)),
|
||||
record_metacognition=bool(data.get("record_metacognition", False)),
|
||||
record_goals=bool(data.get("record_goals", False)),
|
||||
record_epistemic=bool(data.get("record_epistemic", False)),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AFSConfig:
|
||||
general: GeneralConfig = field(default_factory=GeneralConfig)
|
||||
plugins: PluginsConfig = field(default_factory=PluginsConfig)
|
||||
cognitive: CognitiveConfig = field(default_factory=CognitiveConfig)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "AFSConfig":
|
||||
data = data or {}
|
||||
general = GeneralConfig.from_dict(data.get("general", {}))
|
||||
plugins = PluginsConfig.from_dict(data.get("plugins", {}))
|
||||
return cls(general=general, plugins=plugins)
|
||||
cognitive = CognitiveConfig.from_dict(data.get("cognitive", {}))
|
||||
return cls(general=general, plugins=plugins, cognitive=cognitive)
|
||||
|
||||
Reference in New Issue
Block a user