agent-brain-cli 10.3.2__tar.gz → 10.5.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.2 → agent_brain_cli-10.5.0}/PKG-INFO +3 -2
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/__init__.py +1 -1
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/cli.py +4 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/client/transport.py +8 -1
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/__init__.py +2 -0
- agent_brain_cli-10.5.0/agent_brain_cli/commands/graph.py +154 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/install_agent.py +158 -32
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/diagnostics.py +166 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/__init__.py +0 -2
- agent_brain_cli-10.5.0/agent_brain_cli/runtime/mcp_registration.py +316 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/tool_maps.py +1 -16
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/types.py +0 -1
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/pyproject.toml +3 -2
- agent_brain_cli-10.3.2/agent_brain_cli/runtime/gemini_converter.py +0 -119
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/README.md +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/__main__.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/client/__init__.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/client/api_client.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/client/protocol.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/cache.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/config.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/doctor.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/folders.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/index.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/init.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/inject.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/jobs.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/list_cmd.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/mcp.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/prompt.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/query.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/reset.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/resources.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/start.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/status.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/stop.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/types.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/commands/uninstall.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/config.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/config_migrate.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/config_schema.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/mcp_runtime.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/migration.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/claude_converter.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/codex_converter.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/converter_base.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/opencode_converter.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/parser.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.0}/agent_brain_cli/runtime/skill_runtime_converter.py +0 -0
- {agent_brain_cli-10.3.2 → agent_brain_cli-10.5.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.5.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)
|
|
@@ -23,6 +23,7 @@ Requires-Dist: psutil (>=6.0,<7.0)
|
|
|
23
23
|
Requires-Dist: pydantic (>=2.10.0,<3.0.0)
|
|
24
24
|
Requires-Dist: pyyaml (>=6.0.0,<7.0.0)
|
|
25
25
|
Requires-Dist: rich (>=13.9.0,<14.0.0)
|
|
26
|
+
Requires-Dist: tomlkit (>=0.13.0,<0.14.0)
|
|
26
27
|
Project-URL: Documentation, https://github.com/SpillwaveSolutions/agent-brain/wiki
|
|
27
28
|
Project-URL: Repository, https://github.com/SpillwaveSolutions/agent-brain
|
|
28
29
|
Description-Content-Type: text/markdown
|
|
@@ -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")
|
|
@@ -186,7 +186,14 @@ def open_backend(ctx: click.Context, *, timeout: float = 30.0) -> BackendClient:
|
|
|
186
186
|
# mcp_transport == "http" — resolve_mcp_transport guarantees
|
|
187
187
|
# mcp_target is not None for the http branch.
|
|
188
188
|
assert mcp_target is not None # noqa: S101
|
|
189
|
-
|
|
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
|
+
)
|
|
190
197
|
|
|
191
198
|
# --- HTTP / UDS branch (existing v1/v2 path) -----------------
|
|
192
199
|
transport, target = resolve_transport(
|
|
@@ -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.")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Install-agent command for installing runtime-specific plugin files."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Callable
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
@@ -10,7 +12,12 @@ from rich.panel import Panel
|
|
|
10
12
|
|
|
11
13
|
from agent_brain_cli.runtime.claude_converter import ClaudeConverter
|
|
12
14
|
from agent_brain_cli.runtime.codex_converter import CodexConverter
|
|
13
|
-
from agent_brain_cli.runtime.
|
|
15
|
+
from agent_brain_cli.runtime.mcp_registration import (
|
|
16
|
+
McpRegistrationResult,
|
|
17
|
+
register_claude_mcp,
|
|
18
|
+
register_codex_mcp,
|
|
19
|
+
register_opencode_mcp,
|
|
20
|
+
)
|
|
14
21
|
from agent_brain_cli.runtime.opencode_converter import OpenCodeConverter
|
|
15
22
|
from agent_brain_cli.runtime.parser import parse_plugin_dir
|
|
16
23
|
from agent_brain_cli.runtime.skill_runtime_converter import SkillRuntimeConverter
|
|
@@ -28,10 +35,6 @@ INSTALL_DIRS: dict[str, dict[str, str]] = {
|
|
|
28
35
|
"project": ".opencode/plugins/agent-brain",
|
|
29
36
|
"global": "~/.config/opencode/plugins/agent-brain",
|
|
30
37
|
},
|
|
31
|
-
"gemini": {
|
|
32
|
-
"project": ".gemini/plugins/agent-brain",
|
|
33
|
-
"global": "~/.config/gemini/plugins/agent-brain",
|
|
34
|
-
},
|
|
35
38
|
"codex": {
|
|
36
39
|
"project": ".codex/skills/agent-brain",
|
|
37
40
|
"global": "~/.codex/skills/agent-brain",
|
|
@@ -42,17 +45,12 @@ INSTALL_DIRS: dict[str, dict[str, str]] = {
|
|
|
42
45
|
DIR_REQUIRED_RUNTIMES = {"skill-runtime"}
|
|
43
46
|
|
|
44
47
|
ConverterType = type[
|
|
45
|
-
ClaudeConverter
|
|
46
|
-
| OpenCodeConverter
|
|
47
|
-
| GeminiConverter
|
|
48
|
-
| SkillRuntimeConverter
|
|
49
|
-
| CodexConverter
|
|
48
|
+
ClaudeConverter | OpenCodeConverter | SkillRuntimeConverter | CodexConverter
|
|
50
49
|
]
|
|
51
50
|
|
|
52
51
|
CONVERTERS: dict[str, ConverterType] = {
|
|
53
52
|
"claude": ClaudeConverter,
|
|
54
53
|
"opencode": OpenCodeConverter,
|
|
55
|
-
"gemini": GeminiConverter,
|
|
56
54
|
"skill-runtime": SkillRuntimeConverter,
|
|
57
55
|
"codex": CodexConverter,
|
|
58
56
|
}
|
|
@@ -94,7 +92,89 @@ def _resolve_target_dir(
|
|
|
94
92
|
return project_root / dir_template
|
|
95
93
|
|
|
96
94
|
|
|
97
|
-
RUNTIME_CHOICES = ["claude", "opencode", "
|
|
95
|
+
RUNTIME_CHOICES = ["claude", "opencode", "skill-runtime", "codex"]
|
|
96
|
+
|
|
97
|
+
# Runtimes for which we can auto-register the MCP server today, mapped to the
|
|
98
|
+
# writer that knows that runtime's config schema. All writers share the
|
|
99
|
+
# ``(config_path, state_dir, *, backend, auth, dry_run)`` signature.
|
|
100
|
+
MCP_REGISTRARS: dict[str, Callable[..., McpRegistrationResult]] = {
|
|
101
|
+
"claude": register_claude_mcp,
|
|
102
|
+
"opencode": register_opencode_mcp,
|
|
103
|
+
"codex": register_codex_mcp,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _codex_config_path() -> Path:
|
|
108
|
+
"""Return Codex's MCP config file, honoring ``$CODEX_HOME``."""
|
|
109
|
+
codex_home = os.environ.get("CODEX_HOME")
|
|
110
|
+
base = Path(codex_home) if codex_home else Path.home() / ".codex"
|
|
111
|
+
return base / "config.toml"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _resolve_mcp_paths(
|
|
115
|
+
agent: str, scope: str, project_root: Path | None
|
|
116
|
+
) -> tuple[Path, Path]:
|
|
117
|
+
"""Return (config_path, state_dir) for MCP registration.
|
|
118
|
+
|
|
119
|
+
The config file follows each runtime's own discovery rules:
|
|
120
|
+
|
|
121
|
+
* **claude** — project ``.mcp.json`` at the root, global ``~/.claude.json``.
|
|
122
|
+
* **opencode** — project ``opencode.json`` at the root (highest-precedence
|
|
123
|
+
project config), global ``~/.config/opencode/opencode.json``.
|
|
124
|
+
* **codex** — always ``$CODEX_HOME/config.toml`` (default
|
|
125
|
+
``~/.codex/config.toml``); Codex has no project-level MCP config, so the
|
|
126
|
+
file is shared and the project's ``.agent-brain`` is pinned via the entry.
|
|
127
|
+
"""
|
|
128
|
+
root = project_root if project_root is not None else Path.cwd()
|
|
129
|
+
if agent == "opencode":
|
|
130
|
+
if scope == "global":
|
|
131
|
+
config = Path.home() / ".config" / "opencode" / "opencode.json"
|
|
132
|
+
return config, Path.home() / ".agent-brain"
|
|
133
|
+
return root / "opencode.json", root / ".agent-brain"
|
|
134
|
+
if agent == "codex":
|
|
135
|
+
state_dir = (
|
|
136
|
+
Path.home() / ".agent-brain" if scope == "global" else root / ".agent-brain"
|
|
137
|
+
)
|
|
138
|
+
return _codex_config_path(), state_dir
|
|
139
|
+
# claude (and any future mcpServers-style runtime)
|
|
140
|
+
if scope == "global":
|
|
141
|
+
return Path.home() / ".claude.json", Path.home() / ".agent-brain"
|
|
142
|
+
return root / ".mcp.json", root / ".agent-brain"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _register_mcp(
|
|
146
|
+
agent: str,
|
|
147
|
+
scope: str,
|
|
148
|
+
project_root: Path | None,
|
|
149
|
+
mcp_auth: str,
|
|
150
|
+
mcp_backend: str,
|
|
151
|
+
dry_run: bool,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""Register the MCP server for supported runtimes; report what happened."""
|
|
154
|
+
registrar = MCP_REGISTRARS.get(agent)
|
|
155
|
+
if registrar is None:
|
|
156
|
+
return {
|
|
157
|
+
"skipped": True,
|
|
158
|
+
"reason": (
|
|
159
|
+
f"MCP auto-registration is currently supported only for "
|
|
160
|
+
f"{', '.join(sorted(MCP_REGISTRARS))}; configure "
|
|
161
|
+
f"{agent} manually (see the configuring-agent-brain skill)."
|
|
162
|
+
),
|
|
163
|
+
}
|
|
164
|
+
config_path, state_dir = _resolve_mcp_paths(agent, scope, project_root)
|
|
165
|
+
result = registrar(
|
|
166
|
+
config_path,
|
|
167
|
+
state_dir,
|
|
168
|
+
backend=mcp_backend,
|
|
169
|
+
auth=mcp_auth,
|
|
170
|
+
dry_run=dry_run,
|
|
171
|
+
)
|
|
172
|
+
return {
|
|
173
|
+
"skipped": False,
|
|
174
|
+
"action": result.action,
|
|
175
|
+
"path": str(result.path),
|
|
176
|
+
"server_name": result.server_name,
|
|
177
|
+
}
|
|
98
178
|
|
|
99
179
|
|
|
100
180
|
@click.command("install-agent")
|
|
@@ -146,6 +226,23 @@ RUNTIME_CHOICES = ["claude", "opencode", "gemini", "skill-runtime", "codex"]
|
|
|
146
226
|
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
147
227
|
help="Project path for --project scope (default: cwd)",
|
|
148
228
|
)
|
|
229
|
+
@click.option(
|
|
230
|
+
"--with-mcp",
|
|
231
|
+
is_flag=True,
|
|
232
|
+
help="Also register the agent-brain MCP server (Claude Code, OpenCode, Codex)",
|
|
233
|
+
)
|
|
234
|
+
@click.option(
|
|
235
|
+
"--mcp-auth",
|
|
236
|
+
type=click.Choice(["none", "oauth"]),
|
|
237
|
+
default="none",
|
|
238
|
+
help="MCP client auth mode written into the registration (default: none)",
|
|
239
|
+
)
|
|
240
|
+
@click.option(
|
|
241
|
+
"--mcp-backend",
|
|
242
|
+
type=click.Choice(["auto", "uds", "http"]),
|
|
243
|
+
default="auto",
|
|
244
|
+
help="How the MCP server reaches agent-brain-serve (default: auto)",
|
|
245
|
+
)
|
|
149
246
|
def install_agent_command(
|
|
150
247
|
agent: str,
|
|
151
248
|
scope: str,
|
|
@@ -154,6 +251,9 @@ def install_agent_command(
|
|
|
154
251
|
dry_run: bool,
|
|
155
252
|
json_output: bool,
|
|
156
253
|
path: str | None,
|
|
254
|
+
with_mcp: bool,
|
|
255
|
+
mcp_auth: str,
|
|
256
|
+
mcp_backend: str,
|
|
157
257
|
) -> None:
|
|
158
258
|
"""Install Agent Brain plugin for a specific runtime.
|
|
159
259
|
|
|
@@ -164,7 +264,7 @@ def install_agent_command(
|
|
|
164
264
|
Examples:
|
|
165
265
|
agent-brain install-agent --agent claude --project
|
|
166
266
|
agent-brain install-agent --agent opencode --global
|
|
167
|
-
agent-brain install-agent --agent
|
|
267
|
+
agent-brain install-agent --agent claude --dry-run
|
|
168
268
|
agent-brain install-agent --agent skill-runtime --dir ./my-skills
|
|
169
269
|
agent-brain install-agent --agent codex
|
|
170
270
|
"""
|
|
@@ -220,6 +320,12 @@ def install_agent_command(
|
|
|
220
320
|
converter = converter_cls()
|
|
221
321
|
scope_enum = Scope.GLOBAL if scope == "global" else Scope.PROJECT
|
|
222
322
|
|
|
323
|
+
mcp_summary: dict[str, Any] | None = None
|
|
324
|
+
if with_mcp:
|
|
325
|
+
mcp_summary = _register_mcp(
|
|
326
|
+
agent, scope, project_root, mcp_auth, mcp_backend, dry_run
|
|
327
|
+
)
|
|
328
|
+
|
|
223
329
|
if dry_run:
|
|
224
330
|
_handle_dry_run(
|
|
225
331
|
converter,
|
|
@@ -229,6 +335,7 @@ def install_agent_command(
|
|
|
229
335
|
agent,
|
|
230
336
|
scope,
|
|
231
337
|
json_output,
|
|
338
|
+
mcp_summary,
|
|
232
339
|
)
|
|
233
340
|
return
|
|
234
341
|
|
|
@@ -250,6 +357,8 @@ def install_agent_command(
|
|
|
250
357
|
"files_created": len(files),
|
|
251
358
|
"source_dir": str(source),
|
|
252
359
|
}
|
|
360
|
+
if mcp_summary is not None:
|
|
361
|
+
result["mcp_registration"] = mcp_summary
|
|
253
362
|
click.echo(json.dumps(result, indent=2))
|
|
254
363
|
else:
|
|
255
364
|
console.print(
|
|
@@ -263,6 +372,7 @@ def install_agent_command(
|
|
|
263
372
|
border_style="green",
|
|
264
373
|
)
|
|
265
374
|
)
|
|
375
|
+
_print_mcp_summary(mcp_summary)
|
|
266
376
|
|
|
267
377
|
except SystemExit:
|
|
268
378
|
raise
|
|
@@ -276,11 +386,7 @@ def install_agent_command(
|
|
|
276
386
|
|
|
277
387
|
def _handle_dry_run(
|
|
278
388
|
converter: (
|
|
279
|
-
ClaudeConverter
|
|
280
|
-
| OpenCodeConverter
|
|
281
|
-
| GeminiConverter
|
|
282
|
-
| SkillRuntimeConverter
|
|
283
|
-
| CodexConverter
|
|
389
|
+
ClaudeConverter | OpenCodeConverter | SkillRuntimeConverter | CodexConverter
|
|
284
390
|
),
|
|
285
391
|
bundle: Any,
|
|
286
392
|
target: Path,
|
|
@@ -288,18 +394,26 @@ def _handle_dry_run(
|
|
|
288
394
|
agent: str,
|
|
289
395
|
scope: str,
|
|
290
396
|
json_output: bool,
|
|
397
|
+
mcp_summary: dict[str, Any] | None = None,
|
|
291
398
|
) -> None:
|
|
292
399
|
"""Handle dry-run mode: simulate install in temp dir."""
|
|
293
400
|
import tempfile
|
|
294
401
|
|
|
295
402
|
with tempfile.TemporaryDirectory() as tmp:
|
|
296
|
-
tmp_target = Path(tmp)
|
|
297
403
|
# For Codex, pass tmp as project_root so AGENTS.md lands in tmpdir
|
|
298
404
|
if isinstance(converter, CodexConverter):
|
|
405
|
+
tmp_target = Path(tmp)
|
|
299
406
|
files = converter.install(
|
|
300
407
|
bundle, tmp_target, scope_enum, project_root=Path(tmp)
|
|
301
408
|
)
|
|
302
409
|
else:
|
|
410
|
+
# Mirror the real target's full structure under tmp so converters
|
|
411
|
+
# that write *outside* target_dir stay inside the sandbox — e.g.
|
|
412
|
+
# OpenCode writes opencode.json at target_dir.parent.parent, which
|
|
413
|
+
# escapes to an unwritable ancestor (CI: "/") if the temp target is
|
|
414
|
+
# shallow. Rooting at tmp + the target's relative path keeps every
|
|
415
|
+
# parent the converter computes inside the throwaway dir.
|
|
416
|
+
tmp_target = Path(tmp) / target.relative_to(target.anchor)
|
|
303
417
|
files = converter.install(bundle, tmp_target, scope_enum)
|
|
304
418
|
# Remap paths to real target
|
|
305
419
|
planned: list[Path] = []
|
|
@@ -311,19 +425,17 @@ def _handle_dry_run(
|
|
|
311
425
|
planned.append(f)
|
|
312
426
|
|
|
313
427
|
if json_output:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
)
|
|
326
|
-
)
|
|
428
|
+
payload: dict[str, Any] = {
|
|
429
|
+
"dry_run": True,
|
|
430
|
+
"agent": agent,
|
|
431
|
+
"scope": scope,
|
|
432
|
+
"target_dir": str(target),
|
|
433
|
+
"files": [str(f) for f in planned],
|
|
434
|
+
"file_count": len(planned),
|
|
435
|
+
}
|
|
436
|
+
if mcp_summary is not None:
|
|
437
|
+
payload["mcp_registration"] = mcp_summary
|
|
438
|
+
click.echo(json.dumps(payload, indent=2))
|
|
327
439
|
else:
|
|
328
440
|
console.print(
|
|
329
441
|
Panel(
|
|
@@ -338,3 +450,17 @@ def _handle_dry_run(
|
|
|
338
450
|
)
|
|
339
451
|
for f in planned:
|
|
340
452
|
console.print(f" [dim]{f}[/]")
|
|
453
|
+
_print_mcp_summary(mcp_summary)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _print_mcp_summary(mcp_summary: dict[str, Any] | None) -> None:
|
|
457
|
+
"""Render the MCP registration outcome for human-readable output."""
|
|
458
|
+
if mcp_summary is None:
|
|
459
|
+
return
|
|
460
|
+
if mcp_summary.get("skipped"):
|
|
461
|
+
console.print(f"[yellow]MCP:[/] {mcp_summary['reason']}")
|
|
462
|
+
return
|
|
463
|
+
console.print(
|
|
464
|
+
f"[green]MCP server registered[/] ([bold]{mcp_summary['action']}[/]) "
|
|
465
|
+
f"→ {mcp_summary['path']}"
|
|
466
|
+
)
|