agent-brain-cli 10.3.1__tar.gz → 10.4.0__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.3.1 → agent_brain_cli-10.4.0}/PKG-INFO +2 -2
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/__init__.py +1 -1
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/cli.py +4 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/client/transport.py +61 -3
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/__init__.py +2 -0
- agent_brain_cli-10.4.0/agent_brain_cli/commands/graph.py +154 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/diagnostics.py +166 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/pyproject.toml +2 -2
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/README.md +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/__main__.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/client/__init__.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/client/api_client.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/client/protocol.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/cache.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/config.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/doctor.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/folders.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/index.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/init.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/inject.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/install_agent.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/jobs.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/list_cmd.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/mcp.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/prompt.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/query.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/reset.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/resources.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/start.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/status.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/stop.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/types.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/commands/uninstall.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/config.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/config_migrate.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/config_schema.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/mcp_runtime.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/migration.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/__init__.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/claude_converter.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/codex_converter.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/converter_base.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/gemini_converter.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/opencode_converter.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/parser.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/skill_runtime_converter.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/tool_maps.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/types.py +0 -0
- {agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/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.
|
|
3
|
+
Version: 10.4.0
|
|
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.
|
|
18
|
+
Requires-Dist: agent-brain-rag (>=10.4.0,<11.0.0)
|
|
19
19
|
Requires-Dist: agent-brain-uds (>=10.3.0,<11.0.0)
|
|
20
20
|
Requires-Dist: click (>=8.1.0,<9.0.0)
|
|
21
21
|
Requires-Dist: httpx (>=0.28.0,<0.29.0)
|
|
@@ -12,6 +12,7 @@ from .commands import (
|
|
|
12
12
|
config_group,
|
|
13
13
|
doctor_command,
|
|
14
14
|
folders_group,
|
|
15
|
+
graph_group,
|
|
15
16
|
index_command,
|
|
16
17
|
init_command,
|
|
17
18
|
inject_command,
|
|
@@ -164,6 +165,9 @@ cli.add_command(mcp_group, name="mcp")
|
|
|
164
165
|
cli.add_command(prompt_command, name="prompt")
|
|
165
166
|
cli.add_command(resources_group, name="resources")
|
|
166
167
|
|
|
168
|
+
# Register graph management commands
|
|
169
|
+
cli.add_command(graph_group, name="graph")
|
|
170
|
+
|
|
167
171
|
# Register server interaction commands
|
|
168
172
|
cli.add_command(status_command, name="status")
|
|
169
173
|
cli.add_command(query_command, name="query")
|
|
@@ -32,6 +32,7 @@ Three §3.5 design-doc misuse cases surface as ``click.UsageError``
|
|
|
32
32
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
|
+
import os
|
|
35
36
|
import shutil
|
|
36
37
|
from pathlib import Path
|
|
37
38
|
from typing import cast
|
|
@@ -63,6 +64,41 @@ def _resolve_state_dir_for_discovery() -> Path | None:
|
|
|
63
64
|
return None
|
|
64
65
|
|
|
65
66
|
|
|
67
|
+
def _stdio_command(state_dir: Path | None) -> str | list[str]:
|
|
68
|
+
"""Build the ``agent-brain-mcp`` stdio command, pinning the state dir.
|
|
69
|
+
|
|
70
|
+
When ``state_dir`` is resolved, returns
|
|
71
|
+
``["agent-brain-mcp", "--state-dir", <path>]`` so the spawned MCP
|
|
72
|
+
server resolves the same backend the CLI's UDS path would (CLI-MCP-04
|
|
73
|
+
byte-identical guarantee). ``McpStdioBackend`` filters the subprocess
|
|
74
|
+
env through ``DEFAULT_ENV_ALLOWLIST`` (which intentionally drops
|
|
75
|
+
``AGENT_BRAIN_STATE_DIR``), so an explicit arg is the only reliable
|
|
76
|
+
way to propagate the state dir across the env boundary.
|
|
77
|
+
|
|
78
|
+
``AGENT_BRAIN_STATE_DIR`` takes precedence over the discovery value,
|
|
79
|
+
mirroring the UDS leg (``resolve_transport`` →
|
|
80
|
+
``resolve_socket_path`` honors the same override). Without this, a
|
|
81
|
+
user who sets ``AGENT_BRAIN_STATE_DIR`` and runs from a non-project
|
|
82
|
+
cwd would get the project-root discovery dir on the MCP leg but the
|
|
83
|
+
override on the UDS leg — diverging the two and breaking CLI-MCP-04.
|
|
84
|
+
|
|
85
|
+
When neither the env override nor ``state_dir`` resolves, returns the
|
|
86
|
+
bare ``"agent-brain-mcp"`` string and lets the subprocess fall back
|
|
87
|
+
to its own cwd-based discovery — preserving pre-fix behavior.
|
|
88
|
+
"""
|
|
89
|
+
env_override = os.environ.get("AGENT_BRAIN_STATE_DIR")
|
|
90
|
+
if env_override:
|
|
91
|
+
resolved: str | None = str(Path(env_override).expanduser())
|
|
92
|
+
elif state_dir is not None:
|
|
93
|
+
resolved = str(state_dir)
|
|
94
|
+
else:
|
|
95
|
+
resolved = None
|
|
96
|
+
|
|
97
|
+
if resolved is None:
|
|
98
|
+
return "agent-brain-mcp"
|
|
99
|
+
return ["agent-brain-mcp", "--state-dir", resolved]
|
|
100
|
+
|
|
101
|
+
|
|
66
102
|
def open_backend(ctx: click.Context, *, timeout: float = 30.0) -> BackendClient:
|
|
67
103
|
"""Construct a ``BackendClient`` over the transport selected by ``ctx``.
|
|
68
104
|
|
|
@@ -132,15 +168,32 @@ def open_backend(ctx: click.Context, *, timeout: float = 30.0) -> BackendClient:
|
|
|
132
168
|
"agent-brain-mcp not found on PATH; install "
|
|
133
169
|
"agent-brain-mcp into the same Python environment"
|
|
134
170
|
)
|
|
171
|
+
# Pin the resolved state dir into the subprocess as an
|
|
172
|
+
# explicit --state-dir arg (CLI-MCP-04). McpStdioBackend
|
|
173
|
+
# filters env through DEFAULT_ENV_ALLOWLIST, which drops
|
|
174
|
+
# AGENT_BRAIN_STATE_DIR, so without this the spawned
|
|
175
|
+
# agent-brain-mcp falls back to cwd discovery and may resolve
|
|
176
|
+
# a DIFFERENT backend than --transport uds. Passing the dir
|
|
177
|
+
# explicitly makes the byte-identical guarantee hold from any
|
|
178
|
+
# cwd. When state_dir is None (resolution failed) we keep the
|
|
179
|
+
# bare command and let agent-brain-mcp do its own cwd discovery.
|
|
180
|
+
command = _stdio_command(state_dir)
|
|
135
181
|
# cast(): agent_brain_mcp ships with ignore_missing_imports=true
|
|
136
182
|
# so mypy treats McpStdioBackend as Any. The runtime
|
|
137
183
|
# @runtime_checkable Protocol contract is pinned by the
|
|
138
184
|
# Phase 56-03 isinstance test in agent-brain-mcp/tests/.
|
|
139
|
-
return cast(BackendClient, McpStdioBackend(command=
|
|
185
|
+
return cast(BackendClient, McpStdioBackend(command=command))
|
|
140
186
|
# mcp_transport == "http" — resolve_mcp_transport guarantees
|
|
141
187
|
# mcp_target is not None for the http branch.
|
|
142
188
|
assert mcp_target is not None # noqa: S101
|
|
143
|
-
|
|
189
|
+
# Thread state_dir so FileTokenStorage is keyed correctly when
|
|
190
|
+
# AGENT_BRAIN_MCP_AUTH=oauth is set (Phase 69 Plan 03).
|
|
191
|
+
# The opt-in flows via the AGENT_BRAIN_MCP_AUTH env var — no extra
|
|
192
|
+
# param needed here.
|
|
193
|
+
return cast(
|
|
194
|
+
BackendClient,
|
|
195
|
+
McpHttpBackend(url=mcp_target, timeout=timeout, state_dir=state_dir),
|
|
196
|
+
)
|
|
144
197
|
|
|
145
198
|
# --- HTTP / UDS branch (existing v1/v2 path) -----------------
|
|
146
199
|
transport, target = resolve_transport(
|
|
@@ -244,11 +297,16 @@ def open_mcp_backend(ctx: click.Context, *, timeout: float = 30.0) -> McpBackend
|
|
|
244
297
|
"agent-brain-mcp not found on PATH; install "
|
|
245
298
|
"agent-brain-mcp into the same Python environment"
|
|
246
299
|
)
|
|
300
|
+
# Pin the resolved state dir into the subprocess (CLI-MCP-04) —
|
|
301
|
+
# same rationale as open_backend: DEFAULT_ENV_ALLOWLIST drops
|
|
302
|
+
# AGENT_BRAIN_STATE_DIR, so prompts/resources would otherwise run
|
|
303
|
+
# against a cwd-discovered backend.
|
|
304
|
+
command = _stdio_command(state_dir)
|
|
247
305
|
# cast(): mirrors open_backend's pattern. The runtime
|
|
248
306
|
# @runtime_checkable Protocol contract is pinned by the
|
|
249
307
|
# Plan 59-01 isinstance test in agent-brain-mcp/tests/
|
|
250
308
|
# test_mcp_backend_protocol_skeleton.py.
|
|
251
|
-
return cast(McpBackend, McpStdioBackend(command=
|
|
309
|
+
return cast(McpBackend, McpStdioBackend(command=command))
|
|
252
310
|
# mcp_transport == "http" — resolve_mcp_transport guarantees
|
|
253
311
|
# mcp_target is not None for the http branch.
|
|
254
312
|
assert mcp_target is not None # noqa: S101
|
|
@@ -4,6 +4,7 @@ from .cache import cache_group
|
|
|
4
4
|
from .config import config_group
|
|
5
5
|
from .doctor import doctor_command
|
|
6
6
|
from .folders import folders_group
|
|
7
|
+
from .graph import graph_group
|
|
7
8
|
from .index import index_command
|
|
8
9
|
from .init import init_command
|
|
9
10
|
from .inject import inject_command
|
|
@@ -26,6 +27,7 @@ __all__ = [
|
|
|
26
27
|
"config_group",
|
|
27
28
|
"doctor_command",
|
|
28
29
|
"folders_group",
|
|
30
|
+
"graph_group",
|
|
29
31
|
"index_command",
|
|
30
32
|
"inject_command",
|
|
31
33
|
"init_command",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""`agent-brain graph` Click sub-group for graph management commands (Phase 64).
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
restore-from-snapshot — replay the latest (or a specific) kuzu snapshot
|
|
5
|
+
back into the live graph. Confirm-by-default; --yes skips the prompt
|
|
6
|
+
for non-interactive/CI use; --dry-run reports the plan without mutating.
|
|
7
|
+
|
|
8
|
+
Closes #184 bug 1: when kuzu opens cleanly but the live graph is STALE after
|
|
9
|
+
an AGENT_BRAIN_JOB_TIMEOUT rollback, operators can replay the latest valid
|
|
10
|
+
snapshot to bring the graph back in sync.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from agent_brain_cli.config import STATE_DIR_NAME, resolve_project_root
|
|
21
|
+
|
|
22
|
+
# Re-use diagnostics helpers for state-dir + store-type resolution and the
|
|
23
|
+
# server-running guard. These are local imports inside the CLI so the module
|
|
24
|
+
# loads without requiring the server package to be installed.
|
|
25
|
+
from agent_brain_cli.diagnostics import (
|
|
26
|
+
_graph_index_dir,
|
|
27
|
+
_read_graphrag_block,
|
|
28
|
+
_server_is_running,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# agent-brain-rag is a declared dependency of agent-brain-cli (^10.x), so this
|
|
32
|
+
# import is safe at module level. The lazy try/except below is a belt-and-braces
|
|
33
|
+
# guard for edge cases where the server wheel is not installed in the env.
|
|
34
|
+
try:
|
|
35
|
+
from agent_brain_server.storage.graph_store import GraphStoreManager
|
|
36
|
+
except ImportError: # pragma: no cover
|
|
37
|
+
GraphStoreManager = None
|
|
38
|
+
|
|
39
|
+
console = Console()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.group(name="graph")
|
|
43
|
+
def graph_group() -> None:
|
|
44
|
+
"""Manage the GraphRAG graph index (kuzu backend).
|
|
45
|
+
|
|
46
|
+
\b
|
|
47
|
+
Commands:
|
|
48
|
+
restore-from-snapshot Replay a snapshot back into kuzu.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@graph_group.command("restore-from-snapshot")
|
|
53
|
+
@click.option(
|
|
54
|
+
"--snapshot",
|
|
55
|
+
"snapshot_path",
|
|
56
|
+
type=click.Path(exists=True, path_type=Path),
|
|
57
|
+
default=None,
|
|
58
|
+
help=(
|
|
59
|
+
"Snapshot file to restore (default: latest valid snapshot on disk). "
|
|
60
|
+
"Must be a .json snapshot written by the agent-brain-server."
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
@click.option(
|
|
64
|
+
"--dry-run",
|
|
65
|
+
is_flag=True,
|
|
66
|
+
help="Report what would be restored and exit without mutating kuzu.",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--yes",
|
|
70
|
+
"assume_yes",
|
|
71
|
+
is_flag=True,
|
|
72
|
+
help="Skip the confirmation prompt (non-interactive/CI use).",
|
|
73
|
+
)
|
|
74
|
+
def restore_from_snapshot(
|
|
75
|
+
snapshot_path: Path | None,
|
|
76
|
+
dry_run: bool,
|
|
77
|
+
assume_yes: bool,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Replay a kuzu snapshot back into the live graph.
|
|
80
|
+
|
|
81
|
+
Resolves the project's graph_index directory, reads the store type from
|
|
82
|
+
config, then replays the latest valid (or a specific) snapshot back into
|
|
83
|
+
the kuzu database.
|
|
84
|
+
|
|
85
|
+
Default (no flags): prints a summary of what WILL be restored and prompts
|
|
86
|
+
for confirmation. Use --yes for CI/scripted invocations; --dry-run to
|
|
87
|
+
preview without touching the database.
|
|
88
|
+
|
|
89
|
+
The server MUST be stopped before running this command because kuzu does
|
|
90
|
+
not allow concurrent writers.
|
|
91
|
+
"""
|
|
92
|
+
# --- Resolve state dir + store type ----------------------------------- #
|
|
93
|
+
project_root = resolve_project_root()
|
|
94
|
+
state_dir = project_root / STATE_DIR_NAME
|
|
95
|
+
|
|
96
|
+
block = _read_graphrag_block(state_dir)
|
|
97
|
+
store_type = str((block or {}).get("store_type") or "simple").lower()
|
|
98
|
+
|
|
99
|
+
if store_type != "kuzu":
|
|
100
|
+
console.print(
|
|
101
|
+
"[yellow]Graph restore only applies to the kuzu backend "
|
|
102
|
+
f"(configured store_type={store_type!r}). Nothing to do.[/yellow]"
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
# --- Guard: server must be stopped ------------------------------------ #
|
|
107
|
+
if _server_is_running(state_dir):
|
|
108
|
+
console.print(
|
|
109
|
+
"[red]Stop the server first (`agent-brain stop`) — restore "
|
|
110
|
+
"mutates the kuzu database the server holds open.[/red]"
|
|
111
|
+
)
|
|
112
|
+
raise SystemExit(1)
|
|
113
|
+
|
|
114
|
+
# --- Build the graph store manager ------------------------------------ #
|
|
115
|
+
graph_dir = _graph_index_dir(state_dir)
|
|
116
|
+
if GraphStoreManager is None: # pragma: no cover
|
|
117
|
+
console.print(
|
|
118
|
+
"[red]Cannot import agent-brain-server. Install it with "
|
|
119
|
+
"`uv pip install agent-brain-rag`.[/red]"
|
|
120
|
+
)
|
|
121
|
+
raise SystemExit(1)
|
|
122
|
+
|
|
123
|
+
mgr = GraphStoreManager(graph_dir, "kuzu")
|
|
124
|
+
|
|
125
|
+
# --- Preview (plan) --------------------------------------------------- #
|
|
126
|
+
plan = mgr.plan_restore(snapshot_path)
|
|
127
|
+
if plan is None:
|
|
128
|
+
console.print(
|
|
129
|
+
"[yellow]No snapshot available to restore at "
|
|
130
|
+
f"{graph_dir / 'snapshots'}.[/yellow]"
|
|
131
|
+
)
|
|
132
|
+
raise SystemExit(1)
|
|
133
|
+
|
|
134
|
+
snap_file, triplet_count = plan
|
|
135
|
+
kuzu_db_path = graph_dir / "kuzu_db"
|
|
136
|
+
console.print(
|
|
137
|
+
f"Will restore [bold]{triplet_count}[/bold] triplets from "
|
|
138
|
+
f"[cyan]{snap_file.name}[/cyan] into [cyan]{kuzu_db_path}[/cyan]."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if dry_run:
|
|
142
|
+
console.print("(dry run — nothing changed)")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# --- Interactive confirmation ------------------------------------------ #
|
|
146
|
+
if not assume_yes:
|
|
147
|
+
confirmed = click.confirm("Proceed with restore?", default=False)
|
|
148
|
+
if not confirmed:
|
|
149
|
+
console.print("Aborted.")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# --- Execute restore --------------------------------------------------- #
|
|
153
|
+
restored = mgr.restore_from_snapshot(snapshot_path)
|
|
154
|
+
console.print(f"Restored [bold]{restored}[/bold] triplets.")
|
|
@@ -29,6 +29,15 @@ from agent_brain_cli.config import (
|
|
|
29
29
|
resolve_project_root_with_strategy,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
# agent-brain-rag is a declared dependency of agent-brain-cli. Import at module
|
|
33
|
+
# level so apply_safe_fixes can be patched cleanly in tests.
|
|
34
|
+
try:
|
|
35
|
+
from agent_brain_server.storage.graph_store import (
|
|
36
|
+
GraphStoreManager as GraphStoreManager,
|
|
37
|
+
)
|
|
38
|
+
except ImportError: # pragma: no cover
|
|
39
|
+
GraphStoreManager = None
|
|
40
|
+
|
|
32
41
|
#: Severity returned by every diagnostic check.
|
|
33
42
|
SEVERITY_OK = "ok"
|
|
34
43
|
SEVERITY_WARN = "warn"
|
|
@@ -463,6 +472,130 @@ def _check_graph_store_health(state_dir: Path) -> CheckResult | None:
|
|
|
463
472
|
)
|
|
464
473
|
|
|
465
474
|
|
|
475
|
+
def _get_live_kuzu_relationship_count(graph_dir: Path) -> int | None:
|
|
476
|
+
"""Open the kuzu DB read-only and return the live relationship count.
|
|
477
|
+
|
|
478
|
+
Returns ``None`` on any failure (not-installed, corrupt, etc.) — the
|
|
479
|
+
caller treats a None as "can't determine" and skips the staleness check.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
graph_dir: The graph_index directory containing ``kuzu_db``.
|
|
483
|
+
"""
|
|
484
|
+
try:
|
|
485
|
+
import kuzu # noqa: PLC0415
|
|
486
|
+
except ImportError:
|
|
487
|
+
return None
|
|
488
|
+
kuzu_db_path = graph_dir / "kuzu_db"
|
|
489
|
+
try:
|
|
490
|
+
db = kuzu.Database(str(kuzu_db_path), read_only=True)
|
|
491
|
+
conn = kuzu.Connection(db)
|
|
492
|
+
result = conn.execute("MATCH ()-[r]->() RETURN COUNT(r)")
|
|
493
|
+
row = result.get_next()
|
|
494
|
+
return int(row[0]) if row else 0
|
|
495
|
+
except (IndexError, RuntimeError, OSError, TypeError, Exception): # noqa: BLE001
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _check_graph_staleness(state_dir: Path) -> CheckResult | None:
|
|
500
|
+
"""Warn when the live kuzu graph appears STALE (snapshot > live contents).
|
|
501
|
+
|
|
502
|
+
This check is SEPARATE from ``_check_graph_store_health`` — that one
|
|
503
|
+
verifies the DB opens without corruption; this one verifies the live
|
|
504
|
+
relationship count is not significantly below the latest snapshot count,
|
|
505
|
+
which signals an ``AGENT_BRAIN_JOB_TIMEOUT`` rollback left the graph
|
|
506
|
+
partially empty.
|
|
507
|
+
|
|
508
|
+
Returns ``None`` when:
|
|
509
|
+
- GraphRAG is not configured or store_type != "kuzu"
|
|
510
|
+
- The kuzu DB does not exist on disk yet
|
|
511
|
+
- No snapshot exists (nothing to compare against)
|
|
512
|
+
- The live COUNT query fails (could be a format change; don't FAIL here)
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
state_dir: Project state directory.
|
|
516
|
+
"""
|
|
517
|
+
block = _read_graphrag_block(state_dir)
|
|
518
|
+
if not block or not block.get("enabled"):
|
|
519
|
+
return None
|
|
520
|
+
store_type = str(block.get("store_type") or "simple").lower()
|
|
521
|
+
if store_type != "kuzu":
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
graph_dir = _graph_index_dir(state_dir)
|
|
525
|
+
kuzu_db_path = graph_dir / "kuzu_db"
|
|
526
|
+
if not kuzu_db_path.exists():
|
|
527
|
+
return None
|
|
528
|
+
|
|
529
|
+
# Check that at least one snapshot exists.
|
|
530
|
+
try:
|
|
531
|
+
from agent_brain_server.storage.graph_snapshot import ( # noqa: PLC0415
|
|
532
|
+
GraphSnapshotManager,
|
|
533
|
+
)
|
|
534
|
+
except ImportError:
|
|
535
|
+
# Server package not installed — fall back to manual JSON scan.
|
|
536
|
+
snap_dir = graph_dir / "snapshots"
|
|
537
|
+
if not snap_dir.is_dir() or not any(snap_dir.iterdir()):
|
|
538
|
+
return None
|
|
539
|
+
# Best-effort: pick the newest file and count triplets.
|
|
540
|
+
candidates = sorted(
|
|
541
|
+
(p for p in snap_dir.iterdir() if p.is_file() and p.suffix == ".json"),
|
|
542
|
+
key=lambda p: (p.stat().st_mtime, p.name),
|
|
543
|
+
reverse=True,
|
|
544
|
+
)
|
|
545
|
+
snapshot_count = 0
|
|
546
|
+
for snap in candidates:
|
|
547
|
+
try:
|
|
548
|
+
payload = json.loads(snap.read_text())
|
|
549
|
+
snapshot_count = len(payload.get("triplets") or [])
|
|
550
|
+
break
|
|
551
|
+
except (OSError, json.JSONDecodeError):
|
|
552
|
+
continue
|
|
553
|
+
else:
|
|
554
|
+
snap_mgr = GraphSnapshotManager(graph_dir)
|
|
555
|
+
if snap_mgr.latest() is None:
|
|
556
|
+
return None
|
|
557
|
+
loaded = snap_mgr.load_latest_valid()
|
|
558
|
+
if loaded is None:
|
|
559
|
+
return None
|
|
560
|
+
_snap_path, triplets = loaded
|
|
561
|
+
snapshot_count = len(triplets)
|
|
562
|
+
|
|
563
|
+
if snapshot_count == 0:
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
live_count = _get_live_kuzu_relationship_count(graph_dir)
|
|
567
|
+
if live_count is None:
|
|
568
|
+
# Can't determine live count — skip rather than raise a false alarm.
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
if snapshot_count > live_count:
|
|
572
|
+
return CheckResult(
|
|
573
|
+
"graph_staleness",
|
|
574
|
+
SEVERITY_WARN,
|
|
575
|
+
f"Live kuzu graph has {live_count} relationships but the latest "
|
|
576
|
+
f"snapshot has {snapshot_count} — the graph looks stale (likely an "
|
|
577
|
+
"AGENT_BRAIN_JOB_TIMEOUT rollback).",
|
|
578
|
+
fix=(
|
|
579
|
+
"Run `agent-brain graph restore-from-snapshot` (or "
|
|
580
|
+
"`agent-brain doctor --fix`) to replay the snapshot."
|
|
581
|
+
),
|
|
582
|
+
details={
|
|
583
|
+
"live_relationships": live_count,
|
|
584
|
+
"snapshot_triplets": snapshot_count,
|
|
585
|
+
},
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
return CheckResult(
|
|
589
|
+
"graph_staleness",
|
|
590
|
+
SEVERITY_OK,
|
|
591
|
+
"Live kuzu graph is consistent with the latest snapshot.",
|
|
592
|
+
details={
|
|
593
|
+
"live_relationships": live_count,
|
|
594
|
+
"snapshot_triplets": snapshot_count,
|
|
595
|
+
},
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
466
599
|
def _server_is_running(state_dir: Path) -> bool:
|
|
467
600
|
"""Best-effort check for an active server lock under state_dir."""
|
|
468
601
|
return (state_dir / "server.lock").exists() or (state_dir / "lock").exists()
|
|
@@ -538,6 +671,11 @@ def run_doctor(server_url_override: str | None = None) -> DoctorReport:
|
|
|
538
671
|
if graph_check is not None:
|
|
539
672
|
checks.append(graph_check)
|
|
540
673
|
|
|
674
|
+
# Phase 64 GSTAB-02 — warn when live kuzu is stale (snapshot > live count).
|
|
675
|
+
stale_check = _check_graph_staleness(state_dir)
|
|
676
|
+
if stale_check is not None:
|
|
677
|
+
checks.append(stale_check)
|
|
678
|
+
|
|
541
679
|
checks.append(_check_gitignore(project_root))
|
|
542
680
|
|
|
543
681
|
checks.append(_check_server(server_url, runtime_file))
|
|
@@ -627,6 +765,34 @@ def apply_safe_fixes(report: DoctorReport) -> list[str]:
|
|
|
627
765
|
"start empty. Re-run `agent-brain index <folder>` to "
|
|
628
766
|
"rebuild."
|
|
629
767
|
)
|
|
768
|
+
elif check.name == "graph_staleness" and check.status == SEVERITY_WARN:
|
|
769
|
+
# Phase 64 GSTAB-02 — restore a stale (but non-corrupt) kuzu graph.
|
|
770
|
+
if _server_is_running(state_dir):
|
|
771
|
+
actions.append(
|
|
772
|
+
"Skipped graph restore: server appears to be running. "
|
|
773
|
+
"Stop it with `agent-brain stop` first, then re-run "
|
|
774
|
+
"`agent-brain doctor --fix`."
|
|
775
|
+
)
|
|
776
|
+
continue
|
|
777
|
+
graph_dir = _graph_index_dir(state_dir)
|
|
778
|
+
if GraphStoreManager is None: # pragma: no cover
|
|
779
|
+
actions.append(
|
|
780
|
+
"Could not import agent-brain-server to perform restore. "
|
|
781
|
+
"Run `agent-brain graph restore-from-snapshot` manually."
|
|
782
|
+
)
|
|
783
|
+
continue
|
|
784
|
+
try:
|
|
785
|
+
mgr = GraphStoreManager(graph_dir, "kuzu")
|
|
786
|
+
restored_count = mgr.restore_from_snapshot(None)
|
|
787
|
+
actions.append(
|
|
788
|
+
f"Restored {restored_count} triplets from the latest "
|
|
789
|
+
f"snapshot into {graph_dir}."
|
|
790
|
+
)
|
|
791
|
+
except Exception as exc: # noqa: BLE001
|
|
792
|
+
actions.append(
|
|
793
|
+
f"Graph restore failed: {exc}. "
|
|
794
|
+
"Run `agent-brain graph restore-from-snapshot` manually."
|
|
795
|
+
)
|
|
630
796
|
return actions
|
|
631
797
|
|
|
632
798
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "agent-brain-cli"
|
|
3
|
-
version = "10.
|
|
3
|
+
version = "10.4.0"
|
|
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.
|
|
30
|
+
agent-brain-rag = "^10.4.0"
|
|
31
31
|
agent-brain-uds = "^10.3.0"
|
|
32
32
|
psutil = "^6.0"
|
|
33
33
|
|
|
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
|
|
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.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/claude_converter.py
RENAMED
|
File without changes
|
{agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/codex_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/gemini_converter.py
RENAMED
|
File without changes
|
{agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/opencode_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_brain_cli-10.3.1 → agent_brain_cli-10.4.0}/agent_brain_cli/runtime/skill_runtime_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|