pinomad 0.0.0-stage → 0.1.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 +202 -0
- package/README.md +219 -2
- package/dist/chunk-7fqn69ys.js +33 -0
- package/dist/chunk-8h3wrn84.js +63 -0
- package/dist/chunk-9z7echn7.js +14 -0
- package/dist/chunk-cjfmfyjb.js +21 -0
- package/dist/chunk-df32avw6.js +4 -0
- package/dist/chunk-fpb6f9n5.js +10 -0
- package/dist/chunk-fxaewke2.js +113 -0
- package/dist/chunk-hsdzb73r.js +37 -0
- package/dist/chunk-mqbg2cbw.js +2697 -0
- package/dist/chunk-ne6a5q2j.js +284 -0
- package/dist/chunk-r6w5aqkw.js +64 -0
- package/dist/chunk-tpd3trvd.js +36 -0
- package/dist/chunk-wgn3cx5q.js +56 -0
- package/dist/chunk-y73t2jmc.js +487 -0
- package/dist/chunk-z4x88s0q.js +347 -0
- package/dist/chunk-zx77w1nm.js +430 -0
- package/dist/pinomad.js +55 -0
- package/package.json +37 -4
- package/web/assets/index-B9yvK0AN.js +383 -0
- package/web/assets/index-t5dAiohJ.css +1 -0
- package/web/assets/montserrat-latin-ext-wght-normal-BsZE-iaG.woff2 +0 -0
- package/web/assets/montserrat-latin-wght-normal-l_AIctKy.woff2 +0 -0
- package/web/index.html +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,220 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PiNomad
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
以 coding 为核心的 agent 产品,基于 [Pi Durable](https://www.npmjs.com/package/@earendil-works/pi-durable)。
|
|
4
|
+
|
|
5
|
+
- 自托管宿主:在你自己的机器(Mac mini、Linux VPS)上运行,直接使用本机的 shell 和代码仓库;执行可在崩溃或重启后恢复。
|
|
6
|
+
- 多端客户端:桌面、Web、移动端连接同一个宿主,界面由 PiNomad 统一定义。
|
|
7
|
+
|
|
8
|
+
架构决策见 [`docs/adr/`](docs/adr/README.md),路线图见 [`docs/roadmap.md`](docs/roadmap.md),术语见 [`CONTEXT.md`](CONTEXT.md)。
|
|
9
|
+
|
|
10
|
+
## 运行
|
|
11
|
+
|
|
12
|
+
安装只需要 Node 25+(宿主依赖 `node:sqlite`),不需要 Bun:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i -g pinomad
|
|
16
|
+
|
|
17
|
+
# 起宿主:真实模型需要先在 pi 里配好认证(读 ~/.pi/agent);
|
|
18
|
+
# 没有认证可以用脚本化的假模型冒烟:
|
|
19
|
+
pinomad host --faux "hi"
|
|
20
|
+
|
|
21
|
+
# 打印带 token 的浏览器链接,打开即用(Web 客户端由宿主自己提供):
|
|
22
|
+
pinomad link
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 开发 / 从源码运行
|
|
26
|
+
|
|
27
|
+
检出仓库,需要 Bun(装依赖、跑脚本)+ Node 25+:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
bun install
|
|
31
|
+
|
|
32
|
+
bun run host -- --faux "hi"
|
|
33
|
+
|
|
34
|
+
# 另一个终端起 Web 客户端开发服务器(http://127.0.0.1:5199):
|
|
35
|
+
bun run web
|
|
36
|
+
bun run link
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
默认数据目录是 `~/.pinomad`(session.sqlite、token、目录锁),可用 `--data-dir` 或 `PINOMAD_DATA_DIR` 覆盖;同一数据目录同时只能有一个宿主进程。
|
|
40
|
+
|
|
41
|
+
对话按项目组织(ADR-0007):在客户端侧栏点 Add project 登记一个宿主上的目录,也可以启动时用可重复的 `--project DIR` 登记;不选项目的对话归入 Chat。宿主启动时不再自动建对话,第一条消息发出时才新建。
|
|
42
|
+
|
|
43
|
+
Git 项目的对话默认在独立 worktree 里工作(ADR-0010):宿主在 `<数据目录>/worktrees/<对话 id>` 检出项目仓库,分支是 `pinomad/<对话 id>-<随机>`,起点是创建时的 HEAD;项目的原目录不会被改动,改动留在分支上,由你自己 merge 或发 PR 合回。新建对话时勾选 "Work directly in project directory" 可以直接在项目目录里工作;非 Git 目录和还没有 commit 的仓库也直接使用项目目录。fork 会得到自己的 worktree,内容复制自父对话在 fork 那一刻的文件状态(已提交、未提交、未跟踪的都在,被 gitignore 的文件不复制);子代理和所属对话共用一个 worktree。归档对话时干净的 worktree 目录会被删除(分支保留),有未提交改动的保留并提示。注意 worktree 是全新检出,`node_modules`、`.env` 这类被忽略文件不在里面,agent 需要时按 AGENTS.md 自行安装。如果移动或删除了数据目录,在仓库里跑一次 `git worktree prune` 清掉失效的登记。
|
|
44
|
+
|
|
45
|
+
Chat 对话在 `<数据目录>/chats/<对话 id>` 里工作。fork 和子代理的对话显示在所属对话下面。归档只是隐藏,移除项目也不会删除目录和对话。
|
|
46
|
+
|
|
47
|
+
agent 用 read / write / edit / bash 工具,执行前不需要审批;拿不准方向时用 `ask_user_question` 向用户提 1–4 个带选项的问题,问题卡片显示在输入框上方,用户也可以不回卡片、直接在对话里回复(ADR-0011)。agent 还可以用 `subagent` 把一个自包含的任务交给子代理:子代理在自己的对话里从零开始(看不到父对话),和父对话共用同一个检出,同一轮里的多个子代理并发执行;它不能向用户提问,需要澄清时写进回答交回父代理;默认沿用父对话的模型和思考等级,也可以在调用时用 `model`(`provider/modelId`)和 `thinkingLevel` 指定。中止父对话会连带中止子代理。edit / write 的结果带 unified diff,渲染成 diff 而不是文本;对话标题栏的 Changes 按钮随时查看当前检出的全部改动(worktree 相对基线 commit,项目目录则是未提交的改动),busy 转空闲时自动刷新。agent 还有 `codemode`(ADR-0013):写一段 JavaScript,在 QuickJS 沙箱里调用其他工具,只有脚本的输出进入上下文,适合并发读多个文件、先过滤大段 bash 输出再看、批量改文件,以及调用 MCP 工具。脚本里能调 read / bash / edit / write 和 MCP 工具,不能调 `ask_user_question`、`subagent` 和 `codemode` 自己;一次 codemode 调用在 Web 上是一张卡片,列出脚本、每次嵌套调用的状态和耗时(edit / write 的 diff 可以展开),以及脚本输出。嵌套调用不单独进 transcript,宿主崩溃时整段脚本按中断处理,已经执行的调用不会撤销。系统提示词会带上以下内容,每次请求都重新读取(ADR-0006):
|
|
48
|
+
|
|
49
|
+
- AGENTS.md:`~/.agents/AGENTS.md`,以及从根目录到 cwd 沿途的 AGENTS.md / CLAUDE.md;
|
|
50
|
+
- Skills:`<cwd>/.agents/skills` 和 `~/.agents/skills`,同名时项目的生效。
|
|
51
|
+
|
|
52
|
+
MCP server 在 `~/.agents/mcp.json` 里配置(ADR-0012),格式是各家客户端通用的 `mcpServers`:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"docs": { "command": "npx", "args": ["-y", "@example/docs-mcp"] },
|
|
58
|
+
"github": { "url": "https://api.githubcopilot.com/mcp/", "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" } },
|
|
59
|
+
"browser": { "command": "npx", "args": ["-y", "@playwright/mcp"], "exposure": "direct", "description": "Drive a browser" },
|
|
60
|
+
"local": { "command": "my-mcp-server", "env": { "KEY": "${MY_KEY}" }, "enabled": false }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
stdio server 写 `command`(可选 `args`、`env`、`cwd`——相对的 cwd 按主目录解析);HTTP server 写 `url`(可选 `headers`)。`env` 和 `headers` 的值可以用 `${NAME}` 引用宿主进程的环境变量。`timeout`(秒,默认 60)是单次工具调用的超时;`enabled: false` 保留配置但不连接。每个 server 在宿主里只连一份、所有对话共用;工具名是 `mcp__<server>__<tool>`。`exposure` 决定模型怎么够到它们(ADR-0013):默认 `codemode`,工具不声明给模型,只能在 codemode 脚本里调用,模型用 `searchTools` / `describeNamespace` 找到它们,server 连上前后工具声明不变;写 `"direct"` 则同时直接声明给模型,结果里的图片显示在工具卡片里。`description`(可选)是给模型看的一句话说明,列在系统提示词里;不写时只列 server 名和工具数,server 自带的 instructions 由 `describeNamespace` 返回。右侧状态面板的 MCP 区列出每个 server 的状态、工具数和错误,以及配置错误(比如引用了一个不存在的环境变量)。配置只在启动时读一次,改动后重启宿主生效(常驻模式用 `bun run service -- restart`)。暂不支持:项目级 `mcp.json`、OAuth 登录(server 回 401 会在状态里提示)、resources / prompts、`deferred` / `tool_search`。
|
|
66
|
+
|
|
67
|
+
## 远程访问(ADR-0008)
|
|
68
|
+
|
|
69
|
+
一条命令起全部(构建 Web、起宿主、起 vite),并在终端打印电脑浏览器链接和手机配对二维码:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
bun run start
|
|
73
|
+
# 透传宿主参数:bun run start -- --faux "hi" --data-dir /tmp/demo
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
手机扫二维码即完成配对(Noise IK 端到端加密 + 设备登记);二维码是一次性的,5 分钟过期,过期后用 `bun run pair` 重发一个。已登记的设备和吊销在 Web 侧栏的 Devices 里管理;吊销设备也可以在那直接点 Revoke。
|
|
77
|
+
|
|
78
|
+
<details><summary>手动/高级:分开跑各个进程</summary>
|
|
79
|
+
|
|
80
|
+
默认只监听 `127.0.0.1`。`--remote-port N` 另起一个监听所有网卡的安全通道端口,同端口用 HTTP 提供构建好的 Web 客户端(`apps/web/dist`,缺失时页面是 503):
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
bun run build
|
|
84
|
+
bun run host -- --remote-port 7422
|
|
85
|
+
bun run web # 电脑上的开发服务器 http://127.0.0.1:5199
|
|
86
|
+
bun run link # 打印带 token 的浏览器链接
|
|
87
|
+
bun run pair # 打印配对二维码(一次性,5 分钟过期)
|
|
88
|
+
# 走隧道/自建入口,或 LAN 地址探测选错网卡时用 --public-url 覆盖:
|
|
89
|
+
bun run host -- --remote-port 7422 --public-url https://pinomad.example.com
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
远程端口不接受 token:只有完成配对、公钥已登记在设备表里的客户端能连。
|
|
93
|
+
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
### 经自部署中继访问
|
|
97
|
+
|
|
98
|
+
宿主在家里局域网、手机在外面时,把中继跑在一台有公网地址的 VPS 上:宿主和手机都只往外连中继,中继按宿主身份把两端的连接接起来,只转发密文、不存东西;Web 客户端也由中继用 HTTPS 提供,手机扫二维码直接打开。
|
|
99
|
+
|
|
100
|
+
宿主这边:先打印中继身份(Ed25519 公钥,存在 `<dataDir>/relay-key`):
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
pinomad relay-id [--data-dir DIR] # 输出 hostId,把它加进中继的 --allow-host
|
|
104
|
+
pinomad host --relay https://relay.example.com
|
|
105
|
+
# 常驻:pinomad service install --relay https://relay.example.com
|
|
106
|
+
pinomad pair # 打出的二维码指向中继
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
VPS 这边:同一个包(只要 Node 25+)——中继和 Web 客户端来自同一个 `pinomad` 安装,版本天然一致:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
npm i -g pinomad
|
|
113
|
+
pinomad relay --public-origin https://relay.example.com --allow-host <hostId>
|
|
114
|
+
# 默认只监听 127.0.0.1:7430,--port / --listen 可改;多台宿主就写多个 --allow-host
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Caddy 终止 TLS(自动签证书,WebSocket 无需额外配置):
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
relay.example.com {
|
|
121
|
+
reverse_proxy 127.0.0.1:7430
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
systemd 单元示例(VPS 上的系统级单元;node 和包路径都用绝对路径,`command -v node` 和 `npm root -g` 查):
|
|
126
|
+
|
|
127
|
+
```ini
|
|
128
|
+
[Unit]
|
|
129
|
+
Description=PiNomad relay
|
|
130
|
+
After=network-online.target
|
|
131
|
+
|
|
132
|
+
[Service]
|
|
133
|
+
User=pinomad
|
|
134
|
+
ExecStart=/usr/bin/node /usr/lib/node_modules/pinomad/dist/pinomad.js relay --public-origin https://relay.example.com --allow-host <hostId>
|
|
135
|
+
Restart=always
|
|
136
|
+
|
|
137
|
+
[Install]
|
|
138
|
+
WantedBy=multi-user.target
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### 容器平台(Zeabur 等)
|
|
142
|
+
|
|
143
|
+
镜像在 `deploy/relay/Dockerfile`,装的是锁版本的 npm 发布包(`ARG PINOMAD_VERSION`)。平台终结 TLS 并转发到 `$PORT`(默认 8080),不需要 Caddy。中继把转接状态放在内存里,**只能跑一个实例**,不要开扩容。
|
|
144
|
+
|
|
145
|
+
| 环境变量 | 含义 |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `PINOMAD_PUBLIC_ORIGIN` | 客户端打开的 https origin,比如 `https://nomad.example.com` |
|
|
148
|
+
| `PINOMAD_ALLOW_HOST` | 允许登记的 hostId,多个用空格分隔(每台宿主上跑 `pinomad relay-id`) |
|
|
149
|
+
| `ZBPACK_DOCKERFILE_PATH=Dockerfile` | **Zeabur 必加**:目录里只有一个 Dockerfile 时,Zeabur 会把它识别成静态站点、用 Caddy 托管文件,中继根本没启动;这个变量强制按 Dockerfile 构建(部署日志里应是 `PLANTYPE=docker`) |
|
|
150
|
+
|
|
151
|
+
Zeabur 上的步骤(在 Dedicated Server 上部署,用 CLI 上传目录,不接 GitHub):
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
cd deploy/relay
|
|
155
|
+
zeabur deploy --project-id <项目 id> --create --name pinomad-relay # 首次:在项目里建出服务
|
|
156
|
+
# 之后重新部署同一个服务:
|
|
157
|
+
zeabur deploy --service-id <服务 id>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
然后在服务上配好上面的环境变量(`PORT` 由平台注入,不用自己设),在 Domains 里绑定自定义域名,到 DNS 加一条记录:Dedicated Server 用 **A 记录指向服务器 IP**;Cloudflare 上建议**关掉代理(灰云)**,橙云会让国内访问多绕一圈,证书也由 Zeabur 自己签。域名状态变成 `PROVISIONED` 后,验证:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
curl -sI https://nomad.example.com/ # 200,中继托管的 Web 客户端
|
|
164
|
+
pinomad service install --relay https://nomad.example.com … # 宿主接上中继
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
宿主登记成功会在日志里写一行 `{"event":"relay","status":"registered",…}`,掉线重连时写 `"status":"connecting"`(`pinomad service logs` 查看);也可以从外面连设备入口 `wss://<origin>/c/<hostId>` 确认:宿主在线时连接保持打开(等握手),没登记会立刻以 `4604`(hostOffline)关闭。
|
|
168
|
+
|
|
169
|
+
也可以从 GitHub 仓库部署:Root Directory 设 `deploy/relay`、Watch Paths 限 `deploy/relay/**`,其余同上。
|
|
170
|
+
|
|
171
|
+
#### 升级
|
|
172
|
+
|
|
173
|
+
中继托管的 Web 客户端和宿主版本不一致时,客户端会显示需要更新;两边一起升级——宿主 `pinomad upgrade`,VPS 上 `npm i -g pinomad@latest` 后重启中继;容器部署则是改 Dockerfile 里的 `PINOMAD_VERSION`,再在 `deploy/relay` 下重新 `zeabur deploy --service-id <服务 id>`(从 GitHub 部署的话合进 main 即触发)。宿主不在中继的 `--allow-host` 里时,宿主日志和 Web 警告会带着要补的 `--allow-host <hostId>`。
|
|
174
|
+
|
|
175
|
+
npm 装的实例和本机任何开发检出互不相干(各自的数据目录、进程、包路径都独立),所以在同一台机器上开发 PiNomad 不会碰到正在跑的服务。
|
|
176
|
+
|
|
177
|
+
## 常驻运行(ADR-0009)
|
|
178
|
+
|
|
179
|
+
把宿主装成当前用户的系统服务(macOS 是 LaunchAgent,Linux 是 systemd 用户单元),开机自启、崩溃自动拉起(从源码检出跑时用 `bun run service -- …`,下同):
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
pinomad service install [--data-dir DIR --port N --remote-port N --relay URL --project DIR …]
|
|
183
|
+
pinomad service status [--data-dir D] [--url U]
|
|
184
|
+
pinomad service restart [--force] [--data-dir D] [--url U]
|
|
185
|
+
pinomad service logs [--follow] [-n N] [--data-dir D]
|
|
186
|
+
pinomad service uninstall
|
|
187
|
+
pinomad upgrade [--force]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- `install` 把宿主参数原样写进服务定义;`--data-dir`(或 `PINOMAD_DATA_DIR`,默认 `~/.pinomad`)展开成显式的绝对路径。改参数就是带新参数再跑一次 `install`。装好后用 `pinomad link` 拿本机浏览器链接、`pinomad pair` 发手机配对码。
|
|
191
|
+
- **PATH 在安装时固定**:服务里 agent 的 bash 继承的是安装那一刻终端的 PATH(已经剥掉 `node_modules/.bin` 和 Bun 注入的临时目录)。后来装了新工具、PATH 变了,重新跑一次 `install` 刷新。
|
|
192
|
+
- `restart` 先连上宿主读任务图,有未结束的任务就等它们跑完再重启(Ctrl-C 取消);`--force` 立即重启,交给 Durable 恢复。打包安装时 `upgrade` 把 `pinomad@latest` 装进运行包所在的 npm 全局前缀(非全局安装会拒绝执行)+ 同样的 restart;源码检出里(`bun run upgrade`)依次 `git pull --ff-only`、`bun install`、`bun run build`,工作区有未提交改动时拒绝执行。前面任何一步失败都直接停下,不重启,服务继续跑旧代码。
|
|
193
|
+
- `uninstall` 只卸载服务,不动数据目录。
|
|
194
|
+
- macOS:宿主以登录会话运行,机器重启后要能自动起来需在系统设置里打开自动登录,并为这台机器关掉睡眠。日志写到 `<数据目录>/logs/host.log`。
|
|
195
|
+
- Linux:要在未登录时也运行需自己执行 `loginctl enable-linger`(install 检测到没开会提示);日志走 `journalctl --user`。开着 ufw 时,局域网设备访问 `--remote-port` 要先放行,比如 `sudo ufw allow from 192.168.0.0/24 to any port 7422 proto tcp`(只走中继就不用开)。
|
|
196
|
+
- **服务绑定安装时的 node 路径**:单元里写的是 node 和包的绝对路径。用 mise / nvm 管理 Node 时,路径里带着具体版本号(`…/node/26.8.2/…`);换了 Node 版本、旧目录被删后服务就起不来,要用新版本重新 `npm i -g pinomad`,再跑一次 `pinomad service install …`。`pinomad upgrade` 只升 pinomad,装回原来的前缀,不受影响。
|
|
197
|
+
- 服务模式下**回环端口也用 HTTP 提供构建好的 Web 客户端**。npm 安装时 `pinomad link` 总是指向宿主自己提供的 Web;源码检出里按顺序探测:正在跑的 vite 开发服务器(5199)优先,否则宿主提供 Web 的端口,都没有则仍是 5199。开发时 `bun run start` + vite 照旧。
|
|
198
|
+
- 服务在跑时 `bun run start` 会因为目录锁拿不到而失败(同一数据目录只有一个宿主);开发时用另一个 `--data-dir`,或先 `uninstall`。
|
|
199
|
+
|
|
200
|
+
Windows 不在支持范围;其他平台跑 `service` 会直接报错退出。
|
|
201
|
+
|
|
202
|
+
## 验证
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
bun run typecheck
|
|
206
|
+
bun run test # vitest + Playwright,首次跑前先 bunx playwright install chromium
|
|
207
|
+
bun run build
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 发布(ADR-0016)
|
|
211
|
+
|
|
212
|
+
推 `v*` tag 触发 `.github/workflows/release.yml`:跑完 typecheck / test / build、打包、`npm pack`、装包冒烟后才 `npm publish`。带 `-` 的预发布 tag(`v0.2.0-beta.1`)发到 `next`,正式版发到 `latest`。发布用 npm trusted publishing(OIDC),仓库里没有 npm token。
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
git tag v0.1.1
|
|
216
|
+
git push origin v0.1.1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- 手动发布(流水线不可用时):`bun run build && bun run package -- --version <v>`,再在 `apps/cli/out` 里 `npm publish`。终端里的 npm 要先 `npm login`(`npm whoami` 能报出用户名);没登录时 npm 回的是 `404 Not Found` 而不是 401,容易误判成包名问题。
|
|
220
|
+
- 发布后几分钟内 registry 可能还没处理完(新包还会先出现一个 `0.0.0-stage` 占位版本);这期间查过的机器缓存了旧元数据,安装报 `ETARGET` / "No matching version" 时加 `--prefer-online`。
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadRelayKey
|
|
3
|
+
} from "./chunk-fxaewke2.js";
|
|
4
|
+
import {
|
|
5
|
+
relayHostId
|
|
6
|
+
} from "./chunk-z4x88s0q.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_DATA_DIR
|
|
9
|
+
} from "./chunk-cjfmfyjb.js";
|
|
10
|
+
import"./chunk-df32avw6.js";
|
|
11
|
+
|
|
12
|
+
// apps/host/src/cli/relay-id.ts
|
|
13
|
+
import { mkdir } from "node:fs/promises";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { parseArgs } from "node:util";
|
|
16
|
+
async function relayId(argv) {
|
|
17
|
+
const { values } = parseArgs({
|
|
18
|
+
args: [...argv],
|
|
19
|
+
options: { "data-dir": { type: "string" } }
|
|
20
|
+
});
|
|
21
|
+
const dataDir = resolve(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
22
|
+
await mkdir(dataDir, { recursive: true });
|
|
23
|
+
return relayHostId((await loadRelayKey(dataDir)).publicKey);
|
|
24
|
+
}
|
|
25
|
+
async function main(argv) {
|
|
26
|
+
console.log(await relayId(argv));
|
|
27
|
+
}
|
|
28
|
+
if (false)
|
|
29
|
+
;
|
|
30
|
+
export {
|
|
31
|
+
relayId,
|
|
32
|
+
main
|
|
33
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
restartService,
|
|
4
|
+
serviceManager
|
|
5
|
+
} from "./chunk-zx77w1nm.js";
|
|
6
|
+
import {
|
|
7
|
+
globalPrefixOf,
|
|
8
|
+
packageDir,
|
|
9
|
+
packagedVersion
|
|
10
|
+
} from "./chunk-fpb6f9n5.js";
|
|
11
|
+
import"./chunk-y73t2jmc.js";
|
|
12
|
+
import"./chunk-wgn3cx5q.js";
|
|
13
|
+
import"./chunk-cjfmfyjb.js";
|
|
14
|
+
import"./chunk-df32avw6.js";
|
|
15
|
+
|
|
16
|
+
// apps/host/src/cli/upgrade.ts
|
|
17
|
+
import { execFile, spawn } from "node:child_process";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { promisify } from "node:util";
|
|
20
|
+
var run = promisify(execFile);
|
|
21
|
+
var repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
22
|
+
var step = (command, args) => new Promise((resolve) => {
|
|
23
|
+
spawn(command, [...args], { cwd: repoRoot, stdio: "inherit" }).once("exit", (code) => resolve(code ?? 1));
|
|
24
|
+
});
|
|
25
|
+
async function main(argv) {
|
|
26
|
+
const force = argv.includes("--force");
|
|
27
|
+
if (packagedVersion === undefined) {
|
|
28
|
+
const { stdout } = await run("git", ["status", "--porcelain"], { cwd: repoRoot });
|
|
29
|
+
if (stdout.trim() !== "") {
|
|
30
|
+
console.error("upgrade: the checkout has uncommitted changes; commit or stash them first");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
for (const [command, args] of [
|
|
34
|
+
["git", ["pull", "--ff-only"]],
|
|
35
|
+
["bun", ["install"]],
|
|
36
|
+
["bun", ["run", "build"]]
|
|
37
|
+
]) {
|
|
38
|
+
const code = await step(command, args);
|
|
39
|
+
if (code !== 0)
|
|
40
|
+
process.exit(code);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
const prefix = globalPrefixOf(packageDir);
|
|
44
|
+
if (prefix === undefined) {
|
|
45
|
+
console.error(`upgrade: pinomad isn't a global npm install (${packageDir}); upgrade it the way it was installed`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const code = await step("npm", ["install", "-g", "--prefix", prefix, "pinomad@latest"]);
|
|
49
|
+
if (code !== 0)
|
|
50
|
+
process.exit(code);
|
|
51
|
+
}
|
|
52
|
+
const installed = await Promise.resolve().then(() => serviceManager().state()).then((state) => state.installed, () => false);
|
|
53
|
+
if (!installed) {
|
|
54
|
+
console.log("service not installed; upgrade done, nothing restarted");
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
process.exit(await restartService({ force, argv: argv.filter((arg) => arg !== "--force") }));
|
|
58
|
+
}
|
|
59
|
+
if (false)
|
|
60
|
+
;
|
|
61
|
+
export {
|
|
62
|
+
main
|
|
63
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
main,
|
|
4
|
+
restartService
|
|
5
|
+
} from "./chunk-zx77w1nm.js";
|
|
6
|
+
import"./chunk-fpb6f9n5.js";
|
|
7
|
+
import"./chunk-y73t2jmc.js";
|
|
8
|
+
import"./chunk-wgn3cx5q.js";
|
|
9
|
+
import"./chunk-cjfmfyjb.js";
|
|
10
|
+
import"./chunk-df32avw6.js";
|
|
11
|
+
export {
|
|
12
|
+
restartService,
|
|
13
|
+
main
|
|
14
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// apps/host/src/cli/host-address.ts
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
var DEFAULT_DATA_DIR = process.env.PINOMAD_DATA_DIR ?? join(homedir(), ".pinomad");
|
|
7
|
+
async function hostAddress(argv) {
|
|
8
|
+
const { values } = parseArgs({
|
|
9
|
+
args: [...argv],
|
|
10
|
+
options: {
|
|
11
|
+
"data-dir": { type: "string" },
|
|
12
|
+
url: { type: "string", default: "ws://127.0.0.1:7420" },
|
|
13
|
+
token: { type: "string" }
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const dataDir = resolve(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
17
|
+
const token = values.token ?? (await readFile(join(dataDir, "token"), "utf8")).trim();
|
|
18
|
+
return { url: values.url, token, dataDir };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { DEFAULT_DATA_DIR, hostAddress };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// apps/host/src/distribution.ts
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
var packagedVersion = "0.1.1";
|
|
4
|
+
var webRoot = fileURLToPath(new URL(packagedVersion === undefined ? "../../web/dist" : "../web/", import.meta.url));
|
|
5
|
+
var hostEntry = packagedVersion === undefined ? { script: fileURLToPath(new URL("./main.ts", import.meta.url)), args: [] } : { script: fileURLToPath(new URL("./pinomad.js", import.meta.url)), args: ["host"] };
|
|
6
|
+
var commandHint = (name) => packagedVersion === undefined ? `bun run ${name}` : `pinomad ${name}`;
|
|
7
|
+
var packageDir = fileURLToPath(new URL("../", import.meta.url));
|
|
8
|
+
var globalPrefixOf = (dir) => /^(.+)\/lib\/node_modules\/pinomad\/?$/.exec(dir)?.[1];
|
|
9
|
+
|
|
10
|
+
export { packagedVersion, webRoot, hostEntry, commandHint, packageDir, globalPrefixOf };
|