@wax0629/pi-manager 0.1.0

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 wax0629
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.md ADDED
@@ -0,0 +1,93 @@
1
+ # Pi Manager
2
+
3
+ Pi 的本地控制面。不 fork Pi,不改项目 `.pi`。在页面里管供应商、模型和 Thinking,确认后备份并写入 `~/.pi/agent`,给普通 `pi` 命令用。
4
+
5
+ ## 能做什么
6
+
7
+ - **供应商与账号**:新增 OpenAI 兼容中转、从本机 Pi 勾选导入、Pi 原生登录。新增时可请求上游 `/models`,勾选后再保存。
8
+ - **模型资源库**:完整目录、Ctrl+P 循环列表、默认模型、Thinking 映射、Context 窗口。
9
+ - **导入本机 Pi**:备份后合并写入 `~/.pi/agent` 的 `settings.json` / `models.json` / `auth.json`,可回滚。
10
+
11
+ 日常路径:改候选配置 → **导入本机 Pi** → 退出当前 Pi 再进。`/reload` 不会重读模型和循环列表。
12
+
13
+ ## 不做什么
14
+
15
+ - 不 fork、不改 Pi 源码。
16
+ - 不改用户项目里的 `.pi`。
17
+ - 不把 refresh token 拷进 Manager。
18
+ - 不做 Antigravity 专用 OAuth;Antigravity 当本机 OpenAI 兼容桥(`127.0.0.1:8045/v1`)。
19
+
20
+ ## 要求
21
+
22
+ - Node 20+
23
+ - 已安装官方 [Pi](https://github.com/badlogic/pi-mono)(`pi` 在 PATH 里)
24
+
25
+ ## 运行
26
+
27
+ ```bash
28
+ npx @wax0629/pi-manager
29
+ ```
30
+
31
+ 或全局安装:
32
+
33
+ ```bash
34
+ npm i -g @wax0629/pi-manager
35
+ pi-manager
36
+ ```
37
+
38
+ 会起 `http://127.0.0.1:8670` 并打开浏览器。`pi-manager --help` 看 `--port` / `--host` / `--no-open`。端口已被占用时,会打开已有实例,不报崩。
39
+
40
+ 从源码跑(开发):
41
+
42
+ ```bash
43
+ npm start # API http://127.0.0.1:8670
44
+ npm run dev:web # UI http://localhost:5173/
45
+ ```
46
+
47
+ 从源码走 CLI 时先 `npm --prefix web run build`,再 `npx pi-manager`。
48
+
49
+ ## 日常用法
50
+
51
+ 1. 打开供应商页:新增 API,或从本机 Pi 勾选导入,或登录原生渠道。
52
+ 2. 打开模型资源库:核对目录、默认模型、循环列表、Thinking。
53
+ 3. 打开「导入本机 Pi」,确认后写入 `~/.pi/agent`。
54
+ 4. 退出正在跑的 Pi,重新打开。用 `pi --list-models --offline` 核对。
55
+
56
+ 导坏了用同一页的 **回滚本机 Pi**。
57
+
58
+ ## 数据放哪
59
+
60
+ | 路径 | 用途 |
61
+ | --- | --- |
62
+ | `~/.pi-manager/` | Manager 状态、密钥存储、导入备份 |
63
+ | `~/.pi/agent/` | 本机 Pi 真正读取的配置 |
64
+
65
+ 密钥不进 `state.json`,也不在 API 响应里回显。
66
+
67
+ ## 开发
68
+
69
+ ```bash
70
+ npm test
71
+ npm run check
72
+ npm --prefix web run lint
73
+ npm --prefix web run build
74
+ ```
75
+
76
+ `web/dist` 会打进 npm 包,所以跟踪在 git 里。发版前再 build 一次。
77
+
78
+ 设计草案仍在 `docs/`,以本 README 和当前 UI 为准。隔离 Profile 的 apply / launch / stop 后端还在,页面已拿掉,不是日常路径。
79
+
80
+ ## 社区
81
+
82
+ 这不是 Pi extension / skill / theme,**不能**进 [pi.dev/packages](https://pi.dev/packages)(那里只收 `pi install npm:` 的包)。
83
+
84
+ 可以发到:
85
+
86
+ - Discord:https://discord.com/invite/3cU7Bz4UPx
87
+ - GitHub Discussions(分享工具):https://github.com/earendil-works/pi/discussions
88
+
89
+ npm 包:[`@wax0629/pi-manager`](https://www.npmjs.com/package/@wax0629/pi-manager)。未 scoped 的 `pi-manager` 于 2022 年下架,没有抢回。
90
+
91
+ ## License
92
+
93
+ MIT
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@wax0629/pi-manager",
3
+ "version": "0.1.0",
4
+ "description": "Local control plane for Pi: providers, models, and live import into ~/.pi/agent.",
5
+ "type": "module",
6
+ "bin": {
7
+ "pi-manager": "src/cli.mjs"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "web/dist/index.html",
12
+ "web/dist/assets",
13
+ "web/dist/favicon.svg",
14
+ "web/dist/icons.svg",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "start": "node src/server.mjs",
20
+ "pi-manager": "node src/cli.mjs",
21
+ "dev": "node --watch src/server.mjs",
22
+ "dev:web": "npm --prefix web run dev",
23
+ "build:web": "npm --prefix web run build",
24
+ "prepublishOnly": "npm --prefix web install && npm --prefix web run build",
25
+ "test": "node --test",
26
+ "check": "node --check src/server.mjs && node --check src/store.mjs && node --check src/profile.mjs && node --check src/gateway.mjs && node --check src/pi-auth.mjs && node --check src/pi-import.mjs && node --check src/provider-discovery.mjs && node --check src/pi-apply.mjs && node --check src/pi-native.mjs && node --check src/cli.mjs"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "license": "MIT",
32
+ "author": "wax0629",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/wax0629/pi-manager.git"
36
+ },
37
+ "homepage": "https://github.com/wax0629/pi-manager",
38
+ "bugs": {
39
+ "url": "https://github.com/wax0629/pi-manager/issues"
40
+ },
41
+ "keywords": [
42
+ "pi",
43
+ "coding-agent",
44
+ "openai-compatible",
45
+ "local-first",
46
+ "model-management"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const HELP = `Pi Manager — 本地控制面
8
+
9
+ 用法:
10
+ pi-manager [选项]
11
+
12
+ 选项:
13
+ --host <地址> 监听地址(默认 127.0.0.1,或 PI_MANAGER_HOST)
14
+ --port <端口> 监听端口(默认 8670,或 PI_MANAGER_PORT)
15
+ --no-open 不打开浏览器
16
+ -h, --help 显示帮助
17
+
18
+ 缺 web/dist 时先运行: npm --prefix web run build
19
+ `;
20
+
21
+ export function parseCliArgs(argv) {
22
+ const args = { host: undefined, port: undefined, open: true, help: false };
23
+ for (let i = 0; i < argv.length; i += 1) {
24
+ const token = argv[i];
25
+ if (token === "--help" || token === "-h") {
26
+ args.help = true;
27
+ continue;
28
+ }
29
+ if (token === "--no-open") {
30
+ args.open = false;
31
+ continue;
32
+ }
33
+ if (token === "--host") {
34
+ args.host = requireValue(argv, ++i, "--host");
35
+ continue;
36
+ }
37
+ if (token.startsWith("--host=")) {
38
+ args.host = token.slice("--host=".length);
39
+ continue;
40
+ }
41
+ if (token === "--port" || token === "-p") {
42
+ args.port = parsePort(requireValue(argv, ++i, token));
43
+ continue;
44
+ }
45
+ if (token.startsWith("--port=")) {
46
+ args.port = parsePort(token.slice("--port=".length));
47
+ continue;
48
+ }
49
+ throw new Error(`未知参数: ${token}`);
50
+ }
51
+ return args;
52
+ }
53
+
54
+ function requireValue(argv, index, flag) {
55
+ const value = argv[index];
56
+ if (!value || value.startsWith("-")) throw new Error(`${flag} 需要一个值`);
57
+ return value;
58
+ }
59
+
60
+ function parsePort(value) {
61
+ const port = Number(value);
62
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
63
+ throw new Error(`无效端口: ${value}`);
64
+ }
65
+ return port;
66
+ }
67
+
68
+ export function hasWebUi(publicDir) {
69
+ return fs.existsSync(path.join(publicDir, "index.html"));
70
+ }
71
+
72
+ export function browserCommand(url, platform = process.platform) {
73
+ if (platform === "darwin") return { command: "open", args: [url] };
74
+ if (platform === "win32") return { command: "cmd", args: ["/c", "start", "", url] };
75
+ return { command: "xdg-open", args: [url] };
76
+ }
77
+
78
+ export function openBrowser(url, { execFileImpl = execFile, platform = process.platform } = {}) {
79
+ const { command, args } = browserCommand(url, platform);
80
+ return new Promise((resolve, reject) => {
81
+ execFileImpl(command, args, (error) => error ? reject(error) : resolve());
82
+ });
83
+ }
84
+
85
+ export function publicUrl(host, port) {
86
+ const hostname = host === "0.0.0.0" ? "127.0.0.1" : host;
87
+ return `http://${hostname}:${port}`;
88
+ }
89
+
90
+ async function main(argv = process.argv.slice(2)) {
91
+ let args;
92
+ try {
93
+ args = parseCliArgs(argv);
94
+ } catch (error) {
95
+ console.error(error instanceof Error ? error.message : String(error));
96
+ process.exitCode = 1;
97
+ return 1;
98
+ }
99
+ if (args.help) {
100
+ process.stdout.write(HELP);
101
+ return 0;
102
+ }
103
+ if (args.host) process.env.PI_MANAGER_HOST = args.host;
104
+ if (args.port) process.env.PI_MANAGER_PORT = String(args.port);
105
+
106
+ const { startManagerServer, publicDir, store } = await import("./server.mjs");
107
+ if (!hasWebUi(publicDir)) {
108
+ console.error("缺少 web/dist/index.html。先运行: npm --prefix web run build");
109
+ process.exitCode = 1;
110
+ return 1;
111
+ }
112
+
113
+ const host = process.env.PI_MANAGER_HOST || "127.0.0.1";
114
+ const port = Number(process.env.PI_MANAGER_PORT || 8670);
115
+ const url = publicUrl(host, port);
116
+ let started = false;
117
+ try {
118
+ await startManagerServer({ host, port });
119
+ started = true;
120
+ const gateway = store.get().gateway;
121
+ console.log(`Pi Manager ${url}`);
122
+ console.log(`Pi gateway: http://${gateway.host}:${gateway.port}/v1`);
123
+ } catch (error) {
124
+ if (error && error.code === "EADDRINUSE") {
125
+ console.log(`Pi Manager 已在运行: ${url}`);
126
+ } else {
127
+ console.error(`Pi Manager 启动失败: ${error instanceof Error ? error.message : String(error)}`);
128
+ process.exitCode = 1;
129
+ return 1;
130
+ }
131
+ }
132
+
133
+ if (args.open) {
134
+ try {
135
+ await openBrowser(url);
136
+ } catch (error) {
137
+ console.error(`无法打开浏览器: ${error instanceof Error ? error.message : String(error)}`);
138
+ console.error(`请手动打开 ${url}`);
139
+ }
140
+ }
141
+
142
+ if (!started) return 0;
143
+ await new Promise(() => {});
144
+ return 0;
145
+ }
146
+
147
+ const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
148
+ if (isDirectRun) {
149
+ main().catch((error) => {
150
+ console.error(error instanceof Error ? error.message : String(error));
151
+ process.exitCode = 1;
152
+ });
153
+ }
154
+
155
+ export { main, HELP };
@@ -0,0 +1,215 @@
1
+ import crypto from "node:crypto";
2
+ import path from "node:path";
3
+ import { buildThinkingLevelMap } from "./thinking.mjs";
4
+
5
+ function catalogModel(model) {
6
+ return {
7
+ ...model,
8
+ thinkingLevelMap: buildThinkingLevelMap(model),
9
+ thinkingMapSource: "provider-default",
10
+ thinkingMapVerified: false
11
+ };
12
+ }
13
+
14
+ const qiniuModels = [
15
+ {
16
+ id: "gpt-5.6-sol",
17
+ name: "GPT-5.6 Sol",
18
+ family: "GPT-5.6",
19
+ reasoning: true,
20
+ thinkingLevels: ["off", "low", "medium", "high", "xhigh"],
21
+ input: ["text", "image"],
22
+ contextWindow: 1050000,
23
+ maxTokens: 128000,
24
+ cost: { input: 5, output: 20, cacheRead: 0.5, cacheWrite: 5 }
25
+ },
26
+ {
27
+ id: "gpt-5.6-luna",
28
+ name: "GPT-5.6 Luna",
29
+ family: "GPT-5.6",
30
+ reasoning: true,
31
+ thinkingLevels: ["off", "low", "medium", "high", "xhigh"],
32
+ input: ["text", "image"],
33
+ contextWindow: 1050000,
34
+ maxTokens: 128000,
35
+ cost: { input: 4, output: 16, cacheRead: 0.4, cacheWrite: 4 }
36
+ },
37
+ {
38
+ id: "gpt-5.6-terra",
39
+ name: "GPT-5.6 Terra",
40
+ family: "GPT-5.6",
41
+ reasoning: true,
42
+ thinkingLevels: ["off", "low", "medium", "high", "xhigh"],
43
+ input: ["text", "image"],
44
+ contextWindow: 1050000,
45
+ maxTokens: 128000,
46
+ cost: { input: 4, output: 16, cacheRead: 0.4, cacheWrite: 4 }
47
+ },
48
+ {
49
+ id: "grok-4.6",
50
+ name: "Grok 4.6",
51
+ family: "Grok",
52
+ reasoning: true,
53
+ thinkingLevels: ["low", "medium", "high", "xhigh"],
54
+ input: ["text", "image"],
55
+ contextWindow: 500000,
56
+ maxTokens: 64000,
57
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 }
58
+ }
59
+ ].map(catalogModel);
60
+
61
+ const antigravityModels = [
62
+ {
63
+ id: "gemini-3.8-flash-tiered",
64
+ name: "Gemini 3.8 Flash Tiered",
65
+ family: "Gemini",
66
+ reasoning: true,
67
+ thinkingLevels: ["off", "low", "medium", "high"],
68
+ input: ["text", "image"],
69
+ contextWindow: 1000000,
70
+ maxTokens: 32000,
71
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
72
+ },
73
+ {
74
+ id: "gemini-3.1-pro-high",
75
+ name: "Gemini 3.1 Pro High",
76
+ family: "Gemini",
77
+ reasoning: true,
78
+ thinkingLevels: ["off", "low", "medium", "high"],
79
+ input: ["text", "image"],
80
+ contextWindow: 1000000,
81
+ maxTokens: 32000,
82
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
83
+ }
84
+ ].map(catalogModel);
85
+
86
+ const codexModels = [
87
+ {
88
+ id: "gpt-5.6-luna",
89
+ name: "GPT-5.6 Luna",
90
+ family: "GPT-5.6",
91
+ reasoning: true,
92
+ thinkingLevels: ["off", "low", "medium", "high", "xhigh", "max"],
93
+ input: ["text", "image"],
94
+ contextWindow: 1050000,
95
+ maxTokens: 128000,
96
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
97
+ },
98
+ {
99
+ id: "gpt-5.6-sol",
100
+ name: "GPT-5.6 Sol",
101
+ family: "GPT-5.6",
102
+ reasoning: true,
103
+ thinkingLevels: ["off", "low", "medium", "high", "xhigh", "max"],
104
+ input: ["text", "image"],
105
+ contextWindow: 1050000,
106
+ maxTokens: 128000,
107
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
108
+ },
109
+ {
110
+ id: "gpt-5.6-terra",
111
+ name: "GPT-5.6 Terra",
112
+ family: "GPT-5.6",
113
+ reasoning: true,
114
+ thinkingLevels: ["off", "low", "medium", "high", "xhigh", "max"],
115
+ input: ["text", "image"],
116
+ contextWindow: 1050000,
117
+ maxTokens: 128000,
118
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
119
+ }
120
+ ].map(catalogModel);
121
+
122
+ export function createSeedProviders({ projectRoot }) {
123
+ const bridgePath = path.join(projectRoot, "antigravity-bridge");
124
+ return [
125
+ {
126
+ id: "qiniu",
127
+ name: "七牛云",
128
+ kind: "openai-api",
129
+ baseUrl: "https://llmapi.qiniu.io/v1",
130
+ credentialEnv: "QINIU_API_KEY",
131
+ description: "OpenAI 兼容 API",
132
+ models: qiniuModels
133
+ },
134
+ {
135
+ id: "antigravity",
136
+ name: "Google Antigravity",
137
+ kind: "local-bridge",
138
+ baseUrl: "http://127.0.0.1:8045/v1",
139
+ bridgePath,
140
+ credentialEnv: "API_KEY",
141
+ description: "本地订阅桥接",
142
+ models: antigravityModels
143
+ },
144
+ {
145
+ id: "openai-codex",
146
+ name: "OpenAI Codex",
147
+ kind: "native-subscription",
148
+ piProvider: "openai-codex",
149
+ description: "Pi 原生官方订阅",
150
+ models: codexModels
151
+ }
152
+ ];
153
+ }
154
+
155
+ export function seedStoreProviders(store, { projectRoot } = {}) {
156
+ const root = projectRoot || store.get().targetProject;
157
+ store.update((state) => {
158
+ state.providers = createSeedProviders({ projectRoot: root });
159
+ state.active = {
160
+ providerId: "qiniu",
161
+ modelId: "gpt-5.6-luna",
162
+ thinking: "medium"
163
+ };
164
+ state.cycle = {
165
+ modelRefs: [
166
+ "qiniu/gpt-5.6-luna",
167
+ "openai-codex/gpt-5.6-luna",
168
+ "qiniu/gpt-5.6-sol"
169
+ ]
170
+ };
171
+ });
172
+ return store;
173
+ }
174
+
175
+ export function createDefaultState({ projectRoot }) {
176
+ return {
177
+ version: 1,
178
+ targetProject: projectRoot,
179
+ active: {
180
+ providerId: "",
181
+ modelId: "",
182
+ thinking: "off"
183
+ },
184
+ cycle: {
185
+ modelRefs: []
186
+ },
187
+ gateway: {
188
+ enabled: true,
189
+ host: "127.0.0.1",
190
+ port: 8675,
191
+ clientKey: crypto.randomBytes(24).toString("base64url")
192
+ },
193
+ providers: [],
194
+ runtime: {
195
+ profilePath: "",
196
+ extensionPath: "",
197
+ configRevision: 1,
198
+ appliedRevision: 0,
199
+ appliedSnapshot: null,
200
+ lastAppliedAt: null,
201
+ lastLaunchAt: null,
202
+ lastLaunchPid: null,
203
+ lastStopAt: null,
204
+ lastError: null,
205
+ lastLiveImportAt: null,
206
+ lastLiveBackupDir: "",
207
+ lastLiveVerify: null,
208
+ events: []
209
+ }
210
+ };
211
+ }
212
+
213
+ export function clone(value) {
214
+ return JSON.parse(JSON.stringify(value));
215
+ }