rebob 0.1.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.
- rebob-0.1.0/PKG-INFO +87 -0
- rebob-0.1.0/README.md +73 -0
- rebob-0.1.0/pyproject.toml +35 -0
- rebob-0.1.0/rebob/__init__.py +1 -0
- rebob-0.1.0/rebob/__main__.py +6 -0
- rebob-0.1.0/rebob/cli.py +69 -0
- rebob-0.1.0/rebob/commands/__init__.py +0 -0
- rebob-0.1.0/rebob/commands/_util.py +204 -0
- rebob-0.1.0/rebob/commands/doctor.py +125 -0
- rebob-0.1.0/rebob/commands/init.py +120 -0
- rebob-0.1.0/rebob/commands/path.py +11 -0
- rebob-0.1.0/rebob/commands/serve.py +18 -0
- rebob-0.1.0/rebob/config.py +57 -0
- rebob-0.1.0/rebob/contract.py +66 -0
- rebob-0.1.0/rebob/core/__init__.py +1 -0
- rebob-0.1.0/rebob/core/api.py +188 -0
- rebob-0.1.0/rebob/core/assemble.py +50 -0
- rebob-0.1.0/rebob/core/extract.py +224 -0
- rebob-0.1.0/rebob/core/redact.py +193 -0
- rebob-0.1.0/rebob/core/resolve.py +154 -0
- rebob-0.1.0/rebob/core/retrieve.py +338 -0
- rebob-0.1.0/rebob/core/salience.py +124 -0
- rebob-0.1.0/rebob/core/store.py +318 -0
- rebob-0.1.0/rebob/core/watsonx.py +184 -0
- rebob-0.1.0/rebob/core/worker.py +145 -0
- rebob-0.1.0/rebob/hook.py +68 -0
- rebob-0.1.0/rebob/paths.py +160 -0
- rebob-0.1.0/rebob/server.py +80 -0
- rebob-0.1.0/rebob/templates/SKILL.md +13 -0
- rebob-0.1.0/rebob/templates/mem.md +13 -0
- rebob-0.1.0/rebob/templates/rules.md +9 -0
- rebob-0.1.0/rebob.egg-info/PKG-INFO +87 -0
- rebob-0.1.0/rebob.egg-info/SOURCES.txt +49 -0
- rebob-0.1.0/rebob.egg-info/dependency_links.txt +1 -0
- rebob-0.1.0/rebob.egg-info/entry_points.txt +2 -0
- rebob-0.1.0/rebob.egg-info/requires.txt +8 -0
- rebob-0.1.0/rebob.egg-info/top_level.txt +1 -0
- rebob-0.1.0/setup.cfg +4 -0
- rebob-0.1.0/tests/test_api.py +204 -0
- rebob-0.1.0/tests/test_assemble.py +104 -0
- rebob-0.1.0/tests/test_contract.py +38 -0
- rebob-0.1.0/tests/test_extract.py +95 -0
- rebob-0.1.0/tests/test_hook.py +172 -0
- rebob-0.1.0/tests/test_paths.py +135 -0
- rebob-0.1.0/tests/test_pipeline.py +88 -0
- rebob-0.1.0/tests/test_redact.py +112 -0
- rebob-0.1.0/tests/test_resolve.py +88 -0
- rebob-0.1.0/tests/test_retrieve.py +305 -0
- rebob-0.1.0/tests/test_salience.py +167 -0
- rebob-0.1.0/tests/test_store.py +121 -0
- rebob-0.1.0/tests/test_watsonx.py +79 -0
rebob-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rebob
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Persistent memory for IBM Bob 2.0
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastmcp>=2.0
|
|
8
|
+
Requires-Dist: python-dotenv>=1.0
|
|
9
|
+
Requires-Dist: ibm-watsonx-ai<2,>=1.7
|
|
10
|
+
Requires-Dist: numpy>=1.24
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# ReBOB — Memory for IBM Bob
|
|
16
|
+
|
|
17
|
+
ReBOB gives [IBM Bob](https://www.ibm.com/products/watson) persistent memory across conversations.
|
|
18
|
+
It captures, indexes, and retrieves context so Bob can remember what matters.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -m venv .venv
|
|
24
|
+
.venv\Scripts\activate # Windows
|
|
25
|
+
# source .venv/bin/activate # macOS/Linux
|
|
26
|
+
|
|
27
|
+
pip install -e . # from a source checkout
|
|
28
|
+
# or: pip install git+https://github.com/<org>/ReBOB.git
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Set up a project
|
|
32
|
+
|
|
33
|
+
From inside the project you want Bob to remember things about:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
rebob init
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This writes `.env` (watsonx credentials), `.bob/mcp.json`, `.bob/settings.json`, and the `/mem`
|
|
40
|
+
command/rules templates. Non-interactive (for scripting or CI):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
rebob init --non-interactive --api-key <key> --project-id <id>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then verify everything resolved correctly:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
rebob doctor
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`rebob doctor` prints every path ReBOB resolved (project root, data directory, `.env`, etc.) —
|
|
53
|
+
if something looks wrong, that's the first place to look. `rebob doctor --fix` rewrites stale
|
|
54
|
+
Bob config (e.g. after moving the project or reinstalling the venv).
|
|
55
|
+
|
|
56
|
+
`rebob path` prints the same resolved paths as JSON, useful for scripting.
|
|
57
|
+
|
|
58
|
+
## How path resolution works
|
|
59
|
+
|
|
60
|
+
ReBOB never hardcodes `.rebob/` relative to the package install location — that broke under
|
|
61
|
+
`pip install` (data was written inside `site-packages/`). Instead, in priority order:
|
|
62
|
+
|
|
63
|
+
1. `REBOB_HOME` env var, if set (used verbatim as the data directory)
|
|
64
|
+
2. `REBOB_PROJECT_ROOT` env var, if set (data dir becomes `$REBOB_PROJECT_ROOT/.rebob`)
|
|
65
|
+
3. Walk upward from the current directory looking for an existing `.rebob/`, then `.bob/`,
|
|
66
|
+
then `pyproject.toml`, then `.git/` — first match wins
|
|
67
|
+
4. Fall back to the current directory
|
|
68
|
+
|
|
69
|
+
`rebob init` sets `REBOB_HOME` explicitly in the MCP server's `env` block, so Bob finds the
|
|
70
|
+
right data directory regardless of what working directory it happens to launch the server with.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e ".[dev]"
|
|
76
|
+
pytest tests/ -v
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Transport
|
|
80
|
+
|
|
81
|
+
Default is `stdio` — Bob spawns a fresh process per tool call, zero setup required. If you hit
|
|
82
|
+
timeouts on cold start (heavy imports, slow disk), switch to a persistent HTTP server:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
rebob init --transport sse --port 8000
|
|
86
|
+
rebob serve --transport sse --port 8000 # keep this running
|
|
87
|
+
```
|
rebob-0.1.0/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# ReBOB — Memory for IBM Bob
|
|
2
|
+
|
|
3
|
+
ReBOB gives [IBM Bob](https://www.ibm.com/products/watson) persistent memory across conversations.
|
|
4
|
+
It captures, indexes, and retrieves context so Bob can remember what matters.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
python -m venv .venv
|
|
10
|
+
.venv\Scripts\activate # Windows
|
|
11
|
+
# source .venv/bin/activate # macOS/Linux
|
|
12
|
+
|
|
13
|
+
pip install -e . # from a source checkout
|
|
14
|
+
# or: pip install git+https://github.com/<org>/ReBOB.git
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Set up a project
|
|
18
|
+
|
|
19
|
+
From inside the project you want Bob to remember things about:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
rebob init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This writes `.env` (watsonx credentials), `.bob/mcp.json`, `.bob/settings.json`, and the `/mem`
|
|
26
|
+
command/rules templates. Non-interactive (for scripting or CI):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
rebob init --non-interactive --api-key <key> --project-id <id>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then verify everything resolved correctly:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
rebob doctor
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`rebob doctor` prints every path ReBOB resolved (project root, data directory, `.env`, etc.) —
|
|
39
|
+
if something looks wrong, that's the first place to look. `rebob doctor --fix` rewrites stale
|
|
40
|
+
Bob config (e.g. after moving the project or reinstalling the venv).
|
|
41
|
+
|
|
42
|
+
`rebob path` prints the same resolved paths as JSON, useful for scripting.
|
|
43
|
+
|
|
44
|
+
## How path resolution works
|
|
45
|
+
|
|
46
|
+
ReBOB never hardcodes `.rebob/` relative to the package install location — that broke under
|
|
47
|
+
`pip install` (data was written inside `site-packages/`). Instead, in priority order:
|
|
48
|
+
|
|
49
|
+
1. `REBOB_HOME` env var, if set (used verbatim as the data directory)
|
|
50
|
+
2. `REBOB_PROJECT_ROOT` env var, if set (data dir becomes `$REBOB_PROJECT_ROOT/.rebob`)
|
|
51
|
+
3. Walk upward from the current directory looking for an existing `.rebob/`, then `.bob/`,
|
|
52
|
+
then `pyproject.toml`, then `.git/` — first match wins
|
|
53
|
+
4. Fall back to the current directory
|
|
54
|
+
|
|
55
|
+
`rebob init` sets `REBOB_HOME` explicitly in the MCP server's `env` block, so Bob finds the
|
|
56
|
+
right data directory regardless of what working directory it happens to launch the server with.
|
|
57
|
+
|
|
58
|
+
## Tests
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install -e ".[dev]"
|
|
62
|
+
pytest tests/ -v
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Transport
|
|
66
|
+
|
|
67
|
+
Default is `stdio` — Bob spawns a fresh process per tool call, zero setup required. If you hit
|
|
68
|
+
timeouts on cold start (heavy imports, slow disk), switch to a persistent HTTP server:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
rebob init --transport sse --port 8000
|
|
72
|
+
rebob serve --transport sse --port 8000 # keep this running
|
|
73
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rebob"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Persistent memory for IBM Bob 2.0"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastmcp>=2.0",
|
|
13
|
+
"python-dotenv>=1.0",
|
|
14
|
+
"ibm-watsonx-ai>=1.7,<2",
|
|
15
|
+
"numpy>=1.24",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.0",
|
|
21
|
+
"build>=1.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
rebob = "rebob.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["."]
|
|
29
|
+
include = ["rebob*"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
rebob = ["templates/*"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
rebob-0.1.0/rebob/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""rebob — command-line entry point (argparse subcommands)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
10
|
+
parser = argparse.ArgumentParser(prog="rebob", description="ReBOB — persistent memory for Bob.")
|
|
11
|
+
sub = parser.add_subparsers(dest="command")
|
|
12
|
+
|
|
13
|
+
p_init = sub.add_parser("init", help="Configure ReBOB in the current project")
|
|
14
|
+
p_init.add_argument("--non-interactive", "--yes", dest="non_interactive", action="store_true")
|
|
15
|
+
p_init.add_argument("--api-key", default="", help="IBM_CLOUD_API_KEY (for --non-interactive)")
|
|
16
|
+
p_init.add_argument("--project-id", default="", help="WATSONX_PROJECT_ID (for --non-interactive)")
|
|
17
|
+
p_init.add_argument("--url", default="", help="WATSONX_URL (for --non-interactive)")
|
|
18
|
+
p_init.add_argument("--transport", choices=["stdio", "sse"], default="stdio")
|
|
19
|
+
p_init.add_argument("--port", type=int, default=8000)
|
|
20
|
+
|
|
21
|
+
p_doctor = sub.add_parser("doctor", help="Diagnose a ReBOB installation")
|
|
22
|
+
p_doctor.add_argument("--fix", action="store_true", help="Rewrite stale Bob config")
|
|
23
|
+
|
|
24
|
+
p_serve = sub.add_parser("serve", help="Run the ReBOB MCP server")
|
|
25
|
+
p_serve.add_argument("--transport", choices=["stdio", "sse"], default="stdio")
|
|
26
|
+
p_serve.add_argument("--port", type=int, default=8000)
|
|
27
|
+
|
|
28
|
+
sub.add_parser("path", help="Print resolved ReBOB paths as JSON")
|
|
29
|
+
sub.add_parser("version", help="Print ReBOB version and environment info")
|
|
30
|
+
|
|
31
|
+
return parser
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(argv: list[str] | None = None) -> None:
|
|
35
|
+
parser = _build_parser()
|
|
36
|
+
args = parser.parse_args(argv if argv is not None else sys.argv[1:])
|
|
37
|
+
|
|
38
|
+
if args.command is None:
|
|
39
|
+
parser.print_help()
|
|
40
|
+
raise SystemExit(0)
|
|
41
|
+
|
|
42
|
+
if args.command == "init":
|
|
43
|
+
from rebob.commands.init import run_init
|
|
44
|
+
run_init(
|
|
45
|
+
non_interactive=args.non_interactive,
|
|
46
|
+
api_key=args.api_key,
|
|
47
|
+
project_id=args.project_id,
|
|
48
|
+
url=args.url,
|
|
49
|
+
transport=args.transport,
|
|
50
|
+
port=args.port,
|
|
51
|
+
)
|
|
52
|
+
elif args.command == "doctor":
|
|
53
|
+
from rebob.commands.doctor import run_doctor
|
|
54
|
+
run_doctor(fix=args.fix)
|
|
55
|
+
elif args.command == "serve":
|
|
56
|
+
from rebob.commands.serve import run_serve
|
|
57
|
+
run_serve(transport=args.transport, port=args.port)
|
|
58
|
+
elif args.command == "path":
|
|
59
|
+
from rebob.commands.path import run_path
|
|
60
|
+
run_path()
|
|
61
|
+
elif args.command == "version":
|
|
62
|
+
import platform
|
|
63
|
+
|
|
64
|
+
from rebob import __version__
|
|
65
|
+
print(f"rebob {__version__}")
|
|
66
|
+
print(f"python {platform.python_version()} on {platform.system()}")
|
|
67
|
+
else:
|
|
68
|
+
parser.print_help()
|
|
69
|
+
raise SystemExit(1)
|
|
File without changes
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Shared helpers for rebob init / doctor / serve."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from importlib import resources
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_template(name: str) -> str:
|
|
14
|
+
return resources.files("rebob").joinpath("templates", name).read_text(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_python() -> Path:
|
|
18
|
+
"""Prefer the active venv's interpreter, else the current interpreter.
|
|
19
|
+
|
|
20
|
+
Checks both the Windows layout (Scripts\\python.exe) and the POSIX
|
|
21
|
+
layout (bin/python) under VIRTUAL_ENV, so this works correctly
|
|
22
|
+
regardless of host OS.
|
|
23
|
+
"""
|
|
24
|
+
venv = os.environ.get("VIRTUAL_ENV")
|
|
25
|
+
if venv:
|
|
26
|
+
venv_path = Path(venv)
|
|
27
|
+
candidates = [
|
|
28
|
+
venv_path / "Scripts" / "python.exe", # Windows
|
|
29
|
+
venv_path / "bin" / "python3", # POSIX
|
|
30
|
+
venv_path / "bin" / "python", # POSIX
|
|
31
|
+
]
|
|
32
|
+
for candidate in candidates:
|
|
33
|
+
if candidate.exists():
|
|
34
|
+
return candidate
|
|
35
|
+
return Path(sys.executable)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def quote_cmd(path: Path) -> str:
|
|
39
|
+
"""Quote a path for hook command strings (Windows-safe, spaces-safe)."""
|
|
40
|
+
return f'"{path}"'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def hook_command(python: Path, hook_type: str) -> str:
|
|
44
|
+
"""Build the hook invocation as a module launch: `<python> -m rebob.hook <type>`.
|
|
45
|
+
|
|
46
|
+
Using `-m rebob.hook` instead of a script path means there is no second
|
|
47
|
+
absolute path to go stale — only the interpreter path needs to be right.
|
|
48
|
+
"""
|
|
49
|
+
return f"{quote_cmd(python)} -m rebob.hook {hook_type}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_settings(python: Path) -> dict:
|
|
53
|
+
"""Build Bob settings.json hook config with JSON-safe command strings."""
|
|
54
|
+
|
|
55
|
+
def hook_entry(hook_type: str, timeout: int) -> dict:
|
|
56
|
+
return {
|
|
57
|
+
"hooks": [
|
|
58
|
+
{
|
|
59
|
+
"type": "command",
|
|
60
|
+
"command": hook_command(python, hook_type),
|
|
61
|
+
"timeout": timeout,
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"hooks": {
|
|
68
|
+
"UserPromptSubmit": [hook_entry("prompt", 8000)],
|
|
69
|
+
"PostToolUse": [hook_entry("tool", 5000)],
|
|
70
|
+
"Stop": [hook_entry("stop", 5000)],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_mcp(python: Path, project_root: Path, *, transport: str = "stdio", port: int = 8000) -> dict:
|
|
76
|
+
"""Build Bob mcp.json using module launch (portable across installs).
|
|
77
|
+
|
|
78
|
+
REBOB_HOME is set explicitly in the env block so the server finds the
|
|
79
|
+
right data directory regardless of what cwd Bob actually launches it
|
|
80
|
+
with — belt and braces alongside the `cwd` field.
|
|
81
|
+
"""
|
|
82
|
+
rebob_home = str((project_root / ".rebob").resolve())
|
|
83
|
+
env = {
|
|
84
|
+
"REBOB_HOME": rebob_home,
|
|
85
|
+
"PYTHONIOENCODING": "utf-8",
|
|
86
|
+
"PYTHONUTF8": "1",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if transport == "stdio":
|
|
90
|
+
server_entry = {
|
|
91
|
+
"command": str(python),
|
|
92
|
+
"args": ["-m", "rebob.server"],
|
|
93
|
+
"cwd": str(project_root),
|
|
94
|
+
"env": env,
|
|
95
|
+
}
|
|
96
|
+
else:
|
|
97
|
+
server_entry = {
|
|
98
|
+
"url": f"http://127.0.0.1:{port}/{'sse' if transport == 'sse' else 'mcp'}",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {"mcpServers": {"rebob": server_entry}}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_HOOK_CMD_RE = re.compile(r'"([^"]*)"')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_hook_command(command: str) -> tuple[Path, str]:
|
|
108
|
+
"""Parse a Bob hook command string (`"<python>" -m rebob.hook <type>`)
|
|
109
|
+
into (python, hook_type).
|
|
110
|
+
"""
|
|
111
|
+
parts = _HOOK_CMD_RE.findall(command)
|
|
112
|
+
if not parts:
|
|
113
|
+
raise ValueError(f"invalid hook command: {command!r}")
|
|
114
|
+
hook_type = command.strip().split()[-1]
|
|
115
|
+
return Path(parts[0]), hook_type
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_json(path: Path) -> tuple[dict | None, str | None]:
|
|
119
|
+
"""Load JSON from *path*. Returns (data, error)."""
|
|
120
|
+
if not path.is_file():
|
|
121
|
+
return None, "file missing"
|
|
122
|
+
try:
|
|
123
|
+
return json.loads(path.read_text(encoding="utf-8")), None
|
|
124
|
+
except json.JSONDecodeError as e:
|
|
125
|
+
return None, f"invalid JSON: {e}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def write_json(path: Path, data: dict) -> None:
|
|
129
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def write_text(path: Path, content: str, *, overwrite: bool = True) -> bool:
|
|
134
|
+
"""Write text to *path*. Returns True if written, False if skipped."""
|
|
135
|
+
if path.exists() and not overwrite:
|
|
136
|
+
return False
|
|
137
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
path.write_text(content, encoding="utf-8")
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def merge_mcp(existing: dict | None, new: dict) -> dict:
|
|
143
|
+
"""Merge the rebob server entry into an existing mcp.json, preserving
|
|
144
|
+
any other MCP servers the user already configured.
|
|
145
|
+
"""
|
|
146
|
+
merged = dict(existing) if existing else {}
|
|
147
|
+
servers = dict(merged.get("mcpServers", {}))
|
|
148
|
+
servers.update(new.get("mcpServers", {}))
|
|
149
|
+
merged["mcpServers"] = servers
|
|
150
|
+
return merged
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def merge_settings(existing: dict | None, new: dict) -> dict:
|
|
154
|
+
"""Merge rebob's hook entries into an existing settings.json, preserving
|
|
155
|
+
any other hooks the user already configured for the same event names.
|
|
156
|
+
|
|
157
|
+
For each hook event (UserPromptSubmit, PostToolUse, Stop), drops any
|
|
158
|
+
prior entry whose command was a rebob hook invocation, then appends
|
|
159
|
+
the fresh one — avoiding duplicate rebob entries across repeated
|
|
160
|
+
`rebob init` runs while leaving unrelated hooks untouched.
|
|
161
|
+
"""
|
|
162
|
+
merged = dict(existing) if existing else {}
|
|
163
|
+
hooks = dict(merged.get("hooks", {}))
|
|
164
|
+
|
|
165
|
+
for event, entries in new.get("hooks", {}).items():
|
|
166
|
+
existing_entries = list(hooks.get(event, []))
|
|
167
|
+
kept = [
|
|
168
|
+
e for e in existing_entries
|
|
169
|
+
if not _is_rebob_hook_entry(e)
|
|
170
|
+
]
|
|
171
|
+
hooks[event] = kept + entries
|
|
172
|
+
|
|
173
|
+
merged["hooks"] = hooks
|
|
174
|
+
return merged
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _is_rebob_hook_entry(entry: dict) -> bool:
|
|
178
|
+
for h in entry.get("hooks", []):
|
|
179
|
+
if "rebob.hook" in h.get("command", ""):
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def ensure_gitignore_entries(root: Path, entries: list[str]) -> list[str]:
|
|
185
|
+
"""Append any missing entries to .gitignore. Returns the list actually added."""
|
|
186
|
+
gitignore = root / ".gitignore"
|
|
187
|
+
existing_lines = set()
|
|
188
|
+
if gitignore.exists():
|
|
189
|
+
existing_lines = {ln.strip() for ln in gitignore.read_text(encoding="utf-8").splitlines()}
|
|
190
|
+
|
|
191
|
+
to_add = [e for e in entries if e not in existing_lines]
|
|
192
|
+
if to_add:
|
|
193
|
+
with gitignore.open("a", encoding="utf-8") as f:
|
|
194
|
+
if gitignore.exists() and gitignore.stat().st_size > 0:
|
|
195
|
+
f.write("\n")
|
|
196
|
+
f.write("# ReBOB (added by `rebob init`)\n")
|
|
197
|
+
for e in to_add:
|
|
198
|
+
f.write(e + "\n")
|
|
199
|
+
return to_add
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def confirm_overwrite(path: Path) -> bool:
|
|
203
|
+
answer = input(f"{path} already exists - overwrite? [y/N]: ").strip().lower()
|
|
204
|
+
return answer in ("y", "yes")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""rebob doctor — diagnose a ReBOB installation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from rebob import paths
|
|
9
|
+
from rebob.commands._util import (
|
|
10
|
+
build_mcp,
|
|
11
|
+
build_settings,
|
|
12
|
+
load_json,
|
|
13
|
+
merge_mcp,
|
|
14
|
+
merge_settings,
|
|
15
|
+
resolve_python,
|
|
16
|
+
write_json,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_REQUIRED_ENV = ("IBM_CLOUD_API_KEY", "WATSONX_PROJECT_ID", "WATSONX_URL")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _check(label: str, ok: bool, detail: str = "") -> bool:
|
|
23
|
+
status = "[ok]" if ok else "[FAIL]"
|
|
24
|
+
line = f" {status} {label}"
|
|
25
|
+
if detail:
|
|
26
|
+
line += f" - {detail}"
|
|
27
|
+
print(line)
|
|
28
|
+
return ok
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _check_mcp_import() -> tuple[bool, str]:
|
|
32
|
+
try:
|
|
33
|
+
import fastmcp # noqa: F401
|
|
34
|
+
|
|
35
|
+
from rebob import server # noqa: F401
|
|
36
|
+
|
|
37
|
+
return True, f"fastmcp {getattr(fastmcp, '__version__', '?')}"
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return False, str(e)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_doctor(*, fix: bool = False) -> None:
|
|
43
|
+
root = paths.project_root()
|
|
44
|
+
python = resolve_python()
|
|
45
|
+
|
|
46
|
+
print(f"ReBOB doctor - project: {root}")
|
|
47
|
+
print(f"Python: {python} ({platform.python_version()} on {platform.system()})")
|
|
48
|
+
if fix:
|
|
49
|
+
print("Mode: --fix (will rewrite stale Bob config)")
|
|
50
|
+
print()
|
|
51
|
+
|
|
52
|
+
print("Resolved paths:")
|
|
53
|
+
for key, value in paths.describe().items():
|
|
54
|
+
print(f" {key}: {value}")
|
|
55
|
+
print()
|
|
56
|
+
|
|
57
|
+
all_ok = True
|
|
58
|
+
|
|
59
|
+
env_path = paths.env_file()
|
|
60
|
+
if env_path.exists():
|
|
61
|
+
from dotenv import dotenv_values
|
|
62
|
+
|
|
63
|
+
env_vars = dotenv_values(env_path)
|
|
64
|
+
missing = [k for k in _REQUIRED_ENV if not env_vars.get(k)]
|
|
65
|
+
all_ok &= _check(
|
|
66
|
+
".env has required keys",
|
|
67
|
+
not missing,
|
|
68
|
+
f"missing: {', '.join(missing)}" if missing else "ok",
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
all_ok &= _check(".env exists", False, str(env_path))
|
|
72
|
+
|
|
73
|
+
mcp_path = root / ".bob" / "mcp.json"
|
|
74
|
+
settings_path = root / ".bob" / "settings.json"
|
|
75
|
+
|
|
76
|
+
all_ok &= _check(".bob/mcp.json exists", mcp_path.is_file(), str(mcp_path))
|
|
77
|
+
all_ok &= _check(".bob/settings.json exists", settings_path.is_file(), str(settings_path))
|
|
78
|
+
|
|
79
|
+
if fix:
|
|
80
|
+
print()
|
|
81
|
+
print("Repairing Bob config from live values...")
|
|
82
|
+
existing_mcp, _ = load_json(mcp_path)
|
|
83
|
+
transport = "stdio"
|
|
84
|
+
if existing_mcp and "url" in existing_mcp.get("mcpServers", {}).get("rebob", {}):
|
|
85
|
+
transport = "sse"
|
|
86
|
+
write_json(mcp_path, merge_mcp(existing_mcp, build_mcp(python, root, transport=transport)))
|
|
87
|
+
print(f" rewrote {mcp_path}")
|
|
88
|
+
|
|
89
|
+
existing_settings, _ = load_json(settings_path)
|
|
90
|
+
write_json(settings_path, merge_settings(existing_settings, build_settings(python)))
|
|
91
|
+
print(f" rewrote {settings_path}")
|
|
92
|
+
|
|
93
|
+
mcp_data, mcp_err = load_json(mcp_path)
|
|
94
|
+
settings_data, settings_err = load_json(settings_path)
|
|
95
|
+
|
|
96
|
+
rebob_entry = (mcp_data or {}).get("mcpServers", {}).get("rebob")
|
|
97
|
+
mcp_problem = mcp_err or ("no 'rebob' entry in mcpServers" if not rebob_entry else None)
|
|
98
|
+
all_ok &= _check("mcp.json valid", mcp_problem is None, mcp_problem or "ok")
|
|
99
|
+
|
|
100
|
+
settings_problem = settings_err
|
|
101
|
+
all_ok &= _check("settings.json valid", settings_problem is None, settings_problem or "ok")
|
|
102
|
+
|
|
103
|
+
import_ok, import_detail = _check_mcp_import()
|
|
104
|
+
all_ok &= _check("MCP server import", import_ok, import_detail)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
from rebob.core.store import count_by_status, init_db
|
|
108
|
+
|
|
109
|
+
init_db()
|
|
110
|
+
counts = count_by_status()
|
|
111
|
+
all_ok &= _check(
|
|
112
|
+
"memory store readable",
|
|
113
|
+
True,
|
|
114
|
+
f"{counts['total']} total ({counts['active']} active)",
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
all_ok &= _check("memory store readable", False, str(e))
|
|
118
|
+
|
|
119
|
+
print()
|
|
120
|
+
if all_ok:
|
|
121
|
+
print("All checks passed.")
|
|
122
|
+
else:
|
|
123
|
+
hint = "rebob doctor --fix" if not fix else "rebob init"
|
|
124
|
+
print(f"Some checks failed. Run: {hint}")
|
|
125
|
+
raise SystemExit(1)
|