recipebrain 0.0.1__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 (96) hide show
  1. recipebrain-0.0.1/LICENSE +21 -0
  2. recipebrain-0.0.1/PKG-INFO +96 -0
  3. recipebrain-0.0.1/README.md +64 -0
  4. recipebrain-0.0.1/pyproject.toml +64 -0
  5. recipebrain-0.0.1/setup.cfg +4 -0
  6. recipebrain-0.0.1/setup.py +63 -0
  7. recipebrain-0.0.1/src/recipebrain/__init__.py +6 -0
  8. recipebrain-0.0.1/src/recipebrain/__main__.py +6 -0
  9. recipebrain-0.0.1/src/recipebrain/cli.py +319 -0
  10. recipebrain-0.0.1/src/recipebrain/dashboard.py +217 -0
  11. recipebrain-0.0.1/src/recipebrain/doctor.py +143 -0
  12. recipebrain-0.0.1/src/recipebrain/dossier_ops.py +301 -0
  13. recipebrain-0.0.1/src/recipebrain/etl.py +303 -0
  14. recipebrain-0.0.1/src/recipebrain/exceptions.py +19 -0
  15. recipebrain-0.0.1/src/recipebrain/info.py +72 -0
  16. recipebrain-0.0.1/src/recipebrain/ingest_own/__init__.py +3 -0
  17. recipebrain-0.0.1/src/recipebrain/install_skills.py +40 -0
  18. recipebrain-0.0.1/src/recipebrain/log.py +75 -0
  19. recipebrain-0.0.1/src/recipebrain/markdown.py +182 -0
  20. recipebrain-0.0.1/src/recipebrain/mcp_server.py +2037 -0
  21. recipebrain-0.0.1/src/recipebrain/migrations/__init__.py +3 -0
  22. recipebrain-0.0.1/src/recipebrain/migrations/m001_initial.py +3 -0
  23. recipebrain-0.0.1/src/recipebrain/normalise/__init__.py +3 -0
  24. recipebrain-0.0.1/src/recipebrain/normalise/ingredients.py +998 -0
  25. recipebrain-0.0.1/src/recipebrain/observability.py +107 -0
  26. recipebrain-0.0.1/src/recipebrain/parse/__init__.py +3 -0
  27. recipebrain-0.0.1/src/recipebrain/parse/ingredient_line.py +196 -0
  28. recipebrain-0.0.1/src/recipebrain/parse/jsonld.py +189 -0
  29. recipebrain-0.0.1/src/recipebrain/promotions/__init__.py +3 -0
  30. recipebrain-0.0.1/src/recipebrain/promotions/base.py +40 -0
  31. recipebrain-0.0.1/src/recipebrain/query.py +232 -0
  32. recipebrain-0.0.1/src/recipebrain/recommend/__init__.py +3 -0
  33. recipebrain-0.0.1/src/recipebrain/recommend/easy.py +105 -0
  34. recipebrain-0.0.1/src/recipebrain/recommend/frequency.py +123 -0
  35. recipebrain-0.0.1/src/recipebrain/recommend/pantry.py +171 -0
  36. recipebrain-0.0.1/src/recipebrain/recommend/rotation.py +96 -0
  37. recipebrain-0.0.1/src/recipebrain/settings.py +203 -0
  38. recipebrain-0.0.1/src/recipebrain/skills/README.md +91 -0
  39. recipebrain-0.0.1/src/recipebrain/skills/__init__.py +60 -0
  40. recipebrain-0.0.1/src/recipebrain/skills/add-recipe/SKILL.md +54 -0
  41. recipebrain-0.0.1/src/recipebrain/skills/manage/SKILL.md +63 -0
  42. recipebrain-0.0.1/src/recipebrain/skills/pantry/SKILL.md +52 -0
  43. recipebrain-0.0.1/src/recipebrain/skills/recipe-info/SKILL.md +61 -0
  44. recipebrain-0.0.1/src/recipebrain/skills/rotation/SKILL.md +52 -0
  45. recipebrain-0.0.1/src/recipebrain/skills/shopping/SKILL.md +56 -0
  46. recipebrain-0.0.1/src/recipebrain/skills/tonight/SKILL.md +65 -0
  47. recipebrain-0.0.1/src/recipebrain/skills/wine-pairing/SKILL.md +64 -0
  48. recipebrain-0.0.1/src/recipebrain/snapshot.py +122 -0
  49. recipebrain-0.0.1/src/recipebrain/sources/__init__.py +3 -0
  50. recipebrain-0.0.1/src/recipebrain/sources/base.py +45 -0
  51. recipebrain-0.0.1/src/recipebrain/sources/bettybossi.py +289 -0
  52. recipebrain-0.0.1/src/recipebrain/sources/fooby.py +143 -0
  53. recipebrain-0.0.1/src/recipebrain/sources/migusto.py +134 -0
  54. recipebrain-0.0.1/src/recipebrain/sources/schweizerfleisch.py +377 -0
  55. recipebrain-0.0.1/src/recipebrain/sources/swissmilk.py +263 -0
  56. recipebrain-0.0.1/src/recipebrain/transform.py +276 -0
  57. recipebrain-0.0.1/src/recipebrain/validate.py +170 -0
  58. recipebrain-0.0.1/src/recipebrain/writer.py +336 -0
  59. recipebrain-0.0.1/src/recipebrain.egg-info/PKG-INFO +96 -0
  60. recipebrain-0.0.1/src/recipebrain.egg-info/SOURCES.txt +94 -0
  61. recipebrain-0.0.1/src/recipebrain.egg-info/dependency_links.txt +1 -0
  62. recipebrain-0.0.1/src/recipebrain.egg-info/entry_points.txt +2 -0
  63. recipebrain-0.0.1/src/recipebrain.egg-info/requires.txt +22 -0
  64. recipebrain-0.0.1/src/recipebrain.egg-info/top_level.txt +1 -0
  65. recipebrain-0.0.1/tests/test_cli.py +229 -0
  66. recipebrain-0.0.1/tests/test_dashboard.py +129 -0
  67. recipebrain-0.0.1/tests/test_dataset_factory.py +82 -0
  68. recipebrain-0.0.1/tests/test_doctor.py +112 -0
  69. recipebrain-0.0.1/tests/test_dossier_ops.py +276 -0
  70. recipebrain-0.0.1/tests/test_etl.py +385 -0
  71. recipebrain-0.0.1/tests/test_info.py +47 -0
  72. recipebrain-0.0.1/tests/test_install_skills.py +94 -0
  73. recipebrain-0.0.1/tests/test_log.py +85 -0
  74. recipebrain-0.0.1/tests/test_markdown.py +213 -0
  75. recipebrain-0.0.1/tests/test_mcp_server.py +1935 -0
  76. recipebrain-0.0.1/tests/test_normalise_ingredients.py +191 -0
  77. recipebrain-0.0.1/tests/test_observability.py +114 -0
  78. recipebrain-0.0.1/tests/test_parse_ingredient_line.py +153 -0
  79. recipebrain-0.0.1/tests/test_parse_jsonld.py +155 -0
  80. recipebrain-0.0.1/tests/test_query.py +246 -0
  81. recipebrain-0.0.1/tests/test_recommend_easy.py +186 -0
  82. recipebrain-0.0.1/tests/test_recommend_frequency.py +103 -0
  83. recipebrain-0.0.1/tests/test_recommend_pantry.py +242 -0
  84. recipebrain-0.0.1/tests/test_recommend_rotation.py +166 -0
  85. recipebrain-0.0.1/tests/test_settings.py +216 -0
  86. recipebrain-0.0.1/tests/test_smoke.py +14 -0
  87. recipebrain-0.0.1/tests/test_snapshot.py +153 -0
  88. recipebrain-0.0.1/tests/test_sources_bettybossi.py +230 -0
  89. recipebrain-0.0.1/tests/test_sources_fooby.py +132 -0
  90. recipebrain-0.0.1/tests/test_sources_migusto.py +126 -0
  91. recipebrain-0.0.1/tests/test_sources_schweizerfleisch.py +209 -0
  92. recipebrain-0.0.1/tests/test_sources_swissmilk.py +161 -0
  93. recipebrain-0.0.1/tests/test_sync_skills.py +122 -0
  94. recipebrain-0.0.1/tests/test_transform.py +234 -0
  95. recipebrain-0.0.1/tests/test_validate.py +225 -0
  96. recipebrain-0.0.1/tests/test_writer.py +217 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 <YOUR NAME>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: recipebrain
