proofbridge-cli 1.0.2

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,48 @@
1
+ # AGPL-3.0 - GNU Affero General Public License v3.0
2
+
3
+ ## English
4
+
5
+ This software is licensed under the AGPL-3.0 license, which means:
6
+
7
+ - **Attribution Required**: You must give appropriate credit, provide a link to the license, and indicate if changes were made. When using Claude Code Bridge (CCB), please credit the original project.
8
+
9
+ - **Open Source Required**: If you modify this software and distribute it or run it as a service, you must release your source code under AGPL-3.0.
10
+
11
+ - **Network Use (Copyleft)**: If you run this software as a network service (e.g., SaaS), users interacting with it over the network must be able to receive the source code.
12
+
13
+ - **No Closed-Source Use**: You cannot use this software in proprietary/closed-source projects unless you open-source the entire project under AGPL-3.0.
14
+
15
+ **In short**: You can use Claude Code Bridge for free, but if you build upon it, your code must also be open-sourced under AGPL-3.0 with attribution to this project. Closed-source commercial use requires a separate license.
16
+
17
+ For commercial licensing inquiries (closed-source use), please contact the maintainer.
18
+
19
+ ---
20
+
21
+ ## 中文
22
+
23
+ 本软件采用 AGPL-3.0 许可协议,这意味着:
24
+
25
+ - **署名要求**:您必须注明出处,提供许可协议链接,并说明是否进行了修改。使用 Claude Code Bridge (CCB) 时,请注明项目来源。
26
+
27
+ - **开源要求**:如果您修改此软件并将其分发或作为服务运行,则必须根据 AGPL-3.0 发布您的源代码。
28
+
29
+ - **网络使用(Copyleft)**:如果您将此软件作为网络服务(例如 SaaS)运行,则通过网络与其交互的用户必须能够接收源代码。
30
+
31
+ - **禁止闭源使用**:您不能在专有/闭源项目中使用此软件,除非您将整个项目根据 AGPL-3.0 开源。
32
+
33
+ **简单来说**:您可以免费使用 Claude Code Bridge,但如果您基于它进行开发,您的代码也必须根据 AGPL-3.0 开源,并注明本项目。闭源商业用途需要单独的许可证。
34
+
35
+ 对于商业许可咨询(闭源使用),请联系维护者。
36
+
37
+ ---
38
+
39
+ ## Full License Text
40
+
41
+ GNU AFFERO GENERAL PUBLIC LICENSE
42
+ Version 3, 19 November 2007
43
+
44
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
45
+
46
+ Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
47
+
48
+ For the complete license text, see: https://www.gnu.org/licenses/agpl-3.0.txt
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # ProofBridge
2
+
3
+ 在一个终端工作区里使用 Claude Code、Codex、Gemini 等 AI 编程助手,用网页管理项目配置,让多个助手分工协作。
4
+
5
+ - **统一工作区**:在同一项目中启动和切换多个助手。
6
+ - **网页配置**:添加助手、选择平台,在中央画布中安排窗口与分屏。
7
+ - **一步完成设置**:自动检查、保存并应用配置,明确提示结果和必要的重启操作。
8
+ - **协作与记忆**:助手之间可以传递任务,项目记忆保存在 `.pb/pb_memory.md`。
9
+
10
+ ## 安装
11
+
12
+ 通过 npm 全局安装:
13
+
14
+ ```bash
15
+ npm install -g proofbridge-cli
16
+ pb --print-version
17
+ ```
18
+
19
+ 包内包含桌面运行文件,无需 Rust;支持 macOS x64/arm64,以及 Linux x64(glibc 2.35+,不含 Alpine)。仍需 Node.js 18+、Python 3.10+、tmux 和已登录的 AI 编程 CLI,首次启动会联网准备 Python 依赖。
20
+
21
+ 以下为 macOS / Linux 源码安装方式。需要 Git、Python 3.10+、tmux、Rust 工具链(`cargo`、`rustc`)及本机编译工具(macOS Command Line Tools / Linux C/C++ 编译器与链接器),以及至少一个已经安装并完成登录的 AI 编程 CLI。
22
+
23
+ 源码安装会在有 Go 1.26.6+、Node.js 20.19+ 时一并构建用量页和采集器;缺少它们时,助手和配置功能仍可使用。
24
+
25
+ ```bash
26
+ git clone https://github.com/Atingaii/proofBridgeCli.git
27
+ cd proofBridgeCli
28
+ bash ./install.sh install
29
+ pb --print-version
30
+ ```
31
+
32
+ 仓库为私有时,请先配置 GitHub 访问凭据。安装器会准备 Python 依赖,并将命令放入 `~/.local/bin`;如果找不到 `pb`,请把该目录加入 `PATH`:
33
+
34
+ ```bash
35
+ export PATH="$HOME/.local/bin:$PATH"
36
+ ```
37
+
38
+ 源码安装会引用这个目录,请保留它。Windows x64 用户可在仓库目录运行 `.\install.ps1 install -Yes`,需预先准备 Python 3.10+、WezTerm、Git Bash 和 Herdr 0.8.0+。
39
+
40
+ ## 开始使用
41
+
42
+ 进入你要工作的项目目录,启动 ProofBridge:
43
+
44
+ ```bash
45
+ cd /path/to/your-project
46
+ mkdir -p .pb
47
+ pb
48
+ ```
49
+
50
+ 首次启动会选择本机可用的助手;也可以先打开配置页,自行选择:
51
+
52
+ ```bash
53
+ pb config ui
54
+ ```
55
+
56
+ 在助手的终端中直接输入任务。需要协作时,可使用 `/ask <助手名称> <任务>`,例如 `/ask reviewer 检查当前改动`;接收方须已在项目中配置。
57
+
58
+ ## 网页配置
59
+
60
+ 在项目目录运行 `pb config ui` 后,按终端提示打开本地页面:
61
+
62
+ 1. 添加助手,设置名称并选择 Codex、Claude Code 等平台。
63
+ 2. 在中央画布中拖动分屏、调整比例,或添加窗口。
64
+ 3. 点击 **完成设置**,页面会检查、保存并显示实际应用状态。
65
+
66
+ 配置保存在 `.pb/pb.config`。模型、思考强度、Base URL 和 API Key 由原生客户端或 CC Switch 管理;切换后需结束当前 PB 工作区,再重新启动以读取新配置。退出工作区使用 `pb kill`;下次运行 `pb` 时会尝试恢复已有会话。
67
+
68
+ 配置页的 **用量** 入口自动发现本机可读目录中的 Codex、Claude Code 和 PB/CCB 日志,包含独立客户端的记录,并按会话启动目录归类。可按时间、项目和模型筛选,查看明细并导出 CSV;费用按随程序提供的 API 价格估算,并非实际账单。其他平台没有可解析日志时不会显示虚构用量。
69
+
70
+ ## 常用命令
71
+
72
+ | 命令 | 用途 |
73
+ | --- | --- |
74
+ | `pb` | 启动当前项目工作区 |
75
+ | `pb config ui` | 打开网页配置 |
76
+ | `pb config validate` | 检查当前项目配置 |
77
+ | `pb --help` | 查看命令帮助 |
78
+ | `pb --print-version` | 查看本地版本 |
79
+
80
+ npm 安装的用户使用 `npm install -g proofbridge-cli@latest` 更新。
81
+
82
+ 更新源码安装时,在 `proofBridgeCli` 目录执行:
83
+
84
+ ```bash
85
+ git pull --ff-only
86
+ bash ./install.sh install
87
+ ```
88
+
89
+ [许可证](LICENSE) · [项目来源](UPSTREAM.md)
package/UPSTREAM.md ADDED
@@ -0,0 +1,7 @@
1
+ # 项目来源
2
+
3
+ ProofBridge 基于 [Claude Code Bridge](https://github.com/SeemSeam/claude_codex_bridge) 开发,原作者为 SeemSeam 及其贡献者。
4
+
5
+ 本项目调整了品牌、命令与配置命名,并重新设计 Web 配置界面。
6
+
7
+ 原始 [LICENSE](LICENSE) 保持不变;各组件的第三方声明与许可证继续保留。
package/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.2
@@ -0,0 +1,9 @@
1
+ # ProofBridge icon
2
+
3
+ Created for this project with the built-in ImageGen tool on 2026-09-25. The approved working asset is `proofbridge-icon.png` (1024 px), with 256 px and 64 px web derivatives. Android and iOS assets are resized from the same source without changing their existing platform filenames or dimensions.
4
+
5
+ ## Generation prompt
6
+
7
+ Create one polished production app icon for ProofBridge, a multi-agent command-line developer workspace. A minimal geometric bridge formed by two strong vertical piers and one connecting arch, with square negative-space details suggesting terminal windows. Use vivid mint/teal on very dark blue-slate, centered, generous safe padding, crisp and readable at 24 pixels. A single flat graphic, no text, no mockup, no perspective, no watermark.
8
+
9
+ The product icon is original generated artwork. UI component glyphs have separate upstream notices in the configuration UI design documentation.
package/bin/ask.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ require("./pb-npm-runner").run("ask");
package/bin/autonew.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ require("./pb-npm-runner").run("autonew");
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ require("./pb-npm-runner").run("ctx-transfer");
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const childProcess = require("child_process");
5
+ const crypto = require("crypto");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const root = path.resolve(__dirname, "..");
10
+ const manifest = require(path.join(root, "package.json"));
11
+ const version = manifest.version;
12
+ const vendorRoot = path.join(root, ".pb-release");
13
+ const installLock = path.join(root, ".pb-install.lock");
14
+ const runtimeProbe = [
15
+ "import sys",
16
+ "if sys.version_info < (3, 10):",
17
+ " raise SystemExit(1)",
18
+ "try:",
19
+ " import tomllib",
20
+ "except ModuleNotFoundError:",
21
+ " import tomli",
22
+ "import aiohttp",
23
+ "from cryptography.hazmat.primitives.asymmetric import ed25519, x25519",
24
+ "from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305",
25
+ ].join("\n");
26
+
27
+ function artifactForHost(platform = process.platform, arch = process.arch) {
28
+ if (platform === "darwin" && ["x64", "arm64"].includes(arch)) {
29
+ return {
30
+ directory: "pb-macos-universal",
31
+ file: "pb-macos-universal.tar.gz",
32
+ };
33
+ }
34
+ if (platform === "linux" && arch === "x64") {
35
+ return {
36
+ directory: "pb-linux-x86_64",
37
+ file: "pb-linux-x86_64.tar.gz",
38
+ };
39
+ }
40
+ throw new Error(
41
+ `Unsupported platform for proofbridge-cli: ${platform}/${arch}. ` +
42
+ "Supported hosts: Linux x64, macOS x64, and macOS arm64."
43
+ );
44
+ }
45
+
46
+ function assertLinuxRuntime(glibcVersion) {
47
+ const match = typeof glibcVersion === "string" && glibcVersion.match(/^(\d+)\.(\d+)/);
48
+ if (!match) {
49
+ throw new Error("ProofBridge's Linux x64 bundle requires glibc 2.35 or newer. Alpine/musl is not supported; use a glibc-based distribution such as Ubuntu 22.04 or newer.");
50
+ }
51
+ const major = Number(match[1]);
52
+ const minor = Number(match[2]);
53
+ if (major < 2 || (major === 2 && minor < 35)) {
54
+ throw new Error(`ProofBridge's Linux x64 bundle requires glibc 2.35 or newer (detected ${glibcVersion}). Upgrade your Linux distribution before installing.`);
55
+ }
56
+ }
57
+
58
+ function installDir(info) {
59
+ return path.join(vendorRoot, info.directory);
60
+ }
61
+
62
+ function executablePath(command = "pb") {
63
+ const info = artifactForHost();
64
+ const base = installDir(info);
65
+ return command === "pb" ? path.join(base, "pb") : path.join(base, "bin", command);
66
+ }
67
+
68
+ function runtimePythonPath(info) {
69
+ return path.join(installDir(info), ".venv", "bin", "python");
70
+ }
71
+
72
+ function isReleaseInstalled(info) {
73
+ const dir = installDir(info);
74
+ const versionFile = path.join(dir, "VERSION");
75
+ const pbPath = path.join(dir, "pb");
76
+ if (!fs.existsSync(versionFile) || !fs.existsSync(pbPath)) {
77
+ return false;
78
+ }
79
+ try {
80
+ const payload = JSON.parse(fs.readFileSync(path.join(dir, ".npm-payload.json"), "utf8"));
81
+ if (payload.version !== version || !payload.files || !payload.files.pb) return false;
82
+ for (const [name, expected] of Object.entries(payload.files)) {
83
+ const target = path.join(dir, name);
84
+ const stat = fs.lstatSync(target);
85
+ if (!stat.isFile() || stat.size !== expected.size) return false;
86
+ if (expected.executable) fs.accessSync(target, fs.constants.X_OK);
87
+ }
88
+ fs.accessSync(pbPath, fs.constants.X_OK);
89
+ return fs.readFileSync(versionFile, "utf8").trim() === version;
90
+ } catch (_error) {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ function isRuntimeReady(info) {
96
+ const pythonPath = runtimePythonPath(info);
97
+ if (!fs.existsSync(pythonPath)) {
98
+ return false;
99
+ }
100
+ const completed = childProcess.spawnSync(pythonPath, ["-c", runtimeProbe], {
101
+ stdio: "ignore",
102
+ timeout: 15000,
103
+ });
104
+ return !completed.error && completed.status === 0;
105
+ }
106
+
107
+ function isInstalled(info) {
108
+ return isReleaseInstalled(info) && isRuntimeReady(info);
109
+ }
110
+
111
+ function parseSha256Sums(text) {
112
+ const checksums = new Map();
113
+ for (const line of text.split(/\r?\n/)) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed) {
116
+ continue;
117
+ }
118
+ const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
119
+ if (match) {
120
+ if (checksums.has(match[2])) {
121
+ throw new Error(`Duplicate checksum for ${match[2]}`);
122
+ }
123
+ checksums.set(match[2], match[1].toLowerCase());
124
+ }
125
+ }
126
+ return checksums;
127
+ }
128
+
129
+ function sha256File(filePath) {
130
+ const hash = crypto.createHash("sha256");
131
+ hash.update(fs.readFileSync(filePath));
132
+ return hash.digest("hex");
133
+ }
134
+
135
+ function run(command, args, options = {}) {
136
+ const completed = childProcess.spawnSync(command, args, {
137
+ stdio: "inherit",
138
+ ...options,
139
+ });
140
+ if (completed.error) {
141
+ throw completed.error;
142
+ }
143
+ if (completed.status !== 0) {
144
+ const reason = completed.signal ? `signal ${completed.signal}` : `exit ${completed.status}`;
145
+ throw new Error(`${command} ${args.join(" ")} failed with ${reason}`);
146
+ }
147
+ }
148
+
149
+ function bootstrapRuntime(info) {
150
+ const dir = installDir(info);
151
+ const installerPath = path.join(dir, "install.sh");
152
+ if (!isReleaseInstalled(info)) {
153
+ throw new Error(`Cannot bootstrap an incomplete PB release at ${dir}`);
154
+ }
155
+ if (!fs.existsSync(installerPath)) {
156
+ throw new Error(`PB runtime bootstrap installer not found at ${installerPath}`);
157
+ }
158
+
159
+ const env = {
160
+ ...process.env,
161
+ PB_INSTALL_PREFIX: dir,
162
+ PB_BIN_DIR: path.join(dir, ".npm-runtime-bin"),
163
+ PB_SOURCE_KIND: "release",
164
+ PB_USE_MANAGED_VENV: "1",
165
+ PB_INSTALL_TOMLI: "1",
166
+ PB_INSTALL_MOBILE_RELAY_DEPS: "1",
167
+ PB_INSTALL_ROLES: "0",
168
+ PB_INSTALL_NEOVIM: "0",
169
+ };
170
+ run("bash", [installerPath, "runtime-bootstrap"], { env });
171
+ if (!isRuntimeReady(info)) {
172
+ throw new Error(`PB managed Python runtime is not usable at ${runtimePythonPath(info)}`);
173
+ }
174
+ }
175
+
176
+ function sleep(milliseconds) {
177
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
178
+ }
179
+
180
+ function processIsAlive(pid) {
181
+ if (!Number.isInteger(pid) || pid <= 0) {
182
+ return false;
183
+ }
184
+ try {
185
+ process.kill(pid, 0);
186
+ return true;
187
+ } catch (error) {
188
+ return error && error.code === "EPERM";
189
+ }
190
+ }
191
+
192
+ function readLockOwner() {
193
+ try {
194
+ return JSON.parse(fs.readFileSync(path.join(installLock, "owner.json"), "utf8"));
195
+ } catch (_error) {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ function reclaimStaleInstallLock() {
201
+ const owner = readLockOwner();
202
+ if (owner && processIsAlive(Number(owner.pid))) {
203
+ return false;
204
+ }
205
+ try {
206
+ const ageMs = Date.now() - fs.statSync(installLock).mtimeMs;
207
+ if (!owner && ageMs < 30000) {
208
+ return false;
209
+ }
210
+ fs.rmSync(installLock, { recursive: true, force: true });
211
+ return true;
212
+ } catch (_error) {
213
+ return false;
214
+ }
215
+ }
216
+
217
+ async function acquireInstallLock() {
218
+ const configuredTimeout = Number(process.env.PB_NPM_INSTALL_LOCK_TIMEOUT_MS);
219
+ const timeoutMs =
220
+ Number.isFinite(configuredTimeout) && configuredTimeout > 0
221
+ ? configuredTimeout
222
+ : 15 * 60 * 1000;
223
+ const deadline = Date.now() + timeoutMs;
224
+ const token = crypto.randomBytes(16).toString("hex");
225
+
226
+ while (true) {
227
+ try {
228
+ fs.mkdirSync(installLock);
229
+ } catch (error) {
230
+ if (!error || error.code !== "EEXIST") {
231
+ throw error;
232
+ }
233
+ if (reclaimStaleInstallLock()) {
234
+ continue;
235
+ }
236
+ if (Date.now() >= deadline) {
237
+ throw new Error(`Timed out waiting for PB npm install lock: ${installLock}`);
238
+ }
239
+ await sleep(200);
240
+ continue;
241
+ }
242
+
243
+ try {
244
+ fs.writeFileSync(
245
+ path.join(installLock, "owner.json"),
246
+ JSON.stringify({ pid: process.pid, token, createdAt: new Date().toISOString() })
247
+ );
248
+ } catch (error) {
249
+ fs.rmSync(installLock, { recursive: true, force: true });
250
+ throw error;
251
+ }
252
+ return () => {
253
+ const owner = readLockOwner();
254
+ if (owner && owner.token === token) {
255
+ fs.rmSync(installLock, { recursive: true, force: true });
256
+ }
257
+ };
258
+ }
259
+ }
260
+
261
+ function payloadPython() {
262
+ const candidates = process.env.PB_PYTHON_BIN
263
+ ? [process.env.PB_PYTHON_BIN]
264
+ : ["python3", "python3.14", "python3.13", "python3.12", "python3.11", "python3.10", "python"];
265
+ for (const candidate of candidates) {
266
+ const probe = childProcess.spawnSync(candidate, ["-c", "import sys; sys.exit(sys.version_info < (3, 10))"], {
267
+ stdio: "ignore", timeout: 15000,
268
+ });
269
+ if (!probe.error && probe.status === 0) return candidate;
270
+ }
271
+ throw new Error("ProofBridge requires Python 3.10+ to install its bundled runtime. Install Python and retry, or set PB_PYTHON_BIN to its executable.");
272
+ }
273
+
274
+ function checkPayload() {
275
+ run(payloadPython(), [path.join(__dirname, "pb-npm-payload.py"), "validate-all", root]);
276
+ console.error(`Verified both bundled ProofBridge v${version} releases.`);
277
+ }
278
+
279
+ function installBundledRelease(info) {
280
+ const bundles = path.join(root, "npm-bundles");
281
+ const archivePath = path.join(bundles, info.file);
282
+ const sumsPath = path.join(bundles, "SHA256SUMS");
283
+ if (!fs.existsSync(archivePath) || !fs.existsSync(sumsPath)) {
284
+ throw new Error(`ProofBridge npm package is incomplete: missing bundled ${info.file} or SHA256SUMS. Reinstall proofbridge-cli from npm.`);
285
+ }
286
+ const checksums = parseSha256Sums(fs.readFileSync(sumsPath, "utf8"));
287
+ const expected = checksums.get(info.file);
288
+ if (!expected || sha256File(archivePath) !== expected) {
289
+ throw new Error(`Checksum mismatch or missing checksum for bundled ${info.file}. Reinstall proofbridge-cli from npm.`);
290
+ }
291
+ fs.mkdirSync(vendorRoot, { recursive: true });
292
+ const staging = fs.mkdtempSync(path.join(vendorRoot, ".staging-"));
293
+ try {
294
+ run(payloadPython(), [path.join(__dirname, "pb-npm-payload.py"), "extract", archivePath, version, staging]);
295
+ fs.rmSync(installDir(info), { recursive: true, force: true });
296
+ fs.renameSync(path.join(staging, info.directory), installDir(info));
297
+ console.log(`Installed ProofBridge v${version} from bundled ${info.file}.`);
298
+ } finally {
299
+ fs.rmSync(staging, { recursive: true, force: true });
300
+ }
301
+ }
302
+
303
+ async function install() {
304
+ const info = artifactForHost();
305
+ if (process.platform === "linux") {
306
+ assertLinuxRuntime(process.report.getReport().header.glibcVersionRuntime);
307
+ }
308
+ if (isInstalled(info)) {
309
+ return;
310
+ }
311
+
312
+ const releaseLock = await acquireInstallLock();
313
+ try {
314
+ if (isInstalled(info)) {
315
+ return;
316
+ }
317
+ if (!isReleaseInstalled(info)) {
318
+ installBundledRelease(info);
319
+ }
320
+ if (!isRuntimeReady(info)) {
321
+ bootstrapRuntime(info);
322
+ }
323
+ } finally {
324
+ releaseLock();
325
+ }
326
+ }
327
+
328
+ if (require.main === module) {
329
+ Promise.resolve().then(() => process.argv.includes("--check-payload") ? checkPayload() : install()).catch((error) => {
330
+ console.error(error.message || error);
331
+ process.exit(1);
332
+ });
333
+ }
334
+
335
+ module.exports = {
336
+ artifactForHost,
337
+ assertLinuxRuntime,
338
+ bootstrapRuntime,
339
+ checkPayload,
340
+ executablePath,
341
+ install,
342
+ installDir,
343
+ isInstalled,
344
+ isReleaseInstalled,
345
+ isRuntimeReady,
346
+ runtimePythonPath,
347
+ };
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env python3
2
+ """Validate bundled releases before publication and extract only validated files."""
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from html.parser import HTMLParser
7
+ import json
8
+ from pathlib import Path, PurePosixPath
9
+ import re
10
+ import struct
11
+ import sys
12
+ import tarfile
13
+ from urllib.parse import unquote, urlsplit
14
+
15
+ ARTIFACTS = {
16
+ "pb-macos-universal.tar.gz": ("macos", "universal"),
17
+ "pb-linux-x86_64.tar.gz": ("linux", "x86_64"),
18
+ }
19
+ EXECUTABLES = ("pb", "install.sh", "bin/_pb-python", "bin/ask", "bin/autonew", "bin/ctx-transfer")
20
+ BINARIES = ("bin/pb-agent-sidebar", "bin/pb-rs-helper", "bin/pb-runtime-accelerator", "bin/pb-usage")
21
+ REQUIRED = EXECUTABLES + BINARIES + (
22
+ "VERSION", "BUILD_INFO.json", "pb.py", "LICENSE", "UPSTREAM.md",
23
+ "assets/config_ui/index.html", "lib/cli/services/config_ui.py",
24
+ "assets/usage_ui/index.html",
25
+ "deploy/mobile-relay/requirements.txt",
26
+ )
27
+ FORBIDDEN = {".git", ".trellis", ".codex", ".agents", ".venv", "__pycache__", "node_modules", "npm-bundles", ".pb-release", "test", "tests", ".github", ".pytest_cache", ".mypy_cache"}
28
+
29
+
30
+ class _UsageResources(HTMLParser):
31
+ def __init__(self) -> None:
32
+ super().__init__()
33
+ self.references: list[tuple[str, str]] = []
34
+
35
+ def handle_starttag(self, tag: str, attrs) -> None:
36
+ values = dict(attrs)
37
+ if tag == "script" and values.get("src"):
38
+ self.references.append((tag, values["src"]))
39
+ if tag == "link" and values.get("href") and set(str(values.get("rel", "")).split()).intersection(
40
+ {"stylesheet", "modulepreload", "icon", "preload"}
41
+ ):
42
+ self.references.append((tag, values["href"]))
43
+
44
+
45
+ def _javascript_tokens(source: str):
46
+ """Read literal module references without treating comments/strings as code.
47
+
48
+ This is a lexer for built Vite output, not a JavaScript executor. Regex
49
+ literals and template strings are skipped as units; constant templates are
50
+ usable specifiers, while computed imports remain the bundler's responsibility.
51
+ """
52
+ index = 0
53
+ previous = None
54
+ while index < len(source):
55
+ char = source[index]
56
+ if char.isspace():
57
+ index += 1
58
+ continue
59
+ if source.startswith("//", index):
60
+ end = source.find("\n", index + 2)
61
+ index = len(source) if end < 0 else end + 1
62
+ continue
63
+ if source.startswith("/*", index):
64
+ end = source.find("*/", index + 2)
65
+ index = len(source) if end < 0 else end + 2
66
+ continue
67
+ if char in "\"'`":
68
+ quote, start = char, index + 1
69
+ index += 1
70
+ while index < len(source):
71
+ if source[index] == "\\":
72
+ index += 2
73
+ elif source[index] == quote:
74
+ break
75
+ else:
76
+ index += 1
77
+ raw = source[start:index]
78
+ index += 1
79
+ token = ("template" if quote == "`" and "${" in raw else "string", _decode_js_literal(raw))
80
+ elif char == "/" and previous in {None, "=", "(", "[", "{", ",", ":", ";", "!", "?", "return", "case", ">", "&", "|"}:
81
+ index += 1
82
+ in_class = False
83
+ while index < len(source):
84
+ value = source[index]
85
+ if value == "\\":
86
+ index += 2
87
+ continue
88
+ if value == "[":
89
+ in_class = True
90
+ elif value == "]":
91
+ in_class = False
92
+ elif value == "/" and not in_class:
93
+ index += 1
94
+ break
95
+ index += 1
96
+ token = ("regex", "regex")
97
+ elif char.isalpha() or char in "_$":
98
+ start = index
99
+ while index < len(source) and (source[index].isalnum() or source[index] in "_$"):
100
+ index += 1
101
+ token = ("word", source[start:index])
102
+ else:
103
+ index += 1
104
+ token = ("punctuation", char)
105
+ previous = token[1]
106
+ yield token
107
+
108
+
109
+ def _decode_js_literal(value: str) -> str:
110
+ def decode(match):
111
+ escape = match.group(1)
112
+ if escape.startswith("u{"):
113
+ return chr(int(escape[2:-1], 16))
114
+ if escape.startswith(("u", "x")) and len(escape) > 1:
115
+ return chr(int(escape[1:], 16))
116
+ return {"n": "\n", "r": "\r", "t": "\t", "\n": "", "\r\n": ""}.get(escape, escape)
117
+ return re.sub(r"\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|\r\n|[\s\S])", decode, value)
118
+
119
+
120
+ def _javascript_dependencies(source: str):
121
+ tokens = list(_javascript_tokens(source))
122
+ for index, (kind, value) in enumerate(tokens):
123
+ if kind != "word":
124
+ continue
125
+ if value in {"import", "from"} and index + 1 < len(tokens) and tokens[index + 1][0] == "string":
126
+ yield tokens[index + 1][1]
127
+ if (value == "import" and index + 3 < len(tokens)
128
+ and tokens[index + 1][1] == "(" and tokens[index + 2][0] == "string"
129
+ and tokens[index + 3][1] in {")", ","}):
130
+ yield tokens[index + 2][1]
131
+ # Vite records lazy JS/CSS preload dependencies in this generated table.
132
+ if value == "__vite__mapDeps" and index + 1 < len(tokens) and tokens[index + 1][1] == "=":
133
+ for table_kind, table_value in tokens[index + 2:]:
134
+ if table_value == ";":
135
+ break
136
+ if table_kind == "string" and table_value.startswith("assets/"):
137
+ yield "/usage/" + table_value
138
+
139
+
140
+ def _css_dependencies(source: str):
141
+ source = re.sub(r"/\*[\s\S]*?\*/", "", source)
142
+ literals = re.compile(r'''url\(\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^)]*?))\s*\)|@import\s+(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')''', re.IGNORECASE)
143
+ for match in literals.finditer(source):
144
+ value = next(group for group in match.groups() if group is not None).strip()
145
+ # CSS hex escapes may encode dots, slashes or spaces in resource URLs.
146
+ value = re.sub(r"\\([0-9a-fA-F]{1,6})\s?", lambda item: chr(int(item[1], 16)), value)
147
+ value = re.sub(r"\\([^\n\r])", r"\1", value)
148
+ if value.startswith(("data:", "#")):
149
+ continue
150
+ yield value
151
+
152
+
153
+ def _usage_resource_path(reference: str, *, source: str) -> str:
154
+ url = urlsplit(reference)
155
+ path = unquote(url.path)
156
+ if url.scheme or url.netloc or "\\" in path or any(ord(char) < 32 for char in path):
157
+ raise ValueError(f"Usage page requires a non-local resource: {reference}")
158
+ root = PurePosixPath("assets/usage_ui")
159
+ if path.startswith("/usage/"):
160
+ relative = PurePosixPath(path.removeprefix("/usage/"))
161
+ name = root / relative
162
+ else:
163
+ relative = PurePosixPath(path)
164
+ name = PurePosixPath(source).parent / relative
165
+ if relative.is_absolute() or ".." in relative.parts or not relative.parts:
166
+ raise ValueError(f"Invalid Usage resource path from {source}: {reference}")
167
+ allowed = name == root / "favicon.svg" or name.is_relative_to(root / "assets")
168
+ if not allowed:
169
+ raise ValueError(f"Usage resource must be served under /usage/assets/: {reference}")
170
+ return str(name)
171
+
172
+
173
+ def usage_asset_names(files, read) -> tuple[str, ...]:
174
+ parser = _UsageResources()
175
+ parser.feed(read("assets/usage_ui/index.html").decode("utf-8"))
176
+ scripts = 0
177
+ pending = []
178
+ for tag, reference in parser.references:
179
+ if reference.startswith("data:") and tag == "link":
180
+ continue
181
+ name = _usage_resource_path(reference, source="assets/usage_ui/index.html")
182
+ pending.append(name)
183
+ if tag == "script" and PurePosixPath(name).suffix == ".js":
184
+ scripts += 1
185
+ if not scripts:
186
+ raise ValueError("Usage page is missing its built JavaScript entry")
187
+ checked = set()
188
+ while pending:
189
+ name = pending.pop()
190
+ if name in checked:
191
+ continue
192
+ if name not in files or files[name].size == 0:
193
+ raise ValueError(f"Release missing Usage resource: {name}")
194
+ checked.add(name)
195
+ suffix = PurePosixPath(name).suffix
196
+ if suffix not in {".js", ".mjs", ".css"}:
197
+ continue
198
+ content = read(name).decode("utf-8")
199
+ references = _css_dependencies(content) if suffix == ".css" else _javascript_dependencies(content)
200
+ for reference in references:
201
+ if suffix != ".css" and not reference.startswith((".", "/")):
202
+ raise ValueError(f"Unbundled Usage module reference from {name}: {reference}")
203
+ pending.append(_usage_resource_path(reference, source=name))
204
+ names = tuple(sorted(name for name in files if name.startswith("assets/usage_ui/")))
205
+ if any(files[name].size == 0 for name in names):
206
+ raise ValueError("Release contains an empty Usage asset")
207
+ return names
208
+
209
+
210
+ def check_binary(data: bytes, platform: str, name: str) -> None:
211
+ if platform == "linux":
212
+ valid = (len(data) >= 64 and data[:6] == b"\x7fELF\x02\x01"
213
+ and struct.unpack_from("<H", data, 18)[0] == 62)
214
+ else:
215
+ valid = False
216
+ if len(data) >= 48 and data[:4] in (b"\xca\xfe\xba\xbe", b"\xca\xfe\xba\xbf"):
217
+ count = struct.unpack_from(">I", data, 4)[0]
218
+ stride = 32 if data[:4] == b"\xca\xfe\xba\xbf" else 20
219
+ if 1 <= count <= 16 and len(data) >= 8 + count * stride:
220
+ cpus = {struct.unpack_from(">I", data, 8 + i * stride)[0] for i in range(count)}
221
+ valid = {0x01000007, 0x0100000C}.issubset(cpus)
222
+ if not valid:
223
+ raise ValueError(f"Invalid {platform} native binary: {name}")
224
+
225
+
226
+ def inspect_archive(archive: Path, version: str, destination: Path | None = None) -> dict:
227
+ if archive.name not in ARTIFACTS:
228
+ raise ValueError(f"Unsupported release archive: {archive.name}")
229
+ platform, arch = ARTIFACTS[archive.name]
230
+ root = archive.name.removesuffix(".tar.gz")
231
+ with tarfile.open(archive, "r:gz") as tar:
232
+ members = []
233
+ files = {}
234
+ total = 0
235
+ seen = set()
236
+ for member in tar:
237
+ if member.name == archive.name and member.issym() and member.linkname == root:
238
+ continue # Historical top-level convenience alias; never extract it.
239
+ name = PurePosixPath(member.name)
240
+ if (name.is_absolute() or ".." in name.parts or "\\" in member.name
241
+ or not name.parts or name.parts[0] != root):
242
+ raise ValueError(f"Unsafe archive path: {member.name}")
243
+ if any(part in FORBIDDEN for part in name.parts):
244
+ raise ValueError(f"Development-only content in release: {member.name}")
245
+ if str(name) in seen:
246
+ raise ValueError(f"Duplicate archive path: {member.name}")
247
+ seen.add(str(name))
248
+ if not (member.isfile() or member.isdir()):
249
+ raise ValueError(f"Unsupported archive entry: {member.name}")
250
+ if member.mode & 0o7000:
251
+ raise ValueError(f"Unsafe archive permissions: {member.name}")
252
+ total += member.size
253
+ if total > 1024 * 1024 * 1024 or len(seen) > 50000:
254
+ raise ValueError("Release archive exceeds extraction limits")
255
+ members.append(member)
256
+ if member.isfile():
257
+ files[str(name.relative_to(root))] = member
258
+ for required in REQUIRED:
259
+ if required not in files or files[required].size == 0:
260
+ raise ValueError(f"Release missing required file: {required}")
261
+ for executable in EXECUTABLES + BINARIES:
262
+ if not files[executable].mode & 0o111:
263
+ raise ValueError(f"Release file is not executable: {executable}")
264
+ def read(name: str) -> bytes:
265
+ source = tar.extractfile(files[name])
266
+ assert source is not None
267
+ return source.read()
268
+ if read("VERSION").decode().strip() != version:
269
+ raise ValueError(f"Release VERSION does not match npm version {version}")
270
+ metadata = json.loads(read("BUILD_INFO.json"))
271
+ for key, expected in (("version", version), ("platform", platform), ("arch", arch)):
272
+ if metadata.get(key) != expected:
273
+ raise ValueError(f"Release BUILD_INFO {key} must equal {expected}")
274
+ for binary in BINARIES:
275
+ source = tar.extractfile(files[binary])
276
+ assert source is not None
277
+ check_binary(source.read(520), platform, binary)
278
+ checked_files = tuple(dict.fromkeys((*REQUIRED, *usage_asset_names(files, read))))
279
+ if destination is not None:
280
+ if destination.exists() and any(destination.iterdir()):
281
+ raise ValueError(f"Extraction destination must be empty: {destination}")
282
+ destination.mkdir(parents=True, exist_ok=True)
283
+ # Entries were restricted to regular files/directories within one root.
284
+ # No symlink, hardlink or special file can redirect extraction.
285
+ tar.extractall(destination, members=members)
286
+ (destination / root / ".npm-payload.json").write_text(json.dumps({
287
+ "version": version,
288
+ "files": {name: {"size": files[name].size, "executable": name in EXECUTABLES + BINARIES}
289
+ for name in checked_files},
290
+ }))
291
+ return metadata
292
+
293
+
294
+ def validate_all(root: Path) -> None:
295
+ version = json.loads((root / "package.json").read_text())["version"]
296
+ if (root / "VERSION").read_text().strip() != version:
297
+ raise ValueError("Package VERSION differs from package.json")
298
+ bundles = root / "npm-bundles"
299
+ checksums = {}
300
+ for line in (bundles / "SHA256SUMS").read_text().splitlines():
301
+ if not line.strip():
302
+ continue
303
+ match = re.fullmatch(r"([a-fA-F0-9]{64})\s+\*?([^/\\\s]+)", line.strip())
304
+ if not match or match[2] not in ARTIFACTS or match[2] in checksums:
305
+ raise ValueError("Invalid or duplicate entry in npm-bundles/SHA256SUMS")
306
+ checksums[match[2]] = match[1].lower()
307
+ commits = set()
308
+ for filename in ARTIFACTS:
309
+ archive = bundles / filename
310
+ actual = hashlib.sha256(archive.read_bytes()).hexdigest()
311
+ if checksums.get(filename) != actual:
312
+ raise ValueError(f"Checksum mismatch or missing checksum for {filename}")
313
+ metadata = inspect_archive(archive, version)
314
+ if not metadata.get("commit"):
315
+ raise ValueError(f"Missing build commit for {filename}")
316
+ commits.add(metadata["commit"])
317
+ if len(commits) != 1:
318
+ raise ValueError("Bundled platform releases were built from different commits")
319
+
320
+
321
+ def main() -> None:
322
+ mode, *args = sys.argv[1:]
323
+ if mode == "validate-all" and len(args) == 1:
324
+ validate_all(Path(args[0]))
325
+ elif mode == "validate" and len(args) == 2:
326
+ inspect_archive(Path(args[0]), args[1])
327
+ elif mode == "extract" and len(args) == 3:
328
+ inspect_archive(Path(args[0]), args[1], Path(args[2]))
329
+ else:
330
+ raise ValueError("Usage: pb-npm-payload.py validate ARCHIVE VERSION | extract ARCHIVE VERSION DESTINATION | validate-all PACKAGE_ROOT")
331
+
332
+
333
+ if __name__ == "__main__":
334
+ try:
335
+ main()
336
+ except (OSError, ValueError, tarfile.TarError) as error:
337
+ print(f"PB payload validation failed: {error}", file=sys.stderr)
338
+ raise SystemExit(1)
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ const childProcess = require("child_process");
4
+ const { executablePath, install } = require("./pb-npm-install");
5
+ const path = require("path");
6
+
7
+ const packageRoot = path.resolve(__dirname, "..");
8
+ const manifest = require(path.join(packageRoot, "package.json"));
9
+
10
+ function npmManagedEnvironment(baseEnv = process.env) {
11
+ return {
12
+ ...baseEnv,
13
+ PB_INSTALL_KIND: "npm",
14
+ PB_NPM_PACKAGE_NAME: manifest.name,
15
+ PB_NPM_PACKAGE_ROOT: packageRoot,
16
+ PB_NPM_PACKAGE_VERSION: manifest.version,
17
+ };
18
+ }
19
+
20
+ async function run(command) {
21
+ try {
22
+ await install();
23
+ } catch (error) {
24
+ console.error(`ProofBridge could not prepare its runtime: ${error.message || error}`);
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ const target = executablePath(command);
29
+ const child = childProcess.spawn(target, process.argv.slice(2), {
30
+ stdio: "inherit",
31
+ env: npmManagedEnvironment(),
32
+ });
33
+ child.on("error", (error) => {
34
+ console.error(error.message || error);
35
+ process.exit(1);
36
+ });
37
+ child.on("exit", (code, signal) => {
38
+ if (signal) {
39
+ process.kill(process.pid, signal);
40
+ return;
41
+ }
42
+ process.exit(code === null ? 1 : code);
43
+ });
44
+ }
45
+
46
+ module.exports = {
47
+ npmManagedEnvironment,
48
+ run,
49
+ };
package/bin/pb.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ require("./pb-npm-runner").run("pb");
@@ -0,0 +1,2 @@
1
+ 2466349184a9ab4fa224dbeaba652c21c080ac8415cc74473b3d80ebdc2f16a6 pb-linux-x86_64.tar.gz
2
+ 358f39c6d81d54342f07d34e08358125d7c1ef7c237792586e76d3e8654bc95c pb-macos-universal.tar.gz
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "proofbridge-cli",
3
+ "version": "1.0.2",
4
+ "description": "ProofBridge: a multi-agent CLI workspace and cross-provider collaboration layer.",
5
+ "license": "AGPL-3.0-only",
6
+ "homepage": "https://github.com/Atingaii/proofBridgeCli#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Atingaii/proofBridgeCli.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Atingaii/proofBridgeCli/issues"
13
+ },
14
+ "keywords": [
15
+ "pb",
16
+ "multi-agent",
17
+ "codex",
18
+ "claude",
19
+ "gemini",
20
+ "grok",
21
+ "kimi",
22
+ "deepseek",
23
+ "mimo",
24
+ "qwen",
25
+ "cursor",
26
+ "copilot",
27
+ "crush",
28
+ "kiro",
29
+ "pi",
30
+ "zai",
31
+ "opencode",
32
+ "tmux",
33
+ "cli"
34
+ ],
35
+ "bin": {
36
+ "pb": "bin/pb.js",
37
+ "ask": "bin/ask.js",
38
+ "autonew": "bin/autonew.js",
39
+ "ctx-transfer": "bin/ctx-transfer.js"
40
+ },
41
+ "files": [
42
+ "bin/pb.js",
43
+ "bin/ask.js",
44
+ "bin/autonew.js",
45
+ "bin/ctx-transfer.js",
46
+ "bin/pb-npm-install.js",
47
+ "bin/pb-npm-runner.js",
48
+ "README.md",
49
+ "LICENSE",
50
+ "UPSTREAM.md",
51
+ "VERSION",
52
+ "assets/brand/",
53
+ "bin/pb-npm-payload.py",
54
+ "npm-bundles/pb-macos-universal.tar.gz",
55
+ "npm-bundles/pb-linux-x86_64.tar.gz",
56
+ "npm-bundles/SHA256SUMS"
57
+ ],
58
+ "scripts": {
59
+ "postinstall": "node bin/pb-npm-install.js",
60
+ "pack:check": "npm pack --dry-run",
61
+ "prepack": "node bin/pb-npm-install.js --check-payload"
62
+ },
63
+ "engines": {
64
+ "node": ">=18"
65
+ },
66
+ "os": [
67
+ "linux",
68
+ "darwin"
69
+ ],
70
+ "cpu": [
71
+ "x64",
72
+ "arm64"
73
+ ],
74
+ "publishConfig": {
75
+ "access": "public"
76
+ }
77
+ }