mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
feat: prepare self-contained open-source release
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
# Copy this file to .env and fill only the services you use.
|
||||||
|
# .env is ignored by Git and loaded automatically by the managed launcher.
|
||||||
|
|
||||||
|
# Optional repository-local SearXNG extension. Both values are required when enabled.
|
||||||
|
SEARXNG_URL=
|
||||||
|
SEARXNG_TOKEN=
|
||||||
|
|
||||||
|
# Optional higher Context7 quota. Context7 also works at its public unauthenticated limit.
|
||||||
|
CONTEXT7_API_KEY=
|
||||||
|
|
||||||
|
# Optional built-in model-provider credentials. The Web UI can also configure providers.
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
GEMINI_API_KEY=
|
||||||
|
|
||||||
|
# Optional: move mutable runtime data while retaining the same isolation model.
|
||||||
|
# PI_AGENT_DATA_DIR=D:\path\to\pi-agent-data
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.js text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.jsonl text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.mjs text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.ps1 text eol=crlf
|
||||||
|
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.png binary
|
||||||
|
*.wasm binary
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22.19.0
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: |
|
||||||
|
pi/package-lock.json
|
||||||
|
pi-web/package-lock.json
|
||||||
|
- run: npm run setup
|
||||||
|
- run: npm run typecheck
|
||||||
|
- run: npm run test:managed
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
/data/
|
/data/
|
||||||
|
|
||||||
|
# Local configuration and credentials. Keep the shareable template only.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*.key
|
||||||
|
|
||||||
# Root-level dependency and build output, if added later.
|
# Root-level dependency and build output, if added later.
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/.next/
|
/.next/
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Contributing / 贡献指南
|
||||||
|
|
||||||
|
Contributions are welcome. The project is Windows-first and keeps Pi, Pi Web, managed runtime data, and version-controlled resources as separate layers.
|
||||||
|
|
||||||
|
欢迎提交改进。本项目以 Windows 为首要平台,并明确区分 Pi 源码、Pi Web 源码、运行时数据与可版本化资源。
|
||||||
|
|
||||||
|
## Development setup / 开发环境
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://github.com/luckyyzh/pi-agent-new.git
|
||||||
|
cd pi-agent-new
|
||||||
|
npm run setup
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Before submitting a change, run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run check
|
||||||
|
npm run check:profile
|
||||||
|
npm run typecheck
|
||||||
|
npm run test:managed
|
||||||
|
```
|
||||||
|
|
||||||
|
提交前请运行以上检查。它们不会发起付费模型请求。
|
||||||
|
|
||||||
|
## Repository rules / 仓库约定
|
||||||
|
|
||||||
|
- Never commit `.env`, `data/`, credentials, sessions, memory, browser output, or local model/provider configuration.
|
||||||
|
- 不要提交 `.env`、`data/`、凭据、会话、记忆、浏览器输出或本地模型配置。
|
||||||
|
- Put shareable defaults under `config/` and shareable skills, extensions, prompts, or themes under `resources/`.
|
||||||
|
- 可共享缺省配置放在 `config/`,可共享 Skill、扩展、提示词和主题放在 `resources/`。
|
||||||
|
- Keep runtime package versions pinned in `config/settings.default.json` and update the plugin table and `THIRD_PARTY_NOTICES.md` together.
|
||||||
|
- 更新运行时插件时必须固定版本,并同步 README 与第三方声明。
|
||||||
|
- Preserve upstream licenses and record Pi/Pi Web baseline commits when synchronizing source.
|
||||||
|
- 合并上游源码时保留许可证,并更新基线提交信息。
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Pi Agent Integrated contributors
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -2,47 +2,39 @@
|
|||||||
|
|
||||||
[中文](#中文) · [English](#english)
|
[中文](#中文) · [English](#english)
|
||||||
|
|
||||||
An integrated, repository-local distribution of [Pi](https://github.com/earendil-works/pi) and [Pi Web](https://github.com/agegr/pi-web).
|
A Windows-first, repository-local distribution that connects the [Pi](https://github.com/earendil-works/pi) agent runtime to the [Pi Web](https://github.com/agegr/pi-web) browser UI and adds a managed plugin/tool ecosystem.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 中文
|
## 中文
|
||||||
|
|
||||||
Pi Agent Integrated 将 Pi 后端运行时与 Pi Web 浏览器前端整合为一个可独立克隆、配置和运行的项目。项目采用前后端分离结构,但通过根目录脚本统一安装、构建和启动。
|
Pi Agent Integrated 将 Pi 后端与 Pi Web 前端整合成一个可独立克隆、配置和启动的项目。它保留前后端源码边界,通过根目录命令完成依赖安装、Pi 构建、插件安装、Profile 初始化和 Web 启动。
|
||||||
|
|
||||||
运行时状态、会话、认证、Skill、扩展、提示词和主题均可保存在项目目录内,不依赖用户全局的 `~/.pi/agent` 或 `~/.agents`,也不会自动加载所打开工作区中的 `.pi` / `.agents` 资源。
|
### 与两个源项目有什么不同
|
||||||
|
|
||||||
### 当前状态
|
| 能力 | Pi | Pi Web | Pi Agent Integrated |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 主要定位 | CLI/TUI Agent 运行时 | Pi 的浏览器 UI | 带 Web UI 的完整本地 Agent 应用 |
|
||||||
|
| 安装方式 | 安装 CLI 或从源码构建 | 单独连接 Pi 包 | 根目录一次 `npm run setup` |
|
||||||
|
| 运行数据 | 默认使用用户主目录 | 跟随 Pi Profile | 会话、认证、记忆、插件、缓存全部进入项目 `data/` |
|
||||||
|
| 默认工作目录 | 当前终端目录 | 在用户主目录创建日期目录 | `data/workspaces/default/` |
|
||||||
|
| 扩展生态 | 支持 Skill、扩展和包 | 提供管理界面 | 固定版本插件、MCP、Skill、提示词和主题形成项目闭环 |
|
||||||
|
| 网络搜索 | 无项目专属搜索 | 无项目专属搜索 | 可选 SearXNG `web_search` 扩展 |
|
||||||
|
| 浏览器自动化 | 需自行接入 | 无默认浏览器 MCP | Playwright MCP,使用系统 Edge,按需启动 |
|
||||||
|
| 恢复与长期状态 | 会话树与基础状态 | 会话 UI | Rewind 检查点、项目内 Memory、自动重试 |
|
||||||
|
| 多代理 | 核心能力可扩展 | 展示工具调用 | 配置了自动判断复杂度的 `pi-subagents` 策略 |
|
||||||
|
| 本项目修复 | 不适用 | 上游行为 | 修复已结束会话状态 404、隐藏 Next.js 开发指示器、增加 Windows restart |
|
||||||
|
|
||||||
- Pi 基线:`0.83.0`,上游提交 `bb226f9`
|
本仓库不是桌面安装包,也不是公网多用户服务。当前发布目标是:技术用户在 Windows 上克隆仓库、补充自己的模型凭据后,通过命令行启动一个隔离、可扩展的本地 Web Agent。
|
||||||
- Pi Web 基线:`0.8.4`,上游提交 `c9b47e4`
|
|
||||||
- 已在 Windows 10、Node.js 22 环境验证
|
|
||||||
- 其他操作系统理论上可通过 Node.js 脚本运行,但尚未完成实机验证
|
|
||||||
- 当前为命令行启动的 Web 应用,不包含桌面客户端或安装器
|
|
||||||
|
|
||||||
### 项目结构
|
|
||||||
|
|
||||||
```text
|
|
||||||
pi/ Pi 后端、Agent 运行时及 CLI/TUI 源码
|
|
||||||
pi-web/ Web 前端与 HTTP/SSE 服务
|
|
||||||
config/ 可共享的默认配置
|
|
||||||
resources/skills/ 纳入版本控制的应用 Skill
|
|
||||||
resources/extensions/ 纳入版本控制的 Pi 扩展
|
|
||||||
resources/prompts/ 纳入版本控制的提示词模板
|
|
||||||
resources/themes/ 纳入版本控制的主题
|
|
||||||
data/agent/ 会话、认证、模型配置、运行时包与工具
|
|
||||||
data/skills-home/ Skill CLI 使用的隔离目录
|
|
||||||
data/state/ Skill 更新锁及其他可变状态
|
|
||||||
scripts/ 安装、启动、迁移与验收脚本
|
|
||||||
```
|
|
||||||
|
|
||||||
`data/` 包含设备和用户相关的运行数据,因此不会提交到 Git。需要分享和长期维护的自定义内容应放入 `config/` 或 `resources/`。
|
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
- Node.js 22.19.0 或更高版本
|
- Windows 10/11(当前唯一实机验证平台)
|
||||||
- npm
|
- Node.js `22.19.0` 或更高版本
|
||||||
|
- npm 与 Git
|
||||||
- 首次安装时可访问 npm registry
|
- 首次安装时可访问 npm registry
|
||||||
|
- Microsoft Edge(仅 Playwright 浏览器工具需要;不会额外下载 Chromium)
|
||||||
|
- 至少一个由用户自行配置的模型供应商、订阅登录或兼容 API
|
||||||
|
|
||||||
### 快速开始
|
### 快速开始
|
||||||
|
|
||||||
@@ -53,101 +45,167 @@ npm run setup
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
浏览器打开 <http://127.0.0.1:30141>。
|
打开 <http://127.0.0.1:30141>。
|
||||||
|
|
||||||
`npm run setup` 会安装两个子项目的依赖、构建本地 Pi 包,将 Pi Agent、AI、Coding Agent 和 TUI 包接入 Pi Web,并检查版本与构建产物。修改 Pi 源码或任一 lockfile 后应重新执行该命令。
|
`npm run setup` 会:
|
||||||
|
|
||||||
安装脚本默认忽略子进程中的 `HTTP_PROXY` 和 `HTTPS_PROXY`,避免失效的本地代理阻塞 npm。如果当前网络必须使用代理,请先设置 `PI_SETUP_USE_PROXY=1`。
|
1. 初始化项目内 `data/` Profile;
|
||||||
|
2. 安装并构建本地 Pi 源码;
|
||||||
|
3. 安装 Pi Web 并连接本地 Pi 包;
|
||||||
|
4. 安装、固定并加载检查默认插件;
|
||||||
|
5. 预缓存 Playwright MCP 包并确认系统 Edge 可用;
|
||||||
|
6. 检查前后端版本和构建产物。
|
||||||
|
|
||||||
### 项目内 Profile
|
安装脚本默认不继承 `HTTP_PROXY` / `HTTPS_PROXY`,避免失效代理阻塞 npm。如果安装必须使用代理,先设置 `$env:PI_SETUP_USE_PROXY = "1"`。
|
||||||
|
|
||||||
只初始化或修复项目内的空白 Profile,不安装依赖:
|
### 首次模型配置
|
||||||
|
|
||||||
|
仓库不会附带作者的模型地址、API Key、OAuth Token 或默认模型。应用可以在空模型配置下启动,但发送消息前必须完成以下任一方式:
|
||||||
|
|
||||||
|
- 在 Web UI 左下角进入“模型”,为内置供应商登录或填写 API Key;
|
||||||
|
- 在同一界面添加 Ollama、LM Studio、vLLM 或其他兼容 API;
|
||||||
|
- 编辑运行时文件 `data/agent/models.json`;
|
||||||
|
- 使用供应商支持的环境变量,例如 `ANTHROPIC_API_KEY`、`OPENAI_API_KEY` 或 `GEMINI_API_KEY`。
|
||||||
|
|
||||||
|
所有设备相关模型配置都位于被 Git 忽略的 `data/` 或 `.env` 中。
|
||||||
|
|
||||||
|
### 可选环境变量
|
||||||
|
|
||||||
|
需要可选服务时,复制模板:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
根启动器会自动读取 `.env`,已有系统环境变量优先。常用变量如下:
|
||||||
|
|
||||||
|
| 变量 | 是否必需 | 用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SEARXNG_URL` | 使用 SearXNG 时必需 | 用户自己的 JSON 搜索代理端点 |
|
||||||
|
| `SEARXNG_TOKEN` | 使用 SearXNG 时必需 | 作为 `X-Search-Token` 请求头发送 |
|
||||||
|
| `CONTEXT7_API_KEY` | 可选 | 提高 Context7 文档查询限额 |
|
||||||
|
| 模型供应商 API Key | 按供应商 | 模型认证;也可通过 Web UI 配置 |
|
||||||
|
| `PI_AGENT_DATA_DIR` | 可选 | 将可变数据移动到其他目录 |
|
||||||
|
|
||||||
|
不要提交 `.env`、`data/`、真实密钥或包含密钥的截图。
|
||||||
|
|
||||||
|
### 项目结构和闭环边界
|
||||||
|
|
||||||
|
```text
|
||||||
|
pi/ Pi 后端、Agent、CLI/TUI 源码
|
||||||
|
pi-web/ Next.js Web 前端与 HTTP/SSE 服务
|
||||||
|
config/ 可提交的缺省设置、MCP 和子代理策略
|
||||||
|
resources/skills/ 应用级 Skill
|
||||||
|
resources/extensions/ 应用级 Pi 扩展
|
||||||
|
resources/prompts/ 提示词模板
|
||||||
|
resources/themes/ 主题
|
||||||
|
scripts/ 安装、启动、迁移与验证脚本
|
||||||
|
data/agent/ 会话、认证、模型、插件、记忆与工具状态
|
||||||
|
data/home/ Pi 子进程主目录与 Rewind 检查点
|
||||||
|
data/cache/ 项目内 npm 缓存
|
||||||
|
data/workspaces/default/ 默认工作目录
|
||||||
|
```
|
||||||
|
|
||||||
|
`data/` 不进入 Git。应随仓库分发的内容放在 `config/` 或 `resources/`;个人会话、记忆、凭据、插件安装结果和模型配置留在 `data/`。打开其他代码目录时,受管运行时不会自动加载其中的 `.pi` / `.agents` 资源,从而避免和其他 Agent 项目耦合。
|
||||||
|
|
||||||
|
### 内置插件与工具
|
||||||
|
|
||||||
|
所有版本固定在 `config/settings.default.json` 和 `config/mcp.default.json`。
|
||||||
|
|
||||||
|
| 插件/工具 | 功能 | 自动行为与用法 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pi-mcp-adapter@2.15.0` | 用一个紧凑代理工具接入 MCP | 模型使用 `mcp` 搜索并调用 MCP;`/mcp` 查看状态。外部宿主配置发现默认关闭 |
|
||||||
|
| `@playwright/mcp@0.0.78` | 真实网页导航、点击、表单、快照和截图 | 通过 MCP 按需启动,空闲 5 分钟退出;使用无痕 Headless 系统 Edge |
|
||||||
|
| `pi-lens@3.8.73` | LSP、AST、符号检索和项目诊断 | 模型按需激活代码智能工具;可用 `/lens-health`、`/lens-tools`、`/lens-map` 检查 |
|
||||||
|
| `pi-memory@0.4.0` | Markdown 长期记忆、日志、临时工作区和恢复记录 | 模型按需使用 memory 工具;文件位于 `data/agent/memory/`,默认不启用向量索引 |
|
||||||
|
| `pi-subagents@0.37.2` | 创建研究、规划或执行子代理 | 简单任务不委派,跨模块或可并行复杂任务自动判断;也可使用 `/run`、`/parallel` |
|
||||||
|
| `pi-smart-fetch@0.3.17` | 抓取单个或批量 URL 内容 | 模型按需使用 `web_fetch` / `batch_web_fetch`,也提供给研究子代理 |
|
||||||
|
| `@ayulab/pi-rewind@0.4.6` | 每轮前后创建代码检查点 | `/rewind` 恢复代码、会话或两者;`/checkpoint` 管理存储。自动恢复文件默认关闭 |
|
||||||
|
| `@upstash/context7-pi@0.1.2` | 查询当前库、框架、SDK 和 API 文档 | 模型先解析库 ID,再按需查询文档;无 Key 可使用公共限额 |
|
||||||
|
| `@narumitw/pi-retry@0.31.0` | 识别瞬时供应商错误和卡住的流 | 复用 Pi 内置重试;默认 180 秒无事件视为停滞,不增加正常请求的模型调用 |
|
||||||
|
| `resources/extensions/searxng-search.ts` | 用户自有 SearXNG 的 `web_search` | 配置 `SEARXNG_URL` 与 `SEARXNG_TOKEN` 后,模型对时效性或明确搜索请求自动调用 |
|
||||||
|
|
||||||
|
Playwright 不下载独立 Chromium。首次 `setup` 只缓存 MCP 的 Node.js 包;浏览器执行使用系统 Edge。
|
||||||
|
|
||||||
|
### Profile、迁移和自定义扩展
|
||||||
|
|
||||||
|
只创建缺失的项目 Profile 文件:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm run profile:init
|
npm run profile:init
|
||||||
```
|
```
|
||||||
|
|
||||||
现有 Pi 数据不会自动导入。需要复制当前用户的 Pi Profile 和全局 Skill 时,可显式运行:
|
显式复制已有 Pi Profile 和全局 Skill:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm run profile:migrate
|
npm run profile:migrate
|
||||||
```
|
|
||||||
|
|
||||||
迁移只复制、不删除源数据,并默认保留目标目录中已存在的文件。其他用法:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm run profile:migrate -- --dry-run
|
npm run profile:migrate -- --dry-run
|
||||||
npm run profile:migrate -- --force
|
npm run profile:migrate -- --force
|
||||||
npm run profile:migrate -- --from D:\old-pi\agent --skills-from D:\old-skills
|
npm run profile:migrate -- --from D:\old-pi\agent --skills-from D:\old-skills
|
||||||
```
|
```
|
||||||
|
|
||||||
迁移程序会移除指向项目外部的绝对资源路径,维持运行环境闭环。
|
迁移只复制,不删除源数据,并清理指向仓库外部的绝对资源路径。后续共享扩展放入 `resources/extensions/` 并登记到其 `package.json`;共享 Skill、提示词和主题分别放入对应 `resources/` 目录。
|
||||||
|
|
||||||
### 常用命令
|
### 常用命令
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm run dev # 仅本机访问的开发服务
|
npm run setup # 完整安装、构建和 Profile 插件校验
|
||||||
npm run dev:lan # 局域网可访问的开发服务
|
npm run profile:packages # 安装缺失或版本不匹配的受管插件
|
||||||
npm run start # 启动已构建服务
|
npm run dev # 127.0.0.1:30141 开发服务
|
||||||
npm run start:lan # 在局域网启动已构建服务
|
npm run restart # Windows:停止本项目旧开发进程并重新启动
|
||||||
npm run check # 检查整合结构与构建产物
|
npm run dev:lan # 局域网监听;仅在可信网络使用
|
||||||
npm run typecheck # TypeScript 类型检查
|
npm run build # 构建 Pi Web 生产产物
|
||||||
npm run lint # 前端代码检查
|
npm run start # 启动已构建的本机生产服务
|
||||||
npm run test:managed # 项目内 Profile 与资源隔离测试
|
npm run check # 验证前后端整合结构
|
||||||
npm run smoke # 运行中的服务冒烟测试
|
npm run check:profile # 安装/加载并检查受管插件
|
||||||
npm run smoke:isolation # 外部 Skill/扩展隔离测试
|
npm run smoke:fresh-profile # 在临时目录验证全新 Profile 安装后自动清理
|
||||||
|
npm run typecheck # Pi Web TypeScript 检查
|
||||||
|
npm run test:managed # Profile、隔离、搜索契约等测试
|
||||||
|
npm run smoke # 对运行中的 Web 服务做冒烟测试
|
||||||
|
npm run smoke:isolation # 验证外部 Skill/扩展不会泄漏进来
|
||||||
|
npm run smoke:search -- "关键词" # 使用真实 SearXNG;需要配置
|
||||||
```
|
```
|
||||||
|
|
||||||
`smoke` 和 `smoke:isolation` 需要开发服务已启动。测试不会发起付费模型请求。
|
除 `smoke:search` 会访问用户配置的 SearXNG 外,以上验证不会发起付费模型请求。
|
||||||
|
|
||||||
### 上游与许可证
|
### 上游、更新和许可证
|
||||||
|
|
||||||
本项目基于以下项目维护整合版本:
|
- Pi 基线:`0.83.0`,提交 `bb226f9c1f38d3c029156a690e97bbfc602336b9`
|
||||||
|
- Pi Web 基线:`0.8.4`,提交 `c9b47e4543b11ce61e5c49c6bf02cea80aa975f6`
|
||||||
|
|
||||||
- [earendil-works/pi](https://github.com/earendil-works/pi),基线提交 `bb226f9c1f38d3c029156a690e97bbfc602336b9`
|
本整合版不自动跟随上游。更新时应分别评估两个源码树,重新执行 `npm run setup`、类型检查和受管测试,再更新这里的基线信息。根目录整合代码采用 MIT;两个源码树和运行时插件保留各自许可证,详见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。安全边界见 [SECURITY.md](SECURITY.md),贡献约定见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
||||||
- [agegr/pi-web](https://github.com/agegr/pi-web),基线提交 `c9b47e4543b11ce61e5c49c6bf02cea80aa975f6`
|
|
||||||
|
|
||||||
对应许可证保留在 `pi/LICENSE` 与 `pi-web/LICENSE`。后续更新由本仓库自行评估、合并和发布。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## English
|
## English
|
||||||
|
|
||||||
Pi Agent Integrated combines the Pi backend runtime with the Pi Web browser frontend in a single project that can be cloned, configured, and run independently. The backend and frontend remain separate source trees, while root-level scripts provide one installation, build, and launch workflow.
|
Pi Agent Integrated combines the Pi backend and Pi Web frontend into one independently cloneable and runnable repository. It preserves separate source boundaries while root commands handle dependency installation, Pi builds, managed-profile creation, plugin installation, and Web startup.
|
||||||
|
|
||||||
Runtime state, sessions, authentication, skills, extensions, prompts, and themes can all live inside the repository. The managed launcher does not depend on the user's global `~/.pi/agent` or `~/.agents` directories and does not automatically load `.pi` or `.agents` resources from an opened workspace.
|
### How it differs from the two upstream projects
|
||||||
|
|
||||||
### Status
|
| Capability | Pi | Pi Web | Pi Agent Integrated |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Primary role | CLI/TUI agent runtime | Browser UI for Pi | Complete local agent application with Web UI |
|
||||||
|
| Installation | Install CLI or build source | Connect Pi packages separately | One root `npm run setup` |
|
||||||
|
| Mutable state | User home by default | Follows the Pi profile | Sessions, auth, memory, plugins, and caches stay under repository `data/` |
|
||||||
|
| Default workspace | Current terminal directory | Creates a dated home directory | `data/workspaces/default/` |
|
||||||
|
| Extension ecosystem | Supports skills, extensions, packages | Management UI | Pinned plugins, MCP, skills, prompts, and themes form a managed ecosystem |
|
||||||
|
| Web search | No project-specific search | No project-specific search | Optional SearXNG `web_search` extension |
|
||||||
|
| Browser automation | User-integrated | No default browser MCP | Lazy Playwright MCP using system Edge |
|
||||||
|
| 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` |
|
||||||
|
| Integration fixes | Not applicable | Upstream behavior | Handles ended-session state 404s, hides Next dev indicators, adds Windows restart |
|
||||||
|
|
||||||
- Pi baseline: `0.83.0`, upstream commit `bb226f9`
|
This repository is not a desktop installer or a public multi-user service. Its current release target is a technical Windows user who clones the project, supplies personal model credentials, and starts an isolated, extensible local Web agent from the command line.
|
||||||
- Pi Web baseline: `0.8.4`, upstream commit `c9b47e4`
|
|
||||||
- Validated on Windows 10 with Node.js 22
|
|
||||||
- The Node.js scripts are intended to be portable, but other operating systems have not been tested yet
|
|
||||||
- This is currently a command-line-launched web application, without a desktop client or installer
|
|
||||||
|
|
||||||
### Repository layout
|
|
||||||
|
|
||||||
```text
|
|
||||||
pi/ Pi backend, agent runtime, and CLI/TUI source
|
|
||||||
pi-web/ Web frontend and HTTP/SSE service
|
|
||||||
config/ Shareable default configuration
|
|
||||||
resources/skills/ Version-controlled application skills
|
|
||||||
resources/extensions/ Version-controlled Pi extensions
|
|
||||||
resources/prompts/ Version-controlled prompt templates
|
|
||||||
resources/themes/ Version-controlled themes
|
|
||||||
data/agent/ Sessions, auth, model config, packages, and tools
|
|
||||||
data/skills-home/ Isolated home used by the skills CLI
|
|
||||||
data/state/ Skill update locks and other mutable state
|
|
||||||
scripts/ Setup, launch, migration, and verification scripts
|
|
||||||
```
|
|
||||||
|
|
||||||
`data/` contains machine- and user-specific runtime data and is excluded from Git. Put custom resources that should be shared and maintained under `config/` or `resources/`.
|
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- Node.js 22.19.0 or newer
|
- Windows 10/11, the only currently validated platform
|
||||||
- npm
|
- Node.js `22.19.0` or newer
|
||||||
- npm registry access during the first setup
|
- npm and Git
|
||||||
|
- npm registry access during initial setup
|
||||||
|
- Microsoft Edge for Playwright browser tools; no separate Chromium is downloaded
|
||||||
|
- At least one user-configured model provider, subscription login, or compatible API
|
||||||
|
|
||||||
### Quick start
|
### Quick start
|
||||||
|
|
||||||
@@ -160,56 +218,114 @@ npm run dev
|
|||||||
|
|
||||||
Open <http://127.0.0.1:30141>.
|
Open <http://127.0.0.1:30141>.
|
||||||
|
|
||||||
`npm run setup` installs dependencies for both source trees, builds the local Pi packages, connects the Pi Agent, AI, Coding Agent, and TUI packages to Pi Web, and verifies their versions and build output. Run it again after changing Pi source or either lockfile.
|
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 without downloading a browser, verifies system Edge, and checks integration artifacts.
|
||||||
|
|
||||||
The setup script ignores `HTTP_PROXY` and `HTTPS_PROXY` for child processes by default so that a stale local proxy cannot stall npm. Set `PI_SETUP_USE_PROXY=1` first when npm registry access requires your proxy.
|
Child npm operations ignore `HTTP_PROXY` and `HTTPS_PROXY` by default to avoid stale proxy configuration. Set `$env:PI_SETUP_USE_PROXY = "1"` first when registry access requires the proxy.
|
||||||
|
|
||||||
### Repository-local profile
|
### First model configuration
|
||||||
|
|
||||||
Initialize or repair an empty managed profile without installing dependencies:
|
The repository contains no author model endpoint, API key, OAuth token, or default model. The UI starts with an empty model configuration, but one of the following is required before sending a message:
|
||||||
|
|
||||||
|
- open Models in the lower-left Web UI and authenticate or enter an API key for a built-in provider;
|
||||||
|
- add Ollama, LM Studio, vLLM, or another compatible API in the same UI;
|
||||||
|
- edit the runtime file `data/agent/models.json`;
|
||||||
|
- set a supported variable such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY`.
|
||||||
|
|
||||||
|
Device-specific model configuration remains in ignored `data/` or `.env` files.
|
||||||
|
|
||||||
|
### Optional environment configuration
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
The managed launcher automatically reads `.env`, while existing process variables take precedence.
|
||||||
|
|
||||||
|
| Variable | Required | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SEARXNG_URL` | When using SearXNG | The user's JSON search proxy endpoint |
|
||||||
|
| `SEARXNG_TOKEN` | When using SearXNG | Sent as the `X-Search-Token` request header |
|
||||||
|
| `CONTEXT7_API_KEY` | Optional | Higher Context7 documentation quota |
|
||||||
|
| Provider API keys | Provider-specific | Model authentication; the Web UI is another option |
|
||||||
|
| `PI_AGENT_DATA_DIR` | Optional | Relocate mutable runtime data |
|
||||||
|
|
||||||
|
Never commit `.env`, `data/`, real credentials, or screenshots containing credentials.
|
||||||
|
|
||||||
|
### Repository-local ecosystem
|
||||||
|
|
||||||
|
```text
|
||||||
|
pi/ Pi backend, agent, and CLI/TUI source
|
||||||
|
pi-web/ Next.js frontend and HTTP/SSE service
|
||||||
|
config/ Versioned settings, MCP, and subagent policy
|
||||||
|
resources/skills/ Application skills
|
||||||
|
resources/extensions/ Application Pi extensions
|
||||||
|
resources/prompts/ Prompt templates
|
||||||
|
resources/themes/ Themes
|
||||||
|
scripts/ Setup, launch, migration, and verification
|
||||||
|
data/agent/ Sessions, auth, models, packages, memory, tools
|
||||||
|
data/home/ Pi subprocess home and Rewind checkpoints
|
||||||
|
data/cache/ Repository-local npm cache
|
||||||
|
data/workspaces/default/ Default workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
`data/` is excluded from Git. Shareable defaults belong in `config/` or `resources/`; personal sessions, memory, credentials, installed packages, and model settings remain in `data/`. The managed runtime does not automatically load `.pi` or `.agents` resources from another opened workspace.
|
||||||
|
|
||||||
|
### Included plugins and tools
|
||||||
|
|
||||||
|
Versions are pinned in `config/settings.default.json` and `config/mcp.default.json`.
|
||||||
|
|
||||||
|
| Plugin/tool | Function | Automatic behavior and usage |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pi-mcp-adapter@2.15.0` | Compact MCP proxy | The model searches and invokes MCP through `mcp`; `/mcp` shows status. Host-config discovery is off |
|
||||||
|
| `@playwright/mcp@0.0.78` | Real navigation, clicks, forms, snapshots, screenshots | Starts on demand, exits after five idle minutes, uses isolated headless system Edge |
|
||||||
|
| `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 | The model uses memory tools on demand; files live under `data/agent/memory/`; vector indexing is off by default |
|
||||||
|
| `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 |
|
||||||
|
| `@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 |
|
||||||
|
| `@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 |
|
||||||
|
| `resources/extensions/searxng-search.ts` | `web_search` against a user-owned SearXNG proxy | After `SEARXNG_URL` and `SEARXNG_TOKEN` are set, the model calls it for current or explicit search requests |
|
||||||
|
|
||||||
|
Playwright never downloads a standalone Chromium in this project. Setup caches only its Node package; browser execution uses system Edge.
|
||||||
|
|
||||||
|
### Profile, migration, and extension
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm run profile:init
|
npm run profile:init
|
||||||
```
|
|
||||||
|
|
||||||
Existing Pi data is never imported automatically. To copy the current user's Pi profile and global skills explicitly, run:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm run profile:migrate
|
npm run profile:migrate
|
||||||
```
|
|
||||||
|
|
||||||
Migration copies data without deleting its source and preserves existing destination files by default. Other options are:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm run profile:migrate -- --dry-run
|
npm run profile:migrate -- --dry-run
|
||||||
npm run profile:migrate -- --force
|
npm run profile:migrate -- --force
|
||||||
npm run profile:migrate -- --from D:\old-pi\agent --skills-from D:\old-skills
|
npm run profile:migrate -- --from D:\old-pi\agent --skills-from D:\old-skills
|
||||||
```
|
```
|
||||||
|
|
||||||
The migrator removes absolute resource paths that point outside the repository, keeping the managed environment self-contained.
|
Migration copies without deleting source data and removes absolute resource paths pointing outside the repository. Put future shared extensions in `resources/extensions/` and register them in its `package.json`; use the corresponding `resources/` directories for shared skills, prompts, and themes.
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm run dev # Local-only development server
|
npm run setup # Full install, build, and managed-profile validation
|
||||||
npm run dev:lan # LAN-accessible development server
|
npm run profile:packages # Install missing or version-mismatched managed plugins
|
||||||
npm run start # Start a production build
|
npm run dev # Development server on 127.0.0.1:30141
|
||||||
npm run start:lan # Start a production build on the LAN
|
npm run restart # Windows: stop this project's old dev process and restart
|
||||||
npm run check # Verify integration layout and build artifacts
|
npm run dev:lan # Listen on the LAN; trusted networks only
|
||||||
npm run typecheck # TypeScript type checking
|
npm run build # Build the Pi Web production output
|
||||||
npm run lint # Frontend linting
|
npm run start # Start an existing local production build
|
||||||
npm run test:managed # Managed-profile and resource-isolation tests
|
npm run check # Verify backend/frontend integration
|
||||||
npm run smoke # Smoke-test a running server
|
npm run check:profile # Install/load and verify managed plugins
|
||||||
npm run smoke:isolation # Verify external skills/extensions stay isolated
|
npm run smoke:fresh-profile # Verify a clean temporary profile, then remove it
|
||||||
|
npm run typecheck # Pi Web TypeScript check
|
||||||
|
npm run test:managed # Profile, isolation, and search-contract tests
|
||||||
|
npm run smoke # Smoke-test a running Web service
|
||||||
|
npm run smoke:isolation # Verify external skills/extensions remain isolated
|
||||||
|
npm run smoke:search -- "query" # Real configured SearXNG request
|
||||||
```
|
```
|
||||||
|
|
||||||
`smoke` and `smoke:isolation` require a running development server. They do not make a paid model request.
|
Except for `smoke:search`, validation does not make paid model requests.
|
||||||
|
|
||||||
### Upstream projects and licenses
|
### Upstream, updates, and licensing
|
||||||
|
|
||||||
This repository maintains an integrated distribution based on:
|
- Pi baseline: `0.83.0`, commit `bb226f9c1f38d3c029156a690e97bbfc602336b9`
|
||||||
|
- Pi Web baseline: `0.8.4`, commit `c9b47e4543b11ce61e5c49c6bf02cea80aa975f6`
|
||||||
|
|
||||||
- [earendil-works/pi](https://github.com/earendil-works/pi), baseline commit `bb226f9c1f38d3c029156a690e97bbfc602336b9`
|
This integrated distribution does not auto-follow upstream. Update each vendored source tree deliberately, rerun setup, type checking, and managed tests, then update the baseline records above. Root integration code is MIT-licensed; vendored source and runtime plugins retain their own licenses. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), [SECURITY.md](SECURITY.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
- [agegr/pi-web](https://github.com/agegr/pi-web), baseline commit `c9b47e4543b11ce61e5c49c6bf02cea80aa975f6`
|
|
||||||
|
|
||||||
The corresponding license files remain at `pi/LICENSE` and `pi-web/LICENSE`. Future upstream updates are evaluated, merged, and released from this repository.
|
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Security policy / 安全说明
|
||||||
|
|
||||||
|
Pi Agent Integrated is currently a local, single-user application. The default `npm run dev` and `npm run start` commands bind to `127.0.0.1`. LAN commands intentionally bind to all interfaces and should only be used on trusted networks.
|
||||||
|
|
||||||
|
Pi Agent Integrated 当前定位为本地单用户应用。默认启动命令仅监听 `127.0.0.1`;带 `:lan` 的命令会监听所有网卡,只应在可信局域网使用。
|
||||||
|
|
||||||
|
Do not expose this application directly to the public internet. Agent tools can read and modify files in their selected workspace, run commands, access configured services, and invoke MCP servers. Review provider, extension, and MCP configuration before sharing an instance.
|
||||||
|
|
||||||
|
不要将应用直接暴露到公网。Agent 工具能够在所选工作目录内读写文件、执行命令、访问已配置服务并调用 MCP;共享实例前应审核模型、扩展和 MCP 配置。
|
||||||
|
|
||||||
|
Secrets belong in `.env`, environment variables, or the ignored `data/` profile. Never place real credentials in `config/`, `resources/`, screenshots, issues, or logs intended for publication.
|
||||||
|
|
||||||
|
密钥应放在 `.env`、系统环境变量或被忽略的 `data/` Profile 中,不要写入公开配置、资源、截图、Issue 或日志。
|
||||||
|
|
||||||
|
Report vulnerabilities through GitHub private vulnerability reporting when available: <https://github.com/luckyyzh/pi-agent-new/security/advisories/new>. Do not include active secrets in a public issue.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Third-party notices
|
||||||
|
|
||||||
|
Pi Agent Integrated contains vendored upstream source and installs optional runtime packages. The root [MIT license](LICENSE) applies to the integration-specific code and documentation; it does not replace the licenses of the components below.
|
||||||
|
|
||||||
|
## Vendored source
|
||||||
|
|
||||||
|
| Component | Version or baseline | License | Source |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Pi | `0.83.0`, commit `bb226f9c1f38d3c029156a690e97bbfc602336b9` | MIT | <https://github.com/earendil-works/pi> |
|
||||||
|
| Pi Web | `0.8.4`, commit `c9b47e4543b11ce61e5c49c6bf02cea80aa975f6` | MIT | <https://github.com/agegr/pi-web> |
|
||||||
|
|
||||||
|
Their complete license texts remain in `pi/LICENSE` and `pi-web/LICENSE`.
|
||||||
|
|
||||||
|
## Runtime packages enabled by the managed profile
|
||||||
|
|
||||||
|
These packages are fetched from npm during setup or first use and remain under the ignored `data/` runtime directory.
|
||||||
|
|
||||||
|
| Package | Pinned version | License | Source |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `pi-mcp-adapter` | `2.15.0` | MIT | <https://github.com/nicobailon/pi-mcp-adapter> |
|
||||||
|
| `pi-lens` | `3.8.73` | MIT | <https://github.com/apmantza/pi-lens> |
|
||||||
|
| `pi-memory` | `0.4.0` | MIT | <https://github.com/jayzeng/pi-memory> |
|
||||||
|
| `pi-subagents` | `0.37.2` | MIT | <https://github.com/nicobailon/pi-subagents> |
|
||||||
|
| `pi-smart-fetch` | `0.3.17` | MIT | <https://github.com/Thinkscape/agent-smart-fetch> |
|
||||||
|
| `@ayulab/pi-rewind` | `0.4.6` | GPL-3.0 | <https://github.com/ayu-exorcist/oh-my-pi> |
|
||||||
|
| `@upstash/context7-pi` | `0.1.2` | MIT | <https://github.com/upstash/context7> |
|
||||||
|
| `@narumitw/pi-retry` | `0.31.0` | MIT | <https://github.com/narumiruna/pi-extensions> |
|
||||||
|
| `@playwright/mcp` | `0.0.78` | Apache-2.0 | <https://github.com/microsoft/playwright-mcp> |
|
||||||
|
|
||||||
|
Each package may include transitive dependencies under additional licenses. npm package metadata and installed package license files are authoritative for a particular installation.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"hostConfigDiscovery": "off",
|
||||||
|
"idleTimeout": 5
|
||||||
|
},
|
||||||
|
"mcpServers": {
|
||||||
|
"playwright": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@playwright/mcp@0.0.78",
|
||||||
|
"--headless",
|
||||||
|
"--isolated",
|
||||||
|
"--browser",
|
||||||
|
"msedge",
|
||||||
|
"--output-dir",
|
||||||
|
"data/mcp/playwright/output"
|
||||||
|
],
|
||||||
|
"cwd": "${PI_AGENT_APP_ROOT}",
|
||||||
|
"env": {
|
||||||
|
"npm_config_cache": "${npm_config_cache}"
|
||||||
|
},
|
||||||
|
"lifecycle": "lazy",
|
||||||
|
"idleTimeout": 5,
|
||||||
|
"requestTimeoutMs": 120000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,46 @@
|
|||||||
{}
|
{
|
||||||
|
"packages": [
|
||||||
|
"npm:pi-mcp-adapter@2.15.0",
|
||||||
|
"npm:pi-lens@3.8.73",
|
||||||
|
"npm:pi-memory@0.4.0",
|
||||||
|
"npm:pi-subagents@0.37.2",
|
||||||
|
"npm:pi-smart-fetch@0.3.17",
|
||||||
|
"npm:@ayulab/pi-rewind@0.4.6",
|
||||||
|
"npm:@upstash/context7-pi@0.1.2",
|
||||||
|
"npm:@narumitw/pi-retry@0.31.0"
|
||||||
|
],
|
||||||
|
"retry": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"ayu": {
|
||||||
|
"rewind": {
|
||||||
|
"restoreOnTree": "ask"
|
||||||
|
},
|
||||||
|
"checkpoint": {
|
||||||
|
"restoreOnResume": false,
|
||||||
|
"restoreOnFork": false,
|
||||||
|
"restoreOnClone": false,
|
||||||
|
"exclude": [
|
||||||
|
"data/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/.next/**",
|
||||||
|
"**/coverage/**"
|
||||||
|
],
|
||||||
|
"maxFileMB": 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subagents": {
|
||||||
|
"disableThinking": true,
|
||||||
|
"agentOverrides": {
|
||||||
|
"researcher": {
|
||||||
|
"tools": [
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"web_search",
|
||||||
|
"web_fetch",
|
||||||
|
"batch_web_fetch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
AUTOMATIC DELEGATION POLICY:
|
||||||
|
|
||||||
|
- Decide automatically whether delegation materially improves the result. The user should not need to request a subagent or learn subagent commands.
|
||||||
|
- Do not delegate simple factual questions, translations, short explanations, status checks, command lookups, obvious one-file edits, or tasks the parent can complete confidently with a few tool calls.
|
||||||
|
- Delegate only when at least one condition applies: broad investigation across multiple modules; independent external research; substantial uncertainty that benefits from a planner or oracle; a significant implementation deserves fresh-context review; or two or more independent investigations can run concurrently.
|
||||||
|
- Prefer one focused read-only subagent. Use at most two parallel children by default. Use more only when the user explicitly requests broad parallelism and the tasks are genuinely independent.
|
||||||
|
- Keep the parent agent as the sole writer in a shared working directory. Use worker only for a clearly separable implementation with a concrete handoff, or use isolated worktrees when multiple writers are explicitly justified.
|
||||||
|
- Do not run a ceremonial scout-planner-worker-reviewer pipeline for every task. Do not delegate the same question to multiple similar roles without a concrete reason.
|
||||||
|
- Prefer foreground execution so the current request finishes coherently. Use background execution only for long, independent work or when the user explicitly asks for it.
|
||||||
|
- Account for latency, token use, provider quotas, and context value before delegating. If the expected benefit is marginal, continue in the parent.
|
||||||
|
- Keep orchestration unobtrusive. Return a consolidated answer and mention subagents only when their findings, disagreement, or status is useful to the user.
|
||||||
|
|
||||||
|
{{compactDescription}}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"toolDescriptionMode": "custom",
|
||||||
|
"artifactDir": "session",
|
||||||
|
"fleetView": false,
|
||||||
|
"asyncWidget": false,
|
||||||
|
"asyncByDefault": false,
|
||||||
|
"waitTool": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"parallel": {
|
||||||
|
"maxTasks": 4,
|
||||||
|
"concurrency": 2
|
||||||
|
},
|
||||||
|
"globalConcurrencyLimit": 2,
|
||||||
|
"maxSubagentDepth": 1,
|
||||||
|
"maxSubagentSpawnsPerSession": 12,
|
||||||
|
"turnBudget": {
|
||||||
|
"maxTurns": 16,
|
||||||
|
"graceTurns": 2
|
||||||
|
},
|
||||||
|
"toolBudget": {
|
||||||
|
"soft": 30,
|
||||||
|
"hard": 60
|
||||||
|
},
|
||||||
|
"proactiveSkillSubagents": false
|
||||||
|
}
|
||||||
+12
-1
@@ -3,21 +3,32 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Integrated local development launcher for Pi and Pi Web",
|
"description": "Integrated local development launcher for Pi and Pi Web",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/luckyyzh/pi-agent-new.git"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "node scripts/setup.mjs",
|
"setup": "node scripts/setup.mjs",
|
||||||
"profile:init": "node scripts/init-profile.mjs",
|
"profile:init": "node scripts/init-profile.mjs",
|
||||||
|
"profile:packages": "node scripts/install-managed-packages.mjs",
|
||||||
"profile:migrate": "node scripts/migrate-profile.mjs",
|
"profile:migrate": "node scripts/migrate-profile.mjs",
|
||||||
"check": "node scripts/check-integration.mjs",
|
"check": "node scripts/check-integration.mjs",
|
||||||
|
"check:profile": "node scripts/check-managed-profile.mjs",
|
||||||
"smoke": "node scripts/smoke.mjs",
|
"smoke": "node scripts/smoke.mjs",
|
||||||
|
"smoke:fresh-profile": "node scripts/smoke-fresh-profile.mjs",
|
||||||
"smoke:isolation": "node scripts/smoke-isolation.mjs",
|
"smoke:isolation": "node scripts/smoke-isolation.mjs",
|
||||||
|
"smoke:search": "node pi-web/scripts/searxng-smoke.mjs",
|
||||||
"predev": "npm run check",
|
"predev": "npm run check",
|
||||||
"dev": "node scripts/run.mjs dev",
|
"dev": "node scripts/run.mjs dev",
|
||||||
|
"restart": "node scripts/restart.mjs",
|
||||||
"dev:lan": "node scripts/run.mjs dev:lan",
|
"dev:lan": "node scripts/run.mjs dev:lan",
|
||||||
|
"build": "node scripts/run.mjs build",
|
||||||
"start": "node scripts/run.mjs start",
|
"start": "node scripts/run.mjs start",
|
||||||
"start:lan": "node scripts/run.mjs start:lan",
|
"start:lan": "node scripts/run.mjs start:lan",
|
||||||
"typecheck": "node pi-web/node_modules/typescript/bin/tsc --noEmit -p pi-web/tsconfig.json",
|
"typecheck": "node pi-web/node_modules/typescript/bin/tsc --noEmit -p pi-web/tsconfig.json",
|
||||||
"lint": "npm --prefix pi-web run lint",
|
"lint": "npm --prefix pi-web run lint",
|
||||||
"test:managed": "node --test scripts/migrate-profile.test.mjs pi-web/lib/app-runtime.test.mjs pi-web/lib/project-trust.test.mjs pi-web/lib/skill-lock.test.mjs pi-web/lib/skill-updates.test.mjs"
|
"test:managed": "node --test scripts/migrate-profile.test.mjs pi-web/lib/app-runtime.test.mjs pi-web/lib/project-trust.test.mjs pi-web/lib/searxng-extension.test.mjs pi-web/lib/skill-lock.test.mjs pi-web/lib/skill-updates.test.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.19.0"
|
"node": ">=22.19.0"
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ app/api/
|
|||||||
auth/logout/[provider]/route.ts POST OAuth logout
|
auth/logout/[provider]/route.ts POST OAuth logout
|
||||||
auth/providers/route.ts GET OAuth provider list
|
auth/providers/route.ts GET OAuth provider list
|
||||||
cwd/validate/route.ts POST validate/select a cwd
|
cwd/validate/route.ts POST validate/select a cwd
|
||||||
default-cwd/route.ts POST create ~/pi-cwd-YYYYMMDD
|
default-cwd/route.ts POST create managed data/workspaces/default (standalone: ~/pi-cwd-YYYYMMDD)
|
||||||
files/[...path]/route.ts GET file contents for viewer
|
files/[...path]/route.ts GET file contents for viewer
|
||||||
home/route.ts GET user home directory
|
home/route.ts GET user home directory
|
||||||
models/route.ts GET { models, modelList, defaultModel }
|
models/route.ts GET { models, modelList, defaultModel }
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ import { NextResponse } from "next/server";
|
|||||||
import { mkdirSync } from "fs";
|
import { mkdirSync } from "fs";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { getManagedRuntimePaths, isManagedRuntime } from "@/lib/app-runtime";
|
||||||
import { allowFileRoot } from "@/lib/file-access";
|
import { allowFileRoot } from "@/lib/file-access";
|
||||||
|
|
||||||
// POST /api/default-cwd
|
// POST /api/default-cwd
|
||||||
// Creates ~/pi-cwd-<YYYYMMDD> if it doesn't exist and returns the path.
|
// Managed app: creates <dataDir>/workspaces/default.
|
||||||
|
// Standalone pi-web: preserves the upstream ~/pi-cwd-<YYYYMMDD> behavior.
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
const dir = isManagedRuntime()
|
||||||
const dir = join(homedir(), `pi-cwd-${date}`);
|
? join(getManagedRuntimePaths().dataDir, "workspaces", "default")
|
||||||
|
: join(
|
||||||
|
homedir(),
|
||||||
|
`pi-cwd-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}`,
|
||||||
|
);
|
||||||
mkdirSync(dir, { recursive: true });
|
mkdirSync(dir, { recursive: true });
|
||||||
allowFileRoot(dir);
|
allowFileRoot(dir);
|
||||||
return NextResponse.json({ cwd: dir });
|
return NextResponse.json({ cwd: dir });
|
||||||
|
|||||||
@@ -1849,7 +1849,7 @@ export function ModelsConfig({ onClose }: { onClose: () => void }) {
|
|||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 18px", borderBottom: "1px solid var(--border)", flexShrink: 0 }}>
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 18px", borderBottom: "1px solid var(--border)", flexShrink: 0 }}>
|
||||||
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
|
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
|
||||||
<span style={{ fontSize: 15, fontWeight: 700, color: "var(--text)" }}>{t("common.models")}</span>
|
<span style={{ fontSize: 15, fontWeight: 700, color: "var(--text)" }}>{t("common.models")}</span>
|
||||||
<code style={{ fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>~/.pi/agent/models.json</code>
|
<code style={{ fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>data/agent/models.json</code>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", fontSize: 20, lineHeight: 1, padding: "2px 6px" }}>×</button>
|
<button onClick={onClose} style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", fontSize: 20, lineHeight: 1, padding: "2px 6px" }}>×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -466,6 +466,12 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const stateRes = await fetch(`/api/sessions/${encodeURIComponent(sid)}/state`);
|
const stateRes = await fetch(`/api/sessions/${encodeURIComponent(sid)}/state`);
|
||||||
|
if (stateRes.status === 404) {
|
||||||
|
if (sessionIdRef.current === sid) {
|
||||||
|
setQueuedMessages({ steering: [], followUp: [] });
|
||||||
|
}
|
||||||
|
return { running: false };
|
||||||
|
}
|
||||||
if (!stateRes.ok) throw new Error(`HTTP ${stateRes.status}`);
|
if (!stateRes.ok) throw new Error(`HTTP ${stateRes.status}`);
|
||||||
const agentState = await stateRes.json() as { running: boolean; state?: AgentStateResponse };
|
const agentState = await stateRes.json() as { running: boolean; state?: AgentStateResponse };
|
||||||
if (sessionIdRef.current !== sid) return null;
|
if (sessionIdRef.current !== sid) return null;
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import test from "node:test";
|
||||||
|
import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
|
||||||
|
|
||||||
|
const extensionPath = resolve(
|
||||||
|
dirname(fileURLToPath(import.meta.url)),
|
||||||
|
"../../resources/extensions/searxng-search.ts",
|
||||||
|
);
|
||||||
|
const extensionDir = dirname(extensionPath);
|
||||||
|
|
||||||
|
test("managed SearXNG extension loads and maps the complete search contract", async () => {
|
||||||
|
const fixtureRoot = mkdtempSync(join(tmpdir(), "pi-searxng-extension-"));
|
||||||
|
const agentDir = resolve(fixtureRoot, "agent");
|
||||||
|
mkdirSync(agentDir, { recursive: true });
|
||||||
|
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const originalToken = process.env.SEARXNG_TOKEN;
|
||||||
|
const originalUrl = process.env.SEARXNG_URL;
|
||||||
|
let capturedRequest;
|
||||||
|
|
||||||
|
process.env.SEARXNG_TOKEN = "test-token";
|
||||||
|
process.env.SEARXNG_URL = "https://search.example.test/search";
|
||||||
|
globalThis.fetch = async (input, init) => {
|
||||||
|
capturedRequest = { input: String(input), init };
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
answers: ["A current answer"],
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
title: "Example result",
|
||||||
|
url: "https://example.test/result",
|
||||||
|
content: "Example snippet",
|
||||||
|
engines: ["google", "github"],
|
||||||
|
publishedDate: "2026-07-30",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
suggestions: ["refined query"],
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loader = new DefaultResourceLoader({
|
||||||
|
cwd: fixtureRoot,
|
||||||
|
agentDir,
|
||||||
|
additionalExtensionPaths: [extensionDir],
|
||||||
|
});
|
||||||
|
await loader.reload();
|
||||||
|
|
||||||
|
const loaded = loader.getExtensions();
|
||||||
|
assert.deepEqual(loaded.errors, []);
|
||||||
|
const extension = loaded.extensions.find((candidate) => resolve(candidate.path) === extensionPath);
|
||||||
|
assert.ok(extension, "SearXNG extension should load through the managed extension directory manifest");
|
||||||
|
|
||||||
|
const registeredTool = extension.tools.get("web_search");
|
||||||
|
assert.ok(registeredTool, "web_search should be registered");
|
||||||
|
const tool = registeredTool.definition;
|
||||||
|
|
||||||
|
const result = await tool.execute(
|
||||||
|
"test-call",
|
||||||
|
{
|
||||||
|
query: "pi coding agent",
|
||||||
|
num_results: 5,
|
||||||
|
engines: ["google", "github"],
|
||||||
|
language: "en",
|
||||||
|
page: 2,
|
||||||
|
time_range: "week",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(capturedRequest, "web_search should make an HTTP request");
|
||||||
|
const requestUrl = new URL(capturedRequest.input);
|
||||||
|
assert.equal(requestUrl.searchParams.get("q"), "pi coding agent");
|
||||||
|
assert.equal(requestUrl.searchParams.get("format"), "json");
|
||||||
|
assert.equal(requestUrl.searchParams.get("engines"), "google,github");
|
||||||
|
assert.equal(requestUrl.searchParams.get("language"), "en");
|
||||||
|
assert.equal(requestUrl.searchParams.get("pageno"), "2");
|
||||||
|
assert.equal(requestUrl.searchParams.get("time_range"), "week");
|
||||||
|
assert.equal(new Headers(capturedRequest.init.headers).get("X-Search-Token"), "test-token");
|
||||||
|
assert.match(result.content[0].text, /Example result/);
|
||||||
|
assert.match(result.content[0].text, /https:\/\/example\.test\/result/);
|
||||||
|
assert.equal(result.details.resultCount, 1);
|
||||||
|
assert.equal(JSON.stringify(result).includes("test-token"), false);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
if (originalToken === undefined) delete process.env.SEARXNG_TOKEN;
|
||||||
|
else process.env.SEARXNG_TOKEN = originalToken;
|
||||||
|
if (originalUrl === undefined) delete process.env.SEARXNG_URL;
|
||||||
|
else process.env.SEARXNG_URL = originalUrl;
|
||||||
|
rmSync(fixtureRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@ try {
|
|||||||
} catch { /* package not found, use default */ }
|
} catch { /* package not found, use default */ }
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
devIndicators: false,
|
||||||
serverExternalPackages: [
|
serverExternalPackages: [
|
||||||
"undici",
|
"undici",
|
||||||
"@earendil-works/pi-coding-agent",
|
"@earendil-works/pi-coding-agent",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -H 127.0.0.1 -p 30141",
|
"dev": "next dev -H 127.0.0.1 -p 30141",
|
||||||
"dev:lan": "next dev -H 0.0.0.0 -p 30141",
|
"dev:lan": "next dev -H 0.0.0.0 -p 30141",
|
||||||
|
"restart": "node ../scripts/restart.mjs",
|
||||||
"build": "next build --webpack",
|
"build": "next build --webpack",
|
||||||
"start": "next start -H 127.0.0.1 -p 30141",
|
"start": "next start -H 127.0.0.1 -p 30141",
|
||||||
"start:lan": "next start -H 0.0.0.0 -p 30141",
|
"start:lan": "next start -H 0.0.0.0 -p 30141",
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
|
||||||
|
|
||||||
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const appRoot = resolve(scriptDir, "../..");
|
||||||
|
const extensionDir = resolve(appRoot, "resources/extensions");
|
||||||
|
const extensionPath = resolve(appRoot, "resources/extensions/searxng-search.ts");
|
||||||
|
const fixtureRoot = mkdtempSync(join(tmpdir(), "pi-searxng-smoke-"));
|
||||||
|
const agentDir = resolve(fixtureRoot, "agent");
|
||||||
|
const query = process.argv.slice(2).join(" ").trim() || "Pi coding agent";
|
||||||
|
|
||||||
|
if (!process.env.SEARXNG_TOKEN?.trim()) {
|
||||||
|
console.error("SEARXNG_TOKEN is not available to this process.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(agentDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loader = new DefaultResourceLoader({
|
||||||
|
cwd: appRoot,
|
||||||
|
agentDir,
|
||||||
|
additionalExtensionPaths: [extensionDir],
|
||||||
|
});
|
||||||
|
await loader.reload();
|
||||||
|
|
||||||
|
const loaded = loader.getExtensions();
|
||||||
|
if (loaded.errors.length > 0) {
|
||||||
|
throw new Error(`Extension load failed: ${loaded.errors.map((error) => error.error).join("; ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = loaded.extensions.find((candidate) => resolve(candidate.path) === extensionPath);
|
||||||
|
const tool = extension?.tools.get("web_search")?.definition;
|
||||||
|
if (!tool) throw new Error("web_search was not registered");
|
||||||
|
|
||||||
|
const result = await tool.execute(
|
||||||
|
"searxng-smoke",
|
||||||
|
{ query, num_results: 3 },
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
extensionLoaded: true,
|
||||||
|
tool: tool.name,
|
||||||
|
query,
|
||||||
|
resultCount: result.details?.resultCount ?? null,
|
||||||
|
responseHasText: Boolean(result.content[0]?.text),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rmSync(fixtureRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
@@ -9,3 +9,12 @@ This directory is the version-controlled resource layer for the integrated app.
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
## Included extensions
|
||||||
|
|
||||||
|
- `extensions/package.json` is the explicit Pi extension manifest. Add future
|
||||||
|
extension entry points to its `pi.extensions` array so the managed loader can
|
||||||
|
discover them.
|
||||||
|
- `extensions/searxng-search.ts` registers the `web_search` tool. It requires
|
||||||
|
`SEARXNG_TOKEN` and optionally accepts `SEARXNG_URL` in the Pi process
|
||||||
|
environment.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "@pi-agent-integrated/extensions",
|
||||||
|
"private": true,
|
||||||
|
"pi": {
|
||||||
|
"extensions": [
|
||||||
|
"./searxng-search.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* Repository-local SearXNG web search extension.
|
||||||
|
*
|
||||||
|
* Required environment:
|
||||||
|
* SEARXNG_URL=https://search.example.com/search
|
||||||
|
* SEARXNG_TOKEN=<X-Search-Token value>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { Type } from "typebox";
|
||||||
|
|
||||||
|
const DEFAULT_NUM_RESULTS = 10;
|
||||||
|
const MAX_NUM_RESULTS = 20;
|
||||||
|
const MAX_SNIPPET_LENGTH = 2_500;
|
||||||
|
const MAX_OUTPUT_LENGTH = 20_000;
|
||||||
|
const REQUEST_TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
const searchParams = Type.Object({
|
||||||
|
query: Type.String({
|
||||||
|
description: "Specific, keyword-focused web search query",
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 500,
|
||||||
|
}),
|
||||||
|
num_results: Type.Optional(
|
||||||
|
Type.Integer({
|
||||||
|
description: `Maximum results to return (default ${DEFAULT_NUM_RESULTS}, maximum ${MAX_NUM_RESULTS})`,
|
||||||
|
minimum: 1,
|
||||||
|
maximum: MAX_NUM_RESULTS,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
engines: Type.Optional(
|
||||||
|
Type.Array(Type.String({ minLength: 1, maxLength: 50 }), {
|
||||||
|
description: "Search engines to use, for example google, bing, duckduckgo, wikipedia, or github",
|
||||||
|
minItems: 1,
|
||||||
|
maxItems: 10,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
language: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
description: "Result language, for example zh-CN or en",
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 20,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
page: Type.Optional(
|
||||||
|
Type.Integer({
|
||||||
|
description: "Result page number (default 1)",
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 50,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
time_range: Type.Optional(
|
||||||
|
Type.Union(
|
||||||
|
[Type.Literal("day"), Type.Literal("week"), Type.Literal("month"), Type.Literal("year")],
|
||||||
|
{ description: "Optional recency filter" },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SearxngResult {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
content?: string;
|
||||||
|
engine?: string;
|
||||||
|
engines?: string[];
|
||||||
|
publishedDate?: string;
|
||||||
|
score?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearxngResponse {
|
||||||
|
query?: string;
|
||||||
|
results?: SearxngResult[];
|
||||||
|
answers?: string[];
|
||||||
|
suggestions?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactText(value: string, maxLength: number): string {
|
||||||
|
const compacted = value.replace(/\s+/g, " ").trim();
|
||||||
|
return compacted.length <= maxLength ? compacted : `${compacted.slice(0, maxLength)}…`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatResults(data: SearxngResponse, maxResults: number): string {
|
||||||
|
const sections: string[] = [];
|
||||||
|
|
||||||
|
if (data.answers && data.answers.length > 0) {
|
||||||
|
sections.push(`## Answers\n${data.answers.map((answer) => compactText(answer, MAX_SNIPPET_LENGTH)).join("\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = (data.results ?? []).slice(0, maxResults);
|
||||||
|
if (results.length > 0) {
|
||||||
|
const formatted = results.map((result, index) => {
|
||||||
|
const lines = [`${index + 1}. ${compactText(result.title ?? "(no title)", 500)}`];
|
||||||
|
if (result.url) lines.push(` URL: ${result.url}`);
|
||||||
|
const engines = result.engines?.length ? result.engines : result.engine ? [result.engine] : [];
|
||||||
|
if (engines.length > 0) lines.push(` Engines: ${engines.join(", ")}`);
|
||||||
|
if (result.publishedDate) lines.push(` Published: ${result.publishedDate}`);
|
||||||
|
if (result.content) lines.push(` ${compactText(result.content, MAX_SNIPPET_LENGTH)}`);
|
||||||
|
return lines.join("\n");
|
||||||
|
});
|
||||||
|
sections.push(`## Results (${results.length})\n${formatted.join("\n\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.suggestions && data.suggestions.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
`## Suggestions\n${data.suggestions.map((suggestion) => compactText(suggestion, 500)).join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = sections.length > 0 ? sections.join("\n\n") : "No results found.";
|
||||||
|
return text.length <= MAX_OUTPUT_LENGTH ? text : `${text.slice(0, MAX_OUTPUT_LENGTH)}\n\n[Truncated]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configuredEndpoint(): string {
|
||||||
|
const endpoint = process.env.SEARXNG_URL?.trim();
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new Error(
|
||||||
|
"SEARXNG_URL is not available to the Pi process. Set it in .env or the environment and restart Pi Agent Integrated.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function searxngSearchExtension(pi: ExtensionAPI) {
|
||||||
|
pi.registerTool({
|
||||||
|
name: "web_search",
|
||||||
|
label: "Web Search",
|
||||||
|
description:
|
||||||
|
"Search the current web through the application's configured SearXNG service. Returns result titles, URLs, engines, dates, and snippets. Use it for recent or time-sensitive information, current software documentation and versions, unfamiliar errors, explicit lookup requests, and verifiable facts you are uncertain about.",
|
||||||
|
promptSnippet: "Search the current web through the configured SearXNG service",
|
||||||
|
promptGuidelines: [
|
||||||
|
"Use web_search when the user asks to search or verify online, or when information may have changed recently.",
|
||||||
|
"Prefer focused queries. If results are weak, refine the query or select suitable engines and try again.",
|
||||||
|
"Do not search for information already available in the opened codebase, conversation, or provided files.",
|
||||||
|
"When presenting search findings, retain the result URLs so the user can inspect the sources.",
|
||||||
|
],
|
||||||
|
parameters: searchParams,
|
||||||
|
executionMode: "parallel",
|
||||||
|
async execute(_toolCallId, params, signal) {
|
||||||
|
const token = process.env.SEARXNG_TOKEN?.trim();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
"SEARXNG_TOKEN is not available to the Pi process. Set it in the environment and restart Pi Agent Integrated.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = params.query.trim();
|
||||||
|
if (!query) throw new Error("Search query must not be empty");
|
||||||
|
|
||||||
|
const maxResults = params.num_results ?? DEFAULT_NUM_RESULTS;
|
||||||
|
const endpoint = configuredEndpoint();
|
||||||
|
const url = new URL(endpoint);
|
||||||
|
url.searchParams.set("q", query);
|
||||||
|
url.searchParams.set("format", "json");
|
||||||
|
|
||||||
|
const engines = params.engines?.map((engine) => engine.trim()).filter(Boolean);
|
||||||
|
if (engines && engines.length > 0) url.searchParams.set("engines", engines.join(","));
|
||||||
|
if (params.language) url.searchParams.set("language", params.language);
|
||||||
|
if (params.page) url.searchParams.set("pageno", String(params.page));
|
||||||
|
if (params.time_range) url.searchParams.set("time_range", params.time_range);
|
||||||
|
|
||||||
|
const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
||||||
|
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"X-Search-Token": token,
|
||||||
|
},
|
||||||
|
signal: requestSignal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`SearXNG request failed: HTTP ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await response.text();
|
||||||
|
let data: SearxngResponse;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(body) as SearxngResponse;
|
||||||
|
} catch {
|
||||||
|
throw new Error("SearXNG returned a non-JSON response");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultCount = Math.min(data.results?.length ?? 0, maxResults);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: formatResults(data, maxResults) }],
|
||||||
|
details: {
|
||||||
|
query,
|
||||||
|
endpoint,
|
||||||
|
resultCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,7 +6,13 @@ const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|||||||
const webDir = join(rootDir, "pi-web");
|
const webDir = join(rootDir, "pi-web");
|
||||||
const requiredProfilePaths = [
|
const requiredProfilePaths = [
|
||||||
join(rootDir, "config", "settings.default.json"),
|
join(rootDir, "config", "settings.default.json"),
|
||||||
|
join(rootDir, "config", "mcp.default.json"),
|
||||||
join(rootDir, "config", "models.example.json"),
|
join(rootDir, "config", "models.example.json"),
|
||||||
|
join(rootDir, "config", "subagents.default.json"),
|
||||||
|
join(rootDir, "config", "subagent-tool-description.default.md"),
|
||||||
|
join(rootDir, ".env.example"),
|
||||||
|
join(rootDir, "LICENSE"),
|
||||||
|
join(rootDir, "THIRD_PARTY_NOTICES.md"),
|
||||||
join(rootDir, "resources", "skills"),
|
join(rootDir, "resources", "skills"),
|
||||||
join(rootDir, "resources", "extensions"),
|
join(rootDir, "resources", "extensions"),
|
||||||
join(rootDir, "resources", "prompts"),
|
join(rootDir, "resources", "prompts"),
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { platform } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import {
|
||||||
|
agentDir,
|
||||||
|
dataDir,
|
||||||
|
managedEnvironment,
|
||||||
|
resourcesDir,
|
||||||
|
rootDir,
|
||||||
|
} from "./profile.mjs";
|
||||||
|
|
||||||
|
Object.assign(process.env, managedEnvironment());
|
||||||
|
|
||||||
|
const codingAgentEntry = pathToFileURL(
|
||||||
|
join(rootDir, "pi-web", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"),
|
||||||
|
).href;
|
||||||
|
const { DefaultResourceLoader } = await import(codingAgentEntry);
|
||||||
|
|
||||||
|
const loader = new DefaultResourceLoader({
|
||||||
|
cwd: join(dataDir, "workspaces", "default"),
|
||||||
|
agentDir,
|
||||||
|
additionalExtensionPaths: [join(resourcesDir, "extensions")],
|
||||||
|
additionalSkillPaths: [join(resourcesDir, "skills")],
|
||||||
|
});
|
||||||
|
|
||||||
|
await loader.reload();
|
||||||
|
const loaded = loader.getExtensions();
|
||||||
|
if (loaded.errors.length > 0) {
|
||||||
|
for (const error of loaded.errors) console.error(`Extension load error: ${error.error}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = new Set(loaded.extensions.flatMap((extension) => [...extension.tools.keys()]));
|
||||||
|
const commands = new Set(loaded.extensions.flatMap((extension) => [...extension.commands.keys()]));
|
||||||
|
const requiredTools = [
|
||||||
|
"web_search",
|
||||||
|
"mcp",
|
||||||
|
"lens_diagnostics",
|
||||||
|
"subagent",
|
||||||
|
"web_fetch",
|
||||||
|
"memory_write",
|
||||||
|
"resolve-library-id",
|
||||||
|
"query-docs",
|
||||||
|
];
|
||||||
|
const requiredCommands = ["checkpoint", "rewind"];
|
||||||
|
const missing = [
|
||||||
|
...requiredTools.filter((name) => !tools.has(name)).map((name) => `tool:${name}`),
|
||||||
|
...requiredCommands.filter((name) => !commands.has(name)).map((name) => `command:/${name}`),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error(`Managed Pi profile is incomplete: ${missing.join(", ")}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const npmCliPath = process.env.npm_execpath;
|
||||||
|
if (!npmCliPath) {
|
||||||
|
console.error("Run the managed profile check through npm: npm run check:profile");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const playwrightResult = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[npmCliPath, "exec", "--yes", "--package=@playwright/mcp@0.0.78", "--", "playwright-mcp", "--version"],
|
||||||
|
{ cwd: rootDir, env: process.env, encoding: "utf8" },
|
||||||
|
);
|
||||||
|
if (playwrightResult.error || playwrightResult.status !== 0) {
|
||||||
|
process.stderr.write(playwrightResult.stderr ?? "");
|
||||||
|
console.error("Unable to cache @playwright/mcp@0.0.78 for the managed MCP profile.");
|
||||||
|
process.exit(playwrightResult.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform() === "win32") {
|
||||||
|
const edgeCandidates = [
|
||||||
|
join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||||
|
join(process.env.ProgramFiles ?? "C:\\Program Files", "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||||
|
];
|
||||||
|
if (!edgeCandidates.some(existsSync)) {
|
||||||
|
console.error("Microsoft Edge is required by config/mcp.default.json but was not found.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Managed Pi profile ready: ${loaded.extensions.length} extension(s), ${tools.size} tool(s), ${commands.size} command(s).`,
|
||||||
|
);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { agentDir, managedEnvironment, rootDir } from "./profile.mjs";
|
||||||
|
|
||||||
|
const settings = JSON.parse(
|
||||||
|
readFileSync(join(rootDir, "config", "settings.default.json"), "utf8"),
|
||||||
|
);
|
||||||
|
const packages = Array.isArray(settings.packages) ? settings.packages : [];
|
||||||
|
const cliPath = join(rootDir, "pi", "packages", "coding-agent", "dist", "cli.js");
|
||||||
|
const env = managedEnvironment();
|
||||||
|
|
||||||
|
function parseExactNpmSource(source) {
|
||||||
|
if (!source.startsWith("npm:")) return undefined;
|
||||||
|
const spec = source.slice(4);
|
||||||
|
const separator = spec.lastIndexOf("@");
|
||||||
|
if (separator <= 0) return undefined;
|
||||||
|
return { name: spec.slice(0, separator), version: spec.slice(separator + 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function installedAtExpectedVersion(source) {
|
||||||
|
const parsed = parseExactNpmSource(source);
|
||||||
|
if (!parsed) return false;
|
||||||
|
const packageJson = join(agentDir, "npm", "node_modules", ...parsed.name.split("/"), "package.json");
|
||||||
|
if (!existsSync(packageJson)) return false;
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(packageJson, "utf8")).version === parsed.version;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const source of packages) {
|
||||||
|
if (typeof source !== "string" || !source.trim()) continue;
|
||||||
|
if (installedAtExpectedVersion(source)) {
|
||||||
|
console.log(`Managed package ready: ${source}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Installing managed package: ${source}`);
|
||||||
|
const result = spawnSync(process.execPath, [cliPath, "install", source], {
|
||||||
|
cwd: rootDir,
|
||||||
|
env,
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||||
|
}
|
||||||
+67
-2
@@ -1,25 +1,40 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
||||||
|
import { platform } from "node:os";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { loadEnvFile } from "node:process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
export const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
export const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||||
|
const projectEnvPath = join(rootDir, ".env");
|
||||||
|
if (existsSync(projectEnvPath)) loadEnvFile(projectEnvPath);
|
||||||
|
|
||||||
export const configDir = join(rootDir, "config");
|
export const configDir = join(rootDir, "config");
|
||||||
export const resourcesDir = join(rootDir, "resources");
|
export const resourcesDir = join(rootDir, "resources");
|
||||||
export const dataDir = resolve(process.env.PI_AGENT_DATA_DIR ?? join(rootDir, "data"));
|
export const dataDir = resolve(process.env.PI_AGENT_DATA_DIR ?? join(rootDir, "data"));
|
||||||
export const agentDir = join(dataDir, "agent");
|
export const agentDir = join(dataDir, "agent");
|
||||||
|
export const homeDir = join(dataDir, "home");
|
||||||
|
export const memoryDir = join(agentDir, "memory");
|
||||||
export const skillsHomeDir = join(dataDir, "skills-home");
|
export const skillsHomeDir = join(dataDir, "skills-home");
|
||||||
export const stateDir = join(dataDir, "state");
|
export const stateDir = join(dataDir, "state");
|
||||||
|
|
||||||
const managedDirectories = [
|
const managedDirectories = [
|
||||||
agentDir,
|
agentDir,
|
||||||
|
homeDir,
|
||||||
join(agentDir, "sessions"),
|
join(agentDir, "sessions"),
|
||||||
join(agentDir, "npm"),
|
join(agentDir, "npm"),
|
||||||
join(agentDir, "git"),
|
join(agentDir, "git"),
|
||||||
join(agentDir, "bin"),
|
join(agentDir, "bin"),
|
||||||
|
join(agentDir, "extensions", "subagent"),
|
||||||
|
memoryDir,
|
||||||
|
join(memoryDir, "daily"),
|
||||||
|
join(memoryDir, "recovery"),
|
||||||
join(agentDir, "tools"),
|
join(agentDir, "tools"),
|
||||||
join(agentDir, "tmp"),
|
join(agentDir, "tmp"),
|
||||||
join(dataDir, "logs"),
|
join(dataDir, "logs"),
|
||||||
join(dataDir, "cache"),
|
join(dataDir, "cache", "npm"),
|
||||||
|
join(dataDir, "mcp", "playwright", "output"),
|
||||||
|
join(dataDir, "workspaces", "default"),
|
||||||
join(skillsHomeDir, ".agents", "skills"),
|
join(skillsHomeDir, ".agents", "skills"),
|
||||||
join(skillsHomeDir, ".pi", "agent", "skills"),
|
join(skillsHomeDir, ".pi", "agent", "skills"),
|
||||||
stateDir,
|
stateDir,
|
||||||
@@ -31,9 +46,50 @@ const managedDirectories = [
|
|||||||
|
|
||||||
const seedFiles = [
|
const seedFiles = [
|
||||||
[join(configDir, "settings.default.json"), join(agentDir, "settings.json")],
|
[join(configDir, "settings.default.json"), join(agentDir, "settings.json")],
|
||||||
|
[join(configDir, "mcp.default.json"), join(agentDir, "mcp.json")],
|
||||||
[join(configDir, "models.example.json"), join(agentDir, "models.json")],
|
[join(configDir, "models.example.json"), join(agentDir, "models.json")],
|
||||||
|
[join(configDir, "subagents.default.json"), join(agentDir, "extensions", "subagent", "config.json")],
|
||||||
|
[join(configDir, "subagent-tool-description.default.md"), join(agentDir, "subagent-tool-description.md")],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const persistedWindowsEnvironmentKeys = ["SEARXNG_TOKEN", "SEARXNG_URL"];
|
||||||
|
|
||||||
|
function readPersistedWindowsEnvironment(baseEnv) {
|
||||||
|
if (platform() !== "win32") return {};
|
||||||
|
|
||||||
|
const missingKeys = persistedWindowsEnvironmentKeys.filter((key) => !baseEnv[key]?.trim());
|
||||||
|
if (missingKeys.length === 0) return {};
|
||||||
|
|
||||||
|
const keyLiterals = missingKeys.map((key) => `'${key.replaceAll("'", "''")}'`).join(", ");
|
||||||
|
const script = `
|
||||||
|
$result = @{}
|
||||||
|
foreach ($name in @(${keyLiterals})) {
|
||||||
|
$value = [Environment]::GetEnvironmentVariable($name, 'User')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
$value = [Environment]::GetEnvironmentVariable($name, 'Machine')
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
$result[$name] = $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConvertTo-Json -Compress -InputObject $result
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
"powershell.exe",
|
||||||
|
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||||
|
{ encoding: "utf8", windowsHide: true },
|
||||||
|
);
|
||||||
|
if (result.status !== 0 || !result.stdout.trim()) return {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const values = JSON.parse(result.stdout);
|
||||||
|
return values && typeof values === "object" && !Array.isArray(values) ? values : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ensureProfile({ quiet = false } = {}) {
|
export function ensureProfile({ quiet = false } = {}) {
|
||||||
for (const path of managedDirectories) mkdirSync(path, { recursive: true });
|
for (const path of managedDirectories) mkdirSync(path, { recursive: true });
|
||||||
|
|
||||||
@@ -49,19 +105,28 @@ export function ensureProfile({ quiet = false } = {}) {
|
|||||||
if (created.length > 0) console.log(`Initialized ${created.length} profile file(s).`);
|
if (created.length > 0) console.log(`Initialized ${created.length} profile file(s).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { rootDir, configDir, resourcesDir, dataDir, agentDir, skillsHomeDir, stateDir, created };
|
return { rootDir, configDir, resourcesDir, dataDir, agentDir, homeDir, skillsHomeDir, stateDir, created };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function managedEnvironment(baseEnv = process.env) {
|
export function managedEnvironment(baseEnv = process.env) {
|
||||||
ensureProfile({ quiet: true });
|
ensureProfile({ quiet: true });
|
||||||
|
const persistedEnvironment = readPersistedWindowsEnvironment(baseEnv);
|
||||||
return {
|
return {
|
||||||
...baseEnv,
|
...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,
|
||||||
PI_AGENT_RESOURCES_DIR: resourcesDir,
|
PI_AGENT_RESOURCES_DIR: resourcesDir,
|
||||||
PI_AGENT_SKILLS_HOME: skillsHomeDir,
|
PI_AGENT_SKILLS_HOME: skillsHomeDir,
|
||||||
PI_CODING_AGENT_DIR: agentDir,
|
PI_CODING_AGENT_DIR: agentDir,
|
||||||
|
HOME: homeDir,
|
||||||
|
USERPROFILE: homeDir,
|
||||||
|
PI_MEMORY_DIR: baseEnv.PI_MEMORY_DIR?.trim() || memoryDir,
|
||||||
|
PI_MEMORY_SNAPSHOT: baseEnv.PI_MEMORY_SNAPSHOT?.trim() || "stable",
|
||||||
|
PI_MEMORY_QMD_UPDATE: baseEnv.PI_MEMORY_QMD_UPDATE?.trim() || "off",
|
||||||
|
PI_RETRY_STALL_TIMEOUT_MS: baseEnv.PI_RETRY_STALL_TIMEOUT_MS?.trim() || "180000",
|
||||||
|
npm_config_cache: baseEnv.npm_config_cache?.trim() || join(dataDir, "cache", "npm"),
|
||||||
XDG_STATE_HOME: stateDir,
|
XDG_STATE_HOME: stateDir,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { spawn, spawnSync } from "node:child_process";
|
||||||
|
import { platform } from "node:os";
|
||||||
|
import { rootDir } from "./profile.mjs";
|
||||||
|
|
||||||
|
const DEV_PORT = 30141;
|
||||||
|
|
||||||
|
function stopWindowsDevelopmentProcesses() {
|
||||||
|
const projectRootLiteral = rootDir.replaceAll("'", "''");
|
||||||
|
const script = `
|
||||||
|
$projectRoot = '${projectRootLiteral}'
|
||||||
|
$currentProcessId = ${process.pid}
|
||||||
|
$excludedProcessIds = [Collections.Generic.HashSet[int]]::new()
|
||||||
|
$ancestorProcessId = $currentProcessId
|
||||||
|
|
||||||
|
while ($ancestorProcessId -gt 0 -and $excludedProcessIds.Add($ancestorProcessId)) {
|
||||||
|
$ancestor = Get-CimInstance Win32_Process -Filter "ProcessId = $ancestorProcessId" -ErrorAction SilentlyContinue
|
||||||
|
if (-not $ancestor) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
$ancestorProcessId = [int]$ancestor.ParentProcessId
|
||||||
|
}
|
||||||
|
|
||||||
|
$targets = Get-CimInstance Win32_Process | Where-Object {
|
||||||
|
-not $excludedProcessIds.Contains([int]$_.ProcessId) -and
|
||||||
|
$_.CommandLine -and
|
||||||
|
$_.CommandLine.IndexOf($projectRoot, [StringComparison]::OrdinalIgnoreCase) -ge 0 -and
|
||||||
|
$_.Name -in @('node.exe', 'cmd.exe')
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($target in $targets) {
|
||||||
|
Stop-Process -Id $target.ProcessId -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
$listener = Get-NetTCPConnection -LocalPort ${DEV_PORT} -State Listen -ErrorAction SilentlyContinue
|
||||||
|
if ($listener) {
|
||||||
|
throw "Port ${DEV_PORT} is still occupied by PID $($listener[0].OwningProcess)"
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
"powershell.exe",
|
||||||
|
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||||
|
{ cwd: rootDir, stdio: "inherit" },
|
||||||
|
);
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform() !== "win32") {
|
||||||
|
console.error("npm run restart currently supports Windows only. Stop the existing dev process and run npm run dev.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWindowsDevelopmentProcesses();
|
||||||
|
|
||||||
|
const npmCliPath = process.env.npm_execpath;
|
||||||
|
if (!npmCliPath) {
|
||||||
|
console.error("Run this command through npm: npm run restart");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Restarting Pi Agent Integrated at http://127.0.0.1:${DEV_PORT}`);
|
||||||
|
const child = spawn(process.execPath, [npmCliPath, "run", "dev"], {
|
||||||
|
cwd: rootDir,
|
||||||
|
env: process.env,
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||||
|
process.on(signal, () => child.kill(signal));
|
||||||
|
}
|
||||||
|
|
||||||
|
child.on("error", (error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("exit", (code, signal) => {
|
||||||
|
if (signal) process.kill(process.pid, signal);
|
||||||
|
process.exit(code ?? 1);
|
||||||
|
});
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|||||||
import { managedEnvironment, rootDir } from "./profile.mjs";
|
import { managedEnvironment, rootDir } from "./profile.mjs";
|
||||||
|
|
||||||
const target = process.argv[2] ?? "dev";
|
const target = process.argv[2] ?? "dev";
|
||||||
const allowedTargets = new Set(["dev", "dev:lan", "start", "start:lan"]);
|
const allowedTargets = new Set(["build", "dev", "dev:lan", "start", "start:lan"]);
|
||||||
if (!allowedTargets.has(target)) {
|
if (!allowedTargets.has(target)) {
|
||||||
console.error(`Unsupported Pi Web script: ${target}`);
|
console.error(`Unsupported Pi Web script: ${target}`);
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
|
|||||||
+23
-6
@@ -3,11 +3,11 @@ import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { ensureProfile } from "./profile.mjs";
|
import { ensureProfile, managedEnvironment } from "./profile.mjs";
|
||||||
|
|
||||||
const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||||
const npmCliPath = process.env.npm_execpath;
|
const npmCliPath = process.env.npm_execpath;
|
||||||
const setupEnv = { ...process.env };
|
const setupEnv = managedEnvironment();
|
||||||
|
|
||||||
if (process.env.PI_SETUP_USE_PROXY !== "1") {
|
if (process.env.PI_SETUP_USE_PROXY !== "1") {
|
||||||
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]) {
|
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]) {
|
||||||
@@ -91,17 +91,34 @@ function assertSupportedNodeVersion() {
|
|||||||
assertSupportedNodeVersion();
|
assertSupportedNodeVersion();
|
||||||
ensureProfile();
|
ensureProfile();
|
||||||
|
|
||||||
console.log("[1/4] Installing Pi dependencies...");
|
console.log("[1/5] Installing Pi dependencies...");
|
||||||
run(["ci", "--ignore-scripts"], join(rootDir, "pi"));
|
run(["ci", "--ignore-scripts"], join(rootDir, "pi"));
|
||||||
|
|
||||||
console.log("[2/4] Hydrating model data and building local Pi packages...");
|
console.log("[2/5] Hydrating model data and building local Pi packages...");
|
||||||
hydrateModelData();
|
hydrateModelData();
|
||||||
run(["run", "build:offline"], join(rootDir, "pi"));
|
run(["run", "build:offline"], join(rootDir, "pi"));
|
||||||
|
|
||||||
console.log("[3/4] Installing Pi Web with local Pi package links...");
|
console.log("[3/5] Installing Pi Web with local Pi package links...");
|
||||||
run(["ci", "--ignore-scripts", "--install-links"], join(rootDir, "pi-web"));
|
run(["ci", "--ignore-scripts", "--install-links"], join(rootDir, "pi-web"));
|
||||||
|
|
||||||
console.log("[4/4] Verifying the integration...");
|
console.log("[4/5] Installing and validating managed Pi packages...");
|
||||||
|
const packageResult = spawnSync(process.execPath, [join(rootDir, "scripts", "install-managed-packages.mjs")], {
|
||||||
|
cwd: rootDir,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: setupEnv,
|
||||||
|
});
|
||||||
|
if (packageResult.error) throw packageResult.error;
|
||||||
|
if (packageResult.status !== 0) process.exit(packageResult.status ?? 1);
|
||||||
|
|
||||||
|
const profileResult = spawnSync(process.execPath, [join(rootDir, "scripts", "check-managed-profile.mjs")], {
|
||||||
|
cwd: rootDir,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: setupEnv,
|
||||||
|
});
|
||||||
|
if (profileResult.error) throw profileResult.error;
|
||||||
|
if (profileResult.status !== 0) process.exit(profileResult.status ?? 1);
|
||||||
|
|
||||||
|
console.log("[5/5] Verifying the integration...");
|
||||||
const checkResult = spawnSync(process.execPath, [join(rootDir, "scripts", "check-integration.mjs")], {
|
const checkResult = spawnSync(process.execPath, [join(rootDir, "scripts", "check-integration.mjs")], {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { rootDir } from "./profile.mjs";
|
||||||
|
|
||||||
|
const npmCliPath = process.env.npm_execpath;
|
||||||
|
if (!npmCliPath) {
|
||||||
|
console.error("Run this smoke test through npm: npm run smoke:fresh-profile");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixtureDataDir = mkdtempSync(join(tmpdir(), "pi-agent-integrated-profile-"));
|
||||||
|
const childEnv = {
|
||||||
|
...process.env,
|
||||||
|
PI_AGENT_DATA_DIR: fixtureDataDir,
|
||||||
|
};
|
||||||
|
if (process.env.PI_SETUP_USE_PROXY !== "1") {
|
||||||
|
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]) {
|
||||||
|
delete childEnv[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const installResult = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[npmCliPath, "run", "profile:packages"],
|
||||||
|
{ cwd: rootDir, env: childEnv, stdio: "inherit" },
|
||||||
|
);
|
||||||
|
if (installResult.error) throw installResult.error;
|
||||||
|
if (installResult.status !== 0) {
|
||||||
|
throw new Error(`Fresh profile package installation exited with ${installResult.status ?? "unknown status"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkResult = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[npmCliPath, "run", "check:profile"],
|
||||||
|
{ cwd: rootDir, env: childEnv, stdio: "inherit" },
|
||||||
|
);
|
||||||
|
if (checkResult.error) throw checkResult.error;
|
||||||
|
if (checkResult.status !== 0) {
|
||||||
|
throw new Error(`Fresh profile validation exited with ${checkResult.status ?? "unknown status"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPath = join(fixtureDataDir, "agent", "settings.json");
|
||||||
|
const mcpPath = join(fixtureDataDir, "agent", "mcp.json");
|
||||||
|
const packagePath = join(fixtureDataDir, "agent", "npm", "package.json");
|
||||||
|
for (const path of [settingsPath, mcpPath, packagePath]) {
|
||||||
|
if (!existsSync(path)) throw new Error(`Fresh profile is missing ${path}`);
|
||||||
|
JSON.parse(readFileSync(path, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Fresh managed profile installed and loaded successfully.");
|
||||||
|
} finally {
|
||||||
|
rmSync(fixtureDataDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user