mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
Compare commits
3
Commits
a196c3a6b1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb9ec7a869 | ||
|
|
19ae25040d | ||
|
|
836042b97c |
@@ -23,7 +23,7 @@ Pi Agent Integrated 将 Pi 后端与 Pi Web 前端整合成一个可独立克隆
|
|||||||
| 浏览器自动化 | 需自行接入 | 无默认浏览器 MCP | Windows 默认使用 Edge;macOS 默认不启用,按需配置 |
|
| 浏览器自动化 | 需自行接入 | 无默认浏览器 MCP | Windows 默认使用 Edge;macOS 默认不启用,按需配置 |
|
||||||
| 恢复与长期状态 | 会话树与基础状态 | 会话 UI | Rewind 检查点、项目内 Memory、自动重试 |
|
| 恢复与长期状态 | 会话树与基础状态 | 会话 UI | Rewind 检查点、项目内 Memory、自动重试 |
|
||||||
| 多代理 | 核心能力可扩展 | 展示工具调用 | 配置了自动判断复杂度的 `pi-subagents` 策略 |
|
| 多代理 | 核心能力可扩展 | 展示工具调用 | 配置了自动判断复杂度的 `pi-subagents` 策略 |
|
||||||
| 本项目修复 | 不适用 | 上游行为 | 修复已结束会话状态 404、隐藏 Next.js 开发指示器、增加 Windows/macOS restart |
|
| 本项目修复 | 不适用 | 上游行为 | 修复已结束会话状态 404、隐藏 Next.js 开发指示器、增加 Windows/macOS restart、WebUI 流式输出自动跟踪与回到底部按钮 |
|
||||||
|
|
||||||
本仓库不是桌面安装包,也不是公网多用户服务。当前发布目标是:技术用户在 Windows 或 macOS 上克隆仓库、补充自己的模型凭据后,通过命令行启动一个隔离、可扩展的本地 Web Agent。
|
本仓库不是桌面安装包,也不是公网多用户服务。当前发布目标是:技术用户在 Windows 或 macOS 上克隆仓库、补充自己的模型凭据后,通过命令行启动一个隔离、可扩展的本地 Web Agent。
|
||||||
|
|
||||||
@@ -67,6 +67,8 @@ npm run dev
|
|||||||
5. Windows 预缓存 Playwright MCP 包并确认系统 Edge 可用;macOS 跳过 Playwright 安装,浏览器自动化按需配置;
|
5. Windows 预缓存 Playwright MCP 包并确认系统 Edge 可用;macOS 跳过 Playwright 安装,浏览器自动化按需配置;
|
||||||
6. 检查前后端版本和构建产物。
|
6. 检查前后端版本和构建产物。
|
||||||
|
|
||||||
|
`pi-smart-fetch` 使用仓库内的修复版,不再直接加载上游 `0.3.17` 发布包。setup 会在 `resources/packages/pi-smart-fetch/` 安装运行时依赖,并自动迁移旧的 `npm:pi-smart-fetch` 配置;该修复为 ESM 构建注入 `createRequire(import.meta.url)`,避免 Node/Next.js 加载 `mime-types` 时触发 `Dynamic require of "path" is not supported`。
|
||||||
|
|
||||||
安装脚本默认不继承 `HTTP_PROXY` / `HTTPS_PROXY`,避免失效代理阻塞 npm。如果安装必须使用代理,Windows PowerShell 设置 `$env:PI_SETUP_USE_PROXY = "1"`,macOS/Linux 使用 `PI_SETUP_USE_PROXY=1 npm run setup`。
|
安装脚本默认不继承 `HTTP_PROXY` / `HTTPS_PROXY`,避免失效代理阻塞 npm。如果安装必须使用代理,Windows PowerShell 设置 `$env:PI_SETUP_USE_PROXY = "1"`,macOS/Linux 使用 `PI_SETUP_USE_PROXY=1 npm run setup`。
|
||||||
|
|
||||||
### 首次模型配置
|
### 首次模型配置
|
||||||
@@ -80,6 +82,12 @@ npm run dev
|
|||||||
|
|
||||||
所有设备相关模型配置都位于被 Git 忽略的 `data/` 或 `.env` 中。
|
所有设备相关模型配置都位于被 Git 忽略的 `data/` 或 `.env` 中。
|
||||||
|
|
||||||
|
#### GPT Codex 快速模式
|
||||||
|
|
||||||
|
选择 `openai-codex` 的 GPT-5.5 或 GPT-5.6 模型后,输入框底部会显示闪电按钮。开启后按钮显示 `Fast mode ON`,并将该模型的 `serviceTier` 设为 `priority`,请求会使用 Codex 的快速档位;关闭后恢复普通档位。该设置写入 `data/agent/models.json`,下次请求立即生效,无需重启;非 GPT Codex 模型不会显示此按钮。
|
||||||
|
|
||||||
|
快速档位会增加费用:GPT-5.6 通常约为 2 倍,GPT-5.5 的 priority 费率约为 2.5 倍。Web UI 会在按钮提示中明确显示 `service_tier: priority` 和费用倍率。
|
||||||
|
|
||||||
### 可选环境变量
|
### 可选环境变量
|
||||||
|
|
||||||
需要可选服务时,复制模板:
|
需要可选服务时,复制模板:
|
||||||
@@ -116,7 +124,8 @@ pi-web/ Next.js Web 前端与 HTTP/SSE 服务
|
|||||||
config/ 可提交的缺省设置、MCP 和子代理策略
|
config/ 可提交的缺省设置、MCP 和子代理策略
|
||||||
resources/skills/ 应用级 Skill
|
resources/skills/ 应用级 Skill
|
||||||
resources/extensions/ 应用级 Pi 扩展
|
resources/extensions/ 应用级 Pi 扩展
|
||||||
resources/prompts/ 提示词模板
|
resources/packages/ 项目内置包(含 pi-smart-fetch 修复版)
|
||||||
|
resources/prompts/ 提示词模板
|
||||||
resources/themes/ 主题
|
resources/themes/ 主题
|
||||||
scripts/ 安装、启动、迁移与验证脚本
|
scripts/ 安装、启动、迁移与验证脚本
|
||||||
data/agent/ 会话、认证、模型、插件、记忆与工具状态
|
data/agent/ 会话、认证、模型、插件、记忆与工具状态
|
||||||
@@ -138,7 +147,7 @@ data/workspaces/default/ 默认工作目录
|
|||||||
| `pi-lens@3.8.73` | LSP、AST、符号检索和项目诊断 | 模型按需激活代码智能工具;可用 `/lens-health`、`/lens-tools`、`/lens-map` 检查 |
|
| `pi-lens@3.8.73` | LSP、AST、符号检索和项目诊断 | 模型按需激活代码智能工具;可用 `/lens-health`、`/lens-tools`、`/lens-map` 检查 |
|
||||||
| `pi-memory@0.4.0` | Markdown 长期记忆、日志、临时工作区和恢复记录 | 默认轻量模式保留读写、状态和恢复工具,但不注册依赖 qmd 的 `memory_search`;文件位于 `data/agent/memory/` |
|
| `pi-memory@0.4.0` | Markdown 长期记忆、日志、临时工作区和恢复记录 | 默认轻量模式保留读写、状态和恢复工具,但不注册依赖 qmd 的 `memory_search`;文件位于 `data/agent/memory/` |
|
||||||
| `pi-subagents@0.37.2` | 创建研究、规划或执行子代理 | 简单任务不委派,跨模块或可并行复杂任务自动判断;也可使用 `/run`、`/parallel` |
|
| `pi-subagents@0.37.2` | 创建研究、规划或执行子代理 | 简单任务不委派,跨模块或可并行复杂任务自动判断;也可使用 `/run`、`/parallel` |
|
||||||
| `pi-smart-fetch@0.3.17` | 抓取单个或批量 URL 内容 | 模型按需使用 `web_fetch` / `batch_web_fetch`,也提供给研究子代理 |
|
| `pi-smart-fetch`(项目内修复版,基于 `0.3.17`) | 抓取单个或批量 URL 内容 | 模型按需使用 `web_fetch` / `batch_web_fetch`,也提供给研究子代理;修复 Node/Next.js ESM 加载兼容性 |
|
||||||
| `@ayulab/pi-rewind@0.4.6` | 每轮前后创建代码检查点 | `/rewind` 恢复代码、会话或两者;`/checkpoint` 管理存储。自动恢复文件默认关闭 |
|
| `@ayulab/pi-rewind@0.4.6` | 每轮前后创建代码检查点 | `/rewind` 恢复代码、会话或两者;`/checkpoint` 管理存储。自动恢复文件默认关闭 |
|
||||||
| `@upstash/context7-pi@0.1.2` | 查询当前库、框架、SDK 和 API 文档 | 模型先解析库 ID,再按需查询文档;无 Key 可使用公共限额 |
|
| `@upstash/context7-pi@0.1.2` | 查询当前库、框架、SDK 和 API 文档 | 模型先解析库 ID,再按需查询文档;无 Key 可使用公共限额 |
|
||||||
| `@narumitw/pi-retry@0.31.0` | 识别瞬时供应商错误和卡住的流 | 复用 Pi 内置重试;默认 180 秒无事件视为停滞,不增加正常请求的模型调用 |
|
| `@narumitw/pi-retry@0.31.0` | 识别瞬时供应商错误和卡住的流 | 复用 Pi 内置重试;默认 180 秒无事件视为停滞,不增加正常请求的模型调用 |
|
||||||
@@ -147,6 +156,8 @@ data/workspaces/default/ 默认工作目录
|
|||||||
|
|
||||||
Windows 的 Playwright 不下载独立 Chromium;首次 `setup` 只缓存 MCP 的 Node.js 包,浏览器执行使用系统 Edge。macOS 的 setup 不安装或启用 Playwright;如需浏览器自动化,可在 Web UI 的 MCP 面板中手动添加并配置。
|
Windows 的 Playwright 不下载独立 Chromium;首次 `setup` 只缓存 MCP 的 Node.js 包,浏览器执行使用系统 Edge。macOS 的 setup 不安装或启用 Playwright;如需浏览器自动化,可在 Web UI 的 MCP 面板中手动添加并配置。
|
||||||
|
|
||||||
|
Windows 下的 `pi-subagents` 子进程由受管启动器自动处理:启动时将本地 `pi-web/node_modules/.bin` 加入子进程 PATH,并把 Pi Web 使用的 `pi-coding-agent` 包链接到受管 Profile,使子代理直接解析本地 `dist/cli.js`。这不会修改系统级 PATH,每次项目启动时会自动恢复。
|
||||||
|
|
||||||
#### 视觉子代理(vision)
|
#### 视觉子代理(vision)
|
||||||
|
|
||||||
DeepSeek 等纯文本模型不能接收图片。仓库内置 `vision` 子代理(`.agents/vision.md`):它通过 `vision` 工具调用视觉模型读取图片,把完整 OCR、版式结构与语义描述返回给主模型,主模型基于文本继续推理。视觉后端可插拔(本地 Ollama 或任意 OpenAI 兼容视觉 API)——仓库**不预设默认后端**,首次使用前需自行选择并配置。
|
DeepSeek 等纯文本模型不能接收图片。仓库内置 `vision` 子代理(`.agents/vision.md`):它通过 `vision` 工具调用视觉模型读取图片,把完整 OCR、版式结构与语义描述返回给主模型,主模型基于文本继续推理。视觉后端可插拔(本地 Ollama 或任意 OpenAI 兼容视觉 API)——仓库**不预设默认后端**,首次使用前需自行选择并配置。
|
||||||
@@ -285,7 +296,7 @@ Pi Agent Integrated combines the Pi backend and Pi Web frontend into one indepen
|
|||||||
| Browser automation | User-integrated | No default browser MCP | Windows uses Edge by default; macOS leaves Playwright opt-in |
|
| Browser automation | User-integrated | No default browser MCP | Windows uses Edge by default; macOS leaves Playwright opt-in |
|
||||||
| Recovery and durable context | Session tree and core state | Session UI | Rewind checkpoints, repository-local memory, automatic retry |
|
| Recovery and durable context | Session tree and core state | Session UI | Rewind checkpoints, repository-local memory, automatic retry |
|
||||||
| Multi-agent workflow | Extensible core | Renders tool calls | Automatic complexity policy for `pi-subagents` |
|
| Multi-agent workflow | Extensible core | Renders tool calls | Automatic complexity policy for `pi-subagents` |
|
||||||
| Integration fixes | Not applicable | Upstream behavior | Handles ended-session state 404s, hides Next dev indicators, adds Windows/macOS restart |
|
| Integration fixes | Not applicable | Upstream behavior | Handles ended-session state 404s, hides Next dev indicators, adds Windows/macOS restart, and auto-follows streaming output with a scroll-to-bottom button in the Web UI |
|
||||||
|
|
||||||
This repository is not a desktop installer or a public multi-user service. Its current release target is a technical Windows or macOS user who clones the project, supplies personal model credentials, and starts an isolated, extensible local Web agent from the command line.
|
This repository is not a desktop installer or a public multi-user service. Its current release target is a technical Windows or macOS user who clones the project, supplies personal model credentials, and starts an isolated, extensible local Web agent from the command line.
|
||||||
|
|
||||||
@@ -311,7 +322,9 @@ npm run dev
|
|||||||
|
|
||||||
Open <http://127.0.0.1:30141>.
|
Open <http://127.0.0.1:30141>.
|
||||||
|
|
||||||
Setup initializes the repository-local profile, builds Pi, links Pi Web to the local packages, installs and loads the pinned plugins, caches the Playwright MCP Node package and verifies system Edge on Windows, skips Playwright on macOS, and checks integration artifacts.
|
Setup initializes the repository-local profile, builds Pi, links Pi Web to the local packages, installs and loads the pinned plugins, installs the repository-local repaired `pi-smart-fetch` dependencies, caches the Playwright MCP Node package and verifies system Edge on Windows, skips Playwright on macOS, and checks integration artifacts.
|
||||||
|
|
||||||
|
The repaired `pi-smart-fetch` build is based on upstream `0.3.17`; it adds `createRequire(import.meta.url)` for the bundled CommonJS dependencies so Node/Next.js does not fail with `Dynamic require of "path" is not supported`. Existing managed profiles are migrated from `npm:pi-smart-fetch` to `resources/packages/pi-smart-fetch` automatically.
|
||||||
|
|
||||||
Child npm operations ignore `HTTP_PROXY` and `HTTPS_PROXY` by default to avoid stale proxy configuration. Set `$env:PI_SETUP_USE_PROXY = "1"` on Windows, or run `PI_SETUP_USE_PROXY=1 npm run setup` on macOS/Linux, when registry access requires the proxy.
|
Child npm operations ignore `HTTP_PROXY` and `HTTPS_PROXY` by default to avoid stale proxy configuration. Set `$env:PI_SETUP_USE_PROXY = "1"` on Windows, or run `PI_SETUP_USE_PROXY=1 npm run setup` on macOS/Linux, when registry access requires the proxy.
|
||||||
|
|
||||||
@@ -326,6 +339,12 @@ The repository contains no author model endpoint, API key, OAuth token, or defau
|
|||||||
|
|
||||||
Device-specific model configuration remains in ignored `data/` or `.env` files.
|
Device-specific model configuration remains in ignored `data/` or `.env` files.
|
||||||
|
|
||||||
|
#### GPT Codex fast mode
|
||||||
|
|
||||||
|
When an `openai-codex` GPT-5.5 or GPT-5.6 model is selected, the lightning button appears beside the model selector. Enabling it shows `Fast mode ON` and sets that model's `serviceTier` to `priority`, using Codex's faster service tier; disabling it restores the default tier. The setting is written to `data/agent/models.json` and takes effect on the next request without a restart. Non-Codex models do not show the button.
|
||||||
|
|
||||||
|
Fast mode costs more: GPT-5.6 is typically about 2x, while GPT-5.5 priority pricing is about 2.5x. The Web UI tooltip displays `service_tier: priority` and the applicable credit multiplier.
|
||||||
|
|
||||||
### Optional environment configuration
|
### Optional environment configuration
|
||||||
|
|
||||||
macOS/Linux:
|
macOS/Linux:
|
||||||
@@ -360,7 +379,8 @@ pi-web/ Next.js frontend and HTTP/SSE service
|
|||||||
config/ Versioned settings, MCP, and subagent policy
|
config/ Versioned settings, MCP, and subagent policy
|
||||||
resources/skills/ Application skills
|
resources/skills/ Application skills
|
||||||
resources/extensions/ Application Pi extensions
|
resources/extensions/ Application Pi extensions
|
||||||
resources/prompts/ Prompt templates
|
resources/packages/ Repository-local packages (including repaired pi-smart-fetch)
|
||||||
|
resources/prompts/ Prompt templates
|
||||||
resources/themes/ Themes
|
resources/themes/ Themes
|
||||||
scripts/ Setup, launch, migration, and verification
|
scripts/ Setup, launch, migration, and verification
|
||||||
data/agent/ Sessions, auth, models, packages, memory, tools
|
data/agent/ Sessions, auth, models, packages, memory, tools
|
||||||
@@ -382,7 +402,7 @@ Versions are pinned in the platform defaults under `config/`: Windows uses `mcp.
|
|||||||
| `pi-lens@3.8.73` | LSP, AST, symbols, project diagnostics | The model activates code intelligence on demand; inspect with `/lens-health`, `/lens-tools`, `/lens-map` |
|
| `pi-lens@3.8.73` | LSP, AST, symbols, project diagnostics | The model activates code intelligence on demand; inspect with `/lens-health`, `/lens-tools`, `/lens-map` |
|
||||||
| `pi-memory@0.4.0` | Markdown durable facts, logs, scratchpad, recovery | Lightweight mode retains read/write, status, and recovery tools but does not register qmd-dependent `memory_search`; files live under `data/agent/memory/` |
|
| `pi-memory@0.4.0` | Markdown durable facts, logs, scratchpad, recovery | Lightweight mode retains read/write, status, and recovery tools but does not register qmd-dependent `memory_search`; files live under `data/agent/memory/` |
|
||||||
| `pi-subagents@0.37.2` | Research, planning, execution subagents | Simple tasks stay local; complex or parallel work may delegate automatically; `/run` and `/parallel` remain available |
|
| `pi-subagents@0.37.2` | Research, planning, execution subagents | Simple tasks stay local; complex or parallel work may delegate automatically; `/run` and `/parallel` remain available |
|
||||||
| `pi-smart-fetch@0.3.17` | Single and batched URL retrieval | Provides `web_fetch` and `batch_web_fetch` to the main and research agents |
|
| `pi-smart-fetch` (project-local repair, based on `0.3.17`) | Single and batched URL retrieval | Provides `web_fetch` and `batch_web_fetch` to the main and research agents; fixes Node/Next.js ESM loading compatibility |
|
||||||
| `@ayulab/pi-rewind@0.4.6` | Per-turn code checkpoints | `/rewind` restores code, conversation, or both; `/checkpoint` manages storage. Automatic file restore is off |
|
| `@ayulab/pi-rewind@0.4.6` | Per-turn code checkpoints | `/rewind` restores code, conversation, or both; `/checkpoint` manages storage. Automatic file restore is off |
|
||||||
| `@upstash/context7-pi@0.1.2` | Current library, framework, SDK, API docs | Resolves a library ID and queries docs when needed; public quota works without a key |
|
| `@upstash/context7-pi@0.1.2` | Current library, framework, SDK, API docs | Resolves a library ID and queries docs when needed; public quota works without a key |
|
||||||
| `@narumitw/pi-retry@0.31.0` | Transient provider and stalled-stream classification | Uses Pi's built-in retry path; 180 seconds without events is a stall; no extra normal model calls |
|
| `@narumitw/pi-retry@0.31.0` | Transient provider and stalled-stream classification | Uses Pi's built-in retry path; 180 seconds without events is a stall; no extra normal model calls |
|
||||||
@@ -391,6 +411,8 @@ Versions are pinned in the platform defaults under `config/`: Windows uses `mcp.
|
|||||||
|
|
||||||
On Windows, Playwright never downloads a standalone Chromium: setup caches only its Node package and browser execution uses system Edge. On macOS, setup does not install or enable Playwright; add it manually through the MCP panel if browser automation is needed.
|
On Windows, Playwright never downloads a standalone Chromium: setup caches only its Node package and browser execution uses system Edge. On macOS, setup does not install or enable Playwright; add it manually through the MCP panel if browser automation is needed.
|
||||||
|
|
||||||
|
On Windows, the managed launcher prepares `pi-subagents` child processes automatically: it prepends the local `pi-web/node_modules/.bin` directory to the child PATH and links the Pi Web `pi-coding-agent` package into the managed profile, allowing subagents to resolve the local `dist/cli.js` directly. This does not modify the system-wide PATH and is recreated on each project launch.
|
||||||
|
|
||||||
#### Vision subagent
|
#### Vision subagent
|
||||||
|
|
||||||
Text-only models such as DeepSeek cannot receive image attachments. The repository ships a `vision` subagent (`.agents/vision.md`) that calls a vision model through the `vision` tool and returns a full OCR, layout, and semantic description the main model can reason over. The vision backend is pluggable (local Ollama or any OpenAI-compatible vision API) — the repository does **not** ship a default backend; pick and configure one before first use.
|
Text-only models such as DeepSeek cannot receive image attachments. The repository ships a `vision` subagent (`.agents/vision.md`) that calls a vision model through the `vision` tool and returns a full OCR, layout, and semantic description the main model can reason over. The vision backend is pluggable (local Ollama or any OpenAI-compatible vision API) — the repository does **not** ship a default backend; pick and configure one before first use.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"npm:pi-mcp-adapter@2.15.0",
|
"npm:pi-mcp-adapter@2.15.0",
|
||||||
"npm:pi-lens@3.8.73",
|
"npm:pi-lens@3.8.73",
|
||||||
"npm:pi-memory@0.4.0",
|
"npm:pi-memory@0.4.0",
|
||||||
|
"../../resources/packages/pi-smart-fetch",
|
||||||
"npm:pi-subagents@0.37.2",
|
"npm:pi-subagents@0.37.2",
|
||||||
"npm:pi-smart-fetch@0.3.17",
|
|
||||||
"npm:@ayulab/pi-rewind@0.4.6",
|
"npm:@ayulab/pi-rewind@0.4.6",
|
||||||
"npm:@upstash/context7-pi@0.1.2",
|
"npm:@upstash/context7-pi@0.1.2",
|
||||||
"npm:@narumitw/pi-retry@0.31.0"
|
"npm:@narumitw/pi-retry@0.31.0"
|
||||||
|
|||||||
@@ -10,15 +10,16 @@ $supervisorLog = Join-Path $logsDirectory 'pi-web-supervisor.log'
|
|||||||
$restartRequestPath = Join-Path $projectRoot 'data\agent\restart-request.json'
|
$restartRequestPath = Join-Path $projectRoot 'data\agent\restart-request.json'
|
||||||
$webUrl = 'http://127.0.0.1:30141/'
|
$webUrl = 'http://127.0.0.1:30141/'
|
||||||
$restartRequestVersion = 1
|
$restartRequestVersion = 1
|
||||||
|
$maxResumeAttempts = 3
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $logsDirectory -Force | Out-Null
|
New-Item -ItemType Directory -Path $logsDirectory -Force | Out-Null
|
||||||
New-Item -ItemType Directory -Path (Split-Path -Parent $restartRequestPath) -Force | Out-Null
|
New-Item -ItemType Directory -Path (Split-Path -Parent $restartRequestPath) -Force | Out-Null
|
||||||
Set-Content -LiteralPath $supervisorLog -Value "$(Get-Date -Format o) supervisor started"
|
Set-Content -LiteralPath $supervisorLog -Value "$(Get-Date -Format o) supervisor started" -Encoding UTF8
|
||||||
|
|
||||||
function Write-SupervisorLog {
|
function Write-SupervisorLog {
|
||||||
param([string]$Message)
|
param([string]$Message)
|
||||||
|
|
||||||
Add-Content -LiteralPath $supervisorLog -Value "$(Get-Date -Format o) $Message"
|
Add-Content -LiteralPath $supervisorLog -Value "$(Get-Date -Format o) $Message" -Encoding UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-RestartRequest {
|
function Get-RestartRequest {
|
||||||
@@ -72,6 +73,20 @@ function Remove-RestartRequest {
|
|||||||
Remove-Item -LiteralPath $restartRequestPath -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $restartRequestPath -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Archive-RestartRequest {
|
||||||
|
param([object]$Request)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $restartRequestPath -PathType Leaf)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestId = (Get-RequestField $Request 'requestId') -replace '[^A-Za-z0-9-]', '_'
|
||||||
|
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
|
$archivePath = Join-Path $logsDirectory "failed-restart-request-$timestamp-$requestId.json"
|
||||||
|
Move-Item -LiteralPath $restartRequestPath -Destination $archivePath -Force
|
||||||
|
Write-SupervisorLog "archived failed restart request at $archivePath"
|
||||||
|
}
|
||||||
|
|
||||||
function Stop-ProcessTree {
|
function Stop-ProcessTree {
|
||||||
param([System.Diagnostics.Process]$Process)
|
param([System.Diagnostics.Process]$Process)
|
||||||
|
|
||||||
@@ -149,6 +164,7 @@ $testInstructions
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pendingRequest = Get-RestartRequest
|
$pendingRequest = Get-RestartRequest
|
||||||
|
$resumeAttemptCount = 0
|
||||||
|
|
||||||
while ($true) {
|
while ($true) {
|
||||||
$process = $null
|
$process = $null
|
||||||
@@ -176,10 +192,20 @@ while ($true) {
|
|||||||
try {
|
try {
|
||||||
Resume-AgentSession $pendingRequest
|
Resume-AgentSession $pendingRequest
|
||||||
$pendingRequest = $null
|
$pendingRequest = $null
|
||||||
|
$resumeAttemptCount = 0
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
$resumeAttemptCount += 1
|
||||||
Write-SupervisorLog "Agent resume failed: $($_.Exception.Message)"
|
Write-SupervisorLog "Agent resume failed: $($_.Exception.Message)"
|
||||||
$nextResumeAttempt = (Get-Date).AddSeconds(3)
|
if ($resumeAttemptCount -ge $maxResumeAttempts) {
|
||||||
|
Write-SupervisorLog "Agent resume abandoned after $resumeAttemptCount attempts"
|
||||||
|
Archive-RestartRequest $pendingRequest
|
||||||
|
$pendingRequest = $null
|
||||||
|
$resumeAttemptCount = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$nextResumeAttempt = (Get-Date).AddSeconds(3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,6 +214,7 @@ while ($true) {
|
|||||||
$candidate = Get-RestartRequest
|
$candidate = Get-RestartRequest
|
||||||
if ($candidate) {
|
if ($candidate) {
|
||||||
$pendingRequest = $candidate
|
$pendingRequest = $candidate
|
||||||
|
$resumeAttemptCount = 0
|
||||||
Write-SupervisorLog "restart request $($candidate.requestId) detected"
|
Write-SupervisorLog "restart request $($candidate.requestId) detected"
|
||||||
Stop-ProcessTree $process
|
Stop-ProcessTree $process
|
||||||
break
|
break
|
||||||
|
|||||||
+146
-89
@@ -1,123 +1,180 @@
|
|||||||
import { stat } from "fs/promises";
|
import { stat } from "fs/promises";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import { createAgentSessionServices, getAgentDir, type SettingsManager } from "@earendil-works/pi-coding-agent";
|
import {
|
||||||
|
createAgentSessionServices,
|
||||||
|
getAgentDir,
|
||||||
|
type SettingsManager,
|
||||||
|
} from "@earendil-works/pi-coding-agent";
|
||||||
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
||||||
import { loadModelsWithCache, withModelRuntimeError, type ModelsData } from "@/lib/models-cache";
|
import {
|
||||||
import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access";
|
loadModelsWithCache,
|
||||||
|
withModelRuntimeError,
|
||||||
|
type ModelsData,
|
||||||
|
} from "@/lib/models-cache";
|
||||||
|
import {
|
||||||
|
getAllowedFileRoots,
|
||||||
|
isExistingFilePathAllowed,
|
||||||
|
} from "@/lib/file-access";
|
||||||
import { projectTrustReloadOptions } from "@/lib/project-trust";
|
import { projectTrustReloadOptions } from "@/lib/project-trust";
|
||||||
import { createAppSettingsManager, getAppResourceLoaderOptions } from "@/lib/app-runtime";
|
import {
|
||||||
|
createAppSettingsManager,
|
||||||
|
getAppResourceLoaderOptions,
|
||||||
|
} from "@/lib/app-runtime";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const modelNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
const modelNameCollator = new Intl.Collator(undefined, {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: "base",
|
||||||
|
});
|
||||||
|
|
||||||
function compareModelEntries(
|
function compareModelEntries(
|
||||||
a: { id: string; name: string; provider: string },
|
a: { id: string; name: string; provider: string },
|
||||||
b: { id: string; name: string; provider: string }
|
b: { id: string; name: string; provider: string },
|
||||||
): number {
|
): number {
|
||||||
return modelNameCollator.compare(a.name || a.id, b.name || b.id)
|
return (
|
||||||
|| modelNameCollator.compare(a.provider, b.provider)
|
modelNameCollator.compare(a.name || a.id, b.name || b.id) ||
|
||||||
|| modelNameCollator.compare(a.id, b.id);
|
modelNameCollator.compare(a.provider, b.provider) ||
|
||||||
|
modelNameCollator.compare(a.id, b.id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const THINKING_SUFFIXES = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
const THINKING_SUFFIXES = new Set([
|
||||||
|
"off",
|
||||||
|
"minimal",
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"xhigh",
|
||||||
|
"max",
|
||||||
|
]);
|
||||||
|
|
||||||
function stripThinkingSuffix(modelRef: string): string {
|
function stripThinkingSuffix(modelRef: string): string {
|
||||||
const trimmed = modelRef.trim();
|
const trimmed = modelRef.trim();
|
||||||
const colonIndex = trimmed.lastIndexOf(":");
|
const colonIndex = trimmed.lastIndexOf(":");
|
||||||
if (colonIndex === -1) return trimmed;
|
if (colonIndex === -1) return trimmed;
|
||||||
const suffix = trimmed.substring(colonIndex + 1);
|
const suffix = trimmed.substring(colonIndex + 1);
|
||||||
return THINKING_SUFFIXES.has(suffix) ? trimmed.substring(0, colonIndex) : trimmed;
|
return THINKING_SUFFIXES.has(suffix)
|
||||||
|
? trimmed.substring(0, colonIndex)
|
||||||
|
: trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterByExactEnabledModels<T extends { id: string; provider: string }>(
|
function filterByExactEnabledModels<T extends { id: string; provider: string }>(
|
||||||
available: readonly T[],
|
available: readonly T[],
|
||||||
enabledModels: string[] | undefined,
|
enabledModels: string[] | undefined,
|
||||||
): readonly T[] {
|
): readonly T[] {
|
||||||
if (!enabledModels || enabledModels.length === 0) return available;
|
if (!enabledModels || enabledModels.length === 0) return available;
|
||||||
|
|
||||||
const refs = new Set(enabledModels.map(stripThinkingSuffix).filter(Boolean));
|
const refs = new Set(enabledModels.map(stripThinkingSuffix).filter(Boolean));
|
||||||
const visible = available.filter((m) => refs.has(`${m.provider}/${m.id}`) || refs.has(m.id));
|
const visible = available.filter(
|
||||||
return visible.length > 0 ? visible : available;
|
(m) => refs.has(`${m.provider}/${m.id}`) || refs.has(m.id),
|
||||||
|
);
|
||||||
|
return visible.length > 0 ? visible : available;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadModels(cwd: string): Promise<ModelsData> {
|
async function loadModels(cwd: string): Promise<ModelsData> {
|
||||||
const nameMap = new Map<string, string>();
|
const nameMap = new Map<string, string>();
|
||||||
let modelList: { id: string; name: string; provider: string }[] = [];
|
let modelList: { id: string; name: string; provider: string }[] = [];
|
||||||
let defaultModel: { provider: string; modelId: string } | null = null;
|
let defaultModel: { provider: string; modelId: string } | null = null;
|
||||||
const thinkingLevels: Record<string, string[]> = {};
|
const thinkingLevels: Record<string, string[]> = {};
|
||||||
const thinkingLevelMaps: Record<string, Record<string, string | null>> = {};
|
const thinkingLevelMaps: Record<string, Record<string, string | null>> = {};
|
||||||
|
|
||||||
const agentDir = getAgentDir();
|
const agentDir = getAgentDir();
|
||||||
// Gate untrusted project extensions: enumerating models still imports and
|
// Gate untrusted project extensions: enumerating models still imports and
|
||||||
// runs a repository's .pi/extensions factories, so honor project trust here
|
// runs a repository's .pi/extensions factories, so honor project trust here
|
||||||
// too (see lib/project-trust.ts, #236).
|
// too (see lib/project-trust.ts, #236).
|
||||||
const trustReloadOptions = projectTrustReloadOptions(cwd, agentDir);
|
const trustReloadOptions = projectTrustReloadOptions(cwd, agentDir);
|
||||||
const services = await createAgentSessionServices({
|
const services = await createAgentSessionServices({
|
||||||
cwd,
|
cwd,
|
||||||
agentDir,
|
agentDir,
|
||||||
settingsManager: createAppSettingsManager(cwd, agentDir),
|
settingsManager: createAppSettingsManager(cwd, agentDir),
|
||||||
resourceLoaderOptions: getAppResourceLoaderOptions(),
|
resourceLoaderOptions: getAppResourceLoaderOptions(),
|
||||||
...(trustReloadOptions ? { resourceLoaderReloadOptions: trustReloadOptions } : {}),
|
...(trustReloadOptions
|
||||||
});
|
? { resourceLoaderReloadOptions: trustReloadOptions }
|
||||||
const available = await services.modelRuntime.getAvailable();
|
: {}),
|
||||||
const modelError = services.modelRuntime.getError();
|
});
|
||||||
const settings: SettingsManager = services.settingsManager;
|
const available = await services.modelRuntime.getAvailable();
|
||||||
const enabledModels = settings.getEnabledModels();
|
const modelError = services.modelRuntime.getError();
|
||||||
const visible = filterByExactEnabledModels(available, enabledModels);
|
const settings: SettingsManager = services.settingsManager;
|
||||||
modelList = visible.map((m: { id: string; name: string; provider: string }) => ({
|
const enabledModels = settings.getEnabledModels();
|
||||||
id: m.id,
|
const visible = filterByExactEnabledModels(available, enabledModels);
|
||||||
name: m.name,
|
modelList = visible
|
||||||
provider: m.provider,
|
.map(
|
||||||
})).sort(compareModelEntries);
|
(m: {
|
||||||
for (const m of visible) {
|
id: string;
|
||||||
const key = `${m.provider}:${m.id}`;
|
name: string;
|
||||||
nameMap.set(key, m.name);
|
provider: string;
|
||||||
thinkingLevels[key] = getSupportedThinkingLevels(m);
|
serviceTier?: string;
|
||||||
if (m.thinkingLevelMap) thinkingLevelMaps[key] = m.thinkingLevelMap;
|
}) => ({
|
||||||
}
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
provider: m.provider,
|
||||||
|
serviceTier: m.serviceTier,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.sort(compareModelEntries);
|
||||||
|
for (const m of visible) {
|
||||||
|
const key = `${m.provider}:${m.id}`;
|
||||||
|
nameMap.set(key, m.name);
|
||||||
|
thinkingLevels[key] = getSupportedThinkingLevels(m);
|
||||||
|
if (m.thinkingLevelMap) thinkingLevelMaps[key] = m.thinkingLevelMap;
|
||||||
|
}
|
||||||
|
|
||||||
const provider = settings.getDefaultProvider();
|
const provider = settings.getDefaultProvider();
|
||||||
const modelId = settings.getDefaultModel();
|
const modelId = settings.getDefaultModel();
|
||||||
if (provider && modelId && visible.some((m) => m.provider === provider && m.id === modelId)) {
|
if (
|
||||||
defaultModel = { provider, modelId };
|
provider &&
|
||||||
}
|
modelId &&
|
||||||
|
visible.some((m) => m.provider === provider && m.id === modelId)
|
||||||
|
) {
|
||||||
|
defaultModel = { provider, modelId };
|
||||||
|
}
|
||||||
|
|
||||||
return withModelRuntimeError(
|
return withModelRuntimeError(
|
||||||
{ models: Object.fromEntries(nameMap), modelList, defaultModel, thinkingLevels, thinkingLevelMaps },
|
{
|
||||||
modelError,
|
models: Object.fromEntries(nameMap),
|
||||||
);
|
modelList,
|
||||||
|
defaultModel,
|
||||||
|
thinkingLevels,
|
||||||
|
thinkingLevelMaps,
|
||||||
|
},
|
||||||
|
modelError,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_MODELS: ModelsData = {
|
const EMPTY_MODELS: ModelsData = {
|
||||||
models: {},
|
models: {},
|
||||||
modelList: [],
|
modelList: [],
|
||||||
defaultModel: null,
|
defaultModel: null,
|
||||||
thinkingLevels: {},
|
thinkingLevels: {},
|
||||||
thinkingLevelMaps: {},
|
thinkingLevelMaps: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
const requestedCwd = new URL(req.url).searchParams.get("cwd") || process.cwd();
|
const requestedCwd =
|
||||||
const cwd = resolve(requestedCwd);
|
new URL(req.url).searchParams.get("cwd") || process.cwd();
|
||||||
|
const cwd = resolve(requestedCwd);
|
||||||
|
|
||||||
let cwdStat;
|
let cwdStat;
|
||||||
try {
|
try {
|
||||||
cwdStat = await stat(cwd);
|
cwdStat = await stat(cwd);
|
||||||
} catch {
|
} catch {
|
||||||
return Response.json({ error: `Directory does not exist: ${cwd}` }, { status: 400 });
|
return Response.json(
|
||||||
}
|
{ error: `Directory does not exist: ${cwd}` },
|
||||||
if (!cwdStat.isDirectory()) {
|
{ status: 400 },
|
||||||
return Response.json({ error: `Not a directory: ${cwd}` }, { status: 400 });
|
);
|
||||||
}
|
}
|
||||||
const allowedRoots = await getAllowedFileRoots();
|
if (!cwdStat.isDirectory()) {
|
||||||
if (!isExistingFilePathAllowed(cwd, allowedRoots)) {
|
return Response.json({ error: `Not a directory: ${cwd}` }, { status: 400 });
|
||||||
return Response.json({ error: "Access denied" }, { status: 403 });
|
}
|
||||||
}
|
const allowedRoots = await getAllowedFileRoots();
|
||||||
|
if (!isExistingFilePathAllowed(cwd, allowedRoots)) {
|
||||||
|
return Response.json({ error: "Access denied" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Response.json(await loadModelsWithCache(cwd, () => loadModels(cwd)));
|
return Response.json(await loadModelsWithCache(cwd, () => loadModels(cwd)));
|
||||||
} catch {
|
} catch {
|
||||||
return Response.json(EMPTY_MODELS);
|
return Response.json(EMPTY_MODELS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3446
-2204
File diff suppressed because it is too large
Load Diff
@@ -207,11 +207,14 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
isNew,
|
isNew,
|
||||||
sessionIdRef, messagesEndRef, scrollContainerRef,
|
sessionIdRef, messagesEndRef, scrollContainerRef,
|
||||||
lastUserMsgRef,
|
lastUserMsgRef,
|
||||||
|
atBottom,
|
||||||
|
followStreamRef,
|
||||||
handleSend, handleAbort, handleFork, handleNavigate, handleModelChange,
|
handleSend, handleAbort, handleFork, handleNavigate, handleModelChange,
|
||||||
handleCompact, handleSteer, handleFollowUp, handlePromptWithStreamingBehavior, handleAbortCompaction,
|
handleCompact, handleSteer, handleFollowUp, handlePromptWithStreamingBehavior, handleAbortCompaction,
|
||||||
handleRecallQueue,
|
handleRecallQueue,
|
||||||
handleBuiltinSlashCommand,
|
handleBuiltinSlashCommand,
|
||||||
handleToolPresetChange, handleThinkingLevelChange, loadSlashCommands,
|
handleToolPresetChange, handleThinkingLevelChange, loadSlashCommands,
|
||||||
|
scrollToBottom,
|
||||||
} = useAgentSession({
|
} = useAgentSession({
|
||||||
session, newSessionCwd, onAgentEnd: wrappedOnAgentEnd, onSessionCreated, onSessionForked,
|
session, newSessionCwd, onAgentEnd: wrappedOnAgentEnd, onSessionCreated, onSessionForked,
|
||||||
modelsRefreshKey, chatInputRef, onBranchDataChange, onSystemPromptChange, onSessionStatsPanelOpen,
|
modelsRefreshKey, chatInputRef, onBranchDataChange, onSystemPromptChange, onSessionStatsPanelOpen,
|
||||||
@@ -259,6 +262,30 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
container.scrollTop = restoreScrollTop(container.scrollHeight, prevScrollDistanceRef.current);
|
container.scrollTop = restoreScrollTop(container.scrollHeight, prevScrollDistanceRef.current);
|
||||||
prevScrollDistanceRef.current = null;
|
prevScrollDistanceRef.current = null;
|
||||||
}, [visibleCount, scrollContainerRef]);
|
}, [visibleCount, scrollContainerRef]);
|
||||||
|
|
||||||
|
// Live-follow the model's streaming output: pin the streaming bubble to
|
||||||
|
// the bottom of the viewport on every chunk while auto-follow is engaged
|
||||||
|
// (the default). Scrolling away from the bottom flips followStreamRef off
|
||||||
|
// inside useAgentSession; scrolling back to the bottom or clicking the
|
||||||
|
// scroll-to-bottom button re-engages it.
|
||||||
|
const streamBubbleRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!streamState.isStreaming || !streamState.streamingMessage) return;
|
||||||
|
if (!followStreamRef.current) return;
|
||||||
|
streamBubbleRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||||
|
}, [streamState]);
|
||||||
|
|
||||||
|
// Jump back to the newest content: scroll to the streaming bubble while a
|
||||||
|
// stream is live (its bottom edge is the live bottom), otherwise to the
|
||||||
|
// end of the conversation. Also re-engages auto-follow.
|
||||||
|
const handleScrollToConversationBottom = useCallback(() => {
|
||||||
|
followStreamRef.current = true;
|
||||||
|
if (streamState.isStreaming && streamState.streamingMessage) {
|
||||||
|
streamBubbleRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
||||||
|
} else {
|
||||||
|
scrollToBottom("smooth");
|
||||||
|
}
|
||||||
|
}, [streamState, followStreamRef, scrollToBottom]);
|
||||||
// Push session stats up to AppShell for the top bar.
|
// Push session stats up to AppShell for the top bar.
|
||||||
// Compare scalar fields to avoid loops from new object identity each render.
|
// Compare scalar fields to avoid loops from new object identity each render.
|
||||||
const statsKey = sessionStats
|
const statsKey = sessionStats
|
||||||
@@ -341,6 +368,36 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
onSteer={agentRunning ? handleSteer : undefined}
|
onSteer={agentRunning ? handleSteer : undefined}
|
||||||
onFollowUp={agentRunning ? handleFollowUp : undefined}
|
onFollowUp={agentRunning ? handleFollowUp : undefined}
|
||||||
onPromptWithStreamingBehavior={agentRunning ? handlePromptWithStreamingBehavior : undefined}
|
onPromptWithStreamingBehavior={agentRunning ? handlePromptWithStreamingBehavior : undefined}
|
||||||
|
scrollToBottomButton={
|
||||||
|
isEmptyNew ? null : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleScrollToConversationBottom}
|
||||||
|
aria-label={t("chat.scrollToBottom")}
|
||||||
|
title={t("chat.scrollToBottom")}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
background: "var(--bg-panel)",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: atBottom ? 0.55 : 1,
|
||||||
|
transition: "opacity 0.15s, background 0.15s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="6 9 12 15 18 9" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
isStreaming={sessionBusy}
|
isStreaming={sessionBusy}
|
||||||
model={displayModelValue}
|
model={displayModelValue}
|
||||||
isAutoModelSelection={isAutoModelSelection}
|
isAutoModelSelection={isAutoModelSelection}
|
||||||
@@ -683,7 +740,9 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{streamState.isStreaming && streamState.streamingMessage && (
|
{streamState.isStreaming && streamState.streamingMessage && (
|
||||||
<MessageView message={streamState.streamingMessage as AgentMessage} isStreaming modelNames={modelNames} cwd={messageCwd} onOpenFile={onOpenFile} />
|
<div ref={streamBubbleRef}>
|
||||||
|
<MessageView message={streamState.streamingMessage as AgentMessage} isStreaming modelNames={modelNames} cwd={messageCwd} onOpenFile={onOpenFile} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{agentRunning && !streamState.streamingMessage && (
|
{agentRunning && !streamState.streamingMessage && (
|
||||||
@@ -710,10 +769,6 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{agentRunning && (
|
|
||||||
<div style={{ height: scrollContainerRef.current ? scrollContainerRef.current.clientHeight : "80vh" }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2206
-1539
File diff suppressed because it is too large
Load Diff
@@ -240,6 +240,7 @@ export const enLocale: LocalePlugin = {
|
|||||||
"chat.commandCopy": "Copy the last assistant message",
|
"chat.commandCopy": "Copy the last assistant message",
|
||||||
"chat.compacted": "Compacted",
|
"chat.compacted": "Compacted",
|
||||||
"chat.tokensSaved": "{saved} saved",
|
"chat.tokensSaved": "{saved} saved",
|
||||||
|
"chat.scrollToBottom": "Scroll to bottom",
|
||||||
"mcp.addServer": "Add server",
|
"mcp.addServer": "Add server",
|
||||||
"mcp.addEnv": "Add env",
|
"mcp.addEnv": "Add env",
|
||||||
"mcp.serverName": "Server name",
|
"mcp.serverName": "Server name",
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export const zhCNLocale: LocalePlugin = {
|
|||||||
"chat.commandCopy": "复制最后一条助手消息",
|
"chat.commandCopy": "复制最后一条助手消息",
|
||||||
"chat.compacted": "已压缩",
|
"chat.compacted": "已压缩",
|
||||||
"chat.tokensSaved": "节省 {saved}",
|
"chat.tokensSaved": "节省 {saved}",
|
||||||
|
"chat.scrollToBottom": "滚动到底部",
|
||||||
"mcp.addServer": "添加服务器",
|
"mcp.addServer": "添加服务器",
|
||||||
"mcp.addEnv": "添加环境变量",
|
"mcp.addEnv": "添加环境变量",
|
||||||
"mcp.serverName": "服务器名称",
|
"mcp.serverName": "服务器名称",
|
||||||
|
|||||||
+70
-56
@@ -1,80 +1,94 @@
|
|||||||
export interface ModelsData {
|
export interface ModelsData {
|
||||||
models: Record<string, string>;
|
models: Record<string, string>;
|
||||||
modelList: { id: string; name: string; provider: string }[];
|
modelList: {
|
||||||
defaultModel: { provider: string; modelId: string } | null;
|
id: string;
|
||||||
thinkingLevels: Record<string, string[]>;
|
name: string;
|
||||||
thinkingLevelMaps: Record<string, Record<string, string | null>>;
|
provider: string;
|
||||||
modelError?: string;
|
serviceTier?: string;
|
||||||
|
}[];
|
||||||
|
defaultModel: { provider: string; modelId: string } | null;
|
||||||
|
thinkingLevels: Record<string, string[]>;
|
||||||
|
thinkingLevelMaps: Record<string, Record<string, string | null>>;
|
||||||
|
modelError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelsCacheState {
|
interface ModelsCacheState {
|
||||||
entries: Map<string, { data: ModelsData; expiresAt: number }>;
|
entries: Map<string, { data: ModelsData; expiresAt: number }>;
|
||||||
inFlight: Map<string, Promise<ModelsData>>;
|
inFlight: Map<string, Promise<ModelsData>>;
|
||||||
generation: number;
|
generation: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var __piModelsCacheState: ModelsCacheState | undefined;
|
var __piModelsCacheState: ModelsCacheState | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODELS_CACHE_TTL_MS = 60_000;
|
const MODELS_CACHE_TTL_MS = 60_000;
|
||||||
const MAX_MODELS_CACHE_ENTRIES = 32;
|
const MAX_MODELS_CACHE_ENTRIES = 32;
|
||||||
|
|
||||||
function getModelsCacheState(): ModelsCacheState {
|
function getModelsCacheState(): ModelsCacheState {
|
||||||
if (!globalThis.__piModelsCacheState) {
|
if (!globalThis.__piModelsCacheState) {
|
||||||
globalThis.__piModelsCacheState = {
|
globalThis.__piModelsCacheState = {
|
||||||
entries: new Map(),
|
entries: new Map(),
|
||||||
inFlight: new Map(),
|
inFlight: new Map(),
|
||||||
generation: 0,
|
generation: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return globalThis.__piModelsCacheState;
|
return globalThis.__piModelsCacheState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invalidateModelsCache(): void {
|
export function invalidateModelsCache(): void {
|
||||||
const state = getModelsCacheState();
|
const state = getModelsCacheState();
|
||||||
state.generation += 1;
|
state.generation += 1;
|
||||||
state.entries.clear();
|
state.entries.clear();
|
||||||
state.inFlight.clear();
|
state.inFlight.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withModelRuntimeError(data: ModelsData, modelError: string | undefined): ModelsData {
|
export function withModelRuntimeError(
|
||||||
return modelError ? { ...data, modelError } : data;
|
data: ModelsData,
|
||||||
|
modelError: string | undefined,
|
||||||
|
): ModelsData {
|
||||||
|
return modelError ? { ...data, modelError } : data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadModelsWithCache(cwd: string, loader: () => Promise<ModelsData>): Promise<ModelsData> {
|
export function loadModelsWithCache(
|
||||||
const state = getModelsCacheState();
|
cwd: string,
|
||||||
const cached = state.entries.get(cwd);
|
loader: () => Promise<ModelsData>,
|
||||||
if (cached) {
|
): Promise<ModelsData> {
|
||||||
if (cached.expiresAt > Date.now()) return Promise.resolve(cached.data);
|
const state = getModelsCacheState();
|
||||||
state.entries.delete(cwd);
|
const cached = state.entries.get(cwd);
|
||||||
}
|
if (cached) {
|
||||||
|
if (cached.expiresAt > Date.now()) return Promise.resolve(cached.data);
|
||||||
|
state.entries.delete(cwd);
|
||||||
|
}
|
||||||
|
|
||||||
const existingLoad = state.inFlight.get(cwd);
|
const existingLoad = state.inFlight.get(cwd);
|
||||||
if (existingLoad) return existingLoad;
|
if (existingLoad) return existingLoad;
|
||||||
|
|
||||||
const generation = state.generation;
|
const generation = state.generation;
|
||||||
const loadPromise: Promise<ModelsData> = Promise.resolve()
|
const loadPromise: Promise<ModelsData> = Promise.resolve()
|
||||||
.then(loader)
|
.then(loader)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (state.generation === generation && state.inFlight.get(cwd) === loadPromise) {
|
if (
|
||||||
const now = Date.now();
|
state.generation === generation &&
|
||||||
for (const [key, entry] of state.entries) {
|
state.inFlight.get(cwd) === loadPromise
|
||||||
if (entry.expiresAt <= now) state.entries.delete(key);
|
) {
|
||||||
}
|
const now = Date.now();
|
||||||
while (state.entries.size >= MAX_MODELS_CACHE_ENTRIES) {
|
for (const [key, entry] of state.entries) {
|
||||||
const oldestKey = state.entries.keys().next().value;
|
if (entry.expiresAt <= now) state.entries.delete(key);
|
||||||
if (oldestKey === undefined) break;
|
}
|
||||||
state.entries.delete(oldestKey);
|
while (state.entries.size >= MAX_MODELS_CACHE_ENTRIES) {
|
||||||
}
|
const oldestKey = state.entries.keys().next().value;
|
||||||
state.entries.set(cwd, { data, expiresAt: now + MODELS_CACHE_TTL_MS });
|
if (oldestKey === undefined) break;
|
||||||
}
|
state.entries.delete(oldestKey);
|
||||||
return data;
|
}
|
||||||
})
|
state.entries.set(cwd, { data, expiresAt: now + MODELS_CACHE_TTL_MS });
|
||||||
.finally(() => {
|
}
|
||||||
if (state.inFlight.get(cwd) === loadPromise) state.inFlight.delete(cwd);
|
return data;
|
||||||
});
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (state.inFlight.get(cwd) === loadPromise) state.inFlight.delete(cwd);
|
||||||
|
});
|
||||||
|
|
||||||
state.inFlight.set(cwd, loadPromise);
|
state.inFlight.set(cwd, loadPromise);
|
||||||
return loadPromise;
|
return loadPromise;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export function buildBaseOptions(
|
|||||||
maxRetries: options?.maxRetries,
|
maxRetries: options?.maxRetries,
|
||||||
maxRetryDelayMs: options?.maxRetryDelayMs,
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
||||||
metadata: options?.metadata,
|
metadata: options?.metadata,
|
||||||
|
serviceTier: options?.serviceTier,
|
||||||
env: options?.env,
|
env: options?.env,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,13 @@ export interface ProviderResponse {
|
|||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request-level service tier for providers that support it.
|
||||||
|
* OpenAI Codex / Responses: "priority" is fast mode (higher cost, lower latency), "flex" is the cheaper tier.
|
||||||
|
* See https://developers.openai.com/codex/speed for Codex fast mode semantics.
|
||||||
|
*/
|
||||||
|
export type ServiceTier = "auto" | "default" | "flex" | "priority" | "scale";
|
||||||
|
|
||||||
export interface StreamOptions {
|
export interface StreamOptions {
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
@@ -189,6 +196,12 @@ export interface StreamOptions {
|
|||||||
* For example, Anthropic uses `user_id` for abuse tracking and rate limiting.
|
* For example, Anthropic uses `user_id` for abuse tracking and rate limiting.
|
||||||
*/
|
*/
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* Request-level service tier. Providers that understand it (OpenAI Codex,
|
||||||
|
* OpenAI Responses) send it as `service_tier`; other providers ignore it.
|
||||||
|
* "priority" is Codex fast mode (1.5x speed, ~2x cost).
|
||||||
|
*/
|
||||||
|
serviceTier?: ServiceTier | null;
|
||||||
/**
|
/**
|
||||||
* Provider-scoped environment values. These take precedence over process.env for
|
* Provider-scoped environment values. These take precedence over process.env for
|
||||||
* provider configuration such as regional settings, endpoint placeholders, and
|
* provider configuration such as regional settings, endpoint placeholders, and
|
||||||
@@ -773,6 +786,12 @@ export interface Model<TApi extends Api> {
|
|||||||
contextWindow: number;
|
contextWindow: number;
|
||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Optional default request-level service tier for this model (e.g. "priority"
|
||||||
|
* for Codex fast mode). Configured via models.json; forwarded to providers
|
||||||
|
* that support `service_tier`.
|
||||||
|
*/
|
||||||
|
serviceTier?: ServiceTier;
|
||||||
/** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */
|
/** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */
|
||||||
compat?: TApi extends "openai-completions"
|
compat?: TApi extends "openai-completions"
|
||||||
? OpenAICompletionsCompat
|
? OpenAICompletionsCompat
|
||||||
|
|||||||
@@ -151,6 +151,13 @@ const ModelCostSchema = Type.Object({
|
|||||||
tiers: Type.Optional(Type.Array(ModelCostTierSchema)),
|
tiers: Type.Optional(Type.Array(ModelCostTierSchema)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ServiceTierSchema = Type.Union([
|
||||||
|
Type.Literal("auto"),
|
||||||
|
Type.Literal("default"),
|
||||||
|
Type.Literal("priority"),
|
||||||
|
Type.Literal("flex"),
|
||||||
|
]);
|
||||||
|
|
||||||
const ModelDefinitionSchema = Type.Object({
|
const ModelDefinitionSchema = Type.Object({
|
||||||
id: Type.String({ minLength: 1 }),
|
id: Type.String({ minLength: 1 }),
|
||||||
name: Type.Optional(Type.String({ minLength: 1 })),
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
@@ -163,6 +170,7 @@ const ModelDefinitionSchema = Type.Object({
|
|||||||
contextWindow: Type.Optional(Type.Number()),
|
contextWindow: Type.Optional(Type.Number()),
|
||||||
maxTokens: Type.Optional(Type.Number()),
|
maxTokens: Type.Optional(Type.Number()),
|
||||||
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
|
serviceTier: Type.Optional(ServiceTierSchema),
|
||||||
compat: Type.Optional(ProviderCompatSchema),
|
compat: Type.Optional(ProviderCompatSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,6 +191,7 @@ const ModelOverrideSchema = Type.Object({
|
|||||||
contextWindow: Type.Optional(Type.Number()),
|
contextWindow: Type.Optional(Type.Number()),
|
||||||
maxTokens: Type.Optional(Type.Number()),
|
maxTokens: Type.Optional(Type.Number()),
|
||||||
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
|
serviceTier: Type.Optional(ServiceTierSchema),
|
||||||
compat: Type.Optional(ProviderCompatSchema),
|
compat: Type.Optional(ProviderCompatSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ function applyModelOverride(model: Model<Api>, override: ModelsJsonModelOverride
|
|||||||
: model.cost,
|
: model.cost,
|
||||||
contextWindow: override.contextWindow ?? model.contextWindow,
|
contextWindow: override.contextWindow ?? model.contextWindow,
|
||||||
maxTokens: override.maxTokens ?? model.maxTokens,
|
maxTokens: override.maxTokens ?? model.maxTokens,
|
||||||
|
serviceTier: override.serviceTier ?? model.serviceTier,
|
||||||
compat: mergeCompat(model.compat, override.compat),
|
compat: mergeCompat(model.compat, override.compat),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -154,6 +155,7 @@ function modelFromJson(
|
|||||||
contextWindow: definition.contextWindow ?? 128000,
|
contextWindow: definition.contextWindow ?? 128000,
|
||||||
maxTokens: definition.maxTokens ?? 16384,
|
maxTokens: definition.maxTokens ?? 16384,
|
||||||
headers: undefined,
|
headers: undefined,
|
||||||
|
serviceTier: definition.serviceTier,
|
||||||
compat: mergeCompat(providerConfig.compat, definition.compat),
|
compat: mergeCompat(providerConfig.compat, definition.compat),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,8 +309,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||||||
const websocketConnectTimeoutMs =
|
const websocketConnectTimeoutMs =
|
||||||
options?.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs();
|
options?.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs();
|
||||||
const headerRunner = extensionRunnerRef.current;
|
const headerRunner = extensionRunnerRef.current;
|
||||||
|
// Model-level service tier (models.json `serviceTier`, e.g. "priority" for
|
||||||
|
// Codex fast mode) is forwarded as request `service_tier`. Providers that
|
||||||
|
// don't understand it ignore the option.
|
||||||
|
const serviceTier = model.serviceTier;
|
||||||
return modelRuntime.streamSimple(model, context, {
|
return modelRuntime.streamSimple(model, context, {
|
||||||
...options,
|
...options,
|
||||||
|
...(serviceTier ? { serviceTier } : {}),
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
websocketConnectTimeoutMs,
|
websocketConnectTimeoutMs,
|
||||||
maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,
|
maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ This directory is the version-controlled resource layer for the integrated app.
|
|||||||
|
|
||||||
- `skills/` contains authored or reviewed skills.
|
- `skills/` contains authored or reviewed skills.
|
||||||
- `extensions/` contains Pi extensions.
|
- `extensions/` contains Pi extensions.
|
||||||
|
- `packages/` contains repository-local Pi packages, including the repaired `pi-smart-fetch` build.
|
||||||
- `prompts/` contains prompt templates.
|
- `prompts/` contains prompt templates.
|
||||||
- `themes/` contains custom Pi themes.
|
- `themes/` contains custom Pi themes.
|
||||||
|
|
||||||
The managed launcher loads these directories for every workspace. Project-local
|
The managed launcher loads these directories for every workspace. Project-local
|
||||||
`.pi` and `.agents` resources from an opened workspace are not loaded.
|
`.pi` and `.agents` resources from an opened workspace are not loaded.
|
||||||
|
|
||||||
|
The local `resources/packages/pi-smart-fetch` package is configured through
|
||||||
|
`config/settings.default.json`; setup installs its runtime dependencies and
|
||||||
|
migrates older `npm:pi-smart-fetch` profile entries automatically.
|
||||||
|
|
||||||
## Included extensions
|
## Included extensions
|
||||||
|
|
||||||
- `extensions/package.json` is the explicit Pi extension manifest. Add future
|
- `extensions/package.json` is the explicit Pi extension manifest. Add future
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Thinkscape
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# pi-smart-fetch (project-local repair)
|
||||||
|
|
||||||
|
This vendored build is based on `pi-smart-fetch@0.3.17` and adds the ESM/CommonJS compatibility shim required by this project's Node/Next.js runtime. The bundled CommonJS dependencies receive a real `require` from `createRequire(import.meta.url)`, so loading the extension no longer fails with `Dynamic require of "path" is not supported`.
|
||||||
|
|
||||||
|
`pi-smart-fetch` adds smarter web fetching tools to pi.dev.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔐 **Browser-like TLS/SSL + HTTP fingerprints** — better success on bot-defended pages
|
||||||
|
- 🧹 **Defuddle extraction** — clean readable content instead of noisy HTML
|
||||||
|
- 🧠 **Useful metadata** — title, author, site, language, published date when available
|
||||||
|
- 📦 **Downloads + large file support** — stream attachments and binaries to temp files
|
||||||
|
- 🔁 **Client-side `<meta>` redirects** — follows sane meta refresh redirects with loop limits
|
||||||
|
- 🔗 **Alternate content fallback** — when extraction produces no/thin content, follows qualified `<link rel="alternate" type="...">` entries in `<head>` that match the requested output format
|
||||||
|
- ⚡ **Batch fetch** — fetch many URLs with bounded concurrency
|
||||||
|
- 📝 **Multiple output formats** — `markdown`, `html`, `text`, `json`, `raw`
|
||||||
|
|
||||||
|
## Site optimisations
|
||||||
|
|
||||||
|
This package works on general web pages, but some site types benefit especially from Defuddle's extractors and cleanup:
|
||||||
|
|
||||||
|
- YouTube pages and transcripts
|
||||||
|
- Reddit posts and comment threads
|
||||||
|
- X / Twitter posts
|
||||||
|
- GitHub pages, issues, PRs, and discussions
|
||||||
|
- Hacker News threads
|
||||||
|
- Substack posts
|
||||||
|
- Pages with code blocks, footnotes, math, and callouts
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Defuddle is the cleanup layer: it strips common page chrome like nav, sidebars, related links, share widgets, and footers
|
||||||
|
- It does **not** execute JavaScript or solve interactive anti-bot/login flows
|
||||||
|
- If an HTML shell advertises alternate content in `<head>`, smart-fetch can follow matching alternates such as `text/markdown`, `text/plain`, `text/html`, or JSON media types according to the requested `format`
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
This repository loads the repaired package automatically from
|
||||||
|
`resources/packages/pi-smart-fetch`. The setup script installs its runtime
|
||||||
|
dependencies into that package directory; `config/settings.default.json`
|
||||||
|
points Pi at the local package instead of the broken npm build.
|
||||||
|
|
||||||
|
The original upstream package can still be installed separately when needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi install npm:pi-smart-fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pi tools
|
||||||
|
|
||||||
|
Registers:
|
||||||
|
|
||||||
|
- `web_fetch`
|
||||||
|
- `batch_web_fetch`
|
||||||
|
|
||||||
|
Synopsis:
|
||||||
|
|
||||||
|
```text
|
||||||
|
web_fetch(url, browser?, os?, headers?, maxChars?, timeoutMs?, format?, removeImages?, includeReplies?, proxy?, verbose?)
|
||||||
|
batch_web_fetch(requests, verbose?)
|
||||||
|
```
|
||||||
|
|
||||||
|
For `batch_web_fetch`, each item in `requests` accepts the same parameters as `web_fetch` except `verbose`.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
| Format | What you get |
|
||||||
|
| --- | --- |
|
||||||
|
| `markdown` | Best default for readable page content |
|
||||||
|
| `html` | Cleaned HTML output |
|
||||||
|
| `text` | Plain text with markdown stripped |
|
||||||
|
| `json` | Structured JSON for metadata-heavy workflows |
|
||||||
|
| `raw` | Full raw server response without extraction or truncation — for further parsing |
|
||||||
|
|
||||||
|
## Global defaults
|
||||||
|
|
||||||
|
Optional settings in `~/.pi/agent/settings.json` or `.pi/settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smartFetchVerboseByDefault": false,
|
||||||
|
"smartFetchDefaultMaxChars": 50000,
|
||||||
|
"smartFetchDefaultTimeoutMs": 15000,
|
||||||
|
"smartFetchDefaultBrowser": "chrome_145",
|
||||||
|
"smartFetchDefaultOs": "windows",
|
||||||
|
"smartFetchDefaultRemoveImages": false,
|
||||||
|
"smartFetchDefaultIncludeReplies": "extractors",
|
||||||
|
"smartFetchDefaultBatchConcurrency": 8,
|
||||||
|
"smartFetchTempDir": "/tmp/smart-fetch-pi"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| `smartFetchVerboseByDefault` | `false` | Stored default for the compatibility `verbose` flag |
|
||||||
|
| `smartFetchDefaultMaxChars` | `50000` | Default `maxChars` limit |
|
||||||
|
| `smartFetchDefaultTimeoutMs` | `15000` | Default request timeout in milliseconds |
|
||||||
|
| `smartFetchDefaultBrowser` | `chrome_145` | Default browser fingerprint profile |
|
||||||
|
| `smartFetchDefaultOs` | `windows` | Default OS fingerprint profile |
|
||||||
|
| `smartFetchDefaultRemoveImages` | `false` | Strip image references by default |
|
||||||
|
| `smartFetchDefaultIncludeReplies` | `extractors` | Include replies/comments only when site extractors support them |
|
||||||
|
| `smartFetchDefaultBatchConcurrency` | `8` | Default bounded concurrency for `batch_web_fetch` |
|
||||||
|
| `smartFetchTempDir` | OS temp dir | Base directory for attachment and binary downloads |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Project `.pi/settings.json` overrides global `~/.pi/agent/settings.json`
|
||||||
|
- Legacy `webFetch*` aliases are still supported
|
||||||
|
|
||||||
|
## Dev and publishing note
|
||||||
|
|
||||||
|
This repo uses Bun for local development, tests, and workspace scripts. Package publishing still goes through `npm publish` in CI so npm Trusted Publishing can be used.
|
||||||
+12412
File diff suppressed because it is too large
Load Diff
+469
@@ -0,0 +1,469 @@
|
|||||||
|
{
|
||||||
|
"name": "pi-smart-fetch-project-fix",
|
||||||
|
"version": "0.3.17-project.1",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "pi-smart-fetch-project-fix",
|
||||||
|
"version": "0.3.17-project.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@earendil-works/pi-tui": "^0.82.1",
|
||||||
|
"@sinclair/typebox": "^0.34.52",
|
||||||
|
"defuddle": "^0.19.2",
|
||||||
|
"linkedom": "^0.18.13",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
|
"wreq-js": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@earendil-works/pi-coding-agent": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@earendil-works/pi-tui": {
|
||||||
|
"version": "0.82.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.82.1.tgz",
|
||||||
|
"integrity": "sha512-9yN8hALfKaxZq7n54EMxqhFCWnMi6LHkraMJ/1YjHiATq75XrI6XDMVppn9EDtiK7Fks8hUe1SDXUTrIvwRWfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "1.6.0",
|
||||||
|
"marked": "18.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mixmark-io/domino": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/@sinclair/typebox": {
|
||||||
|
"version": "0.34.52",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz",
|
||||||
|
"integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.9.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
|
||||||
|
"integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^2.0.0",
|
||||||
|
"css-what": "^8.0.0",
|
||||||
|
"domhandler": "^6.0.1",
|
||||||
|
"domutils": "^4.0.2",
|
||||||
|
"nth-check": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cssom": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/defuddle": {
|
||||||
|
"version": "0.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/defuddle/-/defuddle-0.19.2.tgz",
|
||||||
|
"integrity": "sha512-FY+a9Byo3uS04SQnQjLaguNoEA0Y58vXTB1XvvnOkO6M0zo897ipekv2Unci42QEY70emZ6OLe7C0vPojr1JEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^12.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"defuddle": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"linkedom": "^0.18.12",
|
||||||
|
"mathml-to-latex": "^1.8.0",
|
||||||
|
"temml": "^0.13.3",
|
||||||
|
"turndown": "^7.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^3.0.0",
|
||||||
|
"domhandler": "^6.0.0",
|
||||||
|
"entities": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^3.0.0",
|
||||||
|
"domelementtype": "^3.0.0",
|
||||||
|
"domhandler": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-east-asian-width": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-escaper": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"entities": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linkedom": {
|
||||||
|
"version": "0.18.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.13.tgz",
|
||||||
|
"integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"css-select": "^7.0.0",
|
||||||
|
"cssom": "^0.5.0",
|
||||||
|
"html-escaper": "^3.0.3",
|
||||||
|
"htmlparser2": "^10.1.0",
|
||||||
|
"uhyphen": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": ">= 2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "18.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz",
|
||||||
|
"integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mathml-to-latex": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mathml-to-latex/-/mathml-to-latex-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-gQ0uK3zqB8HwlfaXJkEL5rgaZNbKUiBMmBP/B/W+b+t6KcseLSuYb1b0BjLgS9ZiQa24ePkqTX8/6FaQuDL7wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@xmldom/xmldom": "^0.9.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/temml": {
|
||||||
|
"version": "0.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/temml/-/temml-0.13.4.tgz",
|
||||||
|
"integrity": "sha512-k1yolMBswx34Jw9hZn5Xh2/GBlwqlW+wiN7QaUYUMhSa1ypfti6rlCfSmCxWyooxH1G32C/Z+qVvgAsBOELZBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/turndown": {
|
||||||
|
"version": "7.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz",
|
||||||
|
"integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@mixmark-io/domino": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"npm": ">=9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/uhyphen": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/wreq-js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/wreq-js/-/wreq-js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-vaKasaKeskrDKEuuO5Q5uamEG9a6FrF5ZSicH7TCvYS4RxF7/gzaU/vYqwJzcs+uydyJPVWY1KCvfVCgp0tiGA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64",
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"os": [
|
||||||
|
"darwin",
|
||||||
|
"linux",
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "pi-smart-fetch-project-fix",
|
||||||
|
"version": "0.3.17-project.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "Project-local repaired build of pi-smart-fetch with ESM/CommonJS compatibility.",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Thinkscape/agent-smart-fetch"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"pi": {
|
||||||
|
"extensions": [
|
||||||
|
"./dist/index.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.19.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@earendil-works/pi-tui": "^0.82.1",
|
||||||
|
"@sinclair/typebox": "^0.34.52",
|
||||||
|
"defuddle": "^0.19.2",
|
||||||
|
"linkedom": "^0.18.13",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
|
"wreq-js": "^2.3.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@earendil-works/pi-coding-agent": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,98 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { configurePiMemory } from "./configure-pi-memory.mjs";
|
import { configurePiMemory } from "./configure-pi-memory.mjs";
|
||||||
import { configurePiAiVision } from "./configure-pi-ai-vision.mjs";
|
import { configurePiAiVision } from "./configure-pi-ai-vision.mjs";
|
||||||
import { agentDir, managedEnvironment, rootDir } from "./profile.mjs";
|
import { agentDir, managedEnvironment, rootDir } from "./profile.mjs";
|
||||||
|
|
||||||
const settings = JSON.parse(
|
let settings;
|
||||||
readFileSync(join(rootDir, "config", "settings.default.json"), "utf8"),
|
try {
|
||||||
);
|
settings = JSON.parse(readFileSync(join(rootDir, "config", "settings.default.json"), "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(`Invalid config/settings.default.json: ${message}`);
|
||||||
|
}
|
||||||
const packages = Array.isArray(settings.packages) ? settings.packages : [];
|
const packages = Array.isArray(settings.packages) ? settings.packages : [];
|
||||||
const cliPath = join(rootDir, "pi", "packages", "coding-agent", "dist", "cli.js");
|
const cliPath = join(rootDir, "pi", "packages", "coding-agent", "dist", "cli.js");
|
||||||
const env = managedEnvironment();
|
const env = managedEnvironment();
|
||||||
|
const npmCliPath = process.env.npm_execpath;
|
||||||
|
const localSmartFetchSource = "../../resources/packages/pi-smart-fetch";
|
||||||
|
|
||||||
|
function parseLocalSource(source) {
|
||||||
|
if (
|
||||||
|
source.startsWith("npm:") ||
|
||||||
|
source.startsWith("git:") ||
|
||||||
|
/^[a-z][a-z0-9+.-]*:\/\//i.test(source)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return resolve(agentDir, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageDependenciesReady(packageDir) {
|
||||||
|
const packageJsonPath = join(packageDir, "package.json");
|
||||||
|
if (!existsSync(packageJsonPath)) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||||
|
const dependencies = packageJson.dependencies ?? {};
|
||||||
|
return Object.keys(dependencies).every((name) =>
|
||||||
|
existsSync(join(packageDir, "node_modules", ...name.split("/"), "package.json")),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureLocalPackage(source) {
|
||||||
|
const packageDir = parseLocalSource(source);
|
||||||
|
if (!packageDir) return false;
|
||||||
|
if (!existsSync(join(packageDir, "package.json"))) {
|
||||||
|
throw new Error(`Local managed package is missing: ${packageDir}`);
|
||||||
|
}
|
||||||
|
if (packageDependenciesReady(packageDir)) {
|
||||||
|
console.log(`Local managed package ready: ${source}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Installing local managed package dependencies: ${source}`);
|
||||||
|
const args = ["install", "--prefix", packageDir, "--omit=dev", "--legacy-peer-deps"];
|
||||||
|
const result = npmCliPath
|
||||||
|
? spawnSync(process.execPath, [npmCliPath, ...args], { cwd: rootDir, env, stdio: "inherit" })
|
||||||
|
: spawnSync("npm", args, { cwd: rootDir, env, stdio: "inherit" });
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSmartFetchConfigured() {
|
||||||
|
const settingsPath = join(agentDir, "settings.json");
|
||||||
|
if (!existsSync(settingsPath)) return;
|
||||||
|
|
||||||
|
let settings;
|
||||||
|
try {
|
||||||
|
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return;
|
||||||
|
|
||||||
|
const packages = Array.isArray(settings.packages) ? settings.packages : [];
|
||||||
|
const migrated = packages.map((entry) =>
|
||||||
|
entry === "npm:pi-smart-fetch" ||
|
||||||
|
(typeof entry === "string" && entry.startsWith("npm:pi-smart-fetch@"))
|
||||||
|
? localSmartFetchSource
|
||||||
|
: entry,
|
||||||
|
);
|
||||||
|
if (!migrated.some((entry) => entry === localSmartFetchSource)) {
|
||||||
|
migrated.push(localSmartFetchSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JSON.stringify(packages) !== JSON.stringify(migrated)) {
|
||||||
|
writeFileSync(settingsPath, `${JSON.stringify({ ...settings, packages: migrated }, null, 2)}\n`, "utf8");
|
||||||
|
console.log(`Configured project-local smart fetch package: ${localSmartFetchSource}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseExactNpmSource(source) {
|
function parseExactNpmSource(source) {
|
||||||
if (!source.startsWith("npm:")) return undefined;
|
if (!source.startsWith("npm:")) return undefined;
|
||||||
@@ -32,8 +114,11 @@ function installedAtExpectedVersion(source) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureSmartFetchConfigured();
|
||||||
|
|
||||||
for (const source of packages) {
|
for (const source of packages) {
|
||||||
if (typeof source !== "string" || !source.trim()) continue;
|
if (typeof source !== "string" || !source.trim()) continue;
|
||||||
|
if (ensureLocalPackage(source)) continue;
|
||||||
if (installedAtExpectedVersion(source)) {
|
if (installedAtExpectedVersion(source)) {
|
||||||
console.log(`Managed package ready: ${source}`);
|
console.log(`Managed package ready: ${source}`);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+54
-2
@@ -1,7 +1,7 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
|
||||||
import { platform } from "node:os";
|
import { platform } from "node:os";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { delimiter, dirname, join, resolve } from "node:path";
|
||||||
import { loadEnvFile } from "node:process";
|
import { loadEnvFile } from "node:process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
@@ -55,6 +55,56 @@ const seedFiles = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const persistedWindowsEnvironmentKeys = ["SEARXNG_TOKEN", "SEARXNG_URL"];
|
const persistedWindowsEnvironmentKeys = ["SEARXNG_TOKEN", "SEARXNG_URL"];
|
||||||
|
const piWebBinDir = join(rootDir, "pi-web", "node_modules", ".bin");
|
||||||
|
const piCodingAgentPackageDir = join(
|
||||||
|
rootDir,
|
||||||
|
"pi-web",
|
||||||
|
"node_modules",
|
||||||
|
"@earendil-works",
|
||||||
|
"pi-coding-agent",
|
||||||
|
);
|
||||||
|
const managedPeerPackageDir = join(
|
||||||
|
agentDir,
|
||||||
|
"npm",
|
||||||
|
"node_modules",
|
||||||
|
"@earendil-works",
|
||||||
|
"pi-coding-agent",
|
||||||
|
);
|
||||||
|
|
||||||
|
function ensurePiSubagentRuntime() {
|
||||||
|
// pi-subagents resolves the host CLI through its optional coding-agent peer.
|
||||||
|
// The managed profile is installed separately from pi-web, so expose the
|
||||||
|
// existing local package instead of installing a second coding-agent copy.
|
||||||
|
if (!existsSync(piCodingAgentPackageDir)) return;
|
||||||
|
|
||||||
|
mkdirSync(dirname(managedPeerPackageDir), { recursive: true });
|
||||||
|
if (existsSync(managedPeerPackageDir)) return;
|
||||||
|
try {
|
||||||
|
if (lstatSync(managedPeerPackageDir)) return;
|
||||||
|
} catch {
|
||||||
|
// The peer link is absent; create it below.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
symlinkSync(
|
||||||
|
piCodingAgentPackageDir,
|
||||||
|
managedPeerPackageDir,
|
||||||
|
platform() === "win32" ? "junction" : "dir",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[subagents] could not link the local Pi CLI package: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prependPiWebBinToPath(baseEnv) {
|
||||||
|
if (!existsSync(piWebBinDir)) return baseEnv.PATH ?? baseEnv.Path;
|
||||||
|
const pathKey = baseEnv.PATH !== undefined || baseEnv.Path === undefined ? "PATH" : "Path";
|
||||||
|
const currentPath = baseEnv[pathKey] ?? "";
|
||||||
|
const entries = currentPath.split(delimiter).filter(Boolean);
|
||||||
|
if (!entries.includes(piWebBinDir)) entries.unshift(piWebBinDir);
|
||||||
|
return entries.join(delimiter);
|
||||||
|
}
|
||||||
|
|
||||||
function readPersistedWindowsEnvironment(baseEnv) {
|
function readPersistedWindowsEnvironment(baseEnv) {
|
||||||
if (platform() !== "win32") return {};
|
if (platform() !== "win32") return {};
|
||||||
@@ -112,10 +162,12 @@ export function ensureProfile({ quiet = false } = {}) {
|
|||||||
|
|
||||||
export function managedEnvironment(baseEnv = process.env) {
|
export function managedEnvironment(baseEnv = process.env) {
|
||||||
ensureProfile({ quiet: true });
|
ensureProfile({ quiet: true });
|
||||||
|
ensurePiSubagentRuntime();
|
||||||
const persistedEnvironment = readPersistedWindowsEnvironment(baseEnv);
|
const persistedEnvironment = readPersistedWindowsEnvironment(baseEnv);
|
||||||
return {
|
return {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
...persistedEnvironment,
|
...persistedEnvironment,
|
||||||
|
PATH: prependPiWebBinToPath({ ...baseEnv, ...persistedEnvironment }),
|
||||||
PI_AGENT_MANAGED_RUNTIME: "1",
|
PI_AGENT_MANAGED_RUNTIME: "1",
|
||||||
PI_AGENT_APP_ROOT: rootDir,
|
PI_AGENT_APP_ROOT: rootDir,
|
||||||
PI_AGENT_DATA_DIR: dataDir,
|
PI_AGENT_DATA_DIR: dataDir,
|
||||||
|
|||||||
Reference in New Issue
Block a user