pidriver 0.0.2__tar.gz → 0.2.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.
- {pidriver-0.0.2 → pidriver-0.2.0}/.gitignore +3 -0
- pidriver-0.2.0/.sc/last-chat-settings.json +4 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/PKG-INFO +141 -43
- {pidriver-0.0.2 → pidriver-0.2.0}/README.md +140 -42
- {pidriver-0.0.2 → pidriver-0.2.0}/pyproject.toml +6 -1
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/__init__.py +52 -7
- pidriver-0.2.0/src/pidriver/_install.py +305 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/_transport.py +5 -3
- pidriver-0.2.0/src/pidriver/assets/pi_login.mjs +115 -0
- pidriver-0.2.0/src/pidriver/auth.py +254 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/config.py +53 -28
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/errors.py +15 -1
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/events.py +43 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/interaction.py +18 -1
- pidriver-0.2.0/src/pidriver/models.py +160 -0
- pidriver-0.2.0/src/pidriver/workspace.py +108 -0
- pidriver-0.2.0/tests/test_auth.py +81 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_config.py +2 -2
- pidriver-0.2.0/tests/test_event_summary.py +56 -0
- pidriver-0.2.0/tests/test_install.py +173 -0
- pidriver-0.2.0/tests/test_install_resolve.py +62 -0
- pidriver-0.2.0/tests/test_models.py +96 -0
- pidriver-0.2.0/tests/test_workspace.py +80 -0
- pidriver-0.0.2/src/pidriver/_install.py +0 -180
- pidriver-0.0.2/tests/test_install.py +0 -184
- {pidriver-0.0.2 → pidriver-0.2.0}/LICENSE +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/examples/basic_prompt.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/client.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/manager.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/py.typed +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/session.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/src/pidriver/usage.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/conftest.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/README.md +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/_capture_driver.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/agent_end.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/agent_start.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_request.confirm.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_request.editor.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_request.input.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_request.select.approval.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_request.select.ask.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_request.setWidget.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_response.approve.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_response.confirm.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_response.editor.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_response.input.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/extension_ui_response.value.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/message_start_assistant.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/message_start_user.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/message_update.text_delta.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/message_update.thinking_delta.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/message_update.toolcall_end.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/message_update.toolcall_start.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/ready.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/response_prompt_ack.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/tool_execution_end.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/tool_execution_start.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/tool_execution_update.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/turn_end.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/frames/turn_start.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/payloads/get_session_stats.response.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/fixtures/payloads/get_state.response.json +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_events.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_fixtures_replay.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_manager.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_replay_session.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_session.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_transport.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_ui_methods.py +0 -0
- {pidriver-0.0.2 → pidriver-0.2.0}/tests/test_usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pidriver
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Async Python driver for the pi coding agent (pi --mode rpc), built for isolated, headless project work.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bobuk/pidriver
|
|
6
6
|
Project-URL: Repository, https://github.com/bobuk/pidriver
|
|
@@ -28,9 +28,10 @@ Async Python driver for the pi coding agent, built for **isolated, headless proj
|
|
|
28
28
|
drives any `pi`-compatible CLI in `--mode rpc` over its JSONL stdin/stdout protocol and exposes a
|
|
29
29
|
small, typed Python API — with no third-party dependencies.
|
|
30
30
|
|
|
31
|
-
The
|
|
32
|
-
|
|
33
|
-
is just a `PiConfig` field
|
|
31
|
+
The default CLI is upstream [**pi**](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
32
|
+
(the `pi` binary); the [oh-my-pi](https://github.com/can1357/oh-my-pi) fork (`omp`) also works (set
|
|
33
|
+
`binary="omp"`, `mode="rpc-ui"`). Which one runs is just a `PiConfig` field, so the same code drives
|
|
34
|
+
either.
|
|
34
35
|
|
|
35
36
|
Embed an agent inside another application (a chat bot, a scheduler, a service) and have it
|
|
36
37
|
develop a project autonomously, in an environment that is **isolated** from your personal pi
|
|
@@ -41,18 +42,11 @@ handling; drop to the [raw transport](#transport) only when you need it.
|
|
|
41
42
|
|
|
42
43
|
## Requirements
|
|
43
44
|
|
|
44
|
-
- Python **3.12+**
|
|
45
|
-
- A `pi`-compatible CLI
|
|
46
|
-
[
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
bun install -g @oh-my-pi/pi-coding-agent # provides the `omp` binary
|
|
50
|
-
# or: curl -fsSL https://omp.sh/install | sh
|
|
51
|
-
omp --version
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
The original `pi` CLI works too: `npm i -g @earendil-works/pi-coding-agent`. Tell `PiConfig`
|
|
55
|
-
which one to launch with `binary` (`"omp"` or `"pi"`); they share the `--mode rpc` protocol.
|
|
45
|
+
- Python **3.12+** (no third-party runtime deps).
|
|
46
|
+
- A `pi`-compatible CLI — but you usually **don't install one manually**: pidriver can
|
|
47
|
+
[auto-install](#auto-install) upstream `pi` via `npm` on first use (requires Node ≥22.19 + npm).
|
|
48
|
+
To use an existing CLI instead, put `pi` (or `omp`) on `PATH`, or point `PiConfig.binary` /
|
|
49
|
+
`install_dir` at it.
|
|
56
50
|
- A provider API key — passed explicitly through `PiConfig`, see [Isolation](#isolation).
|
|
57
51
|
|
|
58
52
|
## Install
|
|
@@ -73,8 +67,7 @@ from pidriver import PiClient, PiConfig, AutoApprove, MessageDelta, ToolStart, A
|
|
|
73
67
|
|
|
74
68
|
async def main():
|
|
75
69
|
client = PiClient(PiConfig(
|
|
76
|
-
|
|
77
|
-
provider="openai",
|
|
70
|
+
provider="openai", # binary defaults to upstream "pi" (--mode rpc)
|
|
78
71
|
model="gpt-4o",
|
|
79
72
|
api_key=os.environ["OPENAI_API_KEY"],
|
|
80
73
|
# Isolation defaults (scrubbed env, no host extensions/skills) are already on.
|
|
@@ -144,13 +137,24 @@ PiConfig(provider="anthropic", model="claude-sonnet-4-6",
|
|
|
144
137
|
api_key="sk-...", workspace="/path/to/project")
|
|
145
138
|
```
|
|
146
139
|
|
|
147
|
-
**Binary & workspace**
|
|
140
|
+
**Binary, mode & workspace**
|
|
148
141
|
|
|
149
142
|
| Field | Default | Purpose |
|
|
150
143
|
| --- | --- | --- |
|
|
151
144
|
| `binary` | `"pi"` | Which `pi`-compatible CLI to launch — set to `"omp"` for oh-my-pi. |
|
|
145
|
+
| `mode` | `"rpc"` | Output mode → `--mode`. Upstream pi accepts `text`/`json`/`rpc`; `rpc` is the JSONL protocol this driver speaks. (`rpc-ui` is omp-only — set it explicitly when `binary="omp"`.) |
|
|
152
146
|
| `workspace` | `None` | Project directory; becomes the subprocess `cwd`. |
|
|
153
147
|
|
|
148
|
+
**Binary auto-install** — see [Auto-install](#auto-install).
|
|
149
|
+
|
|
150
|
+
| Field | Default | Purpose |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `install_dir` | `None` | Where to `npm install`/look for a managed `pi` binary. |
|
|
153
|
+
| `auto_install` | `True` | `npm install` `pi` into `install_dir` if the binary can't be resolved. |
|
|
154
|
+
| `npm_package` | `"@earendil-works/pi-coding-agent"` | The npm package that provides `pi`. |
|
|
155
|
+
| `pi_version` | `"0.78.1"` | Pinned pi version to install (overridable). |
|
|
156
|
+
| `extensions` | `()` | Add-on sources (`npm:…`, `git:…`, path) installed via `pi install` before start. |
|
|
157
|
+
|
|
154
158
|
**Model / provider**
|
|
155
159
|
|
|
156
160
|
| Field | Default | Purpose |
|
|
@@ -164,22 +168,27 @@ PiConfig(provider="anthropic", model="claude-sonnet-4-6",
|
|
|
164
168
|
Prefer a **fully-qualified** `model` id (e.g. `openai/gpt-4o`, `anthropic/claude-haiku-4-5`) —
|
|
165
169
|
bare fuzzy names can mis-route to the wrong backend.
|
|
166
170
|
|
|
171
|
+
**Approvals** — `auto_approve` (`--auto-approve`, run tools without asking), `approval_mode`
|
|
172
|
+
(`--approval-mode <mode>`, e.g. `"always-ask"`). Leave both unset to let your
|
|
173
|
+
[`InteractionHandler`](#answering-the-agent) decide.
|
|
174
|
+
|
|
167
175
|
**Tools** — `tools` (`--tools` allowlist), `exclude_tools` (`--exclude-tools`), `no_tools`,
|
|
168
176
|
`no_builtin_tools`.
|
|
169
177
|
|
|
170
178
|
**Session** — `session` (False → `--no-session`), `session_id`, `session_path` (`--session`),
|
|
171
|
-
`session_name` (`--name`), `continue_session` (`--continue`), `system_prompt
|
|
172
|
-
`append_system_prompt
|
|
179
|
+
`session_name` (`--name`), `continue_session` (`--continue`), `system_prompt`
|
|
180
|
+
(`--system-prompt`), `append_system_prompt` (`--append-system-prompt`, repeatable).
|
|
173
181
|
|
|
174
|
-
**Isolation** — see [Isolation](#isolation): `
|
|
175
|
-
`
|
|
176
|
-
`skip_version_check`, `
|
|
182
|
+
**Isolation** — see [Isolation](#isolation): `home_dir`, `agent_dir`, `session_dir`,
|
|
183
|
+
`package_dir`, `no_extensions`, `no_skills`, `no_context_files`, `no_prompt_templates`,
|
|
184
|
+
`no_themes`, `no_mcp`, `offline`, `skip_version_check`, `telemetry`, `inherit_env`,
|
|
185
|
+
`env_passthrough`.
|
|
177
186
|
|
|
178
187
|
**Escape hatches** — `extra_args` (appended to argv), `extra_env` (merged into the child env).
|
|
179
188
|
|
|
180
189
|
Methods:
|
|
181
190
|
|
|
182
|
-
- `to_argv() -> list[str]` — full command line, starting `[binary, "--mode",
|
|
191
|
+
- `to_argv() -> list[str]` — full command line, starting `[binary, "--mode", mode, ...]`.
|
|
183
192
|
- `to_env(base_environ=None) -> dict[str, str]` — the child environment (scrubbed by default).
|
|
184
193
|
- `api_key_env_name() -> str | None` — which env var `api_key` resolves to (provider-mapped via
|
|
185
194
|
`PROVIDER_API_KEY_ENV`, falling back to `<PROVIDER>_API_KEY`, then `ANTHROPIC_API_KEY`).
|
|
@@ -193,32 +202,68 @@ project from reading or mutating your global pi setup:
|
|
|
193
202
|
environment plus only `env_passthrough` (`PATH`, `HOME`, `LANG`, `LC_ALL`, `TERM`, `TZ`) — so
|
|
194
203
|
a stray `*_API_KEY` in your shell never leaks into the agent. The configured `api_key` is then
|
|
195
204
|
injected under exactly one provider variable.
|
|
196
|
-
- **Private
|
|
197
|
-
|
|
205
|
+
- **Private `HOME`.** `home_dir` overrides `HOME` for the child. This is the **load-bearing**
|
|
206
|
+
isolation lever: omp derives its config root *and logs* from `$HOME/.omp`, so `agent_dir`
|
|
207
|
+
alone isn't enough — without `home_dir` the agent still writes logs into your real `~/.omp`
|
|
208
|
+
(verified empirically against omp 15.7.3). Set `home_dir` and your personal setup stays
|
|
209
|
+
byte-for-byte untouched.
|
|
210
|
+
- **Private state dirs.** `agent_dir` → `PI_CODING_AGENT_DIR` (config/extensions/skills),
|
|
211
|
+
`session_dir` → `PI_CODING_AGENT_SESSION_DIR` (session `.jsonl`s), `package_dir` →
|
|
212
|
+
`PI_PACKAGE_DIR` (installed packages) — each points pi at a private location instead of the
|
|
213
|
+
defaults under `~/.omp`.
|
|
198
214
|
- **No host discovery** (all default **True**): `no_extensions`, `no_skills`,
|
|
199
215
|
`no_context_files`, `no_prompt_templates`, `no_themes` give a clean, reproducible agent.
|
|
200
|
-
- **
|
|
201
|
-
|
|
202
|
-
- `offline` (`--offline` + `PI_OFFLINE=1`)
|
|
203
|
-
default True) round out a hermetic run.
|
|
216
|
+
- **No MCP autoconnect** (`no_mcp=True`, default). Under `rpc-ui` the agent otherwise
|
|
217
|
+
auto-connects MCP servers it finds in the environment; `no_mcp` strips those trigger vars.
|
|
218
|
+
- `offline` (`--offline` + `PI_OFFLINE=1`), `skip_version_check` (`PI_SKIP_VERSION_CHECK=1`,
|
|
219
|
+
default True) and `telemetry=False` (`PI_TELEMETRY=0`) round out a hermetic run.
|
|
204
220
|
|
|
205
221
|
```python
|
|
222
|
+
home = Path("/var/lib/myapp/pi")
|
|
206
223
|
cfg = PiConfig(
|
|
207
|
-
provider="anthropic", model="claude-
|
|
224
|
+
provider="anthropic", model="anthropic/claude-haiku-4-5", api_key=KEY,
|
|
208
225
|
workspace="/srv/projects/acme",
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
home_dir=home / "home", # overrides $HOME — the key isolation lever
|
|
227
|
+
agent_dir=home / "agent", # PI_CODING_AGENT_DIR
|
|
228
|
+
session_dir=home / "sessions", # PI_CODING_AGENT_SESSION_DIR
|
|
229
|
+
package_dir=home / "packages", # PI_PACKAGE_DIR
|
|
230
|
+
# inherit_env=False, no_* discovery, no_mcp, telemetry=False are already the defaults.
|
|
213
231
|
)
|
|
214
232
|
```
|
|
215
233
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
## Auto-install
|
|
235
|
+
|
|
236
|
+
You don't have to install a CLI yourself. Upstream `pi` is an npm package, and pidriver can
|
|
237
|
+
`npm install` it into a managed, isolated prefix on demand (requires **Node ≥22.19 + npm** on
|
|
238
|
+
`PATH` — the `pi` bin is a Node shim, not a self-contained binary).
|
|
239
|
+
|
|
240
|
+
When the transport starts, it resolves the binary in order — explicit `binary` path →
|
|
241
|
+
`install_dir/bin/pi` → `PATH`. If none resolves and `auto_install=True` (default), it runs
|
|
242
|
+
`npm install -g --prefix <install_dir> <npm_package>@<pi_version>` and runs `<install_dir>/bin/pi`.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from pidriver import ensure_binary, install_pi, ensure_extensions # also exported for explicit use
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Extensions / skills / packages
|
|
249
|
+
|
|
250
|
+
Set `PiConfig.extensions` to a tuple of add-on sources and pidriver runs `pi install <source>`
|
|
251
|
+
for each (into the **isolated** config dir, via `PI_CODING_AGENT_DIR`) before the agent starts —
|
|
252
|
+
idempotently (sources already in the dir's `settings.json` are skipped; failures are logged, not
|
|
253
|
+
fatal). Each source is one of `npm:<pkg>[@version]`, `git:github.com/<owner>/<repo>[@ref]`, an
|
|
254
|
+
`https://`/`ssh://` git url, or a local `./path`. To actually load them, turn the relevant
|
|
255
|
+
isolation flags off (e.g. `no_extensions=False`, `no_skills=False`).
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
PiConfig(extensions=("npm:pi-web-access", "git:github.com/owner/repo"),
|
|
259
|
+
no_extensions=False, no_skills=False)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Notes:
|
|
263
|
+
- First `npm install` takes a few seconds; subsequent runs reuse the managed copy.
|
|
264
|
+
- Failures raise `PiInstallError` (missing npm/Node, install or version-verify failure).
|
|
265
|
+
- To skip auto-install, set `auto_install=False` and provide `binary`/`install_dir` pointing at an
|
|
266
|
+
existing CLI.
|
|
222
267
|
|
|
223
268
|
## Transport
|
|
224
269
|
|
|
@@ -295,7 +340,8 @@ All exceptions derive from `PiDriverError`, so one `except` catches the family:
|
|
|
295
340
|
| Exception | Raised when |
|
|
296
341
|
| --- | --- |
|
|
297
342
|
| `PiDriverError` | Base class for everything below. |
|
|
298
|
-
| `PiNotFoundError` | The `pi` executable can't be found or run. |
|
|
343
|
+
| `PiNotFoundError` | The `pi`/`omp` executable can't be found or run. |
|
|
344
|
+
| `PiInstallError` | [Auto-install](#auto-install) failed (unsupported OS/arch, download/verify error). |
|
|
299
345
|
| `PiStartError` | The `pi --mode rpc` process failed to start. |
|
|
300
346
|
| `PiProtocolError` | A line from pi wasn't valid JSON / violated the contract (`.raw`). |
|
|
301
347
|
| `PiCommandError` | An RPC command returned `success: false` (`.command`, `.error`, `.data`). |
|
|
@@ -476,6 +522,58 @@ handler = chain(
|
|
|
476
522
|
)
|
|
477
523
|
```
|
|
478
524
|
|
|
525
|
+
## Workspace jail
|
|
526
|
+
|
|
527
|
+
Run an agent loose inside a project without letting it run anywhere on disk. `resolve_project`
|
|
528
|
+
maps a user-supplied *name* to a real directory only if it is explicitly allow-listed and lives
|
|
529
|
+
physically under `root` — rejecting path separators and symlink escapes.
|
|
530
|
+
|
|
531
|
+
```python
|
|
532
|
+
from pidriver import resolve_project, list_candidate_projects, ProjectError
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
cwd = resolve_project("myrepo", root=PROJECTS_ROOT, allowlist=("myrepo", "docs"))
|
|
536
|
+
except ProjectError as exc:
|
|
537
|
+
... # empty / not allow-listed / not a dir / escapes root (incl. via symlink)
|
|
538
|
+
|
|
539
|
+
# Offer a picker: every immediate sub-dir of a root, flagged git / selected.
|
|
540
|
+
projects = list_candidate_projects(PROJECTS_ROOT, allowlist=("myrepo",)) # list[ProjectDir]
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Model discovery
|
|
544
|
+
|
|
545
|
+
`list_models` runs `pi --list-models` under the same isolation env the agent uses and parses the
|
|
546
|
+
table into typed `ModelInfo` rows; `parse_models` is the pure parser (handy for tests / cached
|
|
547
|
+
output). When a provider is set the result is narrowed to it.
|
|
548
|
+
|
|
549
|
+
```python
|
|
550
|
+
from pidriver import list_models, parse_models
|
|
551
|
+
|
|
552
|
+
models = await list_models(config, provider="anthropic") # list[ModelInfo]
|
|
553
|
+
for m in models:
|
|
554
|
+
print(m.id, m.context) # "anthropic/claude-opus-4-8" "200K"
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Provider OAuth login
|
|
558
|
+
|
|
559
|
+
Some providers authenticate via OAuth (Claude Pro/Max, Codex, …) instead of an API key. `PiOAuthLogin`
|
|
560
|
+
drives a bundled Node helper that reuses pi's own `AuthStorage` (no version skew), writing
|
|
561
|
+
credentials into the isolated `<agent_dir>/auth.json`. Requires `node` on PATH and a resolvable pi
|
|
562
|
+
install.
|
|
563
|
+
|
|
564
|
+
```python
|
|
565
|
+
from pidriver import PiOAuthLogin
|
|
566
|
+
|
|
567
|
+
login = PiOAuthLogin(config) # config.agent_dir receives auth.json
|
|
568
|
+
info = await login.start("anthropic") # {"session", "url", "instructions"}
|
|
569
|
+
# user opens info["url"], authorizes, copies the code:
|
|
570
|
+
await login.submit_code(info["session"], code) # accepts a bare code or a callback URL
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
`valid_extension_source(src)` validates a `pi install` argument, and `resolve_node_modules(config)`
|
|
574
|
+
locates pi's bundled Node libraries (package identity from `config.npm_package`, so the omp fork
|
|
575
|
+
works too) — both exported for hosts building their own extension/auth tooling.
|
|
576
|
+
|
|
479
577
|
## Development
|
|
480
578
|
|
|
481
579
|
```sh
|
|
@@ -4,9 +4,10 @@ Async Python driver for the pi coding agent, built for **isolated, headless proj
|
|
|
4
4
|
drives any `pi`-compatible CLI in `--mode rpc` over its JSONL stdin/stdout protocol and exposes a
|
|
5
5
|
small, typed Python API — with no third-party dependencies.
|
|
6
6
|
|
|
7
|
-
The
|
|
8
|
-
|
|
9
|
-
is just a `PiConfig` field
|
|
7
|
+
The default CLI is upstream [**pi**](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
8
|
+
(the `pi` binary); the [oh-my-pi](https://github.com/can1357/oh-my-pi) fork (`omp`) also works (set
|
|
9
|
+
`binary="omp"`, `mode="rpc-ui"`). Which one runs is just a `PiConfig` field, so the same code drives
|
|
10
|
+
either.
|
|
10
11
|
|
|
11
12
|
Embed an agent inside another application (a chat bot, a scheduler, a service) and have it
|
|
12
13
|
develop a project autonomously, in an environment that is **isolated** from your personal pi
|
|
@@ -17,18 +18,11 @@ handling; drop to the [raw transport](#transport) only when you need it.
|
|
|
17
18
|
|
|
18
19
|
## Requirements
|
|
19
20
|
|
|
20
|
-
- Python **3.12+**
|
|
21
|
-
- A `pi`-compatible CLI
|
|
22
|
-
[
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
bun install -g @oh-my-pi/pi-coding-agent # provides the `omp` binary
|
|
26
|
-
# or: curl -fsSL https://omp.sh/install | sh
|
|
27
|
-
omp --version
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
The original `pi` CLI works too: `npm i -g @earendil-works/pi-coding-agent`. Tell `PiConfig`
|
|
31
|
-
which one to launch with `binary` (`"omp"` or `"pi"`); they share the `--mode rpc` protocol.
|
|
21
|
+
- Python **3.12+** (no third-party runtime deps).
|
|
22
|
+
- A `pi`-compatible CLI — but you usually **don't install one manually**: pidriver can
|
|
23
|
+
[auto-install](#auto-install) upstream `pi` via `npm` on first use (requires Node ≥22.19 + npm).
|
|
24
|
+
To use an existing CLI instead, put `pi` (or `omp`) on `PATH`, or point `PiConfig.binary` /
|
|
25
|
+
`install_dir` at it.
|
|
32
26
|
- A provider API key — passed explicitly through `PiConfig`, see [Isolation](#isolation).
|
|
33
27
|
|
|
34
28
|
## Install
|
|
@@ -49,8 +43,7 @@ from pidriver import PiClient, PiConfig, AutoApprove, MessageDelta, ToolStart, A
|
|
|
49
43
|
|
|
50
44
|
async def main():
|
|
51
45
|
client = PiClient(PiConfig(
|
|
52
|
-
|
|
53
|
-
provider="openai",
|
|
46
|
+
provider="openai", # binary defaults to upstream "pi" (--mode rpc)
|
|
54
47
|
model="gpt-4o",
|
|
55
48
|
api_key=os.environ["OPENAI_API_KEY"],
|
|
56
49
|
# Isolation defaults (scrubbed env, no host extensions/skills) are already on.
|
|
@@ -120,13 +113,24 @@ PiConfig(provider="anthropic", model="claude-sonnet-4-6",
|
|
|
120
113
|
api_key="sk-...", workspace="/path/to/project")
|
|
121
114
|
```
|
|
122
115
|
|
|
123
|
-
**Binary & workspace**
|
|
116
|
+
**Binary, mode & workspace**
|
|
124
117
|
|
|
125
118
|
| Field | Default | Purpose |
|
|
126
119
|
| --- | --- | --- |
|
|
127
120
|
| `binary` | `"pi"` | Which `pi`-compatible CLI to launch — set to `"omp"` for oh-my-pi. |
|
|
121
|
+
| `mode` | `"rpc"` | Output mode → `--mode`. Upstream pi accepts `text`/`json`/`rpc`; `rpc` is the JSONL protocol this driver speaks. (`rpc-ui` is omp-only — set it explicitly when `binary="omp"`.) |
|
|
128
122
|
| `workspace` | `None` | Project directory; becomes the subprocess `cwd`. |
|
|
129
123
|
|
|
124
|
+
**Binary auto-install** — see [Auto-install](#auto-install).
|
|
125
|
+
|
|
126
|
+
| Field | Default | Purpose |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `install_dir` | `None` | Where to `npm install`/look for a managed `pi` binary. |
|
|
129
|
+
| `auto_install` | `True` | `npm install` `pi` into `install_dir` if the binary can't be resolved. |
|
|
130
|
+
| `npm_package` | `"@earendil-works/pi-coding-agent"` | The npm package that provides `pi`. |
|
|
131
|
+
| `pi_version` | `"0.78.1"` | Pinned pi version to install (overridable). |
|
|
132
|
+
| `extensions` | `()` | Add-on sources (`npm:…`, `git:…`, path) installed via `pi install` before start. |
|
|
133
|
+
|
|
130
134
|
**Model / provider**
|
|
131
135
|
|
|
132
136
|
| Field | Default | Purpose |
|
|
@@ -140,22 +144,27 @@ PiConfig(provider="anthropic", model="claude-sonnet-4-6",
|
|
|
140
144
|
Prefer a **fully-qualified** `model` id (e.g. `openai/gpt-4o`, `anthropic/claude-haiku-4-5`) —
|
|
141
145
|
bare fuzzy names can mis-route to the wrong backend.
|
|
142
146
|
|
|
147
|
+
**Approvals** — `auto_approve` (`--auto-approve`, run tools without asking), `approval_mode`
|
|
148
|
+
(`--approval-mode <mode>`, e.g. `"always-ask"`). Leave both unset to let your
|
|
149
|
+
[`InteractionHandler`](#answering-the-agent) decide.
|
|
150
|
+
|
|
143
151
|
**Tools** — `tools` (`--tools` allowlist), `exclude_tools` (`--exclude-tools`), `no_tools`,
|
|
144
152
|
`no_builtin_tools`.
|
|
145
153
|
|
|
146
154
|
**Session** — `session` (False → `--no-session`), `session_id`, `session_path` (`--session`),
|
|
147
|
-
`session_name` (`--name`), `continue_session` (`--continue`), `system_prompt
|
|
148
|
-
`append_system_prompt
|
|
155
|
+
`session_name` (`--name`), `continue_session` (`--continue`), `system_prompt`
|
|
156
|
+
(`--system-prompt`), `append_system_prompt` (`--append-system-prompt`, repeatable).
|
|
149
157
|
|
|
150
|
-
**Isolation** — see [Isolation](#isolation): `
|
|
151
|
-
`
|
|
152
|
-
`skip_version_check`, `
|
|
158
|
+
**Isolation** — see [Isolation](#isolation): `home_dir`, `agent_dir`, `session_dir`,
|
|
159
|
+
`package_dir`, `no_extensions`, `no_skills`, `no_context_files`, `no_prompt_templates`,
|
|
160
|
+
`no_themes`, `no_mcp`, `offline`, `skip_version_check`, `telemetry`, `inherit_env`,
|
|
161
|
+
`env_passthrough`.
|
|
153
162
|
|
|
154
163
|
**Escape hatches** — `extra_args` (appended to argv), `extra_env` (merged into the child env).
|
|
155
164
|
|
|
156
165
|
Methods:
|
|
157
166
|
|
|
158
|
-
- `to_argv() -> list[str]` — full command line, starting `[binary, "--mode",
|
|
167
|
+
- `to_argv() -> list[str]` — full command line, starting `[binary, "--mode", mode, ...]`.
|
|
159
168
|
- `to_env(base_environ=None) -> dict[str, str]` — the child environment (scrubbed by default).
|
|
160
169
|
- `api_key_env_name() -> str | None` — which env var `api_key` resolves to (provider-mapped via
|
|
161
170
|
`PROVIDER_API_KEY_ENV`, falling back to `<PROVIDER>_API_KEY`, then `ANTHROPIC_API_KEY`).
|
|
@@ -169,32 +178,68 @@ project from reading or mutating your global pi setup:
|
|
|
169
178
|
environment plus only `env_passthrough` (`PATH`, `HOME`, `LANG`, `LC_ALL`, `TERM`, `TZ`) — so
|
|
170
179
|
a stray `*_API_KEY` in your shell never leaks into the agent. The configured `api_key` is then
|
|
171
180
|
injected under exactly one provider variable.
|
|
172
|
-
- **Private
|
|
173
|
-
|
|
181
|
+
- **Private `HOME`.** `home_dir` overrides `HOME` for the child. This is the **load-bearing**
|
|
182
|
+
isolation lever: omp derives its config root *and logs* from `$HOME/.omp`, so `agent_dir`
|
|
183
|
+
alone isn't enough — without `home_dir` the agent still writes logs into your real `~/.omp`
|
|
184
|
+
(verified empirically against omp 15.7.3). Set `home_dir` and your personal setup stays
|
|
185
|
+
byte-for-byte untouched.
|
|
186
|
+
- **Private state dirs.** `agent_dir` → `PI_CODING_AGENT_DIR` (config/extensions/skills),
|
|
187
|
+
`session_dir` → `PI_CODING_AGENT_SESSION_DIR` (session `.jsonl`s), `package_dir` →
|
|
188
|
+
`PI_PACKAGE_DIR` (installed packages) — each points pi at a private location instead of the
|
|
189
|
+
defaults under `~/.omp`.
|
|
174
190
|
- **No host discovery** (all default **True**): `no_extensions`, `no_skills`,
|
|
175
191
|
`no_context_files`, `no_prompt_templates`, `no_themes` give a clean, reproducible agent.
|
|
176
|
-
- **
|
|
177
|
-
|
|
178
|
-
- `offline` (`--offline` + `PI_OFFLINE=1`)
|
|
179
|
-
default True) round out a hermetic run.
|
|
192
|
+
- **No MCP autoconnect** (`no_mcp=True`, default). Under `rpc-ui` the agent otherwise
|
|
193
|
+
auto-connects MCP servers it finds in the environment; `no_mcp` strips those trigger vars.
|
|
194
|
+
- `offline` (`--offline` + `PI_OFFLINE=1`), `skip_version_check` (`PI_SKIP_VERSION_CHECK=1`,
|
|
195
|
+
default True) and `telemetry=False` (`PI_TELEMETRY=0`) round out a hermetic run.
|
|
180
196
|
|
|
181
197
|
```python
|
|
198
|
+
home = Path("/var/lib/myapp/pi")
|
|
182
199
|
cfg = PiConfig(
|
|
183
|
-
provider="anthropic", model="claude-
|
|
200
|
+
provider="anthropic", model="anthropic/claude-haiku-4-5", api_key=KEY,
|
|
184
201
|
workspace="/srv/projects/acme",
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
home_dir=home / "home", # overrides $HOME — the key isolation lever
|
|
203
|
+
agent_dir=home / "agent", # PI_CODING_AGENT_DIR
|
|
204
|
+
session_dir=home / "sessions", # PI_CODING_AGENT_SESSION_DIR
|
|
205
|
+
package_dir=home / "packages", # PI_PACKAGE_DIR
|
|
206
|
+
# inherit_env=False, no_* discovery, no_mcp, telemetry=False are already the defaults.
|
|
189
207
|
)
|
|
190
208
|
```
|
|
191
209
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
210
|
+
## Auto-install
|
|
211
|
+
|
|
212
|
+
You don't have to install a CLI yourself. Upstream `pi` is an npm package, and pidriver can
|
|
213
|
+
`npm install` it into a managed, isolated prefix on demand (requires **Node ≥22.19 + npm** on
|
|
214
|
+
`PATH` — the `pi` bin is a Node shim, not a self-contained binary).
|
|
215
|
+
|
|
216
|
+
When the transport starts, it resolves the binary in order — explicit `binary` path →
|
|
217
|
+
`install_dir/bin/pi` → `PATH`. If none resolves and `auto_install=True` (default), it runs
|
|
218
|
+
`npm install -g --prefix <install_dir> <npm_package>@<pi_version>` and runs `<install_dir>/bin/pi`.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from pidriver import ensure_binary, install_pi, ensure_extensions # also exported for explicit use
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Extensions / skills / packages
|
|
225
|
+
|
|
226
|
+
Set `PiConfig.extensions` to a tuple of add-on sources and pidriver runs `pi install <source>`
|
|
227
|
+
for each (into the **isolated** config dir, via `PI_CODING_AGENT_DIR`) before the agent starts —
|
|
228
|
+
idempotently (sources already in the dir's `settings.json` are skipped; failures are logged, not
|
|
229
|
+
fatal). Each source is one of `npm:<pkg>[@version]`, `git:github.com/<owner>/<repo>[@ref]`, an
|
|
230
|
+
`https://`/`ssh://` git url, or a local `./path`. To actually load them, turn the relevant
|
|
231
|
+
isolation flags off (e.g. `no_extensions=False`, `no_skills=False`).
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
PiConfig(extensions=("npm:pi-web-access", "git:github.com/owner/repo"),
|
|
235
|
+
no_extensions=False, no_skills=False)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Notes:
|
|
239
|
+
- First `npm install` takes a few seconds; subsequent runs reuse the managed copy.
|
|
240
|
+
- Failures raise `PiInstallError` (missing npm/Node, install or version-verify failure).
|
|
241
|
+
- To skip auto-install, set `auto_install=False` and provide `binary`/`install_dir` pointing at an
|
|
242
|
+
existing CLI.
|
|
198
243
|
|
|
199
244
|
## Transport
|
|
200
245
|
|
|
@@ -271,7 +316,8 @@ All exceptions derive from `PiDriverError`, so one `except` catches the family:
|
|
|
271
316
|
| Exception | Raised when |
|
|
272
317
|
| --- | --- |
|
|
273
318
|
| `PiDriverError` | Base class for everything below. |
|
|
274
|
-
| `PiNotFoundError` | The `pi` executable can't be found or run. |
|
|
319
|
+
| `PiNotFoundError` | The `pi`/`omp` executable can't be found or run. |
|
|
320
|
+
| `PiInstallError` | [Auto-install](#auto-install) failed (unsupported OS/arch, download/verify error). |
|
|
275
321
|
| `PiStartError` | The `pi --mode rpc` process failed to start. |
|
|
276
322
|
| `PiProtocolError` | A line from pi wasn't valid JSON / violated the contract (`.raw`). |
|
|
277
323
|
| `PiCommandError` | An RPC command returned `success: false` (`.command`, `.error`, `.data`). |
|
|
@@ -452,6 +498,58 @@ handler = chain(
|
|
|
452
498
|
)
|
|
453
499
|
```
|
|
454
500
|
|
|
501
|
+
## Workspace jail
|
|
502
|
+
|
|
503
|
+
Run an agent loose inside a project without letting it run anywhere on disk. `resolve_project`
|
|
504
|
+
maps a user-supplied *name* to a real directory only if it is explicitly allow-listed and lives
|
|
505
|
+
physically under `root` — rejecting path separators and symlink escapes.
|
|
506
|
+
|
|
507
|
+
```python
|
|
508
|
+
from pidriver import resolve_project, list_candidate_projects, ProjectError
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
cwd = resolve_project("myrepo", root=PROJECTS_ROOT, allowlist=("myrepo", "docs"))
|
|
512
|
+
except ProjectError as exc:
|
|
513
|
+
... # empty / not allow-listed / not a dir / escapes root (incl. via symlink)
|
|
514
|
+
|
|
515
|
+
# Offer a picker: every immediate sub-dir of a root, flagged git / selected.
|
|
516
|
+
projects = list_candidate_projects(PROJECTS_ROOT, allowlist=("myrepo",)) # list[ProjectDir]
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Model discovery
|
|
520
|
+
|
|
521
|
+
`list_models` runs `pi --list-models` under the same isolation env the agent uses and parses the
|
|
522
|
+
table into typed `ModelInfo` rows; `parse_models` is the pure parser (handy for tests / cached
|
|
523
|
+
output). When a provider is set the result is narrowed to it.
|
|
524
|
+
|
|
525
|
+
```python
|
|
526
|
+
from pidriver import list_models, parse_models
|
|
527
|
+
|
|
528
|
+
models = await list_models(config, provider="anthropic") # list[ModelInfo]
|
|
529
|
+
for m in models:
|
|
530
|
+
print(m.id, m.context) # "anthropic/claude-opus-4-8" "200K"
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Provider OAuth login
|
|
534
|
+
|
|
535
|
+
Some providers authenticate via OAuth (Claude Pro/Max, Codex, …) instead of an API key. `PiOAuthLogin`
|
|
536
|
+
drives a bundled Node helper that reuses pi's own `AuthStorage` (no version skew), writing
|
|
537
|
+
credentials into the isolated `<agent_dir>/auth.json`. Requires `node` on PATH and a resolvable pi
|
|
538
|
+
install.
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
from pidriver import PiOAuthLogin
|
|
542
|
+
|
|
543
|
+
login = PiOAuthLogin(config) # config.agent_dir receives auth.json
|
|
544
|
+
info = await login.start("anthropic") # {"session", "url", "instructions"}
|
|
545
|
+
# user opens info["url"], authorizes, copies the code:
|
|
546
|
+
await login.submit_code(info["session"], code) # accepts a bare code or a callback URL
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
`valid_extension_source(src)` validates a `pi install` argument, and `resolve_node_modules(config)`
|
|
550
|
+
locates pi's bundled Node libraries (package identity from `config.npm_package`, so the omp fork
|
|
551
|
+
works too) — both exported for hosts building their own extension/auth tooling.
|
|
552
|
+
|
|
455
553
|
## Development
|
|
456
554
|
|
|
457
555
|
```sh
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pidriver"
|
|
3
|
-
version = "0.0
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Async Python driver for the pi coding agent (pi --mode rpc), built for isolated, headless project work."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -36,6 +36,11 @@ build-backend = "hatchling.build"
|
|
|
36
36
|
[tool.hatch.build.targets.wheel]
|
|
37
37
|
packages = ["src/pidriver"]
|
|
38
38
|
|
|
39
|
+
# Ship the Node OAuth helper (pidriver.auth loads it via importlib.resources). Explicit
|
|
40
|
+
# so the data file lands in the wheel regardless of default file selection.
|
|
41
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
42
|
+
"src/pidriver/assets/pi_login.mjs" = "pidriver/assets/pi_login.mjs"
|
|
43
|
+
|
|
39
44
|
[tool.pytest.ini_options]
|
|
40
45
|
asyncio_mode = "auto"
|
|
41
46
|
testpaths = ["tests"]
|