levain-cli 0.1.0__tar.gz → 0.1.10__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.
- {levain_cli-0.1.0 → levain_cli-0.1.10}/.gitignore +14 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/PKG-INFO +3 -3
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/cli.py +45 -9
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/codegen.py +51 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/integrations_check.py +14 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/source_check.py +165 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/pyproject.toml +3 -3
- {levain_cli-0.1.0 → levain_cli-0.1.10}/LICENSE +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/README.md +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/__init__.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/__main__.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/_format.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/_java.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/auth.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/graph.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/known_recipes.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/mcp.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/models.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/parser.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/profile.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/py.typed +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/reducers.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/tlc.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/translate.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/user_input.py +0 -0
- {levain_cli-0.1.0 → levain_cli-0.1.10}/levain_recipe/validate.py +0 -0
|
@@ -117,3 +117,17 @@ kubeconfig
|
|
|
117
117
|
|
|
118
118
|
# Local TLC toolchain drop-in (CI extracts it from the ghcr carrier)
|
|
119
119
|
tools/tla2tools.jar
|
|
120
|
+
|
|
121
|
+
# Box-substrate state left over from local backends: state carries secrets.
|
|
122
|
+
infra/stage1-box/*.tfstate*
|
|
123
|
+
infra/stage2-box/*.tfstate*
|
|
124
|
+
# Terraform's crash dump of unwritten state — can contain secrets, never commit
|
|
125
|
+
errored.tfstate
|
|
126
|
+
|
|
127
|
+
# Saved plans embed resolved variable values, including secrets.
|
|
128
|
+
*.tfplan
|
|
129
|
+
|
|
130
|
+
# HyperFrames build artifacts — the shipped MP4 and poster live in
|
|
131
|
+
# www/public/videos/; the render and proof frames are reproducible.
|
|
132
|
+
assets/video/*/renders/
|
|
133
|
+
assets/video/*/snapshots/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: levain-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
4
4
|
Summary: Levain recipe toolchain: PlusCal validator, TLC model checking, and GraphState codegen for agent recipes
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -9,9 +9,9 @@ Requires-Dist: httpx==0.28.1
|
|
|
9
9
|
Requires-Dist: pydantic==2.13.4
|
|
10
10
|
Requires-Dist: tree-sitter-tlaplus==1.5.0
|
|
11
11
|
Requires-Dist: tree-sitter==0.26.0
|
|
12
|
-
Requires-Dist: typer==0.
|
|
12
|
+
Requires-Dist: typer==0.27.2
|
|
13
13
|
Provides-Extra: mcp
|
|
14
|
-
Requires-Dist: fastmcp==
|
|
14
|
+
Requires-Dist: fastmcp==4.0.5; extra == 'mcp'
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
# levain-cli
|
|
@@ -22,7 +22,7 @@ import typer
|
|
|
22
22
|
from . import LANGUAGE_VERSION
|
|
23
23
|
from ._format import format_errors, format_success
|
|
24
24
|
from ._java import ensure_jar
|
|
25
|
-
from .codegen import generate_state_module
|
|
25
|
+
from .codegen import generate_state_module, state_module_signature
|
|
26
26
|
from .validate import validate, validate_fast, validate_package
|
|
27
27
|
|
|
28
28
|
app = typer.Typer(
|
|
@@ -125,6 +125,29 @@ def validate_package_cmd(
|
|
|
125
125
|
print(format_errors(result))
|
|
126
126
|
raise typer.Exit(1)
|
|
127
127
|
|
|
128
|
+
# ``_state.py`` is generated from the spec; validating a tree
|
|
129
|
+
# whose copy is stale would bless code the graph no longer
|
|
130
|
+
# matches. The comparison is structural (field names, types,
|
|
131
|
+
# reducers) so formatter rewraps and doc metadata don't trip
|
|
132
|
+
# it. Read-only on purpose — ``generate-state`` writes.
|
|
133
|
+
state_path = recipe_dir / "src" / result.module_name / "_state.py"
|
|
134
|
+
if state_path.parent.is_dir():
|
|
135
|
+
expected = state_module_signature(generate_state_module(result))
|
|
136
|
+
current = (
|
|
137
|
+
state_module_signature(state_path.read_text(encoding="utf-8"))
|
|
138
|
+
if state_path.is_file()
|
|
139
|
+
else None
|
|
140
|
+
)
|
|
141
|
+
if current != expected:
|
|
142
|
+
what = "missing" if current is None else "stale"
|
|
143
|
+
print(
|
|
144
|
+
f"Error: {state_path.relative_to(recipe_dir)} is {what} — "
|
|
145
|
+
f"run `levain generate-state {recipe_dir}` and commit "
|
|
146
|
+
f"the result.",
|
|
147
|
+
file=sys.stderr,
|
|
148
|
+
)
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
|
|
128
151
|
print(format_success(result))
|
|
129
152
|
|
|
130
153
|
|
|
@@ -305,20 +328,21 @@ def git_credential_cmd(
|
|
|
305
328
|
|
|
306
329
|
# git gives us the repo it wants, so the credential is scoped to
|
|
307
330
|
# exactly that rather than to the whole workspace.
|
|
308
|
-
|
|
309
|
-
if
|
|
331
|
+
repo = _repo_from_git_path(fields.get("path", ""))
|
|
332
|
+
if repo is None:
|
|
310
333
|
print(
|
|
311
334
|
"levain git-credential: not a Levain recipe URL",
|
|
312
335
|
file=sys.stderr,
|
|
313
336
|
)
|
|
314
337
|
raise typer.Exit(1)
|
|
338
|
+
scope, slug = repo
|
|
315
339
|
|
|
316
340
|
api = api_base()
|
|
317
341
|
try:
|
|
318
342
|
with _client() as client:
|
|
319
343
|
token = access_token(api, client)
|
|
320
344
|
resp = client.post(
|
|
321
|
-
f"{api}/api/v1/
|
|
345
|
+
f"{api}/api/v1/{scope}/{slug}/git-credential",
|
|
322
346
|
headers={"Authorization": f"Bearer {token}"},
|
|
323
347
|
)
|
|
324
348
|
except AuthError as exc:
|
|
@@ -336,9 +360,21 @@ def git_credential_cmd(
|
|
|
336
360
|
print(f"password={body['password']}")
|
|
337
361
|
|
|
338
362
|
|
|
339
|
-
def
|
|
340
|
-
"""
|
|
363
|
+
def _repo_from_git_path(path: str) -> tuple[str, str] | None:
|
|
364
|
+
"""``(api scope, slug)`` for a recipe repo path, or None.
|
|
365
|
+
|
|
366
|
+
Workspace agents live at ``ws-<hex>/<slug>.git`` and mint under
|
|
367
|
+
``agents``; org agents at ``org-<hex>/<slug>.git`` under
|
|
368
|
+
``org/agents``.
|
|
369
|
+
"""
|
|
341
370
|
parts = path.strip("/").split("/")
|
|
342
|
-
if len(parts) != 2
|
|
343
|
-
return
|
|
344
|
-
|
|
371
|
+
if len(parts) != 2:
|
|
372
|
+
return None
|
|
373
|
+
owner, repo = parts
|
|
374
|
+
if owner.startswith("ws-"):
|
|
375
|
+
scope = "agents"
|
|
376
|
+
elif owner.startswith("org-"):
|
|
377
|
+
scope = "org/agents"
|
|
378
|
+
else:
|
|
379
|
+
return None
|
|
380
|
+
return scope, repo.removesuffix(".git")
|
|
@@ -112,3 +112,54 @@ def _py_type_for(var: StateField) -> str:
|
|
|
112
112
|
if desc_literal is not None:
|
|
113
113
|
return f"Annotated[{py_type.__name__}, {desc_literal}]"
|
|
114
114
|
return py_type.__name__
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def state_module_signature(text: str) -> dict[str, str] | None:
|
|
118
|
+
"""Structural signature of a ``_state.py`` module.
|
|
119
|
+
|
|
120
|
+
Maps field name -> normalized annotation, where ``Annotated``
|
|
121
|
+
is reduced to its base type plus any non-string metadata (the
|
|
122
|
+
reducer callables) — docstring metadata and formatting don't
|
|
123
|
+
affect the signature. Lets ``validate-package`` compare an
|
|
124
|
+
on-disk ``_state.py`` against what codegen would emit without
|
|
125
|
+
tripping on ``ruff format`` rewraps. Returns ``None`` when the
|
|
126
|
+
text isn't parseable or has no ``GraphState`` class.
|
|
127
|
+
"""
|
|
128
|
+
import ast as _ast
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
tree = _ast.parse(text)
|
|
132
|
+
except SyntaxError:
|
|
133
|
+
return None
|
|
134
|
+
for node in _ast.iter_child_nodes(tree):
|
|
135
|
+
if isinstance(node, _ast.ClassDef) and node.name == "GraphState":
|
|
136
|
+
fields: dict[str, str] = {}
|
|
137
|
+
for stmt in node.body:
|
|
138
|
+
if not isinstance(stmt, _ast.AnnAssign) or not isinstance(
|
|
139
|
+
stmt.target, _ast.Name
|
|
140
|
+
):
|
|
141
|
+
continue
|
|
142
|
+
fields[stmt.target.id] = _normalize_annotation(stmt.annotation)
|
|
143
|
+
return fields
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _normalize_annotation(node: object) -> str:
|
|
148
|
+
"""Reduce an annotation AST to base type + non-string metadata."""
|
|
149
|
+
import ast as _ast
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
isinstance(node, _ast.Subscript)
|
|
153
|
+
and isinstance(node.value, _ast.Name)
|
|
154
|
+
and node.value.id == "Annotated"
|
|
155
|
+
and isinstance(node.slice, _ast.Tuple)
|
|
156
|
+
and node.slice.elts
|
|
157
|
+
):
|
|
158
|
+
base = _ast.dump(node.slice.elts[0])
|
|
159
|
+
reducers = [
|
|
160
|
+
_ast.dump(e)
|
|
161
|
+
for e in node.slice.elts[1:]
|
|
162
|
+
if not (isinstance(e, _ast.Constant) and isinstance(e.value, str))
|
|
163
|
+
]
|
|
164
|
+
return " | ".join([base, *reducers])
|
|
165
|
+
return _ast.dump(node) if isinstance(node, _ast.AST) else repr(node)
|
|
@@ -68,6 +68,20 @@ def platform_server_names() -> set[str]:
|
|
|
68
68
|
return set(PLATFORM_MCP_SERVERS)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
# Platform servers named after an integration provider, mirroring
|
|
72
|
+
# ``levain_sdk.mcp_registry._PROVIDER_NAMED_SERVERS``. Vendored for the
|
|
73
|
+
# same reason as ``PLATFORM_MCP_SERVERS`` above; kept equal by the same
|
|
74
|
+
# drift test in ``tests/spec/test_known_recipes.py``.
|
|
75
|
+
PROVIDER_MCP_SERVERS = frozenset(
|
|
76
|
+
{"google_ads", "meta_ads", "shopify", "slack", "stripe"}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def provider_server_names() -> set[str]:
|
|
81
|
+
"""Platform server names that double as an integration provider."""
|
|
82
|
+
return set(PROVIDER_MCP_SERVERS)
|
|
83
|
+
|
|
84
|
+
|
|
71
85
|
def _load_env_targets() -> list[TargetIntegration]:
|
|
72
86
|
"""Attached integrations from the sandbox env, when available.
|
|
73
87
|
|
|
@@ -36,6 +36,14 @@ def check_python_sources(
|
|
|
36
36
|
``@node``, or be marked private with a leading underscore.
|
|
37
37
|
Catches recipes that add Python functions for new behaviour
|
|
38
38
|
but forget to wire them into ``graph.tla``.
|
|
39
|
+
6. ``load_prompt`` calls agree with their templates — every
|
|
40
|
+
kwarg is referenced as ``$var``/``${var}`` in the prompt
|
|
41
|
+
file (``{var}`` is str.format syntax and would raise at
|
|
42
|
+
run time), and the prompt file exists.
|
|
43
|
+
7. ``levain_sdk.claude`` calls that mean "no tools" say
|
|
44
|
+
``tools=[]`` — an empty ``allowed_tools`` is only a
|
|
45
|
+
permission allowlist and does not withhold the built-in
|
|
46
|
+
toolset.
|
|
39
47
|
"""
|
|
40
48
|
errors: list[ValidationError] = []
|
|
41
49
|
src_dir = os.path.join(recipe_dir, "src")
|
|
@@ -43,6 +51,8 @@ def check_python_sources(
|
|
|
43
51
|
errors.extend(_check_pyproject(recipe_dir))
|
|
44
52
|
errors.extend(_check_function_refs(spec.nodes, src_dir))
|
|
45
53
|
errors.extend(_check_orphan_node_functions(spec.nodes, src_dir))
|
|
54
|
+
errors.extend(_check_prompt_templates(src_dir))
|
|
55
|
+
errors.extend(_check_claude_tool_guards(src_dir))
|
|
46
56
|
|
|
47
57
|
return errors
|
|
48
58
|
|
|
@@ -277,3 +287,158 @@ def _check_orphan_node_functions(
|
|
|
277
287
|
)
|
|
278
288
|
|
|
279
289
|
return errors
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _iter_source_files(src_dir: str) -> list[Path]:
|
|
293
|
+
"""Every ``.py`` file under ``src/`` except generated ``_state.py``."""
|
|
294
|
+
root = Path(src_dir)
|
|
295
|
+
if not root.is_dir():
|
|
296
|
+
return []
|
|
297
|
+
return [p for p in sorted(root.rglob("*.py")) if p.name != "_state.py"]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _parse_or_none(py_file: Path) -> ast.Module | None:
|
|
301
|
+
"""Parse a file; syntax errors are reported elsewhere."""
|
|
302
|
+
try:
|
|
303
|
+
return ast.parse(py_file.read_text(encoding="utf-8"))
|
|
304
|
+
except (SyntaxError, OSError):
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _imported_names(
|
|
309
|
+
tree: ast.Module, module: str, names: set[str]
|
|
310
|
+
) -> set[str]:
|
|
311
|
+
"""Local aliases bound to ``names`` imported from ``module``."""
|
|
312
|
+
found: set[str] = set()
|
|
313
|
+
for node in ast.walk(tree):
|
|
314
|
+
if isinstance(node, ast.ImportFrom) and node.module == module:
|
|
315
|
+
for alias in node.names:
|
|
316
|
+
if alias.name in names:
|
|
317
|
+
found.add(alias.asname or alias.name)
|
|
318
|
+
return found
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _check_prompt_templates(src_dir: str) -> list[ValidationError]:
|
|
322
|
+
"""Statically mirror ``load_prompt``'s template guard.
|
|
323
|
+
|
|
324
|
+
For every ``load_prompt(__package__, "name", var=...)`` call,
|
|
325
|
+
the prompt file must exist next to the module
|
|
326
|
+
(``prompts/name.md``) and reference each kwarg as ``$var`` or
|
|
327
|
+
``${var}``. A ``{var}`` placeholder is str.format syntax —
|
|
328
|
+
``string.Template`` ignores it and the SDK raises at run time;
|
|
329
|
+
this surfaces the same failure at validate time.
|
|
330
|
+
|
|
331
|
+
Only calls whose first argument is the ``__package__`` name are
|
|
332
|
+
checked — that is the SDK's documented calling convention, and
|
|
333
|
+
a dynamic package expression can't be resolved statically.
|
|
334
|
+
"""
|
|
335
|
+
from string import Template
|
|
336
|
+
|
|
337
|
+
errors: list[ValidationError] = []
|
|
338
|
+
for py_file in _iter_source_files(src_dir):
|
|
339
|
+
tree = _parse_or_none(py_file)
|
|
340
|
+
if tree is None:
|
|
341
|
+
continue
|
|
342
|
+
aliases = _imported_names(tree, "levain_sdk.prompts", {"load_prompt"})
|
|
343
|
+
if not aliases:
|
|
344
|
+
continue
|
|
345
|
+
for call in ast.walk(tree):
|
|
346
|
+
if not isinstance(call, ast.Call):
|
|
347
|
+
continue
|
|
348
|
+
if not (
|
|
349
|
+
isinstance(call.func, ast.Name) and call.func.id in aliases
|
|
350
|
+
):
|
|
351
|
+
continue
|
|
352
|
+
if len(call.args) < 2 or not (
|
|
353
|
+
isinstance(call.args[1], ast.Constant)
|
|
354
|
+
and isinstance(call.args[1].value, str)
|
|
355
|
+
):
|
|
356
|
+
continue
|
|
357
|
+
first = call.args[0]
|
|
358
|
+
if not (isinstance(first, ast.Name) and first.id == "__package__"):
|
|
359
|
+
continue
|
|
360
|
+
name = call.args[1].value
|
|
361
|
+
kwargs = sorted(
|
|
362
|
+
kw.arg for kw in call.keywords if kw.arg is not None
|
|
363
|
+
)
|
|
364
|
+
prompt_file = py_file.parent / "prompts" / f"{name}.md"
|
|
365
|
+
if not prompt_file.is_file():
|
|
366
|
+
errors.append(
|
|
367
|
+
ValidationError(
|
|
368
|
+
stage="source",
|
|
369
|
+
message=(
|
|
370
|
+
f"{py_file.name} loads prompt '{name}' but "
|
|
371
|
+
f"prompts/{name}.md does not exist next to it."
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
continue
|
|
376
|
+
if not kwargs:
|
|
377
|
+
continue
|
|
378
|
+
referenced = set(
|
|
379
|
+
Template(
|
|
380
|
+
prompt_file.read_text(encoding="utf-8")
|
|
381
|
+
).get_identifiers()
|
|
382
|
+
)
|
|
383
|
+
missing = [k for k in kwargs if k not in referenced]
|
|
384
|
+
if missing:
|
|
385
|
+
errors.append(
|
|
386
|
+
ValidationError(
|
|
387
|
+
stage="source",
|
|
388
|
+
message=(
|
|
389
|
+
f"{py_file.name} passes {missing} to prompt "
|
|
390
|
+
f"'{name}' but prompts/{name}.md never "
|
|
391
|
+
f"references them — use $var or ${{var}} "
|
|
392
|
+
f"(templates are string.Template, not "
|
|
393
|
+
f"str.format, so {{var}} is inert)."
|
|
394
|
+
),
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
return errors
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _check_claude_tool_guards(src_dir: str) -> list[ValidationError]:
|
|
401
|
+
"""Flag ``allowed_tools=[]`` used as a no-tools switch.
|
|
402
|
+
|
|
403
|
+
``levain_sdk.claude.run``/``run_stateless`` offer the built-in
|
|
404
|
+
toolset unless ``tools=[]`` is passed; ``allowed_tools`` is a
|
|
405
|
+
permission allowlist, and an empty one is silently ignored.
|
|
406
|
+
A call that passes an empty ``allowed_tools`` without any
|
|
407
|
+
``tools`` kwarg almost certainly meant to withhold the tools.
|
|
408
|
+
"""
|
|
409
|
+
errors: list[ValidationError] = []
|
|
410
|
+
for py_file in _iter_source_files(src_dir):
|
|
411
|
+
tree = _parse_or_none(py_file)
|
|
412
|
+
if tree is None:
|
|
413
|
+
continue
|
|
414
|
+
aliases = _imported_names(
|
|
415
|
+
tree, "levain_sdk.claude", {"run", "run_stateless"}
|
|
416
|
+
)
|
|
417
|
+
if not aliases:
|
|
418
|
+
continue
|
|
419
|
+
for call in ast.walk(tree):
|
|
420
|
+
if not isinstance(call, ast.Call):
|
|
421
|
+
continue
|
|
422
|
+
if not (
|
|
423
|
+
isinstance(call.func, ast.Name) and call.func.id in aliases
|
|
424
|
+
):
|
|
425
|
+
continue
|
|
426
|
+
kwargs = {kw.arg: kw.value for kw in call.keywords if kw.arg}
|
|
427
|
+
allowed = kwargs.get("allowed_tools")
|
|
428
|
+
if (
|
|
429
|
+
isinstance(allowed, ast.List)
|
|
430
|
+
and not allowed.elts
|
|
431
|
+
and "tools" not in kwargs
|
|
432
|
+
):
|
|
433
|
+
errors.append(
|
|
434
|
+
ValidationError(
|
|
435
|
+
stage="source",
|
|
436
|
+
message=(
|
|
437
|
+
f"{py_file.name} passes allowed_tools=[] "
|
|
438
|
+
f"without tools=[] — an empty allowlist "
|
|
439
|
+
f"does not withhold the built-in toolset; "
|
|
440
|
+
f"pass tools=[] to run with no tools."
|
|
441
|
+
),
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
return errors
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "levain-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.10"
|
|
4
4
|
description = "Levain recipe toolchain: PlusCal validator, TLC model checking, and GraphState codegen for agent recipes"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -27,14 +27,14 @@ dependencies = [
|
|
|
27
27
|
"tree-sitter==0.26.0",
|
|
28
28
|
"tree-sitter-tlaplus==1.5.0",
|
|
29
29
|
"pydantic==2.13.4",
|
|
30
|
-
"typer==0.
|
|
30
|
+
"typer==0.27.2",
|
|
31
31
|
# Sign-in and credential minting. Imported lazily so validation
|
|
32
32
|
# never pays for it.
|
|
33
33
|
"httpx==0.28.1",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
|
-
mcp = ["fastmcp==
|
|
37
|
+
mcp = ["fastmcp==4.0.5"]
|
|
38
38
|
|
|
39
39
|
[project.scripts]
|
|
40
40
|
levain = "levain_recipe.cli:app"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|