kodelet-sdk 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.
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ python-sdk:
10
+ name: Python SDK
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Check out repository
14
+ uses: actions/checkout@v6
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v6
18
+ with:
19
+ python-version: "3.14"
20
+
21
+ - name: Set up uv
22
+ uses: astral-sh/setup-uv@v8.1.0
23
+ with:
24
+ enable-cache: true
25
+
26
+ - name: Install dependencies
27
+ run: uv sync --locked
28
+
29
+ - name: Lint
30
+ run: uv run -- ruff check
31
+
32
+ - name: Type check
33
+ run: uv run -- ty check
34
+
35
+ - name: Test
36
+ run: uv run -- pytest -q
37
+
38
+ - name: Build
39
+ run: uv build
@@ -0,0 +1,88 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ name: Publish to PyPI
15
+ runs-on: ubuntu-latest
16
+ environment:
17
+ name: pypi
18
+ url: https://pypi.org/p/kodelet-sdk
19
+ steps:
20
+ - name: Check out repository
21
+ uses: actions/checkout@v6
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v6
25
+ with:
26
+ python-version: "3.14"
27
+
28
+ - name: Set up uv
29
+ uses: astral-sh/setup-uv@v8.1.0
30
+ with:
31
+ enable-cache: true
32
+
33
+ - name: Install dependencies
34
+ run: uv sync --locked
35
+
36
+ - name: Verify tag matches VERSION.txt
37
+ run: |
38
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
39
+ FILE_VERSION="$(cat VERSION.txt)"
40
+ if [ "${TAG_VERSION}" != "${FILE_VERSION}" ]; then
41
+ echo "tag version ${TAG_VERSION} does not match VERSION.txt ${FILE_VERSION}" >&2
42
+ exit 1
43
+ fi
44
+ uv run -- python - <<'PY' "${FILE_VERSION}"
45
+ import sys
46
+ from packaging.version import Version
47
+ version = sys.argv[1]
48
+ parsed = Version(version)
49
+ if str(parsed) != version:
50
+ raise SystemExit(f"version must be normalized PEP 440; got {version!r}, expected {parsed!s}")
51
+ PY
52
+
53
+ - name: Lint
54
+ run: uv run -- ruff check
55
+
56
+ - name: Type check
57
+ run: uv run -- ty check
58
+
59
+ - name: Test
60
+ run: uv run -- pytest -q
61
+
62
+ - name: Build distributions
63
+ run: uv build
64
+
65
+ - name: Check built metadata version
66
+ run: |
67
+ uv run -- python - <<'PY'
68
+ import zipfile
69
+ from pathlib import Path
70
+
71
+ expected = Path("VERSION.txt").read_text(encoding="utf-8").strip()
72
+ wheels = sorted(Path("dist").glob("kodelet_sdk-*.whl"))
73
+ if len(wheels) != 1:
74
+ raise SystemExit(f"expected one wheel, found {wheels}")
75
+ with zipfile.ZipFile(wheels[0]) as wheel:
76
+ metadata_name = next(name for name in wheel.namelist() if name.endswith(".dist-info/METADATA"))
77
+ metadata = wheel.read(metadata_name).decode("utf-8")
78
+ if f"Version: {expected}\n" not in metadata:
79
+ raise SystemExit(f"wheel metadata does not contain Version: {expected}")
80
+ PY
81
+
82
+ - name: Publish to PyPI
83
+ uses: pypa/gh-action-pypi-publish@release/v1
84
+
85
+ - name: Upload release artifacts
86
+ uses: softprops/action-gh-release@v2
87
+ with:
88
+ files: dist/*
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ kodelet-config.yaml
@@ -0,0 +1,72 @@
1
+ # kodelet-sdk Python SDK
2
+
3
+ ## Project overview
4
+
5
+ This repository contains `kodelet-sdk`, a Python SDK for authoring Kodelet extensions. It mirrors the TypeScript SDK protocol shape while using Python idioms: asyncio runtime, decorators, Pydantic schemas, and Jinja2 templates.
6
+
7
+ ## Layout
8
+
9
+ ```text
10
+ src/kodelet_sdk/ # SDK source
11
+ api.py # Extension registration/dispatch API
12
+ context.py # Tool/command/event context helpers
13
+ runtime.py # stdio JSON-RPC runtime
14
+ schemas.py # Pydantic/JSON Schema bridge
15
+ template.py # Jinja2 rendering helper
16
+ test_harness.py # In-process extension test harness
17
+ examples/ # Runnable example extensions
18
+ review/ # Review command extension
19
+ workspace/ # Workspace helper/policy extension
20
+ tests/ # pytest coverage
21
+ .github/workflows/ # GitHub Actions CI
22
+ ```
23
+
24
+ ## Tooling
25
+
26
+ Use `uv` for dependency management and command execution. Do not use raw `pip` or ad-hoc virtualenv commands.
27
+
28
+ Common commands:
29
+
30
+ ```bash
31
+ uv sync
32
+ uv run -- ruff check
33
+ uv run -- ty check
34
+ uv run -- pytest -q
35
+ uv build
36
+ make check
37
+ ```
38
+
39
+ Run all gates before considering a change complete:
40
+
41
+ ```bash
42
+ uv run -- ruff check && uv run -- ty check && uv run -- pytest -q && uv build
43
+ ```
44
+
45
+ Generated build artifacts under `dist/`, virtualenvs, caches, and `__pycache__` should not be committed.
46
+
47
+ ## Releases
48
+
49
+ Package versions are sourced from `VERSION.txt` via Hatchling dynamic version metadata. Edit `VERSION.txt` manually, commit it, then use the Makefile helper:
50
+
51
+ ```bash
52
+ git add VERSION.txt pyproject.toml uv.lock
53
+ git commit -m "chore: release v0.1.0"
54
+ make release
55
+ ```
56
+
57
+ `make release` runs `make check`, creates the `v$(cat VERSION.txt)` tag, and pushes the branch and tag. The tag push triggers `.github/workflows/release.yml`, which publishes to PyPI through trusted publishing/OIDC (`id-token: write`) and uploads artifacts to the GitHub release.
58
+
59
+ ## Coding conventions
60
+
61
+ - Keep public APIs documented with useful docstrings and parameter descriptions.
62
+ - Prefer asyncio-compatible implementations for runtime/context features.
63
+ - Use Pydantic for schema validation and JSON Schema generation.
64
+ - Use Jinja2 for template rendering.
65
+ - Keep examples importable without side effects; guard runtime startup with `if __name__ == "__main__"`.
66
+ - Example entrypoints should be executable `kodelet-extension-*` wrappers; keep importable implementation code in adjacent `*_extension.py` files.
67
+ - Prefer SDK re-exports in examples (`from kodelet_sdk import BaseModel, Extension, Field, render_template`) so examples are self-contained.
68
+ - When adding public functionality, add focused pytest coverage and keep type/lint checks green.
69
+
70
+ ## README guidance
71
+
72
+ Keep `README.md` focused on users of the SDK: quick start, public API shape, examples, and runtime behavior. Put contributor/agent workflow details here in `AGENTS.md` instead of expanding the README.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jingkai He
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,58 @@
1
+ VERSION := $(shell cat VERSION.txt)
2
+ TAG := v$(VERSION)
3
+
4
+ .PHONY: help sync lint typecheck test build check version tag release clean
5
+
6
+ help:
7
+ @printf '%s\n' \
8
+ 'Targets:' \
9
+ ' sync Install/update the uv environment' \
10
+ ' lint Run ruff' \
11
+ ' typecheck Run ty' \
12
+ ' test Run pytest' \
13
+ ' build Build sdist and wheel' \
14
+ ' check Run lint, typecheck, tests, and build' \
15
+ ' version Print VERSION.txt' \
16
+ ' tag Create git tag v$$(cat VERSION.txt)' \
17
+ ' release Run check, create tag, and push branch+tag' \
18
+ ' clean Remove build/test caches'
19
+
20
+ sync:
21
+ uv sync
22
+
23
+ lint:
24
+ uv run -- ruff check
25
+
26
+ typecheck:
27
+ uv run -- ty check
28
+
29
+ test:
30
+ uv run -- pytest -q
31
+
32
+ build:
33
+ uv build
34
+
35
+ check: lint typecheck test build
36
+
37
+ version:
38
+ @cat VERSION.txt
39
+
40
+ tag:
41
+ @uv run -- python -c 'from packaging.version import Version; import pathlib; v=pathlib.Path("VERSION.txt").read_text().strip(); assert str(Version(v)) == v, f"version must be normalized PEP 440: {v}"'
42
+ @if ! git diff --quiet -- VERSION.txt pyproject.toml uv.lock; then \
43
+ echo 'version-related files have uncommitted changes; commit them before tagging' >&2; \
44
+ exit 2; \
45
+ fi
46
+ @if git rev-parse '$(TAG)' >/dev/null 2>&1; then \
47
+ echo 'tag $(TAG) already exists' >&2; \
48
+ exit 2; \
49
+ fi
50
+ git tag '$(TAG)'
51
+
52
+ release: check tag
53
+ git push origin HEAD
54
+ git push origin '$(TAG)'
55
+
56
+ clean:
57
+ rm -rf dist build *.egg-info .pytest_cache .ruff_cache
58
+ find . -type d -name __pycache__ -prune -exec rm -rf {} +
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: kodelet-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for authoring Kodelet extensions
5
+ Project-URL: Repository, https://github.com/jingkaihe/kodelet
6
+ Author: Kodelet
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: jinja2<4,>=3.1
11
+ Requires-Dist: pydantic<3,>=2.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # kodelet-sdk
15
+
16
+ Python SDK for authoring [Kodelet](https://github.com/jingkaihe/kodelet) extensions.
17
+
18
+ The SDK speaks Kodelet's JSON-RPC extension protocol over stdio and provides an asyncio-first API for registering tools, commands, and event handlers.
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ from kodelet_sdk import BaseModel, Extension
24
+
25
+ ext = Extension(name="weather", version="0.1.0")
26
+
27
+
28
+ class WeatherInput(BaseModel):
29
+ location: str
30
+
31
+
32
+ @ext.tool("get_weather", description="Get weather", input_schema=WeatherInput)
33
+ async def get_weather(input: WeatherInput, ctx):
34
+ return {"content": f"Weather for {input.location}"}
35
+
36
+
37
+ @ext.on("session.start")
38
+ async def session_start(event, ctx):
39
+ ctx.log.info("extension started")
40
+
41
+
42
+ if __name__ == "__main__":
43
+ ext.run_sync()
44
+ ```
45
+
46
+ ## Public API
47
+
48
+ ### Extension registration
49
+
50
+ - `Extension(name=None, version=None)` creates an extension host.
51
+ - `@ext.tool(name=None, description=None, input_schema=None, timeout_in_sec=None)` registers a tool.
52
+ - `@ext.command(name=None, description=None, input_schema=None, aliases=None, kind=None, timeout_in_sec=None)` registers a command.
53
+ - `@ext.on(event, priority=0, timeout_in_sec=None)` registers an event handler such as `session.start`, `tool.call`, or `agent.end`.
54
+ - `await ext.run()` starts the async stdio runtime; `ext.run_sync()` is a synchronous entrypoint convenience.
55
+
56
+ Handlers may be synchronous or asynchronous. Tool handlers may return a string, which is converted to `{ "content": ... }`, or a protocol-shaped mapping. Command handlers return `{ "action": "pass" }`, `{ "action": "respond", "response": ... }`, or `{ "action": "runAgent", "prompt": ... }`.
57
+
58
+ ### Pydantic and Jinja2 bridge dependencies
59
+
60
+ `kodelet-sdk` depends on Pydantic and Jinja2 and re-exports common entry points so extensions can be self-contained:
61
+
62
+ ```python
63
+ from kodelet_sdk import BaseModel, Field, Jinja2, Pydantic, render_template
64
+
65
+
66
+ class ReviewInput(BaseModel):
67
+ target: str = Field(min_length=1)
68
+
69
+
70
+ assert render_template("Review {{ target }}", {"target": "main"}) == "Review main"
71
+ assert Jinja2.Template("Hello {{ name }}").render(name="Kodelet") == "Hello Kodelet"
72
+ assert Pydantic.TypeAdapter(int).validate_python("1") == 1
73
+ ```
74
+
75
+ Pydantic input schemas are converted to JSON Schema during initialization and validate incoming tool/command inputs before handlers run. Commands with validation failures return `{"action": "pass"}` so another command route can handle the invocation.
76
+
77
+ ### Context helpers
78
+
79
+ Handlers receive `ctx` with Kodelet call metadata and helper namespaces:
80
+
81
+ - `ctx.storage.read_text/write_text/read_json/write_json(...)` for extension data files.
82
+ - `ctx.path.resolve_workspace_path(...)` and `ctx.path.relative_to_workspace(...)`.
83
+ - `ctx.fs.exists/read_text/write_text/list(...)` for workspace file access.
84
+ - `ctx.process.exec(...)` and `ctx.process.spawn(...)` for async process execution.
85
+ - `ctx.env.get(...)` for environment access.
86
+ - `ctx.log.debug/info/warn/error(...)` for JSON logs to stderr.
87
+ - `ctx.ui.input/confirm/select/notify(...)` for host UI reverse-RPC calls.
88
+
89
+ ### Testing extensions
90
+
91
+ Use `create_test_harness` to exercise registrations without spawning a subprocess:
92
+
93
+ ```python
94
+ from kodelet_sdk import Extension, create_test_harness
95
+
96
+
97
+ async def test_tool():
98
+ ext = Extension(name="example")
99
+
100
+ @ext.tool("echo", description="Echo", input_schema={"type": "object"})
101
+ async def echo(input, ctx):
102
+ return {"content": input["text"]}
103
+
104
+ harness = await create_test_harness(ext)
105
+ result = await harness.execute_tool({"name": "echo", "input": {"text": "hi"}})
106
+ assert result == {"content": "hi"}
107
+ ```
108
+
109
+ ## Examples
110
+
111
+ Runnable example extensions live in `examples/`:
112
+
113
+ - `examples/review/kodelet-extension-review` is a review command extension.
114
+ - `examples/workspace/kodelet-extension-workspace` is a workspace helper/policy extension.
115
+
116
+ From a checked-out SDK repository, run an example with:
117
+
118
+ ```bash
119
+ uv run -s examples/review/kodelet-extension-review
120
+ ```
121
+
122
+ The `kodelet-extension-*` files are executable wrappers so Kodelet can discover and launch them directly.
123
+
124
+ ## Releases
125
+
126
+ Package versions are read from `VERSION.txt`. To publish a release, configure PyPI Trusted Publishing for the `Release` workflow, then update and commit `VERSION.txt` manually:
127
+
128
+ ```bash
129
+ git add VERSION.txt pyproject.toml uv.lock
130
+ git commit -m "chore: release v0.1.0"
131
+ make release
132
+ ```
133
+
134
+ Pushing the `vX.Y.Z` tag runs the GitHub Actions release workflow, builds the package, and publishes to PyPI using OIDC trusted publishing.
@@ -0,0 +1,121 @@
1
+ # kodelet-sdk
2
+
3
+ Python SDK for authoring [Kodelet](https://github.com/jingkaihe/kodelet) extensions.
4
+
5
+ The SDK speaks Kodelet's JSON-RPC extension protocol over stdio and provides an asyncio-first API for registering tools, commands, and event handlers.
6
+
7
+ ## Quick start
8
+
9
+ ```python
10
+ from kodelet_sdk import BaseModel, Extension
11
+
12
+ ext = Extension(name="weather", version="0.1.0")
13
+
14
+
15
+ class WeatherInput(BaseModel):
16
+ location: str
17
+
18
+
19
+ @ext.tool("get_weather", description="Get weather", input_schema=WeatherInput)
20
+ async def get_weather(input: WeatherInput, ctx):
21
+ return {"content": f"Weather for {input.location}"}
22
+
23
+
24
+ @ext.on("session.start")
25
+ async def session_start(event, ctx):
26
+ ctx.log.info("extension started")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ ext.run_sync()
31
+ ```
32
+
33
+ ## Public API
34
+
35
+ ### Extension registration
36
+
37
+ - `Extension(name=None, version=None)` creates an extension host.
38
+ - `@ext.tool(name=None, description=None, input_schema=None, timeout_in_sec=None)` registers a tool.
39
+ - `@ext.command(name=None, description=None, input_schema=None, aliases=None, kind=None, timeout_in_sec=None)` registers a command.
40
+ - `@ext.on(event, priority=0, timeout_in_sec=None)` registers an event handler such as `session.start`, `tool.call`, or `agent.end`.
41
+ - `await ext.run()` starts the async stdio runtime; `ext.run_sync()` is a synchronous entrypoint convenience.
42
+
43
+ Handlers may be synchronous or asynchronous. Tool handlers may return a string, which is converted to `{ "content": ... }`, or a protocol-shaped mapping. Command handlers return `{ "action": "pass" }`, `{ "action": "respond", "response": ... }`, or `{ "action": "runAgent", "prompt": ... }`.
44
+
45
+ ### Pydantic and Jinja2 bridge dependencies
46
+
47
+ `kodelet-sdk` depends on Pydantic and Jinja2 and re-exports common entry points so extensions can be self-contained:
48
+
49
+ ```python
50
+ from kodelet_sdk import BaseModel, Field, Jinja2, Pydantic, render_template
51
+
52
+
53
+ class ReviewInput(BaseModel):
54
+ target: str = Field(min_length=1)
55
+
56
+
57
+ assert render_template("Review {{ target }}", {"target": "main"}) == "Review main"
58
+ assert Jinja2.Template("Hello {{ name }}").render(name="Kodelet") == "Hello Kodelet"
59
+ assert Pydantic.TypeAdapter(int).validate_python("1") == 1
60
+ ```
61
+
62
+ Pydantic input schemas are converted to JSON Schema during initialization and validate incoming tool/command inputs before handlers run. Commands with validation failures return `{"action": "pass"}` so another command route can handle the invocation.
63
+
64
+ ### Context helpers
65
+
66
+ Handlers receive `ctx` with Kodelet call metadata and helper namespaces:
67
+
68
+ - `ctx.storage.read_text/write_text/read_json/write_json(...)` for extension data files.
69
+ - `ctx.path.resolve_workspace_path(...)` and `ctx.path.relative_to_workspace(...)`.
70
+ - `ctx.fs.exists/read_text/write_text/list(...)` for workspace file access.
71
+ - `ctx.process.exec(...)` and `ctx.process.spawn(...)` for async process execution.
72
+ - `ctx.env.get(...)` for environment access.
73
+ - `ctx.log.debug/info/warn/error(...)` for JSON logs to stderr.
74
+ - `ctx.ui.input/confirm/select/notify(...)` for host UI reverse-RPC calls.
75
+
76
+ ### Testing extensions
77
+
78
+ Use `create_test_harness` to exercise registrations without spawning a subprocess:
79
+
80
+ ```python
81
+ from kodelet_sdk import Extension, create_test_harness
82
+
83
+
84
+ async def test_tool():
85
+ ext = Extension(name="example")
86
+
87
+ @ext.tool("echo", description="Echo", input_schema={"type": "object"})
88
+ async def echo(input, ctx):
89
+ return {"content": input["text"]}
90
+
91
+ harness = await create_test_harness(ext)
92
+ result = await harness.execute_tool({"name": "echo", "input": {"text": "hi"}})
93
+ assert result == {"content": "hi"}
94
+ ```
95
+
96
+ ## Examples
97
+
98
+ Runnable example extensions live in `examples/`:
99
+
100
+ - `examples/review/kodelet-extension-review` is a review command extension.
101
+ - `examples/workspace/kodelet-extension-workspace` is a workspace helper/policy extension.
102
+
103
+ From a checked-out SDK repository, run an example with:
104
+
105
+ ```bash
106
+ uv run -s examples/review/kodelet-extension-review
107
+ ```
108
+
109
+ The `kodelet-extension-*` files are executable wrappers so Kodelet can discover and launch them directly.
110
+
111
+ ## Releases
112
+
113
+ Package versions are read from `VERSION.txt`. To publish a release, configure PyPI Trusted Publishing for the `Release` workflow, then update and commit `VERSION.txt` manually:
114
+
115
+ ```bash
116
+ git add VERSION.txt pyproject.toml uv.lock
117
+ git commit -m "chore: release v0.1.0"
118
+ make release
119
+ ```
120
+
121
+ Pushing the `vX.Y.Z` tag runs the GitHub Actions release workflow, builds the package, and publishes to PyPI using OIDC trusted publishing.
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = ["kodelet-sdk"]
5
+ # [tool.uv.sources]
6
+ # kodelet-sdk = { path = "../..", editable = true }
7
+ # ///
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from kodelet_sdk import BaseModel, Extension, Field, render_template
13
+
14
+ PROMPT_PATH = Path(__file__).with_name("reviewer-prompt.md")
15
+
16
+ ext = Extension(name="review", version="0.1.0")
17
+
18
+
19
+ class ReviewChangesInput(BaseModel):
20
+ target: str = Field(default="HEAD", description="Git ref or branch to compare against")
21
+ focus: str = Field(
22
+ default="correctness, tests, and maintainability",
23
+ description="What the review should emphasize",
24
+ )
25
+
26
+
27
+ @ext.command(
28
+ "review",
29
+ aliases=["/review"],
30
+ description="Review local git changes against a target ref",
31
+ input_schema=ReviewChangesInput,
32
+ kind="recipe",
33
+ )
34
+ async def review_changes(input: ReviewChangesInput, ctx):
35
+ status = await ctx.process.exec("git", ["status", "--short"], {"timeoutMs": 2_000})
36
+ diff = await ctx.process.exec("git", ["diff", "--stat", input.target], {"timeoutMs": 5_000})
37
+ prompt_template = await ctx.fs.read_text(str(PROMPT_PATH))
38
+
39
+ return {
40
+ "action": "runAgent",
41
+ "recipeName": "review",
42
+ "prompt": render_template(
43
+ prompt_template,
44
+ {
45
+ "target": input.target,
46
+ "focus": input.focus,
47
+ "gitStatus": status.stdout.strip()
48
+ or "No changes reported by git status --short.",
49
+ "diffStat": diff.stdout.strip() or f"No diff stat against {input.target}.",
50
+ },
51
+ ),
52
+ }
53
+
54
+
55
+ if __name__ == "__main__":
56
+ ext.run_sync()