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.
Files changed (51) hide show
  1. rebob-0.1.0/PKG-INFO +87 -0
  2. rebob-0.1.0/README.md +73 -0
  3. rebob-0.1.0/pyproject.toml +35 -0
  4. rebob-0.1.0/rebob/__init__.py +1 -0
  5. rebob-0.1.0/rebob/__main__.py +6 -0
  6. rebob-0.1.0/rebob/cli.py +69 -0
  7. rebob-0.1.0/rebob/commands/__init__.py +0 -0
  8. rebob-0.1.0/rebob/commands/_util.py +204 -0
  9. rebob-0.1.0/rebob/commands/doctor.py +125 -0
  10. rebob-0.1.0/rebob/commands/init.py +120 -0
  11. rebob-0.1.0/rebob/commands/path.py +11 -0
  12. rebob-0.1.0/rebob/commands/serve.py +18 -0
  13. rebob-0.1.0/rebob/config.py +57 -0
  14. rebob-0.1.0/rebob/contract.py +66 -0
  15. rebob-0.1.0/rebob/core/__init__.py +1 -0
  16. rebob-0.1.0/rebob/core/api.py +188 -0
  17. rebob-0.1.0/rebob/core/assemble.py +50 -0
  18. rebob-0.1.0/rebob/core/extract.py +224 -0
  19. rebob-0.1.0/rebob/core/redact.py +193 -0
  20. rebob-0.1.0/rebob/core/resolve.py +154 -0
  21. rebob-0.1.0/rebob/core/retrieve.py +338 -0
  22. rebob-0.1.0/rebob/core/salience.py +124 -0
  23. rebob-0.1.0/rebob/core/store.py +318 -0
  24. rebob-0.1.0/rebob/core/watsonx.py +184 -0
  25. rebob-0.1.0/rebob/core/worker.py +145 -0
  26. rebob-0.1.0/rebob/hook.py +68 -0
  27. rebob-0.1.0/rebob/paths.py +160 -0
  28. rebob-0.1.0/rebob/server.py +80 -0
  29. rebob-0.1.0/rebob/templates/SKILL.md +13 -0
  30. rebob-0.1.0/rebob/templates/mem.md +13 -0
  31. rebob-0.1.0/rebob/templates/rules.md +9 -0
  32. rebob-0.1.0/rebob.egg-info/PKG-INFO +87 -0
  33. rebob-0.1.0/rebob.egg-info/SOURCES.txt +49 -0
  34. rebob-0.1.0/rebob.egg-info/dependency_links.txt +1 -0
  35. rebob-0.1.0/rebob.egg-info/entry_points.txt +2 -0
  36. rebob-0.1.0/rebob.egg-info/requires.txt +8 -0
  37. rebob-0.1.0/rebob.egg-info/top_level.txt +1 -0
  38. rebob-0.1.0/setup.cfg +4 -0
  39. rebob-0.1.0/tests/test_api.py +204 -0
  40. rebob-0.1.0/tests/test_assemble.py +104 -0
  41. rebob-0.1.0/tests/test_contract.py +38 -0
  42. rebob-0.1.0/tests/test_extract.py +95 -0
  43. rebob-0.1.0/tests/test_hook.py +172 -0
  44. rebob-0.1.0/tests/test_paths.py +135 -0
  45. rebob-0.1.0/tests/test_pipeline.py +88 -0
  46. rebob-0.1.0/tests/test_redact.py +112 -0
  47. rebob-0.1.0/tests/test_resolve.py +88 -0
  48. rebob-0.1.0/tests/test_retrieve.py +305 -0
  49. rebob-0.1.0/tests/test_salience.py +167 -0
  50. rebob-0.1.0/tests/test_store.py +121 -0
  51. 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"
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m rebob` — delegates to the CLI."""
2
+
3
+ from rebob.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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)