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.
- recipebrain-0.0.1/LICENSE +21 -0
- recipebrain-0.0.1/PKG-INFO +96 -0
- recipebrain-0.0.1/README.md +64 -0
- recipebrain-0.0.1/pyproject.toml +64 -0
- recipebrain-0.0.1/setup.cfg +4 -0
- recipebrain-0.0.1/setup.py +63 -0
- recipebrain-0.0.1/src/recipebrain/__init__.py +6 -0
- recipebrain-0.0.1/src/recipebrain/__main__.py +6 -0
- recipebrain-0.0.1/src/recipebrain/cli.py +319 -0
- recipebrain-0.0.1/src/recipebrain/dashboard.py +217 -0
- recipebrain-0.0.1/src/recipebrain/doctor.py +143 -0
- recipebrain-0.0.1/src/recipebrain/dossier_ops.py +301 -0
- recipebrain-0.0.1/src/recipebrain/etl.py +303 -0
- recipebrain-0.0.1/src/recipebrain/exceptions.py +19 -0
- recipebrain-0.0.1/src/recipebrain/info.py +72 -0
- recipebrain-0.0.1/src/recipebrain/ingest_own/__init__.py +3 -0
- recipebrain-0.0.1/src/recipebrain/install_skills.py +40 -0
- recipebrain-0.0.1/src/recipebrain/log.py +75 -0
- recipebrain-0.0.1/src/recipebrain/markdown.py +182 -0
- recipebrain-0.0.1/src/recipebrain/mcp_server.py +2037 -0
- recipebrain-0.0.1/src/recipebrain/migrations/__init__.py +3 -0
- recipebrain-0.0.1/src/recipebrain/migrations/m001_initial.py +3 -0
- recipebrain-0.0.1/src/recipebrain/normalise/__init__.py +3 -0
- recipebrain-0.0.1/src/recipebrain/normalise/ingredients.py +998 -0
- recipebrain-0.0.1/src/recipebrain/observability.py +107 -0
- recipebrain-0.0.1/src/recipebrain/parse/__init__.py +3 -0
- recipebrain-0.0.1/src/recipebrain/parse/ingredient_line.py +196 -0
- recipebrain-0.0.1/src/recipebrain/parse/jsonld.py +189 -0
- recipebrain-0.0.1/src/recipebrain/promotions/__init__.py +3 -0
- recipebrain-0.0.1/src/recipebrain/promotions/base.py +40 -0
- recipebrain-0.0.1/src/recipebrain/query.py +232 -0
- recipebrain-0.0.1/src/recipebrain/recommend/__init__.py +3 -0
- recipebrain-0.0.1/src/recipebrain/recommend/easy.py +105 -0
- recipebrain-0.0.1/src/recipebrain/recommend/frequency.py +123 -0
- recipebrain-0.0.1/src/recipebrain/recommend/pantry.py +171 -0
- recipebrain-0.0.1/src/recipebrain/recommend/rotation.py +96 -0
- recipebrain-0.0.1/src/recipebrain/settings.py +203 -0
- recipebrain-0.0.1/src/recipebrain/skills/README.md +91 -0
- recipebrain-0.0.1/src/recipebrain/skills/__init__.py +60 -0
- recipebrain-0.0.1/src/recipebrain/skills/add-recipe/SKILL.md +54 -0
- recipebrain-0.0.1/src/recipebrain/skills/manage/SKILL.md +63 -0
- recipebrain-0.0.1/src/recipebrain/skills/pantry/SKILL.md +52 -0
- recipebrain-0.0.1/src/recipebrain/skills/recipe-info/SKILL.md +61 -0
- recipebrain-0.0.1/src/recipebrain/skills/rotation/SKILL.md +52 -0
- recipebrain-0.0.1/src/recipebrain/skills/shopping/SKILL.md +56 -0
- recipebrain-0.0.1/src/recipebrain/skills/tonight/SKILL.md +65 -0
- recipebrain-0.0.1/src/recipebrain/skills/wine-pairing/SKILL.md +64 -0
- recipebrain-0.0.1/src/recipebrain/snapshot.py +122 -0
- recipebrain-0.0.1/src/recipebrain/sources/__init__.py +3 -0
- recipebrain-0.0.1/src/recipebrain/sources/base.py +45 -0
- recipebrain-0.0.1/src/recipebrain/sources/bettybossi.py +289 -0
- recipebrain-0.0.1/src/recipebrain/sources/fooby.py +143 -0
- recipebrain-0.0.1/src/recipebrain/sources/migusto.py +134 -0
- recipebrain-0.0.1/src/recipebrain/sources/schweizerfleisch.py +377 -0
- recipebrain-0.0.1/src/recipebrain/sources/swissmilk.py +263 -0
- recipebrain-0.0.1/src/recipebrain/transform.py +276 -0
- recipebrain-0.0.1/src/recipebrain/validate.py +170 -0
- recipebrain-0.0.1/src/recipebrain/writer.py +336 -0
- recipebrain-0.0.1/src/recipebrain.egg-info/PKG-INFO +96 -0
- recipebrain-0.0.1/src/recipebrain.egg-info/SOURCES.txt +94 -0
- recipebrain-0.0.1/src/recipebrain.egg-info/dependency_links.txt +1 -0
- recipebrain-0.0.1/src/recipebrain.egg-info/entry_points.txt +2 -0
- recipebrain-0.0.1/src/recipebrain.egg-info/requires.txt +22 -0
- recipebrain-0.0.1/src/recipebrain.egg-info/top_level.txt +1 -0
- recipebrain-0.0.1/tests/test_cli.py +229 -0
- recipebrain-0.0.1/tests/test_dashboard.py +129 -0
- recipebrain-0.0.1/tests/test_dataset_factory.py +82 -0
- recipebrain-0.0.1/tests/test_doctor.py +112 -0
- recipebrain-0.0.1/tests/test_dossier_ops.py +276 -0
- recipebrain-0.0.1/tests/test_etl.py +385 -0
- recipebrain-0.0.1/tests/test_info.py +47 -0
- recipebrain-0.0.1/tests/test_install_skills.py +94 -0
- recipebrain-0.0.1/tests/test_log.py +85 -0
- recipebrain-0.0.1/tests/test_markdown.py +213 -0
- recipebrain-0.0.1/tests/test_mcp_server.py +1935 -0
- recipebrain-0.0.1/tests/test_normalise_ingredients.py +191 -0
- recipebrain-0.0.1/tests/test_observability.py +114 -0
- recipebrain-0.0.1/tests/test_parse_ingredient_line.py +153 -0
- recipebrain-0.0.1/tests/test_parse_jsonld.py +155 -0
- recipebrain-0.0.1/tests/test_query.py +246 -0
- recipebrain-0.0.1/tests/test_recommend_easy.py +186 -0
- recipebrain-0.0.1/tests/test_recommend_frequency.py +103 -0
- recipebrain-0.0.1/tests/test_recommend_pantry.py +242 -0
- recipebrain-0.0.1/tests/test_recommend_rotation.py +166 -0
- recipebrain-0.0.1/tests/test_settings.py +216 -0
- recipebrain-0.0.1/tests/test_smoke.py +14 -0
- recipebrain-0.0.1/tests/test_snapshot.py +153 -0
- recipebrain-0.0.1/tests/test_sources_bettybossi.py +230 -0
- recipebrain-0.0.1/tests/test_sources_fooby.py +132 -0
- recipebrain-0.0.1/tests/test_sources_migusto.py +126 -0
- recipebrain-0.0.1/tests/test_sources_schweizerfleisch.py +209 -0
- recipebrain-0.0.1/tests/test_sources_swissmilk.py +161 -0
- recipebrain-0.0.1/tests/test_sync_skills.py +122 -0
- recipebrain-0.0.1/tests/test_transform.py +234 -0
- recipebrain-0.0.1/tests/test_validate.py +225 -0
- 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,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,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())
|