PI coding agent source (initial)
This commit is contained in:
commit
6359ee255f
5209
CHANGELOG.md
Normal file
5209
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
707
README.md
Normal file
707
README.md
Normal file
|
|
@ -0,0 +1,707 @@
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://pi.dev">
|
||||||
|
<img alt="pi logo" src="https://pi.dev/logo-auto.svg" width="128">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://discord.com/invite/3cU7Bz4UPx"><img alt="Discord" src="https://img.shields.io/badge/discord-community-5865F2?style=flat-square&logo=discord&logoColor=white" /></a>
|
||||||
|
<a href="https://www.npmjs.com/package/@earendil-works/pi-coding-agent"><img alt="npm" src="https://img.shields.io/npm/v/@earendil-works/pi-coding-agent?style=flat-square" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> New issues and PRs from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. See [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Pi is a minimal terminal coding harness. Adapt pi to your workflows, not the other way around, without having to fork and modify pi internals. Extend it with TypeScript [Extensions](#extensions), [Skills](#skills), [Prompt Templates](#prompt-templates), and [Themes](#themes). Put your extensions, skills, prompt templates, and themes in [Pi Packages](#pi-packages) and share them with others via npm or git.
|
||||||
|
|
||||||
|
Pi ships with powerful defaults but skips features like sub agents and plan mode. Instead, you can ask pi to build what you want or install a third party pi package that matches your workflow.
|
||||||
|
|
||||||
|
Pi runs in four modes: interactive, print or JSON, RPC for process integration, and an SDK for embedding in your own apps.
|
||||||
|
|
||||||
|
## Share your OSS coding agent sessions
|
||||||
|
|
||||||
|
If you use pi for open source work, please share your coding agent sessions.
|
||||||
|
|
||||||
|
Public OSS session data helps improve models, prompts, tools, and evaluations using real development workflows.
|
||||||
|
|
||||||
|
For the full explanation, see [this post on X](https://x.com/badlogicgames/status/2037811643774652911).
|
||||||
|
|
||||||
|
To publish sessions, use [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Read its README.md for setup instructions. All you need is a Hugging Face account, the Hugging Face CLI, and `pi-share-hf`.
|
||||||
|
|
||||||
|
You can also watch [this video](https://x.com/badlogicgames/status/2041151967695634619), where I show how I publish my `pi-mono` sessions.
|
||||||
|
|
||||||
|
I regularly publish my own `pi-mono` work sessions here:
|
||||||
|
|
||||||
|
- [badlogicgames/pi-mono on Hugging Face](https://huggingface.co/datasets/badlogicgames/pi-mono)
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Providers & Models](#providers--models)
|
||||||
|
- [Interactive Mode](#interactive-mode)
|
||||||
|
- [Editor](#editor)
|
||||||
|
- [Commands](#commands)
|
||||||
|
- [Keyboard Shortcuts](#keyboard-shortcuts)
|
||||||
|
- [Message Queue](#message-queue)
|
||||||
|
- [Sessions](#sessions)
|
||||||
|
- [Branching](#branching)
|
||||||
|
- [Compaction](#compaction)
|
||||||
|
- [Settings](#settings)
|
||||||
|
- [Context Files](#context-files)
|
||||||
|
- [Customization](#customization)
|
||||||
|
- [Prompt Templates](#prompt-templates)
|
||||||
|
- [Skills](#skills)
|
||||||
|
- [Extensions](#extensions)
|
||||||
|
- [Themes](#themes)
|
||||||
|
- [Pi Packages](#pi-packages)
|
||||||
|
- [Programmatic Usage](#programmatic-usage)
|
||||||
|
- [Philosophy](#philosophy)
|
||||||
|
- [CLI Reference](#cli-reference)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs.
|
||||||
|
|
||||||
|
Installer alternative:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://pi.dev/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Authenticate with an API key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
pi
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use your existing subscription:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi
|
||||||
|
/login # Then select provider
|
||||||
|
```
|
||||||
|
|
||||||
|
Then just talk to pi. By default, pi gives the model four tools: `read`, `write`, `edit`, and `bash`. The model uses these to fulfill your requests. Add capabilities via [skills](#skills), [prompt templates](#prompt-templates), [extensions](#extensions), or [pi packages](#pi-packages).
|
||||||
|
|
||||||
|
**Platform notes:** [Windows](docs/windows.md) | [Termux (Android)](docs/termux.md) | [tmux](docs/tmux.md) | [Terminal setup](docs/terminal-setup.md) | [Shell aliases](docs/shell-aliases.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Providers & Models
|
||||||
|
|
||||||
|
For each built-in provider, pi maintains a list of tool-capable models. Configured provider catalogs refresh automatically; run `pi update --models` to force an immediate refresh. Authenticate via subscription (`/login`) or API key, then select any model from that provider via `/model` (or Ctrl+L).
|
||||||
|
|
||||||
|
**Subscriptions:**
|
||||||
|
- Anthropic Claude Pro/Max
|
||||||
|
- OpenAI ChatGPT Plus/Pro (Codex)
|
||||||
|
- GitHub Copilot
|
||||||
|
|
||||||
|
**API keys:**
|
||||||
|
- Anthropic
|
||||||
|
- Ant Ling
|
||||||
|
- OpenAI
|
||||||
|
- Azure OpenAI
|
||||||
|
- DeepSeek
|
||||||
|
- NVIDIA NIM
|
||||||
|
- Google Gemini
|
||||||
|
- Google Vertex
|
||||||
|
- Amazon Bedrock
|
||||||
|
- Mistral
|
||||||
|
- Groq
|
||||||
|
- Cerebras
|
||||||
|
- Cloudflare AI Gateway
|
||||||
|
- Cloudflare Workers AI
|
||||||
|
- xAI
|
||||||
|
- OpenRouter
|
||||||
|
- Vercel AI Gateway
|
||||||
|
- ZAI Coding Plan (Global)
|
||||||
|
- ZAI Coding Plan (China)
|
||||||
|
- OpenCode Zen
|
||||||
|
- OpenCode Go
|
||||||
|
- Hugging Face
|
||||||
|
- Fireworks
|
||||||
|
- Together AI
|
||||||
|
- Kimi For Coding
|
||||||
|
- MiniMax
|
||||||
|
- Xiaomi MiMo
|
||||||
|
- Xiaomi MiMo Token Plan (China)
|
||||||
|
- Xiaomi MiMo Token Plan (Amsterdam)
|
||||||
|
- Xiaomi MiMo Token Plan (Singapore)
|
||||||
|
|
||||||
|
Pi also supports the llama.cpp router server. Configure it with `/login llama.cpp`, manage downloads and loaded models with `/llama`, then select a loaded model with `/model`. See [docs/llama-cpp.md](docs/llama-cpp.md) for setup and usage.
|
||||||
|
|
||||||
|
See [docs/providers.md](docs/providers.md) for other provider setup instructions.
|
||||||
|
|
||||||
|
**Custom providers & models:** Add providers via `~/.pi/agent/models.json` if they speak a supported API (OpenAI, Anthropic, Google). For custom APIs or OAuth, use extensions. See [docs/models.md](docs/models.md) and [docs/custom-provider.md](docs/custom-provider.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interactive Mode
|
||||||
|
|
||||||
|
<p align="center"><img src="docs/images/interactive-mode.png" alt="Interactive Mode" width="600"></p>
|
||||||
|
|
||||||
|
The interface from top to bottom:
|
||||||
|
|
||||||
|
- **Startup header** - Shows shortcuts (`/hotkeys` for all), loaded AGENTS.md files, prompt templates, skills, and extensions
|
||||||
|
- **Messages** - Your messages, assistant responses, tool calls and results, notifications, errors, and extension UI
|
||||||
|
- **Editor** - Where you type; border color indicates thinking level
|
||||||
|
- **Footer** - Working directory, session name, total token/cache usage (`↑` input, `↓` output, `R` cache read, `W` cache write, `CH` latest cache hit rate), cost, context usage, current model. Totals include assistant responses, usage reported by tools, and summary generation.
|
||||||
|
|
||||||
|
The editor can be temporarily replaced by other UI, like built-in `/settings` or custom UI from extensions (e.g., a Q&A tool that lets the user answer model questions in a structured format). [Extensions](#extensions) can also replace the editor, add widgets above/below it, a status line, custom footer, or overlays.
|
||||||
|
|
||||||
|
### Editor
|
||||||
|
|
||||||
|
| Feature | How |
|
||||||
|
|---------|-----|
|
||||||
|
| File reference | Type `@` to fuzzy-search project files |
|
||||||
|
| Path completion | Tab to complete paths |
|
||||||
|
| Multi-line | Shift+Enter (or Ctrl+Enter on Windows Terminal) |
|
||||||
|
| External editor | Ctrl+G opens `externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere |
|
||||||
|
| Clipboard | Ctrl+V to paste an image or text (Alt+V on Windows), or drag images onto terminal |
|
||||||
|
| Bash commands | `!command` runs and sends output to LLM, `!!command` runs without sending |
|
||||||
|
|
||||||
|
Standard editing keybindings for delete word, undo, etc. See [docs/keybindings.md](docs/keybindings.md).
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
Type `/` in the editor to trigger commands. [Extensions](#extensions) can register custom commands, [skills](#skills) are available as `/skill:name`, and [prompt templates](#prompt-templates) expand via `/templatename`.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/login`, `/logout` | Manage provider credentials |
|
||||||
|
| [`/llama`](docs/llama-cpp.md) | Download, load, and unload llama.cpp router models |
|
||||||
|
| `/model` | Switch models |
|
||||||
|
| `/scoped-models` | Enable/disable models for Ctrl+P cycling |
|
||||||
|
| `/settings` | Thinking level, theme, message delivery, transport |
|
||||||
|
| `/resume` | Pick from previous sessions |
|
||||||
|
| `/new` | Start a new session |
|
||||||
|
| `/name <name>` | Set session display name |
|
||||||
|
| `/session` | Show session info (file, ID, messages, tokens, cost) |
|
||||||
|
| `/tree` | Jump to any point in the session and continue from there |
|
||||||
|
| `/trust` | Save project trust decision for future sessions (restart required) |
|
||||||
|
| `/fork` | Create a new session from a previous user message |
|
||||||
|
| `/clone` | Duplicate the current active branch into a new session |
|
||||||
|
| `/compact [prompt]` | Manually compact context, optional custom instructions |
|
||||||
|
| `/copy` | Copy last assistant message to clipboard |
|
||||||
|
| `/export [file]` | Export session to HTML or JSONL file |
|
||||||
|
| `/import <file>` | Import and resume a session from a JSONL file |
|
||||||
|
| `/share` | Upload as private GitHub gist with shareable HTML link |
|
||||||
|
| `/reload` | Reload keybindings, extensions, skills, prompts, themes, and context files |
|
||||||
|
| `/hotkeys` | Show all keyboard shortcuts |
|
||||||
|
| `/changelog` | Display version history |
|
||||||
|
| `/quit` | Quit pi |
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
See `/hotkeys` for the full list. Customize via `~/.pi/agent/keybindings.json`. See [docs/keybindings.md](docs/keybindings.md).
|
||||||
|
|
||||||
|
**Commonly used:**
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| Ctrl+C | Clear editor |
|
||||||
|
| Ctrl+C twice | Quit |
|
||||||
|
| Escape | Cancel/abort |
|
||||||
|
| Escape twice | Open `/tree` |
|
||||||
|
| Ctrl+L | Open model selector |
|
||||||
|
| Ctrl+P / Shift+Ctrl+P | Cycle scoped models forward/backward |
|
||||||
|
| Shift+Tab | Cycle thinking level |
|
||||||
|
| Ctrl+O | Collapse/expand tool output |
|
||||||
|
| Ctrl+T | Collapse/expand thinking blocks |
|
||||||
|
| Ctrl+X | Copy the last assistant message |
|
||||||
|
|
||||||
|
### Message Queue
|
||||||
|
|
||||||
|
Submit messages while the agent is working:
|
||||||
|
|
||||||
|
- **Enter** queues a *steering* message, delivered after the current assistant turn finishes executing its tool calls
|
||||||
|
- **Alt+Enter** queues a *follow-up* message, delivered only after the agent finishes all work
|
||||||
|
- **Escape** aborts and restores queued messages to editor
|
||||||
|
- **Alt+Up** retrieves queued messages back to editor
|
||||||
|
|
||||||
|
On Windows Terminal, `Alt+Enter` is fullscreen by default. Remap it in [docs/terminal-setup.md](docs/terminal-setup.md) so pi can receive the follow-up shortcut.
|
||||||
|
|
||||||
|
Configure delivery in [settings](docs/settings.md): `steeringMode` and `followUpMode` can be `"one-at-a-time"` (default, waits for response) or `"all"` (delivers all queued at once). `transport` selects provider transport preference (`"sse"`, `"websocket"`, or `"auto"`) for providers that support multiple transports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sessions
|
||||||
|
|
||||||
|
Sessions are stored as JSONL files with a tree structure. Each entry has an `id` and `parentId`, enabling in-place branching without creating new files. See [docs/session-format.md](docs/session-format.md) for file format.
|
||||||
|
|
||||||
|
### Management
|
||||||
|
|
||||||
|
Sessions auto-save to `~/.pi/agent/sessions/` organized by working directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi -c # Continue most recent session
|
||||||
|
pi -r # Browse and select from past sessions
|
||||||
|
pi --no-session # Ephemeral mode (don't save)
|
||||||
|
pi --name "my task" # Set session display name at startup
|
||||||
|
pi --session <path|id> # Use specific session file or ID
|
||||||
|
pi --fork <path|id> # Fork specific session file or ID into a new session
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `/session` in interactive mode to see the current session ID before reusing it with `--session <id>` or `--fork <id>`.
|
||||||
|
|
||||||
|
### Branching
|
||||||
|
|
||||||
|
**`/tree`** - Navigate the session tree in-place. Select any previous point, continue from there, and switch between branches. All history preserved in a single file.
|
||||||
|
|
||||||
|
<p align="center"><img src="docs/images/tree-view.png" alt="Tree View" width="600"></p>
|
||||||
|
|
||||||
|
- Search by typing, fold/unfold and jump between branches with Ctrl+←/Ctrl+→ or Alt+←/Alt+→, page with ←/→
|
||||||
|
- Filter modes (Ctrl+O): default → no-tools → user-only → labeled-only → all
|
||||||
|
- Press Ctrl+X to copy the selected message
|
||||||
|
- Press Shift+L to label entries as bookmarks and Shift+T to toggle label timestamps
|
||||||
|
|
||||||
|
**`/fork`** - Create a new session file from a previous user message on the active branch. Opens a selector, copies the active path up to that point, and places the selected prompt in the editor for modification.
|
||||||
|
|
||||||
|
**`/clone`** - Duplicate the current active branch into a new session file at the current position. The new session keeps the full active-path history and opens with an empty editor.
|
||||||
|
|
||||||
|
**`--fork <path|id>`** - Fork an existing session file or partial session UUID directly from the CLI. This copies the full source session into a new session file in the current project.
|
||||||
|
|
||||||
|
### Compaction
|
||||||
|
|
||||||
|
Long sessions can exhaust context windows. Compaction summarizes older messages while keeping recent ones.
|
||||||
|
|
||||||
|
**Manual:** `/compact` or `/compact <custom instructions>`
|
||||||
|
|
||||||
|
**Automatic:** Enabled by default. Triggers on context overflow (recovers and retries) or when approaching the limit (proactive). Configure via `/settings` or `settings.json`.
|
||||||
|
|
||||||
|
Compaction is lossy. The full history remains in the JSONL file; use `/tree` to revisit. Customize compaction behavior via [extensions](#extensions). See [docs/compaction.md](docs/compaction.md) for internals.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Use `/settings` to modify common options, or edit JSON files directly:
|
||||||
|
|
||||||
|
| Location | Scope |
|
||||||
|
|----------|-------|
|
||||||
|
| `~/.pi/agent/settings.json` | Global (all projects) |
|
||||||
|
| `.pi/settings.json` | Project (overrides global) |
|
||||||
|
|
||||||
|
See [docs/settings.md](docs/settings.md) for all options.
|
||||||
|
|
||||||
|
### Project Trust
|
||||||
|
|
||||||
|
On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions.
|
||||||
|
|
||||||
|
Before the trust decision, pi loads only context files, user/global extensions, and CLI `-e` extensions so they can handle the `project_trust` event. Project-local extensions, project package-managed extensions, and project settings are loaded only after the project is trusted. This split also applies when switching to a session from a different cwd whose trust has not been resolved in the current process.
|
||||||
|
|
||||||
|
Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run.
|
||||||
|
|
||||||
|
If no extension or saved decision applies, `defaultProjectTrust` controls the fallback behavior. Set it to `"ask"`, `"always"`, or `"never"` in `~/.pi/agent/settings.json`, or change it with `/settings`.
|
||||||
|
|
||||||
|
`pi config` and package commands use the same project trust flow, except `pi update` never prompts. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them.
|
||||||
|
|
||||||
|
Use `/trust` in interactive mode to save a project trust decision for future sessions, including trust for the immediate parent folder. It writes `~/.pi/agent/trust.json` only; the current session is not reloaded, so restart pi for changes to take effect.
|
||||||
|
|
||||||
|
### Telemetry and update checks
|
||||||
|
|
||||||
|
Pi has two separate startup features:
|
||||||
|
|
||||||
|
- **Update check:** fetches `https://pi.dev/api/latest-version` to check whether a newer Pi version exists. Disable it with `PI_SKIP_VERSION_CHECK=1`. Disabling update checks only turns off this check.
|
||||||
|
- **Install/update telemetry:** after first install or a changelog-detected update, sends an anonymous version ping to `https://pi.dev/api/report-install`. This setting also controls optional provider attribution headers for OpenRouter, Cloudflare, and direct NVIDIA NIM requests. Opt out by setting `enableInstallTelemetry` to `false` in `settings.json`, or by setting `PI_TELEMETRY=0`. This does not disable update checks; Pi may still contact `pi.dev` for the latest version unless update checks are disabled or offline mode is enabled.
|
||||||
|
|
||||||
|
Use `--offline` or `PI_OFFLINE=1` to disable all startup network operations described here, including update checks, package update checks, and install/update telemetry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context Files
|
||||||
|
|
||||||
|
Pi loads `AGENTS.md` (or `CLAUDE.md`) at startup from:
|
||||||
|
- `~/.pi/agent/AGENTS.md` (global)
|
||||||
|
- Parent directories (walking up from cwd)
|
||||||
|
- Current directory
|
||||||
|
|
||||||
|
Use for project instructions (`AGENTS.md`/`CLAUDE.md`), conventions, common commands. All matching files are concatenated.
|
||||||
|
|
||||||
|
Disable context file loading with `--no-context-files` (or `-nc`).
|
||||||
|
|
||||||
|
### System Prompt
|
||||||
|
|
||||||
|
Replace the default system prompt with `.pi/SYSTEM.md` (project) or `~/.pi/agent/SYSTEM.md` (global). Append without replacing via `APPEND_SYSTEM.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Prompt Templates
|
||||||
|
|
||||||
|
Reusable prompts as Markdown files. Type `/name` to expand.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<!-- ~/.pi/agent/prompts/review.md -->
|
||||||
|
Review this code for bugs, security issues, and performance problems.
|
||||||
|
Focus on: {{focus}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Place in `~/.pi/agent/prompts/`, `.pi/prompts/`, or a [pi package](#pi-packages) to share with others. See [docs/prompt-templates.md](docs/prompt-templates.md).
|
||||||
|
|
||||||
|
### Skills
|
||||||
|
|
||||||
|
On-demand capability packages following the [Agent Skills standard](https://agentskills.io). Invoke via `/skill:name` or let the agent load them automatically.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<!-- ~/.pi/agent/skills/my-skill/SKILL.md -->
|
||||||
|
# My Skill
|
||||||
|
Use this skill when the user asks about X.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Do this
|
||||||
|
2. Then that
|
||||||
|
```
|
||||||
|
|
||||||
|
Place in `~/.pi/agent/skills/`, `~/.agents/skills/`, `.pi/skills/`, or `.agents/skills/` (from `cwd` up through parent directories) or a [pi package](#pi-packages) to share with others. See [docs/skills.md](docs/skills.md).
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
|
||||||
|
<p align="center"><img src="docs/images/doom-extension.png" alt="Doom Extension" width="600"></p>
|
||||||
|
|
||||||
|
TypeScript modules that extend pi with custom tools, commands, keyboard shortcuts, event handlers, and UI components.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
pi.registerTool({ name: "deploy", ... });
|
||||||
|
pi.registerCommand("stats", { ... });
|
||||||
|
pi.on("tool_call", async (event, ctx) => { ... });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The default export can also be `async`. pi waits for async extension factories before startup continues, which is useful for one-time initialization such as fetching remote model lists before calling `pi.registerProvider()`.
|
||||||
|
|
||||||
|
**What's possible:**
|
||||||
|
- Custom tools (or replace built-in tools entirely)
|
||||||
|
- Sub-agents and plan mode
|
||||||
|
- Custom compaction and summarization
|
||||||
|
- Permission gates and path protection
|
||||||
|
- Custom editors and UI components
|
||||||
|
- Status lines, headers, footers
|
||||||
|
- Git checkpointing and auto-commit
|
||||||
|
- SSH and sandbox execution
|
||||||
|
- MCP server integration
|
||||||
|
- Make pi look like Claude Code
|
||||||
|
- Games while waiting (yes, Doom runs)
|
||||||
|
- ...anything you can dream up
|
||||||
|
|
||||||
|
Place in `~/.pi/agent/extensions/`, `.pi/extensions/`, or a [pi package](#pi-packages) to share with others. See [docs/extensions.md](docs/extensions.md) and [examples/extensions/](examples/extensions/).
|
||||||
|
|
||||||
|
### Themes
|
||||||
|
|
||||||
|
Built-in: `dark`, `light`. Themes hot-reload: modify the active theme file and pi immediately applies changes.
|
||||||
|
|
||||||
|
Place in `~/.pi/agent/themes/`, `.pi/themes/`, or a [pi package](#pi-packages) to share with others. See [docs/themes.md](docs/themes.md).
|
||||||
|
|
||||||
|
### Pi Packages
|
||||||
|
|
||||||
|
Bundle and share extensions, skills, prompts, and themes via npm or git. Find packages on [npmjs.com](https://www.npmjs.com/search?q=keywords%3Api-package) or [Discord](https://discord.com/channels/1456806362351669492/1457744485428629628).
|
||||||
|
|
||||||
|
> **Security:** Pi packages run with full system access. Extensions execute arbitrary code, and skills can instruct the model to perform any action including running executables. Review source code before installing third-party packages.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi install npm:@foo/pi-tools
|
||||||
|
pi install npm:@foo/pi-tools@1.2.3 # pinned version
|
||||||
|
pi install git:github.com/user/repo
|
||||||
|
pi install git:github.com/user/repo@v1 # tag or commit
|
||||||
|
pi install git:git@github.com:user/repo
|
||||||
|
pi install git:git@github.com:user/repo@v1 # tag or commit
|
||||||
|
pi install https://github.com/user/repo
|
||||||
|
pi install https://github.com/user/repo@v1 # tag or commit
|
||||||
|
pi install ssh://git@github.com/user/repo
|
||||||
|
pi install ssh://git@github.com/user/repo@v1 # tag or commit
|
||||||
|
pi remove npm:@foo/pi-tools
|
||||||
|
pi uninstall npm:@foo/pi-tools # alias for remove
|
||||||
|
pi list
|
||||||
|
pi update # update pi only
|
||||||
|
pi update --all # update pi and packages
|
||||||
|
pi update --extensions # update packages only
|
||||||
|
pi update --models # refresh model catalogs only
|
||||||
|
pi update --self # update pi only
|
||||||
|
pi update --self --force # reinstall pi even if current
|
||||||
|
pi update npm:@foo/pi-tools # update one package
|
||||||
|
pi config # enable/disable extensions, skills, prompts, themes
|
||||||
|
```
|
||||||
|
|
||||||
|
Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git `@ref` values are pinned tags or commits; pinned packages are skipped by `pi update --extensions` and `pi update --all`, so use `pi install git:host/user/repo@new-ref` to move an existing package to a new ref. Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`.
|
||||||
|
|
||||||
|
Create a package by adding a `pi` key to `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-pi-package",
|
||||||
|
"keywords": ["pi-package"],
|
||||||
|
"pi": {
|
||||||
|
"extensions": ["./extensions"],
|
||||||
|
"skills": ["./skills"],
|
||||||
|
"prompts": ["./prompts"],
|
||||||
|
"themes": ["./themes"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Without a `pi` manifest, pi auto-discovers from conventional directories (`extensions/`, `skills/`, `prompts/`, `themes/`).
|
||||||
|
|
||||||
|
See [docs/packages.md](docs/packages.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Programmatic Usage
|
||||||
|
|
||||||
|
### SDK
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
|
||||||
|
|
||||||
|
const modelRuntime = await ModelRuntime.create();
|
||||||
|
const { session } = await createAgentSession({
|
||||||
|
sessionManager: SessionManager.inMemory(),
|
||||||
|
modelRuntime,
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.prompt("What files are in the current directory?");
|
||||||
|
```
|
||||||
|
|
||||||
|
For advanced multi-session runtime replacement, use `createAgentSessionRuntime()` and `AgentSessionRuntime`.
|
||||||
|
|
||||||
|
See [docs/sdk.md](docs/sdk.md) and [examples/sdk/](examples/sdk/).
|
||||||
|
|
||||||
|
### RPC Mode
|
||||||
|
|
||||||
|
For non-Node.js integrations, use RPC mode over stdin/stdout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi --mode rpc
|
||||||
|
```
|
||||||
|
|
||||||
|
RPC mode uses strict LF-delimited JSONL framing. Clients must split records on `\n` only. Do not use generic line readers like Node `readline`, which also split on Unicode separators inside JSON payloads.
|
||||||
|
|
||||||
|
See [docs/rpc.md](docs/rpc.md) for the protocol.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
Pi is aggressively extensible so it doesn't have to dictate your workflow. Features that other tools bake in can be built with [extensions](#extensions), [skills](#skills), or installed from third-party [pi packages](#pi-packages). This keeps the core minimal while letting you shape pi to fit how you work.
|
||||||
|
|
||||||
|
**No MCP.** Build CLI tools with READMEs (see [Skills](#skills)), or build an extension that adds MCP support. [Why?](https://mariozechner.at/posts/2025-11-02-what-if-you-dont-need-mcp/)
|
||||||
|
|
||||||
|
**No sub-agents.** There's many ways to do this. Spawn pi instances via tmux, or build your own with [extensions](#extensions), or install a package that does it your way.
|
||||||
|
|
||||||
|
**No permission popups.** Run in a container, or build your own confirmation flow with [extensions](#extensions) inline with your environment and security requirements.
|
||||||
|
|
||||||
|
**No plan mode.** Write plans to files, or build it with [extensions](#extensions), or install a package.
|
||||||
|
|
||||||
|
**No built-in to-dos.** They confuse models. Use a TODO.md file, or build your own with [extensions](#extensions).
|
||||||
|
|
||||||
|
**No background bash.** Use tmux. Full observability, direct interaction.
|
||||||
|
|
||||||
|
Read the [blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/) for the full rationale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi [options] [@files...] [messages...]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi install <source> [-l] # Install package, -l for project-local
|
||||||
|
pi remove <source> [-l] # Remove package
|
||||||
|
pi uninstall <source> [-l] # Alias for remove
|
||||||
|
pi update [source|self|pi] # Update pi only, or one package source
|
||||||
|
pi update --all # Update pi and packages
|
||||||
|
pi update --extensions # Update packages only
|
||||||
|
pi update --models # Refresh model catalogs only
|
||||||
|
pi update --self # Update pi only
|
||||||
|
pi update --self --force # Reinstall pi even if current
|
||||||
|
pi update --extension <src> # Update one package
|
||||||
|
pi list # List installed packages
|
||||||
|
pi config # Enable/disable package resources
|
||||||
|
```
|
||||||
|
|
||||||
|
`pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust.
|
||||||
|
|
||||||
|
### Modes
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| (default) | Interactive mode |
|
||||||
|
| `-p`, `--print` | Print response and exit |
|
||||||
|
| `--mode json` | Output all events as JSON lines (see [docs/json.md](docs/json.md)) |
|
||||||
|
| `--mode rpc` | RPC mode for process integration (see [docs/rpc.md](docs/rpc.md)) |
|
||||||
|
| `--export <in> [out]` | Export session to HTML |
|
||||||
|
|
||||||
|
In print mode, pi also reads piped stdin and merges it into the initial prompt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat README.md | pi -p "Summarize this text"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--provider <name>` | Provider (anthropic, openai, google, etc.) |
|
||||||
|
| `--model <pattern>` | Model pattern or ID (supports `provider/id` and optional `:<thinking>`) |
|
||||||
|
| `--api-key <key>` | API key (overrides env vars) |
|
||||||
|
| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |
|
||||||
|
| `--models <patterns>` | Comma-separated patterns for Ctrl+P cycling |
|
||||||
|
| `--list-models [search]` | List available models |
|
||||||
|
|
||||||
|
### Session Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `-c`, `--continue` | Continue most recent session |
|
||||||
|
| `-r`, `--resume` | Browse and select session |
|
||||||
|
| `--session <path\|id>` | Use specific session file or partial UUID |
|
||||||
|
| `--fork <path\|id>` | Fork specific session file or partial UUID into a new session |
|
||||||
|
| `--session-dir <dir>` | Custom session storage directory |
|
||||||
|
| `--no-session` | Ephemeral mode (don't save) |
|
||||||
|
| `--name <name>`, `-n <name>` | Set session display name at startup |
|
||||||
|
|
||||||
|
### Tool Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--tools <list>`, `-t <list>` | Allowlist specific tool names across built-in, extension, and custom tools |
|
||||||
|
| `--exclude-tools <list>`, `-xt <list>` | Disable specific tool names across built-in, extension, and custom tools |
|
||||||
|
| `--no-builtin-tools`, `-nbt` | Disable built-in tools by default but keep extension/custom tools enabled |
|
||||||
|
| `--no-tools`, `-nt` | Disable all tools by default |
|
||||||
|
|
||||||
|
Available built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`
|
||||||
|
|
||||||
|
### Resource Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `-e`, `--extension <source>` | Load extension from path, npm, or git (repeatable) |
|
||||||
|
| `--no-extensions` | Disable extension discovery |
|
||||||
|
| `--skill <path>` | Load skill (repeatable) |
|
||||||
|
| `--no-skills` | Disable skill discovery |
|
||||||
|
| `--prompt-template <path>` | Load prompt template (repeatable) |
|
||||||
|
| `--no-prompt-templates` | Disable prompt template discovery |
|
||||||
|
| `--theme <path>` | Load theme (repeatable) |
|
||||||
|
| `--no-themes` | Disable theme discovery |
|
||||||
|
| `--no-context-files`, `-nc` | Disable AGENTS.md and CLAUDE.md context file discovery |
|
||||||
|
|
||||||
|
Combine `--no-*` with explicit flags to load exactly what you need, ignoring settings.json (e.g., `--no-extensions -e ./my-ext.ts`).
|
||||||
|
|
||||||
|
### Other Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--system-prompt <text>` | Replace default prompt (context files and skills still appended) |
|
||||||
|
| `--append-system-prompt <text>` | Append to system prompt |
|
||||||
|
| `--verbose` | Force verbose startup |
|
||||||
|
| `-a`, `--approve` | Trust project-local files for this run |
|
||||||
|
| `-na`, `--no-approve` | Ignore project-local files for this run |
|
||||||
|
| `-h`, `--help` | Show help |
|
||||||
|
| `-v`, `--version` | Show version |
|
||||||
|
|
||||||
|
### File Arguments
|
||||||
|
|
||||||
|
Prefix files with `@` to include in the message:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi @prompt.md "Answer this"
|
||||||
|
pi -p @screenshot.png "What's in this image?"
|
||||||
|
pi @code.ts @test.ts "Review these files"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive with initial prompt
|
||||||
|
pi "List all .ts files in src/"
|
||||||
|
|
||||||
|
# Non-interactive
|
||||||
|
pi -p "Summarize this codebase"
|
||||||
|
|
||||||
|
# Non-interactive with piped stdin
|
||||||
|
cat README.md | pi -p "Summarize this text"
|
||||||
|
|
||||||
|
# Named one-shot session
|
||||||
|
pi --name "release audit" -p "Audit this repository"
|
||||||
|
|
||||||
|
# Different model
|
||||||
|
pi --provider openai --model gpt-4o "Help me refactor"
|
||||||
|
|
||||||
|
# Model with provider prefix (no --provider needed)
|
||||||
|
pi --model openai/gpt-4o "Help me refactor"
|
||||||
|
|
||||||
|
# Model with thinking level shorthand
|
||||||
|
pi --model sonnet:high "Solve this complex problem"
|
||||||
|
|
||||||
|
# Limit model cycling
|
||||||
|
pi --models "claude-*,gpt-4o"
|
||||||
|
|
||||||
|
# Read-only mode
|
||||||
|
pi --tools read,grep,find,ls -p "Review the code"
|
||||||
|
|
||||||
|
# Disable one extension or built-in tool while keeping the rest available
|
||||||
|
pi --exclude-tools ask_question
|
||||||
|
|
||||||
|
# High thinking level
|
||||||
|
pi --thinking high "Solve this complex problem"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `PI_CODING_AGENT` | Set to `true` by the CLI and RPC entry points so child processes can detect that they run inside Pi |
|
||||||
|
| `PI_CODING_AGENT_DIR` | Override config directory (default: `~/.pi/agent`) |
|
||||||
|
| `PI_CODING_AGENT_SESSION_DIR` | Override session storage directory (overridden by `--session-dir`) |
|
||||||
|
| `PI_PACKAGE_DIR` | Override package directory (useful for Nix/Guix where store paths tokenize poorly) |
|
||||||
|
| `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry |
|
||||||
|
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
||||||
|
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers. Use `1`/`true`/`yes` to enable or `0`/`false`/`no` to disable. This does not disable update checks |
|
||||||
|
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
|
||||||
|
| `VISUAL`, `EDITOR` | Fallback external editor for Ctrl+G when `externalEditor` is unset; defaults to Notepad on Windows and `nano` elsewhere |
|
||||||
|
|
||||||
|
Commands run by the LLM-callable bash tool also receive current session metadata:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `PI_SESSION_ID` | Current session ID |
|
||||||
|
| `PI_SESSION_FILE` | Absolute session JSONL path; unset for ephemeral sessions |
|
||||||
|
| `PI_PROVIDER` | Currently selected model provider |
|
||||||
|
| `PI_MODEL` | Currently selected model ID |
|
||||||
|
| `PI_REASONING_LEVEL` | Current effective reasoning level |
|
||||||
|
|
||||||
|
These values are resolved when each command starts. See [Environment Variables](docs/environment-variables.md#bash-tool-session-environment) for semantics, examples, and custom-tool opt-out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing & Development
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines and [docs/development.md](docs/development.md) for setup, forking, and debugging.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [@earendil-works/pi-ai](https://www.npmjs.com/package/@earendil-works/pi-ai): Core LLM toolkit
|
||||||
|
- [@earendil-works/pi-agent-core](https://www.npmjs.com/package/@earendil-works/pi-agent-core): Agent framework
|
||||||
|
- [@earendil-works/pi-tui](https://www.npmjs.com/package/@earendil-works/pi-tui): Terminal UI components
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://pi.dev">pi.dev</a> domain graciously donated by
|
||||||
|
<br /><br />
|
||||||
|
<a href="https://exe.dev"><img src="docs/images/exy.png" alt="Exy mascot" width="48" /><br />exe.dev</a>
|
||||||
|
</p>
|
||||||
3
dist/bun/cli.d.ts
vendored
Normal file
3
dist/bun/cli.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=cli.d.ts.map
|
||||||
1
dist/bun/cli.d.ts.map
vendored
Normal file
1
dist/bun/cli.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/bun/cli.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\nimport { registerBunOAuthFlows } from \"@earendil-works/pi-ai/bun-oauth\";\nimport { APP_NAME } from \"../config.ts\";\n\nprocess.title = APP_NAME;\nprocess.emitWarning = (() => {}) as typeof process.emitWarning;\n\nregisterBunOAuthFlows();\n\nimport { restoreSandboxEnv } from \"./restore-sandbox-env.ts\";\n\nrestoreSandboxEnv();\n\nawait import(\"./register-bedrock.ts\");\nawait import(\"../cli.ts\");\n"]}
|
||||||
11
dist/bun/cli.js
vendored
Normal file
11
dist/bun/cli.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import { registerBunOAuthFlows } from "@earendil-works/pi-ai/bun-oauth";
|
||||||
|
import { APP_NAME } from "../config.js";
|
||||||
|
process.title = APP_NAME;
|
||||||
|
process.emitWarning = (() => { });
|
||||||
|
registerBunOAuthFlows();
|
||||||
|
import { restoreSandboxEnv } from "./restore-sandbox-env.js";
|
||||||
|
restoreSandboxEnv();
|
||||||
|
await import("./register-bedrock.js");
|
||||||
|
await import("../cli.js");
|
||||||
|
//# sourceMappingURL=cli.js.map
|
||||||
1
dist/bun/cli.js.map
vendored
Normal file
1
dist/bun/cli.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/bun/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC;AACzB,OAAO,CAAC,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAA+B,CAAC;AAE/D,qBAAqB,EAAE,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,iBAAiB,EAAE,CAAC;AAEpB,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC;AACtC,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { registerBunOAuthFlows } from \"@earendil-works/pi-ai/bun-oauth\";\nimport { APP_NAME } from \"../config.ts\";\n\nprocess.title = APP_NAME;\nprocess.emitWarning = (() => {}) as typeof process.emitWarning;\n\nregisterBunOAuthFlows();\n\nimport { restoreSandboxEnv } from \"./restore-sandbox-env.ts\";\n\nrestoreSandboxEnv();\n\nawait import(\"./register-bedrock.ts\");\nawait import(\"../cli.ts\");\n"]}
|
||||||
2
dist/bun/register-bedrock.d.ts
vendored
Normal file
2
dist/bun/register-bedrock.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=register-bedrock.d.ts.map
|
||||||
1
dist/bun/register-bedrock.d.ts.map
vendored
Normal file
1
dist/bun/register-bedrock.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"register-bedrock.d.ts","sourceRoot":"","sources":["../../src/bun/register-bedrock.ts"],"names":[],"mappings":"","sourcesContent":["import { bedrockProviderModule } from \"@earendil-works/pi-ai/bedrock-provider\";\nimport { setBedrockProviderModule } from \"@earendil-works/pi-ai/compat\";\n\nsetBedrockProviderModule(bedrockProviderModule);\n"]}
|
||||||
4
dist/bun/register-bedrock.js
vendored
Normal file
4
dist/bun/register-bedrock.js
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { bedrockProviderModule } from "@earendil-works/pi-ai/bedrock-provider";
|
||||||
|
import { setBedrockProviderModule } from "@earendil-works/pi-ai/compat";
|
||||||
|
setBedrockProviderModule(bedrockProviderModule);
|
||||||
|
//# sourceMappingURL=register-bedrock.js.map
|
||||||
1
dist/bun/register-bedrock.js.map
vendored
Normal file
1
dist/bun/register-bedrock.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"register-bedrock.js","sourceRoot":"","sources":["../../src/bun/register-bedrock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAC/E,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAExE,wBAAwB,CAAC,qBAAqB,CAAC,CAAC","sourcesContent":["import { bedrockProviderModule } from \"@earendil-works/pi-ai/bedrock-provider\";\nimport { setBedrockProviderModule } from \"@earendil-works/pi-ai/compat\";\n\nsetBedrockProviderModule(bedrockProviderModule);\n"]}
|
||||||
17
dist/bun/restore-sandbox-env.d.ts
vendored
Normal file
17
dist/bun/restore-sandbox-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Workaround for https://github.com/oven-sh/bun/issues/27802
|
||||||
|
*
|
||||||
|
* Bun compiled binaries have an empty `process.env` when running inside
|
||||||
|
* sandbox environments (e.g. nono on Linux/macOS). On Linux we can recover
|
||||||
|
* the environment from `/proc/self/environ`.
|
||||||
|
*
|
||||||
|
* Keep this in sync with getBunSandboxEnvValue() in
|
||||||
|
* packages/ai/src/utils/provider-env.ts. The ai package duplicates the lookup
|
||||||
|
* for direct consumers that do not go through this coding-agent entrypoint.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Restore environment variables from `/proc/self/environ` when running
|
||||||
|
* inside a sandbox where Bun's `process.env` is empty.
|
||||||
|
*/
|
||||||
|
export declare function restoreSandboxEnv(): void;
|
||||||
|
//# sourceMappingURL=restore-sandbox-env.d.ts.map
|
||||||
1
dist/bun/restore-sandbox-env.d.ts.map
vendored
Normal file
1
dist/bun/restore-sandbox-env.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"restore-sandbox-env.d.ts","sourceRoot":"","sources":["../../src/bun/restore-sandbox-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAiBxC","sourcesContent":["/**\n * Workaround for https://github.com/oven-sh/bun/issues/27802\n *\n * Bun compiled binaries have an empty `process.env` when running inside\n * sandbox environments (e.g. nono on Linux/macOS). On Linux we can recover\n * the environment from `/proc/self/environ`.\n *\n * Keep this in sync with getBunSandboxEnvValue() in\n * packages/ai/src/utils/provider-env.ts. The ai package duplicates the lookup\n * for direct consumers that do not go through this coding-agent entrypoint.\n */\n\nimport { readFileSync } from \"node:fs\";\n\n/**\n * Restore environment variables from `/proc/self/environ` when running\n * inside a sandbox where Bun's `process.env` is empty.\n */\nexport function restoreSandboxEnv(): void {\n\tif (!process.versions?.bun) return;\n\n\t// If process.env already has entries, nothing to fix.\n\tif (Object.keys(process.env).length > 0) return;\n\n\ttry {\n\t\tconst data = readFileSync(\"/proc/self/environ\", \"utf-8\");\n\t\tfor (const entry of data.split(\"\\0\")) {\n\t\t\tconst idx = entry.indexOf(\"=\");\n\t\t\tif (idx > 0) {\n\t\t\t\tprocess.env[entry.slice(0, idx)] = entry.slice(idx + 1);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// /proc/self/environ may not be readable; ignore.\n\t}\n}\n"]}
|
||||||
36
dist/bun/restore-sandbox-env.js
vendored
Normal file
36
dist/bun/restore-sandbox-env.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Workaround for https://github.com/oven-sh/bun/issues/27802
|
||||||
|
*
|
||||||
|
* Bun compiled binaries have an empty `process.env` when running inside
|
||||||
|
* sandbox environments (e.g. nono on Linux/macOS). On Linux we can recover
|
||||||
|
* the environment from `/proc/self/environ`.
|
||||||
|
*
|
||||||
|
* Keep this in sync with getBunSandboxEnvValue() in
|
||||||
|
* packages/ai/src/utils/provider-env.ts. The ai package duplicates the lookup
|
||||||
|
* for direct consumers that do not go through this coding-agent entrypoint.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
/**
|
||||||
|
* Restore environment variables from `/proc/self/environ` when running
|
||||||
|
* inside a sandbox where Bun's `process.env` is empty.
|
||||||
|
*/
|
||||||
|
export function restoreSandboxEnv() {
|
||||||
|
if (!process.versions?.bun)
|
||||||
|
return;
|
||||||
|
// If process.env already has entries, nothing to fix.
|
||||||
|
if (Object.keys(process.env).length > 0)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const data = readFileSync("/proc/self/environ", "utf-8");
|
||||||
|
for (const entry of data.split("\0")) {
|
||||||
|
const idx = entry.indexOf("=");
|
||||||
|
if (idx > 0) {
|
||||||
|
process.env[entry.slice(0, idx)] = entry.slice(idx + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// /proc/self/environ may not be readable; ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=restore-sandbox-env.js.map
|
||||||
1
dist/bun/restore-sandbox-env.js.map
vendored
Normal file
1
dist/bun/restore-sandbox-env.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"restore-sandbox-env.js","sourceRoot":"","sources":["../../src/bun/restore-sandbox-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,iBAAiB,GAAS;IACzC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG;QAAE,OAAO;IAEnC,sDAAsD;IACtD,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO;IAEhD,IAAI,CAAC;QACJ,MAAM,IAAI,GAAG,YAAY,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC;QACzD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;gBACb,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;YACzD,CAAC;QACF,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,kDAAkD;IACnD,CAAC;AAAA,CACD","sourcesContent":["/**\n * Workaround for https://github.com/oven-sh/bun/issues/27802\n *\n * Bun compiled binaries have an empty `process.env` when running inside\n * sandbox environments (e.g. nono on Linux/macOS). On Linux we can recover\n * the environment from `/proc/self/environ`.\n *\n * Keep this in sync with getBunSandboxEnvValue() in\n * packages/ai/src/utils/provider-env.ts. The ai package duplicates the lookup\n * for direct consumers that do not go through this coding-agent entrypoint.\n */\n\nimport { readFileSync } from \"node:fs\";\n\n/**\n * Restore environment variables from `/proc/self/environ` when running\n * inside a sandbox where Bun's `process.env` is empty.\n */\nexport function restoreSandboxEnv(): void {\n\tif (!process.versions?.bun) return;\n\n\t// If process.env already has entries, nothing to fix.\n\tif (Object.keys(process.env).length > 0) return;\n\n\ttry {\n\t\tconst data = readFileSync(\"/proc/self/environ\", \"utf-8\");\n\t\tfor (const entry of data.split(\"\\0\")) {\n\t\t\tconst idx = entry.indexOf(\"=\");\n\t\t\tif (idx > 0) {\n\t\t\t\tprocess.env[entry.slice(0, idx)] = entry.slice(idx + 1);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// /proc/self/environ may not be readable; ignore.\n\t}\n}\n"]}
|
||||||
3
dist/cli.d.ts
vendored
Normal file
3
dist/cli.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=cli.d.ts.map
|
||||||
1
dist/cli.d.ts.map
vendored
Normal file
1
dist/cli.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n/**\n * CLI entry point for the refactored coding agent.\n * Uses main.ts with AgentSession and new mode modules.\n *\n * Test with: npx tsx src/cli-new.ts [args...]\n */\nimport { APP_NAME } from \"./config.ts\";\nimport { configureHttpDispatcher } from \"./core/http-dispatcher.ts\";\nimport { main } from \"./main.ts\";\n\nprocess.title = APP_NAME;\nprocess.env.PI_CODING_AGENT = \"true\";\nprocess.emitWarning = (() => {}) as typeof process.emitWarning;\n\n// Configure undici's global dispatcher before provider SDKs issue requests.\n// Runtime settings are applied once SettingsManager has loaded global/project settings.\nconfigureHttpDispatcher();\n\nmain(process.argv.slice(2));\n"]}
|
||||||
18
dist/cli.js
vendored
Executable file
18
dist/cli.js
vendored
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* CLI entry point for the refactored coding agent.
|
||||||
|
* Uses main.ts with AgentSession and new mode modules.
|
||||||
|
*
|
||||||
|
* Test with: npx tsx src/cli-new.ts [args...]
|
||||||
|
*/
|
||||||
|
import { APP_NAME } from "./config.js";
|
||||||
|
import { configureHttpDispatcher } from "./core/http-dispatcher.js";
|
||||||
|
import { main } from "./main.js";
|
||||||
|
process.title = APP_NAME;
|
||||||
|
process.env.PI_CODING_AGENT = "true";
|
||||||
|
process.emitWarning = (() => { });
|
||||||
|
// Configure undici's global dispatcher before provider SDKs issue requests.
|
||||||
|
// Runtime settings are applied once SettingsManager has loaded global/project settings.
|
||||||
|
configureHttpDispatcher();
|
||||||
|
main(process.argv.slice(2));
|
||||||
|
//# sourceMappingURL=cli.js.map
|
||||||
1
dist/cli.js.map
vendored
Normal file
1
dist/cli.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC;AACzB,OAAO,CAAC,GAAG,CAAC,eAAe,GAAG,MAAM,CAAC;AACrC,OAAO,CAAC,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAA+B,CAAC;AAE/D,4EAA4E;AAC5E,wFAAwF;AACxF,uBAAuB,EAAE,CAAC;AAE1B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n/**\n * CLI entry point for the refactored coding agent.\n * Uses main.ts with AgentSession and new mode modules.\n *\n * Test with: npx tsx src/cli-new.ts [args...]\n */\nimport { APP_NAME } from \"./config.ts\";\nimport { configureHttpDispatcher } from \"./core/http-dispatcher.ts\";\nimport { main } from \"./main.ts\";\n\nprocess.title = APP_NAME;\nprocess.env.PI_CODING_AGENT = \"true\";\nprocess.emitWarning = (() => {}) as typeof process.emitWarning;\n\n// Configure undici's global dispatcher before provider SDKs issue requests.\n// Runtime settings are applied once SettingsManager has loaded global/project settings.\nconfigureHttpDispatcher();\n\nmain(process.argv.slice(2));\n"]}
|
||||||
57
dist/cli/args.d.ts
vendored
Normal file
57
dist/cli/args.d.ts
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* CLI argument parsing and help display
|
||||||
|
*/
|
||||||
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||||
|
import type { ExtensionFlag } from "../core/extensions/types.ts";
|
||||||
|
export type Mode = "text" | "json" | "rpc";
|
||||||
|
export interface Args {
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
appendSystemPrompt?: string[];
|
||||||
|
thinking?: ThinkingLevel;
|
||||||
|
continue?: boolean;
|
||||||
|
resume?: boolean;
|
||||||
|
help?: boolean;
|
||||||
|
version?: boolean;
|
||||||
|
mode?: Mode;
|
||||||
|
name?: string;
|
||||||
|
noSession?: boolean;
|
||||||
|
session?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
fork?: string;
|
||||||
|
sessionDir?: string;
|
||||||
|
models?: string[];
|
||||||
|
tools?: string[];
|
||||||
|
excludeTools?: string[];
|
||||||
|
noTools?: boolean;
|
||||||
|
noBuiltinTools?: boolean;
|
||||||
|
extensions?: string[];
|
||||||
|
noExtensions?: boolean;
|
||||||
|
print?: boolean;
|
||||||
|
export?: string;
|
||||||
|
noSkills?: boolean;
|
||||||
|
skills?: string[];
|
||||||
|
promptTemplates?: string[];
|
||||||
|
noPromptTemplates?: boolean;
|
||||||
|
themes?: string[];
|
||||||
|
noThemes?: boolean;
|
||||||
|
noContextFiles?: boolean;
|
||||||
|
listModels?: string | true;
|
||||||
|
offline?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
projectTrustOverride?: boolean;
|
||||||
|
messages: string[];
|
||||||
|
fileArgs: string[];
|
||||||
|
/** Unknown flags (potentially extension flags) - map of flag name to value */
|
||||||
|
unknownFlags: Map<string, boolean | string>;
|
||||||
|
diagnostics: Array<{
|
||||||
|
type: "warning" | "error";
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
export declare function isValidThinkingLevel(level: string): level is ThinkingLevel;
|
||||||
|
export declare function parseArgs(args: string[]): Args;
|
||||||
|
export declare function printHelp(extensionFlags?: ExtensionFlag[]): void;
|
||||||
|
//# sourceMappingURL=args.d.ts.map
|
||||||
1
dist/cli/args.d.ts.map
vendored
Normal file
1
dist/cli/args.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
389
dist/cli/args.js
vendored
Normal file
389
dist/cli/args.js
vendored
Normal file
|
|
@ -0,0 +1,389 @@
|
||||||
|
/**
|
||||||
|
* CLI argument parsing and help display
|
||||||
|
*/
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from "../config.js";
|
||||||
|
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
||||||
|
export function isValidThinkingLevel(level) {
|
||||||
|
return VALID_THINKING_LEVELS.includes(level);
|
||||||
|
}
|
||||||
|
export function parseArgs(args) {
|
||||||
|
const result = {
|
||||||
|
messages: [],
|
||||||
|
fileArgs: [],
|
||||||
|
unknownFlags: new Map(),
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
if (arg === "--help" || arg === "-h") {
|
||||||
|
result.help = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--version" || arg === "-v") {
|
||||||
|
result.version = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--mode" && i + 1 < args.length) {
|
||||||
|
const mode = args[++i];
|
||||||
|
if (mode === "text" || mode === "json" || mode === "rpc") {
|
||||||
|
result.mode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg === "--continue" || arg === "-c") {
|
||||||
|
result.continue = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--resume" || arg === "-r") {
|
||||||
|
result.resume = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--provider" && i + 1 < args.length) {
|
||||||
|
result.provider = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg === "--model" && i + 1 < args.length) {
|
||||||
|
result.model = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg === "--api-key" && i + 1 < args.length) {
|
||||||
|
result.apiKey = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg === "--system-prompt" && i + 1 < args.length) {
|
||||||
|
result.systemPrompt = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg === "--append-system-prompt" && i + 1 < args.length) {
|
||||||
|
result.appendSystemPrompt = result.appendSystemPrompt ?? [];
|
||||||
|
result.appendSystemPrompt.push(args[++i]);
|
||||||
|
}
|
||||||
|
else if (arg === "--name" || arg === "-n") {
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
result.name = args[++i];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result.diagnostics.push({ type: "error", message: "--name requires a value" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg === "--no-session") {
|
||||||
|
result.noSession = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--session" && i + 1 < args.length) {
|
||||||
|
result.session = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg === "--session-id" && i + 1 < args.length) {
|
||||||
|
result.sessionId = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg === "--fork" && i + 1 < args.length) {
|
||||||
|
result.fork = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg === "--session-dir" && i + 1 < args.length) {
|
||||||
|
result.sessionDir = args[++i];
|
||||||
|
}
|
||||||
|
else if (arg === "--models" && i + 1 < args.length) {
|
||||||
|
result.models = args[++i].split(",").map((s) => s.trim());
|
||||||
|
}
|
||||||
|
else if (arg === "--no-tools" || arg === "-nt") {
|
||||||
|
result.noTools = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--no-builtin-tools" || arg === "-nbt") {
|
||||||
|
result.noBuiltinTools = true;
|
||||||
|
}
|
||||||
|
else if ((arg === "--tools" || arg === "-t") && i + 1 < args.length) {
|
||||||
|
result.tools = args[++i]
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((name) => name.length > 0);
|
||||||
|
}
|
||||||
|
else if ((arg === "--exclude-tools" || arg === "-xt") && i + 1 < args.length) {
|
||||||
|
result.excludeTools = args[++i]
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((name) => name.length > 0);
|
||||||
|
}
|
||||||
|
else if (arg === "--thinking" && i + 1 < args.length) {
|
||||||
|
const level = args[++i];
|
||||||
|
if (isValidThinkingLevel(level)) {
|
||||||
|
result.thinking = level;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result.diagnostics.push({
|
||||||
|
type: "warning",
|
||||||
|
message: `Invalid thinking level "${level}". Valid values: ${VALID_THINKING_LEVELS.join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg === "--print" || arg === "-p") {
|
||||||
|
result.print = true;
|
||||||
|
const next = args[i + 1];
|
||||||
|
if (next !== undefined && !next.startsWith("@") && (!next.startsWith("-") || next.startsWith("---"))) {
|
||||||
|
result.messages.push(next);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg === "--export" && i + 1 < args.length) {
|
||||||
|
result.export = args[++i];
|
||||||
|
}
|
||||||
|
else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) {
|
||||||
|
result.extensions = result.extensions ?? [];
|
||||||
|
result.extensions.push(args[++i]);
|
||||||
|
}
|
||||||
|
else if (arg === "--no-extensions" || arg === "-ne") {
|
||||||
|
result.noExtensions = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--skill" && i + 1 < args.length) {
|
||||||
|
result.skills = result.skills ?? [];
|
||||||
|
result.skills.push(args[++i]);
|
||||||
|
}
|
||||||
|
else if (arg === "--prompt-template" && i + 1 < args.length) {
|
||||||
|
result.promptTemplates = result.promptTemplates ?? [];
|
||||||
|
result.promptTemplates.push(args[++i]);
|
||||||
|
}
|
||||||
|
else if (arg === "--theme" && i + 1 < args.length) {
|
||||||
|
result.themes = result.themes ?? [];
|
||||||
|
result.themes.push(args[++i]);
|
||||||
|
}
|
||||||
|
else if (arg === "--no-skills" || arg === "-ns") {
|
||||||
|
result.noSkills = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--no-prompt-templates" || arg === "-np") {
|
||||||
|
result.noPromptTemplates = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--no-themes") {
|
||||||
|
result.noThemes = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--no-context-files" || arg === "-nc") {
|
||||||
|
result.noContextFiles = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--list-models") {
|
||||||
|
// Check if next arg is a search pattern (not a flag or file arg)
|
||||||
|
if (i + 1 < args.length && !args[i + 1].startsWith("-") && !args[i + 1].startsWith("@")) {
|
||||||
|
result.listModels = args[++i];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result.listModels = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg === "--verbose") {
|
||||||
|
result.verbose = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--approve" || arg === "-a") {
|
||||||
|
result.projectTrustOverride = true;
|
||||||
|
}
|
||||||
|
else if (arg === "--no-approve" || arg === "-na") {
|
||||||
|
result.projectTrustOverride = false;
|
||||||
|
}
|
||||||
|
else if (arg === "--offline") {
|
||||||
|
result.offline = true;
|
||||||
|
}
|
||||||
|
else if (arg.startsWith("@")) {
|
||||||
|
result.fileArgs.push(arg.slice(1)); // Remove @ prefix
|
||||||
|
}
|
||||||
|
else if (arg.startsWith("--")) {
|
||||||
|
const eqIndex = arg.indexOf("=");
|
||||||
|
if (eqIndex !== -1) {
|
||||||
|
result.unknownFlags.set(arg.slice(2, eqIndex), arg.slice(eqIndex + 1));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const flagName = arg.slice(2);
|
||||||
|
const next = args[i + 1];
|
||||||
|
if (next !== undefined && !next.startsWith("-") && !next.startsWith("@")) {
|
||||||
|
result.unknownFlags.set(flagName, next);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result.unknownFlags.set(flagName, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg.startsWith("-") && !arg.startsWith("--")) {
|
||||||
|
result.diagnostics.push({ type: "error", message: `Unknown option: ${arg}` });
|
||||||
|
}
|
||||||
|
else if (!arg.startsWith("-")) {
|
||||||
|
result.messages.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
export function printHelp(extensionFlags) {
|
||||||
|
const extensionFlagsText = extensionFlags && extensionFlags.length > 0
|
||||||
|
? `\n${chalk.bold("Extension CLI Flags:")}\n${extensionFlags
|
||||||
|
.map((flag) => {
|
||||||
|
const value = flag.type === "string" ? " <value>" : "";
|
||||||
|
const description = flag.description ?? `Registered by ${flag.extensionPath}`;
|
||||||
|
return ` --${flag.name}${value}`.padEnd(30) + description;
|
||||||
|
})
|
||||||
|
.join("\n")}\n`
|
||||||
|
: "";
|
||||||
|
console.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools
|
||||||
|
|
||||||
|
${chalk.bold("Usage:")}
|
||||||
|
${APP_NAME} [options] [@files...] [messages...]
|
||||||
|
|
||||||
|
${chalk.bold("Commands:")}
|
||||||
|
${APP_NAME} install <source> [-l] Install extension source and add to settings
|
||||||
|
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
||||||
|
${APP_NAME} uninstall <source> [-l] Alias for remove
|
||||||
|
${APP_NAME} update [source|self|pi] Update pi, extensions, or model catalogs
|
||||||
|
${APP_NAME} list List installed extensions from settings
|
||||||
|
${APP_NAME} config [-l] Open TUI to enable/disable package resources (Tab switches scope)
|
||||||
|
${APP_NAME} auth <command> Print credentials for external clients
|
||||||
|
${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list/config/auth
|
||||||
|
|
||||||
|
${chalk.bold("Options:")}
|
||||||
|
--provider <name> Provider name (default: google)
|
||||||
|
--model <pattern> Model pattern or ID (supports "provider/id" and optional ":<thinking>")
|
||||||
|
--api-key <key> API key (defaults to env vars)
|
||||||
|
--system-prompt <text> System prompt (default: coding assistant prompt)
|
||||||
|
--append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)
|
||||||
|
--mode <mode> Output mode: text (default), json, or rpc
|
||||||
|
--print, -p Non-interactive mode: process prompt and exit
|
||||||
|
--continue, -c Continue previous session
|
||||||
|
--resume, -r Select a session to resume
|
||||||
|
--session <path|id> Use specific session file or partial UUID
|
||||||
|
--session-id <id> Use exact project session ID, creating it if missing
|
||||||
|
--fork <path|id> Fork specific session file or partial UUID into a new session
|
||||||
|
--session-dir <dir> Directory for session storage and lookup
|
||||||
|
--no-session Don't save session (ephemeral)
|
||||||
|
--name, -n <name> Set session display name
|
||||||
|
--models <patterns> Comma-separated model patterns for Ctrl+P cycling
|
||||||
|
Supports globs (anthropic/*, *sonnet*) and fuzzy matching
|
||||||
|
--no-tools, -nt Disable all tools by default (built-in and extension)
|
||||||
|
--no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled
|
||||||
|
--tools, -t <tools> Comma-separated allowlist of tool names to enable
|
||||||
|
Applies to built-in, extension, and custom tools
|
||||||
|
--exclude-tools, -xt <tools> Comma-separated denylist of tool names to disable
|
||||||
|
Applies to built-in, extension, and custom tools
|
||||||
|
--thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh, max
|
||||||
|
--extension, -e <path> Load an extension file (can be used multiple times)
|
||||||
|
--no-extensions, -ne Disable extension discovery (explicit -e paths still work)
|
||||||
|
--skill <path> Load a skill file or directory (can be used multiple times)
|
||||||
|
--no-skills, -ns Disable skills discovery and loading
|
||||||
|
--prompt-template <path> Load a prompt template file or directory (can be used multiple times)
|
||||||
|
--no-prompt-templates, -np Disable prompt template discovery and loading
|
||||||
|
--theme <path> Load a theme file or directory (can be used multiple times)
|
||||||
|
--no-themes Disable theme discovery and loading
|
||||||
|
--no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading
|
||||||
|
--export <file> Export session file to HTML and exit
|
||||||
|
--list-models [search] List available models (with optional fuzzy search)
|
||||||
|
--verbose Force verbose startup (overrides quietStartup setting)
|
||||||
|
--approve, -a Trust project-local files for this run
|
||||||
|
--no-approve, -na Ignore project-local files for this run
|
||||||
|
--offline Disable startup network operations (same as PI_OFFLINE=1)
|
||||||
|
--help, -h Show this help
|
||||||
|
--version, -v Show version number
|
||||||
|
|
||||||
|
Extensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}
|
||||||
|
|
||||||
|
${chalk.bold("Examples:")}
|
||||||
|
# Print a provider API key for an external client
|
||||||
|
${APP_NAME} auth print-api-key --provider openai --model gpt-5.5
|
||||||
|
|
||||||
|
# Print an OAuth bearer token for an external client (refreshes if expired)
|
||||||
|
${APP_NAME} auth print-bearer-token --provider openai-codex --model gpt-5.5
|
||||||
|
|
||||||
|
# Interactive mode
|
||||||
|
${APP_NAME}
|
||||||
|
|
||||||
|
# Interactive mode with initial prompt
|
||||||
|
${APP_NAME} "List all .ts files in src/"
|
||||||
|
|
||||||
|
# Include files in initial message
|
||||||
|
${APP_NAME} @prompt.md @image.png "What color is the sky?"
|
||||||
|
|
||||||
|
# Non-interactive mode (process and exit)
|
||||||
|
${APP_NAME} -p "List all .ts files in src/"
|
||||||
|
|
||||||
|
# Multiple messages (interactive)
|
||||||
|
${APP_NAME} "Read package.json" "What dependencies do we have?"
|
||||||
|
|
||||||
|
# Continue previous session
|
||||||
|
${APP_NAME} --continue "What did we discuss?"
|
||||||
|
|
||||||
|
# Start a named session
|
||||||
|
${APP_NAME} --name "Refactor auth module"
|
||||||
|
|
||||||
|
# Use different model
|
||||||
|
${APP_NAME} --provider openai --model gpt-4o-mini "Help me refactor this code"
|
||||||
|
|
||||||
|
# Use model with provider prefix (no --provider needed)
|
||||||
|
${APP_NAME} --model openai/gpt-4o "Help me refactor this code"
|
||||||
|
|
||||||
|
# Use model with thinking level shorthand
|
||||||
|
${APP_NAME} --model sonnet:high "Solve this complex problem"
|
||||||
|
|
||||||
|
# Limit model cycling to specific models
|
||||||
|
${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o
|
||||||
|
|
||||||
|
# Limit to a specific provider with glob pattern
|
||||||
|
${APP_NAME} --models "github-copilot/*"
|
||||||
|
|
||||||
|
# Cycle models with fixed thinking levels
|
||||||
|
${APP_NAME} --models sonnet:high,haiku:low
|
||||||
|
|
||||||
|
# Start with a specific thinking level
|
||||||
|
${APP_NAME} --thinking high "Solve this complex problem"
|
||||||
|
|
||||||
|
# Read-only mode (no file modifications possible)
|
||||||
|
${APP_NAME} --tools read,grep,find,ls -p "Review the code in src/"
|
||||||
|
|
||||||
|
# Disable one tool while keeping the rest available
|
||||||
|
${APP_NAME} --exclude-tools ask_question
|
||||||
|
|
||||||
|
# Export a session file to HTML
|
||||||
|
${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl
|
||||||
|
${APP_NAME} --export session.jsonl output.html
|
||||||
|
|
||||||
|
${chalk.bold("Environment Variables:")}
|
||||||
|
ANTHROPIC_AUTH_TOKEN - Anthropic bearer auth token
|
||||||
|
ANTHROPIC_API_KEY - Anthropic Claude API key
|
||||||
|
ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)
|
||||||
|
ANT_LING_API_KEY - Ant Ling API key
|
||||||
|
OPENAI_API_KEY - OpenAI GPT API key
|
||||||
|
AZURE_OPENAI_API_KEY - Azure OpenAI API key
|
||||||
|
AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)
|
||||||
|
AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)
|
||||||
|
AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)
|
||||||
|
AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)
|
||||||
|
DEEPSEEK_API_KEY - DeepSeek API key
|
||||||
|
NVIDIA_API_KEY - NVIDIA NIM API key
|
||||||
|
GEMINI_API_KEY - Google Gemini API key
|
||||||
|
GROQ_API_KEY - Groq API key
|
||||||
|
CEREBRAS_API_KEY - Cerebras API key
|
||||||
|
XAI_API_KEY - xAI Grok API key
|
||||||
|
FIREWORKS_API_KEY - Fireworks API key
|
||||||
|
TOGETHER_API_KEY - Together AI API key
|
||||||
|
OPENROUTER_API_KEY - OpenRouter API key
|
||||||
|
AI_GATEWAY_API_KEY - Vercel AI Gateway API key
|
||||||
|
ZAI_API_KEY - ZAI Coding Plan API key (Global)
|
||||||
|
ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China)
|
||||||
|
MISTRAL_API_KEY - Mistral API key
|
||||||
|
MINIMAX_API_KEY - MiniMax API key
|
||||||
|
MOONSHOT_API_KEY - Moonshot AI API key
|
||||||
|
OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key
|
||||||
|
KIMI_API_KEY - Kimi For Coding API key
|
||||||
|
CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)
|
||||||
|
CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)
|
||||||
|
CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)
|
||||||
|
QWEN_TOKEN_PLAN_API_KEY - Qwen Token Plan API key (international region)
|
||||||
|
QWEN_TOKEN_PLAN_CN_API_KEY - Qwen Token Plan API key (China region)
|
||||||
|
XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)
|
||||||
|
XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)
|
||||||
|
XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)
|
||||||
|
XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)
|
||||||
|
AWS_PROFILE - AWS profile for Amazon Bedrock
|
||||||
|
AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock
|
||||||
|
AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock
|
||||||
|
AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)
|
||||||
|
AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)
|
||||||
|
${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)
|
||||||
|
${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)
|
||||||
|
PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
|
||||||
|
PI_OFFLINE - Disable startup network operations when set to 1/true/yes
|
||||||
|
PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no
|
||||||
|
PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
|
||||||
|
|
||||||
|
${chalk.bold("Built-in Tool Names:")}
|
||||||
|
read - Read file contents
|
||||||
|
bash - Execute bash commands
|
||||||
|
edit - Edit files with find/replace
|
||||||
|
write - Write files (creates/overwrites)
|
||||||
|
grep - Search file contents (read-only, off by default)
|
||||||
|
find - Find files by glob pattern (read-only, off by default)
|
||||||
|
ls - List directory contents (read-only, off by default)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=args.js.map
|
||||||
1
dist/cli/args.js.map
vendored
Normal file
1
dist/cli/args.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
16
dist/cli/config-selector.d.ts
vendored
Normal file
16
dist/cli/config-selector.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* TUI config selector for `pi config` command
|
||||||
|
*/
|
||||||
|
import type { SettingsManager } from "../core/settings-manager.ts";
|
||||||
|
import { type ScopedResolvedPaths } from "../modes/interactive/components/config-selector.ts";
|
||||||
|
export interface ConfigSelectorOptions {
|
||||||
|
resolvedPaths: ScopedResolvedPaths;
|
||||||
|
settingsManager: SettingsManager;
|
||||||
|
cwd: string;
|
||||||
|
agentDir: string;
|
||||||
|
writeScope: "global" | "project";
|
||||||
|
projectModeAvailable: boolean;
|
||||||
|
}
|
||||||
|
/** Show TUI config selector and return when closed */
|
||||||
|
export declare function selectConfig(options: ConfigSelectorOptions): Promise<void>;
|
||||||
|
//# sourceMappingURL=config-selector.d.ts.map
|
||||||
1
dist/cli/config-selector.d.ts.map
vendored
Normal file
1
dist/cli/config-selector.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"config-selector.d.ts","sourceRoot":"","sources":["../../src/cli/config-selector.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,oDAAoD,CAAC;AAGvH,MAAM,WAAW,qBAAqB;IACrC,aAAa,EAAE,mBAAmB,CAAC;IACnC,eAAe,EAAE,eAAe,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,QAAQ,GAAG,SAAS,CAAC;IACjC,oBAAoB,EAAE,OAAO,CAAC;CAC9B;AAED,sDAAsD;AACtD,wBAAsB,YAAY,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoChF","sourcesContent":["/**\n * TUI config selector for `pi config` command\n */\n\nimport { ProcessTerminal, TUI } from \"@earendil-works/pi-tui\";\nimport type { SettingsManager } from \"../core/settings-manager.ts\";\nimport { ConfigSelectorComponent, type ScopedResolvedPaths } from \"../modes/interactive/components/config-selector.ts\";\nimport { initTheme, stopThemeWatcher } from \"../modes/interactive/theme/theme.ts\";\n\nexport interface ConfigSelectorOptions {\n\tresolvedPaths: ScopedResolvedPaths;\n\tsettingsManager: SettingsManager;\n\tcwd: string;\n\tagentDir: string;\n\twriteScope: \"global\" | \"project\";\n\tprojectModeAvailable: boolean;\n}\n\n/** Show TUI config selector and return when closed */\nexport async function selectConfig(options: ConfigSelectorOptions): Promise<void> {\n\t// Initialize theme before showing TUI\n\tinitTheme(options.settingsManager.getTheme(), true);\n\n\treturn new Promise((resolve) => {\n\t\tconst ui = new TUI(new ProcessTerminal(), undefined, options.agentDir);\n\t\tlet resolved = false;\n\n\t\tconst selector = new ConfigSelectorComponent(\n\t\t\toptions.resolvedPaths,\n\t\t\toptions.settingsManager,\n\t\t\toptions.cwd,\n\t\t\toptions.agentDir,\n\t\t\t() => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tui.stop();\n\t\t\t\t\tstopThemeWatcher();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tui.stop();\n\t\t\t\tstopThemeWatcher();\n\t\t\t\tprocess.exit(0);\n\t\t\t},\n\t\t\t() => ui.requestRender(),\n\t\t\tui.terminal.rows,\n\t\t\toptions.writeScope,\n\t\t\toptions.projectModeAvailable,\n\t\t);\n\n\t\tui.addChild(selector);\n\t\tui.setFocus(selector.getResourceList());\n\t\tui.start();\n\t});\n}\n"]}
|
||||||
31
dist/cli/config-selector.js
vendored
Normal file
31
dist/cli/config-selector.js
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* TUI config selector for `pi config` command
|
||||||
|
*/
|
||||||
|
import { ProcessTerminal, TUI } from "@earendil-works/pi-tui";
|
||||||
|
import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js";
|
||||||
|
import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.js";
|
||||||
|
/** Show TUI config selector and return when closed */
|
||||||
|
export async function selectConfig(options) {
|
||||||
|
// Initialize theme before showing TUI
|
||||||
|
initTheme(options.settingsManager.getTheme(), true);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const ui = new TUI(new ProcessTerminal(), undefined, options.agentDir);
|
||||||
|
let resolved = false;
|
||||||
|
const selector = new ConfigSelectorComponent(options.resolvedPaths, options.settingsManager, options.cwd, options.agentDir, () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
ui.stop();
|
||||||
|
stopThemeWatcher();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
ui.stop();
|
||||||
|
stopThemeWatcher();
|
||||||
|
process.exit(0);
|
||||||
|
}, () => ui.requestRender(), ui.terminal.rows, options.writeScope, options.projectModeAvailable);
|
||||||
|
ui.addChild(selector);
|
||||||
|
ui.setFocus(selector.getResourceList());
|
||||||
|
ui.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=config-selector.js.map
|
||||||
1
dist/cli/config-selector.js.map
vendored
Normal file
1
dist/cli/config-selector.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"config-selector.js","sourceRoot":"","sources":["../../src/cli/config-selector.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,EAAE,uBAAuB,EAA4B,MAAM,oDAAoD,CAAC;AACvH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAWlF,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAA8B,EAAiB;IACjF,sCAAsC;IACtC,SAAS,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,CAAC;IAEpD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/B,MAAM,EAAE,GAAG,IAAI,GAAG,CAAC,IAAI,eAAe,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;QACvE,IAAI,QAAQ,GAAG,KAAK,CAAC;QAErB,MAAM,QAAQ,GAAG,IAAI,uBAAuB,CAC3C,OAAO,CAAC,aAAa,EACrB,OAAO,CAAC,eAAe,EACvB,OAAO,CAAC,GAAG,EACX,OAAO,CAAC,QAAQ,EAChB,GAAG,EAAE,CAAC;YACL,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACf,QAAQ,GAAG,IAAI,CAAC;gBAChB,EAAE,CAAC,IAAI,EAAE,CAAC;gBACV,gBAAgB,EAAE,CAAC;gBACnB,OAAO,EAAE,CAAC;YACX,CAAC;QAAA,CACD,EACD,GAAG,EAAE,CAAC;YACL,EAAE,CAAC,IAAI,EAAE,CAAC;YACV,gBAAgB,EAAE,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAA,CAChB,EACD,GAAG,EAAE,CAAC,EAAE,CAAC,aAAa,EAAE,EACxB,EAAE,CAAC,QAAQ,CAAC,IAAI,EAChB,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,oBAAoB,CAC5B,CAAC;QAEF,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACtB,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC,CAAC;QACxC,EAAE,CAAC,KAAK,EAAE,CAAC;IAAA,CACX,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * TUI config selector for `pi config` command\n */\n\nimport { ProcessTerminal, TUI } from \"@earendil-works/pi-tui\";\nimport type { SettingsManager } from \"../core/settings-manager.ts\";\nimport { ConfigSelectorComponent, type ScopedResolvedPaths } from \"../modes/interactive/components/config-selector.ts\";\nimport { initTheme, stopThemeWatcher } from \"../modes/interactive/theme/theme.ts\";\n\nexport interface ConfigSelectorOptions {\n\tresolvedPaths: ScopedResolvedPaths;\n\tsettingsManager: SettingsManager;\n\tcwd: string;\n\tagentDir: string;\n\twriteScope: \"global\" | \"project\";\n\tprojectModeAvailable: boolean;\n}\n\n/** Show TUI config selector and return when closed */\nexport async function selectConfig(options: ConfigSelectorOptions): Promise<void> {\n\t// Initialize theme before showing TUI\n\tinitTheme(options.settingsManager.getTheme(), true);\n\n\treturn new Promise((resolve) => {\n\t\tconst ui = new TUI(new ProcessTerminal(), undefined, options.agentDir);\n\t\tlet resolved = false;\n\n\t\tconst selector = new ConfigSelectorComponent(\n\t\t\toptions.resolvedPaths,\n\t\t\toptions.settingsManager,\n\t\t\toptions.cwd,\n\t\t\toptions.agentDir,\n\t\t\t() => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tui.stop();\n\t\t\t\t\tstopThemeWatcher();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tui.stop();\n\t\t\t\tstopThemeWatcher();\n\t\t\t\tprocess.exit(0);\n\t\t\t},\n\t\t\t() => ui.requestRender(),\n\t\t\tui.terminal.rows,\n\t\t\toptions.writeScope,\n\t\t\toptions.projectModeAvailable,\n\t\t);\n\n\t\tui.addChild(selector);\n\t\tui.setFocus(selector.getResourceList());\n\t\tui.start();\n\t});\n}\n"]}
|
||||||
23
dist/cli/credential-print.d.ts
vendored
Normal file
23
dist/cli/credential-print.d.ts
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { ModelRuntime } from "../core/model-runtime.ts";
|
||||||
|
import type { Args } from "./args.ts";
|
||||||
|
export type CredentialPrintKind = "api_key" | "bearer_token";
|
||||||
|
export interface CredentialPrintCommand {
|
||||||
|
kind: CredentialPrintKind;
|
||||||
|
args: string[];
|
||||||
|
minExpiryMs?: number;
|
||||||
|
}
|
||||||
|
export declare class CredentialPrintError extends Error {
|
||||||
|
}
|
||||||
|
export declare function isCredentialPrintHelp(args: string[]): boolean;
|
||||||
|
export declare function printCredentialPrintHelp(): void;
|
||||||
|
/** Parse the small, extensible `pi auth` command surface before normal startup. */
|
||||||
|
export declare function parseCredentialPrintCommand(args: string[]): CredentialPrintCommand | undefined;
|
||||||
|
export declare function validateCredentialPrintArgs(args: Args): void;
|
||||||
|
/**
|
||||||
|
* Resolve one request credential for a specific provider/model pair.
|
||||||
|
*
|
||||||
|
* This intentionally calls ModelRuntime.getAuth(), which refreshes and persists
|
||||||
|
* OAuth credentials with less than five minutes remaining through the normal request-auth path.
|
||||||
|
*/
|
||||||
|
export declare function resolveCredentialForPrint(args: Args, modelRuntime: ModelRuntime, kind: CredentialPrintKind, minExpiryMs?: number): Promise<string>;
|
||||||
|
//# sourceMappingURL=credential-print.d.ts.map
|
||||||
1
dist/cli/credential-print.d.ts.map
vendored
Normal file
1
dist/cli/credential-print.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
116
dist/cli/credential-print.js
vendored
Normal file
116
dist/cli/credential-print.js
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { resolveCliModel } from "../core/model-resolver.js";
|
||||||
|
const DEFAULT_BEARER_TOKEN_MIN_EXPIRY_MS = 30 * 60_000;
|
||||||
|
export class CredentialPrintError extends Error {
|
||||||
|
}
|
||||||
|
export function isCredentialPrintHelp(args) {
|
||||||
|
return (args[0] === "auth" && (args[1] === undefined || args[1] === "help" || args[1] === "--help" || args[1] === "-h"));
|
||||||
|
}
|
||||||
|
export function printCredentialPrintHelp() {
|
||||||
|
console.log(`Usage:
|
||||||
|
pi auth print-api-key --model <model> [--provider <provider>]
|
||||||
|
pi auth print-bearer-token --model <model> [--provider <provider>] [--min-expiry <duration>]
|
||||||
|
|
||||||
|
Prints the configured credential alone on stdout. Provider inference uses configured credentials; specify --provider to select explicitly. Bearer tokens have a 30-minute minimum expiry by default. --min-expiry accepts ms, s, m, or h (for example, 30m).`);
|
||||||
|
}
|
||||||
|
/** Parse the small, extensible `pi auth` command surface before normal startup. */
|
||||||
|
export function parseCredentialPrintCommand(args) {
|
||||||
|
if (args[0] !== "auth")
|
||||||
|
return undefined;
|
||||||
|
const kind = args[1] === "print-api-key" ? "api_key" : args[1] === "print-bearer-token" ? "bearer_token" : undefined;
|
||||||
|
if (!kind) {
|
||||||
|
throw new CredentialPrintError(`Unknown auth command "${args[1] ?? ""}". Use "pi auth print-api-key" or "pi auth print-bearer-token".`);
|
||||||
|
}
|
||||||
|
const commandArgs = [];
|
||||||
|
let minExpiryMs;
|
||||||
|
for (let index = 2; index < args.length; index++) {
|
||||||
|
if (args[index] !== "--min-expiry") {
|
||||||
|
commandArgs.push(args[index]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (kind !== "bearer_token") {
|
||||||
|
throw new CredentialPrintError("--min-expiry is only supported by print-bearer-token");
|
||||||
|
}
|
||||||
|
const value = args[++index];
|
||||||
|
const match = value ? /^(\d+)(ms|s|m|h)$/iu.exec(value) : undefined;
|
||||||
|
if (!match) {
|
||||||
|
throw new CredentialPrintError("--min-expiry must use a duration such as 30m or 1h");
|
||||||
|
}
|
||||||
|
const amount = Number(match[1]);
|
||||||
|
const unit = match[2];
|
||||||
|
minExpiryMs = amount * (unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : 3_600_000);
|
||||||
|
}
|
||||||
|
return minExpiryMs === undefined ? { kind, args: commandArgs } : { kind, args: commandArgs, minExpiryMs };
|
||||||
|
}
|
||||||
|
export function validateCredentialPrintArgs(args) {
|
||||||
|
if (!args.model?.trim()) {
|
||||||
|
throw new CredentialPrintError("Credential printing requires --model <model>");
|
||||||
|
}
|
||||||
|
if (args.apiKey !== undefined) {
|
||||||
|
throw new CredentialPrintError("Credential printing reads configured credentials; --api-key is not supported");
|
||||||
|
}
|
||||||
|
if (args.messages.length > 0 || args.fileArgs.length > 0 || args.unknownFlags.size > 0) {
|
||||||
|
throw new CredentialPrintError("Credential printing only accepts --provider and --model");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resolve one request credential for a specific provider/model pair.
|
||||||
|
*
|
||||||
|
* This intentionally calls ModelRuntime.getAuth(), which refreshes and persists
|
||||||
|
* OAuth credentials with less than five minutes remaining through the normal request-auth path.
|
||||||
|
*/
|
||||||
|
export async function resolveCredentialForPrint(args, modelRuntime, kind, minExpiryMs) {
|
||||||
|
validateCredentialPrintArgs(args);
|
||||||
|
const credentialTypes = new Map((await modelRuntime.listCredentials()).map((credential) => [credential.providerId, credential.type]));
|
||||||
|
const models = [];
|
||||||
|
if (args.provider) {
|
||||||
|
const resolved = resolveCliModel({ cliProvider: args.provider, cliModel: args.model, modelRuntime });
|
||||||
|
if (resolved.error || !resolved.model) {
|
||||||
|
throw new CredentialPrintError(resolved.error ?? "Unable to resolve the requested provider/model");
|
||||||
|
}
|
||||||
|
models.push(resolved.model);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const provider of modelRuntime.getProviders()) {
|
||||||
|
if (!credentialTypes.has(provider.id))
|
||||||
|
continue;
|
||||||
|
const resolved = resolveCliModel({ cliProvider: provider.id, cliModel: args.model, modelRuntime });
|
||||||
|
if (resolved.model && !resolved.error && !resolved.warning?.includes("Using custom model id")) {
|
||||||
|
models.push(resolved.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (models.length === 0) {
|
||||||
|
throw new CredentialPrintError(`Model "${args.model}" not found. Use --list-models to see available models.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const credentials = [];
|
||||||
|
for (const model of models) {
|
||||||
|
const type = credentialTypes.get(model.provider);
|
||||||
|
if (kind === "api_key" && type === "oauth")
|
||||||
|
continue;
|
||||||
|
if (kind === "bearer_token" && type !== "oauth")
|
||||||
|
continue;
|
||||||
|
const auth = await modelRuntime.getAuth(model, kind === "bearer_token"
|
||||||
|
? { minOAuthValidityMs: minExpiryMs ?? DEFAULT_BEARER_TOKEN_MIN_EXPIRY_MS }
|
||||||
|
: undefined);
|
||||||
|
const authorization = Object.entries(auth?.auth.headers ?? {}).find(([name]) => name.toLowerCase() === "authorization")?.[1];
|
||||||
|
const bearerToken = typeof authorization === "string" ? /^Bearer\s+(.+)$/iu.exec(authorization)?.[1] : undefined;
|
||||||
|
const value = kind === "bearer_token" ? (auth?.auth.apiKey ?? bearerToken) : auth?.auth.apiKey;
|
||||||
|
if (value)
|
||||||
|
credentials.push({ providerId: model.provider, value });
|
||||||
|
}
|
||||||
|
if (credentials.length === 1)
|
||||||
|
return credentials[0].value;
|
||||||
|
if (credentials.length === 0) {
|
||||||
|
const providerId = models[0]?.provider;
|
||||||
|
const type = providerId ? credentialTypes.get(providerId) : undefined;
|
||||||
|
if (args.provider && kind === "api_key" && type === "oauth") {
|
||||||
|
throw new CredentialPrintError(`Provider "${providerId}" is configured with OAuth, not an API key`);
|
||||||
|
}
|
||||||
|
if (args.provider && kind === "bearer_token" && type !== "oauth") {
|
||||||
|
throw new CredentialPrintError(`Provider "${providerId}" is not configured with an OAuth bearer token`);
|
||||||
|
}
|
||||||
|
throw new CredentialPrintError(`No usable ${kind === "api_key" ? "API key" : "OAuth bearer token"} is configured`);
|
||||||
|
}
|
||||||
|
throw new CredentialPrintError(`Model "${args.model}" has multiple configured providers (${credentials.map(({ providerId }) => providerId).join(", ")}). Specify --provider.`);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=credential-print.js.map
|
||||||
1
dist/cli/credential-print.js.map
vendored
Normal file
1
dist/cli/credential-print.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
15
dist/cli/file-processor.d.ts
vendored
Normal file
15
dist/cli/file-processor.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Process @file CLI arguments into text content and image attachments
|
||||||
|
*/
|
||||||
|
import type { ImageContent } from "@earendil-works/pi-ai";
|
||||||
|
export interface ProcessedFiles {
|
||||||
|
text: string;
|
||||||
|
images: ImageContent[];
|
||||||
|
}
|
||||||
|
export interface ProcessFileOptions {
|
||||||
|
/** Whether to auto-resize images to 2000x2000 max. Default: true */
|
||||||
|
autoResizeImages?: boolean;
|
||||||
|
}
|
||||||
|
/** Process @file arguments into text content and image attachments */
|
||||||
|
export declare function processFileArguments(fileArgs: string[], options?: ProcessFileOptions): Promise<ProcessedFiles>;
|
||||||
|
//# sourceMappingURL=file-processor.d.ts.map
|
||||||
1
dist/cli/file-processor.d.ts.map
vendored
Normal file
1
dist/cli/file-processor.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"file-processor.d.ts","sourceRoot":"","sources":["../../src/cli/file-processor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAO1D,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAClC,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,sEAAsE;AACtE,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CA+DpH","sourcesContent":["/**\n * Process @file CLI arguments into text content and image attachments\n */\n\nimport { access, readFile, stat } from \"node:fs/promises\";\nimport type { ImageContent } from \"@earendil-works/pi-ai\";\nimport chalk from \"chalk\";\nimport { resolve } from \"path\";\nimport { resolveReadPath } from \"../core/tools/path-utils.ts\";\nimport { processImage } from \"../utils/image-process.ts\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../utils/mime.ts\";\n\nexport interface ProcessedFiles {\n\ttext: string;\n\timages: ImageContent[];\n}\n\nexport interface ProcessFileOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n}\n\n/** Process @file arguments into text content and image attachments */\nexport async function processFileArguments(fileArgs: string[], options?: ProcessFileOptions): Promise<ProcessedFiles> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tlet text = \"\";\n\tconst images: ImageContent[] = [];\n\n\tfor (const fileArg of fileArgs) {\n\t\t// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)\n\t\tconst absolutePath = resolve(resolveReadPath(fileArg, process.cwd()));\n\n\t\t// Check if file exists\n\t\ttry {\n\t\t\tawait access(absolutePath);\n\t\t} catch {\n\t\t\tconsole.error(chalk.red(`Error: File not found: ${absolutePath}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\t// Check if file is empty\n\t\tconst stats = await stat(absolutePath);\n\t\tif (stats.size === 0) {\n\t\t\t// Skip empty files\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);\n\n\t\tif (mimeType) {\n\t\t\t// Handle image file\n\t\t\tconst content = await readFile(absolutePath);\n\t\t\tconst processed = await processImage(content, mimeType, { autoResizeImages });\n\n\t\t\tif (!processed.ok) {\n\t\t\t\ttext += `<file name=\"${absolutePath}\">${processed.message}</file>\\n`;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst attachment: ImageContent = {\n\t\t\t\ttype: \"image\",\n\t\t\t\tmimeType: processed.mimeType,\n\t\t\t\tdata: processed.data,\n\t\t\t};\n\t\t\timages.push(attachment);\n\n\t\t\t// Add text reference to image with optional processing hints\n\t\t\tif (processed.hints.length > 0) {\n\t\t\t\ttext += `<file name=\"${absolutePath}\">${processed.hints.join(\"\\n\")}</file>\\n`;\n\t\t\t} else {\n\t\t\t\ttext += `<file name=\"${absolutePath}\"></file>\\n`;\n\t\t\t}\n\t\t} else {\n\t\t\t// Handle text file\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(absolutePath, \"utf-8\");\n\t\t\t\ttext += `<file name=\"${absolutePath}\">\\n${content}\\n</file>\\n`;\n\t\t\t} catch (error: unknown) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tconsole.error(chalk.red(`Error: Could not read file ${absolutePath}: ${message}`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { text, images };\n}\n"]}
|
||||||
70
dist/cli/file-processor.js
vendored
Normal file
70
dist/cli/file-processor.js
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* Process @file CLI arguments into text content and image attachments
|
||||||
|
*/
|
||||||
|
import { access, readFile, stat } from "node:fs/promises";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import { resolveReadPath } from "../core/tools/path-utils.js";
|
||||||
|
import { processImage } from "../utils/image-process.js";
|
||||||
|
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.js";
|
||||||
|
/** Process @file arguments into text content and image attachments */
|
||||||
|
export async function processFileArguments(fileArgs, options) {
|
||||||
|
const autoResizeImages = options?.autoResizeImages ?? true;
|
||||||
|
let text = "";
|
||||||
|
const images = [];
|
||||||
|
for (const fileArg of fileArgs) {
|
||||||
|
// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
|
||||||
|
const absolutePath = resolve(resolveReadPath(fileArg, process.cwd()));
|
||||||
|
// Check if file exists
|
||||||
|
try {
|
||||||
|
await access(absolutePath);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
// Check if file is empty
|
||||||
|
const stats = await stat(absolutePath);
|
||||||
|
if (stats.size === 0) {
|
||||||
|
// Skip empty files
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
|
||||||
|
if (mimeType) {
|
||||||
|
// Handle image file
|
||||||
|
const content = await readFile(absolutePath);
|
||||||
|
const processed = await processImage(content, mimeType, { autoResizeImages });
|
||||||
|
if (!processed.ok) {
|
||||||
|
text += `<file name="${absolutePath}">${processed.message}</file>\n`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const attachment = {
|
||||||
|
type: "image",
|
||||||
|
mimeType: processed.mimeType,
|
||||||
|
data: processed.data,
|
||||||
|
};
|
||||||
|
images.push(attachment);
|
||||||
|
// Add text reference to image with optional processing hints
|
||||||
|
if (processed.hints.length > 0) {
|
||||||
|
text += `<file name="${absolutePath}">${processed.hints.join("\n")}</file>\n`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
text += `<file name="${absolutePath}"></file>\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Handle text file
|
||||||
|
try {
|
||||||
|
const content = await readFile(absolutePath, "utf-8");
|
||||||
|
text += `<file name="${absolutePath}">\n${content}\n</file>\n`;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(chalk.red(`Error: Could not read file ${absolutePath}: ${message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { text, images };
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=file-processor.js.map
|
||||||
1
dist/cli/file-processor.js.map
vendored
Normal file
1
dist/cli/file-processor.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
18
dist/cli/initial-message.d.ts
vendored
Normal file
18
dist/cli/initial-message.d.ts
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { ImageContent } from "@earendil-works/pi-ai";
|
||||||
|
import type { Args } from "./args.ts";
|
||||||
|
export interface InitialMessageInput {
|
||||||
|
parsed: Args;
|
||||||
|
fileText?: string;
|
||||||
|
fileImages?: ImageContent[];
|
||||||
|
stdinContent?: string;
|
||||||
|
}
|
||||||
|
export interface InitialMessageResult {
|
||||||
|
initialMessage?: string;
|
||||||
|
initialImages?: ImageContent[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Combine stdin content, @file text, and the first CLI message into a single
|
||||||
|
* initial prompt for non-interactive mode.
|
||||||
|
*/
|
||||||
|
export declare function buildInitialMessage({ parsed, fileText, fileImages, stdinContent }: InitialMessageInput): InitialMessageResult;
|
||||||
|
//# sourceMappingURL=initial-message.d.ts.map
|
||||||
1
dist/cli/initial-message.d.ts.map
vendored
Normal file
1
dist/cli/initial-message.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"initial-message.d.ts","sourceRoot":"","sources":["../../src/cli/initial-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,WAAW,mBAAmB;IACnC,MAAM,EAAE,IAAI,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EACnC,MAAM,EACN,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,EAAE,mBAAmB,GAAG,oBAAoB,CAkB5C","sourcesContent":["import type { ImageContent } from \"@earendil-works/pi-ai\";\nimport type { Args } from \"./args.ts\";\n\nexport interface InitialMessageInput {\n\tparsed: Args;\n\tfileText?: string;\n\tfileImages?: ImageContent[];\n\tstdinContent?: string;\n}\n\nexport interface InitialMessageResult {\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}\n\n/**\n * Combine stdin content, @file text, and the first CLI message into a single\n * initial prompt for non-interactive mode.\n */\nexport function buildInitialMessage({\n\tparsed,\n\tfileText,\n\tfileImages,\n\tstdinContent,\n}: InitialMessageInput): InitialMessageResult {\n\tconst parts: string[] = [];\n\tif (stdinContent !== undefined) {\n\t\tparts.push(stdinContent);\n\t}\n\tif (fileText) {\n\t\tparts.push(fileText);\n\t}\n\n\tif (parsed.messages.length > 0) {\n\t\tparts.push(parsed.messages[0]);\n\t\tparsed.messages.shift();\n\t}\n\n\treturn {\n\t\tinitialMessage: parts.length > 0 ? parts.join(\"\") : undefined,\n\t\tinitialImages: fileImages && fileImages.length > 0 ? fileImages : undefined,\n\t};\n}\n"]}
|
||||||
22
dist/cli/initial-message.js
vendored
Normal file
22
dist/cli/initial-message.js
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* Combine stdin content, @file text, and the first CLI message into a single
|
||||||
|
* initial prompt for non-interactive mode.
|
||||||
|
*/
|
||||||
|
export function buildInitialMessage({ parsed, fileText, fileImages, stdinContent, }) {
|
||||||
|
const parts = [];
|
||||||
|
if (stdinContent !== undefined) {
|
||||||
|
parts.push(stdinContent);
|
||||||
|
}
|
||||||
|
if (fileText) {
|
||||||
|
parts.push(fileText);
|
||||||
|
}
|
||||||
|
if (parsed.messages.length > 0) {
|
||||||
|
parts.push(parsed.messages[0]);
|
||||||
|
parsed.messages.shift();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
initialMessage: parts.length > 0 ? parts.join("") : undefined,
|
||||||
|
initialImages: fileImages && fileImages.length > 0 ? fileImages : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=initial-message.js.map
|
||||||
1
dist/cli/initial-message.js.map
vendored
Normal file
1
dist/cli/initial-message.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"initial-message.js","sourceRoot":"","sources":["../../src/cli/initial-message.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,EACnC,MAAM,EACN,QAAQ,EACR,UAAU,EACV,YAAY,GACS,EAAwB;IAC7C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC1B,CAAC;IACD,IAAI,QAAQ,EAAE,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAED,OAAO;QACN,cAAc,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;QAC7D,aAAa,EAAE,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;KAC3E,CAAC;AAAA,CACF","sourcesContent":["import type { ImageContent } from \"@earendil-works/pi-ai\";\nimport type { Args } from \"./args.ts\";\n\nexport interface InitialMessageInput {\n\tparsed: Args;\n\tfileText?: string;\n\tfileImages?: ImageContent[];\n\tstdinContent?: string;\n}\n\nexport interface InitialMessageResult {\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}\n\n/**\n * Combine stdin content, @file text, and the first CLI message into a single\n * initial prompt for non-interactive mode.\n */\nexport function buildInitialMessage({\n\tparsed,\n\tfileText,\n\tfileImages,\n\tstdinContent,\n}: InitialMessageInput): InitialMessageResult {\n\tconst parts: string[] = [];\n\tif (stdinContent !== undefined) {\n\t\tparts.push(stdinContent);\n\t}\n\tif (fileText) {\n\t\tparts.push(fileText);\n\t}\n\n\tif (parsed.messages.length > 0) {\n\t\tparts.push(parsed.messages[0]);\n\t\tparsed.messages.shift();\n\t}\n\n\treturn {\n\t\tinitialMessage: parts.length > 0 ? parts.join(\"\") : undefined,\n\t\tinitialImages: fileImages && fileImages.length > 0 ? fileImages : undefined,\n\t};\n}\n"]}
|
||||||
9
dist/cli/list-models.d.ts
vendored
Normal file
9
dist/cli/list-models.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* List available models with optional fuzzy search
|
||||||
|
*/
|
||||||
|
import type { ModelRuntime } from "../core/model-runtime.ts";
|
||||||
|
/**
|
||||||
|
* List available models, optionally filtered by search pattern
|
||||||
|
*/
|
||||||
|
export declare function listModels(modelRuntime: ModelRuntime, searchPattern?: string): Promise<void>;
|
||||||
|
//# sourceMappingURL=list-models.d.ts.map
|
||||||
1
dist/cli/list-models.d.ts.map
vendored
Normal file
1
dist/cli/list-models.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"list-models.d.ts","sourceRoot":"","sources":["../../src/cli/list-models.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAiB7D;;GAEG;AACH,wBAAsB,UAAU,CAAC,YAAY,EAAE,YAAY,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkFlG","sourcesContent":["/**\n * List available models with optional fuzzy search\n */\n\nimport type { Api, Model } from \"@earendil-works/pi-ai\";\nimport { fuzzyFilter } from \"@earendil-works/pi-tui\";\nimport chalk from \"chalk\";\nimport { formatNoModelsAvailableMessage } from \"../core/auth-guidance.ts\";\nimport type { ModelRuntime } from \"../core/model-runtime.ts\";\n\n/**\n * Format a number as human-readable (e.g., 200000 -> \"200K\", 1000000 -> \"1M\")\n */\nfunction formatTokenCount(count: number): string {\n\tif (count >= 1_000_000) {\n\t\tconst millions = count / 1_000_000;\n\t\treturn millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;\n\t}\n\tif (count >= 1_000) {\n\t\tconst thousands = count / 1_000;\n\t\treturn thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;\n\t}\n\treturn count.toString();\n}\n\n/**\n * List available models, optionally filtered by search pattern\n */\nexport async function listModels(modelRuntime: ModelRuntime, searchPattern?: string): Promise<void> {\n\tconst loadError = modelRuntime.getError();\n\tif (loadError) {\n\t\tconsole.error(chalk.yellow(`Warning: errors loading models.json:\\n${loadError}`));\n\t}\n\n\tconst models = [...(await modelRuntime.getAvailable())];\n\n\tif (models.length === 0) {\n\t\tconsole.log(formatNoModelsAvailableMessage());\n\t\treturn;\n\t}\n\n\t// Apply fuzzy filter if search pattern provided\n\tlet filteredModels: Model<Api>[] = models;\n\tif (searchPattern) {\n\t\tfilteredModels = fuzzyFilter(models, searchPattern, (m) => `${m.provider} ${m.id}`);\n\t}\n\n\tif (filteredModels.length === 0) {\n\t\tconsole.log(`No models matching \"${searchPattern}\"`);\n\t\treturn;\n\t}\n\n\t// Sort by provider, then by model id\n\tfilteredModels.sort((a, b) => {\n\t\tconst providerCmp = a.provider.localeCompare(b.provider);\n\t\tif (providerCmp !== 0) return providerCmp;\n\t\treturn a.id.localeCompare(b.id);\n\t});\n\n\t// Calculate column widths\n\tconst rows = filteredModels.map((m) => ({\n\t\tprovider: m.provider,\n\t\tmodel: m.id,\n\t\tcontext: formatTokenCount(m.contextWindow),\n\t\tmaxOut: formatTokenCount(m.maxTokens),\n\t\tthinking: m.reasoning ? \"yes\" : \"no\",\n\t\timages: m.input.includes(\"image\") ? \"yes\" : \"no\",\n\t}));\n\n\tconst headers = {\n\t\tprovider: \"provider\",\n\t\tmodel: \"model\",\n\t\tcontext: \"context\",\n\t\tmaxOut: \"max-out\",\n\t\tthinking: \"thinking\",\n\t\timages: \"images\",\n\t};\n\n\tconst widths = {\n\t\tprovider: Math.max(headers.provider.length, ...rows.map((r) => r.provider.length)),\n\t\tmodel: Math.max(headers.model.length, ...rows.map((r) => r.model.length)),\n\t\tcontext: Math.max(headers.context.length, ...rows.map((r) => r.context.length)),\n\t\tmaxOut: Math.max(headers.maxOut.length, ...rows.map((r) => r.maxOut.length)),\n\t\tthinking: Math.max(headers.thinking.length, ...rows.map((r) => r.thinking.length)),\n\t\timages: Math.max(headers.images.length, ...rows.map((r) => r.images.length)),\n\t};\n\n\t// Print header\n\tconst headerLine = [\n\t\theaders.provider.padEnd(widths.provider),\n\t\theaders.model.padEnd(widths.model),\n\t\theaders.context.padEnd(widths.context),\n\t\theaders.maxOut.padEnd(widths.maxOut),\n\t\theaders.thinking.padEnd(widths.thinking),\n\t\theaders.images.padEnd(widths.images),\n\t].join(\" \");\n\tconsole.log(headerLine);\n\n\t// Print rows\n\tfor (const row of rows) {\n\t\tconst line = [\n\t\t\trow.provider.padEnd(widths.provider),\n\t\t\trow.model.padEnd(widths.model),\n\t\t\trow.context.padEnd(widths.context),\n\t\t\trow.maxOut.padEnd(widths.maxOut),\n\t\t\trow.thinking.padEnd(widths.thinking),\n\t\t\trow.images.padEnd(widths.images),\n\t\t].join(\" \");\n\t\tconsole.log(line);\n\t}\n}\n"]}
|
||||||
98
dist/cli/list-models.js
vendored
Normal file
98
dist/cli/list-models.js
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* List available models with optional fuzzy search
|
||||||
|
*/
|
||||||
|
import { fuzzyFilter } from "@earendil-works/pi-tui";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { formatNoModelsAvailableMessage } from "../core/auth-guidance.js";
|
||||||
|
/**
|
||||||
|
* Format a number as human-readable (e.g., 200000 -> "200K", 1000000 -> "1M")
|
||||||
|
*/
|
||||||
|
function formatTokenCount(count) {
|
||||||
|
if (count >= 1_000_000) {
|
||||||
|
const millions = count / 1_000_000;
|
||||||
|
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (count >= 1_000) {
|
||||||
|
const thousands = count / 1_000;
|
||||||
|
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return count.toString();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* List available models, optionally filtered by search pattern
|
||||||
|
*/
|
||||||
|
export async function listModels(modelRuntime, searchPattern) {
|
||||||
|
const loadError = modelRuntime.getError();
|
||||||
|
if (loadError) {
|
||||||
|
console.error(chalk.yellow(`Warning: errors loading models.json:\n${loadError}`));
|
||||||
|
}
|
||||||
|
const models = [...(await modelRuntime.getAvailable())];
|
||||||
|
if (models.length === 0) {
|
||||||
|
console.log(formatNoModelsAvailableMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Apply fuzzy filter if search pattern provided
|
||||||
|
let filteredModels = models;
|
||||||
|
if (searchPattern) {
|
||||||
|
filteredModels = fuzzyFilter(models, searchPattern, (m) => `${m.provider} ${m.id}`);
|
||||||
|
}
|
||||||
|
if (filteredModels.length === 0) {
|
||||||
|
console.log(`No models matching "${searchPattern}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Sort by provider, then by model id
|
||||||
|
filteredModels.sort((a, b) => {
|
||||||
|
const providerCmp = a.provider.localeCompare(b.provider);
|
||||||
|
if (providerCmp !== 0)
|
||||||
|
return providerCmp;
|
||||||
|
return a.id.localeCompare(b.id);
|
||||||
|
});
|
||||||
|
// Calculate column widths
|
||||||
|
const rows = filteredModels.map((m) => ({
|
||||||
|
provider: m.provider,
|
||||||
|
model: m.id,
|
||||||
|
context: formatTokenCount(m.contextWindow),
|
||||||
|
maxOut: formatTokenCount(m.maxTokens),
|
||||||
|
thinking: m.reasoning ? "yes" : "no",
|
||||||
|
images: m.input.includes("image") ? "yes" : "no",
|
||||||
|
}));
|
||||||
|
const headers = {
|
||||||
|
provider: "provider",
|
||||||
|
model: "model",
|
||||||
|
context: "context",
|
||||||
|
maxOut: "max-out",
|
||||||
|
thinking: "thinking",
|
||||||
|
images: "images",
|
||||||
|
};
|
||||||
|
const widths = {
|
||||||
|
provider: Math.max(headers.provider.length, ...rows.map((r) => r.provider.length)),
|
||||||
|
model: Math.max(headers.model.length, ...rows.map((r) => r.model.length)),
|
||||||
|
context: Math.max(headers.context.length, ...rows.map((r) => r.context.length)),
|
||||||
|
maxOut: Math.max(headers.maxOut.length, ...rows.map((r) => r.maxOut.length)),
|
||||||
|
thinking: Math.max(headers.thinking.length, ...rows.map((r) => r.thinking.length)),
|
||||||
|
images: Math.max(headers.images.length, ...rows.map((r) => r.images.length)),
|
||||||
|
};
|
||||||
|
// Print header
|
||||||
|
const headerLine = [
|
||||||
|
headers.provider.padEnd(widths.provider),
|
||||||
|
headers.model.padEnd(widths.model),
|
||||||
|
headers.context.padEnd(widths.context),
|
||||||
|
headers.maxOut.padEnd(widths.maxOut),
|
||||||
|
headers.thinking.padEnd(widths.thinking),
|
||||||
|
headers.images.padEnd(widths.images),
|
||||||
|
].join(" ");
|
||||||
|
console.log(headerLine);
|
||||||
|
// Print rows
|
||||||
|
for (const row of rows) {
|
||||||
|
const line = [
|
||||||
|
row.provider.padEnd(widths.provider),
|
||||||
|
row.model.padEnd(widths.model),
|
||||||
|
row.context.padEnd(widths.context),
|
||||||
|
row.maxOut.padEnd(widths.maxOut),
|
||||||
|
row.thinking.padEnd(widths.thinking),
|
||||||
|
row.images.padEnd(widths.images),
|
||||||
|
].join(" ");
|
||||||
|
console.log(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=list-models.js.map
|
||||||
1
dist/cli/list-models.js.map
vendored
Normal file
1
dist/cli/list-models.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
10
dist/cli/project-trust.d.ts
vendored
Normal file
10
dist/cli/project-trust.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { ProjectTrustContext } from "../core/extensions/types.ts";
|
||||||
|
import type { AppMode } from "../core/project-trust.ts";
|
||||||
|
import type { SettingsManager } from "../core/settings-manager.ts";
|
||||||
|
export declare function createProjectTrustContext(options: {
|
||||||
|
cwd: string;
|
||||||
|
mode: AppMode;
|
||||||
|
settingsManager: SettingsManager;
|
||||||
|
hasUI: boolean;
|
||||||
|
}): ProjectTrustContext;
|
||||||
|
//# sourceMappingURL=project-trust.d.ts.map
|
||||||
1
dist/cli/project-trust.d.ts.map
vendored
Normal file
1
dist/cli/project-trust.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"project-trust.d.ts","sourceRoot":"","sources":["../../src/cli/project-trust.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAGnE,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,EAAE,eAAe,CAAC;IACjC,KAAK,EAAE,OAAO,CAAC;CACf,GAAG,mBAAmB,CAkDtB","sourcesContent":["import chalk from \"chalk\";\nimport type { ProjectTrustContext } from \"../core/extensions/types.ts\";\nimport type { AppMode } from \"../core/project-trust.ts\";\nimport type { SettingsManager } from \"../core/settings-manager.ts\";\nimport { showStartupInput, showStartupSelector } from \"./startup-ui.ts\";\n\nexport function createProjectTrustContext(options: {\n\tcwd: string;\n\tmode: AppMode;\n\tsettingsManager: SettingsManager;\n\thasUI: boolean;\n}): ProjectTrustContext {\n\treturn {\n\t\tcwd: options.cwd,\n\t\tmode: options.mode === \"interactive\" ? \"tui\" : options.mode,\n\t\thasUI: options.hasUI,\n\t\tui: {\n\t\t\tselect: async (title, selectOptions) => {\n\t\t\t\tif (!options.hasUI) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tif (options.mode !== \"interactive\") {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn showStartupSelector(\n\t\t\t\t\toptions.settingsManager,\n\t\t\t\t\ttitle,\n\t\t\t\t\tselectOptions.map((option) => ({ label: option, value: option })),\n\t\t\t\t);\n\t\t\t},\n\t\t\tconfirm: async (title, message) => {\n\t\t\t\tif (!options.hasUI) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (options.mode !== \"interactive\") {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\treturn (\n\t\t\t\t\t(await showStartupSelector(options.settingsManager, `${title}\\n${message}`, [\n\t\t\t\t\t\t{ label: \"Yes\", value: true },\n\t\t\t\t\t\t{ label: \"No\", value: false },\n\t\t\t\t\t])) ?? false\n\t\t\t\t);\n\t\t\t},\n\t\t\tinput: async (title, placeholder) => {\n\t\t\t\tif (!options.hasUI) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tif (options.mode !== \"interactive\") {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn showStartupInput(options.settingsManager, title, placeholder);\n\t\t\t},\n\t\t\tnotify: (message, type = \"info\") => {\n\t\t\t\tif (options.mode !== \"interactive\") {\n\t\t\t\t\tconst color = type === \"error\" ? chalk.red : type === \"warning\" ? chalk.yellow : chalk.cyan;\n\t\t\t\t\tconsole.error(color(message));\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t};\n}\n"]}
|
||||||
48
dist/cli/project-trust.js
vendored
Normal file
48
dist/cli/project-trust.js
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { showStartupInput, showStartupSelector } from "./startup-ui.js";
|
||||||
|
export function createProjectTrustContext(options) {
|
||||||
|
return {
|
||||||
|
cwd: options.cwd,
|
||||||
|
mode: options.mode === "interactive" ? "tui" : options.mode,
|
||||||
|
hasUI: options.hasUI,
|
||||||
|
ui: {
|
||||||
|
select: async (title, selectOptions) => {
|
||||||
|
if (!options.hasUI) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (options.mode !== "interactive") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return showStartupSelector(options.settingsManager, title, selectOptions.map((option) => ({ label: option, value: option })));
|
||||||
|
},
|
||||||
|
confirm: async (title, message) => {
|
||||||
|
if (!options.hasUI) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (options.mode !== "interactive") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ((await showStartupSelector(options.settingsManager, `${title}\n${message}`, [
|
||||||
|
{ label: "Yes", value: true },
|
||||||
|
{ label: "No", value: false },
|
||||||
|
])) ?? false);
|
||||||
|
},
|
||||||
|
input: async (title, placeholder) => {
|
||||||
|
if (!options.hasUI) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (options.mode !== "interactive") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return showStartupInput(options.settingsManager, title, placeholder);
|
||||||
|
},
|
||||||
|
notify: (message, type = "info") => {
|
||||||
|
if (options.mode !== "interactive") {
|
||||||
|
const color = type === "error" ? chalk.red : type === "warning" ? chalk.yellow : chalk.cyan;
|
||||||
|
console.error(color(message));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=project-trust.js.map
|
||||||
1
dist/cli/project-trust.js.map
vendored
Normal file
1
dist/cli/project-trust.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"project-trust.js","sourceRoot":"","sources":["../../src/cli/project-trust.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAExE,MAAM,UAAU,yBAAyB,CAAC,OAKzC,EAAuB;IACvB,OAAO;QACN,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI;QAC3D,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,EAAE,EAAE;YACH,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC;gBACvC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;oBACpB,OAAO,SAAS,CAAC;gBAClB,CAAC;gBACD,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBACpC,OAAO,SAAS,CAAC;gBAClB,CAAC;gBACD,OAAO,mBAAmB,CACzB,OAAO,CAAC,eAAe,EACvB,KAAK,EACL,aAAa,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CACjE,CAAC;YAAA,CACF;YACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;gBAClC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;oBACpB,OAAO,KAAK,CAAC;gBACd,CAAC;gBACD,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBACpC,OAAO,KAAK,CAAC;gBACd,CAAC;gBACD,OAAO,CACN,CAAC,MAAM,mBAAmB,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,KAAK,KAAK,OAAO,EAAE,EAAE;oBAC3E,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;oBAC7B,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;iBAC7B,CAAC,CAAC,IAAI,KAAK,CACZ,CAAC;YAAA,CACF;YACD,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,CAAC;gBACpC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;oBACpB,OAAO,SAAS,CAAC;gBAClB,CAAC;gBACD,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBACpC,OAAO,SAAS,CAAC;gBAClB,CAAC;gBACD,OAAO,gBAAgB,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;YAAA,CACrE;YACD,MAAM,EAAE,CAAC,OAAO,EAAE,IAAI,GAAG,MAAM,EAAE,EAAE,CAAC;gBACnC,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBACpC,MAAM,KAAK,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;oBAC5F,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC/B,CAAC;YAAA,CACD;SACD;KACD,CAAC;AAAA,CACF","sourcesContent":["import chalk from \"chalk\";\nimport type { ProjectTrustContext } from \"../core/extensions/types.ts\";\nimport type { AppMode } from \"../core/project-trust.ts\";\nimport type { SettingsManager } from \"../core/settings-manager.ts\";\nimport { showStartupInput, showStartupSelector } from \"./startup-ui.ts\";\n\nexport function createProjectTrustContext(options: {\n\tcwd: string;\n\tmode: AppMode;\n\tsettingsManager: SettingsManager;\n\thasUI: boolean;\n}): ProjectTrustContext {\n\treturn {\n\t\tcwd: options.cwd,\n\t\tmode: options.mode === \"interactive\" ? \"tui\" : options.mode,\n\t\thasUI: options.hasUI,\n\t\tui: {\n\t\t\tselect: async (title, selectOptions) => {\n\t\t\t\tif (!options.hasUI) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tif (options.mode !== \"interactive\") {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn showStartupSelector(\n\t\t\t\t\toptions.settingsManager,\n\t\t\t\t\ttitle,\n\t\t\t\t\tselectOptions.map((option) => ({ label: option, value: option })),\n\t\t\t\t);\n\t\t\t},\n\t\t\tconfirm: async (title, message) => {\n\t\t\t\tif (!options.hasUI) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (options.mode !== \"interactive\") {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\treturn (\n\t\t\t\t\t(await showStartupSelector(options.settingsManager, `${title}\\n${message}`, [\n\t\t\t\t\t\t{ label: \"Yes\", value: true },\n\t\t\t\t\t\t{ label: \"No\", value: false },\n\t\t\t\t\t])) ?? false\n\t\t\t\t);\n\t\t\t},\n\t\t\tinput: async (title, placeholder) => {\n\t\t\t\tif (!options.hasUI) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tif (options.mode !== \"interactive\") {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn showStartupInput(options.settingsManager, title, placeholder);\n\t\t\t},\n\t\t\tnotify: (message, type = \"info\") => {\n\t\t\t\tif (options.mode !== \"interactive\") {\n\t\t\t\t\tconst color = type === \"error\" ? chalk.red : type === \"warning\" ? chalk.yellow : chalk.cyan;\n\t\t\t\t\tconsole.error(color(message));\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t};\n}\n"]}
|
||||||
10
dist/cli/session-picker.d.ts
vendored
Normal file
10
dist/cli/session-picker.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* TUI session selector for --resume flag
|
||||||
|
*/
|
||||||
|
import type { SessionInfo, SessionListProgress } from "../core/session-manager.ts";
|
||||||
|
import type { SettingsManager } from "../core/settings-manager.ts";
|
||||||
|
type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;
|
||||||
|
/** Show TUI session selector and return selected session path or null if cancelled */
|
||||||
|
export declare function selectSession(currentSessionsLoader: SessionsLoader, allSessionsLoader: SessionsLoader, settingsManager: SettingsManager): Promise<string | null>;
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=session-picker.d.ts.map
|
||||||
1
dist/cli/session-picker.d.ts.map
vendored
Normal file
1
dist/cli/session-picker.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"session-picker.d.ts","sourceRoot":"","sources":["../../src/cli/session-picker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAInE,KAAK,cAAc,GAAG,CAAC,UAAU,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAEnF,sFAAsF;AACtF,wBAAsB,aAAa,CAClC,qBAAqB,EAAE,cAAc,EACrC,iBAAiB,EAAE,cAAc,EACjC,eAAe,EAAE,eAAe,GAC9B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoCxB","sourcesContent":["/**\n * TUI session selector for --resume flag\n */\n\nimport { setKeybindings } from \"@earendil-works/pi-tui\";\nimport { KeybindingsManager } from \"../core/keybindings.ts\";\nimport type { SessionInfo, SessionListProgress } from \"../core/session-manager.ts\";\nimport type { SettingsManager } from \"../core/settings-manager.ts\";\nimport { SessionSelectorComponent } from \"../modes/interactive/components/session-selector.ts\";\nimport { createStartupTui, startStartupTui } from \"./startup-ui.ts\";\n\ntype SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;\n\n/** Show TUI session selector and return selected session path or null if cancelled */\nexport async function selectSession(\n\tcurrentSessionsLoader: SessionsLoader,\n\tallSessionsLoader: SessionsLoader,\n\tsettingsManager: SettingsManager,\n): Promise<string | null> {\n\tconst ui = await createStartupTui(settingsManager);\n\treturn new Promise((resolve) => {\n\t\tconst keybindings = KeybindingsManager.create();\n\t\tsetKeybindings(keybindings);\n\t\tlet resolved = false;\n\n\t\tconst selector = new SessionSelectorComponent(\n\t\t\tcurrentSessionsLoader,\n\t\t\tallSessionsLoader,\n\t\t\t(path: string) => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tui.stop();\n\t\t\t\t\tresolve(path);\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tui.stop();\n\t\t\t\t\tresolve(null);\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tui.stop();\n\t\t\t\tprocess.exit(0);\n\t\t\t},\n\t\t\t() => ui.requestRender(),\n\t\t\t{ showRenameHint: false, keybindings },\n\t\t);\n\n\t\tui.addChild(selector);\n\t\tui.setFocus(selector.getSessionList());\n\t\tstartStartupTui(ui, settingsManager);\n\t});\n}\n"]}
|
||||||
36
dist/cli/session-picker.js
vendored
Normal file
36
dist/cli/session-picker.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* TUI session selector for --resume flag
|
||||||
|
*/
|
||||||
|
import { setKeybindings } from "@earendil-works/pi-tui";
|
||||||
|
import { KeybindingsManager } from "../core/keybindings.js";
|
||||||
|
import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js";
|
||||||
|
import { createStartupTui, startStartupTui } from "./startup-ui.js";
|
||||||
|
/** Show TUI session selector and return selected session path or null if cancelled */
|
||||||
|
export async function selectSession(currentSessionsLoader, allSessionsLoader, settingsManager) {
|
||||||
|
const ui = await createStartupTui(settingsManager);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const keybindings = KeybindingsManager.create();
|
||||||
|
setKeybindings(keybindings);
|
||||||
|
let resolved = false;
|
||||||
|
const selector = new SessionSelectorComponent(currentSessionsLoader, allSessionsLoader, (path) => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
ui.stop();
|
||||||
|
resolve(path);
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
ui.stop();
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
ui.stop();
|
||||||
|
process.exit(0);
|
||||||
|
}, () => ui.requestRender(), { showRenameHint: false, keybindings });
|
||||||
|
ui.addChild(selector);
|
||||||
|
ui.setFocus(selector.getSessionList());
|
||||||
|
startStartupTui(ui, settingsManager);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=session-picker.js.map
|
||||||
1
dist/cli/session-picker.js.map
vendored
Normal file
1
dist/cli/session-picker.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"session-picker.js","sourceRoot":"","sources":["../../src/cli/session-picker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,qDAAqD,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAIpE,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,qBAAqC,EACrC,iBAAiC,EACjC,eAAgC,EACP;IACzB,MAAM,EAAE,GAAG,MAAM,gBAAgB,CAAC,eAAe,CAAC,CAAC;IACnD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/B,MAAM,WAAW,GAAG,kBAAkB,CAAC,MAAM,EAAE,CAAC;QAChD,cAAc,CAAC,WAAW,CAAC,CAAC;QAC5B,IAAI,QAAQ,GAAG,KAAK,CAAC;QAErB,MAAM,QAAQ,GAAG,IAAI,wBAAwB,CAC5C,qBAAqB,EACrB,iBAAiB,EACjB,CAAC,IAAY,EAAE,EAAE,CAAC;YACjB,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACf,QAAQ,GAAG,IAAI,CAAC;gBAChB,EAAE,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,CAAC,IAAI,CAAC,CAAC;YACf,CAAC;QAAA,CACD,EACD,GAAG,EAAE,CAAC;YACL,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACf,QAAQ,GAAG,IAAI,CAAC;gBAChB,EAAE,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,CAAC,IAAI,CAAC,CAAC;YACf,CAAC;QAAA,CACD,EACD,GAAG,EAAE,CAAC;YACL,EAAE,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAA,CAChB,EACD,GAAG,EAAE,CAAC,EAAE,CAAC,aAAa,EAAE,EACxB,EAAE,cAAc,EAAE,KAAK,EAAE,WAAW,EAAE,CACtC,CAAC;QAEF,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACtB,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC;QACvC,eAAe,CAAC,EAAE,EAAE,eAAe,CAAC,CAAC;IAAA,CACrC,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * TUI session selector for --resume flag\n */\n\nimport { setKeybindings } from \"@earendil-works/pi-tui\";\nimport { KeybindingsManager } from \"../core/keybindings.ts\";\nimport type { SessionInfo, SessionListProgress } from \"../core/session-manager.ts\";\nimport type { SettingsManager } from \"../core/settings-manager.ts\";\nimport { SessionSelectorComponent } from \"../modes/interactive/components/session-selector.ts\";\nimport { createStartupTui, startStartupTui } from \"./startup-ui.ts\";\n\ntype SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;\n\n/** Show TUI session selector and return selected session path or null if cancelled */\nexport async function selectSession(\n\tcurrentSessionsLoader: SessionsLoader,\n\tallSessionsLoader: SessionsLoader,\n\tsettingsManager: SettingsManager,\n): Promise<string | null> {\n\tconst ui = await createStartupTui(settingsManager);\n\treturn new Promise((resolve) => {\n\t\tconst keybindings = KeybindingsManager.create();\n\t\tsetKeybindings(keybindings);\n\t\tlet resolved = false;\n\n\t\tconst selector = new SessionSelectorComponent(\n\t\t\tcurrentSessionsLoader,\n\t\t\tallSessionsLoader,\n\t\t\t(path: string) => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tui.stop();\n\t\t\t\t\tresolve(path);\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tif (!resolved) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tui.stop();\n\t\t\t\t\tresolve(null);\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tui.stop();\n\t\t\t\tprocess.exit(0);\n\t\t\t},\n\t\t\t() => ui.requestRender(),\n\t\t\t{ showRenameHint: false, keybindings },\n\t\t);\n\n\t\tui.addChild(selector);\n\t\tui.setFocus(selector.getSessionList());\n\t\tstartStartupTui(ui, settingsManager);\n\t});\n}\n"]}
|
||||||
20
dist/cli/startup-ui.d.ts
vendored
Normal file
20
dist/cli/startup-ui.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TUI } from "@earendil-works/pi-tui";
|
||||||
|
import { SettingsManager } from "../core/settings-manager.ts";
|
||||||
|
export declare function createStartupTui(settingsManager: SettingsManager): Promise<TUI>;
|
||||||
|
export declare function startStartupTui(ui: TUI, settingsManager: SettingsManager): void;
|
||||||
|
/**
|
||||||
|
* First-time setup runs when all of these hold:
|
||||||
|
* - this is the official Pi distribution (not a fork/rebrand)
|
||||||
|
* - experimental features are enabled (PI_EXPERIMENTAL=1)
|
||||||
|
* - the default agent directory is used (no custom agent dir override)
|
||||||
|
* - setup was not completed before (settings.json does not exist)
|
||||||
|
*/
|
||||||
|
export declare function shouldRunFirstTimeSetup(settingsPath?: string): boolean;
|
||||||
|
export declare function showStartupSelector<T>(settingsManager: SettingsManager, title: string, options: Array<{
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
}>): Promise<T | undefined>;
|
||||||
|
/** Show the first-time setup dialog and persist the result */
|
||||||
|
export declare function showFirstTimeSetup(settingsManager: SettingsManager): Promise<void>;
|
||||||
|
export declare function showStartupInput(settingsManager: SettingsManager, title: string, placeholder?: string): Promise<string | undefined>;
|
||||||
|
//# sourceMappingURL=startup-ui.d.ts.map
|
||||||
1
dist/cli/startup-ui.d.ts.map
vendored
Normal file
1
dist/cli/startup-ui.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
184
dist/cli/startup-ui.js
vendored
Normal file
184
dist/cli/startup-ui.js
vendored
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getAgentDir, getSettingsPath, PACKAGE_NAME } from "../config.js";
|
||||||
|
import { areExperimentalFeaturesEnabled } from "../core/experimental.js";
|
||||||
|
import { KeybindingsManager } from "../core/keybindings.js";
|
||||||
|
import { DefaultPackageManager } from "../core/package-manager.js";
|
||||||
|
import { SettingsManager } from "../core/settings-manager.js";
|
||||||
|
import { ExtensionInputComponent } from "../modes/interactive/components/extension-input.js";
|
||||||
|
import { ExtensionSelectorComponent } from "../modes/interactive/components/extension-selector.js";
|
||||||
|
import { FirstTimeSetupComponent, } from "../modes/interactive/components/first-time-setup.js";
|
||||||
|
import { detectTerminalBackgroundFromEnv, detectTerminalThemeForAuto, initTheme, loadThemeFromPath, parseAutoThemeSetting, resolveThemeSetting, setRegisteredThemes, setTheme, } from "../modes/interactive/theme/theme.js";
|
||||||
|
const OFFICIAL_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
||||||
|
const OFFICIAL_APP_NAME = "pi";
|
||||||
|
const OFFICIAL_CONFIG_DIR_NAME = ".pi";
|
||||||
|
function isOfficialDistribution({ packageName, appName, configDirName }) {
|
||||||
|
return (packageName === OFFICIAL_PACKAGE_NAME &&
|
||||||
|
appName === OFFICIAL_APP_NAME &&
|
||||||
|
configDirName === OFFICIAL_CONFIG_DIR_NAME);
|
||||||
|
}
|
||||||
|
function loadThemes(resources) {
|
||||||
|
const themes = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const resource of resources) {
|
||||||
|
if (!resource.enabled)
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const loadedTheme = loadThemeFromPath(resource.path);
|
||||||
|
if (loadedTheme.name) {
|
||||||
|
if (seen.has(loadedTheme.name))
|
||||||
|
continue;
|
||||||
|
seen.add(loadedTheme.name);
|
||||||
|
}
|
||||||
|
themes.push(loadedTheme);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Startup prompts should not fail because a theme is broken. The normal
|
||||||
|
// resource loader reports theme diagnostics later in startup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return themes;
|
||||||
|
}
|
||||||
|
async function loadStartupThemes(settingsManager) {
|
||||||
|
const globalSettingsManager = SettingsManager.inMemory(settingsManager.getGlobalSettings(), {
|
||||||
|
projectTrusted: false,
|
||||||
|
});
|
||||||
|
const packageManager = new DefaultPackageManager({
|
||||||
|
cwd: process.cwd(),
|
||||||
|
agentDir: getAgentDir(),
|
||||||
|
settingsManager: globalSettingsManager,
|
||||||
|
});
|
||||||
|
const resolvedPaths = await packageManager.resolve(async () => "skip");
|
||||||
|
return loadThemes(resolvedPaths.themes);
|
||||||
|
}
|
||||||
|
export async function createStartupTui(settingsManager) {
|
||||||
|
setRegisteredThemes(await loadStartupThemes(settingsManager));
|
||||||
|
const terminalTheme = detectTerminalBackgroundFromEnv().theme;
|
||||||
|
initTheme(resolveThemeSetting(settingsManager.getThemeSetting(), terminalTheme) ?? terminalTheme);
|
||||||
|
setKeybindings(KeybindingsManager.create());
|
||||||
|
const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor(), getAgentDir());
|
||||||
|
ui.setClearOnShrink(settingsManager.getClearOnShrink());
|
||||||
|
return ui;
|
||||||
|
}
|
||||||
|
export function startStartupTui(ui, settingsManager) {
|
||||||
|
ui.start();
|
||||||
|
void applyDetectedStartupTheme(ui, settingsManager);
|
||||||
|
}
|
||||||
|
async function applyDetectedStartupTheme(ui, settingsManager) {
|
||||||
|
const themeSetting = settingsManager.getThemeSetting();
|
||||||
|
if (themeSetting && !parseAutoThemeSetting(themeSetting))
|
||||||
|
return;
|
||||||
|
const terminalTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 });
|
||||||
|
setTheme(resolveThemeSetting(themeSetting, terminalTheme) ?? terminalTheme);
|
||||||
|
ui.invalidate();
|
||||||
|
ui.requestRender();
|
||||||
|
}
|
||||||
|
async function clearStartupTui(ui) {
|
||||||
|
ui.clear();
|
||||||
|
ui.requestRender();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* First-time setup runs when all of these hold:
|
||||||
|
* - this is the official Pi distribution (not a fork/rebrand)
|
||||||
|
* - experimental features are enabled (PI_EXPERIMENTAL=1)
|
||||||
|
* - the default agent directory is used (no custom agent dir override)
|
||||||
|
* - setup was not completed before (settings.json does not exist)
|
||||||
|
*/
|
||||||
|
export function shouldRunFirstTimeSetup(settingsPath = getSettingsPath()) {
|
||||||
|
if (!isOfficialDistribution({
|
||||||
|
packageName: PACKAGE_NAME,
|
||||||
|
appName: APP_NAME,
|
||||||
|
configDirName: CONFIG_DIR_NAME,
|
||||||
|
})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!areExperimentalFeaturesEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (process.env[ENV_AGENT_DIR]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !existsSync(settingsPath);
|
||||||
|
}
|
||||||
|
export async function showStartupSelector(settingsManager, title, options) {
|
||||||
|
const ui = await createStartupTui(settingsManager);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const finish = async (result) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
await clearStartupTui(ui);
|
||||||
|
ui.stop();
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
const selector = new ExtensionSelectorComponent(title, options.map((option) => option.label), (option) => void finish(options.find((entry) => entry.label === option)?.value), () => void finish(undefined), { tui: ui });
|
||||||
|
ui.addChild(selector);
|
||||||
|
ui.setFocus(selector);
|
||||||
|
startStartupTui(ui, settingsManager);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** Show the first-time setup dialog and persist the result */
|
||||||
|
export async function showFirstTimeSetup(settingsManager) {
|
||||||
|
const ui = await createStartupTui(settingsManager);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const finish = async (result) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
if (result) {
|
||||||
|
settingsManager.setTheme(result.theme);
|
||||||
|
settingsManager.setEnableAnalytics(result.shareAnalytics);
|
||||||
|
await settingsManager.flush();
|
||||||
|
}
|
||||||
|
await clearStartupTui(ui);
|
||||||
|
ui.stop();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const showSetup = async () => {
|
||||||
|
ui.start();
|
||||||
|
const detectedTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 });
|
||||||
|
setTheme(detectedTheme);
|
||||||
|
const component = new FirstTimeSetupComponent({
|
||||||
|
detectedTheme,
|
||||||
|
onThemePreview: (themeName) => {
|
||||||
|
setTheme(themeName);
|
||||||
|
ui.requestRender();
|
||||||
|
},
|
||||||
|
onSubmit: (result) => void finish(result),
|
||||||
|
onCancel: () => void finish(undefined),
|
||||||
|
});
|
||||||
|
ui.addChild(component);
|
||||||
|
ui.setFocus(component);
|
||||||
|
ui.requestRender();
|
||||||
|
};
|
||||||
|
void showSetup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function showStartupInput(settingsManager, title, placeholder) {
|
||||||
|
const ui = await createStartupTui(settingsManager);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const finish = async (result) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
input.dispose();
|
||||||
|
await clearStartupTui(ui);
|
||||||
|
ui.stop();
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
const input = new ExtensionInputComponent(title, placeholder, (value) => void finish(value), () => void finish(undefined), {
|
||||||
|
tui: ui,
|
||||||
|
});
|
||||||
|
ui.addChild(input);
|
||||||
|
ui.setFocus(input);
|
||||||
|
startStartupTui(ui, settingsManager);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=startup-ui.js.map
|
||||||
1
dist/cli/startup-ui.js.map
vendored
Normal file
1
dist/cli/startup-ui.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
96
dist/config.d.ts
vendored
Normal file
96
dist/config.d.ts
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/**
|
||||||
|
* Detect if we're running as a Bun compiled binary.
|
||||||
|
* Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path)
|
||||||
|
*/
|
||||||
|
export declare const isBunBinary: boolean;
|
||||||
|
/** Detect if Bun is the runtime (compiled binary or bun run) */
|
||||||
|
export declare const isBunRuntime: boolean;
|
||||||
|
export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown";
|
||||||
|
interface SelfUpdateCommandStep {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
display: string;
|
||||||
|
}
|
||||||
|
export interface SelfUpdateCommand extends SelfUpdateCommandStep {
|
||||||
|
steps?: SelfUpdateCommandStep[];
|
||||||
|
}
|
||||||
|
export type SelfUpdatePackageTarget = string | {
|
||||||
|
packageName: string;
|
||||||
|
installSpec?: string;
|
||||||
|
};
|
||||||
|
export declare function detectInstallMethod(): InstallMethod;
|
||||||
|
export declare function getSelfUpdateCommand(packageName: string, npmCommand?: string[], updatePackageTarget?: SelfUpdatePackageTarget): SelfUpdateCommand | undefined;
|
||||||
|
export declare function getSelfUpdateUnavailableInstruction(packageName: string, npmCommand?: string[], updatePackageTarget?: SelfUpdatePackageTarget): string;
|
||||||
|
export declare function getUpdateInstruction(packageName: string): string;
|
||||||
|
/**
|
||||||
|
* Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).
|
||||||
|
* - For Bun binary: returns the directory containing the executable
|
||||||
|
* - For Node.js (dist/): returns __dirname (the dist/ directory)
|
||||||
|
* - For tsx (src/): returns parent directory (the package root)
|
||||||
|
*/
|
||||||
|
export declare function getPackageDir(): string;
|
||||||
|
/**
|
||||||
|
* Get path to built-in themes directory (shipped with package)
|
||||||
|
* - For Bun binary: theme/ next to executable
|
||||||
|
* - For Node.js (dist/): dist/modes/interactive/theme/
|
||||||
|
* - For tsx (src/): src/modes/interactive/theme/
|
||||||
|
*/
|
||||||
|
export declare function getThemesDir(): string;
|
||||||
|
/**
|
||||||
|
* Get path to HTML export template directory (shipped with package)
|
||||||
|
* - For Bun binary: export-html/ next to executable
|
||||||
|
* - For Node.js (dist/): dist/core/export-html/
|
||||||
|
* - For tsx (src/): src/core/export-html/
|
||||||
|
*/
|
||||||
|
export declare function getExportTemplateDir(): string;
|
||||||
|
/** Get path to package.json */
|
||||||
|
export declare function getPackageJsonPath(): string;
|
||||||
|
/** Get path to README.md */
|
||||||
|
export declare function getReadmePath(): string;
|
||||||
|
/** Get path to docs directory */
|
||||||
|
export declare function getDocsPath(): string;
|
||||||
|
/** Get path to examples directory */
|
||||||
|
export declare function getExamplesPath(): string;
|
||||||
|
/** Get path to CHANGELOG.md */
|
||||||
|
export declare function getChangelogPath(): string;
|
||||||
|
/**
|
||||||
|
* Get path to built-in interactive assets directory.
|
||||||
|
* - For Bun binary: assets/ next to executable
|
||||||
|
* - For Node.js (dist/): dist/modes/interactive/assets/
|
||||||
|
* - For tsx (src/): src/modes/interactive/assets/
|
||||||
|
*/
|
||||||
|
export declare function getInteractiveAssetsDir(): string;
|
||||||
|
/** Get path to a bundled interactive asset */
|
||||||
|
export declare function getBundledInteractiveAssetPath(name: string): string;
|
||||||
|
export declare const PACKAGE_NAME: string;
|
||||||
|
export declare const APP_NAME: string;
|
||||||
|
export declare const APP_TITLE: string;
|
||||||
|
export declare const CONFIG_DIR_NAME: string;
|
||||||
|
export declare const VERSION: string;
|
||||||
|
export declare const ENV_AGENT_DIR: string;
|
||||||
|
export declare const ENV_SESSION_DIR: string;
|
||||||
|
export declare function expandTildePath(path: string): string;
|
||||||
|
/** Get the share viewer URL for a gist ID */
|
||||||
|
export declare function getShareViewerUrl(gistId: string): string;
|
||||||
|
/** Get the agent config directory (e.g., ~/.pi/agent/) */
|
||||||
|
export declare function getAgentDir(): string;
|
||||||
|
/** Get path to user's custom themes directory */
|
||||||
|
export declare function getCustomThemesDir(): string;
|
||||||
|
/** Get path to models.json */
|
||||||
|
export declare function getModelsPath(): string;
|
||||||
|
/** Get path to auth.json */
|
||||||
|
export declare function getAuthPath(): string;
|
||||||
|
/** Get path to settings.json */
|
||||||
|
export declare function getSettingsPath(): string;
|
||||||
|
/** Get path to tools directory */
|
||||||
|
export declare function getToolsDir(): string;
|
||||||
|
/** Get path to managed binaries directory (fd, rg) */
|
||||||
|
export declare function getBinDir(): string;
|
||||||
|
/** Get path to prompt templates directory */
|
||||||
|
export declare function getPromptsDir(): string;
|
||||||
|
/** Get path to sessions directory */
|
||||||
|
export declare function getSessionsDir(): string;
|
||||||
|
/** Get path to debug log file */
|
||||||
|
export declare function getDebugLogPath(): string;
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=config.d.ts.map
|
||||||
1
dist/config.d.ts.map
vendored
Normal file
1
dist/config.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
455
dist/config.js
vendored
Normal file
455
dist/config.js
vendored
Normal file
|
|
@ -0,0 +1,455 @@
|
||||||
|
import { accessSync, constants, existsSync, readFileSync, realpathSync } from "fs";
|
||||||
|
import { homedir } from "os";
|
||||||
|
import { basename, dirname, join, resolve, sep, win32 } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { spawnProcessSync } from "./utils/child-process.js";
|
||||||
|
import { normalizePath } from "./utils/paths.js";
|
||||||
|
// =============================================================================
|
||||||
|
// Package Detection
|
||||||
|
// =============================================================================
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
/**
|
||||||
|
* Detect if we're running as a Bun compiled binary.
|
||||||
|
* Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path)
|
||||||
|
*/
|
||||||
|
export const isBunBinary = import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN");
|
||||||
|
/** Detect if Bun is the runtime (compiled binary or bun run) */
|
||||||
|
export const isBunRuntime = !!process.versions.bun;
|
||||||
|
function normalizeSelfUpdatePackageTarget(target) {
|
||||||
|
if (typeof target === "string") {
|
||||||
|
return { packageName: target, installSpec: target };
|
||||||
|
}
|
||||||
|
return { packageName: target.packageName, installSpec: target.installSpec ?? target.packageName };
|
||||||
|
}
|
||||||
|
function makeSelfUpdateCommand(installStep, uninstallStep) {
|
||||||
|
if (!uninstallStep)
|
||||||
|
return installStep;
|
||||||
|
return {
|
||||||
|
...installStep,
|
||||||
|
display: `${uninstallStep.display} && ${installStep.display}`,
|
||||||
|
steps: [uninstallStep, installStep],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function makeSelfUpdateCommandStep(command, args) {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function detectInstallMethod() {
|
||||||
|
if (isBunBinary) {
|
||||||
|
return "bun-binary";
|
||||||
|
}
|
||||||
|
const resolvedPath = `${__dirname}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/");
|
||||||
|
if (resolvedPath.includes("/pnpm/") || resolvedPath.includes("/.pnpm/")) {
|
||||||
|
return "pnpm";
|
||||||
|
}
|
||||||
|
if (resolvedPath.includes("/yarn/") || resolvedPath.includes("/.yarn/")) {
|
||||||
|
return "yarn";
|
||||||
|
}
|
||||||
|
if (isBunRuntime || resolvedPath.includes("/install/global/node_modules/")) {
|
||||||
|
return "bun";
|
||||||
|
}
|
||||||
|
if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) {
|
||||||
|
return "npm";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
function getInferredNpmInstall() {
|
||||||
|
const packageDir = getPackageDir();
|
||||||
|
const path = process.platform === "win32" || packageDir.includes("\\") ? win32 : { basename, dirname };
|
||||||
|
const parent = path.dirname(packageDir);
|
||||||
|
let root;
|
||||||
|
if (path.basename(parent).startsWith("@") && path.basename(path.dirname(parent)) === "node_modules") {
|
||||||
|
root = path.dirname(parent);
|
||||||
|
}
|
||||||
|
else if (path.basename(parent) === "node_modules") {
|
||||||
|
root = parent;
|
||||||
|
}
|
||||||
|
if (!root)
|
||||||
|
return undefined;
|
||||||
|
const rootParent = path.dirname(root);
|
||||||
|
if (path.basename(rootParent) === "lib")
|
||||||
|
return { root, prefix: path.dirname(rootParent) };
|
||||||
|
// Windows global npm prefixes use `<prefix>\\node_modules`, which is
|
||||||
|
// indistinguishable from local project installs by path shape alone. Do not
|
||||||
|
// infer unsupported Windows custom prefixes without `npm root -g` evidence.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
function getSelfUpdateCommandForMethod(method, installedPackageName, updatePackageTarget = installedPackageName, npmCommand) {
|
||||||
|
const target = normalizeSelfUpdatePackageTarget(updatePackageTarget);
|
||||||
|
switch (method) {
|
||||||
|
case "bun-binary":
|
||||||
|
return undefined;
|
||||||
|
case "pnpm": {
|
||||||
|
const match = readCommandOutput("pnpm", ["root", "-g"])
|
||||||
|
? undefined
|
||||||
|
: /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(getPackageDir());
|
||||||
|
const binDirArgs = match
|
||||||
|
? [`--config.global-bin-dir=${process.env.PNPM_HOME || dirname(dirname(match[1]))}`]
|
||||||
|
: [];
|
||||||
|
return makeSelfUpdateCommand(makeSelfUpdateCommandStep("pnpm", [
|
||||||
|
"install",
|
||||||
|
"-g",
|
||||||
|
"--ignore-scripts",
|
||||||
|
"--config.minimumReleaseAge=0",
|
||||||
|
...binDirArgs,
|
||||||
|
target.installSpec,
|
||||||
|
]), target.packageName === installedPackageName
|
||||||
|
? undefined
|
||||||
|
: makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]));
|
||||||
|
}
|
||||||
|
case "yarn":
|
||||||
|
return makeSelfUpdateCommand(makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", target.installSpec]), target.packageName === installedPackageName
|
||||||
|
? undefined
|
||||||
|
: makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]));
|
||||||
|
case "bun":
|
||||||
|
return makeSelfUpdateCommand(makeSelfUpdateCommandStep("bun", [
|
||||||
|
"install",
|
||||||
|
"-g",
|
||||||
|
"--ignore-scripts",
|
||||||
|
"--minimum-release-age=0",
|
||||||
|
target.installSpec,
|
||||||
|
]), target.packageName === installedPackageName
|
||||||
|
? undefined
|
||||||
|
: makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]));
|
||||||
|
case "npm": {
|
||||||
|
const [command = "npm", ...npmArgs] = npmCommand ?? [];
|
||||||
|
const inferred = npmCommand?.length ? undefined : getInferredNpmInstall();
|
||||||
|
const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])];
|
||||||
|
const installStep = makeSelfUpdateCommandStep(command, [
|
||||||
|
...prefixArgs,
|
||||||
|
"install",
|
||||||
|
"-g",
|
||||||
|
"--ignore-scripts",
|
||||||
|
"--min-release-age=0",
|
||||||
|
target.installSpec,
|
||||||
|
]);
|
||||||
|
const uninstallStep = target.packageName === installedPackageName
|
||||||
|
? undefined
|
||||||
|
: makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]);
|
||||||
|
return makeSelfUpdateCommand(installStep, uninstallStep);
|
||||||
|
}
|
||||||
|
case "unknown":
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function readCommandOutput(command, args, options = {}) {
|
||||||
|
const result = spawnProcessSync(command, args, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
if (result.status === 0)
|
||||||
|
return result.stdout.trim() || undefined;
|
||||||
|
if (options.requireSuccess) {
|
||||||
|
const reason = result.error?.message || result.stderr.trim() || `exit code ${result.status ?? "unknown"}`;
|
||||||
|
throw new Error(`Failed to run ${[command, ...args].join(" ")}: ${reason}`);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
function getGlobalPackageRoots(method, _packageName, npmCommand) {
|
||||||
|
switch (method) {
|
||||||
|
case "npm": {
|
||||||
|
const configured = !!npmCommand?.length;
|
||||||
|
const [command = "npm", ...npmArgs] = npmCommand ?? [];
|
||||||
|
if (configured && command === "bun") {
|
||||||
|
const bunBin = readCommandOutput(command, [...npmArgs, "pm", "bin", "-g"], {
|
||||||
|
requireSuccess: true,
|
||||||
|
});
|
||||||
|
const roots = [join(homedir(), ".bun", "install", "global", "node_modules")];
|
||||||
|
if (bunBin) {
|
||||||
|
roots.push(join(dirname(bunBin), "install", "global", "node_modules"));
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
const root = readCommandOutput(command, [...npmArgs, "root", "-g"], {
|
||||||
|
requireSuccess: configured,
|
||||||
|
});
|
||||||
|
const inferred = configured ? undefined : getInferredNpmInstall();
|
||||||
|
return [root, inferred?.root].filter((x) => !!x);
|
||||||
|
}
|
||||||
|
case "pnpm": {
|
||||||
|
const root = readCommandOutput("pnpm", ["root", "-g"]);
|
||||||
|
if (root)
|
||||||
|
return [root, dirname(root)];
|
||||||
|
const match = /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(getPackageDir());
|
||||||
|
return match ? [match[1]] : [];
|
||||||
|
}
|
||||||
|
case "yarn": {
|
||||||
|
const dir = readCommandOutput("yarn", ["global", "dir"]);
|
||||||
|
return dir ? [dir, join(dir, "node_modules")] : [];
|
||||||
|
}
|
||||||
|
case "bun": {
|
||||||
|
const bunBin = readCommandOutput("bun", ["pm", "bin", "-g"]);
|
||||||
|
const roots = [join(homedir(), ".bun", "install", "global", "node_modules")];
|
||||||
|
if (bunBin) {
|
||||||
|
roots.push(join(dirname(bunBin), "install", "global", "node_modules"));
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
case "bun-binary":
|
||||||
|
case "unknown":
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function normalizeExistingPathForComparison(path, resolveSymlinks) {
|
||||||
|
const resolvedPath = resolve(path);
|
||||||
|
if (!existsSync(resolvedPath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let normalizedPath = resolvedPath;
|
||||||
|
if (resolveSymlinks) {
|
||||||
|
try {
|
||||||
|
normalizedPath = realpathSync(resolvedPath);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
normalizedPath = normalizedPath.toLowerCase();
|
||||||
|
}
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
function getPathComparisonCandidates(path) {
|
||||||
|
return Array.from(new Set([normalizeExistingPathForComparison(path, false), normalizeExistingPathForComparison(path, true)].filter((candidate) => !!candidate)));
|
||||||
|
}
|
||||||
|
function getEntrypointPackageDir() {
|
||||||
|
const entrypoint = process.argv[1];
|
||||||
|
if (!entrypoint)
|
||||||
|
return undefined;
|
||||||
|
let dir = dirname(entrypoint);
|
||||||
|
while (dir !== dirname(dir)) {
|
||||||
|
if (existsSync(join(dir, "package.json"))) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
dir = dirname(dir);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
function isSelfUpdatePathWritable() {
|
||||||
|
const packageDir = getPackageDir();
|
||||||
|
try {
|
||||||
|
accessSync(packageDir, constants.W_OK);
|
||||||
|
accessSync(dirname(packageDir), constants.W_OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isManagedByGlobalPackageManager(method, packageName, npmCommand) {
|
||||||
|
const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter((dir) => !!dir);
|
||||||
|
const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir));
|
||||||
|
return getGlobalPackageRoots(method, packageName, npmCommand).some((root) => {
|
||||||
|
return getPathComparisonCandidates(root).some((normalizedRoot) => {
|
||||||
|
const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
|
||||||
|
return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function getSelfUpdateCommand(packageName, npmCommand, updatePackageTarget = packageName) {
|
||||||
|
const method = detectInstallMethod();
|
||||||
|
const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageTarget, npmCommand);
|
||||||
|
if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
export function getSelfUpdateUnavailableInstruction(packageName, npmCommand, updatePackageTarget = packageName) {
|
||||||
|
const method = detectInstallMethod();
|
||||||
|
const target = normalizeSelfUpdatePackageTarget(updatePackageTarget);
|
||||||
|
if (method === "bun-binary") {
|
||||||
|
return `Download from: https://github.com/earendil-works/pi-mono/releases/latest`;
|
||||||
|
}
|
||||||
|
const command = getSelfUpdateCommandForMethod(method, packageName, target, npmCommand);
|
||||||
|
if (command) {
|
||||||
|
if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) {
|
||||||
|
return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`;
|
||||||
|
}
|
||||||
|
return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`;
|
||||||
|
}
|
||||||
|
return `Update ${target.installSpec} using the package manager, wrapper, or source checkout that provides this installation.`;
|
||||||
|
}
|
||||||
|
export function getUpdateInstruction(packageName) {
|
||||||
|
const method = detectInstallMethod();
|
||||||
|
const command = getSelfUpdateCommandForMethod(method, packageName);
|
||||||
|
if (command) {
|
||||||
|
return `Run: ${command.display}`;
|
||||||
|
}
|
||||||
|
return getSelfUpdateUnavailableInstruction(packageName);
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// Package Asset Paths (shipped with executable)
|
||||||
|
// =============================================================================
|
||||||
|
/**
|
||||||
|
* Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).
|
||||||
|
* - For Bun binary: returns the directory containing the executable
|
||||||
|
* - For Node.js (dist/): returns __dirname (the dist/ directory)
|
||||||
|
* - For tsx (src/): returns parent directory (the package root)
|
||||||
|
*/
|
||||||
|
export function getPackageDir() {
|
||||||
|
// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)
|
||||||
|
const envDir = process.env.PI_PACKAGE_DIR;
|
||||||
|
if (envDir) {
|
||||||
|
return normalizePath(envDir);
|
||||||
|
}
|
||||||
|
if (isBunBinary) {
|
||||||
|
// Bun binary: process.execPath points to the compiled executable
|
||||||
|
return dirname(process.execPath);
|
||||||
|
}
|
||||||
|
// Node.js: walk up from __dirname until we find package.json
|
||||||
|
let dir = __dirname;
|
||||||
|
while (dir !== dirname(dir)) {
|
||||||
|
if (existsSync(join(dir, "package.json"))) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
dir = dirname(dir);
|
||||||
|
}
|
||||||
|
// Fallback (shouldn't happen)
|
||||||
|
return __dirname;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get path to built-in themes directory (shipped with package)
|
||||||
|
* - For Bun binary: theme/ next to executable
|
||||||
|
* - For Node.js (dist/): dist/modes/interactive/theme/
|
||||||
|
* - For tsx (src/): src/modes/interactive/theme/
|
||||||
|
*/
|
||||||
|
export function getThemesDir() {
|
||||||
|
if (isBunBinary) {
|
||||||
|
return join(getPackageDir(), "theme");
|
||||||
|
}
|
||||||
|
// Theme is in modes/interactive/theme/ relative to src/ or dist/
|
||||||
|
const packageDir = getPackageDir();
|
||||||
|
const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
|
||||||
|
return join(packageDir, srcOrDist, "modes", "interactive", "theme");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get path to HTML export template directory (shipped with package)
|
||||||
|
* - For Bun binary: export-html/ next to executable
|
||||||
|
* - For Node.js (dist/): dist/core/export-html/
|
||||||
|
* - For tsx (src/): src/core/export-html/
|
||||||
|
*/
|
||||||
|
export function getExportTemplateDir() {
|
||||||
|
if (isBunBinary) {
|
||||||
|
return join(getPackageDir(), "export-html");
|
||||||
|
}
|
||||||
|
const packageDir = getPackageDir();
|
||||||
|
const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
|
||||||
|
return join(packageDir, srcOrDist, "core", "export-html");
|
||||||
|
}
|
||||||
|
/** Get path to package.json */
|
||||||
|
export function getPackageJsonPath() {
|
||||||
|
return join(getPackageDir(), "package.json");
|
||||||
|
}
|
||||||
|
/** Get path to README.md */
|
||||||
|
export function getReadmePath() {
|
||||||
|
return resolve(join(getPackageDir(), "README.md"));
|
||||||
|
}
|
||||||
|
/** Get path to docs directory */
|
||||||
|
export function getDocsPath() {
|
||||||
|
return resolve(join(getPackageDir(), "docs"));
|
||||||
|
}
|
||||||
|
/** Get path to examples directory */
|
||||||
|
export function getExamplesPath() {
|
||||||
|
return resolve(join(getPackageDir(), "examples"));
|
||||||
|
}
|
||||||
|
/** Get path to CHANGELOG.md */
|
||||||
|
export function getChangelogPath() {
|
||||||
|
return resolve(join(getPackageDir(), "CHANGELOG.md"));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get path to built-in interactive assets directory.
|
||||||
|
* - For Bun binary: assets/ next to executable
|
||||||
|
* - For Node.js (dist/): dist/modes/interactive/assets/
|
||||||
|
* - For tsx (src/): src/modes/interactive/assets/
|
||||||
|
*/
|
||||||
|
export function getInteractiveAssetsDir() {
|
||||||
|
if (isBunBinary) {
|
||||||
|
return join(getPackageDir(), "assets");
|
||||||
|
}
|
||||||
|
const packageDir = getPackageDir();
|
||||||
|
const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
|
||||||
|
return join(packageDir, srcOrDist, "modes", "interactive", "assets");
|
||||||
|
}
|
||||||
|
/** Get path to a bundled interactive asset */
|
||||||
|
export function getBundledInteractiveAssetPath(name) {
|
||||||
|
return join(getInteractiveAssetsDir(), name);
|
||||||
|
}
|
||||||
|
let pkg = {};
|
||||||
|
try {
|
||||||
|
pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
const err = e;
|
||||||
|
if (err.code !== "ENOENT")
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
const piConfigName = pkg.piConfig?.name;
|
||||||
|
export const PACKAGE_NAME = pkg.name || "@earendil-works/pi-coding-agent";
|
||||||
|
export const APP_NAME = piConfigName || "pi";
|
||||||
|
export const APP_TITLE = piConfigName ? APP_NAME : "π";
|
||||||
|
export const CONFIG_DIR_NAME = pkg.piConfig?.configDir || ".pi";
|
||||||
|
export const VERSION = pkg.version || "0.0.0";
|
||||||
|
// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
|
||||||
|
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
||||||
|
export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
|
||||||
|
export function expandTildePath(path) {
|
||||||
|
return normalizePath(path);
|
||||||
|
}
|
||||||
|
const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/";
|
||||||
|
/** Get the share viewer URL for a gist ID */
|
||||||
|
export function getShareViewerUrl(gistId) {
|
||||||
|
const baseUrl = process.env.PI_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;
|
||||||
|
return `${baseUrl}#${gistId}`;
|
||||||
|
}
|
||||||
|
// =============================================================================
|
||||||
|
// User Config Paths (~/.pi/agent/*)
|
||||||
|
// =============================================================================
|
||||||
|
/** Get the agent config directory (e.g., ~/.pi/agent/) */
|
||||||
|
export function getAgentDir() {
|
||||||
|
const envDir = process.env[ENV_AGENT_DIR];
|
||||||
|
if (envDir) {
|
||||||
|
return expandTildePath(envDir);
|
||||||
|
}
|
||||||
|
return join(homedir(), CONFIG_DIR_NAME, "agent");
|
||||||
|
}
|
||||||
|
/** Get path to user's custom themes directory */
|
||||||
|
export function getCustomThemesDir() {
|
||||||
|
return join(getAgentDir(), "themes");
|
||||||
|
}
|
||||||
|
/** Get path to models.json */
|
||||||
|
export function getModelsPath() {
|
||||||
|
return join(getAgentDir(), "models.json");
|
||||||
|
}
|
||||||
|
/** Get path to auth.json */
|
||||||
|
export function getAuthPath() {
|
||||||
|
return join(getAgentDir(), "auth.json");
|
||||||
|
}
|
||||||
|
/** Get path to settings.json */
|
||||||
|
export function getSettingsPath() {
|
||||||
|
return join(getAgentDir(), "settings.json");
|
||||||
|
}
|
||||||
|
/** Get path to tools directory */
|
||||||
|
export function getToolsDir() {
|
||||||
|
return join(getAgentDir(), "tools");
|
||||||
|
}
|
||||||
|
/** Get path to managed binaries directory (fd, rg) */
|
||||||
|
export function getBinDir() {
|
||||||
|
return join(getAgentDir(), "bin");
|
||||||
|
}
|
||||||
|
/** Get path to prompt templates directory */
|
||||||
|
export function getPromptsDir() {
|
||||||
|
return join(getAgentDir(), "prompts");
|
||||||
|
}
|
||||||
|
/** Get path to sessions directory */
|
||||||
|
export function getSessionsDir() {
|
||||||
|
return join(getAgentDir(), "sessions");
|
||||||
|
}
|
||||||
|
/** Get path to debug log file */
|
||||||
|
export function getDebugLogPath() {
|
||||||
|
return join(getAgentDir(), `${APP_NAME}-debug.log`);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=config.js.map
|
||||||
1
dist/config.js.map
vendored
Normal file
1
dist/config.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
119
dist/core/agent-session-runtime.d.ts
vendored
Normal file
119
dist/core/agent-session-runtime.d.ts
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import type { AgentSession } from "./agent-session.ts";
|
||||||
|
import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.ts";
|
||||||
|
import type { ProjectTrustContext, ReplacedSessionContext, SessionStartEvent } from "./extensions/index.ts";
|
||||||
|
import type { CreateAgentSessionResult } from "./sdk.ts";
|
||||||
|
import { SessionManager } from "./session-manager.ts";
|
||||||
|
/**
|
||||||
|
* Result returned by runtime creation.
|
||||||
|
*
|
||||||
|
* The caller gets the created session, its cwd-bound services, and all
|
||||||
|
* diagnostics collected during setup.
|
||||||
|
*/
|
||||||
|
export interface CreateAgentSessionRuntimeResult extends CreateAgentSessionResult {
|
||||||
|
services: AgentSessionServices;
|
||||||
|
diagnostics: AgentSessionRuntimeDiagnostic[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a full runtime for a target cwd and session manager.
|
||||||
|
*
|
||||||
|
* The factory closes over process-global fixed inputs, recreates cwd-bound
|
||||||
|
* services for the effective cwd, resolves session options against those
|
||||||
|
* services, and finally creates the AgentSession.
|
||||||
|
*/
|
||||||
|
export type CreateAgentSessionRuntimeFactory = (options: {
|
||||||
|
cwd: string;
|
||||||
|
agentDir: string;
|
||||||
|
sessionManager: SessionManager;
|
||||||
|
sessionStartEvent?: SessionStartEvent;
|
||||||
|
projectTrustContext?: ProjectTrustContext;
|
||||||
|
}) => Promise<CreateAgentSessionRuntimeResult>;
|
||||||
|
/**
|
||||||
|
* Thrown when /import references a JSONL file path that does not exist.
|
||||||
|
*/
|
||||||
|
export declare class SessionImportFileNotFoundError extends Error {
|
||||||
|
readonly filePath: string;
|
||||||
|
constructor(filePath: string);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Owns the current AgentSession plus its cwd-bound services.
|
||||||
|
*
|
||||||
|
* Session replacement methods tear down the current runtime first, then create
|
||||||
|
* and apply the next runtime. If creation fails, the error is propagated to the
|
||||||
|
* caller. The caller is responsible for user-facing error handling.
|
||||||
|
*/
|
||||||
|
export declare class AgentSessionRuntime {
|
||||||
|
private rebindSession?;
|
||||||
|
private beforeSessionInvalidate?;
|
||||||
|
private _session;
|
||||||
|
private _services;
|
||||||
|
private readonly createRuntime;
|
||||||
|
private _diagnostics;
|
||||||
|
private _modelFallbackMessage?;
|
||||||
|
constructor(_session: AgentSession, _services: AgentSessionServices, createRuntime: CreateAgentSessionRuntimeFactory, _diagnostics?: AgentSessionRuntimeDiagnostic[], _modelFallbackMessage?: string);
|
||||||
|
get services(): AgentSessionServices;
|
||||||
|
get session(): AgentSession;
|
||||||
|
get cwd(): string;
|
||||||
|
get diagnostics(): readonly AgentSessionRuntimeDiagnostic[];
|
||||||
|
get modelFallbackMessage(): string | undefined;
|
||||||
|
setRebindSession(rebindSession?: (session: AgentSession) => Promise<void>): void;
|
||||||
|
/**
|
||||||
|
* Set a synchronous callback that runs after `session_shutdown` handlers finish
|
||||||
|
* but before the current session is invalidated.
|
||||||
|
*
|
||||||
|
* This is for host-owned UI teardown that must not yield to the event loop,
|
||||||
|
* such as detaching extension-provided TUI components before the old extension
|
||||||
|
* context becomes stale.
|
||||||
|
*/
|
||||||
|
setBeforeSessionInvalidate(beforeSessionInvalidate?: () => void): void;
|
||||||
|
private emitBeforeSwitch;
|
||||||
|
private emitBeforeFork;
|
||||||
|
private teardownCurrent;
|
||||||
|
private apply;
|
||||||
|
private finishSessionReplacement;
|
||||||
|
switchSession(sessionPath: string, options?: {
|
||||||
|
cwdOverride?: string;
|
||||||
|
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
||||||
|
projectTrustContextFactory?: (cwd: string) => ProjectTrustContext;
|
||||||
|
}): Promise<{
|
||||||
|
cancelled: boolean;
|
||||||
|
}>;
|
||||||
|
newSession(options?: {
|
||||||
|
parentSession?: string;
|
||||||
|
setup?: (sessionManager: SessionManager) => Promise<void>;
|
||||||
|
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
||||||
|
}): Promise<{
|
||||||
|
cancelled: boolean;
|
||||||
|
}>;
|
||||||
|
fork(entryId: string, options?: {
|
||||||
|
position?: "before" | "at";
|
||||||
|
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
||||||
|
}): Promise<{
|
||||||
|
cancelled: boolean;
|
||||||
|
selectedText?: string;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Import a session JSONL file and switch runtime state to the imported session.
|
||||||
|
*
|
||||||
|
* @returns `{ cancelled: true }` when cancelled by `session_before_switch`, otherwise `{ cancelled: false }`.
|
||||||
|
* @throws {SessionImportFileNotFoundError} When the input path does not exist.
|
||||||
|
* @throws {MissingSessionCwdError} When the imported session cwd cannot be resolved and no override is provided.
|
||||||
|
*/
|
||||||
|
importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{
|
||||||
|
cancelled: boolean;
|
||||||
|
}>;
|
||||||
|
dispose(): Promise<void>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create the initial runtime from a runtime factory and initial session target.
|
||||||
|
*
|
||||||
|
* The same factory is stored on the returned AgentSessionRuntime and reused for
|
||||||
|
* later /new, /resume, /fork, and import flows.
|
||||||
|
*/
|
||||||
|
export declare function createAgentSessionRuntime(createRuntime: CreateAgentSessionRuntimeFactory, options: {
|
||||||
|
cwd: string;
|
||||||
|
agentDir: string;
|
||||||
|
sessionManager: SessionManager;
|
||||||
|
sessionStartEvent?: SessionStartEvent;
|
||||||
|
}): Promise<AgentSessionRuntime>;
|
||||||
|
export { type AgentSessionRuntimeDiagnostic, type AgentSessionServices, type CreateAgentSessionFromServicesOptions, type CreateAgentSessionServicesOptions, createAgentSessionFromServices, createAgentSessionServices, } from "./agent-session-services.ts";
|
||||||
|
//# sourceMappingURL=agent-session-runtime.d.ts.map
|
||||||
1
dist/core/agent-session-runtime.d.ts.map
vendored
Normal file
1
dist/core/agent-session-runtime.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
309
dist/core/agent-session-runtime.js
vendored
Normal file
309
dist/core/agent-session-runtime.js
vendored
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
||||||
|
import { basename, join, resolve } from "node:path";
|
||||||
|
import { resolvePath } from "../utils/paths.js";
|
||||||
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
||||||
|
import { assertSessionCwdExists } from "./session-cwd.js";
|
||||||
|
import { SessionManager } from "./session-manager.js";
|
||||||
|
/**
|
||||||
|
* Thrown when /import references a JSONL file path that does not exist.
|
||||||
|
*/
|
||||||
|
export class SessionImportFileNotFoundError extends Error {
|
||||||
|
filePath;
|
||||||
|
constructor(filePath) {
|
||||||
|
super(`File not found: ${filePath}`);
|
||||||
|
this.name = "SessionImportFileNotFoundError";
|
||||||
|
this.filePath = filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function extractUserMessageText(content) {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
.filter((part) => part.type === "text" && typeof part.text === "string")
|
||||||
|
.map((part) => part.text)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Owns the current AgentSession plus its cwd-bound services.
|
||||||
|
*
|
||||||
|
* Session replacement methods tear down the current runtime first, then create
|
||||||
|
* and apply the next runtime. If creation fails, the error is propagated to the
|
||||||
|
* caller. The caller is responsible for user-facing error handling.
|
||||||
|
*/
|
||||||
|
export class AgentSessionRuntime {
|
||||||
|
rebindSession;
|
||||||
|
beforeSessionInvalidate;
|
||||||
|
_session;
|
||||||
|
_services;
|
||||||
|
createRuntime;
|
||||||
|
_diagnostics;
|
||||||
|
_modelFallbackMessage;
|
||||||
|
constructor(_session, _services, createRuntime, _diagnostics = [], _modelFallbackMessage) {
|
||||||
|
this._session = _session;
|
||||||
|
this._services = _services;
|
||||||
|
this.createRuntime = createRuntime;
|
||||||
|
this._diagnostics = _diagnostics;
|
||||||
|
this._modelFallbackMessage = _modelFallbackMessage;
|
||||||
|
}
|
||||||
|
get services() {
|
||||||
|
return this._services;
|
||||||
|
}
|
||||||
|
get session() {
|
||||||
|
return this._session;
|
||||||
|
}
|
||||||
|
get cwd() {
|
||||||
|
return this._services.cwd;
|
||||||
|
}
|
||||||
|
get diagnostics() {
|
||||||
|
return this._diagnostics;
|
||||||
|
}
|
||||||
|
get modelFallbackMessage() {
|
||||||
|
return this._modelFallbackMessage;
|
||||||
|
}
|
||||||
|
setRebindSession(rebindSession) {
|
||||||
|
this.rebindSession = rebindSession;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set a synchronous callback that runs after `session_shutdown` handlers finish
|
||||||
|
* but before the current session is invalidated.
|
||||||
|
*
|
||||||
|
* This is for host-owned UI teardown that must not yield to the event loop,
|
||||||
|
* such as detaching extension-provided TUI components before the old extension
|
||||||
|
* context becomes stale.
|
||||||
|
*/
|
||||||
|
setBeforeSessionInvalidate(beforeSessionInvalidate) {
|
||||||
|
this.beforeSessionInvalidate = beforeSessionInvalidate;
|
||||||
|
}
|
||||||
|
async emitBeforeSwitch(reason, targetSessionFile) {
|
||||||
|
const runner = this.session.extensionRunner;
|
||||||
|
if (!runner.hasHandlers("session_before_switch")) {
|
||||||
|
return { cancelled: false };
|
||||||
|
}
|
||||||
|
const result = await runner.emit({
|
||||||
|
type: "session_before_switch",
|
||||||
|
reason,
|
||||||
|
targetSessionFile,
|
||||||
|
});
|
||||||
|
return { cancelled: result?.cancel === true };
|
||||||
|
}
|
||||||
|
async emitBeforeFork(entryId, options) {
|
||||||
|
const runner = this.session.extensionRunner;
|
||||||
|
if (!runner.hasHandlers("session_before_fork")) {
|
||||||
|
return { cancelled: false };
|
||||||
|
}
|
||||||
|
const result = await runner.emit({
|
||||||
|
type: "session_before_fork",
|
||||||
|
entryId,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return { cancelled: result?.cancel === true };
|
||||||
|
}
|
||||||
|
async teardownCurrent(reason, targetSessionFile) {
|
||||||
|
// Settle any active response first so the aborted turn (including tool
|
||||||
|
// results) is persisted to the outgoing session before it is replaced.
|
||||||
|
await this.session.abort();
|
||||||
|
await emitSessionShutdownEvent(this.session.extensionRunner, {
|
||||||
|
type: "session_shutdown",
|
||||||
|
reason,
|
||||||
|
targetSessionFile,
|
||||||
|
});
|
||||||
|
this.beforeSessionInvalidate?.();
|
||||||
|
this.session.dispose();
|
||||||
|
}
|
||||||
|
apply(result) {
|
||||||
|
this._session = result.session;
|
||||||
|
this._services = result.services;
|
||||||
|
this._diagnostics = result.diagnostics;
|
||||||
|
this._modelFallbackMessage = result.modelFallbackMessage;
|
||||||
|
}
|
||||||
|
async finishSessionReplacement(withSession) {
|
||||||
|
if (this.rebindSession) {
|
||||||
|
await this.rebindSession(this.session);
|
||||||
|
}
|
||||||
|
if (withSession) {
|
||||||
|
await withSession(this.session.createReplacedSessionContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async switchSession(sessionPath, options) {
|
||||||
|
const beforeResult = await this.emitBeforeSwitch("resume", sessionPath);
|
||||||
|
if (beforeResult.cancelled) {
|
||||||
|
return beforeResult;
|
||||||
|
}
|
||||||
|
const previousSessionFile = this.session.sessionFile;
|
||||||
|
const sessionManager = SessionManager.open(sessionPath, undefined, options?.cwdOverride);
|
||||||
|
assertSessionCwdExists(sessionManager, this.cwd);
|
||||||
|
await this.teardownCurrent("resume", sessionManager.getSessionFile());
|
||||||
|
this.apply(await this.createRuntime({
|
||||||
|
cwd: sessionManager.getCwd(),
|
||||||
|
agentDir: this.services.agentDir,
|
||||||
|
sessionManager,
|
||||||
|
sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile },
|
||||||
|
projectTrustContext: options?.projectTrustContextFactory?.(sessionManager.getCwd()),
|
||||||
|
}));
|
||||||
|
await this.finishSessionReplacement(options?.withSession);
|
||||||
|
return { cancelled: false };
|
||||||
|
}
|
||||||
|
async newSession(options) {
|
||||||
|
const beforeResult = await this.emitBeforeSwitch("new");
|
||||||
|
if (beforeResult.cancelled) {
|
||||||
|
return beforeResult;
|
||||||
|
}
|
||||||
|
const previousSessionFile = this.session.sessionFile;
|
||||||
|
const sessionDir = this.session.sessionManager.getSessionDir();
|
||||||
|
const sessionManager = this.session.sessionManager.isPersisted()
|
||||||
|
? SessionManager.create(this.cwd, sessionDir)
|
||||||
|
: SessionManager.inMemory(this.cwd);
|
||||||
|
if (options?.parentSession) {
|
||||||
|
sessionManager.newSession({ parentSession: options.parentSession });
|
||||||
|
}
|
||||||
|
await this.teardownCurrent("new", sessionManager.getSessionFile());
|
||||||
|
this.apply(await this.createRuntime({
|
||||||
|
cwd: this.cwd,
|
||||||
|
agentDir: this.services.agentDir,
|
||||||
|
sessionManager,
|
||||||
|
sessionStartEvent: { type: "session_start", reason: "new", previousSessionFile },
|
||||||
|
}));
|
||||||
|
if (options?.setup) {
|
||||||
|
await options.setup(this.session.sessionManager);
|
||||||
|
this.session.agent.state.messages = this.session.sessionManager.buildSessionContext().messages;
|
||||||
|
}
|
||||||
|
await this.finishSessionReplacement(options?.withSession);
|
||||||
|
return { cancelled: false };
|
||||||
|
}
|
||||||
|
async fork(entryId, options) {
|
||||||
|
const position = options?.position ?? "before";
|
||||||
|
const beforeResult = await this.emitBeforeFork(entryId, { position });
|
||||||
|
if (beforeResult.cancelled) {
|
||||||
|
return { cancelled: true };
|
||||||
|
}
|
||||||
|
let targetLeafId;
|
||||||
|
let selectedText;
|
||||||
|
const selectedEntry = this.session.sessionManager.getEntry(entryId);
|
||||||
|
if (!selectedEntry) {
|
||||||
|
throw new Error("Invalid entry ID for forking");
|
||||||
|
}
|
||||||
|
if (position === "at") {
|
||||||
|
targetLeafId = selectedEntry.id;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
|
||||||
|
throw new Error("Invalid entry ID for forking");
|
||||||
|
}
|
||||||
|
targetLeafId = selectedEntry.parentId;
|
||||||
|
selectedText = extractUserMessageText(selectedEntry.message.content);
|
||||||
|
}
|
||||||
|
const previousSessionFile = this.session.sessionFile;
|
||||||
|
if (this.session.sessionManager.isPersisted()) {
|
||||||
|
const currentSessionFile = this.session.sessionFile;
|
||||||
|
if (!currentSessionFile) {
|
||||||
|
throw new Error("Persisted session is missing a session file");
|
||||||
|
}
|
||||||
|
const sessionDir = this.session.sessionManager.getSessionDir();
|
||||||
|
if (!targetLeafId) {
|
||||||
|
const sessionManager = SessionManager.create(this.cwd, sessionDir);
|
||||||
|
sessionManager.newSession({ parentSession: currentSessionFile });
|
||||||
|
await this.teardownCurrent("fork", sessionManager.getSessionFile());
|
||||||
|
this.apply(await this.createRuntime({
|
||||||
|
cwd: this.cwd,
|
||||||
|
agentDir: this.services.agentDir,
|
||||||
|
sessionManager,
|
||||||
|
sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile },
|
||||||
|
}));
|
||||||
|
await this.finishSessionReplacement(options?.withSession);
|
||||||
|
return { cancelled: false, selectedText };
|
||||||
|
}
|
||||||
|
if (!existsSync(currentSessionFile)) {
|
||||||
|
throw new Error("This session has not been saved yet. Wait for the first assistant response before cloning or forking it.");
|
||||||
|
}
|
||||||
|
const sessionManager = SessionManager.open(currentSessionFile, sessionDir);
|
||||||
|
const forkedSessionPath = sessionManager.createBranchedSession(targetLeafId);
|
||||||
|
if (!forkedSessionPath) {
|
||||||
|
throw new Error("Failed to create forked session");
|
||||||
|
}
|
||||||
|
await this.teardownCurrent("fork", sessionManager.getSessionFile());
|
||||||
|
this.apply(await this.createRuntime({
|
||||||
|
cwd: sessionManager.getCwd(),
|
||||||
|
agentDir: this.services.agentDir,
|
||||||
|
sessionManager,
|
||||||
|
sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile },
|
||||||
|
}));
|
||||||
|
await this.finishSessionReplacement(options?.withSession);
|
||||||
|
return { cancelled: false, selectedText };
|
||||||
|
}
|
||||||
|
const sessionManager = this.session.sessionManager;
|
||||||
|
if (!targetLeafId) {
|
||||||
|
sessionManager.newSession({ parentSession: this.session.sessionFile });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sessionManager.createBranchedSession(targetLeafId);
|
||||||
|
}
|
||||||
|
await this.teardownCurrent("fork", sessionManager.getSessionFile());
|
||||||
|
this.apply(await this.createRuntime({
|
||||||
|
cwd: this.cwd,
|
||||||
|
agentDir: this.services.agentDir,
|
||||||
|
sessionManager,
|
||||||
|
sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile },
|
||||||
|
}));
|
||||||
|
await this.finishSessionReplacement(options?.withSession);
|
||||||
|
return { cancelled: false, selectedText };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Import a session JSONL file and switch runtime state to the imported session.
|
||||||
|
*
|
||||||
|
* @returns `{ cancelled: true }` when cancelled by `session_before_switch`, otherwise `{ cancelled: false }`.
|
||||||
|
* @throws {SessionImportFileNotFoundError} When the input path does not exist.
|
||||||
|
* @throws {MissingSessionCwdError} When the imported session cwd cannot be resolved and no override is provided.
|
||||||
|
*/
|
||||||
|
async importFromJsonl(inputPath, cwdOverride) {
|
||||||
|
const resolvedPath = resolvePath(inputPath);
|
||||||
|
if (!existsSync(resolvedPath)) {
|
||||||
|
throw new SessionImportFileNotFoundError(resolvedPath);
|
||||||
|
}
|
||||||
|
const sessionDir = this.session.sessionManager.getSessionDir();
|
||||||
|
if (!existsSync(sessionDir)) {
|
||||||
|
mkdirSync(sessionDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const destinationPath = join(sessionDir, basename(resolvedPath));
|
||||||
|
const beforeResult = await this.emitBeforeSwitch("resume", destinationPath);
|
||||||
|
if (beforeResult.cancelled) {
|
||||||
|
return beforeResult;
|
||||||
|
}
|
||||||
|
const previousSessionFile = this.session.sessionFile;
|
||||||
|
if (resolve(destinationPath) !== resolvedPath) {
|
||||||
|
copyFileSync(resolvedPath, destinationPath);
|
||||||
|
}
|
||||||
|
const sessionManager = SessionManager.open(destinationPath, sessionDir, cwdOverride);
|
||||||
|
assertSessionCwdExists(sessionManager, this.cwd);
|
||||||
|
await this.teardownCurrent("resume", sessionManager.getSessionFile());
|
||||||
|
this.apply(await this.createRuntime({
|
||||||
|
cwd: sessionManager.getCwd(),
|
||||||
|
agentDir: this.services.agentDir,
|
||||||
|
sessionManager,
|
||||||
|
sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile },
|
||||||
|
}));
|
||||||
|
await this.finishSessionReplacement();
|
||||||
|
return { cancelled: false };
|
||||||
|
}
|
||||||
|
async dispose() {
|
||||||
|
await emitSessionShutdownEvent(this.session.extensionRunner, {
|
||||||
|
type: "session_shutdown",
|
||||||
|
reason: "quit",
|
||||||
|
});
|
||||||
|
this.beforeSessionInvalidate?.();
|
||||||
|
this.session.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create the initial runtime from a runtime factory and initial session target.
|
||||||
|
*
|
||||||
|
* The same factory is stored on the returned AgentSessionRuntime and reused for
|
||||||
|
* later /new, /resume, /fork, and import flows.
|
||||||
|
*/
|
||||||
|
export async function createAgentSessionRuntime(createRuntime, options) {
|
||||||
|
assertSessionCwdExists(options.sessionManager, options.cwd);
|
||||||
|
const result = await createRuntime(options);
|
||||||
|
return new AgentSessionRuntime(result.session, result.services, createRuntime, result.diagnostics, result.modelFallbackMessage);
|
||||||
|
}
|
||||||
|
export { createAgentSessionFromServices, createAgentSessionServices, } from "./agent-session-services.js";
|
||||||
|
//# sourceMappingURL=agent-session-runtime.js.map
|
||||||
1
dist/core/agent-session-runtime.js.map
vendored
Normal file
1
dist/core/agent-session-runtime.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
85
dist/core/agent-session-services.d.ts
vendored
Normal file
85
dist/core/agent-session-services.d.ts
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||||
|
import type { Model } from "@earendil-works/pi-ai";
|
||||||
|
import type { SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
|
||||||
|
import { ModelRuntime } from "./model-runtime.ts";
|
||||||
|
import { type DefaultResourceLoaderOptions, type ResourceLoader, type ResourceLoaderReloadOptions } from "./resource-loader.ts";
|
||||||
|
import { type CreateAgentSessionOptions, type CreateAgentSessionResult } from "./sdk.ts";
|
||||||
|
import type { SessionManager } from "./session-manager.ts";
|
||||||
|
import { SettingsManager } from "./settings-manager.ts";
|
||||||
|
/**
|
||||||
|
* Non-fatal issues collected while creating services or sessions.
|
||||||
|
*
|
||||||
|
* Runtime creation returns diagnostics to the caller instead of printing or
|
||||||
|
* exiting. The app layer decides whether warnings should be shown and whether
|
||||||
|
* errors should abort startup.
|
||||||
|
*/
|
||||||
|
export interface AgentSessionRuntimeDiagnostic {
|
||||||
|
type: "info" | "warning" | "error";
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Inputs for creating cwd-bound runtime services.
|
||||||
|
*
|
||||||
|
* These services are recreated whenever the effective session cwd changes.
|
||||||
|
* CLI-provided resource paths should be resolved to absolute paths before they
|
||||||
|
* reach this function, so later cwd switches do not reinterpret them.
|
||||||
|
*/
|
||||||
|
export interface CreateAgentSessionServicesOptions {
|
||||||
|
cwd: string;
|
||||||
|
agentDir?: string;
|
||||||
|
settingsManager?: SettingsManager;
|
||||||
|
modelRuntime?: ModelRuntime;
|
||||||
|
extensionFlagValues?: Map<string, boolean | string>;
|
||||||
|
resourceLoaderOptions?: Omit<DefaultResourceLoaderOptions, "cwd" | "agentDir" | "settingsManager">;
|
||||||
|
resourceLoaderReloadOptions?: ResourceLoaderReloadOptions;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Inputs for creating an AgentSession from already-created services.
|
||||||
|
*
|
||||||
|
* Use this after services exist and any cwd-bound model/tool/session options
|
||||||
|
* have been resolved against those services.
|
||||||
|
*/
|
||||||
|
export interface CreateAgentSessionFromServicesOptions {
|
||||||
|
services: AgentSessionServices;
|
||||||
|
sessionManager: SessionManager;
|
||||||
|
sessionStartEvent?: SessionStartEvent;
|
||||||
|
model?: Model<any>;
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
|
scopedModels?: Array<{
|
||||||
|
model: Model<any>;
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
|
}>;
|
||||||
|
tools?: string[];
|
||||||
|
excludeTools?: CreateAgentSessionOptions["excludeTools"];
|
||||||
|
noTools?: CreateAgentSessionOptions["noTools"];
|
||||||
|
customTools?: ToolDefinition[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Coherent cwd-bound runtime services for one effective session cwd.
|
||||||
|
*
|
||||||
|
* This is infrastructure only. The AgentSession itself is created separately so
|
||||||
|
* session options can be resolved against these services first.
|
||||||
|
*/
|
||||||
|
export interface AgentSessionServices {
|
||||||
|
cwd: string;
|
||||||
|
agentDir: string;
|
||||||
|
modelRuntime: ModelRuntime;
|
||||||
|
settingsManager: SettingsManager;
|
||||||
|
resourceLoader: ResourceLoader;
|
||||||
|
diagnostics: AgentSessionRuntimeDiagnostic[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create cwd-bound runtime services.
|
||||||
|
*
|
||||||
|
* Returns services plus diagnostics. It does not create an AgentSession.
|
||||||
|
*/
|
||||||
|
export declare function createAgentSessionServices(options: CreateAgentSessionServicesOptions): Promise<AgentSessionServices>;
|
||||||
|
/**
|
||||||
|
* Create an AgentSession from previously created services.
|
||||||
|
*
|
||||||
|
* This keeps session creation separate from service creation so callers can
|
||||||
|
* resolve model, thinking, tools, and other session inputs against the target
|
||||||
|
* cwd before constructing the session.
|
||||||
|
*/
|
||||||
|
export declare function createAgentSessionFromServices(options: CreateAgentSessionFromServicesOptions): Promise<CreateAgentSessionResult>;
|
||||||
|
//# sourceMappingURL=agent-session-services.d.ts.map
|
||||||
1
dist/core/agent-session-services.d.ts.map
vendored
Normal file
1
dist/core/agent-session-services.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
133
dist/core/agent-session-services.js
vendored
Normal file
133
dist/core/agent-session-services.js
vendored
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { getAgentDir } from "../config.js";
|
||||||
|
import { resolvePath } from "../utils/paths.js";
|
||||||
|
import { ModelRuntime } from "./model-runtime.js";
|
||||||
|
import { DefaultResourceLoader, } from "./resource-loader.js";
|
||||||
|
import { createAgentSession } from "./sdk.js";
|
||||||
|
import { SettingsManager } from "./settings-manager.js";
|
||||||
|
function applyExtensionFlagValues(resourceLoader, extensionFlagValues) {
|
||||||
|
if (!extensionFlagValues) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const diagnostics = [];
|
||||||
|
const extensionsResult = resourceLoader.getExtensions();
|
||||||
|
const registeredFlags = new Map();
|
||||||
|
for (const extension of extensionsResult.extensions) {
|
||||||
|
for (const [name, flag] of extension.flags) {
|
||||||
|
registeredFlags.set(name, { type: flag.type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unknownFlags = [];
|
||||||
|
for (const [name, value] of extensionFlagValues) {
|
||||||
|
const flag = registeredFlags.get(name);
|
||||||
|
if (!flag) {
|
||||||
|
unknownFlags.push(name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (flag.type === "boolean") {
|
||||||
|
extensionsResult.runtime.flagValues.set(name, true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
extensionsResult.runtime.flagValues.set(name, value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
diagnostics.push({
|
||||||
|
type: "error",
|
||||||
|
message: `Extension flag "--${name}" requires a value`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (unknownFlags.length > 0) {
|
||||||
|
diagnostics.push({
|
||||||
|
type: "error",
|
||||||
|
message: `Unknown option${unknownFlags.length === 1 ? "" : "s"}: ${unknownFlags.map((name) => `--${name}`).join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create cwd-bound runtime services.
|
||||||
|
*
|
||||||
|
* Returns services plus diagnostics. It does not create an AgentSession.
|
||||||
|
*/
|
||||||
|
export async function createAgentSessionServices(options) {
|
||||||
|
const cwd = resolvePath(options.cwd);
|
||||||
|
const agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir();
|
||||||
|
const modelRuntime = options.modelRuntime ??
|
||||||
|
(await ModelRuntime.create({
|
||||||
|
authPath: join(agentDir, "auth.json"),
|
||||||
|
modelsPath: join(agentDir, "models.json"),
|
||||||
|
}));
|
||||||
|
const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
|
||||||
|
const resourceLoader = new DefaultResourceLoader({
|
||||||
|
...(options.resourceLoaderOptions ?? {}),
|
||||||
|
cwd,
|
||||||
|
agentDir,
|
||||||
|
settingsManager,
|
||||||
|
});
|
||||||
|
await resourceLoader.reload(options.resourceLoaderReloadOptions);
|
||||||
|
const diagnostics = [];
|
||||||
|
const extensionsResult = resourceLoader.getExtensions();
|
||||||
|
for (const { name, config, extensionPath } of extensionsResult.runtime.pendingProviderRegistrations) {
|
||||||
|
try {
|
||||||
|
modelRuntime.registerProvider(name, config);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
diagnostics.push({
|
||||||
|
type: "error",
|
||||||
|
message: `Extension "${extensionPath}" error: ${message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extensionsResult.runtime.pendingProviderRegistrations = [];
|
||||||
|
for (const { provider, extensionPath } of extensionsResult.runtime.pendingNativeProviderRegistrations) {
|
||||||
|
try {
|
||||||
|
modelRuntime.registerNativeProvider(provider);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
diagnostics.push({
|
||||||
|
type: "error",
|
||||||
|
message: `Extension "${extensionPath}" error: ${message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extensionsResult.runtime.pendingNativeProviderRegistrations = [];
|
||||||
|
await modelRuntime.refresh({ allowNetwork: false });
|
||||||
|
diagnostics.push(...applyExtensionFlagValues(resourceLoader, options.extensionFlagValues));
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
agentDir,
|
||||||
|
modelRuntime,
|
||||||
|
settingsManager,
|
||||||
|
resourceLoader,
|
||||||
|
diagnostics,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create an AgentSession from previously created services.
|
||||||
|
*
|
||||||
|
* This keeps session creation separate from service creation so callers can
|
||||||
|
* resolve model, thinking, tools, and other session inputs against the target
|
||||||
|
* cwd before constructing the session.
|
||||||
|
*/
|
||||||
|
export async function createAgentSessionFromServices(options) {
|
||||||
|
return createAgentSession({
|
||||||
|
cwd: options.services.cwd,
|
||||||
|
agentDir: options.services.agentDir,
|
||||||
|
modelRuntime: options.services.modelRuntime,
|
||||||
|
settingsManager: options.services.settingsManager,
|
||||||
|
resourceLoader: options.services.resourceLoader,
|
||||||
|
sessionManager: options.sessionManager,
|
||||||
|
model: options.model,
|
||||||
|
thinkingLevel: options.thinkingLevel,
|
||||||
|
scopedModels: options.scopedModels,
|
||||||
|
tools: options.tools,
|
||||||
|
excludeTools: options.excludeTools,
|
||||||
|
noTools: options.noTools,
|
||||||
|
customTools: options.customTools,
|
||||||
|
sessionStartEvent: options.sessionStartEvent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=agent-session-services.js.map
|
||||||
1
dist/core/agent-session-services.js.map
vendored
Normal file
1
dist/core/agent-session-services.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
653
dist/core/agent-session.d.ts
vendored
Normal file
653
dist/core/agent-session.d.ts
vendored
Normal file
|
|
@ -0,0 +1,653 @@
|
||||||
|
/**
|
||||||
|
* AgentSession - Core abstraction for agent lifecycle and session management.
|
||||||
|
*
|
||||||
|
* This class is shared between all run modes (interactive, print, rpc).
|
||||||
|
* It encapsulates:
|
||||||
|
* - Agent state access
|
||||||
|
* - Event subscription with automatic session persistence
|
||||||
|
* - Model and thinking level management
|
||||||
|
* - Compaction (manual and auto)
|
||||||
|
* - Bash execution
|
||||||
|
* - Session switching and branching
|
||||||
|
*
|
||||||
|
* Modes use this class and add their own I/O layer on top.
|
||||||
|
*/
|
||||||
|
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||||
|
import type { ImageContent, Model, TextContent } from "@earendil-works/pi-ai/compat";
|
||||||
|
import { type BashResult } from "./bash-executor.ts";
|
||||||
|
import { type CompactionResult } from "./compaction/index.ts";
|
||||||
|
import { type ContextUsage, type ExtensionCommandContextActions, type ExtensionErrorListener, type ExtensionMode, ExtensionRunner, type ExtensionUIContext, type InputSource, type ReplacedSessionContext, type SessionStartEvent, type ShutdownHandler, type ToolDefinition, type ToolInfo } from "./extensions/index.ts";
|
||||||
|
import type { CustomMessage } from "./messages.ts";
|
||||||
|
import type { ModelRuntime } from "./model-runtime.ts";
|
||||||
|
import { type PromptTemplate } from "./prompt-templates.ts";
|
||||||
|
import type { ResourceLoader } from "./resource-loader.ts";
|
||||||
|
import type { BranchSummaryEntry, SessionEntry, SessionManager } from "./session-manager.ts";
|
||||||
|
import type { SettingsManager } from "./settings-manager.ts";
|
||||||
|
import { type BashOperations } from "./tools/bash.ts";
|
||||||
|
/** Parsed skill block from a user message */
|
||||||
|
export interface ParsedSkillBlock {
|
||||||
|
name: string;
|
||||||
|
location: string;
|
||||||
|
content: string;
|
||||||
|
userMessage: string | undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse a skill block from message text.
|
||||||
|
* Returns null if the text doesn't contain a skill block.
|
||||||
|
*/
|
||||||
|
export declare function parseSkillBlock(text: string): ParsedSkillBlock | null;
|
||||||
|
/** Session-specific events that extend the core AgentEvent */
|
||||||
|
export type AgentSessionEvent = Exclude<AgentEvent, {
|
||||||
|
type: "agent_end";
|
||||||
|
}> | {
|
||||||
|
type: "agent_end";
|
||||||
|
messages: AgentMessage[];
|
||||||
|
willRetry: boolean;
|
||||||
|
} | {
|
||||||
|
type: "agent_settled";
|
||||||
|
} | {
|
||||||
|
type: "queue_update";
|
||||||
|
steering: readonly string[];
|
||||||
|
followUp: readonly string[];
|
||||||
|
} | {
|
||||||
|
type: "compaction_start";
|
||||||
|
reason: "manual" | "threshold" | "overflow";
|
||||||
|
} | {
|
||||||
|
type: "entry_appended";
|
||||||
|
entry: SessionEntry;
|
||||||
|
} | {
|
||||||
|
type: "session_info_changed";
|
||||||
|
name: string | undefined;
|
||||||
|
} | {
|
||||||
|
type: "thinking_level_changed";
|
||||||
|
level: ThinkingLevel;
|
||||||
|
} | {
|
||||||
|
type: "compaction_end";
|
||||||
|
reason: "manual" | "threshold" | "overflow";
|
||||||
|
result: CompactionResult | undefined;
|
||||||
|
aborted: boolean;
|
||||||
|
willRetry: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
} | {
|
||||||
|
type: "auto_retry_start";
|
||||||
|
attempt: number;
|
||||||
|
maxAttempts: number;
|
||||||
|
delayMs: number;
|
||||||
|
errorMessage: string;
|
||||||
|
} | {
|
||||||
|
type: "auto_retry_end";
|
||||||
|
success: boolean;
|
||||||
|
attempt: number;
|
||||||
|
finalError?: string;
|
||||||
|
} | {
|
||||||
|
type: "summarization_retry_scheduled";
|
||||||
|
attempt: number;
|
||||||
|
maxAttempts: number;
|
||||||
|
delayMs: number;
|
||||||
|
errorMessage: string;
|
||||||
|
} | {
|
||||||
|
type: "summarization_retry_attempt_start";
|
||||||
|
source: "branchSummary";
|
||||||
|
} | {
|
||||||
|
type: "summarization_retry_attempt_start";
|
||||||
|
source: "compaction";
|
||||||
|
reason: "manual" | "threshold" | "overflow";
|
||||||
|
} | {
|
||||||
|
type: "summarization_retry_finished";
|
||||||
|
} | {
|
||||||
|
type: "auto_retry_end";
|
||||||
|
success: boolean;
|
||||||
|
attempt: number;
|
||||||
|
finalError?: string;
|
||||||
|
} | {
|
||||||
|
type: "bash_execution_update";
|
||||||
|
id?: string;
|
||||||
|
delta: string;
|
||||||
|
};
|
||||||
|
/** Listener function for agent session events */
|
||||||
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
||||||
|
export interface AgentSessionConfig {
|
||||||
|
agent: Agent;
|
||||||
|
sessionManager: SessionManager;
|
||||||
|
settingsManager: SettingsManager;
|
||||||
|
cwd: string;
|
||||||
|
/** Models to cycle through with Ctrl+P (from --models flag) */
|
||||||
|
scopedModels?: Array<{
|
||||||
|
model: Model<any>;
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
|
}>;
|
||||||
|
/** Resource loader for extensions, skills, prompts, themes, context files, and system prompt */
|
||||||
|
resourceLoader: ResourceLoader;
|
||||||
|
/** SDK custom tools registered outside extensions */
|
||||||
|
customTools?: ToolDefinition[];
|
||||||
|
/** Canonical model/auth runtime used by coding-agent internals. */
|
||||||
|
modelRuntime: ModelRuntime;
|
||||||
|
/** Initial active built-in tool names. Default: [read, bash, edit, write] */
|
||||||
|
initialActiveToolNames?: string[];
|
||||||
|
/** Optional allowlist of tool names. When provided, only these tool names are exposed. */
|
||||||
|
allowedToolNames?: string[];
|
||||||
|
/** Optional denylist of tool names. When provided, these tool names are not exposed. */
|
||||||
|
excludedToolNames?: string[];
|
||||||
|
/**
|
||||||
|
* Override base tools (useful for custom runtimes).
|
||||||
|
*
|
||||||
|
* These are synthesized into minimal ToolDefinitions internally so AgentSession can keep
|
||||||
|
* a definition-first registry even when callers provide plain AgentTool instances.
|
||||||
|
*/
|
||||||
|
baseToolsOverride?: Record<string, AgentTool>;
|
||||||
|
/** Mutable ref used by Agent to access the current ExtensionRunner */
|
||||||
|
extensionRunnerRef?: {
|
||||||
|
current?: ExtensionRunner;
|
||||||
|
};
|
||||||
|
/** Session start event metadata emitted when extensions bind to this runtime. */
|
||||||
|
sessionStartEvent?: SessionStartEvent;
|
||||||
|
}
|
||||||
|
export interface ExtensionBindings {
|
||||||
|
uiContext?: ExtensionUIContext;
|
||||||
|
mode?: ExtensionMode;
|
||||||
|
commandContextActions?: ExtensionCommandContextActions;
|
||||||
|
abortHandler?: () => void;
|
||||||
|
shutdownHandler?: ShutdownHandler;
|
||||||
|
onError?: ExtensionErrorListener;
|
||||||
|
}
|
||||||
|
/** Options for AgentSession.prompt() */
|
||||||
|
export interface PromptOptions {
|
||||||
|
/** Whether to expand file-based prompt templates (default: true) */
|
||||||
|
expandPromptTemplates?: boolean;
|
||||||
|
/** Image attachments */
|
||||||
|
images?: ImageContent[];
|
||||||
|
/** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). Required if streaming. */
|
||||||
|
streamingBehavior?: "steer" | "followUp";
|
||||||
|
/** Source of input for extension input event handlers. Defaults to "interactive". */
|
||||||
|
source?: InputSource;
|
||||||
|
/** Internal hook used by RPC mode to observe prompt preflight acceptance or rejection. */
|
||||||
|
preflightResult?: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
/** Result from cycleModel() */
|
||||||
|
export interface ModelCycleResult {
|
||||||
|
model: Model<any>;
|
||||||
|
thinkingLevel: ThinkingLevel;
|
||||||
|
/** Whether cycling through scoped models (--models flag) or all available */
|
||||||
|
isScoped: boolean;
|
||||||
|
}
|
||||||
|
/** Session statistics for /session command */
|
||||||
|
export interface SessionStats {
|
||||||
|
sessionFile: string | undefined;
|
||||||
|
sessionId: string;
|
||||||
|
userMessages: number;
|
||||||
|
assistantMessages: number;
|
||||||
|
toolCalls: number;
|
||||||
|
toolResults: number;
|
||||||
|
totalMessages: number;
|
||||||
|
tokens: {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
cost: number;
|
||||||
|
contextUsage?: ContextUsage;
|
||||||
|
}
|
||||||
|
export declare class AgentSession {
|
||||||
|
readonly agent: Agent;
|
||||||
|
readonly sessionManager: SessionManager;
|
||||||
|
readonly settingsManager: SettingsManager;
|
||||||
|
private _scopedModels;
|
||||||
|
private _unsubscribeAgent?;
|
||||||
|
private _eventListeners;
|
||||||
|
private _isAgentRunActive;
|
||||||
|
private _idleWaitPromise;
|
||||||
|
private _resolveIdleWait;
|
||||||
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
||||||
|
private _steeringMessages;
|
||||||
|
/** Tracks pending follow-up messages for UI display. Removed when delivered. */
|
||||||
|
private _followUpMessages;
|
||||||
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
||||||
|
private _pendingNextTurnMessages;
|
||||||
|
private _compactionAbortController;
|
||||||
|
private _autoCompactionAbortController;
|
||||||
|
private _overflowRecoveryAttempted;
|
||||||
|
private _branchSummaryAbortController;
|
||||||
|
private _retryAbortController;
|
||||||
|
private _retryAttempt;
|
||||||
|
private readonly _bashAbortControllers;
|
||||||
|
private _pendingBashMessages;
|
||||||
|
private _extensionRunner;
|
||||||
|
private _turnIndex;
|
||||||
|
private _resourceLoader;
|
||||||
|
private _customTools;
|
||||||
|
private _baseToolDefinitions;
|
||||||
|
private _cwd;
|
||||||
|
private _extensionRunnerRef?;
|
||||||
|
private _initialActiveToolNames?;
|
||||||
|
private _allowedToolNames?;
|
||||||
|
private _excludedToolNames?;
|
||||||
|
private _baseToolsOverride?;
|
||||||
|
private _sessionStartEvent;
|
||||||
|
private _extensionUIContext?;
|
||||||
|
private _extensionMode;
|
||||||
|
private _extensionCommandContextActions?;
|
||||||
|
private _extensionAbortHandler?;
|
||||||
|
private _extensionShutdownHandler?;
|
||||||
|
private _extensionErrorListener?;
|
||||||
|
private _extensionErrorUnsubscriber?;
|
||||||
|
private _modelRuntime;
|
||||||
|
private _toolRegistry;
|
||||||
|
private _toolDefinitions;
|
||||||
|
private _toolPromptSnippets;
|
||||||
|
private _toolPromptGuidelines;
|
||||||
|
private _baseSystemPrompt;
|
||||||
|
private _baseSystemPromptOptions;
|
||||||
|
private _systemPromptOverride?;
|
||||||
|
constructor(config: AgentSessionConfig);
|
||||||
|
get modelRuntime(): ModelRuntime;
|
||||||
|
private _getRequiredRequestAuth;
|
||||||
|
private _getSummarizationRequestAuth;
|
||||||
|
/**
|
||||||
|
* Install tool hooks once on the Agent instance.
|
||||||
|
*
|
||||||
|
* The callbacks read `this._extensionRunner` at execution time, so extension reload swaps in the
|
||||||
|
* new runner without reinstalling hooks. Extension-specific tool wrappers are still used to adapt
|
||||||
|
* registered tool execution to the extension context. Tool call and tool result interception now
|
||||||
|
* happens here instead of in wrappers.
|
||||||
|
*/
|
||||||
|
private _installAgentToolHooks;
|
||||||
|
private _installAgentNextTurnRefresh;
|
||||||
|
/** Emit an event to all listeners */
|
||||||
|
private _emit;
|
||||||
|
private _emitQueueUpdate;
|
||||||
|
private _getIdleWaitPromise;
|
||||||
|
private _resolveIdleWaitIfIdle;
|
||||||
|
private _emitAgentSettled;
|
||||||
|
private _lastAssistantMessage;
|
||||||
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
||||||
|
private _handleAgentEvent;
|
||||||
|
private _willRetryAfterAgentEnd;
|
||||||
|
/** Find the last assistant message in agent state (including aborted ones) */
|
||||||
|
private _findLastAssistantMessage;
|
||||||
|
private _replaceMessageInPlace;
|
||||||
|
private _emitExtensionEvent;
|
||||||
|
/**
|
||||||
|
* Subscribe to agent events.
|
||||||
|
* Session persistence is handled internally (saves messages on message_end).
|
||||||
|
* Multiple listeners can be added. Returns unsubscribe function for this listener.
|
||||||
|
*/
|
||||||
|
subscribe(listener: AgentSessionEventListener): () => void;
|
||||||
|
/**
|
||||||
|
* Temporarily disconnect from agent events.
|
||||||
|
* User listeners are preserved and will receive events again after resubscribe().
|
||||||
|
* Used internally during operations that need to pause event processing.
|
||||||
|
*/
|
||||||
|
private _disconnectFromAgent;
|
||||||
|
/**
|
||||||
|
* Reconnect to agent events after _disconnectFromAgent().
|
||||||
|
* Preserves all existing listeners.
|
||||||
|
*/
|
||||||
|
private _reconnectToAgent;
|
||||||
|
/**
|
||||||
|
* Remove all listeners and disconnect from agent.
|
||||||
|
* Call this when completely done with the session.
|
||||||
|
*/
|
||||||
|
dispose(): void;
|
||||||
|
/** Full agent state */
|
||||||
|
get state(): AgentState;
|
||||||
|
/** Current model (may be undefined if not yet selected) */
|
||||||
|
get model(): Model<any> | undefined;
|
||||||
|
/** Current thinking level */
|
||||||
|
get thinkingLevel(): ThinkingLevel;
|
||||||
|
/** Whether the session is currently processing an agent run or post-run continuation. */
|
||||||
|
get isStreaming(): boolean;
|
||||||
|
/** Whether the session has no active agent run, retry, auto-compaction, or queued continuation. */
|
||||||
|
get isIdle(): boolean;
|
||||||
|
/** Current effective system prompt (includes any per-turn extension modifications) */
|
||||||
|
get systemPrompt(): string;
|
||||||
|
/** Current retry attempt (0 if not retrying) */
|
||||||
|
get retryAttempt(): number;
|
||||||
|
/**
|
||||||
|
* Get the names of currently active tools.
|
||||||
|
* Returns the names of tools currently set on the agent.
|
||||||
|
*/
|
||||||
|
getActiveToolNames(): string[];
|
||||||
|
/**
|
||||||
|
* Get all configured tools with name, description, parameter schema, prompt guidelines, and source metadata.
|
||||||
|
*/
|
||||||
|
getAllTools(): ToolInfo[];
|
||||||
|
getToolDefinition(name: string): ToolDefinition | undefined;
|
||||||
|
/**
|
||||||
|
* Set active tools by name.
|
||||||
|
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
||||||
|
* Also rebuilds the system prompt to reflect the new tool set.
|
||||||
|
* Changes take effect on the next agent turn.
|
||||||
|
*/
|
||||||
|
setActiveToolsByName(toolNames: string[]): void;
|
||||||
|
/** Whether compaction or branch summarization is currently running */
|
||||||
|
get isCompacting(): boolean;
|
||||||
|
/** All messages including custom types like BashExecutionMessage */
|
||||||
|
get messages(): AgentMessage[];
|
||||||
|
/** Current steering mode */
|
||||||
|
get steeringMode(): "all" | "one-at-a-time";
|
||||||
|
/** Current follow-up mode */
|
||||||
|
get followUpMode(): "all" | "one-at-a-time";
|
||||||
|
/** Current session file path, or undefined if sessions are disabled */
|
||||||
|
get sessionFile(): string | undefined;
|
||||||
|
/** Current session ID */
|
||||||
|
get sessionId(): string;
|
||||||
|
/** Current session display name, if set */
|
||||||
|
get sessionName(): string | undefined;
|
||||||
|
/** Scoped models for cycling (from --models flag) */
|
||||||
|
get scopedModels(): ReadonlyArray<{
|
||||||
|
model: Model<any>;
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
|
}>;
|
||||||
|
/** Update scoped models for cycling */
|
||||||
|
setScopedModels(scopedModels: Array<{
|
||||||
|
model: Model<any>;
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
|
}>): void;
|
||||||
|
/** File-based prompt templates */
|
||||||
|
get promptTemplates(): ReadonlyArray<PromptTemplate>;
|
||||||
|
private _normalizePromptSnippet;
|
||||||
|
private _normalizePromptGuidelines;
|
||||||
|
private _rebuildSystemPrompt;
|
||||||
|
private _runAgentPrompt;
|
||||||
|
private _handlePostAgentRun;
|
||||||
|
/**
|
||||||
|
* Send a prompt to the agent.
|
||||||
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
||||||
|
* - Expands file-based prompt templates by default
|
||||||
|
* - During streaming, queues via steer() or followUp() based on streamingBehavior option
|
||||||
|
* - Validates model and API key before sending (when not streaming)
|
||||||
|
* @throws Error if streaming and no streamingBehavior specified
|
||||||
|
* @throws Error if no model selected or no API key available (when not streaming)
|
||||||
|
*/
|
||||||
|
prompt(text: string, options?: PromptOptions): Promise<void>;
|
||||||
|
private _tryExecuteExtensionCommand;
|
||||||
|
/**
|
||||||
|
* Expand skill commands (/skill:name args) to their full content.
|
||||||
|
* Returns the expanded text, or the original text if not a skill command or skill not found.
|
||||||
|
* Emits errors via extension runner if file read fails.
|
||||||
|
*/
|
||||||
|
private _expandSkillCommand;
|
||||||
|
/**
|
||||||
|
* Queue a steering message while the agent is running.
|
||||||
|
* Delivered after the current assistant turn finishes executing its tool calls,
|
||||||
|
* before the next LLM call.
|
||||||
|
* Expands skill commands and prompt templates. Errors on extension commands.
|
||||||
|
* @param images Optional image attachments to include with the message
|
||||||
|
* @throws Error if text is an extension command
|
||||||
|
*/
|
||||||
|
steer(text: string, images?: ImageContent[]): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Queue a follow-up message to be processed after the agent finishes.
|
||||||
|
* Delivered only when agent has no more tool calls or steering messages.
|
||||||
|
* Expands skill commands and prompt templates. Errors on extension commands.
|
||||||
|
* @param images Optional image attachments to include with the message
|
||||||
|
* @throws Error if text is an extension command
|
||||||
|
*/
|
||||||
|
followUp(text: string, images?: ImageContent[]): Promise<void>;
|
||||||
|
private _queueSteer;
|
||||||
|
private _queueFollowUp;
|
||||||
|
/**
|
||||||
|
* Throw an error if the text is an extension command.
|
||||||
|
*/
|
||||||
|
private _throwIfExtensionCommand;
|
||||||
|
/**
|
||||||
|
* Send a custom message to the session. Creates a CustomMessageEntry.
|
||||||
|
*
|
||||||
|
* Handles three cases:
|
||||||
|
* - Streaming: queues message, processed when loop pulls from queue
|
||||||
|
* - Not streaming + triggerTurn: appends to state/session, starts new turn
|
||||||
|
* - Not streaming + no trigger: appends to state/session, no turn
|
||||||
|
*
|
||||||
|
* @param message Custom message with customType, content, display, details
|
||||||
|
* @param options.triggerTurn If true and not streaming, triggers a new LLM turn
|
||||||
|
* @param options.deliverAs Delivery mode: "steer", "followUp", or "nextTurn"
|
||||||
|
*/
|
||||||
|
sendCustomMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">, options?: {
|
||||||
|
triggerTurn?: boolean;
|
||||||
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
||||||
|
}): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Send a user message to the agent. Always triggers a turn.
|
||||||
|
* When the agent is streaming, use deliverAs to specify how to queue the message.
|
||||||
|
*
|
||||||
|
* @param content User message content (string or content array)
|
||||||
|
* @param options.deliverAs Delivery mode when streaming: "steer" or "followUp"
|
||||||
|
*/
|
||||||
|
sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
|
||||||
|
deliverAs?: "steer" | "followUp";
|
||||||
|
}): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Clear all queued messages and return them.
|
||||||
|
* Useful for restoring to editor when user aborts.
|
||||||
|
* @returns Object with steering and followUp arrays
|
||||||
|
*/
|
||||||
|
clearQueue(): {
|
||||||
|
steering: string[];
|
||||||
|
followUp: string[];
|
||||||
|
};
|
||||||
|
/** Number of pending messages (includes both steering and follow-up) */
|
||||||
|
get pendingMessageCount(): number;
|
||||||
|
/** Get pending steering messages (read-only) */
|
||||||
|
getSteeringMessages(): readonly string[];
|
||||||
|
/** Get pending follow-up messages (read-only) */
|
||||||
|
getFollowUpMessages(): readonly string[];
|
||||||
|
get resourceLoader(): ResourceLoader;
|
||||||
|
/**
|
||||||
|
* Abort current operation and wait for agent to become idle.
|
||||||
|
*/
|
||||||
|
abort(): Promise<void>;
|
||||||
|
waitForIdle(): Promise<void>;
|
||||||
|
private _emitModelSelect;
|
||||||
|
/**
|
||||||
|
* Set model directly.
|
||||||
|
* Validates that auth is configured, saves to session and settings.
|
||||||
|
* @throws Error if no auth is configured for the model
|
||||||
|
*/
|
||||||
|
setModel(model: Model<any>): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Cycle to next/previous model.
|
||||||
|
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
||||||
|
* @param direction - "forward" (default) or "backward"
|
||||||
|
* @returns The new model info, or undefined if only one model available
|
||||||
|
*/
|
||||||
|
cycleModel(direction?: "forward" | "backward"): Promise<ModelCycleResult | undefined>;
|
||||||
|
private _cycleScopedModel;
|
||||||
|
private _cycleAvailableModel;
|
||||||
|
/**
|
||||||
|
* Set thinking level.
|
||||||
|
* Clamps to model capabilities based on available thinking levels.
|
||||||
|
* Saves to session and settings only if the level actually changes.
|
||||||
|
*/
|
||||||
|
setThinkingLevel(level: ThinkingLevel): void;
|
||||||
|
/**
|
||||||
|
* Cycle to next thinking level.
|
||||||
|
* @returns New level, or undefined if model doesn't support thinking
|
||||||
|
*/
|
||||||
|
cycleThinkingLevel(): ThinkingLevel | undefined;
|
||||||
|
/**
|
||||||
|
* Get available thinking levels for current model.
|
||||||
|
* The provider will clamp to what the specific model supports internally.
|
||||||
|
*/
|
||||||
|
getAvailableThinkingLevels(): ThinkingLevel[];
|
||||||
|
/**
|
||||||
|
* Check if current model supports thinking/reasoning.
|
||||||
|
*/
|
||||||
|
supportsThinking(): boolean;
|
||||||
|
private _getThinkingLevelForModelSwitch;
|
||||||
|
private _clampThinkingLevel;
|
||||||
|
private syncQueueModesFromSettings;
|
||||||
|
/**
|
||||||
|
* Set steering message mode.
|
||||||
|
* Saves to settings.
|
||||||
|
*/
|
||||||
|
setSteeringMode(mode: "all" | "one-at-a-time"): void;
|
||||||
|
/**
|
||||||
|
* Set follow-up message mode.
|
||||||
|
* Saves to settings.
|
||||||
|
*/
|
||||||
|
setFollowUpMode(mode: "all" | "one-at-a-time"): void;
|
||||||
|
/**
|
||||||
|
* Manually compact the session context.
|
||||||
|
* Aborts current agent operation first.
|
||||||
|
* @param customInstructions Optional instructions for the compaction summary
|
||||||
|
*/
|
||||||
|
compact(customInstructions?: string): Promise<CompactionResult>;
|
||||||
|
/**
|
||||||
|
* Cancel in-progress compaction (manual or auto).
|
||||||
|
*/
|
||||||
|
abortCompaction(): void;
|
||||||
|
/**
|
||||||
|
* Cancel in-progress branch summarization.
|
||||||
|
*/
|
||||||
|
abortBranchSummary(): void;
|
||||||
|
private _checkCompaction;
|
||||||
|
private _runAutoCompaction;
|
||||||
|
/**
|
||||||
|
* Toggle auto-compaction setting.
|
||||||
|
*/
|
||||||
|
setAutoCompactionEnabled(enabled: boolean): void;
|
||||||
|
/** Whether auto-compaction is enabled */
|
||||||
|
get autoCompactionEnabled(): boolean;
|
||||||
|
bindExtensions(bindings: ExtensionBindings): Promise<void>;
|
||||||
|
private extendResourcesFromExtensions;
|
||||||
|
private buildExtensionResourcePaths;
|
||||||
|
private getExtensionSourceLabel;
|
||||||
|
private _applyExtensionBindings;
|
||||||
|
private _refreshCurrentModelFromRegistry;
|
||||||
|
private _bindExtensionCore;
|
||||||
|
private _refreshToolRegistry;
|
||||||
|
private _buildRuntime;
|
||||||
|
reload(options?: {
|
||||||
|
beforeSessionStart?: () => void | Promise<void>;
|
||||||
|
}): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Check if an error is retryable (overloaded, rate limit, server errors).
|
||||||
|
* Context overflow errors are NOT retryable (handled by compaction instead).
|
||||||
|
*/
|
||||||
|
private _isRetryableError;
|
||||||
|
/**
|
||||||
|
* Retry policy + callbacks shared by compaction and branch-summary summarization calls.
|
||||||
|
* Uses the same `settings.retry` budget/backoff as agent-turn retries so a single transient
|
||||||
|
* stream drop no longer fails the whole operation. `source` carries the context
|
||||||
|
* the TUI needs to render the retry and recreate the underlying indicator.
|
||||||
|
*/
|
||||||
|
private _summarizationRetryCallbacks;
|
||||||
|
private _prepareRetry;
|
||||||
|
/**
|
||||||
|
* Cancel in-progress retry.
|
||||||
|
*/
|
||||||
|
abortRetry(): void;
|
||||||
|
/** Whether auto-retry is currently in progress */
|
||||||
|
get isRetrying(): boolean;
|
||||||
|
/** Whether auto-retry is enabled */
|
||||||
|
get autoRetryEnabled(): boolean;
|
||||||
|
/**
|
||||||
|
* Toggle auto-retry setting.
|
||||||
|
*/
|
||||||
|
setAutoRetryEnabled(enabled: boolean): void;
|
||||||
|
/**
|
||||||
|
* Execute a bash command.
|
||||||
|
* Adds result to agent context and session.
|
||||||
|
* @param command The bash command to execute
|
||||||
|
* @param onChunk Optional streaming callback for output
|
||||||
|
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
||||||
|
* @param options.id Optional identifier included in bash execution update events
|
||||||
|
* @param options.operations Custom BashOperations for remote execution
|
||||||
|
*/
|
||||||
|
executeBash(command: string, onChunk?: (chunk: string) => void, options?: {
|
||||||
|
excludeFromContext?: boolean;
|
||||||
|
id?: string;
|
||||||
|
operations?: BashOperations;
|
||||||
|
}): Promise<BashResult>;
|
||||||
|
/**
|
||||||
|
* Record a bash execution result in session history.
|
||||||
|
* Used by executeBash and by extensions that handle bash execution themselves.
|
||||||
|
*/
|
||||||
|
recordBashResult(command: string, result: BashResult, options?: {
|
||||||
|
excludeFromContext?: boolean;
|
||||||
|
}): void;
|
||||||
|
/**
|
||||||
|
* Cancel running bash command.
|
||||||
|
*/
|
||||||
|
abortBash(): void;
|
||||||
|
/** Whether a bash command is currently running */
|
||||||
|
get isBashRunning(): boolean;
|
||||||
|
/** Whether there are pending bash messages waiting to be flushed */
|
||||||
|
get hasPendingBashMessages(): boolean;
|
||||||
|
/**
|
||||||
|
* Flush pending bash messages to agent state and session.
|
||||||
|
* Called after agent turn completes to maintain proper message ordering.
|
||||||
|
*/
|
||||||
|
private _flushPendingBashMessages;
|
||||||
|
/**
|
||||||
|
* Set a display name for the current session.
|
||||||
|
*/
|
||||||
|
setSessionName(name: string): void;
|
||||||
|
/**
|
||||||
|
* Navigate to a different node in the session tree.
|
||||||
|
* Unlike fork() which creates a new session file, this stays in the same file.
|
||||||
|
*
|
||||||
|
* @param targetId The entry ID to navigate to
|
||||||
|
* @param options.summarize Whether user wants to summarize abandoned branch
|
||||||
|
* @param options.customInstructions Custom instructions for summarizer
|
||||||
|
* @param options.replaceInstructions If true, customInstructions replaces the default prompt
|
||||||
|
* @param options.label Label to attach to the branch summary entry
|
||||||
|
* @returns Result with editorText (if user message) and cancelled status
|
||||||
|
*/
|
||||||
|
navigateTree(targetId: string, options?: {
|
||||||
|
summarize?: boolean;
|
||||||
|
customInstructions?: string;
|
||||||
|
replaceInstructions?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}): Promise<{
|
||||||
|
editorText?: string;
|
||||||
|
cancelled: boolean;
|
||||||
|
aborted?: boolean;
|
||||||
|
summaryEntry?: BranchSummaryEntry;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Get all user messages from session for fork selector.
|
||||||
|
*/
|
||||||
|
getUserMessagesForForking(): Array<{
|
||||||
|
entryId: string;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Get session statistics. Aggregates over ALL session entries (including
|
||||||
|
* history that was compacted away), so token/cost totals reflect what was
|
||||||
|
* actually billed across the session.
|
||||||
|
*/
|
||||||
|
getSessionStats(): SessionStats;
|
||||||
|
getContextUsage(): ContextUsage | undefined;
|
||||||
|
/**
|
||||||
|
* Export session to HTML.
|
||||||
|
* @param outputPath Optional output path (defaults to session directory)
|
||||||
|
* @returns Path to exported file
|
||||||
|
*/
|
||||||
|
exportToHtml(outputPath?: string): Promise<string>;
|
||||||
|
/**
|
||||||
|
* Export the current session branch to a JSONL file.
|
||||||
|
* Writes the session header followed by all entries on the current branch path.
|
||||||
|
* @param outputPath Target file path. If omitted, generates a timestamped file in cwd.
|
||||||
|
* @returns The resolved output file path.
|
||||||
|
*/
|
||||||
|
exportToJsonl(outputPath?: string): string;
|
||||||
|
/**
|
||||||
|
* Get text content of last assistant message.
|
||||||
|
* Useful for /copy command.
|
||||||
|
* @returns Text content, or undefined if no assistant message exists
|
||||||
|
*/
|
||||||
|
getLastAssistantText(): string | undefined;
|
||||||
|
createReplacedSessionContext(): ReplacedSessionContext;
|
||||||
|
/**
|
||||||
|
* Check if extensions have handlers for a specific event type.
|
||||||
|
*/
|
||||||
|
hasExtensionHandlers(eventType: string): boolean;
|
||||||
|
/**
|
||||||
|
* Get the extension runner (for setting UI context and error handlers).
|
||||||
|
*/
|
||||||
|
get extensionRunner(): ExtensionRunner;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=agent-session.d.ts.map
|
||||||
1
dist/core/agent-session.d.ts.map
vendored
Normal file
1
dist/core/agent-session.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2685
dist/core/agent-session.js
vendored
Normal file
2685
dist/core/agent-session.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
dist/core/agent-session.js.map
vendored
Normal file
1
dist/core/agent-session.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5
dist/core/auth-guidance.d.ts
vendored
Normal file
5
dist/core/auth-guidance.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export declare function getProviderLoginHelp(): string;
|
||||||
|
export declare function formatNoModelsAvailableMessage(): string;
|
||||||
|
export declare function formatNoModelSelectedMessage(): string;
|
||||||
|
export declare function formatNoApiKeyFoundMessage(provider: string): string;
|
||||||
|
//# sourceMappingURL=auth-guidance.d.ts.map
|
||||||
1
dist/core/auth-guidance.d.ts.map
vendored
Normal file
1
dist/core/auth-guidance.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"auth-guidance.d.ts","sourceRoot":"","sources":["../../src/core/auth-guidance.ts"],"names":[],"mappings":"AAKA,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAED,wBAAgB,8BAA8B,IAAI,MAAM,CAEvD;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGnE","sourcesContent":["import { join } from \"node:path\";\nimport { getDocsPath } from \"../config.ts\";\n\nconst UNKNOWN_PROVIDER = \"unknown\";\n\nexport function getProviderLoginHelp(): string {\n\treturn [\n\t\t\"Use /login to log into a provider via OAuth or API key. See:\",\n\t\t` ${join(getDocsPath(), \"providers.md\")}`,\n\t\t` ${join(getDocsPath(), \"models.md\")}`,\n\t].join(\"\\n\");\n}\n\nexport function formatNoModelsAvailableMessage(): string {\n\treturn `No models available. ${getProviderLoginHelp()}`;\n}\n\nexport function formatNoModelSelectedMessage(): string {\n\treturn `No model selected.\\n\\n${getProviderLoginHelp()}\\n\\nThen use /model to select a model.`;\n}\n\nexport function formatNoApiKeyFoundMessage(provider: string): string {\n\tconst providerDisplay = provider === UNKNOWN_PROVIDER ? \"the selected model\" : provider;\n\treturn `No API key found for ${providerDisplay}.\\n\\n${getProviderLoginHelp()}`;\n}\n"]}
|
||||||
21
dist/core/auth-guidance.js
vendored
Normal file
21
dist/core/auth-guidance.js
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { getDocsPath } from "../config.js";
|
||||||
|
const UNKNOWN_PROVIDER = "unknown";
|
||||||
|
export function getProviderLoginHelp() {
|
||||||
|
return [
|
||||||
|
"Use /login to log into a provider via OAuth or API key. See:",
|
||||||
|
` ${join(getDocsPath(), "providers.md")}`,
|
||||||
|
` ${join(getDocsPath(), "models.md")}`,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
export function formatNoModelsAvailableMessage() {
|
||||||
|
return `No models available. ${getProviderLoginHelp()}`;
|
||||||
|
}
|
||||||
|
export function formatNoModelSelectedMessage() {
|
||||||
|
return `No model selected.\n\n${getProviderLoginHelp()}\n\nThen use /model to select a model.`;
|
||||||
|
}
|
||||||
|
export function formatNoApiKeyFoundMessage(provider) {
|
||||||
|
const providerDisplay = provider === UNKNOWN_PROVIDER ? "the selected model" : provider;
|
||||||
|
return `No API key found for ${providerDisplay}.\n\n${getProviderLoginHelp()}`;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=auth-guidance.js.map
|
||||||
1
dist/core/auth-guidance.js.map
vendored
Normal file
1
dist/core/auth-guidance.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"auth-guidance.js","sourceRoot":"","sources":["../../src/core/auth-guidance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,gBAAgB,GAAG,SAAS,CAAC;AAEnC,MAAM,UAAU,oBAAoB,GAAW;IAC9C,OAAO;QACN,8DAA8D;QAC9D,KAAK,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,EAAE;QAC1C,KAAK,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,EAAE;KACvC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACb;AAED,MAAM,UAAU,8BAA8B,GAAW;IACxD,OAAO,wBAAwB,oBAAoB,EAAE,EAAE,CAAC;AAAA,CACxD;AAED,MAAM,UAAU,4BAA4B,GAAW;IACtD,OAAO,yBAAyB,oBAAoB,EAAE,wCAAwC,CAAC;AAAA,CAC/F;AAED,MAAM,UAAU,0BAA0B,CAAC,QAAgB,EAAU;IACpE,MAAM,eAAe,GAAG,QAAQ,KAAK,gBAAgB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxF,OAAO,wBAAwB,eAAe,QAAQ,oBAAoB,EAAE,EAAE,CAAC;AAAA,CAC/E","sourcesContent":["import { join } from \"node:path\";\nimport { getDocsPath } from \"../config.ts\";\n\nconst UNKNOWN_PROVIDER = \"unknown\";\n\nexport function getProviderLoginHelp(): string {\n\treturn [\n\t\t\"Use /login to log into a provider via OAuth or API key. See:\",\n\t\t` ${join(getDocsPath(), \"providers.md\")}`,\n\t\t` ${join(getDocsPath(), \"models.md\")}`,\n\t].join(\"\\n\");\n}\n\nexport function formatNoModelsAvailableMessage(): string {\n\treturn `No models available. ${getProviderLoginHelp()}`;\n}\n\nexport function formatNoModelSelectedMessage(): string {\n\treturn `No model selected.\\n\\n${getProviderLoginHelp()}\\n\\nThen use /model to select a model.`;\n}\n\nexport function formatNoApiKeyFoundMessage(provider: string): string {\n\tconst providerDisplay = provider === UNKNOWN_PROVIDER ? \"the selected model\" : provider;\n\treturn `No API key found for ${providerDisplay}.\\n\\n${getProviderLoginHelp()}`;\n}\n"]}
|
||||||
56
dist/core/auth-storage.d.ts
vendored
Normal file
56
dist/core/auth-storage.d.ts
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* CredentialStore implementation backed by auth.json.
|
||||||
|
* Provider auth orchestration belongs to ModelRuntime and pi-ai Models.
|
||||||
|
*/
|
||||||
|
import type { Credential, CredentialInfo, CredentialStore } from "@earendil-works/pi-ai";
|
||||||
|
type AuthStorageData = Record<string, Credential>;
|
||||||
|
type LockResult<T> = {
|
||||||
|
result: T;
|
||||||
|
next?: string;
|
||||||
|
};
|
||||||
|
export interface AuthStorageBackend {
|
||||||
|
withLock<T>(fn: (current: string | undefined) => LockResult<T>): T;
|
||||||
|
withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;
|
||||||
|
}
|
||||||
|
export declare class FileAuthStorageBackend implements AuthStorageBackend {
|
||||||
|
private authPath;
|
||||||
|
constructor(authPath?: string);
|
||||||
|
private ensureParentDir;
|
||||||
|
private ensureFileExists;
|
||||||
|
private acquireLockSyncWithRetry;
|
||||||
|
withLock<T>(fn: (current: string | undefined) => LockResult<T>): T;
|
||||||
|
withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;
|
||||||
|
}
|
||||||
|
export declare class InMemoryAuthStorageBackend implements AuthStorageBackend {
|
||||||
|
private value;
|
||||||
|
withLock<T>(fn: (current: string | undefined) => LockResult<T>): T;
|
||||||
|
withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Credential storage backed by a JSON file.
|
||||||
|
*/
|
||||||
|
export declare class AuthStorage implements CredentialStore {
|
||||||
|
private data;
|
||||||
|
private storage;
|
||||||
|
private constructor();
|
||||||
|
static create(authPath?: string): AuthStorage;
|
||||||
|
static fromStorage(storage: AuthStorageBackend): AuthStorage;
|
||||||
|
static inMemory(data?: AuthStorageData): AuthStorage;
|
||||||
|
private parseStorageData;
|
||||||
|
/**
|
||||||
|
* Reload credentials from storage.
|
||||||
|
*/
|
||||||
|
reload(): void;
|
||||||
|
read(provider: string): Promise<Credential | undefined>;
|
||||||
|
modify(provider: string, fn: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
|
||||||
|
delete(provider: string): Promise<void>;
|
||||||
|
/** List credential metadata without resolving configured key values. */
|
||||||
|
list(): Promise<readonly CredentialInfo[]>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* One-off synchronous read of a stored credential from an auth.json file,
|
||||||
|
* without instantiating a store or resolving configured key values.
|
||||||
|
*/
|
||||||
|
export declare function readStoredCredential(providerId: string, authPath?: string): Credential | undefined;
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=auth-storage.d.ts.map
|
||||||
1
dist/core/auth-storage.d.ts.map
vendored
Normal file
1
dist/core/auth-storage.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
229
dist/core/auth-storage.js
vendored
Normal file
229
dist/core/auth-storage.js
vendored
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
/**
|
||||||
|
* CredentialStore implementation backed by auth.json.
|
||||||
|
* Provider auth orchestration belongs to ModelRuntime and pi-ai Models.
|
||||||
|
*/
|
||||||
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import lockfile from "proper-lockfile";
|
||||||
|
import { getAgentDir } from "../config.js";
|
||||||
|
import { normalizePath } from "../utils/paths.js";
|
||||||
|
import { resolveConfigValue } from "./resolve-config-value.js";
|
||||||
|
const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf-8", mode: 0o600 };
|
||||||
|
export class FileAuthStorageBackend {
|
||||||
|
authPath;
|
||||||
|
constructor(authPath = join(getAgentDir(), "auth.json")) {
|
||||||
|
this.authPath = normalizePath(authPath);
|
||||||
|
}
|
||||||
|
ensureParentDir() {
|
||||||
|
const dir = dirname(this.authPath);
|
||||||
|
if (!existsSync(dir)) {
|
||||||
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ensureFileExists() {
|
||||||
|
if (!existsSync(this.authPath)) {
|
||||||
|
writeFileSync(this.authPath, "{}", AUTH_FILE_WRITE_OPTIONS);
|
||||||
|
chmodSync(this.authPath, 0o600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acquireLockSyncWithRetry(path) {
|
||||||
|
const maxAttempts = 10;
|
||||||
|
const delayMs = 20;
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return lockfile.lockSync(path, { realpath: false });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const code = typeof error === "object" && error !== null && "code" in error
|
||||||
|
? String(error.code)
|
||||||
|
: undefined;
|
||||||
|
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
lastError = error;
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < delayMs) {
|
||||||
|
// Sleep synchronously to avoid changing callers to async.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError ?? new Error("Failed to acquire auth storage lock");
|
||||||
|
}
|
||||||
|
withLock(fn) {
|
||||||
|
this.ensureParentDir();
|
||||||
|
this.ensureFileExists();
|
||||||
|
let release;
|
||||||
|
try {
|
||||||
|
release = this.acquireLockSyncWithRetry(this.authPath);
|
||||||
|
const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined;
|
||||||
|
const { result, next } = fn(current);
|
||||||
|
if (next !== undefined) {
|
||||||
|
writeFileSync(this.authPath, next, AUTH_FILE_WRITE_OPTIONS);
|
||||||
|
chmodSync(this.authPath, 0o600);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (release) {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async withLockAsync(fn) {
|
||||||
|
this.ensureParentDir();
|
||||||
|
this.ensureFileExists();
|
||||||
|
let release;
|
||||||
|
let lockCompromised = false;
|
||||||
|
let lockCompromisedError;
|
||||||
|
const throwIfCompromised = () => {
|
||||||
|
if (lockCompromised) {
|
||||||
|
throw lockCompromisedError ?? new Error("Auth storage lock was compromised");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
release = await lockfile.lock(this.authPath, {
|
||||||
|
retries: {
|
||||||
|
retries: 10,
|
||||||
|
factor: 2,
|
||||||
|
minTimeout: 100,
|
||||||
|
maxTimeout: 10000,
|
||||||
|
randomize: true,
|
||||||
|
},
|
||||||
|
stale: 30000,
|
||||||
|
onCompromised: (err) => {
|
||||||
|
lockCompromised = true;
|
||||||
|
lockCompromisedError = err;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
throwIfCompromised();
|
||||||
|
const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined;
|
||||||
|
const { result, next } = await fn(current);
|
||||||
|
throwIfCompromised();
|
||||||
|
if (next !== undefined) {
|
||||||
|
writeFileSync(this.authPath, next, AUTH_FILE_WRITE_OPTIONS);
|
||||||
|
chmodSync(this.authPath, 0o600);
|
||||||
|
}
|
||||||
|
throwIfCompromised();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (release) {
|
||||||
|
try {
|
||||||
|
await release();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Ignore unlock errors when lock is compromised.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class InMemoryAuthStorageBackend {
|
||||||
|
value;
|
||||||
|
withLock(fn) {
|
||||||
|
const { result, next } = fn(this.value);
|
||||||
|
if (next !== undefined) {
|
||||||
|
this.value = next;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async withLockAsync(fn) {
|
||||||
|
const { result, next } = await fn(this.value);
|
||||||
|
if (next !== undefined) {
|
||||||
|
this.value = next;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Credential storage backed by a JSON file.
|
||||||
|
*/
|
||||||
|
export class AuthStorage {
|
||||||
|
data = {};
|
||||||
|
storage;
|
||||||
|
constructor(storage) {
|
||||||
|
this.storage = storage;
|
||||||
|
this.reload();
|
||||||
|
}
|
||||||
|
static create(authPath) {
|
||||||
|
return new AuthStorage(new FileAuthStorageBackend(authPath ?? join(getAgentDir(), "auth.json")));
|
||||||
|
}
|
||||||
|
static fromStorage(storage) {
|
||||||
|
return new AuthStorage(storage);
|
||||||
|
}
|
||||||
|
static inMemory(data = {}) {
|
||||||
|
const storage = new InMemoryAuthStorageBackend();
|
||||||
|
storage.withLock(() => ({ result: undefined, next: JSON.stringify(data, null, 2) }));
|
||||||
|
return AuthStorage.fromStorage(storage);
|
||||||
|
}
|
||||||
|
parseStorageData(content) {
|
||||||
|
if (!content) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Reload credentials from storage.
|
||||||
|
*/
|
||||||
|
reload() {
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
this.storage.withLock((current) => {
|
||||||
|
content = current;
|
||||||
|
return { result: undefined };
|
||||||
|
});
|
||||||
|
this.data = this.parseStorageData(content);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Preserve the last valid in-memory snapshot.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async read(provider) {
|
||||||
|
const credential = this.data[provider];
|
||||||
|
if (credential?.type !== "api_key")
|
||||||
|
return credential;
|
||||||
|
if (credential.key === undefined)
|
||||||
|
return credential;
|
||||||
|
return { ...credential, key: resolveConfigValue(credential.key, credential.env) };
|
||||||
|
}
|
||||||
|
async modify(provider, fn) {
|
||||||
|
return this.storage.withLockAsync(async (content) => {
|
||||||
|
const currentData = this.parseStorageData(content);
|
||||||
|
const next = await fn(currentData[provider]);
|
||||||
|
if (next === undefined) {
|
||||||
|
this.data = currentData;
|
||||||
|
return { result: currentData[provider] };
|
||||||
|
}
|
||||||
|
const merged = { ...currentData, [provider]: next };
|
||||||
|
this.data = merged;
|
||||||
|
return { result: next, next: JSON.stringify(merged, null, 2) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async delete(provider) {
|
||||||
|
await this.storage.withLockAsync(async (content) => {
|
||||||
|
const currentData = this.parseStorageData(content);
|
||||||
|
delete currentData[provider];
|
||||||
|
this.data = currentData;
|
||||||
|
return { result: undefined, next: JSON.stringify(currentData, null, 2) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** List credential metadata without resolving configured key values. */
|
||||||
|
async list() {
|
||||||
|
return Object.entries(this.data).map(([providerId, credential]) => ({ providerId, type: credential.type }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* One-off synchronous read of a stored credential from an auth.json file,
|
||||||
|
* without instantiating a store or resolving configured key values.
|
||||||
|
*/
|
||||||
|
export function readStoredCredential(providerId, authPath = join(getAgentDir(), "auth.json")) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(readFileSync(normalizePath(authPath), "utf-8"));
|
||||||
|
return data[providerId];
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=auth-storage.js.map
|
||||||
1
dist/core/auth-storage.js.map
vendored
Normal file
1
dist/core/auth-storage.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
32
dist/core/bash-executor.d.ts
vendored
Normal file
32
dist/core/bash-executor.d.ts
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Bash command execution with streaming support and cancellation.
|
||||||
|
*
|
||||||
|
* This module provides a unified bash execution implementation used by:
|
||||||
|
* - AgentSession.executeBash() for interactive and RPC modes
|
||||||
|
* - Direct calls from modes that need bash execution
|
||||||
|
*/
|
||||||
|
import type { BashOperations } from "./tools/bash.ts";
|
||||||
|
export interface BashExecutorOptions {
|
||||||
|
/** Callback for streaming output chunks (already sanitized) */
|
||||||
|
onChunk?: (chunk: string) => void;
|
||||||
|
/** AbortSignal for cancellation */
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
export interface BashResult {
|
||||||
|
/** Combined stdout + stderr output (sanitized, possibly truncated) */
|
||||||
|
output: string;
|
||||||
|
/** Process exit code (undefined if killed/cancelled) */
|
||||||
|
exitCode: number | undefined;
|
||||||
|
/** Whether the command was cancelled via signal */
|
||||||
|
cancelled: boolean;
|
||||||
|
/** Whether the output was truncated */
|
||||||
|
truncated: boolean;
|
||||||
|
/** Path to temp file containing full output (if output exceeded truncation threshold) */
|
||||||
|
fullOutputPath?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Execute a bash command using custom BashOperations.
|
||||||
|
* Used for remote execution (SSH, containers, etc.).
|
||||||
|
*/
|
||||||
|
export declare function executeBashWithOperations(command: string, cwd: string, operations: BashOperations, options?: BashExecutorOptions): Promise<BashResult>;
|
||||||
|
//# sourceMappingURL=bash-executor.d.ts.map
|
||||||
1
dist/core/bash-executor.d.ts.map
vendored
Normal file
1
dist/core/bash-executor.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
111
dist/core/bash-executor.js
vendored
Normal file
111
dist/core/bash-executor.js
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* Bash command execution with streaming support and cancellation.
|
||||||
|
*
|
||||||
|
* This module provides a unified bash execution implementation used by:
|
||||||
|
* - AgentSession.executeBash() for interactive and RPC modes
|
||||||
|
* - Direct calls from modes that need bash execution
|
||||||
|
*/
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
import { createWriteStream } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { stripAnsi } from "../utils/ansi.js";
|
||||||
|
import { sanitizeBinaryOutput } from "../utils/shell.js";
|
||||||
|
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
|
||||||
|
// ============================================================================
|
||||||
|
// Implementation
|
||||||
|
// ============================================================================
|
||||||
|
/**
|
||||||
|
* Execute a bash command using custom BashOperations.
|
||||||
|
* Used for remote execution (SSH, containers, etc.).
|
||||||
|
*/
|
||||||
|
export async function executeBashWithOperations(command, cwd, operations, options) {
|
||||||
|
const outputChunks = [];
|
||||||
|
let outputBytes = 0;
|
||||||
|
const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
|
||||||
|
let tempFilePath;
|
||||||
|
let tempFileStream;
|
||||||
|
let totalBytes = 0;
|
||||||
|
const ensureTempFile = () => {
|
||||||
|
if (tempFilePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = randomBytes(8).toString("hex");
|
||||||
|
tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
|
||||||
|
tempFileStream = createWriteStream(tempFilePath);
|
||||||
|
for (const chunk of outputChunks) {
|
||||||
|
tempFileStream.write(chunk);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const onData = (data) => {
|
||||||
|
totalBytes += data.length;
|
||||||
|
// Sanitize: strip ANSI, replace binary garbage, normalize newlines
|
||||||
|
const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\r/g, "");
|
||||||
|
// Start writing to temp file if exceeds threshold
|
||||||
|
if (totalBytes > DEFAULT_MAX_BYTES) {
|
||||||
|
ensureTempFile();
|
||||||
|
}
|
||||||
|
if (tempFileStream) {
|
||||||
|
tempFileStream.write(text);
|
||||||
|
}
|
||||||
|
// Keep rolling buffer
|
||||||
|
outputChunks.push(text);
|
||||||
|
outputBytes += text.length;
|
||||||
|
while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
|
||||||
|
const removed = outputChunks.shift();
|
||||||
|
outputBytes -= removed.length;
|
||||||
|
}
|
||||||
|
// Stream to callback
|
||||||
|
if (options?.onChunk) {
|
||||||
|
options.onChunk(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const result = await operations.exec(command, cwd, {
|
||||||
|
onData,
|
||||||
|
signal: options?.signal,
|
||||||
|
});
|
||||||
|
const fullOutput = outputChunks.join("");
|
||||||
|
const truncationResult = truncateTail(fullOutput);
|
||||||
|
if (truncationResult.truncated) {
|
||||||
|
ensureTempFile();
|
||||||
|
}
|
||||||
|
if (tempFileStream) {
|
||||||
|
tempFileStream.end();
|
||||||
|
}
|
||||||
|
const cancelled = options?.signal?.aborted ?? false;
|
||||||
|
return {
|
||||||
|
output: truncationResult.truncated ? truncationResult.content : fullOutput,
|
||||||
|
exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
|
||||||
|
cancelled,
|
||||||
|
truncated: truncationResult.truncated,
|
||||||
|
fullOutputPath: tempFilePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// Check if it was an abort
|
||||||
|
if (options?.signal?.aborted) {
|
||||||
|
const fullOutput = outputChunks.join("");
|
||||||
|
const truncationResult = truncateTail(fullOutput);
|
||||||
|
if (truncationResult.truncated) {
|
||||||
|
ensureTempFile();
|
||||||
|
}
|
||||||
|
if (tempFileStream) {
|
||||||
|
tempFileStream.end();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
output: truncationResult.truncated ? truncationResult.content : fullOutput,
|
||||||
|
exitCode: undefined,
|
||||||
|
cancelled: true,
|
||||||
|
truncated: truncationResult.truncated,
|
||||||
|
fullOutputPath: tempFilePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (tempFileStream) {
|
||||||
|
tempFileStream.end();
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=bash-executor.js.map
|
||||||
1
dist/core/bash-executor.js.map
vendored
Normal file
1
dist/core/bash-executor.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
49
dist/core/cache-stats.d.ts
vendored
Normal file
49
dist/core/cache-stats.d.ts
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
||||||
|
import type { SessionEntry } from "./session-manager.ts";
|
||||||
|
/**
|
||||||
|
* Prompt-cache TTL: idle gaps longer than this are worth mentioning as the
|
||||||
|
* likely cause of a miss. Anthropic's default cache TTL is 5 minutes.
|
||||||
|
*/
|
||||||
|
export declare const CACHE_TTL_MS: number;
|
||||||
|
/** A counted cache miss on a single assistant message. */
|
||||||
|
export interface CacheMiss {
|
||||||
|
/** Prompt tokens that were in the previous turn's prompt but not read from cache. */
|
||||||
|
missedTokens: number;
|
||||||
|
/** Extra dollars paid vs. a full cache hit; 0 when pricing is unknown. */
|
||||||
|
missedCost: number;
|
||||||
|
/** Milliseconds since the previous request (which last refreshed the cache). */
|
||||||
|
idleMs: number;
|
||||||
|
/** True when the model changed relative to the previous request. */
|
||||||
|
modelChanged: boolean;
|
||||||
|
}
|
||||||
|
export interface CacheWasteTotals {
|
||||||
|
missedTokens: number;
|
||||||
|
missedCost: number;
|
||||||
|
/** Number of counted misses (turns above the noise floor). */
|
||||||
|
missCount: number;
|
||||||
|
}
|
||||||
|
/** Minimal pricing lookup, satisfied by ModelRuntime. Cost is $/million tokens. */
|
||||||
|
export interface ModelPriceSource {
|
||||||
|
getModel(provider: string, modelId: string): {
|
||||||
|
cost: {
|
||||||
|
cacheRead: number;
|
||||||
|
};
|
||||||
|
} | undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cumulative cache waste across a session: prompt tokens that should have been
|
||||||
|
* cache reads (they were in the previous turn's prompt) but were re-billed.
|
||||||
|
*/
|
||||||
|
export declare function computeCacheWaste(entries: SessionEntry[], models: ModelPriceSource): CacheWasteTotals;
|
||||||
|
/**
|
||||||
|
* All counted cache misses across a session, keyed by the assistant message
|
||||||
|
* (by reference) that paid for them. Used to re-derive transcript notices when
|
||||||
|
* rebuilding the chat from entries (resume, post-compaction rebuild).
|
||||||
|
*/
|
||||||
|
export declare function collectCacheMisses(entries: SessionEntry[], models: ModelPriceSource): Map<AssistantMessage, CacheMiss>;
|
||||||
|
/**
|
||||||
|
* Detect a cache miss on a just-completed assistant message.
|
||||||
|
* `entries` must not yet contain `message` (message_end fires before persistence).
|
||||||
|
*/
|
||||||
|
export declare function detectCacheMiss(entries: SessionEntry[], message: AssistantMessage, models: ModelPriceSource): CacheMiss | undefined;
|
||||||
|
//# sourceMappingURL=cache-stats.d.ts.map
|
||||||
1
dist/core/cache-stats.d.ts.map
vendored
Normal file
1
dist/core/cache-stats.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
101
dist/core/cache-stats.js
vendored
Normal file
101
dist/core/cache-stats.js
vendored
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* Prompt-cache TTL: idle gaps longer than this are worth mentioning as the
|
||||||
|
* likely cause of a miss. Anthropic's default cache TTL is 5 minutes.
|
||||||
|
*/
|
||||||
|
export const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||||
|
/** Per-turn misses at or below this are cache breakpoint granularity noise. */
|
||||||
|
const NOISE_FLOOR_TOKENS = 1024;
|
||||||
|
/**
|
||||||
|
* Compute the cache miss for one assistant message relative to the previous
|
||||||
|
* request. Returns undefined when nothing is counted: first turn, after a
|
||||||
|
* reset, no cache activity ever reported (provider without cache support), or
|
||||||
|
* miss below the noise floor.
|
||||||
|
*/
|
||||||
|
function detectMiss(prev, message, models) {
|
||||||
|
const usage = message.usage;
|
||||||
|
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
||||||
|
// A zero-cache turn only counts when cache activity was reported before:
|
||||||
|
// on cache-read-only providers that is a total miss, while on providers
|
||||||
|
// that never report caching it means nothing.
|
||||||
|
if (!prev || promptTokens <= 0 || (usage.cacheRead + usage.cacheWrite === 0 && !prev.reportedCache)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const missedTokens = Math.min(prev.promptTokens, promptTokens) - usage.cacheRead;
|
||||||
|
if (missedTokens <= NOISE_FLOOR_TOKENS)
|
||||||
|
return undefined;
|
||||||
|
// Extra cost = missed tokens billed at the actual paid rate (input/cacheWrite,
|
||||||
|
// incl. write premium) instead of the cache-read rate. Missed tokens can only
|
||||||
|
// land in the input or cacheWrite buckets, so the paid rate comes straight
|
||||||
|
// from this message's own cost breakdown.
|
||||||
|
const paidTokens = usage.input + usage.cacheWrite;
|
||||||
|
const paidPerToken = paidTokens > 0 ? (usage.cost.input + usage.cost.cacheWrite) / paidTokens : 0;
|
||||||
|
const readPerToken = usage.cacheRead > 0
|
||||||
|
? usage.cost.cacheRead / usage.cacheRead
|
||||||
|
: (models.getModel(message.provider, message.model)?.cost.cacheRead ?? 0) / 1_000_000;
|
||||||
|
return {
|
||||||
|
missedTokens,
|
||||||
|
missedCost: missedTokens * Math.max(0, paidPerToken - readPerToken),
|
||||||
|
idleMs: Math.max(0, message.timestamp - prev.timestamp),
|
||||||
|
modelChanged: `${message.provider}/${message.model}` !== prev.modelKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function asPreviousRequest(message, reportedCache) {
|
||||||
|
const usage = message.usage;
|
||||||
|
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
||||||
|
if (promptTokens <= 0)
|
||||||
|
return undefined;
|
||||||
|
return {
|
||||||
|
promptTokens,
|
||||||
|
modelKey: `${message.provider}/${message.model}`,
|
||||||
|
timestamp: message.timestamp,
|
||||||
|
reportedCache: reportedCache || usage.cacheRead + usage.cacheWrite > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function scan(entries, models) {
|
||||||
|
let prev;
|
||||||
|
const totals = { missedTokens: 0, missedCost: 0, missCount: 0 };
|
||||||
|
const misses = new Map();
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
||||||
|
// The context legitimately changed; the next turn's prompt is new content,
|
||||||
|
// not re-billed content. Model switches are NOT exempt: they re-bill the
|
||||||
|
// full prompt and should be counted.
|
||||||
|
prev = undefined;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
||||||
|
const miss = detectMiss(prev, entry.message, models);
|
||||||
|
if (miss) {
|
||||||
|
totals.missedTokens += miss.missedTokens;
|
||||||
|
totals.missedCost += miss.missedCost;
|
||||||
|
totals.missCount += 1;
|
||||||
|
misses.set(entry.message, miss);
|
||||||
|
}
|
||||||
|
prev = asPreviousRequest(entry.message, prev?.reportedCache ?? false) ?? prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { prev, totals, misses };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cumulative cache waste across a session: prompt tokens that should have been
|
||||||
|
* cache reads (they were in the previous turn's prompt) but were re-billed.
|
||||||
|
*/
|
||||||
|
export function computeCacheWaste(entries, models) {
|
||||||
|
return scan(entries, models).totals;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All counted cache misses across a session, keyed by the assistant message
|
||||||
|
* (by reference) that paid for them. Used to re-derive transcript notices when
|
||||||
|
* rebuilding the chat from entries (resume, post-compaction rebuild).
|
||||||
|
*/
|
||||||
|
export function collectCacheMisses(entries, models) {
|
||||||
|
return scan(entries, models).misses;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Detect a cache miss on a just-completed assistant message.
|
||||||
|
* `entries` must not yet contain `message` (message_end fires before persistence).
|
||||||
|
*/
|
||||||
|
export function detectCacheMiss(entries, message, models) {
|
||||||
|
return detectMiss(scan(entries, models).prev, message, models);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=cache-stats.js.map
|
||||||
1
dist/core/cache-stats.js.map
vendored
Normal file
1
dist/core/cache-stats.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
98
dist/core/compaction/branch-summarization.d.ts
vendored
Normal file
98
dist/core/compaction/branch-summarization.d.ts
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* Branch summarization for tree navigation.
|
||||||
|
*
|
||||||
|
* When navigating to a different point in the session tree, this generates
|
||||||
|
* a summary of the branch being left so context isn't lost.
|
||||||
|
*/
|
||||||
|
import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core";
|
||||||
|
import type { RetryCallbacks, RetryPolicy } from "@earendil-works/pi-ai";
|
||||||
|
import type { Model, Usage } from "@earendil-works/pi-ai/compat";
|
||||||
|
import type { ReadonlySessionManager, SessionEntry } from "../session-manager.ts";
|
||||||
|
import { type FileOperations } from "./utils.ts";
|
||||||
|
export interface BranchSummaryResult {
|
||||||
|
summary?: string;
|
||||||
|
usage?: Usage;
|
||||||
|
readFiles?: string[];
|
||||||
|
modifiedFiles?: string[];
|
||||||
|
aborted?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
/** Details stored in BranchSummaryEntry.details for file tracking */
|
||||||
|
export interface BranchSummaryDetails {
|
||||||
|
readFiles: string[];
|
||||||
|
modifiedFiles: string[];
|
||||||
|
}
|
||||||
|
export type { FileOperations } from "./utils.ts";
|
||||||
|
export interface BranchPreparation {
|
||||||
|
/** Messages extracted for summarization, in chronological order */
|
||||||
|
messages: AgentMessage[];
|
||||||
|
/** File operations extracted from tool calls */
|
||||||
|
fileOps: FileOperations;
|
||||||
|
/** Total estimated tokens in messages */
|
||||||
|
totalTokens: number;
|
||||||
|
}
|
||||||
|
export interface CollectEntriesResult {
|
||||||
|
/** Entries to summarize, in chronological order */
|
||||||
|
entries: SessionEntry[];
|
||||||
|
/** Common ancestor between old and new position, if any */
|
||||||
|
commonAncestorId: string | null;
|
||||||
|
}
|
||||||
|
export interface GenerateBranchSummaryOptions {
|
||||||
|
/** Model to use for summarization */
|
||||||
|
model: Model<any>;
|
||||||
|
/** API key for the model */
|
||||||
|
apiKey?: string;
|
||||||
|
/** Request headers for the model */
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
/** Provider-scoped environment values for the model */
|
||||||
|
env?: Record<string, string>;
|
||||||
|
/** Abort signal for cancellation */
|
||||||
|
signal: AbortSignal;
|
||||||
|
/** Optional custom instructions for summarization */
|
||||||
|
customInstructions?: string;
|
||||||
|
/** If true, customInstructions replaces the default prompt instead of being appended */
|
||||||
|
replaceInstructions?: boolean;
|
||||||
|
/** Tokens reserved for prompt + LLM response (default 16384) */
|
||||||
|
reserveTokens?: number;
|
||||||
|
/** Optional session stream function. Used to preserve SDK request behavior without mutating agent state. */
|
||||||
|
streamFn?: StreamFn;
|
||||||
|
/** Retry policy for transient summarization errors. Reuses coding-agent's `settings.retry`. */
|
||||||
|
retry?: RetryPolicy;
|
||||||
|
/** Optional callbacks for retry reporting (e.g. TUI retry indicators). */
|
||||||
|
callbacks?: RetryCallbacks;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Collect entries that should be summarized when navigating from one position to another.
|
||||||
|
*
|
||||||
|
* Walks from oldLeafId back to the common ancestor with targetId, collecting entries
|
||||||
|
* along the way. Does NOT stop at compaction boundaries - those are included and their
|
||||||
|
* summaries become context.
|
||||||
|
*
|
||||||
|
* @param session - Session manager (read-only access)
|
||||||
|
* @param oldLeafId - Current position (where we're navigating from)
|
||||||
|
* @param targetId - Target position (where we're navigating to)
|
||||||
|
* @returns Entries to summarize and the common ancestor
|
||||||
|
*/
|
||||||
|
export declare function collectEntriesForBranchSummary(session: ReadonlySessionManager, oldLeafId: string | null, targetId: string): CollectEntriesResult;
|
||||||
|
/**
|
||||||
|
* Prepare entries for summarization with token budget.
|
||||||
|
*
|
||||||
|
* Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget.
|
||||||
|
* This ensures we keep the most recent context when the branch is too long.
|
||||||
|
*
|
||||||
|
* Also collects file operations from:
|
||||||
|
* - Tool calls in assistant messages
|
||||||
|
* - Existing branch_summary entries' details (for cumulative tracking)
|
||||||
|
*
|
||||||
|
* @param entries - Entries in chronological order
|
||||||
|
* @param tokenBudget - Maximum tokens to include (0 = no limit)
|
||||||
|
*/
|
||||||
|
export declare function prepareBranchEntries(entries: SessionEntry[], tokenBudget?: number): BranchPreparation;
|
||||||
|
/**
|
||||||
|
* Generate a summary of abandoned branch entries.
|
||||||
|
*
|
||||||
|
* @param entries - Session entries to summarize (chronological order)
|
||||||
|
* @param options - Generation options
|
||||||
|
*/
|
||||||
|
export declare function generateBranchSummary(entries: SessionEntry[], options: GenerateBranchSummaryOptions): Promise<BranchSummaryResult>;
|
||||||
|
//# sourceMappingURL=branch-summarization.d.ts.map
|
||||||
1
dist/core/compaction/branch-summarization.d.ts.map
vendored
Normal file
1
dist/core/compaction/branch-summarization.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
246
dist/core/compaction/branch-summarization.js
vendored
Normal file
246
dist/core/compaction/branch-summarization.js
vendored
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
/**
|
||||||
|
* Branch summarization for tree navigation.
|
||||||
|
*
|
||||||
|
* When navigating to a different point in the session tree, this generates
|
||||||
|
* a summary of the branch being left so context isn't lost.
|
||||||
|
*/
|
||||||
|
import { contentText } from "@earendil-works/pi-ai";
|
||||||
|
import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
||||||
|
import { completeSummarization, estimateTokens } from "./compaction.js";
|
||||||
|
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
||||||
|
// ============================================================================
|
||||||
|
// Entry Collection
|
||||||
|
// ============================================================================
|
||||||
|
/**
|
||||||
|
* Collect entries that should be summarized when navigating from one position to another.
|
||||||
|
*
|
||||||
|
* Walks from oldLeafId back to the common ancestor with targetId, collecting entries
|
||||||
|
* along the way. Does NOT stop at compaction boundaries - those are included and their
|
||||||
|
* summaries become context.
|
||||||
|
*
|
||||||
|
* @param session - Session manager (read-only access)
|
||||||
|
* @param oldLeafId - Current position (where we're navigating from)
|
||||||
|
* @param targetId - Target position (where we're navigating to)
|
||||||
|
* @returns Entries to summarize and the common ancestor
|
||||||
|
*/
|
||||||
|
export function collectEntriesForBranchSummary(session, oldLeafId, targetId) {
|
||||||
|
// If no old position, nothing to summarize
|
||||||
|
if (!oldLeafId) {
|
||||||
|
return { entries: [], commonAncestorId: null };
|
||||||
|
}
|
||||||
|
// Find common ancestor (deepest node that's on both paths)
|
||||||
|
const oldPath = new Set(session.getBranch(oldLeafId).map((e) => e.id));
|
||||||
|
const targetPath = session.getBranch(targetId);
|
||||||
|
// targetPath is root-first, so iterate backwards to find deepest common ancestor
|
||||||
|
let commonAncestorId = null;
|
||||||
|
for (let i = targetPath.length - 1; i >= 0; i--) {
|
||||||
|
if (oldPath.has(targetPath[i].id)) {
|
||||||
|
commonAncestorId = targetPath[i].id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Collect entries from old leaf back to common ancestor
|
||||||
|
const entries = [];
|
||||||
|
let current = oldLeafId;
|
||||||
|
while (current && current !== commonAncestorId) {
|
||||||
|
const entry = session.getEntry(current);
|
||||||
|
if (!entry)
|
||||||
|
break;
|
||||||
|
entries.push(entry);
|
||||||
|
current = entry.parentId;
|
||||||
|
}
|
||||||
|
// Reverse to get chronological order
|
||||||
|
entries.reverse();
|
||||||
|
return { entries, commonAncestorId };
|
||||||
|
}
|
||||||
|
// ============================================================================
|
||||||
|
// Entry to Message Conversion
|
||||||
|
// ============================================================================
|
||||||
|
/**
|
||||||
|
* Extract AgentMessage from a session entry.
|
||||||
|
* Similar to getMessageFromEntry in compaction.ts but also handles compaction entries.
|
||||||
|
*/
|
||||||
|
function getMessageFromEntry(entry) {
|
||||||
|
switch (entry.type) {
|
||||||
|
case "message":
|
||||||
|
// Skip tool results - context is in assistant's tool call
|
||||||
|
if (entry.message.role === "toolResult")
|
||||||
|
return undefined;
|
||||||
|
return entry.message;
|
||||||
|
case "custom_message":
|
||||||
|
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
|
||||||
|
case "branch_summary":
|
||||||
|
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
||||||
|
case "compaction":
|
||||||
|
return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);
|
||||||
|
// These don't contribute to conversation content
|
||||||
|
case "thinking_level_change":
|
||||||
|
case "model_change":
|
||||||
|
case "custom":
|
||||||
|
case "label":
|
||||||
|
case "session_info":
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Prepare entries for summarization with token budget.
|
||||||
|
*
|
||||||
|
* Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget.
|
||||||
|
* This ensures we keep the most recent context when the branch is too long.
|
||||||
|
*
|
||||||
|
* Also collects file operations from:
|
||||||
|
* - Tool calls in assistant messages
|
||||||
|
* - Existing branch_summary entries' details (for cumulative tracking)
|
||||||
|
*
|
||||||
|
* @param entries - Entries in chronological order
|
||||||
|
* @param tokenBudget - Maximum tokens to include (0 = no limit)
|
||||||
|
*/
|
||||||
|
export function prepareBranchEntries(entries, tokenBudget = 0) {
|
||||||
|
const messages = [];
|
||||||
|
const fileOps = createFileOps();
|
||||||
|
let totalTokens = 0;
|
||||||
|
// First pass: collect file ops from ALL entries (even if they don't fit in token budget)
|
||||||
|
// This ensures we capture cumulative file tracking from nested branch summaries
|
||||||
|
// Only extract from pi-generated summaries (fromHook !== true), not extension-generated ones
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.type === "branch_summary" && !entry.fromHook && entry.details) {
|
||||||
|
const details = entry.details;
|
||||||
|
if (Array.isArray(details.readFiles)) {
|
||||||
|
for (const f of details.readFiles)
|
||||||
|
fileOps.read.add(f);
|
||||||
|
}
|
||||||
|
if (Array.isArray(details.modifiedFiles)) {
|
||||||
|
// Modified files go into both edited and written for proper deduplication
|
||||||
|
for (const f of details.modifiedFiles) {
|
||||||
|
fileOps.edited.add(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Second pass: walk from newest to oldest, adding messages until token budget
|
||||||
|
for (let i = entries.length - 1; i >= 0; i--) {
|
||||||
|
const entry = entries[i];
|
||||||
|
const message = getMessageFromEntry(entry);
|
||||||
|
if (!message)
|
||||||
|
continue;
|
||||||
|
// Extract file ops from assistant messages (tool calls)
|
||||||
|
extractFileOpsFromMessage(message, fileOps);
|
||||||
|
const tokens = estimateTokens(message);
|
||||||
|
// Check budget before adding
|
||||||
|
if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
|
||||||
|
// If this is a summary entry, try to fit it anyway as it's important context
|
||||||
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
||||||
|
if (totalTokens < tokenBudget * 0.9) {
|
||||||
|
messages.unshift(message);
|
||||||
|
totalTokens += tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Stop - we've hit the budget
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
messages.unshift(message);
|
||||||
|
totalTokens += tokens;
|
||||||
|
}
|
||||||
|
return { messages, fileOps, totalTokens };
|
||||||
|
}
|
||||||
|
// ============================================================================
|
||||||
|
// Summary Generation
|
||||||
|
// ============================================================================
|
||||||
|
const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here.
|
||||||
|
Summary of that exploration:
|
||||||
|
|
||||||
|
`;
|
||||||
|
const BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later.
|
||||||
|
|
||||||
|
Use this EXACT format:
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
[What was the user trying to accomplish in this branch?]
|
||||||
|
|
||||||
|
## Constraints & Preferences
|
||||||
|
- [Any constraints, preferences, or requirements mentioned]
|
||||||
|
- [Or "(none)" if none were mentioned]
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
### Done
|
||||||
|
- [x] [Completed tasks/changes]
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
- [ ] [Work that was started but not finished]
|
||||||
|
|
||||||
|
### Blocked
|
||||||
|
- [Issues preventing progress, if any]
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- **[Decision]**: [Brief rationale]
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. [What should happen next to continue this work]
|
||||||
|
|
||||||
|
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
||||||
|
/**
|
||||||
|
* Generate a summary of abandoned branch entries.
|
||||||
|
*
|
||||||
|
* @param entries - Session entries to summarize (chronological order)
|
||||||
|
* @param options - Generation options
|
||||||
|
*/
|
||||||
|
export async function generateBranchSummary(entries, options) {
|
||||||
|
const { model, apiKey, headers, env, signal, customInstructions, replaceInstructions, reserveTokens = 16384, streamFn, retry, callbacks, } = options;
|
||||||
|
// Token budget = context window minus reserved space for prompt + response
|
||||||
|
const contextWindow = model.contextWindow || 128000;
|
||||||
|
const tokenBudget = contextWindow - reserveTokens;
|
||||||
|
const { messages, fileOps } = prepareBranchEntries(entries, tokenBudget);
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return { summary: "No content to summarize" };
|
||||||
|
}
|
||||||
|
// Transform to LLM-compatible messages, then serialize to text
|
||||||
|
// Serialization prevents the model from treating it as a conversation to continue
|
||||||
|
const llmMessages = convertToLlm(messages);
|
||||||
|
const conversationText = serializeConversation(llmMessages);
|
||||||
|
// Build prompt
|
||||||
|
let instructions;
|
||||||
|
if (replaceInstructions && customInstructions) {
|
||||||
|
instructions = customInstructions;
|
||||||
|
}
|
||||||
|
else if (customInstructions) {
|
||||||
|
instructions = `${BRANCH_SUMMARY_PROMPT}\n\nAdditional focus: ${customInstructions}`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
instructions = BRANCH_SUMMARY_PROMPT;
|
||||||
|
}
|
||||||
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${instructions}`;
|
||||||
|
const summarizationMessages = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: promptText }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// Call LLM for summarization. Prefer the session stream function so SDK
|
||||||
|
// request behavior (timeouts, retries, attribution headers) stays consistent
|
||||||
|
// without running through agent state/events. Retried via completeSummarization
|
||||||
|
// so transient stream drops reuse the configured retry policy.
|
||||||
|
const context = { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages };
|
||||||
|
const requestOptions = { apiKey, headers, env, signal, maxTokens: 2048 };
|
||||||
|
const response = await completeSummarization(model, context, requestOptions, streamFn, retry, callbacks);
|
||||||
|
// Check if aborted or errored
|
||||||
|
if (response.stopReason === "aborted") {
|
||||||
|
return { aborted: true };
|
||||||
|
}
|
||||||
|
if (response.stopReason === "error") {
|
||||||
|
return { error: response.errorMessage || "Summarization failed" };
|
||||||
|
}
|
||||||
|
let summary = contentText(response.content);
|
||||||
|
// Prepend preamble to provide context about the branch summary
|
||||||
|
summary = BRANCH_SUMMARY_PREAMBLE + summary;
|
||||||
|
// Compute file lists and append to summary
|
||||||
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
||||||
|
summary += formatFileOperations(readFiles, modifiedFiles);
|
||||||
|
return {
|
||||||
|
summary: summary || "No summary generated",
|
||||||
|
usage: response.usage,
|
||||||
|
readFiles,
|
||||||
|
modifiedFiles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=branch-summarization.js.map
|
||||||
1
dist/core/compaction/branch-summarization.js.map
vendored
Normal file
1
dist/core/compaction/branch-summarization.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
137
dist/core/compaction/compaction.d.ts
vendored
Normal file
137
dist/core/compaction/compaction.d.ts
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
/**
|
||||||
|
* Context compaction for long sessions.
|
||||||
|
*
|
||||||
|
* Pure functions for compaction logic. The session manager handles I/O,
|
||||||
|
* and after compaction the session is reloaded.
|
||||||
|
*/
|
||||||
|
import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||||
|
import { type RetryCallbacks, type RetryPolicy } from "@earendil-works/pi-ai";
|
||||||
|
import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai/compat";
|
||||||
|
import { type SessionEntry } from "../session-manager.ts";
|
||||||
|
import { type FileOperations } from "./utils.ts";
|
||||||
|
/** Details stored in CompactionEntry.details for file tracking */
|
||||||
|
export interface CompactionDetails {
|
||||||
|
readFiles: string[];
|
||||||
|
modifiedFiles: string[];
|
||||||
|
}
|
||||||
|
/** Result from compact() - SessionManager adds uuid/parentUuid when saving */
|
||||||
|
export interface CompactionResult<T = unknown> {
|
||||||
|
summary: string;
|
||||||
|
firstKeptEntryId: string;
|
||||||
|
tokensBefore: number;
|
||||||
|
estimatedTokensAfter?: number;
|
||||||
|
/** Usage from the LLM call(s) that generated this summary, if available */
|
||||||
|
usage?: Usage;
|
||||||
|
/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
||||||
|
details?: T;
|
||||||
|
}
|
||||||
|
export interface CompactionSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
reserveTokens: number;
|
||||||
|
keepRecentTokens: number;
|
||||||
|
}
|
||||||
|
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
||||||
|
/**
|
||||||
|
* Calculate total context tokens from usage.
|
||||||
|
* Uses the native totalTokens field when available, falls back to computing from components.
|
||||||
|
*/
|
||||||
|
export declare function calculateContextTokens(usage: Usage): number;
|
||||||
|
/**
|
||||||
|
* Find the last valid assistant message usage from session entries.
|
||||||
|
*/
|
||||||
|
export declare function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined;
|
||||||
|
export interface ContextUsageEstimate {
|
||||||
|
tokens: number;
|
||||||
|
usageTokens: number;
|
||||||
|
trailingTokens: number;
|
||||||
|
lastUsageIndex: number | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Estimate context tokens from messages, using the last assistant usage when available.
|
||||||
|
* If there are messages after the last usage, estimate their tokens with estimateTokens.
|
||||||
|
*/
|
||||||
|
export declare function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate;
|
||||||
|
/**
|
||||||
|
* Check if compaction should trigger based on context usage.
|
||||||
|
*/
|
||||||
|
export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
|
||||||
|
/**
|
||||||
|
* Estimate token count for a message using chars/4 heuristic.
|
||||||
|
* This is conservative (overestimates tokens).
|
||||||
|
*/
|
||||||
|
export declare function estimateTokens(message: AgentMessage): number;
|
||||||
|
/**
|
||||||
|
* Find the context-visible user-role message that starts the turn containing the given entry index.
|
||||||
|
* Returns -1 if no turn start found before the index.
|
||||||
|
*/
|
||||||
|
export declare function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number;
|
||||||
|
export interface CutPointResult {
|
||||||
|
/** Index of first entry to keep */
|
||||||
|
firstKeptEntryIndex: number;
|
||||||
|
/** Index of user message that starts the turn being split, or -1 if not splitting */
|
||||||
|
turnStartIndex: number;
|
||||||
|
/** Whether this cut splits a turn (cut point is not a user message) */
|
||||||
|
isSplitTurn: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
||||||
|
*
|
||||||
|
* Algorithm: Walk backwards from newest, accumulating estimated message sizes.
|
||||||
|
* Stop when we've accumulated >= keepRecentTokens. Cut at that point.
|
||||||
|
*
|
||||||
|
* Can cut at user OR assistant messages (never tool results). When cutting at an
|
||||||
|
* assistant message with tool calls, its tool results come after and will be kept.
|
||||||
|
*
|
||||||
|
* Returns CutPointResult with:
|
||||||
|
* - firstKeptEntryIndex: the entry index to start keeping from
|
||||||
|
* - turnStartIndex: if cutting mid-turn, the user message that started that turn
|
||||||
|
* - isSplitTurn: whether we're cutting in the middle of a turn
|
||||||
|
*
|
||||||
|
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
||||||
|
*/
|
||||||
|
export declare function findCutPoint(entries: SessionEntry[], startIndex: number, endIndex: number, keepRecentTokens: number): CutPointResult;
|
||||||
|
/**
|
||||||
|
* Shared choke point for every compaction/branch-summary summarization call. Wraps the
|
||||||
|
* single LLM call in {@link retryAssistantCall} so transient stream drops (e.g.
|
||||||
|
* `terminated`, socket close) honor the configured retry policy instead of failing
|
||||||
|
* the whole compaction on the first attempt. Deterministic errors and aborts return
|
||||||
|
* immediately (see {@link retryAssistantCall}).
|
||||||
|
*/
|
||||||
|
export declare function completeSummarization(model: Model<any>, context: Context, options: SimpleStreamOptions, streamFn?: StreamFn, retry?: RetryPolicy, callbacks?: RetryCallbacks): Promise<AssistantMessage>;
|
||||||
|
/**
|
||||||
|
* Generate a summary of the conversation using the LLM.
|
||||||
|
* If previousSummary is provided, uses the update prompt to merge.
|
||||||
|
*/
|
||||||
|
export declare function generateSummary(currentMessages: AgentMessage[], model: Model<any>, reserveTokens: number, apiKey: string | undefined, headers?: Record<string, string>, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, env?: Record<string, string>, retry?: RetryPolicy, callbacks?: RetryCallbacks): Promise<string>;
|
||||||
|
/** Generate or update a conversation summary and return its provider usage. */
|
||||||
|
export declare function generateSummaryWithUsage(currentMessages: AgentMessage[], model: Model<any>, reserveTokens: number, apiKey: string | undefined, headers?: Record<string, string>, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, env?: Record<string, string>, retry?: RetryPolicy, callbacks?: RetryCallbacks): Promise<{
|
||||||
|
text: string;
|
||||||
|
usage: Usage;
|
||||||
|
}>;
|
||||||
|
export interface CompactionPreparation {
|
||||||
|
/** UUID of first entry to keep */
|
||||||
|
firstKeptEntryId: string;
|
||||||
|
/** Messages that will be summarized and discarded */
|
||||||
|
messagesToSummarize: AgentMessage[];
|
||||||
|
/** Messages that will be turned into turn prefix summary (if splitting) */
|
||||||
|
turnPrefixMessages: AgentMessage[];
|
||||||
|
/** Whether this is a split turn (cut point in middle of turn) */
|
||||||
|
isSplitTurn: boolean;
|
||||||
|
tokensBefore: number;
|
||||||
|
/** Summary from previous compaction, for iterative update */
|
||||||
|
previousSummary?: string;
|
||||||
|
/** File operations extracted from messagesToSummarize */
|
||||||
|
fileOps: FileOperations;
|
||||||
|
/** Compaction settions from settings.jsonl */
|
||||||
|
settings: CompactionSettings;
|
||||||
|
}
|
||||||
|
export declare function prepareCompaction(pathEntries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | undefined;
|
||||||
|
/**
|
||||||
|
* Generate summaries for compaction using prepared data.
|
||||||
|
* Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.
|
||||||
|
*
|
||||||
|
* @param preparation - Pre-calculated preparation from prepareCompaction()
|
||||||
|
* @param customInstructions - Optional custom focus for the summary
|
||||||
|
*/
|
||||||
|
export declare function compact(preparation: CompactionPreparation, model: Model<any>, apiKey: string | undefined, headers?: Record<string, string>, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, env?: Record<string, string>, retry?: RetryPolicy, callbacks?: RetryCallbacks): Promise<CompactionResult>;
|
||||||
|
//# sourceMappingURL=compaction.d.ts.map
|
||||||
1
dist/core/compaction/compaction.d.ts.map
vendored
Normal file
1
dist/core/compaction/compaction.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
645
dist/core/compaction/compaction.js
vendored
Normal file
645
dist/core/compaction/compaction.js
vendored
Normal file
|
|
@ -0,0 +1,645 @@
|
||||||
|
/**
|
||||||
|
* Context compaction for long sessions.
|
||||||
|
*
|
||||||
|
* Pure functions for compaction logic. The session manager handles I/O,
|
||||||
|
* and after compaction the session is reloaded.
|
||||||
|
*/
|
||||||
|
import { contentText, retryAssistantCall, uuidv7 } from "@earendil-works/pi-ai";
|
||||||
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
||||||
|
import { convertToLlm } from "../messages.js";
|
||||||
|
import { buildSessionContext, sessionEntryToContextMessages, } from "../session-manager.js";
|
||||||
|
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
||||||
|
/**
|
||||||
|
* Extract file operations from messages and previous compaction entries.
|
||||||
|
*/
|
||||||
|
function extractFileOperations(messages, entries, prevCompactionIndex) {
|
||||||
|
const fileOps = createFileOps();
|
||||||
|
// Collect from previous compaction's details (if pi-generated)
|
||||||
|
if (prevCompactionIndex >= 0) {
|
||||||
|
const prevCompaction = entries[prevCompactionIndex];
|
||||||
|
if (!prevCompaction.fromHook && prevCompaction.details) {
|
||||||
|
// fromHook field kept for session file compatibility
|
||||||
|
const details = prevCompaction.details;
|
||||||
|
if (Array.isArray(details.readFiles)) {
|
||||||
|
for (const f of details.readFiles)
|
||||||
|
fileOps.read.add(f);
|
||||||
|
}
|
||||||
|
if (Array.isArray(details.modifiedFiles)) {
|
||||||
|
for (const f of details.modifiedFiles)
|
||||||
|
fileOps.edited.add(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Extract from tool calls in messages
|
||||||
|
for (const msg of messages) {
|
||||||
|
extractFileOpsFromMessage(msg, fileOps);
|
||||||
|
}
|
||||||
|
return fileOps;
|
||||||
|
}
|
||||||
|
// ============================================================================
|
||||||
|
// Message Extraction
|
||||||
|
// ============================================================================
|
||||||
|
/**
|
||||||
|
* Extract AgentMessage from an entry if it produces one.
|
||||||
|
* Returns undefined for entries that don't contribute to LLM context.
|
||||||
|
*/
|
||||||
|
function getMessageFromEntryForCompaction(entry) {
|
||||||
|
if (entry.type === "compaction") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return sessionEntryToContextMessages(entry)[0];
|
||||||
|
}
|
||||||
|
function combineUsage(first, second) {
|
||||||
|
return {
|
||||||
|
input: first.input + second.input,
|
||||||
|
output: first.output + second.output,
|
||||||
|
cacheRead: first.cacheRead + second.cacheRead,
|
||||||
|
cacheWrite: first.cacheWrite + second.cacheWrite,
|
||||||
|
...(first.cacheWrite1h !== undefined || second.cacheWrite1h !== undefined
|
||||||
|
? { cacheWrite1h: (first.cacheWrite1h ?? 0) + (second.cacheWrite1h ?? 0) }
|
||||||
|
: {}),
|
||||||
|
...(first.reasoning !== undefined || second.reasoning !== undefined
|
||||||
|
? { reasoning: (first.reasoning ?? 0) + (second.reasoning ?? 0) }
|
||||||
|
: {}),
|
||||||
|
totalTokens: first.totalTokens + second.totalTokens,
|
||||||
|
cost: {
|
||||||
|
input: first.cost.input + second.cost.input,
|
||||||
|
output: first.cost.output + second.cost.output,
|
||||||
|
cacheRead: first.cost.cacheRead + second.cost.cacheRead,
|
||||||
|
cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
|
||||||
|
total: first.cost.total + second.cost.total,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const DEFAULT_COMPACTION_SETTINGS = {
|
||||||
|
enabled: true,
|
||||||
|
reserveTokens: 16384,
|
||||||
|
keepRecentTokens: 20000,
|
||||||
|
};
|
||||||
|
// ============================================================================
|
||||||
|
// Token calculation
|
||||||
|
// ============================================================================
|
||||||
|
/**
|
||||||
|
* Calculate total context tokens from usage.
|
||||||
|
* Uses the native totalTokens field when available, falls back to computing from components.
|
||||||
|
*/
|
||||||
|
export function calculateContextTokens(usage) {
|
||||||
|
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get usage from an assistant message if available.
|
||||||
|
* Skips aborted, error, and all-zero usage messages as they don't have valid usage data.
|
||||||
|
*/
|
||||||
|
function getAssistantUsage(msg) {
|
||||||
|
if (msg.role === "assistant" && "usage" in msg) {
|
||||||
|
const assistantMsg = msg;
|
||||||
|
if (assistantMsg.stopReason !== "aborted" &&
|
||||||
|
assistantMsg.stopReason !== "error" &&
|
||||||
|
assistantMsg.usage &&
|
||||||
|
calculateContextTokens(assistantMsg.usage) > 0) {
|
||||||
|
return assistantMsg.usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Find the last valid assistant message usage from session entries.
|
||||||
|
*/
|
||||||
|
export function getLastAssistantUsage(entries) {
|
||||||
|
for (let i = entries.length - 1; i >= 0; i--) {
|
||||||
|
const entry = entries[i];
|
||||||
|
if (entry.type === "message") {
|
||||||
|
const usage = getAssistantUsage(entry.message);
|
||||||
|
if (usage)
|
||||||
|
return usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
function getLastAssistantUsageInfo(messages) {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const usage = getAssistantUsage(messages[i]);
|
||||||
|
if (usage)
|
||||||
|
return { usage, index: i };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Estimate context tokens from messages, using the last assistant usage when available.
|
||||||
|
* If there are messages after the last usage, estimate their tokens with estimateTokens.
|
||||||
|
*/
|
||||||
|
export function estimateContextTokens(messages) {
|
||||||
|
const usageInfo = getLastAssistantUsageInfo(messages);
|
||||||
|
if (!usageInfo) {
|
||||||
|
let estimated = 0;
|
||||||
|
for (const message of messages) {
|
||||||
|
estimated += estimateTokens(message);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tokens: estimated,
|
||||||
|
usageTokens: 0,
|
||||||
|
trailingTokens: estimated,
|
||||||
|
lastUsageIndex: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const usageTokens = calculateContextTokens(usageInfo.usage);
|
||||||
|
let trailingTokens = 0;
|
||||||
|
for (let i = usageInfo.index + 1; i < messages.length; i++) {
|
||||||
|
trailingTokens += estimateTokens(messages[i]);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tokens: usageTokens + trailingTokens,
|
||||||
|
usageTokens,
|
||||||
|
trailingTokens,
|
||||||
|
lastUsageIndex: usageInfo.index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if compaction should trigger based on context usage.
|
||||||
|
*/
|
||||||
|
export function shouldCompact(contextTokens, contextWindow, settings) {
|
||||||
|
if (!settings.enabled)
|
||||||
|
return false;
|
||||||
|
return contextTokens > contextWindow - settings.reserveTokens;
|
||||||
|
}
|
||||||
|
// ============================================================================
|
||||||
|
// Cut point detection
|
||||||
|
// ============================================================================
|
||||||
|
const ESTIMATED_IMAGE_CHARS = 4800;
|
||||||
|
function estimateTextAndImageContentChars(content) {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content.length;
|
||||||
|
}
|
||||||
|
let chars = 0;
|
||||||
|
for (const block of content) {
|
||||||
|
if (block.type === "text" && block.text) {
|
||||||
|
chars += block.text.length;
|
||||||
|
}
|
||||||
|
else if (block.type === "image") {
|
||||||
|
chars += ESTIMATED_IMAGE_CHARS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chars;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Estimate token count for a message using chars/4 heuristic.
|
||||||
|
* This is conservative (overestimates tokens).
|
||||||
|
*/
|
||||||
|
export function estimateTokens(message) {
|
||||||
|
let chars = 0;
|
||||||
|
switch (message.role) {
|
||||||
|
case "user": {
|
||||||
|
chars = estimateTextAndImageContentChars(message.content);
|
||||||
|
return Math.ceil(chars / 4);
|
||||||
|
}
|
||||||
|
case "assistant": {
|
||||||
|
const assistant = message;
|
||||||
|
for (const block of assistant.content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
chars += block.text.length;
|
||||||
|
}
|
||||||
|
else if (block.type === "thinking") {
|
||||||
|
chars += block.thinking.length;
|
||||||
|
}
|
||||||
|
else if (block.type === "toolCall") {
|
||||||
|
chars += block.name.length + JSON.stringify(block.arguments).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.ceil(chars / 4);
|
||||||
|
}
|
||||||
|
case "custom":
|
||||||
|
case "toolResult": {
|
||||||
|
chars = estimateTextAndImageContentChars(message.content);
|
||||||
|
return Math.ceil(chars / 4);
|
||||||
|
}
|
||||||
|
case "bashExecution": {
|
||||||
|
chars = message.command.length + message.output.length;
|
||||||
|
return Math.ceil(chars / 4);
|
||||||
|
}
|
||||||
|
case "branchSummary":
|
||||||
|
case "compactionSummary": {
|
||||||
|
chars = message.summary.length;
|
||||||
|
return Math.ceil(chars / 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
function isCutPointMessage(message) {
|
||||||
|
switch (message.role) {
|
||||||
|
case "user":
|
||||||
|
case "assistant":
|
||||||
|
case "bashExecution":
|
||||||
|
case "custom":
|
||||||
|
case "branchSummary":
|
||||||
|
case "compactionSummary":
|
||||||
|
return true;
|
||||||
|
case "toolResult":
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function isTurnStartMessage(message) {
|
||||||
|
switch (message.role) {
|
||||||
|
case "user":
|
||||||
|
case "bashExecution":
|
||||||
|
case "custom":
|
||||||
|
case "branchSummary":
|
||||||
|
case "compactionSummary":
|
||||||
|
return true;
|
||||||
|
case "assistant":
|
||||||
|
case "toolResult":
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function isTurnStartEntry(entry) {
|
||||||
|
if (entry.type === "compaction") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return sessionEntryToContextMessages(entry).some(isTurnStartMessage);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Find valid cut points: indices of context-visible user-like or assistant messages.
|
||||||
|
* Never cut at tool results (they must follow their tool call).
|
||||||
|
* When we cut at an assistant message with tool calls, its tool results follow it
|
||||||
|
* and will be kept.
|
||||||
|
*/
|
||||||
|
function findValidCutPoints(entries, startIndex, endIndex) {
|
||||||
|
const cutPoints = [];
|
||||||
|
for (let i = startIndex; i < endIndex; i++) {
|
||||||
|
const entry = entries[i];
|
||||||
|
if (entry.type === "compaction") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sessionEntryToContextMessages(entry).some(isCutPointMessage)) {
|
||||||
|
cutPoints.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cutPoints;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Find the context-visible user-role message that starts the turn containing the given entry index.
|
||||||
|
* Returns -1 if no turn start found before the index.
|
||||||
|
*/
|
||||||
|
export function findTurnStartIndex(entries, entryIndex, startIndex) {
|
||||||
|
for (let i = entryIndex; i >= startIndex; i--) {
|
||||||
|
if (isTurnStartEntry(entries[i])) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
||||||
|
*
|
||||||
|
* Algorithm: Walk backwards from newest, accumulating estimated message sizes.
|
||||||
|
* Stop when we've accumulated >= keepRecentTokens. Cut at that point.
|
||||||
|
*
|
||||||
|
* Can cut at user OR assistant messages (never tool results). When cutting at an
|
||||||
|
* assistant message with tool calls, its tool results come after and will be kept.
|
||||||
|
*
|
||||||
|
* Returns CutPointResult with:
|
||||||
|
* - firstKeptEntryIndex: the entry index to start keeping from
|
||||||
|
* - turnStartIndex: if cutting mid-turn, the user message that started that turn
|
||||||
|
* - isSplitTurn: whether we're cutting in the middle of a turn
|
||||||
|
*
|
||||||
|
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
||||||
|
*/
|
||||||
|
export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
|
||||||
|
const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
|
||||||
|
if (cutPoints.length === 0) {
|
||||||
|
return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
|
||||||
|
}
|
||||||
|
// Walk backwards from newest, accumulating estimated message sizes
|
||||||
|
let accumulatedTokens = 0;
|
||||||
|
let cutIndex = cutPoints[0]; // Default: keep from first message (not header)
|
||||||
|
for (let i = endIndex - 1; i >= startIndex; i--) {
|
||||||
|
const entry = entries[i];
|
||||||
|
const messageTokens = sessionEntryToContextMessages(entry).reduce((sum, message) => sum + estimateTokens(message), 0);
|
||||||
|
if (messageTokens === 0)
|
||||||
|
continue;
|
||||||
|
accumulatedTokens += messageTokens;
|
||||||
|
// Check if we've exceeded the budget
|
||||||
|
if (accumulatedTokens >= keepRecentTokens) {
|
||||||
|
// Find the closest valid cut point at or after this entry
|
||||||
|
for (let c = 0; c < cutPoints.length; c++) {
|
||||||
|
if (cutPoints[c] >= i) {
|
||||||
|
cutIndex = cutPoints[c];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Scan backwards from cutIndex to include adjacent metadata entries that do not affect context.
|
||||||
|
while (cutIndex > startIndex) {
|
||||||
|
const prevEntry = entries[cutIndex - 1];
|
||||||
|
// Stop at compaction boundaries or context-visible entries.
|
||||||
|
if (prevEntry.type === "compaction" || sessionEntryToContextMessages(prevEntry).length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cutIndex--;
|
||||||
|
}
|
||||||
|
// Determine if this is a split turn
|
||||||
|
const cutEntry = entries[cutIndex];
|
||||||
|
const startsTurn = isTurnStartEntry(cutEntry);
|
||||||
|
const turnStartIndex = startsTurn ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
|
||||||
|
return {
|
||||||
|
firstKeptEntryIndex: cutIndex,
|
||||||
|
turnStartIndex,
|
||||||
|
isSplitTurn: !startsTurn && turnStartIndex !== -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// ============================================================================
|
||||||
|
// Summarization
|
||||||
|
// ============================================================================
|
||||||
|
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
|
||||||
|
|
||||||
|
Use this EXACT format:
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
|
||||||
|
|
||||||
|
## Constraints & Preferences
|
||||||
|
- [Any constraints, preferences, or requirements mentioned by user]
|
||||||
|
- [Or "(none)" if none were mentioned]
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
### Done
|
||||||
|
- [x] [Completed tasks/changes]
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
- [ ] [Current work]
|
||||||
|
|
||||||
|
### Blocked
|
||||||
|
- [Issues preventing progress, if any]
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- **[Decision]**: [Brief rationale]
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. [Ordered list of what should happen next]
|
||||||
|
|
||||||
|
## Critical Context
|
||||||
|
- [Any data, examples, or references needed to continue]
|
||||||
|
- [Or "(none)" if not applicable]
|
||||||
|
|
||||||
|
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
||||||
|
const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
|
||||||
|
|
||||||
|
Update the existing structured summary with new information. RULES:
|
||||||
|
- PRESERVE all existing information from the previous summary
|
||||||
|
- ADD new progress, decisions, and context from the new messages
|
||||||
|
- UPDATE the Progress section: move items from "In Progress" to "Done" when completed
|
||||||
|
- UPDATE "Next Steps" based on what was accomplished
|
||||||
|
- PRESERVE exact file paths, function names, and error messages
|
||||||
|
- If something is no longer relevant, you may remove it
|
||||||
|
|
||||||
|
Use this EXACT format:
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
[Preserve existing goals, add new ones if the task expanded]
|
||||||
|
|
||||||
|
## Constraints & Preferences
|
||||||
|
- [Preserve existing, add new ones discovered]
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
### Done
|
||||||
|
- [x] [Include previously done items AND newly completed items]
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
- [ ] [Current work - update based on progress]
|
||||||
|
|
||||||
|
### Blocked
|
||||||
|
- [Current blockers - remove if resolved]
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- **[Decision]**: [Brief rationale] (preserve all previous, add new)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. [Update based on current state]
|
||||||
|
|
||||||
|
## Critical Context
|
||||||
|
- [Preserve important context, add new if needed]
|
||||||
|
|
||||||
|
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
||||||
|
function createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel) {
|
||||||
|
const options = { maxTokens, signal, apiKey, headers, env };
|
||||||
|
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
|
||||||
|
options.reasoning = thinkingLevel;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shared choke point for every compaction/branch-summary summarization call. Wraps the
|
||||||
|
* single LLM call in {@link retryAssistantCall} so transient stream drops (e.g.
|
||||||
|
* `terminated`, socket close) honor the configured retry policy instead of failing
|
||||||
|
* the whole compaction on the first attempt. Deterministic errors and aborts return
|
||||||
|
* immediately (see {@link retryAssistantCall}).
|
||||||
|
*/
|
||||||
|
export async function completeSummarization(model, context, options, streamFn, retry, callbacks) {
|
||||||
|
// Summaries are standalone requests, so isolate routing and avoid cache writes that cannot be reused.
|
||||||
|
const requestOptions = {
|
||||||
|
...options,
|
||||||
|
cacheRetention: "none",
|
||||||
|
sessionId: uuidv7(),
|
||||||
|
};
|
||||||
|
const produce = async () => streamFn
|
||||||
|
? (await streamFn(model, context, requestOptions)).result()
|
||||||
|
: completeSimple(model, context, requestOptions);
|
||||||
|
return retryAssistantCall(produce, retry, requestOptions.signal, callbacks);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Generate a summary of the conversation using the LLM.
|
||||||
|
* If previousSummary is provided, uses the update prompt to merge.
|
||||||
|
*/
|
||||||
|
export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks) {
|
||||||
|
return (await generateSummaryWithUsage(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks)).text;
|
||||||
|
}
|
||||||
|
/** Generate or update a conversation summary and return its provider usage. */
|
||||||
|
export async function generateSummaryWithUsage(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks) {
|
||||||
|
const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
|
||||||
|
// Use update prompt if we have a previous summary, otherwise initial prompt
|
||||||
|
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
||||||
|
if (customInstructions) {
|
||||||
|
basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`;
|
||||||
|
}
|
||||||
|
// Serialize conversation to text so model doesn't try to continue it
|
||||||
|
// Convert to LLM messages first (handles custom types like bashExecution, custom, etc.)
|
||||||
|
const llmMessages = convertToLlm(currentMessages);
|
||||||
|
const conversationText = serializeConversation(llmMessages);
|
||||||
|
// Build the prompt with conversation wrapped in tags
|
||||||
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
||||||
|
if (previousSummary) {
|
||||||
|
promptText += `<previous-summary>\n${previousSummary}\n</previous-summary>\n\n`;
|
||||||
|
}
|
||||||
|
promptText += basePrompt;
|
||||||
|
const summarizationMessages = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: promptText }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel);
|
||||||
|
const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, streamFn, retry, callbacks);
|
||||||
|
if (response.stopReason === "error") {
|
||||||
|
throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
|
||||||
|
}
|
||||||
|
const textContent = contentText(response.content);
|
||||||
|
return { text: textContent, usage: response.usage };
|
||||||
|
}
|
||||||
|
export function prepareCompaction(pathEntries, settings) {
|
||||||
|
if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let prevCompactionIndex = -1;
|
||||||
|
for (let i = pathEntries.length - 1; i >= 0; i--) {
|
||||||
|
if (pathEntries[i].type === "compaction") {
|
||||||
|
prevCompactionIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let previousSummary;
|
||||||
|
let boundaryStart = 0;
|
||||||
|
if (prevCompactionIndex >= 0) {
|
||||||
|
const prevCompaction = pathEntries[prevCompactionIndex];
|
||||||
|
previousSummary = prevCompaction.summary;
|
||||||
|
const firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);
|
||||||
|
boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
|
||||||
|
}
|
||||||
|
const boundaryEnd = pathEntries.length;
|
||||||
|
const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;
|
||||||
|
const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
|
||||||
|
// Get UUID of first kept entry
|
||||||
|
const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
|
||||||
|
if (!firstKeptEntry?.id) {
|
||||||
|
return undefined; // Session needs migration
|
||||||
|
}
|
||||||
|
const firstKeptEntryId = firstKeptEntry.id;
|
||||||
|
const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
|
||||||
|
// Messages to summarize (will be discarded after summary)
|
||||||
|
const messagesToSummarize = [];
|
||||||
|
for (let i = boundaryStart; i < historyEnd; i++) {
|
||||||
|
const msg = getMessageFromEntryForCompaction(pathEntries[i]);
|
||||||
|
if (msg)
|
||||||
|
messagesToSummarize.push(msg);
|
||||||
|
}
|
||||||
|
// Messages for turn prefix summary (if splitting a turn)
|
||||||
|
const turnPrefixMessages = [];
|
||||||
|
if (cutPoint.isSplitTurn) {
|
||||||
|
for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {
|
||||||
|
const msg = getMessageFromEntryForCompaction(pathEntries[i]);
|
||||||
|
if (msg)
|
||||||
|
turnPrefixMessages.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// Extract file operations from messages and previous compaction
|
||||||
|
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
||||||
|
// Also extract file ops from turn prefix if splitting
|
||||||
|
if (cutPoint.isSplitTurn) {
|
||||||
|
for (const msg of turnPrefixMessages) {
|
||||||
|
extractFileOpsFromMessage(msg, fileOps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
firstKeptEntryId,
|
||||||
|
messagesToSummarize,
|
||||||
|
turnPrefixMessages,
|
||||||
|
isSplitTurn: cutPoint.isSplitTurn,
|
||||||
|
tokensBefore,
|
||||||
|
previousSummary,
|
||||||
|
fileOps,
|
||||||
|
settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// ============================================================================
|
||||||
|
// Main compaction function
|
||||||
|
// ============================================================================
|
||||||
|
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
|
||||||
|
|
||||||
|
Summarize the prefix to provide context for the retained suffix:
|
||||||
|
|
||||||
|
## Original Request
|
||||||
|
[What did the user ask for in this turn?]
|
||||||
|
|
||||||
|
## Early Progress
|
||||||
|
- [Key decisions and work done in the prefix]
|
||||||
|
|
||||||
|
## Context for Suffix
|
||||||
|
- [Information needed to understand the retained recent work]
|
||||||
|
|
||||||
|
Be concise. Focus on what's needed to understand the kept suffix.`;
|
||||||
|
/**
|
||||||
|
* Generate summaries for compaction using prepared data.
|
||||||
|
* Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.
|
||||||
|
*
|
||||||
|
* @param preparation - Pre-calculated preparation from prepareCompaction()
|
||||||
|
* @param customInstructions - Optional custom focus for the summary
|
||||||
|
*/
|
||||||
|
export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, env, retry, callbacks) {
|
||||||
|
const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
|
||||||
|
// Generate summaries and merge into one
|
||||||
|
let summary;
|
||||||
|
let summaryUsage;
|
||||||
|
if (isSplitTurn && turnPrefixMessages.length > 0) {
|
||||||
|
let historyText = "No prior history.";
|
||||||
|
let historyUsage;
|
||||||
|
if (messagesToSummarize.length > 0) {
|
||||||
|
const historyResult = await generateSummaryWithUsage(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks);
|
||||||
|
historyText = historyResult.text;
|
||||||
|
historyUsage = historyResult.usage;
|
||||||
|
}
|
||||||
|
const turnPrefixResult = await generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, env, signal, thinkingLevel, streamFn, retry, callbacks);
|
||||||
|
// Merge into single summary
|
||||||
|
summary = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.text}`;
|
||||||
|
summaryUsage = historyUsage ? combineUsage(historyUsage, turnPrefixResult.usage) : turnPrefixResult.usage;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Just generate history summary
|
||||||
|
const result = await generateSummaryWithUsage(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks);
|
||||||
|
summary = result.text;
|
||||||
|
summaryUsage = result.usage;
|
||||||
|
}
|
||||||
|
// Compute file lists and append to summary
|
||||||
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
||||||
|
summary += formatFileOperations(readFiles, modifiedFiles);
|
||||||
|
if (!firstKeptEntryId) {
|
||||||
|
throw new Error("First kept entry has no UUID - session may need migration");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
summary,
|
||||||
|
firstKeptEntryId,
|
||||||
|
tokensBefore,
|
||||||
|
usage: summaryUsage,
|
||||||
|
details: { readFiles, modifiedFiles },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Generate a summary for a turn prefix (when splitting a turn).
|
||||||
|
*/
|
||||||
|
async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, env, signal, thinkingLevel, streamFn, retry, callbacks) {
|
||||||
|
const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); // Smaller budget for turn prefix
|
||||||
|
const llmMessages = convertToLlm(messages);
|
||||||
|
const conversationText = serializeConversation(llmMessages);
|
||||||
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
||||||
|
const summarizationMessages = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: promptText }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel), streamFn, retry, callbacks);
|
||||||
|
if (response.stopReason === "error") {
|
||||||
|
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: contentText(response.content),
|
||||||
|
usage: response.usage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=compaction.js.map
|
||||||
1
dist/core/compaction/compaction.js.map
vendored
Normal file
1
dist/core/compaction/compaction.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
dist/core/compaction/index.d.ts
vendored
Normal file
7
dist/core/compaction/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Compaction and summarization utilities.
|
||||||
|
*/
|
||||||
|
export * from "./branch-summarization.ts";
|
||||||
|
export * from "./compaction.ts";
|
||||||
|
export * from "./utils.ts";
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
1
dist/core/compaction/index.d.ts.map
vendored
Normal file
1
dist/core/compaction/index.d.ts.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC","sourcesContent":["/**\n * Compaction and summarization utilities.\n */\n\nexport * from \"./branch-summarization.ts\";\nexport * from \"./compaction.ts\";\nexport * from \"./utils.ts\";\n"]}
|
||||||
7
dist/core/compaction/index.js
vendored
Normal file
7
dist/core/compaction/index.js
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Compaction and summarization utilities.
|
||||||
|
*/
|
||||||
|
export * from "./branch-summarization.js";
|
||||||
|
export * from "./compaction.js";
|
||||||
|
export * from "./utils.js";
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
dist/core/compaction/index.js.map
vendored
Normal file
1
dist/core/compaction/index.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/compaction/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC","sourcesContent":["/**\n * Compaction and summarization utilities.\n */\n\nexport * from \"./branch-summarization.ts\";\nexport * from \"./compaction.ts\";\nexport * from \"./utils.ts\";\n"]}
|
||||||
38
dist/core/compaction/utils.d.ts
vendored
Normal file
38
dist/core/compaction/utils.d.ts
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Shared utilities for compaction and branch summarization.
|
||||||
|
*/
|
||||||
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||||
|
import { type Message } from "@earendil-works/pi-ai";
|
||||||
|
export interface FileOperations {
|
||||||
|
read: Set<string>;
|
||||||
|
written: Set<string>;
|
||||||
|
edited: Set<string>;
|
||||||
|
}
|
||||||
|
export declare function createFileOps(): FileOperations;
|
||||||
|
/**
|
||||||
|
* Extract file operations from tool calls in an assistant message.
|
||||||
|
*/
|
||||||
|
export declare function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void;
|
||||||
|
/**
|
||||||
|
* Compute final file lists from file operations.
|
||||||
|
* Returns readFiles (files only read, not modified) and modifiedFiles.
|
||||||
|
*/
|
||||||
|
export declare function computeFileLists(fileOps: FileOperations): {
|
||||||
|
readFiles: string[];
|
||||||
|
modifiedFiles: string[];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Format file operations as XML tags for summary.
|
||||||
|
*/
|
||||||
|
export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string;
|
||||||
|
/**
|
||||||
|
* Serialize LLM messages to text for summarization.
|
||||||
|
* This prevents the model from treating it as a conversation to continue.
|
||||||
|
* Call convertToLlm() first to handle custom message types.
|
||||||
|
*
|
||||||
|
* Tool results are truncated to keep the summarization request within
|
||||||
|
* reasonable token budgets. Full content is not needed for summarization.
|
||||||
|
*/
|
||||||
|
export declare function serializeConversation(messages: Message[]): string;
|
||||||
|
export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
|
||||||
|
//# sourceMappingURL=utils.d.ts.map
|
||||||
1
dist/core/compaction/utils.d.ts.map
vendored
Normal file
1
dist/core/compaction/utils.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue