@averyyy/pi-client 0.80.3-piclient.5 → 0.80.3-piclient.6
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/CHANGELOG.md +6 -1
- package/README.md +4 -2
- package/bin/update.js +6 -170
- package/bin/web.js +37 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
- Initial `pi-client` package as a lightweight wrapper that exposes only the `pi-client` bin without `pi`.
|
|
6
6
|
- Global install wrapper that launches the local forked coding-agent entrypoint while sharing the original `~/.pi/agent` configuration.
|
|
7
7
|
- `pi-client update` command for updating the fork checkout and reinstalling both `pi-client` and `pi-server`.
|
|
8
|
-
- npm global package updates
|
|
8
|
+
- npm global package updates during `pi-client update`.
|
|
9
9
|
- `pi-client web` command that starts the PI WEB client GUI on port `1838` by default.
|
|
10
10
|
- Pi Server status, URL settings, client actions, project visibility, and global `AGENTS.md` editing inside `pi-client web`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Used `--legacy-peer-deps` for npm-global fork updates and documented installs so existing upstream Pi installs do not trigger peer override warnings for forked prerelease aliases.
|
|
15
|
+
- Fixed `pi-client web` startup on Windows by using PI WEB's TCP session daemon mode instead of the default Unix socket path.
|
package/README.md
CHANGED
|
@@ -5,9 +5,11 @@ Client CLI for connecting Pi to a `pi-server` instance.
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm i -g @averyyy/pi-client
|
|
8
|
+
npm i -g --ignore-scripts --legacy-peer-deps @averyyy/pi-client
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`--legacy-peer-deps` avoids npm peer override warnings when upstream Pi is already installed globally.
|
|
12
|
+
|
|
11
13
|
## Use
|
|
12
14
|
|
|
13
15
|
Connect to the hosted server:
|
|
@@ -43,5 +45,5 @@ PI_SERVER_AUTH_TOKEN=your-token PI_SERVER_URL=http://127.0.0.1:4217 pi-client
|
|
|
43
45
|
Install the server separately:
|
|
44
46
|
|
|
45
47
|
```bash
|
|
46
|
-
npm i -g @averyyy/pi-server
|
|
48
|
+
npm i -g --ignore-scripts @averyyy/pi-server
|
|
47
49
|
```
|
package/bin/update.js
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { Buffer } from "node:buffer";
|
|
3
2
|
import { readFileSync } from "node:fs";
|
|
4
3
|
import { dirname, resolve } from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
|
|
7
|
-
const H_PROXY_WARNING_URL_PATTERN =
|
|
8
|
-
/https?:\/\/114\.114\.114\.114:\d+\/proxycontrolwarn\/httpwarning_\d+\.html\?ori_url=[A-Za-z0-9+/=]+(?:&uid=\d+)?/;
|
|
9
|
-
const H_PROXY_WARNING_HOST = "114.114.114.114:9421";
|
|
10
|
-
const H_PROXY_HEADERS = {
|
|
11
|
-
"User-Agent":
|
|
12
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121 Safari/537.36",
|
|
13
|
-
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
14
|
-
"Accept-Language": "en-US,en;q=0.5",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
6
|
function defaultPackageRoot() {
|
|
18
7
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
19
8
|
}
|
|
@@ -30,173 +19,23 @@ function runStep(runner, command, args, cwd, stdio = "inherit") {
|
|
|
30
19
|
return runner(command, args, { cwd, stdio, encoding: "utf-8" });
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
function responseStatus(response) {
|
|
34
|
-
return response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getQueryValue(url, name) {
|
|
38
|
-
const match = new RegExp(`[?&]${name}=([^&]+)`).exec(url);
|
|
39
|
-
return match?.[1] ?? "";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getInputValue(html, id) {
|
|
43
|
-
const match = new RegExp(`id="${id}"[^>]*value="([^"]*)"`).exec(html);
|
|
44
|
-
return match?.[1] ?? "";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function bitReverse(value) {
|
|
48
|
-
return (
|
|
49
|
-
((1 & value) << 7) |
|
|
50
|
-
((2 & value) << 5) |
|
|
51
|
-
((4 & value) << 3) |
|
|
52
|
-
((8 & value) << 1) |
|
|
53
|
-
((16 & value) >> 1) |
|
|
54
|
-
((32 & value) >> 3) |
|
|
55
|
-
((64 & value) >> 5) |
|
|
56
|
-
((128 & value) >> 7)
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function encodeWarningByte(value) {
|
|
61
|
-
if (value === 32) return "+";
|
|
62
|
-
if (
|
|
63
|
-
(value < 48 && value !== 45 && value !== 46) ||
|
|
64
|
-
(value < 65 && value > 57) ||
|
|
65
|
-
(value > 90 && value < 97 && value !== 95) ||
|
|
66
|
-
value > 122
|
|
67
|
-
) {
|
|
68
|
-
return `%${value.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
69
|
-
}
|
|
70
|
-
return String.fromCharCode(value);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function md6(value) {
|
|
74
|
-
let result = "";
|
|
75
|
-
for (let index = 0; index < value.length; index++) {
|
|
76
|
-
result += encodeWarningByte(53 ^ bitReverse(value.charCodeAt(index)) ^ (255 & index));
|
|
77
|
-
}
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function b64(value) {
|
|
82
|
-
return Buffer.from(value, "utf-8").toString("base64");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function findHProxyWarningUrl(response) {
|
|
86
|
-
const location = response.headers.get("location") ?? "";
|
|
87
|
-
if (response.status === 302 && H_PROXY_WARNING_URL_PATTERN.test(location)) return location;
|
|
88
|
-
if (H_PROXY_WARNING_URL_PATTERN.test(response.url)) return response.url;
|
|
89
|
-
|
|
90
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
91
|
-
if (!contentType.includes("text/html")) return undefined;
|
|
92
|
-
|
|
93
|
-
const body = await response.clone().text();
|
|
94
|
-
return H_PROXY_WARNING_URL_PATTERN.exec(body)?.[0];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function approveHProxyWarning(warningUrl, fetchImpl) {
|
|
98
|
-
const warningResponse = await fetchImpl(warningUrl, {
|
|
99
|
-
method: "GET",
|
|
100
|
-
headers: H_PROXY_HEADERS,
|
|
101
|
-
redirect: "manual",
|
|
102
|
-
});
|
|
103
|
-
const html = await warningResponse.text();
|
|
104
|
-
if (!warningResponse.ok) {
|
|
105
|
-
throw new Error(`Proxy warning page fetch failed: ${responseStatus(warningResponse)}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const oriUrl = getQueryValue(warningUrl, "ori_url");
|
|
109
|
-
const sessionId = getInputValue(html, "sessionid");
|
|
110
|
-
if (!oriUrl || !sessionId) {
|
|
111
|
-
throw new Error("Proxy warning page did not include approval fields");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const pid = getInputValue(html, "pid");
|
|
115
|
-
const uid = getInputValue(html, "uid");
|
|
116
|
-
const payload = `ori_url=${oriUrl}&sessionid=${sessionId}&pid=${pid}&uid=${uid}`;
|
|
117
|
-
const checkUrl = `http://${H_PROXY_WARNING_HOST}/proxycontrolwarn/check?${b64(md6(b64(payload)))}`;
|
|
118
|
-
const checkResponse = await fetchImpl(checkUrl, {
|
|
119
|
-
method: "GET",
|
|
120
|
-
headers: H_PROXY_HEADERS,
|
|
121
|
-
redirect: "manual",
|
|
122
|
-
});
|
|
123
|
-
const checkBody = await checkResponse.text();
|
|
124
|
-
if (!checkResponse.ok) {
|
|
125
|
-
throw new Error(`Proxy approval failed: ${responseStatus(checkResponse)} ${checkBody.slice(0, 80)}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function approveHProxyTarget(url, fetchImpl) {
|
|
130
|
-
let response;
|
|
131
|
-
try {
|
|
132
|
-
response = await fetchImpl(url, { method: "GET", headers: H_PROXY_HEADERS, redirect: "manual" });
|
|
133
|
-
} catch {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const warningUrl = await findHProxyWarningUrl(response);
|
|
138
|
-
if (!warningUrl) return false;
|
|
139
|
-
|
|
140
|
-
await approveHProxyWarning(warningUrl, fetchImpl);
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function maybeGitHttpUrl(value) {
|
|
145
|
-
const trimmed = value.trim();
|
|
146
|
-
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
|
|
147
|
-
const ssh = /^git@([^:]+):(.+)$/.exec(trimmed);
|
|
148
|
-
if (ssh) return `https://${ssh[1]}/${ssh[2]}`;
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function maybeHttpUrl(value) {
|
|
153
|
-
const trimmed = value.trim();
|
|
154
|
-
return trimmed.startsWith("http://") || trimmed.startsWith("https://") ? trimmed : undefined;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function getProxyApprovalTargets(runner, repoRoot, includeGitRemote = true) {
|
|
158
|
-
const targets = [];
|
|
159
|
-
if (includeGitRemote) {
|
|
160
|
-
const remote = runStep(runner, "git", ["config", "--get", "remote.origin.url"], repoRoot, "pipe");
|
|
161
|
-
if (remote.status === 0) {
|
|
162
|
-
const url = maybeGitHttpUrl(String(remote.stdout ?? ""));
|
|
163
|
-
if (url) targets.push(url);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const registry = runStep(runner, "npm", ["config", "get", "registry"], repoRoot, "pipe");
|
|
168
|
-
if (registry.status === 0) {
|
|
169
|
-
const url = maybeHttpUrl(String(registry.stdout ?? ""));
|
|
170
|
-
if (url) targets.push(url);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return [...new Set(targets)];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function approveUpdateProxyTargets(runner, repoRoot, stdout, fetchImpl, includeGitRemote = true) {
|
|
177
|
-
for (const target of getProxyApprovalTargets(runner, repoRoot, includeGitRemote)) {
|
|
178
|
-
if (await approveHProxyTarget(target, fetchImpl)) {
|
|
179
|
-
stdout.write(`Approved proxy warning for ${target}\n`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
22
|
function isMissingGitCheckout(result) {
|
|
185
23
|
const text = `${String(result.stdout ?? "")}\n${String(result.stderr ?? "")}`;
|
|
186
24
|
return result.status !== 0 && text.includes("not a git repository");
|
|
187
25
|
}
|
|
188
26
|
|
|
189
|
-
async function runNpmGlobalUpdate(runner, repoRoot, stdout, stderr
|
|
190
|
-
await approveUpdateProxyTargets(runner, repoRoot, stdout, fetchImpl, false);
|
|
27
|
+
async function runNpmGlobalUpdate(runner, repoRoot, stdout, stderr) {
|
|
191
28
|
stdout.write("Updating npm packages: @averyyy/pi-client@latest @averyyy/pi-server@latest\n");
|
|
192
29
|
const result = runStep(
|
|
193
30
|
runner,
|
|
194
31
|
"npm",
|
|
195
|
-
["install", "-g", "--ignore-scripts", "@averyyy/pi-client@latest", "@averyyy/pi-server@latest"],
|
|
32
|
+
["install", "-g", "--ignore-scripts", "--legacy-peer-deps", "@averyyy/pi-client@latest", "@averyyy/pi-server@latest"],
|
|
196
33
|
repoRoot,
|
|
197
34
|
);
|
|
198
35
|
if (result.status !== 0) {
|
|
199
|
-
stderr.write(
|
|
36
|
+
stderr.write(
|
|
37
|
+
"pi-client update failed: npm install -g --ignore-scripts --legacy-peer-deps @averyyy/pi-client@latest @averyyy/pi-server@latest\n",
|
|
38
|
+
);
|
|
200
39
|
return result.status ?? 1;
|
|
201
40
|
}
|
|
202
41
|
stdout.write("pi-client update complete\n");
|
|
@@ -207,7 +46,6 @@ export async function runPiClientUpdate(_args = [], options = {}) {
|
|
|
207
46
|
const packageRoot = options.packageRoot ?? defaultPackageRoot();
|
|
208
47
|
const repoRoot = options.repoRoot ?? defaultRepoRoot();
|
|
209
48
|
const runner = options.runner ?? spawnSync;
|
|
210
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
211
49
|
const stdout = options.stdout ?? process.stdout;
|
|
212
50
|
const stderr = options.stderr ?? process.stderr;
|
|
213
51
|
const pkg = readPackageMetadata(packageRoot);
|
|
@@ -220,7 +58,7 @@ export async function runPiClientUpdate(_args = [], options = {}) {
|
|
|
220
58
|
const status = runStep(runner, "git", ["status", "--porcelain"], repoRoot, "pipe");
|
|
221
59
|
if (status.status !== 0) {
|
|
222
60
|
if (isMissingGitCheckout(status)) {
|
|
223
|
-
return runNpmGlobalUpdate(runner, repoRoot, stdout, stderr
|
|
61
|
+
return runNpmGlobalUpdate(runner, repoRoot, stdout, stderr);
|
|
224
62
|
}
|
|
225
63
|
stderr.write("pi-client update failed: unable to inspect git status\n");
|
|
226
64
|
return status.status ?? 1;
|
|
@@ -230,8 +68,6 @@ export async function runPiClientUpdate(_args = [], options = {}) {
|
|
|
230
68
|
return 1;
|
|
231
69
|
}
|
|
232
70
|
|
|
233
|
-
await approveUpdateProxyTargets(runner, repoRoot, stdout, fetchImpl);
|
|
234
|
-
|
|
235
71
|
const steps = [
|
|
236
72
|
["git", ["pull", "--ff-only"]],
|
|
237
73
|
["npm", ["install", "--ignore-scripts"]],
|
package/bin/web.js
CHANGED
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { createServer } from "node:net";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
import { dirname, join } from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,6 +20,24 @@ const binDir = dirname(fileURLToPath(import.meta.url));
|
|
|
19
20
|
const defaultPort = "1838";
|
|
20
21
|
const defaultPiServerUrl = "http://127.0.0.1:4217";
|
|
21
22
|
|
|
23
|
+
export async function piClientWebSessiondEnv(env = process.env, platform = process.platform) {
|
|
24
|
+
if (platform !== "win32") return {};
|
|
25
|
+
if (env.PI_WEB_SESSIOND_URL !== undefined && env.PI_WEB_SESSIOND_URL !== "") return {};
|
|
26
|
+
if (env.PI_WEB_SESSIOND_SOCKET !== undefined && env.PI_WEB_SESSIOND_SOCKET !== "") return {};
|
|
27
|
+
|
|
28
|
+
const host = env.PI_WEB_SESSIOND_HOST !== undefined && env.PI_WEB_SESSIOND_HOST !== "" ? env.PI_WEB_SESSIOND_HOST : "127.0.0.1";
|
|
29
|
+
const port =
|
|
30
|
+
env.PI_WEB_SESSIOND_PORT !== undefined && env.PI_WEB_SESSIOND_PORT !== ""
|
|
31
|
+
? env.PI_WEB_SESSIOND_PORT
|
|
32
|
+
: String(await findOpenTcpPort(host));
|
|
33
|
+
const urlHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
34
|
+
return {
|
|
35
|
+
PI_WEB_SESSIOND_HOST: host,
|
|
36
|
+
PI_WEB_SESSIOND_PORT: port,
|
|
37
|
+
PI_WEB_SESSIOND_URL: `http://${urlHost}:${port}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
export async function runPiClientWeb(args = process.argv.slice(2)) {
|
|
23
42
|
const parsed = parseArgs(args);
|
|
24
43
|
if (parsed.help) {
|
|
@@ -28,8 +47,11 @@ export async function runPiClientWeb(args = process.argv.slice(2)) {
|
|
|
28
47
|
|
|
29
48
|
process.title = "pi-client web";
|
|
30
49
|
const piServer = await effectivePiServerSettings(process.env);
|
|
50
|
+
const sessiondEnv = await piClientWebSessiondEnv(process.env);
|
|
51
|
+
Object.assign(process.env, sessiondEnv);
|
|
31
52
|
const childEnv = {
|
|
32
53
|
...process.env,
|
|
54
|
+
...sessiondEnv,
|
|
33
55
|
PI_CODING_AGENT: "true",
|
|
34
56
|
PI_SERVER_MODE: "true",
|
|
35
57
|
PI_SERVER_URL: piServer.serverUrl,
|
|
@@ -77,6 +99,21 @@ export async function runPiClientWeb(args = process.argv.slice(2)) {
|
|
|
77
99
|
});
|
|
78
100
|
}
|
|
79
101
|
|
|
102
|
+
async function findOpenTcpPort(host) {
|
|
103
|
+
return await new Promise((resolve, reject) => {
|
|
104
|
+
const server = createServer();
|
|
105
|
+
server.once("error", reject);
|
|
106
|
+
server.listen(0, host, () => {
|
|
107
|
+
const address = server.address();
|
|
108
|
+
if (address === null || typeof address === "string") {
|
|
109
|
+
server.close(() => reject(new Error("Could not reserve a session daemon TCP port")));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
server.close(() => resolve(address.port));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
80
117
|
function registerPiClientRoutes(app, state) {
|
|
81
118
|
app.get("/api/pi-client/pi-server", async (_request, _reply) => piServerStatus(state));
|
|
82
119
|
app.put("/api/pi-client/pi-server", async (request, reply) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@averyyy/pi-client",
|
|
3
|
-
"version": "0.80.3-piclient.
|
|
3
|
+
"version": "0.80.3-piclient.6",
|
|
4
4
|
"description": "Lightweight CLI wrapper that connects to a pi-server instance",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piClient": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@earendil-works/pi-coding-agent": "npm:@averyyy/pi-coding-agent@0.80.3-piclient.
|
|
28
|
+
"@earendil-works/pi-coding-agent": "npm:@averyyy/pi-coding-agent@0.80.3-piclient.6",
|
|
29
29
|
"@jmfederico/pi-web": "1.202606.7"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|