mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Merge branch 'main' into fix/amazon-bedrock-mantle-provider-for-OpenAI-compatible-models
This commit is contained in:
commit
3722202df7
69 changed files with 1252 additions and 263 deletions
9
.github/workflows/build-binaries.yml
vendored
9
.github/workflows/build-binaries.yml
vendored
|
|
@ -182,7 +182,14 @@ jobs:
|
|||
esac
|
||||
|
||||
mkdir -p extracted
|
||||
tar -xf "${archive}" -C extracted
|
||||
if [[ "${archive}" == *.zip ]]; then
|
||||
export ARCHIVE_PATH="$(cygpath -w "${archive}")"
|
||||
export DESTINATION_PATH="$(cygpath -w extracted)"
|
||||
powershell.exe -NoProfile -NonInteractive -Command \
|
||||
'Expand-Archive -LiteralPath $env:ARCHIVE_PATH -DestinationPath $env:DESTINATION_PATH -Force'
|
||||
else
|
||||
tar -xf "${archive}" -C extracted
|
||||
fi
|
||||
echo "root=${root}" >> "${GITHUB_OUTPUT}"
|
||||
echo "binary=${binary}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
|
|
|
|||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -5404,11 +5404,11 @@
|
|||
},
|
||||
"packages/agent": {
|
||||
"name": "@earendil-works/pi-agent-core",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-telemetry": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-telemetry": "^0.84.3",
|
||||
"diff": "8.0.4",
|
||||
"ignore": "7.0.5",
|
||||
"typebox": "1.3.7",
|
||||
|
|
@ -5433,12 +5433,12 @@
|
|||
},
|
||||
"packages/ai": {
|
||||
"name": "@earendil-works/pi-ai",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.91.1",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1048.0",
|
||||
"@earendil-works/pi-telemetry": "^0.84.2",
|
||||
"@earendil-works/pi-telemetry": "^0.84.3",
|
||||
"@google/genai": "1.52.0",
|
||||
"@smithy/node-http-handler": "4.7.3",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
|
|
@ -5469,10 +5469,10 @@
|
|||
},
|
||||
"packages/client": {
|
||||
"name": "@earendil-works/pi-client",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-protocol": "^0.84.2"
|
||||
"@earendil-works/pi-protocol": "^0.84.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "0.4.0",
|
||||
|
|
@ -5484,15 +5484,15 @@
|
|||
},
|
||||
"packages/coding-agent": {
|
||||
"name": "@earendil-works/pi-coding-agent",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws/bedrock-token-generator": "1.1.0",
|
||||
"@earendil-works/pi-agent-core": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-client": "^0.84.2",
|
||||
"@earendil-works/pi-protocol": "^0.84.2",
|
||||
"@earendil-works/pi-tui": "^0.84.2",
|
||||
"@earendil-works/pi-agent-core": "^0.84.3",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-client": "^0.84.3",
|
||||
"@earendil-works/pi-protocol": "^0.84.3",
|
||||
"@earendil-works/pi-tui": "^0.84.3",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"chalk": "5.6.2",
|
||||
"cross-spawn": "7.0.6",
|
||||
|
|
@ -5531,32 +5531,32 @@
|
|||
},
|
||||
"packages/coding-agent/examples/extensions/custom-provider-anthropic": {
|
||||
"name": "pi-extension-custom-provider-anthropic",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.52.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": {
|
||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||
"version": "0.84.2"
|
||||
"version": "0.84.3"
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/gondolin": {
|
||||
"name": "pi-extension-gondolin",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"dependencies": {
|
||||
"@earendil-works/gondolin": "0.12.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/sandbox": {
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.26"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/with-deps": {
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"dependencies": {
|
||||
"ms": "2.1.3"
|
||||
},
|
||||
|
|
@ -5610,10 +5610,10 @@
|
|||
},
|
||||
"packages/evals": {
|
||||
"name": "@earendil-works/pi-evals",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"devDependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.3",
|
||||
"@types/node": "22.19.19",
|
||||
"shx": "0.4.0",
|
||||
"typescript": "5.9.3",
|
||||
|
|
@ -5623,7 +5623,7 @@
|
|||
},
|
||||
"packages/protocol": {
|
||||
"name": "@earendil-works/pi-protocol",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"typebox": "1.3.7"
|
||||
|
|
@ -5638,11 +5638,11 @@
|
|||
},
|
||||
"packages/server": {
|
||||
"name": "@earendil-works/pi-server",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-protocol": "^0.84.2"
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-protocol": "^0.84.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "0.4.0",
|
||||
|
|
@ -5654,11 +5654,11 @@
|
|||
},
|
||||
"packages/session-backends/sqlite-node": {
|
||||
"name": "@earendil-works/pi-session-backend-sqlite-node",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.2"
|
||||
"@earendil-works/pi-agent-core": "^0.84.3",
|
||||
"@earendil-works/pi-ai": "^0.84.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
|
|
@ -5670,7 +5670,7 @@
|
|||
},
|
||||
"packages/telemetry": {
|
||||
"name": "@earendil-works/pi-telemetry",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "22.19.19",
|
||||
|
|
@ -5682,7 +5682,7 @@
|
|||
},
|
||||
"packages/tui": {
|
||||
"name": "@earendil-works/pi-tui",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "1.6.0",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed single-object `edit` tool inputs failing validation by accepting them as one-edit arrays ([#7835](https://github.com/earendil-works/pi/issues/7835)).
|
||||
- Fixed root Markdown files such as `README.md` and `AGENTS.md` in skill directories being reported as broken skills unless they declare valid skill frontmatter ([#7805](https://github.com/earendil-works/pi/issues/7805)).
|
||||
|
||||
## [0.84.2] - 2026-08-14
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-agent-core",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -35,8 +35,8 @@
|
|||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-telemetry": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-telemetry": "^0.84.3",
|
||||
"diff": "8.0.4",
|
||||
"ignore": "7.0.5",
|
||||
"typebox": "1.3.7",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Renamed `GoogleThinkingLevel` to `GoogleApiThinkingLevel` and added `ResolvedGoogleThinkingLevel` for normalized adapter levels.
|
||||
|
|
@ -10,15 +12,26 @@
|
|||
|
||||
- Added Amazon Bedrock internal routing for OpenAI Responses-compatible Bedrock models.
|
||||
- Added provider-neutral `toolChoice` support to simple stream requests.
|
||||
- Added automatic Anthropic server-side refusal fallback for supported first-party models, including returned-model usage pricing ([#8017](https://github.com/earendil-works/pi/issues/8017)).
|
||||
- Added configurable OpenAI-compatible thinking-token budget fields for vLLM, Qwen/SGLang, and llama.cpp servers ([#8275](https://github.com/earendil-works/pi/pull/8275) by [@bnsd55](https://github.com/bnsd55)).
|
||||
- Added China-specific ZAI Coding Plan models, including GLM-4.6V vision support, and API-equivalent usage cost estimates for models with published PAYG prices ([#8220](https://github.com/earendil-works/pi/issues/8220)).
|
||||
- Added `deepseek-v4-pro-0813` to the Qwen Token Plan Individual catalog ([#8194](https://github.com/earendil-works/pi/issues/8194)).
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed built-in xAI models to use the Responses API with encrypted reasoning replay and made Grok 4.6 the default xAI model ([#8124](https://github.com/earendil-works/pi/pull/8124) by [@Jaaneek](https://github.com/Jaaneek)).
|
||||
- Changed the Anthropic, Azure OpenAI, Google Generative AI, Google Vertex, Mistral, OpenAI Chat Completions, and OpenAI Responses adapters to send Pi's default `User-Agent` unless overridden ([#8305](https://github.com/earendil-works/pi/issues/8305)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI GPT-5.x models from the Bedrock catalog being routed through the Amazon Bedrock Converse adapter instead of Bedrock's OpenAI-compatible Responses endpoint (Mantle).
|
||||
- Fixed OpenAI-compatible Chat Completions reasoning replay to preserve and resend assistant-level `reasoning_details` (`reasoning.text`, `reasoning.summary`, and `reasoning.encrypted`) verbatim and in order ([#7994](https://github.com/earendil-works/pi/issues/7994)).
|
||||
- Fixed Anthropic server-side fallback responses being priced with the requested model instead of the returned fallback model ([#8285](https://github.com/earendil-works/pi/issues/8285)).
|
||||
- Fixed GitHub Copilot login triggering model-policy rate limits by limiting policy updates, retrying model discovery once, and honoring server retry delays ([#7850](https://github.com/earendil-works/pi/issues/7850)).
|
||||
- Fixed Amazon Bedrock dropping and failing to replay opaque redacted reasoning from non-Anthropic models ([#8314](https://github.com/earendil-works/pi/pull/8314) by [@seiji](https://github.com/seiji)).
|
||||
- Fixed Z.AI Coding Plan models deriving incomplete reasoning-effort metadata, including missing GLM-5.3 low, high, and max levels ([#8336](https://github.com/earendil-works/pi/issues/8336)).
|
||||
- Fixed DeepSeek V4 Flash on OpenCode and OpenCode Go omitting its supported low thinking level ([#8181](https://github.com/earendil-works/pi/pull/8181) by [@tianshuang](https://github.com/tianshuang)).
|
||||
- Fixed Azure OpenAI Responses ignoring `toolChoice` in provider-specific stream requests.
|
||||
- Added `deepseek-v4-pro-0813` to the Qwen Token Plan Individual catalog ([#8194](https://github.com/earendil-works/pi/issues/8194)).
|
||||
- Fixed Amazon Bedrock `after_provider_response`/`onResponse` to forward the raw response headers instead of only the synthesized request id header ([#8234](https://github.com/earendil-works/pi/issues/8234)).
|
||||
- Fixed Kimi OpenAI-compatible usage reporting so top-level `cached_tokens` count as cache reads instead of normal input tokens ([#8075](https://github.com/earendil-works/pi/issues/8075)).
|
||||
- Fixed Google Generative AI and Vertex AI custom models ignoring `thinkingLevelMap`, which dropped extended thinking controls ([#8135](https://github.com/earendil-works/pi/issues/8135)).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-ai",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.91.1",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1048.0",
|
||||
"@earendil-works/pi-telemetry": "^0.84.2",
|
||||
"@earendil-works/pi-telemetry": "^0.84.3",
|
||||
"@google/genai": "1.52.0",
|
||||
"@smithy/node-http-handler": "4.7.3",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
## [0.84.2] - 2026-08-14
|
||||
|
||||
## [0.84.1] - 2026-08-07
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-client",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "Transport-neutral client for remote pi sessions over framed CBOR bytes",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
"node": ">=22.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-protocol": "^0.84.2"
|
||||
"@earendil-works/pi-protocol": "^0.84.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "0.4.0",
|
||||
|
|
|
|||
|
|
@ -2,12 +2,36 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
### New Features
|
||||
|
||||
- **PowerShell tool** — Use optional native PowerShell command execution on Windows. See [PowerShell Tool](docs/windows.md#powershell-tool).
|
||||
- **Safer managed updates** — Stage, verify, and atomically activate updates for installer-managed installations. See [Install and Manage](docs/packages.md#install-and-manage).
|
||||
- **Model and thinking controls** — Select thinking levels with `/thinking`, search defaults, keep selections session-scoped, and persist them explicitly with Ctrl+S. See [Models and Thinking](docs/keybindings.md#models-and-thinking).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Renamed the inherited `GoogleThinkingLevel` type to `GoogleApiThinkingLevel` and added `ResolvedGoogleThinkingLevel` for normalized adapter levels.
|
||||
|
||||
### Added
|
||||
|
||||
- Added an optional `powershell` tool for Windows, configurable through `defaultTools` and the SDK. See [PowerShell Tool](docs/windows.md#powershell-tool).
|
||||
- Added a `/thinking` selector and searchable default choices to the model and thinking selectors; Ctrl+S saves the selected model as the global default. See [Models and Thinking](docs/keybindings.md#models-and-thinking).
|
||||
- Added optional routing session IDs to exported compaction summary helpers so callers can preserve provider routing without enabling prompt cache writes.
|
||||
- Added transcript usage notices for compaction and branch summaries when cache miss notices are enabled.
|
||||
- Added `session_compact_failed` extension events so compaction failures and aborts expose their reason, retry state, source, and error message to handlers ([#8175](https://github.com/earendil-works/pi/issues/8175)).
|
||||
- Added inherited provider-neutral `toolChoice` support to simple stream requests.
|
||||
- Added inherited automatic Anthropic server-side refusal fallback for supported first-party models, including returned-model usage pricing ([#8017](https://github.com/earendil-works/pi/issues/8017)).
|
||||
- Added inherited configurable OpenAI-compatible thinking-token budget fields for vLLM, Qwen/SGLang, and llama.cpp servers. See [OpenAI Compatibility](docs/models.md#openai-compatibility) ([#8275](https://github.com/earendil-works/pi/pull/8275) by [@bnsd55](https://github.com/bnsd55)).
|
||||
- Added inherited China-specific ZAI Coding Plan models, including GLM-4.6V vision support and API-equivalent usage cost estimates ([#8220](https://github.com/earendil-works/pi/issues/8220)).
|
||||
- Added inherited `deepseek-v4-pro-0813` support to the Qwen Token Plan Individual catalog ([#8194](https://github.com/earendil-works/pi/issues/8194)).
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed experimental installer-managed installations so `pi update` stages, verifies, and atomically activates the selected release in place. See [Install and Manage](docs/packages.md#install-and-manage).
|
||||
- Changed inherited built-in xAI models to use the Responses API with encrypted reasoning replay and made Grok 4.6 the default xAI model ([#8124](https://github.com/earendil-works/pi/pull/8124) by [@Jaaneek](https://github.com/Jaaneek)).
|
||||
- Changed inherited Anthropic, Azure OpenAI, Google, Mistral, and OpenAI adapters to send Pi's default `User-Agent` unless overridden ([#8305](https://github.com/earendil-works/pi/issues/8305)).
|
||||
- Changed Windows and WSL keybinding defaults to avoid terminal-reserved shortcuts for image paste, model cycling, editor undo, fullscreen transcript navigation and search, and message queueing ([#8372](https://github.com/earendil-works/pi/issues/8372)).
|
||||
- Changed Bun release archives to ship the native clipboard binary only inside the wrapper package, removing a duplicate platform package from each archive.
|
||||
- Changed package resource glob expansion to use Node.js's built-in implementation with deterministic visible-path matching, reducing the installed runtime dependency tree.
|
||||
|
|
@ -18,18 +42,40 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Fixed `models.json` typings omitting the documented OpenAI-compatible `compat.supportsFinishReason` provider and model override ([#8460](https://github.com/earendil-works/pi/issues/8460)).
|
||||
- Fixed failed extension factories leaving event subscriptions, provider registrations, and default flag state active ([#8424](https://github.com/earendil-works/pi/pull/8424) by [@acmerfight](https://github.com/acmerfight)).
|
||||
- Fixed `models.json` typings omitting the documented OpenAI-compatible `compat.supportsFinishReason` provider and model override ([#8487](https://github.com/earendil-works/pi/pull/8487) by [@petrroll](https://github.com/petrroll)).
|
||||
- Fixed `/model` and `/thinking` selections being persisted globally unless explicitly saved with Ctrl+S ([#5263](https://github.com/earendil-works/pi/issues/5263)).
|
||||
- Fixed JSON and RPC `toolcall_start` events omitting the tool call id and name ([#7953](https://github.com/earendil-works/pi/pull/7953) by [@christianklotz](https://github.com/christianklotz)).
|
||||
- Fixed extensions failing to load when the Node.js CLI runs as a single-executable application ([#8237](https://github.com/earendil-works/pi/issues/8237)).
|
||||
- Fixed nested Markdown skills inside `.agents/skills/` grouping directories not being discovered.
|
||||
- Fixed compaction and branch summarization requests exposing tools to providers.
|
||||
- Fixed single-object `edit` tool inputs failing validation by accepting them as one-edit arrays in both coding-agent and harness edit tools ([#7835](https://github.com/earendil-works/pi/issues/7835)).
|
||||
- Fixed root Markdown files such as `README.md` and `AGENTS.md` in skill directories being reported as broken skills unless they declare valid skill frontmatter ([#7805](https://github.com/earendil-works/pi/issues/7805)).
|
||||
- Fixed the default Cerebras model referencing an unavailable Z.AI model.
|
||||
- Fixed inherited OpenAI-compatible Chat Completions reasoning replay to preserve and resend assistant-level `reasoning_details` verbatim and in order ([#7994](https://github.com/earendil-works/pi/issues/7994)).
|
||||
- Fixed inherited Anthropic server-side fallback responses being priced with the requested model instead of the returned fallback model ([#8285](https://github.com/earendil-works/pi/issues/8285)).
|
||||
- Fixed inherited GitHub Copilot login triggering model-policy rate limits by limiting policy updates, retrying model discovery once, and honoring server retry delays ([#7850](https://github.com/earendil-works/pi/issues/7850)).
|
||||
- Fixed inherited Amazon Bedrock dropping and failing to replay opaque redacted reasoning from non-Anthropic models ([#8314](https://github.com/earendil-works/pi/pull/8314) by [@seiji](https://github.com/seiji)).
|
||||
- Fixed inherited Z.AI Coding Plan models deriving incomplete reasoning-effort metadata, including missing GLM-5.3 low, high, and max levels ([#8336](https://github.com/earendil-works/pi/issues/8336)).
|
||||
- Fixed inherited DeepSeek V4 Flash on OpenCode and OpenCode Go omitting its supported low thinking level ([#8181](https://github.com/earendil-works/pi/pull/8181) by [@tianshuang](https://github.com/tianshuang)).
|
||||
- Fixed inherited Azure OpenAI Responses ignoring `toolChoice` in provider-specific stream requests.
|
||||
- Fixed inherited Amazon Bedrock response hooks receiving only a synthesized request id instead of the raw response headers ([#8234](https://github.com/earendil-works/pi/issues/8234)).
|
||||
- Fixed inherited Kimi usage reporting so top-level `cached_tokens` count as cache reads instead of normal input tokens ([#8075](https://github.com/earendil-works/pi/issues/8075)).
|
||||
- Fixed inherited Google custom models ignoring `thinkingLevelMap`, which dropped extended thinking controls ([#8135](https://github.com/earendil-works/pi/issues/8135)).
|
||||
- Fixed writes to `auth.json` and `models-store.json` overriding administrator-managed file permissions and ACLs ([#7779](https://github.com/earendil-works/pi/issues/7779)).
|
||||
- Fixed UTF-8 BOM markers preventing frontmatter and user configuration files from loading ([#8337](https://github.com/earendil-works/pi/issues/8337)).
|
||||
- Fixed invalid settings files being easy to miss during interactive startup by rendering warnings with the file path inside the TUI ([#7829](https://github.com/earendil-works/pi/issues/7829)).
|
||||
- Fixed the subagent example repeatedly prompting before running project-local agents in trusted repositories ([#8261](https://github.com/earendil-works/pi/issues/8261)).
|
||||
- Added `session_compact_failed` extension events so compaction failures and aborts expose their reason, retry state, source, and error message to handlers ([#8175](https://github.com/earendil-works/pi/issues/8175)).
|
||||
- Fixed truncated compaction and branch summaries being persisted when generation reaches its output token limit ([#7048](https://github.com/earendil-works/pi/issues/7048)).
|
||||
- Fixed npm package update checks treating older registry versions as available updates, preventing `pi update` from downgrading already-newer installed packages ([#8226](https://github.com/earendil-works/pi/issues/8226)).
|
||||
- Fixed built-in llama.cpp models disappearing from `/model` when `/llama` refreshed a configured server under `PI_OFFLINE`, and included idle-slept `sleeping` router models in the selectable catalog ([#8167](https://github.com/earendil-works/pi/issues/8167)).
|
||||
- Fixed built-in llama.cpp models disappearing from `/model` when `/llama` refreshed a configured server under `PI_OFFLINE`, and included idle-slept `sleeping` router models plus autoloadable unloaded presets in the selectable catalog ([#8167](https://github.com/earendil-works/pi/issues/8167)).
|
||||
- Fixed `pi.registerFlag()` accepting default values that do not match the declared flag type ([#8064](https://github.com/earendil-works/pi/issues/8064)).
|
||||
- Fixed Z.AI Coding Plan defaults referencing the removed GLM-5.1 model ([#8096](https://github.com/earendil-works/pi/issues/8096)).
|
||||
- Fixed repeated ambiguous truncated-response recovery being mislabeled as context overflow ([#8130](https://github.com/earendil-works/pi/issues/8130)).
|
||||
- Fixed duplicate fullscreen right-click paste in VS Code-based terminals on Windows ([#8186](https://github.com/earendil-works/pi/issues/8186)).
|
||||
- Fixed inherited padded text exceeding narrow terminal widths ([#8252](https://github.com/earendil-works/pi/issues/8252)).
|
||||
- Fixed inherited wrapped Markdown table links leaking color into borders and neighboring cells, including tables inside blockquotes ([#8335](https://github.com/earendil-works/pi/issues/8335)).
|
||||
- Fixed llama.cpp login guidance to direct users to `/llama` before `/model` when no local models are loaded ([#8203](https://github.com/earendil-works/pi/issues/8203)).
|
||||
- Fixed hung pi.dev model catalog requests consuming the entire refresh deadline without retrying ([#8198](https://github.com/earendil-works/pi/issues/8198)).
|
||||
- Fixed inherited Xiaomi model catalogs listing shut-down MiMo V2 models in `/model` and `--list-models` ([#8187](https://github.com/earendil-works/pi/issues/8187)).
|
||||
|
|
|
|||
|
|
@ -584,7 +584,7 @@ cat README.md | pi -p "Summarize this text"
|
|||
| `--no-builtin-tools`, `-nbt` | Disable built-in tools by default but keep extension/custom tools enabled |
|
||||
| `--no-tools`, `-nt` | Disable all tools by default |
|
||||
|
||||
Available built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`
|
||||
Available built-in tools: `read`, `bash`, `powershell` (Windows), `edit`, `write`, `grep`, `find`, `ls`
|
||||
|
||||
### Resource Options
|
||||
|
||||
|
|
@ -682,7 +682,7 @@ pi --thinking high "Solve this complex problem"
|
|||
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
|
||||
| `VISUAL`, `EDITOR` | Fallback external editor for Ctrl+G when `externalEditor` is unset; defaults to Notepad on Windows and `nano` elsewhere |
|
||||
|
||||
Commands run by the LLM-callable bash tool also receive current session metadata:
|
||||
Commands run by the LLM-callable `bash` and `powershell` tools also receive current session metadata:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
|
|
@ -692,7 +692,7 @@ Commands run by the LLM-callable bash tool also receive current session metadata
|
|||
| `PI_MODEL` | Currently selected model ID |
|
||||
| `PI_REASONING_LEVEL` | Current effective reasoning level |
|
||||
|
||||
These values are resolved when each command starts. See [Environment Variables](docs/environment-variables.md#bash-tool-session-environment) for semantics, examples, and custom-tool opt-out.
|
||||
These values are resolved when each command starts. See [Environment Variables](docs/environment-variables.md#shell-tool-session-environment) for semantics, examples, and custom-tool opt-out.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Pi uses environment variables in three ways:
|
|||
|
||||
- Variables such as `PI_OFFLINE` configure the Pi process.
|
||||
- Pi sets process markers so child processes can identify Pi as the launching agent.
|
||||
- Commands run by the LLM-callable bash tool receive `PI_*` variables describing the current session.
|
||||
- Commands run by the LLM-callable shell tools receive `PI_*` variables describing the current session.
|
||||
|
||||
Provider API-key variables are documented separately in [Providers](providers.md#environment-variables-or-auth-file).
|
||||
|
||||
|
|
@ -17,9 +17,9 @@ The CLI and RPC entry points set two process markers:
|
|||
|
||||
Child processes inherit both markers. They are not session-specific and are not set automatically when Pi is embedded through the SDK.
|
||||
|
||||
## Bash Tool Session Environment
|
||||
## Shell Tool Session Environment
|
||||
|
||||
Commands run by the bash tool receive the current Pi session state:
|
||||
Commands run by the `bash` and `powershell` tools receive the current Pi session state:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
|
|
@ -29,7 +29,7 @@ Commands run by the bash tool receive the current Pi session state:
|
|||
| `PI_MODEL` | Currently selected model ID |
|
||||
| `PI_REASONING_LEVEL` | Current effective reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |
|
||||
|
||||
The values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next bash command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.
|
||||
The values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next shell command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.
|
||||
|
||||
When asked which model or provider is running, inspect these variables instead of inferring the answer from the system prompt:
|
||||
|
||||
|
|
@ -46,11 +46,11 @@ if [ -n "$PI_SESSION_FILE" ]; then
|
|||
fi
|
||||
```
|
||||
|
||||
These variables are injected into the LLM-callable bash tool. They are not injected into user-entered `!` or `!!` commands.
|
||||
These variables are injected into the LLM-callable `bash` and `powershell` tools. They are not injected into user-entered `!` or `!!` commands.
|
||||
|
||||
### Custom Bash Tools
|
||||
### Custom Shell Tools
|
||||
|
||||
Bash tools created with `createBashTool()` expose the session environment by default when registered with Pi. Injection happens before `spawnHook`, so a hook receives the variables in `ctx.env`:
|
||||
Tools created with `createBashTool()` or `createPowerShellTool()` expose the session environment by default when registered with Pi. Injection happens before `spawnHook`, so a hook receives the variables in `ctx.env`:
|
||||
|
||||
```typescript
|
||||
const bashTool = createBashTool(cwd, {
|
||||
|
|
@ -64,7 +64,7 @@ const bashTool = createBashTool(cwd, {
|
|||
Disable session metadata independently of the spawn hook:
|
||||
|
||||
```typescript
|
||||
const bashTool = createBashTool(cwd, {
|
||||
const powershellTool = createPowerShellTool(cwd, {
|
||||
exposeSessionEnvironment: false,
|
||||
spawnHook: (ctx) => ctx,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2059,7 +2059,7 @@ pi.registerTool({
|
|||
|
||||
### Overriding Built-in Tools
|
||||
|
||||
Extensions can override built-in tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) by registering a tool with the same name. Interactive mode displays a warning when this happens.
|
||||
Extensions can override built-in tools (`read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, `ls`) by registering a tool with the same name. Interactive mode displays a warning when this happens.
|
||||
|
||||
```bash
|
||||
# Extension's read tool replaces built-in read
|
||||
|
|
@ -2083,6 +2083,7 @@ See [examples/extensions/tool-override.ts](../examples/extensions/tool-override.
|
|||
Built-in tool implementations:
|
||||
- [read.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`
|
||||
- [bash.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`
|
||||
- [powershell.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/powershell.ts) - `PowerShellToolDetails`
|
||||
- [edit.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)
|
||||
- [write.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)
|
||||
- [grep.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`
|
||||
|
|
@ -2118,11 +2119,11 @@ pi.registerTool({
|
|||
});
|
||||
```
|
||||
|
||||
**Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`
|
||||
**Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `PowerShellOperations`, `LsOperations`, `GrepOperations`, `FindOperations`
|
||||
|
||||
For `user_bash`, extensions can reuse pi's local shell backend via `createLocalBashOperations()` instead of reimplementing local process spawning, shell resolution, and process-tree termination.
|
||||
|
||||
The bash tool also supports a spawn hook to adjust the command, cwd, or env before execution:
|
||||
The `bash` and `powershell` tools also support a spawn hook to adjust the command, cwd, or env before execution:
|
||||
|
||||
```typescript
|
||||
import { createBashTool } from "@earendil-works/pi-coding-agent";
|
||||
|
|
@ -2136,7 +2137,7 @@ const bashTool = createBashTool(cwd, {
|
|||
});
|
||||
```
|
||||
|
||||
`createBashTool()` exposes the current session to commands through `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL`. Injection happens before `spawnHook`, so hooks receive these values in `env` and preserve them when they spread the existing environment as above. Set `exposeSessionEnvironment: false` to disable them:
|
||||
`createBashTool()` and `createPowerShellTool()` expose the current session to commands through `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL`. Injection happens before `spawnHook`, so hooks receive these values in `env` and preserve them when they spread the existing environment as above. Set `exposeSessionEnvironment: false` to disable them:
|
||||
|
||||
```typescript
|
||||
const bashTool = createBashTool(cwd, {
|
||||
|
|
@ -2144,7 +2145,7 @@ const bashTool = createBashTool(cwd, {
|
|||
});
|
||||
```
|
||||
|
||||
See [Bash tool session environment](environment-variables.md#bash-tool-session-environment) for variable semantics. See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag.
|
||||
See [Shell tool session environment](environment-variables.md#shell-tool-session-environment) for variable semantics. See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag.
|
||||
|
||||
### Output Truncation
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ pi update npm:@foo/bar # update one package
|
|||
pi update --extension npm:@foo/bar
|
||||
```
|
||||
|
||||
These commands manage pi packages and `pi update` can update the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall).
|
||||
These commands manage pi packages and `pi update` can update the pi CLI installation. For experimental installer-managed installations, `pi update` installs the exact checked version into a staged, lockfile-backed release and activates it only after verification, leaving the current release intact if the update fails. Managed installations do not support `--force`; rerun the installer to repair one. 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 after the project is trusted.
|
||||
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ const { session } = await createAgentSession({ resourceLoader: loader });
|
|||
|
||||
Specify which built-in tools to enable:
|
||||
|
||||
- Built-in tool names: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`
|
||||
- Built-in tool names: `read`, `bash`, `powershell`, `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
|
||||
|
|
@ -540,6 +540,11 @@ const { session } = await createAgentSession({
|
|||
tools: ["read", "bash", "grep"],
|
||||
});
|
||||
|
||||
// Use PowerShell instead of Bash on Windows
|
||||
const { session } = await createAgentSession({
|
||||
tools: ["read", "powershell", "edit", "write"],
|
||||
});
|
||||
|
||||
// Disable one tool while keeping the rest available
|
||||
const { session } = await createAgentSession({
|
||||
excludeTools: ["ask_question"],
|
||||
|
|
@ -1196,7 +1201,7 @@ SettingsManager
|
|||
// Tool factories
|
||||
createCodingTools
|
||||
createReadOnlyTools
|
||||
createReadTool, createBashTool, createEditTool, createWriteTool
|
||||
createReadTool, createBashTool, createPowerShellTool, createEditTool, createWriteTool
|
||||
createGrepTool, createFindTool, createLsTool
|
||||
|
||||
// Types
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ Windows paths in JSON must use forward slashes or escaped backslashes:
|
|||
|---------|------|---------|-------------|
|
||||
| `defaultTools` | string[] | - | Built-in tools enabled initially. When omitted, Pi uses its standard defaults |
|
||||
|
||||
`defaultTools` selects the built-in tools enabled at startup. Extension and SDK custom tools remain enabled:
|
||||
`defaultTools` selects the built-in tools enabled at startup. Extension and SDK custom tools remain enabled. Available built-ins are `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -228,6 +228,14 @@ Windows paths in JSON must use forward slashes or escaped backslashes:
|
|||
}
|
||||
```
|
||||
|
||||
On Windows, select `powershell` instead of `bash`, or include both:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultTools": ["read", "powershell", "edit", "write"]
|
||||
}
|
||||
```
|
||||
|
||||
An empty array starts with no built-in tools while preserving extension and SDK custom tools. `--tools` replaces this behavior with a strict allowlist for all tools, `--no-tools` disables all tools, and `--no-builtin-tools` disables the built-in defaults. `--exclude-tools` filters the resulting list. A project `defaultTools` array replaces the global array.
|
||||
|
||||
### Sessions
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ cat README.md | pi -p "Summarize this text"
|
|||
| `--no-builtin-tools`, `-nbt` | Disable built-in tools but keep extension/custom tools enabled |
|
||||
| `--no-tools`, `-nt` | Disable all tools |
|
||||
|
||||
Built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.
|
||||
Built-in tools: `read`, `bash`, `powershell` (Windows), `edit`, `write`, `grep`, `find`, `ls`.
|
||||
|
||||
### Resource Options
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Windows Setup
|
||||
|
||||
Pi requires a bash shell on Windows. Checked locations (in order):
|
||||
Pi uses Git Bash by default on Windows. Checked locations (in order):
|
||||
|
||||
1. Custom path from `~/.pi/agent/settings.json`
|
||||
2. Git Bash (`C:\Program Files\Git\bin\bash.exe`)
|
||||
|
|
@ -8,7 +8,29 @@ Pi requires a bash shell on Windows. Checked locations (in order):
|
|||
|
||||
For most users, [Git for Windows](https://git-scm.com/download/win) is sufficient.
|
||||
|
||||
## Custom Shell Path
|
||||
## PowerShell Tool
|
||||
|
||||
The optional `powershell` tool runs commands through `pwsh.exe` when available, otherwise Windows PowerShell. It starts PowerShell with `-NoProfile -NonInteractive -ExecutionPolicy Bypass`. Administrator-enforced execution policies can still take precedence.
|
||||
|
||||
Use `defaultTools` to replace the model-facing `bash` tool:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultTools": ["read", "powershell", "edit", "write"]
|
||||
}
|
||||
```
|
||||
|
||||
Or enable both while comparing behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultTools": ["read", "bash", "powershell", "edit", "write"]
|
||||
}
|
||||
```
|
||||
|
||||
The `!` and `!!` editor commands still use Bash.
|
||||
|
||||
## Custom Bash Path
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "pi-extension-custom-provider-anthropic",
|
||||
"private": true,
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||
"private": true,
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "pi-extension-gondolin",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-gondolin",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"dependencies": {
|
||||
"@earendil-works/gondolin": "0.12.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "pi-extension-gondolin",
|
||||
"private": true,
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"private": true,
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"private": true,
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
|
|
|||
52
packages/coding-agent/install-lock/package-lock.json
generated
52
packages/coding-agent/install-lock/package-lock.json
generated
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-coding-agent-install",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@earendil-works/pi-coding-agent-install",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-coding-agent": "0.84.2"
|
||||
"@earendil-works/pi-coding-agent": "0.84.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
|
|
@ -552,12 +552,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-agent-core": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-telemetry": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-telemetry": "^0.84.3",
|
||||
"diff": "8.0.4",
|
||||
"ignore": "7.0.5",
|
||||
"typebox": "1.3.7",
|
||||
|
|
@ -568,13 +568,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-ai": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.91.1",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1048.0",
|
||||
"@earendil-works/pi-telemetry": "^0.84.2",
|
||||
"@earendil-works/pi-telemetry": "^0.84.3",
|
||||
"@google/genai": "1.52.0",
|
||||
"@smithy/node-http-handler": "4.7.3",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
|
|
@ -599,27 +599,27 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-client": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-protocol": "^0.84.2"
|
||||
"@earendil-works/pi-protocol": "^0.84.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-coding-agent": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws/bedrock-token-generator": "1.1.0",
|
||||
"@earendil-works/pi-agent-core": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-client": "^0.84.2",
|
||||
"@earendil-works/pi-protocol": "^0.84.2",
|
||||
"@earendil-works/pi-tui": "^0.84.2",
|
||||
"@earendil-works/pi-agent-core": "^0.84.3",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-client": "^0.84.3",
|
||||
"@earendil-works/pi-protocol": "^0.84.3",
|
||||
"@earendil-works/pi-tui": "^0.84.3",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"chalk": "5.6.2",
|
||||
"cross-spawn": "7.0.6",
|
||||
|
|
@ -647,8 +647,8 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-protocol": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"typebox": "1.3.7"
|
||||
|
|
@ -658,16 +658,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-telemetry": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-tui": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "1.6.0",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-coding-agent-install",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"private": true,
|
||||
"description": "Lockfile root used by the Pi installer and updater.",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-coding-agent": "0.84.2"
|
||||
"@earendil-works/pi-coding-agent": "0.84.3"
|
||||
},
|
||||
"overrides": {
|
||||
"protobufjs": "7.6.5",
|
||||
|
|
|
|||
46
packages/coding-agent/npm-shrinkwrap.json
generated
46
packages/coding-agent/npm-shrinkwrap.json
generated
|
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-coding-agent",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@earendil-works/pi-coding-agent",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws/bedrock-token-generator": "1.1.0",
|
||||
"@earendil-works/pi-agent-core": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-client": "^0.84.2",
|
||||
"@earendil-works/pi-protocol": "^0.84.2",
|
||||
"@earendil-works/pi-tui": "^0.84.2",
|
||||
"@earendil-works/pi-agent-core": "^0.84.3",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-client": "^0.84.3",
|
||||
"@earendil-works/pi-protocol": "^0.84.3",
|
||||
"@earendil-works/pi-tui": "^0.84.3",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"chalk": "5.6.2",
|
||||
"cross-spawn": "7.0.6",
|
||||
|
|
@ -579,12 +579,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-agent-core": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-telemetry": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-telemetry": "^0.84.3",
|
||||
"diff": "8.0.4",
|
||||
"ignore": "7.0.5",
|
||||
"typebox": "1.3.7",
|
||||
|
|
@ -595,13 +595,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-ai": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.91.1",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1048.0",
|
||||
"@earendil-works/pi-telemetry": "^0.84.2",
|
||||
"@earendil-works/pi-telemetry": "^0.84.3",
|
||||
"@google/genai": "1.52.0",
|
||||
"@smithy/node-http-handler": "4.7.3",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
|
|
@ -626,19 +626,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-client": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-protocol": "^0.84.2"
|
||||
"@earendil-works/pi-protocol": "^0.84.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-protocol": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"typebox": "1.3.7"
|
||||
|
|
@ -648,16 +648,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-telemetry": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-tui": {
|
||||
"version": "0.84.2",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz",
|
||||
"version": "0.84.3",
|
||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.3.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "1.6.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-coding-agent",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"type": "module",
|
||||
"piConfig": {
|
||||
|
|
@ -45,11 +45,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@aws/bedrock-token-generator": "1.1.0",
|
||||
"@earendil-works/pi-agent-core": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-client": "^0.84.2",
|
||||
"@earendil-works/pi-protocol": "^0.84.2",
|
||||
"@earendil-works/pi-tui": "^0.84.2",
|
||||
"@earendil-works/pi-agent-core": "^0.84.3",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-client": "^0.84.3",
|
||||
"@earendil-works/pi-protocol": "^0.84.3",
|
||||
"@earendil-works/pi-tui": "^0.84.3",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"chalk": "5.6.2",
|
||||
"cross-spawn": "7.0.6",
|
||||
|
|
|
|||
|
|
@ -435,12 +435,13 @@ ${chalk.bold("Environment Variables:")}
|
|||
PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
|
||||
|
||||
${chalk.bold("Built-in Tool Names:")}
|
||||
read - Read file contents
|
||||
bash - Execute bash commands
|
||||
edit - Edit files with find/replace
|
||||
write - Write files (creates/overwrites)
|
||||
grep - Search file contents (read-only, off by default)
|
||||
find - Find files by glob pattern (read-only, off by default)
|
||||
ls - List directory contents (read-only, off by default)
|
||||
read - Read file contents
|
||||
bash - Execute bash commands
|
||||
powershell - Execute PowerShell commands on Windows
|
||||
edit - Edit files with find/replace
|
||||
write - Write files (creates/overwrites)
|
||||
grep - Search file contents (read-only, off by default)
|
||||
find - Find files by glob pattern (read-only, off by default)
|
||||
ls - List directory contents (read-only, off by default)
|
||||
`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
createCustomMessage,
|
||||
} from "../messages.ts";
|
||||
import type { ReadonlySessionManager, SessionEntry } from "../session-manager.ts";
|
||||
import { completeSummarization, estimateTokens } from "./compaction.ts";
|
||||
import { completeSummarization, estimateTokens, getSummarizationFailure } from "./compaction.ts";
|
||||
import {
|
||||
computeFileLists,
|
||||
createFileOps,
|
||||
|
|
@ -354,8 +354,9 @@ export async function generateBranchSummary(
|
|||
if (response.stopReason === "aborted") {
|
||||
return { aborted: true };
|
||||
}
|
||||
if (response.stopReason === "error") {
|
||||
return { error: response.errorMessage || "Summarization failed" };
|
||||
const failure = getSummarizationFailure(response, "Branch summarization");
|
||||
if (failure) {
|
||||
return { error: failure };
|
||||
}
|
||||
if (response.content.some((block) => block.type === "toolCall")) {
|
||||
return { error: "Branch summarization attempted to call a tool" };
|
||||
|
|
|
|||
|
|
@ -538,6 +538,20 @@ const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation mes
|
|||
|
||||
${UPDATE_SUMMARIZATION_INSTRUCTIONS}`;
|
||||
|
||||
/**
|
||||
* Returns an error message when a summarization response cannot safely be persisted.
|
||||
* A length stop contains partial text and must not become a session checkpoint.
|
||||
*/
|
||||
export function getSummarizationFailure(response: AssistantMessage, label: string): string | undefined {
|
||||
if (response.stopReason === "error") {
|
||||
return `${label} failed: ${response.errorMessage || "Unknown error"}`;
|
||||
}
|
||||
if (response.stopReason === "length") {
|
||||
return `${label} failed: generation hit the token cap and the summary is incomplete`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createSummarizationOptions(
|
||||
model: Model<any>,
|
||||
maxTokens: number,
|
||||
|
|
@ -699,8 +713,9 @@ export async function generateSummaryWithUsage(
|
|||
callbacks,
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
|
||||
const failure = getSummarizationFailure(response, "Summarization");
|
||||
if (failure) {
|
||||
throw new Error(failure);
|
||||
}
|
||||
if (response.content.some((block) => block.type === "toolCall")) {
|
||||
throw new Error("Summarization attempted to call a tool");
|
||||
|
|
@ -983,8 +998,9 @@ async function generateTurnPrefixSummary(
|
|||
callbacks,
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
|
||||
const failure = getSummarizationFailure(response, "Turn prefix summarization");
|
||||
if (failure) {
|
||||
throw new Error(failure);
|
||||
}
|
||||
if (response.content.some((block) => block.type === "toolCall")) {
|
||||
throw new Error("Turn prefix summarization attempted to call a tool");
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ export type {
|
|||
MessageUpdateEvent,
|
||||
ModelSelectEvent,
|
||||
ModelSelectSource,
|
||||
PowerShellToolCallEvent,
|
||||
PowerShellToolResultEvent,
|
||||
ProjectTrustContext,
|
||||
ProjectTrustEvent,
|
||||
ProjectTrustEventDecision,
|
||||
|
|
@ -180,6 +182,7 @@ export {
|
|||
isFindToolResult,
|
||||
isGrepToolResult,
|
||||
isLsToolResult,
|
||||
isPowerShellToolResult,
|
||||
isReadToolResult,
|
||||
isToolCallEventType,
|
||||
isWriteToolResult,
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ import type {
|
|||
GrepToolInput,
|
||||
LsToolDetails,
|
||||
LsToolInput,
|
||||
PowerShellToolDetails,
|
||||
PowerShellToolInput,
|
||||
ReadToolDetails,
|
||||
ReadToolInput,
|
||||
WriteToolInput,
|
||||
|
|
@ -876,6 +878,11 @@ export interface BashToolCallEvent extends ToolCallEventBase {
|
|||
input: BashToolInput;
|
||||
}
|
||||
|
||||
export interface PowerShellToolCallEvent extends ToolCallEventBase {
|
||||
toolName: "powershell";
|
||||
input: PowerShellToolInput;
|
||||
}
|
||||
|
||||
export interface ReadToolCallEvent extends ToolCallEventBase {
|
||||
toolName: "read";
|
||||
input: ReadToolInput;
|
||||
|
|
@ -919,6 +926,7 @@ export interface CustomToolCallEvent extends ToolCallEventBase {
|
|||
*/
|
||||
export type ToolCallEvent =
|
||||
| BashToolCallEvent
|
||||
| PowerShellToolCallEvent
|
||||
| ReadToolCallEvent
|
||||
| EditToolCallEvent
|
||||
| WriteToolCallEvent
|
||||
|
|
@ -942,6 +950,11 @@ export interface BashToolResultEvent extends ToolResultEventBase {
|
|||
details: BashToolDetails | undefined;
|
||||
}
|
||||
|
||||
export interface PowerShellToolResultEvent extends ToolResultEventBase {
|
||||
toolName: "powershell";
|
||||
details: PowerShellToolDetails | undefined;
|
||||
}
|
||||
|
||||
export interface ReadToolResultEvent extends ToolResultEventBase {
|
||||
toolName: "read";
|
||||
details: ReadToolDetails | undefined;
|
||||
|
|
@ -980,6 +993,7 @@ export interface CustomToolResultEvent extends ToolResultEventBase {
|
|||
/** Fired after a tool executes. Can modify result. */
|
||||
export type ToolResultEvent =
|
||||
| BashToolResultEvent
|
||||
| PowerShellToolResultEvent
|
||||
| ReadToolResultEvent
|
||||
| EditToolResultEvent
|
||||
| WriteToolResultEvent
|
||||
|
|
@ -992,6 +1006,9 @@ export type ToolResultEvent =
|
|||
export function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {
|
||||
return e.toolName === "bash";
|
||||
}
|
||||
export function isPowerShellToolResult(e: ToolResultEvent): e is PowerShellToolResultEvent {
|
||||
return e.toolName === "powershell";
|
||||
}
|
||||
export function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {
|
||||
return e.toolName === "read";
|
||||
}
|
||||
|
|
@ -1032,6 +1049,7 @@ export function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {
|
|||
* CustomToolCallEvent.toolName is `string` which overlaps with all literals.
|
||||
*/
|
||||
export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is BashToolCallEvent;
|
||||
export function isToolCallEventType(toolName: "powershell", event: ToolCallEvent): event is PowerShellToolCallEvent;
|
||||
export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent;
|
||||
export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent;
|
||||
export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
createFindTool,
|
||||
createGrepTool,
|
||||
createLsTool,
|
||||
createPowerShellTool,
|
||||
createReadOnlyTools,
|
||||
createReadTool,
|
||||
createWriteTool,
|
||||
|
|
@ -125,6 +126,7 @@ export {
|
|||
createGrepTool,
|
||||
createFindTool,
|
||||
createLsTool,
|
||||
createPowerShellTool,
|
||||
};
|
||||
|
||||
// Helper Functions
|
||||
|
|
|
|||
|
|
@ -95,14 +95,21 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
|
|||
};
|
||||
|
||||
const hasBash = tools.includes("bash");
|
||||
const hasPowerShell = tools.includes("powershell");
|
||||
const hasGrep = tools.includes("grep");
|
||||
const hasFind = tools.includes("find");
|
||||
const hasLs = tools.includes("ls");
|
||||
const hasRead = tools.includes("read");
|
||||
|
||||
// File exploration guidelines
|
||||
if (hasBash && !hasGrep && !hasFind && !hasLs) {
|
||||
addGuideline("Use bash for file operations like ls, rg, find");
|
||||
if ((hasBash || hasPowerShell) && !hasGrep && !hasFind && !hasLs) {
|
||||
if (hasBash && hasPowerShell) {
|
||||
addGuideline("Use bash or PowerShell for file operations like listing, searching, and finding files");
|
||||
} else if (hasPowerShell) {
|
||||
addGuideline("Use PowerShell for file operations like listing, searching, and finding files");
|
||||
} else {
|
||||
addGuideline("Use bash for file operations like ls, rg, find");
|
||||
}
|
||||
}
|
||||
|
||||
for (const guideline of promptGuidelines ?? []) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
getShellConfig,
|
||||
getShellEnv,
|
||||
killProcessTree,
|
||||
type ShellConfig,
|
||||
trackDetachedChildPid,
|
||||
untrackDetachedChildPid,
|
||||
} from "../../utils/shell.ts";
|
||||
|
|
@ -39,7 +40,7 @@ function resolveTimeoutMs(timeout: number | undefined): number | undefined {
|
|||
}
|
||||
|
||||
const bashSchema = Type.Object({
|
||||
command: Type.String({ description: "Bash command to execute" }),
|
||||
command: Type.String({ description: "Shell command to execute" }),
|
||||
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
||||
});
|
||||
|
||||
|
|
@ -79,24 +80,19 @@ export interface BashOperations {
|
|||
) => Promise<{ exitCode: number | null }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create bash operations using pi's built-in local shell execution backend.
|
||||
*
|
||||
* This is useful for extensions that intercept user_bash and still want pi's
|
||||
* standard local shell behavior while wrapping or rewriting commands.
|
||||
*/
|
||||
export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {
|
||||
/** Shared process execution used by the built-in shell tools. */
|
||||
export function createLocalShellOperations(shellName: string, resolveShellConfig: () => ShellConfig): BashOperations {
|
||||
return {
|
||||
exec: async (command, cwd, { onData, signal, timeout, env }) => {
|
||||
const timeoutMs = resolveTimeoutMs(timeout);
|
||||
if (signal?.aborted) {
|
||||
throw new Error("aborted");
|
||||
}
|
||||
const shellConfig = getShellConfig(options?.shellPath);
|
||||
const shellConfig = resolveShellConfig();
|
||||
try {
|
||||
await fsAccess(cwd, constants.F_OK);
|
||||
} catch {
|
||||
throw new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`);
|
||||
throw new Error(`Working directory does not exist: ${cwd}\nCannot execute ${shellName} commands.`);
|
||||
}
|
||||
|
||||
const commandFromStdin = shellConfig.commandTransport === "stdin";
|
||||
|
|
@ -153,6 +149,16 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create bash operations using pi's built-in local shell execution backend.
|
||||
*
|
||||
* This is useful for extensions that intercept user_bash and still want pi's
|
||||
* standard local shell behavior while wrapping or rewriting commands.
|
||||
*/
|
||||
export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {
|
||||
return createLocalShellOperations("bash", () => getShellConfig(options?.shellPath));
|
||||
}
|
||||
|
||||
export interface BashSpawnContext {
|
||||
command: string;
|
||||
cwd: string;
|
||||
|
|
@ -205,7 +211,7 @@ export interface BashToolOptions {
|
|||
const BASH_PREVIEW_LINES = 5;
|
||||
const BASH_UPDATE_THROTTLE_MS = 100;
|
||||
|
||||
type BashRenderState = {
|
||||
export type BashRenderState = {
|
||||
startedAt: number | undefined;
|
||||
endedAt: number | undefined;
|
||||
interval: NodeJS.Timeout | undefined;
|
||||
|
|
@ -229,12 +235,12 @@ function formatDuration(ms: number): string {
|
|||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
function formatBashCall(args: { command?: string; timeout?: number } | undefined): string {
|
||||
function formatShellCall(args: { command?: string; timeout?: number } | undefined, prompt: string): string {
|
||||
const command = str(args?.command);
|
||||
const timeout = args?.timeout as number | undefined;
|
||||
const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
|
||||
const commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg("toolOutput", "...");
|
||||
return theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;
|
||||
return theme.fg("toolTitle", theme.bold(`${prompt} ${commandDisplay}`)) + timeoutSuffix;
|
||||
}
|
||||
|
||||
function rebuildBashResultRenderComponent(
|
||||
|
|
@ -319,8 +325,19 @@ function rebuildBashResultRenderComponent(
|
|||
}
|
||||
}
|
||||
|
||||
export function createBashToolDefinition(
|
||||
export interface ShellToolConfig {
|
||||
name: string;
|
||||
label: string;
|
||||
shellName: string;
|
||||
prompt: string;
|
||||
promptSnippet: string;
|
||||
promptGuidelines?: readonly string[];
|
||||
tempFilePrefix: string;
|
||||
}
|
||||
|
||||
export function createShellToolDefinition(
|
||||
cwd: string,
|
||||
config: ShellToolConfig,
|
||||
options?: BashToolOptions,
|
||||
): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {
|
||||
const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });
|
||||
|
|
@ -328,11 +345,11 @@ export function createBashToolDefinition(
|
|||
const exposeSessionEnvironment = options?.exposeSessionEnvironment ?? true;
|
||||
const spawnHook = options?.spawnHook;
|
||||
return {
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
||||
promptSnippet: bashToolSystemPromptContribution.snippet,
|
||||
promptGuidelines: exposeSessionEnvironment ? [...bashToolSystemPromptContribution.guidelines] : undefined,
|
||||
name: config.name,
|
||||
label: config.label,
|
||||
description: `Execute a ${config.shellName} command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
||||
promptSnippet: config.promptSnippet,
|
||||
promptGuidelines: exposeSessionEnvironment && config.promptGuidelines ? [...config.promptGuidelines] : undefined,
|
||||
parameters: bashSchema,
|
||||
constrainedSampling: getExperimentalToolSampling(),
|
||||
async execute(
|
||||
|
|
@ -344,7 +361,7 @@ export function createBashToolDefinition(
|
|||
) {
|
||||
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
||||
const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook, exposeSessionEnvironment, ctx);
|
||||
const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" });
|
||||
const output = new OutputAccumulator({ tempFilePrefix: config.tempFilePrefix });
|
||||
let acceptingOutput = true;
|
||||
let updateTimer: NodeJS.Timeout | undefined;
|
||||
let updateDirty = false;
|
||||
|
|
@ -468,7 +485,7 @@ export function createBashToolDefinition(
|
|||
state.endedAt = undefined;
|
||||
}
|
||||
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
||||
text.setText(formatBashCall(args));
|
||||
text.setText(formatShellCall(args, config.prompt));
|
||||
return text;
|
||||
},
|
||||
renderResult(result, options, _theme, context) {
|
||||
|
|
@ -499,6 +516,23 @@ export function createBashToolDefinition(
|
|||
};
|
||||
}
|
||||
|
||||
const bashToolConfig: ShellToolConfig = {
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
shellName: "bash",
|
||||
prompt: "$",
|
||||
promptSnippet: bashToolSystemPromptContribution.snippet,
|
||||
promptGuidelines: bashToolSystemPromptContribution.guidelines,
|
||||
tempFilePrefix: "pi-bash",
|
||||
};
|
||||
|
||||
export function createBashToolDefinition(
|
||||
cwd: string,
|
||||
options?: BashToolOptions,
|
||||
): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {
|
||||
return createShellToolDefinition(cwd, bashToolConfig, options);
|
||||
}
|
||||
|
||||
export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
|
||||
const definition = createBashToolDefinition(cwd, options);
|
||||
const tool = wrapToolDefinition(definition);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,17 @@ export {
|
|||
type LsToolInput,
|
||||
type LsToolOptions,
|
||||
} from "./ls.ts";
|
||||
export {
|
||||
createLocalPowerShellOperations,
|
||||
createPowerShellTool,
|
||||
createPowerShellToolDefinition,
|
||||
type PowerShellOperations,
|
||||
type PowerShellSpawnContext,
|
||||
type PowerShellSpawnHook,
|
||||
type PowerShellToolDetails,
|
||||
type PowerShellToolInput,
|
||||
type PowerShellToolOptions,
|
||||
} from "./powershell.ts";
|
||||
export {
|
||||
createReadTool,
|
||||
createReadToolDefinition,
|
||||
|
|
@ -75,17 +86,28 @@ import { createEditTool, createEditToolDefinition, type EditToolOptions } from "
|
|||
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 { createPowerShellTool, createPowerShellToolDefinition, type PowerShellToolOptions } from "./powershell.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>;
|
||||
export type ToolName = "read" | "bash" | "edit" | "write" | "grep" | "find" | "ls";
|
||||
export const allToolNames: Set<ToolName> = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
||||
export type ToolName = "read" | "bash" | "powershell" | "edit" | "write" | "grep" | "find" | "ls";
|
||||
export const allToolNames: Set<ToolName> = new Set([
|
||||
"read",
|
||||
"bash",
|
||||
"powershell",
|
||||
"edit",
|
||||
"write",
|
||||
"grep",
|
||||
"find",
|
||||
"ls",
|
||||
]);
|
||||
|
||||
export interface ToolsOptions {
|
||||
read?: ReadToolOptions;
|
||||
bash?: BashToolOptions;
|
||||
powershell?: PowerShellToolOptions;
|
||||
write?: WriteToolOptions;
|
||||
edit?: EditToolOptions;
|
||||
grep?: GrepToolOptions;
|
||||
|
|
@ -99,6 +121,8 @@ export function createToolDefinition(toolName: ToolName, cwd: string, options?:
|
|||
return createReadToolDefinition(cwd, options?.read);
|
||||
case "bash":
|
||||
return createBashToolDefinition(cwd, options?.bash);
|
||||
case "powershell":
|
||||
return createPowerShellToolDefinition(cwd, options?.powershell);
|
||||
case "edit":
|
||||
return createEditToolDefinition(cwd, options?.edit);
|
||||
case "write":
|
||||
|
|
@ -120,6 +144,8 @@ export function createTool(toolName: ToolName, cwd: string, options?: ToolsOptio
|
|||
return createReadTool(cwd, options?.read);
|
||||
case "bash":
|
||||
return createBashTool(cwd, options?.bash);
|
||||
case "powershell":
|
||||
return createPowerShellTool(cwd, options?.powershell);
|
||||
case "edit":
|
||||
return createEditTool(cwd, options?.edit);
|
||||
case "write":
|
||||
|
|
@ -157,6 +183,7 @@ export function createAllToolDefinitions(cwd: string, options?: ToolsOptions): R
|
|||
return {
|
||||
read: createReadToolDefinition(cwd, options?.read),
|
||||
bash: createBashToolDefinition(cwd, options?.bash),
|
||||
powershell: createPowerShellToolDefinition(cwd, options?.powershell),
|
||||
edit: createEditToolDefinition(cwd, options?.edit),
|
||||
write: createWriteToolDefinition(cwd, options?.write),
|
||||
grep: createGrepToolDefinition(cwd, options?.grep),
|
||||
|
|
@ -187,6 +214,7 @@ export function createAllTools(cwd: string, options?: ToolsOptions): Record<Tool
|
|||
return {
|
||||
read: createReadTool(cwd, options?.read),
|
||||
bash: createBashTool(cwd, options?.bash),
|
||||
powershell: createPowerShellTool(cwd, options?.powershell),
|
||||
edit: createEditTool(cwd, options?.edit),
|
||||
write: createWriteTool(cwd, options?.write),
|
||||
grep: createGrepTool(cwd, options?.grep),
|
||||
|
|
|
|||
67
packages/coding-agent/src/core/tools/powershell.ts
Normal file
67
packages/coding-agent/src/core/tools/powershell.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { getPowerShellConfig } from "../../utils/shell.ts";
|
||||
import {
|
||||
type BashOperations,
|
||||
type BashSpawnContext,
|
||||
type BashSpawnHook,
|
||||
type BashToolDetails,
|
||||
type BashToolInput,
|
||||
type BashToolOptions,
|
||||
type createBashTool,
|
||||
createLocalShellOperations,
|
||||
createShellToolDefinition,
|
||||
type ShellToolConfig,
|
||||
} from "./bash.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
|
||||
const UTF8_OUTPUT_PREFIX = "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n";
|
||||
|
||||
export const powershellToolSystemPromptContribution = {
|
||||
snippet: "Execute PowerShell commands",
|
||||
guidelines: ["You can inspect PI_* environment variables for current model and session details."],
|
||||
} as const;
|
||||
|
||||
export type PowerShellOperations = BashOperations;
|
||||
export type PowerShellSpawnContext = BashSpawnContext;
|
||||
export type PowerShellSpawnHook = BashSpawnHook;
|
||||
export type PowerShellToolDetails = BashToolDetails;
|
||||
export type PowerShellToolInput = BashToolInput;
|
||||
|
||||
export interface PowerShellToolOptions
|
||||
extends Pick<BashToolOptions, "operations" | "exposeSessionEnvironment" | "spawnHook"> {}
|
||||
|
||||
export function createLocalPowerShellOperations(): PowerShellOperations {
|
||||
const operations = createLocalShellOperations("PowerShell", getPowerShellConfig);
|
||||
return {
|
||||
exec: (command, cwd, options) => operations.exec(`${UTF8_OUTPUT_PREFIX}${command}`, cwd, options),
|
||||
};
|
||||
}
|
||||
|
||||
const powershellToolConfig: ShellToolConfig = {
|
||||
name: "powershell",
|
||||
label: "powershell",
|
||||
shellName: "PowerShell",
|
||||
prompt: "PS>",
|
||||
promptSnippet: powershellToolSystemPromptContribution.snippet,
|
||||
promptGuidelines: powershellToolSystemPromptContribution.guidelines,
|
||||
tempFilePrefix: "pi-powershell",
|
||||
};
|
||||
|
||||
export function createPowerShellToolDefinition(
|
||||
cwd: string,
|
||||
options?: PowerShellToolOptions,
|
||||
): ReturnType<typeof createShellToolDefinition> {
|
||||
return createShellToolDefinition(cwd, powershellToolConfig, {
|
||||
...options,
|
||||
operations: options?.operations ?? createLocalPowerShellOperations(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createPowerShellTool(cwd: string, options?: PowerShellToolOptions): ReturnType<typeof createBashTool> {
|
||||
const definition = createPowerShellToolDefinition(cwd, options);
|
||||
const tool = wrapToolDefinition(definition);
|
||||
Object.assign(tool, {
|
||||
promptSnippet: definition.promptSnippet,
|
||||
promptGuidelines: definition.promptGuidelines,
|
||||
});
|
||||
return tool;
|
||||
}
|
||||
|
|
@ -28,6 +28,10 @@ export interface LlamaModelsResponse {
|
|||
object?: string;
|
||||
}
|
||||
|
||||
export interface LlamaServerProps {
|
||||
models_autoload?: boolean;
|
||||
}
|
||||
|
||||
export interface LlamaModelEvent {
|
||||
model: string;
|
||||
event: string;
|
||||
|
|
@ -189,6 +193,13 @@ export class LlamaClient {
|
|||
return data;
|
||||
}
|
||||
|
||||
async props(options: { signal?: AbortSignal } = {}): Promise<LlamaServerProps> {
|
||||
const payload = await this.request("/props", { signal: options.signal });
|
||||
if (typeof payload !== "object" || payload === null) return {};
|
||||
const { models_autoload: modelsAutoload } = payload as Record<string, unknown>;
|
||||
return typeof modelsAutoload === "boolean" ? { models_autoload: modelsAutoload } : {};
|
||||
}
|
||||
|
||||
async load(model: string, signal?: AbortSignal): Promise<void> {
|
||||
await this.request("/models/load", { method: "POST", body: JSON.stringify({ model }), signal });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,25 @@ async function resolveServerUrl(
|
|||
return configured ? normalizeLlamaServerUrl(configured) : undefined;
|
||||
}
|
||||
|
||||
function modelIsSelectable(model: LlamaModelInfo): boolean {
|
||||
function modelIsSelectable(model: LlamaModelInfo, routerAutoload: boolean): boolean {
|
||||
if (model.status.value === "loaded") return true;
|
||||
// llama.cpp reports idle-slept models as "sleeping"; requests wake them automatically.
|
||||
return model.status.value === "loaded" || model.status.value === "sleeping";
|
||||
if (model.status.value === "sleeping") return true;
|
||||
// Unloaded presets are routable only when llama.cpp router autoload can load them on first use.
|
||||
return routerAutoload && model.status.value === "unloaded" && !model.status.failed && model.source === "preset";
|
||||
}
|
||||
|
||||
async function routerAutoloadEnabled(
|
||||
client: LlamaClient,
|
||||
catalog: readonly LlamaModelInfo[],
|
||||
signal: AbortSignal,
|
||||
): Promise<boolean> {
|
||||
if (!catalog.some((model) => model.status.value === "unloaded" && model.source === "preset")) return false;
|
||||
try {
|
||||
return (await client.props({ signal })).models_autoload === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function toPiModel(model: LlamaModelInfo, serverUrl: string): Model<"openai-completions"> {
|
||||
|
|
@ -57,14 +73,20 @@ function toPiModel(model: LlamaModelInfo, serverUrl: string): Model<"openai-comp
|
|||
|
||||
export interface LlamaProviderController {
|
||||
provider: Provider<"openai-completions">;
|
||||
setCatalog(models: readonly LlamaModelInfo[], serverUrl: string): void;
|
||||
setCatalog(models: readonly LlamaModelInfo[], serverUrl: string, options?: { routerAutoload?: boolean }): void;
|
||||
}
|
||||
|
||||
export function createLlamaProvider(): LlamaProviderController {
|
||||
let models: readonly Model<"openai-completions">[] = [];
|
||||
|
||||
const setCatalog = (catalog: readonly LlamaModelInfo[], serverUrl: string): void => {
|
||||
models = catalog.filter((model) => modelIsSelectable(model)).map((model) => toPiModel(model, serverUrl));
|
||||
const setCatalog = (
|
||||
catalog: readonly LlamaModelInfo[],
|
||||
serverUrl: string,
|
||||
options: { routerAutoload?: boolean } = {},
|
||||
): void => {
|
||||
models = catalog
|
||||
.filter((model) => modelIsSelectable(model, options.routerAutoload === true))
|
||||
.map((model) => toPiModel(model, serverUrl));
|
||||
};
|
||||
|
||||
const provider: Provider<"openai-completions"> = {
|
||||
|
|
@ -135,10 +157,13 @@ export function createLlamaProvider(): LlamaProviderController {
|
|||
if (!context.allowNetwork || context.signal.aborted || context.credential?.type !== "api_key") return;
|
||||
const serverUrl = credentialServerUrl(context.credential);
|
||||
if (!serverUrl) return;
|
||||
const catalog = await new LlamaClient(serverUrl, context.credential.key).list({ signal: context.signal });
|
||||
const client = new LlamaClient(serverUrl, context.credential.key);
|
||||
const catalog = await client.list({ signal: context.signal });
|
||||
if (context.signal.aborted) return;
|
||||
const routerAutoload = await routerAutoloadEnabled(client, catalog, context.signal);
|
||||
if (context.signal.aborted) return;
|
||||
const refreshed = catalog
|
||||
.filter((model) => modelIsSelectable(model))
|
||||
.filter((model) => modelIsSelectable(model, routerAutoload))
|
||||
.map((model) => toPiModel(model, serverUrl));
|
||||
await context.publish({
|
||||
persist: { models: refreshed, checkedAt: Date.now() },
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ export type {
|
|||
MessageRenderOptions,
|
||||
MessageStartEvent,
|
||||
MessageUpdateEvent,
|
||||
PowerShellToolCallEvent,
|
||||
ProjectTrustContext,
|
||||
ProjectTrustEvent,
|
||||
ProjectTrustEventDecision,
|
||||
|
|
@ -159,6 +160,7 @@ export {
|
|||
isFindToolResult,
|
||||
isGrepToolResult,
|
||||
isLsToolResult,
|
||||
isPowerShellToolResult,
|
||||
isReadToolResult,
|
||||
isToolCallEventType,
|
||||
isWriteToolResult,
|
||||
|
|
@ -218,6 +220,7 @@ export {
|
|||
createFindTool,
|
||||
createGrepTool,
|
||||
createLsTool,
|
||||
createPowerShellTool,
|
||||
createReadOnlyTools,
|
||||
createReadTool,
|
||||
createWriteTool,
|
||||
|
|
@ -285,7 +288,9 @@ export {
|
|||
createFindToolDefinition,
|
||||
createGrepToolDefinition,
|
||||
createLocalBashOperations,
|
||||
createLocalPowerShellOperations,
|
||||
createLsToolDefinition,
|
||||
createPowerShellToolDefinition,
|
||||
createReadToolDefinition,
|
||||
createWriteToolDefinition,
|
||||
DEFAULT_MAX_BYTES,
|
||||
|
|
@ -307,6 +312,12 @@ export {
|
|||
type LsToolDetails,
|
||||
type LsToolInput,
|
||||
type LsToolOptions,
|
||||
type PowerShellOperations,
|
||||
type PowerShellSpawnContext,
|
||||
type PowerShellSpawnHook,
|
||||
type PowerShellToolDetails,
|
||||
type PowerShellToolInput,
|
||||
type PowerShellToolOptions,
|
||||
type ReadOperations,
|
||||
type ReadToolDetails,
|
||||
type ReadToolInput,
|
||||
|
|
@ -405,4 +416,4 @@ export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.ts";
|
|||
export { convertToPng } from "./utils/image-convert.ts";
|
||||
export { formatDimensionNote, type ResizedImage, resizeImage } from "./utils/image-resize.ts";
|
||||
// Shell utilities
|
||||
export { getShellConfig } from "./utils/shell.ts";
|
||||
export { getPowerShellConfig, getShellConfig } from "./utils/shell.ts";
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ import { builtInExtensions } from "./extensions/index.ts";
|
|||
import { runMigrations, showDeprecationWarnings } from "./migrations.ts";
|
||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts";
|
||||
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts";
|
||||
import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts";
|
||||
import { cleanupManagedInstall, handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts";
|
||||
import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts";
|
||||
import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts";
|
||||
|
||||
|
|
@ -573,6 +573,7 @@ export async function main(args: string[], options?: MainOptions) {
|
|||
if (process.platform === "win32") {
|
||||
cleanupWindowsSelfUpdateQuarantine(getPackageDir());
|
||||
}
|
||||
cleanupManagedInstall();
|
||||
|
||||
const cwd = process.cwd();
|
||||
const agentDir = getAgentDir();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,17 @@
|
|||
import { join } from "node:path";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import lockfile from "proper-lockfile";
|
||||
import { selectConfig } from "./cli/config-selector.ts";
|
||||
import { createProjectTrustContext } from "./cli/project-trust.ts";
|
||||
import {
|
||||
|
|
@ -23,7 +34,9 @@ import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts";
|
|||
import { DefaultResourceLoader } from "./core/resource-loader.ts";
|
||||
import { SettingsManager } from "./core/settings-manager.ts";
|
||||
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts";
|
||||
import { spawnProcess } from "./utils/child-process.ts";
|
||||
import { spawnProcess, spawnProcessSync, waitForChildProcess } from "./utils/child-process.ts";
|
||||
import { canonicalizePath, getCwdRelativePath } from "./utils/paths.ts";
|
||||
import { getPiUserAgent } from "./utils/pi-user-agent.ts";
|
||||
import { formatVersionCheckError, getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts";
|
||||
import {
|
||||
cleanupWindowsSelfUpdateQuarantine,
|
||||
|
|
@ -34,6 +47,179 @@ export type PackageCommand = "install" | "remove" | "update" | "list";
|
|||
|
||||
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string } | { type: "models" };
|
||||
|
||||
const DEFAULT_INSTALLER_API_BASE = "https://pi.dev/api/installer/releases";
|
||||
const MANAGED_INSTALL_MARKER = "managed-install.json";
|
||||
const MANAGED_RELEASE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
|
||||
function getActiveManagedInstallRoot(): string | undefined {
|
||||
const configuredRoot = process.env.PI_MANAGED_INSTALL_ROOT?.trim();
|
||||
if (!configuredRoot) return undefined;
|
||||
|
||||
const managedRoot = resolve(configuredRoot);
|
||||
const releasesDir = canonicalizePath(join(managedRoot, "releases"));
|
||||
// The launcher environment is inherited by child processes. Do not classify a
|
||||
// source checkout or another Pi installation launched from managed Pi as managed.
|
||||
if (getCwdRelativePath(canonicalizePath(getPackageDir()), releasesDir) === undefined) return undefined;
|
||||
|
||||
const markerPath = join(managedRoot, MANAGED_INSTALL_MARKER);
|
||||
try {
|
||||
const marker = JSON.parse(readFileSync(markerPath, "utf8")) as {
|
||||
kind?: unknown;
|
||||
layout?: unknown;
|
||||
schemaVersion?: unknown;
|
||||
};
|
||||
if (marker.kind !== "pi-managed-install" || marker.schemaVersion !== 1 || marker.layout !== "releases-v1") {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
throw new Error(`Managed install marker is missing or invalid: ${markerPath}`);
|
||||
}
|
||||
|
||||
return managedRoot;
|
||||
}
|
||||
|
||||
async function fetchInstallerArtifact(url: string, label: string): Promise<string> {
|
||||
const response = await fetch(url, { headers: { "User-Agent": getPiUserAgent(VERSION) } });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not download managed installer ${label} from ${url}: HTTP ${response.status}`);
|
||||
}
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
async function runManagedNpmCi(stageDir: string): Promise<void> {
|
||||
const args = [
|
||||
"ci",
|
||||
"--ignore-scripts",
|
||||
"--min-release-age=0",
|
||||
"--omit=dev",
|
||||
"--include=optional",
|
||||
"--no-fund",
|
||||
"--no-audit",
|
||||
"--loglevel=error",
|
||||
"--progress=false",
|
||||
];
|
||||
const code = await waitForChildProcess(spawnProcess("npm", args, { cwd: stageDir, stdio: "inherit" }));
|
||||
if (code !== 0) throw new Error(`npm ${args.join(" ")} exited with code ${code ?? "unknown"}`);
|
||||
}
|
||||
|
||||
function verifyManagedRelease(releaseDir: string, expectedVersion: string): void {
|
||||
const binPath = join(
|
||||
releaseDir,
|
||||
"node_modules",
|
||||
".bin",
|
||||
process.platform === "win32" ? `${APP_NAME}.cmd` : APP_NAME,
|
||||
);
|
||||
const result = spawnProcessSync(binPath, ["--version"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
const reason = result.error?.message || result.stderr.trim() || `exit code ${result.status ?? "unknown"}`;
|
||||
throw new Error(`Could not verify managed Pi ${expectedVersion}: ${reason}`);
|
||||
}
|
||||
const installedVersion = result.stdout.trim();
|
||||
if (installedVersion !== expectedVersion) {
|
||||
throw new Error(`Managed Pi smoke test returned version ${installedVersion}; expected ${expectedVersion}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function activateManagedRelease(managedRoot: string, version: string): void {
|
||||
const currentPath = join(managedRoot, "current-version");
|
||||
const temporaryPath = join(managedRoot, `current-version.tmp.${process.pid}-${Date.now()}`);
|
||||
try {
|
||||
writeFileSync(temporaryPath, `${version}\n`);
|
||||
renameSync(temporaryPath, currentPath);
|
||||
} finally {
|
||||
rmSync(temporaryPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupManagedStaging(managedRoot: string): void {
|
||||
const stagingRoot = join(managedRoot, "staging");
|
||||
try {
|
||||
for (const entry of readdirSync(stagingRoot)) {
|
||||
if (entry.startsWith("update-")) {
|
||||
rmSync(join(stagingRoot, entry), { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// The staging directory does not exist yet or is not writable.
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupManagedInstall(): void {
|
||||
let managedRoot: string | undefined;
|
||||
try {
|
||||
managedRoot = getActiveManagedInstallRoot();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!managedRoot) return;
|
||||
|
||||
try {
|
||||
const releaseLock = lockfile.lockSync(join(managedRoot, "update"), { realpath: false });
|
||||
try {
|
||||
cleanupManagedStaging(managedRoot);
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
} catch {
|
||||
// A live update owns the staging directory, or cleanup is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
async function runManagedSelfUpdate(managedRoot: string, version: string): Promise<void> {
|
||||
if (!MANAGED_RELEASE_VERSION_RE.test(version)) {
|
||||
throw new Error(`Invalid managed release version: ${version}`);
|
||||
}
|
||||
|
||||
let releaseLock: () => Promise<void>;
|
||||
try {
|
||||
releaseLock = await lockfile.lock(join(managedRoot, "update"), { realpath: false });
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && "code" in error && error.code === "ELOCKED") {
|
||||
throw new Error("Another managed Pi update is already running.");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let stageDir: string | undefined;
|
||||
try {
|
||||
cleanupManagedStaging(managedRoot);
|
||||
const installerApiBase = (process.env.PI_INSTALLER_API_BASE?.trim() || DEFAULT_INSTALLER_API_BASE).replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
const releaseUrl = `${installerApiBase}/${encodeURIComponent(version)}`;
|
||||
const stagingRoot = join(managedRoot, "staging");
|
||||
const releasesRoot = join(managedRoot, "releases");
|
||||
mkdirSync(releasesRoot, { recursive: true });
|
||||
const releaseDir = join(releasesRoot, version);
|
||||
if (existsSync(releaseDir)) {
|
||||
verifyManagedRelease(releaseDir, version);
|
||||
activateManagedRelease(managedRoot, version);
|
||||
return;
|
||||
}
|
||||
|
||||
mkdirSync(stagingRoot, { recursive: true });
|
||||
stageDir = mkdtempSync(join(stagingRoot, "update-"));
|
||||
const [packageJsonContent, packageLockContent] = await Promise.all([
|
||||
fetchInstallerArtifact(`${releaseUrl}/package.json`, "package.json"),
|
||||
fetchInstallerArtifact(`${releaseUrl}/package-lock.json`, "package-lock.json"),
|
||||
]);
|
||||
writeFileSync(join(stageDir, "package.json"), packageJsonContent);
|
||||
writeFileSync(join(stageDir, "package-lock.json"), packageLockContent);
|
||||
|
||||
await runManagedNpmCi(stageDir);
|
||||
verifyManagedRelease(stageDir, version);
|
||||
renameSync(stageDir, releaseDir);
|
||||
activateManagedRelease(managedRoot, version);
|
||||
} finally {
|
||||
if (stageDir) rmSync(stageDir, { force: true, recursive: true });
|
||||
await releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
const SELF_UPDATE_NOTE_MARKDOWN_THEME: MarkdownTheme = {
|
||||
heading: (text) => chalk.bold(chalk.yellow(text)),
|
||||
link: (text) => chalk.cyan(text),
|
||||
|
|
@ -834,10 +1020,37 @@ export async function handlePackageCommand(
|
|||
}
|
||||
}
|
||||
if (updateTargetIncludesSelf(target)) {
|
||||
const managedInstallRoot = getActiveManagedInstallRoot();
|
||||
if (managedInstallRoot && options.force) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Managed ${APP_NAME} installations do not support --force; rerun the installer to repair this installation.`,
|
||||
),
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
const selfUpdatePlan = await getSelfUpdatePlan(options.force);
|
||||
if (!selfUpdatePlan.shouldRun) {
|
||||
return true;
|
||||
}
|
||||
if (managedInstallRoot) {
|
||||
if (selfUpdatePlan.note) {
|
||||
printSelfUpdateNote(selfUpdatePlan.note);
|
||||
}
|
||||
try {
|
||||
console.log(chalk.dim(`Updating managed ${APP_NAME} installation...`));
|
||||
await runManagedSelfUpdate(managedInstallRoot, selfUpdatePlan.version);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown managed update error";
|
||||
console.error(chalk.red(`Error: ${message}`));
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
console.log(chalk.green(`Updated ${APP_NAME} from ${VERSION} to ${selfUpdatePlan.version}`));
|
||||
return true;
|
||||
}
|
||||
|
||||
const installMethod = detectInstallMethod();
|
||||
if (process.platform === "win32" && installMethod !== "npm" && installMethod !== "pnpm") {
|
||||
console.error(
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ function getBashShellConfig(shell: string): ShellConfig {
|
|||
return isLegacyWslBashPath(shell) ? { shell, args: ["-s"], commandTransport: "stdin" } : { shell, args: ["-c"] };
|
||||
}
|
||||
|
||||
function findBashOnPath(): string | null {
|
||||
function findExecutableOnPath(executable: string): string | null {
|
||||
if (process.platform === "win32") {
|
||||
// Windows: Use 'where' and verify file exists (where can return non-existent paths)
|
||||
try {
|
||||
const result = spawnSync("where", ["bash.exe"], {
|
||||
const result = spawnSync("where", [executable], {
|
||||
encoding: "utf-8",
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
|
|
@ -44,7 +44,7 @@ function findBashOnPath(): string | null {
|
|||
|
||||
// Unix: Use 'which' and trust its output (handles Termux and special filesystems)
|
||||
try {
|
||||
const result = spawnSync("which", ["bash"], { encoding: "utf-8", timeout: 5000 });
|
||||
const result = spawnSync("which", [executable], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
|
||||
if (firstMatch) {
|
||||
|
|
@ -92,7 +92,7 @@ export function getShellConfig(customShellPath?: string): ShellConfig {
|
|||
}
|
||||
|
||||
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
||||
const bashOnPath = findBashOnPath();
|
||||
const bashOnPath = findExecutableOnPath("bash.exe");
|
||||
if (bashOnPath) {
|
||||
return getBashShellConfig(bashOnPath);
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ export function getShellConfig(customShellPath?: string): ShellConfig {
|
|||
return getBashShellConfig("/bin/bash");
|
||||
}
|
||||
|
||||
const bashOnPath = findBashOnPath();
|
||||
const bashOnPath = findExecutableOnPath("bash");
|
||||
if (bashOnPath) {
|
||||
return getBashShellConfig(bashOnPath);
|
||||
}
|
||||
|
|
@ -119,6 +119,22 @@ export function getShellConfig(customShellPath?: string): ShellConfig {
|
|||
return { shell: "sh", args: ["-c"] };
|
||||
}
|
||||
|
||||
export const POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"] as const;
|
||||
|
||||
/** Resolve PowerShell on Windows, preferring PowerShell 7 when available. */
|
||||
export function getPowerShellConfig(): ShellConfig {
|
||||
if (process.platform !== "win32") {
|
||||
throw new Error("The powershell tool is only available on Windows.");
|
||||
}
|
||||
|
||||
const shell = findExecutableOnPath("pwsh.exe") ?? findExecutableOnPath("powershell.exe");
|
||||
if (!shell) {
|
||||
throw new Error("No PowerShell executable found. Install PowerShell or add powershell.exe/pwsh.exe to PATH.");
|
||||
}
|
||||
|
||||
return { shell, args: [...POWERSHELL_ARGS] };
|
||||
}
|
||||
|
||||
export function getShellEnv(): NodeJS.ProcessEnv {
|
||||
const binDir = getBinDir();
|
||||
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
||||
|
|
|
|||
|
|
@ -87,4 +87,28 @@ describe("branch summarization", () => {
|
|||
|
||||
expect(result.error).toBe("Branch summarization attempted to call a tool");
|
||||
});
|
||||
|
||||
it("rejects length-limited branch summaries", async () => {
|
||||
const streamFn: StreamFn = () => {
|
||||
const stream = createAssistantMessageEventStream();
|
||||
queueMicrotask(() =>
|
||||
stream.push({
|
||||
type: "done",
|
||||
reason: "length",
|
||||
message: { ...response([{ type: "text", text: "partial" }]), stopReason: "length" },
|
||||
}),
|
||||
);
|
||||
return stream;
|
||||
};
|
||||
|
||||
const result = await generateBranchSummary(entries, {
|
||||
model,
|
||||
signal: new AbortController().signal,
|
||||
streamFn,
|
||||
});
|
||||
|
||||
expect(result.error).toBe(
|
||||
"Branch summarization failed: generation hit the token cap and the summary is incomplete",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -173,6 +173,39 @@ describe("generateSummary reasoning options", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("rejects a length-limited history summary", async () => {
|
||||
completeSimpleMock.mockResolvedValueOnce({
|
||||
...mockSummaryResponse,
|
||||
stopReason: "length",
|
||||
content: [{ type: "text", text: "partial" }],
|
||||
});
|
||||
|
||||
await expect(generateSummaryWithUsage(messages, createModel(false), 2000, "test-key")).rejects.toThrow(
|
||||
"generation hit the token cap",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a length-limited split-turn summary", async () => {
|
||||
completeSimpleMock.mockResolvedValueOnce({
|
||||
...mockSummaryResponse,
|
||||
stopReason: "length",
|
||||
content: [{ type: "text", text: "partial" }],
|
||||
});
|
||||
const preparation: CompactionPreparation = {
|
||||
firstKeptEntryId: "entry-keep",
|
||||
messagesToSummarize: [],
|
||||
turnPrefixMessages: messages,
|
||||
isSplitTurn: true,
|
||||
tokensBefore: 100,
|
||||
fileOps: { read: new Set(), written: new Set(), edited: new Set() },
|
||||
settings: { enabled: true, reserveTokens: 2000, keepRecentTokens: 20 },
|
||||
};
|
||||
|
||||
await expect(compact(preparation, createModel(false), "test-key")).rejects.toThrow(
|
||||
"generation hit the token cap",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not set reasoning when thinking is off", async () => {
|
||||
await generateSummary(
|
||||
messages,
|
||||
|
|
|
|||
|
|
@ -63,13 +63,22 @@ describe("defaultTools setting", () => {
|
|||
.getAllTools()
|
||||
.map((tool) => tool.name)
|
||||
.sort(),
|
||||
).toEqual(["bash", "edit", "find", "grep", "ls", "read", "write"]);
|
||||
).toEqual(["bash", "edit", "find", "grep", "ls", "powershell", "read", "write"]);
|
||||
expect(session.getActiveToolNames()).toEqual(["grep", "find"]);
|
||||
expect(session.systemPrompt).toContain("- grep:");
|
||||
expect(session.systemPrompt).not.toContain("- read:");
|
||||
session.dispose();
|
||||
});
|
||||
|
||||
it("can select powershell instead of bash", async () => {
|
||||
const session = await createSession(["read", "powershell", "edit", "write"]);
|
||||
|
||||
expect(session.getActiveToolNames()).toEqual(["read", "powershell", "edit", "write"]);
|
||||
expect(session.systemPrompt).toContain("- powershell: Execute PowerShell commands");
|
||||
expect(session.systemPrompt).not.toContain("- bash:");
|
||||
session.dispose();
|
||||
});
|
||||
|
||||
it("keeps extension and SDK custom tools enabled", async () => {
|
||||
const session = await createSession(
|
||||
["grep"],
|
||||
|
|
@ -143,7 +152,7 @@ describe("defaultTools setting", () => {
|
|||
.getAllTools()
|
||||
.map((tool) => tool.name)
|
||||
.sort(),
|
||||
).toEqual(["bash", "edit", "find", "grep", "ls", "read", "write"]);
|
||||
).toEqual(["bash", "edit", "find", "grep", "ls", "powershell", "read", "write"]);
|
||||
expect(session.getActiveToolNames()).toEqual(["ls"]);
|
||||
session.dispose();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
|||
import {
|
||||
createBashToolDefinition,
|
||||
createEditToolDefinition,
|
||||
createPowerShellToolDefinition,
|
||||
createReadToolDefinition,
|
||||
createWriteToolDefinition,
|
||||
} from "../src/core/tools/index.ts";
|
||||
|
|
@ -10,6 +11,7 @@ function createBuiltInTools() {
|
|||
return [
|
||||
createReadToolDefinition(process.cwd()),
|
||||
createBashToolDefinition(process.cwd()),
|
||||
createPowerShellToolDefinition(process.cwd()),
|
||||
createEditToolDefinition(process.cwd()),
|
||||
createWriteToolDefinition(process.cwd()),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -138,6 +138,78 @@ describe("llama.cpp extension", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("exposes unloaded presets only when router autoload is enabled", async () => {
|
||||
let propsRequests = 0;
|
||||
const { url } = await listen((request, response) => {
|
||||
expect(request.headers.authorization).toBe("Bearer local");
|
||||
if (request.url === "/models") {
|
||||
json(response, {
|
||||
data: [
|
||||
{ id: "preset", status: { value: "unloaded" }, source: "preset", meta: { n_ctx: 65536 } },
|
||||
{ id: "failed-preset", status: { value: "unloaded", failed: true }, source: "preset" },
|
||||
{ id: "cache", status: { value: "unloaded" }, source: "cache" },
|
||||
{ id: "models-dir", status: { value: "unloaded" }, source: "models_dir" },
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.url === "/props") {
|
||||
propsRequests++;
|
||||
json(response, { role: "router", models_autoload: true });
|
||||
return;
|
||||
}
|
||||
response.writeHead(404).end();
|
||||
});
|
||||
|
||||
let cachedEntry: ModelsStoreEntry | undefined;
|
||||
const controller = createLlamaProvider();
|
||||
await controller.provider.refreshModels?.({
|
||||
credential: { type: "api_key", key: "local", env: { LLAMA_BASE_URL: url } },
|
||||
stored: undefined,
|
||||
publish: async (publication) => {
|
||||
if (publication.persist !== undefined && publication.persist !== null) {
|
||||
cachedEntry = structuredClone(publication.persist);
|
||||
}
|
||||
publication.update?.();
|
||||
return true;
|
||||
},
|
||||
allowNetwork: true,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
|
||||
expect(propsRequests).toBe(1);
|
||||
expect(controller.provider.getModels().map((model) => model.id)).toEqual(["preset"]);
|
||||
expect(cachedEntry?.models.map((model) => model.id)).toEqual(["preset"]);
|
||||
});
|
||||
|
||||
it("hides unloaded presets when router autoload is disabled", async () => {
|
||||
const { url } = await listen((request, response) => {
|
||||
if (request.url === "/models") {
|
||||
json(response, { data: [{ id: "preset", status: { value: "unloaded" }, source: "preset" }] });
|
||||
return;
|
||||
}
|
||||
if (request.url === "/props") {
|
||||
json(response, { role: "router", models_autoload: false });
|
||||
return;
|
||||
}
|
||||
response.writeHead(404).end();
|
||||
});
|
||||
|
||||
const controller = createLlamaProvider();
|
||||
await controller.provider.refreshModels?.({
|
||||
credential: { type: "api_key", key: "local", env: { LLAMA_BASE_URL: url } },
|
||||
stored: undefined,
|
||||
publish: async (publication) => {
|
||||
publication.update?.();
|
||||
return true;
|
||||
},
|
||||
allowNetwork: true,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
|
||||
expect(controller.provider.getModels()).toEqual([]);
|
||||
});
|
||||
|
||||
it("stays dormant until configured and stores URL plus optional key", async () => {
|
||||
const { provider } = createLlamaProvider();
|
||||
const auth = provider.auth.apiKey!;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,17 @@
|
|||
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join } from "node:path";
|
||||
import lockfile from "proper-lockfile";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
|
||||
import { ModelRuntime } from "../src/core/model-runtime.ts";
|
||||
|
|
@ -29,6 +40,77 @@ describe("package commands", () => {
|
|||
return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`;
|
||||
}
|
||||
|
||||
function prepareManagedInstall(
|
||||
targetVersion: string,
|
||||
npmExitCode = 0,
|
||||
): { managedRoot: string; npmRecordPath: string } {
|
||||
const managedRoot = join(agentDir, "install");
|
||||
const activeRelease = join(managedRoot, "releases", VERSION);
|
||||
const selfPackageDir = join(activeRelease, "node_modules", ...PACKAGE_NAME.split("/"));
|
||||
mkdirSync(selfPackageDir, { recursive: true });
|
||||
writeFileSync(join(activeRelease, "active.txt"), "active");
|
||||
writeFileSync(join(managedRoot, "current-version"), `${VERSION}\n`);
|
||||
writeFileSync(
|
||||
join(managedRoot, "managed-install.json"),
|
||||
`${JSON.stringify({ kind: "pi-managed-install", schemaVersion: 1, layout: "releases-v1" })}\n`,
|
||||
);
|
||||
|
||||
const binDir = join(tempDir, "managed-bin");
|
||||
const fakeNpmPath = join(tempDir, "managed-npm.cjs");
|
||||
const npmRecordPath = join(tempDir, "managed-npm-record.json");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
writeFileSync(
|
||||
fakeNpmPath,
|
||||
`const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const args = process.argv.slice(2);
|
||||
fs.writeFileSync(${JSON.stringify(npmRecordPath)}, JSON.stringify(args));
|
||||
if (${npmExitCode} !== 0) process.exit(${npmExitCode});
|
||||
const binDir = path.join(process.cwd(), "node_modules", ".bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
const piPath = path.join(binDir, process.platform === "win32" ? "pi.cmd" : "pi");
|
||||
fs.writeFileSync(
|
||||
piPath,
|
||||
process.platform === "win32"
|
||||
? "@echo off\\r\\necho ${targetVersion}\\r\\n"
|
||||
: "#!/bin/sh\\nprintf '%s\\n' ${targetVersion}\\n",
|
||||
);
|
||||
if (process.platform !== "win32") fs.chmodSync(piPath, 0o755);
|
||||
`,
|
||||
);
|
||||
const npmPath = join(binDir, process.platform === "win32" ? "npm.cmd" : "npm");
|
||||
writeFileSync(
|
||||
npmPath,
|
||||
process.platform === "win32"
|
||||
? `@echo off\r\n"${originalExecPath}" "${fakeNpmPath}" %*\r\n`
|
||||
: `#!/bin/sh\nexec "${originalExecPath}" "${fakeNpmPath}" "$@"\n`,
|
||||
);
|
||||
chmodSync(npmPath, 0o755);
|
||||
|
||||
vi.stubEnv("PI_INSTALLER_API_BASE", "https://example.test/api/installer/releases");
|
||||
vi.stubEnv("PI_MANAGED_INSTALL_ROOT", managedRoot);
|
||||
process.env.PI_PACKAGE_DIR = selfPackageDir;
|
||||
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`;
|
||||
return { managedRoot, npmRecordPath };
|
||||
}
|
||||
|
||||
function mockManagedUpdate(targetVersion: string): void {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: string | URL | Request) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
if (url === "https://pi.dev/api/latest-version") {
|
||||
return Response.json({ packageName: PACKAGE_NAME, version: targetVersion });
|
||||
}
|
||||
const releaseUrl = `https://example.test/api/installer/releases/${targetVersion}`;
|
||||
if (url === `${releaseUrl}/package.json` || url === `${releaseUrl}/package-lock.json`) {
|
||||
return Response.json({});
|
||||
}
|
||||
throw new Error(`Unexpected fetch: ${url}`);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function runPackageCommandDirectly(args: string[]): Promise<void> {
|
||||
expect(await handlePackageCommand(args)).toBe(true);
|
||||
}
|
||||
|
|
@ -82,6 +164,7 @@ describe("package commands", () => {
|
|||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
process.chdir(originalCwd);
|
||||
process.exitCode = originalExitCode;
|
||||
|
|
@ -523,10 +606,103 @@ describe("package commands", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("uses the update check version for forced self updates even when current", async () => {
|
||||
it("updates installer-managed Pi through a staged immutable release", async () => {
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
const { managedRoot, npmRecordPath } = prepareManagedInstall(targetVersion);
|
||||
const abandonedStage = join(managedRoot, "staging", "update-abandoned");
|
||||
mkdirSync(abandonedStage, { recursive: true });
|
||||
writeFileSync(join(abandonedStage, "partial"), "partial");
|
||||
const abandonedLock = join(managedRoot, "update.lock");
|
||||
mkdirSync(abandonedLock);
|
||||
utimesSync(abandonedLock, new Date(0), new Date(0));
|
||||
mockManagedUpdate(targetVersion);
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(readFileSync(join(managedRoot, "current-version"), "utf8")).toBe(`${targetVersion}\n`);
|
||||
expect(existsSync(join(managedRoot, "releases", targetVersion))).toBe(true);
|
||||
expect(existsSync(join(managedRoot, "releases", VERSION, "active.txt"))).toBe(true);
|
||||
expect(readdirSync(join(managedRoot, "staging"))).toEqual([]);
|
||||
expect(JSON.parse(readFileSync(npmRecordPath, "utf8")) as string[]).toEqual(
|
||||
expect.arrayContaining(["ci", "--ignore-scripts"]),
|
||||
);
|
||||
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
|
||||
`Updated pi from ${VERSION} to ${targetVersion}`,
|
||||
);
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects a concurrent managed update", async () => {
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
const { managedRoot, npmRecordPath } = prepareManagedInstall(targetVersion);
|
||||
const releaseLock = await lockfile.lock(join(managedRoot, "update"), { realpath: false });
|
||||
mockManagedUpdate(targetVersion);
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
|
||||
} finally {
|
||||
await releaseLock();
|
||||
}
|
||||
|
||||
expect(readFileSync(join(managedRoot, "current-version"), "utf8")).toBe(`${VERSION}\n`);
|
||||
expect(existsSync(npmRecordPath)).toBe(false);
|
||||
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).not.toContain("Updated pi from");
|
||||
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
|
||||
"Another managed Pi update is already running.",
|
||||
);
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects forced managed reinstalls", async () => {
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
const { npmRecordPath } = prepareManagedInstall(targetVersion);
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await expect(runPackageCommandDirectly(["update", "--self", "--force"])).resolves.toBeUndefined();
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(existsSync(npmRecordPath)).toBe(false);
|
||||
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
|
||||
"Managed pi installations do not support --force",
|
||||
);
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps the managed release active when its update fails", async () => {
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
const { managedRoot } = prepareManagedInstall(targetVersion, 23);
|
||||
mockManagedUpdate(targetVersion);
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(readFileSync(join(managedRoot, "current-version"), "utf8")).toBe(`${VERSION}\n`);
|
||||
expect(existsSync(join(managedRoot, "releases", targetVersion))).toBe(false);
|
||||
expect(readdirSync(join(managedRoot, "staging"))).toEqual([]);
|
||||
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).not.toContain("Updated pi from");
|
||||
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain("exited with code 23");
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps npm self-updates non-managed when the managed environment is inherited", async () => {
|
||||
const globalPrefix = join(tempDir, "global-prefix");
|
||||
const projectPrefix = join(tempDir, "project-prefix");
|
||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@earendil-works", "pi-coding-agent");
|
||||
const inheritedManagedRoot = join(tempDir, "inherited-managed-install");
|
||||
mkdirSync(join(inheritedManagedRoot, "releases"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(inheritedManagedRoot, "managed-install.json"),
|
||||
JSON.stringify({ kind: "pi-managed-install", schemaVersion: 1, layout: "releases-v1" }),
|
||||
);
|
||||
vi.stubEnv("PI_MANAGED_INSTALL_ROOT", inheritedManagedRoot);
|
||||
const fakeNpmPath = join(tempDir, "fake-npm.cjs");
|
||||
const recordPath = join(tempDir, "self-update.json");
|
||||
mkdirSync(selfPackageDir, { recursive: true });
|
||||
|
|
|
|||
30
packages/coding-agent/test/powershell-tool.test.ts
Normal file
30
packages/coding-agent/test/powershell-tool.test.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { createPowerShellTool } from "../src/core/tools/powershell.ts";
|
||||
import { getPowerShellConfig, POWERSHELL_ARGS } from "../src/utils/shell.ts";
|
||||
|
||||
function getTextOutput(result: { content: Array<{ type: string; text?: string }> }): string {
|
||||
return result.content
|
||||
.filter((content) => content.type === "text")
|
||||
.map((content) => content.text ?? "")
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
describe("powershell tool", () => {
|
||||
it("uses process-local execution policy bypass", () => {
|
||||
expect(POWERSHELL_ARGS).toEqual(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"]);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform !== "win32")("executes PowerShell commands with UTF-8 output", async () => {
|
||||
const config = getPowerShellConfig();
|
||||
expect(config.args).toEqual(POWERSHELL_ARGS);
|
||||
|
||||
const tool = createPowerShellTool(process.cwd());
|
||||
const result = await tool.execute("powershell-test", {
|
||||
command: "Write-Output 'héllo €'; Get-ExecutionPolicy -Scope Process",
|
||||
});
|
||||
const output = getTextOutput(result);
|
||||
|
||||
expect(output).toContain("héllo €");
|
||||
expect(output).toContain("Bypass");
|
||||
});
|
||||
});
|
||||
|
|
@ -78,7 +78,7 @@ describe("regression #3592: no-builtin-tools keeps extension tools enabled", ()
|
|||
.getAllTools()
|
||||
.map((tool) => tool.name)
|
||||
.sort(),
|
||||
).toEqual(["bash", "dynamic_tool", "edit", "find", "grep", "ls", "read", "write"]);
|
||||
).toEqual(["bash", "dynamic_tool", "edit", "find", "grep", "ls", "powershell", "read", "write"]);
|
||||
expect(session.getActiveToolNames()).toEqual(["dynamic_tool"]);
|
||||
expect(session.systemPrompt).toContain("- dynamic_tool: Run dynamic test behavior");
|
||||
expect(session.systemPrompt).not.toContain("- read:");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { type AssistantMessage, fauxAssistantMessage } from "@earendil-works/pi-ai";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createHarness, type Harness } from "../harness.ts";
|
||||
|
||||
function seedCompactableSession(harness: Harness): void {
|
||||
harness.settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } });
|
||||
const now = Date.now();
|
||||
harness.sessionManager.appendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "message to compact" }],
|
||||
timestamp: now - 1000,
|
||||
});
|
||||
const model = harness.getModel();
|
||||
const assistant: AssistantMessage = {
|
||||
...fauxAssistantMessage("assistant response to compact", { timestamp: now - 500 }),
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: {
|
||||
input: 100,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 100,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
};
|
||||
harness.sessionManager.appendMessage(assistant);
|
||||
harness.session.agent.state.messages = harness.sessionManager.buildSessionContext().messages;
|
||||
}
|
||||
|
||||
describe("#7048 truncated compaction summaries", () => {
|
||||
let harness: Harness | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
harness?.cleanup();
|
||||
harness = undefined;
|
||||
});
|
||||
|
||||
it("does not persist a length-limited summary", async () => {
|
||||
harness = await createHarness();
|
||||
seedCompactableSession(harness);
|
||||
harness.setResponses([fauxAssistantMessage("partial summar", { stopReason: "length" })]);
|
||||
|
||||
await expect(harness.session.compact()).rejects.toThrow("generation hit the token cap");
|
||||
expect(harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -46,6 +46,20 @@ describe("buildSystemPrompt", () => {
|
|||
expect(prompt).toContain("- write:");
|
||||
});
|
||||
|
||||
test.each([
|
||||
[["powershell"], "Use PowerShell for file operations"],
|
||||
[["bash", "powershell"], "Use bash or PowerShell for file operations"],
|
||||
] as const)("uses shell-specific guidance for %j", (selectedTools, expected) => {
|
||||
const prompt = buildSystemPrompt({
|
||||
selectedTools: [...selectedTools],
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(prompt).toContain(expected);
|
||||
});
|
||||
|
||||
test("instructs models to resolve pi docs and examples under absolute base paths", () => {
|
||||
const prompt = buildSystemPrompt({
|
||||
contextFiles: [],
|
||||
|
|
|
|||
|
|
@ -4,12 +4,17 @@ import { createEditToolDefinition, editToolSystemPromptContribution } from "../s
|
|||
import { createFindToolDefinition, findToolSystemPromptContribution } from "../src/core/tools/find.ts";
|
||||
import { createGrepToolDefinition, grepToolSystemPromptContribution } from "../src/core/tools/grep.ts";
|
||||
import { createLsToolDefinition, lsToolSystemPromptContribution } from "../src/core/tools/ls.ts";
|
||||
import {
|
||||
createPowerShellToolDefinition,
|
||||
powershellToolSystemPromptContribution,
|
||||
} from "../src/core/tools/powershell.ts";
|
||||
import { createReadToolDefinition, readToolSystemPromptContribution } from "../src/core/tools/read.ts";
|
||||
import { createWriteToolDefinition, writeToolSystemPromptContribution } from "../src/core/tools/write.ts";
|
||||
|
||||
const cases = [
|
||||
["read", readToolSystemPromptContribution, createReadToolDefinition],
|
||||
["bash", bashToolSystemPromptContribution, createBashToolDefinition],
|
||||
["powershell", powershellToolSystemPromptContribution, createPowerShellToolDefinition],
|
||||
["edit", editToolSystemPromptContribution, createEditToolDefinition],
|
||||
["write", writeToolSystemPromptContribution, createWriteToolDefinition],
|
||||
["grep", grepToolSystemPromptContribution, createGrepToolDefinition],
|
||||
|
|
@ -28,8 +33,11 @@ describe("built-in tool system prompt contributions", () => {
|
|||
},
|
||||
);
|
||||
|
||||
test("keeps bash session-environment guidance conditional", () => {
|
||||
const definition = createBashToolDefinition("/workspace", { exposeSessionEnvironment: false });
|
||||
test.each([
|
||||
["bash", createBashToolDefinition],
|
||||
["powershell", createPowerShellToolDefinition],
|
||||
] as const)("keeps %s session-environment guidance conditional", (_name, createDefinition) => {
|
||||
const definition = createDefinition("/workspace", { exposeSessionEnvironment: false });
|
||||
|
||||
expect(definition.promptGuidelines).toBeUndefined();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-evals",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
@ -9,8 +9,8 @@
|
|||
"test": "vitest run --config vitest.test.config.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.3",
|
||||
"@types/node": "22.19.19",
|
||||
"shx": "0.4.0",
|
||||
"typescript": "5.9.3",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
## [0.84.2] - 2026-08-14
|
||||
|
||||
## [0.84.1] - 2026-08-07
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-protocol",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "Transport-neutral CBOR protocol for remote pi sessions",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
## [0.84.2] - 2026-08-14
|
||||
|
||||
## [0.84.1] - 2026-08-07
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-server",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "experimental server package for pi",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -47,8 +47,8 @@
|
|||
"node": ">=22.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-protocol": "^0.84.2"
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-protocol": "^0.84.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "0.4.0",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
## [0.84.2] - 2026-08-14
|
||||
|
||||
## [0.84.1] - 2026-08-07
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-session-backend-sqlite-node",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "Node sqlite session backend for @earendil-works/pi-agent-core sessions",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -34,8 +34,8 @@
|
|||
"node": ">=22.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-agent-core": "^0.84.2"
|
||||
"@earendil-works/pi-ai": "^0.84.3",
|
||||
"@earendil-works/pi-agent-core": "^0.84.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
## [0.84.2] - 2026-08-14
|
||||
|
||||
## [0.84.1] - 2026-08-07
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-telemetry",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "Vendor-neutral telemetry contracts and typed schema utilities for pi",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.84.3] - 2026-08-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed duplicate fullscreen right-click paste in VS Code-based terminals on Windows ([#8186](https://github.com/earendil-works/pi/issues/8186)).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@earendil-works/pi-tui",
|
||||
"version": "0.84.2",
|
||||
"version": "0.84.3",
|
||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ function parseArgs(args) {
|
|||
if (!options.version || !STABLE_SEMVER_RE.test(options.version)) {
|
||||
throw new Error("--version must be a stable semver version");
|
||||
}
|
||||
if (Boolean(options.installerPackageJson) !== Boolean(options.installerPackageLock)) {
|
||||
throw new Error("--installer-package-json and --installer-package-lock must be provided together");
|
||||
if (!options.installerPackageJson || !options.installerPackageLock) {
|
||||
throw new Error("--installer-package-json and --installer-package-lock are required");
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
|
@ -302,6 +302,57 @@ async function main() {
|
|||
}
|
||||
|
||||
writeFileSync(latestPath, `${JSON.stringify(release, null, "\t")}\n`);
|
||||
validateInstallerArtifacts(options.installerPackageJson, options.installerPackageLock, options.version);
|
||||
const installerReleasePrefix = `${INSTALLER_PREFIX}/releases/${options.version}`;
|
||||
putObject(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
options.installerPackageJson,
|
||||
`${installerReleasePrefix}/package.json`,
|
||||
"public, max-age=31536000, immutable",
|
||||
{ missing: true },
|
||||
);
|
||||
putObject(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
options.installerPackageLock,
|
||||
`${installerReleasePrefix}/package-lock.json`,
|
||||
"public, max-age=31536000, immutable",
|
||||
{ missing: true },
|
||||
);
|
||||
putJson(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
releasePath,
|
||||
`${installerReleasePrefix}/metadata.json`,
|
||||
"public, max-age=31536000, immutable",
|
||||
{ missing: true },
|
||||
);
|
||||
const installerLatest = await advanceLatestRelease(
|
||||
options.version,
|
||||
() =>
|
||||
readLatestRelease(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
`${INSTALLER_PREFIX}/latest.json`,
|
||||
join(temporaryDirectory, "installer-latest-current.json"),
|
||||
),
|
||||
(condition) =>
|
||||
putJson(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
latestPath,
|
||||
`${INSTALLER_PREFIX}/latest.json`,
|
||||
"no-store",
|
||||
condition,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
installerLatest.advanced
|
||||
? `Published installer artifacts for Pi ${options.version} through s3://${options.bucket}/${INSTALLER_PREFIX}/latest.json`
|
||||
: `Pi ${installerLatest.version} is already the latest installer release.`,
|
||||
);
|
||||
|
||||
const result = await advanceLatestRelease(
|
||||
options.version,
|
||||
() =>
|
||||
|
|
@ -326,59 +377,6 @@ async function main() {
|
|||
? `Announced Pi ${options.version} through s3://${options.bucket}/${RELEASES_PREFIX}/latest.json`
|
||||
: `Pi ${result.version} is already the latest announced release.`,
|
||||
);
|
||||
|
||||
if (options.installerPackageJson) {
|
||||
validateInstallerArtifacts(options.installerPackageJson, options.installerPackageLock, options.version);
|
||||
const installerReleasePrefix = `${INSTALLER_PREFIX}/releases/${options.version}`;
|
||||
putObject(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
options.installerPackageJson,
|
||||
`${installerReleasePrefix}/package.json`,
|
||||
"public, max-age=31536000, immutable",
|
||||
{ missing: true },
|
||||
);
|
||||
putObject(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
options.installerPackageLock,
|
||||
`${installerReleasePrefix}/package-lock.json`,
|
||||
"public, max-age=31536000, immutable",
|
||||
{ missing: true },
|
||||
);
|
||||
putJson(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
releasePath,
|
||||
`${installerReleasePrefix}/metadata.json`,
|
||||
"public, max-age=31536000, immutable",
|
||||
{ missing: true },
|
||||
);
|
||||
const installerLatest = await advanceLatestRelease(
|
||||
options.version,
|
||||
() =>
|
||||
readLatestRelease(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
`${INSTALLER_PREFIX}/latest.json`,
|
||||
join(temporaryDirectory, "installer-latest-current.json"),
|
||||
),
|
||||
(condition) =>
|
||||
putJson(
|
||||
options.bucket,
|
||||
options.endpoint,
|
||||
latestPath,
|
||||
`${INSTALLER_PREFIX}/latest.json`,
|
||||
"no-store",
|
||||
condition,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
installerLatest.advanced
|
||||
? `Published installer artifacts for Pi ${options.version} through s3://${options.bucket}/${INSTALLER_PREFIX}/latest.json`
|
||||
: `Pi ${installerLatest.version} is already the latest installer release.`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
rmSync(temporaryDirectory, { force: true, recursive: true });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue