agent-brain-cli 10.0.3__tar.gz → 10.0.5__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-10.0.3 → agent_brain_cli-10.0.5}/PKG-INFO +2 -2
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/__init__.py +1 -1
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/doctor.py +38 -2
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/jobs.py +31 -7
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/stop.py +1 -33
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/config.py +23 -10
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/diagnostics.py +149 -8
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/pyproject.toml +2 -2
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/README.md +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/cli.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/client/__init__.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/client/api_client.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/__init__.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/cache.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/config.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/folders.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/index.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/init.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/inject.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/install_agent.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/list_cmd.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/query.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/reset.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/start.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/status.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/types.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/commands/uninstall.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/config_migrate.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/config_schema.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/migration.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/__init__.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/claude_converter.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/codex_converter.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/converter_base.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/gemini_converter.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/opencode_converter.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/parser.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/skill_runtime_converter.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/tool_maps.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/types.py +0 -0
- {agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/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: 10.0.
|
|
3
|
+
Version: 10.0.5
|
|
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 (>=10.0.
|
|
18
|
+
Requires-Dist: agent-brain-rag (>=10.0.5,<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)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
|
|
5
7
|
import click
|
|
6
8
|
from rich.console import Console
|
|
7
9
|
from rich.panel import Panel
|
|
@@ -11,6 +13,7 @@ from agent_brain_cli.diagnostics import (
|
|
|
11
13
|
SEVERITY_FAIL,
|
|
12
14
|
SEVERITY_OK,
|
|
13
15
|
SEVERITY_WARN,
|
|
16
|
+
apply_safe_fixes,
|
|
14
17
|
report_to_json,
|
|
15
18
|
run_doctor,
|
|
16
19
|
)
|
|
@@ -33,18 +36,40 @@ _STATUS_STYLE = {
|
|
|
33
36
|
help="Server URL to probe (default: resolved from runtime.json or config).",
|
|
34
37
|
)
|
|
35
38
|
@click.option("--json", "json_output", is_flag=True, help="Emit machine-readable JSON.")
|
|
36
|
-
|
|
39
|
+
@click.option(
|
|
40
|
+
"--fix",
|
|
41
|
+
"apply_fixes",
|
|
42
|
+
is_flag=True,
|
|
43
|
+
help=(
|
|
44
|
+
"Apply safe, idempotent, offline fixes (add .agent-brain/ to .gitignore, "
|
|
45
|
+
"create state dir + stub config.json). Will not touch API keys, network, "
|
|
46
|
+
"or user code. Re-runs the report after fixing."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
def doctor_command(url: str | None, json_output: bool, apply_fixes: bool) -> None:
|
|
37
50
|
"""Diagnose your Agent Brain setup.
|
|
38
51
|
|
|
39
52
|
Inspects Python version, project init state, provider config, required
|
|
40
53
|
API keys, optional dependencies, .gitignore hygiene, and whether the
|
|
41
54
|
server is reachable. Exits non-zero on any critical failure so it can
|
|
42
55
|
be used in scripts (``agent-brain doctor || agent-brain init``).
|
|
56
|
+
|
|
57
|
+
Pass ``--fix`` to auto-apply the safe subset of remediations and re-run.
|
|
43
58
|
"""
|
|
44
59
|
report = run_doctor(server_url_override=url)
|
|
45
60
|
|
|
61
|
+
fix_actions: list[str] = []
|
|
62
|
+
if apply_fixes:
|
|
63
|
+
fix_actions = apply_safe_fixes(report)
|
|
64
|
+
if fix_actions:
|
|
65
|
+
# Re-run so the printed report reflects the fixed state.
|
|
66
|
+
report = run_doctor(server_url_override=url)
|
|
67
|
+
|
|
46
68
|
if json_output:
|
|
47
|
-
|
|
69
|
+
payload = json.loads(report_to_json(report))
|
|
70
|
+
if apply_fixes:
|
|
71
|
+
payload["applied_fixes"] = fix_actions
|
|
72
|
+
click.echo(json.dumps(payload, indent=2))
|
|
48
73
|
raise SystemExit(report.exit_code)
|
|
49
74
|
|
|
50
75
|
header_color = "green" if report.exit_code == 0 else "red"
|
|
@@ -75,6 +100,17 @@ def doctor_command(url: str | None, json_output: bool) -> None:
|
|
|
75
100
|
|
|
76
101
|
console.print(table)
|
|
77
102
|
|
|
103
|
+
if apply_fixes:
|
|
104
|
+
if fix_actions:
|
|
105
|
+
console.print("\n[cyan]Applied safe fixes:[/]")
|
|
106
|
+
for action in fix_actions:
|
|
107
|
+
console.print(f" • {action}")
|
|
108
|
+
else:
|
|
109
|
+
console.print(
|
|
110
|
+
"\n[dim]No safe fixes applied (nothing actionable, or all "
|
|
111
|
+
"checks already passing).[/]"
|
|
112
|
+
)
|
|
113
|
+
|
|
78
114
|
if report.exit_code != 0:
|
|
79
115
|
console.print(
|
|
80
116
|
"\n[red]Doctor reported critical issues.[/] "
|
|
@@ -25,13 +25,32 @@ def _format_timestamp(ts: str | None) -> str:
|
|
|
25
25
|
return ts
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def _format_progress(
|
|
29
|
-
|
|
28
|
+
def _format_progress(
|
|
29
|
+
progress: float | int | dict[str, Any] | None,
|
|
30
|
+
total: int | None,
|
|
31
|
+
) -> str:
|
|
32
|
+
"""Format progress for display.
|
|
33
|
+
|
|
34
|
+
Copes with both the legacy float shape and the structured ``JobProgress``
|
|
35
|
+
dict the server emits today (#150). Falls back to a string coercion for
|
|
36
|
+
unknown types so the CLI never raises.
|
|
37
|
+
"""
|
|
30
38
|
if progress is None:
|
|
31
39
|
return "-"
|
|
32
|
-
if
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
if isinstance(progress, dict):
|
|
41
|
+
pct = progress.get("percent_complete")
|
|
42
|
+
if isinstance(pct, (int, float)):
|
|
43
|
+
files_total = progress.get("files_total") or 0
|
|
44
|
+
files_done = progress.get("files_processed") or 0
|
|
45
|
+
if files_total:
|
|
46
|
+
return f"{pct:.1f}% ({files_done}/{files_total} files)"
|
|
47
|
+
return f"{pct:.1f}%"
|
|
48
|
+
return ", ".join(f"{k}={v}" for k, v in progress.items())
|
|
49
|
+
if isinstance(progress, (int, float)):
|
|
50
|
+
if total:
|
|
51
|
+
return f"{progress:.1f}% ({total} files)"
|
|
52
|
+
return f"{progress:.1f}%"
|
|
53
|
+
return str(progress)
|
|
35
54
|
|
|
36
55
|
|
|
37
56
|
def _get_status_style(status: str) -> str:
|
|
@@ -121,12 +140,17 @@ def _create_job_detail_panel(job: dict[str, Any]) -> Panel:
|
|
|
121
140
|
if (progress := job.get("progress_percent", job.get("progress"))) is not None:
|
|
122
141
|
total = job.get("total_files", 0)
|
|
123
142
|
processed = job.get("processed_files", 0)
|
|
124
|
-
|
|
143
|
+
# #150: server emits structured JobProgress dicts in addition to floats.
|
|
144
|
+
# Defer formatting to _format_progress so list-view and detail-view
|
|
145
|
+
# never diverge on type handling.
|
|
146
|
+
if isinstance(progress, (int, float)) and total:
|
|
125
147
|
lines.append(
|
|
126
148
|
f"[bold]Progress:[/] {progress:.1f}% ({processed}/{total} files)"
|
|
127
149
|
)
|
|
128
150
|
else:
|
|
129
|
-
lines.append(
|
|
151
|
+
lines.append(
|
|
152
|
+
f"[bold]Progress:[/] {_format_progress(progress, total or None)}"
|
|
153
|
+
)
|
|
130
154
|
|
|
131
155
|
if enqueued := job.get("enqueued_at", job.get("created_at")):
|
|
132
156
|
lines.append(f"[bold]Enqueued:[/] {_format_timestamp(enqueued)}")
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import signal
|
|
6
|
-
import subprocess
|
|
7
6
|
import time
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
from typing import Any
|
|
@@ -12,6 +11,7 @@ from urllib.request import Request, urlopen
|
|
|
12
11
|
import click
|
|
13
12
|
from rich.console import Console
|
|
14
13
|
|
|
14
|
+
from agent_brain_cli.config import resolve_project_root
|
|
15
15
|
from agent_brain_cli.migration import resolve_state_dir_with_fallback
|
|
16
16
|
from agent_brain_cli.xdg_paths import get_registry_path
|
|
17
17
|
|
|
@@ -23,38 +23,6 @@ PID_FILE = "agent-brain.pid"
|
|
|
23
23
|
RUNTIME_FILE = "runtime.json"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
27
|
-
"""Resolve the canonical project root directory."""
|
|
28
|
-
start = (start_path or Path.cwd()).resolve()
|
|
29
|
-
|
|
30
|
-
# Try git root first
|
|
31
|
-
try:
|
|
32
|
-
result = subprocess.run(
|
|
33
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
34
|
-
capture_output=True,
|
|
35
|
-
text=True,
|
|
36
|
-
timeout=5,
|
|
37
|
-
cwd=str(start),
|
|
38
|
-
)
|
|
39
|
-
if result.returncode == 0:
|
|
40
|
-
return Path(result.stdout.strip()).resolve()
|
|
41
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
42
|
-
pass
|
|
43
|
-
|
|
44
|
-
# Walk up looking for markers
|
|
45
|
-
current = start
|
|
46
|
-
while current != current.parent:
|
|
47
|
-
if (current / ".agent-brain").is_dir():
|
|
48
|
-
return current
|
|
49
|
-
if (current / ".claude").is_dir():
|
|
50
|
-
return current
|
|
51
|
-
if (current / "pyproject.toml").is_file():
|
|
52
|
-
return current
|
|
53
|
-
current = current.parent
|
|
54
|
-
|
|
55
|
-
return start
|
|
56
|
-
|
|
57
|
-
|
|
58
26
|
def read_runtime(state_dir: Path) -> dict[str, Any] | None:
|
|
59
27
|
"""Read runtime state from state directory."""
|
|
60
28
|
runtime_path = state_dir / RUNTIME_FILE
|
|
@@ -254,6 +254,20 @@ def load_config(start_path: Path | None = None) -> AgentBrainConfig:
|
|
|
254
254
|
def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
255
255
|
"""Find the project root by looking for markers.
|
|
256
256
|
|
|
257
|
+
Thin wrapper around :func:`resolve_project_root_with_strategy` that drops
|
|
258
|
+
the strategy label for callers that only need the path.
|
|
259
|
+
"""
|
|
260
|
+
return resolve_project_root_with_strategy(start_path)[0]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def resolve_project_root_with_strategy(
|
|
264
|
+
start_path: Path | None = None,
|
|
265
|
+
) -> tuple[Path, str]:
|
|
266
|
+
"""Find the project root and report *which* rule matched.
|
|
267
|
+
|
|
268
|
+
Used by ``agent-brain doctor`` to explain why a given directory was
|
|
269
|
+
selected (issue #146).
|
|
270
|
+
|
|
257
271
|
Resolution order (first match wins):
|
|
258
272
|
1. Walk up from ``start_path`` looking for ``.agent-brain/`` — this lets a
|
|
259
273
|
sub-project inside a mono-repo keep its own state dir and not get
|
|
@@ -263,11 +277,10 @@ def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
|
263
277
|
4. Walk up looking for ``.claude/`` or ``pyproject.toml``.
|
|
264
278
|
5. Fall back to ``start_path``.
|
|
265
279
|
|
|
266
|
-
Args:
|
|
267
|
-
start_path: Starting directory. Defaults to cwd.
|
|
268
|
-
|
|
269
280
|
Returns:
|
|
270
|
-
|
|
281
|
+
``(root, strategy)`` where ``strategy`` is one of
|
|
282
|
+
``"agent_brain_dir"``, ``"legacy_claude_dir"``, ``"git_root"``,
|
|
283
|
+
``"claude_dir"``, ``"pyproject"``, ``"cwd_fallback"``.
|
|
271
284
|
"""
|
|
272
285
|
import subprocess
|
|
273
286
|
|
|
@@ -277,9 +290,9 @@ def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
|
277
290
|
current = start
|
|
278
291
|
while current != current.parent:
|
|
279
292
|
if (current / STATE_DIR_NAME).is_dir():
|
|
280
|
-
return current
|
|
293
|
+
return current, "agent_brain_dir"
|
|
281
294
|
if (current / LEGACY_STATE_DIR_NAME).is_dir():
|
|
282
|
-
return current
|
|
295
|
+
return current, "legacy_claude_dir"
|
|
283
296
|
current = current.parent
|
|
284
297
|
|
|
285
298
|
# 3. Git root next — useful when this is the first time the user runs init.
|
|
@@ -292,7 +305,7 @@ def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
|
292
305
|
cwd=str(start),
|
|
293
306
|
)
|
|
294
307
|
if result.returncode == 0:
|
|
295
|
-
return Path(result.stdout.strip()).resolve()
|
|
308
|
+
return Path(result.stdout.strip()).resolve(), "git_root"
|
|
296
309
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
297
310
|
pass
|
|
298
311
|
|
|
@@ -300,12 +313,12 @@ def resolve_project_root(start_path: Path | None = None) -> Path:
|
|
|
300
313
|
current = start
|
|
301
314
|
while current != current.parent:
|
|
302
315
|
if (current / ".claude").is_dir():
|
|
303
|
-
return current
|
|
316
|
+
return current, "claude_dir"
|
|
304
317
|
if (current / "pyproject.toml").is_file():
|
|
305
|
-
return current
|
|
318
|
+
return current, "pyproject"
|
|
306
319
|
current = current.parent
|
|
307
320
|
|
|
308
|
-
return start
|
|
321
|
+
return start, "cwd_fallback"
|
|
309
322
|
|
|
310
323
|
|
|
311
324
|
# Backwards-compatible alias for any external callers.
|
|
@@ -26,6 +26,7 @@ from agent_brain_cli.config import (
|
|
|
26
26
|
get_server_url,
|
|
27
27
|
load_config,
|
|
28
28
|
resolve_project_root,
|
|
29
|
+
resolve_project_root_with_strategy,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
#: Severity returned by every diagnostic check.
|
|
@@ -69,6 +70,49 @@ class DoctorReport:
|
|
|
69
70
|
return data
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
_RESOLVE_STRATEGY_LABEL: dict[str, str] = {
|
|
74
|
+
"agent_brain_dir": f"found {STATE_DIR_NAME}/ in this dir or an ancestor",
|
|
75
|
+
"legacy_claude_dir": f"found legacy {LEGACY_STATE_DIR_NAME}/",
|
|
76
|
+
"git_root": "git repository root (no state dir present yet)",
|
|
77
|
+
"claude_dir": ".claude/ marker in this dir or an ancestor",
|
|
78
|
+
"pyproject": "pyproject.toml marker in this dir or an ancestor",
|
|
79
|
+
"cwd_fallback": "no markers found — falling back to cwd",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _check_version() -> CheckResult:
|
|
84
|
+
"""Confirm the installed agent-brain-cli is importable and report version.
|
|
85
|
+
|
|
86
|
+
Issue #146 check #2 — surfaces broken installs (missing entry-point,
|
|
87
|
+
namespace shadowing, half-rolled-back upgrades) at the top of the doctor
|
|
88
|
+
report instead of leaving the user to discover them later.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
92
|
+
|
|
93
|
+
ver = version("agent-brain-cli")
|
|
94
|
+
except PackageNotFoundError as exc:
|
|
95
|
+
return CheckResult(
|
|
96
|
+
"cli_version",
|
|
97
|
+
SEVERITY_FAIL,
|
|
98
|
+
"agent-brain-cli is not installed in this Python environment.",
|
|
99
|
+
fix="pip install agent-brain-cli (or uv tool install agent-brain-cli)",
|
|
100
|
+
details={"error": str(exc)},
|
|
101
|
+
)
|
|
102
|
+
except Exception as exc: # noqa: BLE001
|
|
103
|
+
return CheckResult(
|
|
104
|
+
"cli_version",
|
|
105
|
+
SEVERITY_FAIL,
|
|
106
|
+
f"Could not determine agent-brain-cli version: {exc}",
|
|
107
|
+
)
|
|
108
|
+
return CheckResult(
|
|
109
|
+
"cli_version",
|
|
110
|
+
SEVERITY_OK,
|
|
111
|
+
f"agent-brain-cli {ver}",
|
|
112
|
+
details={"version": ver},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
72
116
|
def _check_python() -> CheckResult:
|
|
73
117
|
major, minor = sys.version_info[:2]
|
|
74
118
|
version = f"{major}.{minor}.{sys.version_info.micro}"
|
|
@@ -88,23 +132,35 @@ def _check_python() -> CheckResult:
|
|
|
88
132
|
)
|
|
89
133
|
|
|
90
134
|
|
|
91
|
-
def _check_project_init(
|
|
135
|
+
def _check_project_init(
|
|
136
|
+
project_root: Path, state_dir: Path, resolved_via: str
|
|
137
|
+
) -> CheckResult:
|
|
138
|
+
"""Validate the resolved project root and explain *why* it was picked.
|
|
139
|
+
|
|
140
|
+
Issue #146 check #3 — operators on monorepos / nested projects can be
|
|
141
|
+
surprised by which directory wins; the strategy label tells them.
|
|
142
|
+
"""
|
|
143
|
+
strategy_msg = _RESOLVE_STRATEGY_LABEL.get(resolved_via, resolved_via)
|
|
92
144
|
config_path = state_dir / "config.json"
|
|
93
145
|
if config_path.exists():
|
|
94
146
|
return CheckResult(
|
|
95
147
|
"project_initialized",
|
|
96
148
|
SEVERITY_OK,
|
|
97
|
-
f"Project initialized at {state_dir}",
|
|
98
|
-
details={
|
|
149
|
+
f"Project initialized at {state_dir} ({strategy_msg})",
|
|
150
|
+
details={
|
|
151
|
+
"state_dir": str(state_dir),
|
|
152
|
+
"resolved_via": resolved_via,
|
|
153
|
+
},
|
|
99
154
|
)
|
|
100
155
|
return CheckResult(
|
|
101
156
|
"project_initialized",
|
|
102
157
|
SEVERITY_FAIL,
|
|
103
|
-
f"No {STATE_DIR_NAME}/config.json under {project_root}",
|
|
158
|
+
(f"No {STATE_DIR_NAME}/config.json under {project_root} " f"({strategy_msg})"),
|
|
104
159
|
fix="Run `agent-brain init` in your project directory.",
|
|
105
160
|
details={
|
|
106
161
|
"project_root": str(project_root),
|
|
107
162
|
"expected_path": str(config_path),
|
|
163
|
+
"resolved_via": resolved_via,
|
|
108
164
|
},
|
|
109
165
|
)
|
|
110
166
|
|
|
@@ -223,11 +279,21 @@ def _check_server(server_url: str, runtime_file: Path | None) -> CheckResult:
|
|
|
223
279
|
req = Request(server_url.rstrip("/") + "/health")
|
|
224
280
|
with urlopen(req, timeout=3) as resp: # noqa: S310 — local URL
|
|
225
281
|
body = resp.read().decode("utf-8", errors="replace")
|
|
282
|
+
# Issue #146 check #7 — also pull /health/status for the richer
|
|
283
|
+
# indexing summary. Tolerate older servers that 404 here.
|
|
284
|
+
indexing_summary, indexing_payload = _fetch_indexing_summary(server_url)
|
|
285
|
+
message = f"Server responded at {server_url}"
|
|
286
|
+
if indexing_summary:
|
|
287
|
+
message = f"{message} — {indexing_summary}"
|
|
226
288
|
return CheckResult(
|
|
227
289
|
"server_reachable",
|
|
228
290
|
SEVERITY_OK,
|
|
229
|
-
|
|
230
|
-
details={
|
|
291
|
+
message,
|
|
292
|
+
details={
|
|
293
|
+
"server_url": server_url,
|
|
294
|
+
"response_preview": body[:120],
|
|
295
|
+
"indexing": indexing_payload,
|
|
296
|
+
},
|
|
231
297
|
)
|
|
232
298
|
except URLError as exc:
|
|
233
299
|
return CheckResult(
|
|
@@ -247,6 +313,30 @@ def _check_server(server_url: str, runtime_file: Path | None) -> CheckResult:
|
|
|
247
313
|
)
|
|
248
314
|
|
|
249
315
|
|
|
316
|
+
def _fetch_indexing_summary(
|
|
317
|
+
server_url: str,
|
|
318
|
+
) -> tuple[str | None, dict[str, Any] | None]:
|
|
319
|
+
"""Best-effort fetch of /health/status, returning (one-line summary, raw)."""
|
|
320
|
+
try:
|
|
321
|
+
req = Request(server_url.rstrip("/") + "/health/status")
|
|
322
|
+
with urlopen(req, timeout=3) as resp: # noqa: S310 — local URL
|
|
323
|
+
payload = json.loads(resp.read().decode("utf-8", errors="replace"))
|
|
324
|
+
except Exception: # noqa: BLE001 — old server or transient error is fine
|
|
325
|
+
return None, None
|
|
326
|
+
if not isinstance(payload, dict):
|
|
327
|
+
return None, None
|
|
328
|
+
state = payload.get("state") or payload.get("indexing_state") or "unknown"
|
|
329
|
+
chunk_count = (
|
|
330
|
+
payload.get("chunk_count")
|
|
331
|
+
or payload.get("total_chunks")
|
|
332
|
+
or payload.get("document_count")
|
|
333
|
+
)
|
|
334
|
+
parts = [f"indexing={state}"]
|
|
335
|
+
if isinstance(chunk_count, int):
|
|
336
|
+
parts.append(f"chunks={chunk_count}")
|
|
337
|
+
return ", ".join(parts), payload
|
|
338
|
+
|
|
339
|
+
|
|
250
340
|
def _check_optional_dep(provider: str, module_name: str, extra: str) -> CheckResult:
|
|
251
341
|
"""Report on an optional Python package that a chosen provider needs."""
|
|
252
342
|
if shutil.which("python3"):
|
|
@@ -314,7 +404,7 @@ def _check_gitignore(project_root: Path) -> CheckResult:
|
|
|
314
404
|
|
|
315
405
|
def run_doctor(server_url_override: str | None = None) -> DoctorReport:
|
|
316
406
|
"""Run every check and return a structured report."""
|
|
317
|
-
project_root =
|
|
407
|
+
project_root, resolved_via = resolve_project_root_with_strategy()
|
|
318
408
|
state_dir = project_root / STATE_DIR_NAME
|
|
319
409
|
runtime_file: Path | None
|
|
320
410
|
if state_dir.exists():
|
|
@@ -327,7 +417,8 @@ def run_doctor(server_url_override: str | None = None) -> DoctorReport:
|
|
|
327
417
|
|
|
328
418
|
checks: list[CheckResult] = []
|
|
329
419
|
checks.append(_check_python())
|
|
330
|
-
checks.append(
|
|
420
|
+
checks.append(_check_version())
|
|
421
|
+
checks.append(_check_project_init(project_root, state_dir, resolved_via))
|
|
331
422
|
checks.append(_check_provider_config(state_dir))
|
|
332
423
|
checks.extend(_check_api_keys())
|
|
333
424
|
|
|
@@ -338,6 +429,13 @@ def run_doctor(server_url_override: str | None = None) -> DoctorReport:
|
|
|
338
429
|
cfg = None
|
|
339
430
|
if cfg and cfg.embedding.provider.lower() == "cohere":
|
|
340
431
|
checks.append(_check_optional_dep("cohere", "cohere", "cohere"))
|
|
432
|
+
|
|
433
|
+
# Issue #146 check #8 — surface graphrag's langextract dependency.
|
|
434
|
+
if cfg and getattr(getattr(cfg, "graphrag", None), "enabled", False):
|
|
435
|
+
checks.append(
|
|
436
|
+
_check_optional_dep("graphrag (langextract)", "langextract", "graphrag")
|
|
437
|
+
)
|
|
438
|
+
|
|
341
439
|
checks.append(_check_gitignore(project_root))
|
|
342
440
|
|
|
343
441
|
checks.append(_check_server(server_url, runtime_file))
|
|
@@ -352,6 +450,49 @@ def run_doctor(server_url_override: str | None = None) -> DoctorReport:
|
|
|
352
450
|
)
|
|
353
451
|
|
|
354
452
|
|
|
453
|
+
def apply_safe_fixes(report: DoctorReport) -> list[str]:
|
|
454
|
+
"""Apply the subset of fixes that are safe + idempotent + offline.
|
|
455
|
+
|
|
456
|
+
Returns the list of human-readable actions taken (empty if nothing to fix).
|
|
457
|
+
Used by ``agent-brain doctor --fix``. Anything that calls the network,
|
|
458
|
+
modifies user code, or requires an API key is *not* covered here — the
|
|
459
|
+
user must still address those manually.
|
|
460
|
+
"""
|
|
461
|
+
actions: list[str] = []
|
|
462
|
+
project_root = Path(report.project_root)
|
|
463
|
+
state_dir = Path(report.state_dir)
|
|
464
|
+
for check in report.checks:
|
|
465
|
+
if check.name == "gitignore_state_dir" and check.status != SEVERITY_OK:
|
|
466
|
+
gi = project_root / ".gitignore"
|
|
467
|
+
line = f"{STATE_DIR_NAME}/\n"
|
|
468
|
+
if gi.exists():
|
|
469
|
+
content = gi.read_text()
|
|
470
|
+
if not content.endswith("\n"):
|
|
471
|
+
content += "\n"
|
|
472
|
+
gi.write_text(content + line)
|
|
473
|
+
else:
|
|
474
|
+
gi.write_text(line)
|
|
475
|
+
actions.append(f"Added {STATE_DIR_NAME}/ to {gi}.")
|
|
476
|
+
elif check.name == "project_initialized" and check.status == SEVERITY_FAIL:
|
|
477
|
+
# Create the state dir + a minimal config.json shell so a follow-up
|
|
478
|
+
# `agent-brain init` (or any command) has something to read.
|
|
479
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
480
|
+
cfg_json = state_dir / "config.json"
|
|
481
|
+
if not cfg_json.exists():
|
|
482
|
+
cfg_json.write_text(
|
|
483
|
+
json.dumps(
|
|
484
|
+
{
|
|
485
|
+
"project_root": str(project_root),
|
|
486
|
+
"created_by": "agent-brain doctor --fix",
|
|
487
|
+
},
|
|
488
|
+
indent=2,
|
|
489
|
+
)
|
|
490
|
+
+ "\n"
|
|
491
|
+
)
|
|
492
|
+
actions.append(f"Created {cfg_json}.")
|
|
493
|
+
return actions
|
|
494
|
+
|
|
495
|
+
|
|
355
496
|
def doctor_hint_message(project_root: Path | None = None) -> str:
|
|
356
497
|
"""Suggest the doctor command — and call out the most likely setup issue.
|
|
357
498
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "agent-brain-cli"
|
|
3
|
-
version = "10.0.
|
|
3
|
+
version = "10.0.5"
|
|
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 = "^10.0.
|
|
30
|
+
agent-brain-rag = "^10.0.5"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/claude_converter.py
RENAMED
|
File without changes
|
{agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/codex_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/gemini_converter.py
RENAMED
|
File without changes
|
{agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/opencode_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_brain_cli-10.0.3 → agent_brain_cli-10.0.5}/agent_brain_cli/runtime/skill_runtime_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|