unified-cli 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.
- unified_cli-0.1.0/CHANGELOG.md +51 -0
- unified_cli-0.1.0/LICENSE +21 -0
- unified_cli-0.1.0/MANIFEST.in +28 -0
- unified_cli-0.1.0/PKG-INFO +447 -0
- unified_cli-0.1.0/README.ko.md +381 -0
- unified_cli-0.1.0/README.md +406 -0
- unified_cli-0.1.0/USAGE.ko.md +458 -0
- unified_cli-0.1.0/USAGE.md +504 -0
- unified_cli-0.1.0/pyproject.toml +72 -0
- unified_cli-0.1.0/setup.cfg +4 -0
- unified_cli-0.1.0/src/unified_cli/__init__.py +76 -0
- unified_cli-0.1.0/src/unified_cli/base.py +510 -0
- unified_cli-0.1.0/src/unified_cli/cli.py +528 -0
- unified_cli-0.1.0/src/unified_cli/conversation.py +235 -0
- unified_cli-0.1.0/src/unified_cli/core.py +171 -0
- unified_cli-0.1.0/src/unified_cli/dashboard_tpl.py +177 -0
- unified_cli-0.1.0/src/unified_cli/discovery.py +87 -0
- unified_cli-0.1.0/src/unified_cli/errors.py +209 -0
- unified_cli-0.1.0/src/unified_cli/factory.py +69 -0
- unified_cli-0.1.0/src/unified_cli/models.py +220 -0
- unified_cli-0.1.0/src/unified_cli/onboarding.py +269 -0
- unified_cli-0.1.0/src/unified_cli/providers/__init__.py +7 -0
- unified_cli-0.1.0/src/unified_cli/providers/claude.py +356 -0
- unified_cli-0.1.0/src/unified_cli/providers/codex.py +273 -0
- unified_cli-0.1.0/src/unified_cli/providers/gemini.py +374 -0
- unified_cli-0.1.0/src/unified_cli/py.typed +0 -0
- unified_cli-0.1.0/src/unified_cli/repl.py +379 -0
- unified_cli-0.1.0/src/unified_cli/server.py +306 -0
- unified_cli-0.1.0/src/unified_cli/state.py +122 -0
- unified_cli-0.1.0/src/unified_cli/ui.py +180 -0
- unified_cli-0.1.0/src/unified_cli/usage.py +126 -0
- unified_cli-0.1.0/src/unified_cli.egg-info/PKG-INFO +447 -0
- unified_cli-0.1.0/src/unified_cli.egg-info/SOURCES.txt +42 -0
- unified_cli-0.1.0/src/unified_cli.egg-info/dependency_links.txt +1 -0
- unified_cli-0.1.0/src/unified_cli.egg-info/entry_points.txt +2 -0
- unified_cli-0.1.0/src/unified_cli.egg-info/requires.txt +15 -0
- unified_cli-0.1.0/src/unified_cli.egg-info/top_level.txt +1 -0
- unified_cli-0.1.0/tests/test_attachments.py +284 -0
- unified_cli-0.1.0/tests/test_errors.py +108 -0
- unified_cli-0.1.0/tests/test_fixes.py +118 -0
- unified_cli-0.1.0/tests/test_models.py +83 -0
- unified_cli-0.1.0/tests/test_phase1.py +219 -0
- unified_cli-0.1.0/tests/test_state.py +132 -0
- unified_cli-0.1.0/tests/test_usage.py +95 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-06-23
|
|
11
|
+
|
|
12
|
+
Initial public release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Unified Python API + CLI** over three subscription-authenticated agentic
|
|
17
|
+
CLIs — Claude Code (`claude`), OpenAI Codex (`codex`), and Google Antigravity
|
|
18
|
+
(`agy`). Any subset of the three works; the wrapper shells out to whichever
|
|
19
|
+
CLIs are present and never carries its own credentials.
|
|
20
|
+
- **OpenAI-compatible HTTP server** (`unified-cli[server]` extra): drop-in
|
|
21
|
+
`/v1/chat/completions` and `/v1/models` endpoints with model-name auto-routing
|
|
22
|
+
and a `user`-field-as-conversation-id history model.
|
|
23
|
+
- **Streaming**: normalized event stream (`text` / `tool_use` / `tool_result` /
|
|
24
|
+
`reasoning` / `usage` / `session` / `done` / `error`) across the three native
|
|
25
|
+
JSONL schemas.
|
|
26
|
+
- **Managed multi-turn history** with **cross-provider context injection** — a
|
|
27
|
+
single `UnifiedConversation` can switch providers mid-chat and auto-inject the
|
|
28
|
+
recent turns into the new provider's prompt.
|
|
29
|
+
- **Web search** enabled by default (Claude `WebSearch`, Codex `web_search`;
|
|
30
|
+
the `agy`-backed provider decides agentically on its own).
|
|
31
|
+
- **Image (multimodal) input** across all three providers, via each CLI's native
|
|
32
|
+
vision path.
|
|
33
|
+
- **Dynamic model listing** per provider (Claude models API, Codex local cache,
|
|
34
|
+
`agy models`), with arbitrary model IDs always passed straight through.
|
|
35
|
+
- **Structured error classification** (`UnifiedError` across seven categories)
|
|
36
|
+
with automatic auth-expiry fallback to API-key env vars for Claude/Codex.
|
|
37
|
+
- **Onboarding wizard** (`unified-cli setup`), **status UI** (`doctor`,
|
|
38
|
+
`status --watch`), and an auto-updating **web dashboard** at `/dashboard`.
|
|
39
|
+
- **Interactive REPL** (`unified-cli repl`) with slash commands and
|
|
40
|
+
cross-provider switching.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- The `gemini` provider now wraps the **Google Antigravity `agy` CLI** instead
|
|
45
|
+
of the standalone Gemini CLI, which Google blocked for individual accounts in
|
|
46
|
+
2026. The provider key remains `"gemini"` and model slugs such as
|
|
47
|
+
`gemini-3.5-flash` continue to route to it. Note: `agy` headless output is
|
|
48
|
+
plain text and does **not** report token usage.
|
|
49
|
+
|
|
50
|
+
[Unreleased]: https://github.com/MinwooKim1990/unified_cli/compare/v0.1.0...HEAD
|
|
51
|
+
[0.1.0]: https://github.com/MinwooKim1990/unified_cli/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Minwoo Kim
|
|
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,28 @@
|
|
|
1
|
+
# Documentation and metadata to ship in the sdist
|
|
2
|
+
include LICENSE
|
|
3
|
+
include README.md
|
|
4
|
+
include README.ko.md
|
|
5
|
+
include USAGE.md
|
|
6
|
+
include USAGE.ko.md
|
|
7
|
+
include CHANGELOG.md
|
|
8
|
+
|
|
9
|
+
# Typed marker
|
|
10
|
+
include src/unified_cli/py.typed
|
|
11
|
+
|
|
12
|
+
# Keep the test suite in the sdist
|
|
13
|
+
recursive-include tests *.py
|
|
14
|
+
|
|
15
|
+
# Exclude internal development docs
|
|
16
|
+
exclude FIX_PLAN.md
|
|
17
|
+
exclude PHASE0_VERIFICATION.md
|
|
18
|
+
exclude UX_TEST_REPORT.md
|
|
19
|
+
exclude simulation.md
|
|
20
|
+
|
|
21
|
+
# Prune build/dev/cache noise
|
|
22
|
+
prune .venv
|
|
23
|
+
prune .codegraph
|
|
24
|
+
prune .pytest_cache
|
|
25
|
+
prune workflows
|
|
26
|
+
|
|
27
|
+
# Drop bytecode and OS cruft everywhere
|
|
28
|
+
global-exclude __pycache__ *.py[cod] .DS_Store
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unified-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drive Claude Code, OpenAI Codex, and Google Antigravity CLIs through one unified Python API and an OpenAI-compatible server — using your existing CLI subscriptions, no API keys required.
|
|
5
|
+
Author-email: Minwoo Kim <kimminwoo190@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/MinwooKim1990/unified_cli
|
|
8
|
+
Project-URL: Repository, https://github.com/MinwooKim1990/unified_cli
|
|
9
|
+
Project-URL: Issues, https://github.com/MinwooKim1990/unified_cli/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/MinwooKim1990/unified_cli#readme
|
|
11
|
+
Keywords: claude,claude-code,codex,antigravity,cli,wrapper,llm,agent,openai-compatible,subprocess
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: POSIX
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: rich>=13
|
|
29
|
+
Provides-Extra: server
|
|
30
|
+
Requires-Dist: fastapi>=0.100; extra == "server"
|
|
31
|
+
Requires-Dist: uvicorn>=0.23; extra == "server"
|
|
32
|
+
Requires-Dist: pydantic>=2; extra == "server"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
35
|
+
Provides-Extra: all
|
|
36
|
+
Requires-Dist: fastapi>=0.100; extra == "all"
|
|
37
|
+
Requires-Dist: uvicorn>=0.23; extra == "all"
|
|
38
|
+
Requires-Dist: pydantic>=2; extra == "all"
|
|
39
|
+
Requires-Dist: pytest>=7; extra == "all"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# unified-cli
|
|
43
|
+
|
|
44
|
+
**One Python + CLI interface for Claude Code, OpenAI Codex, and Google
|
|
45
|
+
Antigravity (`agy`).**
|
|
46
|
+
|
|
47
|
+
[](https://pypi.org/project/unified-cli/)
|
|
48
|
+
[](https://pypi.org/project/unified-cli/)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+
|
|
51
|
+
🇰🇷 [한국어 README](README.ko.md) · 📖 [Detailed usage (EN)](USAGE.md) · 📖 [상세 가이드 (한국어)](USAGE.ko.md)
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install unified-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For the OpenAI-compatible HTTP server, install the optional `server` extra:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install "unified-cli[server]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> **Prerequisites — this package installs and authenticates _nothing_.**
|
|
66
|
+
> `unified-cli` is a thin wrapper that shells out to the official agentic CLIs
|
|
67
|
+
> you already have. It ships **no API keys and no credentials**, and it
|
|
68
|
+
> **stores or transmits no credentials of its own** — every call reuses the
|
|
69
|
+
> login already on your machine.
|
|
70
|
+
>
|
|
71
|
+
> Before using a provider you must have installed the corresponding CLI **and
|
|
72
|
+
> signed in with your own subscription**:
|
|
73
|
+
>
|
|
74
|
+
> - **Claude** → the `claude` CLI (Claude Code), logged in with Claude Pro/Max
|
|
75
|
+
> - **Codex** → the `codex` CLI, logged in with ChatGPT Plus/Pro
|
|
76
|
+
> - **Gemini** → the `agy` CLI (Google Antigravity), logged in with your Google
|
|
77
|
+
> Antigravity account
|
|
78
|
+
>
|
|
79
|
+
> **Any subset works** — you do not need all three. The wrapper simply uses
|
|
80
|
+
> whichever of `claude` / `codex` / `agy` it finds on your `$PATH`.
|
|
81
|
+
|
|
82
|
+
Use all three AI coding CLIs — each signed in with your personal subscription
|
|
83
|
+
(Claude Pro/Max, ChatGPT Plus/Pro, Google Antigravity) — from a single unified
|
|
84
|
+
interface, both as a **terminal CLI** and as a **Python library you can
|
|
85
|
+
`import` in your own code**.
|
|
86
|
+
|
|
87
|
+
> The provider key for the Google side is still `"gemini"` (and `-m
|
|
88
|
+
> gemini-3.5-flash` etc. still route to it), but it now wraps the **Antigravity
|
|
89
|
+
> `agy` CLI** — Google blocked the old `gemini` CLI for individual accounts in
|
|
90
|
+
> 2026. See the migration note below.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# CLI
|
|
94
|
+
$ unified-cli chat "hi" -m haiku
|
|
95
|
+
# or: unified-cli repl → interactive mode with slash commands
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# Python
|
|
100
|
+
from unified_cli import create, UnifiedConversation
|
|
101
|
+
resp = create("claude").chat("hi")
|
|
102
|
+
conv = UnifiedConversation()
|
|
103
|
+
conv.send("Hello", provider="claude")
|
|
104
|
+
conv.send("Continue", provider="gemini") # context auto-injected
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Why this exists
|
|
108
|
+
|
|
109
|
+
Each of the three CLIs (`claude`, `codex`, `agy`) ships great subscription
|
|
110
|
+
auth but lives in its own world. Want to route "quick query" to the fastest
|
|
111
|
+
model regardless of provider? Want a single OpenAI-compatible `/v1/chat/completions`
|
|
112
|
+
endpoint backed by whatever CLI is cheapest/freshest? Want your Python app to
|
|
113
|
+
switch providers mid-conversation with automatic context handoff? That's what
|
|
114
|
+
this wrapper does — **as a CLI you can shell into, and as a Python package you
|
|
115
|
+
can import**.
|
|
116
|
+
|
|
117
|
+
## Features
|
|
118
|
+
|
|
119
|
+
- **Dual mode**: full-featured CLI (`unified-cli chat`, `repl`, `status`, ...)
|
|
120
|
+
AND clean Python API (`from unified_cli import ...`) — same code, same state
|
|
121
|
+
- **Subscription-aware**: uses your existing `claude` / `codex login` / `agy`
|
|
122
|
+
OAuth. Claude/Codex fall back automatically to `ANTHROPIC_API_KEY` /
|
|
123
|
+
`OPENAI_API_KEY` if OAuth expires (agy is OAuth-only)
|
|
124
|
+
- **Multi-turn history**: CLI via `--continue` / `--resume`, Python via
|
|
125
|
+
`session_id=` or `UnifiedConversation`
|
|
126
|
+
- **Cross-provider conversation**: one `UnifiedConversation` can switch providers
|
|
127
|
+
mid-chat; the last 8 turns auto-inject as context into the new provider's prompt
|
|
128
|
+
- **Unified streaming events**: `kind="text" | "tool_use" | "tool_result" |
|
|
129
|
+
"reasoning" | "usage" | "session" | "done" | "error"` — normalized across
|
|
130
|
+
the three native JSONL schemas
|
|
131
|
+
- **Web search by default**: Claude `WebSearch`, Codex `web_search`. The
|
|
132
|
+
`gemini` provider (now the Antigravity `agy` CLI) is agentic and decides
|
|
133
|
+
when to web-search on its own — always available.
|
|
134
|
+
- **Image input** (multimodal, all 3 providers): pass `images=[paths]` to
|
|
135
|
+
`chat()` / `stream()` or `--image foo.png` on the CLI. Each provider uses
|
|
136
|
+
its native vision path:
|
|
137
|
+
- **Codex** — `-i, --image <FILE>` flag (codex CLI 0.129+).
|
|
138
|
+
- **Gemini (`agy`)** — `@<path>` reference embedded in the prompt +
|
|
139
|
+
`--dangerously-skip-permissions` so the agent can read the file.
|
|
140
|
+
- **Claude** — Routed through Claude Code's built-in `Read` tool with
|
|
141
|
+
`--permission-mode bypassPermissions`; the image path is prepended to
|
|
142
|
+
the prompt. PNG / JPEG / GIF / WebP all supported.
|
|
143
|
+
- **Structured errors**: every failure → `UnifiedError(kind=...)` from one of
|
|
144
|
+
seven categories (`auth_expired` / `rate_limit` / `model_not_allowed` /
|
|
145
|
+
`not_found` / `network` / `config` / `internal`) with Korean recovery hints
|
|
146
|
+
- **OpenAI-compatible server**: drop-in `/v1/chat/completions` + auto-updating
|
|
147
|
+
dashboard at `/dashboard`
|
|
148
|
+
- **Rich terminal UI**: `doctor` health table, `status --watch` live dashboard,
|
|
149
|
+
`setup` interactive wizard, streaming spinner
|
|
150
|
+
|
|
151
|
+
## Default models (lightweight, subscription-friendly)
|
|
152
|
+
|
|
153
|
+
| Provider | Default | Latest flagship (override with `-m`) |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| Claude | `claude-haiku-4-5` | `claude-opus-4-7` (or alias `opus`) |
|
|
156
|
+
| Codex | `gpt-5.4-mini` | `gpt-5.4` (or `gpt-5.5` if your `codex` CLI is up to date) |
|
|
157
|
+
| Gemini (`agy`) | `gemini-3.5-flash` | `gemini-3.1-pro` |
|
|
158
|
+
|
|
159
|
+
Override via `-m <name>`. The wrapper passes any model ID straight through to
|
|
160
|
+
the underlying CLI; `unified-cli models` shows the available list as a starting
|
|
161
|
+
point. For the absolute fastest interactive feel use `-m gpt-5.3-codex-spark`.
|
|
162
|
+
|
|
163
|
+
> **Gemini → Antigravity migration**: As of 2026, Google blocked the old
|
|
164
|
+
> `gemini` CLI for individual accounts (`IneligibleTierError: ... migrate to
|
|
165
|
+
> the Antigravity suite`). The `gemini` provider now wraps the **Antigravity
|
|
166
|
+
> `agy` CLI** (`~/.local/bin/agy`). `agy` is fully agentic (web search,
|
|
167
|
+
> shell, file tools) and routes to several model families — run
|
|
168
|
+
> `unified-cli models gemini` (which calls `agy models`) to see them, e.g.
|
|
169
|
+
> `Gemini 3.5 Flash (Medium)`, `Gemini 3.1 Pro (High)`,
|
|
170
|
+
> `Claude Sonnet 4.6 (Thinking)`, `GPT-OSS 120B (Medium)`. Both the display
|
|
171
|
+
> names and slugs like `gemini-3.5-flash` work with `-m`. Unknown names
|
|
172
|
+
> silently fall back to the default. Note: `agy` headless mode outputs plain
|
|
173
|
+
> text (no token-usage reporting).
|
|
174
|
+
|
|
175
|
+
## Install from source (development)
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
git clone https://github.com/MinwooKim1990/unified_cli.git
|
|
179
|
+
cd unified_cli
|
|
180
|
+
python3 -m venv .venv
|
|
181
|
+
source .venv/bin/activate
|
|
182
|
+
pip install -e '.[server,dev]'
|
|
183
|
+
|
|
184
|
+
unified-cli setup # first-time onboarding wizard (see note below)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Requires Python 3.9+ and at least one of `claude`, `codex`, `agy` already
|
|
188
|
+
installed and logged in — see **Prerequisites** above. The optional `setup`
|
|
189
|
+
wizard only *suggests* the official install commands for any missing CLI (e.g.
|
|
190
|
+
npm/brew for Claude/Codex; `agy` ships with the Antigravity suite —
|
|
191
|
+
https://antigravity.google) and opens each provider's own browser login; it
|
|
192
|
+
never stores credentials and you can decline any step.
|
|
193
|
+
|
|
194
|
+
## Usage at a glance
|
|
195
|
+
|
|
196
|
+
### CLI
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Single turn
|
|
200
|
+
unified-cli chat "explain python list reversal in one line"
|
|
201
|
+
|
|
202
|
+
# Continue the last conversation
|
|
203
|
+
unified-cli chat "what about in-place?" --continue
|
|
204
|
+
|
|
205
|
+
# Resume a specific session
|
|
206
|
+
unified-cli chat "continue from earlier" --resume <session_id>
|
|
207
|
+
|
|
208
|
+
# Interactive REPL with slash commands (/provider, /model, /history, /save, ...)
|
|
209
|
+
unified-cli repl
|
|
210
|
+
|
|
211
|
+
# Stream + web-search (both defaults)
|
|
212
|
+
unified-cli chat "latest Python release?" --stream
|
|
213
|
+
|
|
214
|
+
# Cheapest fast query
|
|
215
|
+
unified-cli chat "quick q" -m gpt-5.3-codex-spark
|
|
216
|
+
|
|
217
|
+
# Image input (works with all 3 providers — see Features above for details)
|
|
218
|
+
unified-cli chat "what's in this photo?" --image cat.png -m haiku
|
|
219
|
+
unified-cli chat "compare these two" --image a.jpg --image b.jpg -m gpt-5.4-mini
|
|
220
|
+
|
|
221
|
+
# Status & dashboard
|
|
222
|
+
unified-cli doctor # one-time health check
|
|
223
|
+
unified-cli status --watch # live terminal dashboard (5s refresh)
|
|
224
|
+
uvicorn unified_cli.server:app --port 8000 # + http://localhost:8000/dashboard
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Interactive REPL — `unified-cli repl`
|
|
228
|
+
|
|
229
|
+
```text
|
|
230
|
+
[claude/haiku] > hello
|
|
231
|
+
[claude/haiku] > /provider codex # switch providers (context auto-injected)
|
|
232
|
+
[codex/gpt-5.4-mini] > /image photo.png # attach image for the next turn
|
|
233
|
+
[codex/gpt-5.4-mini] > describe this
|
|
234
|
+
[codex/gpt-5.4-mini] > /history # last 10 turns
|
|
235
|
+
[codex/gpt-5.4-mini] > /save # current session_id + resume hint
|
|
236
|
+
[codex/gpt-5.4-mini] > /exit # state saved → `chat --continue` from here
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Slash commands: `/help` `/model` `/provider` `/new` `/save` `/history`
|
|
240
|
+
`/tokens` `/doctor` `/image` `/images` `/clear-images` `/exit`.
|
|
241
|
+
|
|
242
|
+
### Python
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from unified_cli import create, UnifiedConversation, UnifiedError, load_last_session
|
|
246
|
+
|
|
247
|
+
# Pattern 1 — single call
|
|
248
|
+
resp = create("claude").chat("hi")
|
|
249
|
+
|
|
250
|
+
# Pattern 2 — external code manages history (typical for chatbots)
|
|
251
|
+
cli = create("codex")
|
|
252
|
+
sessions = {}
|
|
253
|
+
def reply(user_id: str, prompt: str) -> str:
|
|
254
|
+
r = cli.chat(prompt, session_id=sessions.get(user_id))
|
|
255
|
+
sessions[user_id] = r.session_id
|
|
256
|
+
return r.text
|
|
257
|
+
|
|
258
|
+
# Pattern 3 — wrapper manages history + cross-provider
|
|
259
|
+
conv = UnifiedConversation()
|
|
260
|
+
conv.send("My name is Minwoo.", provider="claude")
|
|
261
|
+
conv.send("What's my name?", provider="gemini") # knows "Minwoo"
|
|
262
|
+
|
|
263
|
+
# Pattern 4 — resume from CLI session
|
|
264
|
+
state = load_last_session() # reads ~/.unified-cli/state.json
|
|
265
|
+
if state:
|
|
266
|
+
resp = create(state.provider, model=state.model).chat(
|
|
267
|
+
"follow-up from REPL", session_id=state.session_id,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Pattern 5 — error-aware fallback
|
|
271
|
+
for p in ("claude", "codex", "gemini"):
|
|
272
|
+
try:
|
|
273
|
+
return create(p).chat("...")
|
|
274
|
+
except UnifiedError as e:
|
|
275
|
+
if e.kind in ("auth_expired", "rate_limit"):
|
|
276
|
+
continue
|
|
277
|
+
raise
|
|
278
|
+
|
|
279
|
+
# Pattern 6 — image input (works on all 3 providers)
|
|
280
|
+
resp = create("claude").chat(
|
|
281
|
+
"What single color is this image?",
|
|
282
|
+
images=["/path/to/photo.png"],
|
|
283
|
+
)
|
|
284
|
+
print(resp.text)
|
|
285
|
+
# `images` accepts mixed inputs:
|
|
286
|
+
# - file path (str or pathlib.Path)
|
|
287
|
+
# - raw bytes
|
|
288
|
+
# - http(s) URL or "data:image/png;base64,..." (Anthropic Attachment)
|
|
289
|
+
images = [
|
|
290
|
+
"cat.png",
|
|
291
|
+
b"\\x89PNG...", # bytes
|
|
292
|
+
"https://example.com/dog.jpg", # URL
|
|
293
|
+
"data:image/png;base64,iVBOR...", # data URL
|
|
294
|
+
]
|
|
295
|
+
# CLI equivalent:
|
|
296
|
+
# unified-cli chat "describe" --image a.png --image b.jpg -m gpt-5.4-mini
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
See [USAGE.md](USAGE.md) (English) or [USAGE.ko.md](USAGE.ko.md) (Korean) for
|
|
300
|
+
the full cookbook — 9 patterns including sync, async, streaming, tool events,
|
|
301
|
+
error fallback, image input, CLI↔Python state sharing, and advanced provider
|
|
302
|
+
options.
|
|
303
|
+
|
|
304
|
+
### OpenAI-compatible server
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
uvicorn unified_cli.server:app --port 8000
|
|
308
|
+
# Browse: http://localhost:8000/dashboard (live usage / sessions)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Drop-in for any OpenAI client — model is auto-routed by name; the `user`
|
|
312
|
+
field acts as a conversation id (preserves history across calls):
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from openai import OpenAI
|
|
316
|
+
client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
|
|
317
|
+
|
|
318
|
+
# Plain text turn
|
|
319
|
+
client.chat.completions.create(
|
|
320
|
+
model="haiku", # → claude
|
|
321
|
+
messages=[{"role":"user","content":"hi"}],
|
|
322
|
+
user="session-1",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Image input (OpenAI multi-content schema, works for all 3 providers)
|
|
326
|
+
client.chat.completions.create(
|
|
327
|
+
model="gpt-5.4-mini", # → codex
|
|
328
|
+
messages=[{"role":"user","content":[
|
|
329
|
+
{"type":"text","text":"describe"},
|
|
330
|
+
{"type":"image_url",
|
|
331
|
+
"image_url":{"url":"data:image/png;base64,iVBOR..."}}
|
|
332
|
+
]}],
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Continue in a different provider (cross-provider conversation)
|
|
336
|
+
client.chat.completions.create(
|
|
337
|
+
model="gemini-3.5-flash", # → gemini (agy)
|
|
338
|
+
messages=[{"role":"user","content":"summarize what we discussed"}],
|
|
339
|
+
user="session-1", # last 8 turns auto-injected
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Known limitations
|
|
344
|
+
|
|
345
|
+
**Speed**: every call spawns a fresh subprocess (`claude -p` / `codex exec` /
|
|
346
|
+
`agy` for the `gemini` provider) — these CLIs don't support a long-lived
|
|
347
|
+
daemon. Measured latency:
|
|
348
|
+
|
|
349
|
+
| Stage | Claude | Codex | Gemini |
|
|
350
|
+
|---|---|---|---|
|
|
351
|
+
| Subprocess spawn | ~50 ms | ~60 ms | ~460 ms (Node bundle) |
|
|
352
|
+
| API round-trip (API round-trip) | 3–6 s | 2–3 s | 3–4 s |
|
|
353
|
+
| **Full chat turn** | **5–6 s** | **2.7–3 s** | **3–4 s** |
|
|
354
|
+
|
|
355
|
+
For the absolute fastest interactive feel, use `-m gpt-5.3-codex-spark`. Even
|
|
356
|
+
then, expect 2–3 seconds per turn. This is a **structural limit of the
|
|
357
|
+
subprocess architecture** — not something the wrapper can fix without either
|
|
358
|
+
(a) losing subscription auth by calling provider APIs directly, or (b) using
|
|
359
|
+
experimental daemon modes (e.g. `codex app-server`) that aren't fully stable
|
|
360
|
+
yet.
|
|
361
|
+
|
|
362
|
+
**Subscription ToS**: each provider's terms forbid reselling/exposing your
|
|
363
|
+
personal subscription as a third-party service. This wrapper is designed for
|
|
364
|
+
**personal local automation**, not as a SaaS gateway. Don't ship a web service
|
|
365
|
+
backed by your personal OAuth.
|
|
366
|
+
|
|
367
|
+
**macOS-first**: Claude's Desktop app bundle is auto-discovered on macOS. On
|
|
368
|
+
Linux/Windows the `claude` binary needs to be on `$PATH`. REPL's arrow-key
|
|
369
|
+
history needs `readline` (stdlib on macOS/Linux; Windows users may need
|
|
370
|
+
`pyreadline3`).
|
|
371
|
+
|
|
372
|
+
**Gemini (`agy`) specifics**: `agy` headless mode prints plain text (no JSON
|
|
373
|
+
event stream), so the wrapper can't surface per-token usage — `tokens in/out`
|
|
374
|
+
shows as `None`. Session resume uses `--conversation <UUID>` / `--continue`;
|
|
375
|
+
the conversation id is recovered from the newest `.db` in
|
|
376
|
+
`~/.gemini/antigravity-cli/conversations/`. Because `agy` runs full agentic
|
|
377
|
+
loops (web/shell/file), a turn can take longer than a one-shot completion, so
|
|
378
|
+
this provider defaults to a larger timeout (300s).
|
|
379
|
+
|
|
380
|
+
**No persistent usage tracking**: `UsageTracker` keeps per-provider aggregates
|
|
381
|
+
and recent-call history in process memory only. Restart = counters reset. For
|
|
382
|
+
long-term usage analytics you'd need to log separately.
|
|
383
|
+
|
|
384
|
+
## Comparison with similar projects
|
|
385
|
+
|
|
386
|
+
| Project | Language | CLI + Python import | 3-CLI subprocess | OpenAI server | Dashboard | REPL |
|
|
387
|
+
|---|---|---|---|---|---|---|
|
|
388
|
+
| **unified-cli** (this) | Python | ✅ | ✅ (direct) | ✅ | ✅ | ✅ |
|
|
389
|
+
| [oauth-cli-coder](https://github.com/codeninja/oauth-cli-coder) | Python | ✅ | ✅ (via tmux) | ❌ | ❌ | — |
|
|
390
|
+
| [coding-cli-runtime](https://pypi.org/project/coding-cli-runtime/) | Python | library only | ✅ | ❌ | ❌ | ❌ |
|
|
391
|
+
| [router-for-me/CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) | Go | ❌ (server only) | ✅ | ✅ | ✅ | ❌ |
|
|
392
|
+
| [codeking-ai/cligate](https://github.com/codeking-ai/cligate) | TypeScript | ❌ (server only) | ✅ | ✅ | — | ❌ |
|
|
393
|
+
| [PleasePrompto/ductor](https://github.com/PleasePrompto/ductor) | Python | ❌ (bot only) | ✅ | ❌ | ❌ | ❌ |
|
|
394
|
+
| [simonw/llm + llm-claude-code](https://github.com/simonw/llm) | Python | ✅ | Claude only | ❌ | ❌ | ❌ |
|
|
395
|
+
| [litellm](https://github.com/BerriAI/litellm) | Python | ❌ | direct API | ✅ | ❌ | ❌ |
|
|
396
|
+
|
|
397
|
+
**Closest neighbour**: `oauth-cli-coder` — same dual-mode idea, but uses `tmux`
|
|
398
|
+
sessions as the integration primitive (requires tmux on user's machine). This
|
|
399
|
+
project uses direct `subprocess.Popen` for a simpler deployment story
|
|
400
|
+
(stdlib-only core, no external process manager), adds the OpenAI-compatible
|
|
401
|
+
server + live dashboard + rich REPL + state-file sharing between CLI and
|
|
402
|
+
Python code.
|
|
403
|
+
|
|
404
|
+
**Closest library-only alternative**: `coding-cli-runtime` on PyPI — pure
|
|
405
|
+
Python library that wraps multiple coding CLIs per its PyPI page (verify the
|
|
406
|
+
exact set yourself). No CLI entry point, no server, no REPL.
|
|
407
|
+
|
|
408
|
+
If your use case is *just* "spawn a CLI and get text back" — `coding-cli-runtime`
|
|
409
|
+
is smaller. If you want dual-mode + richer infrastructure (state, server,
|
|
410
|
+
dashboard, REPL), this is the one.
|
|
411
|
+
|
|
412
|
+
## Project structure
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
unified_cli/
|
|
416
|
+
├── src/unified_cli/
|
|
417
|
+
│ ├── core.py # Message, Response, Usage, ModelInfo dataclasses
|
|
418
|
+
│ ├── errors.py # UnifiedError + classify() per-provider matchers
|
|
419
|
+
│ ├── discovery.py # find_{claude,codex,gemini}_bin()
|
|
420
|
+
│ ├── base.py # BaseProvider ABC + retry/fallback
|
|
421
|
+
│ ├── providers/ # claude.py, codex.py, gemini.py
|
|
422
|
+
│ ├── conversation.py # UnifiedConversation (cross-provider context)
|
|
423
|
+
│ ├── state.py # ~/.unified-cli/state.json read/write
|
|
424
|
+
│ ├── usage.py # UsageTracker (per-process aggregates)
|
|
425
|
+
│ ├── factory.py # create() + route()
|
|
426
|
+
│ ├── cli.py # doctor / setup / status / chat / repl / models
|
|
427
|
+
│ ├── repl.py # interactive REPL with slash commands
|
|
428
|
+
│ ├── server.py # FastAPI OpenAI-compat server + /dashboard
|
|
429
|
+
│ └── ui.py # rich helpers (tables, panels)
|
|
430
|
+
├── tests/ # 46 unit tests, stdlib only
|
|
431
|
+
└── examples/ # 8 runnable scripts
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## License
|
|
435
|
+
|
|
436
|
+
MIT License · Copyright (c) 2026 Minwoo Kim — see [LICENSE](LICENSE).
|
|
437
|
+
|
|
438
|
+
Anyone is free to use, modify, and redistribute this software, provided the
|
|
439
|
+
copyright notice and license text are preserved in the redistribution.
|
|
440
|
+
Personal use of provider subscriptions (Claude Pro/Max, ChatGPT Plus/Pro,
|
|
441
|
+
Google AI Pro) is your own responsibility under each provider's Terms of
|
|
442
|
+
Service — see "Known limitations" above.
|
|
443
|
+
|
|
444
|
+
## Contributing
|
|
445
|
+
|
|
446
|
+
Issues and PRs welcome. Please run `python tests/test_errors.py` (and the
|
|
447
|
+
other `tests/test_*.py`) before opening a PR — all 46 should stay green.
|