@oldsuns/pi-switch 0.2.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/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # pi-switch
2
+
3
+ 维护本地 provider 库并按需同步 [Pi](https://github.com/earendil-works/pi) 配置的终端 TUI:管理 provider / 模型 / 默认模型,以及 Pi session 的列表、预览与删除。
4
+
5
+ CLI:`pi-switch` · npm 包:`@oldsuns/pi-switch` · Node 薄壳 + Rust/napi 原生核心
6
+
7
+ ## 功能
8
+
9
+ - **主页**:provider / model 计数、默认模型、关键路径
10
+ - **配置 (Profiles)**:本地 provider 库与 Pi 启用子集;新建 / 编辑 / 删除 / 复制;在线导入模型
11
+ - **会话 (Sessions)**:浏览 JSONL session、筛选、预览、复制消息、删除
12
+ - **设置 (Settings)**:语言、models.dev 元数据、默认参数、重载、校验、备份、OpenCode 导入
13
+ - 完整库保存在 `~/.pi-switch/providers.json`,只有已启用项写入 Pi 的 `models.json`
14
+
15
+ ## 要求
16
+
17
+ - Node.js `>= 20`
18
+ - 本地构建原生模块需要 Rust 工具链与 `@napi-rs/cli`
19
+ - 预构建目标:macOS (x64/arm64)、Linux (gnu/musl x64)、Windows (msvc x64)
20
+
21
+ ## 安装与运行
22
+
23
+ ```bash
24
+ npm install
25
+ npm run build:native:debug
26
+ node ./bin/pi-switch.js
27
+ ```
28
+
29
+ CLI:
30
+
31
+ ```bash
32
+ pi-switch # 打开 TUI(等同 tui)
33
+ pi-switch tui
34
+ pi-switch doctor # 校验配置与默认模型
35
+ pi-switch --version # / -v
36
+ pi-switch --help # / -h / help
37
+ ```
38
+
39
+ ## 界面与快捷键
40
+
41
+ 全局导航:`j/k` 或方向键移动;菜单中 `Enter` / `l` 进入内容,`h` / `Esc` / `Tab` 回菜单;`?` 帮助;`q` 退出(多数界面 `Ctrl+C` 也退出,会话预览除外)。
42
+
43
+ ### 配置 (Profiles)
44
+
45
+ | 键 | 作用 |
46
+ |----|------|
47
+ | `n` / `e` / `d` / `c` | 新建 / 编辑 / 删除 / 复制当前焦点(provider 或 model) |
48
+ | `Space`(provider) | 加入 / 移出 Pi;`[x]` 已加入,`[ ]` 仅本地 |
49
+ | `Space`(model) | 设为默认模型(仅已加入 Pi 的 provider) |
50
+ | `i` | 从当前 provider 在线导入模型 |
51
+ | `/` | 筛选 provider |
52
+ | `Enter` / `l` | 进入模型列表 |
53
+ | `Esc` / `h` | 从模型回到 provider,或从 provider 回菜单 |
54
+ | `r` | 从磁盘重载配置 |
55
+ | `b` | 浏览备份 |
56
+ | `v` | 校验配置(doctor) |
57
+
58
+ 新建 provider 默认加入 Pi,表单可关闭;复制继承源的加入状态。
59
+
60
+ Provider 表单:`baseUrl`、`api`(`openai-completions` / `openai-responses` / `anthropic-messages` / `google-generative-ai`)、`apiKey`、`authHeader`、Headers(独立 `User-Agent` + 其余 JSON)、`compat`(含一等开关 Session affinity = `sendSessionAffinityHeaders`)。
61
+
62
+ Model 表单:`id`、`name`、API override、reasoning、文本/图像输入、context window、max tokens。`cost`、`thinkingLevelMap`、`modelOverrides`、OAuth 等未知字段会无损保留。字段语义以 [Pi Custom Models](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/models.md) 为准。
63
+
64
+ ### 会话 (Sessions)
65
+
66
+ | 键 | 作用 |
67
+ |----|------|
68
+ | `/` | 筛选 |
69
+ | `n` | 仅显示手动命名的 session(不是新建) |
70
+ | `r` | 重新扫描磁盘 |
71
+ | `u` | 预览仅用户消息 |
72
+ | `Enter` / `Right` | 进入右侧预览 |
73
+ | 预览 `Up` / `Down` | 按消息切换 |
74
+ | 滚轮 / `PageUp` / `PageDown` | 按行滚动长消息 |
75
+ | 预览 `Ctrl+C` | 复制当前消息到剪贴板(不退出) |
76
+ | `d` | 删除当前 session(确认后优先 trash) |
77
+
78
+ 黄标题 = 手动命名;白标题 = 使用第一条用户消息。
79
+
80
+ Session 根目录优先级:`PI_CODING_AGENT_SESSION_DIR` → `PI_CODING_AGENT_DIR/sessions` → `~/.pi/agent/sessions/`。
81
+
82
+ 剪贴板:Windows `clip`、macOS `pbcopy`、Linux `wl-copy` / `xclip` / `xsel`。
83
+
84
+ ### 设置 (Settings)
85
+
86
+ | 项 | 说明 |
87
+ |----|------|
88
+ | 语言 | English / 中文(`en` / `zh-CN`) |
89
+ | 从 models.dev 获取模型信息 | 开关实时元数据(默认开) |
90
+ | 默认模型参数 | 仅关闭实时元数据时显示,用于导入缺省 |
91
+ | 重载配置 | 从磁盘重读 |
92
+ | 验证配置 | doctor |
93
+ | 浏览备份 | 恢复 version 2 备份 |
94
+ | 从 OpenCode 导入 | 只读导入 `opencode.json` |
95
+
96
+ `Enter` / `Space` 执行当前项。
97
+
98
+ ## Provider 库与 Pi 同步
99
+
100
+ - `~/.pi-switch/providers.json` 是完整本地库;`~/.pi/agent/models.json` 只含当前加入 Pi 的子集。
101
+ - 首次运行把现有 `models.json` 全量导入本地库,不修改 Pi 配置。
102
+ - 已加入 provider 的编辑与 model 变更会同步两份文件;仅本地项只更新本地库。
103
+ - 在线导入 model **不会**隐式加入 Pi。
104
+ - 启动或手动重载时,以 `models.json` 中同 ID provider 为准回灌本地库;外部从 Pi 删除的 provider 仍作为仅本地项保留。
105
+ - 从 Pi 移除当前默认 provider 会先确认并清除默认模型;`d` 永久删除本地副本,必要时同时从 Pi 删除。
106
+
107
+ ## 模型导入与价格
108
+
109
+ 在线导入(Profiles 中 `i`):
110
+
111
+ 1. 按 provider 的 `api` / `baseUrl` / 鉴权请求模型列表。
112
+ 2. **NewAPI 网关价格(best-effort)**:去掉 `baseUrl` 尾部 `/v1` 后依次尝试
113
+ - `GET /api/ratio_config`(可能含 `create_cache_ratio`)
114
+ - `GET /api/pricing`
115
+ 成功则按 NewAPI 换算覆盖模型 `cost`(`1 USD = 500_000 quota`,每 1M tokens 成本 ≈ `ratio × 2` USD);失败静默忽略。
116
+ 3. 若开启 models.dev 元数据:请求 `https://models.dev/api.json`,补全 `contextWindow`、`maxTokens`、`cost`、reasoning 等。
117
+ - **在线导入**遇到同 model ID 多源歧义时:**自动取第一个候选**,不弹选择框;缺少可用元数据的模型跳过并提示计数。
118
+ - 导入列表会标注价格来源:`ratio_config` 或 `models.dev`。
119
+ - 网关价格在 catalog 元数据之上叠加。
120
+ 4. 关闭实时元数据:使用 Settings 中的默认参数;空字段回落 Pi 官方默认(context window `128000`、max tokens `16384`、cost `0`)。
121
+
122
+ **OpenCode 导入**(Settings):只读 `~/.config/opencode/opencode.json`,可全选或勾选 provider;导入项默认加入 Pi。若 models.dev 仍有歧义,**需要用户选择候选**。OpenCode 配置本身不会被修改。
123
+
124
+ ## 配置路径与 Settings 字段
125
+
126
+ | 路径 | 角色 |
127
+ |------|------|
128
+ | `~/.pi-switch/providers.json` | 完整本地 provider 库(`version: 1`) |
129
+ | `~/.pi/agent/models.json` | 已加入 Pi 的 provider 子集 |
130
+ | `~/.pi/agent/settings.json` | 默认模型 + pi-switch 设置 |
131
+ | `~/.config/opencode/opencode.json` | OpenCode 只读导入源 |
132
+ | `~/.pi-switch/backups/` | version 2 备份(providers + models + settings),最多 10 份 |
133
+ | `~/.pi-switch/write.lock` | 写入互斥锁 |
134
+
135
+ `settings.json` 中与 pi-switch 相关的字段:
136
+
137
+ | 字段 | 含义 |
138
+ |------|------|
139
+ | `defaultProvider` + `defaultModel` | 默认模型(成对存在或同时缺省) |
140
+ | `piSwitch.language` | `en` \| `zh-CN` |
141
+ | `piSwitch.fetchModelMetadata` | 是否拉 models.dev(默认 `true`) |
142
+ | `piSwitch.modelDefaults` | 关闭实时元数据时的导入缺省(context / maxTokens / cost) |
143
+
144
+ ## 数据安全
145
+
146
+ - 写前备份 `providers.json`、`models.json`、`settings.json` 到 `~/.pi-switch/backups/`(version 2);最多保留最近 10 份。旧版双文件备份不支持恢复。
147
+ - 写入使用 `write.lock` 互斥;异常残留锁时 `doctor` 会提示。
148
+ - `providers.json` 损坏时归档为 `corrupt-providers-*.json`,再从当前 Pi 配置重建,启动时显示归档路径。
149
+ - 原子写入,只 patch 目标字段,保留未知 JSON;格式错误时停止写入并显示错误。
150
+ - 支持 Pi 的 `$ENV` / `${ENV}` 插值与 `$$` / `$!` 转义;`!command` 原样保存,在线拉取**不会**执行它。
151
+ - Session 删除只作用于选中的 JSONL,并校验路径必须位于 session 根目录内;优先调用系统 `trash`,失败后再永久删除。
152
+ - 正常退出、错误和 panic 都会恢复 raw mode、alternate screen 与光标。
153
+
154
+ ## 验证
155
+
156
+ ```bash
157
+ cargo test --locked --lib
158
+ cargo fmt -- --check
159
+ cargo clippy --locked --all-targets -- -D warnings
160
+ npm run build:native:debug
161
+ npm run pack:check
162
+ ```
163
+
164
+ ## 许可
165
+
166
+ MIT
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { doctor, runTui, version } from "../index.js";
3
+
4
+ const command = process.argv[2];
5
+
6
+ if (command === "--help" || command === "-h" || command === "help") {
7
+ console.log(`pi-switch ${version()}
8
+
9
+ Usage:
10
+ pi-switch open the terminal UI
11
+ pi-switch doctor validate Pi documents and defaults
12
+ pi-switch --version print the native module version
13
+ `);
14
+ } else if (command === "--version" || command === "-v") {
15
+ console.log(version());
16
+ } else if (command === "doctor") {
17
+ const checks = doctor();
18
+ for (const check of checks) {
19
+ console.log(`${check.ok ? "OK" : "!!"} ${check.label}: ${check.detail}`);
20
+ }
21
+ if (checks.some((check) => !check.ok)) process.exitCode = 1;
22
+ } else if (!command || command === "tui") {
23
+ runTui();
24
+ } else {
25
+ console.error("Usage: pi-switch [tui|doctor|--version]");
26
+ process.exitCode = 2;
27
+ }
package/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface DoctorCheck {
2
+ ok: boolean;
3
+ label: string;
4
+ detail: string;
5
+ }
6
+
7
+ export function version(): string;
8
+ export function doctor(): DoctorCheck[];
9
+ export function runTui(): void;
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import binding from "./pi-switch-native.cjs";
2
+
3
+ export const version = binding.version;
4
+ export const doctor = binding.doctor;
5
+ export const runTui = binding.runTui;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@oldsuns/pi-switch",
3
+ "version": "0.2.1",
4
+ "description": "A focused terminal UI for Pi provider and model configuration",
5
+ "type": "module",
6
+ "bin": {
7
+ "pi-switch": "./bin/pi-switch.js"
8
+ },
9
+ "main": "./index.js",
10
+ "types": "./index.d.ts",
11
+ "files": [
12
+ "bin",
13
+ "index.js",
14
+ "index.d.ts",
15
+ "pi-switch-native.cjs",
16
+ "pi-switch-native.*.node"
17
+ ],
18
+ "napi": {
19
+ "binaryName": "pi-switch-native",
20
+ "targets": [
21
+ "x86_64-apple-darwin",
22
+ "aarch64-apple-darwin",
23
+ "x86_64-unknown-linux-gnu",
24
+ "x86_64-unknown-linux-musl",
25
+ "x86_64-pc-windows-msvc"
26
+ ]
27
+ },
28
+ "scripts": {
29
+ "build:native": "napi build --platform --release --dts native.d.ts",
30
+ "build:native:debug": "napi build --platform --dts native.d.ts",
31
+ "test": "cargo test --locked --lib",
32
+ "check": "cargo fmt --check && cargo clippy --locked --all-targets -- -D warnings",
33
+ "pack:check": "npm pack --dry-run"
34
+ },
35
+ "keywords": [
36
+ "pi",
37
+ "tui",
38
+ "provider",
39
+ "models"
40
+ ],
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "devDependencies": {
46
+ "@napi-rs/cli": "^3.7.1"
47
+ }
48
+ }
@@ -0,0 +1,20 @@
1
+ const { resolve } = require("node:path");
2
+
3
+ function linuxLibc() {
4
+ if (process.platform !== "linux") return "";
5
+ const report = process.report?.getReport?.();
6
+ if (report?.header?.glibcVersionRuntime) return "gnu";
7
+ if (report?.sharedObjects?.some((path) => path.includes("musl"))) return "musl";
8
+ throw new Error("Unable to determine Linux libc (glibc or musl)");
9
+ }
10
+
11
+ function targetSuffix() {
12
+ const { platform, arch } = process;
13
+ if (platform === "win32" && arch === "x64") return "win32-x64-msvc";
14
+ if (platform === "darwin" && ["x64", "arm64"].includes(arch)) return `darwin-${arch}`;
15
+ if (platform === "linux" && arch === "x64") return `linux-x64-${linuxLibc()}`;
16
+ throw new Error(`Unsupported platform: ${platform}-${arch}`);
17
+ }
18
+
19
+ const filename = `pi-switch-native.${targetSuffix()}.node`;
20
+ module.exports = require(resolve(__dirname, filename));