pdo-agent 2.0.0__py3-none-any.whl

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.
pdo/tools/web.py ADDED
@@ -0,0 +1,163 @@
1
+ """Web tools: fetch a URL, search the web, and make raw HTTP requests.
2
+
3
+ Uses the standard library only (``urllib``) so there's no extra dependency.
4
+ Web search scrapes DuckDuckGo's HTML endpoint (no API key); it's best-effort and
5
+ degrades gracefully if the page format changes.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import html
10
+ import re
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+ from typing import Any
15
+
16
+ from .base import Tool, truncate
17
+ from .registry import register_tool
18
+
19
+ # A browser-like UA; some endpoints (e.g. DuckDuckGo) reject non-browser agents.
20
+ _HEADERS = {
21
+ "User-Agent": (
22
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
23
+ "(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
24
+ )
25
+ }
26
+
27
+
28
+ def _http_get(url: str, timeout: int = 20) -> tuple[int, str]:
29
+ request = urllib.request.Request(url, headers=_HEADERS)
30
+ with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310
31
+ charset = response.headers.get_content_charset() or "utf-8"
32
+ return response.status, response.read().decode(charset, "replace")
33
+
34
+
35
+ def _http_post_form(url: str, fields: dict[str, str], timeout: int = 20) -> tuple[int, str]:
36
+ data = urllib.parse.urlencode(fields).encode("utf-8")
37
+ request = urllib.request.Request(url, data=data, headers=_HEADERS)
38
+ with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310
39
+ charset = response.headers.get_content_charset() or "utf-8"
40
+ return response.status, response.read().decode(charset, "replace")
41
+
42
+
43
+ def _html_to_text(markup: str) -> str:
44
+ text = re.sub(r"(?is)<(script|style)\b.*?</\1>", " ", markup)
45
+ text = re.sub(r"(?s)<[^>]+>", " ", text)
46
+ text = html.unescape(text)
47
+ return re.sub(r"[ \t]+", " ", text).strip()
48
+
49
+
50
+ def _ddg_unwrap(href: str) -> str:
51
+ match = re.search(r"uddg=([^&]+)", href)
52
+ if match:
53
+ return urllib.parse.unquote(match.group(1))
54
+ if href.startswith("//"):
55
+ return "https:" + href
56
+ return href
57
+
58
+
59
+ def _parse_ddg_results(markup: str, limit: int) -> list[tuple[str, str]]:
60
+ results: list[tuple[str, str]] = []
61
+ for match in re.finditer(
62
+ r'<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)</a>', markup, re.S
63
+ ):
64
+ title = _html_to_text(match.group(2))
65
+ url = _ddg_unwrap(match.group(1))
66
+ if title and url:
67
+ results.append((title, url))
68
+ if len(results) >= limit:
69
+ break
70
+ return results
71
+
72
+
73
+ @register_tool
74
+ class WebFetchTool(Tool):
75
+ name = "web_fetch"
76
+ description = "Fetch a web page and return its readable text content."
77
+ parameters = {
78
+ "type": "object",
79
+ "properties": {"url": {"type": "string", "description": "The URL to fetch."}},
80
+ "required": ["url"],
81
+ }
82
+
83
+ def run(self, url: str, **_: Any) -> str:
84
+ if not url.startswith(("http://", "https://")):
85
+ url = "https://" + url
86
+ try:
87
+ status, body = _http_get(url)
88
+ except Exception as exc: # noqa: BLE001 — report network errors to the model
89
+ return f"Error fetching {url}: {exc}"
90
+ return f"[HTTP {status}] {url}\n\n{truncate(_html_to_text(body))}"
91
+
92
+
93
+ @register_tool
94
+ class WebSearchTool(Tool):
95
+ name = "web_search"
96
+ description = "Search the web (via DuckDuckGo) and return the top result titles and URLs."
97
+ parameters = {
98
+ "type": "object",
99
+ "properties": {
100
+ "query": {"type": "string", "description": "The search query."},
101
+ "max_results": {
102
+ "type": "integer",
103
+ "description": "Number of results to return (default 5).",
104
+ },
105
+ },
106
+ "required": ["query"],
107
+ }
108
+
109
+ def run(self, query: str, max_results: int = 5, **_: Any) -> str:
110
+ # DuckDuckGo's HTML endpoint requires a POST form and a browser UA.
111
+ try:
112
+ _, body = _http_post_form("https://html.duckduckgo.com/html/", {"q": query})
113
+ except Exception as exc: # noqa: BLE001
114
+ return f"Error searching: {exc}"
115
+ results = _parse_ddg_results(body, max(1, max_results))
116
+ if not results:
117
+ return "No results found (or the search page format changed)."
118
+ return "\n".join(f"{i}. {title}\n {link}" for i, (title, link) in enumerate(results, 1))
119
+
120
+
121
+ @register_tool
122
+ class HttpRequestTool(Tool):
123
+ name = "http_request"
124
+ description = (
125
+ "Make an HTTP request to an API and return the status and response body. "
126
+ "Supports GET/POST/PUT/PATCH/DELETE with optional headers and body."
127
+ )
128
+ parameters = {
129
+ "type": "object",
130
+ "properties": {
131
+ "url": {"type": "string"},
132
+ "method": {"type": "string", "description": "HTTP method (default GET)."},
133
+ "headers": {"type": "object", "description": "Optional request headers."},
134
+ "body": {
135
+ "type": "string",
136
+ "description": "Optional request body (raw string or JSON text).",
137
+ },
138
+ },
139
+ "required": ["url"],
140
+ }
141
+
142
+ def run(
143
+ self,
144
+ url: str,
145
+ method: str = "GET",
146
+ headers: dict[str, str] | None = None,
147
+ body: str | None = None,
148
+ **_: Any,
149
+ ) -> str:
150
+ data = body.encode("utf-8") if isinstance(body, str) else None
151
+ request = urllib.request.Request(
152
+ url, data=data, method=method.upper(), headers={**_HEADERS, **(headers or {})}
153
+ )
154
+ try:
155
+ with urllib.request.urlopen(request, timeout=30) as response: # noqa: S310
156
+ charset = response.headers.get_content_charset() or "utf-8"
157
+ text = response.read().decode(charset, "replace")
158
+ return f"[HTTP {response.status}]\n{truncate(text)}"
159
+ except urllib.error.HTTPError as exc:
160
+ detail = exc.read().decode("utf-8", "replace")
161
+ return f"[HTTP {exc.code}] {exc.reason}\n{truncate(detail)}"
162
+ except Exception as exc: # noqa: BLE001
163
+ return f"Error: {exc}"
@@ -0,0 +1,456 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdo-agent
3
+ Version: 2.0.0
4
+ Summary: PDO (Python Do) — a terminal-first AI agent that reasons, plans, and safely executes real tasks.
5
+ Author: PDO Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/uaedoom/pdo
8
+ Project-URL: Repository, https://github.com/uaedoom/pdo
9
+ Project-URL: Issues, https://github.com/uaedoom/pdo/issues
10
+ Keywords: ai,agent,terminal,cli,llm,openai,automation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: openai<2,>=1.30
22
+ Requires-Dist: python-dotenv<2,>=1.0
23
+ Requires-Dist: rich<14,>=13.7
24
+ Requires-Dist: pydantic<3,>=2.6
25
+ Requires-Dist: prompt_toolkit<4,>=3.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest<9,>=8.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.4; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ <h1 align="center">PDO — Python Do</h1>
32
+
33
+ <div align="center">
34
+ <pre>
35
+ ████████ ██████ ████████
36
+ ██ ██ ██ ██ ██ ██
37
+ ████████ ██ ██ ██ ██
38
+ ██ ██ ██ ██ ██
39
+ ██ ██████ ████████
40
+ </pre>
41
+ </div>
42
+
43
+ <p align="center"><b>Think. Plan. Do.</b><br/><sub>The same pixel-art logo greets you on every launch.</sub></p>
44
+
45
+ <p align="center">
46
+ <img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg">
47
+ <img alt="Python 3.12+" src="https://img.shields.io/badge/Python-3.12%2B-blue.svg">
48
+ <img alt="Open Source" src="https://img.shields.io/badge/Open%20Source-%E2%9D%A4-red.svg">
49
+ </p>
50
+
51
+ > **PDO is free and open source** (MIT licensed). Contributions are welcome —
52
+ > see [CONTRIBUTING.md](CONTRIBUTING.md). Star the repo if you find it useful! ⭐
53
+
54
+ PDO is a terminal-first AI agent that completes real tasks — it doesn't just
55
+ answer questions. Give it a goal and it reasons about it, plans the steps,
56
+ decides whether tools are needed, executes them **safely**, reviews the result,
57
+ and replies clearly. When a plain answer is enough, it just answers; it never
58
+ reaches for a tool it doesn't need.
59
+
60
+ ```
61
+ you ▸ list all markdown files in this repo and summarise the README
62
+ 🔧 run_shell(command='find . -name "*.md"')
63
+ 🔧 read_file(path='README.md')
64
+ PDO Here are the Markdown files… and a three-line summary of the README…
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Features
70
+
71
+ - **ReAct-style agent loop** built on the LLM's **native function/tool calling** —
72
+ the model picks tools and arguments; PDO executes them and feeds results back
73
+ until the task is done. Tool calls are never parsed out of free text.
74
+ - **Many providers** — OpenAI, Anthropic, OpenRouter, local **Ollama**, or any
75
+ OpenAI-compatible endpoint. Switch provider and model at runtime with `/models`
76
+ (live model listing). The core depends only on an `LLMClient` interface.
77
+ - **18 built-in tools** — filesystem (read / write / append / **edit** / list /
78
+ mkdir), shell (dangerous-command guard), code search (**glob** / **grep**),
79
+ **git**, **web search & fetch**, **HTTP**, **Python exec**, **SQLite**, and
80
+ long-term memory (save / search / delete).
81
+ - **Extensible** three ways without touching the core:
82
+ - **Plugins** — drop a `Tool` subclass in `<PDO_HOME>/plugins/` (or ship one via
83
+ the `pdo.plugins` entry-point group).
84
+ - **Skills** — Markdown prompt recipes become slash commands (`/review`, …).
85
+ - **MCP** — connect any Model Context Protocol server; its tools appear as
86
+ `mcp__<server>__<tool>`.
87
+ - **Conversation management** — named **sessions** (`/new`, `/resume`), automatic
88
+ **summarisation** of long history, `@file` references (text **and images** for
89
+ vision models), and `/export`.
90
+ - **Sub-agents** — a `delegate_task` tool spawns a fresh child agent for
91
+ self-contained subtasks, keeping the main context small on big jobs.
92
+ - **Codebase search** — `/index` builds a local BM25 index of your project; the
93
+ agent then uses `codebase_search` to find relevant code with `path:line` refs
94
+ (no embeddings API needed — works fully offline).
95
+ - **Safety & control** — typed confirmation for destructive commands, working-dir
96
+ write sandbox, per-tool **permission policies**, and a structured **audit log**.
97
+ - **Polished terminal UX** — pixel-art splash, a bordered input box with slash
98
+ autocomplete, Markdown rendering, a thinking spinner, color **themes**, and a
99
+ live token-usage footer.
100
+ - **Scriptable** — one-shot mode (`pdo "prompt"`), `--json` output, and a Docker
101
+ image.
102
+ - **Tested & CI-ready** — a `pytest` suite that mocks the LLM (no API key needed)
103
+ and a GitHub Actions workflow.
104
+
105
+ ---
106
+
107
+ ## Installation
108
+
109
+ > [!IMPORTANT]
110
+ > **PDO requires Python 3.12+.** Create the virtual environment with a 3.12
111
+ > interpreter explicitly — don't rely on the system `python3` (macOS ships 3.9,
112
+ > which will not work).
113
+
114
+ ```bash
115
+ # 1. Clone
116
+ git clone https://github.com/uaedoom/pdo.git
117
+ cd pdo
118
+
119
+ # 2. Create a virtual environment WITH Python 3.12+
120
+ python3.12 -m venv .venv # macOS (Homebrew): brew install python@3.12
121
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
122
+
123
+ # 3. Upgrade pip, then install (editable, with dev extras)
124
+ python -m pip install --upgrade pip
125
+ pip install -e ".[dev]"
126
+ ```
127
+
128
+ This installs the `pdo` console command. Verify with `python --version`
129
+ (should be 3.12.x) and `pdo --version`.
130
+
131
+ ### Notes for users / troubleshooting
132
+
133
+ - **`ERROR: ... Directory cannot be installed in editable mode` / "requires a
134
+ setuptools-based build"** — your virtual environment is on an old Python (and
135
+ old pip). Recreate it with Python 3.12 and upgrade pip:
136
+ ```bash
137
+ deactivate; rm -rf .venv
138
+ python3.12 -m venv .venv && source .venv/bin/activate
139
+ python -m pip install --upgrade pip
140
+ pip install -e ".[dev]"
141
+ ```
142
+ - **No `python3.12`?** Install it first: macOS `brew install python@3.12`,
143
+ Ubuntu `sudo apt install python3.12 python3.12-venv`.
144
+ - **Not yet on PyPI** — install by cloning as above (`pip install pdo` isn't
145
+ available yet).
146
+ - **Tested on macOS and Linux.** Windows should work but is less tested.
147
+ - Runtime data (memory, sessions, logs) lives in `~/.pdo` if you set
148
+ `PDO_HOME=~/.pdo`; otherwise it defaults to the package directory.
149
+
150
+ ---
151
+
152
+ ## Quick Start
153
+
154
+ ```bash
155
+ # Set your API key (or copy .env.example to .env and fill it in)
156
+ export OPENAI_API_KEY=sk-...
157
+
158
+ # Optionally choose a model (gpt-4.1-mini is the default)
159
+ export OPENAI_MODEL=gpt-4.1-mini
160
+
161
+ # Run it interactively
162
+ pdo
163
+
164
+ # …or one-shot (great for scripts / pipes)
165
+ pdo "list all markdown files and summarise the README"
166
+ pdo --json "what is 2+2" # machine-readable output
167
+ pdo --version
168
+ ```
169
+
170
+ Interactive mode gives you a prompt; type a goal, or a slash command (`/help`).
171
+ Replies render as Markdown, with a thinking spinner and a Codex-style activity
172
+ log. Switch colors live with `/theme green` (or set `PDO_THEME`).
173
+
174
+ ### Configuration
175
+
176
+ All configuration is read from the environment (a `.env` file is auto-loaded):
177
+
178
+ | Variable | Default | Description |
179
+ | ----------------- | ---------------- | --------------------------------------------- |
180
+ | `OPENAI_API_KEY` | *(required)* | Your API key (OpenAI **or** OpenRouter, etc.). |
181
+ | `OPENAI_MODEL` | `gpt-4.1-mini` | Model to use. |
182
+ | `OPENAI_BASE_URL` | *(OpenAI)* | API endpoint. Set to use an OpenAI-compatible provider. |
183
+ | `TEMPERATURE` | `0.2` | Sampling temperature (0–2). |
184
+ | `PDO_HOME` | package dir | Where memory and logs are stored (e.g. `~/.pdo`). |
185
+
186
+ PDO fails fast with a friendly message if `OPENAI_API_KEY` is missing.
187
+
188
+ ### Using OpenRouter (or other OpenAI-compatible APIs)
189
+
190
+ PDO talks to any OpenAI-compatible endpoint — just point `OPENAI_BASE_URL` at it
191
+ and use that provider's key and model names. For [OpenRouter](https://openrouter.ai):
192
+
193
+ ```bash
194
+ export OPENAI_API_KEY=sk-or-... # your OpenRouter key
195
+ export OPENAI_BASE_URL=https://openrouter.ai/api/v1
196
+ export OPENAI_MODEL=openai/gpt-4.1-mini # any OpenRouter model id
197
+ pdo
198
+ ```
199
+
200
+ The same pattern works for local servers (e.g. Ollama/LM Studio at
201
+ `http://localhost:11434/v1`). The model must support **tool/function calling**
202
+ for PDO's agent loop to use tools.
203
+
204
+ ---
205
+
206
+ ## Example Usage
207
+
208
+ ```text
209
+ you ▸ build a minimal Flask API in ./hello-api
210
+ you ▸ explain this repository
211
+ you ▸ fix this Python error: <paste traceback>
212
+ you ▸ list all markdown files
213
+ you ▸ create a README for this project
214
+ ```
215
+
216
+ ### Terminal commands
217
+
218
+ | Command | What it does |
219
+ | ----------- | ------------------------------------- |
220
+ | `/help` | Show available commands |
221
+ | `/models` | Switch provider & model (OpenAI / Anthropic / OpenRouter / Ollama) |
222
+ | `/tools` | List registered tools |
223
+ | `/mcp` | Show connected MCP servers and their tools |
224
+ | `/theme` | Change the color theme (e.g. `/theme green`) |
225
+ | `/export` | Save the conversation to a Markdown file |
226
+ | `/sessions` | List saved conversation sessions |
227
+ | `/new` | Start a new session (e.g. `/new feature-x`) |
228
+ | `/resume` | Switch to another session (e.g. `/resume default`) |
229
+ | `/memory` | Show saved facts and preferences |
230
+ | `/history` | Show recent conversation history |
231
+ | `/clear` | Clear the current session's history |
232
+ | `/version` | Show the PDO version |
233
+ | `/exit` | Quit |
234
+
235
+ ### Switching models at runtime
236
+
237
+ Type `/models` to pick a provider (OpenAI, Anthropic, or OpenRouter) and a model
238
+ interactively — the change applies immediately for the rest of the session. If a
239
+ key for the chosen provider isn't in your environment, PDO prompts for one and
240
+ keeps it in memory for the session only (never written to disk). Set
241
+ `ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY` in your `.env` to skip the prompt.
242
+
243
+ ---
244
+
245
+ ## Project Structure
246
+
247
+ ```
248
+ pdo/
249
+ ├─ pyproject.toml # Packaging + console script (pdo = pdo.main:main)
250
+ ├─ requirements.txt # Convenience mirror of runtime deps
251
+ ├─ .env.example # Sample configuration
252
+ ├─ .github/workflows/ci.yml
253
+ ├─ src/pdo/
254
+ │ ├─ main.py # Terminal entry point + REPL + slash commands
255
+ │ ├─ config.py # Env-based config, validated with pydantic
256
+ │ ├─ llm.py # LLMClient interface + OpenAI implementation
257
+ │ ├─ logging_setup.py # Rotating file logging for the `pdo` namespace
258
+ │ ├─ agent/
259
+ │ │ ├─ core.py # Coordinates components; runs the ReAct loop
260
+ │ │ ├─ planner.py # Breaks a goal into steps (thin, advisory)
261
+ │ │ ├─ router.py # Plain chat vs. tool use (thin; model decides)
262
+ │ │ ├─ executor.py # Runs approved tool calls, safely
263
+ │ │ ├─ reviewer.py # Sanity-checks the final answer
264
+ │ │ ├─ memory.py # Local JSON memory store
265
+ │ │ └─ messages.py # Message/ToolCall dataclasses
266
+ │ ├─ tools/
267
+ │ │ ├─ base.py # Tool base class + confirmation helper
268
+ │ │ ├─ registry.py # The single tool registry + auto-registration
269
+ │ │ ├─ filesystem.py # read / write / append / list / mkdir
270
+ │ │ ├─ shell.py # run command + dangerous-command detector
271
+ │ │ └─ memory.py # save / search / delete memory tools
272
+ │ ├─ prompts/system.md # The system prompt
273
+ │ ├─ data/ # Runtime JSON state (memory.json, history.json)
274
+ │ └─ logs/ # Rotating logs (pdo.log)
275
+ ├─ tests/ # pytest suite (LLM is mocked)
276
+ └─ docs/ # Architecture notes
277
+ ```
278
+
279
+ > **Where is my data?** By default PDO stores `memory.json`, `history.json` and
280
+ > logs inside the installed package directory so a fresh clone works immediately.
281
+ > Set `PDO_HOME=~/.pdo` to keep that state in your home directory instead.
282
+
283
+ ---
284
+
285
+ ## Adding New Tools
286
+
287
+ A tool is a small class. Subclass `Tool`, declare a JSON parameter schema, and
288
+ decorate it with `@register_tool` — that's it. The agent picks it up
289
+ automatically; you never touch the core.
290
+
291
+ ```python
292
+ # src/pdo/tools/clock.py
293
+ from datetime import datetime
294
+ from typing import Any
295
+
296
+ from .base import Tool
297
+ from .registry import register_tool
298
+
299
+
300
+ @register_tool
301
+ class CurrentTimeTool(Tool):
302
+ name = "current_time"
303
+ description = "Return the current local date and time."
304
+ parameters = {"type": "object", "properties": {}}
305
+
306
+ def run(self, **_: Any) -> str:
307
+ return datetime.now().isoformat(timespec="seconds")
308
+ ```
309
+
310
+ For a built-in tool, add the module to the lazy import in
311
+ `tools/registry.py:get_registry`. For tools that perform sensitive actions,
312
+ accept an injectable `confirm` callback (see `tools/filesystem.py`) so they can
313
+ be tested deterministically and prompt the user when needed.
314
+
315
+ ### Plugins (no fork required)
316
+
317
+ You don't have to edit PDO to add a tool. PDO **auto-discovers plugins** on
318
+ startup from two places:
319
+
320
+ 1. **A plugins directory** — drop a `.py` file defining a `Tool` subclass into
321
+ your plugins folder (run `/tools` to see its path; default
322
+ `<PDO_HOME>/plugins`). The `@register_tool` decorator is optional there — PDO
323
+ finds `Tool` subclasses automatically. A ready-made example lives in
324
+ [`examples/plugins/current_time_tool.py`](examples/plugins/current_time_tool.py):
325
+
326
+ ```bash
327
+ mkdir -p ~/.pdo/plugins # if you run with PDO_HOME=~/.pdo
328
+ cp examples/plugins/current_time_tool.py ~/.pdo/plugins/
329
+ pdo # the `current_time` tool is now available
330
+ ```
331
+
332
+ 2. **Installed packages** — a third-party package can advertise tools via the
333
+ `pdo.plugins` entry-point group. Each entry point may resolve to a `Tool`
334
+ subclass or a `register(registry)` callable:
335
+
336
+ ```toml
337
+ # in the plugin package's pyproject.toml
338
+ [project.entry-points."pdo.plugins"]
339
+ my_tool = "my_package.tools:MyTool"
340
+ ```
341
+
342
+ A broken plugin is logged and skipped — it never crashes PDO.
343
+
344
+ ### Skills (reusable prompt commands)
345
+
346
+ Drop a Markdown file in your skills directory (`<PDO_HOME>/skills/`) and it
347
+ becomes a slash command named after the file. An optional first line
348
+ `description: …` (or `# Title`) sets the menu text, and `{{args}}` interpolates
349
+ whatever you type after the command. Example
350
+ [`examples/skills/review.md`](examples/skills/review.md) becomes `/review`:
351
+
352
+ ```bash
353
+ mkdir -p ~/.pdo/skills
354
+ cp examples/skills/review.md ~/.pdo/skills/
355
+ pdo # now type: /review the auth module
356
+ ```
357
+
358
+ ### Referencing files with `@`
359
+
360
+ In any message, mention a file with `@path` and PDO inlines its contents for the
361
+ model — e.g. `explain @src/pdo/main.py` or `fix the bug in @app.py`.
362
+
363
+ **Images too:** `@screenshot.png` (png/jpg/gif/webp) attaches the image itself,
364
+ so vision-capable models can see it — e.g. `what's wrong in this UI? @shot.png`.
365
+
366
+ ### Multi-line input
367
+
368
+ Press **Enter** to send. Press **Option/Alt+Enter** (⌥⏎) to insert a newline and
369
+ compose a multi-line message before sending.
370
+
371
+ ### MCP servers (Model Context Protocol)
372
+
373
+ PDO is an MCP client: connect any MCP server and its tools become available to
374
+ the agent automatically (named `mcp__<server>__<tool>`). Declare servers in
375
+ `<PDO_HOME>/mcp.json` using the standard format (see
376
+ [`examples/mcp.json`](examples/mcp.json)):
377
+
378
+ ```json
379
+ {
380
+ "mcpServers": {
381
+ "filesystem": {
382
+ "command": "npx",
383
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
384
+ }
385
+ }
386
+ }
387
+ ```
388
+
389
+ Servers start on launch (over the stdio transport); run `/mcp` to see what's
390
+ connected. A server that fails to start is reported and skipped — it never
391
+ crashes PDO. No extra Python dependencies are required.
392
+
393
+ ### PDO *as* an MCP server / SDK
394
+
395
+ It works both ways — `pdo --serve` exposes the whole agent as an **MCP server**
396
+ over stdio, so Claude Desktop, Claude Code, or any MCP client can call its
397
+ `run_task` tool and get back the agent's final answer (with all of PDO's tools,
398
+ sub-agents, and codebase search behind it). For example, in a client's
399
+ `mcp.json`:
400
+
401
+ ```json
402
+ { "mcpServers": { "pdo": { "command": "pdo", "args": ["--serve"] } } }
403
+ ```
404
+
405
+ In serve mode nothing is printed to stdout and interactive confirmations are
406
+ auto-denied (dangerous commands are refused rather than prompted).
407
+
408
+ And from Python scripts, embed the agent directly:
409
+
410
+ ```python
411
+ from pdo import run_agent
412
+
413
+ answer = run_agent("list the markdown files here and summarise the README")
414
+ ```
415
+
416
+ `run_agent` accepts overrides (`model=`, `base_url=`, `api_key=`,
417
+ `temperature=`, or a custom `llm=` client) and uses an ephemeral memory so it
418
+ never touches your interactive sessions.
419
+
420
+ ---
421
+
422
+ ## Roadmap
423
+
424
+ **v1 — done**
425
+ - Native tool-calling agent loop; multi-provider (OpenAI / Anthropic / OpenRouter
426
+ / Ollama); 18 built-in tools; plugins, skills, and MCP client; named sessions +
427
+ auto-summary; permission policies + audit log; themed TUI with one-shot/JSON
428
+ modes; tests + CI; Docker.
429
+
430
+ **v2 (this release) — done**
431
+ - Multi-line input and image/vision attachments (`@image.png`).
432
+ - Sub-agents (`delegate_task`) and codebase retrieval (`/index` +
433
+ `codebase_search`, BM25 — fully offline).
434
+ - `pdo --serve` (PDO as an MCP server) and the `run_agent` Python API.
435
+
436
+ **Next**
437
+ - Embedding-based retrieval as an optional upgrade to the BM25 index.
438
+ - Streamed responses inside serve mode; richer sub-agent orchestration.
439
+ - PyPI publication and a Homebrew formula.
440
+
441
+ Each of these arrives as a new `Tool` (or `LLMClient`) — by design, none require
442
+ changes to the core.
443
+
444
+ ---
445
+
446
+ ## Contributing
447
+
448
+ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for setup,
449
+ coding standards, and how to add tools. Please run `ruff check .` and `pytest`
450
+ before opening a pull request.
451
+
452
+ ---
453
+
454
+ ## License
455
+
456
+ [MIT](LICENSE) © PDO Contributors
@@ -0,0 +1,42 @@
1
+ pdo/__init__.py,sha256=Xxr-opmHP9x5hfJUTCszt4hviHsr2a8rOBrYqA4NMEc,726
2
+ pdo/api.py,sha256=nYRAnhgzZ3Yba-pEt-yLqBkHUne8CtelH7vtX38vkw8,2057
3
+ pdo/banner.py,sha256=Gono50iLQKhQI3O8Yi1Vfe6oARQHMuC5C2Ddr8CTCuY,1280
4
+ pdo/config.py,sha256=2088w1617WFUO9khvyL2FmVPDPTquVKPtzYMfR7ksB4,5497
5
+ pdo/llm.py,sha256=YRWIugBB8Di-ZxteRjA_0WHhnwYG3qlTysYVm1CXgGw,8029
6
+ pdo/logging_setup.py,sha256=WHzOYvLhZO6tHyk_NoBSjVFG32AToa443PL76rZ64NY,1119
7
+ pdo/main.py,sha256=icdsIJE6Q9VzkhhZXkuIZiYCkgTHaGGOdQU4lykgeDY,34868
8
+ pdo/mcp.py,sha256=fM-AxV7LJ0nF_whMb2bse5_zBvH6Y-u56Ex95wozJI4,9726
9
+ pdo/providers.py,sha256=LHcMhihi00B3u_Ucwgd4LhwR89j5sK4FVnCkYqjwru0,2661
10
+ pdo/rag.py,sha256=M7OTMC3JBWn2Bi9d14nQvErNZWnbJhTz1vHUwzHSTKI,6162
11
+ pdo/serve.py,sha256=NMV2reuJyOB7LYS-OFJevjF8PS_vg9Inune78lUsVTU,4822
12
+ pdo/skills.py,sha256=VTHzzHwY2cOQWWPTR1ff_NSUiL22rZVwKmvjxaOyAUQ,2220
13
+ pdo/theme.py,sha256=HGSMzZFVqwqq-78c0asNQaaIYTO_kouQcLxywlfwD9Q,1262
14
+ pdo/agent/__init__.py,sha256=BLjRrvO9Dlr-E28F5HEc8-ruVa9rzGnim5ZhmkLMr8Q,278
15
+ pdo/agent/core.py,sha256=eNnZB_loGErSS3KAub-N-pX0iQdR1z_kjkfOCKXq7uA,10821
16
+ pdo/agent/delegate.py,sha256=GSJznSxDRyc5FFhhfEmLMW9yYN7_lqyC8FA4r-zMYhs,2054
17
+ pdo/agent/executor.py,sha256=BHuCDMn2JDjh6YW9tFh7D5PydK9zGlshOQk-3L6etbg,3482
18
+ pdo/agent/memory.py,sha256=OF-dqsebcVMv4j7CAxiWviXtTIjn_bQYvP9MoWne9jw,7458
19
+ pdo/agent/messages.py,sha256=XyOl5TQP4DFvtc7HtI9U0aUvpj8AngUa0ZJNvytWB7k,3133
20
+ pdo/agent/planner.py,sha256=aZiDIO9fL9fTwKkmZTJwMz4FKADdQfOsrQjhnxkkAvw,1398
21
+ pdo/agent/reviewer.py,sha256=hqhP4_I8BIufVsmBr2CCVfO8n5pxvZdGl03-Pxbp_pw,804
22
+ pdo/agent/router.py,sha256=OVBvl_GqDLh7zDG8RYNSAkklxvOEvcWtZgZvjbiK8c4,1299
23
+ pdo/prompts/system.md,sha256=ZVWOAPTKb3JeXd5oQ3q8gY7wAsnLEtoL4nfLYliLBAg,2372
24
+ pdo/tools/__init__.py,sha256=nWFCJ92L5b5EHB5t7F1jPl9QYHpeGYxeUMMwqjZfF6k,260
25
+ pdo/tools/base.py,sha256=Bkwz0DDU1V_acrzADQvdSHbrRrlKcMECx-TwlZCKeIE,2971
26
+ pdo/tools/code.py,sha256=9dnHP3VYlO6aK1NnVD9xqvYRjdnG5kaQ77lLzyatVwU,1597
27
+ pdo/tools/data.py,sha256=oU9IuZtf02XDFBC4s1v2MetePdrz5CVqmBnIBU9782E,2075
28
+ pdo/tools/edit.py,sha256=-2nbsLK5FqO3Bmx4oEGa9mW46LPXisiaMUp_rXK_DRI,2151
29
+ pdo/tools/filesystem.py,sha256=0OnaQEy-i2uHJn4Ke0svB5jZPh9RYN8UfsI65OvyI6Q,6121
30
+ pdo/tools/git.py,sha256=1cVhJn0P4e7M0cf19dSzBPUWpJoQp2tEs05CAwoqj5k,1470
31
+ pdo/tools/memory.py,sha256=o2x6GlkCNj1yilLP4T2jr0j30kOOg1mGqiqO1eVMYeA,2250
32
+ pdo/tools/rag.py,sha256=XnAtcgzar1JdYJncEBpP-8zDD2cpNQKhuxCLjuaUsb8,2107
33
+ pdo/tools/registry.py,sha256=0gH9jmNrlOnmGyqWq4n8tjVDpn4xYMI0cTJMHTVIyvY,6877
34
+ pdo/tools/search.py,sha256=5-uE0_pjOuTgV9tRJfRXPkdE7bH98e4LgUn719YBkGE,3001
35
+ pdo/tools/shell.py,sha256=4N2QZyDm6Ofb18WzDxuNE-H52cU9zV6_H01Rv4f1cwo,4345
36
+ pdo/tools/web.py,sha256=F7Upy4xHbMfsr1UpXwhC3eVu9ZKC86_E6TRURQ5ahwE,6021
37
+ pdo_agent-2.0.0.dist-info/licenses/LICENSE,sha256=-JeGB2BTy2zvTSNS-cM07rU8bwh5xyvfgBGVJnUNDo8,1073
38
+ pdo_agent-2.0.0.dist-info/METADATA,sha256=KgSx06yk196_HH-ZkfOHfuOJb38cWtBUzVPJMTGMGLA,18214
39
+ pdo_agent-2.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
40
+ pdo_agent-2.0.0.dist-info/entry_points.txt,sha256=-3TXhrTZ6qxcbaC-wSYNlaX5RnG7dHCPKICbupKI8cQ,38
41
+ pdo_agent-2.0.0.dist-info/top_level.txt,sha256=Rn0Tk_WljqskxeoKnTyug7X6FRTcgvmwwZwL4d_vOkc,4
42
+ pdo_agent-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pdo = pdo.main:main