diff --git a/src/llm_cli/assets/prompts/commit.md b/src/llm_cli/assets/prompts/commit.md index 56132f2..129c265 100644 --- a/src/llm_cli/assets/prompts/commit.md +++ b/src/llm_cli/assets/prompts/commit.md @@ -8,7 +8,7 @@ ${GIT_DIFF} Now, if provided, use this context to understand the motivation behind the changes and any relevant background information: -${FILES} +${GIT_FILES} @@ -45,6 +45,7 @@ The commit message should be structured as follows: ``` - lines must not be longer than 74 characters +- use a markdown list for the optional body if it contains multiple items - choose only 1 type from the type-to-description below: - feat: Introduce new features - fix: Fix a bug diff --git a/src/llm_cli/cmd/__init__.pyi b/src/llm_cli/cmd/__init__.pyi index cdbd2ac..bb5eadd 100644 --- a/src/llm_cli/cmd/__init__.pyi +++ b/src/llm_cli/cmd/__init__.pyi @@ -1,4 +1,4 @@ -from . import repo +from . import commit, repo from ._app import app -__all__ = ["app", "repo"] +__all__ = ["app", "commit", "repo"] diff --git a/src/llm_cli/cmd/_app.py b/src/llm_cli/cmd/_app.py index be8531b..ef5cb06 100644 --- a/src/llm_cli/cmd/_app.py +++ b/src/llm_cli/cmd/_app.py @@ -1,7 +1,8 @@ import typer -import llm_cli.cmd as lc import llm_cli.utils as lu +from llm_cli import cmd app: typer.Typer = typer.Typer(name="llm-cli", no_args_is_help=True) -lu.add_command(app, lc.repo.app) +lu.add_command(app, cmd.repo.app) +lu.add_command(app, cmd.commit.app) diff --git a/src/llm_cli/cmd/commit/__init__.py b/src/llm_cli/cmd/commit/__init__.py new file mode 100644 index 0000000..62d86f7 --- /dev/null +++ b/src/llm_cli/cmd/commit/__init__.py @@ -0,0 +1,3 @@ +import lazy_loader as lazy + +__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) diff --git a/src/llm_cli/cmd/commit/__init__.pyi b/src/llm_cli/cmd/commit/__init__.pyi new file mode 100644 index 0000000..f35f969 --- /dev/null +++ b/src/llm_cli/cmd/commit/__init__.pyi @@ -0,0 +1,4 @@ +from ._app import app +from ._main import main + +__all__ = ["app", "main"] diff --git a/src/llm_cli/cmd/commit/_app.py b/src/llm_cli/cmd/commit/_app.py new file mode 100644 index 0000000..1c0e955 --- /dev/null +++ b/src/llm_cli/cmd/commit/_app.py @@ -0,0 +1,12 @@ +import asyncio + +import typer + +app = typer.Typer(name="commit") + + +@app.command() +def main() -> None: + from ._main import main + + asyncio.run(main()) diff --git a/src/llm_cli/cmd/commit/_main.py b/src/llm_cli/cmd/commit/_main.py new file mode 100644 index 0000000..4d594b5 --- /dev/null +++ b/src/llm_cli/cmd/commit/_main.py @@ -0,0 +1,30 @@ +import asyncio +import string + +import git +import litellm +import typer + +import llm_cli as lc +import llm_cli.utils as lu + + +async def main(*, verify: bool = True) -> None: + prompt_template = string.Template(lu.get_prompt("commit")) + repo = git.Repo(search_parent_directories=True) + diff: str = repo.git.diff("--cached", "--no-ext-diff") + files: str = repo.git.ls_files() + prompt: str = prompt_template.substitute({"GIT_DIFF": diff, "GIT_FILES": files}) + resp: litellm.ModelResponse = await lc.output(prompt, prefix="") + choices: litellm.Choices = resp.choices[0] # pyright: ignore [reportAssignmentType] + message: str = lu.extract_between_tags(choices.message.content) + proc: asyncio.subprocess.Process = await lu.run( + "git", + "commit", + f"--message={message}", + "--verify" if verify else "--no-verify", + "--edit", + check=False, + ) + if proc.returncode: + raise typer.Exit(proc.returncode) diff --git a/src/llm_cli/cmd/repo/description/__init__.pyi b/src/llm_cli/cmd/repo/description/__init__.pyi index 2a9db04..f35f969 100644 --- a/src/llm_cli/cmd/repo/description/__init__.pyi +++ b/src/llm_cli/cmd/repo/description/__init__.pyi @@ -1,3 +1,4 @@ from ._app import app +from ._main import main -__all__ = ["app"] +__all__ = ["app", "main"] diff --git a/src/llm_cli/cmd/repo/topics/__init__.pyi b/src/llm_cli/cmd/repo/topics/__init__.pyi index 2a9db04..f35f969 100644 --- a/src/llm_cli/cmd/repo/topics/__init__.pyi +++ b/src/llm_cli/cmd/repo/topics/__init__.pyi @@ -1,3 +1,4 @@ from ._app import app +from ._main import main -__all__ = ["app"] +__all__ = ["app", "main"] diff --git a/src/llm_cli/interactive/_output.py b/src/llm_cli/interactive/_output.py index 27b4a2f..5185cac 100644 --- a/src/llm_cli/interactive/_output.py +++ b/src/llm_cli/interactive/_output.py @@ -1,4 +1,4 @@ -from collections.abc import Sequence +from collections.abc import Callable, Sequence from typing import Any import litellm @@ -7,12 +7,14 @@ from rich.panel import Panel import llm_cli as lc +import llm_cli.utils as lu async def output( prompt: str, *, prefix: str | None = None, + sanitize: Callable[[str], str] | None = lu.extract_between_tags, stop: str | Sequence[str] | None = None, title: str | None = None, ) -> litellm.ModelResponse: @@ -36,7 +38,8 @@ async def output( response = litellm.stream_chunk_builder(chunks) # pyright: ignore [reportAssignmentType] choices: litellm.Choices = response.choices[0] # pyright: ignore [reportAssignmentType] content: str = choices.message.content or "" - content = content.strip() + if sanitize: + content = sanitize(content) live.update( Group( Panel(content, title=title, title_align="left"), diff --git a/src/llm_cli/utils/__init__.pyi b/src/llm_cli/utils/__init__.pyi index 9b36ef5..95bf94b 100644 --- a/src/llm_cli/utils/__init__.pyi +++ b/src/llm_cli/utils/__init__.pyi @@ -1,8 +1,17 @@ from . import git from ._add_command import add_command +from ._extract_between_tags import extract_between_tags from ._get_app_dir import get_app_dir from ._get_prompt import get_prompt from ._repomix import repomix from ._run import run -__all__ = ["add_command", "get_app_dir", "get_prompt", "git", "repomix", "run"] +__all__ = [ + "add_command", + "extract_between_tags", + "get_app_dir", + "get_prompt", + "git", + "repomix", + "run", +] diff --git a/src/llm_cli/utils/_extract_between_tags.py b/src/llm_cli/utils/_extract_between_tags.py new file mode 100644 index 0000000..c1289e4 --- /dev/null +++ b/src/llm_cli/utils/_extract_between_tags.py @@ -0,0 +1,11 @@ +def extract_between_tags(content: str | None, tag: str = "Answer") -> str: + if content is None: + return "" + start: int = content.find("<" + tag + ">") + if start >= 0: + start += len(tag) + 2 + content = content[start:] + end: int = content.find("") + if end >= 0: + content = content[:end] + return content.strip() diff --git a/src/llm_cli/utils/_run.py b/src/llm_cli/utils/_run.py index dedebb3..f1df1fa 100644 --- a/src/llm_cli/utils/_run.py +++ b/src/llm_cli/utils/_run.py @@ -7,8 +7,9 @@ async def run( program: _StrOrBytesPath, *args: _StrOrBytesPath, check: bool = True -) -> None: +) -> asp.Process: proc: asp.Process = await asp.create_subprocess_exec(program, *args) returncode: int = await proc.wait() if check and returncode != 0: raise subprocess.CalledProcessError(returncode, [program, *args]) + return proc