Compare commits

...

5 Commits

Author SHA1 Message Date
a7fb0a4369 fix: 同步 PI_CODING_AGENT_DIR 并更新模型列表
Some checks failed
CI / build-check-test (push) Has been cancelled
- build.sh 导出 PI_CODING_AGENT_DIR 以对齐 pi-mcp-adapter 配置路径
- 更新 models.generated.ts
- 移除 bmi-calculator.html 示例文件
2026-06-26 13:21:14 +08:00
539599dbb8 feat(coding-agent): isolate SproutClaw namespace from upstream pi
Some checks failed
CI / build-check-test (push) Has been cancelled
Use piConfig name sproutclaw and configDir .sproutclaw so commands, env vars,
and project config no longer collide with the stock pi install.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:29:28 +08:00
294ce75892 docs: 更新 README,修正过时内容
Some checks failed
CI / build-check-test (push) Has been cancelled
- 移除 .pi/ 目录条目(已 gitignore)
- 更新 WebUI 说明为独立 sproutclaw-web 服务
- 修正 build.sh/test.sh 说明和脚本引用错误
- 更新合并上游流程(直接 merge,不再用临时分支)
- 补充 Gitea remote 配置说明

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 20:56:31 +08:00
f1c5e7dc1d chore: rename pi-built/pi-test scripts to build/test
Some checks failed
CI / build-check-test (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 20:54:00 +08:00
812bca79a3 docs: 将 CONTRIBUTING.md 和 SECURITY.md 翻译为中文
Some checks failed
CI / build-check-test (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 20:49:18 +08:00
17 changed files with 783 additions and 886 deletions

54
.gitignore vendored
View File

@@ -1,8 +1,8 @@
node_modules/
dist/
# Pi persistent config — never commit
.pi/
# SproutClaw persistent config — never commit
.sproutclaw/
*.log
.DS_Store
*.tsbuildinfo
@@ -28,7 +28,7 @@ packages/*/dist-firefox/
.npm/
coverage/
.nyc_output/
.pi_config/
.sproutclaw_config/
tui-debug.log
compaction-results/
.opencode/
@@ -39,34 +39,34 @@ out.html
packages/coding-agent/binaries/
todo.md
plans/
.pi/hf-sessions/
.pi/hf-sessions-backup/
.pi/sessions/
.pi/extensions/webui/.webui.pid
.pi/extensions/webui/.webui.log
.sproutclaw/hf-sessions/
.sproutclaw/hf-sessions-backup/
.sproutclaw/sessions/
.sproutclaw/extensions/webui/.webui.pid
.sproutclaw/extensions/webui/.webui.log
# Local runtime / secrets (do not commit)
.local/
progress.md
# Pi agent: track extensions, ignore local runtime secrets and sessions
.pi/agent/sessions/
.pi/agent/git/
.pi/agent/npm/
.pi/agent/bin/
.pi/agent/data/
.pi/agent/auth.json
.pi/agent/models.json
.pi/agent/settings.json
.pi/agent/mcp.json
.pi/agent/mcp-cache.json
.pi/agent/mcp-npx-cache.json
.pi/agent/run-history.jsonl
.pi/agent/taskplane/
.pi/agent/skills/
.pi/agent/skills-disabled/
.pi/agent/extensions/webui/frontend/node_modules/
.pi/agent/extensions/webui/.webui.pid
.pi/agent/extensions/webui/data/
# SproutClaw agent: track extensions, ignore local runtime secrets and sessions
.sproutclaw/agent/sessions/
.sproutclaw/agent/git/
.sproutclaw/agent/npm/
.sproutclaw/agent/bin/
.sproutclaw/agent/data/
.sproutclaw/agent/auth.json
.sproutclaw/agent/models.json
.sproutclaw/agent/settings.json
.sproutclaw/agent/mcp.json
.sproutclaw/agent/mcp-cache.json
.sproutclaw/agent/mcp-npx-cache.json
.sproutclaw/agent/run-history.jsonl
.sproutclaw/agent/taskplane/
.sproutclaw/agent/skills/
.sproutclaw/agent/skills-disabled/
.sproutclaw/agent/extensions/webui/frontend/node_modules/
.sproutclaw/agent/extensions/webui/.webui.pid
.sproutclaw/agent/extensions/webui/data/
tmp/
bun.lock
collect.sh

View File

@@ -94,7 +94,7 @@ Run the TUI in a controlled terminal (from the repo root):
```bash
tmux new-session -d -s pi-test -x 80 -y 24
tmux send-keys -t pi-test "./pi-test.sh" Enter
tmux send-keys -t pi-test "./test.sh" Enter
sleep 3 && tmux capture-pane -t pi-test -p # capture after startup
tmux send-keys -t pi-test "your prompt here" Enter
tmux send-keys -t pi-test Escape # special keys (also C-o for ctrl+o, etc.)

View File

@@ -1,102 +1,97 @@
# Contributing to pi
# 贡献指南
This guide exists to save both sides time.
本指南旨在为双方节省时间。
## Philosophy
## 设计理念
First things first: **pi's core is minimal**.
首先明确一点:**SproutClaw 的核心保持精简**。
If your feature does not belong in the core, it should be an extension. PRs that bloat the core will likely be rejected.
如果你的功能不属于核心,它应该以扩展的形式实现。会让核心臃肿的 PR 大概率会被拒绝。
Pi's core exists to be minimal and to be extensible so that it can be influenced and manipulated by extensions. Even hook points for extensions however should be well considered and discussed to avoid adding unmaintainable bloat and complex interactions.
SproutClaw 的核心存在的意义在于精简且可扩展,以便扩展能够影响和定制其行为。即便是扩展的 hook 点也需要经过充分考量和讨论,避免引入难以维护的冗余代码和复杂交互。
## The One Rule
## 唯一准则
**You must understand your code.** If you cannot explain what your changes do and how they interact with the rest of the system, your PR will be closed.
**你必须理解自己提交的代码。** 如果你无法解释你的改动做了什么、以及它与系统其他部分如何交互,你的 PR 将被关闭。
Using AI to write code is fine. Submitting AI-generated slop without understanding it is not.
使用 AI 辅助编写代码没有问题,但在不理解代码的情况下直接提交 AI 生成的内容是不被接受的。
If you use an agent, run it from the `pi` root directory so it picks up `AGENTS.md` automatically. Your agent must follow the rules and guidelines in that file.
如果你使用 Agent请在 `sproutclaw` 根目录运行,这样它会自动读取 `AGENTS.md`。你的 Agent 必须遵循该文件中的规则和指南。
## Contribution Gate
## 贡献门槛
All issues and PRs from new contributors are auto-closed by default.
来自新贡献者的 issue 和 PR 默认会被自动关闭。
Issues submitted Friday through Sunday are not guaranteed to be reviewed. If something is urgent, ask on Discord: https://discord.com/invite/3cU7Bz4UPx
周五至周日提交的 issue 不保证会被审查。如有紧急问题,请在内部联系维护者。
Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar below will not be reopened or receive a reply.
维护者会每日审查被自动关闭的 issue并重新开放值得处理的内容。不符合以下质量标准的 issue 将不会被重新开放或收到回复。
Approval happens through maintainer replies on issues:
审批通过由维护者在 issue 中回复:
- `lgtmi`: your future issues will not be auto-closed
- `lgtm`: your future issues and PRs will not be auto-closed
- `lgtmi`:你未来提交的 issue 将不再被自动关闭
- `lgtm`:你未来提交的 issue 和 PR 将不再被自动关闭
`lgtmi` does not grant rights to submit PRs. Only `lgtm` grants rights to submit PRs.
`lgtmi` 不授予提交 PR 的权限,只有 `lgtm` 才授予该权限。
## Quality Bar For Issues
## Issue 质量标准
If you open an issue, you must use one of the two GitHub issue templates.
提交 issue 时,必须使用两个 GitHub issue 模板之一。
If you open an issue, keep it short, concrete, and worth reading.
提交的 issue 应简洁、具体、有阅读价值:
- Keep it concise. If it does not fit on one screen, it is too long.
- Write in your own voice (do not use an LLM to generate text, if you must, follow up with a clearly AI labeled comment).
- State the bug or request clearly.
- Explain why it matters.
- If you want to implement the change yourself, say so.
- 保持简短。如果超过一屏,就太长了。
- 用自己的语言表达(不要用 LLM 生成文本;如果必须,请在后续评论中明确标注为 AI 生成)。
- 清晰说明 bug 或需求。
- 解释其重要性。
- 如果你希望自己来实现,请说明。
If the issue is real and written well, a maintainer may reopen it, reply `lgtmi`, or reply `lgtm`.
如果 issue 真实且表述良好,维护者可能会重新开放、回复 `lgtmi` 或回复 `lgtm`
## Blocking
## 封禁规则
If you ignore this document twice, or if you spam the tracker with agent-generated issues, your GitHub account will be permanently blocked.
如果你两次无视本文档,或通过 Agent 批量刷 issue你的账号将被永久封禁。
If you send a large volume of issues through automation, your GitHub account will be permanently blocked. No taksies backsies.
如果你通过自动化手段大量提交 issue你的账号将被永久封禁且不可申诉。
## Before Submitting a PR
## 提交 PR
Do not open a PR unless you have already been approved with `lgtm`.
未获得 `lgtm` 批准前,请勿提交 PR。
Before submitting a PR:
提交 PR 前,请先执行:
```bash
npm run check
./test.sh
```
Both must pass.
两项均须通过。
Do not edit `CHANGELOG.md`. Changelog entries are added by maintainers.
不要修改 `CHANGELOG.md`,更新日志条目由维护者添加。
If you are adding a new provider to `packages/ai`, see `AGENTS.md` for required tests.
如果你要在 `packages/ai` 中添加新的 provider请参阅 `AGENTS.md` 中关于必要测试的说明。
## Questions?
## 有疑问?
Ask on [Discord](https://discord.com/invite/nKXTsAcmbT).
请联系项目维护者。
## FAQ
## 常见问题
### Why are new issues and PRs auto-closed?
### 为什么新 issue 和 PR 会被自动关闭?
pi receives more issues than the maintainers can responsibly review in real time. Many reports do not meet the quality bar in this guide or do not follow CONTRIBUTING.md. Some are slung at the repository mindlessly via an agent instead of being reviewed and shaped by the person submitting them. Auto-closing creates a buffer so maintainers can review the tracker on their own schedule and reopen the issues that meet the quality bar.
SproutClaw 收到的 issue 数量超过维护者能够实时处理的上限。许多报告不符合本指南的质量标准,或者是通过 Agent 随意提交而未经本人审查和整理的。自动关闭机制让维护者可以按自己的节奏审查,并重新开放符合质量要求的 issue。
### Why are weekend issues lower priority?
### 为什么周末的 issue 优先级较低?
We triage the tracker during working hours. That means more issues can accumulate over the weekend. Anything submitted Friday through Sunday may be missed or given lower priority in the Monday review queue. If a problem is urgent, ask on Discord and include the short version, a repro, and the relevant logs.
维护者在工作时间处理 issue。周末可能积累更多未处理内容周五至周日提交的 issue 可能在周一审查时被遗漏或优先级降低。如有紧急问题,请直接联系维护者并附上简要描述、复现步骤和相关日志。
### Why do some issues get no reply?
### 为什么有些 issue 没有收到回复?
A reply is maintenance work too. Low-signal issues, unclear reports, duplicates, and issues that do not follow this guide may be closed without discussion. This keeps time available for reproducible bugs, thoughtful requests, and contributors who have done the work to make their report actionable.
回复本身也是一种维护成本。低质量 issue、表述不清的报告、重复 issue 以及不遵循本指南的 issue 可能被直接关闭而不作说明。这样可以将时间留给可复现的 bug、深思熟虑的需求以及真正用心整理报告的贡献者。
### Why not let AI triage everything?
### 为什么不用 AI 来做所有分类工作?
AI can help group duplicates, summarize reports, and spot missing information. It is not trusted to make final maintainer decisions. Polished AI-generated issues can still be wrong, misleading, or expensive to investigate. Human review remains the final gate.
AI 可以帮助归纳重复内容、汇总报告、发现缺失信息,但不被信任来做最终的维护决策。经过润色的 AI 生成 issue 仍然可能是错误的、有误导性的,或需要大量时间排查。人工审查始终是最终把关环节。
### Is this hostile to contributors?
### 这对贡献者是否过于苛刻?
No. It is a guardrail against burnout and tracker spam. Short, concrete, reproducible issues are welcome. Thoughtful contributions are welcome. Automated slop, entitlement, and large volumes of low-effort reports are not.
## Where can I learn about plans?
Earendil uses RFCs to discuss larger changes. Not all of them are public, but
quite a few are. They can be found at [rfc.earendil.com](https://rfc.earendil.com/keyword/pi/).
不是。这是防止维护者精力耗尽和 issue 刷屏的保护机制。简洁、具体、可复现的 issue 是受欢迎的,有深度的贡献同样受欢迎。自动化生成的低质量内容、无理取闹以及大量低价值报告则不被接受。

154
README.md
View File

@@ -8,7 +8,7 @@
- 开发协作助手:理解本地项目结构,执行代码修改、测试验证和 Git 工作流。
- Docker 部署助手:按项目目录组织 `docker compose` 服务,关注数据持久化、端口规划和资源限制。
- 内网服务助手:适配 smallmengya、bigmengya、alycd 等内网服务器使用习惯。
- WebUI/TUI 双入口:保留控制台 TUI同时扩展网页前端,方便在浏览器里管理会话和模型
- WebUI/TUI 双入口:保留控制台 TUI同时提供独立的网页前端(`sproutclaw-web`
## 项目结构
@@ -18,158 +18,182 @@
| `packages/agent` | Agent 运行时、工具调用和状态管理 |
| `packages/ai` | 多模型、多 provider 的 LLM 接入层 |
| `packages/tui` | 终端 UI 渲染库 |
| `.pi/agent/extensions/webui` | 网页对话入口React 前端 + `backend/` HTTP 服务 + systemd 集成 |
| `.pi/agent/extensions/*/index.ts` | sproutclaw 本地扩展(中文启动页、状态行、命令安装等) |
| `.pi/agent/prompts/` | 自定义 slash prompt`cl`/`is`/`pr`/`wr` 等) |
| `.pi/agent/skills/` | 可复用 SkillsGitea、SSH、GitHub 等) |
| `pi-built.sh` | 使用已构建 `dist/cli.js` 启动 Agent |
| `sproutclaw` / `sproutclaw.bat` | 启动入口(从已构建 `dist/cli.js` 启动 |
| `build.sh` / `build.bat` | 同上,使用已构建版启动 |
| `test.sh` / `test.bat` | 从源码tsx启动用于开发调试 |
| `scripts/sync-upstream.sh` | 合并上游 pi-mono 更新 |
| `scripts/push-sproutclaw.sh` | 推送到 GitHub / Gitea |
> `.sproutclaw/agent/` 为本地持久化配置目录settings、auth、sessions、extensions、skills 等),已整体加入 `.gitignore`,不进入版本控制。与原版 pi 的 `~/.pi/agent/` 完全隔离。
## 常用命令
安装依赖并构建:
```bash
npm install --ignore-scripts
npm run build
npm run check
```
从源码运行(开发调试,无需 build
```bash
./test.sh
./pi-test.sh
```
运行本地源码版 Agent开发调试
从已构建版运行(日常更快
```bash
./pi-test.sh
./build.sh
# 或
./sproutclaw
```
运行已构建版 Agent日常更快
检查代码
```bash
npm run build
./pi-built.sh
npm run check
```
启动 WebUI 时,在 TUI 中执行:
## WebUIsproutclaw-web
```text
/webui on 19133
```
WebUI 是独立项目,位于 `../sproutclaw-web/`,通过 systemd 以服务形式运行。
关闭 WebUI
```text
/webui off
```
WebUI 前端单独构建:
构建后端
```bash
cd .pi/agent/extensions/webui/frontend && npm install && npm run build
cd ../sproutclaw-web
bash build.sh
```
构建前端:
```bash
cd ../sproutclaw-web/frontend
npm install && npm run build
```
管理服务:
```bash
systemctl start sproutclaw-web
systemctl stop sproutclaw-web
systemctl restart sproutclaw-web
systemctl status sproutclaw-web
```
## Git 分支策略
本仓库是 pi-mono 的 fork采用三层分支,同时维护**上游同步**和 **sproutclaw 定制**
本仓库是 pi-mono 的 fork同时维护**上游同步**和 **sproutclaw 定制**
| 分支 | 用途 |
| --- | --- |
| `main` | 稳定可跑版本:已合并的上游 + sproutclaw 定制,推送到 GitHub |
| `upstream-sync` | 临时同步分支:专门用来 `merge upstream/main`,解决冲突后再合回 `main` |
| `main` | 稳定可跑版本:已合并的上游 + sproutclaw 定制,推送到 GitHub / Gitea |
| `feature/*` | 日常开发WebUI、扩展、补丁等完成后 merge 进 `main` |
### Remote 配置
```text
origin → https://github.com/shumengya/sproutclaw.git (你的 GitHub
upstream → https://github.com/badlogic/pi-mono.git (上游 pi
gitea → ssh://git@git.shumengya.top:8022/shumengya/sproutclaw.git (内网备份,可选)
origin → https://github.com/shumengya/sproutclaw.git
upstream → https://github.com/badlogic/pi-mono.git
gitea → ssh://git@git.shumengya.top:8022/shumengya/sproutclaw.git
```
首次配置 upstream
```bash
git remote add upstream https://github.com/badlogic/pi-mono.git
git remote add gitea ssh://git@git.shumengya.top:8022/shumengya/sproutclaw.git
```
GitHub 网络不稳定时:
### 日常开发
```bash
export GIT_HTTP_VERSION=1.1
```
### 日常开发feature 分支)
```bash
./scripts/new-feature.sh webui-avatar-fix # 从 main 创建 feature/webui-avatar-fix
git checkout -b feature/my-fix
# ... 开发、测试 ...
git add <改动的具体文件> # 不要用 git add -A
git commit -m "fix(webui): agent avatar during tool calls"
git add <改动的具体文件>
git commit -m "fix: ..."
git checkout main
git merge feature/webui-avatar-fix
git merge feature/my-fix
./scripts/push-sproutclaw.sh
git branch -d feature/webui-avatar-fix # 合并后可删
git branch -d feature/my-fix
```
### 推送本地更新到 GitHub
### 推送到 GitHub / Gitea
```bash
git checkout main
./scripts/push-sproutclaw.sh
# 或手动
git push origin main && git push gitea main
```
### 合并上游 pi-mono 更新
```bash
./scripts/sync-upstream.sh
# 若有冲突,按下方规则解决后:
git fetch upstream
git merge upstream/main --no-commit --no-ff
# 解决冲突(见下方惯例),然后:
git add <resolved-files>
git commit
git checkout main && git merge upstream-sync
PI_ALLOW_LOCKFILE_CHANGE=1 git commit
./scripts/push-sproutclaw.sh
```
**合并冲突处理惯例:**
| 文件 | 处理方式 |
| 文件类型 | 处理方式 |
| --- | --- |
| `README.md`、sproutclaw 定制代码 | 保留本仓库版本 |
| `package-lock.json` | 采用 upstream再执行 `npm install --ignore-scripts` |
| `*.generated.ts` | 采用 upstream |
| `.pi/agent/extensions/` | 保留 sproutclaw 版本 |
| `package-lock.json` | 采用 upstream`npm install --ignore-scripts` |
| 测试/文档/新增上游功能 | 采用 upstream |
合并后必做:
```bash
npm install --ignore-scripts
npm run build
./pi-test.sh
./test.sh
```
### sproutclaw 相对 upstream 的 intentional 改动
### sproutclaw 相对 upstream 的定制改动
合并上游时,以下定制**不要误删**
合并上游时,以下内容**不要误删**
- `packages/coding-agent/package.json``piConfig.name = sproutclaw``configDir = .sproutclaw`(命名空间隔离)
- `showChangelogOnStartup` 设置项(关闭启动 What's New
- `.pi/agent/extensions/webui/` 网页 UI
- `.pi/agent/extensions/startup-chinese/` 等本地扩展目录
- `.pi/agent/extensions/webui/backend/` WebUI 后端模块化拆分
- 本 README(仅 sproutclaw 说明,不含上游文档)
- RPC 扩展:`reload` 命令、bash 流式输出(`bash_update`)、`get_extensions` 接口
- `sproutclaw` / `build.sh` / `test.sh` 等启动脚本
- `scripts/sync-upstream.sh``scripts/push-sproutclaw.sh` 等工具脚本
- 本 README 及中文版 CONTRIBUTING.md / SECURITY.md
## 本地配置
`.pi/agent/` 中以下内容**不进 Git**(含密钥/内网地址)
`.sproutclaw/agent/` 中以下内容不进 Git
- `settings.json` → 参考 `settings.example.json` 本地复制
- `models.json``auth.json`
- `sessions/`
- `models.json``auth.json``mcp.json`
- `sessions/``skills/``extensions/``prompts/`
环境变量(可选):
- `SPROUTCLAW_CODING_AGENT_DIR` — 覆盖 agent 配置目录,默认 `<repo>/.sproutclaw/agent`
首次部署:
```bash
cp .pi/agent/settings.example.json .pi/agent/settings.json
# 编辑 settings.json 填入 provider / model
cp .sproutclaw/agent/settings.example.json .sproutclaw/agent/settings.json
# 编辑 settings.json 填入 provider / model / auth 信息
```
### 与原版 pi 并存
| | 原版 pi | SproutClaw |
|---|---|---|
| 命令 | `pi` | `sproutclaw` / `mengya` |
| 全局配置 | `~/.pi/agent/` | `~/.sproutclaw/agent/` |
| 项目配置 | `.pi/` | `.sproutclaw/` |
| 环境变量 | `PI_CODING_AGENT_DIR` | `SPROUTCLAW_CODING_AGENT_DIR` |
## 说明
这个仓库是自用 fork默认围绕树萌芽的服务器环境、部署规范和日常开发习惯调整。上游能力来自 pi-mono通过 `upstream-sync` 分支定期同步sproutclaw 定制在 `feature/*` 开发后合入 `main`
这个仓库是自用 fork默认围绕树萌芽的服务器环境、部署规范和日常开发习惯调整。上游能力来自 pi-mono通过 `git fetch upstream && git merge upstream/main` 定期同步sproutclaw 定制在 `feature/*` 开发后合入 `main`

View File

@@ -1,87 +1,58 @@
# Security Policy
# 安全政策
This document should guide you about understanding the security concept behind
Pi and also where the boundaries are.
本文档旨在帮助你了解 SproutClaw 的安全设计理念及其边界范围。
In general Pi is a coding agent that runs locally within the security boundary
of the user that is running it. It's the responsibiltiy of the user to monitor
its operations or to contain it within a container, virtual machine or other
Sandbox solution.
## 安全模型概述
Pi treats the local user account and files writable by that account as inside
the same trust boundary as the Pi process itself. If an attacker can modify files
under the user's home directory, workspace, shell startup files, environment, or
Pi configuration, they can generally influence Pi or other local developer tools.
Reports that depend on such prior local write access are not security
vulnerabilities unless they demonstrate how Pi grants that write access or crosses
an operating-system privilege boundary.
SproutClaw 是一个在本地运行的 Coding Agent运行于使用者账号的安全边界之内。监控其操作或将其限制在容器、虚拟机或其他沙箱环境中是使用者自身的责任。
Pi relies on users installing trustworthy extensions and loading trustworthy
skills and only to use pi within trusted repositories. This is because files
like `AGENTS.md` or instructions in comments can be used to prompt inject the
coding agent trivially and this cannot be protected against.
SproutClaw 将本地用户账号及该账号可写的文件视为与 SproutClaw 进程处于同一信任边界内。如果攻击者能够修改用户主目录、工作区、Shell 启动文件、环境变量或 SproutClaw 配置文件,通常即可影响 SproutClaw 或其他本地开发工具的行为。依赖此类本地写入权限的报告不属于安全漏洞,除非其能证明 SproutClaw 本身授予了该写入权限,或越过了操作系统的权限边界。
## Reporting a Vulnerability
SproutClaw 依赖使用者仅安装可信扩展、加载可信技能,并仅在可信仓库中使用。这是因为 `AGENTS.md` 等文件中的内容或代码注释可被用于对 Coding Agent 进行提示词注入,且此类攻击无法从根本上防御。
If you believe you found a security vulnerability in pi or another package in
this repository, please report it privately by either:
## 报告漏洞
- Emailing `security@earendil.com`, or
- Opening a private report through GitHub Security Advisories for this repository
如果你认为在 SproutClaw 中发现了安全漏洞,请通过以下方式私下报告:
Please include:
- 直接联系项目维护者
- A description of the issue and its impact
- Steps to reproduce, proof of concept, or relevant logs
- Affected package, version, commit, or configuration
- Any known mitigations
报告时请包含:
Do not open a public issue for security-sensitive reports. We will review
reports and coordinate disclosure as appropriate.
- 问题描述及其影响
- 复现步骤、概念验证或相关日志
- 受影响的包、版本、提交哈希或配置信息
- 已知的缓解措施(如有)
## Scope
请勿为安全敏感问题创建公开 issue。
Security issues in the distributed packages, command-line tools, APIs, and
repository code are in scope as well as earendil operated infrastricture
on `pi.dev`.
## 范围内In Scope
## Out Of Scope
以下属于安全问题的受理范围:
- Local code execution or sandboxing behavior (the Pi coding agent intentionally does not have a sandbox)
- Behavior of pi extensions or skills installed by the user
- Risks from working in untrusted repositories
- Risks from installing untrusted extensions, skills, packages, or tools
- Isuses caused by non trustworthy MITM proxies
- Public internet exposure of a Pi installation
- Prompt injection attacks
- Exposed secrets that are third-party/user-controlled credentials
- Reports requiring the ability to create, modify, delete, or replace files,
directories, symlinks, environment variables, shell configuration, or other
user-controlled local state on the target machine. This includes `~/.pi`,
`~/.pi/agent/models.json`, workspace files, `AGENTS.md`, skills, extensions,
extension configuration, dotfiles, and files synchronized through NFS, roaming
profiles, or dotfile managers, unless the report shows how Pi itself grants
that access.
- Issues caused by intentionally weakened user configuration.
- Resource/DOS claims that require trusted local input/config against the pi coding agent.
- Reports about malicious model output.
- User-approved or user-initiated local actions presented as vulnerabilities.
- 分发包、命令行工具、API 及仓库代码中的安全问题
## Notes for Reporters
## 范围外Out of Scope
The most useful reports show a current, reproducible security boundary bypass
with demonstrated impact. Reports that only show expected local-agent behavior,
prompt injection, or a malicious trusted extension/skill are not security
vulnerabilities under this model.
以下内容不在安全问题受理范围之内:
For example, a report showing that malicious contents written to a trusted Pi
configuration file cause Pi to execute commands, load attacker-controlled tools,
send credentials to an attacker-controlled endpoint, or otherwise change behavior
is out of scope.
- 本地代码执行或沙箱行为SproutClaw Coding Agent 有意不提供沙箱)
- 用户自行安装的扩展或技能的行为
- 在不可信仓库中工作所带来的风险
- 安装不可信扩展、技能、包或工具所带来的风险
- 由不可信中间人代理MITM Proxy导致的问题
- 将 SproutClaw 暴露在公网上的风险
- 提示词注入攻击
- 第三方或用户自行控制的凭据泄露
- 需要在目标机器上具备创建、修改、删除或替换文件、目录、符号链接、环境变量、Shell 配置或其他用户可控本地状态的能力才能复现的报告(包括 `~/.pi``~/.pi/agent/models.json`、工作区文件、`AGENTS.md`、技能、扩展、扩展配置、dotfiles 及通过 NFS/漫游配置/dotfile 管理器同步的文件),除非报告能证明 SproutClaw 本身授予了该访问权限
- 由用户有意弱化配置导致的问题
- 针对 SproutClaw Coding Agent 使用可信本地输入/配置的资源耗尽/DoS 声明
- 关于恶意模型输出的报告
- 用户批准或用户主动发起的本地操作被当作漏洞提交的报告
When possible, include the exact affected path, package version or commit SHA,
configuration, and a proof of concept against the latest release or latest
`main`. For dependency reports, include evidence that the shipped dependency is
affected and that the issue is reachable through Pi. For exposed-secret reports,
include evidence that the credential is owned by Earendil or grants access to
Earendil-operated infrastructure or services.
## 报告者须知
最有价值的报告应展示当前可复现的安全边界绕过,并附有已证明的实际影响。仅展示预期本地 Agent 行为、提示词注入或恶意可信扩展/技能的报告,在本安全模型下不构成安全漏洞。
例如,若报告显示向可信 SproutClaw 配置文件写入恶意内容会导致 SproutClaw 执行命令、加载攻击者控制的工具、向攻击者控制的端点发送凭据或改变其他行为,此类报告属于范围之外。
提交报告时,请尽量包含以下信息:受影响的确切路径、包版本或提交 SHA、配置信息以及针对最新版本或最新 `main` 分支的概念验证。对于依赖项报告,请提供证据说明已分发的依赖项确实受影响,且该问题可通过 SproutClaw 触达。

View File

@@ -1,404 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BMI 指数计算器</title>
<style>
:root {
--bg: #f4f7fa;
--card: #ffffff;
--text: #1f2937;
--muted: #6b7280;
--primary: #3b82f6;
--primary-dark: #2563eb;
--radius: 16px;
--shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 480px;
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 32px;
}
h1 {
text-align: center;
font-size: 1.6rem;
margin-bottom: 8px;
}
.subtitle {
text-align: center;
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 28px;
}
.form-group {
margin-bottom: 18px;
}
label {
display: block;
margin-bottom: 6px;
font-size: 0.9rem;
font-weight: 500;
}
.unit-toggle {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.unit-toggle button {
flex: 1;
padding: 10px;
border: 1px solid #d1d5db;
background: #f9fafb;
border-radius: 10px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.unit-toggle button.active {
background: var(--primary);
color: #fff;
border-color: var(--primary);
}
input[type="number"] {
width: 100%;
padding: 12px 14px;
border: 1px solid #d1d5db;
border-radius: 10px;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
input[type="number"]:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.input-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.btn {
width: 100%;
padding: 14px;
margin-top: 10px;
border: none;
border-radius: 10px;
background: var(--primary);
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn:hover {
background: var(--primary-dark);
}
.result {
margin-top: 24px;
text-align: center;
display: none;
}
.result.show {
display: block;
animation: fadeIn 0.3s ease;
}
.bmi-value {
font-size: 3rem;
font-weight: 700;
line-height: 1;
}
.bmi-category {
font-size: 1.2rem;
margin-top: 8px;
font-weight: 600;
}
.bmi-tip {
margin-top: 10px;
color: var(--muted);
font-size: 0.9rem;
}
.scale {
margin-top: 22px;
display: flex;
height: 14px;
border-radius: 999px;
overflow: hidden;
position: relative;
}
.scale span {
flex: 1;
}
.scale .under { background: #60a5fa; }
.scale .normal { background: #22c55e; }
.scale .over { background: #f59e0b; }
.scale .obese { background: #ef4444; }
.scale-marker {
position: absolute;
top: -4px;
width: 4px;
height: 22px;
background: #1f2937;
border-radius: 2px;
transform: translateX(-50%);
transition: left 0.4s ease;
}
.scale-labels {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-size: 0.75rem;
color: var(--muted);
}
.info-cards {
margin-top: 28px;
display: grid;
gap: 10px;
}
.info-card {
display: flex;
align-items: center;
padding: 12px 14px;
border-radius: 10px;
background: #f9fafb;
font-size: 0.9rem;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
}
.dot.under { background: #60a5fa; }
.dot.normal { background: #22c55e; }
.dot.over { background: #f59e0b; }
.dot.obese { background: #ef4444; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 480px) {
body {
padding: 0;
}
.container {
border-radius: 0;
min-height: 100vh;
max-width: 100%;
padding: 24px;
display: flex;
flex-direction: column;
justify-content: center;
}
h1 {
font-size: 1.4rem;
}
.bmi-value {
font-size: 2.6rem;
}
.input-row {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>BMI 指数计算器</h1>
<p class="subtitle">输入身高体重,快速了解你的身体质量指数</p>
<div class="unit-toggle">
<button id="unitMetric" class="active" onclick="setUnit('metric')">公制 (cm / kg)</button>
<button id="unitImperial" onclick="setUnit('imperial')">英制 (ft+in / lb)</button>
</div>
<form id="bmiForm" onsubmit="event.preventDefault(); calculateBMI();">
<div id="metricInputs">
<div class="form-group">
<label for="heightCm">身高(厘米)</label>
<input type="number" id="heightCm" placeholder="例如175" min="1" step="0.1" />
</div>
<div class="form-group">
<label for="weightKg">体重(公斤)</label>
<input type="number" id="weightKg" placeholder="例如65" min="1" step="0.1" />
</div>
</div>
<div id="imperialInputs" style="display: none;">
<div class="input-row">
<div class="form-group">
<label for="heightFt">身高(英尺)</label>
<input type="number" id="heightFt" placeholder="ft" min="1" step="1" />
</div>
<div class="form-group">
<label for="heightIn">身高(英寸)</label>
<input type="number" id="heightIn" placeholder="in" min="0" step="0.1" />
</div>
</div>
<div class="form-group">
<label for="weightLb">体重(磅)</label>
<input type="number" id="weightLb" placeholder="例如150" min="1" step="0.1" />
</div>
</div>
<button type="submit" class="btn">计算 BMI</button>
</form>
<div id="result" class="result">
<div class="bmi-value" id="bmiValue">--</div>
<div class="bmi-category" id="bmiCategory">--</div>
<div class="bmi-tip" id="bmiTip">--</div>
<div class="scale">
<span class="under"></span>
<span class="normal"></span>
<span class="over"></span>
<span class="obese"></span>
<div class="scale-marker" id="scaleMarker" style="left: 0%;"></div>
</div>
<div class="scale-labels">
<span>偏瘦</span>
<span>正常</span>
<span>偏胖</span>
<span>肥胖</span>
</div>
</div>
<div class="info-cards">
<div class="info-card"><span class="dot under"></span>偏瘦BMI &lt; 18.5</div>
<div class="info-card"><span class="dot normal"></span>正常18.5 ≤ BMI &lt; 24</div>
<div class="info-card"><span class="dot over"></span>偏胖24 ≤ BMI &lt; 28</div>
<div class="info-card"><span class="dot obese"></span>肥胖BMI ≥ 28</div>
</div>
</div>
<script>
let currentUnit = 'metric';
function setUnit(unit) {
currentUnit = unit;
document.getElementById('unitMetric').classList.toggle('active', unit === 'metric');
document.getElementById('unitImperial').classList.toggle('active', unit === 'imperial');
document.getElementById('metricInputs').style.display = unit === 'metric' ? 'block' : 'none';
document.getElementById('imperialInputs').style.display = unit === 'imperial' ? 'block' : 'none';
document.getElementById('result').classList.remove('show');
}
function calculateBMI() {
let bmi = 0;
if (currentUnit === 'metric') {
const heightCm = parseFloat(document.getElementById('heightCm').value);
const weightKg = parseFloat(document.getElementById('weightKg').value);
if (!heightCm || !weightKg || heightCm <= 0 || weightKg <= 0) {
alert('请输入有效的身高和体重');
return;
}
const heightM = heightCm / 100;
bmi = weightKg / (heightM * heightM);
} else {
const heightFt = parseFloat(document.getElementById('heightFt').value) || 0;
const heightIn = parseFloat(document.getElementById('heightIn').value) || 0;
const weightLb = parseFloat(document.getElementById('weightLb').value);
const totalInches = heightFt * 12 + heightIn;
if (!totalInches || !weightLb || totalInches <= 0 || weightLb <= 0) {
alert('请输入有效的身高和体重');
return;
}
bmi = 703 * weightLb / (totalInches * totalInches);
}
bmi = Math.round(bmi * 10) / 10;
let category = '';
let tip = '';
let color = '';
if (bmi < 18.5) {
category = '偏瘦';
tip = '建议适当增加营养摄入,配合力量训练增肌。';
color = '#60a5fa';
} else if (bmi < 24) {
category = '正常';
tip = 'BMI 在正常范围内,继续保持健康的生活方式!';
color = '#22c55e';
} else if (bmi < 28) {
category = '偏胖';
tip = '建议适当控制饮食,增加有氧运动。';
color = '#f59e0b';
} else {
category = '肥胖';
tip = '建议咨询医生或营养师,制定科学的减重计划。';
color = '#ef4444';
}
document.getElementById('bmiValue').textContent = bmi;
document.getElementById('bmiValue').style.color = color;
document.getElementById('bmiCategory').textContent = category;
document.getElementById('bmiCategory').style.color = color;
document.getElementById('bmiTip').textContent = tip;
// 刻度位置:把 BMI 映射到 15~35 的区间
let percent = ((bmi - 15) / (35 - 15)) * 100;
percent = Math.max(0, Math.min(100, percent));
document.getElementById('scaleMarker').style.left = percent + '%';
document.getElementById('result').classList.add('show');
}
</script>
</body>
</html>

View File

@@ -6,8 +6,8 @@ set "SCRIPT_DIR=%~dp0"
if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
set "PI_CLI_DIST=%SCRIPT_DIR%\packages\coding-agent\dist\cli.js"
if not defined PI_CODING_AGENT_DIR (
set "PI_CODING_AGENT_DIR=%SCRIPT_DIR%\.pi\agent"
if not defined SPROUTCLAW_CODING_AGENT_DIR (
set "SPROUTCLAW_CODING_AGENT_DIR=%SCRIPT_DIR%\.sproutclaw\agent"
)
set "NO_ENV=false"

View File

@@ -4,7 +4,10 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PI_CLI_DIST="$SCRIPT_DIR/packages/coding-agent/dist/cli.js"
export PI_CODING_AGENT_DIR="${PI_CODING_AGENT_DIR:-$SCRIPT_DIR/.pi/agent}"
export SPROUTCLAW_CODING_AGENT_DIR="${SPROUTCLAW_CODING_AGENT_DIR:-$SCRIPT_DIR/.sproutclaw/agent}"
# Align pi-mcp-adapter (and other third-party packages that only read PI_CODING_AGENT_DIR)
# with SproutClaw's agent dir, so MCP configs in .sproutclaw/agent/mcp.json are loaded.
export PI_CODING_AGENT_DIR="$SPROUTCLAW_CODING_AGENT_DIR"
NO_ENV=false
ARGS=()

View File

@@ -5155,6 +5155,24 @@ export const MODELS = {
} satisfies Model<"openai-completions">,
},
"huggingface": {
"MiniMaxAI/MiniMax-M2": {
id: "MiniMaxAI/MiniMax-M2",
name: "MiniMax-M2",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.3,
output: 1.2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 204800,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"MiniMaxAI/MiniMax-M2.1": {
id: "MiniMaxAI/MiniMax-M2.1",
name: "MiniMax-M2.1",
@@ -5209,6 +5227,42 @@ export const MODELS = {
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"MiniMaxAI/MiniMax-M3": {
id: "MiniMaxAI/MiniMax-M3",
name: "MiniMax-M3",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.3,
output: 1.2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 524288,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3-235B-A22B": {
id: "Qwen/Qwen3-235B-A22B",
name: "Qwen3 235B-A22B",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.2,
output: 0.8,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 40960,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3-235B-A22B-Thinking-2507": {
id: "Qwen/Qwen3-235B-A22B-Thinking-2507",
name: "Qwen3-235B-A22B-Thinking-2507",
@@ -5227,6 +5281,42 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3-32B": {
id: "Qwen/Qwen3-32B",
name: "Qwen3 32B",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.29,
output: 0.59,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3-Coder-30B-A3B-Instruct": {
id: "Qwen/Qwen3-Coder-30B-A3B-Instruct",
name: "Qwen3-Coder 30B-A3B Instruct",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: false,
input: ["text"],
cost: {
input: 0.07,
output: 0.26,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3-Coder-480B-A35B-Instruct": {
id: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
name: "Qwen3-Coder-480B-A35B-Instruct",
@@ -5299,6 +5389,60 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3.5-122B-A10B": {
id: "Qwen/Qwen3.5-122B-A10B",
name: "Qwen3.5 122B-A10B",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.4,
output: 3.2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3.5-27B": {
id: "Qwen/Qwen3.5-27B",
name: "Qwen3.5 27B",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.3,
output: 2.4,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3.5-35B-A3B": {
id: "Qwen/Qwen3.5-35B-A3B",
name: "Qwen3.5 35B-A3B",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.25,
output: 2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3.5-397B-A17B": {
id: "Qwen/Qwen3.5-397B-A17B",
name: "Qwen3.5-397B-A17B",
@@ -5317,6 +5461,42 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3.5-9B": {
id: "Qwen/Qwen3.5-9B",
name: "Qwen3.5 9B",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.17,
output: 0.25,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"Qwen/Qwen3.6-35B-A3B": {
id: "Qwen/Qwen3.6-35B-A3B",
name: "Qwen3.6 35B-A3B",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.15,
output: 0.95,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"XiaomiMiMo/MiMo-V2-Flash": {
id: "XiaomiMiMo/MiMo-V2-Flash",
name: "MiMo-V2-Flash",
@@ -5335,6 +5515,24 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"deepseek-ai/DeepSeek-R1": {
id: "deepseek-ai/DeepSeek-R1",
name: "DeepSeek-R1",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.7,
output: 2.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 64000,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"deepseek-ai/DeepSeek-R1-0528": {
id: "deepseek-ai/DeepSeek-R1-0528",
name: "DeepSeek-R1-0528",
@@ -5371,6 +5569,24 @@ export const MODELS = {
contextWindow: 163840,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"deepseek-ai/DeepSeek-V4-Flash": {
id: "deepseek-ai/DeepSeek-V4-Flash",
name: "DeepSeek V4 Flash",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.14,
output: 0.28,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 384000,
} satisfies Model<"openai-completions">,
"deepseek-ai/DeepSeek-V4-Pro": {
id: "deepseek-ai/DeepSeek-V4-Pro",
name: "DeepSeek V4 Pro",
@@ -5389,6 +5605,60 @@ export const MODELS = {
contextWindow: 1048576,
maxTokens: 393216,
} satisfies Model<"openai-completions">,
"google/gemma-4-26B-A4B-it": {
id: "google/gemma-4-26B-A4B-it",
name: "Gemma 4 26B A4B IT",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.13,
output: 0.4,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"google/gemma-4-31B-it": {
id: "google/gemma-4-31B-it",
name: "Gemma 4 31B IT",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.14,
output: 0.4,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"meta-llama/Llama-3.3-70B-Instruct": {
id: "meta-llama/Llama-3.3-70B-Instruct",
name: "Llama-3.3-70B-Instruct",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: false,
input: ["text"],
cost: {
input: 0.59,
output: 0.79,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"moonshotai/Kimi-K2-Instruct": {
id: "moonshotai/Kimi-K2-Instruct",
name: "Kimi-K2-Instruct",
@@ -5479,6 +5749,114 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"moonshotai/Kimi-K2.7-Code": {
id: "moonshotai/Kimi-K2.7-Code",
name: "Kimi K2.7 Code",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.95,
output: 4,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"stepfun-ai/Step-3.5-Flash": {
id: "stepfun-ai/Step-3.5-Flash",
name: "Step 3.5 Flash",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.1,
output: 0.3,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 256000,
} satisfies Model<"openai-completions">,
"zai-org/GLM-4.5": {
id: "zai-org/GLM-4.5",
name: "GLM-4.5",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.6,
output: 2.2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 98304,
} satisfies Model<"openai-completions">,
"zai-org/GLM-4.5-Air": {
id: "zai-org/GLM-4.5-Air",
name: "GLM-4.5-Air",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.13,
output: 0.85,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 98304,
} satisfies Model<"openai-completions">,
"zai-org/GLM-4.5V": {
id: "zai-org/GLM-4.5V",
name: "GLM-4.5V",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.6,
output: 1.8,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 65536,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"zai-org/GLM-4.6": {
id: "zai-org/GLM-4.6",
name: "GLM-4.6",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 0.55,
output: 2.2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"zai-org/GLM-4.7": {
id: "zai-org/GLM-4.7",
name: "GLM-4.7",
@@ -5551,6 +5929,24 @@ export const MODELS = {
contextWindow: 202752,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"zai-org/GLM-5.2": {
id: "zai-org/GLM-5.2",
name: "GLM-5.2",
api: "openai-completions",
provider: "huggingface",
baseUrl: "https://router.huggingface.co/v1",
compat: {"supportsDeveloperRole":false},
reasoning: true,
input: ["text"],
cost: {
input: 1.4,
output: 4.4,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
},
"kimi-coding": {
"k2p7": {
@@ -8061,6 +8457,24 @@ export const MODELS = {
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"glm-5.2": {
id: "glm-5.2",
name: "GLM-5.2",
api: "openai-completions",
provider: "opencode",
baseUrl: "https://opencode.ai/zen/v1",
compat: {"maxTokensField":"max_tokens"},
reasoning: true,
input: ["text"],
cost: {
input: 1.4,
output: 4.4,
cacheRead: 0.26,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"gpt-5": {
id: "gpt-5",
name: "GPT-5",
@@ -8885,23 +9299,6 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"anthropic/claude-3.5-haiku": {
id: "anthropic/claude-3.5-haiku",
name: "Anthropic: Claude 3.5 Haiku",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.8,
output: 4,
cacheRead: 0.08,
cacheWrite: 1,
},
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
"anthropic/claude-fable-5": {
id: "anthropic/claude-fable-5",
name: "Anthropic: Claude Fable 5",
@@ -9507,23 +9904,6 @@ export const MODELS = {
contextWindow: 1048576,
maxTokens: 384000,
} satisfies Model<"openai-completions">,
"essentialai/rnj-1-instruct": {
id: "essentialai/rnj-1-instruct",
name: "EssentialAI: Rnj 1 Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.15,
output: 0.15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"google/gemini-2.5-flash": {
id: "google/gemini-2.5-flash",
name: "Google: Gemini 2.5 Flash",
@@ -9657,7 +10037,7 @@ export const MODELS = {
cacheRead: 0.2,
cacheWrite: 0.375,
},
contextWindow: 131072,
contextWindow: 65536,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"google/gemini-3.1-flash-lite": {
@@ -9845,7 +10225,7 @@ export const MODELS = {
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 32768,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
"ibm-granite/granite-4.1-8b": {
id: "ibm-granite/granite-4.1-8b",
@@ -9947,8 +10327,8 @@ export const MODELS = {
cacheRead: 0.06,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 144000,
contextWindow: 256000,
maxTokens: 80000,
} satisfies Model<"openai-completions">,
"liquid/lfm-2.5-1.2b-thinking:free": {
id: "liquid/lfm-2.5-1.2b-thinking:free",
@@ -10146,13 +10526,13 @@ export const MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.25,
output: 1,
cacheRead: 0.05,
input: 0.24,
output: 0.96,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 204800,
maxTokens: 131072,
maxTokens: 196608,
} satisfies Model<"openai-completions">,
"minimax/minimax-m3": {
id: "minimax/minimax-m3",
@@ -10169,7 +10549,7 @@ export const MODELS = {
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 4096,
maxTokens: 512000,
} satisfies Model<"openai-completions">,
"mistralai/codestral-2508": {
id: "mistralai/codestral-2508",
@@ -10505,7 +10885,7 @@ export const MODELS = {
cost: {
input: 0.6,
output: 2.5,
cacheRead: 0.6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
@@ -10539,12 +10919,12 @@ export const MODELS = {
input: ["text", "image"],
cost: {
input: 0.66,
output: 3.5,
cacheRead: 0.33,
output: 3.41,
cacheRead: 0.144,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262142,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"moonshotai/kimi-k2.7-code": {
id: "moonshotai/kimi-k2.7-code",
@@ -10555,30 +10935,13 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.612,
output: 3.069,
cacheRead: 0.1296,
input: 0.74,
output: 3.5,
cacheRead: 0.15,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"nex-agi/nex-n2-pro:free": {
id: "nex-agi/nex-n2-pro:free",
name: "Nex AGI: Nex-N2-Pro (free)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"nvidia/llama-3.3-nemotron-super-49b-v1.5": {
id: "nvidia/llama-3.3-nemotron-super-49b-v1.5",
@@ -10884,7 +11247,7 @@ export const MODELS = {
cacheWrite: 0,
},
contextWindow: 1047576,
maxTokens: 4096,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"openai/gpt-4.1-nano": {
id: "openai/gpt-4.1-nano",
@@ -10914,7 +11277,7 @@ export const MODELS = {
cost: {
input: 2.5,
output: 10,
cacheRead: 1.25,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
@@ -11050,11 +11413,11 @@ export const MODELS = {
cost: {
input: 0.25,
output: 2,
cacheRead: 0.03,
cacheRead: 0.025,
cacheWrite: 0,
},
contextWindow: 400000,
maxTokens: 4096,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"openai/gpt-5-nano": {
id: "openai/gpt-5-nano",
@@ -11118,11 +11481,11 @@ export const MODELS = {
cost: {
input: 1.25,
output: 10,
cacheRead: 0.125,
cacheRead: 0.13,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 16384,
maxTokens: 32000,
} satisfies Model<"openai-completions">,
"openai/gpt-5.1-codex": {
id: "openai/gpt-5.1-codex",
@@ -11209,7 +11572,7 @@ export const MODELS = {
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 32000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"openai/gpt-5.2-codex": {
id: "openai/gpt-5.2-codex",
@@ -11816,23 +12179,6 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"prime-intellect/intellect-3": {
id: "prime-intellect/intellect-3",
name: "Prime Intellect: INTELLECT-3",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.2,
output: 1.1,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"qwen/qwen-2.5-72b-instruct": {
id: "qwen/qwen-2.5-72b-instruct",
name: "Qwen2.5 72B Instruct",
@@ -12422,11 +12768,11 @@ export const MODELS = {
cost: {
input: 0.14,
output: 1,
cacheRead: 0.05,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 81920,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3.5-397b-a17b": {
id: "qwen/qwen3.5-397b-a17b",
@@ -12459,8 +12805,8 @@ export const MODELS = {
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 32768,
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3.5-flash-02-23": {
id: "qwen/qwen3.5-flash-02-23",
@@ -12734,23 +13080,6 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"thedrummer/rocinante-12b": {
id: "thedrummer/rocinante-12b",
name: "TheDrummer: Rocinante 12B",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.17,
output: 0.43,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"thedrummer/unslopnemo-12b": {
id: "thedrummer/unslopnemo-12b",
name: "TheDrummer: UnslopNemo 12B",
@@ -12845,13 +13174,13 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.14,
input: 0.105,
output: 0.28,
cacheRead: 0.0028,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"xiaomi/mimo-v2.5-pro": {
id: "xiaomi/mimo-v2.5-pro",
@@ -13034,11 +13363,11 @@ export const MODELS = {
cost: {
input: 0.98,
output: 3.08,
cacheRead: 0.182,
cacheRead: 0.49,
cacheWrite: 0,
},
contextWindow: 202752,
maxTokens: 4096,
maxTokens: 65535,
} satisfies Model<"openai-completions">,
"z-ai/glm-5.2": {
id: "z-ai/glm-5.2",
@@ -13050,12 +13379,29 @@ export const MODELS = {
thinkingLevelMap: {"xhigh":"xhigh"},
input: ["text"],
cost: {
input: 1.2,
output: 4.1,
cacheRead: 0.2,
input: 0.95,
output: 3,
cacheRead: 0.18,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"z-ai/glm-5v-turbo": {
id: "z-ai/glm-5v-turbo",
name: "Z.ai: GLM 5V Turbo",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1.2,
output: 4,
cacheRead: 0.24,
cacheWrite: 0,
},
contextWindow: 202752,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"~anthropic/claude-fable-latest": {
@@ -13170,12 +13516,12 @@ export const MODELS = {
input: ["text", "image"],
cost: {
input: 0.66,
output: 3.5,
cacheRead: 0.33,
output: 3.41,
cacheRead: 0.144,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262142,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"~openai/gpt-latest": {
id: "~openai/gpt-latest",
@@ -15936,6 +16282,23 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 8000,
} satisfies Model<"anthropic-messages">,
"sakana/fugu-ultra": {
id: "sakana/fugu-ultra",
name: "Fugu Ultra",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
reasoning: true,
input: ["text", "image"],
cost: {
input: 5,
output: 30,
cacheRead: 0.5,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 1000000,
} satisfies Model<"anthropic-messages">,
"stepfun/step-3.5-flash": {
id: "stepfun/step-3.5-flash",
name: "StepFun 3.5 Flash",
@@ -16429,6 +16792,23 @@ export const MODELS = {
contextWindow: 1000000,
maxTokens: 128000,
} satisfies Model<"anthropic-messages">,
"zai/glm-5.2-fast": {
id: "zai/glm-5.2-fast",
name: "GLM 5.2 Fast",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
reasoning: true,
input: ["text"],
cost: {
input: 3,
output: 10.25,
cacheRead: 0.5,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 128000,
} satisfies Model<"anthropic-messages">,
"zai/glm-5v-turbo": {
id: "zai/glm-5v-turbo",
name: "GLM 5V Turbo",

View File

@@ -14,7 +14,7 @@ npm run build
Run from source:
```bash
/path/to/pi-mono/pi-test.sh
/path/to/sproutclaw/test.sh
```
The script can be run from any directory. Pi keeps the caller's current working directory.

View File

@@ -4,7 +4,8 @@
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module",
"piConfig": {
"configDir": ".pi"
"name": "sproutclaw",
"configDir": ".sproutclaw"
},
"bin": {
"pi": "dist/cli.js"

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default to the repository-local pi config so this source checkout uses the
# copied learning config instead of ~/.pi.
export PI_CODING_AGENT_DIR="${PI_CODING_AGENT_DIR:-$SCRIPT_DIR/.pi/agent}"
# Check for --no-env flag
NO_ENV=false
ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--no-env" ]]; then
NO_ENV=true
else
ARGS+=("$arg")
fi
done
if [[ "$NO_ENV" == "true" ]]; then
# Unset API keys (see packages/ai/src/env-api-keys.ts)
unset ANTHROPIC_API_KEY
unset ANTHROPIC_OAUTH_TOKEN
unset OPENAI_API_KEY
unset GEMINI_API_KEY
unset GROQ_API_KEY
unset CEREBRAS_API_KEY
unset XAI_API_KEY
unset OPENROUTER_API_KEY
unset ZAI_API_KEY
unset MISTRAL_API_KEY
unset MINIMAX_API_KEY
unset MINIMAX_CN_API_KEY
unset AI_GATEWAY_API_KEY
unset OPENCODE_API_KEY
unset COPILOT_GITHUB_TOKEN
unset GH_TOKEN
unset GITHUB_TOKEN
unset HF_TOKEN
unset GOOGLE_APPLICATION_CREDENTIALS
unset GOOGLE_CLOUD_PROJECT
unset GCLOUD_PROJECT
unset GOOGLE_CLOUD_LOCATION
unset AWS_PROFILE
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN
unset AWS_REGION
unset AWS_DEFAULT_REGION
unset AWS_BEARER_TOKEN_BEDROCK
unset AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
unset AWS_CONTAINER_CREDENTIALS_FULL_URI
unset AWS_WEB_IDENTITY_TOKEN_FILE
unset AZURE_OPENAI_API_KEY
unset AZURE_OPENAI_BASE_URL
unset AZURE_OPENAI_RESOURCE_NAME
echo "Running without API keys..."
fi
"$SCRIPT_DIR/node_modules/.bin/tsx" --tsconfig "$SCRIPT_DIR/tsconfig.json" "$SCRIPT_DIR/packages/coding-agent/src/cli.ts" ${ARGS[@]+"${ARGS[@]}"}

View File

@@ -4,7 +4,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PI_CLI_DIST="$SCRIPT_DIR/packages/coding-agent/dist/cli.js"
export PI_CODING_AGENT_DIR="${PI_CODING_AGENT_DIR:-$SCRIPT_DIR/.pi/agent}"
export SPROUTCLAW_CODING_AGENT_DIR="${SPROUTCLAW_CODING_AGENT_DIR:-$SCRIPT_DIR/.sproutclaw/agent}"
if [[ ! -f "$PI_CLI_DIST" ]]; then
echo "Build output not found: $PI_CLI_DIST" >&2

View File

@@ -2,8 +2,8 @@
setlocal
set "SCRIPT_DIR=%~dp0"
if "%SCRIPT_DIR:~-1%"=="" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
if not defined PI_CODING_AGENT_DIR (
set "PI_CODING_AGENT_DIR=%SCRIPT_DIR%.piagent"
if not defined SPROUTCLAW_CODING_AGENT_DIR (
set "SPROUTCLAW_CODING_AGENT_DIR=%SCRIPT_DIR%\.sproutclaw\agent"
)
if "%~1"=="" goto run
@@ -21,7 +21,7 @@ if "%~1"=="-h" goto help
if "%~1"=="--help" goto help
:run
call "%SCRIPT_DIR%pi-built.bat" %*
call "%SCRIPT_DIR%build.bat" %*
exit /b %ERRORLEVEL%
:help

View File

@@ -6,9 +6,9 @@ set "POWERSHELL_EXE=powershell.exe"
where %POWERSHELL_EXE% >nul 2>nul
if errorlevel 1 (
>&2 echo powershell.exe not found. Install PowerShell or run pi-test.ps1 directly.
>&2 echo powershell.exe not found. Install PowerShell or run test.ps1 directly.
exit /b 1
)
%POWERSHELL_EXE% -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%pi-test.ps1" %*
%POWERSHELL_EXE% -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%test.ps1" %*
exit /b %ERRORLEVEL%

View File

@@ -1,6 +1,9 @@
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
if (-not $env:SPROUTCLAW_CODING_AGENT_DIR) {
$env:SPROUTCLAW_CODING_AGENT_DIR = Join-Path $scriptDir ".sproutclaw\agent"
}
$noEnv = $false
$forwardArgs = New-Object System.Collections.Generic.List[string]

125
test.sh
View File

@@ -1,76 +1,61 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
AUTH_FILE="$HOME/.pi/agent/auth.json"
AUTH_BACKUP="$HOME/.pi/agent/auth.json.bak"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Restore auth.json on exit (success or failure)
cleanup() {
if [[ -f "$AUTH_BACKUP" ]]; then
mv "$AUTH_BACKUP" "$AUTH_FILE"
echo "Restored auth.json"
fi
}
trap cleanup EXIT
# Default to the repository-local pi config so this source checkout uses the
# copied learning config instead of ~/.pi.
export SPROUTCLAW_CODING_AGENT_DIR="${SPROUTCLAW_CODING_AGENT_DIR:-$SCRIPT_DIR/.sproutclaw/agent}"
# Move auth.json out of the way
if [[ -f "$AUTH_FILE" ]]; then
mv "$AUTH_FILE" "$AUTH_BACKUP"
echo "Moved auth.json to backup"
# Check for --no-env flag
NO_ENV=false
ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--no-env" ]]; then
NO_ENV=true
else
ARGS+=("$arg")
fi
done
if [[ "$NO_ENV" == "true" ]]; then
# Unset API keys (see packages/ai/src/env-api-keys.ts)
unset ANTHROPIC_API_KEY
unset ANTHROPIC_OAUTH_TOKEN
unset OPENAI_API_KEY
unset GEMINI_API_KEY
unset GROQ_API_KEY
unset CEREBRAS_API_KEY
unset XAI_API_KEY
unset OPENROUTER_API_KEY
unset ZAI_API_KEY
unset MISTRAL_API_KEY
unset MINIMAX_API_KEY
unset MINIMAX_CN_API_KEY
unset AI_GATEWAY_API_KEY
unset OPENCODE_API_KEY
unset COPILOT_GITHUB_TOKEN
unset GH_TOKEN
unset GITHUB_TOKEN
unset HF_TOKEN
unset GOOGLE_APPLICATION_CREDENTIALS
unset GOOGLE_CLOUD_PROJECT
unset GCLOUD_PROJECT
unset GOOGLE_CLOUD_LOCATION
unset AWS_PROFILE
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN
unset AWS_REGION
unset AWS_DEFAULT_REGION
unset AWS_BEARER_TOKEN_BEDROCK
unset AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
unset AWS_CONTAINER_CREDENTIALS_FULL_URI
unset AWS_WEB_IDENTITY_TOKEN_FILE
unset AZURE_OPENAI_API_KEY
unset AZURE_OPENAI_BASE_URL
unset AZURE_OPENAI_RESOURCE_NAME
echo "Running without API keys..."
fi
# Skip local LLM tests (ollama, lmstudio)
export PI_NO_LOCAL_LLM=1
# Unset API keys (see packages/ai/src/stream.ts getEnvApiKey)
unset ANTHROPIC_API_KEY
unset ANTHROPIC_OAUTH_TOKEN
unset OPENAI_API_KEY
unset AZURE_OPENAI_API_KEY
unset DEEPSEEK_API_KEY
unset GEMINI_API_KEY
unset GOOGLE_CLOUD_API_KEY
unset GROQ_API_KEY
unset CEREBRAS_API_KEY
unset XAI_API_KEY
unset OPENROUTER_API_KEY
unset ZAI_API_KEY
unset ZAI_CODING_CN_API_KEY
unset MISTRAL_API_KEY
unset MINIMAX_API_KEY
unset MINIMAX_CN_API_KEY
unset MOONSHOT_API_KEY
unset KIMI_API_KEY
unset HF_TOKEN
unset FIREWORKS_API_KEY
unset TOGETHER_API_KEY
unset AI_GATEWAY_API_KEY
unset OPENCODE_API_KEY
unset CLOUDFLARE_API_KEY
unset CLOUDFLARE_ACCOUNT_ID
unset CLOUDFLARE_GATEWAY_ID
unset XIAOMI_API_KEY
unset XIAOMI_TOKEN_PLAN_CN_API_KEY
unset XIAOMI_TOKEN_PLAN_AMS_API_KEY
unset XIAOMI_TOKEN_PLAN_SGP_API_KEY
unset COPILOT_GITHUB_TOKEN
unset GH_TOKEN
unset GITHUB_TOKEN
unset GOOGLE_APPLICATION_CREDENTIALS
unset GOOGLE_CLOUD_PROJECT
unset GCLOUD_PROJECT
unset GOOGLE_CLOUD_LOCATION
unset AWS_PROFILE
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN
unset AWS_REGION
unset AWS_DEFAULT_REGION
unset AWS_BEARER_TOKEN_BEDROCK
unset AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
unset AWS_CONTAINER_CREDENTIALS_FULL_URI
unset AWS_WEB_IDENTITY_TOKEN_FILE
unset BEDROCK_EXTENSIVE_MODEL_TEST
echo "Running tests without API keys..."
npm test
"$SCRIPT_DIR/node_modules/.bin/tsx" --tsconfig "$SCRIPT_DIR/tsconfig.json" "$SCRIPT_DIR/packages/coding-agent/src/cli.ts" ${ARGS[@]+"${ARGS[@]}"}