Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package with dev dependencies
run: pip install -e ".[dev]"

- name: Run tests
run: pytest tests/ -v

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install build tools
run: pip install build

- name: Build package
run: python -m build

- name: Verify wheel contents
run: |
pip install dist/*.whl
python -c "from c3_invoke import get_provider, list_providers; print('Import OK')"
47 changes: 47 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Publish to PyPI

on:
release:
types: [published]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package with dev dependencies
run: pip install -e ".[dev]"

- name: Run tests
run: pytest tests/ -v

publish:
needs: test
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install build tools
run: pip install build

- name: Build package
run: python -m build

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
87 changes: 87 additions & 0 deletions .github/workflows/test-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Test Publish to TestPyPI

on:
workflow_dispatch:
inputs:
version_suffix:
description: "Version suffix (e.g. dev1, rc1). Leave empty to use pyproject.toml version as-is."
required: false
default: ""

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package with dev dependencies
run: pip install -e ".[dev]"

- name: Run tests
run: pytest tests/ -v

build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install build tools
run: pip install build

- name: Append version suffix
if: ${{ inputs.version_suffix != '' }}
run: |
sed -i "s/^version = \"\\(.*\\)\"/version = \"\\1.${{ inputs.version_suffix }}\"/" pyproject.toml
echo "Updated version to:"
grep '^version' pyproject.toml

- name: Build package
run: python -m build

- name: Verify wheel contents
run: |
pip install dist/*.whl
python -c "from c3_invoke import get_provider, list_providers; print('Import OK')"

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment: testpypi
permissions:
id-token: write
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/

- name: Verify installation from TestPyPI
run: |
sleep 15
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ c3-invoke
python -c "from c3_invoke import get_provider, list_providers; print('TestPyPI install OK')"
111 changes: 110 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,110 @@
# c3-invoke
# C3 Invoke

Unified interface to AI CLIs (Gemini, Claude, Codex) from Python or HTTP.

## Installation

```bash
pip install c3-invoke # Core library (zero dependencies)
pip install c3-invoke[server] # With FastAPI HTTP server
```

## Quick Start

```python
from c3_invoke import GeminiProvider, ClaudeProvider, parse_json_output
from c3_invoke.types import PromptRequest

# Call any AI CLI with a unified interface
gemini = GeminiProvider()
response = gemini.run(PromptRequest(prompt="Explain Python decorators"))
print(response.raw_output)

# Parse JSON from CLI output (handles markdown fences, embedded JSON)
data = parse_json_output(response.raw_output)

# Run multiple prompts in parallel
from c3_invoke import run_batch
responses = run_batch(gemini, [
PromptRequest(prompt="Explain decorators"),
PromptRequest(prompt="Explain generators"),
], max_workers=4)
```

## Providers

| Provider | CLI Binary | Status |
|----------|-----------|--------|
| `GeminiProvider` | `gemini` | Fully implemented |
| `ClaudeProvider` | `claude` | Fully implemented |
| `CodexProvider` | `codex` | Fully implemented |

```python
from c3_invoke import get_provider, list_providers

# Get a specific provider
provider = get_provider("claude")

# List all providers with availability status
for info in list_providers():
print(f"{info.display_name}: {'available' if info.available else 'not found'}")
```

## HTTP Server

```bash
pip install c3-invoke[server]
c3-invoke-server # Starts on port 8766
```

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Server status + available providers |
| `/providers` | GET | List all providers with availability |
| `/providers/{name}/test` | GET | Test specific provider |
| `/prompt` | POST | Send a single prompt to a provider |
| `/batch` | POST | Send multiple prompts in parallel |

### Example

```bash
# Single prompt
curl -X POST http://localhost:8766/prompt \
-H "Content-Type: application/json" \
-d '{"provider": "gemini", "prompt": "What is Python?"}'

# Batch prompts
curl -X POST http://localhost:8766/batch \
-H "Content-Type: application/json" \
-d '{"provider": "gemini", "prompts": [{"prompt": "Q1"}, {"prompt": "Q2"}]}'
```

## Development

```bash
pip install -e ".[dev]"
pytest tests/ -v
```

## Architecture

```
src/c3_invoke/
├── __init__.py # Public API: get_provider(), list_providers()
├── types.py # PromptRequest, PromptResponse, ProviderInfo
├── output.py # parse_json_output()
├── pool.py # run_batch() — parallel execution
├── providers/
│ ├── base.py # BaseProvider ABC
│ ├── gemini.py # GeminiProvider
│ ├── claude.py # ClaudeProvider
│ └── codex.py # CodexProvider
└── server/ # Optional FastAPI server
├── app.py
├── main.py
└── routers/
```

## Part of C3 Ecosystem

C3 Invoke is a standalone package in the [C3 organization](https://github.com/C3) ecosystem. See the [C3 Architecture Overview](https://github.com/C3/job-compass/blob/main/docs/C3-ORGANIZATION.md) for how it fits with Job Compass and C3 CLI Desktop.
46 changes: 46 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[project]
name = "c3-invoke"
version = "0.1.0"
description = "Unified interface to AI CLIs (Gemini, Claude, Codex) from Python or HTTP"
requires-python = ">=3.11"
license = "MIT"
readme = "README.md"
authors = [{ name = "CodeCanvas Collective", email = "hello@codecanvascollective.com" }]
keywords = ["ai", "cli", "gemini", "claude", "codex", "llm"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = []

[project.urls]
Homepage = "https://github.com/CodeCanvasCollective/c3-invoke"
Repository = "https://github.com/CodeCanvasCollective/c3-invoke"
Issues = "https://github.com/CodeCanvasCollective/c3-invoke/issues"

[project.optional-dependencies]
server = ["fastapi>=0.115.0", "uvicorn[standard]>=0.30.0"]
dev = ["pytest>=8.0.0", "pytest-asyncio>=0.24.0", "httpx>=0.27.0"]
all = ["c3-invoke[server,dev]"]

[project.scripts]
c3-invoke-server = "c3_invoke.server.main:main"

[project.entry-points."c3.products"]
c3-invoke = "c3_invoke.server:product_info"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/c3_invoke"]

[tool.pytest.ini_options]
testpaths = ["tests"]
45 changes: 45 additions & 0 deletions src/c3_invoke/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""C3 Invoke — Unified interface to AI CLIs (Gemini, Claude, Codex)."""

from __future__ import annotations

from .output import parse_json_output
from .pool import run_batch
from .providers.base import BaseProvider
from .providers.claude import ClaudeProvider
from .providers.codex import CodexProvider
from .providers.gemini import GeminiProvider
from .types import OutputFormat, PromptRequest, PromptResponse, ProviderInfo

__all__ = [
"BaseProvider",
"ClaudeProvider",
"CodexProvider",
"GeminiProvider",
"OutputFormat",
"PromptRequest",
"PromptResponse",
"ProviderInfo",
"get_provider",
"list_providers",
"parse_json_output",
"run_batch",
]

_PROVIDERS: dict[str, BaseProvider] = {
"gemini": GeminiProvider(),
"claude": ClaudeProvider(),
"codex": CodexProvider(),
}


def get_provider(name: str) -> BaseProvider:
"""Get a provider instance by name."""
try:
return _PROVIDERS[name.lower()]
except KeyError:
raise KeyError(f"Unknown provider: {name!r}. Available: {list(_PROVIDERS)}")


def list_providers() -> list[ProviderInfo]:
"""List all registered providers with their availability status."""
return [p.info() for p in _PROVIDERS.values()]
Loading
Loading