pisper 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kongran Ling and Pisper contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,216 @@
1
+ <p align="right"><a href="./README.md">简体中文</a> · <strong>English</strong></p>
2
+
3
+ <a id="top"></a>
4
+
5
+ <p align="center">
6
+ <img src="docs/brand/pisper-logo.svg" width="112" alt="Pisper project logo" />
7
+ </p>
8
+
9
+ <h1 align="center">Pisper</h1>
10
+
11
+ <p align="center"><strong>A multi-Agent workspace powered by Pi</strong></p>
12
+
13
+ <p align="center">
14
+ <img src="https://img.shields.io/badge/Node.js-17141F?style=flat-square&logo=nodedotjs&logoColor=F59E0B" alt="Node.js" />
15
+ <img src="https://img.shields.io/badge/TypeScript-17141F?style=flat-square&logo=typescript&logoColor=F59E0B" alt="TypeScript" />
16
+ <img src="https://img.shields.io/badge/React-17141F?style=flat-square&logo=react&logoColor=F59E0B" alt="React" />
17
+ <img src="https://img.shields.io/badge/Tauri-17141F?style=flat-square&logo=tauri&logoColor=F59E0B" alt="Tauri" />
18
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-F59E0B?style=flat-square" alt="MIT License" /></a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="https://github.com/ling-kong-ran/pisper/releases/latest">
23
+ <img src="https://img.shields.io/badge/Download%20Desktop-Windows%20%7C%20macOS%20%7C%20Linux-F59E0B?style=for-the-badge&logo=github&logoColor=17141F" alt="Download Pisper desktop" />
24
+ </a>
25
+ </p>
26
+
27
+ <a id="sponsors"></a>
28
+
29
+ ## ❤️ Sponsors
30
+
31
+ Thanks to the following partners for supporting the Pisper community. If you'd like to appear here too, feel free to reach out via [Issues](https://github.com/ling-kong-ran/pisper/issues).
32
+
33
+ <details open>
34
+ <summary>View sponsors</summary>
35
+
36
+ <table>
37
+ <tr>
38
+ <td width="180" align="center">
39
+ <a href="https://matrix.000328.xyz/sign-up?aff=ZPeH"><strong>Matrix</strong></a>
40
+ </td>
41
+ <td>
42
+ Thanks to <a href="https://matrix.000328.xyz/sign-up?aff=ZPeH">Matrix</a> for supporting the Pisper community. Registering through <a href="https://matrix.000328.xyz/sign-up?aff=ZPeH">this link</a> may generate referral revenue for the project.
43
+ </td>
44
+ </tr>
45
+ </table>
46
+
47
+ > Sponsor links contain a referral parameter. Sponsor content is not targeted using conversations, workspaces, providers, models, or API configuration, and Pisper does not send that data to sponsors. The public configuration for the in-app sponsor placement is maintained in [`docs/sponsors.json`](./docs/sponsors.json).
48
+
49
+ </details>
50
+
51
+ <p align="center">
52
+ <a href="#overview">Overview</a> ·
53
+ <a href="https://ling-kong-ran.github.io/pisper/">Project Site</a> ·
54
+ <a href="#features">Features</a> ·
55
+ <a href="#data-safety">Data Safety</a> ·
56
+ <a href="#desktop-pet">Desktop Pet</a> ·
57
+ <a href="#install">Install</a> ·
58
+ <a href="#tui">Terminal Client</a> ·
59
+ <a href="#development">Development</a> ·
60
+ <a href="#sponsors">Sponsors</a> ·
61
+ <a href="#license">License</a>
62
+ </p>
63
+
64
+ ---
65
+
66
+ <a id="overview"></a>
67
+
68
+ ## Overview
69
+
70
+ Pisper is a desktop and terminal client built on [Pi Coding Agent](https://github.com/earendil-works/pi/tree/main/packages/coding-agent), with multiple sessions, Tools, Skills, MCP, automation, and per-session permissions.
71
+
72
+ See the [Pisper project site](https://ling-kong-ran.github.io/pisper/) for the product overview and interface preview.
73
+
74
+ ### Get Started in Three Steps
75
+
76
+ 1. [Download the desktop app](https://github.com/ling-kong-ran/pisper/releases/latest) (Windows / macOS / Linux, no Node.js required)
77
+ 2. Configure any model provider with an API key
78
+ 3. Create a session and start working in parallel
79
+
80
+ <a id="features"></a>
81
+
82
+ ## Features
83
+
84
+ - **Multiple sessions:** independent models, context, workspaces, and permissions with Dock splits and layout restoration.
85
+ - **Tools, Skills, and MCP:** manage capabilities and call permissions in one place.
86
+ - **Subagents:** run temporary tasks in isolated contexts and return results to the parent session.
87
+ - **Memory and multimodal input:** retrieve project memory and process images, documents, and code.
88
+ - **Automation and channels:** schedules, visual workflows, Feishu, and personal Weixin.
89
+ - **Desktop and terminal:** Tauri desktop app and Ratatui TUI.
90
+ - **Permission controls:** `Read only / Full access` execution modes, one-shot approval, and credential isolation.
91
+
92
+ <a id="data-safety"></a>
93
+
94
+ ## Data Safety & Privacy
95
+
96
+ Pisper is local-first and does not provide a Pisper cloud service that hosts or relays your conversations. Sessions, memory, workflows, schedules, usage, and connection settings are stored under `~/.pisper/agent` by default. Set `PISPER_AGENT_DIR` to move this data root.
97
+
98
+ - **Protected local service:** the desktop Runtime listens on `127.0.0.1` by default and uses a random bootstrap token, an `HttpOnly` / `SameSite=Strict` cookie, and Origin checks for write requests. Pi telemetry is disabled by default.
99
+ - **Known-secret redaction:** known formats such as API keys, Bearer/JWT values, common access tokens, private keys, and database connection strings are replaced before memory persistence, secondary model-based memory extraction, and display boundaries including errors, MCP configuration, and approval summaries. Redacted values appear as `[REDACTED SECRET]` or `***`.
100
+ - **Credential isolation:** Provider credentials, MCP headers/environment variables, and channel credentials stay in local configuration and are not echoed to the Agent through normal configuration APIs. The host Shell tool also removes common credential and Shell-injection environment variables.
101
+ - **Controllable memory:** automatically inferred conversation memories remain pending and cannot affect recall until confirmed. You can reject candidates, delete individual memories, or delete sessions; deletion removes the associated content and retrieval data.
102
+ - **Explicit outbound boundaries:** regular prompts, attachments, and tool results are not globally rewritten or anonymized. Model calls send the required context directly to your configured Provider. Remote MCP, Web search, image/video generation, and messaging channels send their corresponding requests to those third parties only when enabled and used. Model-based memory extraction and semantic summaries apply the known-format redaction above before calling the active Provider.
103
+
104
+ > **Important boundary:** redaction is defense in depth against accidental disclosure of common credential formats. It is not complete DLP, a sandbox, or end-to-end encryption, and it cannot recognize every sensitive value in free-form text. Provider and channel credentials currently live in the local Agent data directory rather than an operating-system keychain. Protect that directory and its backups, review prompts, files, and tool arguments before sending, and follow the privacy policy of each configured third party.
105
+
106
+ <a id="desktop-pet"></a>
107
+
108
+ ## Desktop Pet
109
+
110
+ Pisper supports [Petdex](https://petdex.dev)-compatible pets. Install and manage them under **Settings → Desktop pet**. See [`docs/petdex-integration.md`](./docs/petdex-integration.md).
111
+
112
+ <a id="install"></a>
113
+
114
+ ## Install
115
+
116
+ ### Desktop (Recommended)
117
+
118
+ Download a Windows, macOS, or Linux installer from [GitHub Releases](https://github.com/ling-kong-ran/pisper/releases/latest). It works out of the box — no separate Node.js install is required.
119
+
120
+ #### If macOS Refuses to Open Pisper
121
+
122
+ Pisper is not currently notarized by Apple. Confirm that it came from the official Releases page, then select **Open Anyway** in **System Settings → Privacy & Security**. If that option is unavailable, run:
123
+
124
+ ```bash
125
+ sudo xattr -rd com.apple.quarantine /Applications/Pisper.app
126
+ ```
127
+
128
+ Use this command only for Pisper downloaded from the official Releases page and placed in `/Applications`.
129
+
130
+ #### If the Linux AppImage Does Not Start
131
+
132
+ ```bash
133
+ chmod +x Pisper_*_linux_x86_64.AppImage
134
+ ./Pisper_*_linux_x86_64.AppImage
135
+ ```
136
+
137
+ If FUSE is missing, install `libfuse2` or `libfuse2t64`, or use the `.deb` package:
138
+
139
+ ```bash
140
+ sudo apt install ./Pisper-*-linux-amd64.deb
141
+ ```
142
+
143
+ ### Component Updates
144
+
145
+ **Settings → Updates** in the desktop app is the unified update entry point. A single **Check for updates** action checks the Desktop package, TUI client, and Runtime together. Install controls appear only for components with a newer version, so a small component update does not require downloading the full desktop installer. Each component uses an independent signed Release channel. Runtime updates take effect after restarting the app, and Pisper falls back to the versions bundled with the desktop package if an installed component is unavailable or fails to start.
146
+
147
+ <a id="tui"></a>
148
+
149
+ ### Terminal Client (TUI)
150
+
151
+ With Node.js 20+, you can install the Pisper CLI through npm:
152
+
153
+ ```bash
154
+ npm install -g pisper
155
+ pisper
156
+ ```
157
+
158
+ After installing the desktop app, you can also install the `pisper` command from **Settings → Terminal**. The first installation remains explicit; after later desktop updates restart Pisper, it automatically refreshes this managed terminal client:
159
+
160
+ ```bash
161
+ pisper # start a new conversation
162
+ pisper resume # resume from an interactive list across all workspaces
163
+ pisper doctor # diagnose the runtime
164
+ ```
165
+
166
+ See the **[Pisper TUI user guide](./src-tui/README.en.md)** for installation, commands, keyboard controls, attachments, execution modes, and approvals.
167
+
168
+ ### Run from Source
169
+
170
+ Requires Node.js 20+, npm, and at least one model provider with an API key.
171
+
172
+ ```bash
173
+ git clone https://github.com/ling-kong-ran/pisper.git
174
+ cd pisper
175
+ npm install
176
+ npm run dev
177
+ ```
178
+
179
+ Desktop development and packaging:
180
+
181
+ ```bash
182
+ npm run desktop:webview:dev
183
+ npm run desktop:webview:build
184
+ ```
185
+
186
+ Data is stored under `~/.pisper/agent` by default. Set `PISPER_AGENT_DIR` to use another location.
187
+
188
+ <a id="development"></a>
189
+
190
+ ## Development
191
+
192
+ Main stack: React, TypeScript, Tauri, Rust, Node SEA, and Pi Coding Agent.
193
+
194
+ ```bash
195
+ npm run check
196
+ npm test
197
+ npm run build
198
+ ```
199
+
200
+ [Issues](https://github.com/ling-kong-ran/pisper/issues) and [pull requests](https://github.com/ling-kong-ran/pisper/pulls) are welcome. Do not commit API keys, bot credentials, or personal data from `~/.pisper/agent`.
201
+
202
+ <a id="license"></a>
203
+
204
+ ## License
205
+
206
+ Pisper is released under the [MIT License](./LICENSE). Third-party dependencies and community resources retain their own licenses.
207
+
208
+ ## Acknowledgments
209
+
210
+ Thanks to [Pi Coding Agent](https://github.com/earendil-works/pi/tree/main/packages/coding-agent), [Petdex](https://petdex.dev), and the open-source software used by this project.
211
+
212
+ Contributors:
213
+
214
+ - [@mik-myp](https://github.com/mik-myp) — Frontend TypeScript architecture, shadcn/ui / AI Elements, Zustand, and i18n refactor ([#1](https://github.com/ling-kong-ran/pisper/pull/1))
215
+
216
+ <p align="right"><a href="#top">Back to top ↑</a></p>
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ <p align="right"><strong>简体中文</strong> · <a href="./README.en.md">English</a></p>
2
+
3
+ <a id="top"></a>
4
+
5
+ <p align="center">
6
+ <img src="docs/brand/pisper-logo.svg" width="112" alt="Pisper 项目标志" />
7
+ </p>
8
+
9
+ <h1 align="center">Pisper</h1>
10
+
11
+ <p align="center"><strong>Pi 驱动的多 Agent 工作台</strong></p>
12
+
13
+ <p align="center">
14
+ <img src="https://img.shields.io/badge/Node.js-17141F?style=flat-square&logo=nodedotjs&logoColor=F59E0B" alt="Node.js" />
15
+ <img src="https://img.shields.io/badge/TypeScript-17141F?style=flat-square&logo=typescript&logoColor=F59E0B" alt="TypeScript" />
16
+ <img src="https://img.shields.io/badge/React-17141F?style=flat-square&logo=react&logoColor=F59E0B" alt="React" />
17
+ <img src="https://img.shields.io/badge/Tauri-17141F?style=flat-square&logo=tauri&logoColor=F59E0B" alt="Tauri" />
18
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-F59E0B?style=flat-square" alt="MIT License" /></a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="https://github.com/ling-kong-ran/pisper/releases/latest">
23
+ <img src="https://img.shields.io/badge/下载桌面版-Windows%20%7C%20macOS%20%7C%20Linux-F59E0B?style=for-the-badge&logo=github&logoColor=17141F" alt="下载 Pisper 桌面版" />
24
+ </a>
25
+ </p>
26
+
27
+ <a id="sponsors"></a>
28
+
29
+ ## ❤️ 赞助
30
+
31
+ 感谢以下合作伙伴对 Pisper 社区的支持。如果你也希望出现在这里,欢迎通过 [Issue](https://github.com/ling-kong-ran/pisper/issues) 联系我们。
32
+
33
+ <details open>
34
+ <summary>查看赞助商</summary>
35
+
36
+ <table>
37
+ <tr>
38
+ <td width="180" align="center">
39
+ <a href="https://matrix.000328.xyz/sign-up?aff=ZPeH"><strong>Matrix</strong></a>
40
+ </td>
41
+ <td>
42
+ 感谢 <a href="https://matrix.000328.xyz/sign-up?aff=ZPeH">Matrix</a> 对 Pisper 社区的支持。通过<a href="https://matrix.000328.xyz/sign-up?aff=ZPeH">此链接</a>注册,可能为 Pisper 项目带来推广收益。
43
+ </td>
44
+ </tr>
45
+ </table>
46
+
47
+ > 赞助链接包含推广参数。Pisper 的赞助内容不会使用会话、工作区、Provider、模型或 API 配置进行定向,也不会向赞助商发送这些数据。客户端赞助位的公开配置维护在 [`docs/sponsors.json`](./docs/sponsors.json)。
48
+
49
+ </details>
50
+
51
+ <p align="center">
52
+ <a href="#overview">简介</a> ·
53
+ <a href="https://ling-kong-ran.github.io/pisper/">项目主页</a> ·
54
+ <a href="#features">功能</a> ·
55
+ <a href="#data-safety">数据安全</a> ·
56
+ <a href="#desktop-pet">桌面宠物</a> ·
57
+ <a href="#install">安装</a> ·
58
+ <a href="#tui">终端客户端</a> ·
59
+ <a href="#development">开发</a> ·
60
+ <a href="#sponsors">赞助</a> ·
61
+ <a href="#license">许可</a>
62
+ </p>
63
+
64
+ ---
65
+
66
+ <a id="overview"></a>
67
+
68
+ ## 简介
69
+
70
+ Pisper 是基于 [Pi Coding Agent](https://github.com/earendil-works/pi/tree/main/packages/coding-agent) 的桌面与终端客户端,支持多会话、工具、Skills、MCP、自动化和会话级权限。
71
+
72
+ 产品介绍与界面预览:[Pisper 项目主页](https://ling-kong-ran.github.io/pisper/)。
73
+
74
+ ### 三步上手
75
+
76
+ 1. [下载桌面版](https://github.com/ling-kong-ran/pisper/releases/latest)(Windows / macOS / Linux)
77
+ 2. 配置任意一个模型 Provider 与 API Key
78
+ 3. 新建会话,开始并行工作
79
+
80
+ <a id="features"></a>
81
+
82
+ ## 功能
83
+
84
+ - **多会话**:独立模型、上下文、工作目录和权限,支持 Dock 分屏与布局恢复。
85
+ - **Tools、Skills 与 MCP**:统一管理能力和调用权限。
86
+ - **Subagent**:在隔离上下文中执行临时任务,完成后将结果返回父会话。
87
+ - **记忆与多模态**:检索项目记忆,处理图片、文档和代码。
88
+ - **自动化与渠道**:定时任务、可视化工作流、飞书和个人微信。
89
+ - **桌面与终端**:提供 Tauri 桌面端和 Ratatui TUI。
90
+ - **权限控制**:支持 `只读 / 完全访问` 两种执行模式,并提供单次审批和凭据隔离。
91
+
92
+ <a id="data-safety"></a>
93
+
94
+ ## 数据安全与隐私
95
+
96
+ Pisper 是本地优先应用,不提供用于托管或中转会话的 Pisper 云服务。会话、记忆、工作流、日程、用量和连接配置默认保存在本机 `~/.pisper/agent`;可通过 `PISPER_AGENT_DIR` 更改位置。
97
+
98
+ - **本地服务保护**:桌面 Runtime 默认只监听 `127.0.0.1`,使用随机启动 Token、`HttpOnly` / `SameSite=Strict` Cookie 和写请求 Origin 校验;Pi 遥测默认关闭。
99
+ - **已知敏感信息脱敏**:API Key、Bearer/JWT、常见访问令牌、私钥和数据库连接串等已知格式,会在记忆落盘、记忆二次模型提取,以及错误、MCP 配置和审批摘要等展示边界进入前被替换。脱敏后的内容使用 `[REDACTED SECRET]` 或 `***` 标记。
100
+ - **凭据隔离**:Provider 凭据、MCP Header/环境变量和渠道凭据保存在本机配置中,不通过普通配置接口回显给 Agent;宿主 Shell 工具也会移除常见凭据环境变量和 Shell 注入变量。
101
+ - **记忆可控**:自动推断的会话记忆先进入待确认区,未确认前不参与召回;用户可以拒绝候选、删除单条记忆或删除会话,删除操作会清除对应正文与检索数据。
102
+ - **外发边界明确**:普通 Prompt、附件、工具结果不会被全局改写或匿名化。调用模型时,必要上下文会直接发送到你配置的 Provider;启用远程 MCP、Web 搜索、图像/视频生成或消息渠道时,相应请求也会发送给该第三方。记忆提取和语义摘要如使用模型,会先执行上述已知格式脱敏,再调用当前 Provider。
103
+
104
+ > **重要边界:** 脱敏是降低常见凭据意外泄露风险的纵深防护,不是完整的 DLP、沙箱或端到端加密,也无法识别所有自由文本中的敏感数据。Provider 和渠道凭据目前保存在本机 Agent 数据目录,而不是操作系统钥匙串;请保护该目录与备份,并在发送前检查 Prompt、文件和工具参数,同时遵守所选第三方的隐私政策。
105
+
106
+ <a id="desktop-pet"></a>
107
+
108
+ ## 桌面宠物
109
+
110
+ Pisper 支持 [Petdex](https://petdex.dev) 兼容宠物,可在 **设置 → 桌面宠物** 中安装和管理。详见 [`docs/petdex-integration.md`](./docs/petdex-integration.md)。
111
+
112
+ <a id="install"></a>
113
+
114
+ ## 安装
115
+
116
+ ### 桌面版(推荐)
117
+
118
+ 前往 [GitHub Releases](https://github.com/ling-kong-ran/pisper/releases/latest) 下载 Windows、macOS 或 Linux 安装包,开箱即用,无需额外安装 Node.js。
119
+
120
+ #### macOS 无法打开
121
+
122
+ Pisper 当前尚未经过 Apple 公证。请确认应用来自官方 Releases,并优先在 **系统设置 → 隐私与安全性** 中选择 **仍要打开**。若没有该选项,可执行:
123
+
124
+ ```bash
125
+ sudo xattr -rd com.apple.quarantine /Applications/Pisper.app
126
+ ```
127
+
128
+ 请仅对从官方 Releases 下载并放入 `/Applications` 的应用使用此命令。
129
+
130
+ #### Linux AppImage 无法启动
131
+
132
+ ```bash
133
+ chmod +x Pisper_*_linux_x86_64.AppImage
134
+ ./Pisper_*_linux_x86_64.AppImage
135
+ ```
136
+
137
+ 缺少 FUSE 时安装 `libfuse2` 或 `libfuse2t64`,也可改用 `.deb`:
138
+
139
+ ```bash
140
+ sudo apt install ./Pisper-*-linux-amd64.deb
141
+ ```
142
+
143
+ ### 分块更新
144
+
145
+ 桌面端的 **设置 → 更新** 是统一更新入口。点击一次 **检查更新** 会同时检查 Desktop、TUI 客户端和 Runtime;只有发现新版本的组件才会显示安装操作,因此小范围更新不必重新下载完整桌面安装包。三个组件使用独立的签名 Release 通道,Runtime 更新在重启应用后生效;独立组件不可用或启动失败时,Pisper 会继续使用桌面安装包内置的版本。
146
+
147
+ <a id="tui"></a>
148
+
149
+ ### 终端客户端(TUI)
150
+
151
+ 使用 Node.js 20+ 时,也可通过 npm 安装 Pisper CLI:
152
+
153
+ ```bash
154
+ npm install -g pisper
155
+ pisper
156
+ ```
157
+
158
+ 安装桌面版后,也可以在 **设置 → 终端** 中安装 `pisper` 命令。首次安装由你主动确认;之后桌面应用更新并重启时,Pisper 会自动刷新这份已托管的终端客户端:
159
+
160
+ ```bash
161
+ pisper # 新建会话
162
+ pisper resume # 从所有工作目录的交互列表中恢复会话
163
+ pisper doctor # 诊断运行环境
164
+ ```
165
+
166
+ 安装、命令、快捷键、附件、执行模式和审批说明见 **[Pisper TUI 使用指南](./src-tui/README.md)**。
167
+
168
+ ### 从源码运行
169
+
170
+ 需要 Node.js 20+、npm,以及至少一个模型 Provider 与 API Key。
171
+
172
+ ```bash
173
+ git clone https://github.com/ling-kong-ran/pisper.git
174
+ cd pisper
175
+ npm install
176
+ npm run dev
177
+ ```
178
+
179
+ 桌面开发与打包:
180
+
181
+ ```bash
182
+ npm run desktop:webview:dev
183
+ npm run desktop:webview:build
184
+ ```
185
+
186
+ 数据默认保存在 `~/.pisper/agent`,可通过 `PISPER_AGENT_DIR` 修改。
187
+
188
+ <a id="development"></a>
189
+
190
+ ## 开发
191
+
192
+ 主要技术栈:React、TypeScript、Tauri、Rust、Node SEA 与 Pi Coding Agent。
193
+
194
+ ```bash
195
+ npm run check
196
+ npm test
197
+ npm run build
198
+ ```
199
+
200
+ 欢迎提交 [Issue](https://github.com/ling-kong-ran/pisper/issues) 与 [Pull Request](https://github.com/ling-kong-ran/pisper/pulls)。请勿提交 API Key、机器人凭据,或 `~/.pisper/agent` 中的个人数据。
201
+
202
+ <a id="license"></a>
203
+
204
+ ## 许可
205
+
206
+ Pisper 采用 [MIT License](./LICENSE)。第三方依赖和社区资源遵循各自许可证。
207
+
208
+ ## 致谢
209
+
210
+ 感谢 [Pi Coding Agent](https://github.com/earendil-works/pi/tree/main/packages/coding-agent)、[Petdex](https://petdex.dev) 及本项目使用的开源软件。
211
+
212
+ 贡献者:
213
+
214
+ - [@mik-myp](https://github.com/mik-myp) — 前端 TypeScript 架构、shadcn/ui / AI Elements、Zustand 与 i18n 重构([#1](https://github.com/ling-kong-ran/pisper/pull/1))
215
+
216
+ <p align="right"><a href="#top">返回顶部 ↑</a></p>
package/bin/pisper.mjs ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process'
4
+ import process from 'node:process'
5
+ import { ensurePisperInstallation } from '../lib/install.mjs'
6
+
7
+ try {
8
+ const installation = await ensurePisperInstallation()
9
+ const child = spawn(installation.executable, process.argv.slice(2), {
10
+ env: {
11
+ ...process.env,
12
+ PISPER_APP_ROOT: installation.appRoot,
13
+ PISPER_DISTRIBUTION: 'npm',
14
+ PISPER_SIDECAR_PATH: installation.sidecar,
15
+ },
16
+ stdio: 'inherit',
17
+ windowsHide: false,
18
+ })
19
+
20
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
21
+ process.on(signal, () => {
22
+ if (!child.killed) child.kill(signal)
23
+ })
24
+ }
25
+
26
+ child.once('error', (error) => {
27
+ console.error(`pisper: failed to start the terminal client: ${error.message}`)
28
+ process.exitCode = 1
29
+ })
30
+ child.once('exit', (code, signal) => {
31
+ if (signal) {
32
+ process.kill(process.pid, signal)
33
+ return
34
+ }
35
+ process.exitCode = code ?? 1
36
+ })
37
+ } catch (error) {
38
+ console.error(`pisper: ${error instanceof Error ? error.message : String(error)}`)
39
+ process.exitCode = 1
40
+ }
@@ -0,0 +1,301 @@
1
+ import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import process from 'node:process'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { PublicKey, Signature } from '@threema/wasm-minisign-verify'
6
+ import { x as extractTar } from 'tar'
7
+ import {
8
+ componentsRoot,
9
+ executableName,
10
+ releaseArchitecture,
11
+ releasePlatform,
12
+ supportedTarget,
13
+ } from './platform.mjs'
14
+
15
+ const packageRoot = fileURLToPath(new URL('..', import.meta.url))
16
+ const packageManifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
17
+ const repository = packageManifest.pisper?.repository
18
+ const tuiVersion = packageManifest.pisper?.tuiVersion
19
+ const runtimeVersion = packageManifest.pisper?.runtimeVersion
20
+ const MAX_ARCHIVE_BYTES = 128 * 1024 * 1024
21
+ const INSTALL_WAIT_MS = 600_000
22
+
23
+ for (const [label, value] of [
24
+ ['TUI', tuiVersion],
25
+ ['Runtime', runtimeVersion],
26
+ ]) {
27
+ if (!/^\d+\.\d+\.\d+$/.test(value || '')) {
28
+ throw new Error(`pisper package metadata does not contain a valid ${label} version`)
29
+ }
30
+ }
31
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repository || '')) {
32
+ throw new Error('pisper package metadata does not contain a valid repository')
33
+ }
34
+
35
+ function decodeWrapped(value) {
36
+ const trimmed = String(value).trim()
37
+ if (trimmed.startsWith('untrusted comment:')) return trimmed
38
+ if (!/^[A-Za-z0-9+/=\r\n]+$/.test(trimmed)) return trimmed
39
+ const decoded = Buffer.from(trimmed.replace(/\s/g, ''), 'base64').toString('utf8').trim()
40
+ return decoded.startsWith('untrusted comment:') ? decoded : trimmed
41
+ }
42
+
43
+ function componentAsset(component, version, platform, arch) {
44
+ const target = supportedTarget(platform, arch)
45
+ const label = component === 'tui' ? 'TUI_Component' : 'Runtime'
46
+ return `Pisper_${label}_${version}_${target}.tar.gz`
47
+ }
48
+
49
+ function componentUrls({ component, version, platform, arch, baseUrl }) {
50
+ const asset = componentAsset(component, version, platform, arch)
51
+ const root = baseUrl
52
+ ? String(baseUrl).replace(/\/$/, '')
53
+ : `https://github.com/${repository}/releases/download/${component}-v${version}`
54
+ return { archive: `${root}/${asset}`, signature: `${root}/${asset}.sig` }
55
+ }
56
+
57
+ export function releaseAsset(
58
+ version = tuiVersion,
59
+ platform = process.platform,
60
+ arch = process.arch,
61
+ ) {
62
+ return componentAsset('tui', version, platform, arch)
63
+ }
64
+
65
+ export function releaseAssetUrls({
66
+ version = tuiVersion,
67
+ platform = process.platform,
68
+ arch = process.arch,
69
+ baseUrl = process.env.PISPER_CLI_DOWNLOAD_BASE_URL,
70
+ } = {}) {
71
+ return componentUrls({ component: 'tui', version, platform, arch, baseUrl })
72
+ }
73
+
74
+ function compareVersions(left, right) {
75
+ const a = left.split('.').map(Number)
76
+ const b = right.split('.').map(Number)
77
+ for (let index = 0; index < 3; index += 1) {
78
+ if (a[index] !== b[index]) return a[index] - b[index]
79
+ }
80
+ return 0
81
+ }
82
+
83
+ async function isFile(path) {
84
+ try {
85
+ return (await stat(path)).isFile()
86
+ } catch (error) {
87
+ if (error?.code === 'ENOENT') return false
88
+ throw error
89
+ }
90
+ }
91
+
92
+ function commandName(component) {
93
+ if (component === 'tui') return executableName()
94
+ return process.platform === 'win32' ? 'pisper-sidecar.exe' : 'pisper-sidecar'
95
+ }
96
+
97
+ async function validateComponent(component, version, destination) {
98
+ const executable = join(destination, commandName(component))
99
+ if (!(await isFile(executable))) return ''
100
+ if (
101
+ component === 'runtime' &&
102
+ !(await isFile(join(destination, 'sidecar-runtime', 'package.json')))
103
+ ) {
104
+ return ''
105
+ }
106
+ try {
107
+ const manifest = JSON.parse(await readFile(join(destination, 'manifest.json'), 'utf8'))
108
+ if (
109
+ manifest.version !== version ||
110
+ manifest.platform !== releasePlatform() ||
111
+ manifest.arch !== releaseArchitecture() ||
112
+ manifest.command !== commandName(component)
113
+ ) {
114
+ return ''
115
+ }
116
+ } catch {
117
+ return ''
118
+ }
119
+ return executable
120
+ }
121
+
122
+ async function resolveCurrentComponent(root, component, minimumVersion) {
123
+ try {
124
+ const pointer = JSON.parse(await readFile(join(root, component, 'current.json'), 'utf8'))
125
+ if (!/^\d+\.\d+\.\d+$/.test(pointer.version || '')) return null
126
+ if (compareVersions(pointer.version, minimumVersion) < 0) return null
127
+ const destination = join(root, component, 'versions', pointer.version)
128
+ const executable = await validateComponent(component, pointer.version, destination)
129
+ return executable ? { version: pointer.version, destination, executable } : null
130
+ } catch (error) {
131
+ if (error?.code === 'ENOENT' || error instanceof SyntaxError) return null
132
+ throw error
133
+ }
134
+ }
135
+
136
+ async function download(url) {
137
+ const response = await fetch(url, {
138
+ headers: { 'User-Agent': `pisper/${packageManifest.version}` },
139
+ redirect: 'follow',
140
+ signal: AbortSignal.timeout(600_000),
141
+ })
142
+ if (!response.ok) throw new Error(`download failed with HTTP ${response.status}: ${url}`)
143
+ const declaredSize = Number(response.headers.get('content-length') || 0)
144
+ if (declaredSize > MAX_ARCHIVE_BYTES) {
145
+ throw new Error(`download is larger than ${MAX_ARCHIVE_BYTES} bytes`)
146
+ }
147
+ const bytes = Buffer.from(await response.arrayBuffer())
148
+ if (bytes.length === 0) throw new Error(`downloaded an empty file: ${url}`)
149
+ if (bytes.length > MAX_ARCHIVE_BYTES) {
150
+ throw new Error(`download is larger than ${MAX_ARCHIVE_BYTES} bytes`)
151
+ }
152
+ return bytes
153
+ }
154
+
155
+ async function verifyArchive(component, archive, signatureBytes) {
156
+ const publicKeyText = await readFile(join(packageRoot, 'updater.pubkey'), 'utf8')
157
+ const publicKey = PublicKey.decode(decodeWrapped(publicKeyText))
158
+ const signature = Signature.decode(decodeWrapped(signatureBytes.toString('utf8')))
159
+ try {
160
+ if (!publicKey.verify(archive, signature)) {
161
+ throw new Error('signature verification returned false')
162
+ }
163
+ } catch (error) {
164
+ throw new Error(`${component} archive signature verification failed: ${error.message}`, {
165
+ cause: error,
166
+ })
167
+ } finally {
168
+ signature.free()
169
+ publicKey.free()
170
+ }
171
+ }
172
+
173
+ async function wait(milliseconds) {
174
+ await new Promise((resolve) => setTimeout(resolve, milliseconds))
175
+ }
176
+
177
+ async function acquireLock(lockPath, validate) {
178
+ const deadline = Date.now() + INSTALL_WAIT_MS
179
+ while (true) {
180
+ try {
181
+ return await open(lockPath, 'wx')
182
+ } catch (error) {
183
+ if (error?.code !== 'EEXIST') throw error
184
+ if (await validate()) return null
185
+ if (Date.now() >= deadline) {
186
+ throw new Error('timed out waiting for another Pisper installer')
187
+ }
188
+ await wait(250)
189
+ }
190
+ }
191
+ }
192
+
193
+ async function activateComponent(root, component, version) {
194
+ const componentRoot = join(root, component)
195
+ const pointer = join(componentRoot, 'current.json')
196
+ const temporary = join(componentRoot, `current.json.tmp-${process.pid}`)
197
+ await mkdir(componentRoot, { recursive: true })
198
+ await writeFile(temporary, `${JSON.stringify({ version }, null, 2)}\n`, 'utf8')
199
+ await rm(pointer, { force: true })
200
+ await rename(temporary, pointer)
201
+ }
202
+
203
+ async function installComponent({ root, component, version, destination, activate = false }) {
204
+ const lockRoot = join(root, 'npm')
205
+ await mkdir(lockRoot, { recursive: true })
206
+ const lockPath = join(lockRoot, `${component}-install.lock`)
207
+ const validate = () => validateComponent(component, version, destination)
208
+ const lock = await acquireLock(lockPath, validate)
209
+ if (!lock) {
210
+ if (activate) await activateComponent(root, component, version)
211
+ return { version, destination, executable: await validate() }
212
+ }
213
+
214
+ const staging = `${destination}.tmp-${process.pid}-${Date.now()}`
215
+ const archivePath = `${staging}.tar.gz`
216
+ try {
217
+ const existing = await validate()
218
+ if (existing) {
219
+ if (activate) await activateComponent(root, component, version)
220
+ return { version, destination, executable: existing }
221
+ }
222
+ await rm(staging, { recursive: true, force: true })
223
+ await mkdir(staging, { recursive: true })
224
+ const urls = componentUrls({
225
+ component,
226
+ version,
227
+ platform: process.platform,
228
+ arch: process.arch,
229
+ baseUrl: process.env.PISPER_CLI_DOWNLOAD_BASE_URL,
230
+ })
231
+ const [archive, signature] = await Promise.all([
232
+ download(urls.archive),
233
+ download(urls.signature),
234
+ ])
235
+ await verifyArchive(component, archive, signature)
236
+ await writeFile(archivePath, archive, { mode: 0o600 })
237
+ await extractTar({
238
+ cwd: staging,
239
+ file: archivePath,
240
+ preservePaths: false,
241
+ strict: true,
242
+ strip: 1,
243
+ filter: (_path, entry) => entry.type === 'File' || entry.type === 'Directory',
244
+ })
245
+ if (process.platform !== 'win32') await chmod(join(staging, commandName(component)), 0o755)
246
+ const stagedExecutable = await validateComponent(component, version, staging)
247
+ if (!stagedExecutable) {
248
+ throw new Error(`downloaded ${component} archive has an invalid package layout`)
249
+ }
250
+ await mkdir(dirname(destination), { recursive: true })
251
+ await rm(destination, { recursive: true, force: true })
252
+ await rename(staging, destination)
253
+ if (activate) await activateComponent(root, component, version)
254
+ return { version, destination, executable: join(destination, commandName(component)) }
255
+ } finally {
256
+ await lock.close()
257
+ await Promise.all([
258
+ rm(lockPath, { force: true }),
259
+ rm(staging, { recursive: true, force: true }),
260
+ rm(archivePath, { force: true }),
261
+ ])
262
+ }
263
+ }
264
+
265
+ export async function ensurePisperInstallation() {
266
+ const root = componentsRoot()
267
+ const npmTuiDestination = join(root, 'npm', 'versions', tuiVersion)
268
+ const runtimeDestination = join(root, 'runtime', 'versions', runtimeVersion)
269
+ const [tui, runtime] = await Promise.all([
270
+ resolveCurrentComponent(root, 'tui', tuiVersion).then(
271
+ (current) =>
272
+ current ||
273
+ installComponent({
274
+ root,
275
+ component: 'tui',
276
+ version: tuiVersion,
277
+ destination: npmTuiDestination,
278
+ }),
279
+ ),
280
+ resolveCurrentComponent(root, 'runtime', runtimeVersion).then(
281
+ (current) =>
282
+ current ||
283
+ installComponent({
284
+ root,
285
+ component: 'runtime',
286
+ version: runtimeVersion,
287
+ destination: runtimeDestination,
288
+ activate: true,
289
+ }),
290
+ ),
291
+ ])
292
+ return {
293
+ executable: tui.executable,
294
+ sidecar: runtime.executable,
295
+ appRoot: join(runtime.destination, 'sidecar-runtime'),
296
+ }
297
+ }
298
+
299
+ export async function ensurePisperExecutable() {
300
+ return (await ensurePisperInstallation()).executable
301
+ }
@@ -0,0 +1,43 @@
1
+ import { homedir } from 'node:os'
2
+ import { join } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ export function releasePlatform(platform = process.platform) {
6
+ if (platform === 'win32') return 'windows'
7
+ if (platform === 'darwin') return 'darwin'
8
+ if (platform === 'linux') return 'linux'
9
+ throw new Error(`unsupported operating system: ${platform}`)
10
+ }
11
+
12
+ export function releaseArchitecture(arch = process.arch) {
13
+ if (arch === 'x64') return 'x86_64'
14
+ if (arch === 'arm64') return 'aarch64'
15
+ throw new Error(`unsupported CPU architecture: ${arch}`)
16
+ }
17
+
18
+ export function supportedTarget(platform = process.platform, arch = process.arch) {
19
+ const target = `${releasePlatform(platform)}_${releaseArchitecture(arch)}`
20
+ const supported = new Set(['windows_x86_64', 'darwin_x86_64', 'darwin_aarch64', 'linux_x86_64'])
21
+ if (!supported.has(target)) {
22
+ throw new Error(`Pisper does not publish a TUI package for ${target}`)
23
+ }
24
+ return target
25
+ }
26
+
27
+ export function executableName(platform = process.platform) {
28
+ return platform === 'win32' ? 'pisper.exe' : 'pisper'
29
+ }
30
+
31
+ export function localDataDirectory(platform = process.platform, env = process.env) {
32
+ if (env.PISPER_NPM_INSTALL_DIR) return env.PISPER_NPM_INSTALL_DIR
33
+ if (platform === 'win32') {
34
+ if (!env.LOCALAPPDATA) throw new Error('LOCALAPPDATA is unavailable')
35
+ return env.LOCALAPPDATA
36
+ }
37
+ if (platform === 'darwin') return join(homedir(), 'Library', 'Application Support')
38
+ return env.XDG_DATA_HOME || join(homedir(), '.local', 'share')
39
+ }
40
+
41
+ export function componentsRoot(platform = process.platform, env = process.env) {
42
+ return join(localDataDirectory(platform, env), 'com.lingkongran.pisper', 'components')
43
+ }
@@ -0,0 +1,15 @@
1
+ import process from 'node:process'
2
+ import { ensurePisperInstallation } from './install.mjs'
3
+
4
+ if (process.env.PISPER_CLI_SKIP_INSTALL !== '1') {
5
+ try {
6
+ const installation = await ensurePisperInstallation()
7
+ console.log(`pisper: installed the signed Pisper terminal client at ${installation.executable}`)
8
+ } catch (error) {
9
+ console.warn(
10
+ `pisper: deferred terminal client download until the first pisper command: ${
11
+ error instanceof Error ? error.message : String(error)
12
+ }`,
13
+ )
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "pisper",
3
+ "version": "0.0.1",
4
+ "description": "Pisper CLI.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "ling-kong-ran",
8
+ "email": "65328093+ling-kong-ran@users.noreply.github.com",
9
+ "url": "https://github.com/ling-kong-ran"
10
+ },
11
+ "type": "module",
12
+ "homepage": "https://github.com/ling-kong-ran/pisper#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/ling-kong-ran/pisper.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/ling-kong-ran/pisper/issues"
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "bin": {
24
+ "pisper": "bin/pisper.mjs"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "lib/",
29
+ "updater.pubkey",
30
+ "README.md",
31
+ "README.en.md",
32
+ "LICENSE"
33
+ ],
34
+ "scripts": {
35
+ "postinstall": "node lib/postinstall.mjs"
36
+ },
37
+ "dependencies": {
38
+ "@threema/wasm-minisign-verify": "0.1.3",
39
+ "tar": "7.5.22"
40
+ },
41
+ "pisper": {
42
+ "repository": "ling-kong-ran/pisper",
43
+ "tuiVersion": "0.4.28",
44
+ "runtimeVersion": "0.4.29"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ }
50
+ }
package/updater.pubkey ADDED
@@ -0,0 +1 @@
1
+ dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYwRTUzMkExNjAyRUNGQTQKUldTa3p5NWdvVExsOEQ5TElOa3BVcUxZQ2I1RlNieHJHeFQ3YVdqeTRMK0VKS2dWSlZsMXRNazQK