chore: renamed package scope to @oh-my-pi for consistent branding
- Renamed npm package scope from @mariozechner to @oh-my-pi across all packages for consistent branding. - Removed packages/pods directory and all related pod management functionality. - Updated import paths and module declarations throughout codebase to reflect new package names. - Reformatted code with consistent trailing comma removal and ternary operator alignment.
This commit is contained in:
@@ -10,7 +10,6 @@ read README.md, then ask which module(s) to work on. Based on the answer, read t
|
||||
- packages/agent/README.md
|
||||
- packages/coding-agent/README.md
|
||||
- packages/mom/README.md
|
||||
- packages/pods/README.md
|
||||
- packages/web-ui/README.md
|
||||
|
||||
## Code Quality
|
||||
@@ -287,7 +286,7 @@ When reading issues:
|
||||
When creating issues:
|
||||
|
||||
- Add `pkg:*` labels to indicate which package(s) the issue affects
|
||||
- Available labels: `pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:mom`, `pkg:pods`, `pkg:tui`, `pkg:web-ui`
|
||||
- Available labels: `pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:mom`, `pkg:tui`, `pkg:web-ui`
|
||||
- If an issue spans multiple packages, add all relevant labels
|
||||
|
||||
When closing issues via commit:
|
||||
@@ -298,7 +297,7 @@ When closing issues via commit:
|
||||
## Tools
|
||||
|
||||
- GitHub CLI for issues/PRs
|
||||
- Add package labels to issues/PRs: pkg:agent, pkg:ai, pkg:coding-agent, pkg:mom, pkg:pods, pkg:tui, pkg:web-ui
|
||||
- Add package labels to issues/PRs: pkg:agent, pkg:ai, pkg:coding-agent, pkg:mom, pkg:tui, pkg:web-ui
|
||||
- TUI interaction: use tmux
|
||||
|
||||
## Style
|
||||
|
||||
@@ -1,61 +1,144 @@
|
||||
# Pi Monorepo
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/can1357/oh-my-pi/main/assets/banner.png" alt="Pi Monorepo">
|
||||
</p>
|
||||
|
||||
Tools for building AI agents and managing LLM deployments.
|
||||
<p align="center">
|
||||
<strong>AI coding agent for the terminal</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent"><img src="https://img.shields.io/npm/v/@oh-my-pi/pi-coding-agent?style=flat&colorA=222222&colorB=CB3837" alt="npm version"></a>
|
||||
<a href="https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-keep-E05735?style=flat&colorA=222222" alt="Changelog"></a>
|
||||
<a href="https://github.com/can1357/oh-my-pi/actions"><img src="https://img.shields.io/github/actions/workflow/status/can1357/oh-my-pi/ci.yml?style=flat&colorA=222222&colorB=3FB950" alt="CI"></a>
|
||||
<a href="https://github.com/can1357/oh-my-pi/blob/main/LICENSE"><img src="https://img.shields.io/github/license/can1357/oh-my-pi?style=flat&colorA=222222&colorB=58A6FF" alt="License"></a>
|
||||
<a href="https://www.typescriptlang.org"><img src="https://img.shields.io/badge/TypeScript-3178C6?style=flat&colorA=222222&logo=typescript&logoColor=white" alt="TypeScript"></a>
|
||||
<a href="https://bun.sh"><img src="https://img.shields.io/badge/runtime-Bun-f472b6?style=flat&colorA=222222" alt="Bun"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Fork of <a href="https://github.com/badlogic/pi-mono">badlogic/pi-mono</a> by <a href="https://github.com/mariozechner">@mariozechner</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Fork Enhancements
|
||||
|
||||
Features added on top of upstream pi:
|
||||
|
||||
### MCP & Plugin System
|
||||
|
||||
Full Model Context Protocol support with external tool integration:
|
||||
|
||||
- Stdio and HTTP transports for connecting to MCP servers
|
||||
- Plugin CLI (`pi plugin install/enable/configure/doctor`)
|
||||
- Hot-loadable plugins from `~/.pi/plugins/` with npm/bun integration
|
||||
- 22 pre-built Exa MCP tools for web research, LinkedIn, fact-finding
|
||||
|
||||
### LSP Tool (Language Server Protocol)
|
||||
|
||||
IDE-like code intelligence via rust-analyzer and extensible to other languages:
|
||||
|
||||
- File diagnostics with error/warning/info classification
|
||||
- Hover documentation, symbol references, implementations
|
||||
- Code actions and refactoring suggestions
|
||||
- Workspace-wide symbol search
|
||||
|
||||
### Task Tool (Subagent System)
|
||||
|
||||
Parallel execution framework with specialized agents:
|
||||
|
||||
- **5 bundled agents**: explore, plan, browser, task, reviewer
|
||||
- User-level (`~/.pi/agent/agents/`) and project-level (`.pi/agents/`) custom agents
|
||||
- Concurrency-limited batch execution with progress tracking
|
||||
- Pre-defined commands: implement, architect-plan, implement-with-critic
|
||||
|
||||
### Web Search & Fetch
|
||||
|
||||
Multi-provider search and full-page scraping:
|
||||
|
||||
- Anthropic and Perplexity search integration with caching
|
||||
- HTML-to-markdown conversion with link preservation
|
||||
- JavaScript rendering support, image handling
|
||||
|
||||
### TUI Overhaul
|
||||
|
||||
- **Welcome screen**: Logo, tips, recent sessions with selection
|
||||
- **Powerline footer**: Model, cwd, git branch/status, token usage, context %
|
||||
- **Hotkeys**: `?` displays shortcuts when editor empty
|
||||
- **Emergency terminal restore**: Crash handlers prevent terminal corruption
|
||||
|
||||
### Git Context
|
||||
|
||||
System prompt includes repo awareness:
|
||||
|
||||
- Current branch, main branch auto-detection
|
||||
- Git status snapshot (staged/unstaged/untracked)
|
||||
- Recent 5 commits summary
|
||||
|
||||
### Bun Runtime
|
||||
|
||||
Migrated from Node.js for native TypeScript:
|
||||
|
||||
- Runs `.ts` files directly without build step
|
||||
- Faster CLI startup times
|
||||
- All 7 packages converted to Bun APIs
|
||||
|
||||
### Additional Tools
|
||||
|
||||
- **Ask Tool**: Interactive user questioning (211 lines)
|
||||
- **AST Tool**: Structural code analysis via ast-grep (271 lines)
|
||||
- **Replace Tool**: Find & replace across files (297 lines)
|
||||
|
||||
---
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Description |
|
||||
| ---------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| **[@mariozechner/pi-ai](packages/ai)** | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||
| **[@mariozechner/pi-agent-core](packages/agent)** | Agent runtime with tool calling and state management |
|
||||
| **[@mariozechner/pi-coding-agent](packages/coding-agent)** | Interactive coding agent CLI |
|
||||
| **[@mariozechner/pi-mom](packages/mom)** | Slack bot that delegates messages to the pi coding agent |
|
||||
| **[@mariozechner/pi-tui](packages/tui)** | Terminal UI library with differential rendering |
|
||||
| **[@mariozechner/pi-web-ui](packages/web-ui)** | Web components for AI chat interfaces |
|
||||
| **[@mariozechner/pi-pods](packages/pods)** | CLI for managing vLLM deployments on GPU pods |
|
||||
| Package | Description |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
| **[@oh-my-pi/pi-ai](packages/ai)** | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||
| **[@oh-my-pi/pi-agent-core](packages/agent)** | Agent runtime with tool calling and state management |
|
||||
| **[@oh-my-pi/pi-coding-agent](packages/coding-agent)** | Interactive coding agent CLI |
|
||||
| **[@oh-my-pi/pi-mom](packages/mom)** | Slack bot that delegates messages to the pi coding agent |
|
||||
| **[@oh-my-pi/pi-tui](packages/tui)** | Terminal UI library with differential rendering |
|
||||
| **[@oh-my-pi/pi-web-ui](packages/web-ui)** | Web components for AI chat interfaces |
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
bun install # Install all dependencies
|
||||
bun run build # Build all packages
|
||||
bun run check # Lint, format, and type check
|
||||
bun run dev:install # Install deps and link all packages
|
||||
bun run build # Build all packages
|
||||
bun run check # Lint, format, and type check
|
||||
```
|
||||
|
||||
> **Note:** `bun run check` requires `bun run build` to be run first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies.
|
||||
> **Note:** `bun run check` requires `bun run build` first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies.
|
||||
|
||||
### CI
|
||||
|
||||
GitHub Actions runs on push to `main` and on pull requests. The workflow runs `bun run check` and `bun test` for each package in parallel.
|
||||
|
||||
**Do not add LLM API keys as secrets to this repository.** Tests that require LLM access use `describe.skipIf()` to skip when API keys are missing. This is intentional:
|
||||
|
||||
- PRs from external contributors would have access to secrets in the CI environment
|
||||
- Malicious PR code could exfiltrate API keys
|
||||
- Tests that need LLM calls are skipped on CI and run locally by developers who have keys configured
|
||||
|
||||
If you need to run LLM-dependent tests, run them locally with your own API keys.
|
||||
|
||||
### Development
|
||||
|
||||
Start watch builds for all packages:
|
||||
### Watch Mode
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Then run directly with Bun:
|
||||
Then run directly:
|
||||
|
||||
```bash
|
||||
cd packages/coding-agent && bunx tsx src/cli.ts
|
||||
cd packages/pods && bunx tsx src/cli.ts
|
||||
```
|
||||
|
||||
### Versioning (Lockstep)
|
||||
### CI
|
||||
|
||||
**All packages MUST always have the same version number.** Use these commands to bump versions:
|
||||
GitHub Actions runs on push to `main` and on pull requests. The workflow runs `bun run check` and `bun test` for each package in parallel.
|
||||
|
||||
**Do not add LLM API keys as secrets.** Tests requiring LLM access use `describe.skipIf()` and run locally.
|
||||
|
||||
---
|
||||
|
||||
## Versioning
|
||||
|
||||
All packages use lockstep versioning:
|
||||
|
||||
```bash
|
||||
bun run version:patch # 0.7.5 -> 0.7.6
|
||||
@@ -63,17 +146,11 @@ bun run version:minor # 0.7.5 -> 0.8.0
|
||||
bun run version:major # 0.7.5 -> 1.0.0
|
||||
```
|
||||
|
||||
These commands:
|
||||
**Never manually edit version numbers.**
|
||||
|
||||
1. Update all package versions to the same number
|
||||
2. Update inter-package dependency versions (e.g., `pi-agent` depends on `pi-ai@^0.7.7`)
|
||||
3. Update `bun.lockb`
|
||||
---
|
||||
|
||||
**Note:** Version bumping uses `npm version -ws` since Bun doesn't yet have workspace version equivalent.
|
||||
|
||||
**Never manually edit version numbers.** The lockstep system ensures consistency across the monorepo.
|
||||
|
||||
### Publishing
|
||||
## Publishing
|
||||
|
||||
```bash
|
||||
bun run release:patch # Bug fixes
|
||||
@@ -81,16 +158,10 @@ bun run release:minor # New features
|
||||
bun run release:major # Breaking changes
|
||||
```
|
||||
|
||||
This handles version bump, CHANGELOG updates, commit, tag, publish, and push.
|
||||
Requires an npm token with "Bypass 2FA on publish" enabled.
|
||||
|
||||
**Note:** Publishing uses `npm publish` since Bun delegates to npm for publishing.
|
||||
|
||||
**NPM Token Setup**: Requires a granular access token with "Bypass 2FA on publish" enabled.
|
||||
|
||||
- Go to https://www.npmjs.com/settings/badlogic/tokens/
|
||||
- Create a new "Granular Access Token" with "Bypass 2FA on publish"
|
||||
- Set the token: `npm config set //registry.npmjs.org/:_authToken=YOUR_TOKEN`
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT - Original work copyright Mario Zechner
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Oh My Pi Logo</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
||||
}
|
||||
.logo-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #0d0d0d 0%, #1a1a1a 50%, #0f0f0f 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.03;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
.circuit {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0.2;
|
||||
}
|
||||
.circuit.left { left: 38px; }
|
||||
.circuit.right { right: 38px; }
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.pi-container {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
filter: blur(32px);
|
||||
opacity: 0.3;
|
||||
background: radial-gradient(circle, #f97316 0%, transparent 70%);
|
||||
transform: scale(1.5);
|
||||
}
|
||||
.pi-symbol { position: relative; }
|
||||
.title {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.3em;
|
||||
color: rgba(255,255,255,0.9);
|
||||
text-shadow: 0 0 40px rgba(249, 115, 22, 0.3);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tagline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.tagline .bracket { font-size: 18px; font-weight: 300; }
|
||||
.tagline .text { font-size: 14px; letter-spacing: 0.4em; text-transform: uppercase; }
|
||||
.status {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
.corner {
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
.corner.tl { top: 16px; left: 16px; border-top-width: 2px; border-left-width: 2px; }
|
||||
.corner.tr { top: 16px; right: 16px; border-top-width: 2px; border-right-width: 2px; }
|
||||
.corner.bl { bottom: 16px; left: 16px; border-bottom-width: 2px; border-left-width: 2px; }
|
||||
.corner.br { bottom: 16px; right: 16px; border-bottom-width: 2px; border-right-width: 2px; }
|
||||
.bottom-glow {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 96px;
|
||||
opacity: 0.5;
|
||||
background: linear-gradient(to top, rgba(249, 115, 22, 0.05), transparent);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="logo-container">
|
||||
<div class="grid"></div>
|
||||
|
||||
<!-- Circuit traces - left -->
|
||||
<svg class="circuit left" width="60" height="200" viewBox="0 0 60 200">
|
||||
<path d="M30 0 L30 60 L10 80 L10 120 L30 140 L30 200" stroke="#f97316" stroke-width="2" fill="none"/>
|
||||
<circle cx="30" cy="60" r="4" fill="#f97316"/>
|
||||
<circle cx="10" cy="80" r="3" fill="#f97316"/>
|
||||
<circle cx="10" cy="120" r="3" fill="#f97316"/>
|
||||
<circle cx="30" cy="140" r="4" fill="#f97316"/>
|
||||
<rect x="5" y="95" width="10" height="10" fill="none" stroke="#f97316" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
||||
<!-- Circuit traces - right -->
|
||||
<svg class="circuit right" width="60" height="200" viewBox="0 0 60 200">
|
||||
<path d="M30 0 L30 60 L50 80 L50 120 L30 140 L30 200" stroke="#f97316" stroke-width="2" fill="none"/>
|
||||
<circle cx="30" cy="60" r="4" fill="#f97316"/>
|
||||
<circle cx="50" cy="80" r="3" fill="#f97316"/>
|
||||
<circle cx="50" cy="120" r="3" fill="#f97316"/>
|
||||
<circle cx="30" cy="140" r="4" fill="#f97316"/>
|
||||
<rect x="45" y="95" width="10" height="10" fill="none" stroke="#f97316" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
||||
<div class="content">
|
||||
<div class="pi-container">
|
||||
<div class="glow"></div>
|
||||
<svg class="pi-symbol" width="120" height="90" viewBox="0 0 120 90">
|
||||
<rect x="10" y="8" width="100" height="12" rx="2" fill="#fafafa"/>
|
||||
<rect x="25" y="20" width="12" height="62" rx="2" fill="#fafafa"/>
|
||||
<rect x="75" y="20" width="12" height="45" rx="2" fill="#fafafa"/>
|
||||
<g>
|
||||
<rect x="71" y="55" width="20" height="16" rx="3" fill="#f97316"/>
|
||||
<rect x="76" y="59" width="3" height="8" rx="1" fill="#0d0d0d"/>
|
||||
<rect x="82" y="59" width="3" height="8" rx="1" fill="#0d0d0d"/>
|
||||
</g>
|
||||
<circle cx="18" cy="14" r="2" fill="#f97316" opacity="0.8"/>
|
||||
<circle cx="102" cy="14" r="2" fill="#f97316" opacity="0.8"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="title">oh my PI</div>
|
||||
|
||||
<div class="tagline">
|
||||
<span class="bracket">[</span>
|
||||
<span class="text">ai coding agent</span>
|
||||
<span class="bracket">]</span>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<div class="status-dot"></div>
|
||||
<span>MCP · LSP · Subagents · Web Search</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="corner tl"></div>
|
||||
<div class="corner tr"></div>
|
||||
<div class="corner bl"></div>
|
||||
<div class="corner br"></div>
|
||||
<div class="bottom-glow"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 90" width="120" height="90">
|
||||
<!-- Pi symbol with plugin connector -->
|
||||
<!-- Horizontal bar -->
|
||||
<rect x="10" y="8" width="100" height="12" rx="2" fill="#fafafa"/>
|
||||
<!-- Left leg -->
|
||||
<rect x="25" y="20" width="12" height="62" rx="2" fill="#fafafa"/>
|
||||
<!-- Right leg -->
|
||||
<rect x="75" y="20" width="12" height="45" rx="2" fill="#fafafa"/>
|
||||
<!-- Plugin connector -->
|
||||
<rect x="71" y="55" width="20" height="16" rx="3" fill="#f97316"/>
|
||||
<rect x="76" y="59" width="3" height="8" rx="1" fill="#0d0d0d"/>
|
||||
<rect x="82" y="59" width="3" height="8" rx="1" fill="#0d0d0d"/>
|
||||
<!-- Decorative dots -->
|
||||
<circle cx="18" cy="14" r="2" fill="#f97316" opacity="0.8"/>
|
||||
<circle cx="102" cy="14" r="2" fill="#f97316" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 795 B |
@@ -18,11 +18,11 @@
|
||||
},
|
||||
},
|
||||
"packages/agent": {
|
||||
"name": "@mariozechner/pi-agent-core",
|
||||
"name": "@oh-my-pi/pi-agent-core",
|
||||
"version": "1.337.0",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "workspace:*",
|
||||
"@mariozechner/pi-tui": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-tui": "workspace:*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.0",
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"name": "@oh-my-pi/pi-ai",
|
||||
"version": "1.337.0",
|
||||
"bin": {
|
||||
"pi-ai": "./src/cli.ts",
|
||||
@@ -54,15 +54,17 @@
|
||||
},
|
||||
},
|
||||
"packages/coding-agent": {
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"name": "@oh-my-pi/pi-coding-agent",
|
||||
"version": "1.337.0",
|
||||
"bin": {
|
||||
"pi": "src/cli.ts",
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent-core": "workspace:*",
|
||||
"@mariozechner/pi-ai": "workspace:*",
|
||||
"@mariozechner/pi-tui": "workspace:*",
|
||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-tui": "workspace:*",
|
||||
"@sinclair/typebox": "^0.34.46",
|
||||
"ajv": "^8.17.1",
|
||||
"chalk": "^5.5.0",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"diff": "^8.0.2",
|
||||
@@ -81,16 +83,16 @@
|
||||
},
|
||||
},
|
||||
"packages/mom": {
|
||||
"name": "@mariozechner/pi-mom",
|
||||
"name": "@oh-my-pi/pi-mom",
|
||||
"version": "1.337.0",
|
||||
"bin": {
|
||||
"mom": "src/main.ts",
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.16",
|
||||
"@mariozechner/pi-agent-core": "workspace:*",
|
||||
"@mariozechner/pi-ai": "workspace:*",
|
||||
"@mariozechner/pi-coding-agent": "workspace:*",
|
||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-coding-agent": "workspace:*",
|
||||
"@sinclair/typebox": "^0.34.0",
|
||||
"@slack/socket-mode": "^2.0.0",
|
||||
"@slack/web-api": "^7.0.0",
|
||||
@@ -103,19 +105,8 @@
|
||||
"@types/node": "^24.3.0",
|
||||
},
|
||||
},
|
||||
"packages/pods": {
|
||||
"name": "@mariozechner/pi",
|
||||
"version": "1.337.0",
|
||||
"bin": {
|
||||
"pi-pods": "dist/cli.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent-core": "^0.30.2",
|
||||
"chalk": "^5.5.0",
|
||||
},
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@mariozechner/pi-tui",
|
||||
"name": "@oh-my-pi/pi-tui",
|
||||
"version": "1.337.0",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
@@ -130,13 +121,13 @@
|
||||
},
|
||||
},
|
||||
"packages/web-ui": {
|
||||
"name": "@mariozechner/pi-web-ui",
|
||||
"name": "@oh-my-pi/pi-web-ui",
|
||||
"version": "1.337.0",
|
||||
"dependencies": {
|
||||
"@lmstudio/sdk": "^1.5.0",
|
||||
"@mariozechner/pi-agent-core": "workspace:*",
|
||||
"@mariozechner/pi-ai": "workspace:*",
|
||||
"@mariozechner/pi-tui": "workspace:*",
|
||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-tui": "workspace:*",
|
||||
"docx-preview": "^0.3.7",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jszip": "^3.10.1",
|
||||
@@ -160,8 +151,8 @@
|
||||
"version": "1.19.1",
|
||||
"dependencies": {
|
||||
"@mariozechner/mini-lit": "^0.2.0",
|
||||
"@mariozechner/pi-ai": "workspace:*",
|
||||
"@mariozechner/pi-web-ui": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-web-ui": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"lit": "^3.3.1",
|
||||
"lucide": "^0.544.0",
|
||||
@@ -278,20 +269,6 @@
|
||||
|
||||
"@mariozechner/mini-lit": ["@mariozechner/mini-lit@0.2.1", "", { "dependencies": { "@preact/signals-core": "^1.12.1", "class-variance-authority": "^0.7.1", "diff": "^8.0.2", "highlight.js": "^11.11.1", "html-parse-string": "^0.0.9", "katex": "^0.16.22", "lucide": "^0.544.0", "marked": "^16.3.0", "tailwind-merge": "^3.3.1", "tailwind-variants": "^3.1.1", "uhtml": "^5.0.9" }, "peerDependencies": { "lit": "^3.3.1" } }, "sha512-u300euLgCsDDlb8o2Wbz+55eSJga5X2vB58s9XBuFIr2Bi3iI+GMR7t/NYo/O6Vr6obXShXgYjR3SRUJVgo+kQ=="],
|
||||
|
||||
"@mariozechner/pi": ["@mariozechner/pi@workspace:packages/pods"],
|
||||
|
||||
"@mariozechner/pi-agent-core": ["@mariozechner/pi-agent-core@workspace:packages/agent"],
|
||||
|
||||
"@mariozechner/pi-ai": ["@mariozechner/pi-ai@workspace:packages/ai"],
|
||||
|
||||
"@mariozechner/pi-coding-agent": ["@mariozechner/pi-coding-agent@workspace:packages/coding-agent"],
|
||||
|
||||
"@mariozechner/pi-mom": ["@mariozechner/pi-mom@workspace:packages/mom"],
|
||||
|
||||
"@mariozechner/pi-tui": ["@mariozechner/pi-tui@workspace:packages/tui"],
|
||||
|
||||
"@mariozechner/pi-web-ui": ["@mariozechner/pi-web-ui@workspace:packages/web-ui"],
|
||||
|
||||
"@mistralai/mistralai": ["@mistralai/mistralai@1.10.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg=="],
|
||||
|
||||
"@napi-rs/canvas": ["@napi-rs/canvas@0.1.88", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.88", "@napi-rs/canvas-darwin-arm64": "0.1.88", "@napi-rs/canvas-darwin-x64": "0.1.88", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.88", "@napi-rs/canvas-linux-arm64-gnu": "0.1.88", "@napi-rs/canvas-linux-arm64-musl": "0.1.88", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.88", "@napi-rs/canvas-linux-x64-gnu": "0.1.88", "@napi-rs/canvas-linux-x64-musl": "0.1.88", "@napi-rs/canvas-win32-arm64-msvc": "0.1.88", "@napi-rs/canvas-win32-x64-msvc": "0.1.88" } }, "sha512-/p08f93LEbsL5mDZFQ3DBxcPv/I4QG9EDYRRq1WNlCOXVfAHBTHMSVMwxlqG/AtnSfUr9+vgfN7MKiyDo0+Weg=="],
|
||||
@@ -318,6 +295,18 @@
|
||||
|
||||
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.88", "", { "os": "win32", "cpu": "x64" }, "sha512-ROVqbfS4QyZxYkqmaIBBpbz/BQvAR+05FXM5PAtTYVc0uyY8Y4BHJSMdGAaMf6TdIVRsQsiq+FG/dH9XhvWCFQ=="],
|
||||
|
||||
"@oh-my-pi/pi-agent-core": ["@oh-my-pi/pi-agent-core@workspace:packages/agent"],
|
||||
|
||||
"@oh-my-pi/pi-ai": ["@oh-my-pi/pi-ai@workspace:packages/ai"],
|
||||
|
||||
"@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@workspace:packages/coding-agent"],
|
||||
|
||||
"@oh-my-pi/pi-mom": ["@oh-my-pi/pi-mom@workspace:packages/mom"],
|
||||
|
||||
"@oh-my-pi/pi-tui": ["@oh-my-pi/pi-tui@workspace:packages/tui"],
|
||||
|
||||
"@oh-my-pi/pi-web-ui": ["@oh-my-pi/pi-web-ui@workspace:packages/web-ui"],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
|
||||
@@ -670,8 +659,6 @@
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||
|
||||
"glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
@@ -894,8 +881,6 @@
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||
|
||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||
@@ -986,8 +971,6 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"uhtml": ["uhtml@5.0.9", "", { "dependencies": { "@webreflection/alien-signals": "^0.3.2" } }, "sha512-qPyu3vGilaLe6zrjOCD/xezWEHLwdevxmbY3hzyhT25KBDF4F7YYW3YZcL3kylD/6dMoVISHjn8ggV3+9FY+5g=="],
|
||||
@@ -1040,13 +1023,13 @@
|
||||
|
||||
"@mariozechner/mini-lit/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"@mariozechner/pi-agent-core/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
"@oh-my-pi/pi-agent-core/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
|
||||
"@mariozechner/pi-ai/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
"@oh-my-pi/pi-ai/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
|
||||
"@mariozechner/pi-coding-agent/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
"@oh-my-pi/pi-coding-agent/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
|
||||
"@mariozechner/pi-mom/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
"@oh-my-pi/pi-mom/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
|
||||
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||
|
||||
@@ -1084,14 +1067,8 @@
|
||||
|
||||
"concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"jwa/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"jws/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@@ -1110,8 +1087,6 @@
|
||||
|
||||
"tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"tunnel-agent/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
@@ -1122,13 +1097,13 @@
|
||||
|
||||
"@lmstudio/sdk/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"@mariozechner/pi-agent-core/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
"@oh-my-pi/pi-agent-core/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"@mariozechner/pi-ai/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
"@oh-my-pi/pi-ai/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"@mariozechner/pi-coding-agent/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
"@oh-my-pi/pi-coding-agent/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"@mariozechner/pi-mom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
"@oh-my-pi/pi-mom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"@slack/logger/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -1138,8 +1113,6 @@
|
||||
|
||||
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
@@ -1164,20 +1137,14 @@
|
||||
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"bl/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"tar-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"packages/web-ui/example"
|
||||
],
|
||||
"scripts": {
|
||||
"dev:install": "bun install && bun --cwd=packages/coding-agent link && bun --cwd=packages/ai link && bun --cwd=packages/mom link && bun --cwd=packages/pods link",
|
||||
"dev:install": "bun install && bun --cwd=packages/coding-agent link && bun --cwd=packages/ai link && bun --cwd=packages/mom link",
|
||||
"build": "bun --cwd=packages/web-ui run build:css",
|
||||
"dev": "bun --cwd=packages/web-ui run dev",
|
||||
"check": "biome check --write . && bun --cwd=packages/web-ui run check",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- **Transport abstraction removed**: `ProviderTransport`, `AppTransport`, and `AgentTransport` interface have been removed. Use the `streamFn` option directly for custom streaming implementations.
|
||||
|
||||
- **Agent options renamed**:
|
||||
|
||||
- `transport` → removed (use `streamFn` instead)
|
||||
- `messageTransformer` → `convertToLlm`
|
||||
- `preprocessor` → `transformContext`
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
- **`UserMessageWithAttachments` and `Attachment` types removed**: Attachment handling is now the responsibility of the `convertToLlm` function.
|
||||
|
||||
- **Agent loop moved from `@mariozechner/pi-ai`**: The `agentLoop`, `agentLoopContinue`, and related types have moved to this package. Import from `@mariozechner/pi-agent` instead.
|
||||
- **Agent loop moved from `@oh-my-pi/pi-ai`**: The `agentLoop`, `agentLoopContinue`, and related types have moved to this package. Import from `@oh-my-pi/pi-agent` instead.
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
+88
-87
@@ -1,31 +1,31 @@
|
||||
# @mariozechner/pi-agent
|
||||
# @oh-my-pi/pi-agent
|
||||
|
||||
Stateful agent with tool execution and event streaming. Built on `@mariozechner/pi-ai`.
|
||||
Stateful agent with tool execution and event streaming. Built on `@oh-my-pi/pi-ai`.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @mariozechner/pi-agent
|
||||
npm install @oh-my-pi/pi-agent
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mariozechner/pi-agent";
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { Agent } from "@oh-my-pi/pi-agent";
|
||||
import { getModel } from "@oh-my-pi/pi-ai";
|
||||
|
||||
const agent = new Agent({
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model: getModel("anthropic", "claude-sonnet-4-20250514"),
|
||||
},
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model: getModel("anthropic", "claude-sonnet-4-20250514"),
|
||||
},
|
||||
});
|
||||
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
// Stream just the new text chunk
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
// Stream just the new text chunk
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await agent.prompt("Hello!");
|
||||
@@ -36,6 +36,7 @@ await agent.prompt("Hello!");
|
||||
### AgentMessage vs LLM Message
|
||||
|
||||
The agent works with `AgentMessage`, a flexible type that can include:
|
||||
|
||||
- Standard LLM messages (`user`, `assistant`, `toolResult`)
|
||||
- Custom app-specific message types via declaration merging
|
||||
|
||||
@@ -112,18 +113,18 @@ The last message in context must be `user` or `toolResult` (not `assistant`).
|
||||
|
||||
### Event Types
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `agent_start` | Agent begins processing |
|
||||
| `agent_end` | Agent completes with all new messages |
|
||||
| `turn_start` | New turn begins (one LLM call + tool executions) |
|
||||
| `turn_end` | Turn completes with assistant message and tool results |
|
||||
| `message_start` | Any message begins (user, assistant, toolResult) |
|
||||
| `message_update` | **Assistant only.** Includes `assistantMessageEvent` with delta |
|
||||
| `message_end` | Message completes |
|
||||
| `tool_execution_start` | Tool begins |
|
||||
| `tool_execution_update` | Tool streams progress |
|
||||
| `tool_execution_end` | Tool completes |
|
||||
| Event | Description |
|
||||
| ----------------------- | --------------------------------------------------------------- |
|
||||
| `agent_start` | Agent begins processing |
|
||||
| `agent_end` | Agent completes with all new messages |
|
||||
| `turn_start` | New turn begins (one LLM call + tool executions) |
|
||||
| `turn_end` | Turn completes with assistant message and tool results |
|
||||
| `message_start` | Any message begins (user, assistant, toolResult) |
|
||||
| `message_update` | **Assistant only.** Includes `assistantMessageEvent` with delta |
|
||||
| `message_end` | Message completes |
|
||||
| `tool_execution_start` | Tool begins |
|
||||
| `tool_execution_update` | Tool streams progress |
|
||||
| `tool_execution_end` | Tool completes |
|
||||
|
||||
## Agent Options
|
||||
|
||||
@@ -162,15 +163,15 @@ const agent = new Agent({
|
||||
|
||||
```typescript
|
||||
interface AgentState {
|
||||
systemPrompt: string;
|
||||
model: Model<any>;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
tools: AgentTool<any>[];
|
||||
messages: AgentMessage[];
|
||||
isStreaming: boolean;
|
||||
streamMessage: AgentMessage | null; // Current partial during streaming
|
||||
pendingToolCalls: Set<string>;
|
||||
error?: string;
|
||||
systemPrompt: string;
|
||||
model: Model<any>;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
tools: AgentTool<any>[];
|
||||
messages: AgentMessage[];
|
||||
isStreaming: boolean;
|
||||
streamMessage: AgentMessage | null; // Current partial during streaming
|
||||
pendingToolCalls: Set<string>;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -185,9 +186,7 @@ Access via `agent.state`. During streaming, `streamMessage` contains the partial
|
||||
await agent.prompt("Hello");
|
||||
|
||||
// With images
|
||||
await agent.prompt("What's in this image?", [
|
||||
{ type: "image", data: base64Data, mimeType: "image/jpeg" }
|
||||
]);
|
||||
await agent.prompt("What's in this image?", [{ type: "image", data: base64Data, mimeType: "image/jpeg" }]);
|
||||
|
||||
// AgentMessage directly
|
||||
await agent.prompt({ role: "user", content: "Hello", timestamp: Date.now() });
|
||||
@@ -206,13 +205,13 @@ agent.setTools([myTool]);
|
||||
agent.replaceMessages(newMessages);
|
||||
agent.appendMessage(message);
|
||||
agent.clearMessages();
|
||||
agent.reset(); // Clear everything
|
||||
agent.reset(); // Clear everything
|
||||
```
|
||||
|
||||
### Control
|
||||
|
||||
```typescript
|
||||
agent.abort(); // Cancel current operation
|
||||
agent.abort(); // Cancel current operation
|
||||
await agent.waitForIdle(); // Wait for completion
|
||||
```
|
||||
|
||||
@@ -220,7 +219,7 @@ await agent.waitForIdle(); // Wait for completion
|
||||
|
||||
```typescript
|
||||
const unsubscribe = agent.subscribe((event) => {
|
||||
console.log(event.type);
|
||||
console.log(event.type);
|
||||
});
|
||||
unsubscribe();
|
||||
```
|
||||
@@ -234,13 +233,14 @@ agent.setQueueMode("one-at-a-time");
|
||||
|
||||
// While agent is running tools
|
||||
agent.queueMessage({
|
||||
role: "user",
|
||||
content: "Stop! Do this instead.",
|
||||
timestamp: Date.now(),
|
||||
role: "user",
|
||||
content: "Stop! Do this instead.",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
```
|
||||
|
||||
When queued messages are detected after a tool completes:
|
||||
|
||||
1. Remaining tools are skipped with error results
|
||||
2. Queued message is injected
|
||||
3. LLM responds to the interruption
|
||||
@@ -250,10 +250,10 @@ When queued messages are detected after a tool completes:
|
||||
Extend `AgentMessage` via declaration merging:
|
||||
|
||||
```typescript
|
||||
declare module "@mariozechner/pi-agent" {
|
||||
interface CustomAgentMessages {
|
||||
notification: { role: "notification"; text: string; timestamp: number };
|
||||
}
|
||||
declare module "@oh-my-pi/pi-agent" {
|
||||
interface CustomAgentMessages {
|
||||
notification: { role: "notification"; text: string; timestamp: number };
|
||||
}
|
||||
}
|
||||
|
||||
// Now valid
|
||||
@@ -264,10 +264,11 @@ Handle custom types in `convertToLlm`:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
convertToLlm: (messages) => messages.flatMap(m => {
|
||||
if (m.role === "notification") return []; // Filter out
|
||||
return [m];
|
||||
}),
|
||||
convertToLlm: (messages) =>
|
||||
messages.flatMap((m) => {
|
||||
if (m.role === "notification") return []; // Filter out
|
||||
return [m];
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -279,23 +280,23 @@ Define tools using `AgentTool`:
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
const readFileTool: AgentTool = {
|
||||
name: "read_file",
|
||||
label: "Read File", // For UI display
|
||||
description: "Read a file's contents",
|
||||
parameters: Type.Object({
|
||||
path: Type.String({ description: "File path" }),
|
||||
}),
|
||||
execute: async (toolCallId, params, signal, onUpdate, context) => {
|
||||
const content = await fs.readFile(params.path, "utf-8");
|
||||
name: "read_file",
|
||||
label: "Read File", // For UI display
|
||||
description: "Read a file's contents",
|
||||
parameters: Type.Object({
|
||||
path: Type.String({ description: "File path" }),
|
||||
}),
|
||||
execute: async (toolCallId, params, signal, onUpdate, context) => {
|
||||
const content = await fs.readFile(params.path, "utf-8");
|
||||
|
||||
// Optional: stream progress
|
||||
onUpdate?.({ content: [{ type: "text", text: "Reading..." }], details: {} });
|
||||
// Optional: stream progress
|
||||
onUpdate?.({ content: [{ type: "text", text: "Reading..." }], details: {} });
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: content }],
|
||||
details: { path: params.path, size: content.length },
|
||||
};
|
||||
},
|
||||
return {
|
||||
content: [{ type: "text", text: content }],
|
||||
details: { path: params.path, size: content.length },
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
agent.setTools([readFileTool]);
|
||||
@@ -307,12 +308,12 @@ agent.setTools([readFileTool]);
|
||||
|
||||
```typescript
|
||||
execute: async (toolCallId, params, signal, onUpdate) => {
|
||||
if (!fs.existsSync(params.path)) {
|
||||
throw new Error(`File not found: ${params.path}`);
|
||||
}
|
||||
// Return content only on success
|
||||
return { content: [{ type: "text", text: "..." }] };
|
||||
}
|
||||
if (!fs.existsSync(params.path)) {
|
||||
throw new Error(`File not found: ${params.path}`);
|
||||
}
|
||||
// Return content only on success
|
||||
return { content: [{ type: "text", text: "..." }] };
|
||||
};
|
||||
```
|
||||
|
||||
Thrown errors are caught by the agent and reported to the LLM as tool errors with `isError: true`.
|
||||
@@ -322,15 +323,15 @@ Thrown errors are caught by the agent and reported to the LLM as tool errors wit
|
||||
For browser apps that proxy through a backend:
|
||||
|
||||
```typescript
|
||||
import { Agent, streamProxy } from "@mariozechner/pi-agent";
|
||||
import { Agent, streamProxy } from "@oh-my-pi/pi-agent";
|
||||
|
||||
const agent = new Agent({
|
||||
streamFn: (model, context, options) =>
|
||||
streamProxy(model, context, {
|
||||
...options,
|
||||
authToken: "...",
|
||||
proxyUrl: "https://your-server.com",
|
||||
}),
|
||||
streamFn: (model, context, options) =>
|
||||
streamProxy(model, context, {
|
||||
...options,
|
||||
authToken: "...",
|
||||
proxyUrl: "https://your-server.com",
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -339,28 +340,28 @@ const agent = new Agent({
|
||||
For direct control without the Agent class:
|
||||
|
||||
```typescript
|
||||
import { agentLoop, agentLoopContinue } from "@mariozechner/pi-agent";
|
||||
import { agentLoop, agentLoopContinue } from "@oh-my-pi/pi-agent";
|
||||
|
||||
const context: AgentContext = {
|
||||
systemPrompt: "You are helpful.",
|
||||
messages: [],
|
||||
tools: [],
|
||||
systemPrompt: "You are helpful.",
|
||||
messages: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const config: AgentLoopConfig = {
|
||||
model: getModel("openai", "gpt-4o"),
|
||||
convertToLlm: (msgs) => msgs.filter(m => ["user", "assistant", "toolResult"].includes(m.role)),
|
||||
model: getModel("openai", "gpt-4o"),
|
||||
convertToLlm: (msgs) => msgs.filter((m) => ["user", "assistant", "toolResult"].includes(m.role)),
|
||||
};
|
||||
|
||||
const userMessage = { role: "user", content: "Hello", timestamp: Date.now() };
|
||||
|
||||
for await (const event of agentLoop([userMessage], context, config)) {
|
||||
console.log(event.type);
|
||||
console.log(event.type);
|
||||
}
|
||||
|
||||
// Continue from existing context
|
||||
for await (const event of agentLoopContinue(context, config)) {
|
||||
console.log(event.type);
|
||||
console.log(event.type);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-agent-core",
|
||||
"name": "@oh-my-pi/pi-agent-core",
|
||||
"version": "1.337.0",
|
||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||
"type": "module",
|
||||
@@ -13,8 +13,8 @@
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "workspace:*",
|
||||
"@mariozechner/pi-tui": "workspace:*"
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-tui": "workspace:*"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
@@ -27,7 +27,7 @@
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/badlogic/pi-mono.git",
|
||||
"url": "git+https://github.com/can1357/oh-my-pi.git",
|
||||
"directory": "packages/agent"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
streamSimple,
|
||||
type ToolResultMessage,
|
||||
validateToolArguments,
|
||||
} from "@mariozechner/pi-ai";
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import type {
|
||||
AgentContext,
|
||||
AgentEvent,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type ReasoningEffort,
|
||||
streamSimple,
|
||||
type TextContent,
|
||||
} from "@mariozechner/pi-ai";
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { agentLoop, agentLoopContinue } from "./agent-loop.js";
|
||||
import type {
|
||||
AgentContext,
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
type SimpleStreamOptions,
|
||||
type StopReason,
|
||||
type ToolCall,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import { parseStreamingJson } from "@mariozechner/pi-ai/utils/json-parse";
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { parseStreamingJson } from "@oh-my-pi/pi-ai/utils/json-parse";
|
||||
|
||||
// Create stream class matching ProxyMessageEventStream
|
||||
class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
TextContent,
|
||||
Tool,
|
||||
ToolResultMessage,
|
||||
} from "@mariozechner/pi-ai";
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import type { Static, TSchema } from "@sinclair/typebox";
|
||||
|
||||
/** Stream function - can return sync or Promise for async config lookup */
|
||||
@@ -101,7 +101,7 @@ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhi
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* declare module "@mariozechner/agent" {
|
||||
* declare module "@oh-my-pi/agent" {
|
||||
* interface CustomAgentMessages {
|
||||
* artifact: ArtifactMessage;
|
||||
* notification: NotificationMessage;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type Message,
|
||||
type Model,
|
||||
type UserMessage,
|
||||
} from "@mariozechner/pi-ai";
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { agentLoop, agentLoopContinue } from "../src/agent-loop.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { getModel } from "@oh-my-pi/pi-ai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Agent } from "../src/index.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AssistantMessage, Model, ToolResultMessage, UserMessage } from "@mariozechner/pi-ai";
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import type { AssistantMessage, Model, ToolResultMessage, UserMessage } from "@oh-my-pi/pi-ai";
|
||||
import { getModel } from "@oh-my-pi/pi-ai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Agent } from "../src/index.js";
|
||||
import { calculateTool } from "./utils/calculate.js";
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **Agent API moved**: All agent functionality (`agentLoop`, `agentLoopContinue`, `AgentContext`, `AgentEvent`, `AgentTool`, `AgentToolResult`, etc.) has moved to `@mariozechner/pi-agent-core`. Import from that package instead of `@mariozechner/pi-ai`.
|
||||
- **Agent API moved**: All agent functionality (`agentLoop`, `agentLoopContinue`, `AgentContext`, `AgentEvent`, `AgentTool`, `AgentToolResult`, etc.) has moved to `@oh-my-pi/pi-agent-core`. Import from that package instead of `@oh-my-pi/pi-ai`.
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
+415
-394
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"name": "@oh-my-pi/pi-ai",
|
||||
"version": "1.337.0",
|
||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||
"type": "module",
|
||||
@@ -46,7 +46,7 @@
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/badlogic/pi-mono.git",
|
||||
"url": "git+https://github.com/can1357/oh-my-pi.git",
|
||||
"directory": "packages/ai"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -101,7 +101,7 @@ async function main(): Promise<void> {
|
||||
const command = args[0];
|
||||
|
||||
if (!command || command === "help" || command === "--help" || command === "-h") {
|
||||
console.log(`Usage: npx @mariozechner/pi-ai <command> [provider]
|
||||
console.log(`Usage: npx @oh-my-pi/pi-ai <command> [provider]
|
||||
|
||||
Commands:
|
||||
login [provider] Login to an OAuth provider
|
||||
@@ -114,9 +114,9 @@ Providers:
|
||||
google-antigravity Antigravity (Gemini 3, Claude, GPT-OSS)
|
||||
|
||||
Examples:
|
||||
npx @mariozechner/pi-ai login # interactive provider selection
|
||||
npx @mariozechner/pi-ai login anthropic # login to specific provider
|
||||
npx @mariozechner/pi-ai list # list providers
|
||||
npx @oh-my-pi/pi-ai login # interactive provider selection
|
||||
npx @oh-my-pi/pi-ai login anthropic # login to specific provider
|
||||
npx @oh-my-pi/pi-ai list # list providers
|
||||
`);
|
||||
return;
|
||||
}
|
||||
@@ -151,7 +151,7 @@ Examples:
|
||||
|
||||
if (!PROVIDERS.some((p) => p.id === provider)) {
|
||||
console.error(`Unknown provider: ${provider}`);
|
||||
console.error(`Use 'npx @mariozechner/pi-ai list' to see available providers`);
|
||||
console.error(`Use 'npx @oh-my-pi/pi-ai list' to see available providers`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ Examples:
|
||||
}
|
||||
|
||||
console.error(`Unknown command: ${command}`);
|
||||
console.error(`Use 'npx @mariozechner/pi-ai --help' for usage`);
|
||||
console.error(`Use 'npx @oh-my-pi/pi-ai --help' for usage`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,12 @@ See [docs/session.md](docs/session.md) for the file format and `SessionManager`
|
||||
The hooks API has been restructured with more granular events and better session access.
|
||||
|
||||
**Type renames:**
|
||||
|
||||
- `HookEventContext` → `HookContext`
|
||||
- `HookCommandContext` is now a new interface extending `HookContext` with session control methods
|
||||
|
||||
**Event changes:**
|
||||
|
||||
- The monolithic `session` event is now split into granular events: `session_start`, `session_before_switch`, `session_switch`, `session_before_branch`, `session_branch`, `session_before_compact`, `session_compact`, `session_shutdown`
|
||||
- `session_before_switch` and `session_switch` events now include `reason: "new" | "resume"` to distinguish between `/new` and `/resume`
|
||||
- New `session_before_tree` and `session_tree` events for `/tree` navigation (hook can provide custom branch summary)
|
||||
@@ -40,6 +42,7 @@ The hooks API has been restructured with more granular events and better session
|
||||
- Session entries are no longer passed in events. Use `ctx.sessionManager.getEntries()` or `ctx.sessionManager.getBranch()` instead
|
||||
|
||||
**API changes:**
|
||||
|
||||
- `pi.send(text, attachments?)` → `pi.sendMessage(message, triggerTurn?)` (creates `CustomMessageEntry`)
|
||||
- New `pi.appendEntry(customType, data?)` for hook state persistence (not in LLM context)
|
||||
- New `pi.registerCommand(name, options)` for custom slash commands (handler receives `HookCommandContext`)
|
||||
@@ -54,6 +57,7 @@ The hooks API has been restructured with more granular events and better session
|
||||
- New `ctx.modelRegistry` and `ctx.model` for API key resolution
|
||||
|
||||
**HookCommandContext (slash commands only):**
|
||||
|
||||
- `ctx.waitForIdle()` - wait for agent to finish streaming
|
||||
- `ctx.newSession(options?)` - create new sessions with optional setup callback
|
||||
- `ctx.branch(entryId)` - branch from a specific entry
|
||||
@@ -62,6 +66,7 @@ The hooks API has been restructured with more granular events and better session
|
||||
These methods are only on `HookCommandContext` (not `HookContext`) because they can deadlock if called from event handlers that run inside the agent loop.
|
||||
|
||||
**Removed:**
|
||||
|
||||
- `hookTimeout` setting (hooks no longer have timeouts; use Ctrl+C to abort)
|
||||
- `resolveApiKey` parameter (use `ctx.modelRegistry.getApiKey(model)`)
|
||||
|
||||
@@ -72,12 +77,14 @@ See [docs/hooks.md](docs/hooks.md) and [examples/hooks/](examples/hooks/) for th
|
||||
The custom tools API has been restructured to mirror the hooks pattern with a context object.
|
||||
|
||||
**Type renames:**
|
||||
|
||||
- `CustomAgentTool` → `CustomTool`
|
||||
- `ToolAPI` → `CustomToolAPI`
|
||||
- `ToolContext` → `CustomToolContext`
|
||||
- `ToolSessionEvent` → `CustomToolSessionEvent`
|
||||
|
||||
**Execute signature changed:**
|
||||
|
||||
```typescript
|
||||
// Before (v0.30.2)
|
||||
execute(toolCallId, params, signal, onUpdate)
|
||||
@@ -87,11 +94,13 @@ execute(toolCallId, params, onUpdate, ctx, signal?)
|
||||
```
|
||||
|
||||
The new `ctx: CustomToolContext` provides `sessionManager`, `modelRegistry`, `model`, and agent state methods:
|
||||
|
||||
- `ctx.isIdle()` - check if agent is streaming
|
||||
- `ctx.hasQueuedMessages()` - check if user has queued messages (skip interactive prompts)
|
||||
- `ctx.abort()` - abort current operation (fire-and-forget)
|
||||
|
||||
**Session event changes:**
|
||||
|
||||
- `CustomToolSessionEvent` now only has `reason` and `previousSessionFile`
|
||||
- Session entries are no longer in the event. Use `ctx.sessionManager.getBranch()` or `ctx.sessionManager.getEntries()` to reconstruct state
|
||||
- Reasons: `"start" | "switch" | "branch" | "tree" | "shutdown"` (no separate `"new"` reason; `/new` triggers `"switch"`)
|
||||
@@ -102,13 +111,15 @@ See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](ex
|
||||
### SDK Migration
|
||||
|
||||
**Type changes:**
|
||||
|
||||
- `CustomAgentTool` → `CustomTool`
|
||||
- `AppMessage` → `AgentMessage`
|
||||
- `sessionFile` returns `string | undefined` (was `string | null`)
|
||||
- `model` returns `Model | undefined` (was `Model | null`)
|
||||
- `Attachment` type removed. Use `ImageContent` from `@mariozechner/pi-ai` instead. Add images directly to message content arrays.
|
||||
- `Attachment` type removed. Use `ImageContent` from `@oh-my-pi/pi-ai` instead. Add images directly to message content arrays.
|
||||
|
||||
**AgentSession API:**
|
||||
|
||||
- `branch(entryIndex: number)` → `branch(entryId: string)`
|
||||
- `getUserMessagesForBranching()` returns `{ entryId, text }` instead of `{ entryIndex, text }`
|
||||
- `reset()` → `newSession(options?)` where options has optional `parentSession` for lineage tracking
|
||||
@@ -116,9 +127,11 @@ See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](ex
|
||||
- New `navigateTree(targetId, options?)` for in-place tree navigation
|
||||
|
||||
**Hook integration:**
|
||||
|
||||
- New `sendHookMessage(message, triggerTurn?)` for hook message injection
|
||||
|
||||
**SessionManager API:**
|
||||
|
||||
- Method renames: `saveXXX()` → `appendXXX()` (e.g., `appendMessage`, `appendCompaction`)
|
||||
- `branchInPlace()` → `branch()`
|
||||
- `reset()` → `newSession(options?)` with optional `parentSession` for lineage tracking
|
||||
@@ -136,10 +149,10 @@ See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](ex
|
||||
`ModelRegistry` is a new class that manages model discovery and API key resolution. It combines built-in models with custom models from `models.json` and resolves API keys via `AuthStorage`.
|
||||
|
||||
```typescript
|
||||
import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
|
||||
import { discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const authStorage = discoverAuthStorage(); // ~/.pi/agent/auth.json
|
||||
const modelRegistry = discoverModels(authStorage); // + ~/.pi/agent/models.json
|
||||
const authStorage = discoverAuthStorage(); // ~/.pi/agent/auth.json
|
||||
const modelRegistry = discoverModels(authStorage); // + ~/.pi/agent/models.json
|
||||
|
||||
// Get all models (built-in + custom)
|
||||
const allModels = modelRegistry.getAll();
|
||||
@@ -157,6 +170,7 @@ const apiKey = await modelRegistry.getApiKey(model);
|
||||
This replaces the old `resolveApiKey` callback pattern. Hooks and custom tools access it via `ctx.modelRegistry`.
|
||||
|
||||
**Renamed exports:**
|
||||
|
||||
- `messageTransformer` → `convertToLlm`
|
||||
- `SessionContext` alias `LoadedSession` removed
|
||||
|
||||
@@ -165,17 +179,21 @@ See [docs/sdk.md](docs/sdk.md) and [examples/sdk/](examples/sdk/) for the curren
|
||||
### RPC Migration
|
||||
|
||||
**Session commands:**
|
||||
|
||||
- `reset` command → `new_session` command with optional `parentSession` field
|
||||
|
||||
**Branching commands:**
|
||||
|
||||
- `branch` command: `entryIndex` → `entryId`
|
||||
- `get_branch_messages` response: `entryIndex` → `entryId`
|
||||
|
||||
**Type changes:**
|
||||
|
||||
- Messages are now `AgentMessage` (was `AppMessage`)
|
||||
- `prompt` command: `attachments` field replaced with `images` field using `ImageContent` format
|
||||
|
||||
**Compaction events:**
|
||||
|
||||
- `auto_compaction_start` now includes `reason` field (`"threshold"` or `"overflow"`)
|
||||
- `auto_compaction_end` now includes `willRetry` field
|
||||
- `compact` response includes full `CompactionResult` (`summary`, `firstKeptEntryId`, `tokensBefore`, `details`)
|
||||
@@ -185,6 +203,7 @@ See [docs/rpc.md](docs/rpc.md) for the current protocol.
|
||||
### Structured Compaction
|
||||
|
||||
Compaction and branch summarization now use a structured output format:
|
||||
|
||||
- Clear sections: Goal, Progress, Key Information, File Operations
|
||||
- File tracking: `readFiles` and `modifiedFiles` arrays in `details`, accumulated across compactions
|
||||
- Conversations are serialized to text before summarization to prevent the model from "continuing" them
|
||||
@@ -194,6 +213,7 @@ The `before_compact` and `before_tree` hook events allow custom compaction imple
|
||||
### Interactive Mode
|
||||
|
||||
**`/tree` command:**
|
||||
|
||||
- Navigate the full session tree in-place
|
||||
- Search by typing, page with ←/→
|
||||
- Filter modes (Ctrl+O): default → no-tools → user-only → labeled-only → all
|
||||
@@ -201,12 +221,14 @@ The `before_compact` and `before_tree` hook events allow custom compaction imple
|
||||
- Selecting a branch switches context and optionally injects a summary of the abandoned branch
|
||||
|
||||
**Entry labels:**
|
||||
|
||||
- Bookmark any entry via `/tree` → select → `l`
|
||||
- Labels appear in tree view and persist as `LabelEntry`
|
||||
|
||||
**Theme changes (breaking for custom themes):**
|
||||
|
||||
Custom themes must add these new color tokens or they will fail to load:
|
||||
|
||||
- `selectedBg`: background for selected/highlighted items in tree selector and other components
|
||||
- `customMessageBg`: background for hook-injected messages (`CustomMessageEntry`)
|
||||
- `customMessageText`: text color for hook messages
|
||||
@@ -215,6 +237,7 @@ Custom themes must add these new color tokens or they will fail to load:
|
||||
Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) for the full color list and copy values from the built-in dark/light themes.
|
||||
|
||||
**Settings:**
|
||||
|
||||
- `enabledModels`: allowlist models in `settings.json` (same format as `--models` CLI)
|
||||
|
||||
### Added
|
||||
@@ -312,13 +335,14 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
- **Credential storage refactored**: API keys and OAuth tokens are now stored in `~/.pi/agent/auth.json` instead of `oauth.json` and `settings.json`. Existing credentials are automatically migrated on first run. ([#296](https://github.com/badlogic/pi-mono/issues/296))
|
||||
|
||||
- **SDK API changes** ([#296](https://github.com/badlogic/pi-mono/issues/296)):
|
||||
|
||||
- Added `AuthStorage` class for credential management (API keys and OAuth tokens)
|
||||
- Added `ModelRegistry` class for model discovery and API key resolution
|
||||
- Added `discoverAuthStorage()` and `discoverModels()` discovery functions
|
||||
- `createAgentSession()` now accepts `authStorage` and `modelRegistry` options
|
||||
- Removed `configureOAuthStorage()`, `defaultGetApiKey()`, `findModel()`, `discoverAvailableModels()`
|
||||
- Removed `getApiKey` callback option (use `AuthStorage.setRuntimeApiKey()` for runtime overrides)
|
||||
- Use `getModel()` from `@mariozechner/pi-ai` for built-in models, `modelRegistry.find()` for custom models + built-in models
|
||||
- Use `getModel()` from `@oh-my-pi/pi-ai` for built-in models, `modelRegistry.find()` for custom models + built-in models
|
||||
- See updated [SDK documentation](docs/sdk.md) and [README](README.md)
|
||||
|
||||
- **Settings changes**: Removed `apiKeys` from `settings.json`. Use `auth.json` instead. ([#296](https://github.com/badlogic/pi-mono/issues/296))
|
||||
@@ -350,6 +374,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
### Added
|
||||
|
||||
- **Compaction hook improvements**: The `before_compact` session event now includes:
|
||||
|
||||
- `previousSummary`: Summary from the last compaction (if any), so hooks can preserve accumulated context
|
||||
- `messagesToKeep`: Messages that will be kept after the summary (recent turns), in addition to `messagesToSummarize`
|
||||
- `resolveApiKey`: Function to resolve API keys for any model (checks settings, OAuth, env vars)
|
||||
@@ -522,7 +547,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
|
||||
### Added
|
||||
|
||||
- **OAuth and model config exports**: Scripts using `AgentSession` directly can now import `getAvailableModels`, `getApiKeyForModel`, `findModel`, `login`, `logout`, and `getOAuthProviders` from `@mariozechner/pi-coding-agent` to reuse OAuth token storage and model resolution. ([#245](https://github.com/badlogic/pi-mono/issues/245))
|
||||
- **OAuth and model config exports**: Scripts using `AgentSession` directly can now import `getAvailableModels`, `getApiKeyForModel`, `findModel`, `login`, `logout`, and `getOAuthProviders` from `@oh-my-pi/pi-coding-agent` to reuse OAuth token storage and model resolution. ([#245](https://github.com/badlogic/pi-mono/issues/245))
|
||||
|
||||
- **xhigh thinking level for gpt-5.2 models**: The thinking level selector and shift+tab cycling now show xhigh option for gpt-5.2 and gpt-5.2-codex models (in addition to gpt-5.1-codex-max). ([#236](https://github.com/badlogic/pi-mono/pull/236) by [@theBucky](https://github.com/theBucky))
|
||||
|
||||
@@ -548,7 +573,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
|
||||
- **Subagent orchestration example**: Added comprehensive custom tool example for spawning and orchestrating sub-agents with isolated context windows. Includes scout/planner/reviewer/worker agents and workflow commands for multi-agent pipelines. ([#215](https://github.com/badlogic/pi-mono/pull/215) by [@nicobailon](https://github.com/nicobailon))
|
||||
|
||||
- **`getMarkdownTheme()` export**: Custom tools can now import `getMarkdownTheme()` from `@mariozechner/pi-coding-agent` to use the same markdown styling as the main UI.
|
||||
- **`getMarkdownTheme()` export**: Custom tools can now import `getMarkdownTheme()` from `@oh-my-pi/pi-coding-agent` to use the same markdown styling as the main UI.
|
||||
|
||||
- **`pi.exec()` signal and timeout support**: Custom tools and hooks can now pass `{ signal, timeout }` options to `pi.exec()` for cancellation and timeout handling. The result includes a `killed` flag when the process was terminated.
|
||||
|
||||
@@ -612,6 +637,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
- Improved system prompt documentation section with clearer pointers to specific doc files for custom models, themes, skills, hooks, custom tools, and RPC.
|
||||
|
||||
- Cleaned up documentation:
|
||||
|
||||
- `theme.md`: Added missing color tokens (`thinkingXhigh`, `bashMode`)
|
||||
- `skills.md`: Rewrote with better framing and examples
|
||||
- `hooks.md`: Fixed timeout/error handling docs, added import aliases section
|
||||
@@ -619,7 +645,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
- `rpc.md`: Added missing `hook_error` event documentation
|
||||
- `README.md`: Complete settings table, condensed philosophy section, standardized OAuth docs
|
||||
|
||||
- Hooks loader now supports same import aliases as custom tools (`@sinclair/typebox`, `@mariozechner/pi-ai`, `@mariozechner/pi-tui`, `@mariozechner/pi-coding-agent`).
|
||||
- Hooks loader now supports same import aliases as custom tools (`@sinclair/typebox`, `@oh-my-pi/pi-ai`, `@oh-my-pi/pi-tui`, `@oh-my-pi/pi-coding-agent`).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -641,7 +667,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
|
||||
- Fixed TUI performance regression caused by Box component lacking render caching. Built-in tools now use Text directly (like v0.22.5), and Box has proper caching for custom tool rendering.
|
||||
|
||||
- Fixed custom tools failing to load from `~/.pi/agent/tools/` when pi is installed globally. Module imports (`@sinclair/typebox`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`) are now resolved via aliases.
|
||||
- Fixed custom tools failing to load from `~/.pi/agent/tools/` when pi is installed globally. Module imports (`@sinclair/typebox`, `@oh-my-pi/pi-tui`, `@oh-my-pi/pi-ai`) are now resolved via aliases.
|
||||
|
||||
## [0.23.0] - 2025-12-17
|
||||
|
||||
@@ -681,7 +707,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
|
||||
- **Tool output display**: When collapsed, tool output now shows the last N lines instead of the first N lines, making streaming output more useful.
|
||||
|
||||
- Updated `@mariozechner/pi-ai` with X-Initiator header support for GitHub Copilot, ensuring agent calls are not deducted from quota. ([#200](https://github.com/badlogic/pi-mono/pull/200) by [@kim0](https://github.com/kim0))
|
||||
- Updated `@oh-my-pi/pi-ai` with X-Initiator header support for GitHub Copilot, ensuring agent calls are not deducted from quota. ([#200](https://github.com/badlogic/pi-mono/pull/200) by [@kim0](https://github.com/kim0))
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -693,7 +719,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `@mariozechner/pi-ai` with interleaved thinking enabled by default for Anthropic Claude 4 models.
|
||||
- Updated `@oh-my-pi/pi-ai` with interleaved thinking enabled by default for Anthropic Claude 4 models.
|
||||
|
||||
## [0.22.1] - 2025-12-15
|
||||
|
||||
@@ -701,7 +727,7 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `@mariozechner/pi-ai` with interleaved thinking support for Anthropic models.
|
||||
- Updated `@oh-my-pi/pi-ai` with interleaved thinking support for Anthropic models.
|
||||
|
||||
## [0.22.0] - 2025-12-15
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ The coding-agent is structured into distinct layers:
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ External Dependencies │
|
||||
│ @mariozechner/pi-agent (Agent, tools) │
|
||||
│ @mariozechner/pi-ai (models, providers) │
|
||||
│ @mariozechner/pi-tui (TUI components) │
|
||||
│ @oh-my-pi/pi-agent (Agent, tools) │
|
||||
│ @oh-my-pi/pi-ai (models, providers) │
|
||||
│ @oh-my-pi/pi-tui (TUI components) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -65,7 +65,7 @@ src/
|
||||
│ ├── system-prompt.ts # buildSystemPrompt(), loadProjectContextFiles()
|
||||
│ │
|
||||
│ ├── oauth/ # OAuth authentication (thin wrapper)
|
||||
│ │ └── index.ts # Re-exports from @mariozechner/pi-ai with convenience wrappers
|
||||
│ │ └── index.ts # Re-exports from @oh-my-pi/pi-ai with convenience wrappers
|
||||
│ │
|
||||
│ ├── hooks/ # Hook system for extending behavior
|
||||
│ │ ├── index.ts # Hook exports
|
||||
@@ -143,6 +143,7 @@ src/
|
||||
### AgentSession (core/agent-session.ts)
|
||||
|
||||
The central abstraction that wraps the low-level `Agent` with:
|
||||
|
||||
- Session persistence (via SessionManager)
|
||||
- Settings persistence (via SettingsManager)
|
||||
- Model cycling with scoped models
|
||||
@@ -157,6 +158,7 @@ All three modes (interactive, print, rpc) use AgentSession.
|
||||
### InteractiveMode (modes/interactive/interactive-mode.ts)
|
||||
|
||||
Handles TUI rendering and user interaction:
|
||||
|
||||
- Subscribes to AgentSession events
|
||||
- Renders messages, tool executions, streaming
|
||||
- Manages editor, selectors, key handlers
|
||||
@@ -175,6 +177,7 @@ The RPC mode exposes the full AgentSession API via JSON commands. See [docs/rpc.
|
||||
### SessionManager (core/session-manager.ts)
|
||||
|
||||
Handles session persistence:
|
||||
|
||||
- JSONL format for append-only writes
|
||||
- Session file location management
|
||||
- Message loading/saving
|
||||
@@ -183,6 +186,7 @@ Handles session persistence:
|
||||
### SettingsManager (core/settings-manager.ts)
|
||||
|
||||
Handles user preferences:
|
||||
|
||||
- Default model/provider
|
||||
- Theme selection
|
||||
- Queue mode
|
||||
@@ -193,6 +197,7 @@ Handles user preferences:
|
||||
### Hook System (core/hooks/)
|
||||
|
||||
Extensibility layer for intercepting agent behavior:
|
||||
|
||||
- **loader.ts**: Discovers and loads hooks from `~/.pi/agent/hooks/`, `.pi/hooks/`, and CLI
|
||||
- **runner.ts**: Dispatches events to registered hooks
|
||||
- **tool-wrapper.ts**: Wraps tools to emit `tool_call` and `tool_result` events
|
||||
@@ -203,6 +208,7 @@ See [docs/hooks.md](docs/hooks.md) for full documentation.
|
||||
### Custom Tools (core/custom-tools/)
|
||||
|
||||
System for adding LLM-callable tools:
|
||||
|
||||
- **loader.ts**: Discovers and loads tools from `~/.pi/agent/tools/`, `.pi/tools/`, and CLI
|
||||
- **types.ts**: `CustomToolFactory`, `CustomToolDefinition`, `CustomToolResult`
|
||||
|
||||
@@ -211,6 +217,7 @@ See [docs/custom-tools.md](docs/custom-tools.md) for full documentation.
|
||||
### Skills (core/skills.ts)
|
||||
|
||||
On-demand capability packages:
|
||||
|
||||
- Discovers SKILL.md files from multiple locations
|
||||
- Provides specialized workflows and instructions
|
||||
- Loaded when task matches description
|
||||
|
||||
+258
-232
@@ -51,20 +51,20 @@ Works on Linux, macOS, and Windows (requires bash; see [Windows Setup](#windows-
|
||||
**npm (recommended):**
|
||||
|
||||
```bash
|
||||
npm install -g @mariozechner/pi-coding-agent
|
||||
npm install -g @oh-my-pi/pi-coding-agent
|
||||
```
|
||||
|
||||
**Standalone binary:**
|
||||
|
||||
Download from [GitHub Releases](https://github.com/badlogic/pi-mono/releases):
|
||||
|
||||
| Platform | Archive |
|
||||
|----------|---------|
|
||||
| Platform | Archive |
|
||||
| ------------------- | ------------------------ |
|
||||
| macOS Apple Silicon | `pi-darwin-arm64.tar.gz` |
|
||||
| macOS Intel | `pi-darwin-x64.tar.gz` |
|
||||
| Linux x64 | `pi-linux-x64.tar.gz` |
|
||||
| Linux ARM64 | `pi-linux-arm64.tar.gz` |
|
||||
| Windows x64 | `pi-windows-x64.zip` |
|
||||
| macOS Intel | `pi-darwin-x64.tar.gz` |
|
||||
| Linux x64 | `pi-linux-x64.tar.gz` |
|
||||
| Linux ARM64 | `pi-linux-arm64.tar.gz` |
|
||||
| Windows x64 | `pi-windows-x64.zip` |
|
||||
|
||||
```bash
|
||||
# macOS/Linux
|
||||
@@ -81,7 +81,7 @@ pi.exe
|
||||
**Build from source** (requires [Bun](https://bun.sh) 1.0+):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/badlogic/pi-mono.git
|
||||
git clone https://github.com/can1357/oh-my-pi.git
|
||||
cd pi-mono && npm install
|
||||
cd packages/coding-agent && npm run build:binary
|
||||
./dist/pi
|
||||
@@ -102,7 +102,7 @@ For most users, [Git for Windows](https://git-scm.com/download/win) is sufficien
|
||||
```json
|
||||
// ~/.pi/agent/settings.json
|
||||
{
|
||||
"shellPath": "C:\\cygwin64\\bin\\bash.exe"
|
||||
"shellPath": "C:\\cygwin64\\bin\\bash.exe"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -114,25 +114,25 @@ Add API keys to `~/.pi/agent/auth.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
|
||||
"openai": { "type": "api_key", "key": "sk-..." },
|
||||
"google": { "type": "api_key", "key": "..." }
|
||||
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
|
||||
"openai": { "type": "api_key", "key": "sk-..." },
|
||||
"google": { "type": "api_key", "key": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Environment variables**
|
||||
|
||||
| Provider | Auth Key | Environment Variable |
|
||||
|----------|--------------|---------------------|
|
||||
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` |
|
||||
| OpenAI | `openai` | `OPENAI_API_KEY` |
|
||||
| Google | `google` | `GEMINI_API_KEY` |
|
||||
| Mistral | `mistral` | `MISTRAL_API_KEY` |
|
||||
| Groq | `groq` | `GROQ_API_KEY` |
|
||||
| Cerebras | `cerebras` | `CEREBRAS_API_KEY` |
|
||||
| xAI | `xai` | `XAI_API_KEY` |
|
||||
| Provider | Auth Key | Environment Variable |
|
||||
| ---------- | ------------ | -------------------- |
|
||||
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` |
|
||||
| OpenAI | `openai` | `OPENAI_API_KEY` |
|
||||
| Google | `google` | `GEMINI_API_KEY` |
|
||||
| Mistral | `mistral` | `MISTRAL_API_KEY` |
|
||||
| Groq | `groq` | `GROQ_API_KEY` |
|
||||
| Cerebras | `cerebras` | `CEREBRAS_API_KEY` |
|
||||
| xAI | `xai` | `XAI_API_KEY` |
|
||||
| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` |
|
||||
| ZAI | `zai` | `ZAI_API_KEY` |
|
||||
| ZAI | `zai` | `ZAI_API_KEY` |
|
||||
|
||||
Auth file keys take priority over environment variables.
|
||||
|
||||
@@ -140,12 +140,12 @@ Auth file keys take priority over environment variables.
|
||||
|
||||
Use `/login` to authenticate with subscription-based or free-tier providers:
|
||||
|
||||
| Provider | Models | Cost |
|
||||
|----------|--------|------|
|
||||
| Anthropic (Claude Pro/Max) | Claude models via your subscription | Subscription |
|
||||
| GitHub Copilot | GPT-4o, Claude, Gemini via Copilot subscription | Subscription |
|
||||
| Google Gemini CLI | Gemini 2.0/2.5 models | Free (Google account) |
|
||||
| Google Antigravity | Gemini 3, Claude, GPT-OSS | Free (Google account) |
|
||||
| Provider | Models | Cost |
|
||||
| -------------------------- | ----------------------------------------------- | --------------------- |
|
||||
| Anthropic (Claude Pro/Max) | Claude models via your subscription | Subscription |
|
||||
| GitHub Copilot | GPT-4o, Claude, Gemini via Copilot subscription | Subscription |
|
||||
| Google Gemini CLI | Gemini 2.0/2.5 models | Free (Google account) |
|
||||
| Google Antigravity | Gemini 3, Claude, GPT-OSS | Free (Google account) |
|
||||
|
||||
```bash
|
||||
pi
|
||||
@@ -155,10 +155,12 @@ pi
|
||||
**Note:** `/login` replaces any existing API key for that provider with OAuth credentials in `auth.json`.
|
||||
|
||||
**GitHub Copilot notes:**
|
||||
|
||||
- Press Enter for github.com, or enter your GitHub Enterprise Server domain
|
||||
- If you get "model not supported" error, enable it in VS Code: Copilot Chat → model selector → select model → "Enable"
|
||||
|
||||
**Google providers notes:**
|
||||
|
||||
- Gemini CLI uses the production Cloud Code Assist endpoint (standard Gemini models)
|
||||
- Antigravity uses a sandbox endpoint with access to Gemini 3, Claude (sonnet/opus thinking), and GPT-OSS models
|
||||
- Both are free with any Google account, subject to rate limits
|
||||
@@ -186,23 +188,23 @@ The agent reads, writes, and edits files, and executes commands via bash.
|
||||
|
||||
### Slash Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/settings` | Open settings menu (thinking, theme, queue mode, toggles) |
|
||||
| `/model` | Switch models mid-session (fuzzy search, arrow keys, Enter to select) |
|
||||
| `/export [file]` | Export session to self-contained HTML |
|
||||
| `/share` | Upload session as secret GitHub gist, get shareable URL (requires `gh` CLI) |
|
||||
| `/session` | Show session info: path, message counts, token usage, cost |
|
||||
| `/hotkeys` | Show all keyboard shortcuts |
|
||||
| `/changelog` | Display full version history |
|
||||
| `/tree` | Navigate session tree in-place (search, filter, label entries) |
|
||||
| `/branch` | Create new conversation branch from a previous message |
|
||||
| `/resume` | Switch to a different session (interactive selector) |
|
||||
| `/login` | OAuth login for subscription-based models |
|
||||
| `/logout` | Clear OAuth tokens |
|
||||
| `/new` | Start a new session |
|
||||
| `/copy` | Copy last agent message to clipboard |
|
||||
| `/compact [instructions]` | Manually compact conversation context |
|
||||
| Command | Description |
|
||||
| ------------------------- | --------------------------------------------------------------------------- |
|
||||
| `/settings` | Open settings menu (thinking, theme, queue mode, toggles) |
|
||||
| `/model` | Switch models mid-session (fuzzy search, arrow keys, Enter to select) |
|
||||
| `/export [file]` | Export session to self-contained HTML |
|
||||
| `/share` | Upload session as secret GitHub gist, get shareable URL (requires `gh` CLI) |
|
||||
| `/session` | Show session info: path, message counts, token usage, cost |
|
||||
| `/hotkeys` | Show all keyboard shortcuts |
|
||||
| `/changelog` | Display full version history |
|
||||
| `/tree` | Navigate session tree in-place (search, filter, label entries) |
|
||||
| `/branch` | Create new conversation branch from a previous message |
|
||||
| `/resume` | Switch to a different session (interactive selector) |
|
||||
| `/login` | OAuth login for subscription-based models |
|
||||
| `/logout` | Clear OAuth tokens |
|
||||
| `/new` | Start a new session |
|
||||
| `/copy` | Copy last agent message to clipboard |
|
||||
| `/compact [instructions]` | Manually compact conversation context |
|
||||
|
||||
### Editor Features
|
||||
|
||||
@@ -220,38 +222,38 @@ The agent reads, writes, and edits files, and executes commands via bash.
|
||||
|
||||
**Navigation:**
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Arrow keys | Move cursor / browse history (Up when empty) |
|
||||
| Option+Left/Right | Move by word |
|
||||
| Ctrl+A / Home / Cmd+Left | Start of line |
|
||||
| Ctrl+E / End / Cmd+Right | End of line |
|
||||
| Key | Action |
|
||||
| ------------------------ | -------------------------------------------- |
|
||||
| Arrow keys | Move cursor / browse history (Up when empty) |
|
||||
| Option+Left/Right | Move by word |
|
||||
| Ctrl+A / Home / Cmd+Left | Start of line |
|
||||
| Ctrl+E / End / Cmd+Right | End of line |
|
||||
|
||||
**Editing:**
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Enter | Send message |
|
||||
| Shift+Enter / Alt+Enter | New line (Ctrl+Enter on WSL) |
|
||||
| Ctrl+W / Option+Backspace | Delete word backwards |
|
||||
| Ctrl+U | Delete to start of line |
|
||||
| Ctrl+K | Delete to end of line |
|
||||
| Key | Action |
|
||||
| ------------------------- | ---------------------------- |
|
||||
| Enter | Send message |
|
||||
| Shift+Enter / Alt+Enter | New line (Ctrl+Enter on WSL) |
|
||||
| Ctrl+W / Option+Backspace | Delete word backwards |
|
||||
| Ctrl+U | Delete to start of line |
|
||||
| Ctrl+K | Delete to end of line |
|
||||
|
||||
**Other:**
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Tab | Path completion / accept autocomplete |
|
||||
| Escape | Cancel autocomplete / abort streaming |
|
||||
| Ctrl+C | Clear editor (first) / exit (second) |
|
||||
| Ctrl+D | Exit (when editor is empty) |
|
||||
| Ctrl+Z | Suspend to background (use `fg` in shell to resume) |
|
||||
| Shift+Tab | Cycle thinking level |
|
||||
| Ctrl+P / Shift+Ctrl+P | Cycle models forward/backward (scoped by `--models`) |
|
||||
| Ctrl+L | Open model selector |
|
||||
| Ctrl+O | Toggle tool output expansion |
|
||||
| Ctrl+T | Toggle thinking block visibility |
|
||||
| Ctrl+G | Edit message in external editor (`$VISUAL` or `$EDITOR`) |
|
||||
| Key | Action |
|
||||
| --------------------- | -------------------------------------------------------- |
|
||||
| Tab | Path completion / accept autocomplete |
|
||||
| Escape | Cancel autocomplete / abort streaming |
|
||||
| Ctrl+C | Clear editor (first) / exit (second) |
|
||||
| Ctrl+D | Exit (when editor is empty) |
|
||||
| Ctrl+Z | Suspend to background (use `fg` in shell to resume) |
|
||||
| Shift+Tab | Cycle thinking level |
|
||||
| Ctrl+P / Shift+Ctrl+P | Cycle models forward/backward (scoped by `--models`) |
|
||||
| Ctrl+L | Open model selector |
|
||||
| Ctrl+O | Toggle tool output expansion |
|
||||
| Ctrl+T | Toggle thinking block visibility |
|
||||
| Ctrl+G | Edit message in external editor (`$VISUAL` or `$EDITOR`) |
|
||||
|
||||
### Bash Mode
|
||||
|
||||
@@ -270,6 +272,7 @@ The output becomes part of your next prompt, formatted as:
|
||||
```
|
||||
Ran `ls -la`
|
||||
```
|
||||
|
||||
<output here>
|
||||
```
|
||||
```
|
||||
@@ -321,6 +324,7 @@ Long sessions can exhaust context windows. Compaction summarizes older messages
|
||||
**Manual:** `/compact` or `/compact Focus on the API changes`
|
||||
|
||||
**Automatic:** Enable via `/settings`. When enabled, triggers in two cases:
|
||||
|
||||
- **Overflow recovery**: LLM returns context overflow error. Compacts and auto-retries.
|
||||
- **Threshold maintenance**: Context exceeds `contextWindow - reserveTokens` after a successful turn. Compacts without retry.
|
||||
|
||||
@@ -330,11 +334,11 @@ When disabled, neither case triggers automatic compaction (use `/compact` manual
|
||||
|
||||
```json
|
||||
{
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
}
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -371,6 +375,7 @@ Pi loads `AGENTS.md` (or `CLAUDE.md`) files at startup in this order:
|
||||
3. **Current directory:** `./AGENTS.md`
|
||||
|
||||
Use these for:
|
||||
|
||||
- Project instructions and guidelines
|
||||
- Common commands and workflows
|
||||
- Architecture documentation
|
||||
@@ -379,10 +384,12 @@ Use these for:
|
||||
|
||||
```markdown
|
||||
# Common Commands
|
||||
|
||||
- npm run build: Build the project
|
||||
- npm test: Run tests
|
||||
|
||||
# Code Style
|
||||
|
||||
- Use TypeScript strict mode
|
||||
- Prefer async/await over promises
|
||||
```
|
||||
@@ -400,6 +407,7 @@ This is useful when using pi as different types of agents across repos (coding a
|
||||
You are a technical writing assistant. Help users write clear documentation.
|
||||
|
||||
Focus on:
|
||||
|
||||
- Concise explanations
|
||||
- Code examples
|
||||
- Proper formatting
|
||||
@@ -413,24 +421,24 @@ Add custom models (Ollama, vLLM, LM Studio, etc.) via `~/.pi/agent/models.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"apiKey": "OLLAMA_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "llama-3.1-8b",
|
||||
"name": "Llama 3.1 8B (Local)",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 32000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"apiKey": "OLLAMA_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "llama-3.1-8b",
|
||||
"name": "Llama 3.1 8B (Local)",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 32000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -463,16 +471,17 @@ Add custom models (Ollama, vLLM, LM Studio, etc.) via `~/.pi/agent/models.json`:
|
||||
|
||||
**OpenAI compatibility (`compat` field):**
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `supportsStore` | Whether provider supports `store` field |
|
||||
| `supportsDeveloperRole` | Use `developer` vs `system` role |
|
||||
| `supportsReasoningEffort` | Support for `reasoning_effort` parameter |
|
||||
| `maxTokensField` | Use `max_completion_tokens` or `max_tokens` |
|
||||
| Field | Description |
|
||||
| ------------------------- | ------------------------------------------- |
|
||||
| `supportsStore` | Whether provider supports `store` field |
|
||||
| `supportsDeveloperRole` | Use `developer` vs `system` role |
|
||||
| `supportsReasoningEffort` | Support for `reasoning_effort` parameter |
|
||||
| `maxTokensField` | Use `max_completion_tokens` or `max_tokens` |
|
||||
|
||||
**Live reload:** The file reloads each time you open `/model`. Edit during session; no restart needed.
|
||||
|
||||
**Model selection priority:**
|
||||
|
||||
1. CLI args (`--provider`, `--model`)
|
||||
2. First from `--models` scope (new sessions only)
|
||||
3. Restored from session (`--continue`, `--resume`)
|
||||
@@ -494,57 +503,57 @@ Global `~/.pi/agent/settings.json` stores persistent preferences:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "dark",
|
||||
"defaultProvider": "anthropic",
|
||||
"defaultModel": "claude-sonnet-4-20250514",
|
||||
"defaultThinkingLevel": "medium",
|
||||
"enabledModels": ["anthropic/*", "*gpt*", "gemini-2.5-pro:high"],
|
||||
"queueMode": "one-at-a-time",
|
||||
"shellPath": "C:\\path\\to\\bash.exe",
|
||||
"hideThinkingBlock": false,
|
||||
"collapseChangelog": false,
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
},
|
||||
"skills": {
|
||||
"enabled": true
|
||||
},
|
||||
"retry": {
|
||||
"enabled": true,
|
||||
"maxRetries": 3,
|
||||
"baseDelayMs": 2000
|
||||
},
|
||||
"terminal": {
|
||||
"showImages": true
|
||||
},
|
||||
"hooks": ["/path/to/hook.ts"],
|
||||
"customTools": ["/path/to/tool.ts"]
|
||||
"theme": "dark",
|
||||
"defaultProvider": "anthropic",
|
||||
"defaultModel": "claude-sonnet-4-20250514",
|
||||
"defaultThinkingLevel": "medium",
|
||||
"enabledModels": ["anthropic/*", "*gpt*", "gemini-2.5-pro:high"],
|
||||
"queueMode": "one-at-a-time",
|
||||
"shellPath": "C:\\path\\to\\bash.exe",
|
||||
"hideThinkingBlock": false,
|
||||
"collapseChangelog": false,
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
},
|
||||
"skills": {
|
||||
"enabled": true
|
||||
},
|
||||
"retry": {
|
||||
"enabled": true,
|
||||
"maxRetries": 3,
|
||||
"baseDelayMs": 2000
|
||||
},
|
||||
"terminal": {
|
||||
"showImages": true
|
||||
},
|
||||
"hooks": ["/path/to/hook.ts"],
|
||||
"customTools": ["/path/to/tool.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `theme` | Color theme name | auto-detected |
|
||||
| `defaultProvider` | Default model provider | - |
|
||||
| `defaultModel` | Default model ID | - |
|
||||
| `defaultThinkingLevel` | Thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | - |
|
||||
| `enabledModels` | Model patterns for cycling. Supports glob patterns (`github-copilot/*`, `*sonnet*`) and fuzzy matching. Same as `--models` CLI flag | - |
|
||||
| `queueMode` | Message queue mode: `all` or `one-at-a-time` | `one-at-a-time` |
|
||||
| `shellPath` | Custom bash path (Windows) | auto-detected |
|
||||
| `hideThinkingBlock` | Hide thinking blocks in output (Ctrl+T to toggle) | `false` |
|
||||
| `collapseChangelog` | Show condensed changelog after update | `false` |
|
||||
| `compaction.enabled` | Enable auto-compaction | `true` |
|
||||
| `compaction.reserveTokens` | Tokens to reserve before compaction triggers | `16384` |
|
||||
| `compaction.keepRecentTokens` | Recent tokens to keep after compaction | `20000` |
|
||||
| `skills.enabled` | Enable skills discovery | `true` |
|
||||
| `retry.enabled` | Auto-retry on transient errors | `true` |
|
||||
| `retry.maxRetries` | Maximum retry attempts | `3` |
|
||||
| `retry.baseDelayMs` | Base delay for exponential backoff | `2000` |
|
||||
| `terminal.showImages` | Render images inline (supported terminals) | `true` |
|
||||
| `hooks` | Additional hook file paths | `[]` |
|
||||
| `customTools` | Additional custom tool file paths | `[]` |
|
||||
| Setting | Description | Default |
|
||||
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------- |
|
||||
| `theme` | Color theme name | auto-detected |
|
||||
| `defaultProvider` | Default model provider | - |
|
||||
| `defaultModel` | Default model ID | - |
|
||||
| `defaultThinkingLevel` | Thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | - |
|
||||
| `enabledModels` | Model patterns for cycling. Supports glob patterns (`github-copilot/*`, `*sonnet*`) and fuzzy matching. Same as `--models` CLI flag | - |
|
||||
| `queueMode` | Message queue mode: `all` or `one-at-a-time` | `one-at-a-time` |
|
||||
| `shellPath` | Custom bash path (Windows) | auto-detected |
|
||||
| `hideThinkingBlock` | Hide thinking blocks in output (Ctrl+T to toggle) | `false` |
|
||||
| `collapseChangelog` | Show condensed changelog after update | `false` |
|
||||
| `compaction.enabled` | Enable auto-compaction | `true` |
|
||||
| `compaction.reserveTokens` | Tokens to reserve before compaction triggers | `16384` |
|
||||
| `compaction.keepRecentTokens` | Recent tokens to keep after compaction | `20000` |
|
||||
| `skills.enabled` | Enable skills discovery | `true` |
|
||||
| `retry.enabled` | Auto-retry on transient errors | `true` |
|
||||
| `retry.maxRetries` | Maximum retry attempts | `3` |
|
||||
| `retry.baseDelayMs` | Base delay for exponential backoff | `2000` |
|
||||
| `terminal.showImages` | Render images inline (supported terminals) | `true` |
|
||||
| `hooks` | Additional hook file paths | `[]` |
|
||||
| `customTools` | Additional custom tool file paths | `[]` |
|
||||
|
||||
---
|
||||
|
||||
@@ -560,7 +569,7 @@ Select theme via `/settings` or set in `~/.pi/agent/settings.json`.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.pi/agent/themes
|
||||
cp $(npm root -g)/@mariozechner/pi-coding-agent/dist/theme/dark.json ~/.pi/agent/themes/my-theme.json
|
||||
cp $(npm root -g)/@oh-my-pi/pi-coding-agent/dist/theme/dark.json ~/.pi/agent/themes/my-theme.json
|
||||
```
|
||||
|
||||
Select with `/settings`, then edit the file. Changes apply on save.
|
||||
@@ -574,6 +583,7 @@ Select with `/settings`, then edit the file. Changes apply on save.
|
||||
Define reusable prompts as Markdown files:
|
||||
|
||||
**Locations:**
|
||||
|
||||
- Global: `~/.pi/agent/commands/*.md`
|
||||
- Project: `.pi/commands/*.md`
|
||||
|
||||
@@ -583,7 +593,9 @@ Define reusable prompts as Markdown files:
|
||||
---
|
||||
description: Review staged git changes
|
||||
---
|
||||
|
||||
Review the staged changes (`git diff --cached`). Focus on:
|
||||
|
||||
- Bugs and logic errors
|
||||
- Security issues
|
||||
- Error handling gaps
|
||||
@@ -597,16 +609,17 @@ Filename (without `.md`) becomes the command name. Description shown in autocomp
|
||||
---
|
||||
description: Create a component
|
||||
---
|
||||
|
||||
Create a React component named $1 with features: $@
|
||||
```
|
||||
|
||||
Usage: `/component Button "onClick handler" "disabled support"`
|
||||
|
||||
- `$1` = `Button`
|
||||
- `$@` = all arguments joined
|
||||
|
||||
**Namespacing:** Subdirectories create prefixes. `.pi/commands/frontend/component.md` → `/component (project:frontend)`
|
||||
|
||||
|
||||
### Skills
|
||||
|
||||
Skills are self-contained capability packages that the agent loads on-demand. Pi implements the [Agent Skills standard](https://agentskills.io/specification), warning about violations but remaining lenient.
|
||||
@@ -614,6 +627,7 @@ Skills are self-contained capability packages that the agent loads on-demand. Pi
|
||||
A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks. Skills are loaded when the agent decides a task matches the description, or when you explicitly ask to use one.
|
||||
|
||||
**Example use cases:**
|
||||
|
||||
- Web search and content extraction (Brave Search API)
|
||||
- Browser automation via Chrome DevTools Protocol
|
||||
- Google Calendar, Gmail, Drive integration
|
||||
@@ -622,6 +636,7 @@ A skill provides specialized workflows, setup instructions, helper scripts, and
|
||||
- YouTube transcript extraction
|
||||
|
||||
**Skill locations:**
|
||||
|
||||
- Pi user: `~/.pi/agent/skills/**/SKILL.md` (recursive)
|
||||
- Pi project: `.pi/skills/**/SKILL.md` (recursive)
|
||||
- Claude Code: `~/.claude/skills/*/SKILL.md` and `.claude/skills/*/SKILL.md`
|
||||
@@ -638,13 +653,15 @@ description: Web search via Brave Search API. Use for documentation, facts, or w
|
||||
# Brave Search
|
||||
|
||||
## Setup
|
||||
|
||||
\`\`\`bash
|
||||
cd /path/to/brave-search && npm install
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`bash
|
||||
./search.js "query" # Basic search
|
||||
./search.js "query" # Basic search
|
||||
./search.js "query" --content # Include page content
|
||||
\`\`\`
|
||||
```
|
||||
@@ -667,6 +684,7 @@ Hooks are TypeScript modules that extend pi's behavior by subscribing to lifecyc
|
||||
- **Inject messages from external sources to wake up the agent** (file watchers, webhooks, CI systems)
|
||||
|
||||
**Hook locations:**
|
||||
|
||||
- Global: `~/.pi/agent/hooks/*.ts`
|
||||
- Project: `.pi/hooks/*.ts`
|
||||
- CLI: `--hook <path>` (for debugging)
|
||||
@@ -674,16 +692,16 @@ Hooks are TypeScript modules that extend pi's behavior by subscribing to lifecyc
|
||||
**Quick example** (permission gate):
|
||||
|
||||
```typescript
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName === "bash" && /sudo/.test(event.input.command as string)) {
|
||||
const ok = await ctx.ui.confirm("Allow sudo?", event.input.command as string);
|
||||
if (!ok) return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName === "bash" && /sudo/.test(event.input.command as string)) {
|
||||
const ok = await ctx.ui.confirm("Allow sudo?", event.input.command as string);
|
||||
if (!ok) return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -693,21 +711,24 @@ Use `pi.sendMessage(message, triggerTurn?)` to inject messages into the session.
|
||||
|
||||
```typescript
|
||||
import * as fs from "node:fs";
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_start", async () => {
|
||||
fs.watch("/tmp/trigger.txt", () => {
|
||||
const content = fs.readFileSync("/tmp/trigger.txt", "utf-8").trim();
|
||||
if (content) {
|
||||
pi.sendMessage({
|
||||
customType: "file-trigger",
|
||||
content,
|
||||
display: true,
|
||||
}, true); // triggerTurn: start agent loop
|
||||
}
|
||||
});
|
||||
});
|
||||
pi.on("session_start", async () => {
|
||||
fs.watch("/tmp/trigger.txt", () => {
|
||||
const content = fs.readFileSync("/tmp/trigger.txt", "utf-8").trim();
|
||||
if (content) {
|
||||
pi.sendMessage(
|
||||
{
|
||||
customType: "file-trigger",
|
||||
content,
|
||||
display: true,
|
||||
},
|
||||
true
|
||||
); // triggerTurn: start agent loop
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -720,10 +741,12 @@ export default function (pi: HookAPI) {
|
||||
Custom tools let you extend the built-in toolset (read, write, edit, bash, ...) and are called by the LLM directly. They are TypeScript modules that define tools with optional custom TUI integration for getting user input and custom tool call and result rendering.
|
||||
|
||||
**Tool locations (auto-discovered):**
|
||||
|
||||
- Global: `~/.pi/agent/tools/*/index.ts`
|
||||
- Project: `.pi/tools/*/index.ts`
|
||||
|
||||
**Explicit paths:**
|
||||
|
||||
- CLI: `--tool <path>` (any .ts file)
|
||||
- Settings: `customTools` array in `settings.json`
|
||||
|
||||
@@ -731,29 +754,30 @@ Custom tools let you extend the built-in toolset (read, write, edit, bash, ...)
|
||||
|
||||
```typescript
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
||||
import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const factory: CustomToolFactory = (pi) => ({
|
||||
name: "greet",
|
||||
label: "Greeting",
|
||||
description: "Generate a greeting",
|
||||
parameters: Type.Object({
|
||||
name: Type.String({ description: "Name to greet" }),
|
||||
}),
|
||||
name: "greet",
|
||||
label: "Greeting",
|
||||
description: "Generate a greeting",
|
||||
parameters: Type.Object({
|
||||
name: Type.String({ description: "Name to greet" }),
|
||||
}),
|
||||
|
||||
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
const { name } = params as { name: string };
|
||||
return {
|
||||
content: [{ type: "text", text: `Hello, ${name}!` }],
|
||||
details: { greeted: name },
|
||||
};
|
||||
},
|
||||
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
const { name } = params as { name: string };
|
||||
return {
|
||||
content: [{ type: "text", text: `Hello, ${name}!` }],
|
||||
details: { greeted: name },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default factory;
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Access to `pi.cwd`, `pi.exec()`, `pi.ui` (select/confirm/input dialogs)
|
||||
- Session lifecycle via `onSession` callback (for state reconstruction)
|
||||
- Custom rendering via `renderCall()` and `renderResult()` methods
|
||||
@@ -775,29 +799,29 @@ pi [options] [@files...] [messages...]
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--provider <name>` | Provider: `anthropic`, `openai`, `google`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`, `github-copilot`, `google-gemini-cli`, `google-antigravity`, or custom |
|
||||
| `--model <id>` | Model ID |
|
||||
| `--api-key <key>` | API key (overrides environment) |
|
||||
| `--system-prompt <text\|file>` | Custom system prompt (text or file path) |
|
||||
| `--append-system-prompt <text\|file>` | Append to system prompt |
|
||||
| `--mode <mode>` | Output mode: `text`, `json`, `rpc` (implies `--print`) |
|
||||
| `--print`, `-p` | Non-interactive: process prompt and exit |
|
||||
| `--no-session` | Don't save session |
|
||||
| `--session <path>` | Use specific session file |
|
||||
| `--session-dir <dir>` | Directory for session storage and lookup |
|
||||
| `--continue`, `-c` | Continue most recent session |
|
||||
| `--resume`, `-r` | Select session to resume |
|
||||
| `--models <patterns>` | Comma-separated patterns for Ctrl+P cycling. Supports glob patterns (e.g., `anthropic/*`, `*sonnet*:high`) and fuzzy matching (e.g., `sonnet,haiku:low`) |
|
||||
| `--tools <tools>` | Comma-separated tool list (default: `read,bash,edit,write`) |
|
||||
| `--thinking <level>` | Thinking level: `off`, `minimal`, `low`, `medium`, `high` |
|
||||
| `--hook <path>` | Load a hook file (can be used multiple times) |
|
||||
| `--no-skills` | Disable skills discovery and loading |
|
||||
| `--skills <patterns>` | Comma-separated glob patterns to filter skills (e.g., `git-*,docker`) |
|
||||
| `--export <file> [output]` | Export session to HTML |
|
||||
| `--help`, `-h` | Show help |
|
||||
| `--version`, `-v` | Show version |
|
||||
| Option | Description |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--provider <name>` | Provider: `anthropic`, `openai`, `google`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`, `github-copilot`, `google-gemini-cli`, `google-antigravity`, or custom |
|
||||
| `--model <id>` | Model ID |
|
||||
| `--api-key <key>` | API key (overrides environment) |
|
||||
| `--system-prompt <text\|file>` | Custom system prompt (text or file path) |
|
||||
| `--append-system-prompt <text\|file>` | Append to system prompt |
|
||||
| `--mode <mode>` | Output mode: `text`, `json`, `rpc` (implies `--print`) |
|
||||
| `--print`, `-p` | Non-interactive: process prompt and exit |
|
||||
| `--no-session` | Don't save session |
|
||||
| `--session <path>` | Use specific session file |
|
||||
| `--session-dir <dir>` | Directory for session storage and lookup |
|
||||
| `--continue`, `-c` | Continue most recent session |
|
||||
| `--resume`, `-r` | Select session to resume |
|
||||
| `--models <patterns>` | Comma-separated patterns for Ctrl+P cycling. Supports glob patterns (e.g., `anthropic/*`, `*sonnet*:high`) and fuzzy matching (e.g., `sonnet,haiku:low`) |
|
||||
| `--tools <tools>` | Comma-separated tool list (default: `read,bash,edit,write`) |
|
||||
| `--thinking <level>` | Thinking level: `off`, `minimal`, `low`, `medium`, `high` |
|
||||
| `--hook <path>` | Load a hook file (can be used multiple times) |
|
||||
| `--no-skills` | Disable skills discovery and loading |
|
||||
| `--skills <patterns>` | Comma-separated glob patterns to filter skills (e.g., `git-*,docker`) |
|
||||
| `--export <file> [output]` | Export session to HTML |
|
||||
| `--help`, `-h` | Show help |
|
||||
| `--version`, `-v` | Show version |
|
||||
|
||||
### File Arguments
|
||||
|
||||
@@ -857,22 +881,22 @@ pi --export session.jsonl output.html
|
||||
|
||||
### Default Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `read` | Read file contents. Images sent as attachments. Text: first 2000 lines, lines truncated at 2000 chars. Use offset/limit for large files. |
|
||||
| `write` | Write/overwrite file. Creates parent directories. |
|
||||
| `edit` | Replace exact text in file. Must match exactly including whitespace. Fails if text appears multiple times or not found. |
|
||||
| `bash` | Execute command. Returns stdout/stderr. Optional `timeout` parameter. |
|
||||
| Tool | Description |
|
||||
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `read` | Read file contents. Images sent as attachments. Text: first 2000 lines, lines truncated at 2000 chars. Use offset/limit for large files. |
|
||||
| `write` | Write/overwrite file. Creates parent directories. |
|
||||
| `edit` | Replace exact text in file. Must match exactly including whitespace. Fails if text appears multiple times or not found. |
|
||||
| `bash` | Execute command. Returns stdout/stderr. Optional `timeout` parameter. |
|
||||
|
||||
### Read-Only Tools
|
||||
|
||||
Available via `--tools` flag:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| Tool | Description |
|
||||
| ------ | --------------------------------------------------------------- |
|
||||
| `grep` | Search file contents (regex or literal). Respects `.gitignore`. |
|
||||
| `find` | Search for files by glob pattern. Respects `.gitignore`. |
|
||||
| `ls` | List directory contents. Includes dotfiles. |
|
||||
| `find` | Search for files by glob pattern. Respects `.gitignore`. |
|
||||
| `ls` | List directory contents. Includes dotfiles. |
|
||||
|
||||
Example: `--tools read,grep,find,ls` for code review without modification.
|
||||
|
||||
@@ -887,27 +911,28 @@ For adding new tools, see [Custom Tools](#custom-tools) in the Configuration sec
|
||||
For embedding pi in Node.js/TypeScript applications, use the SDK:
|
||||
|
||||
```typescript
|
||||
import { createAgentSession, discoverAuthStorage, discoverModels, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, discoverAuthStorage, discoverModels, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const authStorage = discoverAuthStorage();
|
||||
const modelRegistry = discoverModels(authStorage);
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await session.prompt("What files are in the current directory?");
|
||||
```
|
||||
|
||||
The SDK provides full control over:
|
||||
|
||||
- Model selection and thinking level
|
||||
- System prompt (replace or modify)
|
||||
- Tools (built-in subsets, custom tools)
|
||||
@@ -930,6 +955,7 @@ pi --mode rpc --no-session
|
||||
```
|
||||
|
||||
Send JSON commands on stdin:
|
||||
|
||||
```json
|
||||
{"type":"prompt","message":"List all .ts files"}
|
||||
{"type":"abort"}
|
||||
@@ -976,10 +1002,10 @@ Configure via `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"piConfig": {
|
||||
"name": "pi",
|
||||
"configDir": ".pi"
|
||||
}
|
||||
"piConfig": {
|
||||
"name": "pi",
|
||||
"configDir": ".pi"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1011,5 +1037,5 @@ MIT
|
||||
|
||||
## See Also
|
||||
|
||||
- [@mariozechner/pi-ai](https://www.npmjs.com/package/@mariozechner/pi-ai): Core LLM toolkit
|
||||
- [@mariozechner/pi-agent](https://www.npmjs.com/package/@mariozechner/pi-agent): Agent framework
|
||||
- [@oh-my-pi/pi-ai](https://www.npmjs.com/package/@oh-my-pi/pi-ai): Core LLM toolkit
|
||||
- [@oh-my-pi/pi-agent](https://www.npmjs.com/package/@oh-my-pi/pi-agent): Agent framework
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
LLMs have limited context windows. When conversations grow too long, pi uses compaction to summarize older content while preserving recent work. This page covers both auto-compaction and branch summarization.
|
||||
|
||||
**Source files:**
|
||||
|
||||
- [`src/core/compaction/compaction.ts`](../src/core/compaction/compaction.ts) - Auto-compaction logic
|
||||
- [`src/core/compaction/branch-summarization.ts`](../src/core/compaction/branch-summarization.ts) - Branch summarization
|
||||
- [`src/core/compaction/utils.ts`](../src/core/compaction/utils.ts) - Shared utilities (file tracking, serialization)
|
||||
@@ -13,10 +14,10 @@ LLMs have limited context windows. When conversations grow too long, pi uses com
|
||||
|
||||
Pi has two summarization mechanisms:
|
||||
|
||||
| Mechanism | Trigger | Purpose |
|
||||
|-----------|---------|---------|
|
||||
| Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |
|
||||
| Branch summarization | `/tree` navigation | Preserve context when switching branches |
|
||||
| Mechanism | Trigger | Purpose |
|
||||
| -------------------- | ---------------------------------------- | ----------------------------------------- |
|
||||
| Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |
|
||||
| Branch summarization | `/tree` navigation | Preserve context when switching branches |
|
||||
|
||||
Both use the same structured summary format and track file operations cumulatively.
|
||||
|
||||
@@ -99,12 +100,14 @@ Split turn (one huge turn exceeds budget):
|
||||
```
|
||||
|
||||
For split turns, pi generates two summaries and merges them:
|
||||
|
||||
1. **History summary**: Previous context (if any)
|
||||
2. **Turn prefix summary**: The early part of the split turn
|
||||
|
||||
### Cut Point Rules
|
||||
|
||||
Valid cut points are:
|
||||
|
||||
- User messages
|
||||
- Assistant messages
|
||||
- BashExecution messages
|
||||
@@ -118,21 +121,21 @@ Defined in [`src/core/session-manager.ts`](../src/core/session-manager.ts):
|
||||
|
||||
```typescript
|
||||
interface CompactionEntry<T = unknown> {
|
||||
type: "compaction";
|
||||
id: string;
|
||||
parentId: string;
|
||||
timestamp: number;
|
||||
summary: string;
|
||||
firstKeptEntryId: string;
|
||||
tokensBefore: number;
|
||||
fromHook?: boolean; // true if hook provided the compaction
|
||||
details?: T; // hook-specific data
|
||||
type: "compaction";
|
||||
id: string;
|
||||
parentId: string;
|
||||
timestamp: number;
|
||||
summary: string;
|
||||
firstKeptEntryId: string;
|
||||
tokensBefore: number;
|
||||
fromHook?: boolean; // true if hook provided the compaction
|
||||
details?: T; // hook-specific data
|
||||
}
|
||||
|
||||
// Default compaction uses this for details (from compaction.ts):
|
||||
interface CompactionDetails {
|
||||
readFiles: string[];
|
||||
modifiedFiles: string[];
|
||||
readFiles: string[];
|
||||
modifiedFiles: string[];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -174,6 +177,7 @@ After navigation with summary:
|
||||
### Cumulative File Tracking
|
||||
|
||||
Both compaction and branch summarization track files cumulatively. When generating a summary, pi extracts file operations from:
|
||||
|
||||
- Tool calls in the messages being summarized
|
||||
- Previous compaction or branch summary `details` (if any)
|
||||
|
||||
@@ -185,20 +189,20 @@ Defined in [`src/core/session-manager.ts`](../src/core/session-manager.ts):
|
||||
|
||||
```typescript
|
||||
interface BranchSummaryEntry<T = unknown> {
|
||||
type: "branch_summary";
|
||||
id: string;
|
||||
parentId: string;
|
||||
timestamp: number;
|
||||
summary: string;
|
||||
fromId: string; // Entry we navigated from
|
||||
fromHook?: boolean; // true if hook provided the summary
|
||||
details?: T; // hook-specific data
|
||||
type: "branch_summary";
|
||||
id: string;
|
||||
parentId: string;
|
||||
timestamp: number;
|
||||
summary: string;
|
||||
fromId: string; // Entry we navigated from
|
||||
fromHook?: boolean; // true if hook provided the summary
|
||||
details?: T; // hook-specific data
|
||||
}
|
||||
|
||||
// Default branch summarization uses this for details (from branch-summarization.ts):
|
||||
interface BranchSummaryDetails {
|
||||
readFiles: string[];
|
||||
modifiedFiles: string[];
|
||||
readFiles: string[];
|
||||
modifiedFiles: string[];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -212,28 +216,37 @@ Both compaction and branch summarization use the same structured format:
|
||||
|
||||
```markdown
|
||||
## Goal
|
||||
|
||||
[What the user is trying to accomplish]
|
||||
|
||||
## Constraints & Preferences
|
||||
|
||||
- [Requirements mentioned by user]
|
||||
|
||||
## Progress
|
||||
|
||||
### Done
|
||||
|
||||
- [x] [Completed tasks]
|
||||
|
||||
### In Progress
|
||||
|
||||
- [ ] [Current work]
|
||||
|
||||
### Blocked
|
||||
|
||||
- [Issues, if any]
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- **[Decision]**: [Rationale]
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. [What should happen next]
|
||||
|
||||
## Critical Context
|
||||
|
||||
- [Data needed to continue]
|
||||
|
||||
<read-files>
|
||||
@@ -270,31 +283,33 @@ Fired before auto-compaction or `/compact`. Can cancel or provide custom summary
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
const { preparation, branchEntries, customInstructions, signal } = event;
|
||||
const { preparation, branchEntries, customInstructions, signal } = event;
|
||||
|
||||
// preparation.messagesToSummarize - messages to summarize
|
||||
// preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)
|
||||
// preparation.previousSummary - previous compaction summary
|
||||
// preparation.fileOps - extracted file operations
|
||||
// preparation.tokensBefore - context tokens before compaction
|
||||
// preparation.firstKeptEntryId - where kept messages start
|
||||
// preparation.settings - compaction settings
|
||||
// preparation.messagesToSummarize - messages to summarize
|
||||
// preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)
|
||||
// preparation.previousSummary - previous compaction summary
|
||||
// preparation.fileOps - extracted file operations
|
||||
// preparation.tokensBefore - context tokens before compaction
|
||||
// preparation.firstKeptEntryId - where kept messages start
|
||||
// preparation.settings - compaction settings
|
||||
|
||||
// branchEntries - all entries on current branch (for custom state)
|
||||
// signal - AbortSignal (pass to LLM calls)
|
||||
// branchEntries - all entries on current branch (for custom state)
|
||||
// signal - AbortSignal (pass to LLM calls)
|
||||
|
||||
// Cancel:
|
||||
return { cancel: true };
|
||||
// Cancel:
|
||||
return { cancel: true };
|
||||
|
||||
// Custom summary:
|
||||
return {
|
||||
compaction: {
|
||||
summary: "Your summary...",
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
details: { /* custom data */ },
|
||||
}
|
||||
};
|
||||
// Custom summary:
|
||||
return {
|
||||
compaction: {
|
||||
summary: "Your summary...",
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
details: {
|
||||
/* custom data */
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
@@ -303,32 +318,30 @@ pi.on("session_before_compact", async (event, ctx) => {
|
||||
To generate a summary with your own model, convert messages to text using `serializeConversation`:
|
||||
|
||||
```typescript
|
||||
import { convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
|
||||
import { convertToLlm, serializeConversation } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
const { preparation } = event;
|
||||
|
||||
// Convert AgentMessage[] to Message[], then serialize to text
|
||||
const conversationText = serializeConversation(
|
||||
convertToLlm(preparation.messagesToSummarize)
|
||||
);
|
||||
// Returns:
|
||||
// [User]: message text
|
||||
// [Assistant thinking]: thinking content
|
||||
// [Assistant]: response text
|
||||
// [Assistant tool calls]: read(path="..."); bash(command="...")
|
||||
// [Tool result]: output text
|
||||
const { preparation } = event;
|
||||
|
||||
// Now send to your model for summarization
|
||||
const summary = await myModel.summarize(conversationText);
|
||||
|
||||
return {
|
||||
compaction: {
|
||||
summary,
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
}
|
||||
};
|
||||
// Convert AgentMessage[] to Message[], then serialize to text
|
||||
const conversationText = serializeConversation(convertToLlm(preparation.messagesToSummarize));
|
||||
// Returns:
|
||||
// [User]: message text
|
||||
// [Assistant thinking]: thinking content
|
||||
// [Assistant]: response text
|
||||
// [Assistant tool calls]: read(path="..."); bash(command="...")
|
||||
// [Tool result]: output text
|
||||
|
||||
// Now send to your model for summarization
|
||||
const summary = await myModel.summarize(conversationText);
|
||||
|
||||
return {
|
||||
compaction: {
|
||||
summary,
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
@@ -340,26 +353,28 @@ Fired before `/tree` navigation. Always fires regardless of whether user chose t
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_tree", async (event, ctx) => {
|
||||
const { preparation, signal } = event;
|
||||
const { preparation, signal } = event;
|
||||
|
||||
// preparation.targetId - where we're navigating to
|
||||
// preparation.oldLeafId - current position (being abandoned)
|
||||
// preparation.commonAncestorId - shared ancestor
|
||||
// preparation.entriesToSummarize - entries that would be summarized
|
||||
// preparation.userWantsSummary - whether user chose to summarize
|
||||
// preparation.targetId - where we're navigating to
|
||||
// preparation.oldLeafId - current position (being abandoned)
|
||||
// preparation.commonAncestorId - shared ancestor
|
||||
// preparation.entriesToSummarize - entries that would be summarized
|
||||
// preparation.userWantsSummary - whether user chose to summarize
|
||||
|
||||
// Cancel navigation entirely:
|
||||
return { cancel: true };
|
||||
// Cancel navigation entirely:
|
||||
return { cancel: true };
|
||||
|
||||
// Provide custom summary (only used if userWantsSummary is true):
|
||||
if (preparation.userWantsSummary) {
|
||||
return {
|
||||
summary: {
|
||||
summary: "Your summary...",
|
||||
details: { /* custom data */ },
|
||||
}
|
||||
};
|
||||
}
|
||||
// Provide custom summary (only used if userWantsSummary is true):
|
||||
if (preparation.userWantsSummary) {
|
||||
return {
|
||||
summary: {
|
||||
summary: "Your summary...",
|
||||
details: {
|
||||
/* custom data */
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -371,18 +386,18 @@ Configure compaction in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settin
|
||||
|
||||
```json
|
||||
{
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
}
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `enabled` | `true` | Enable auto-compaction |
|
||||
| `reserveTokens` | `16384` | Tokens to reserve for LLM response |
|
||||
| Setting | Default | Description |
|
||||
| ------------------ | ------- | -------------------------------------- |
|
||||
| `enabled` | `true` | Enable auto-compaction |
|
||||
| `reserveTokens` | `16384` | Tokens to reserve for LLM response |
|
||||
| `keepRecentTokens` | `20000` | Recent tokens to keep (not summarized) |
|
||||
|
||||
Disable auto-compaction with `"enabled": false`. You can still compact manually with `/compact`.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
Custom tools are additional tools that the LLM can call directly, just like the built-in `read`, `write`, `edit`, and `bash` tools. They are TypeScript modules that define callable functions with parameters, return values, and optional TUI rendering.
|
||||
|
||||
**Key capabilities:**
|
||||
|
||||
- **User interaction** - Prompt users via `pi.ui` (select, confirm, input dialogs)
|
||||
- **Custom rendering** - Control how tool calls and results appear via `renderCall`/`renderResult`
|
||||
- **TUI components** - Render custom components with `pi.ui.custom()` (see [tui.md](tui.md))
|
||||
@@ -12,6 +13,7 @@ Custom tools are additional tools that the LLM can call directly, just like the
|
||||
- **Streaming results** - Send partial updates via `onUpdate` callback
|
||||
|
||||
**Example use cases:**
|
||||
|
||||
- Interactive dialogs (questions with selectable options)
|
||||
- Stateful tools (todo lists, connection pools)
|
||||
- Rich output rendering (progress indicators, structured views)
|
||||
@@ -19,12 +21,12 @@ Custom tools are additional tools that the LLM can call directly, just like the
|
||||
|
||||
**When to use custom tools vs. alternatives:**
|
||||
|
||||
| Need | Solution |
|
||||
|------|----------|
|
||||
| Always-needed context (conventions, commands) | AGENTS.md |
|
||||
| User triggers a specific prompt template | Slash command |
|
||||
| On-demand capability package (workflows, scripts, setup) | Skill |
|
||||
| Additional tool directly callable by the LLM | **Custom tool** |
|
||||
| Need | Solution |
|
||||
| -------------------------------------------------------- | --------------- |
|
||||
| Always-needed context (conventions, commands) | AGENTS.md |
|
||||
| User triggers a specific prompt template | Slash command |
|
||||
| On-demand capability package (workflows, scripts, setup) | Skill |
|
||||
| Additional tool directly callable by the LLM | **Custom tool** |
|
||||
|
||||
See [examples/custom-tools/](../examples/custom-tools/) for working examples.
|
||||
|
||||
@@ -33,23 +35,23 @@ See [examples/custom-tools/](../examples/custom-tools/) for working examples.
|
||||
Create a file `~/.pi/agent/tools/hello/index.ts`:
|
||||
|
||||
```typescript
|
||||
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
||||
import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const factory: CustomToolFactory = (pi) => ({
|
||||
name: "hello",
|
||||
label: "Hello",
|
||||
description: "A simple greeting tool",
|
||||
parameters: pi.typebox.Type.Object({
|
||||
name: pi.typebox.Type.String({ description: "Name to greet" }),
|
||||
}),
|
||||
name: "hello",
|
||||
label: "Hello",
|
||||
description: "A simple greeting tool",
|
||||
parameters: pi.typebox.Type.Object({
|
||||
name: pi.typebox.Type.String({ description: "Name to greet" }),
|
||||
}),
|
||||
|
||||
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
const { name } = params as { name: string };
|
||||
return {
|
||||
content: [{ type: "text", text: `Hello, ${name}!` }],
|
||||
details: { greeted: name },
|
||||
};
|
||||
},
|
||||
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
const { name } = params as { name: string };
|
||||
return {
|
||||
content: [{ type: "text", text: `Hello, ${name}!` }],
|
||||
details: { greeted: name },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default factory;
|
||||
@@ -61,14 +63,15 @@ The tool is automatically discovered and available in your next pi session.
|
||||
|
||||
Tools must be in a subdirectory with an `index.ts` entry point:
|
||||
|
||||
| Location | Scope | Auto-discovered |
|
||||
|----------|-------|-----------------|
|
||||
| `~/.pi/agent/tools/*/index.ts` | Global (all projects) | Yes |
|
||||
| `.pi/tools/*/index.ts` | Project-local | Yes |
|
||||
| `settings.json` `customTools` array | Configured paths | Yes |
|
||||
| `--tool <path>` CLI flag | One-off/debugging | No |
|
||||
| Location | Scope | Auto-discovered |
|
||||
| ----------------------------------- | --------------------- | --------------- |
|
||||
| `~/.pi/agent/tools/*/index.ts` | Global (all projects) | Yes |
|
||||
| `.pi/tools/*/index.ts` | Project-local | Yes |
|
||||
| `settings.json` `customTools` array | Configured paths | Yes |
|
||||
| `--tool <path>` CLI flag | One-off/debugging | No |
|
||||
|
||||
**Example structure:**
|
||||
|
||||
```
|
||||
~/.pi/agent/tools/
|
||||
├── hello/
|
||||
@@ -87,12 +90,12 @@ Tools must be in a subdirectory with an `index.ts` entry point:
|
||||
|
||||
Custom tools can import from these packages:
|
||||
|
||||
| Package | Purpose | Import Method |
|
||||
|---------|---------|---------------|
|
||||
| `@sinclair/typebox` | Schema definitions (`Type.Object`, `Type.String`, etc.) | Via `pi.typebox.*` (injected) |
|
||||
| `@mariozechner/pi-coding-agent` | Types and utilities | Via `pi.pi.*` (injected) or direct import for types |
|
||||
| `@mariozechner/pi-ai` | AI utilities (`StringEnum` for Google-compatible enums) | Via `pi.pi.*` (re-exported through coding-agent) |
|
||||
| `@mariozechner/pi-tui` | TUI components (`Text`, `Box`, etc. for custom rendering) | Via `pi.pi.*` (re-exported through coding-agent) |
|
||||
| Package | Purpose | Import Method |
|
||||
| --------------------------- | --------------------------------------------------------- | --------------------------------------------------- |
|
||||
| `@sinclair/typebox` | Schema definitions (`Type.Object`, `Type.String`, etc.) | Via `pi.typebox.*` (injected) |
|
||||
| `@oh-my-pi/pi-coding-agent` | Types and utilities | Via `pi.pi.*` (injected) or direct import for types |
|
||||
| `@oh-my-pi/pi-ai` | AI utilities (`StringEnum` for Google-compatible enums) | Via `pi.pi.*` (re-exported through coding-agent) |
|
||||
| `@oh-my-pi/pi-tui` | TUI components (`Text`, `Box`, etc. for custom rendering) | Via `pi.pi.*` (re-exported through coding-agent) |
|
||||
|
||||
Node.js built-in modules (`node:fs`, `node:path`, etc.) are also available.
|
||||
|
||||
@@ -102,51 +105,57 @@ Node.js built-in modules (`node:fs`, `node:path`, etc.) are also available.
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
CustomTool,
|
||||
CustomToolContext,
|
||||
CustomToolFactory,
|
||||
CustomToolSessionEvent,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
CustomTool,
|
||||
CustomToolContext,
|
||||
CustomToolFactory,
|
||||
CustomToolSessionEvent,
|
||||
} from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const factory: CustomToolFactory = (pi) => {
|
||||
// Destructure injected dependencies
|
||||
const { Type } = pi.typebox;
|
||||
const { StringEnum } = pi.pi;
|
||||
const { Text } = pi.pi;
|
||||
// Destructure injected dependencies
|
||||
const { Type } = pi.typebox;
|
||||
const { StringEnum } = pi.pi;
|
||||
const { Text } = pi.pi;
|
||||
|
||||
return {
|
||||
name: "my_tool",
|
||||
label: "My Tool",
|
||||
description: "What this tool does (be specific for LLM)",
|
||||
parameters: Type.Object({
|
||||
// Use StringEnum for string enums (Google API compatible)
|
||||
action: StringEnum(["list", "add", "remove"] as const),
|
||||
text: Type.Optional(Type.String()),
|
||||
}),
|
||||
return {
|
||||
name: "my_tool",
|
||||
label: "My Tool",
|
||||
description: "What this tool does (be specific for LLM)",
|
||||
parameters: Type.Object({
|
||||
// Use StringEnum for string enums (Google API compatible)
|
||||
action: StringEnum(["list", "add", "remove"] as const),
|
||||
text: Type.Optional(Type.String()),
|
||||
}),
|
||||
|
||||
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
// signal - AbortSignal for cancellation
|
||||
// onUpdate - Callback for streaming partial results
|
||||
// ctx - CustomToolContext with sessionManager, modelRegistry, model
|
||||
return {
|
||||
content: [{ type: "text", text: "Result for LLM" }],
|
||||
details: { /* structured data for rendering */ },
|
||||
};
|
||||
},
|
||||
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
// signal - AbortSignal for cancellation
|
||||
// onUpdate - Callback for streaming partial results
|
||||
// ctx - CustomToolContext with sessionManager, modelRegistry, model
|
||||
return {
|
||||
content: [{ type: "text", text: "Result for LLM" }],
|
||||
details: {
|
||||
/* structured data for rendering */
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// Optional: Session lifecycle callback
|
||||
onSession(event, ctx) {
|
||||
if (event.reason === "shutdown") {
|
||||
// Cleanup resources (close connections, save state, etc.)
|
||||
return;
|
||||
}
|
||||
// Reconstruct state from ctx.sessionManager.getBranch()
|
||||
},
|
||||
// Optional: Session lifecycle callback
|
||||
onSession(event, ctx) {
|
||||
if (event.reason === "shutdown") {
|
||||
// Cleanup resources (close connections, save state, etc.)
|
||||
return;
|
||||
}
|
||||
// Reconstruct state from ctx.sessionManager.getBranch()
|
||||
},
|
||||
|
||||
// Optional: Custom rendering
|
||||
renderCall(args, theme) { /* return Component */ },
|
||||
renderResult(result, options, theme) { /* return Component */ },
|
||||
};
|
||||
// Optional: Custom rendering
|
||||
renderCall(args, theme) {
|
||||
/* return Component */
|
||||
},
|
||||
renderResult(result, options, theme) {
|
||||
/* return Component */
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default factory;
|
||||
@@ -160,32 +169,32 @@ The factory receives a `CustomToolAPI` object (named `pi` by convention):
|
||||
|
||||
```typescript
|
||||
interface CustomToolAPI {
|
||||
cwd: string; // Current working directory
|
||||
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
||||
ui: ToolUIContext;
|
||||
hasUI: boolean; // false in --print or --mode rpc
|
||||
typebox: typeof import("@sinclair/typebox"); // Injected @sinclair/typebox
|
||||
pi: typeof import("@mariozechner/pi-coding-agent"); // Injected pi-coding-agent exports
|
||||
cwd: string; // Current working directory
|
||||
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
||||
ui: ToolUIContext;
|
||||
hasUI: boolean; // false in --print or --mode rpc
|
||||
typebox: typeof import("@sinclair/typebox"); // Injected @sinclair/typebox
|
||||
pi: typeof import("@oh-my-pi/pi-coding-agent"); // Injected pi-coding-agent exports
|
||||
}
|
||||
|
||||
interface ToolUIContext {
|
||||
select(title: string, options: string[]): Promise<string | undefined>;
|
||||
confirm(title: string, message: string): Promise<boolean>;
|
||||
input(title: string, placeholder?: string): Promise<string | undefined>;
|
||||
notify(message: string, type?: "info" | "warning" | "error"): void;
|
||||
custom(component: Component & { dispose?(): void }): { close: () => void; requestRender: () => void };
|
||||
select(title: string, options: string[]): Promise<string | undefined>;
|
||||
confirm(title: string, message: string): Promise<boolean>;
|
||||
input(title: string, placeholder?: string): Promise<string | undefined>;
|
||||
notify(message: string, type?: "info" | "warning" | "error"): void;
|
||||
custom(component: Component & { dispose?(): void }): { close: () => void; requestRender: () => void };
|
||||
}
|
||||
|
||||
interface ExecOptions {
|
||||
signal?: AbortSignal; // Cancel the process
|
||||
timeout?: number; // Timeout in milliseconds
|
||||
signal?: AbortSignal; // Cancel the process
|
||||
timeout?: number; // Timeout in milliseconds
|
||||
}
|
||||
|
||||
interface ExecResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
killed?: boolean; // True if process was killed by signal/timeout
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
killed?: boolean; // True if process was killed by signal/timeout
|
||||
}
|
||||
```
|
||||
|
||||
@@ -212,18 +221,19 @@ async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
```typescript
|
||||
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
const { path } = params as { path: string };
|
||||
|
||||
|
||||
// Throw on error - pi will catch it and report to the LLM
|
||||
if (!fs.existsSync(path)) {
|
||||
throw new Error(`File not found: ${path}`);
|
||||
}
|
||||
|
||||
|
||||
// Return content only on success
|
||||
return { content: [{ type: "text", text: "Success" }] };
|
||||
}
|
||||
```
|
||||
|
||||
Thrown errors are:
|
||||
|
||||
- Reported to the LLM as tool errors (with `isError: true`)
|
||||
- Emitted to hooks via `tool_result` event (hooks can inspect `event.isError`)
|
||||
- Displayed in the TUI with error styling
|
||||
@@ -234,12 +244,12 @@ The `execute` and `onSession` callbacks receive a `CustomToolContext`:
|
||||
|
||||
```typescript
|
||||
interface CustomToolContext {
|
||||
sessionManager: ReadonlySessionManager; // Read-only access to session
|
||||
modelRegistry: ModelRegistry; // For API key resolution
|
||||
model: Model | undefined; // Current model (may be undefined)
|
||||
isIdle(): boolean; // Whether agent is streaming
|
||||
hasQueuedMessages(): boolean; // Whether user has queued messages
|
||||
abort(): void; // Abort current operation (fire-and-forget)
|
||||
sessionManager: ReadonlySessionManager; // Read-only access to session
|
||||
modelRegistry: ModelRegistry; // For API key resolution
|
||||
model: Model | undefined; // Current model (may be undefined)
|
||||
isIdle(): boolean; // Whether agent is streaming
|
||||
hasQueuedMessages(): boolean; // Whether user has queued messages
|
||||
abort(): void; // Abort current operation (fire-and-forget)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -273,7 +283,7 @@ async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
const text = await pi.ui.editor("Edit your response:", "prefilled text");
|
||||
// Returns edited text or undefined if cancelled (Escape)
|
||||
// Ctrl+Enter to submit, Ctrl+G to open $VISUAL or $EDITOR
|
||||
|
||||
|
||||
if (!text) {
|
||||
return { content: [{ type: "text", text: "Cancelled" }] };
|
||||
}
|
||||
@@ -287,12 +297,13 @@ Tools can implement `onSession` to react to session changes:
|
||||
|
||||
```typescript
|
||||
interface CustomToolSessionEvent {
|
||||
reason: "start" | "switch" | "branch" | "tree" | "shutdown";
|
||||
previousSessionFile: string | undefined;
|
||||
reason: "start" | "switch" | "branch" | "tree" | "shutdown";
|
||||
previousSessionFile: string | undefined;
|
||||
}
|
||||
```
|
||||
|
||||
**Reasons:**
|
||||
|
||||
- `start`: Initial session load on startup
|
||||
- `switch`: User started a new session (`/new`) or switched to a different session (`/resume`)
|
||||
- `branch`: User branched from a previous message (`/branch`)
|
||||
@@ -357,6 +368,7 @@ const factory: CustomToolFactory = (pi) => {
|
||||
```
|
||||
|
||||
This pattern ensures:
|
||||
|
||||
- When user branches, state is correct for that point in history
|
||||
- When user switches sessions, state matches that session
|
||||
- When user starts a new session, state resets
|
||||
@@ -368,6 +380,7 @@ Custom tools can provide `renderCall` and `renderResult` methods to control how
|
||||
### How It Works
|
||||
|
||||
Tool output is wrapped in a `Box` component that handles:
|
||||
|
||||
- Padding (1 character horizontal, 1 line vertical)
|
||||
- Background color based on state (pending/success/error)
|
||||
|
||||
@@ -389,6 +402,7 @@ renderCall(args, theme) {
|
||||
```
|
||||
|
||||
Called when:
|
||||
|
||||
- Tool call starts (may have partial args during streaming)
|
||||
- Args are updated during streaming
|
||||
|
||||
@@ -412,7 +426,7 @@ renderResult(result, { expanded, isPartial }, theme) {
|
||||
|
||||
// Normal result
|
||||
let text = theme.fg("success", "✓ ") + theme.fg("muted", "Done");
|
||||
|
||||
|
||||
// Support expanded view (Ctrl+O)
|
||||
if (expanded && details?.items) {
|
||||
for (const item of details.items) {
|
||||
@@ -425,6 +439,7 @@ renderResult(result, { expanded, isPartial }, theme) {
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `expanded`: User pressed Ctrl+O to expand
|
||||
- `isPartial`: Result is from `onUpdate` (streaming), not final
|
||||
|
||||
@@ -441,23 +456,24 @@ renderResult(result, { expanded, isPartial }, theme) {
|
||||
|
||||
```typescript
|
||||
// Foreground
|
||||
theme.fg("toolTitle", text) // Tool names
|
||||
theme.fg("accent", text) // Highlights
|
||||
theme.fg("success", text) // Success
|
||||
theme.fg("error", text) // Errors
|
||||
theme.fg("warning", text) // Warnings
|
||||
theme.fg("muted", text) // Secondary text
|
||||
theme.fg("dim", text) // Tertiary text
|
||||
theme.fg("toolOutput", text) // Output content
|
||||
theme.fg("toolTitle", text); // Tool names
|
||||
theme.fg("accent", text); // Highlights
|
||||
theme.fg("success", text); // Success
|
||||
theme.fg("error", text); // Errors
|
||||
theme.fg("warning", text); // Warnings
|
||||
theme.fg("muted", text); // Secondary text
|
||||
theme.fg("dim", text); // Tertiary text
|
||||
theme.fg("toolOutput", text); // Output content
|
||||
|
||||
// Styles
|
||||
theme.bold(text)
|
||||
theme.italic(text)
|
||||
theme.bold(text);
|
||||
theme.italic(text);
|
||||
```
|
||||
|
||||
### Fallback Behavior
|
||||
|
||||
If `renderCall` or `renderResult` is not defined or throws an error:
|
||||
|
||||
- `renderCall`: Shows tool name
|
||||
- `renderResult`: Shows raw text output from `content`
|
||||
|
||||
@@ -513,11 +529,13 @@ const factory: CustomToolFactory = (pi) => {
|
||||
## Examples
|
||||
|
||||
See [`examples/custom-tools/todo/index.ts`](../examples/custom-tools/todo/index.ts) for a complete example with:
|
||||
|
||||
- `onSession` for state reconstruction
|
||||
- Custom `renderCall` and `renderResult`
|
||||
- Proper branching support via details storage
|
||||
|
||||
Test with:
|
||||
|
||||
```bash
|
||||
pi --tool packages/coding-agent/examples/custom-tools/todo/index.ts
|
||||
```
|
||||
|
||||
+223
-211
@@ -5,6 +5,7 @@
|
||||
Hooks are TypeScript modules that extend pi's behavior by subscribing to lifecycle events. They can intercept tool calls, prompt the user, modify results, inject messages, and more.
|
||||
|
||||
**Key capabilities:**
|
||||
|
||||
- **User interaction** - Hooks can prompt users via `ctx.ui` (select, confirm, input, notify)
|
||||
- **Custom UI components** - Full TUI components with keyboard input via `ctx.ui.custom()`
|
||||
- **Custom slash commands** - Register commands like `/mycommand` via `pi.registerCommand()`
|
||||
@@ -12,6 +13,7 @@ Hooks are TypeScript modules that extend pi's behavior by subscribing to lifecyc
|
||||
- **Session persistence** - Store hook state that survives restarts via `pi.appendEntry()`
|
||||
|
||||
**Example use cases:**
|
||||
|
||||
- Permission gates (confirm before `rm -rf`, `sudo`, etc.)
|
||||
- Git checkpointing (stash at each turn, restore on `/branch`)
|
||||
- Path protection (block writes to `.env`, `node_modules/`)
|
||||
@@ -25,19 +27,19 @@ See [examples/hooks/](../examples/hooks/) for working implementations, including
|
||||
Create `~/.pi/agent/hooks/my-hook.ts`:
|
||||
|
||||
```typescript
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
ctx.ui.notify("Hook loaded!", "info");
|
||||
});
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
ctx.ui.notify("Hook loaded!", "info");
|
||||
});
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
|
||||
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
|
||||
if (!ok) return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
});
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
|
||||
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
|
||||
if (!ok) return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -51,27 +53,27 @@ pi --hook ./my-hook.ts
|
||||
|
||||
Hooks are auto-discovered from:
|
||||
|
||||
| Location | Scope |
|
||||
|----------|-------|
|
||||
| Location | Scope |
|
||||
| ------------------------ | --------------------- |
|
||||
| `~/.pi/agent/hooks/*.ts` | Global (all projects) |
|
||||
| `.pi/hooks/*.ts` | Project-local |
|
||||
| `.pi/hooks/*.ts` | Project-local |
|
||||
|
||||
Additional paths via `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": ["/path/to/hook.ts"]
|
||||
"hooks": ["/path/to/hook.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
## Available Imports
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `@mariozechner/pi-coding-agent/hooks` | Hook types (`HookAPI`, `HookContext`, events) |
|
||||
| `@mariozechner/pi-coding-agent` | Additional types if needed |
|
||||
| `@mariozechner/pi-ai` | AI utilities |
|
||||
| `@mariozechner/pi-tui` | TUI components |
|
||||
| Package | Purpose |
|
||||
| --------------------------------- | --------------------------------------------- |
|
||||
| `@oh-my-pi/pi-coding-agent/hooks` | Hook types (`HookAPI`, `HookContext`, events) |
|
||||
| `@oh-my-pi/pi-coding-agent` | Additional types if needed |
|
||||
| `@oh-my-pi/pi-ai` | AI utilities |
|
||||
| `@oh-my-pi/pi-tui` | TUI components |
|
||||
|
||||
Node.js built-ins (`node:fs`, `node:path`, etc.) are also available.
|
||||
|
||||
@@ -80,13 +82,13 @@ Node.js built-ins (`node:fs`, `node:path`, etc.) are also available.
|
||||
A hook exports a default function that receives `HookAPI`:
|
||||
|
||||
```typescript
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
// Subscribe to events
|
||||
pi.on("event_name", async (event, ctx) => {
|
||||
// Handle event
|
||||
});
|
||||
// Subscribe to events
|
||||
pi.on("event_name", async (event, ctx) => {
|
||||
// Handle event
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -151,7 +153,7 @@ Fired on initial session load.
|
||||
|
||||
```typescript
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
|
||||
ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
|
||||
});
|
||||
```
|
||||
|
||||
@@ -161,20 +163,20 @@ Fired when starting a new session (`/new`) or switching sessions (`/resume`).
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_switch", async (event, ctx) => {
|
||||
// event.reason - "new" (starting fresh) or "resume" (switching to existing)
|
||||
// event.targetSessionFile - session we're switching to (only for "resume")
|
||||
|
||||
if (event.reason === "new") {
|
||||
const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
|
||||
if (!ok) return { cancel: true };
|
||||
}
|
||||
|
||||
return { cancel: true }; // Cancel the switch/new
|
||||
// event.reason - "new" (starting fresh) or "resume" (switching to existing)
|
||||
// event.targetSessionFile - session we're switching to (only for "resume")
|
||||
|
||||
if (event.reason === "new") {
|
||||
const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
|
||||
if (!ok) return { cancel: true };
|
||||
}
|
||||
|
||||
return { cancel: true }; // Cancel the switch/new
|
||||
});
|
||||
|
||||
pi.on("session_switch", async (event, ctx) => {
|
||||
// event.reason - "new" or "resume"
|
||||
// event.previousSessionFile - session we came from
|
||||
// event.reason - "new" or "resume"
|
||||
// event.previousSessionFile - session we came from
|
||||
});
|
||||
```
|
||||
|
||||
@@ -184,15 +186,15 @@ Fired when branching via `/branch`.
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_branch", async (event, ctx) => {
|
||||
// event.entryId - ID of the entry being branched from
|
||||
// event.entryId - ID of the entry being branched from
|
||||
|
||||
return { cancel: true }; // Cancel branch
|
||||
// OR
|
||||
return { skipConversationRestore: true }; // Branch but don't rewind messages
|
||||
return { cancel: true }; // Cancel branch
|
||||
// OR
|
||||
return { skipConversationRestore: true }; // Branch but don't rewind messages
|
||||
});
|
||||
|
||||
pi.on("session_branch", async (event, ctx) => {
|
||||
// event.previousSessionFile - previous session file
|
||||
// event.previousSessionFile - previous session file
|
||||
});
|
||||
```
|
||||
|
||||
@@ -204,24 +206,24 @@ Fired on compaction. See [compaction.md](compaction.md) for details.
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
const { preparation, branchEntries, customInstructions, signal } = event;
|
||||
const { preparation, branchEntries, customInstructions, signal } = event;
|
||||
|
||||
// Cancel:
|
||||
return { cancel: true };
|
||||
// Cancel:
|
||||
return { cancel: true };
|
||||
|
||||
// Custom summary:
|
||||
return {
|
||||
compaction: {
|
||||
summary: "...",
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
}
|
||||
};
|
||||
// Custom summary:
|
||||
return {
|
||||
compaction: {
|
||||
summary: "...",
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
pi.on("session_compact", async (event, ctx) => {
|
||||
// event.compactionEntry - the saved compaction
|
||||
// event.fromHook - whether hook provided it
|
||||
// event.compactionEntry - the saved compaction
|
||||
// event.fromHook - whether hook provided it
|
||||
});
|
||||
```
|
||||
|
||||
@@ -231,17 +233,17 @@ Fired on `/tree` navigation. Always fires regardless of user's summarization cho
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_tree", async (event, ctx) => {
|
||||
const { preparation, signal } = event;
|
||||
// preparation.targetId, oldLeafId, commonAncestorId, entriesToSummarize
|
||||
// preparation.userWantsSummary - whether user chose to summarize
|
||||
const { preparation, signal } = event;
|
||||
// preparation.targetId, oldLeafId, commonAncestorId, entriesToSummarize
|
||||
// preparation.userWantsSummary - whether user chose to summarize
|
||||
|
||||
return { cancel: true };
|
||||
// OR provide custom summary (only used if userWantsSummary is true):
|
||||
return { summary: { summary: "...", details: {} } };
|
||||
return { cancel: true };
|
||||
// OR provide custom summary (only used if userWantsSummary is true):
|
||||
return { summary: { summary: "...", details: {} } };
|
||||
});
|
||||
|
||||
pi.on("session_tree", async (event, ctx) => {
|
||||
// event.newLeafId, oldLeafId, summaryEntry, fromHook
|
||||
// event.newLeafId, oldLeafId, summaryEntry, fromHook
|
||||
});
|
||||
```
|
||||
|
||||
@@ -251,7 +253,7 @@ Fired on exit (Ctrl+C, Ctrl+D, SIGTERM).
|
||||
|
||||
```typescript
|
||||
pi.on("session_shutdown", async (_event, ctx) => {
|
||||
// Cleanup, save state, etc.
|
||||
// Cleanup, save state, etc.
|
||||
});
|
||||
```
|
||||
|
||||
@@ -263,16 +265,16 @@ Fired after user submits prompt, before agent loop. Can inject a persistent mess
|
||||
|
||||
```typescript
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
// event.prompt - user's prompt text
|
||||
// event.images - attached images (if any)
|
||||
// event.prompt - user's prompt text
|
||||
// event.images - attached images (if any)
|
||||
|
||||
return {
|
||||
message: {
|
||||
customType: "my-hook",
|
||||
content: "Additional context for the LLM",
|
||||
display: true, // Show in TUI
|
||||
}
|
||||
};
|
||||
return {
|
||||
message: {
|
||||
customType: "my-hook",
|
||||
content: "Additional context for the LLM",
|
||||
display: true, // Show in TUI
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
@@ -286,7 +288,7 @@ Fired once per user prompt.
|
||||
pi.on("agent_start", async (_event, ctx) => {});
|
||||
|
||||
pi.on("agent_end", async (event, ctx) => {
|
||||
// event.messages - messages from this prompt
|
||||
// event.messages - messages from this prompt
|
||||
});
|
||||
```
|
||||
|
||||
@@ -296,13 +298,13 @@ Fired for each turn (one LLM response + tool calls).
|
||||
|
||||
```typescript
|
||||
pi.on("turn_start", async (event, ctx) => {
|
||||
// event.turnIndex, event.timestamp
|
||||
// event.turnIndex, event.timestamp
|
||||
});
|
||||
|
||||
pi.on("turn_end", async (event, ctx) => {
|
||||
// event.turnIndex
|
||||
// event.message - assistant's response
|
||||
// event.toolResults - tool results from this turn
|
||||
// event.turnIndex
|
||||
// event.message - assistant's response
|
||||
// event.toolResults - tool results from this turn
|
||||
});
|
||||
```
|
||||
|
||||
@@ -312,11 +314,11 @@ Fired before each LLM call. Modify messages non-destructively (session unchanged
|
||||
|
||||
```typescript
|
||||
pi.on("context", async (event, ctx) => {
|
||||
// event.messages - deep copy, safe to modify
|
||||
// event.messages - deep copy, safe to modify
|
||||
|
||||
// Filter or transform messages
|
||||
const filtered = event.messages.filter(m => !shouldPrune(m));
|
||||
return { messages: filtered };
|
||||
// Filter or transform messages
|
||||
const filtered = event.messages.filter((m) => !shouldPrune(m));
|
||||
return { messages: filtered };
|
||||
});
|
||||
```
|
||||
|
||||
@@ -328,17 +330,18 @@ Fired before tool executes. **Can block.**
|
||||
|
||||
```typescript
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
// event.toolName - "bash", "read", "write", "edit", etc.
|
||||
// event.toolCallId
|
||||
// event.input - tool parameters
|
||||
// event.toolName - "bash", "read", "write", "edit", etc.
|
||||
// event.toolCallId
|
||||
// event.input - tool parameters
|
||||
|
||||
if (shouldBlock(event)) {
|
||||
return { block: true, reason: "Not allowed" };
|
||||
}
|
||||
if (shouldBlock(event)) {
|
||||
return { block: true, reason: "Not allowed" };
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Tool inputs:
|
||||
|
||||
- `bash`: `{ command, timeout? }`
|
||||
- `read`: `{ path, offset?, limit? }`
|
||||
- `write`: `{ path, content }`
|
||||
@@ -372,15 +375,15 @@ pi.on("tool_result", async (event, ctx) => {
|
||||
Use type guards for typed details:
|
||||
|
||||
```typescript
|
||||
import { isBashToolResult } from "@mariozechner/pi-coding-agent";
|
||||
import { isBashToolResult } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
pi.on("tool_result", async (event, ctx) => {
|
||||
if (isBashToolResult(event)) {
|
||||
// event.details is BashToolDetails | undefined
|
||||
if (event.details?.truncation?.truncated) {
|
||||
// Full output at event.details.fullOutputPath
|
||||
}
|
||||
}
|
||||
if (isBashToolResult(event)) {
|
||||
// event.details is BashToolDetails | undefined
|
||||
if (event.details?.truncation?.truncated) {
|
||||
// Full output at event.details.fullOutputPath
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -415,11 +418,11 @@ const text = await ctx.ui.editor("Edit prompt:", "prefilled text");
|
||||
// Ctrl+Enter to submit, Ctrl+G to open $VISUAL or $EDITOR
|
||||
|
||||
// Notification (non-blocking)
|
||||
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
||||
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
||||
|
||||
// Set status text in footer (persistent until cleared)
|
||||
ctx.ui.setStatus("my-hook", "Processing 5/10..."); // Set status
|
||||
ctx.ui.setStatus("my-hook", undefined); // Clear status
|
||||
ctx.ui.setStatus("my-hook", "Processing 5/10..."); // Set status
|
||||
ctx.ui.setStatus("my-hook", undefined); // Clear status
|
||||
|
||||
// Set the core input editor text (pre-fill prompts, generated content)
|
||||
ctx.ui.setEditorText("Generated prompt text here...");
|
||||
@@ -429,6 +432,7 @@ const currentText = ctx.ui.getEditorText();
|
||||
```
|
||||
|
||||
**Status text notes:**
|
||||
|
||||
- Multiple hooks can set their own status using unique keys
|
||||
- Statuses are displayed on a single line in the footer, sorted alphabetically by key
|
||||
- Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width
|
||||
@@ -457,19 +461,22 @@ See [examples/hooks/status-line.ts](../examples/hooks/status-line.ts) for a comp
|
||||
Show a custom TUI component with keyboard focus:
|
||||
|
||||
```typescript
|
||||
import { BorderedLoader } from "@mariozechner/pi-coding-agent";
|
||||
import { BorderedLoader } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const result = await ctx.ui.custom((tui, theme, done) => {
|
||||
const loader = new BorderedLoader(tui, theme, "Working...");
|
||||
loader.onAbort = () => done(null);
|
||||
|
||||
doWork(loader.signal).then(done).catch(() => done(null));
|
||||
|
||||
return loader;
|
||||
const loader = new BorderedLoader(tui, theme, "Working...");
|
||||
loader.onAbort = () => done(null);
|
||||
|
||||
doWork(loader.signal)
|
||||
.then(done)
|
||||
.catch(() => done(null));
|
||||
|
||||
return loader;
|
||||
});
|
||||
```
|
||||
|
||||
Your component can:
|
||||
|
||||
- Implement `handleInput(data: string)` to receive keyboard input
|
||||
- Implement `render(width: number): string[]` to render lines
|
||||
- Implement `invalidate()` to clear cached render
|
||||
@@ -501,23 +508,23 @@ Read-only access to session state. See `ReadonlySessionManager` in [`src/core/se
|
||||
|
||||
```typescript
|
||||
// Session info
|
||||
ctx.sessionManager.getCwd() // Working directory
|
||||
ctx.sessionManager.getSessionDir() // Session directory (~/.pi/agent/sessions)
|
||||
ctx.sessionManager.getSessionId() // Current session ID
|
||||
ctx.sessionManager.getSessionFile() // Session file path (undefined with --no-session)
|
||||
ctx.sessionManager.getCwd(); // Working directory
|
||||
ctx.sessionManager.getSessionDir(); // Session directory (~/.pi/agent/sessions)
|
||||
ctx.sessionManager.getSessionId(); // Current session ID
|
||||
ctx.sessionManager.getSessionFile(); // Session file path (undefined with --no-session)
|
||||
|
||||
// Entries
|
||||
ctx.sessionManager.getEntries() // All entries (excludes header)
|
||||
ctx.sessionManager.getHeader() // Session header entry
|
||||
ctx.sessionManager.getEntry(id) // Specific entry by ID
|
||||
ctx.sessionManager.getLabel(id) // Entry label (if any)
|
||||
ctx.sessionManager.getEntries(); // All entries (excludes header)
|
||||
ctx.sessionManager.getHeader(); // Session header entry
|
||||
ctx.sessionManager.getEntry(id); // Specific entry by ID
|
||||
ctx.sessionManager.getLabel(id); // Entry label (if any)
|
||||
|
||||
// Tree navigation
|
||||
ctx.sessionManager.getBranch() // Current branch (root to leaf)
|
||||
ctx.sessionManager.getBranch(leafId) // Specific branch
|
||||
ctx.sessionManager.getTree() // Full tree structure
|
||||
ctx.sessionManager.getLeafId() // Current leaf entry ID
|
||||
ctx.sessionManager.getLeafEntry() // Current leaf entry
|
||||
ctx.sessionManager.getBranch(); // Current branch (root to leaf)
|
||||
ctx.sessionManager.getBranch(leafId); // Specific branch
|
||||
ctx.sessionManager.getTree(); // Full tree structure
|
||||
ctx.sessionManager.getLeafId(); // Current leaf entry ID
|
||||
ctx.sessionManager.getLeafEntry(); // Current leaf entry
|
||||
```
|
||||
|
||||
Use `pi.sendMessage()` or `pi.appendEntry()` for writes.
|
||||
@@ -540,8 +547,8 @@ Current model, or `undefined` if none selected yet. Use for LLM calls in hooks:
|
||||
|
||||
```typescript
|
||||
if (ctx.model) {
|
||||
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
|
||||
// Use with @mariozechner/pi-ai complete()
|
||||
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
|
||||
// Use with @oh-my-pi/pi-ai complete()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -551,7 +558,7 @@ Returns `true` if the agent is not currently streaming:
|
||||
|
||||
```typescript
|
||||
if (ctx.isIdle()) {
|
||||
// Agent is not processing
|
||||
// Agent is not processing
|
||||
}
|
||||
```
|
||||
|
||||
@@ -569,8 +576,8 @@ Check if there are messages queued (user typed while agent was streaming):
|
||||
|
||||
```typescript
|
||||
if (ctx.hasQueuedMessages()) {
|
||||
// Skip interactive prompt, let queued message take over
|
||||
return;
|
||||
// Skip interactive prompt, let queued message take over
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -593,19 +600,19 @@ Create a new session, optionally with initialization:
|
||||
|
||||
```typescript
|
||||
const result = await ctx.newSession({
|
||||
parentSession: ctx.sessionManager.getSessionFile(), // Track lineage
|
||||
setup: async (sm) => {
|
||||
// Initialize the new session
|
||||
sm.appendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Context from previous session..." }],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
},
|
||||
parentSession: ctx.sessionManager.getSessionFile(), // Track lineage
|
||||
setup: async (sm) => {
|
||||
// Initialize the new session
|
||||
sm.appendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Context from previous session..." }],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (result.cancelled) {
|
||||
// A hook cancelled the new session
|
||||
// A hook cancelled the new session
|
||||
}
|
||||
```
|
||||
|
||||
@@ -616,7 +623,7 @@ Branch from a specific entry, creating a new session file:
|
||||
```typescript
|
||||
const result = await ctx.branch("entry-id-123");
|
||||
if (!result.cancelled) {
|
||||
// Now in the branched session
|
||||
// Now in the branched session
|
||||
}
|
||||
```
|
||||
|
||||
@@ -626,7 +633,7 @@ Navigate to a different point in the session tree:
|
||||
|
||||
```typescript
|
||||
const result = await ctx.navigateTree("entry-id-456", {
|
||||
summarize: true, // Summarize the abandoned branch
|
||||
summarize: true, // Summarize the abandoned branch
|
||||
});
|
||||
```
|
||||
|
||||
@@ -650,15 +657,18 @@ pi.sendMessage({
|
||||
```
|
||||
|
||||
**Storage and timing:**
|
||||
|
||||
- The message is appended to the session file immediately as a `CustomMessageEntry`
|
||||
- If the agent is currently streaming, the message is queued and appended after the current turn
|
||||
- If `triggerTurn` is true and the agent is idle, a new agent loop starts
|
||||
|
||||
**LLM context:**
|
||||
|
||||
- `CustomMessageEntry` is converted to a user message when building context for the LLM
|
||||
- Only `content` is sent to the LLM; `details` is for rendering/state only
|
||||
|
||||
**TUI display:**
|
||||
|
||||
- If `display: true`, the message appears in the chat with purple styling (customMessageBg, customMessageText, customMessageLabel theme colors)
|
||||
- If `display: false`, the message is hidden from the TUI but still sent to the LLM
|
||||
- Use `pi.registerMessageRenderer()` to customize how your messages render (see below)
|
||||
@@ -673,11 +683,11 @@ pi.appendEntry("my-hook-state", { count: 42 });
|
||||
|
||||
// Restore on reload
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
for (const entry of ctx.sessionManager.getEntries()) {
|
||||
if (entry.type === "custom" && entry.customType === "my-hook-state") {
|
||||
// Reconstruct from entry.data
|
||||
}
|
||||
}
|
||||
for (const entry of ctx.sessionManager.getEntries()) {
|
||||
if (entry.type === "custom" && entry.customType === "my-hook-state") {
|
||||
// Reconstruct from entry.data
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -687,12 +697,12 @@ Register a custom slash command:
|
||||
|
||||
```typescript
|
||||
pi.registerCommand("stats", {
|
||||
description: "Show session statistics",
|
||||
handler: async (args, ctx) => {
|
||||
// args = everything after /stats
|
||||
const count = ctx.sessionManager.getEntries().length;
|
||||
ctx.ui.notify(`${count} entries`, "info");
|
||||
}
|
||||
description: "Show session statistics",
|
||||
handler: async (args, ctx) => {
|
||||
// args = everything after /stats
|
||||
const count = ctx.sessionManager.getEntries().length;
|
||||
ctx.ui.notify(`${count} entries`, "info");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -705,28 +715,30 @@ To trigger LLM after command, call `pi.sendMessage(..., true)`.
|
||||
Register a custom TUI renderer for `CustomMessageEntry` messages with your `customType`. Without a custom renderer, messages display with default purple styling showing the content as-is.
|
||||
|
||||
```typescript
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
|
||||
pi.registerMessageRenderer("my-hook", (message, options, theme) => {
|
||||
// message.content - the message content (string or content array)
|
||||
// message.details - your custom metadata
|
||||
// options.expanded - true if user pressed Ctrl+O
|
||||
|
||||
const prefix = theme.fg("accent", `[${message.details?.label ?? "INFO"}] `);
|
||||
const text = typeof message.content === "string"
|
||||
? message.content
|
||||
: message.content.map(c => c.type === "text" ? c.text : "[image]").join("");
|
||||
|
||||
return new Text(prefix + theme.fg("text", text), 0, 0);
|
||||
// message.content - the message content (string or content array)
|
||||
// message.details - your custom metadata
|
||||
// options.expanded - true if user pressed Ctrl+O
|
||||
|
||||
const prefix = theme.fg("accent", `[${message.details?.label ?? "INFO"}] `);
|
||||
const text =
|
||||
typeof message.content === "string"
|
||||
? message.content
|
||||
: message.content.map((c) => (c.type === "text" ? c.text : "[image]")).join("");
|
||||
|
||||
return new Text(prefix + theme.fg("text", text), 0, 0);
|
||||
});
|
||||
```
|
||||
|
||||
**Renderer signature:**
|
||||
|
||||
```typescript
|
||||
type HookMessageRenderer = (
|
||||
message: CustomMessageEntry,
|
||||
options: { expanded: boolean },
|
||||
theme: Theme
|
||||
message: CustomMessageEntry,
|
||||
options: { expanded: boolean },
|
||||
theme: Theme
|
||||
) => Component | null;
|
||||
```
|
||||
|
||||
@@ -738,8 +750,8 @@ Execute a shell command:
|
||||
|
||||
```typescript
|
||||
const result = await pi.exec("git", ["status"], {
|
||||
signal, // AbortSignal
|
||||
timeout, // Milliseconds
|
||||
signal, // AbortSignal
|
||||
timeout, // Milliseconds
|
||||
});
|
||||
|
||||
// result.stdout, result.stderr, result.code, result.killed
|
||||
@@ -750,79 +762,79 @@ const result = await pi.exec("git", ["status"], {
|
||||
### Permission Gate
|
||||
|
||||
```typescript
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
const dangerous = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i];
|
||||
const dangerous = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i];
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName !== "bash") return;
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName !== "bash") return;
|
||||
|
||||
const cmd = event.input.command as string;
|
||||
if (dangerous.some(p => p.test(cmd))) {
|
||||
if (!ctx.hasUI) {
|
||||
return { block: true, reason: "Dangerous (no UI)" };
|
||||
}
|
||||
const ok = await ctx.ui.confirm("Dangerous!", `Allow: ${cmd}?`);
|
||||
if (!ok) return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
});
|
||||
const cmd = event.input.command as string;
|
||||
if (dangerous.some((p) => p.test(cmd))) {
|
||||
if (!ctx.hasUI) {
|
||||
return { block: true, reason: "Dangerous (no UI)" };
|
||||
}
|
||||
const ok = await ctx.ui.confirm("Dangerous!", `Allow: ${cmd}?`);
|
||||
if (!ok) return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Protected Paths
|
||||
|
||||
```typescript
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
const protectedPaths = [".env", ".git/", "node_modules/"];
|
||||
const protectedPaths = [".env", ".git/", "node_modules/"];
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
||||
|
||||
const path = event.input.path as string;
|
||||
if (protectedPaths.some(p => path.includes(p))) {
|
||||
ctx.ui.notify(`Blocked: ${path}`, "warning");
|
||||
return { block: true, reason: `Protected: ${path}` };
|
||||
}
|
||||
});
|
||||
const path = event.input.path as string;
|
||||
if (protectedPaths.some((p) => path.includes(p))) {
|
||||
ctx.ui.notify(`Blocked: ${path}`, "warning");
|
||||
return { block: true, reason: `Protected: ${path}` };
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Git Checkpoint
|
||||
|
||||
```typescript
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
const checkpoints = new Map<string, string>();
|
||||
let currentEntryId: string | undefined;
|
||||
const checkpoints = new Map<string, string>();
|
||||
let currentEntryId: string | undefined;
|
||||
|
||||
pi.on("tool_result", async (_event, ctx) => {
|
||||
const leaf = ctx.sessionManager.getLeafEntry();
|
||||
if (leaf) currentEntryId = leaf.id;
|
||||
});
|
||||
pi.on("tool_result", async (_event, ctx) => {
|
||||
const leaf = ctx.sessionManager.getLeafEntry();
|
||||
if (leaf) currentEntryId = leaf.id;
|
||||
});
|
||||
|
||||
pi.on("turn_start", async () => {
|
||||
const { stdout } = await pi.exec("git", ["stash", "create"]);
|
||||
if (stdout.trim() && currentEntryId) {
|
||||
checkpoints.set(currentEntryId, stdout.trim());
|
||||
}
|
||||
});
|
||||
pi.on("turn_start", async () => {
|
||||
const { stdout } = await pi.exec("git", ["stash", "create"]);
|
||||
if (stdout.trim() && currentEntryId) {
|
||||
checkpoints.set(currentEntryId, stdout.trim());
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_before_branch", async (event, ctx) => {
|
||||
const ref = checkpoints.get(event.entryId);
|
||||
if (!ref || !ctx.hasUI) return;
|
||||
pi.on("session_before_branch", async (event, ctx) => {
|
||||
const ref = checkpoints.get(event.entryId);
|
||||
if (!ref || !ctx.hasUI) return;
|
||||
|
||||
const ok = await ctx.ui.confirm("Restore?", "Restore code to checkpoint?");
|
||||
if (ok) {
|
||||
await pi.exec("git", ["stash", "apply", ref]);
|
||||
ctx.ui.notify("Code restored", "info");
|
||||
}
|
||||
});
|
||||
const ok = await ctx.ui.confirm("Restore?", "Restore code to checkpoint?");
|
||||
if (ok) {
|
||||
await pi.exec("git", ["stash", "apply", ref]);
|
||||
ctx.ui.notify("Code restored", "info");
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("agent_end", () => checkpoints.clear());
|
||||
pi.on("agent_end", () => checkpoints.clear());
|
||||
}
|
||||
```
|
||||
|
||||
@@ -832,10 +844,10 @@ See [examples/hooks/snake.ts](../examples/hooks/snake.ts) for a complete example
|
||||
|
||||
## Mode Behavior
|
||||
|
||||
| Mode | UI Methods | Notes |
|
||||
|------|-----------|-------|
|
||||
| Interactive | Full TUI | Normal operation |
|
||||
| RPC | JSON protocol | Host handles UI |
|
||||
| Mode | UI Methods | Notes |
|
||||
| ------------ | -------------------------- | -------------------------- |
|
||||
| Interactive | Full TUI | Normal operation |
|
||||
| RPC | JSON protocol | Host handles UI |
|
||||
| Print (`-p`) | No-op (returns null/false) | Hooks run but can't prompt |
|
||||
|
||||
In print mode, `select()` returns `undefined`, `confirm()` returns `false`, `input()` returns `undefined`, `getEditorText()` returns `""`, and `setEditorText()`/`setStatus()` are no-ops. Design hooks to handle this by checking `ctx.hasUI`.
|
||||
|
||||
+309
-267
File diff suppressed because it is too large
Load Diff
+379
-355
File diff suppressed because it is too large
Load Diff
+249
-229
@@ -10,56 +10,56 @@ Every theme must define all color tokens. There are no optional colors.
|
||||
|
||||
### Core UI (10 colors)
|
||||
|
||||
| Token | Purpose | Examples |
|
||||
|-------|---------|----------|
|
||||
| `accent` | Primary accent color | Logo, selected items, cursor (›) |
|
||||
| `border` | Normal borders | Selector borders, horizontal lines |
|
||||
| `borderAccent` | Highlighted borders | Changelog borders, special panels |
|
||||
| `borderMuted` | Subtle borders | Editor borders, secondary separators |
|
||||
| `success` | Success states | Success messages, diff additions |
|
||||
| `error` | Error states | Error messages, diff deletions |
|
||||
| `warning` | Warning states | Warning messages |
|
||||
| `muted` | Secondary/dimmed text | Metadata, descriptions, output |
|
||||
| `dim` | Very dimmed text | Less important info, placeholders |
|
||||
| `text` | Default text color | Main content (usually `""`) |
|
||||
| `thinkingText` | Thinking block text | Assistant reasoning traces |
|
||||
| Token | Purpose | Examples |
|
||||
| -------------- | --------------------- | ------------------------------------ |
|
||||
| `accent` | Primary accent color | Logo, selected items, cursor (›) |
|
||||
| `border` | Normal borders | Selector borders, horizontal lines |
|
||||
| `borderAccent` | Highlighted borders | Changelog borders, special panels |
|
||||
| `borderMuted` | Subtle borders | Editor borders, secondary separators |
|
||||
| `success` | Success states | Success messages, diff additions |
|
||||
| `error` | Error states | Error messages, diff deletions |
|
||||
| `warning` | Warning states | Warning messages |
|
||||
| `muted` | Secondary/dimmed text | Metadata, descriptions, output |
|
||||
| `dim` | Very dimmed text | Less important info, placeholders |
|
||||
| `text` | Default text color | Main content (usually `""`) |
|
||||
| `thinkingText` | Thinking block text | Assistant reasoning traces |
|
||||
|
||||
### Backgrounds & Content Text (11 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `selectedBg` | Selected/active line background (e.g., tree selector) |
|
||||
| `userMessageBg` | User message background |
|
||||
| `userMessageText` | User message text color |
|
||||
| `customMessageBg` | Hook custom message background |
|
||||
| `customMessageText` | Hook custom message text color |
|
||||
| `customMessageLabel` | Hook custom message label/type text |
|
||||
| `toolPendingBg` | Tool execution box (pending state) |
|
||||
| `toolSuccessBg` | Tool execution box (success state) |
|
||||
| `toolErrorBg` | Tool execution box (error state) |
|
||||
| `toolTitle` | Tool execution title/heading (e.g., `$ command`, `read file.txt`) |
|
||||
| `toolOutput` | Tool execution output text |
|
||||
| Token | Purpose |
|
||||
| -------------------- | ----------------------------------------------------------------- |
|
||||
| `selectedBg` | Selected/active line background (e.g., tree selector) |
|
||||
| `userMessageBg` | User message background |
|
||||
| `userMessageText` | User message text color |
|
||||
| `customMessageBg` | Hook custom message background |
|
||||
| `customMessageText` | Hook custom message text color |
|
||||
| `customMessageLabel` | Hook custom message label/type text |
|
||||
| `toolPendingBg` | Tool execution box (pending state) |
|
||||
| `toolSuccessBg` | Tool execution box (success state) |
|
||||
| `toolErrorBg` | Tool execution box (error state) |
|
||||
| `toolTitle` | Tool execution title/heading (e.g., `$ command`, `read file.txt`) |
|
||||
| `toolOutput` | Tool execution output text |
|
||||
|
||||
### Markdown (10 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `mdHeading` | Heading text (`#`, `##`, etc) |
|
||||
| `mdLink` | Link text |
|
||||
| `mdLinkUrl` | Link URL (in parentheses) |
|
||||
| `mdCode` | Inline code (backticks) |
|
||||
| `mdCodeBlock` | Code block content |
|
||||
| `mdCodeBlockBorder` | Code block fences (```) |
|
||||
| `mdQuote` | Blockquote text |
|
||||
| `mdQuoteBorder` | Blockquote border (`│`) |
|
||||
| `mdHr` | Horizontal rule (`---`) |
|
||||
| `mdListBullet` | List bullets/numbers |
|
||||
| Token | Purpose |
|
||||
| ------------------- | ----------------------------- |
|
||||
| `mdHeading` | Heading text (`#`, `##`, etc) |
|
||||
| `mdLink` | Link text |
|
||||
| `mdLinkUrl` | Link URL (in parentheses) |
|
||||
| `mdCode` | Inline code (backticks) |
|
||||
| `mdCodeBlock` | Code block content |
|
||||
| `mdCodeBlockBorder` | Code block fences (```) |
|
||||
| `mdQuote` | Blockquote text |
|
||||
| `mdQuoteBorder` | Blockquote border (`│`) |
|
||||
| `mdHr` | Horizontal rule (`---`) |
|
||||
| `mdListBullet` | List bullets/numbers |
|
||||
|
||||
### Tool Diffs (3 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `toolDiffAdded` | Added lines in tool diffs |
|
||||
| Token | Purpose |
|
||||
| ----------------- | --------------------------- |
|
||||
| `toolDiffAdded` | Added lines in tool diffs |
|
||||
| `toolDiffRemoved` | Removed lines in tool diffs |
|
||||
| `toolDiffContext` | Context lines in tool diffs |
|
||||
|
||||
@@ -69,37 +69,37 @@ Note: Diff colors are specific to tool execution boxes and must work with tool b
|
||||
|
||||
Future-proofing for syntax highlighting support:
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `syntaxComment` | Comments |
|
||||
| `syntaxKeyword` | Keywords (`if`, `function`, etc) |
|
||||
| `syntaxFunction` | Function names |
|
||||
| `syntaxVariable` | Variable names |
|
||||
| `syntaxString` | String literals |
|
||||
| `syntaxNumber` | Number literals |
|
||||
| `syntaxType` | Type names |
|
||||
| `syntaxOperator` | Operators (`+`, `-`, etc) |
|
||||
| `syntaxPunctuation` | Punctuation (`;`, `,`, etc) |
|
||||
| Token | Purpose |
|
||||
| ------------------- | -------------------------------- |
|
||||
| `syntaxComment` | Comments |
|
||||
| `syntaxKeyword` | Keywords (`if`, `function`, etc) |
|
||||
| `syntaxFunction` | Function names |
|
||||
| `syntaxVariable` | Variable names |
|
||||
| `syntaxString` | String literals |
|
||||
| `syntaxNumber` | Number literals |
|
||||
| `syntaxType` | Type names |
|
||||
| `syntaxOperator` | Operators (`+`, `-`, etc) |
|
||||
| `syntaxPunctuation` | Punctuation (`;`, `,`, etc) |
|
||||
|
||||
### Thinking Level Borders (6 colors)
|
||||
|
||||
Editor border colors that indicate the current thinking/reasoning level:
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `thinkingOff` | Border when thinking is off (most subtle) |
|
||||
| `thinkingMinimal` | Border for minimal thinking |
|
||||
| `thinkingLow` | Border for low thinking |
|
||||
| `thinkingMedium` | Border for medium thinking |
|
||||
| `thinkingHigh` | Border for high thinking |
|
||||
| `thinkingXhigh` | Border for xhigh thinking (most prominent, OpenAI codex-max only) |
|
||||
| Token | Purpose |
|
||||
| ----------------- | ----------------------------------------------------------------- |
|
||||
| `thinkingOff` | Border when thinking is off (most subtle) |
|
||||
| `thinkingMinimal` | Border for minimal thinking |
|
||||
| `thinkingLow` | Border for low thinking |
|
||||
| `thinkingMedium` | Border for medium thinking |
|
||||
| `thinkingHigh` | Border for high thinking |
|
||||
| `thinkingXhigh` | Border for xhigh thinking (most prominent, OpenAI codex-max only) |
|
||||
|
||||
These create a visual hierarchy: off → minimal → low → medium → high → xhigh
|
||||
|
||||
### Bash Mode (1 color)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| Token | Purpose |
|
||||
| ---------- | ------------------------------------------------ |
|
||||
| `bashMode` | Editor border color when in bash mode (! prefix) |
|
||||
|
||||
**Total: 50 color tokens** (all required)
|
||||
@@ -108,20 +108,21 @@ These create a visual hierarchy: off → minimal → low → medium → high →
|
||||
|
||||
The `export` section is optional and controls colors used when exporting sessions to HTML via `/export`. If not specified, these colors are automatically derived from `userMessageBg` based on luminance detection.
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `pageBg` | Page background color |
|
||||
| `cardBg` | Card/container background (headers, stats boxes) |
|
||||
| Token | Purpose |
|
||||
| -------- | ------------------------------------------------------------- |
|
||||
| `pageBg` | Page background color |
|
||||
| `cardBg` | Card/container background (headers, stats boxes) |
|
||||
| `infoBg` | Info sections background (system prompt, notices, compaction) |
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"export": {
|
||||
"pageBg": "#18181e",
|
||||
"cardBg": "#1e1e24",
|
||||
"infoBg": "#3c3728"
|
||||
}
|
||||
"export": {
|
||||
"pageBg": "#18181e",
|
||||
"cardBg": "#1e1e24",
|
||||
"infoBg": "#3c3728"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -163,21 +164,22 @@ The optional `vars` section allows you to define reusable colors:
|
||||
|
||||
```json
|
||||
{
|
||||
"vars": {
|
||||
"nord0": "#2E3440",
|
||||
"nord1": "#3B4252",
|
||||
"nord8": "#88C0D0",
|
||||
"brightBlue": 39
|
||||
},
|
||||
"colors": {
|
||||
"accent": "nord8",
|
||||
"muted": "nord1",
|
||||
"mdLink": "brightBlue"
|
||||
}
|
||||
"vars": {
|
||||
"nord0": "#2E3440",
|
||||
"nord1": "#3B4252",
|
||||
"nord8": "#88C0D0",
|
||||
"brightBlue": 39
|
||||
},
|
||||
"colors": {
|
||||
"accent": "nord8",
|
||||
"muted": "nord1",
|
||||
"mdLink": "brightBlue"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Benefits:
|
||||
|
||||
- Reuse colors across multiple tokens
|
||||
- Easier to maintain theme consistency
|
||||
- Can reference standard color palettes
|
||||
@@ -190,13 +192,14 @@ Use `""` (empty string) to inherit the terminal's default foreground/background
|
||||
|
||||
```json
|
||||
{
|
||||
"colors": {
|
||||
"text": "" // Uses terminal's default text color
|
||||
}
|
||||
"colors": {
|
||||
"text": "" // Uses terminal's default text color
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Main text color (adapts to user's terminal theme)
|
||||
- Creating themes that blend with terminal appearance
|
||||
|
||||
@@ -218,7 +221,7 @@ Themes are configured in the settings (accessible via `/settings`):
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "dark"
|
||||
"theme": "dark"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -235,73 +238,76 @@ Custom themes are loaded from `~/.pi/agent/themes/*.json`.
|
||||
### Creating a Custom Theme
|
||||
|
||||
1. **Create theme directory:**
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.pi/agent/themes
|
||||
```
|
||||
|
||||
2. **Create theme file:**
|
||||
|
||||
```bash
|
||||
vim ~/.pi/agent/themes/my-theme.json
|
||||
```
|
||||
|
||||
3. **Define all colors:**
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
|
||||
"name": "my-theme",
|
||||
"vars": {
|
||||
"primary": "#00aaff",
|
||||
"secondary": 242,
|
||||
"brightGreen": 46
|
||||
},
|
||||
"colors": {
|
||||
"accent": "primary",
|
||||
"border": "primary",
|
||||
"borderAccent": "#00ffff",
|
||||
"borderMuted": "secondary",
|
||||
"success": "brightGreen",
|
||||
"error": "#ff0000",
|
||||
"warning": "#ffff00",
|
||||
"muted": "secondary",
|
||||
"text": "",
|
||||
|
||||
"userMessageBg": "#2d2d30",
|
||||
"userMessageText": "",
|
||||
"toolPendingBg": "#1e1e2e",
|
||||
"toolSuccessBg": "#1e2e1e",
|
||||
"toolErrorBg": "#2e1e1e",
|
||||
"toolText": "",
|
||||
|
||||
"mdHeading": "#ffaa00",
|
||||
"mdLink": "primary",
|
||||
"mdCode": "#00ffff",
|
||||
"mdCodeBlock": "#00ff00",
|
||||
"mdCodeBlockBorder": "secondary",
|
||||
"mdQuote": "secondary",
|
||||
"mdQuoteBorder": "secondary",
|
||||
"mdHr": "secondary",
|
||||
"mdListBullet": "#00ffff",
|
||||
|
||||
"toolDiffAdded": "#00ff00",
|
||||
"toolDiffRemoved": "#ff0000",
|
||||
"toolDiffContext": "secondary",
|
||||
|
||||
"syntaxComment": "secondary",
|
||||
"syntaxKeyword": "primary",
|
||||
"syntaxFunction": "#00aaff",
|
||||
"syntaxVariable": "#ffaa00",
|
||||
"syntaxString": "#00ff00",
|
||||
"syntaxNumber": "#ff00ff",
|
||||
"syntaxType": "#00aaff",
|
||||
"syntaxOperator": "primary",
|
||||
"syntaxPunctuation": "secondary",
|
||||
|
||||
"thinkingOff": "secondary",
|
||||
"thinkingMinimal": "primary",
|
||||
"thinkingLow": "#00aaff",
|
||||
"thinkingMedium": "#00ffff",
|
||||
"thinkingHigh": "#ff00ff"
|
||||
}
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
|
||||
"name": "my-theme",
|
||||
"vars": {
|
||||
"primary": "#00aaff",
|
||||
"secondary": 242,
|
||||
"brightGreen": 46
|
||||
},
|
||||
"colors": {
|
||||
"accent": "primary",
|
||||
"border": "primary",
|
||||
"borderAccent": "#00ffff",
|
||||
"borderMuted": "secondary",
|
||||
"success": "brightGreen",
|
||||
"error": "#ff0000",
|
||||
"warning": "#ffff00",
|
||||
"muted": "secondary",
|
||||
"text": "",
|
||||
|
||||
"userMessageBg": "#2d2d30",
|
||||
"userMessageText": "",
|
||||
"toolPendingBg": "#1e1e2e",
|
||||
"toolSuccessBg": "#1e2e1e",
|
||||
"toolErrorBg": "#2e1e1e",
|
||||
"toolText": "",
|
||||
|
||||
"mdHeading": "#ffaa00",
|
||||
"mdLink": "primary",
|
||||
"mdCode": "#00ffff",
|
||||
"mdCodeBlock": "#00ff00",
|
||||
"mdCodeBlockBorder": "secondary",
|
||||
"mdQuote": "secondary",
|
||||
"mdQuoteBorder": "secondary",
|
||||
"mdHr": "secondary",
|
||||
"mdListBullet": "#00ffff",
|
||||
|
||||
"toolDiffAdded": "#00ff00",
|
||||
"toolDiffRemoved": "#ff0000",
|
||||
"toolDiffContext": "secondary",
|
||||
|
||||
"syntaxComment": "secondary",
|
||||
"syntaxKeyword": "primary",
|
||||
"syntaxFunction": "#00aaff",
|
||||
"syntaxVariable": "#ffaa00",
|
||||
"syntaxString": "#00ff00",
|
||||
"syntaxNumber": "#ff00ff",
|
||||
"syntaxType": "#00aaff",
|
||||
"syntaxOperator": "primary",
|
||||
"syntaxPunctuation": "secondary",
|
||||
|
||||
"thinkingOff": "secondary",
|
||||
"thinkingMinimal": "primary",
|
||||
"thinkingLow": "#00aaff",
|
||||
"thinkingMedium": "#00ffff",
|
||||
"thinkingHigh": "#ff00ff"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -314,11 +320,13 @@ Custom themes are loaded from `~/.pi/agent/themes/*.json`.
|
||||
### Light vs Dark Themes
|
||||
|
||||
**For dark terminals:**
|
||||
|
||||
- Use bright, saturated colors
|
||||
- Higher contrast
|
||||
- Example: `#00ffff` (bright cyan)
|
||||
|
||||
**For light terminals:**
|
||||
|
||||
- Use darker, muted colors
|
||||
- Lower contrast to avoid eye strain
|
||||
- Example: `#008888` (dark cyan)
|
||||
@@ -332,6 +340,7 @@ Custom themes are loaded from `~/.pi/agent/themes/*.json`.
|
||||
### Testing
|
||||
|
||||
Test your theme with:
|
||||
|
||||
- Different message types (user, assistant, errors)
|
||||
- Tool executions (success and error states)
|
||||
- Markdown content (headings, code, lists, etc)
|
||||
@@ -342,6 +351,7 @@ Test your theme with:
|
||||
### Hex Colors
|
||||
|
||||
Standard 6-digit hex format:
|
||||
|
||||
- `"#ff0000"` - Red
|
||||
- `"#00ff00"` - Green
|
||||
- `"#0000ff"` - Blue
|
||||
@@ -356,6 +366,7 @@ RGB values: `#RRGGBB` where each component is `00-ff` (0-255)
|
||||
Use numeric indices (0-255) to reference the xterm 256-color palette:
|
||||
|
||||
**Colors 0-15:** Basic ANSI colors (terminal-dependent, may be themed)
|
||||
|
||||
- `0` - Black
|
||||
- `1` - Red
|
||||
- `2` - Green
|
||||
@@ -367,29 +378,33 @@ Use numeric indices (0-255) to reference the xterm 256-color palette:
|
||||
- `8-15` - Bright variants
|
||||
|
||||
**Colors 16-231:** 6×6×6 RGB cube (standardized)
|
||||
|
||||
- Formula: `16 + 36×R + 6×G + B` where R, G, B are 0-5
|
||||
- Example: `39` = bright cyan, `196` = bright red
|
||||
|
||||
**Colors 232-255:** Grayscale ramp (standardized)
|
||||
|
||||
- `232` - Darkest gray
|
||||
- `255` - Near white
|
||||
|
||||
Example usage:
|
||||
|
||||
```json
|
||||
{
|
||||
"vars": {
|
||||
"gray": 242,
|
||||
"brightCyan": 51,
|
||||
"darkBlue": 18
|
||||
},
|
||||
"colors": {
|
||||
"muted": "gray",
|
||||
"accent": "brightCyan"
|
||||
}
|
||||
"vars": {
|
||||
"gray": 242,
|
||||
"brightCyan": 51,
|
||||
"darkBlue": 18
|
||||
},
|
||||
"colors": {
|
||||
"muted": "gray",
|
||||
"accent": "brightCyan"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Works everywhere (`TERM=xterm-256color`)
|
||||
- No truecolor detection needed
|
||||
- Standardized RGB cube (16-231) looks the same on all terminals
|
||||
@@ -406,6 +421,7 @@ Pi uses 24-bit RGB colors (`\x1b[38;2;R;G;Bm`). Most modern terminals support th
|
||||
For older terminals with only 256-color support, Pi automatically falls back to the nearest 256-color approximation.
|
||||
|
||||
To check if your terminal supports truecolor:
|
||||
|
||||
```bash
|
||||
echo $COLORTERM # Should output "truecolor" or "24bit"
|
||||
```
|
||||
@@ -413,6 +429,7 @@ echo $COLORTERM # Should output "truecolor" or "24bit"
|
||||
## Example Themes
|
||||
|
||||
See the built-in themes for complete examples:
|
||||
|
||||
- [Dark theme](../src/themes/dark.json)
|
||||
- [Light theme](../src/themes/light.json)
|
||||
|
||||
@@ -421,6 +438,7 @@ See the built-in themes for complete examples:
|
||||
Themes are validated on load using [TypeBox](https://github.com/sinclairzx81/typebox) + [Ajv](https://ajv.js.org/).
|
||||
|
||||
Invalid themes will show an error with details about what's wrong:
|
||||
|
||||
```
|
||||
Error loading theme 'my-theme':
|
||||
- colors.accent: must be string or number
|
||||
@@ -428,11 +446,13 @@ Error loading theme 'my-theme':
|
||||
```
|
||||
|
||||
For editor support, the JSON schema is available at:
|
||||
|
||||
```
|
||||
https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json
|
||||
```
|
||||
|
||||
Add to your theme file for auto-completion and validation:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
|
||||
@@ -448,16 +468,16 @@ Themes are loaded and converted to a `Theme` class that provides type-safe color
|
||||
|
||||
```typescript
|
||||
class Theme {
|
||||
// Apply foreground color
|
||||
fg(color: ThemeColor, text: string): string
|
||||
|
||||
// Apply background color
|
||||
bg(color: ThemeBg, text: string): string
|
||||
|
||||
// Text attributes (preserve current colors)
|
||||
bold(text: string): string
|
||||
italic(text: string): string
|
||||
underline(text: string): string
|
||||
// Apply foreground color
|
||||
fg(color: ThemeColor, text: string): string;
|
||||
|
||||
// Apply background color
|
||||
bg(color: ThemeBg, text: string): string;
|
||||
|
||||
// Text attributes (preserve current colors)
|
||||
bold(text: string): string;
|
||||
italic(text: string): string;
|
||||
underline(text: string): string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -470,37 +490,37 @@ The active theme is available as a global singleton in `coding-agent`:
|
||||
export let theme: Theme;
|
||||
|
||||
export function setTheme(name: string) {
|
||||
theme = loadTheme(name);
|
||||
theme = loadTheme(name);
|
||||
}
|
||||
|
||||
// Usage throughout coding-agent
|
||||
import { theme } from './theme.js';
|
||||
import { theme } from "./theme.js";
|
||||
|
||||
theme.fg('accent', 'Selected')
|
||||
theme.bg('userMessageBg', content)
|
||||
theme.fg("accent", "Selected");
|
||||
theme.bg("userMessageBg", content);
|
||||
```
|
||||
|
||||
### TUI Component Theming
|
||||
|
||||
TUI components (like `Markdown`, `SelectList`, `Editor`) are in the `@mariozechner/pi-tui` package and don't have direct access to the theme. Instead, they define interfaces for the colors they need:
|
||||
TUI components (like `Markdown`, `SelectList`, `Editor`) are in the `@oh-my-pi/pi-tui` package and don't have direct access to the theme. Instead, they define interfaces for the colors they need:
|
||||
|
||||
```typescript
|
||||
// In @mariozechner/pi-tui
|
||||
// In @oh-my-pi/pi-tui
|
||||
export interface MarkdownTheme {
|
||||
heading: (text: string) => string;
|
||||
link: (text: string) => string;
|
||||
linkUrl: (text: string) => string;
|
||||
code: (text: string) => string;
|
||||
codeBlock: (text: string) => string;
|
||||
codeBlockBorder: (text: string) => string;
|
||||
quote: (text: string) => string;
|
||||
quoteBorder: (text: string) => string;
|
||||
hr: (text: string) => string;
|
||||
listBullet: (text: string) => string;
|
||||
bold: (text: string) => string;
|
||||
italic: (text: string) => string;
|
||||
strikethrough: (text: string) => string;
|
||||
underline: (text: string) => string;
|
||||
heading: (text: string) => string;
|
||||
link: (text: string) => string;
|
||||
linkUrl: (text: string) => string;
|
||||
code: (text: string) => string;
|
||||
codeBlock: (text: string) => string;
|
||||
codeBlockBorder: (text: string) => string;
|
||||
quote: (text: string) => string;
|
||||
quoteBorder: (text: string) => string;
|
||||
hr: (text: string) => string;
|
||||
listBullet: (text: string) => string;
|
||||
bold: (text: string) => string;
|
||||
italic: (text: string) => string;
|
||||
strikethrough: (text: string) => string;
|
||||
underline: (text: string) => string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -508,70 +528,66 @@ The `coding-agent` provides themed functions when creating components:
|
||||
|
||||
```typescript
|
||||
// In coding-agent
|
||||
import { theme } from './theme.js';
|
||||
import { Markdown } from '@mariozechner/pi-tui';
|
||||
import { theme } from "./theme.js";
|
||||
import { Markdown } from "@oh-my-pi/pi-tui";
|
||||
|
||||
// Helper to create markdown theme functions
|
||||
function getMarkdownTheme(): MarkdownTheme {
|
||||
return {
|
||||
heading: (text) => theme.fg('mdHeading', text),
|
||||
link: (text) => theme.fg('mdLink', text),
|
||||
linkUrl: (text) => theme.fg('mdLinkUrl', text),
|
||||
code: (text) => theme.fg('mdCode', text),
|
||||
codeBlock: (text) => theme.fg('mdCodeBlock', text),
|
||||
codeBlockBorder: (text) => theme.fg('mdCodeBlockBorder', text),
|
||||
quote: (text) => theme.fg('mdQuote', text),
|
||||
quoteBorder: (text) => theme.fg('mdQuoteBorder', text),
|
||||
hr: (text) => theme.fg('mdHr', text),
|
||||
listBullet: (text) => theme.fg('mdListBullet', text),
|
||||
bold: (text) => theme.bold(text),
|
||||
italic: (text) => theme.italic(text),
|
||||
underline: (text) => theme.underline(text),
|
||||
strikethrough: (text) => chalk.strikethrough(text),
|
||||
};
|
||||
return {
|
||||
heading: (text) => theme.fg("mdHeading", text),
|
||||
link: (text) => theme.fg("mdLink", text),
|
||||
linkUrl: (text) => theme.fg("mdLinkUrl", text),
|
||||
code: (text) => theme.fg("mdCode", text),
|
||||
codeBlock: (text) => theme.fg("mdCodeBlock", text),
|
||||
codeBlockBorder: (text) => theme.fg("mdCodeBlockBorder", text),
|
||||
quote: (text) => theme.fg("mdQuote", text),
|
||||
quoteBorder: (text) => theme.fg("mdQuoteBorder", text),
|
||||
hr: (text) => theme.fg("mdHr", text),
|
||||
listBullet: (text) => theme.fg("mdListBullet", text),
|
||||
bold: (text) => theme.bold(text),
|
||||
italic: (text) => theme.italic(text),
|
||||
underline: (text) => theme.underline(text),
|
||||
strikethrough: (text) => chalk.strikethrough(text),
|
||||
};
|
||||
}
|
||||
|
||||
// Create markdown with theme
|
||||
const md = new Markdown(
|
||||
text,
|
||||
1, 1,
|
||||
{ bgColor: theme.bg('userMessageBg') },
|
||||
getMarkdownTheme()
|
||||
);
|
||||
const md = new Markdown(text, 1, 1, { bgColor: theme.bg("userMessageBg") }, getMarkdownTheme());
|
||||
```
|
||||
|
||||
This approach:
|
||||
|
||||
- Keeps TUI components theme-agnostic (reusable in other projects)
|
||||
- Maintains type safety via interfaces
|
||||
- Allows components to have sensible defaults if no theme provided
|
||||
- Centralizes theme access in `coding-agent`
|
||||
|
||||
**Example usage:**
|
||||
|
||||
```typescript
|
||||
const theme = loadTheme('dark');
|
||||
const theme = loadTheme("dark");
|
||||
|
||||
// Apply foreground colors
|
||||
theme.fg('accent', 'Selected')
|
||||
theme.fg('success', '✓ Done')
|
||||
theme.fg('error', 'Failed')
|
||||
theme.fg("accent", "Selected");
|
||||
theme.fg("success", "✓ Done");
|
||||
theme.fg("error", "Failed");
|
||||
|
||||
// Apply background colors
|
||||
theme.bg('userMessageBg', content)
|
||||
theme.bg('toolSuccessBg', output)
|
||||
theme.bg("userMessageBg", content);
|
||||
theme.bg("toolSuccessBg", output);
|
||||
|
||||
// Combine styles
|
||||
theme.bold(theme.fg('accent', 'Title'))
|
||||
theme.italic(theme.fg('muted', 'metadata'))
|
||||
theme.bold(theme.fg("accent", "Title"));
|
||||
theme.italic(theme.fg("muted", "metadata"));
|
||||
|
||||
// Nested foreground + background
|
||||
const userMsg = theme.bg('userMessageBg',
|
||||
theme.fg('userMessageText', 'Hello')
|
||||
)
|
||||
const userMsg = theme.bg("userMessageBg", theme.fg("userMessageText", "Hello"));
|
||||
```
|
||||
|
||||
**Color resolution:**
|
||||
|
||||
1. **Detect terminal capabilities:**
|
||||
|
||||
- Check `$COLORTERM` env var (`truecolor` or `24bit` → truecolor support)
|
||||
- Check `$TERM` env var (`*-256color` → 256-color support)
|
||||
- Fallback to 256-color mode if detection fails
|
||||
@@ -579,37 +595,41 @@ const userMsg = theme.bg('userMessageBg',
|
||||
2. **Load JSON theme file**
|
||||
|
||||
3. **Resolve `vars` references recursively:**
|
||||
|
||||
```json
|
||||
{
|
||||
"vars": {
|
||||
"primary": "#0066cc",
|
||||
"accent": "primary"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "accent" // → "primary" → "#0066cc"
|
||||
}
|
||||
"vars": {
|
||||
"primary": "#0066cc",
|
||||
"accent": "primary"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "accent" // → "primary" → "#0066cc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Convert colors to ANSI codes based on terminal capability:**
|
||||
|
||||
|
||||
**Truecolor mode (24-bit):**
|
||||
|
||||
- Hex (`"#ff0000"`) → `\x1b[38;2;255;0;0m`
|
||||
- 256-color (`42`) → `\x1b[38;5;42m` (keep as-is)
|
||||
- Empty string (`""`) → `\x1b[39m`
|
||||
|
||||
|
||||
**256-color mode:**
|
||||
|
||||
- Hex (`"#ff0000"`) → convert to nearest RGB cube color → `\x1b[38;5;196m`
|
||||
- 256-color (`42`) → `\x1b[38;5;42m` (keep as-is)
|
||||
- Empty string (`""`) → `\x1b[39m`
|
||||
|
||||
|
||||
**Hex to 256-color conversion:**
|
||||
|
||||
```typescript
|
||||
// Convert RGB to 6x6x6 cube (colors 16-231)
|
||||
r_index = Math.round(r / 255 * 5)
|
||||
g_index = Math.round(g / 255 * 5)
|
||||
b_index = Math.round(b / 255 * 5)
|
||||
color_index = 16 + 36 * r_index + 6 * g_index + b_index
|
||||
r_index = Math.round((r / 255) * 5);
|
||||
g_index = Math.round((g / 255) * 5);
|
||||
b_index = Math.round((b / 255) * 5);
|
||||
color_index = 16 + 36 * r_index + 6 * g_index + b_index;
|
||||
```
|
||||
|
||||
5. **Cache as `Theme` instance**
|
||||
|
||||
+124
-126
@@ -4,7 +4,7 @@
|
||||
|
||||
Hooks and custom tools can render custom TUI components for interactive user interfaces. This page covers the component system and available building blocks.
|
||||
|
||||
**Source:** [`@mariozechner/pi-tui`](https://github.com/badlogic/pi-mono/tree/main/packages/tui)
|
||||
**Source:** [`@oh-my-pi/pi-tui`](https://github.com/badlogic/pi-mono/tree/main/packages/tui)
|
||||
|
||||
## Component Interface
|
||||
|
||||
@@ -12,17 +12,17 @@ All components implement:
|
||||
|
||||
```typescript
|
||||
interface Component {
|
||||
render(width: number): string[];
|
||||
handleInput?(data: string): void;
|
||||
invalidate?(): void;
|
||||
render(width: number): string[];
|
||||
handleInput?(data: string): void;
|
||||
invalidate?(): void;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
|
||||
| `handleInput?(data)` | Receive keyboard input when component has focus. |
|
||||
| `invalidate?()` | Clear cached render state. |
|
||||
| Method | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------ |
|
||||
| `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
|
||||
| `handleInput?(data)` | Receive keyboard input when component has focus. |
|
||||
| `invalidate?()` | Clear cached render state. |
|
||||
|
||||
## Using Components
|
||||
|
||||
@@ -30,9 +30,9 @@ interface Component {
|
||||
|
||||
```typescript
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
const handle = ctx.ui.custom(myComponent);
|
||||
// handle.requestRender() - trigger re-render
|
||||
// handle.close() - restore normal UI
|
||||
const handle = ctx.ui.custom(myComponent);
|
||||
// handle.requestRender() - trigger re-render
|
||||
// handle.close() - restore normal UI
|
||||
});
|
||||
```
|
||||
|
||||
@@ -48,10 +48,10 @@ async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
|
||||
## Built-in Components
|
||||
|
||||
Import from `@mariozechner/pi-tui`:
|
||||
Import from `@oh-my-pi/pi-tui`:
|
||||
|
||||
```typescript
|
||||
import { Text, Box, Container, Spacer, Markdown } from "@mariozechner/pi-tui";
|
||||
import { Text, Box, Container, Spacer, Markdown } from "@oh-my-pi/pi-tui";
|
||||
```
|
||||
|
||||
### Text
|
||||
@@ -60,10 +60,10 @@ Multi-line text with word wrapping.
|
||||
|
||||
```typescript
|
||||
const text = new Text(
|
||||
"Hello World", // content
|
||||
1, // paddingX (default: 1)
|
||||
1, // paddingY (default: 1)
|
||||
(s) => bgGray(s) // optional background function
|
||||
"Hello World", // content
|
||||
1, // paddingX (default: 1)
|
||||
1, // paddingY (default: 1)
|
||||
(s) => bgGray(s) // optional background function
|
||||
);
|
||||
text.setText("Updated");
|
||||
```
|
||||
@@ -74,9 +74,9 @@ Container with padding and background color.
|
||||
|
||||
```typescript
|
||||
const box = new Box(
|
||||
1, // paddingX
|
||||
1, // paddingY
|
||||
(s) => bgGray(s) // background function
|
||||
1, // paddingX
|
||||
1, // paddingY
|
||||
(s) => bgGray(s) // background function
|
||||
);
|
||||
box.addChild(new Text("Content", 0, 0));
|
||||
box.setBgFn((s) => bgBlue(s));
|
||||
@@ -98,7 +98,7 @@ container.removeChild(component1);
|
||||
Empty vertical space.
|
||||
|
||||
```typescript
|
||||
const spacer = new Spacer(2); // 2 empty lines
|
||||
const spacer = new Spacer(2); // 2 empty lines
|
||||
```
|
||||
|
||||
### Markdown
|
||||
@@ -107,10 +107,10 @@ Renders markdown with syntax highlighting.
|
||||
|
||||
```typescript
|
||||
const md = new Markdown(
|
||||
"# Title\n\nSome **bold** text",
|
||||
1, // paddingX
|
||||
1, // paddingY
|
||||
theme // MarkdownTheme (see below)
|
||||
"# Title\n\nSome **bold** text",
|
||||
1, // paddingX
|
||||
1, // paddingY
|
||||
theme // MarkdownTheme (see below)
|
||||
);
|
||||
md.setText("Updated markdown");
|
||||
```
|
||||
@@ -121,10 +121,10 @@ Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm).
|
||||
|
||||
```typescript
|
||||
const image = new Image(
|
||||
base64Data, // base64-encoded image
|
||||
"image/png", // MIME type
|
||||
theme, // ImageTheme
|
||||
{ maxWidthCells: 80, maxHeightCells: 24 }
|
||||
base64Data, // base64-encoded image
|
||||
"image/png", // MIME type
|
||||
theme, // ImageTheme
|
||||
{ maxWidthCells: 80, maxHeightCells: 24 }
|
||||
);
|
||||
```
|
||||
|
||||
@@ -138,7 +138,7 @@ import {
|
||||
isArrowUp, isArrowDown, isArrowLeft, isArrowRight,
|
||||
isCtrlC, isCtrlO, isBackspace, isDelete,
|
||||
// ... and more
|
||||
} from "@mariozechner/pi-tui";
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
|
||||
handleInput(data: string) {
|
||||
if (isArrowUp(data)) {
|
||||
@@ -156,7 +156,7 @@ handleInput(data: string) {
|
||||
**Critical:** Each line from `render()` must not exceed the `width` parameter.
|
||||
|
||||
```typescript
|
||||
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { visibleWidth, truncateToWidth } from "@oh-my-pi/pi-tui";
|
||||
|
||||
render(width: number): string[] {
|
||||
// Truncate long lines
|
||||
@@ -165,6 +165,7 @@ render(width: number): string[] {
|
||||
```
|
||||
|
||||
Utilities:
|
||||
|
||||
- `visibleWidth(str)` - Get display width (ignores ANSI codes)
|
||||
- `truncateToWidth(str, width, ellipsis?)` - Truncate with optional ellipsis
|
||||
- `wrapTextWithAnsi(str, width)` - Word wrap preserving ANSI codes
|
||||
@@ -174,55 +175,52 @@ Utilities:
|
||||
Example: Interactive selector
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isEnter, isEscape, isArrowUp, isArrowDown,
|
||||
truncateToWidth, visibleWidth
|
||||
} from "@mariozechner/pi-tui";
|
||||
import { isEnter, isEscape, isArrowUp, isArrowDown, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
||||
|
||||
class MySelector {
|
||||
private items: string[];
|
||||
private selected = 0;
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
public onSelect?: (item: string) => void;
|
||||
public onCancel?: () => void;
|
||||
private items: string[];
|
||||
private selected = 0;
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
constructor(items: string[]) {
|
||||
this.items = items;
|
||||
}
|
||||
public onSelect?: (item: string) => void;
|
||||
public onCancel?: () => void;
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (isArrowUp(data) && this.selected > 0) {
|
||||
this.selected--;
|
||||
this.invalidate();
|
||||
} else if (isArrowDown(data) && this.selected < this.items.length - 1) {
|
||||
this.selected++;
|
||||
this.invalidate();
|
||||
} else if (isEnter(data)) {
|
||||
this.onSelect?.(this.items[this.selected]);
|
||||
} else if (isEscape(data)) {
|
||||
this.onCancel?.();
|
||||
}
|
||||
}
|
||||
constructor(items: string[]) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) {
|
||||
return this.cachedLines;
|
||||
}
|
||||
handleInput(data: string): void {
|
||||
if (isArrowUp(data) && this.selected > 0) {
|
||||
this.selected--;
|
||||
this.invalidate();
|
||||
} else if (isArrowDown(data) && this.selected < this.items.length - 1) {
|
||||
this.selected++;
|
||||
this.invalidate();
|
||||
} else if (isEnter(data)) {
|
||||
this.onSelect?.(this.items[this.selected]);
|
||||
} else if (isEscape(data)) {
|
||||
this.onCancel?.();
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedLines = this.items.map((item, i) => {
|
||||
const prefix = i === this.selected ? "> " : " ";
|
||||
return truncateToWidth(prefix + item, width);
|
||||
});
|
||||
this.cachedWidth = width;
|
||||
return this.cachedLines;
|
||||
}
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) {
|
||||
return this.cachedLines;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
this.cachedLines = this.items.map((item, i) => {
|
||||
const prefix = i === this.selected ? "> " : " ";
|
||||
return truncateToWidth(prefix + item, width);
|
||||
});
|
||||
this.cachedWidth = width;
|
||||
return this.cachedLines;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -230,26 +228,26 @@ Usage in a hook:
|
||||
|
||||
```typescript
|
||||
pi.registerCommand("pick", {
|
||||
description: "Pick an item",
|
||||
handler: async (args, ctx) => {
|
||||
const items = ["Option A", "Option B", "Option C"];
|
||||
const selector = new MySelector(items);
|
||||
|
||||
let handle: { close: () => void; requestRender: () => void };
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
selector.onSelect = (item) => {
|
||||
ctx.ui.notify(`Selected: ${item}`, "info");
|
||||
handle.close();
|
||||
resolve();
|
||||
};
|
||||
selector.onCancel = () => {
|
||||
handle.close();
|
||||
resolve();
|
||||
};
|
||||
handle = ctx.ui.custom(selector);
|
||||
});
|
||||
}
|
||||
description: "Pick an item",
|
||||
handler: async (args, ctx) => {
|
||||
const items = ["Option A", "Option B", "Option C"];
|
||||
const selector = new MySelector(items);
|
||||
|
||||
let handle: { close: () => void; requestRender: () => void };
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
selector.onSelect = (item) => {
|
||||
ctx.ui.notify(`Selected: ${item}`, "info");
|
||||
handle.close();
|
||||
resolve();
|
||||
};
|
||||
selector.onCancel = () => {
|
||||
handle.close();
|
||||
resolve();
|
||||
};
|
||||
handle = ctx.ui.custom(selector);
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -263,7 +261,7 @@ Components accept theme objects for styling.
|
||||
renderResult(result, options, theme) {
|
||||
// Use theme.fg() for foreground colors
|
||||
return new Text(theme.fg("success", "Done!"), 0, 0);
|
||||
|
||||
|
||||
// Use theme.bg() for background colors
|
||||
const styled = theme.bg("toolPendingBg", theme.fg("accent", "text"));
|
||||
}
|
||||
@@ -271,18 +269,18 @@ renderResult(result, options, theme) {
|
||||
|
||||
**Foreground colors** (`theme.fg(color, text)`):
|
||||
|
||||
| Category | Colors |
|
||||
|----------|--------|
|
||||
| General | `text`, `accent`, `muted`, `dim` |
|
||||
| Status | `success`, `error`, `warning` |
|
||||
| Borders | `border`, `borderAccent`, `borderMuted` |
|
||||
| Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |
|
||||
| Tools | `toolTitle`, `toolOutput` |
|
||||
| Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |
|
||||
| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |
|
||||
| Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
|
||||
| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` |
|
||||
| Modes | `bashMode` |
|
||||
| Category | Colors |
|
||||
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| General | `text`, `accent`, `muted`, `dim` |
|
||||
| Status | `success`, `error`, `warning` |
|
||||
| Borders | `border`, `borderAccent`, `borderMuted` |
|
||||
| Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |
|
||||
| Tools | `toolTitle`, `toolOutput` |
|
||||
| Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |
|
||||
| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |
|
||||
| Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
|
||||
| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` |
|
||||
| Modes | `bashMode` |
|
||||
|
||||
**Background colors** (`theme.bg(color, text)`):
|
||||
|
||||
@@ -291,8 +289,8 @@ renderResult(result, options, theme) {
|
||||
**For Markdown**, use `getMarkdownTheme()`:
|
||||
|
||||
```typescript
|
||||
import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
||||
import { Markdown } from "@mariozechner/pi-tui";
|
||||
import { getMarkdownTheme } from "@oh-my-pi/pi-coding-agent";
|
||||
import { Markdown } from "@oh-my-pi/pi-tui";
|
||||
|
||||
renderResult(result, options, theme) {
|
||||
const mdTheme = getMarkdownTheme();
|
||||
@@ -304,8 +302,8 @@ renderResult(result, options, theme) {
|
||||
|
||||
```typescript
|
||||
interface MyTheme {
|
||||
selected: (s: string) => string;
|
||||
normal: (s: string) => string;
|
||||
selected: (s: string) => string;
|
||||
normal: (s: string) => string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -315,23 +313,23 @@ Cache rendered output when possible:
|
||||
|
||||
```typescript
|
||||
class CachedComponent {
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) {
|
||||
return this.cachedLines;
|
||||
}
|
||||
// ... compute lines ...
|
||||
this.cachedWidth = width;
|
||||
this.cachedLines = lines;
|
||||
return lines;
|
||||
}
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) {
|
||||
return this.cachedLines;
|
||||
}
|
||||
// ... compute lines ...
|
||||
this.cachedWidth = width;
|
||||
this.cachedLines = lines;
|
||||
return lines;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
invalidate(): void {
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -7,20 +7,26 @@ Example custom tools for pi-coding-agent.
|
||||
Each example uses the `subdirectory/index.ts` structure required for tool discovery.
|
||||
|
||||
### hello/
|
||||
|
||||
Minimal example showing the basic structure of a custom tool.
|
||||
|
||||
### question/
|
||||
|
||||
Demonstrates `pi.ui.select()` for asking the user questions with options.
|
||||
|
||||
### todo/
|
||||
|
||||
Full-featured example demonstrating:
|
||||
|
||||
- `onSession` for state reconstruction from session history
|
||||
- Custom `renderCall` and `renderResult`
|
||||
- Proper branching support via details storage
|
||||
- State management without external files
|
||||
|
||||
### subagent/
|
||||
|
||||
Delegate tasks to specialized subagents with isolated context windows. Includes:
|
||||
|
||||
- `index.ts` - The custom tool (single, parallel, and chain modes)
|
||||
- `agents.ts` - Agent discovery helper
|
||||
- `agents/` - Sample agent definitions (scout, planner, reviewer, worker)
|
||||
@@ -39,6 +45,7 @@ cp -r todo ~/.pi/agent/tools/
|
||||
```
|
||||
|
||||
Then in pi:
|
||||
|
||||
```
|
||||
> add a todo "test custom tools"
|
||||
> list todos
|
||||
@@ -53,37 +60,41 @@ See [docs/custom-tools.md](../../docs/custom-tools.md) for full documentation.
|
||||
### Key Points
|
||||
|
||||
**Factory pattern:**
|
||||
|
||||
```typescript
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { StringEnum } from "@mariozechner/pi-ai";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
||||
import { StringEnum } from "@oh-my-pi/pi-ai";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const factory: CustomToolFactory = (pi) => ({
|
||||
name: "my_tool",
|
||||
label: "My Tool",
|
||||
description: "Tool description for LLM",
|
||||
parameters: Type.Object({
|
||||
action: StringEnum(["list", "add"] as const),
|
||||
}),
|
||||
|
||||
// Called on session start/switch/branch/clear
|
||||
onSession(event) {
|
||||
// Reconstruct state from event.entries
|
||||
},
|
||||
|
||||
async execute(toolCallId, params) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Result" }],
|
||||
details: { /* for rendering and state reconstruction */ },
|
||||
};
|
||||
},
|
||||
name: "my_tool",
|
||||
label: "My Tool",
|
||||
description: "Tool description for LLM",
|
||||
parameters: Type.Object({
|
||||
action: StringEnum(["list", "add"] as const),
|
||||
}),
|
||||
|
||||
// Called on session start/switch/branch/clear
|
||||
onSession(event) {
|
||||
// Reconstruct state from event.entries
|
||||
},
|
||||
|
||||
async execute(toolCallId, params) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Result" }],
|
||||
details: {
|
||||
/* for rendering and state reconstruction */
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default factory;
|
||||
```
|
||||
|
||||
**Custom rendering:**
|
||||
|
||||
```typescript
|
||||
renderCall(args, theme) {
|
||||
return new Text(
|
||||
@@ -101,12 +112,13 @@ renderResult(result, { expanded, isPartial }, theme) {
|
||||
```
|
||||
|
||||
**Use StringEnum for string parameters** (required for Google API compatibility):
|
||||
|
||||
```typescript
|
||||
import { StringEnum } from "@mariozechner/pi-ai";
|
||||
import { StringEnum } from "@oh-my-pi/pi-ai";
|
||||
|
||||
// Good
|
||||
action: StringEnum(["list", "add"] as const)
|
||||
action: StringEnum(["list", "add"] as const);
|
||||
|
||||
// Bad - doesn't work with Google
|
||||
action: Type.Union([Type.Literal("list"), Type.Literal("add")])
|
||||
action: Type.Union([Type.Literal("list"), Type.Literal("add")]);
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
||||
import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const factory: CustomToolFactory = (pi) => ({
|
||||
name: "hello",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Question Tool - Let the LLM ask the user a question with options
|
||||
*/
|
||||
|
||||
import type { CustomTool, CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
||||
import type { CustomTool, CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
interface QuestionDetails {
|
||||
question: string;
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { Message } from "@mariozechner/pi-ai";
|
||||
import type { CustomTool, CustomToolAPI, CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
||||
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Message } from "@oh-my-pi/pi-ai";
|
||||
import type { CustomTool, CustomToolAPI, CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
|
||||
import { type AgentConfig, type AgentScope, discoverAgents, formatAgentList } from "./agents.js";
|
||||
|
||||
const MAX_PARALLEL_TASKS = 8;
|
||||
@@ -755,7 +755,10 @@ const factory: CustomToolFactory = (pi) => {
|
||||
|
||||
if (expanded) {
|
||||
const container = new Container();
|
||||
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
||||
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg(
|
||||
"muted",
|
||||
` (${r.agentSource})`,
|
||||
)}`;
|
||||
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
||||
container.addChild(new Text(header, 0, 0));
|
||||
if (isError && r.errorMessage)
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
CustomToolContext,
|
||||
CustomToolFactory,
|
||||
CustomToolSessionEvent,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
} from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
interface Todo {
|
||||
id: number;
|
||||
@@ -188,10 +188,7 @@ const factory: CustomToolFactory = (pi) => {
|
||||
case "add": {
|
||||
const added = todoList[todoList.length - 1];
|
||||
return new Text(
|
||||
theme.fg("success", "✓ Added ") +
|
||||
theme.fg("accent", `#${added.id}`) +
|
||||
" " +
|
||||
theme.fg("muted", added.text),
|
||||
`${theme.fg("success", "✓ Added ") + theme.fg("accent", `#${added.id}`)} ${theme.fg("muted", added.text)}`,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -14,43 +14,43 @@ cp permission-gate.ts ~/.pi/agent/hooks/
|
||||
|
||||
## Examples
|
||||
|
||||
| Hook | Description |
|
||||
|------|-------------|
|
||||
| `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
|
||||
| `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
|
||||
| `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
|
||||
| `file-trigger.ts` | Watches a trigger file and injects contents into conversation |
|
||||
| `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, branch) |
|
||||
| `dirty-repo-guard.ts` | Prevents session changes with uncommitted git changes |
|
||||
| `auto-commit-on-exit.ts` | Auto-commits on exit using last assistant message for commit message |
|
||||
| `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
|
||||
| `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
|
||||
| `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
|
||||
| `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
|
||||
| `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
|
||||
| Hook | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------------ |
|
||||
| `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
|
||||
| `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
|
||||
| `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
|
||||
| `file-trigger.ts` | Watches a trigger file and injects contents into conversation |
|
||||
| `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, branch) |
|
||||
| `dirty-repo-guard.ts` | Prevents session changes with uncommitted git changes |
|
||||
| `auto-commit-on-exit.ts` | Auto-commits on exit using last assistant message for commit message |
|
||||
| `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
|
||||
| `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
|
||||
| `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
|
||||
| `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
|
||||
| `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
|
||||
|
||||
## Writing Hooks
|
||||
|
||||
See [docs/hooks.md](../../docs/hooks.md) for full documentation.
|
||||
|
||||
```typescript
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
// Subscribe to events
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
|
||||
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
|
||||
if (!ok) return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
});
|
||||
// Subscribe to events
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
|
||||
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
|
||||
if (!ok) return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
});
|
||||
|
||||
// Register custom commands
|
||||
pi.registerCommand("hello", {
|
||||
description: "Say hello",
|
||||
handler: async (args, ctx) => {
|
||||
ctx.ui.notify("Hello!", "info");
|
||||
},
|
||||
});
|
||||
// Register custom commands
|
||||
pi.registerCommand("hello", {
|
||||
description: "Say hello",
|
||||
handler: async (args, ctx) => {
|
||||
ctx.ui.notify("Hello!", "info");
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Uses the last assistant message to generate a commit message.
|
||||
*/
|
||||
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_shutdown", async (_event, ctx) => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Demonstrates how to cancel session events using the before_* events.
|
||||
*/
|
||||
|
||||
import type { HookAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
* pi --hook examples/hooks/custom-compaction.ts
|
||||
*/
|
||||
|
||||
import { complete, getModel } from "@mariozechner/pi-ai";
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
|
||||
import { complete, getModel } from "@oh-my-pi/pi-ai";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
import { convertToLlm, serializeConversation } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
@@ -42,7 +42,9 @@ export default function (pi: HookAPI) {
|
||||
const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
|
||||
|
||||
ctx.ui.notify(
|
||||
`Custom compaction: summarizing ${allMessages.length} messages (${tokensBefore.toLocaleString()} tokens) with ${model.id}...`,
|
||||
`Custom compaction: summarizing ${allMessages.length} messages (${tokensBefore.toLocaleString()} tokens) with ${
|
||||
model.id
|
||||
}...`,
|
||||
"info",
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Useful to ensure work is committed before switching context.
|
||||
*/
|
||||
|
||||
import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI, HookContext } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
async function checkDirtyRepo(pi: HookAPI, ctx: HookContext, action: string): Promise<{ cancel: boolean } | undefined> {
|
||||
// Check for uncommitted changes
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* When branching, offers to restore code to that point in history.
|
||||
*/
|
||||
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
const checkpoints = new Map<string, string>();
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
* The generated prompt appears as a draft in the editor for review/editing.
|
||||
*/
|
||||
|
||||
import { complete, type Message } from "@mariozechner/pi-ai";
|
||||
import type { HookAPI, SessionEntry } from "@mariozechner/pi-coding-agent";
|
||||
import { BorderedLoader, convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
|
||||
import { complete, type Message } from "@oh-my-pi/pi-ai";
|
||||
import type { HookAPI, SessionEntry } from "@oh-my-pi/pi-coding-agent";
|
||||
import { BorderedLoader, convertToLlm, serializeConversation } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Patterns checked: rm -rf, sudo, chmod/chown 777
|
||||
*/
|
||||
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Useful for preventing accidental modifications to sensitive files.
|
||||
*/
|
||||
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
const protectedPaths = [".env", ".git/", "node_modules/"];
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
* 3. Loads the result into the editor for user to fill in answers
|
||||
*/
|
||||
|
||||
import { complete, type UserMessage } from "@mariozechner/pi-ai";
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { BorderedLoader } from "@mariozechner/pi-coding-agent";
|
||||
import { complete, type UserMessage } from "@oh-my-pi/pi-ai";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
import { BorderedLoader } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const SYSTEM_PROMPT = `You are a question extractor. Given text from a conversation, extract any questions that need answering and format them for the user to fill in.
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Snake game hook - play snake with /snake command
|
||||
*/
|
||||
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { isArrowDown, isArrowLeft, isArrowRight, isArrowUp, isEscape, visibleWidth } from "@mariozechner/pi-tui";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
import { isArrowDown, isArrowLeft, isArrowRight, isArrowUp, isEscape, visibleWidth } from "@oh-my-pi/pi-tui";
|
||||
|
||||
const GAME_WIDTH = 40;
|
||||
const GAME_HEIGHT = 15;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Shows turn progress with themed colors.
|
||||
*/
|
||||
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
let turnCount = 0;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* from cwd and ~/.pi/agent. Model chosen from settings or first available.
|
||||
*/
|
||||
|
||||
import { createAgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
const { session } = await createAgentSession();
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Shows how to select a specific model and thinking level.
|
||||
*/
|
||||
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { createAgentSession, discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
|
||||
import { getModel } from "@oh-my-pi/pi-ai";
|
||||
import { createAgentSession, discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Set up auth storage and model registry
|
||||
const authStorage = discoverAuthStorage();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Shows how to replace or modify the default system prompt.
|
||||
*/
|
||||
|
||||
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Option 1: Replace prompt entirely
|
||||
const { session: session1 } = await createAgentSession({
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Discover, filter, merge, or replace them.
|
||||
*/
|
||||
|
||||
import { createAgentSession, discoverSkills, SessionManager, type Skill } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, discoverSkills, SessionManager, type Skill } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Discover all skills from cwd/.pi/skills, ~/.pi/agent/skills, etc.
|
||||
const allSkills = discoverSkills();
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
readOnlyTools, // read, grep, find, ls - uses process.cwd()
|
||||
readTool,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
} from "@oh-my-pi/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
// Read-only mode (no edit/write) - uses process.cwd()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Hooks intercept agent events for logging, blocking, or modification.
|
||||
*/
|
||||
|
||||
import { createAgentSession, type HookFactory, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, type HookFactory, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Logging hook
|
||||
const loggingHook: HookFactory = (api) => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Context files provide project-specific instructions loaded into the system prompt.
|
||||
*/
|
||||
|
||||
import { createAgentSession, discoverContextFiles, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, discoverContextFiles, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Discover AGENTS.md files walking up from cwd
|
||||
const discovered = discoverContextFiles();
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
discoverSlashCommands,
|
||||
type FileSlashCommand,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
} from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Discover commands from cwd/.pi/commands/ and ~/.pi/agent/commands/
|
||||
const discovered = discoverSlashCommands();
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
discoverModels,
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
} from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Default: discoverAuthStorage() uses ~/.pi/agent/auth.json
|
||||
// discoverModels() loads built-in + custom models from ~/.pi/agent/models.json
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Override settings using SettingsManager.
|
||||
*/
|
||||
|
||||
import { createAgentSession, loadSettings, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, loadSettings, SessionManager, SettingsManager } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Load current settings (merged global + project)
|
||||
const settings = loadSettings();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Control session persistence: in-memory, new file, continue, or open specific.
|
||||
*/
|
||||
|
||||
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// In-memory (no persistence)
|
||||
const { session: inMemory } = await createAgentSession({
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* paths relative to your cwd.
|
||||
*/
|
||||
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { getModel } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
AuthStorage,
|
||||
type CustomTool,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
} from "@oh-my-pi/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
// Custom auth storage location
|
||||
|
||||
@@ -4,20 +4,20 @@ Programmatic usage of pi-coding-agent via `createAgentSession()`.
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `01-minimal.ts` | Simplest usage with all defaults |
|
||||
| `02-custom-model.ts` | Select model and thinking level |
|
||||
| `03-custom-prompt.ts` | Replace or modify system prompt |
|
||||
| `04-skills.ts` | Discover, filter, or replace skills |
|
||||
| `05-tools.ts` | Built-in tools, custom tools |
|
||||
| `06-hooks.ts` | Logging, blocking, result modification |
|
||||
| `07-context-files.ts` | AGENTS.md context files |
|
||||
| `08-slash-commands.ts` | File-based slash commands |
|
||||
| `09-api-keys-and-oauth.ts` | API key resolution, OAuth config |
|
||||
| `10-settings.ts` | Override compaction, retry, terminal settings |
|
||||
| `11-sessions.ts` | In-memory, persistent, continue, list sessions |
|
||||
| `12-full-control.ts` | Replace everything, no discovery |
|
||||
| File | Description |
|
||||
| -------------------------- | ---------------------------------------------- |
|
||||
| `01-minimal.ts` | Simplest usage with all defaults |
|
||||
| `02-custom-model.ts` | Select model and thinking level |
|
||||
| `03-custom-prompt.ts` | Replace or modify system prompt |
|
||||
| `04-skills.ts` | Discover, filter, or replace skills |
|
||||
| `05-tools.ts` | Built-in tools, custom tools |
|
||||
| `06-hooks.ts` | Logging, blocking, result modification |
|
||||
| `07-context-files.ts` | AGENTS.md context files |
|
||||
| `08-slash-commands.ts` | File-based slash commands |
|
||||
| `09-api-keys-and-oauth.ts` | API key resolution, OAuth config |
|
||||
| `10-settings.ts` | Override compaction, retry, terminal settings |
|
||||
| `11-sessions.ts` | In-memory, persistent, continue, list sessions |
|
||||
| `12-full-control.ts` | Replace everything, no discovery |
|
||||
|
||||
## Running
|
||||
|
||||
@@ -29,25 +29,28 @@ npx tsx examples/sdk/01-minimal.ts
|
||||
## Quick Reference
|
||||
|
||||
```typescript
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { getModel } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
AuthStorage,
|
||||
createAgentSession,
|
||||
discoverAuthStorage,
|
||||
discoverModels,
|
||||
discoverSkills,
|
||||
discoverHooks,
|
||||
discoverCustomTools,
|
||||
discoverContextFiles,
|
||||
discoverSlashCommands,
|
||||
loadSettings,
|
||||
buildSystemPrompt,
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
codingTools,
|
||||
readOnlyTools,
|
||||
readTool, bashTool, editTool, writeTool,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
AuthStorage,
|
||||
createAgentSession,
|
||||
discoverAuthStorage,
|
||||
discoverModels,
|
||||
discoverSkills,
|
||||
discoverHooks,
|
||||
discoverCustomTools,
|
||||
discoverContextFiles,
|
||||
discoverSlashCommands,
|
||||
loadSettings,
|
||||
buildSystemPrompt,
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
codingTools,
|
||||
readOnlyTools,
|
||||
readTool,
|
||||
bashTool,
|
||||
editTool,
|
||||
writeTool,
|
||||
} from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Auth and models setup
|
||||
const authStorage = discoverAuthStorage();
|
||||
@@ -62,9 +65,9 @@ const { session } = await createAgentSession({ model, thinkingLevel: "high", aut
|
||||
|
||||
// Modify prompt
|
||||
const { session } = await createAgentSession({
|
||||
systemPrompt: (defaultPrompt) => defaultPrompt + "\n\nBe concise.",
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
systemPrompt: (defaultPrompt) => defaultPrompt + "\n\nBe concise.",
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
// Read-only
|
||||
@@ -72,9 +75,9 @@ const { session } = await createAgentSession({ tools: readOnlyTools, authStorage
|
||||
|
||||
// In-memory
|
||||
const { session } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
// Full control
|
||||
@@ -83,69 +86,69 @@ customAuth.setRuntimeApiKey("anthropic", process.env.MY_KEY!);
|
||||
const customRegistry = new ModelRegistry(customAuth);
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
model,
|
||||
authStorage: customAuth,
|
||||
modelRegistry: customRegistry,
|
||||
systemPrompt: "You are helpful.",
|
||||
tools: [readTool, bashTool],
|
||||
customTools: [{ tool: myTool }],
|
||||
hooks: [{ factory: myHook }],
|
||||
skills: [],
|
||||
contextFiles: [],
|
||||
slashCommands: [],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
model,
|
||||
authStorage: customAuth,
|
||||
modelRegistry: customRegistry,
|
||||
systemPrompt: "You are helpful.",
|
||||
tools: [readTool, bashTool],
|
||||
customTools: [{ tool: myTool }],
|
||||
hooks: [{ factory: myHook }],
|
||||
skills: [],
|
||||
contextFiles: [],
|
||||
slashCommands: [],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
// Run prompts
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
await session.prompt("Hello");
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `authStorage` | `discoverAuthStorage()` | Credential storage |
|
||||
| `modelRegistry` | `discoverModels(authStorage)` | Model registry |
|
||||
| `cwd` | `process.cwd()` | Working directory |
|
||||
| `agentDir` | `~/.pi/agent` | Config directory |
|
||||
| `model` | From settings/first available | Model to use |
|
||||
| `thinkingLevel` | From settings/"off" | off, low, medium, high |
|
||||
| `systemPrompt` | Discovered | String or `(default) => modified` |
|
||||
| `tools` | `codingTools` | Built-in tools |
|
||||
| `customTools` | Discovered | Replaces discovery |
|
||||
| `additionalCustomToolPaths` | `[]` | Merge with discovery |
|
||||
| `hooks` | Discovered | Replaces discovery |
|
||||
| `additionalHookPaths` | `[]` | Merge with discovery |
|
||||
| `skills` | Discovered | Skills for prompt |
|
||||
| `contextFiles` | Discovered | AGENTS.md files |
|
||||
| `slashCommands` | Discovered | File commands |
|
||||
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
|
||||
| `settingsManager` | From agentDir | Settings overrides |
|
||||
| Option | Default | Description |
|
||||
| --------------------------- | ----------------------------- | --------------------------------- |
|
||||
| `authStorage` | `discoverAuthStorage()` | Credential storage |
|
||||
| `modelRegistry` | `discoverModels(authStorage)` | Model registry |
|
||||
| `cwd` | `process.cwd()` | Working directory |
|
||||
| `agentDir` | `~/.pi/agent` | Config directory |
|
||||
| `model` | From settings/first available | Model to use |
|
||||
| `thinkingLevel` | From settings/"off" | off, low, medium, high |
|
||||
| `systemPrompt` | Discovered | String or `(default) => modified` |
|
||||
| `tools` | `codingTools` | Built-in tools |
|
||||
| `customTools` | Discovered | Replaces discovery |
|
||||
| `additionalCustomToolPaths` | `[]` | Merge with discovery |
|
||||
| `hooks` | Discovered | Replaces discovery |
|
||||
| `additionalHookPaths` | `[]` | Merge with discovery |
|
||||
| `skills` | Discovered | Skills for prompt |
|
||||
| `contextFiles` | Discovered | AGENTS.md files |
|
||||
| `slashCommands` | Discovered | File commands |
|
||||
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
|
||||
| `settingsManager` | From agentDir | Settings overrides |
|
||||
|
||||
## Events
|
||||
|
||||
```typescript
|
||||
session.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "message_update":
|
||||
if (event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
break;
|
||||
case "tool_execution_start":
|
||||
console.log(`Tool: ${event.toolName}`);
|
||||
break;
|
||||
case "tool_execution_end":
|
||||
console.log(`Result: ${event.result}`);
|
||||
break;
|
||||
case "agent_end":
|
||||
console.log("Done");
|
||||
break;
|
||||
}
|
||||
switch (event.type) {
|
||||
case "message_update":
|
||||
if (event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
break;
|
||||
case "tool_execution_start":
|
||||
console.log(`Tool: ${event.toolName}`);
|
||||
break;
|
||||
case "tool_execution_end":
|
||||
console.log(`Result: ${event.result}`);
|
||||
break;
|
||||
case "agent_end":
|
||||
console.log("Done");
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"name": "@oh-my-pi/pi-coding-agent",
|
||||
"version": "1.337.0",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"type": "module",
|
||||
@@ -38,9 +38,11 @@
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent-core": "workspace:*",
|
||||
"@mariozechner/pi-ai": "workspace:*",
|
||||
"@mariozechner/pi-tui": "workspace:*",
|
||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-tui": "workspace:*",
|
||||
"@sinclair/typebox": "^0.34.46",
|
||||
"ajv": "^8.17.1",
|
||||
"chalk": "^5.5.0",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"cli-highlight": "^2.1.11",
|
||||
@@ -69,7 +71,7 @@
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/badlogic/pi-mono.git",
|
||||
"url": "git+https://github.com/can1357/oh-my-pi.git",
|
||||
"directory": "packages/coding-agent"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* CLI argument parsing and help display
|
||||
*/
|
||||
|
||||
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import chalk from "chalk";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config.js";
|
||||
import { allTools, type ToolName } from "../core/tools/index.js";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { access, readFile, stat } from "node:fs/promises";
|
||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import { resolve } from "path";
|
||||
import { resolveReadPath } from "../core/tools/path-utils.js";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* List available models with optional fuzzy search
|
||||
*/
|
||||
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
||||
import type { ModelRegistry } from "../core/model-registry.js";
|
||||
import { fuzzyFilter } from "../utils/fuzzy.js";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* TUI session selector for --resume flag
|
||||
*/
|
||||
|
||||
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
||||
import { ProcessTerminal, TUI } from "@oh-my-pi/pi-tui";
|
||||
import type { SessionInfo } from "../core/session-manager.js";
|
||||
import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js";
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
* Modes use this class and add their own I/O layer on top.
|
||||
*/
|
||||
|
||||
import type { Agent, AgentEvent, AgentMessage, AgentState, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai";
|
||||
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
||||
import type { Agent, AgentEvent, AgentMessage, AgentState, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
|
||||
import { getAuthPath } from "../config.js";
|
||||
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor.js";
|
||||
import {
|
||||
@@ -1162,7 +1162,9 @@ export class AgentSession {
|
||||
|
||||
if (reason === "overflow") {
|
||||
throw new Error(
|
||||
`Context overflow: ${error instanceof Error ? error.message : "compaction failed"}. Your input may be too large for the context window.`,
|
||||
`Context overflow: ${
|
||||
error instanceof Error ? error.message : "compaction failed"
|
||||
}. Your input may be too large for the context window.`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
loginGitHubCopilot,
|
||||
type OAuthCredentials,
|
||||
type OAuthProvider,
|
||||
} from "@mariozechner/pi-ai";
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
|
||||
export type ApiKeyCredential = {
|
||||
type: "api_key";
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* a summary of the branch being left so context isn't lost.
|
||||
*/
|
||||
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import { completeSimple } from "@mariozechner/pi-ai";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { completeSimple } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
convertToLlm,
|
||||
createBranchSummaryMessage,
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* and after compaction the session is reloaded.
|
||||
*/
|
||||
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { AssistantMessage, Model, Usage } from "@mariozechner/pi-ai";
|
||||
import { complete, completeSimple } from "@mariozechner/pi-ai";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
|
||||
import { complete, completeSimple } from "@oh-my-pi/pi-ai";
|
||||
import { convertToLlm, createBranchSummaryMessage, createHookMessage } from "../messages.js";
|
||||
import type { CompactionEntry, SessionEntry } from "../session-manager.js";
|
||||
import {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Shared utilities for compaction and branch summarization.
|
||||
*/
|
||||
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { Message } from "@mariozechner/pi-ai";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Message } from "@oh-my-pi/pi-ai";
|
||||
|
||||
// ============================================================================
|
||||
// File Operation Tracking
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* They can provide custom rendering for tool calls and results in the TUI.
|
||||
*/
|
||||
|
||||
import type { AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-agent-core";
|
||||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import type { Component } from "@mariozechner/pi-tui";
|
||||
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import type { Static, TSchema } from "@sinclair/typebox";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ExecOptions, ExecResult } from "../exec.js";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Wraps CustomTool instances into AgentTool for use with the agent.
|
||||
*/
|
||||
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CustomTool, CustomToolContext, LoadedCustomTool } from "./types.js";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentState } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentState } from "@oh-my-pi/pi-agent-core";
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { basename, join } from "path";
|
||||
import { APP_NAME, getExportTemplateDir } from "../../config.js";
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Hook runner - executes hooks and manages their lifecycle.
|
||||
*/
|
||||
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ModelRegistry } from "../model-registry.js";
|
||||
import type { SessionManager } from "../session-manager.js";
|
||||
@@ -400,7 +400,7 @@ export class HookRunner {
|
||||
*/
|
||||
async emitBeforeAgentStart(
|
||||
prompt: string,
|
||||
images?: import("@mariozechner/pi-ai").ImageContent[],
|
||||
images?: import("@oh-my-pi/pi-ai").ImageContent[],
|
||||
): Promise<BeforeAgentStartEventResult | undefined> {
|
||||
const ctx = this.createContext();
|
||||
let result: BeforeAgentStartEventResult | undefined;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tool wrapper - wraps tools with hook callbacks for interception.
|
||||
*/
|
||||
|
||||
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import type { HookRunner } from "./runner.js";
|
||||
import type { ToolCallEventResult, ToolResultEventResult } from "./types.js";
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* and interact with the user via UI primitives.
|
||||
*/
|
||||
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
|
||||
import type { Component, TUI } from "@mariozechner/pi-tui";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
||||
import type { Component, TUI } from "@oh-my-pi/pi-tui";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
|
||||
import type { ExecOptions, ExecResult } from "../exec.js";
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
* Integrates MCP tool discovery with the custom tools system.
|
||||
*/
|
||||
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
import type { LoadedCustomTool } from "../custom-tools/types.js";
|
||||
import { createMCPManager, type MCPLoadResult, MCPManager } from "./manager.js";
|
||||
import type { MCPToolDetails } from "./tool-bridge.js";
|
||||
import { type MCPLoadResult, MCPManager } from "./manager.js";
|
||||
|
||||
/** Result from loading MCP tools */
|
||||
export interface MCPToolsLoadResult {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
import type { CustomTool, CustomToolResult } from "../custom-tools/types.js";
|
||||
import { callTool } from "./client.js";
|
||||
import type { MCPContent, MCPServerConnection, MCPToolDefinition, MCPToolWithServer } from "./types.js";
|
||||
import type { MCPContent, MCPServerConnection, MCPToolDefinition } from "./types.js";
|
||||
|
||||
/** Details included in MCP tool results for rendering */
|
||||
export interface MCPToolDetails {
|
||||
|
||||
@@ -187,7 +187,7 @@ export class StdioTransport implements MCPTransport {
|
||||
reject,
|
||||
});
|
||||
|
||||
const message = JSON.stringify(request) + "\n";
|
||||
const message = `${JSON.stringify(request)}\n`;
|
||||
try {
|
||||
// Bun's FileSink has write() method directly
|
||||
this.process!.stdin.write(message);
|
||||
@@ -210,7 +210,7 @@ export class StdioTransport implements MCPTransport {
|
||||
params: params ?? {},
|
||||
};
|
||||
|
||||
const message = JSON.stringify(notification) + "\n";
|
||||
const message = `${JSON.stringify(notification)}\n`;
|
||||
// Bun's FileSink has write() method directly
|
||||
this.process.stdin.write(message);
|
||||
this.process.stdin.flush();
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* and provides a transformer to convert them to LLM-compatible messages.
|
||||
*/
|
||||
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
||||
|
||||
export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
|
||||
|
||||
@@ -65,7 +65,7 @@ export interface CompactionSummaryMessage {
|
||||
}
|
||||
|
||||
// Extend CustomAgentMessages via declaration merging
|
||||
declare module "@mariozechner/pi-agent-core" {
|
||||
declare module "@oh-my-pi/pi-agent-core" {
|
||||
interface CustomAgentMessages {
|
||||
bashExecution: BashExecutionMessage;
|
||||
hookMessage: HookMessage;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type KnownProvider,
|
||||
type Model,
|
||||
normalizeDomain,
|
||||
} from "@mariozechner/pi-ai";
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import AjvModule from "ajv";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
@@ -196,7 +196,9 @@ export class ModelRegistry {
|
||||
}
|
||||
return {
|
||||
models: [],
|
||||
error: `Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsJsonPath}`,
|
||||
error: `Failed to load models.json: ${
|
||||
error instanceof Error ? error.message : error
|
||||
}\n\nFile: ${modelsJsonPath}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Model resolution, scoping, and initial selection
|
||||
*/
|
||||
|
||||
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@mariozechner/pi-ai";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import { minimatch } from "minimatch";
|
||||
import { isValidThinkingLevel } from "../cli/args.js";
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { Agent, type ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import { Agent, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { join } from "path";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { AgentSession } from "./agent-session.js";
|
||||
@@ -432,7 +432,7 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
||||
* const { session } = await createAgentSession();
|
||||
*
|
||||
* // With explicit model
|
||||
* import { getModel } from '@mariozechner/pi-ai';
|
||||
* import { getModel } from '@oh-my-pi/pi-ai';
|
||||
* const { session } = await createAgentSession({
|
||||
* model: getModel('anthropic', 'claude-opus-4-5'),
|
||||
* thinkingLevel: 'high',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
appendFileSync,
|
||||
closeSync,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* and add "(Recommended)" at the end of the label
|
||||
*/
|
||||
|
||||
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { Subprocess } from "bun";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
@@ -218,10 +218,14 @@ Output truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB.
|
||||
// Format output based on action
|
||||
let formattedOutput: string;
|
||||
if (action === "apply") {
|
||||
formattedOutput = `Applied ${matchCount} replacement${matchCount !== 1 ? "s" : ""} in ${fileCount} file${fileCount !== 1 ? "s" : ""}:\n`;
|
||||
formattedOutput = `Applied ${matchCount} replacement${matchCount !== 1 ? "s" : ""} in ${fileCount} file${
|
||||
fileCount !== 1 ? "s" : ""
|
||||
}:\n`;
|
||||
formattedOutput += Array.from(files).join("\n");
|
||||
} else if (action === "preview") {
|
||||
formattedOutput = `Preview of ${matchCount} replacement${matchCount !== 1 ? "s" : ""} in ${fileCount} file${fileCount !== 1 ? "s" : ""}:\n\n`;
|
||||
formattedOutput = `Preview of ${matchCount} replacement${matchCount !== 1 ? "s" : ""} in ${fileCount} file${
|
||||
fileCount !== 1 ? "s" : ""
|
||||
}:\n\n`;
|
||||
for (const m of matches) {
|
||||
formattedOutput += `${m.file}:${m.line}\n`;
|
||||
formattedOutput += ` - ${m.text}\n`;
|
||||
@@ -232,7 +236,9 @@ Output truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB.
|
||||
}
|
||||
} else {
|
||||
// search mode
|
||||
formattedOutput = `Found ${matchCount} match${matchCount !== 1 ? "es" : ""} in ${fileCount} file${fileCount !== 1 ? "s" : ""}:\n\n`;
|
||||
formattedOutput = `Found ${matchCount} match${matchCount !== 1 ? "es" : ""} in ${fileCount} file${
|
||||
fileCount !== 1 ? "s" : ""
|
||||
}:\n\n`;
|
||||
for (const m of matches) {
|
||||
formattedOutput += `${m.file}:${m.line}: ${m.text}\n`;
|
||||
}
|
||||
@@ -255,7 +261,9 @@ Output truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB.
|
||||
if (truncation.truncatedBy === "lines") {
|
||||
finalOutput += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}]`;
|
||||
} else {
|
||||
finalOutput += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit)]`;
|
||||
finalOutput += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(
|
||||
DEFAULT_MAX_BYTES,
|
||||
)} limit)]`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { Subprocess } from "bun";
|
||||
import { getShellConfig, killProcessTree } from "../../utils/shell.js";
|
||||
@@ -30,7 +30,9 @@ export function createBashTool(cwd: string): AgentTool<typeof bashSchema> {
|
||||
return {
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
||||
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${
|
||||
DEFAULT_MAX_BYTES / 1024
|
||||
}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
||||
parameters: bashSchema,
|
||||
execute: async (
|
||||
_toolCallId: string,
|
||||
@@ -187,11 +189,15 @@ export function createBashTool(cwd: string): AgentTool<typeof bashSchema> {
|
||||
|
||||
if (truncation.lastLinePartial) {
|
||||
const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
|
||||
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
|
||||
outputText += `\n\n[Showing last ${formatSize(
|
||||
truncation.outputBytes,
|
||||
)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
|
||||
} else if (truncation.truncatedBy === "lines") {
|
||||
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
|
||||
} else {
|
||||
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
|
||||
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(
|
||||
DEFAULT_MAX_BYTES,
|
||||
)} limit). Full output: ${tempFilePath}]`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentToolContext } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentToolContext } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CustomToolContext } from "../custom-tools/types.js";
|
||||
import type { HookUIContext } from "../hooks/types.js";
|
||||
|
||||
declare module "@mariozechner/pi-agent-core" {
|
||||
declare module "@oh-my-pi/pi-agent-core" {
|
||||
interface AgentToolContext extends CustomToolContext {
|
||||
ui?: HookUIContext;
|
||||
hasUI?: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { constants } from "fs";
|
||||
import { access, readFile, writeFile } from "fs/promises";
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Tree-based rendering with collapsed/expanded states for Exa search results.
|
||||
*/
|
||||
|
||||
import type { Component } from "@mariozechner/pi-tui";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import type { Theme } from "../../../modes/interactive/theme/theme.js";
|
||||
import type { RenderResultOptions } from "../../custom-tools/types.js";
|
||||
import { logViewError } from "./logger.js";
|
||||
@@ -81,7 +81,10 @@ export function renderExaResult(
|
||||
const expandHint = expanded ? "" : theme.fg("dim", " (Ctrl+O to expand)");
|
||||
const toolLabel = details?.toolName ?? "Exa Search";
|
||||
|
||||
let headerParts = `${icon} ${theme.fg("toolTitle", toolLabel)} · ${theme.fg("dim", `${resultCount} result${resultCount !== 1 ? "s" : ""}`)}`;
|
||||
let headerParts = `${icon} ${theme.fg("toolTitle", toolLabel)} · ${theme.fg(
|
||||
"dim",
|
||||
`${resultCount} result${resultCount !== 1 ? "s" : ""}`,
|
||||
)}`;
|
||||
|
||||
if (cost !== undefined) {
|
||||
headerParts += ` · ${theme.fg("muted", `$${cost.toFixed(4)}`)}`;
|
||||
@@ -109,7 +112,10 @@ export function renderExaResult(
|
||||
}
|
||||
|
||||
if (resultCount > 1) {
|
||||
text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `${resultCount - 1} more result${resultCount !== 2 ? "s" : ""}`)}`;
|
||||
text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg(
|
||||
"muted",
|
||||
`${resultCount - 1} more result${resultCount !== 2 ? "s" : ""}`,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -129,7 +135,10 @@ export function renderExaResult(
|
||||
const domain = res.url ? getDomain(res.url) : "";
|
||||
const domainPart = domain ? theme.fg("dim", ` (${domain})`) : "";
|
||||
|
||||
text += `\n ${theme.fg("dim", TREE_SPACE)} ${theme.fg("dim", branch)} ${theme.fg("accent", title)}${domainPart}`;
|
||||
text += `\n ${theme.fg("dim", TREE_SPACE)} ${theme.fg("dim", branch)} ${theme.fg(
|
||||
"accent",
|
||||
title,
|
||||
)}${domainPart}`;
|
||||
|
||||
// URL
|
||||
if (res.url) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { existsSync, type Stats, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { globSync } from "glob";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
@@ -41,7 +41,9 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
|
||||
return {
|
||||
name: "find",
|
||||
label: "find",
|
||||
description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
||||
description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${
|
||||
DEFAULT_MAX_BYTES / 1024
|
||||
}KB (whichever is hit first).`,
|
||||
parameters: findSchema,
|
||||
execute: async (
|
||||
_toolCallId: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFileSync, type Stats, statSync } from "node:fs";
|
||||
import nodePath from "node:path";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { Subprocess } from "bun";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
@@ -64,7 +64,9 @@ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
|
||||
return {
|
||||
name: "grep",
|
||||
label: "grep",
|
||||
description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
|
||||
description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${
|
||||
DEFAULT_MAX_BYTES / 1024
|
||||
}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
|
||||
parameters: grepSchema,
|
||||
execute: async (
|
||||
_toolCallId: string,
|
||||
|
||||
@@ -24,7 +24,7 @@ export {
|
||||
} from "./web-search/index.js";
|
||||
export { createWriteTool, writeTool } from "./write.js";
|
||||
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { askTool, createAskTool } from "./ask.js";
|
||||
import { astTool, createAstTool } from "./ast.js";
|
||||
import { bashTool, createBashTool } from "./bash.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { existsSync, readdirSync, statSync } from "fs";
|
||||
import nodePath from "path";
|
||||
@@ -21,7 +21,9 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
|
||||
return {
|
||||
name: "ls",
|
||||
label: "ls",
|
||||
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
||||
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${
|
||||
DEFAULT_MAX_BYTES / 1024
|
||||
}KB (whichever is hit first).`,
|
||||
parameters: lsSchema,
|
||||
execute: async (
|
||||
_toolCallId: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Theme } from "../../../modes/interactive/theme/theme.js";
|
||||
import { resolveToCwd } from "../path-utils.js";
|
||||
import { ensureFileOpen, getOrCreateClient, refreshFile, sendRequest } from "./client.js";
|
||||
@@ -429,7 +429,9 @@ Rust-analyzer specific (require rust-analyzer):
|
||||
output = `No symbols matching "${query}"`;
|
||||
} else {
|
||||
const lines = result.map((s) => formatSymbolInformation(s, cwd));
|
||||
output = `Found ${result.length} symbol(s) matching "${query}":\n${lines.map((l) => ` ${l}`).join("\n")}`;
|
||||
output = `Found ${result.length} symbol(s) matching "${query}":\n${lines
|
||||
.map((l) => ` ${l}`)
|
||||
.join("\n")}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -564,7 +566,9 @@ Rust-analyzer specific (require rust-analyzer):
|
||||
}
|
||||
return ` [${i}] ${actionItem.title}`;
|
||||
});
|
||||
output = `Available code actions:\n${lines.join("\n")}\n\nUse action_index parameter to apply a specific action.`;
|
||||
output = `Available code actions:\n${lines.join(
|
||||
"\n",
|
||||
)}\n\nUse action_index parameter to apply a specific action.`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* - Collapsible/expandable views
|
||||
*/
|
||||
|
||||
import type { AgentToolResult, RenderResultOptions } from "@mariozechner/pi-agent-core";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import type { AgentToolResult, RenderResultOptions } from "@oh-my-pi/pi-agent-core";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import { highlight, supportsLanguage } from "cli-highlight";
|
||||
import type { Theme } from "../../../modes/interactive/theme/theme.js";
|
||||
import type { LspParams, LspToolDetails } from "./types.js";
|
||||
@@ -273,7 +273,10 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
||||
const fileCont = isLastFile ? " " : `${TREE_PIPE} `;
|
||||
|
||||
if (locs.length === 1) {
|
||||
output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("accent", file)}:${theme.fg("muted", `${locs[0][0]}:${locs[0][1]}`)}`;
|
||||
output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("accent", file)}:${theme.fg(
|
||||
"muted",
|
||||
`${locs[0][0]}:${locs[0][1]}`,
|
||||
)}`;
|
||||
} else {
|
||||
output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("accent", file)}`;
|
||||
|
||||
@@ -372,7 +375,10 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
||||
const sym = symbols[i];
|
||||
const prefix = getPrefix(i);
|
||||
const branch = isLastSibling(i) ? TREE_END : TREE_MID;
|
||||
output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("accent", sym.name)} ${theme.fg("muted", `@${sym.line}`)}`;
|
||||
output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("accent", sym.name)} ${theme.fg(
|
||||
"muted",
|
||||
`@${sym.line}`,
|
||||
)}`;
|
||||
}
|
||||
return new Text(output, 0, 0);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { spawnSync } from "child_process";
|
||||
import { constants } from "fs";
|
||||
@@ -46,7 +46,9 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
||||
name: "read",
|
||||
label: "read",
|
||||
description: `Read the contents of a file. Supports:
|
||||
- Text files (truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB, use offset/limit for large files)
|
||||
- Text files (truncated to ${DEFAULT_MAX_LINES} lines or ${
|
||||
DEFAULT_MAX_BYTES / 1024
|
||||
}KB, use offset/limit for large files)
|
||||
- Images (jpg, png, gif, webp) - sent as attachments
|
||||
- Documents (pdf, docx, pptx, xlsx, epub, rtf) - converted to markdown via markitdown if available`,
|
||||
parameters: readSchema,
|
||||
@@ -113,7 +115,9 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
||||
let outputText = truncation.content;
|
||||
|
||||
if (truncation.truncated) {
|
||||
outputText += `\n\n[Document converted via markitdown. Output truncated to ${formatSize(DEFAULT_MAX_BYTES)}]`;
|
||||
outputText += `\n\n[Document converted via markitdown. Output truncated to ${formatSize(
|
||||
DEFAULT_MAX_BYTES,
|
||||
)}]`;
|
||||
details = { truncation };
|
||||
}
|
||||
|
||||
@@ -160,7 +164,9 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
||||
if (truncation.firstLineExceedsLimit) {
|
||||
// First line at offset exceeds 30KB - tell model to use bash
|
||||
const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8"));
|
||||
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
|
||||
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(
|
||||
DEFAULT_MAX_BYTES,
|
||||
)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
|
||||
details = { truncation };
|
||||
} else if (truncation.truncated) {
|
||||
// Truncation occurred - build actionable notice
|
||||
@@ -172,7 +178,9 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
||||
if (truncation.truncatedBy === "lines") {
|
||||
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
||||
} else {
|
||||
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;
|
||||
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(
|
||||
DEFAULT_MAX_BYTES,
|
||||
)} limit). Use offset=${nextOffset} to continue]`;
|
||||
}
|
||||
details = { truncation };
|
||||
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user