3
+ Version: 0.0.1
4
+ Summary: Personal Swiss recipe knowledge base with promotion-aware meal planning, exposed via MCP. Companion to cellarbrain.
5
+ License-Expression: MIT
6
+ Keywords: recipes,mcp,swiss,meal-planning,duckdb,parquet,personal-data
7
+ Classifier: Development Status :: 2 - Pre-Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: duckdb>=1.0
15
+ Requires-Dist: pyarrow>=15
16
+ Requires-Dist: tomli>=2; python_version < "3.11"
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: selectolax>=0.3
19
+ Requires-Dist: extruct>=0.16
20
+ Requires-Dist: mcp>=1.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == "dev"
23
+ Requires-Dist: pytest-cov; extra == "dev"
24
+ Requires-Dist: ruff>=0.5; extra == "dev"
25
+ Requires-Dist: mypy>=1.10; extra == "dev"
26
+ Provides-Extra: ocr
27
+ Requires-Dist: pytesseract; extra == "ocr"
28
+ Requires-Dist: Pillow; extra == "ocr"
29
+ Provides-Extra: llm
30
+ Requires-Dist: openai>=1.30; extra == "llm"
31
+ Dynamic: license-file
32
+
33
+ # recipebrain
34
+
35
+ Personal Swiss recipe knowledge base with promotion-aware meal planning, exposed via MCP.
36
+
37
+ **Companion to [cellarbrain](https://github.com/<owner>/cellarbrain).** Standalone — no Python coupling. Interop via MCP at runtime, orchestrated by the chat host.
38
+
39
+ ## Status
40
+
41
+ **Pre-alpha.** Skeleton only. No scraping, no real MCP tools, no queries yet.
42
+
43
+ ## Quickstart
44
+
45
+ ```bash
46
+ git clone https://github.com/<owner>/recipebrain.git
47
+ cd recipebrain
48
+ python -m venv .venv
49
+ .venv/Scripts/activate # Windows
50
+ # source .venv/bin/activate # macOS/Linux
51
+ pip install -e ".[dev]"
52
+ cp recipebrain.toml.example recipebrain.toml
53
+ pytest
54
+ recipebrain --help
55
+ ```
56
+
57
+ ## Documentation
58
+
59
+ | Doc | Topic |
60
+ |-----|-------|
61
+ | [docs/01-vision-and-usecases.md](docs/01-vision-and-usecases.md) | Vision, user persona, 10 use cases |
62
+ | [docs/02-requirements.md](docs/02-requirements.md) | Functional & non-functional requirements |
63
+ | [docs/03-data-sources.md](docs/03-data-sources.md) | Swiss recipe & promotion sources |
64
+ | [docs/04-entity-model.md](docs/04-entity-model.md) | Parquet table schemas, views |
65
+ | [docs/05-mcp-tools.md](docs/05-mcp-tools.md) | 15 MCP tools + resources |
66
+ | [docs/06-architecture.md](docs/06-architecture.md) | Pipeline, modules, build phasing |
67
+ | [docs/decisions/](docs/decisions/) | Architecture Decision Records |
68
+
69
+ ## Relationship to cellarbrain
70
+
71
+ recipebrain and cellarbrain are independent Python packages that interoperate via MCP:
72
+
73
+ - **cellarbrain** answers "which wine pairs with this dish?"
74
+ - **recipebrain** answers "which dish pairs with this wine I want to open?"
75
+ - Together (both MCP servers configured in the same host) = end-to-end meal planning with wine pairing.
76
+
77
+ No shared library. No Python imports between them. The chat host orchestrates cross-server tool calls. See [docs/06-architecture.md](docs/06-architecture.md) for details.
78
+
79
+ ## Privacy
80
+
81
+ All data stays local. No cloud sync, no telemetry, no accounts. Recipe data is scraped from public websites and stored as local Parquet files. Your cook log, pantry, and preferences never leave your machine.
82
+
83
+ ## Releasing
84
+
85
+ PyPI publishing uses trusted publishers (OIDC). Configure the `pypi` environment in GitHub repo settings with PyPI's trusted publisher binding. Then push a version tag:
86
+
87
+ ```bash
88
+ git tag v0.0.1
89
+ git push origin v0.0.1
90
+ ```
91
+
92
+ The `publish.yml` workflow handles the rest.
93
+
94
+ ## License
95
+
96
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,64 @@
1
+ # recipebrain
2
+
3
+ Personal Swiss recipe knowledge base with promotion-aware meal planning, exposed via MCP.
4
+
5
+ **Companion to [cellarbrain](https://github.com/<owner>/cellarbrain).** Standalone — no Python coupling. Interop via MCP at runtime, orchestrated by the chat host.
6
+
7
+ ## Status
8
+
9
+ **Pre-alpha.** Skeleton only. No scraping, no real MCP tools, no queries yet.
10
+
11
+ ## Quickstart
12
+
13
+ ```bash
14
+ git clone https://github.com/<owner>/recipebrain.git
15
+ cd recipebrain
16
+ python -m venv .venv
17
+ .venv/Scripts/activate # Windows
18
+ # source .venv/bin/activate # macOS/Linux
19
+ pip install -e ".[dev]"
20
+ cp recipebrain.toml.example recipebrain.toml
21
+ pytest
22
+ recipebrain --help
23
+ ```
24
+
25
+ ## Documentation
26
+
27
+ | Doc | Topic |
28
+ |-----|-------|
29
+ | [docs/01-vision-and-usecases.md](docs/01-vision-and-usecases.md) | Vision, user persona, 10 use cases |
30
+ | [docs/02-requirements.md](docs/02-requirements.md) | Functional & non-functional requirements |
31
+ | [docs/03-data-sources.md](docs/03-data-sources.md) | Swiss recipe & promotion sources |
32
+ | [docs/04-entity-model.md](docs/04-entity-model.md) | Parquet table schemas, views |
33
+ | [docs/05-mcp-tools.md](docs/05-mcp-tools.md) | 15 MCP tools + resources |
34
+ | [docs/06-architecture.md](docs/06-architecture.md) | Pipeline, modules, build phasing |
35
+ | [docs/decisions/](docs/decisions/) | Architecture Decision Records |
36
+
37
+ ## Relationship to cellarbrain
38
+
39
+ recipebrain and cellarbrain are independent Python packages that interoperate via MCP:
40
+
41
+ - **cellarbrain** answers "which wine pairs with this dish?"
42
+ - **recipebrain** answers "which dish pairs with this wine I want to open?"
43
+ - Together (both MCP servers configured in the same host) = end-to-end meal planning with wine pairing.
44
+
45
+ No shared library. No Python imports between them. The chat host orchestrates cross-server tool calls. See [docs/06-architecture.md](docs/06-architecture.md) for details.
46
+
47
+ ## Privacy
48
+
49
+ All data stays local. No cloud sync, no telemetry, no accounts. Recipe data is scraped from public websites and stored as local Parquet files. Your cook log, pantry, and preferences never leave your machine.
50
+
51
+ ## Releasing
52
+
53
+ PyPI publishing uses trusted publishers (OIDC). Configure the `pypi` environment in GitHub repo settings with PyPI's trusted publisher binding. Then push a version tag:
54
+
55
+ ```bash
56
+ git tag v0.0.1
57
+ git push origin v0.0.1
58
+ ```
59
+
60
+ The `publish.yml` workflow handles the rest.
61
+
62
+ ## License
63
+
64
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "recipebrain"
7
+ version = "0.0.1"
8
+ description = "Personal Swiss recipe knowledge base with promotion-aware meal planning, exposed via MCP. Companion to cellarbrain."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ keywords = ["recipes", "mcp", "swiss", "meal-planning", "duckdb", "parquet", "personal-data"]
13
+ classifiers = [
14
+ "Development Status :: 2 - Pre-Alpha",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ ]
19
+ dependencies = [
20
+ "duckdb >= 1.0",
21
+ "pyarrow >= 15",
22
+ "tomli >= 2; python_version < '3.11'",
23
+ "httpx >= 0.27",
24
+ "selectolax >= 0.3",
25
+ "extruct >= 0.16",
26
+ "mcp >= 1.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = ["pytest >= 8", "pytest-cov", "ruff >= 0.5", "mypy >= 1.10"]
31
+ ocr = ["pytesseract", "Pillow"]
32
+ llm = ["openai >= 1.30"]
33
+
34
+ [project.scripts]
35
+ recipebrain = "recipebrain.cli:main"
36
+
37
+ [tool.setuptools.package-data]
38
+ recipebrain = ["skills/**/*.md", "skills/README.md"]
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.ruff]
44
+ line-length = 100
45
+ target-version = "py311"
46
+
47
+ [tool.ruff.lint]
48
+ select = ["E", "F", "W", "I", "B", "UP"]
49
+
50
+ [tool.ruff.lint.per-file-ignores]
51
+ "tests/**" = ["E501"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ addopts = "-q"
56
+
57
+ [tool.mypy]
58
+ python_version = "3.11"
59
+ strict = false
60
+ warn_unused_ignores = true
61
+
62
+ [[tool.mypy.overrides]]
63
+ module = ["extruct.*"]
64
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,63 @@
1
+ """Custom build hook: sync .openclaw/ skills before packaging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pathlib
6
+ import shutil
7
+
8
+ from setuptools import setup
9
+ from setuptools.command.build_py import build_py
10
+ from setuptools.command.sdist import sdist
11
+
12
+ REPO_ROOT = pathlib.Path(__file__).resolve().parent
13
+ SRC_DIR = REPO_ROOT / ".openclaw"
14
+ DST_DIR = REPO_ROOT / "src" / "recipebrain" / "skills"
15
+ SKIP = {"_archive", "__pycache__"}
16
+
17
+
18
+ def _sync_skills() -> None:
19
+ """One-way sync .openclaw/ -> src/recipebrain/skills/."""
20
+ if not SRC_DIR.is_dir():
21
+ return
22
+
23
+ DST_DIR.mkdir(parents=True, exist_ok=True)
24
+
25
+ readme = SRC_DIR / "README.md"
26
+ if readme.is_file():
27
+ shutil.copy2(readme, DST_DIR / "README.md")
28
+
29
+ for child in sorted(SRC_DIR.iterdir()):
30
+ if not child.is_dir():
31
+ continue
32
+ if child.name in SKIP or child.name.startswith("."):
33
+ continue
34
+ skill_file = child / "SKILL.md"
35
+ if not skill_file.is_file():
36
+ continue
37
+ dst_skill_dir = DST_DIR / child.name
38
+ dst_skill_dir.mkdir(parents=True, exist_ok=True)
39
+ shutil.copy2(skill_file, dst_skill_dir / "SKILL.md")
40
+
41
+
42
+ class SyncSkillsBuildPy(build_py):
43
+ """Run skill sync before build_py."""
44
+
45
+ def run(self) -> None:
46
+ _sync_skills()
47
+ super().run()
48
+
49
+
50
+ class SyncSkillsSdist(sdist):
51
+ """Run skill sync before sdist."""
52
+
53
+ def run(self) -> None:
54
+ _sync_skills()
55
+ super().run()
56
+
57
+
58
+ setup(
59
+ cmdclass={
60
+ "build_py": SyncSkillsBuildPy,
61
+ "sdist": SyncSkillsSdist,
62
+ },
63
+ )
@@ -0,0 +1,6 @@
1
+ """recipebrain — personal Swiss recipe knowledge base, exposed via MCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.0.1"
6
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from recipebrain.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,319 @@
1
+ """CLI entry point for recipebrain.
2
+
3
+ Subcommands mirror the planned tool surface in docs/05-mcp-tools.md.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+
10
+ from recipebrain import __version__
11
+
12
+
13
+ def _stub(name: str) -> int:
14
+ print(f"recipebrain {name}: not yet implemented — see docs/06-architecture.md build phasing")
15
+ return 0
16
+
17
+
18
+ def _cmd_etl(args: argparse.Namespace) -> int:
19
+ from recipebrain.etl import run_etl
20
+ from recipebrain.settings import Settings
21
+
22
+ settings = Settings.load(args.config)
23
+ results = run_etl(settings, source_filter=args.source, limit=args.limit)
24
+
25
+ if not results:
26
+ print("No sources processed.")
27
+ return 0
28
+
29
+ for r in results:
30
+ print(
31
+ f" {r.source}: discovered={r.discovered} fetched={r.fetched} "
32
+ f"skipped={r.skipped} errors={r.errors}"
33
+ )
34
+ for detail in r.error_details:
35
+ print(f" ! {detail}")
36
+
37
+ total_errors = sum(r.errors for r in results)
38
+ return 1 if total_errors > 0 and all(r.fetched == 0 for r in results) else 0
39
+
40
+
41
+ def _cmd_promotions_refresh(args: argparse.Namespace) -> int:
42
+ return _stub("promotions refresh")
43
+
44
+
45
+ def _cmd_ingest(args: argparse.Namespace) -> int:
46
+ return _stub("ingest")
47
+
48
+
49
+ def _cmd_validate(args: argparse.Namespace) -> int:
50
+ from pathlib import Path
51
+
52
+ from recipebrain.settings import Settings
53
+ from recipebrain.validate import validate
54
+
55
+ settings = Settings.load(args.config)
56
+ output_dir = Path(settings.paths.output_dir)
57
+ result = validate(output_dir)
58
+
59
+ if result.ok:
60
+ print(f"All checks passed ({result.checks_run} checks).")
61
+ return 0
62
+
63
+ print(f"Validation found {len(result.issues)} issue(s) ({result.checks_run} checks):")
64
+ for issue in result.issues:
65
+ print(f" ! {issue}")
66
+ return 1
67
+
68
+
69
+ def _cmd_mcp(args: argparse.Namespace) -> int:
70
+ import os
71
+ from pathlib import Path
72
+
73
+ from recipebrain.settings import Settings
74
+
75
+ # Resolve paths to absolute before starting MCP server — the server
76
+ # may be launched by an external client with a different CWD.
77
+ settings = Settings.load(args.config)
78
+ os.environ["RECIPEBRAIN_OUTPUT_DIR"] = str(Path(settings.paths.output_dir).resolve())
79
+ if args.config:
80
+ os.environ["RECIPEBRAIN_CONFIG"] = str(Path(args.config).resolve())
81
+
82
+ from recipebrain.mcp_server import run
83
+
84
+ return run()
85
+
86
+
87
+ def _cmd_reindex(args: argparse.Namespace) -> int:
88
+ return _stub("reindex")
89
+
90
+
91
+ def _cmd_doctor(args: argparse.Namespace) -> int:
92
+ from pathlib import Path
93
+
94
+ from recipebrain.doctor import Severity, run_doctor
95
+ from recipebrain.settings import Settings
96
+
97
+ settings = Settings.load(args.config)
98
+ report = run_doctor(
99
+ output_dir=Path(settings.paths.output_dir),
100
+ snapshot_dir=Path(settings.paths.snapshot_dir),
101
+ dossier_dir=Path(settings.paths.dossier_dir),
102
+ )
103
+
104
+ icons = {Severity.OK: "✓", Severity.WARN: "!", Severity.ERROR: "✗"}
105
+ for check in report.checks:
106
+ icon = icons[check.severity]
107
+ print(f" {icon} {check.name}: {check.message}")
108
+
109
+ if report.ok:
110
+ print("\nAll checks passed.")
111
+ return 0
112
+ print(f"\n{report.worst.value.upper()}: some checks need attention.")
113
+ return 1 if report.worst == Severity.ERROR else 0
114
+
115
+
116
+ def _cmd_info(args: argparse.Namespace) -> int:
117
+ from pathlib import Path
118
+
119
+ from recipebrain.info import format_info, gather_info
120
+ from recipebrain.settings import Settings
121
+
122
+ settings = Settings.load(args.config)
123
+ report = gather_info(Path(settings.paths.output_dir))
124
+ print(format_info(report))
125
+ return 0
126
+
127
+
128
+ def _cmd_snapshot(args: argparse.Namespace) -> int:
129
+ from pathlib import Path
130
+
131
+ from recipebrain.settings import Settings
132
+ from recipebrain.snapshot import create_snapshot, list_snapshots, restore_snapshot
133
+
134
+ settings = Settings.load(args.config)
135
+ output_dir = Path(settings.paths.output_dir)
136
+ snapshot_dir = Path(settings.paths.snapshot_dir)
137
+
138
+ action = getattr(args, "snapshot_action", None) or "create"
139
+
140
+ if action == "list":
141
+ snaps = list_snapshots(snapshot_dir)
142
+ if not snaps:
143
+ print("No snapshots found.")
144
+ return 0
145
+ for s in snaps:
146
+ print(f" {s['name']} ({s['file_count']} files)")
147
+ return 0
148
+
149
+ if action == "restore":
150
+ name = args.name
151
+ snap_path = snapshot_dir / name
152
+ try:
153
+ count = restore_snapshot(snap_path, output_dir)
154
+ print(f"Restored {count} file(s) from snapshot '{name}'.")
155
+ return 0
156
+ except FileNotFoundError as exc:
157
+ print(f"Error: {exc}")
158
+ return 1
159
+
160
+ # Default: create
161
+ label = getattr(args, "label", None)
162
+ result = create_snapshot(output_dir, snapshot_dir, label=label)
163
+ if result is None:
164
+ print("Nothing to snapshot (no parquet files found).")
165
+ return 0
166
+ print(f"Snapshot created: {result.name}")
167
+ return 0
168
+
169
+
170
+ def _cmd_install_skills(args: argparse.Namespace) -> int:
171
+ from pathlib import Path
172
+
173
+ from recipebrain.install_skills import install
174
+
175
+ target = Path(args.target) if args.target else None
176
+ count = install(target=target, force=args.force)
177
+ if count < 0:
178
+ return 1
179
+ print(f"Installed {count} skill file(s).")
180
+ return 0
181
+
182
+
183
+ def _cmd_log(args: argparse.Namespace) -> int:
184
+ from recipebrain.mcp_server import log_cook
185
+
186
+ result = log_cook(
187
+ recipe_id=args.recipe_id,
188
+ rating=args.rating,
189
+ notes=args.notes,
190
+ servings=args.servings,
191
+ scale_factor=args.scale_factor,
192
+ )
193
+ print(result)
194
+ return 1 if result.startswith("Error") else 0
195
+
196
+
197
+ def _cmd_dashboard(args: argparse.Namespace) -> int:
198
+ from recipebrain.dashboard import run_dashboard
199
+
200
+ host = args.host
201
+ port = args.port
202
+ print(f"Starting dashboard at http://{host}:{port}")
203
+ run_dashboard(host=host, port=port)
204
+ return 0
205
+
206
+
207
+ def main() -> int:
208
+ parser = argparse.ArgumentParser(
209
+ prog="recipebrain",
210
+ description="Personal Swiss recipe knowledge base with promotion-aware meal planning.",
211
+ )
212
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
213
+ parser.add_argument(
214
+ "--config",
215
+ "-c",
216
+ default="recipebrain.toml",
217
+ help="Path to config file (default: recipebrain.toml)",
218
+ )
219
+
220
+ subparsers = parser.add_subparsers(dest="command")
221
+
222
+ # etl
223
+ sp_etl = subparsers.add_parser("etl", help="Run ETL pipeline for recipe sources")
224
+ sp_etl.add_argument("--source", "-s", help="Specific source to scrape (default: all enabled)")
225
+ sp_etl.add_argument(
226
+ "--limit",
227
+ "-l",
228
+ type=int,
229
+ default=None,
230
+ help="Max new recipes to fetch per source (default: unlimited)",
231
+ )
232
+ sp_etl.set_defaults(func=_cmd_etl)
233
+
234
+ # promotions
235
+ sp_promo = subparsers.add_parser("promotions", help="Manage promotion data")
236
+ promo_sub = sp_promo.add_subparsers(dest="promotions_command")
237
+ sp_promo_refresh = promo_sub.add_parser("refresh", help="Refresh promotion data")
238
+ sp_promo_refresh.set_defaults(func=_cmd_promotions_refresh)
239
+ sp_promo.set_defaults(func=_cmd_promotions_refresh)
240
+
241
+ # ingest
242
+ sp_ingest = subparsers.add_parser("ingest", help="Ingest a recipe from file or URL")
243
+ sp_ingest.add_argument("target", help="File path or URL to ingest")
244
+ sp_ingest.set_defaults(func=_cmd_ingest)
245
+
246
+ # validate
247
+ sp_validate = subparsers.add_parser("validate", help="Validate data integrity")
248
+ sp_validate.set_defaults(func=_cmd_validate)
249
+
250
+ # mcp
251
+ sp_mcp = subparsers.add_parser("mcp", help="Start MCP server")
252
+ sp_mcp.set_defaults(func=_cmd_mcp)
253
+
254
+ # reindex
255
+ sp_reindex = subparsers.add_parser("reindex", help="Rebuild search indexes")
256
+ sp_reindex.set_defaults(func=_cmd_reindex)
257
+
258
+ # doctor
259
+ sp_doctor = subparsers.add_parser("doctor", help="Run health checks on data and config")
260
+ sp_doctor.set_defaults(func=_cmd_doctor)
261
+
262
+ # info
263
+ sp_info = subparsers.add_parser("info", help="Show version, environment, and data summary")
264
+ sp_info.set_defaults(func=_cmd_info)
265
+
266
+ # snapshot
267
+ sp_snapshot = subparsers.add_parser("snapshot", help="Create or manage data snapshots")
268
+ snap_sub = sp_snapshot.add_subparsers(dest="snapshot_action")
269
+ sp_snap_create = snap_sub.add_parser("create", help="Create a new snapshot")
270
+ sp_snap_create.add_argument("--label", "-l", default=None, help="Label for the snapshot")
271
+ snap_sub.add_parser("list", help="List available snapshots")
272
+ sp_snap_restore = snap_sub.add_parser("restore", help="Restore a snapshot")
273
+ sp_snap_restore.add_argument("name", help="Snapshot name to restore")
274
+ sp_snapshot.set_defaults(func=_cmd_snapshot)
275
+
276
+ # install-skills
277
+ sp_skills = subparsers.add_parser("install-skills", help="Install OpenClaw skills")
278
+ sp_skills.add_argument(
279
+ "--target",
280
+ "-t",
281
+ default=None,
282
+ help="Target directory (default: ~/.openclaw/skills/recipebrain/)",
283
+ )
284
+ sp_skills.add_argument("--force", action="store_true", help="Overwrite existing skill files")
285
+ sp_skills.set_defaults(func=_cmd_install_skills)
286
+
287
+ # log
288
+ sp_log = subparsers.add_parser("log", help="Log a cook event for a recipe")
289
+ sp_log.add_argument("recipe_id", type=int, help="ID of the recipe cooked")
290
+ sp_log.add_argument("--rating", "-r", type=int, default=None, help="Rating 1-5")
291
+ sp_log.add_argument("--notes", "-n", default=None, help="Free-form notes")
292
+ sp_log.add_argument("--servings", "-s", type=int, default=None, help="Number of servings made")
293
+ sp_log.add_argument(
294
+ "--scale-factor", type=float, default=None, help="Scale multiplier (e.g. 2.0 for doubled)"
295
+ )
296
+ sp_log.set_defaults(func=_cmd_log)
297
+
298
+ # dashboard
299
+ sp_dash = subparsers.add_parser("dashboard", help="Start observability dashboard")
300
+ sp_dash.add_argument("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)")
301
+ sp_dash.add_argument("--port", "-p", type=int, default=8777, help="Bind port (default: 8777)")
302
+ sp_dash.set_defaults(func=_cmd_dashboard)
303
+
304
+ args = parser.parse_args()
305
+
306
+ if not args.command:
307
+ parser.print_help()
308
+ return 0
309
+
310
+ # Initialise structured logging before running any command
311
+ from recipebrain.log import setup_logging
312
+
313
+ setup_logging()
314
+
315
+ return args.func(args)
316
+
317
+
318
+ if __name__ == "__main__":
319
+ raise SystemExit(main())