merge: sync upstream pi-mono main into sproutclaw
Some checks failed
CI / build-check-test (push) Has been cancelled
Some checks failed
CI / build-check-test (push) Has been cancelled
Merge upstream/main (195 commits) while keeping sproutclaw-specific README, webui workflow docs, and restored upstream .pi prompt templates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,10 +4,146 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Added Together AI to built-in provider setup, `/login` API-key auth, and default model resolution ([#3624](https://github.com/earendil-works/pi-mono/pull/3624) by [@Nutlope](https://github.com/Nutlope)).
|
||||
- Added a standard unified patch to edit tool result details for SDK consumers ([#4821](https://github.com/earendil-works/pi/issues/4821)).
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the root development install documentation to use `npm install --ignore-scripts` ([#4868](https://github.com/earendil-works/pi/issues/4868)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed git package installs to reconcile existing checkouts to the requested ref and update package settings without losing filters ([#4870](https://github.com/earendil-works/pi/issues/4870)).
|
||||
- Published a 0.74.2 rescue release that tells Node 20 users to upgrade Node before updating to newer Pi versions ([#4876](https://github.com/earendil-works/pi/issues/4876)).
|
||||
- Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)).
|
||||
- Fixed bash tool truncation line counts to ignore the trailing newline as an extra output line ([#4818](https://github.com/earendil-works/pi/issues/4818)).
|
||||
|
||||
## [0.75.4] - 2026-05-20
|
||||
|
||||
### New Features
|
||||
|
||||
- **Hardened npm install and release path** - Pi now ships the CLI with a generated shrinkwrap for transitive dependencies, blocks accidental lockfile changes, verifies dependency pinning and lifecycle-script allowlists in checks, disables lifecycle scripts for self-update and local release installs where supported, and smoke-tests isolated npm and Bun installs before release. See [Supply-chain hardening](../../README.md#supply-chain-hardening).
|
||||
|
||||
### Added
|
||||
|
||||
- Added interactive update notes after `pi update` runs, so users can see the installed version's changelog before continuing ([#4724](https://github.com/earendil-works/pi-mono/pull/4724) by [@mitsuhiko](https://github.com/mitsuhiko)).
|
||||
- Exported image resize utilities from the package root for SDK consumers ([#4775](https://github.com/earendil-works/pi-mono/pull/4775) by [@xl0](https://github.com/xl0)).
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping core sources compatible with Node.js strip-only TypeScript checks.
|
||||
- Removed web UI workspace references from the CLI package and dropped the package-level development watch script.
|
||||
- Published npm installs now include an `npm-shrinkwrap.json` to lock transitive dependencies for the CLI package.
|
||||
- Improved terminal theme detection for light/dark and truecolor handling.
|
||||
- Changed self-update package-manager commands to disable lifecycle scripts during reinstall.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the system prompt to tell models to resolve pi docs and examples under the absolute package paths before reading topic-specific relative references ([#4752](https://github.com/earendil-works/pi/issues/4752)).
|
||||
- Fixed extension `ctx.abort()` during tool-call preflight to stop later confirmations and restore queued interactive input like Escape ([#4276](https://github.com/earendil-works/pi/issues/4276)).
|
||||
- Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue, and added `willRetry` to `agent_end` session events.
|
||||
- Fixed forked session runtime state to keep the active session id aligned with the fork target ([#4799](https://github.com/earendil-works/pi-mono/pull/4799) by [@Perlence](https://github.com/Perlence)).
|
||||
- Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)).
|
||||
- Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)).
|
||||
- Fixed managed npm extension folders to set cloud-sync ignore metadata where supported ([#4763](https://github.com/earendil-works/pi/issues/4763)).
|
||||
- Fixed HTTP idle timeout configuration so long-running provider streams can avoid premature idle disconnects ([#4759](https://github.com/earendil-works/pi-mono/pull/4759) by [@mitsuhiko](https://github.com/mitsuhiko)).
|
||||
- Fixed default system prompt boundaries to use explicit XML tags for clearer file separation ([#4709](https://github.com/earendil-works/pi-mono/pull/4709) by [@herrnel](https://github.com/herrnel)).
|
||||
- Fixed HTML share/export sidebar clicks for shared tool entries to scroll to the rendered tool call ([#4664](https://github.com/earendil-works/pi-mono/pull/4664) by [@yzhg1983](https://github.com/yzhg1983)).
|
||||
- Fixed theme palettes to set explicit text colors and avoid terminal-default color drift.
|
||||
- Fixed truecolor detection to align terminal image rendering and interactive theme decisions.
|
||||
- Fixed loader indicator startup inherited from `@earendil-works/pi-tui` so initialization cannot run before frames are available.
|
||||
- Fixed OpenAI-compatible default output token requests inherited from `@earendil-works/pi-ai` to avoid reserving impossible context windows on servers such as vLLM ([#4675](https://github.com/earendil-works/pi/issues/4675)).
|
||||
- Fixed OpenAI prompt cache keys inherited from `@earendil-works/pi-ai` to stay within the 64-character provider limit ([#4720](https://github.com/earendil-works/pi/issues/4720)).
|
||||
- Fixed Windows npm-family package commands for fnm-managed Node.js installs that expose both extensionless Unix scripts and `.cmd` shims ([#4793](https://github.com/earendil-works/pi/issues/4793)).
|
||||
|
||||
## [0.75.3] - 2026-05-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed undici 8 HTTP/2 destroyed-session races crashing the Node CLI by preserving the previous HTTP/1.1-only fetch dispatcher behavior ([#4681](https://github.com/earendil-works/pi/issues/4681)).
|
||||
|
||||
## [0.75.2] - 2026-05-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Bun-compiled release binaries failing to start when Bun's built-in undici shim lacks npm undici's `install` export ([#4661](https://github.com/earendil-works/pi-mono/pull/4661) by [@dmasiero](https://github.com/dmasiero)).
|
||||
- Fixed Xiaomi MiMo generated model metadata to replay assistant tool-call messages with `reasoning_content` for thinking-mode multi-turn requests, inherited from `@earendil-works/pi-ai` ([#4678](https://github.com/earendil-works/pi/issues/4678)).
|
||||
- Fixed Windows external editor handoff so vim/nvim can receive input after opening from the TUI ([#4612](https://github.com/earendil-works/pi/issues/4612)).
|
||||
- Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)).
|
||||
- Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)).
|
||||
- Fixed Windows pnpm self-updates to resolve pnpm command shims and run through pnpm instead of requiring manual updates ([#4157](https://github.com/earendil-works/pi/issues/4157)).
|
||||
- Fixed Windows npm-family command execution to use cross-spawn instead of parsing `.cmd` shim internals ([#4665](https://github.com/earendil-works/pi/issues/4665)).
|
||||
|
||||
## [0.75.1] - 2026-05-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed config selectors to scale their visible row count to terminal height ([#4243](https://github.com/earendil-works/pi-mono/pull/4243) by [@samjonester](https://github.com/samjonester)).
|
||||
- Fixed Anthropic-compatible API-key requests to ignore unrelated `ANTHROPIC_AUTH_TOKEN` environment values, avoiding invalid bearer credentials for providers such as Xiaomi MiMo inherited from `@earendil-works/pi-ai` ([#4342](https://github.com/earendil-works/pi/issues/4342)).
|
||||
- Fixed Amazon Bedrock message conversion to skip unknown content blocks instead of failing the stream, inherited from `@earendil-works/pi-ai` ([#4223](https://github.com/earendil-works/pi/issues/4223)).
|
||||
- Fixed Azure OpenAI Responses and OpenAI Responses error formatting to prefix HTTP status codes onto `errorMessage`, so transient 5xx and 429 errors are correctly matched by the agent-level auto-retry classifier inherited from `@earendil-works/pi-ai` ([#4232](https://github.com/earendil-works/pi/issues/4232)).
|
||||
- Fixed OpenCode Go Kimi reasoning replay by normalizing streamed `reasoning` fields back to `reasoning_content` for OpenCode Go only, inherited from `@earendil-works/pi-ai` ([#4251](https://github.com/earendil-works/pi/issues/4251)).
|
||||
- Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions inherited from `@earendil-works/pi-ai` ([#4505](https://github.com/earendil-works/pi/issues/4505)).
|
||||
- Fixed JSON parse failures for compressed fetch responses under Node 26.0 by installing undici fetch globals alongside pi's global dispatcher ([#4650](https://github.com/earendil-works/pi/issues/4650), [#4652](https://github.com/earendil-works/pi/issues/4652), [#4653](https://github.com/earendil-works/pi/issues/4653)).
|
||||
- Fixed npm-family package commands on Windows to avoid shell argument splitting when install prefixes contain spaces ([#4623](https://github.com/earendil-works/pi/issues/4623)).
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed non-working OpenAI Codex fast model variants inherited from `@earendil-works/pi-ai`.
|
||||
|
||||
## [0.75.0] - 2026-05-17
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Raised the minimum supported Node.js version to 22.19.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed compaction summary calls to use custom agent stream functions, preserving proxy-backed LLM routing ([#4484](https://github.com/earendil-works/pi/issues/4484)).
|
||||
- Fixed system prompt and context file boundaries to use explicit XML tags instead of Markdown headings, reducing inconsistent boundary ingestion by models ([#4541](https://github.com/earendil-works/pi-mono/pull/4541) by [@herrnel](https://github.com/herrnel)).
|
||||
- Fixed OpenAI Codex generated model metadata to use the current upstream model list inherited from `@earendil-works/pi-ai` ([#4603](https://github.com/earendil-works/pi-mono/pull/4603) by [@mattiacerutti](https://github.com/mattiacerutti)).
|
||||
- Fixed GitHub Copilot GPT model thinking metadata inherited from `@earendil-works/pi-ai` to map unsupported minimal thinking to low ([#4622](https://github.com/earendil-works/pi-mono/pull/4622) by [@mattiacerutti](https://github.com/mattiacerutti)).
|
||||
- Fixed user-scoped npm pi packages to install under `~/.pi/agent/npm/` instead of npm's global package root, avoiding permission errors with system-managed Node installs ([#4587](https://github.com/earendil-works/pi/issues/4587)).
|
||||
- Fixed Mistral requests failing after the global fetch proxy/timeout workaround by removing the custom fetch override and using undici 8 dispatcher support instead ([#4619](https://github.com/earendil-works/pi/issues/4619)).
|
||||
- Fixed default output token requests for models whose advertised output limit is effectively their full context window, avoiding impossible provider requests inherited from `@earendil-works/pi-ai` ([#4614](https://github.com/earendil-works/pi/issues/4614)).
|
||||
|
||||
## [0.74.1] - 2026-05-16
|
||||
|
||||
### New Features
|
||||
|
||||
- **Image generation support** - Added image generation APIs, generated image model metadata, and built-in OpenRouter image generation support inherited from `@earendil-works/pi-ai`.
|
||||
- **Together AI provider** - Added Together AI as a built-in provider with `/login` API-key auth, default model resolution, and setup docs. See [README.md#providers--models](README.md#providers--models) and [docs/providers.md](docs/providers.md).
|
||||
- **Windows ARM64 standalone binaries** - Added standalone release artifacts for Windows ARM64.
|
||||
- **Improved terminal and markdown rendering** - Added markdown list indentation, task-list checkbox rendering, large markdown robustness, and inline image placement fixes inherited from `@earendil-works/pi-tui`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added image generation support from `@earendil-works/pi-ai`, including image generation APIs, image model metadata, and built-in OpenRouter image generation support ([#3887](https://github.com/earendil-works/pi-mono/pull/3887) by [@cristinaponcela](https://github.com/cristinaponcela)).
|
||||
- Added Together AI to built-in provider setup, `/login` API-key auth, and default model resolution ([#3624](https://github.com/earendil-works/pi-mono/pull/3624) by [@Nutlope](https://github.com/Nutlope)).
|
||||
- Added Windows ARM64 standalone binary release artifacts ([#4458](https://github.com/earendil-works/pi/pull/4458) by [@brianmichel](https://github.com/brianmichel)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Node 26 OpenAI-compatible streams timing out after five idle minutes by routing global fetch through pi's undici dispatcher ([#4519](https://github.com/earendil-works/pi/issues/4519)).
|
||||
- Fixed pnpm global package installs by resolving the global package root from pnpm's layout.
|
||||
- Fixed macOS clipboard access errors under sandboxed pasteboard denial so they do not abort the process ([#4492](https://github.com/earendil-works/pi/issues/4492)).
|
||||
- Fixed the scoped model startup hint to show the configured model-cycle keybinding ([#4508](https://github.com/earendil-works/pi/issues/4508)).
|
||||
- Fixed resource path display to disambiguate package/resource names that collide across package locations.
|
||||
- Fixed `fd` auto-download on macOS x86_64 by pinning the last release that ships an Intel macOS binary ([#4559](https://github.com/earendil-works/pi/issues/4559)).
|
||||
- Fixed skill diagnostics to stop warning when a skill name differs from its parent directory ([#4534](https://github.com/earendil-works/pi/issues/4534)).
|
||||
- Fixed prompt template argument parsing to split unquoted multiline input on newlines ([#4553](https://github.com/earendil-works/pi/issues/4553)).
|
||||
- Fixed `--resume` session listing to cap in-flight session metadata loads and avoid OOM on large session histories ([#4583](https://github.com/earendil-works/pi/issues/4583)).
|
||||
- Fixed interactive error messages to render with trailing spacing so reload errors do not run into resource listings ([#4510](https://github.com/earendil-works/pi/issues/4510)).
|
||||
- Fixed `.agents` package provenance metadata to survive package-manager scans.
|
||||
- Fixed nested code fences in the Termux setup documentation so the example AGENTS.md renders correctly ([#4503](https://github.com/earendil-works/pi/issues/4503)).
|
||||
- Fixed tool output expansion while extension confirmation dialogs are focused ([#4429](https://github.com/earendil-works/pi/issues/4429)).
|
||||
- Fixed auto-retry for Anthropic streams that end before `message_stop` ([#4433](https://github.com/earendil-works/pi/issues/4433)).
|
||||
- Fixed compaction summary calls to clamp requested output tokens to model limits.
|
||||
- Fixed uncaught interactive-mode exceptions to restore the terminal before exiting ([#4426](https://github.com/earendil-works/pi-mono/pull/4426) by [@ofa1](https://github.com/ofa1)).
|
||||
- Fixed ANSI stripping to match `strip-ansi` behavior after dependency removal.
|
||||
- Fixed UUIDv7 sequence generation shared by session IDs after dependency removal.
|
||||
- Fixed OpenRouter cached-token usage accounting, Fireworks caching compatibility, and OpenAI Codex WebSocket proxy handling inherited from `@earendil-works/pi-ai`.
|
||||
- Fixed markdown list wrapping, task-list checkboxes, large markdown rendering, WezTerm Kitty keyboard escape handling, and short-viewport inline image placement inherited from `@earendil-works/pi-tui`.
|
||||
- Fixed theme sharing across package scopes so extensions do not crash with `Theme not initialized` ([#4333](https://github.com/earendil-works/pi/issues/4333)).
|
||||
- Fixed keybinding hints to show Option instead of Alt on macOS ([#4289](https://github.com/earendil-works/pi/issues/4289)).
|
||||
- Fixed the interactive update notification to render the changelog as an OSC 8 hyperlink when the terminal supports hyperlinks ([#4280](https://github.com/earendil-works/pi/issues/4280)).
|
||||
|
||||
|
||||
@@ -68,13 +68,15 @@ I regularly publish my own `pi-mono` work sessions here:
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
curl -fsSL https://pi.dev/install.sh | sh
|
||||
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
||||
```
|
||||
|
||||
Or with npm:
|
||||
`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs.
|
||||
|
||||
Installer alternative:
|
||||
|
||||
```bash
|
||||
npm install -g @earendil-works/pi-coding-agent
|
||||
curl -fsSL https://pi.dev/install.sh | sh
|
||||
```
|
||||
|
||||
Authenticate with an API key:
|
||||
@@ -406,7 +408,7 @@ 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 global npm. Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). 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"]`.
|
||||
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`, 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`:
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ See these complete provider examples:
|
||||
- [Unregister Provider](#unregister-provider)
|
||||
- [OAuth Support](#oauth-support)
|
||||
- [Custom Streaming API](#custom-streaming-api)
|
||||
- [Context Overflow Errors](#context-overflow-errors)
|
||||
- [Testing Your Implementation](#testing-your-implementation)
|
||||
- [Config Reference](#config-reference)
|
||||
- [Model Definition Reference](#model-definition-reference)
|
||||
@@ -506,6 +507,60 @@ output.usage.totalTokens = output.usage.input + output.usage.output +
|
||||
calculateCost(model, output.usage);
|
||||
```
|
||||
|
||||
### Context Overflow Errors
|
||||
|
||||
When a request exceeds the model's context window, pi can recover automatically by compacting the conversation and retrying. This recovery only kicks in if pi recognizes the failure as an overflow.
|
||||
|
||||
Detection runs on the finalized assistant message:
|
||||
|
||||
- `stopReason === "error"`
|
||||
- `errorMessage` matches one of pi's known overflow patterns (see [`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts))
|
||||
|
||||
If your provider returns overflow errors with a message pi does not recognize, normalize the error from the same extension that registers the provider. Use a `message_end` handler to rewrite the assistant message so its `errorMessage` starts with a phrase pi recognizes. The generic fallback `context_length_exceeded` is the safest choice.
|
||||
|
||||
```typescript
|
||||
const MY_PROVIDER_OVERFLOW_PATTERN = /your provider's overflow phrase/i;
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerProvider("my-provider", { /* ... */ });
|
||||
|
||||
pi.on("message_end", (event, ctx) => {
|
||||
const message = event.message;
|
||||
if (message.role !== "assistant") return;
|
||||
if (message.stopReason !== "error") return;
|
||||
if (
|
||||
message.provider !== "my-provider" &&
|
||||
ctx.model?.provider !== "my-provider"
|
||||
)
|
||||
return;
|
||||
|
||||
const errorMessage = message.errorMessage ?? "";
|
||||
if (errorMessage.includes("context_length_exceeded")) return;
|
||||
if (!MY_PROVIDER_OVERFLOW_PATTERN.test(errorMessage)) return;
|
||||
|
||||
return {
|
||||
message: {
|
||||
...message,
|
||||
errorMessage: `context_length_exceeded: ${errorMessage}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`message_end` runs before pi tracks the assistant message for auto-compaction, so the rewritten `errorMessage` is what pi checks. With this in place, pi will:
|
||||
|
||||
1. Detect the overflow from `errorMessage`.
|
||||
2. Drop the failed assistant message from live context.
|
||||
3. Run compaction.
|
||||
4. Retry the request once.
|
||||
|
||||
Guard the rewrite carefully:
|
||||
|
||||
- Scope it to your provider (`message.provider` and `ctx.model?.provider`) so unrelated errors from other providers are untouched.
|
||||
- Match a provider-specific pattern, not pi's generic overflow patterns. Rewriting rate-limit or throttling errors (`rate limit`, `too many requests`) would falsely trigger compaction instead of pi's normal retry-with-backoff path.
|
||||
- Skip when `errorMessage` already includes `context_length_exceeded` so the handler is idempotent.
|
||||
|
||||
### Registration
|
||||
|
||||
Register your stream function:
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function (pi: ExtensionAPI) {
|
||||
pi.on("event_name", async (event, ctx) => {
|
||||
// ctx.ui for user interaction
|
||||
const ok = await ctx.ui.confirm("Title", "Are you sure?");
|
||||
ctx.ui.notify("Done!", "success");
|
||||
ctx.ui.notify("Done!", "info");
|
||||
ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
|
||||
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
|
||||
});
|
||||
|
||||
@@ -4,18 +4,28 @@ Pi is a minimal terminal coding harness. It is designed to stay small at the cor
|
||||
|
||||
## Quick start
|
||||
|
||||
On linux or mac you can install Pi with curl:
|
||||
Install Pi with npm:
|
||||
|
||||
```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.
|
||||
|
||||
On Linux or macOS, you can also use the installer:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://pi.dev/install.sh | sh
|
||||
```
|
||||
|
||||
Or alternatively with npm:
|
||||
To uninstall pi itself, use npm for curl and npm installs:
|
||||
|
||||
```bash
|
||||
npm install -g @earendil-works/pi-coding-agent
|
||||
npm uninstall -g @earendil-works/pi-coding-agent
|
||||
```
|
||||
|
||||
For pnpm, Yarn, or Bun installs, use the matching global remove command: `pnpm remove -g @earendil-works/pi-coding-agent`, `yarn global remove @earendil-works/pi-coding-agent`, or `bun uninstall -g @earendil-works/pi-coding-agent`.
|
||||
|
||||
Then run it in a project directory:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -36,7 +36,9 @@ pi update npm:@foo/bar # update one package
|
||||
pi update --extension npm:@foo/bar
|
||||
```
|
||||
|
||||
By default, `install` and `remove` write to global settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup.
|
||||
These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall).
|
||||
|
||||
By default, `install` and `remove` write to user settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup.
|
||||
|
||||
To try a package without installing it, use `--extension` or `-e`. This installs to a temporary directory for the current run only:
|
||||
|
||||
@@ -57,7 +59,7 @@ npm:pkg
|
||||
```
|
||||
|
||||
- Versioned specs are pinned and skipped by package updates (`pi update`, `pi update --extensions`).
|
||||
- Global installs use `npm install -g`.
|
||||
- User installs go under `~/.pi/agent/npm/`.
|
||||
- Project installs go under `.pi/npm/`.
|
||||
- Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`.
|
||||
|
||||
@@ -83,9 +85,9 @@ ssh://git@github.com/user/repo@v1
|
||||
- HTTPS and SSH URLs are both supported.
|
||||
- SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`).
|
||||
- For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast.
|
||||
- Refs pin the package and skip package updates (`pi update`, `pi update --extensions`).
|
||||
- Refs are pinned tags or commits and skip package updates (`pi update`, `pi update --extensions`). Use `pi install git:host/user/repo@new-ref` to move an existing package to a new pinned ref.
|
||||
- Cloned to `~/.pi/agent/git/<host>/<path>` (global) or `.pi/git/<host>/<path>` (project).
|
||||
- Runs `npm install` after clone or pull if `package.json` exists.
|
||||
- Runs `npm install` after clone, pull, or pinned ref change if `package.json` exists.
|
||||
|
||||
**SSH examples:**
|
||||
```bash
|
||||
|
||||
@@ -7,9 +7,31 @@ This page gets you from install to a useful first pi session.
|
||||
Pi is distributed as an npm package:
|
||||
|
||||
```bash
|
||||
npm install -g @earendil-works/pi-coding-agent
|
||||
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.
|
||||
|
||||
### Uninstall
|
||||
|
||||
Use the package manager that installed pi. The curl installer uses npm globally, so curl and npm installs are removed with npm:
|
||||
|
||||
```bash
|
||||
# curl installer or npm install -g
|
||||
npm uninstall -g @earendil-works/pi-coding-agent
|
||||
|
||||
# pnpm
|
||||
pnpm remove -g @earendil-works/pi-coding-agent
|
||||
|
||||
# Yarn
|
||||
yarn global remove @earendil-works/pi-coding-agent
|
||||
|
||||
# Bun
|
||||
bun uninstall -g @earendil-works/pi-coding-agent
|
||||
```
|
||||
|
||||
Uninstalling pi leaves settings, credentials, sessions, and installed pi packages in `~/.pi/agent/`.
|
||||
|
||||
Then start pi in the project directory you want it to work on:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -54,7 +54,7 @@ The main factory function for a single `AgentSession`.
|
||||
`createAgentSession()` uses a `ResourceLoader` to supply extensions, skills, prompt templates, themes, and context files. If you do not provide one, it uses `DefaultResourceLoader` with standard discovery.
|
||||
|
||||
```typescript
|
||||
import { createAgentSession } from "@earendil-works/pi-coding-agent";
|
||||
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
// Minimal: defaults with DefaultResourceLoader
|
||||
const { session } = await createAgentSession();
|
||||
@@ -62,7 +62,7 @@ const { session } = await createAgentSession();
|
||||
// Custom: override specific options
|
||||
const { session } = await createAgentSession({
|
||||
model: myModel,
|
||||
tools: [readTool, bashTool],
|
||||
tools: ["read", "bash"],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
```
|
||||
@@ -466,64 +466,52 @@ const { session } = await createAgentSession({ resourceLoader: loader });
|
||||
|
||||
### Tools
|
||||
|
||||
```typescript
|
||||
import {
|
||||
codingTools, // read, bash, edit, write (default)
|
||||
readOnlyTools, // read, grep, find, ls
|
||||
readTool, bashTool, editTool, writeTool,
|
||||
grepTool, findTool, lsTool,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
Specify which built-in tools to enable:
|
||||
|
||||
// Use built-in tool set
|
||||
- Built-in tool names: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`
|
||||
- Default built-ins: `read`, `bash`, `edit`, `write`
|
||||
- `noTools: "all"` disables all tools
|
||||
- `noTools: "builtin"` disables default built-ins while keeping extension and custom tools enabled
|
||||
|
||||
The `edit` tool returns `details.diff` for Pi's TUI display and `details.patch` as a standard unified patch for SDK consumers.
|
||||
|
||||
```typescript
|
||||
import { createAgentSession } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
// Read-only mode
|
||||
const { session } = await createAgentSession({
|
||||
tools: readOnlyTools,
|
||||
tools: ["read", "grep", "find", "ls"],
|
||||
});
|
||||
|
||||
// Pick specific tools
|
||||
const { session } = await createAgentSession({
|
||||
tools: [readTool, bashTool, grepTool],
|
||||
tools: ["read", "bash", "grep"],
|
||||
});
|
||||
```
|
||||
|
||||
#### Tools with Custom cwd
|
||||
|
||||
**Important:** The pre-built tool instances (`readTool`, `bashTool`, etc.) use `process.cwd()` for path resolution. When you specify a custom `cwd` AND provide explicit `tools`, you must use the tool factory functions to ensure paths resolve correctly:
|
||||
When you pass a custom `cwd`, `createAgentSession()` builds selected built-in tools for that cwd.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createCodingTools, // Creates [read, bash, edit, write] for specific cwd
|
||||
createReadOnlyTools, // Creates [read, grep, find, ls] for specific cwd
|
||||
createReadTool,
|
||||
createBashTool,
|
||||
createEditTool,
|
||||
createWriteTool,
|
||||
createGrepTool,
|
||||
createFindTool,
|
||||
createLsTool,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const cwd = "/path/to/project";
|
||||
|
||||
// Use factory for tool sets
|
||||
// Use default tools for custom cwd
|
||||
const { session } = await createAgentSession({
|
||||
cwd,
|
||||
tools: createCodingTools(cwd), // Tools resolve paths relative to cwd
|
||||
sessionManager: SessionManager.inMemory(cwd),
|
||||
});
|
||||
|
||||
// Or pick specific tools
|
||||
// Or pick specific tools for custom cwd
|
||||
const { session } = await createAgentSession({
|
||||
cwd,
|
||||
tools: [createReadTool(cwd), createBashTool(cwd), createGrepTool(cwd)],
|
||||
tools: ["read", "bash", "grep"],
|
||||
sessionManager: SessionManager.inMemory(cwd),
|
||||
});
|
||||
```
|
||||
|
||||
**When you don't need factories:**
|
||||
- If you omit `tools`, pi automatically creates them with the correct `cwd`
|
||||
- If you use `process.cwd()` as your `cwd`, the pre-built instances work fine
|
||||
|
||||
**When you must use factories:**
|
||||
- When you specify both `cwd` (different from `process.cwd()`) AND `tools`
|
||||
|
||||
> See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)
|
||||
|
||||
### Custom Tools
|
||||
@@ -556,6 +544,8 @@ Use `defineTool()` for standalone definitions and arrays like `customTools: [myT
|
||||
|
||||
Custom tools passed via `customTools` are combined with extension-registered tools. Extensions loaded by the ResourceLoader can also register tools via `pi.registerTool()`.
|
||||
|
||||
If you pass `tools`, include each custom or extension tool name you want enabled, for example `tools: ["read", "bash", "my_tool"]`.
|
||||
|
||||
> See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)
|
||||
|
||||
### Extensions
|
||||
@@ -885,12 +875,10 @@ import { getModel } from "@earendil-works/pi-ai";
|
||||
import { Type } from "typebox";
|
||||
import {
|
||||
AuthStorage,
|
||||
bashTool,
|
||||
createAgentSession,
|
||||
DefaultResourceLoader,
|
||||
defineTool,
|
||||
ModelRegistry,
|
||||
readTool,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
@@ -944,7 +932,7 @@ const { session } = await createAgentSession({
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
|
||||
tools: [readTool, bashTool],
|
||||
tools: ["read", "bash", "status"],
|
||||
customTools: [statusTool],
|
||||
resourceLoader: loader,
|
||||
|
||||
@@ -1123,13 +1111,7 @@ defineTool
|
||||
SessionManager
|
||||
SettingsManager
|
||||
|
||||
// Built-in tools (use process.cwd())
|
||||
codingTools
|
||||
readOnlyTools
|
||||
readTool, bashTool, editTool, writeTool
|
||||
grepTool, findTool, lsTool
|
||||
|
||||
// Tool factories (for custom cwd)
|
||||
// Tool factories
|
||||
createCodingTools
|
||||
createReadOnlyTools
|
||||
createReadTool, createBashTool, createEditTool, createWriteTool
|
||||
|
||||
@@ -153,9 +153,7 @@ When a provider requests a retry delay longer than `retry.provider.maxRetryDelay
|
||||
}
|
||||
```
|
||||
|
||||
`npmCommand` is used for all npm package-manager operations, including installs, uninstalls, and dependency installs inside git packages. Use argv-style entries exactly as the process should be launched. When `npmCommand` is configured, git package dependency installs use plain `install` to avoid npm-specific flags in wrappers or alternate package managers.
|
||||
|
||||
Normally the package manager's global modules location is queried using `root -g`. As a special case, if the first element of `npmCommand` is `"bun"`, the modules location will instead be queried with `pm bin -g`.
|
||||
`npmCommand` is used for all npm package-manager operations, including installs, uninstalls, and dependency installs inside git packages. User-scoped npm packages install under `~/.pi/agent/npm/`; project-scoped npm packages install under `.pi/npm/`. Use argv-style entries exactly as the process should be launched. When `npmCommand` is configured, git package dependency installs use plain `install` to avoid npm-specific flags in wrappers or alternate package managers.
|
||||
|
||||
### Sessions
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Skills are self-contained capability packages that the agent loads on-demand. A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks.
|
||||
|
||||
Pi implements the [Agent Skills standard](https://agentskills.io/specification), warning about violations but remaining lenient.
|
||||
Pi implements the [Agent Skills standard](https://agentskills.io/specification), warning about most violations but remaining lenient. Pi allows skill names to differ from their parent directory even though the standard disallows it; that rule is suboptimal for shared skill directories used across multiple agent harnesses.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -140,7 +140,7 @@ Per the [Agent Skills specification](https://agentskills.io/specification#frontm
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | Max 64 chars. Lowercase a-z, 0-9, hyphens. Must match parent directory. |
|
||||
| `name` | Yes | Max 64 chars. Lowercase a-z, 0-9, hyphens. Unlike the standard, Pi does not require this to match the parent directory because that standard requirement is suboptimal for shared skill directories. |
|
||||
| `description` | Yes | Max 1024 chars. What the skill does and when to use it. |
|
||||
| `license` | No | License name or reference to bundled file. |
|
||||
| `compatibility` | No | Max 500 chars. Environment requirements. |
|
||||
@@ -154,7 +154,7 @@ Per the [Agent Skills specification](https://agentskills.io/specification#frontm
|
||||
- Lowercase letters, numbers, hyphens only
|
||||
- No leading/trailing hyphens
|
||||
- No consecutive hyphens
|
||||
- Must match parent directory name
|
||||
Pi does not require the name to match the parent directory. The Agent Skills standard does, but that requirement is suboptimal for shared skill directories used by multiple tools.
|
||||
|
||||
Valid: `pdf-processing`, `data-analysis`, `code-review`
|
||||
Invalid: `PDF-Processing`, `-pdf`, `pdf--processing`
|
||||
@@ -177,7 +177,6 @@ description: Helps with PDFs.
|
||||
|
||||
Pi validates skills against the Agent Skills standard. Most issues produce warnings but still load the skill:
|
||||
|
||||
- Name doesn't match parent directory
|
||||
- Name exceeds 64 characters or contains invalid characters
|
||||
- Name starts/ends with hyphen or has consecutive hyphens
|
||||
- Description exceeds 1024 characters
|
||||
|
||||
@@ -17,7 +17,7 @@ pkg update && pkg upgrade
|
||||
pkg install nodejs termux-api git
|
||||
|
||||
# Install pi
|
||||
npm install -g @earendil-works/pi-coding-agent
|
||||
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
||||
|
||||
# Create config directory
|
||||
mkdir -p ~/.pi/agent
|
||||
@@ -36,7 +36,7 @@ Image clipboard is not supported on Termux (the `ctrl+v` image paste feature wil
|
||||
|
||||
Create `~/.pi/agent/AGENTS.md` to help the agent understand the Termux environment:
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
# Agent Environment: Termux on Android
|
||||
|
||||
## Location
|
||||
@@ -91,7 +91,7 @@ termux-camera-photo out.jpg # Take photo
|
||||
- Termux:API app must be installed for `termux-*` commands
|
||||
- Use `pkg install termux-api` for the command-line tools
|
||||
- Storage permission needed for `/storage/emulated/0` access
|
||||
```
|
||||
````
|
||||
|
||||
## Limitations
|
||||
|
||||
|
||||
@@ -137,6 +137,8 @@ pi list # List installed packages
|
||||
pi config # Enable/disable package resources
|
||||
```
|
||||
|
||||
These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall).
|
||||
|
||||
See [Pi Packages](packages.md) for package sources and security notes.
|
||||
|
||||
### Modes
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.74.0",
|
||||
"version": "0.75.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.74.0",
|
||||
"version": "0.75.4",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider-anthropic",
|
||||
"private": true,
|
||||
"version": "0.74.0",
|
||||
"version": "0.75.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
@@ -14,6 +14,6 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0"
|
||||
"@anthropic-ai/sdk": "0.52.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||
"private": true,
|
||||
"version": "0.74.0",
|
||||
"version": "0.75.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -12,7 +12,7 @@ import { type Api, type Context, type Model, registerApiProvider, streamSimple }
|
||||
import { readFileSync } from "fs";
|
||||
import { getAgentDir } from "packages/coding-agent/src/config.js";
|
||||
import { join } from "path";
|
||||
import { MODELS, streamGitLabDuo } from "./index.js";
|
||||
import { MODELS, streamGitLabDuo } from "./index.ts";
|
||||
|
||||
const MODEL_MAP = new Map(MODELS.map((m) => [m.id, m]));
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import type { Component } from "@earendil-works/pi-tui";
|
||||
import { isKeyRelease, type TUI } from "@earendil-works/pi-tui";
|
||||
import type { DoomEngine } from "./doom-engine.js";
|
||||
import { DoomKeys, mapKeyToDoom } from "./doom-keys.js";
|
||||
import type { DoomEngine } from "./doom-engine.ts";
|
||||
import { DoomKeys, mapKeyToDoom } from "./doom-keys.ts";
|
||||
|
||||
function renderHalfBlock(
|
||||
rgba: Uint8Array,
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { DoomOverlayComponent } from "./doom-component.js";
|
||||
import { DoomEngine } from "./doom-engine.js";
|
||||
import { ensureWadFile } from "./wad-finder.js";
|
||||
import { DoomOverlayComponent } from "./doom-component.ts";
|
||||
import { DoomEngine } from "./doom-engine.ts";
|
||||
import { ensureWadFile } from "./wad-finder.ts";
|
||||
|
||||
// Persistent engine instance - survives between invocations
|
||||
let activeEngine: DoomEngine | null = null;
|
||||
|
||||
@@ -303,7 +303,11 @@ function sleep(ms: number): Promise<void> {
|
||||
|
||||
// Base overlay component with common rendering
|
||||
abstract class BaseOverlay {
|
||||
constructor(protected theme: Theme) {}
|
||||
protected theme: Theme;
|
||||
|
||||
constructor(theme: Theme) {
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
protected box(lines: string[], width: number, title?: string): string[] {
|
||||
const th = this.theme;
|
||||
@@ -330,12 +334,13 @@ abstract class BaseOverlay {
|
||||
|
||||
// Anchor position test
|
||||
class AnchorTestComponent extends BaseOverlay {
|
||||
constructor(
|
||||
theme: Theme,
|
||||
private anchor: OverlayAnchor,
|
||||
private done: (result: "next" | "confirm" | "cancel") => void,
|
||||
) {
|
||||
private anchor: OverlayAnchor;
|
||||
private done: (result: "next" | "confirm" | "cancel") => void;
|
||||
|
||||
constructor(theme: Theme, anchor: OverlayAnchor, done: (result: "next" | "confirm" | "cancel") => void) {
|
||||
super(theme);
|
||||
this.anchor = anchor;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -368,12 +373,17 @@ class AnchorTestComponent extends BaseOverlay {
|
||||
|
||||
// Margin/offset test
|
||||
class MarginTestComponent extends BaseOverlay {
|
||||
private config: { name: string; options: OverlayOptions };
|
||||
private done: (result: "next" | "close") => void;
|
||||
|
||||
constructor(
|
||||
theme: Theme,
|
||||
private config: { name: string; options: OverlayOptions },
|
||||
private done: (result: "next" | "close") => void,
|
||||
config: { name: string; options: OverlayOptions },
|
||||
done: (result: "next" | "close") => void,
|
||||
) {
|
||||
super(theme);
|
||||
this.config = config;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -403,13 +413,15 @@ class MarginTestComponent extends BaseOverlay {
|
||||
|
||||
// Stacked overlay test
|
||||
class StackOverlayComponent extends BaseOverlay {
|
||||
constructor(
|
||||
theme: Theme,
|
||||
private num: number,
|
||||
private position: string,
|
||||
private done: (result: string) => void,
|
||||
) {
|
||||
private num: number;
|
||||
private position: string;
|
||||
private done: (result: string) => void;
|
||||
|
||||
constructor(theme: Theme, num: number, position: string, done: (result: string) => void) {
|
||||
super(theme);
|
||||
this.num = num;
|
||||
this.position = position;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -446,19 +458,19 @@ class StackOverlayComponent extends BaseOverlay {
|
||||
|
||||
// Streaming overflow test - spawns real process with colored output (original crash scenario)
|
||||
class StreamingOverflowComponent extends BaseOverlay {
|
||||
private tui: TUI;
|
||||
private lines: string[] = [];
|
||||
private proc: ReturnType<typeof spawn> | null = null;
|
||||
private scrollOffset = 0;
|
||||
private maxVisibleLines = 15;
|
||||
private finished = false;
|
||||
private disposed = false;
|
||||
private done: () => void;
|
||||
|
||||
constructor(
|
||||
private tui: TUI,
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
constructor(tui: TUI, theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.tui = tui;
|
||||
this.done = done;
|
||||
this.startProcess();
|
||||
}
|
||||
|
||||
@@ -579,11 +591,11 @@ class StreamingOverflowComponent extends BaseOverlay {
|
||||
|
||||
// Edge position test
|
||||
class EdgeTestComponent extends BaseOverlay {
|
||||
constructor(
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
private done: () => void;
|
||||
|
||||
constructor(theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -614,12 +626,17 @@ class EdgeTestComponent extends BaseOverlay {
|
||||
|
||||
// Percentage positioning test
|
||||
class PercentTestComponent extends BaseOverlay {
|
||||
private config: { name: string; row: number; col: number };
|
||||
private done: (result: "next" | "close") => void;
|
||||
|
||||
constructor(
|
||||
theme: Theme,
|
||||
private config: { name: string; row: number; col: number },
|
||||
private done: (result: "next" | "close") => void,
|
||||
config: { name: string; row: number; col: number },
|
||||
done: (result: "next" | "close") => void,
|
||||
) {
|
||||
super(theme);
|
||||
this.config = config;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -649,11 +666,11 @@ class PercentTestComponent extends BaseOverlay {
|
||||
|
||||
// MaxHeight test - renders 20 lines, truncated to 10 by maxHeight
|
||||
class MaxHeightTestComponent extends BaseOverlay {
|
||||
constructor(
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
private done: () => void;
|
||||
|
||||
constructor(theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -684,15 +701,15 @@ class MaxHeightTestComponent extends BaseOverlay {
|
||||
|
||||
// Responsive sidepanel - demonstrates percentage width and visibility callback
|
||||
class SidepanelComponent extends BaseOverlay {
|
||||
private tui: TUI;
|
||||
private items = ["Dashboard", "Messages", "Settings", "Help", "About"];
|
||||
private selectedIndex = 0;
|
||||
private done: () => void;
|
||||
|
||||
constructor(
|
||||
private tui: TUI,
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
constructor(tui: TUI, theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.tui = tui;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -745,18 +762,18 @@ class SidepanelComponent extends BaseOverlay {
|
||||
|
||||
// Animation demo - proves overlays can handle real-time updates like pi-doom
|
||||
class AnimationDemoComponent extends BaseOverlay {
|
||||
private tui: TUI;
|
||||
private frame = 0;
|
||||
private interval: ReturnType<typeof setInterval> | null = null;
|
||||
private fps = 0;
|
||||
private lastFpsUpdate = Date.now();
|
||||
private framesSinceLastFps = 0;
|
||||
private done: () => void;
|
||||
|
||||
constructor(
|
||||
private tui: TUI,
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
constructor(tui: TUI, theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.tui = tui;
|
||||
this.done = done;
|
||||
this.startAnimation();
|
||||
}
|
||||
|
||||
@@ -860,15 +877,15 @@ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
||||
|
||||
// Toggle demo - demonstrates OverlayHandle.setHidden() via onHandle callback
|
||||
class ToggleDemoComponent extends BaseOverlay {
|
||||
private tui: TUI;
|
||||
private toggleCount = 0;
|
||||
private isToggling = false;
|
||||
private done: () => void;
|
||||
|
||||
constructor(
|
||||
private tui: TUI,
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
constructor(tui: TUI, theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.tui = tui;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -923,19 +940,19 @@ class ToggleDemoComponent extends BaseOverlay {
|
||||
|
||||
class PassiveDemoController extends BaseOverlay {
|
||||
focused = false;
|
||||
private tui: TUI;
|
||||
private typed = "";
|
||||
private timerComponent: TimerPanel;
|
||||
private timerHandle: OverlayHandle | null = null;
|
||||
private interval: ReturnType<typeof setInterval> | null = null;
|
||||
private inputCount = 0;
|
||||
private lastInputDebug = "";
|
||||
private done: () => void;
|
||||
|
||||
constructor(
|
||||
private tui: TUI,
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
constructor(tui: TUI, theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.tui = tui;
|
||||
this.done = done;
|
||||
this.timerComponent = new TimerPanel(theme);
|
||||
this.timerHandle = this.tui.showOverlay(this.timerComponent, {
|
||||
nonCapturing: true,
|
||||
@@ -1015,16 +1032,16 @@ class TimerPanel extends BaseOverlay {
|
||||
// === Focus cycling demo ===
|
||||
|
||||
class FocusDemoController extends BaseOverlay {
|
||||
private tui: TUI;
|
||||
private panels: FocusPanel[] = [];
|
||||
private handles: OverlayHandle[] = [];
|
||||
private focusIndex = -1;
|
||||
private done: () => void;
|
||||
|
||||
constructor(
|
||||
private tui: TUI,
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
constructor(tui: TUI, theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.tui = tui;
|
||||
this.done = done;
|
||||
const colors = ["error", "success", "accent"] as const;
|
||||
const labels = ["Alpha", "Beta", "Gamma"];
|
||||
|
||||
@@ -1107,16 +1124,22 @@ class FocusDemoController extends BaseOverlay {
|
||||
class FocusPanel extends BaseOverlay {
|
||||
handle: OverlayHandle | null = null;
|
||||
readonly label: string;
|
||||
private color: "error" | "success" | "accent";
|
||||
private onTab: () => void;
|
||||
private onClose: () => void;
|
||||
|
||||
constructor(
|
||||
theme: Theme,
|
||||
label: string,
|
||||
private color: "error" | "success" | "accent",
|
||||
private onTab: () => void,
|
||||
private onClose: () => void,
|
||||
color: "error" | "success" | "accent",
|
||||
onTab: () => void,
|
||||
onClose: () => void,
|
||||
) {
|
||||
super(theme);
|
||||
this.label = label;
|
||||
this.color = color;
|
||||
this.onTab = onTab;
|
||||
this.onClose = onClose;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
@@ -1155,19 +1178,19 @@ class FocusPanel extends BaseOverlay {
|
||||
// === Streaming input panel test (/overlay-streaming) ===
|
||||
|
||||
class StreamingInputController extends BaseOverlay {
|
||||
private tui: TUI;
|
||||
private panels: StreamingInputPanel[] = [];
|
||||
private handles: OverlayHandle[] = [];
|
||||
private focusIndex = -1; // -1 = controller focused, 0-2 = panel focused
|
||||
private streamLines: string[] = [];
|
||||
private streamInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private lineCount = 0;
|
||||
private done: () => void;
|
||||
|
||||
constructor(
|
||||
private tui: TUI,
|
||||
theme: Theme,
|
||||
private done: () => void,
|
||||
) {
|
||||
constructor(tui: TUI, theme: Theme, done: () => void) {
|
||||
super(theme);
|
||||
this.tui = tui;
|
||||
this.done = done;
|
||||
|
||||
// Create 3 input panels as non-capturing overlays
|
||||
const colors = ["error", "success", "accent"] as const;
|
||||
@@ -1287,17 +1310,25 @@ class StreamingInputController extends BaseOverlay {
|
||||
|
||||
class StreamingInputPanel implements Component {
|
||||
handle: OverlayHandle | null = null;
|
||||
private theme: Theme;
|
||||
private typed = "";
|
||||
readonly label: string;
|
||||
private color: "error" | "success" | "accent";
|
||||
private onTab: () => void;
|
||||
private onClose: () => void;
|
||||
|
||||
constructor(
|
||||
private theme: Theme,
|
||||
theme: Theme,
|
||||
label: string,
|
||||
private color: "error" | "success" | "accent",
|
||||
private onTab: () => void,
|
||||
private onClose: () => void,
|
||||
color: "error" | "success" | "accent",
|
||||
onTab: () => void,
|
||||
onClose: () => void,
|
||||
) {
|
||||
this.theme = theme;
|
||||
this.label = label;
|
||||
this.color = color;
|
||||
this.onTab = onTab;
|
||||
this.onClose = onClose;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
|
||||
@@ -42,10 +42,13 @@ class OverlayTestComponent implements Focusable {
|
||||
{ label: "Cancel", hasInput: false, text: "", cursor: 0 },
|
||||
];
|
||||
|
||||
constructor(
|
||||
private theme: Theme,
|
||||
private done: (result: { action: string; query?: string } | undefined) => void,
|
||||
) {}
|
||||
private theme: Theme;
|
||||
private done: (result: { action: string; query?: string } | undefined) => void;
|
||||
|
||||
constructor(theme: Theme, done: (result: { action: string; query?: string } | undefined) => void) {
|
||||
this.theme = theme;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, "escape")) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { Key } from "@earendil-works/pi-tui";
|
||||
import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.js";
|
||||
import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.ts";
|
||||
|
||||
// Tools
|
||||
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.4",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"private": true,
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
@@ -14,6 +14,6 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.26"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,8 @@ Use a chain: first have scout find the read tool, then have planner suggest impr
|
||||
- Shows all tasks with live status (⏳ running, ✓ done, ✗ failed)
|
||||
- Updates as each task makes progress
|
||||
- Shows "2/3 done, 1 running" status
|
||||
- Returns each completed task's final output to the parent model, capped at 50 KB per task
|
||||
- Returns failure diagnostics from stderr/error messages when a child exits before producing output
|
||||
|
||||
**Tool call formatting** (mimics built-in tools):
|
||||
- `$ command` for bash
|
||||
@@ -168,5 +170,6 @@ Project agents override user agents with the same name when `agentScope: "both"`
|
||||
## Limitations
|
||||
|
||||
- Output truncated to last 10 items in collapsed view (expand to see all)
|
||||
- Parallel model-visible output is capped at 50 KB per task; full results remain in tool details
|
||||
- Agents discovered fresh on each invocation (allows editing mid-session)
|
||||
- Parallel mode limited to 8 tasks, 4 concurrent
|
||||
|
||||
@@ -22,11 +22,12 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
||||
import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
||||
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import { Type } from "typebox";
|
||||
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
||||
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts";
|
||||
|
||||
const MAX_PARALLEL_TASKS = 8;
|
||||
const MAX_CONCURRENCY = 4;
|
||||
const COLLAPSED_ITEM_COUNT = 10;
|
||||
const PER_TASK_OUTPUT_CAP = 50 * 1024;
|
||||
|
||||
function formatTokens(count: number): string {
|
||||
if (count < 1000) return count.toString();
|
||||
@@ -172,6 +173,28 @@ function getFinalOutput(messages: Message[]): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
function isFailedResult(result: SingleResult): boolean {
|
||||
return result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
||||
}
|
||||
|
||||
function getResultOutput(result: SingleResult): string {
|
||||
if (isFailedResult(result)) {
|
||||
return result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
||||
}
|
||||
return getFinalOutput(result.messages) || "(no output)";
|
||||
}
|
||||
|
||||
function truncateParallelOutput(output: string): string {
|
||||
const byteLength = Buffer.byteLength(output, "utf8");
|
||||
if (byteLength <= PER_TASK_OUTPUT_CAP) return output;
|
||||
|
||||
let truncated = output.slice(0, PER_TASK_OUTPUT_CAP);
|
||||
while (Buffer.byteLength(truncated, "utf8") > PER_TASK_OUTPUT_CAP) {
|
||||
truncated = truncated.slice(0, -1);
|
||||
}
|
||||
return `${truncated}\n\n[Output truncated: ${byteLength - Buffer.byteLength(truncated, "utf8")} bytes omitted. Full output preserved in tool details.]`;
|
||||
}
|
||||
|
||||
type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, any> };
|
||||
|
||||
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
||||
@@ -534,11 +557,9 @@ export default function (pi: ExtensionAPI) {
|
||||
);
|
||||
results.push(result);
|
||||
|
||||
const isError =
|
||||
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
||||
const isError = isFailedResult(result);
|
||||
if (isError) {
|
||||
const errorMsg =
|
||||
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
||||
const errorMsg = getResultOutput(result);
|
||||
return {
|
||||
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
||||
details: makeDetails("chain")(results),
|
||||
@@ -617,17 +638,19 @@ export default function (pi: ExtensionAPI) {
|
||||
return result;
|
||||
});
|
||||
|
||||
const successCount = results.filter((r) => r.exitCode === 0).length;
|
||||
const successCount = results.filter((r) => !isFailedResult(r)).length;
|
||||
const summaries = results.map((r) => {
|
||||
const output = getFinalOutput(r.messages);
|
||||
const preview = output.slice(0, 100) + (output.length > 100 ? "..." : "");
|
||||
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
|
||||
const output = truncateParallelOutput(getResultOutput(r));
|
||||
const status = isFailedResult(r)
|
||||
? `failed${r.stopReason && r.stopReason !== "end" ? ` (${r.stopReason})` : ""}`
|
||||
: "completed";
|
||||
return `### [${r.agent}] ${status}\n\n${output}`;
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
||||
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}`,
|
||||
},
|
||||
],
|
||||
details: makeDetails("parallel")(results),
|
||||
@@ -646,10 +669,9 @@ export default function (pi: ExtensionAPI) {
|
||||
onUpdate,
|
||||
makeDetails("single"),
|
||||
);
|
||||
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
||||
const isError = isFailedResult(result);
|
||||
if (isError) {
|
||||
const errorMsg =
|
||||
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
||||
const errorMsg = getResultOutput(result);
|
||||
return {
|
||||
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
||||
details: makeDetails("single")([result]),
|
||||
@@ -740,7 +762,7 @@ export default function (pi: ExtensionAPI) {
|
||||
|
||||
if (details.mode === "single" && details.results.length === 1) {
|
||||
const r = details.results[0];
|
||||
const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
||||
const isError = isFailedResult(r);
|
||||
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
||||
const displayItems = getDisplayItems(r.messages);
|
||||
const finalOutput = getFinalOutput(r.messages);
|
||||
@@ -893,8 +915,8 @@ export default function (pi: ExtensionAPI) {
|
||||
|
||||
if (details.mode === "parallel") {
|
||||
const running = details.results.filter((r) => r.exitCode === -1).length;
|
||||
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
||||
const failCount = details.results.filter((r) => r.exitCode > 0).length;
|
||||
const successCount = details.results.filter((r) => r.exitCode !== -1 && !isFailedResult(r)).length;
|
||||
const failCount = details.results.filter((r) => r.exitCode !== -1 && isFailedResult(r)).length;
|
||||
const isRunning = running > 0;
|
||||
const icon = isRunning
|
||||
? theme.fg("warning", "⏳")
|
||||
@@ -916,7 +938,7 @@ export default function (pi: ExtensionAPI) {
|
||||
);
|
||||
|
||||
for (const r of details.results) {
|
||||
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
||||
const rIcon = isFailedResult(r) ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
||||
const displayItems = getDisplayItems(r.messages);
|
||||
const finalOutput = getFinalOutput(r.messages);
|
||||
|
||||
@@ -963,9 +985,9 @@ export default function (pi: ExtensionAPI) {
|
||||
const rIcon =
|
||||
r.exitCode === -1
|
||||
? theme.fg("warning", "⏳")
|
||||
: r.exitCode === 0
|
||||
? theme.fg("success", "✓")
|
||||
: theme.fg("error", "✗");
|
||||
: isFailedResult(r)
|
||||
? theme.fg("error", "✗")
|
||||
: theme.fg("success", "✓");
|
||||
const displayItems = getDisplayItems(r.messages);
|
||||
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
||||
if (displayItems.length === 0)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.74.0",
|
||||
"version": "0.75.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.74.0",
|
||||
"version": "0.75.4",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"private": true,
|
||||
"version": "0.74.0",
|
||||
"version": "0.75.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
@@ -14,9 +14,9 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
"ms": "2.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ms": "^2.1.0"
|
||||
"@types/ms": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,18 @@ import { createAgentSession } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const { session } = await createAgentSession();
|
||||
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
try {
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await session.prompt("What files are in the current directory?");
|
||||
session.state.messages.forEach((msg) => {
|
||||
console.log(msg);
|
||||
});
|
||||
console.log();
|
||||
await session.prompt("What files are in the current directory?");
|
||||
session.state.messages.forEach((msg) => {
|
||||
console.log(msg);
|
||||
});
|
||||
console.log();
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
@@ -38,12 +38,16 @@ if (available.length > 0) {
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
try {
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await session.prompt("Say hello in one sentence.");
|
||||
console.log();
|
||||
await session.prompt("Say hello in one sentence.");
|
||||
console.log();
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +30,19 @@ const { session: session1 } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
session1.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
try {
|
||||
session1.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("=== Replace prompt ===");
|
||||
await session1.prompt("What is 2 + 2?");
|
||||
console.log("\n");
|
||||
console.log("=== Replace prompt ===");
|
||||
await session1.prompt("What is 2 + 2?");
|
||||
console.log("\n");
|
||||
} finally {
|
||||
session1.dispose();
|
||||
}
|
||||
|
||||
// Option 2: Append instructions to the default prompt
|
||||
const loader2 = new DefaultResourceLoader({
|
||||
@@ -56,12 +60,16 @@ const { session: session2 } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
session2.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
try {
|
||||
session2.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("=== Modify prompt ===");
|
||||
await session2.prompt("List 3 benefits of TypeScript.");
|
||||
console.log();
|
||||
console.log("=== Modify prompt ===");
|
||||
await session2.prompt("List 3 benefits of TypeScript.");
|
||||
console.log();
|
||||
} finally {
|
||||
session2.dispose();
|
||||
}
|
||||
|
||||
@@ -47,9 +47,9 @@ if (diagnostics.length > 0) {
|
||||
console.log("Warnings:", diagnostics);
|
||||
}
|
||||
|
||||
await createAgentSession({
|
||||
const { session } = await createAgentSession({
|
||||
resourceLoader: loader,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
console.log("Session created with filtered skills");
|
||||
session.dispose();
|
||||
|
||||
@@ -13,32 +13,36 @@
|
||||
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
// Read-only mode (no edit/write)
|
||||
await createAgentSession({
|
||||
const { session: readOnlySession } = await createAgentSession({
|
||||
tools: ["read", "grep", "find", "ls"],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
console.log("Read-only session created");
|
||||
readOnlySession.dispose();
|
||||
|
||||
// Custom tool selection
|
||||
await createAgentSession({
|
||||
const { session: customToolsSession } = await createAgentSession({
|
||||
tools: ["read", "bash", "grep"],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
console.log("Custom tools session created");
|
||||
customToolsSession.dispose();
|
||||
|
||||
// With custom cwd
|
||||
const customCwd = "/path/to/project";
|
||||
await createAgentSession({
|
||||
const { session: customCwdSession } = await createAgentSession({
|
||||
cwd: customCwd,
|
||||
tools: ["read", "bash", "edit", "write"],
|
||||
sessionManager: SessionManager.inMemory(customCwd),
|
||||
});
|
||||
console.log("Custom cwd session created");
|
||||
customCwdSession.dispose();
|
||||
|
||||
// Or pick specific tools for custom cwd
|
||||
await createAgentSession({
|
||||
const { session: specificToolsSession } = await createAgentSession({
|
||||
cwd: customCwd,
|
||||
tools: ["read", "bash", "grep"],
|
||||
sessionManager: SessionManager.inMemory(customCwd),
|
||||
});
|
||||
console.log("Specific tools with custom cwd session created");
|
||||
specificToolsSession.dispose();
|
||||
|
||||
@@ -42,14 +42,18 @@ const { session } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
try {
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await session.prompt("List files in the current directory.");
|
||||
console.log();
|
||||
await session.prompt("List files in the current directory.");
|
||||
console.log();
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
// Example extension file (./my-logging-extension.ts):
|
||||
/*
|
||||
|
||||
@@ -39,9 +39,9 @@ for (const file of discovered) {
|
||||
console.log(` - ${file.path} (${file.content.length} chars)`);
|
||||
}
|
||||
|
||||
await createAgentSession({
|
||||
const { session } = await createAgentSession({
|
||||
resourceLoader: loader,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
console.log(`Session created with ${discovered.length + 1} context files`);
|
||||
session.dispose();
|
||||
|
||||
@@ -43,9 +43,9 @@ for (const template of discovered) {
|
||||
console.log(` /${template.name}: ${template.description}`);
|
||||
}
|
||||
|
||||
await createAgentSession({
|
||||
const { session } = await createAgentSession({
|
||||
resourceLoader: loader,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
console.log(`Session created with ${discovered.length + 1} prompt templates`);
|
||||
session.dispose();
|
||||
|
||||
@@ -11,38 +11,42 @@ import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "
|
||||
const authStorage = AuthStorage.create();
|
||||
const modelRegistry = ModelRegistry.create(authStorage);
|
||||
|
||||
await createAgentSession({
|
||||
const { session: defaultAuthSession } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
});
|
||||
console.log("Session with default auth storage and model registry");
|
||||
defaultAuthSession.dispose();
|
||||
|
||||
// Custom auth storage location
|
||||
const customAuthStorage = AuthStorage.create("/tmp/my-app/auth.json");
|
||||
const customModelRegistry = ModelRegistry.create(customAuthStorage, "/tmp/my-app/models.json");
|
||||
|
||||
await createAgentSession({
|
||||
const { session: customAuthSession } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
authStorage: customAuthStorage,
|
||||
modelRegistry: customModelRegistry,
|
||||
});
|
||||
console.log("Session with custom auth storage location");
|
||||
customAuthSession.dispose();
|
||||
|
||||
// Runtime API key override (not persisted to disk)
|
||||
authStorage.setRuntimeApiKey("anthropic", "sk-my-temp-key");
|
||||
await createAgentSession({
|
||||
const { session: runtimeKeySession } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
});
|
||||
console.log("Session with runtime API key override");
|
||||
runtimeKeySession.dispose();
|
||||
|
||||
// No models.json - only built-in models
|
||||
const simpleRegistry = ModelRegistry.inMemory(authStorage);
|
||||
await createAgentSession({
|
||||
const { session: builtInModelsSession } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
authStorage,
|
||||
modelRegistry: simpleRegistry,
|
||||
});
|
||||
console.log("Session with only built-in models");
|
||||
builtInModelsSession.dispose();
|
||||
|
||||
@@ -19,12 +19,12 @@ settingsManager.applyOverrides({
|
||||
retry: { enabled: true, maxRetries: 5, baseDelayMs: 1000 },
|
||||
});
|
||||
|
||||
await createAgentSession({
|
||||
const { session: customSettingsSession } = await createAgentSession({
|
||||
settingsManager,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
console.log("Session created with custom settings");
|
||||
customSettingsSession.dispose();
|
||||
|
||||
// Setters update memory immediately and queue persistence writes.
|
||||
// Call flush() when you need a durability boundary.
|
||||
@@ -45,9 +45,9 @@ const inMemorySettings = SettingsManager.inMemory({
|
||||
retry: { enabled: false },
|
||||
});
|
||||
|
||||
await createAgentSession({
|
||||
const { session: testSession } = await createAgentSession({
|
||||
settingsManager: inMemorySettings,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
console.log("Test session created with in-memory settings");
|
||||
testSession.dispose();
|
||||
|
||||
@@ -11,12 +11,14 @@ const { session: inMemory } = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
console.log("In-memory session:", inMemory.sessionFile ?? "(none)");
|
||||
inMemory.dispose();
|
||||
|
||||
// New persistent session
|
||||
const { session: newSession } = await createAgentSession({
|
||||
sessionManager: SessionManager.create(process.cwd()),
|
||||
});
|
||||
console.log("New session file:", newSession.sessionFile);
|
||||
newSession.dispose();
|
||||
|
||||
// Continue most recent session (or create new if none)
|
||||
const { session: continued, modelFallbackMessage } = await createAgentSession({
|
||||
@@ -24,6 +26,7 @@ const { session: continued, modelFallbackMessage } = await createAgentSession({
|
||||
});
|
||||
if (modelFallbackMessage) console.log("Note:", modelFallbackMessage);
|
||||
console.log("Continued session:", continued.sessionFile);
|
||||
continued.dispose();
|
||||
|
||||
// List and open specific session
|
||||
const sessions = await SessionManager.list(process.cwd());
|
||||
@@ -37,6 +40,7 @@ if (sessions.length > 0) {
|
||||
sessionManager: SessionManager.open(sessions[0].path),
|
||||
});
|
||||
console.log(`\nOpened: ${opened.sessionId}`);
|
||||
opened.dispose();
|
||||
}
|
||||
|
||||
// Custom session directory (no cwd encoding)
|
||||
|
||||
@@ -63,11 +63,15 @@ const { session } = await createAgentSession({
|
||||
settingsManager,
|
||||
});
|
||||
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
try {
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await session.prompt("List files in the current directory.");
|
||||
console.log();
|
||||
await session.prompt("List files in the current directory.");
|
||||
console.log();
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ The runtime example shows how to build a recreate function that closes over proc
|
||||
| `02-custom-model.ts` | Select model and thinking level |
|
||||
| `03-custom-prompt.ts` | Replace or modify system prompt |
|
||||
| `04-skills.ts` | Discover, filter, or replace skills |
|
||||
| `05-tools.ts` | Built-in tools, custom tools |
|
||||
| `05-tools.ts` | Built-in tool allowlists |
|
||||
| `06-extensions.ts` | Logging, blocking, result modification |
|
||||
| `07-context-files.ts` | AGENTS.md context files |
|
||||
| `08-slash-commands.ts` | File-based slash commands |
|
||||
@@ -40,9 +40,6 @@ import {
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
codingTools,
|
||||
readOnlyTools,
|
||||
readTool, bashTool, editTool, writeTool,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
|
||||
// Auth and models setup
|
||||
@@ -64,7 +61,7 @@ await loader.reload();
|
||||
const { session } = await createAgentSession({ resourceLoader: loader, authStorage, modelRegistry });
|
||||
|
||||
// Read-only
|
||||
const { session } = await createAgentSession({ tools: readOnlyTools, authStorage, modelRegistry });
|
||||
const { session } = await createAgentSession({ tools: ["read", "grep", "find", "ls"], authStorage, modelRegistry });
|
||||
|
||||
// In-memory
|
||||
const { session } = await createAgentSession({
|
||||
@@ -92,8 +89,8 @@ const { session } = await createAgentSession({
|
||||
authStorage: customAuth,
|
||||
modelRegistry: customRegistry,
|
||||
resourceLoader,
|
||||
tools: [readTool, bashTool],
|
||||
customTools: [{ tool: myTool }],
|
||||
tools: ["read", "bash", "my_tool"],
|
||||
customTools: [myTool],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settingsManager: SettingsManager.inMemory(),
|
||||
});
|
||||
@@ -117,7 +114,7 @@ await session.prompt("Hello");
|
||||
| `agentDir` | `~/.pi/agent` | Config directory |
|
||||
| `model` | From settings/first available | Model to use |
|
||||
| `thinkingLevel` | From settings/"off" | off, low, medium, high |
|
||||
| `tools` | `codingTools` | Built-in tools |
|
||||
| `tools` | `["read", "bash", "edit", "write"]` built-ins | Allowlist tool names across built-in, extension, and custom tools |
|
||||
| `customTools` | `[]` | Additional tool definitions |
|
||||
| `resourceLoader` | DefaultResourceLoader | Resource loader for extensions, skills, prompts, themes |
|
||||
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
|
||||
|
||||
1789
packages/coding-agent/npm-shrinkwrap.json
generated
Normal file
1789
packages/coding-agent/npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@earendil-works/pi-coding-agent",
|
||||
"version": "0.74.0",
|
||||
"version": "0.75.4",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"type": "module",
|
||||
"piConfig": {
|
||||
@@ -25,40 +25,37 @@
|
||||
"dist",
|
||||
"docs",
|
||||
"examples",
|
||||
"CHANGELOG.md"
|
||||
"CHANGELOG.md",
|
||||
"npm-shrinkwrap.json"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
|
||||
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
|
||||
"build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets",
|
||||
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
|
||||
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
|
||||
"test": "vitest --run",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
"shrinkwrap": "node ../../scripts/generate-coding-agent-shrinkwrap.mjs",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "^0.74.0",
|
||||
"@earendil-works/pi-ai": "^0.74.0",
|
||||
"@earendil-works/pi-tui": "^0.74.0",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"chalk": "^5.5.0",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"diff": "^8.0.2",
|
||||
"extract-zip": "^2.0.1",
|
||||
"file-type": "^21.1.1",
|
||||
"glob": "^13.0.1",
|
||||
"hosted-git-info": "^9.0.2",
|
||||
"ignore": "^7.0.5",
|
||||
"jiti": "^2.7.0",
|
||||
"marked": "^15.0.12",
|
||||
"minimatch": "^10.2.3",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"typebox": "^1.1.24",
|
||||
"undici": "^7.19.1",
|
||||
"uuid": "^14.0.0",
|
||||
"yaml": "^2.8.2"
|
||||
"@earendil-works/pi-agent-core": "^0.75.4",
|
||||
"@earendil-works/pi-ai": "^0.75.4",
|
||||
"@earendil-works/pi-tui": "^0.75.4",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"chalk": "5.6.2",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "8.0.4",
|
||||
"glob": "13.0.6",
|
||||
"highlight.js": "10.7.3",
|
||||
"hosted-git-info": "9.0.3",
|
||||
"ignore": "7.0.5",
|
||||
"jiti": "2.7.0",
|
||||
"minimatch": "10.2.5",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"typebox": "1.1.38",
|
||||
"undici": "8.3.0",
|
||||
"yaml": "2.9.0"
|
||||
},
|
||||
"overrides": {
|
||||
"rimraf": "6.1.2",
|
||||
@@ -67,17 +64,18 @@
|
||||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@mariozechner/clipboard": "^0.3.5"
|
||||
"@mariozechner/clipboard": "0.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/hosted-git-info": "^3.0.5",
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"shx": "^0.4.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.2.4"
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/diff": "7.0.2",
|
||||
"@types/hosted-git-info": "3.0.5",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "24.12.4",
|
||||
"@types/proper-lockfile": "4.1.4",
|
||||
"shx": "0.4.0",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "3.2.4"
|
||||
},
|
||||
"keywords": [
|
||||
"coding-agent",
|
||||
@@ -95,6 +93,6 @@
|
||||
"directory": "packages/coding-agent"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import { APP_NAME } from "../config.js";
|
||||
import { APP_NAME } from "../config.ts";
|
||||
|
||||
process.title = APP_NAME;
|
||||
process.emitWarning = (() => {}) as typeof process.emitWarning;
|
||||
|
||||
import { restoreSandboxEnv } from "./restore-sandbox-env.js";
|
||||
import { restoreSandboxEnv } from "./restore-sandbox-env.ts";
|
||||
|
||||
restoreSandboxEnv();
|
||||
|
||||
await import("./register-bedrock.js");
|
||||
await import("../cli.js");
|
||||
await import("./register-bedrock.ts");
|
||||
await import("../cli.ts");
|
||||
|
||||
@@ -5,18 +5,16 @@
|
||||
*
|
||||
* Test with: npx tsx src/cli-new.ts [args...]
|
||||
*/
|
||||
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
||||
import { APP_NAME } from "./config.js";
|
||||
import { main } from "./main.js";
|
||||
import { APP_NAME } from "./config.ts";
|
||||
import { configureHttpDispatcher } from "./core/http-dispatcher.ts";
|
||||
import { main } from "./main.ts";
|
||||
|
||||
process.title = APP_NAME;
|
||||
process.env.PI_CODING_AGENT = "true";
|
||||
process.emitWarning = (() => {}) as typeof process.emitWarning;
|
||||
|
||||
// bodyTimeout/headersTimeout default to 300s in undici; long local-LLM stalls
|
||||
// (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream
|
||||
// with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own
|
||||
// AbortController-based deadlines via retry.provider.timeoutMs.
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }));
|
||||
// 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));
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import chalk from "chalk";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from "../config.js";
|
||||
import type { ExtensionFlag } from "../core/extensions/types.js";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from "../config.ts";
|
||||
import type { ExtensionFlag } from "../core/extensions/types.ts";
|
||||
|
||||
export type Mode = "text" | "json" | "rpc";
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
*/
|
||||
|
||||
import { ProcessTerminal, TUI } from "@earendil-works/pi-tui";
|
||||
import type { ResolvedPaths } from "../core/package-manager.js";
|
||||
import type { SettingsManager } from "../core/settings-manager.js";
|
||||
import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js";
|
||||
import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.js";
|
||||
import type { ResolvedPaths } from "../core/package-manager.ts";
|
||||
import type { SettingsManager } from "../core/settings-manager.ts";
|
||||
import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.ts";
|
||||
import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.ts";
|
||||
|
||||
export interface ConfigSelectorOptions {
|
||||
resolvedPaths: ResolvedPaths;
|
||||
@@ -43,6 +43,7 @@ export async function selectConfig(options: ConfigSelectorOptions): Promise<void
|
||||
process.exit(0);
|
||||
},
|
||||
() => ui.requestRender(),
|
||||
ui.terminal.rows,
|
||||
);
|
||||
|
||||
ui.addChild(selector);
|
||||
|
||||
@@ -6,9 +6,9 @@ import { access, readFile, stat } from "node:fs/promises";
|
||||
import type { ImageContent } from "@earendil-works/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import { resolve } from "path";
|
||||
import { resolveReadPath } from "../core/tools/path-utils.js";
|
||||
import { formatDimensionNote, resizeImage } from "../utils/image-resize.js";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.js";
|
||||
import { resolveReadPath } from "../core/tools/path-utils.ts";
|
||||
import { formatDimensionNote, resizeImage } from "../utils/image-resize.ts";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.ts";
|
||||
|
||||
export interface ProcessedFiles {
|
||||
text: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ImageContent } from "@earendil-works/pi-ai";
|
||||
import type { Args } from "./args.js";
|
||||
import type { Args } from "./args.ts";
|
||||
|
||||
export interface InitialMessageInput {
|
||||
parsed: Args;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
import type { Api, Model } from "@earendil-works/pi-ai";
|
||||
import { fuzzyFilter } from "@earendil-works/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { formatNoModelsAvailableMessage } from "../core/auth-guidance.js";
|
||||
import type { ModelRegistry } from "../core/model-registry.js";
|
||||
import { formatNoModelsAvailableMessage } from "../core/auth-guidance.ts";
|
||||
import type { ModelRegistry } from "../core/model-registry.ts";
|
||||
|
||||
/**
|
||||
* Format a number as human-readable (e.g., 200000 -> "200K", 1000000 -> "1M")
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
*/
|
||||
|
||||
import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";
|
||||
import { KeybindingsManager } from "../core/keybindings.js";
|
||||
import type { SessionInfo, SessionListProgress } from "../core/session-manager.js";
|
||||
import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js";
|
||||
import { KeybindingsManager } from "../core/keybindings.ts";
|
||||
import type { SessionInfo, SessionListProgress } from "../core/session-manager.ts";
|
||||
import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.ts";
|
||||
|
||||
type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { spawnSync } from "child_process";
|
||||
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 { shouldUseWindowsShell } from "./utils/child-process.js";
|
||||
import { spawnProcessSync } from "./utils/child-process.ts";
|
||||
import { normalizePath } from "./utils/paths.ts";
|
||||
|
||||
// =============================================================================
|
||||
// Package Detection
|
||||
@@ -111,21 +111,21 @@ function getSelfUpdateCommandForMethod(
|
||||
return undefined;
|
||||
case "pnpm":
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("pnpm", ["install", "-g", updatePackageName]),
|
||||
makeSelfUpdateCommandStep("pnpm", ["install", "-g", "--ignore-scripts", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]),
|
||||
);
|
||||
case "yarn":
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("yarn", ["global", "add", updatePackageName]),
|
||||
makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]),
|
||||
);
|
||||
case "bun":
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("bun", ["install", "-g", updatePackageName]),
|
||||
makeSelfUpdateCommandStep("bun", ["install", "-g", "--ignore-scripts", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]),
|
||||
@@ -134,7 +134,13 @@ function getSelfUpdateCommandForMethod(
|
||||
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", updatePackageName]);
|
||||
const installStep = makeSelfUpdateCommandStep(command, [
|
||||
...prefixArgs,
|
||||
"install",
|
||||
"-g",
|
||||
"--ignore-scripts",
|
||||
updatePackageName,
|
||||
]);
|
||||
const uninstallStep =
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
@@ -151,10 +157,9 @@ function readCommandOutput(
|
||||
args: string[],
|
||||
options: { requireSuccess?: boolean } = {},
|
||||
): string | undefined {
|
||||
const result = spawnSync(command, args, {
|
||||
const result = spawnProcessSync(command, args, {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: shouldUseWindowsShell(command),
|
||||
});
|
||||
if (result.status === 0) return result.stdout.trim() || undefined;
|
||||
if (options.requireSuccess) {
|
||||
@@ -207,16 +212,18 @@ function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmC
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExistingPathForComparison(path: string): string | undefined {
|
||||
function normalizeExistingPathForComparison(path: string, resolveSymlinks: boolean): string | undefined {
|
||||
const resolvedPath = resolve(path);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
return undefined;
|
||||
}
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return undefined;
|
||||
let normalizedPath = resolvedPath;
|
||||
if (resolveSymlinks) {
|
||||
try {
|
||||
normalizedPath = realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
normalizedPath = normalizedPath.toLowerCase();
|
||||
@@ -224,6 +231,29 @@ function normalizeExistingPathForComparison(path: string): string | undefined {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
function getPathComparisonCandidates(path: string): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
[normalizeExistingPathForComparison(path, false), normalizeExistingPathForComparison(path, true)].filter(
|
||||
(candidate): candidate is string => !!candidate,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function getEntrypointPackageDir(): string | undefined {
|
||||
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(): boolean {
|
||||
const packageDir = getPackageDir();
|
||||
try {
|
||||
@@ -236,17 +266,14 @@ function isSelfUpdatePathWritable(): boolean {
|
||||
}
|
||||
|
||||
function isManagedByGlobalPackageManager(method: InstallMethod, packageName: string, npmCommand?: string[]): boolean {
|
||||
const packageDir = normalizeExistingPathForComparison(getPackageDir());
|
||||
return (
|
||||
!!packageDir &&
|
||||
getGlobalPackageRoots(method, packageName, npmCommand).some((root) => {
|
||||
const normalizedRoot = normalizeExistingPathForComparison(root);
|
||||
return (
|
||||
!!normalizedRoot &&
|
||||
packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`)
|
||||
);
|
||||
})
|
||||
);
|
||||
const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter((dir): dir is string => !!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(
|
||||
@@ -304,9 +331,7 @@ export function getPackageDir(): string {
|
||||
// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)
|
||||
const envDir = process.env.PI_PACKAGE_DIR;
|
||||
if (envDir) {
|
||||
if (envDir === "~") return homedir();
|
||||
if (envDir.startsWith("~/")) return homedir() + envDir.slice(1);
|
||||
return envDir;
|
||||
return normalizePath(envDir);
|
||||
}
|
||||
|
||||
if (isBunBinary) {
|
||||
@@ -428,9 +453,7 @@ 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: string): string {
|
||||
if (path === "~") return homedir();
|
||||
if (path.startsWith("~/")) return homedir() + path.slice(1);
|
||||
return path;
|
||||
return normalizePath(path);
|
||||
}
|
||||
|
||||
const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/";
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import type { AgentSession } from "./agent-session.js";
|
||||
import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.js";
|
||||
import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.js";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
||||
import type { CreateAgentSessionResult } from "./sdk.js";
|
||||
import { assertSessionCwdExists } from "./session-cwd.js";
|
||||
import { SessionManager } from "./session-manager.js";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import type { AgentSession } from "./agent-session.ts";
|
||||
import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.ts";
|
||||
import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.ts";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.ts";
|
||||
import type { CreateAgentSessionResult } from "./sdk.ts";
|
||||
import { assertSessionCwdExists } from "./session-cwd.ts";
|
||||
import { SessionManager } from "./session-manager.ts";
|
||||
|
||||
/**
|
||||
* Result returned by runtime creation.
|
||||
@@ -67,14 +68,25 @@ function extractUserMessageText(content: string | Array<{ type: string; text?: s
|
||||
export class AgentSessionRuntime {
|
||||
private rebindSession?: (session: AgentSession) => Promise<void>;
|
||||
private beforeSessionInvalidate?: () => void;
|
||||
private _session: AgentSession;
|
||||
private _services: AgentSessionServices;
|
||||
private readonly createRuntime: CreateAgentSessionRuntimeFactory;
|
||||
private _diagnostics: AgentSessionRuntimeDiagnostic[];
|
||||
private _modelFallbackMessage?: string;
|
||||
|
||||
constructor(
|
||||
private _session: AgentSession,
|
||||
private _services: AgentSessionServices,
|
||||
private readonly createRuntime: CreateAgentSessionRuntimeFactory,
|
||||
private _diagnostics: AgentSessionRuntimeDiagnostic[] = [],
|
||||
private _modelFallbackMessage?: string,
|
||||
) {}
|
||||
_session: AgentSession,
|
||||
_services: AgentSessionServices,
|
||||
createRuntime: CreateAgentSessionRuntimeFactory,
|
||||
_diagnostics: AgentSessionRuntimeDiagnostic[] = [],
|
||||
_modelFallbackMessage?: string,
|
||||
) {
|
||||
this._session = _session;
|
||||
this._services = _services;
|
||||
this.createRuntime = createRuntime;
|
||||
this._diagnostics = _diagnostics;
|
||||
this._modelFallbackMessage = _modelFallbackMessage;
|
||||
}
|
||||
|
||||
get services(): AgentSessionServices {
|
||||
return this._services;
|
||||
@@ -281,12 +293,11 @@ export class AgentSessionRuntime {
|
||||
return { cancelled: false, selectedText };
|
||||
}
|
||||
|
||||
const sourceManager = SessionManager.open(currentSessionFile, sessionDir);
|
||||
const forkedSessionPath = sourceManager.createBranchedSession(targetLeafId);
|
||||
const sessionManager = SessionManager.open(currentSessionFile, sessionDir);
|
||||
const forkedSessionPath = sessionManager.createBranchedSession(targetLeafId);
|
||||
if (!forkedSessionPath) {
|
||||
throw new Error("Failed to create forked session");
|
||||
}
|
||||
const sessionManager = SessionManager.open(forkedSessionPath, sessionDir);
|
||||
await this.teardownCurrent("fork", sessionManager.getSessionFile());
|
||||
this.apply(
|
||||
await this.createRuntime({
|
||||
@@ -327,7 +338,7 @@ export class AgentSessionRuntime {
|
||||
* @throws {MissingSessionCwdError} When the imported session cwd cannot be resolved and no override is provided.
|
||||
*/
|
||||
async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
|
||||
const resolvedPath = resolve(inputPath);
|
||||
const resolvedPath = resolvePath(inputPath);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
throw new SessionImportFileNotFoundError(resolvedPath);
|
||||
}
|
||||
@@ -406,4 +417,4 @@ export {
|
||||
type CreateAgentSessionServicesOptions,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionServices,
|
||||
} from "./agent-session-services.js";
|
||||
} from "./agent-session-services.ts";
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { join } from "node:path";
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import type { Model } from "@earendil-works/pi-ai";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { AuthStorage } from "./auth-storage.js";
|
||||
import type { SessionStartEvent, ToolDefinition } from "./extensions/index.js";
|
||||
import { ModelRegistry } from "./model-registry.js";
|
||||
import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.js";
|
||||
import { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession } from "./sdk.js";
|
||||
import type { SessionManager } from "./session-manager.js";
|
||||
import { SettingsManager } from "./settings-manager.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { AuthStorage } from "./auth-storage.ts";
|
||||
import type { SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
|
||||
import { ModelRegistry } from "./model-registry.ts";
|
||||
import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.ts";
|
||||
import { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession } 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.
|
||||
@@ -129,8 +130,8 @@ function applyExtensionFlagValues(
|
||||
export async function createAgentSessionServices(
|
||||
options: CreateAgentSessionServicesOptions,
|
||||
): Promise<AgentSessionServices> {
|
||||
const cwd = options.cwd;
|
||||
const agentDir = options.agentDir ?? getAgentDir();
|
||||
const cwd = resolvePath(options.cwd);
|
||||
const agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir();
|
||||
const authStorage = options.authStorage ?? AuthStorage.create(join(agentDir, "auth.json"));
|
||||
const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
|
||||
const modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, join(agentDir, "models.json"));
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { basename, dirname } from "node:path";
|
||||
import type {
|
||||
Agent,
|
||||
AgentEvent,
|
||||
@@ -31,12 +31,14 @@ import {
|
||||
isContextOverflow,
|
||||
modelsAreEqual,
|
||||
resetApiProviders,
|
||||
streamSimple,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { theme } from "../modes/interactive/theme/theme.js";
|
||||
import { stripFrontmatter } from "../utils/frontmatter.js";
|
||||
import { sleep } from "../utils/sleep.js";
|
||||
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
||||
import { type BashResult, executeBashWithOperations } from "./bash-executor.js";
|
||||
import { theme } from "../modes/interactive/theme/theme.ts";
|
||||
import { stripFrontmatter } from "../utils/frontmatter.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { sleep } from "../utils/sleep.ts";
|
||||
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.ts";
|
||||
import { type BashResult, executeBashWithOperations } from "./bash-executor.ts";
|
||||
import {
|
||||
type CompactionResult,
|
||||
calculateContextTokens,
|
||||
@@ -46,10 +48,10 @@ import {
|
||||
generateBranchSummary,
|
||||
prepareCompaction,
|
||||
shouldCompact,
|
||||
} from "./compaction/index.js";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
||||
import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.js";
|
||||
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
||||
} from "./compaction/index.ts";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
||||
import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.ts";
|
||||
import { createToolHtmlRenderer } from "./export-html/tool-renderer.ts";
|
||||
import {
|
||||
type ContextUsage,
|
||||
type ExtensionCommandContextActions,
|
||||
@@ -74,21 +76,21 @@ import {
|
||||
type TurnEndEvent,
|
||||
type TurnStartEvent,
|
||||
wrapRegisteredTools,
|
||||
} from "./extensions/index.js";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
||||
import type { BashExecutionMessage, CustomMessage } from "./messages.js";
|
||||
import type { ModelRegistry } from "./model-registry.js";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js";
|
||||
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
|
||||
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.js";
|
||||
import type { SettingsManager } from "./settings-manager.js";
|
||||
import type { SlashCommandInfo } from "./slash-commands.js";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.js";
|
||||
import { type BashOperations, createLocalBashOperations } from "./tools/bash.js";
|
||||
import { createAllToolDefinitions } from "./tools/index.js";
|
||||
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
||||
} from "./extensions/index.ts";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.ts";
|
||||
import type { BashExecutionMessage, CustomMessage } from "./messages.ts";
|
||||
import type { ModelRegistry } from "./model-registry.ts";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.ts";
|
||||
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.ts";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.ts";
|
||||
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.ts";
|
||||
import type { SettingsManager } from "./settings-manager.ts";
|
||||
import type { SlashCommandInfo } from "./slash-commands.ts";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.ts";
|
||||
import { type BashOperations, createLocalBashOperations } from "./tools/bash.ts";
|
||||
import { createAllToolDefinitions } from "./tools/index.ts";
|
||||
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.ts";
|
||||
|
||||
// ============================================================================
|
||||
// Skill Block Parsing
|
||||
@@ -119,7 +121,12 @@ export function parseSkillBlock(text: string): ParsedSkillBlock | null {
|
||||
|
||||
/** Session-specific events that extend the core AgentEvent */
|
||||
export type AgentSessionEvent =
|
||||
| AgentEvent
|
||||
| Exclude<AgentEvent, { type: "agent_end" }>
|
||||
| {
|
||||
type: "agent_end";
|
||||
messages: AgentMessage[];
|
||||
willRetry: boolean;
|
||||
}
|
||||
| {
|
||||
type: "queue_update";
|
||||
steering: readonly string[];
|
||||
@@ -179,6 +186,7 @@ export interface AgentSessionConfig {
|
||||
export interface ExtensionBindings {
|
||||
uiContext?: ExtensionUIContext;
|
||||
commandContextActions?: ExtensionCommandContextActions;
|
||||
abortHandler?: () => void;
|
||||
shutdownHandler?: ShutdownHandler;
|
||||
onError?: ExtensionErrorListener;
|
||||
}
|
||||
@@ -251,7 +259,6 @@ export class AgentSession {
|
||||
// Event subscription state
|
||||
private _unsubscribeAgent?: () => void;
|
||||
private _eventListeners: AgentSessionEventListener[] = [];
|
||||
private _agentEventQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
||||
private _steeringMessages: string[] = [];
|
||||
@@ -271,8 +278,6 @@ export class AgentSession {
|
||||
// Retry state
|
||||
private _retryAbortController: AbortController | undefined = undefined;
|
||||
private _retryAttempt = 0;
|
||||
private _retryPromise: Promise<void> | undefined = undefined;
|
||||
private _retryResolve: (() => void) | undefined = undefined;
|
||||
|
||||
// Bash execution state
|
||||
private _bashAbortController: AbortController | undefined = undefined;
|
||||
@@ -293,6 +298,7 @@ export class AgentSession {
|
||||
private _sessionStartEvent: SessionStartEvent;
|
||||
private _extensionUIContext?: ExtensionUIContext;
|
||||
private _extensionCommandContextActions?: ExtensionCommandContextActions;
|
||||
private _extensionAbortHandler?: () => void;
|
||||
private _extensionShutdownHandler?: ShutdownHandler;
|
||||
private _extensionErrorListener?: ExtensionErrorListener;
|
||||
private _extensionErrorUnsubscriber?: () => void;
|
||||
@@ -367,6 +373,18 @@ export class AgentSession {
|
||||
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
||||
}
|
||||
|
||||
private async _getCompactionRequestAuth(model: Model<any>): Promise<{
|
||||
apiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
}> {
|
||||
if (this.agent.streamFn === streamSimple) {
|
||||
return this._getRequiredRequestAuth(model);
|
||||
}
|
||||
|
||||
const result = await this._modelRegistry.getApiKeyAndHeaders(model);
|
||||
return result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install tool hooks once on the Agent instance.
|
||||
*
|
||||
@@ -382,8 +400,6 @@ export class AgentSession {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this._agentEventQueue;
|
||||
|
||||
try {
|
||||
return await runner.emitToolCall({
|
||||
type: "tool_call",
|
||||
@@ -450,54 +466,7 @@ export class AgentSession {
|
||||
private _lastAssistantMessage: AssistantMessage | undefined = undefined;
|
||||
|
||||
/** Internal handler for agent events - shared by subscribe and reconnect */
|
||||
private _handleAgentEvent = (event: AgentEvent): void => {
|
||||
// Create retry promise synchronously before queueing async processing.
|
||||
// Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry()
|
||||
// as soon as agent.prompt() resolves. If _retryPromise is created only inside
|
||||
// _processAgentEvent, slow earlier queued events can delay agent_end processing
|
||||
// and waitForRetry() can miss the in-flight retry.
|
||||
this._createRetryPromiseForAgentEnd(event);
|
||||
|
||||
this._agentEventQueue = this._agentEventQueue.then(
|
||||
() => this._processAgentEvent(event),
|
||||
() => this._processAgentEvent(event),
|
||||
);
|
||||
|
||||
// Keep queue alive if an event handler fails
|
||||
this._agentEventQueue.catch(() => {});
|
||||
};
|
||||
|
||||
private _createRetryPromiseForAgentEnd(event: AgentEvent): void {
|
||||
if (event.type !== "agent_end" || this._retryPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastAssistant = this._findLastAssistantInMessages(event.messages);
|
||||
if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._retryPromise = new Promise((resolve) => {
|
||||
this._retryResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
private _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message.role === "assistant") {
|
||||
return message as AssistantMessage;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _processAgentEvent(event: AgentEvent): Promise<void> {
|
||||
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
||||
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
||||
// This ensures the UI sees the updated queue state
|
||||
if (event.type === "message_start" && event.message.role === "user") {
|
||||
@@ -524,7 +493,7 @@ export class AgentSession {
|
||||
await this._emitExtensionEvent(event);
|
||||
|
||||
// Notify all listeners
|
||||
this._emit(event);
|
||||
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
||||
|
||||
// Handle session persistence
|
||||
if (event.type === "message_end") {
|
||||
@@ -568,30 +537,21 @@ export class AgentSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check auto-retry and auto-compaction after agent completes
|
||||
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
||||
const msg = this._lastAssistantMessage;
|
||||
this._lastAssistantMessage = undefined;
|
||||
private _willRetryAfterAgentEnd(event: Extract<AgentEvent, { type: "agent_end" }>): boolean {
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for retryable errors first (overloaded, rate limit, server errors)
|
||||
if (this._isRetryableError(msg)) {
|
||||
const didRetry = await this._handleRetryableError(msg);
|
||||
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
||||
for (let i = event.messages.length - 1; i >= 0; i--) {
|
||||
const message = event.messages[i];
|
||||
if (message.role === "assistant") {
|
||||
return this._isRetryableError(message as AssistantMessage);
|
||||
}
|
||||
|
||||
this._resolveRetry();
|
||||
await this._checkCompaction(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the pending retry promise */
|
||||
private _resolveRetry(): void {
|
||||
if (this._retryResolve) {
|
||||
this._retryResolve();
|
||||
this._retryResolve = undefined;
|
||||
this._retryPromise = undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Extract text content from a message */
|
||||
@@ -617,7 +577,7 @@ export class AgentSession {
|
||||
|
||||
private _replaceMessageInPlace(target: AgentMessage, replacement: AgentMessage): void {
|
||||
// Agent-core stores the finalized message object in its state before emitting message_end.
|
||||
// SessionManager persistence happens later in _processAgentEvent() with event.message.
|
||||
// SessionManager persistence happens later in _handleAgentEvent() with event.message.
|
||||
// Mutating this object in place keeps agent state, later turn/agent events, listeners,
|
||||
// and the eventual SessionManager.appendMessage(event.message) persistence in sync.
|
||||
if (target === replacement) {
|
||||
@@ -963,6 +923,41 @@ export class AgentSession {
|
||||
// Prompting
|
||||
// =========================================================================
|
||||
|
||||
private async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {
|
||||
try {
|
||||
await this.agent.prompt(messages);
|
||||
while (await this._handlePostAgentRun()) {
|
||||
await this.agent.continue();
|
||||
}
|
||||
} finally {
|
||||
this._flushPendingBashMessages();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handlePostAgentRun(): Promise<boolean> {
|
||||
const msg = this._lastAssistantMessage;
|
||||
this._lastAssistantMessage = undefined;
|
||||
if (!msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.stopReason === "error" && this._retryAttempt > 0) {
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt: this._retryAttempt,
|
||||
finalError: msg.errorMessage,
|
||||
});
|
||||
this._retryAttempt = 0;
|
||||
}
|
||||
|
||||
return await this._checkCompaction(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a prompt to the agent.
|
||||
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
||||
@@ -1053,8 +1048,15 @@ export class AgentSession {
|
||||
|
||||
// Check if we need to compact before sending (catches aborted responses)
|
||||
const lastAssistant = this._findLastAssistantMessage();
|
||||
if (lastAssistant) {
|
||||
await this._checkCompaction(lastAssistant, false);
|
||||
if (lastAssistant && (await this._checkCompaction(lastAssistant, false))) {
|
||||
try {
|
||||
await this.agent.continue();
|
||||
while (await this._handlePostAgentRun()) {
|
||||
await this.agent.continue();
|
||||
}
|
||||
} finally {
|
||||
this._flushPendingBashMessages();
|
||||
}
|
||||
}
|
||||
|
||||
// Build messages array (custom message if any, then user message)
|
||||
@@ -1114,8 +1116,7 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
preflightResult?.(true);
|
||||
await this.agent.prompt(messages);
|
||||
await this.waitForRetry();
|
||||
await this._runAgentPrompt(messages);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1301,7 +1302,7 @@ export class AgentSession {
|
||||
this.agent.steer(appMessage);
|
||||
}
|
||||
} else if (options?.triggerTurn) {
|
||||
await this.agent.prompt(appMessage);
|
||||
await this._runAgentPrompt(appMessage);
|
||||
} else {
|
||||
this.agent.state.messages.push(appMessage);
|
||||
this.sessionManager.appendCustomMessageEntry(
|
||||
@@ -1626,7 +1627,7 @@ export class AgentSession {
|
||||
throw new Error(formatNoModelSelectedMessage());
|
||||
}
|
||||
|
||||
const { apiKey, headers } = await this._getRequiredRequestAuth(this.model);
|
||||
const { apiKey, headers } = await this._getCompactionRequestAuth(this.model);
|
||||
|
||||
const pathEntries = this.sessionManager.getBranch();
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
@@ -1684,6 +1685,7 @@ export class AgentSession {
|
||||
customInstructions,
|
||||
this._compactionAbortController.signal,
|
||||
this.thinkingLevel,
|
||||
this.agent.streamFn,
|
||||
);
|
||||
summary = result.summary;
|
||||
firstKeptEntryId = result.firstKeptEntryId;
|
||||
@@ -1771,12 +1773,12 @@ export class AgentSession {
|
||||
* @param assistantMessage The assistant message to check
|
||||
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
||||
*/
|
||||
private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {
|
||||
private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
if (!settings.enabled) return;
|
||||
if (!settings.enabled) return false;
|
||||
|
||||
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
||||
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
|
||||
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false;
|
||||
|
||||
const contextWindow = this.model?.contextWindow ?? 0;
|
||||
|
||||
@@ -1794,7 +1796,7 @@ export class AgentSession {
|
||||
const assistantIsFromBeforeCompaction =
|
||||
compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
|
||||
if (assistantIsFromBeforeCompaction) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 1: Overflow - LLM returned context overflow error
|
||||
@@ -1809,7 +1811,7 @@ export class AgentSession {
|
||||
errorMessage:
|
||||
"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this._overflowRecoveryAttempted = true;
|
||||
@@ -1819,8 +1821,7 @@ export class AgentSession {
|
||||
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
||||
this.agent.state.messages = messages.slice(0, -1);
|
||||
}
|
||||
await this._runAutoCompaction("overflow", true);
|
||||
return;
|
||||
return await this._runAutoCompaction("overflow", true);
|
||||
}
|
||||
|
||||
// Case 2: Threshold - context is getting large
|
||||
@@ -1830,7 +1831,7 @@ export class AgentSession {
|
||||
if (assistantMessage.stopReason === "error") {
|
||||
const messages = this.agent.state.messages;
|
||||
const estimate = estimateContextTokens(messages);
|
||||
if (estimate.lastUsageIndex === null) return; // No usage data at all
|
||||
if (estimate.lastUsageIndex === null) return false; // No usage data at all
|
||||
// Verify the usage source is post-compaction. Kept pre-compaction messages
|
||||
// have stale usage reflecting the old (larger) context and would falsely
|
||||
// trigger compaction right after one just finished.
|
||||
@@ -1840,21 +1841,22 @@ export class AgentSession {
|
||||
usageMsg.role === "assistant" &&
|
||||
(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
contextTokens = estimate.tokens;
|
||||
} else {
|
||||
contextTokens = calculateContextTokens(assistantMessage.usage);
|
||||
}
|
||||
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
||||
await this._runAutoCompaction("threshold", false);
|
||||
return await this._runAutoCompaction("threshold", false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Run auto-compaction with events.
|
||||
*/
|
||||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
|
||||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<boolean> {
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
|
||||
this._emit({ type: "compaction_start", reason });
|
||||
@@ -1869,21 +1871,28 @@ export class AgentSession {
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
|
||||
if (!authResult.ok || !authResult.apiKey) {
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
let apiKey: string | undefined;
|
||||
let headers: Record<string, string> | undefined;
|
||||
if (this.agent.streamFn === streamSimple) {
|
||||
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
|
||||
if (!authResult.ok || !authResult.apiKey) {
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
apiKey = authResult.apiKey;
|
||||
headers = authResult.headers;
|
||||
} else {
|
||||
({ apiKey, headers } = await this._getCompactionRequestAuth(this.model));
|
||||
}
|
||||
const { apiKey, headers } = authResult;
|
||||
|
||||
const pathEntries = this.sessionManager.getBranch();
|
||||
|
||||
@@ -1896,7 +1905,7 @@ export class AgentSession {
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let extensionCompaction: CompactionResult | undefined;
|
||||
@@ -1919,7 +1928,7 @@ export class AgentSession {
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extensionResult?.compaction) {
|
||||
@@ -1949,6 +1958,7 @@ export class AgentSession {
|
||||
undefined,
|
||||
this._autoCompactionAbortController.signal,
|
||||
this.thinkingLevel,
|
||||
this.agent.streamFn,
|
||||
);
|
||||
summary = compactResult.summary;
|
||||
firstKeptEntryId = compactResult.firstKeptEntryId;
|
||||
@@ -1964,7 +1974,7 @@ export class AgentSession {
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
||||
@@ -1999,17 +2009,12 @@ export class AgentSession {
|
||||
if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") {
|
||||
this.agent.state.messages = messages.slice(0, -1);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {});
|
||||
}, 100);
|
||||
} else if (this.agent.hasQueuedMessages()) {
|
||||
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
||||
// Kick the loop so queued messages are actually delivered.
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {});
|
||||
}, 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
||||
// Continue once so queued messages are delivered.
|
||||
return this.agent.hasQueuedMessages();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
||||
this._emit({
|
||||
@@ -2023,6 +2028,7 @@ export class AgentSession {
|
||||
? `Context overflow recovery failed: ${errorMessage}`
|
||||
: `Auto-compaction failed: ${errorMessage}`,
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
this._autoCompactionAbortController = undefined;
|
||||
}
|
||||
@@ -2047,6 +2053,9 @@ export class AgentSession {
|
||||
if (bindings.commandContextActions !== undefined) {
|
||||
this._extensionCommandContextActions = bindings.commandContextActions;
|
||||
}
|
||||
if (bindings.abortHandler !== undefined) {
|
||||
this._extensionAbortHandler = bindings.abortHandler;
|
||||
}
|
||||
if (bindings.shutdownHandler !== undefined) {
|
||||
this._extensionShutdownHandler = bindings.shutdownHandler;
|
||||
}
|
||||
@@ -2211,7 +2220,13 @@ export class AgentSession {
|
||||
getModel: () => this.model,
|
||||
isIdle: () => !this.isStreaming,
|
||||
getSignal: () => this.agent.signal,
|
||||
abort: () => this.abort(),
|
||||
abort: () => {
|
||||
if (this._extensionAbortHandler) {
|
||||
this._extensionAbortHandler();
|
||||
return;
|
||||
}
|
||||
void this.abort();
|
||||
},
|
||||
hasPendingMessages: () => this.pendingMessageCount > 0,
|
||||
shutdown: () => {
|
||||
this._extensionShutdownHandler?.();
|
||||
@@ -2427,43 +2442,27 @@ export class AgentSession {
|
||||
if (isContextOverflow(message, contextWindow)) return false;
|
||||
|
||||
const err = message.errorMessage;
|
||||
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, request ended without sending chunks, HTTP/2 closed before response, terminated, retry delay exceeded
|
||||
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(
|
||||
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded
|
||||
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle retryable errors with exponential backoff.
|
||||
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
||||
* Prepare a retryable error for continuation with exponential backoff.
|
||||
* @returns true if the caller should continue the agent, false otherwise
|
||||
*/
|
||||
private async _handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
||||
private async _prepareRetry(message: AssistantMessage): Promise<boolean> {
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled) {
|
||||
this._resolveRetry();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry promise is created synchronously in _handleAgentEvent for agent_end.
|
||||
// Keep a defensive fallback here in case a future refactor bypasses that path.
|
||||
if (!this._retryPromise) {
|
||||
this._retryPromise = new Promise((resolve) => {
|
||||
this._retryResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
this._retryAttempt++;
|
||||
|
||||
if (this._retryAttempt > settings.maxRetries) {
|
||||
// Max retries exceeded, emit final failure and reset
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt: this._retryAttempt - 1,
|
||||
finalError: message.errorMessage,
|
||||
});
|
||||
this._retryAttempt = 0;
|
||||
this._resolveRetry(); // Resolve so waitForRetry() completes
|
||||
// Preserve the completed attempt count so post-run handling can emit the final failure.
|
||||
this._retryAttempt--;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2491,24 +2490,16 @@ export class AgentSession {
|
||||
// Aborted during sleep - emit end event so UI can clean up
|
||||
const attempt = this._retryAttempt;
|
||||
this._retryAttempt = 0;
|
||||
this._retryAbortController = undefined;
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt,
|
||||
finalError: "Retry cancelled",
|
||||
});
|
||||
this._resolveRetry();
|
||||
return false;
|
||||
} finally {
|
||||
this._retryAbortController = undefined;
|
||||
}
|
||||
this._retryAbortController = undefined;
|
||||
|
||||
// Retry via continue() - use setTimeout to break out of event handler chain
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {
|
||||
// Retry failed - will be caught by next agent_end
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2518,26 +2509,11 @@ export class AgentSession {
|
||||
*/
|
||||
abortRetry(): void {
|
||||
this._retryAbortController?.abort();
|
||||
// Note: _retryAttempt is reset in the catch block of _autoRetry
|
||||
this._resolveRetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any in-progress retry to complete.
|
||||
* Returns immediately if no retry is in progress.
|
||||
*/
|
||||
private async waitForRetry(): Promise<void> {
|
||||
if (!this._retryPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._retryPromise;
|
||||
await this.agent.waitForIdle();
|
||||
}
|
||||
|
||||
/** Whether auto-retry is currently in progress */
|
||||
get isRetrying(): boolean {
|
||||
return this._retryPromise !== undefined;
|
||||
return this._retryAbortController !== undefined;
|
||||
}
|
||||
|
||||
/** Whether auto-retry is enabled */
|
||||
@@ -3026,7 +3002,10 @@ export class AgentSession {
|
||||
* @returns The resolved output file path.
|
||||
*/
|
||||
exportToJsonl(outputPath?: string): string {
|
||||
const filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`);
|
||||
const filePath = resolvePath(
|
||||
outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`,
|
||||
process.cwd(),
|
||||
);
|
||||
const dir = dirname(filePath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { join } from "node:path";
|
||||
import { getDocsPath } from "../config.js";
|
||||
import { getDocsPath } from "../config.ts";
|
||||
|
||||
const UNKNOWN_PROVIDER = "unknown";
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@earendil-w
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import lockfile from "proper-lockfile";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { resolveConfigValue } from "./resolve-config-value.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
import { normalizePath } from "../utils/paths.ts";
|
||||
import { resolveConfigValue } from "./resolve-config-value.ts";
|
||||
|
||||
export type ApiKeyCredential = {
|
||||
type: "api_key";
|
||||
@@ -50,7 +51,11 @@ export interface AuthStorageBackend {
|
||||
}
|
||||
|
||||
export class FileAuthStorageBackend implements AuthStorageBackend {
|
||||
constructor(private authPath: string = join(getAgentDir(), "auth.json")) {}
|
||||
private authPath: string;
|
||||
|
||||
constructor(authPath: string = join(getAgentDir(), "auth.json")) {
|
||||
this.authPath = normalizePath(authPath);
|
||||
}
|
||||
|
||||
private ensureParentDir(): void {
|
||||
const dir = dirname(this.authPath);
|
||||
@@ -194,8 +199,10 @@ export class AuthStorage {
|
||||
private fallbackResolver?: (provider: string) => string | undefined;
|
||||
private loadError: Error | null = null;
|
||||
private errors: Error[] = [];
|
||||
private storage: AuthStorageBackend;
|
||||
|
||||
private constructor(private storage: AuthStorageBackend) {
|
||||
private constructor(storage: AuthStorageBackend) {
|
||||
this.storage = storage;
|
||||
this.reload();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import { randomBytes } from "node:crypto";
|
||||
import { createWriteStream, type WriteStream } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { sanitizeBinaryOutput } from "../utils/shell.js";
|
||||
import type { BashOperations } from "./tools/bash.js";
|
||||
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
|
||||
import { stripAnsi } from "../utils/ansi.ts";
|
||||
import { sanitizeBinaryOutput } from "../utils/shell.ts";
|
||||
import type { BashOperations } from "./tools/bash.ts";
|
||||
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.ts";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
createBranchSummaryMessage,
|
||||
createCompactionSummaryMessage,
|
||||
createCustomMessage,
|
||||
} from "../messages.js";
|
||||
import type { ReadonlySessionManager, SessionEntry } from "../session-manager.js";
|
||||
import { estimateTokens } from "./compaction.js";
|
||||
} from "../messages.ts";
|
||||
import type { ReadonlySessionManager, SessionEntry } from "../session-manager.ts";
|
||||
import { estimateTokens } from "./compaction.ts";
|
||||
import {
|
||||
computeFileLists,
|
||||
createFileOps,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
formatFileOperations,
|
||||
SUMMARIZATION_SYSTEM_PROMPT,
|
||||
serializeConversation,
|
||||
} from "./utils.js";
|
||||
} from "./utils.ts";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -44,7 +44,7 @@ export interface BranchSummaryDetails {
|
||||
modifiedFiles: string[];
|
||||
}
|
||||
|
||||
export type { FileOperations } from "./utils.js";
|
||||
export type { FileOperations } from "./utils.ts";
|
||||
|
||||
export interface BranchPreparation {
|
||||
/** Messages extracted for summarization, in chronological order */
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
* and after compaction the session is reloaded.
|
||||
*/
|
||||
|
||||
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import type { AssistantMessage, Model, Usage } from "@earendil-works/pi-ai";
|
||||
import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai";
|
||||
import { completeSimple } from "@earendil-works/pi-ai";
|
||||
import {
|
||||
convertToLlm,
|
||||
createBranchSummaryMessage,
|
||||
createCompactionSummaryMessage,
|
||||
createCustomMessage,
|
||||
} from "../messages.js";
|
||||
import { buildSessionContext, type CompactionEntry, type SessionEntry } from "../session-manager.js";
|
||||
} from "../messages.ts";
|
||||
import { buildSessionContext, type CompactionEntry, type SessionEntry } from "../session-manager.ts";
|
||||
import {
|
||||
computeFileLists,
|
||||
createFileOps,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
formatFileOperations,
|
||||
SUMMARIZATION_SYSTEM_PROMPT,
|
||||
serializeConversation,
|
||||
} from "./utils.js";
|
||||
} from "./utils.ts";
|
||||
|
||||
// ============================================================================
|
||||
// File Operation Tracking
|
||||
@@ -523,6 +523,34 @@ Use this EXACT format:
|
||||
|
||||
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
||||
|
||||
function createSummarizationOptions(
|
||||
model: Model<any>,
|
||||
maxTokens: number,
|
||||
apiKey: string | undefined,
|
||||
headers: Record<string, string> | undefined,
|
||||
signal: AbortSignal | undefined,
|
||||
thinkingLevel: ThinkingLevel | undefined,
|
||||
): SimpleStreamOptions {
|
||||
const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers };
|
||||
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
|
||||
options.reasoning = thinkingLevel;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function completeSummarization(
|
||||
model: Model<any>,
|
||||
context: Context,
|
||||
options: SimpleStreamOptions,
|
||||
streamFn?: StreamFn,
|
||||
): Promise<AssistantMessage> {
|
||||
if (!streamFn) {
|
||||
return completeSimple(model, context, options);
|
||||
}
|
||||
const stream = await streamFn(model, context, options);
|
||||
return stream.result();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a summary of the conversation using the LLM.
|
||||
* If previousSummary is provided, uses the update prompt to merge.
|
||||
@@ -531,14 +559,18 @@ export async function generateSummary(
|
||||
currentMessages: AgentMessage[],
|
||||
model: Model<any>,
|
||||
reserveTokens: number,
|
||||
apiKey: string,
|
||||
apiKey: string | undefined,
|
||||
headers?: Record<string, string>,
|
||||
signal?: AbortSignal,
|
||||
customInstructions?: string,
|
||||
previousSummary?: string,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
streamFn?: StreamFn,
|
||||
): Promise<string> {
|
||||
const maxTokens = Math.floor(0.8 * reserveTokens);
|
||||
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;
|
||||
@@ -566,15 +598,13 @@ export async function generateSummary(
|
||||
},
|
||||
];
|
||||
|
||||
const completionOptions =
|
||||
model.reasoning && thinkingLevel && thinkingLevel !== "off"
|
||||
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
|
||||
: { maxTokens, signal, apiKey, headers };
|
||||
const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel);
|
||||
|
||||
const response = await completeSimple(
|
||||
const response = await completeSummarization(
|
||||
model,
|
||||
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
||||
completionOptions,
|
||||
streamFn,
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
@@ -717,11 +747,12 @@ Be concise. Focus on what's needed to understand the kept suffix.`;
|
||||
export async function compact(
|
||||
preparation: CompactionPreparation,
|
||||
model: Model<any>,
|
||||
apiKey: string,
|
||||
apiKey: string | undefined,
|
||||
headers?: Record<string, string>,
|
||||
customInstructions?: string,
|
||||
signal?: AbortSignal,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
streamFn?: StreamFn,
|
||||
): Promise<CompactionResult> {
|
||||
const {
|
||||
firstKeptEntryId,
|
||||
@@ -751,6 +782,7 @@ export async function compact(
|
||||
customInstructions,
|
||||
previousSummary,
|
||||
thinkingLevel,
|
||||
streamFn,
|
||||
)
|
||||
: Promise.resolve("No prior history."),
|
||||
generateTurnPrefixSummary(
|
||||
@@ -761,6 +793,7 @@ export async function compact(
|
||||
headers,
|
||||
signal,
|
||||
thinkingLevel,
|
||||
streamFn,
|
||||
),
|
||||
]);
|
||||
// Merge into single summary
|
||||
@@ -777,6 +810,7 @@ export async function compact(
|
||||
customInstructions,
|
||||
previousSummary,
|
||||
thinkingLevel,
|
||||
streamFn,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -803,12 +837,16 @@ async function generateTurnPrefixSummary(
|
||||
messages: AgentMessage[],
|
||||
model: Model<any>,
|
||||
reserveTokens: number,
|
||||
apiKey: string,
|
||||
apiKey: string | undefined,
|
||||
headers?: Record<string, string>,
|
||||
signal?: AbortSignal,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
streamFn?: StreamFn,
|
||||
): Promise<string> {
|
||||
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
||||
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}`;
|
||||
@@ -820,12 +858,11 @@ async function generateTurnPrefixSummary(
|
||||
},
|
||||
];
|
||||
|
||||
const response = await completeSimple(
|
||||
const response = await completeSummarization(
|
||||
model,
|
||||
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
||||
model.reasoning && thinkingLevel && thinkingLevel !== "off"
|
||||
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
|
||||
: { maxTokens, signal, apiKey, headers },
|
||||
createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),
|
||||
streamFn,
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
* Compaction and summarization utilities.
|
||||
*/
|
||||
|
||||
export * from "./branch-summarization.js";
|
||||
export * from "./compaction.js";
|
||||
export * from "./utils.js";
|
||||
export * from "./branch-summarization.ts";
|
||||
export * from "./compaction.ts";
|
||||
export * from "./utils.ts";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { waitForChildProcess } from "../utils/child-process.js";
|
||||
import { waitForChildProcess } from "../utils/child-process.ts";
|
||||
|
||||
/**
|
||||
* Options for executing shell commands.
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { AgentState } from "@earendil-works/pi-agent-core";
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { basename, join } from "path";
|
||||
import { APP_NAME, getExportTemplateDir } from "../../config.js";
|
||||
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ToolDefinition } from "../extensions/types.js";
|
||||
import type { SessionEntry } from "../session-manager.js";
|
||||
import { SessionManager } from "../session-manager.js";
|
||||
import { APP_NAME, getExportTemplateDir } from "../../config.ts";
|
||||
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.ts";
|
||||
import { normalizePath, resolvePath } from "../../utils/paths.ts";
|
||||
import type { ToolDefinition } from "../extensions/types.ts";
|
||||
import type { SessionEntry } from "../session-manager.ts";
|
||||
import { SessionManager } from "../session-manager.ts";
|
||||
|
||||
/**
|
||||
* Interface for rendering custom tools to HTML.
|
||||
@@ -270,7 +271,7 @@ export async function exportSessionToHtml(
|
||||
|
||||
const html = generateHtml(sessionData, opts.themeName);
|
||||
|
||||
let outputPath = opts.outputPath;
|
||||
let outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;
|
||||
if (!outputPath) {
|
||||
const sessionBasename = basename(sessionFile, ".jsonl");
|
||||
outputPath = `${APP_NAME}-session-${sessionBasename}.html`;
|
||||
@@ -286,12 +287,13 @@ export async function exportSessionToHtml(
|
||||
*/
|
||||
export async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {
|
||||
const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
|
||||
const resolvedInputPath = resolvePath(inputPath);
|
||||
|
||||
if (!existsSync(inputPath)) {
|
||||
throw new Error(`File not found: ${inputPath}`);
|
||||
if (!existsSync(resolvedInputPath)) {
|
||||
throw new Error(`File not found: ${resolvedInputPath}`);
|
||||
}
|
||||
|
||||
const sm = SessionManager.open(inputPath);
|
||||
const sm = SessionManager.open(resolvedInputPath);
|
||||
|
||||
const sessionData: SessionData = {
|
||||
header: sm.getHeader(),
|
||||
@@ -303,9 +305,9 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
|
||||
|
||||
const html = generateHtml(sessionData, opts.themeName);
|
||||
|
||||
let outputPath = opts.outputPath;
|
||||
let outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;
|
||||
if (!outputPath) {
|
||||
const inputBasename = basename(inputPath, ".jsonl");
|
||||
const inputBasename = basename(resolvedInputPath, ".jsonl");
|
||||
outputPath = `${APP_NAME}-session-${inputBasename}.html`;
|
||||
}
|
||||
|
||||
|
||||
@@ -910,7 +910,8 @@
|
||||
'</div>';
|
||||
};
|
||||
|
||||
let html = `<div class="tool-execution ${statusClass}">`;
|
||||
const toolDomId = `tool-call-${escapeHtml(call.id)}`;
|
||||
let html = `<div class="tool-execution ${statusClass}" id="${toolDomId}">`;
|
||||
const args = call.arguments || {};
|
||||
const name = call.name;
|
||||
|
||||
@@ -1445,6 +1446,16 @@
|
||||
// Cache for rendered entry DOM nodes
|
||||
const entryCache = new Map();
|
||||
|
||||
function getScrollTargetElementId(entryId) {
|
||||
const entry = byId.get(entryId);
|
||||
if (entry?.type === 'message' && entry.message.role === 'toolResult' && entry.message.toolCallId) {
|
||||
// getElementById() matches the parsed DOM id attribute, whose HTML entities
|
||||
// were already resolved from the escaped id rendered by renderToolCall().
|
||||
return `tool-call-${entry.message.toolCallId}`;
|
||||
}
|
||||
return `entry-${entryId}`;
|
||||
}
|
||||
|
||||
function renderEntryToNode(entry) {
|
||||
// Check cache first
|
||||
if (entryCache.has(entry.id)) {
|
||||
@@ -1506,9 +1517,12 @@
|
||||
if (scrollMode === 'bottom') {
|
||||
content.scrollTop = content.scrollHeight;
|
||||
} else if (scrollMode === 'target') {
|
||||
// If scrollToEntryId is provided, scroll to that specific entry
|
||||
// If scrollToEntryId is provided, scroll to that specific entry.
|
||||
// Tool result entries are rendered inside their assistant tool-call block,
|
||||
// so route them to the visible tool-call element instead.
|
||||
const scrollTargetId = scrollToEntryId || targetId;
|
||||
const targetEl = document.getElementById(`entry-${scrollTargetId}`);
|
||||
const targetEl = document.getElementById(getScrollTargetElementId(scrollTargetId)) ||
|
||||
document.getElementById(`entry-${scrollTargetId}`);
|
||||
if (targetEl) {
|
||||
targetEl.scrollIntoView({ block: 'center' });
|
||||
// Briefly highlight the target message
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
||||
import type { Component } from "@earendil-works/pi-tui";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ToolDefinition, ToolRenderContext } from "../extensions/types.js";
|
||||
import { ansiLinesToHtml } from "./ansi-to-html.js";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { ToolDefinition, ToolRenderContext } from "../extensions/types.ts";
|
||||
import { ansiLinesToHtml } from "./ansi-to-html.ts";
|
||||
|
||||
export interface ToolHtmlRendererDeps {
|
||||
/** Function to look up tool definition by name */
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* Extension system for lifecycle events and custom tools.
|
||||
*/
|
||||
|
||||
export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.js";
|
||||
export type { SourceInfo } from "../source-info.js";
|
||||
export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.ts";
|
||||
export type { SourceInfo } from "../source-info.ts";
|
||||
export {
|
||||
createExtensionRuntime,
|
||||
discoverAndLoadExtensions,
|
||||
loadExtensionFromFactory,
|
||||
loadExtensions,
|
||||
} from "./loader.js";
|
||||
} from "./loader.ts";
|
||||
export type {
|
||||
ExtensionErrorListener,
|
||||
ForkHandler,
|
||||
@@ -17,8 +17,8 @@ export type {
|
||||
NewSessionHandler,
|
||||
ShutdownHandler,
|
||||
SwitchSessionHandler,
|
||||
} from "./runner.js";
|
||||
export { ExtensionRunner } from "./runner.js";
|
||||
} from "./runner.ts";
|
||||
export { ExtensionRunner } from "./runner.ts";
|
||||
export type {
|
||||
AfterProviderResponseEvent,
|
||||
AgentEndEvent,
|
||||
@@ -156,7 +156,7 @@ export type {
|
||||
WorkingIndicatorOptions,
|
||||
WriteToolCallEvent,
|
||||
WriteToolResultEvent,
|
||||
} from "./types.js";
|
||||
} from "./types.ts";
|
||||
// Type guards
|
||||
export {
|
||||
defineTool,
|
||||
@@ -168,5 +168,5 @@ export {
|
||||
isReadToolResult,
|
||||
isToolCallEventType,
|
||||
isWriteToolResult,
|
||||
} from "./types.js";
|
||||
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.js";
|
||||
} from "./types.ts";
|
||||
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.ts";
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core";
|
||||
@@ -20,14 +19,15 @@ import { createJiti } from "jiti";
|
||||
import * as _bundledTypebox from "typebox";
|
||||
import * as _bundledTypeboxCompile from "typebox/compile";
|
||||
import * as _bundledTypeboxValue from "typebox/value";
|
||||
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
|
||||
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.ts";
|
||||
// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
|
||||
// avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
|
||||
import * as _bundledPiCodingAgent from "../../index.js";
|
||||
import { createEventBus, type EventBus } from "../event-bus.js";
|
||||
import type { ExecOptions } from "../exec.js";
|
||||
import { execCommand } from "../exec.js";
|
||||
import { createSyntheticSourceInfo } from "../source-info.js";
|
||||
import * as _bundledPiCodingAgent from "../../index.ts";
|
||||
import { resolvePath } from "../../utils/paths.ts";
|
||||
import { createEventBus, type EventBus } from "../event-bus.ts";
|
||||
import type { ExecOptions } from "../exec.ts";
|
||||
import { execCommand } from "../exec.ts";
|
||||
import { createSyntheticSourceInfo } from "../source-info.ts";
|
||||
import type {
|
||||
Extension,
|
||||
ExtensionAPI,
|
||||
@@ -38,7 +38,7 @@ import type {
|
||||
ProviderConfig,
|
||||
RegisteredCommand,
|
||||
ToolDefinition,
|
||||
} from "./types.js";
|
||||
} from "./types.ts";
|
||||
|
||||
/** Modules available to extensions via virtualModules (for compiled Bun binary) */
|
||||
const VIRTUAL_MODULES: Record<string, unknown> = {
|
||||
@@ -115,31 +115,6 @@ function getAliases(): Record<string, string> {
|
||||
return _aliases;
|
||||
}
|
||||
|
||||
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
||||
|
||||
function normalizeUnicodeSpaces(str: string): string {
|
||||
return str.replace(UNICODE_SPACES, " ");
|
||||
}
|
||||
|
||||
function expandPath(p: string): string {
|
||||
const normalized = normalizeUnicodeSpaces(p);
|
||||
if (normalized.startsWith("~/")) {
|
||||
return path.join(os.homedir(), normalized.slice(2));
|
||||
}
|
||||
if (normalized.startsWith("~")) {
|
||||
return path.join(os.homedir(), normalized.slice(1));
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolvePath(extPath: string, cwd: string): string {
|
||||
const expanded = expandPath(extPath);
|
||||
if (path.isAbsolute(expanded)) {
|
||||
return expanded;
|
||||
}
|
||||
return path.resolve(cwd, expanded);
|
||||
}
|
||||
|
||||
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
/**
|
||||
@@ -236,7 +211,7 @@ function createExtensionAPI(
|
||||
shortcut: KeyId,
|
||||
options: {
|
||||
description?: string;
|
||||
handler: (ctx: import("./types.js").ExtensionContext) => Promise<void> | void;
|
||||
handler: (ctx: import("./types.ts").ExtensionContext) => Promise<void> | void;
|
||||
},
|
||||
): void {
|
||||
runtime.assertActive();
|
||||
@@ -396,7 +371,7 @@ async function loadExtension(
|
||||
eventBus: EventBus,
|
||||
runtime: ExtensionRuntime,
|
||||
): Promise<{ extension: Extension | null; error: string | null }> {
|
||||
const resolvedPath = resolvePath(extensionPath, cwd);
|
||||
const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });
|
||||
|
||||
try {
|
||||
const factory = await loadExtensionModule(resolvedPath);
|
||||
@@ -426,7 +401,8 @@ export async function loadExtensionFromFactory(
|
||||
extensionPath = "<inline>",
|
||||
): Promise<Extension> {
|
||||
const extension = createExtension(extensionPath, extensionPath);
|
||||
const api = createExtensionAPI(extension, runtime, cwd, eventBus);
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);
|
||||
await factory(api);
|
||||
return extension;
|
||||
}
|
||||
@@ -437,11 +413,12 @@ export async function loadExtensionFromFactory(
|
||||
export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
|
||||
const extensions: Extension[] = [];
|
||||
const errors: Array<{ path: string; error: string }> = [];
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const resolvedEventBus = eventBus ?? createEventBus();
|
||||
const runtime = createExtensionRuntime();
|
||||
|
||||
for (const extPath of paths) {
|
||||
const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);
|
||||
const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, runtime);
|
||||
|
||||
if (error) {
|
||||
errors.push({ path: extPath, error });
|
||||
@@ -578,6 +555,8 @@ export async function discoverAndLoadExtensions(
|
||||
agentDir: string = getAgentDir(),
|
||||
eventBus?: EventBus,
|
||||
): Promise<LoadExtensionsResult> {
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const resolvedAgentDir = resolvePath(agentDir);
|
||||
const allPaths: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
@@ -592,16 +571,16 @@ export async function discoverAndLoadExtensions(
|
||||
};
|
||||
|
||||
// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/
|
||||
const localExtDir = path.join(cwd, CONFIG_DIR_NAME, "extensions");
|
||||
const localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, "extensions");
|
||||
addPaths(discoverExtensionsInDir(localExtDir));
|
||||
|
||||
// 2. Global extensions: agentDir/extensions/
|
||||
const globalExtDir = path.join(agentDir, "extensions");
|
||||
const globalExtDir = path.join(resolvedAgentDir, "extensions");
|
||||
addPaths(discoverExtensionsInDir(globalExtDir));
|
||||
|
||||
// 3. Explicitly configured paths
|
||||
for (const p of configuredPaths) {
|
||||
const resolved = resolvePath(p, cwd);
|
||||
const resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });
|
||||
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
||||
// Check for package.json with pi manifest or index.ts
|
||||
const entries = resolveExtensionEntries(resolved);
|
||||
@@ -617,5 +596,5 @@ export async function discoverAndLoadExtensions(
|
||||
addPaths([resolved]);
|
||||
}
|
||||
|
||||
return loadExtensions(allPaths, cwd, eventBus);
|
||||
return loadExtensions(allPaths, resolvedCwd, eventBus);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import type { ImageContent, Model } from "@earendil-works/pi-ai";
|
||||
import type { KeyId } from "@earendil-works/pi-tui";
|
||||
import { type Theme, theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ResourceDiagnostic } from "../diagnostics.js";
|
||||
import type { KeybindingsConfig } from "../keybindings.js";
|
||||
import type { ModelRegistry } from "../model-registry.js";
|
||||
import type { SessionManager } from "../session-manager.js";
|
||||
import type { BuildSystemPromptOptions } from "../system-prompt.js";
|
||||
import { type Theme, theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { ResourceDiagnostic } from "../diagnostics.ts";
|
||||
import type { KeybindingsConfig } from "../keybindings.ts";
|
||||
import type { ModelRegistry } from "../model-registry.ts";
|
||||
import type { SessionManager } from "../session-manager.ts";
|
||||
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
||||
import type {
|
||||
BeforeAgentStartEvent,
|
||||
BeforeAgentStartEventResult,
|
||||
@@ -55,7 +55,7 @@ import type {
|
||||
ToolResultEventResult,
|
||||
UserBashEvent,
|
||||
UserBashEventResult,
|
||||
} from "./types.js";
|
||||
} from "./types.ts";
|
||||
|
||||
// Extension shortcuts compete with canonical keybinding ids from keybindings.json.
|
||||
// Only editor-global shortcuts are reserved here. Picker-specific bindings are not.
|
||||
|
||||
@@ -40,27 +40,27 @@ import type {
|
||||
TUI,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import type { Static, TSchema } from "typebox";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { BashResult } from "../bash-executor.js";
|
||||
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
|
||||
import type { EventBus } from "../event-bus.js";
|
||||
import type { ExecOptions, ExecResult } from "../exec.js";
|
||||
import type { ReadonlyFooterDataProvider } from "../footer-data-provider.js";
|
||||
import type { KeybindingsManager } from "../keybindings.js";
|
||||
import type { CustomMessage } from "../messages.js";
|
||||
import type { ModelRegistry } from "../model-registry.js";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { BashResult } from "../bash-executor.ts";
|
||||
import type { CompactionPreparation, CompactionResult } from "../compaction/index.ts";
|
||||
import type { EventBus } from "../event-bus.ts";
|
||||
import type { ExecOptions, ExecResult } from "../exec.ts";
|
||||
import type { ReadonlyFooterDataProvider } from "../footer-data-provider.ts";
|
||||
import type { KeybindingsManager } from "../keybindings.ts";
|
||||
import type { CustomMessage } from "../messages.ts";
|
||||
import type { ModelRegistry } from "../model-registry.ts";
|
||||
import type {
|
||||
BranchSummaryEntry,
|
||||
CompactionEntry,
|
||||
ReadonlySessionManager,
|
||||
SessionEntry,
|
||||
SessionManager,
|
||||
} from "../session-manager.js";
|
||||
import type { SlashCommandInfo } from "../slash-commands.js";
|
||||
import type { SourceInfo } from "../source-info.js";
|
||||
import type { BuildSystemPromptOptions } from "../system-prompt.js";
|
||||
import type { BashOperations } from "../tools/bash.js";
|
||||
import type { EditToolDetails } from "../tools/edit.js";
|
||||
} from "../session-manager.ts";
|
||||
import type { SlashCommandInfo } from "../slash-commands.ts";
|
||||
import type { SourceInfo } from "../source-info.ts";
|
||||
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
||||
import type { BashOperations } from "../tools/bash.ts";
|
||||
import type { EditToolDetails } from "../tools/edit.ts";
|
||||
import type {
|
||||
BashToolDetails,
|
||||
BashToolInput,
|
||||
@@ -74,12 +74,12 @@ import type {
|
||||
ReadToolDetails,
|
||||
ReadToolInput,
|
||||
WriteToolInput,
|
||||
} from "../tools/index.js";
|
||||
} from "../tools/index.ts";
|
||||
|
||||
export type { ExecOptions, ExecResult } from "../exec.js";
|
||||
export type { BuildSystemPromptOptions } from "../system-prompt.js";
|
||||
export type { ExecOptions, ExecResult } from "../exec.ts";
|
||||
export type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
||||
export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode };
|
||||
export type { AppKeybinding, KeybindingsManager } from "../keybindings.js";
|
||||
export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts";
|
||||
|
||||
// ============================================================================
|
||||
// UI Context
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
*/
|
||||
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import { wrapToolDefinition, wrapToolDefinitions } from "../tools/tool-definition-wrapper.js";
|
||||
import type { ExtensionRunner } from "./runner.js";
|
||||
import type { RegisteredTool } from "./types.js";
|
||||
import { wrapToolDefinition, wrapToolDefinitions } from "../tools/tool-definition-wrapper.ts";
|
||||
import type { ExtensionRunner } from "./runner.ts";
|
||||
import type { RegisteredTool } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Wrap a RegisteredTool into an AgentTool.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type ExecFileException, execFile, spawnSync } from "child_process";
|
||||
import { existsSync, type FSWatcher, readFileSync, statSync, unwatchFile, watchFile } from "fs";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.js";
|
||||
import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.ts";
|
||||
|
||||
type GitPaths = {
|
||||
repoDir: string;
|
||||
|
||||
55
packages/coding-agent/src/core/http-dispatcher.ts
Normal file
55
packages/coding-agent/src/core/http-dispatcher.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as undici from "undici";
|
||||
|
||||
export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
|
||||
|
||||
export const HTTP_IDLE_TIMEOUT_CHOICES = [
|
||||
{ label: "30 sec", timeoutMs: 30_000 },
|
||||
{ label: "1 min", timeoutMs: 60_000 },
|
||||
{ label: "2 min", timeoutMs: 120_000 },
|
||||
{ label: "5 min", timeoutMs: 300_000 },
|
||||
{ label: "disabled", timeoutMs: 0 },
|
||||
] as const;
|
||||
|
||||
export function parseHttpIdleTimeoutMs(value: unknown): number | undefined {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.toLowerCase() === "disabled") {
|
||||
return 0;
|
||||
}
|
||||
if (trimmed.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return parseHttpIdleTimeoutMs(Number(trimmed));
|
||||
}
|
||||
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
export function formatHttpIdleTimeoutMs(timeoutMs: number): string {
|
||||
const choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);
|
||||
if (choice) {
|
||||
return choice.label;
|
||||
}
|
||||
return `${timeoutMs / 1000} sec`;
|
||||
}
|
||||
|
||||
export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {
|
||||
const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);
|
||||
if (normalizedTimeoutMs === undefined) {
|
||||
throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);
|
||||
}
|
||||
undici.setGlobalDispatcher(
|
||||
new undici.EnvHttpProxyAgent({
|
||||
allowH2: false,
|
||||
bodyTimeout: normalizedTimeoutMs,
|
||||
headersTimeout: normalizedTimeoutMs,
|
||||
}),
|
||||
);
|
||||
// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's
|
||||
// bundled fetch can otherwise consume compressed responses through npm undici's
|
||||
// dispatcher without decompressing them, causing response.json() failures.
|
||||
undici.install?.();
|
||||
}
|
||||
@@ -10,13 +10,13 @@ export {
|
||||
type ModelCycleResult,
|
||||
type PromptOptions,
|
||||
type SessionStats,
|
||||
} from "./agent-session.js";
|
||||
} from "./agent-session.ts";
|
||||
export {
|
||||
AgentSessionRuntime,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
type CreateAgentSessionRuntimeResult,
|
||||
createAgentSessionRuntime,
|
||||
} from "./agent-session-runtime.js";
|
||||
} from "./agent-session-runtime.ts";
|
||||
export {
|
||||
type AgentSessionRuntimeDiagnostic,
|
||||
type AgentSessionServices,
|
||||
@@ -24,10 +24,10 @@ export {
|
||||
type CreateAgentSessionServicesOptions,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionServices,
|
||||
} from "./agent-session-services.js";
|
||||
export { type BashExecutorOptions, type BashResult, executeBashWithOperations } from "./bash-executor.js";
|
||||
export type { CompactionResult } from "./compaction/index.js";
|
||||
export { createEventBus, type EventBus, type EventBusController } from "./event-bus.js";
|
||||
} from "./agent-session-services.ts";
|
||||
export { type BashExecutorOptions, type BashResult, executeBashWithOperations } from "./bash-executor.ts";
|
||||
export type { CompactionResult } from "./compaction/index.ts";
|
||||
export { createEventBus, type EventBus, type EventBusController } from "./event-bus.ts";
|
||||
// Extensions system
|
||||
export {
|
||||
type AgentEndEvent,
|
||||
@@ -73,5 +73,5 @@ export {
|
||||
type TurnEndEvent,
|
||||
type TurnStartEvent,
|
||||
type WorkingIndicatorOptions,
|
||||
} from "./extensions/index.js";
|
||||
export { createSyntheticSourceInfo } from "./source-info.js";
|
||||
} from "./extensions/index.ts";
|
||||
export { createSyntheticSourceInfo } from "./source-info.ts";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
|
||||
export interface AppKeybindings {
|
||||
"app.interrupt": true;
|
||||
|
||||
@@ -24,15 +24,16 @@ import { join } from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { Compile } from "typebox/compile";
|
||||
import type { TLocalizedValidationError } from "typebox/error";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import type { AuthStatus, AuthStorage } from "./auth-storage.js";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
import { normalizePath } from "../utils/paths.ts";
|
||||
import type { AuthStatus, AuthStorage } from "./auth-storage.ts";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.ts";
|
||||
import {
|
||||
clearConfigValueCache,
|
||||
resolveConfigValueOrThrow,
|
||||
resolveConfigValueUncached,
|
||||
resolveHeadersOrThrow,
|
||||
} from "./resolve-config-value.js";
|
||||
} from "./resolve-config-value.ts";
|
||||
|
||||
// Schema for OpenRouter routing preferences
|
||||
const PercentileCutoffsSchema = Type.Object({
|
||||
@@ -334,11 +335,12 @@ export class ModelRegistry {
|
||||
private modelRequestHeaders: Map<string, Record<string, string>> = new Map();
|
||||
private registeredProviders: Map<string, ProviderConfigInput> = new Map();
|
||||
private loadError: string | undefined = undefined;
|
||||
readonly authStorage: AuthStorage;
|
||||
private modelsJsonPath: string | undefined;
|
||||
|
||||
private constructor(
|
||||
readonly authStorage: AuthStorage,
|
||||
private modelsJsonPath: string | undefined,
|
||||
) {
|
||||
private constructor(authStorage: AuthStorage, modelsJsonPath: string | undefined) {
|
||||
this.authStorage = authStorage;
|
||||
this.modelsJsonPath = modelsJsonPath ? normalizePath(modelsJsonPath) : undefined;
|
||||
this.loadModels();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@earendil-works/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import { minimatch } from "minimatch";
|
||||
import { isValidThinkingLevel } from "../cli/args.js";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
||||
import type { ModelRegistry } from "./model-registry.js";
|
||||
import { isValidThinkingLevel } from "../cli/args.ts";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
||||
import type { ModelRegistry } from "./model-registry.ts";
|
||||
|
||||
/** Default model IDs for each known provider */
|
||||
export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ChildProcess, type ChildProcessByStdio, spawn, spawnSync } from "node:child_process";
|
||||
import type { ChildProcess, ChildProcessByStdio } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
@@ -27,12 +27,12 @@ import type { Readable } from "node:stream";
|
||||
import { globSync } from "glob";
|
||||
import ignore from "ignore";
|
||||
import { minimatch } from "minimatch";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { shouldUseWindowsShell } from "../utils/child-process.js";
|
||||
import { type GitSource, parseGitUrl } from "../utils/git.js";
|
||||
import { canonicalizePath, isLocalPath } from "../utils/paths.js";
|
||||
import { isStdoutTakenOver } from "./output-guard.js";
|
||||
import type { PackageSource, SettingsManager } from "./settings-manager.js";
|
||||
import { CONFIG_DIR_NAME } from "../config.ts";
|
||||
import { spawnProcess, spawnProcessSync } from "../utils/child-process.ts";
|
||||
import { type GitSource, parseGitUrl } from "../utils/git.ts";
|
||||
import { canonicalizePath, isLocalPath, markPathIgnoredByCloudSync, resolvePath } from "../utils/paths.ts";
|
||||
import { isStdoutTakenOver } from "./output-guard.ts";
|
||||
import type { PackageSource, SettingsManager } from "./settings-manager.ts";
|
||||
|
||||
const NETWORK_TIMEOUT_MS = 10000;
|
||||
const UPDATE_CHECK_CONCURRENCY = 4;
|
||||
@@ -763,8 +763,8 @@ export class DefaultPackageManager implements PackageManager {
|
||||
private progressCallback: ProgressCallback | undefined;
|
||||
|
||||
constructor(options: PackageManagerOptions) {
|
||||
this.cwd = options.cwd;
|
||||
this.agentDir = options.agentDir;
|
||||
this.cwd = resolvePath(options.cwd);
|
||||
this.agentDir = resolvePath(options.agentDir);
|
||||
this.settingsManager = options.settingsManager;
|
||||
}
|
||||
|
||||
@@ -778,9 +778,21 @@ export class DefaultPackageManager implements PackageManager {
|
||||
scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();
|
||||
const currentPackages = currentSettings.packages ?? [];
|
||||
const normalizedSource = this.normalizePackageSourceForSettings(source, scope);
|
||||
const exists = currentPackages.some((existing) => this.packageSourcesMatch(existing, source, scope));
|
||||
if (exists) {
|
||||
return false;
|
||||
const matchIndex = currentPackages.findIndex((existing) => this.packageSourcesMatch(existing, source, scope));
|
||||
if (matchIndex !== -1) {
|
||||
const existing = currentPackages[matchIndex];
|
||||
if (this.getPackageSourceString(existing) === normalizedSource) {
|
||||
return false;
|
||||
}
|
||||
const nextPackages = [...currentPackages];
|
||||
nextPackages[matchIndex] =
|
||||
typeof existing === "string" ? normalizedSource : { ...existing, source: normalizedSource };
|
||||
if (scope === "project") {
|
||||
this.settingsManager.setProjectPackages(nextPackages);
|
||||
} else {
|
||||
this.settingsManager.setPackages(nextPackages);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const nextPackages = [...currentPackages, normalizedSource];
|
||||
if (scope === "project") {
|
||||
@@ -1084,7 +1096,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private async shouldUpdateNpmSource(source: NpmSource, scope: InstalledSourceScope): Promise<boolean> {
|
||||
const installedPath = this.getNpmInstallPath(source, scope);
|
||||
const installedPath = this.getManagedNpmInstallPath(source, scope);
|
||||
const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined;
|
||||
if (!installedVersion) {
|
||||
return true;
|
||||
@@ -1114,13 +1126,9 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private async installNpmBatch(specs: string[], scope: InstalledSourceScope): Promise<void> {
|
||||
if (scope === "user") {
|
||||
await this.runNpmCommand(["install", "-g", ...specs]);
|
||||
return;
|
||||
}
|
||||
const installRoot = this.getNpmInstallRoot(scope, false);
|
||||
this.ensureNpmProject(installRoot);
|
||||
await this.runNpmCommand(["install", ...specs, "--prefix", installRoot]);
|
||||
await this.runNpmCommand(this.getNpmInstallArgs(specs, installRoot));
|
||||
}
|
||||
|
||||
async checkForAvailableUpdates(): Promise<PackageUpdate[]> {
|
||||
@@ -1221,13 +1229,14 @@ export class DefaultPackageManager implements PackageManager {
|
||||
};
|
||||
|
||||
if (parsed.type === "npm") {
|
||||
const installedPath = this.getNpmInstallPath(parsed, scope);
|
||||
let installedPath = this.getNpmInstallPath(parsed, scope);
|
||||
const needsInstall =
|
||||
!existsSync(installedPath) ||
|
||||
(parsed.pinned && !(await this.installedNpmMatchesPinnedVersion(parsed, installedPath)));
|
||||
if (needsInstall) {
|
||||
const installed = await installMissing();
|
||||
if (!installed) continue;
|
||||
installedPath = this.getNpmInstallPath(parsed, scope);
|
||||
}
|
||||
metadata.baseDir = installedPath;
|
||||
this.collectPackageResources(installedPath, accumulator, filter, metadata);
|
||||
@@ -1668,6 +1677,14 @@ export class DefaultPackageManager implements PackageManager {
|
||||
return { command, args };
|
||||
}
|
||||
|
||||
private getPackageManagerName(): string {
|
||||
const npmCommand = this.getNpmCommand();
|
||||
const commandParts = [npmCommand.command, ...npmCommand.args];
|
||||
const separatorIndex = commandParts.lastIndexOf("--");
|
||||
const packageManagerCommand = separatorIndex >= 0 ? commandParts[separatorIndex + 1] : npmCommand.command;
|
||||
return packageManagerCommand ? basename(packageManagerCommand).replace(/\.(cmd|exe)$/i, "") : "";
|
||||
}
|
||||
|
||||
private async runNpmCommand(args: string[], options?: { cwd?: string }): Promise<void> {
|
||||
const npmCommand = this.getNpmCommand();
|
||||
await this.runCommand(npmCommand.command, [...npmCommand.args, ...args], options);
|
||||
@@ -1686,31 +1703,44 @@ export class DefaultPackageManager implements PackageManager {
|
||||
return this.runCommandSync(npmCommand.command, [...npmCommand.args, ...args]);
|
||||
}
|
||||
|
||||
private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {
|
||||
if (scope === "user" && !temporary) {
|
||||
await this.runNpmCommand(["install", "-g", source.spec]);
|
||||
return;
|
||||
private getNpmInstallArgs(specs: string[], installRoot: string): string[] {
|
||||
const packageManagerName = this.getPackageManagerName();
|
||||
if (packageManagerName === "bun") {
|
||||
return ["install", ...specs, "--cwd", installRoot];
|
||||
}
|
||||
if (packageManagerName === "pnpm") {
|
||||
return ["install", ...specs, "--prefix", installRoot, "--config.strict-dep-builds=false"];
|
||||
}
|
||||
return ["install", ...specs, "--prefix", installRoot];
|
||||
}
|
||||
|
||||
private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {
|
||||
const installRoot = this.getNpmInstallRoot(scope, temporary);
|
||||
this.ensureNpmProject(installRoot);
|
||||
await this.runNpmCommand(["install", source.spec, "--prefix", installRoot]);
|
||||
await this.runNpmCommand(this.getNpmInstallArgs([source.spec], installRoot));
|
||||
}
|
||||
|
||||
private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> {
|
||||
if (scope === "user") {
|
||||
await this.runNpmCommand(["uninstall", "-g", source.name]);
|
||||
return;
|
||||
}
|
||||
const installRoot = this.getNpmInstallRoot(scope, false);
|
||||
if (!existsSync(installRoot)) {
|
||||
return;
|
||||
}
|
||||
if (this.getPackageManagerName() === "bun") {
|
||||
await this.runNpmCommand(["uninstall", source.name, "--cwd", installRoot]);
|
||||
return;
|
||||
}
|
||||
await this.runNpmCommand(["uninstall", source.name, "--prefix", installRoot]);
|
||||
}
|
||||
|
||||
private async installGit(source: GitSource, scope: SourceScope): Promise<void> {
|
||||
const targetDir = this.getGitInstallPath(source, scope);
|
||||
if (existsSync(targetDir)) {
|
||||
if (source.ref) {
|
||||
await this.ensureGitRef(targetDir, ["fetch", "origin", source.ref], "FETCH_HEAD");
|
||||
return;
|
||||
}
|
||||
const target = await this.getLocalGitUpdateTarget(targetDir);
|
||||
await this.ensureGitRef(targetDir, target.fetchArgs, target.ref);
|
||||
return;
|
||||
}
|
||||
const gitRoot = this.getGitInstallRoot(scope);
|
||||
@@ -1737,23 +1767,26 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
const target = await this.getLocalGitUpdateTarget(targetDir);
|
||||
await this.ensureGitRef(targetDir, target.fetchArgs, target.ref);
|
||||
}
|
||||
|
||||
private async ensureGitRef(targetDir: string, fetchArgs: string[], ref: string): Promise<void> {
|
||||
// Fetch only the ref we will reset to, avoiding unrelated branch/tag noise.
|
||||
await this.runCommand("git", target.fetchArgs, { cwd: targetDir });
|
||||
await this.runCommand("git", fetchArgs, { cwd: targetDir });
|
||||
|
||||
const localHead = await this.runCommandCapture("git", ["rev-parse", "HEAD"], {
|
||||
cwd: targetDir,
|
||||
timeoutMs: NETWORK_TIMEOUT_MS,
|
||||
});
|
||||
const refreshedTargetHead = await this.runCommandCapture("git", ["rev-parse", target.ref], {
|
||||
const targetHead = await this.runCommandCapture("git", ["rev-parse", ref], {
|
||||
cwd: targetDir,
|
||||
timeoutMs: NETWORK_TIMEOUT_MS,
|
||||
});
|
||||
if (localHead.trim() === refreshedTargetHead.trim()) {
|
||||
if (localHead.trim() === targetHead.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runCommand("git", ["reset", "--hard", target.ref], { cwd: targetDir });
|
||||
await this.runCommand("git", ["reset", "--hard", ref], { cwd: targetDir });
|
||||
|
||||
// Clean untracked files (extensions should be pristine)
|
||||
await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
|
||||
@@ -1810,6 +1843,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
if (!existsSync(installRoot)) {
|
||||
mkdirSync(installRoot, { recursive: true });
|
||||
}
|
||||
markPathIgnoredByCloudSync(installRoot);
|
||||
this.ensureGitIgnore(installRoot);
|
||||
const packageJsonPath = join(installRoot, "package.json");
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
@@ -1835,7 +1869,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
if (scope === "project") {
|
||||
return join(this.cwd, CONFIG_DIR_NAME, "npm");
|
||||
}
|
||||
return join(this.getGlobalNpmRoot(), "..");
|
||||
return join(this.agentDir, "npm");
|
||||
}
|
||||
|
||||
private getGlobalNpmRoot(): string {
|
||||
@@ -1844,8 +1878,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
if (this.globalNpmRoot && this.globalNpmRootCommandKey === commandKey) {
|
||||
return this.globalNpmRoot;
|
||||
}
|
||||
const isBunPackageManager = npmCommand.command === "bun";
|
||||
if (isBunPackageManager) {
|
||||
if (this.getPackageManagerName() === "bun") {
|
||||
const binDir = this.runNpmCommandSync(["pm", "bin", "-g"]).trim();
|
||||
this.globalNpmRoot = join(dirname(binDir), "install", "global", "node_modules");
|
||||
} else {
|
||||
@@ -1855,14 +1888,45 @@ export class DefaultPackageManager implements PackageManager {
|
||||
return this.globalNpmRoot;
|
||||
}
|
||||
|
||||
private getNpmInstallPath(source: NpmSource, scope: SourceScope): string {
|
||||
private getPnpmGlobalPackagePath(packageName: string): string | undefined {
|
||||
if (this.getPackageManagerName() !== "pnpm") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const output = this.runNpmCommandSync(["list", "-g", "--depth", "0", "--json"]);
|
||||
const entries = JSON.parse(output) as Array<{ dependencies?: Record<string, { path?: string }> }>;
|
||||
for (const entry of entries) {
|
||||
const path = entry.dependencies?.[packageName]?.path;
|
||||
if (path) return path;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getManagedNpmInstallPath(source: NpmSource, scope: SourceScope): string {
|
||||
if (scope === "temporary") {
|
||||
return join(this.getTemporaryDir("npm"), "node_modules", source.name);
|
||||
}
|
||||
if (scope === "project") {
|
||||
return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name);
|
||||
}
|
||||
return join(this.getGlobalNpmRoot(), source.name);
|
||||
return join(this.agentDir, "npm", "node_modules", source.name);
|
||||
}
|
||||
|
||||
private getLegacyGlobalNpmInstallPath(source: NpmSource): string | undefined {
|
||||
try {
|
||||
return this.getPnpmGlobalPackagePath(source.name) ?? join(this.getGlobalNpmRoot(), source.name);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getNpmInstallPath(source: NpmSource, scope: SourceScope): string {
|
||||
const managedPath = this.getManagedNpmInstallPath(source, scope);
|
||||
if (scope !== "user" || existsSync(managedPath)) {
|
||||
return managedPath;
|
||||
}
|
||||
const legacyPath = this.getLegacyGlobalNpmInstallPath(source);
|
||||
return legacyPath && existsSync(legacyPath) ? legacyPath : managedPath;
|
||||
}
|
||||
|
||||
private getGitInstallPath(source: GitSource, scope: SourceScope): string {
|
||||
@@ -1904,19 +1968,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private resolvePath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === "~") return getHomeDir();
|
||||
if (trimmed.startsWith("~/")) return join(getHomeDir(), trimmed.slice(2));
|
||||
if (trimmed.startsWith("~")) return join(getHomeDir(), trimmed.slice(1));
|
||||
return resolve(this.cwd, trimmed);
|
||||
return resolvePath(input, this.cwd, { homeDir: getHomeDir(), trim: true });
|
||||
}
|
||||
|
||||
private resolvePathFromBase(input: string, baseDir: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === "~") return getHomeDir();
|
||||
if (trimmed.startsWith("~/")) return join(getHomeDir(), trimmed.slice(2));
|
||||
if (trimmed.startsWith("~")) return join(getHomeDir(), trimmed.slice(1));
|
||||
return resolve(baseDir, trimmed);
|
||||
return resolvePath(input, baseDir, { homeDir: getHomeDir(), trim: true });
|
||||
}
|
||||
|
||||
private collectPackageResources(
|
||||
@@ -2366,11 +2422,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private spawnCommand(command: string, args: string[], options?: { cwd?: string }): ChildProcess {
|
||||
return spawn(command, args, {
|
||||
const env = getEnv();
|
||||
return spawnProcess(command, args, {
|
||||
cwd: options?.cwd,
|
||||
stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit",
|
||||
shell: shouldUseWindowsShell(command),
|
||||
env: getEnv(),
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2380,11 +2436,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
options?: { cwd?: string; env?: Record<string, string> },
|
||||
): ChildProcessByStdio<null, Readable, Readable> {
|
||||
const baseEnv = getEnv();
|
||||
return spawn(command, args, {
|
||||
const env = options?.env ? { ...baseEnv, ...options.env } : baseEnv;
|
||||
return spawnProcess(command, args, {
|
||||
cwd: options?.cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: shouldUseWindowsShell(command),
|
||||
env: options?.env ? { ...baseEnv, ...options.env } : baseEnv,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2447,11 +2503,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private runCommandSync(command: string, args: string[]): string {
|
||||
const result = spawnSync(command, args, {
|
||||
const env = getEnv();
|
||||
const result = spawnProcessSync(command, args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
shell: shouldUseWindowsShell(command),
|
||||
env: getEnv(),
|
||||
env,
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { basename, dirname, isAbsolute, join, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { basename, dirname, join, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME } from "../config.ts";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
|
||||
/**
|
||||
* Represents a prompt template loaded from a markdown file
|
||||
@@ -37,7 +37,7 @@ export function parseCommandArgs(argsString: string): string[] {
|
||||
}
|
||||
} else if (char === '"' || char === "'") {
|
||||
inQuote = char;
|
||||
} else if (char === " " || char === "\t") {
|
||||
} else if (/\s/.test(char)) {
|
||||
if (current) {
|
||||
args.push(current);
|
||||
current = "";
|
||||
@@ -185,19 +185,6 @@ export interface LoadPromptTemplatesOptions {
|
||||
includeDefaults: boolean;
|
||||
}
|
||||
|
||||
function normalizePath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === "~") return homedir();
|
||||
if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2));
|
||||
if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1));
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function resolvePromptPath(p: string, cwd: string): string {
|
||||
const normalized = normalizePath(p);
|
||||
return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all prompt templates from:
|
||||
* 1. Global: agentDir/prompts/
|
||||
@@ -205,14 +192,14 @@ function resolvePromptPath(p: string, cwd: string): string {
|
||||
* 3. Explicit prompt paths
|
||||
*/
|
||||
export function loadPromptTemplates(options: LoadPromptTemplatesOptions): PromptTemplate[] {
|
||||
const resolvedCwd = options.cwd;
|
||||
const resolvedAgentDir = options.agentDir;
|
||||
const resolvedCwd = resolvePath(options.cwd);
|
||||
const resolvedAgentDir = resolvePath(options.agentDir);
|
||||
const promptPaths = options.promptPaths;
|
||||
const includeDefaults = options.includeDefaults;
|
||||
|
||||
const templates: PromptTemplate[] = [];
|
||||
|
||||
const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
|
||||
const globalPromptsDir = join(resolvedAgentDir, "prompts");
|
||||
const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
|
||||
|
||||
const isUnderPath = (target: string, root: string): boolean => {
|
||||
@@ -252,7 +239,7 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions): Prompt
|
||||
|
||||
// 3. Load explicit prompt paths
|
||||
for (const rawPath of promptPaths) {
|
||||
const resolvedPath = resolvePromptPath(rawPath, resolvedCwd);
|
||||
const resolvedPath = resolvePath(rawPath, resolvedCwd, { trim: true });
|
||||
if (!existsSync(resolvedPath)) {
|
||||
continue;
|
||||
}
|
||||
@@ -282,9 +269,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions): Prompt
|
||||
export function expandPromptTemplate(text: string, templates: PromptTemplate[]): string {
|
||||
if (!text.startsWith("/")) return text;
|
||||
|
||||
const spaceIndex = text.indexOf(" ");
|
||||
const templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
||||
const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
|
||||
const match = text.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/);
|
||||
if (!match) return text;
|
||||
|
||||
const templateName = match[1];
|
||||
const argsString = match[2] ?? "";
|
||||
|
||||
const template = templates.find((t) => t.name === templateName);
|
||||
if (template) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { execSync, spawnSync } from "child_process";
|
||||
import { getShellConfig } from "../utils/shell.js";
|
||||
import { getShellConfig } from "../utils/shell.ts";
|
||||
|
||||
// Cache for shell command results (persists for process lifetime)
|
||||
const commandResultCache = new Map<string, string | undefined>();
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve, sep } from "node:path";
|
||||
import chalk from "chalk";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.js";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||
import { CONFIG_DIR_NAME } from "../config.ts";
|
||||
import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.ts";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.ts";
|
||||
|
||||
export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js";
|
||||
export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.ts";
|
||||
|
||||
import { canonicalizePath, isLocalPath } from "../utils/paths.js";
|
||||
import { createEventBus, type EventBus } from "./event-bus.js";
|
||||
import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.js";
|
||||
import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.js";
|
||||
import { DefaultPackageManager, type PathMetadata } from "./package-manager.js";
|
||||
import type { PromptTemplate } from "./prompt-templates.js";
|
||||
import { loadPromptTemplates } from "./prompt-templates.js";
|
||||
import { SettingsManager } from "./settings-manager.js";
|
||||
import type { Skill } from "./skills.js";
|
||||
import { loadSkills } from "./skills.js";
|
||||
import { createSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.ts";
|
||||
import { createEventBus, type EventBus } from "./event-bus.ts";
|
||||
import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.ts";
|
||||
import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.ts";
|
||||
import { DefaultPackageManager, type PathMetadata } from "./package-manager.ts";
|
||||
import type { PromptTemplate } from "./prompt-templates.ts";
|
||||
import { loadPromptTemplates } from "./prompt-templates.ts";
|
||||
import { SettingsManager } from "./settings-manager.ts";
|
||||
import type { Skill } from "./skills.ts";
|
||||
import { loadSkills } from "./skills.ts";
|
||||
import { createSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
|
||||
export interface ResourceExtensionPaths {
|
||||
skillPaths?: Array<{ path: string; metadata: PathMetadata }>;
|
||||
@@ -77,8 +76,8 @@ export function loadProjectContextFiles(options: {
|
||||
cwd: string;
|
||||
agentDir: string;
|
||||
}): Array<{ path: string; content: string }> {
|
||||
const resolvedCwd = options.cwd;
|
||||
const resolvedAgentDir = options.agentDir;
|
||||
const resolvedCwd = resolvePath(options.cwd);
|
||||
const resolvedAgentDir = resolvePath(options.agentDir);
|
||||
|
||||
const contextFiles: Array<{ path: string; content: string }> = [];
|
||||
const seenPaths = new Set<string>();
|
||||
@@ -205,8 +204,8 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
private lastThemePaths: string[];
|
||||
|
||||
constructor(options: DefaultResourceLoaderOptions) {
|
||||
this.cwd = options.cwd;
|
||||
this.agentDir = options.agentDir;
|
||||
this.cwd = resolvePath(options.cwd);
|
||||
this.agentDir = resolvePath(options.agentDir);
|
||||
this.settingsManager = options.settingsManager ?? SettingsManager.create(this.cwd, this.agentDir);
|
||||
this.eventBus = options.eventBus ?? createEventBus();
|
||||
this.packageManager = new DefaultPackageManager({
|
||||
@@ -409,8 +408,11 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
|
||||
for (const p of this.additionalExtensionPaths) {
|
||||
if (isLocalPath(p) && !existsSync(p)) {
|
||||
extensionsResult.errors.push({ path: p, error: `Extension path does not exist: ${p}` });
|
||||
if (isLocalPath(p)) {
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved)) {
|
||||
extensionsResult.errors.push({ path: resolved, error: `Extension path does not exist: ${resolved}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.extensionsResult = this.extensionsOverride ? this.extensionsOverride(extensionsResult) : extensionsResult;
|
||||
@@ -423,8 +425,11 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.lastSkillPaths = skillPaths;
|
||||
this.updateSkillsFromPaths(skillPaths, metadataByPath);
|
||||
for (const p of this.additionalSkillPaths) {
|
||||
if (isLocalPath(p) && !existsSync(p) && !this.skillDiagnostics.some((d) => d.path === p)) {
|
||||
this.skillDiagnostics.push({ type: "error", message: "Skill path does not exist", path: p });
|
||||
if (isLocalPath(p)) {
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved) && !this.skillDiagnostics.some((d) => d.path === resolved)) {
|
||||
this.skillDiagnostics.push({ type: "error", message: "Skill path does not exist", path: resolved });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,8 +440,15 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.lastPromptPaths = promptPaths;
|
||||
this.updatePromptsFromPaths(promptPaths, metadataByPath);
|
||||
for (const p of this.additionalPromptTemplatePaths) {
|
||||
if (isLocalPath(p) && !existsSync(p) && !this.promptDiagnostics.some((d) => d.path === p)) {
|
||||
this.promptDiagnostics.push({ type: "error", message: "Prompt template path does not exist", path: p });
|
||||
if (isLocalPath(p)) {
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved) && !this.promptDiagnostics.some((d) => d.path === resolved)) {
|
||||
this.promptDiagnostics.push({
|
||||
type: "error",
|
||||
message: "Prompt template path does not exist",
|
||||
path: resolved,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,8 +459,9 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.lastThemePaths = themePaths;
|
||||
this.updateThemesFromPaths(themePaths, metadataByPath);
|
||||
for (const p of this.additionalThemePaths) {
|
||||
if (!existsSync(p) && !this.themeDiagnostics.some((d) => d.path === p)) {
|
||||
this.themeDiagnostics.push({ type: "error", message: "Theme path does not exist", path: p });
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved) && !this.themeDiagnostics.some((d) => d.path === resolved)) {
|
||||
this.themeDiagnostics.push({ type: "error", message: "Theme path does not exist", path: resolved });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,10 +491,15 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
private normalizeExtensionPaths(
|
||||
entries: Array<{ path: string; metadata: PathMetadata }>,
|
||||
): Array<{ path: string; metadata: PathMetadata }> {
|
||||
return entries.map((entry) => ({
|
||||
path: this.resolveResourcePath(entry.path),
|
||||
metadata: entry.metadata,
|
||||
}));
|
||||
return entries.map((entry) => {
|
||||
const metadata = entry.metadata.baseDir
|
||||
? { ...entry.metadata, baseDir: this.resolveResourcePath(entry.metadata.baseDir) }
|
||||
: entry.metadata;
|
||||
return {
|
||||
path: this.resolveResourcePath(entry.path),
|
||||
metadata,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private updateSkillsFromPaths(skillPaths: string[], metadataByPath?: Map<string, PathMetadata>): void {
|
||||
@@ -674,16 +692,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
|
||||
private resolveResourcePath(p: string): string {
|
||||
const trimmed = p.trim();
|
||||
let expanded = trimmed;
|
||||
if (trimmed === "~") {
|
||||
expanded = homedir();
|
||||
} else if (trimmed.startsWith("~/")) {
|
||||
expanded = join(homedir(), trimmed.slice(2));
|
||||
} else if (trimmed.startsWith("~")) {
|
||||
expanded = join(homedir(), trimmed.slice(1));
|
||||
}
|
||||
return resolve(this.cwd, expanded);
|
||||
return resolvePath(p, this.cwd, { trim: true });
|
||||
}
|
||||
|
||||
private loadThemes(
|
||||
@@ -704,7 +713,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
|
||||
for (const p of paths) {
|
||||
const resolved = resolve(this.cwd, p);
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved)) {
|
||||
diagnostics.push({ type: "warning", message: "theme path does not exist", path: resolved });
|
||||
continue;
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { join } from "node:path";
|
||||
import { Agent, type AgentMessage, type ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { AgentSession } from "./agent-session.js";
|
||||
import { formatNoModelsAvailableMessage } from "./auth-guidance.js";
|
||||
import { AuthStorage } from "./auth-storage.js";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
||||
import type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.js";
|
||||
import { convertToLlm } from "./messages.js";
|
||||
import { ModelRegistry } from "./model-registry.js";
|
||||
import { findInitialModel } from "./model-resolver.js";
|
||||
import type { ResourceLoader } from "./resource-loader.js";
|
||||
import { DefaultResourceLoader } from "./resource-loader.js";
|
||||
import { getDefaultSessionDir, SessionManager } from "./session-manager.js";
|
||||
import { SettingsManager } from "./settings-manager.js";
|
||||
import { isInstallTelemetryEnabled } from "./telemetry.js";
|
||||
import { time } from "./timings.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { AgentSession } from "./agent-session.ts";
|
||||
import { formatNoModelsAvailableMessage } from "./auth-guidance.ts";
|
||||
import { AuthStorage } from "./auth-storage.ts";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
||||
import type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
|
||||
import { convertToLlm } from "./messages.ts";
|
||||
import { ModelRegistry } from "./model-registry.ts";
|
||||
import { findInitialModel } from "./model-resolver.ts";
|
||||
import type { ResourceLoader } from "./resource-loader.ts";
|
||||
import { DefaultResourceLoader } from "./resource-loader.ts";
|
||||
import { getDefaultSessionDir, SessionManager } from "./session-manager.ts";
|
||||
import { SettingsManager } from "./settings-manager.ts";
|
||||
import { isInstallTelemetryEnabled } from "./telemetry.ts";
|
||||
import { time } from "./timings.ts";
|
||||
import {
|
||||
createBashTool,
|
||||
createCodingTools,
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
createWriteTool,
|
||||
type ToolName,
|
||||
withFileMutationQueue,
|
||||
} from "./tools/index.js";
|
||||
} from "./tools/index.ts";
|
||||
|
||||
export interface CreateAgentSessionOptions {
|
||||
/** Working directory for project-local discovery. Default: process.cwd() */
|
||||
@@ -91,7 +92,7 @@ export interface CreateAgentSessionResult {
|
||||
|
||||
// Re-exports
|
||||
|
||||
export * from "./agent-session-runtime.js";
|
||||
export * from "./agent-session-runtime.ts";
|
||||
export type {
|
||||
ExtensionAPI,
|
||||
ExtensionCommandContext,
|
||||
@@ -100,10 +101,10 @@ export type {
|
||||
SlashCommandInfo,
|
||||
SlashCommandSource,
|
||||
ToolDefinition,
|
||||
} from "./extensions/index.js";
|
||||
export type { PromptTemplate } from "./prompt-templates.js";
|
||||
export type { Skill } from "./skills.js";
|
||||
export type { Tool } from "./tools/index.js";
|
||||
} from "./extensions/index.ts";
|
||||
export type { PromptTemplate } from "./prompt-templates.ts";
|
||||
export type { Skill } from "./skills.ts";
|
||||
export type { Tool } from "./tools/index.ts";
|
||||
|
||||
export {
|
||||
withFileMutationQueue,
|
||||
@@ -184,15 +185,15 @@ function getAttributionHeaders(
|
||||
* await loader.reload();
|
||||
* const { session } = await createAgentSession({
|
||||
* model: myModel,
|
||||
* tools: [readTool, bashTool],
|
||||
* tools: ["read", "bash"],
|
||||
* resourceLoader: loader,
|
||||
* sessionManager: SessionManager.inMemory(),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
|
||||
const cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();
|
||||
const agentDir = options.agentDir ?? getDefaultAgentDir();
|
||||
const cwd = resolvePath(options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd());
|
||||
const agentDir = options.agentDir ? resolvePath(options.agentDir) : getDefaultAgentDir();
|
||||
let resourceLoader = options.resourceLoader;
|
||||
|
||||
// Use provided or create AuthStorage and ModelRegistry
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import { type AgentMessage, uuidv7 } from "@earendil-works/pi-agent-core";
|
||||
import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
@@ -15,15 +15,15 @@ import {
|
||||
} from "fs";
|
||||
import { readdir, readFile, stat } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
|
||||
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.ts";
|
||||
import { normalizePath, resolvePath } from "../utils/paths.ts";
|
||||
import {
|
||||
type BashExecutionMessage,
|
||||
type CustomMessage,
|
||||
createBranchSummaryMessage,
|
||||
createCompactionSummaryMessage,
|
||||
createCustomMessage,
|
||||
} from "./messages.js";
|
||||
} from "./messages.ts";
|
||||
|
||||
export const CURRENT_SESSION_VERSION = 3;
|
||||
|
||||
@@ -426,8 +426,10 @@ export function buildSessionContext(
|
||||
* Encodes cwd into a safe directory name under ~/.pi/agent/sessions/.
|
||||
*/
|
||||
export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultAgentDir()): string {
|
||||
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
||||
const sessionDir = join(agentDir, "sessions", safePath);
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const resolvedAgentDir = resolvePath(agentDir);
|
||||
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
||||
const sessionDir = join(resolvedAgentDir, "sessions", safePath);
|
||||
if (!existsSync(sessionDir)) {
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
}
|
||||
@@ -436,9 +438,10 @@ export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultA
|
||||
|
||||
/** Exported for testing */
|
||||
export function loadEntriesFromFile(filePath: string): FileEntry[] {
|
||||
if (!existsSync(filePath)) return [];
|
||||
const resolvedFilePath = normalizePath(filePath);
|
||||
if (!existsSync(resolvedFilePath)) return [];
|
||||
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
const content = readFileSync(resolvedFilePath, "utf8");
|
||||
const entries: FileEntry[] = [];
|
||||
const lines = content.trim().split("\n");
|
||||
|
||||
@@ -479,10 +482,11 @@ function isValidSessionFile(filePath: string): boolean {
|
||||
|
||||
/** Exported for testing */
|
||||
export function findMostRecentSession(sessionDir: string): string | null {
|
||||
const resolvedSessionDir = normalizePath(sessionDir);
|
||||
try {
|
||||
const files = readdirSync(sessionDir)
|
||||
const files = readdirSync(resolvedSessionDir)
|
||||
.filter((f) => f.endsWith(".jsonl"))
|
||||
.map((f) => join(sessionDir, f))
|
||||
.map((f) => join(resolvedSessionDir, f))
|
||||
.filter(isValidSessionFile)
|
||||
.map((path) => ({ path, mtime: statSync(path).mtime }))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
@@ -618,6 +622,48 @@ async function buildSessionInfo(filePath: string): Promise<SessionInfo | null> {
|
||||
|
||||
export type SessionListProgress = (loaded: number, total: number) => void;
|
||||
|
||||
const MAX_CONCURRENT_SESSION_INFO_LOADS = 10;
|
||||
|
||||
async function buildSessionInfosWithConcurrency(
|
||||
files: string[],
|
||||
onLoaded: () => void,
|
||||
): Promise<(SessionInfo | null)[]> {
|
||||
const results: (SessionInfo | null)[] = new Array(files.length).fill(null);
|
||||
const inFlight = new Set<Promise<void>>();
|
||||
let nextIndex = 0;
|
||||
|
||||
const startNext = (): void => {
|
||||
const index = nextIndex++;
|
||||
const file = files[index];
|
||||
if (!file) return;
|
||||
|
||||
let task: Promise<void>;
|
||||
task = buildSessionInfo(file)
|
||||
.then((info) => {
|
||||
results[index] = info;
|
||||
})
|
||||
.catch(() => {
|
||||
results[index] = null;
|
||||
})
|
||||
.finally(() => {
|
||||
inFlight.delete(task);
|
||||
onLoaded();
|
||||
});
|
||||
inFlight.add(task);
|
||||
};
|
||||
|
||||
while (nextIndex < files.length || inFlight.size > 0) {
|
||||
while (nextIndex < files.length && inFlight.size < MAX_CONCURRENT_SESSION_INFO_LOADS) {
|
||||
startNext();
|
||||
}
|
||||
if (inFlight.size > 0) {
|
||||
await Promise.race(inFlight);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function listSessionsFromDir(
|
||||
dir: string,
|
||||
onProgress?: SessionListProgress,
|
||||
@@ -635,14 +681,10 @@ async function listSessionsFromDir(
|
||||
const total = progressTotal ?? files.length;
|
||||
|
||||
let loaded = 0;
|
||||
const results = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const info = await buildSessionInfo(file);
|
||||
loaded++;
|
||||
onProgress?.(progressOffset + loaded, total);
|
||||
return info;
|
||||
}),
|
||||
);
|
||||
const results = await buildSessionInfosWithConcurrency(files, () => {
|
||||
loaded++;
|
||||
onProgress?.(progressOffset + loaded, total);
|
||||
});
|
||||
for (const info of results) {
|
||||
if (info) {
|
||||
sessions.push(info);
|
||||
@@ -680,11 +722,11 @@ export class SessionManager {
|
||||
private leafId: string | null = null;
|
||||
|
||||
private constructor(cwd: string, sessionDir: string, sessionFile: string | undefined, persist: boolean) {
|
||||
this.cwd = cwd;
|
||||
this.sessionDir = sessionDir;
|
||||
this.cwd = resolvePath(cwd);
|
||||
this.sessionDir = normalizePath(sessionDir);
|
||||
this.persist = persist;
|
||||
if (persist && sessionDir && !existsSync(sessionDir)) {
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
if (persist && this.sessionDir && !existsSync(this.sessionDir)) {
|
||||
mkdirSync(this.sessionDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (sessionFile) {
|
||||
@@ -696,7 +738,7 @@ export class SessionManager {
|
||||
|
||||
/** Switch to a different session file (used for resume and branching) */
|
||||
setSessionFile(sessionFile: string): void {
|
||||
this.sessionFile = resolve(sessionFile);
|
||||
this.sessionFile = resolvePath(sessionFile);
|
||||
if (existsSync(this.sessionFile)) {
|
||||
this.fileEntries = loadEntriesFromFile(this.sessionFile);
|
||||
|
||||
@@ -1267,7 +1309,7 @@ export class SessionManager {
|
||||
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
|
||||
*/
|
||||
static create(cwd: string, sessionDir?: string): SessionManager {
|
||||
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
||||
return new SessionManager(cwd, dir, undefined, true);
|
||||
}
|
||||
|
||||
@@ -1278,13 +1320,14 @@ export class SessionManager {
|
||||
* @param cwdOverride Optional cwd override instead of the session header cwd.
|
||||
*/
|
||||
static open(path: string, sessionDir?: string, cwdOverride?: string): SessionManager {
|
||||
const resolvedPath = resolvePath(path);
|
||||
// Extract cwd from session header if possible, otherwise use process.cwd()
|
||||
const entries = loadEntriesFromFile(path);
|
||||
const entries = loadEntriesFromFile(resolvedPath);
|
||||
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
|
||||
const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
|
||||
// If no sessionDir provided, derive from file's parent directory
|
||||
const dir = sessionDir ?? resolve(path, "..");
|
||||
return new SessionManager(cwd, dir, path, true);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
|
||||
return new SessionManager(cwd, dir, resolvedPath, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1293,7 +1336,7 @@ export class SessionManager {
|
||||
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
|
||||
*/
|
||||
static continueRecent(cwd: string, sessionDir?: string): SessionManager {
|
||||
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
||||
const mostRecent = findMostRecentSession(dir);
|
||||
if (mostRecent) {
|
||||
return new SessionManager(cwd, dir, mostRecent, true);
|
||||
@@ -1314,17 +1357,19 @@ export class SessionManager {
|
||||
* @param sessionDir Optional session directory. If omitted, uses default for targetCwd.
|
||||
*/
|
||||
static forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string): SessionManager {
|
||||
const sourceEntries = loadEntriesFromFile(sourcePath);
|
||||
const resolvedSourcePath = resolvePath(sourcePath);
|
||||
const resolvedTargetCwd = resolvePath(targetCwd);
|
||||
const sourceEntries = loadEntriesFromFile(resolvedSourcePath);
|
||||
if (sourceEntries.length === 0) {
|
||||
throw new Error(`Cannot fork: source session file is empty or invalid: ${sourcePath}`);
|
||||
throw new Error(`Cannot fork: source session file is empty or invalid: ${resolvedSourcePath}`);
|
||||
}
|
||||
|
||||
const sourceHeader = sourceEntries.find((e) => e.type === "session") as SessionHeader | undefined;
|
||||
if (!sourceHeader) {
|
||||
throw new Error(`Cannot fork: source session has no header: ${sourcePath}`);
|
||||
throw new Error(`Cannot fork: source session has no header: ${resolvedSourcePath}`);
|
||||
}
|
||||
|
||||
const dir = sessionDir ?? getDefaultSessionDir(targetCwd);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(resolvedTargetCwd);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
@@ -1341,8 +1386,8 @@ export class SessionManager {
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: newSessionId,
|
||||
timestamp,
|
||||
cwd: targetCwd,
|
||||
parentSession: sourcePath,
|
||||
cwd: resolvedTargetCwd,
|
||||
parentSession: resolvedSourcePath,
|
||||
};
|
||||
appendFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`);
|
||||
|
||||
@@ -1353,7 +1398,7 @@ export class SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
return new SessionManager(targetCwd, dir, newSessionFile, true);
|
||||
return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1363,7 +1408,7 @@ export class SessionManager {
|
||||
* @param onProgress Optional callback for progress updates (loaded, total)
|
||||
*/
|
||||
static async list(cwd: string, sessionDir?: string, onProgress?: SessionListProgress): Promise<SessionInfo[]> {
|
||||
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
||||
const sessions = await listSessionsFromDir(dir, onProgress);
|
||||
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
return sessions;
|
||||
@@ -1401,14 +1446,10 @@ export class SessionManager {
|
||||
const sessions: SessionInfo[] = [];
|
||||
const allFiles = dirFiles.flat();
|
||||
|
||||
const results = await Promise.all(
|
||||
allFiles.map(async (file) => {
|
||||
const info = await buildSessionInfo(file);
|
||||
loaded++;
|
||||
onProgress?.(loaded, totalFiles);
|
||||
return info;
|
||||
}),
|
||||
);
|
||||
const results = await buildSessionInfosWithConcurrency(allFiles, () => {
|
||||
loaded++;
|
||||
onProgress?.(loaded, totalFiles);
|
||||
});
|
||||
|
||||
for (const info of results) {
|
||||
if (info) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Transport } from "@earendil-works/pi-ai";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { dirname, join } from "path";
|
||||
import lockfile from "proper-lockfile";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts";
|
||||
import { normalizePath, resolvePath } from "../utils/paths.ts";
|
||||
import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.ts";
|
||||
|
||||
export interface CompactionSettings {
|
||||
enabled?: boolean; // default: true
|
||||
@@ -110,6 +111,7 @@ export interface Settings {
|
||||
markdown?: MarkdownSettings;
|
||||
warnings?: WarningSettings;
|
||||
sessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)
|
||||
httpIdleTimeoutMs?: number; // HTTP header/body idle timeout in milliseconds; 0 disables it
|
||||
}
|
||||
|
||||
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
||||
@@ -159,8 +161,10 @@ export class FileSettingsStorage implements SettingsStorage {
|
||||
private projectSettingsPath: string;
|
||||
|
||||
constructor(cwd: string, agentDir: string) {
|
||||
this.globalSettingsPath = join(agentDir, "settings.json");
|
||||
this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json");
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const resolvedAgentDir = resolvePath(agentDir);
|
||||
this.globalSettingsPath = join(resolvedAgentDir, "settings.json");
|
||||
this.projectSettingsPath = join(resolvedCwd, CONFIG_DIR_NAME, "settings.json");
|
||||
}
|
||||
|
||||
private acquireLockSyncWithRetry(path: string): () => void {
|
||||
@@ -575,16 +579,7 @@ export class SettingsManager {
|
||||
|
||||
getSessionDir(): string | undefined {
|
||||
const sessionDir = this.settings.sessionDir;
|
||||
if (!sessionDir) {
|
||||
return sessionDir;
|
||||
}
|
||||
if (sessionDir === "~") {
|
||||
return homedir();
|
||||
}
|
||||
if (sessionDir.startsWith("~/")) {
|
||||
return join(homedir(), sessionDir.slice(2));
|
||||
}
|
||||
return sessionDir;
|
||||
return sessionDir ? normalizePath(sessionDir) : sessionDir;
|
||||
}
|
||||
|
||||
getDefaultProvider(): string | undefined {
|
||||
@@ -726,6 +721,27 @@ export class SettingsManager {
|
||||
};
|
||||
}
|
||||
|
||||
getHttpIdleTimeoutMs(): number {
|
||||
const value = this.settings.httpIdleTimeoutMs;
|
||||
const timeoutMs = parseHttpIdleTimeoutMs(value);
|
||||
if (timeoutMs !== undefined) {
|
||||
return timeoutMs;
|
||||
}
|
||||
if (value !== undefined) {
|
||||
throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(value)}`);
|
||||
}
|
||||
return DEFAULT_HTTP_IDLE_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
setHttpIdleTimeoutMs(timeoutMs: number): void {
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
||||
throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(timeoutMs)}`);
|
||||
}
|
||||
this.globalSettings.httpIdleTimeoutMs = Math.floor(timeoutMs);
|
||||
this.markModified("httpIdleTimeoutMs");
|
||||
this.save();
|
||||
}
|
||||
|
||||
getProviderRetrySettings(): { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs: number } {
|
||||
return {
|
||||
timeoutMs: this.settings.retry?.provider?.timeoutMs,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
||||
import ignore from "ignore";
|
||||
import { homedir } from "os";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import { canonicalizePath } from "../utils/paths.js";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { basename, dirname, join, relative, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.ts";
|
||||
import { canonicalizePath, resolvePath } from "../utils/paths.ts";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.ts";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
|
||||
/** Max name length per spec */
|
||||
const MAX_NAME_LENGTH = 64;
|
||||
@@ -90,13 +89,9 @@ export interface LoadSkillsResult {
|
||||
* Validate skill name per Agent Skills spec.
|
||||
* Returns array of validation error messages (empty if valid).
|
||||
*/
|
||||
function validateName(name: string, parentDirName: string): string[] {
|
||||
function validateName(name: string): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (name !== parentDirName) {
|
||||
errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
|
||||
}
|
||||
|
||||
if (name.length > MAX_NAME_LENGTH) {
|
||||
errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
|
||||
}
|
||||
@@ -301,7 +296,7 @@ function loadSkillFromFile(
|
||||
const name = frontmatter.name || parentDirName;
|
||||
|
||||
// Validate name
|
||||
const nameErrors = validateName(name, parentDirName);
|
||||
const nameErrors = validateName(name);
|
||||
for (const error of nameErrors) {
|
||||
diagnostics.push({ type: "warning", message: error, path: filePath });
|
||||
}
|
||||
@@ -385,28 +380,16 @@ export interface LoadSkillsOptions {
|
||||
includeDefaults: boolean;
|
||||
}
|
||||
|
||||
function normalizePath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === "~") return homedir();
|
||||
if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2));
|
||||
if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1));
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function resolveSkillPath(p: string, cwd: string): string {
|
||||
const normalized = normalizePath(p);
|
||||
return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from all configured locations.
|
||||
* Returns skills and any validation diagnostics.
|
||||
*/
|
||||
export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
|
||||
const { cwd, agentDir, skillPaths, includeDefaults } = options;
|
||||
const { agentDir, skillPaths, includeDefaults } = options;
|
||||
|
||||
// Resolve agentDir - if not provided, use default from config
|
||||
const resolvedAgentDir = agentDir ?? getAgentDir();
|
||||
const resolvedCwd = resolvePath(options.cwd);
|
||||
const resolvedAgentDir = resolvePath(agentDir ?? getAgentDir());
|
||||
|
||||
const skillMap = new Map<string, Skill>();
|
||||
const realPathSet = new Set<string>();
|
||||
@@ -446,11 +429,11 @@ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
|
||||
|
||||
if (includeDefaults) {
|
||||
addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true));
|
||||
addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", true));
|
||||
addSkills(loadSkillsFromDirInternal(resolve(resolvedCwd, CONFIG_DIR_NAME, "skills"), "project", true));
|
||||
}
|
||||
|
||||
const userSkillsDir = join(resolvedAgentDir, "skills");
|
||||
const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
|
||||
const projectSkillsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "skills");
|
||||
|
||||
const isUnderPath = (target: string, root: string): boolean => {
|
||||
const normalizedRoot = resolve(root);
|
||||
@@ -470,7 +453,7 @@ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
|
||||
};
|
||||
|
||||
for (const rawPath of skillPaths) {
|
||||
const resolvedPath = resolveSkillPath(rawPath, cwd);
|
||||
const resolvedPath = resolvePath(rawPath, resolvedCwd, { trim: true });
|
||||
if (!existsSync(resolvedPath)) {
|
||||
allDiagnostics.push({ type: "warning", message: "skill path does not exist", path: resolvedPath });
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { APP_NAME } from "../config.js";
|
||||
import type { SourceInfo } from "./source-info.js";
|
||||
import { APP_NAME } from "../config.ts";
|
||||
import type { SourceInfo } from "./source-info.ts";
|
||||
|
||||
export type SlashCommandSource = "extension" | "prompt" | "skill";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PathMetadata } from "./package-manager.js";
|
||||
import type { PathMetadata } from "./package-manager.ts";
|
||||
|
||||
export type SourceScope = "user" | "project" | "temporary";
|
||||
export type SourceOrigin = "package" | "top-level";
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* System prompt construction and project context loading
|
||||
*/
|
||||
|
||||
import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
|
||||
import { formatSkillsForPrompt, type Skill } from "./skills.js";
|
||||
import { getDocsPath, getExamplesPath, getReadmePath } from "../config.ts";
|
||||
import { formatSkillsForPrompt, type Skill } from "./skills.ts";
|
||||
|
||||
export interface BuildSystemPromptOptions {
|
||||
/** Custom system prompt (replaces default). */
|
||||
@@ -59,11 +59,12 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
|
||||
|
||||
// Append project context files
|
||||
if (contextFiles.length > 0) {
|
||||
prompt += "\n\n# Project Context\n\n";
|
||||
prompt += "\n\n<project_context>\n\n";
|
||||
prompt += "Project-specific instructions and guidelines:\n\n";
|
||||
for (const { path: filePath, content } of contextFiles) {
|
||||
prompt += `## ${filePath}\n\n${content}\n\n`;
|
||||
prompt += `<project_instructions path="${filePath}">\n${content}\n</project_instructions>\n\n`;
|
||||
}
|
||||
prompt += "</project_context>\n";
|
||||
}
|
||||
|
||||
// Append skills section (only if read tool is available)
|
||||
@@ -142,6 +143,7 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi
|
||||
- Main documentation: ${readmePath}
|
||||
- Additional docs: ${docsPath}
|
||||
- Examples: ${examplesPath} (extensions, custom tools, SDK)
|
||||
- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
|
||||
- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)
|
||||
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
|
||||
- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
|
||||
@@ -152,11 +154,12 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi
|
||||
|
||||
// Append project context files
|
||||
if (contextFiles.length > 0) {
|
||||
prompt += "\n\n# Project Context\n\n";
|
||||
prompt += "\n\n<project_context>\n\n";
|
||||
prompt += "Project-specific instructions and guidelines:\n\n";
|
||||
for (const { path: filePath, content } of contextFiles) {
|
||||
prompt += `## ${filePath}\n\n${content}\n\n`;
|
||||
prompt += `<project_instructions path="${filePath}">\n${content}\n</project_instructions>\n\n`;
|
||||
}
|
||||
prompt += "</project_context>\n";
|
||||
}
|
||||
|
||||
// Append skills section (only if read tool is available)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SettingsManager } from "./settings-manager.js";
|
||||
import type { SettingsManager } from "./settings-manager.ts";
|
||||
|
||||
function isTruthyEnvFlag(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
|
||||
@@ -3,22 +3,22 @@ import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
||||
import { spawn } from "child_process";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
|
||||
import { theme } from "../../modes/interactive/theme/theme.js";
|
||||
import { waitForChildProcess } from "../../utils/child-process.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.ts";
|
||||
import { theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import { waitForChildProcess } from "../../utils/child-process.ts";
|
||||
import {
|
||||
getShellConfig,
|
||||
getShellEnv,
|
||||
killProcessTree,
|
||||
trackDetachedChildPid,
|
||||
untrackDetachedChildPid,
|
||||
} from "../../utils/shell.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { OutputAccumulator } from "./output-accumulator.js";
|
||||
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.js";
|
||||
} from "../../utils/shell.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { OutputAccumulator } from "./output-accumulator.ts";
|
||||
import { getTextOutput, invalidArgText, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.ts";
|
||||
|
||||
const bashSchema = Type.Object({
|
||||
command: Type.String({ description: "Bash command to execute" }),
|
||||
@@ -76,6 +76,7 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas
|
||||
detached: process.platform !== "win32",
|
||||
env: env ?? getShellEnv(),
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
if (child.pid) trackDetachedChildPid(child.pid);
|
||||
let timedOut = false;
|
||||
@@ -199,7 +200,15 @@ function rebuildBashResultRenderComponent(
|
||||
const state = component.state;
|
||||
component.clear();
|
||||
|
||||
const output = getTextOutput(result as any, showImages).trim();
|
||||
let output = getTextOutput(result as any, showImages).trim();
|
||||
const truncation = result.details?.truncation;
|
||||
const fullOutputPath = result.details?.fullOutputPath;
|
||||
if (!options.isPartial && truncation?.truncated && fullOutputPath && output.endsWith("]")) {
|
||||
const footerStart = output.lastIndexOf("\n\n[");
|
||||
if (footerStart !== -1 && output.slice(footerStart).includes(fullOutputPath)) {
|
||||
output = output.slice(0, footerStart).trimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
if (output) {
|
||||
const styledOutput = output
|
||||
@@ -235,8 +244,6 @@ function rebuildBashResultRenderComponent(
|
||||
}
|
||||
}
|
||||
|
||||
const truncation = result.details?.truncation;
|
||||
const fullOutputPath = result.details?.fullOutputPath;
|
||||
if (truncation?.truncated || fullOutputPath) {
|
||||
const warnings: string[] = [];
|
||||
if (fullOutputPath) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as Diff from "diff";
|
||||
import { constants } from "fs";
|
||||
import { access, readFile } from "fs/promises";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
|
||||
export function detectLineEnding(content: string): "\r\n" | "\n" {
|
||||
const crlfIdx = content.indexOf("\r\n");
|
||||
@@ -259,8 +259,16 @@ export function applyEditsToNormalizedContent(
|
||||
return { baseContent, newContent };
|
||||
}
|
||||
|
||||
/** Generate a standard unified patch. */
|
||||
export function generateUnifiedPatch(path: string, oldContent: string, newContent: string, contextLines = 4): string {
|
||||
return Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {
|
||||
context: contextLines,
|
||||
headerOptions: Diff.FILE_HEADERS_ONLY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unified diff string with line numbers and context.
|
||||
* Generate a display-oriented diff string with line numbers and context.
|
||||
* Returns both the diff string and the first changed line number (in the new file).
|
||||
*/
|
||||
export function generateDiffString(
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import { constants } from "fs";
|
||||
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { renderDiff } from "../../modes/interactive/components/diff.js";
|
||||
import type { ToolDefinition } from "../extensions/types.js";
|
||||
import { renderDiff } from "../../modes/interactive/components/diff.ts";
|
||||
import type { ToolDefinition } from "../extensions/types.ts";
|
||||
import {
|
||||
applyEditsToNormalizedContent,
|
||||
computeEditsDiff,
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
type EditDiffError,
|
||||
type EditDiffResult,
|
||||
generateDiffString,
|
||||
generateUnifiedPatch,
|
||||
normalizeToLF,
|
||||
restoreLineEndings,
|
||||
stripBom,
|
||||
} from "./edit-diff.js";
|
||||
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { invalidArgText, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
} from "./edit-diff.ts";
|
||||
import { withFileMutationQueue } from "./file-mutation-queue.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
|
||||
type EditPreview = EditDiffResult | EditDiffError;
|
||||
|
||||
@@ -57,8 +58,10 @@ type LegacyEditToolInput = EditToolInput & {
|
||||
};
|
||||
|
||||
export interface EditToolDetails {
|
||||
/** Unified diff of the changes made */
|
||||
/** Display-oriented diff of the changes made */
|
||||
diff: string;
|
||||
/** Standard unified patch of the changes made */
|
||||
patch: string;
|
||||
/** Line number of the first change in the new file (for editor navigation) */
|
||||
firstChangedLine?: number;
|
||||
}
|
||||
@@ -190,7 +193,7 @@ function getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path
|
||||
|
||||
function formatEditCall(
|
||||
args: RenderableEditArgs | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const invalidArg = invalidArgText(theme);
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
@@ -203,7 +206,7 @@ function formatEditResult(
|
||||
args: RenderableEditArgs | undefined,
|
||||
preview: EditPreview | undefined,
|
||||
result: EditToolResultLike,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
isError: boolean,
|
||||
): string | undefined {
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
@@ -231,7 +234,7 @@ function formatEditResult(
|
||||
function getEditHeaderBg(
|
||||
preview: EditPreview | undefined,
|
||||
settledError: boolean | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): (text: string) => string {
|
||||
if (preview) {
|
||||
if ("error" in preview) {
|
||||
@@ -248,7 +251,7 @@ function getEditHeaderBg(
|
||||
function buildEditCallComponent(
|
||||
component: EditCallRenderComponent,
|
||||
args: RenderableEditArgs | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): EditCallRenderComponent {
|
||||
component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));
|
||||
component.clear();
|
||||
@@ -394,6 +397,7 @@ export function createEditToolDefinition(
|
||||
}
|
||||
|
||||
const diffResult = generateDiffString(baseContent, newContent);
|
||||
const patch = generateUnifiedPatch(path, baseContent, newContent);
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
@@ -401,7 +405,7 @@ export function createEditToolDefinition(
|
||||
text: `Successfully replaced ${edits.length} block(s) in ${path}.`,
|
||||
},
|
||||
],
|
||||
details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine },
|
||||
details: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
// Clean up abort handler.
|
||||
|
||||
@@ -5,13 +5,13 @@ import { spawn } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { ensureTool } from "../../utils/tools-manager.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
||||
|
||||
function toPosixPath(value: string): string {
|
||||
return value.split(path.sep).join("/");
|
||||
@@ -58,7 +58,7 @@ export interface FindToolOptions {
|
||||
|
||||
function formatFindCall(
|
||||
args: { pattern: string; path?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const pattern = str(args?.pattern);
|
||||
const rawPath = str(args?.path);
|
||||
@@ -82,7 +82,7 @@ function formatFindResult(
|
||||
details?: FindToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
|
||||
@@ -5,12 +5,12 @@ import { spawn } from "child_process";
|
||||
import { readFileSync, statSync } from "fs";
|
||||
import path from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { ensureTool } from "../../utils/tools-manager.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import {
|
||||
DEFAULT_MAX_BYTES,
|
||||
formatSize,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type TruncationResult,
|
||||
truncateHead,
|
||||
truncateLine,
|
||||
} from "./truncate.js";
|
||||
} from "./truncate.ts";
|
||||
|
||||
const grepSchema = Type.Object({
|
||||
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
|
||||
@@ -66,7 +66,7 @@ export interface GrepToolOptions {
|
||||
|
||||
function formatGrepCall(
|
||||
args: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const pattern = str(args?.pattern);
|
||||
const rawPath = str(args?.path);
|
||||
@@ -90,7 +90,7 @@ function formatGrepResult(
|
||||
details?: GrepToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
|
||||
@@ -8,7 +8,7 @@ export {
|
||||
createBashTool,
|
||||
createBashToolDefinition,
|
||||
createLocalBashOperations,
|
||||
} from "./bash.js";
|
||||
} from "./bash.ts";
|
||||
export {
|
||||
createEditTool,
|
||||
createEditToolDefinition,
|
||||
@@ -16,8 +16,8 @@ export {
|
||||
type EditToolDetails,
|
||||
type EditToolInput,
|
||||
type EditToolOptions,
|
||||
} from "./edit.js";
|
||||
export { withFileMutationQueue } from "./file-mutation-queue.js";
|
||||
} from "./edit.ts";
|
||||
export { withFileMutationQueue } from "./file-mutation-queue.ts";
|
||||
export {
|
||||
createFindTool,
|
||||
createFindToolDefinition,
|
||||
@@ -25,7 +25,7 @@ export {
|
||||
type FindToolDetails,
|
||||
type FindToolInput,
|
||||
type FindToolOptions,
|
||||
} from "./find.js";
|
||||
} from "./find.ts";
|
||||
export {
|
||||
createGrepTool,
|
||||
createGrepToolDefinition,
|
||||
@@ -33,7 +33,7 @@ export {
|
||||
type GrepToolDetails,
|
||||
type GrepToolInput,
|
||||
type GrepToolOptions,
|
||||
} from "./grep.js";
|
||||
} from "./grep.ts";
|
||||
export {
|
||||
createLsTool,
|
||||
createLsToolDefinition,
|
||||
@@ -41,7 +41,7 @@ export {
|
||||
type LsToolDetails,
|
||||
type LsToolInput,
|
||||
type LsToolOptions,
|
||||
} from "./ls.js";
|
||||
} from "./ls.ts";
|
||||
export {
|
||||
createReadTool,
|
||||
createReadToolDefinition,
|
||||
@@ -49,7 +49,7 @@ export {
|
||||
type ReadToolDetails,
|
||||
type ReadToolInput,
|
||||
type ReadToolOptions,
|
||||
} from "./read.js";
|
||||
} from "./read.ts";
|
||||
export {
|
||||
DEFAULT_MAX_BYTES,
|
||||
DEFAULT_MAX_LINES,
|
||||
@@ -59,24 +59,24 @@ export {
|
||||
truncateHead,
|
||||
truncateLine,
|
||||
truncateTail,
|
||||
} from "./truncate.js";
|
||||
} from "./truncate.ts";
|
||||
export {
|
||||
createWriteTool,
|
||||
createWriteToolDefinition,
|
||||
type WriteOperations,
|
||||
type WriteToolInput,
|
||||
type WriteToolOptions,
|
||||
} from "./write.js";
|
||||
} from "./write.ts";
|
||||
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import type { ToolDefinition } from "../extensions/types.js";
|
||||
import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.js";
|
||||
import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.js";
|
||||
import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.js";
|
||||
import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.js";
|
||||
import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.js";
|
||||
import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.js";
|
||||
import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.js";
|
||||
import type { ToolDefinition } from "../extensions/types.ts";
|
||||
import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.ts";
|
||||
import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.ts";
|
||||
import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.ts";
|
||||
import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.ts";
|
||||
import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.ts";
|
||||
import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.ts";
|
||||
import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.ts";
|
||||
|
||||
export type Tool = AgentTool<any>;
|
||||
export type ToolDef = ToolDefinition<any, any>;
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Text } from "@earendil-works/pi-tui";
|
||||
import { existsSync, readdirSync, statSync } from "fs";
|
||||
import nodePath from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
||||
|
||||
const lsSchema = Type.Object({
|
||||
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
|
||||
@@ -50,7 +50,7 @@ export interface LsToolOptions {
|
||||
|
||||
function formatLsCall(
|
||||
args: { path?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const rawPath = str(args?.path);
|
||||
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
||||
@@ -69,7 +69,7 @@ function formatLsResult(
|
||||
details?: LsToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
||||
import { createWriteStream, type WriteStream } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.js";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.ts";
|
||||
|
||||
export interface OutputAccumulatorOptions {
|
||||
maxLines?: number;
|
||||
@@ -45,8 +45,10 @@ export class OutputAccumulator {
|
||||
private tailStartsAtLineBoundary = true;
|
||||
private totalRawBytes = 0;
|
||||
private totalDecodedBytes = 0;
|
||||
private totalLines = 1;
|
||||
private completedLines = 0;
|
||||
private totalLines = 0;
|
||||
private currentLineBytes = 0;
|
||||
private hasOpenLine = false;
|
||||
private finished = false;
|
||||
|
||||
private tempFilePath: string | undefined;
|
||||
@@ -164,10 +166,14 @@ export class OutputAccumulator {
|
||||
}
|
||||
if (newlines === 0) {
|
||||
this.currentLineBytes += bytes;
|
||||
this.hasOpenLine = true;
|
||||
} else {
|
||||
this.totalLines += newlines;
|
||||
this.currentLineBytes = byteLength(text.slice(lastNewline + 1));
|
||||
this.completedLines += newlines;
|
||||
const tail = text.slice(lastNewline + 1);
|
||||
this.currentLineBytes = byteLength(tail);
|
||||
this.hasOpenLine = tail.length > 0;
|
||||
}
|
||||
this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
|
||||
}
|
||||
|
||||
private trimTail(): void {
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { accessSync, constants } from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import { isAbsolute, resolve as resolvePath } from "node:path";
|
||||
import { normalizePath, resolvePath } from "../../utils/paths.ts";
|
||||
|
||||
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
||||
const NARROW_NO_BREAK_SPACE = "\u202F";
|
||||
function normalizeUnicodeSpaces(str: string): string {
|
||||
return str.replace(UNICODE_SPACES, " ");
|
||||
}
|
||||
|
||||
function tryMacOSScreenshotPath(filePath: string): string {
|
||||
return filePath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);
|
||||
@@ -32,19 +27,8 @@ function fileExists(filePath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAtPrefix(filePath: string): string {
|
||||
return filePath.startsWith("@") ? filePath.slice(1) : filePath;
|
||||
}
|
||||
|
||||
export function expandPath(filePath: string): string {
|
||||
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
|
||||
if (normalized === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
if (normalized.startsWith("~/")) {
|
||||
return os.homedir() + normalized.slice(1);
|
||||
}
|
||||
return normalized;
|
||||
return normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,11 +36,7 @@ export function expandPath(filePath: string): string {
|
||||
* Handles ~ expansion and absolute paths.
|
||||
*/
|
||||
export function resolveToCwd(filePath: string, cwd: string): string {
|
||||
const expanded = expandPath(filePath);
|
||||
if (isAbsolute(expanded)) {
|
||||
return expanded;
|
||||
}
|
||||
return resolvePath(cwd, expanded);
|
||||
return resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
|
||||
}
|
||||
|
||||
export function resolveReadPath(filePath: string, cwd: string): string {
|
||||
|
||||
@@ -5,17 +5,17 @@ import { Text } from "@earendil-works/pi-tui";
|
||||
import { constants } from "fs";
|
||||
import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { getReadmePath } from "../../config.js";
|
||||
import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
||||
import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveReadPath } from "./path-utils.js";
|
||||
import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||
import { getReadmePath } from "../../config.ts";
|
||||
import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.ts";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.ts";
|
||||
import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveReadPath } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
||||
|
||||
const readSchema = Type.Object({
|
||||
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as os from "node:os";
|
||||
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
||||
import { getCapabilities, getImageDimensions, imageFallback } from "@earendil-works/pi-tui";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { sanitizeBinaryOutput } from "../../utils/shell.js";
|
||||
import { stripAnsi } from "../../utils/ansi.ts";
|
||||
import { sanitizeBinaryOutput } from "../../utils/shell.ts";
|
||||
|
||||
export function shortenPath(path: unknown): string {
|
||||
if (typeof path !== "string") return "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import type { ExtensionContext, ToolDefinition } from "../extensions/types.js";
|
||||
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
||||
|
||||
/** Wrap a ToolDefinition into an AgentTool for the core runtime. */
|
||||
export function wrapToolDefinition<TDetails = unknown>(
|
||||
|
||||
@@ -44,6 +44,17 @@ export interface TruncationOptions {
|
||||
maxBytes?: number;
|
||||
}
|
||||
|
||||
function splitLinesForCounting(content: string): string[] {
|
||||
if (content.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const lines = content.split("\n");
|
||||
if (content.endsWith("\n")) {
|
||||
lines.pop();
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes as human-readable size.
|
||||
*/
|
||||
@@ -69,7 +80,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
||||
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||
|
||||
const totalBytes = Buffer.byteLength(content, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const lines = splitLinesForCounting(content);
|
||||
const totalLines = lines.length;
|
||||
|
||||
// Check if no truncation needed
|
||||
@@ -159,7 +170,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
||||
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||
|
||||
const totalBytes = Buffer.byteLength(content, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const lines = splitLinesForCounting(content);
|
||||
const totalLines = lines.length;
|
||||
|
||||
// Check if no truncation needed
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user