agent-brain-cli 9.6.0__tar.gz → 10.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/PKG-INFO +2 -2
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/__init__.py +1 -1
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/cli.py +3 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/__init__.py +2 -0
- agent_brain_cli-10.0.1/agent_brain_cli/commands/doctor.py +86 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/index.py +2 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/init.py +1 -47
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/jobs.py +2 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/query.py +2 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/reset.py +2 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/start.py +1 -32
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/status.py +2 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/config.py +26 -11
- agent_brain_cli-10.0.1/agent_brain_cli/diagnostics.py +384 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/pyproject.toml +2 -2
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/README.md +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/client/__init__.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/client/api_client.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/cache.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/config.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/folders.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/inject.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/install_agent.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/list_cmd.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/stop.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/types.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/commands/uninstall.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/config_migrate.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/config_schema.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/migration.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/__init__.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/claude_converter.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/codex_converter.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/converter_base.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/gemini_converter.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/opencode_converter.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/parser.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/skill_runtime_converter.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/tool_maps.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/types.py +0 -0
- {agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/xdg_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: agent-brain-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 10.0.1
|
|
4
4
|
Summary: Agent Brain CLI - Command-line interface for managing AI agent memory and knowledge retrieval
|
|
5
5
|
Home-page: https://github.com/SpillwaveSolutions/agent-brain
|
|
6
6
|
License: MIT
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Requires-Dist: agent-brain-rag (>=
|
|
18
|
+
Requires-Dist: agent-brain-rag (>=10.0.1,<11.0.0)
|
|
19
19
|
Requires-Dist: click (>=8.1.0,<9.0.0)
|
|
20
20
|
Requires-Dist: httpx (>=0.28.0,<0.29.0)
|
|
21
21
|
Requires-Dist: pydantic (>=2.10.0,<3.0.0)
|
|
@@ -10,6 +10,7 @@ from . import __version__
|
|
|
10
10
|
from .commands import (
|
|
11
11
|
cache_group,
|
|
12
12
|
config_group,
|
|
13
|
+
doctor_command,
|
|
13
14
|
folders_group,
|
|
14
15
|
index_command,
|
|
15
16
|
init_command,
|
|
@@ -50,6 +51,7 @@ def cli() -> None:
|
|
|
50
51
|
inject Index documents with content injection
|
|
51
52
|
jobs View and manage job queue
|
|
52
53
|
reset Clear all indexed documents
|
|
54
|
+
doctor Diagnose installation, configuration, and server state
|
|
53
55
|
|
|
54
56
|
\b
|
|
55
57
|
Cache Commands:
|
|
@@ -103,6 +105,7 @@ cli.add_command(types_group, name="types")
|
|
|
103
105
|
cli.add_command(cache_group, name="cache")
|
|
104
106
|
cli.add_command(uninstall_command, name="uninstall")
|
|
105
107
|
cli.add_command(install_agent_command, name="install-agent")
|
|
108
|
+
cli.add_command(doctor_command, name="doctor")
|
|
106
109
|
|
|
107
110
|
|
|
108
111
|
if __name__ == "__main__":
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from .cache import cache_group
|
|
4
4
|
from .config import config_group
|
|
5
|
+
from .doctor import doctor_command
|
|
5
6
|
from .folders import folders_group
|
|
6
7
|
from .index import index_command
|
|
7
8
|
from .init import init_command
|
|
@@ -20,6 +21,7 @@ from .uninstall import uninstall_command
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"cache_group",
|
|
22
23
|
"config_group",
|
|
24
|
+
"doctor_command",
|
|
23
25
|
"folders_group",
|
|
24
26
|
"index_command",
|
|
25
27
|
"inject_command",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""``agent-brain doctor`` — diagnose installation, configuration and server state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from agent_brain_cli.diagnostics import (
|
|
11
|
+
SEVERITY_FAIL,
|
|
12
|
+
SEVERITY_OK,
|
|
13
|
+
SEVERITY_WARN,
|
|
14
|
+
report_to_json,
|
|
15
|
+
run_doctor,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_STATUS_STYLE = {
|
|
22
|
+
SEVERITY_OK: ("green", "OK"),
|
|
23
|
+
SEVERITY_WARN: ("yellow", "WARN"),
|
|
24
|
+
SEVERITY_FAIL: ("red", "FAIL"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.command("doctor")
|
|
29
|
+
@click.option(
|
|
30
|
+
"--url",
|
|
31
|
+
envvar="AGENT_BRAIN_URL",
|
|
32
|
+
default=None,
|
|
33
|
+
help="Server URL to probe (default: resolved from runtime.json or config).",
|
|
34
|
+
)
|
|
35
|
+
@click.option("--json", "json_output", is_flag=True, help="Emit machine-readable JSON.")
|
|
36
|
+
def doctor_command(url: str | None, json_output: bool) -> None:
|
|
37
|
+
"""Diagnose your Agent Brain setup.
|
|
38
|
+
|
|
39
|
+
Inspects Python version, project init state, provider config, required
|
|
40
|
+
API keys, optional dependencies, .gitignore hygiene, and whether the
|
|
41
|
+
server is reachable. Exits non-zero on any critical failure so it can
|
|
42
|
+
be used in scripts (``agent-brain doctor || agent-brain init``).
|
|
43
|
+
"""
|
|
44
|
+
report = run_doctor(server_url_override=url)
|
|
45
|
+
|
|
46
|
+
if json_output:
|
|
47
|
+
click.echo(report_to_json(report))
|
|
48
|
+
raise SystemExit(report.exit_code)
|
|
49
|
+
|
|
50
|
+
header_color = "green" if report.exit_code == 0 else "red"
|
|
51
|
+
console.print(
|
|
52
|
+
Panel(
|
|
53
|
+
(
|
|
54
|
+
f"[bold]Project root:[/] {report.project_root}\n"
|
|
55
|
+
f"[bold]State dir:[/] {report.state_dir} "
|
|
56
|
+
f"({'present' if report.state_dir_exists else 'missing'})\n"
|
|
57
|
+
f"[bold]Server URL:[/] {report.server_url}"
|
|
58
|
+
),
|
|
59
|
+
title="Agent Brain Doctor",
|
|
60
|
+
border_style=header_color,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
65
|
+
table.add_column("Check", style="dim")
|
|
66
|
+
table.add_column("Status")
|
|
67
|
+
table.add_column("Details", overflow="fold")
|
|
68
|
+
|
|
69
|
+
for check in report.checks:
|
|
70
|
+
style, label = _STATUS_STYLE.get(check.status, ("white", check.status.upper()))
|
|
71
|
+
body = check.message
|
|
72
|
+
if check.fix:
|
|
73
|
+
body = f"{body}\n[dim]→ {check.fix}[/]"
|
|
74
|
+
table.add_row(check.name, f"[{style}]{label}[/]", body)
|
|
75
|
+
|
|
76
|
+
console.print(table)
|
|
77
|
+
|
|
78
|
+
if report.exit_code != 0:
|
|
79
|
+
console.print(
|
|
80
|
+
"\n[red]Doctor reported critical issues.[/] "
|
|
81
|
+
"Fix the items above and re-run.",
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
console.print("\n[green]All critical checks passed.[/]")
|
|
85
|
+
|
|
86
|
+
raise SystemExit(report.exit_code)
|
|
@@ -7,6 +7,7 @@ from rich.console import Console
|
|
|
7
7
|
|
|
8
8
|
from ..client import ConnectionError, DocServeClient, ServerError
|
|
9
9
|
from ..config import get_server_url
|
|
10
|
+
from ..diagnostics import doctor_hint_message
|
|
10
11
|
|
|
11
12
|
console = Console()
|
|
12
13
|
|
|
@@ -184,6 +185,7 @@ def index_command(
|
|
|
184
185
|
click.echo(json.dumps({"error": str(e)}))
|
|
185
186
|
else:
|
|
186
187
|
console.print(f"[red]Connection Error:[/] {e}")
|
|
188
|
+
console.print(f"[dim]{doctor_hint_message()}[/]")
|
|
187
189
|
raise SystemExit(1) from e
|
|
188
190
|
|
|
189
191
|
except ServerError as e:
|
|
@@ -7,6 +7,7 @@ import click
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
from rich.panel import Panel
|
|
9
9
|
|
|
10
|
+
from agent_brain_cli.config import resolve_project_root
|
|
10
11
|
from agent_brain_cli.migration import migrate_state_dir
|
|
11
12
|
from agent_brain_cli.xdg_paths import migrate_legacy_paths
|
|
12
13
|
|
|
@@ -37,53 +38,6 @@ DEFAULT_CONFIG = {
|
|
|
37
38
|
STATE_DIR_NAME = ".agent-brain"
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
41
|
-
"""Resolve the canonical project root directory.
|
|
42
|
-
|
|
43
|
-
Resolution order:
|
|
44
|
-
1. Git repository root (git rev-parse --show-toplevel)
|
|
45
|
-
2. Walk up looking for .claude/ directory
|
|
46
|
-
3. Walk up looking for pyproject.toml
|
|
47
|
-
4. Fall back to cwd
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
start_path: Starting path for resolution. Defaults to cwd.
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
Resolved project root path.
|
|
54
|
-
"""
|
|
55
|
-
import subprocess
|
|
56
|
-
|
|
57
|
-
start = (start_path or Path.cwd()).resolve()
|
|
58
|
-
|
|
59
|
-
# Try git root first
|
|
60
|
-
try:
|
|
61
|
-
result = subprocess.run(
|
|
62
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
63
|
-
capture_output=True,
|
|
64
|
-
text=True,
|
|
65
|
-
timeout=5,
|
|
66
|
-
cwd=str(start),
|
|
67
|
-
)
|
|
68
|
-
if result.returncode == 0:
|
|
69
|
-
return Path(result.stdout.strip()).resolve()
|
|
70
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
# Walk up looking for markers
|
|
74
|
-
current = start
|
|
75
|
-
while current != current.parent:
|
|
76
|
-
if (current / ".agent-brain").is_dir():
|
|
77
|
-
return current
|
|
78
|
-
if (current / ".claude").is_dir():
|
|
79
|
-
return current
|
|
80
|
-
if (current / "pyproject.toml").is_file():
|
|
81
|
-
return current
|
|
82
|
-
current = current.parent
|
|
83
|
-
|
|
84
|
-
return start
|
|
85
|
-
|
|
86
|
-
|
|
87
41
|
@click.command("init")
|
|
88
42
|
@click.option(
|
|
89
43
|
"--path",
|
|
@@ -10,6 +10,7 @@ from rich.table import Table
|
|
|
10
10
|
|
|
11
11
|
from ..client import ConnectionError, DocServeClient, ServerError
|
|
12
12
|
from ..config import get_server_url
|
|
13
|
+
from ..diagnostics import doctor_hint_message
|
|
13
14
|
|
|
14
15
|
console = Console()
|
|
15
16
|
|
|
@@ -310,6 +311,7 @@ def jobs_command(
|
|
|
310
311
|
click.echo(json.dumps({"error": str(e)}))
|
|
311
312
|
else:
|
|
312
313
|
console.print(f"[red]Connection Error:[/] {e}")
|
|
314
|
+
console.print(f"[dim]{doctor_hint_message()}[/]")
|
|
313
315
|
raise SystemExit(1) from e
|
|
314
316
|
|
|
315
317
|
except ServerError as e:
|
|
@@ -7,6 +7,7 @@ from rich.text import Text
|
|
|
7
7
|
|
|
8
8
|
from ..client import ConnectionError, DocServeClient, ServerError
|
|
9
9
|
from ..config import get_server_url
|
|
10
|
+
from ..diagnostics import doctor_hint_message
|
|
10
11
|
|
|
11
12
|
console = Console()
|
|
12
13
|
|
|
@@ -208,6 +209,7 @@ def query_command(
|
|
|
208
209
|
click.echo(json.dumps({"error": str(e)}))
|
|
209
210
|
else:
|
|
210
211
|
console.print(f"[red]Connection Error:[/] {e}")
|
|
212
|
+
console.print(f"[dim]{doctor_hint_message()}[/]")
|
|
211
213
|
raise SystemExit(1) from e
|
|
212
214
|
|
|
213
215
|
except ServerError as e:
|
|
@@ -6,6 +6,7 @@ from rich.prompt import Confirm
|
|
|
6
6
|
|
|
7
7
|
from ..client import ConnectionError, DocServeClient, ServerError
|
|
8
8
|
from ..config import get_server_url
|
|
9
|
+
from ..diagnostics import doctor_hint_message
|
|
9
10
|
|
|
10
11
|
console = Console()
|
|
11
12
|
|
|
@@ -67,6 +68,7 @@ def reset_command(url: str | None, yes: bool, json_output: bool) -> None:
|
|
|
67
68
|
click.echo(json.dumps({"error": str(e)}))
|
|
68
69
|
else:
|
|
69
70
|
console.print(f"[red]Connection Error:[/] {e}")
|
|
71
|
+
console.print(f"[dim]{doctor_hint_message()}[/]")
|
|
70
72
|
raise SystemExit(1) from e
|
|
71
73
|
|
|
72
74
|
except ServerError as e:
|
|
@@ -15,6 +15,7 @@ import click
|
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
from rich.panel import Panel
|
|
17
17
|
|
|
18
|
+
from agent_brain_cli.config import resolve_project_root
|
|
18
19
|
from agent_brain_cli.migration import resolve_state_dir_with_fallback
|
|
19
20
|
from agent_brain_cli.xdg_paths import get_xdg_state_dir, migrate_legacy_paths
|
|
20
21
|
|
|
@@ -26,38 +27,6 @@ PID_FILE = "agent-brain.pid"
|
|
|
26
27
|
RUNTIME_FILE = "runtime.json"
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
30
|
-
"""Resolve the canonical project root directory."""
|
|
31
|
-
start = (start_path or Path.cwd()).resolve()
|
|
32
|
-
|
|
33
|
-
# Try git root first
|
|
34
|
-
try:
|
|
35
|
-
result = subprocess.run(
|
|
36
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
37
|
-
capture_output=True,
|
|
38
|
-
text=True,
|
|
39
|
-
timeout=5,
|
|
40
|
-
cwd=str(start),
|
|
41
|
-
)
|
|
42
|
-
if result.returncode == 0:
|
|
43
|
-
return Path(result.stdout.strip()).resolve()
|
|
44
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
# Walk up looking for markers
|
|
48
|
-
current = start
|
|
49
|
-
while current != current.parent:
|
|
50
|
-
if (current / ".agent-brain").is_dir():
|
|
51
|
-
return current
|
|
52
|
-
if (current / ".claude").is_dir():
|
|
53
|
-
return current
|
|
54
|
-
if (current / "pyproject.toml").is_file():
|
|
55
|
-
return current
|
|
56
|
-
current = current.parent
|
|
57
|
-
|
|
58
|
-
return start
|
|
59
|
-
|
|
60
|
-
|
|
61
30
|
def read_config(state_dir: Path) -> dict[str, Any]:
|
|
62
31
|
"""Read configuration from state directory."""
|
|
63
32
|
config_path = state_dir / "config.json"
|
|
@@ -7,6 +7,7 @@ from rich.table import Table
|
|
|
7
7
|
|
|
8
8
|
from ..client import ConnectionError, DocServeClient, ServerError
|
|
9
9
|
from ..config import get_server_url
|
|
10
|
+
from ..diagnostics import doctor_hint_message
|
|
10
11
|
|
|
11
12
|
console = Console()
|
|
12
13
|
|
|
@@ -151,6 +152,7 @@ def status_command(url: str | None, json_output: bool, verbose: bool) -> None:
|
|
|
151
152
|
click.echo(json.dumps({"error": str(e)}))
|
|
152
153
|
else:
|
|
153
154
|
console.print(f"[red]Connection Error:[/] {e}")
|
|
155
|
+
console.print(f"[dim]{doctor_hint_message()}[/]")
|
|
154
156
|
raise SystemExit(1) from e
|
|
155
157
|
|
|
156
158
|
except ServerError as e:
|
|
@@ -251,25 +251,38 @@ def load_config(start_path: Path | None = None) -> AgentBrainConfig:
|
|
|
251
251
|
return config
|
|
252
252
|
|
|
253
253
|
|
|
254
|
-
def
|
|
254
|
+
def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
255
255
|
"""Find the project root by looking for markers.
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
1.
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
Resolution order (first match wins):
|
|
258
|
+
1. Walk up from ``start_path`` looking for ``.agent-brain/`` — this lets a
|
|
259
|
+
sub-project inside a mono-repo keep its own state dir and not get
|
|
260
|
+
pulled to the git top-level (issues #124, #128).
|
|
261
|
+
2. Walk up looking for legacy ``.claude/agent-brain/``.
|
|
262
|
+
3. Git repository root (``git rev-parse --show-toplevel``).
|
|
263
|
+
4. Walk up looking for ``.claude/`` or ``pyproject.toml``.
|
|
264
|
+
5. Fall back to ``start_path``.
|
|
261
265
|
|
|
262
266
|
Args:
|
|
263
267
|
start_path: Starting directory. Defaults to cwd.
|
|
264
268
|
|
|
265
269
|
Returns:
|
|
266
|
-
Project root path
|
|
270
|
+
Project root path.
|
|
267
271
|
"""
|
|
268
272
|
import subprocess
|
|
269
273
|
|
|
270
274
|
start = (start_path or Path.cwd()).resolve()
|
|
271
275
|
|
|
272
|
-
#
|
|
276
|
+
# 1 & 2. Prefer a local state dir over git root so nested projects work.
|
|
277
|
+
current = start
|
|
278
|
+
while current != current.parent:
|
|
279
|
+
if (current / STATE_DIR_NAME).is_dir():
|
|
280
|
+
return current
|
|
281
|
+
if (current / LEGACY_STATE_DIR_NAME).is_dir():
|
|
282
|
+
return current
|
|
283
|
+
current = current.parent
|
|
284
|
+
|
|
285
|
+
# 3. Git root next — useful when this is the first time the user runs init.
|
|
273
286
|
try:
|
|
274
287
|
result = subprocess.run(
|
|
275
288
|
["git", "rev-parse", "--show-toplevel"],
|
|
@@ -283,11 +296,9 @@ def _find_project_root(start_path: Path | None = None) -> Path:
|
|
|
283
296
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
284
297
|
pass
|
|
285
298
|
|
|
286
|
-
#
|
|
299
|
+
# 4. Other markers.
|
|
287
300
|
current = start
|
|
288
301
|
while current != current.parent:
|
|
289
|
-
if (current / ".agent-brain").is_dir():
|
|
290
|
-
return current
|
|
291
302
|
if (current / ".claude").is_dir():
|
|
292
303
|
return current
|
|
293
304
|
if (current / "pyproject.toml").is_file():
|
|
@@ -297,6 +308,10 @@ def _find_project_root(start_path: Path | None = None) -> Path:
|
|
|
297
308
|
return start
|
|
298
309
|
|
|
299
310
|
|
|
311
|
+
# Backwards-compatible alias for any external callers.
|
|
312
|
+
_find_project_root = resolve_project_root
|
|
313
|
+
|
|
314
|
+
|
|
300
315
|
def get_state_dir(
|
|
301
316
|
config: AgentBrainConfig | None = None,
|
|
302
317
|
project_root: Path | None = None,
|
|
@@ -318,7 +333,7 @@ def get_state_dir(
|
|
|
318
333
|
"""
|
|
319
334
|
# 1. Auto-detect project root and check for existing state dir
|
|
320
335
|
if project_root is None:
|
|
321
|
-
project_root =
|
|
336
|
+
project_root = resolve_project_root()
|
|
322
337
|
|
|
323
338
|
# Check new path first, then legacy
|
|
324
339
|
new_state_dir = project_root / STATE_DIR_NAME
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Shared diagnostics helpers for the Agent Brain CLI.
|
|
2
|
+
|
|
3
|
+
The functions here power both the ``agent-brain doctor`` command and the
|
|
4
|
+
"tip: run doctor" hint that appears when a command can't reach the server.
|
|
5
|
+
Keeping the logic in one place means the hint and the diagnosis can never
|
|
6
|
+
drift out of sync.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
import shutil
|
|
15
|
+
import socket
|
|
16
|
+
import sys
|
|
17
|
+
from dataclasses import asdict, dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
from urllib.error import URLError
|
|
21
|
+
from urllib.request import Request, urlopen
|
|
22
|
+
|
|
23
|
+
from agent_brain_cli.config import (
|
|
24
|
+
LEGACY_STATE_DIR_NAME,
|
|
25
|
+
STATE_DIR_NAME,
|
|
26
|
+
get_server_url,
|
|
27
|
+
load_config,
|
|
28
|
+
resolve_project_root,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
#: Severity returned by every diagnostic check.
|
|
32
|
+
SEVERITY_OK = "ok"
|
|
33
|
+
SEVERITY_WARN = "warn"
|
|
34
|
+
SEVERITY_FAIL = "fail"
|
|
35
|
+
|
|
36
|
+
DOCTOR_HINT = "Tip: run `agent-brain doctor` to diagnose your setup."
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class CheckResult:
|
|
41
|
+
"""One row in the doctor output."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
status: str # ok | warn | fail
|
|
45
|
+
message: str
|
|
46
|
+
fix: str | None = None
|
|
47
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class DoctorReport:
|
|
52
|
+
"""The full diagnostic snapshot."""
|
|
53
|
+
|
|
54
|
+
project_root: str
|
|
55
|
+
state_dir: str
|
|
56
|
+
state_dir_exists: bool
|
|
57
|
+
runtime_file: str | None
|
|
58
|
+
server_url: str
|
|
59
|
+
checks: list[CheckResult]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def exit_code(self) -> int:
|
|
63
|
+
"""Non-zero when any critical check failed."""
|
|
64
|
+
return 1 if any(c.status == SEVERITY_FAIL for c in self.checks) else 0
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict[str, Any]:
|
|
67
|
+
data = asdict(self)
|
|
68
|
+
data["exit_code"] = self.exit_code
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_python() -> CheckResult:
|
|
73
|
+
major, minor = sys.version_info[:2]
|
|
74
|
+
version = f"{major}.{minor}.{sys.version_info.micro}"
|
|
75
|
+
if (major, minor) >= (3, 10):
|
|
76
|
+
return CheckResult(
|
|
77
|
+
"python_version",
|
|
78
|
+
SEVERITY_OK,
|
|
79
|
+
f"Python {version}",
|
|
80
|
+
details={"version": version},
|
|
81
|
+
)
|
|
82
|
+
return CheckResult(
|
|
83
|
+
"python_version",
|
|
84
|
+
SEVERITY_FAIL,
|
|
85
|
+
f"Python {version} — Agent Brain requires 3.10+",
|
|
86
|
+
fix="Upgrade to Python 3.10 or newer.",
|
|
87
|
+
details={"version": version},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _check_project_init(project_root: Path, state_dir: Path) -> CheckResult:
|
|
92
|
+
config_path = state_dir / "config.json"
|
|
93
|
+
if config_path.exists():
|
|
94
|
+
return CheckResult(
|
|
95
|
+
"project_initialized",
|
|
96
|
+
SEVERITY_OK,
|
|
97
|
+
f"Project initialized at {state_dir}",
|
|
98
|
+
details={"state_dir": str(state_dir)},
|
|
99
|
+
)
|
|
100
|
+
return CheckResult(
|
|
101
|
+
"project_initialized",
|
|
102
|
+
SEVERITY_FAIL,
|
|
103
|
+
f"No {STATE_DIR_NAME}/config.json under {project_root}",
|
|
104
|
+
fix="Run `agent-brain init` in your project directory.",
|
|
105
|
+
details={
|
|
106
|
+
"project_root": str(project_root),
|
|
107
|
+
"expected_path": str(config_path),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _check_provider_config(state_dir: Path) -> CheckResult:
|
|
113
|
+
yaml_path = state_dir / "config.yaml"
|
|
114
|
+
try:
|
|
115
|
+
cfg = load_config()
|
|
116
|
+
except Exception as exc: # pragma: no cover — pydantic noise
|
|
117
|
+
return CheckResult(
|
|
118
|
+
"provider_config",
|
|
119
|
+
SEVERITY_FAIL,
|
|
120
|
+
f"Failed to load config.yaml: {exc}",
|
|
121
|
+
fix=f"Fix or delete {yaml_path} and re-run `agent-brain doctor`.",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return CheckResult(
|
|
125
|
+
"provider_config",
|
|
126
|
+
SEVERITY_OK,
|
|
127
|
+
(
|
|
128
|
+
f"embedding={cfg.embedding.provider}:{cfg.embedding.model}, "
|
|
129
|
+
f"summarization={cfg.summarization.provider}:{cfg.summarization.model}"
|
|
130
|
+
),
|
|
131
|
+
details={
|
|
132
|
+
"config_path": str(yaml_path) if yaml_path.exists() else None,
|
|
133
|
+
"embedding_provider": cfg.embedding.provider,
|
|
134
|
+
"embedding_model": cfg.embedding.model,
|
|
135
|
+
"summarization_provider": cfg.summarization.provider,
|
|
136
|
+
"summarization_model": cfg.summarization.model,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_PROVIDER_KEY_ENV: dict[str, str] = {
|
|
142
|
+
"openai": "OPENAI_API_KEY",
|
|
143
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
144
|
+
"claude": "ANTHROPIC_API_KEY",
|
|
145
|
+
"cohere": "COHERE_API_KEY",
|
|
146
|
+
"gemini": "GEMINI_API_KEY",
|
|
147
|
+
"google": "GEMINI_API_KEY",
|
|
148
|
+
"grok": "XAI_API_KEY",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _check_api_keys() -> list[CheckResult]:
|
|
153
|
+
try:
|
|
154
|
+
cfg = load_config()
|
|
155
|
+
except Exception: # pragma: no cover
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
results: list[CheckResult] = []
|
|
159
|
+
for label, provider, model in (
|
|
160
|
+
("embedding", cfg.embedding.provider, cfg.embedding.model),
|
|
161
|
+
("summarization", cfg.summarization.provider, cfg.summarization.model),
|
|
162
|
+
):
|
|
163
|
+
if provider == "ollama":
|
|
164
|
+
continue
|
|
165
|
+
env_name = (
|
|
166
|
+
cfg.embedding.api_key_env
|
|
167
|
+
if label == "embedding"
|
|
168
|
+
else cfg.summarization.api_key_env
|
|
169
|
+
) or _PROVIDER_KEY_ENV.get(provider.lower())
|
|
170
|
+
if not env_name:
|
|
171
|
+
continue
|
|
172
|
+
present = bool(os.environ.get(env_name))
|
|
173
|
+
results.append(
|
|
174
|
+
CheckResult(
|
|
175
|
+
f"api_key_{label}",
|
|
176
|
+
SEVERITY_OK if present else SEVERITY_FAIL,
|
|
177
|
+
(
|
|
178
|
+
f"{env_name} is set"
|
|
179
|
+
if present
|
|
180
|
+
else f"{env_name} is not set (required by {provider})"
|
|
181
|
+
),
|
|
182
|
+
fix=(
|
|
183
|
+
None
|
|
184
|
+
if present
|
|
185
|
+
else f"export {env_name}=… then re-run `agent-brain doctor`."
|
|
186
|
+
),
|
|
187
|
+
details={
|
|
188
|
+
"provider": provider,
|
|
189
|
+
"model": model,
|
|
190
|
+
"env_var": env_name,
|
|
191
|
+
"present": present,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
return results
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _is_listening(host: str, port: int, timeout: float = 0.5) -> bool:
|
|
199
|
+
try:
|
|
200
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
201
|
+
return True
|
|
202
|
+
except (OSError, ConnectionRefusedError):
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _check_server(server_url: str, runtime_file: Path | None) -> CheckResult:
|
|
207
|
+
if runtime_file and not runtime_file.exists():
|
|
208
|
+
return CheckResult(
|
|
209
|
+
"server_reachable",
|
|
210
|
+
SEVERITY_WARN,
|
|
211
|
+
(
|
|
212
|
+
f"No runtime.json at {runtime_file} — server is probably not "
|
|
213
|
+
"running for this project."
|
|
214
|
+
),
|
|
215
|
+
fix="Run `agent-brain start` to launch the server.",
|
|
216
|
+
details={
|
|
217
|
+
"runtime_file": str(runtime_file),
|
|
218
|
+
"server_url": server_url,
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
req = Request(server_url.rstrip("/") + "/health")
|
|
224
|
+
with urlopen(req, timeout=3) as resp: # noqa: S310 — local URL
|
|
225
|
+
body = resp.read().decode("utf-8", errors="replace")
|
|
226
|
+
return CheckResult(
|
|
227
|
+
"server_reachable",
|
|
228
|
+
SEVERITY_OK,
|
|
229
|
+
f"Server responded at {server_url}",
|
|
230
|
+
details={"server_url": server_url, "response_preview": body[:120]},
|
|
231
|
+
)
|
|
232
|
+
except URLError as exc:
|
|
233
|
+
return CheckResult(
|
|
234
|
+
"server_reachable",
|
|
235
|
+
SEVERITY_FAIL,
|
|
236
|
+
f"Cannot reach server at {server_url}: {exc.reason}",
|
|
237
|
+
fix="Start it with `agent-brain start` (or pass --url).",
|
|
238
|
+
details={"server_url": server_url, "error": str(exc.reason)},
|
|
239
|
+
)
|
|
240
|
+
except Exception as exc: # noqa: BLE001
|
|
241
|
+
return CheckResult(
|
|
242
|
+
"server_reachable",
|
|
243
|
+
SEVERITY_FAIL,
|
|
244
|
+
f"Error contacting server at {server_url}: {exc}",
|
|
245
|
+
fix="Start it with `agent-brain start` (or pass --url).",
|
|
246
|
+
details={"server_url": server_url, "error": str(exc)},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _check_optional_dep(provider: str, module_name: str, extra: str) -> CheckResult:
|
|
251
|
+
"""Report on an optional Python package that a chosen provider needs."""
|
|
252
|
+
if shutil.which("python3"):
|
|
253
|
+
# We import in-process so test mocks of installed packages work.
|
|
254
|
+
try:
|
|
255
|
+
__import__(module_name)
|
|
256
|
+
return CheckResult(
|
|
257
|
+
f"optional_dep_{module_name}",
|
|
258
|
+
SEVERITY_OK,
|
|
259
|
+
f"{module_name} is installed ({provider} provider)",
|
|
260
|
+
details={"module": module_name, "provider": provider},
|
|
261
|
+
)
|
|
262
|
+
except ImportError:
|
|
263
|
+
return CheckResult(
|
|
264
|
+
f"optional_dep_{module_name}",
|
|
265
|
+
SEVERITY_FAIL,
|
|
266
|
+
(
|
|
267
|
+
f"{provider} provider selected but {module_name} is not "
|
|
268
|
+
"installed."
|
|
269
|
+
),
|
|
270
|
+
fix=f"pip install 'agent-brain-rag[{extra}]'",
|
|
271
|
+
details={
|
|
272
|
+
"module": module_name,
|
|
273
|
+
"provider": provider,
|
|
274
|
+
"extras_install": extra,
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
return CheckResult(
|
|
278
|
+
f"optional_dep_{module_name}",
|
|
279
|
+
SEVERITY_WARN,
|
|
280
|
+
"Could not run Python interpreter to verify imports.",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _check_gitignore(project_root: Path) -> CheckResult:
|
|
285
|
+
gi = project_root / ".gitignore"
|
|
286
|
+
if not gi.exists():
|
|
287
|
+
return CheckResult(
|
|
288
|
+
"gitignore_state_dir",
|
|
289
|
+
SEVERITY_WARN,
|
|
290
|
+
f"No .gitignore at {project_root} — {STATE_DIR_NAME}/ may get committed.",
|
|
291
|
+
fix=f"Add `{STATE_DIR_NAME}/` to .gitignore.",
|
|
292
|
+
)
|
|
293
|
+
try:
|
|
294
|
+
lines = {line.strip() for line in gi.read_text().splitlines()}
|
|
295
|
+
except OSError:
|
|
296
|
+
return CheckResult(
|
|
297
|
+
"gitignore_state_dir",
|
|
298
|
+
SEVERITY_WARN,
|
|
299
|
+
f"Could not read {gi}.",
|
|
300
|
+
)
|
|
301
|
+
if any(entry in lines for entry in (STATE_DIR_NAME, f"{STATE_DIR_NAME}/")):
|
|
302
|
+
return CheckResult(
|
|
303
|
+
"gitignore_state_dir",
|
|
304
|
+
SEVERITY_OK,
|
|
305
|
+
f"{STATE_DIR_NAME}/ is in .gitignore",
|
|
306
|
+
)
|
|
307
|
+
return CheckResult(
|
|
308
|
+
"gitignore_state_dir",
|
|
309
|
+
SEVERITY_WARN,
|
|
310
|
+
f"{STATE_DIR_NAME}/ is not in .gitignore — index data may get committed.",
|
|
311
|
+
fix=f"Add `{STATE_DIR_NAME}/` to .gitignore.",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def run_doctor(server_url_override: str | None = None) -> DoctorReport:
|
|
316
|
+
"""Run every check and return a structured report."""
|
|
317
|
+
project_root = resolve_project_root()
|
|
318
|
+
state_dir = project_root / STATE_DIR_NAME
|
|
319
|
+
runtime_file: Path | None
|
|
320
|
+
if state_dir.exists():
|
|
321
|
+
runtime_file = state_dir / "runtime.json"
|
|
322
|
+
else:
|
|
323
|
+
legacy = project_root / LEGACY_STATE_DIR_NAME
|
|
324
|
+
runtime_file = legacy / "runtime.json" if legacy.exists() else None
|
|
325
|
+
|
|
326
|
+
server_url = server_url_override or get_server_url()
|
|
327
|
+
|
|
328
|
+
checks: list[CheckResult] = []
|
|
329
|
+
checks.append(_check_python())
|
|
330
|
+
checks.append(_check_project_init(project_root, state_dir))
|
|
331
|
+
checks.append(_check_provider_config(state_dir))
|
|
332
|
+
checks.extend(_check_api_keys())
|
|
333
|
+
|
|
334
|
+
# Optional deps that surface common install failures (issues #122/#125/#129).
|
|
335
|
+
try:
|
|
336
|
+
cfg = load_config()
|
|
337
|
+
except Exception: # pragma: no cover
|
|
338
|
+
cfg = None
|
|
339
|
+
if cfg and cfg.embedding.provider.lower() == "cohere":
|
|
340
|
+
checks.append(_check_optional_dep("cohere", "cohere", "cohere"))
|
|
341
|
+
checks.append(_check_gitignore(project_root))
|
|
342
|
+
|
|
343
|
+
checks.append(_check_server(server_url, runtime_file))
|
|
344
|
+
|
|
345
|
+
return DoctorReport(
|
|
346
|
+
project_root=str(project_root),
|
|
347
|
+
state_dir=str(state_dir),
|
|
348
|
+
state_dir_exists=state_dir.exists(),
|
|
349
|
+
runtime_file=str(runtime_file) if runtime_file else None,
|
|
350
|
+
server_url=server_url,
|
|
351
|
+
checks=checks,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def doctor_hint_message(project_root: Path | None = None) -> str:
|
|
356
|
+
"""Suggest the doctor command — and call out the most likely setup issue.
|
|
357
|
+
|
|
358
|
+
When ``runtime.json`` is missing, the user almost certainly hasn't run
|
|
359
|
+
``agent-brain init && agent-brain start`` in this directory. Saying so
|
|
360
|
+
is more useful than the generic "connection refused".
|
|
361
|
+
"""
|
|
362
|
+
root = project_root or resolve_project_root()
|
|
363
|
+
state_dir = root / STATE_DIR_NAME
|
|
364
|
+
runtime_file = state_dir / "runtime.json"
|
|
365
|
+
if not runtime_file.exists():
|
|
366
|
+
return (
|
|
367
|
+
"Tip: no `.agent-brain/runtime.json` found under "
|
|
368
|
+
f"{root}. Run `agent-brain init` and `agent-brain start` here "
|
|
369
|
+
"first, or run `agent-brain doctor` to diagnose."
|
|
370
|
+
)
|
|
371
|
+
return DOCTOR_HINT
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def report_to_json(report: DoctorReport) -> str:
|
|
375
|
+
return json.dumps(report.to_dict(), indent=2)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def env_snapshot() -> dict[str, Any]:
|
|
379
|
+
"""Lightweight environment summary used in JSON output."""
|
|
380
|
+
return {
|
|
381
|
+
"platform": platform.platform(),
|
|
382
|
+
"python": platform.python_version(),
|
|
383
|
+
"cwd": str(Path.cwd()),
|
|
384
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "agent-brain-cli"
|
|
3
|
-
version = "
|
|
3
|
+
version = "10.0.1"
|
|
4
4
|
description = "Agent Brain CLI - Command-line interface for managing AI agent memory and knowledge retrieval"
|
|
5
5
|
authors = ["Spillwave Solutions"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -27,7 +27,7 @@ httpx = "^0.28.0"
|
|
|
27
27
|
rich = "^13.9.0"
|
|
28
28
|
pyyaml = "^6.0.0"
|
|
29
29
|
pydantic = "^2.10.0"
|
|
30
|
-
agent-brain-rag = "^
|
|
30
|
+
agent-brain-rag = "^10.0.1"
|
|
31
31
|
|
|
32
32
|
[tool.poetry.group.dev.dependencies]
|
|
33
33
|
pytest = "^8.3.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/claude_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/gemini_converter.py
RENAMED
|
File without changes
|
{agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/opencode_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_brain_cli-9.6.0 → agent_brain_cli-10.0.1}/agent_brain_cli/runtime/skill_runtime_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|