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.
Files changed (44) hide show
  1. unified_cli-0.1.0/CHANGELOG.md +51 -0
  2. unified_cli-0.1.0/LICENSE +21 -0
  3. unified_cli-0.1.0/MANIFEST.in +28 -0
  4. unified_cli-0.1.0/PKG-INFO +447 -0
  5. unified_cli-0.1.0/README.ko.md +381 -0
  6. unified_cli-0.1.0/README.md +406 -0
  7. unified_cli-0.1.0/USAGE.ko.md +458 -0
  8. unified_cli-0.1.0/USAGE.md +504 -0
  9. unified_cli-0.1.0/pyproject.toml +72 -0
  10. unified_cli-0.1.0/setup.cfg +4 -0
  11. unified_cli-0.1.0/src/unified_cli/__init__.py +76 -0
  12. unified_cli-0.1.0/src/unified_cli/base.py +510 -0
  13. unified_cli-0.1.0/src/unified_cli/cli.py +528 -0
  14. unified_cli-0.1.0/src/unified_cli/conversation.py +235 -0
  15. unified_cli-0.1.0/src/unified_cli/core.py +171 -0
  16. unified_cli-0.1.0/src/unified_cli/dashboard_tpl.py +177 -0
  17. unified_cli-0.1.0/src/unified_cli/discovery.py +87 -0
  18. unified_cli-0.1.0/src/unified_cli/errors.py +209 -0
  19. unified_cli-0.1.0/src/unified_cli/factory.py +69 -0
  20. unified_cli-0.1.0/src/unified_cli/models.py +220 -0
  21. unified_cli-0.1.0/src/unified_cli/onboarding.py +269 -0
  22. unified_cli-0.1.0/src/unified_cli/providers/__init__.py +7 -0
  23. unified_cli-0.1.0/src/unified_cli/providers/claude.py +356 -0
  24. unified_cli-0.1.0/src/unified_cli/providers/codex.py +273 -0
  25. unified_cli-0.1.0/src/unified_cli/providers/gemini.py +374 -0
  26. unified_cli-0.1.0/src/unified_cli/py.typed +0 -0
  27. unified_cli-0.1.0/src/unified_cli/repl.py +379 -0
  28. unified_cli-0.1.0/src/unified_cli/server.py +306 -0
  29. unified_cli-0.1.0/src/unified_cli/state.py +122 -0
  30. unified_cli-0.1.0/src/unified_cli/ui.py +180 -0
  31. unified_cli-0.1.0/src/unified_cli/usage.py +126 -0
  32. unified_cli-0.1.0/src/unified_cli.egg-info/PKG-INFO +447 -0
  33. unified_cli-0.1.0/src/unified_cli.egg-info/SOURCES.txt +42 -0
  34. unified_cli-0.1.0/src/unified_cli.egg-info/dependency_links.txt +1 -0
  35. unified_cli-0.1.0/src/unified_cli.egg-info/entry_points.txt +2 -0
  36. unified_cli-0.1.0/src/unified_cli.egg-info/requires.txt +15 -0
  37. unified_cli-0.1.0/src/unified_cli.egg-info/top_level.txt +1 -0
  38. unified_cli-0.1.0/tests/test_attachments.py +284 -0
  39. unified_cli-0.1.0/tests/test_errors.py +108 -0
  40. unified_cli-0.1.0/tests/test_fixes.py +118 -0
  41. unified_cli-0.1.0/tests/test_models.py +83 -0
  42. unified_cli-0.1.0/tests/test_phase1.py +219 -0
  43. unified_cli-0.1.0/tests/test_state.py +132 -0
  44. 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
+ [![PyPI version](https://img.shields.io/pypi/v/unified-cli)](https://pypi.org/project/unified-cli/)
48
+ [![Python versions](https://img.shields.io/pypi/pyversions/unified-cli)](https://pypi.org/project/unified-cli/)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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.