@averyyy/pi-server 0.80.3-piclient.3 → 0.80.3-piclient.5
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 +1 -0
- package/bin/pi-server.js +26 -0
- package/bin/update.js +250 -0
- package/package.json +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -12,5 +12,6 @@
|
|
|
12
12
|
- `/api/request/chunk` endpoint for reassembling oversized client requests before dispatch.
|
|
13
13
|
- `DELETE /api/session/:id` endpoint for removing one server-side session.
|
|
14
14
|
- `/health` endpoint for health checks.
|
|
15
|
+
- `pi-server update` command with npm global package updates and proxy warning acknowledgement.
|
|
15
16
|
- Configurable via `PI_SERVER_CONFIG` or environment variables: `PI_SERVER_HOST`, `PI_SERVER_PORT`, `PI_SERVER_AUTH_TOKEN`.
|
|
16
17
|
- Persistent session tree storage under `PI_SERVER_SESSION_STORE_DIR`, including exact tree hashes in session responses.
|
package/bin/pi-server.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const modulePromise =
|
|
8
|
+
args[0] === "update"
|
|
9
|
+
? import("./update.js").then(async ({ runPiServerUpdate }) => {
|
|
10
|
+
process.exitCode = await runPiServerUpdate(args.slice(1));
|
|
11
|
+
})
|
|
12
|
+
: runPiServer(args);
|
|
13
|
+
|
|
14
|
+
Promise.resolve(modulePromise).catch((e) => {
|
|
15
|
+
console.error(e);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function runPiServer(args) {
|
|
20
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
21
|
+
const result = spawnSync(process.execPath, [join(packageRoot, "dist", "cli.js"), ...args], {
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
});
|
|
24
|
+
if (result.error) throw result.error;
|
|
25
|
+
process.exitCode = result.status ?? (result.signal === "SIGINT" ? 130 : 1);
|
|
26
|
+
}
|
package/bin/update.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
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
|
+
function defaultPackageRoot() {
|
|
18
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function defaultRepoRoot() {
|
|
22
|
+
return resolve(defaultPackageRoot(), "..", "..");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readPackageMetadata(packageRoot) {
|
|
26
|
+
return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf-8"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runStep(runner, command, args, cwd, stdio = "inherit") {
|
|
30
|
+
return runner(command, args, { cwd, stdio, encoding: "utf-8" });
|
|
31
|
+
}
|
|
32
|
+
|
|
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
|
+
function isMissingGitCheckout(result) {
|
|
185
|
+
const text = `${String(result.stdout ?? "")}\n${String(result.stderr ?? "")}`;
|
|
186
|
+
return result.status !== 0 && text.includes("not a git repository");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function runNpmGlobalUpdate(runner, repoRoot, stdout, stderr, fetchImpl) {
|
|
190
|
+
await approveUpdateProxyTargets(runner, repoRoot, stdout, fetchImpl, false);
|
|
191
|
+
stdout.write("Updating npm packages: @averyyy/pi-client@latest @averyyy/pi-server@latest\n");
|
|
192
|
+
const result = runStep(
|
|
193
|
+
runner,
|
|
194
|
+
"npm",
|
|
195
|
+
["install", "-g", "--ignore-scripts", "@averyyy/pi-client@latest", "@averyyy/pi-server@latest"],
|
|
196
|
+
repoRoot,
|
|
197
|
+
);
|
|
198
|
+
if (result.status !== 0) {
|
|
199
|
+
stderr.write("pi-server update failed: npm install -g --ignore-scripts @averyyy/pi-client@latest @averyyy/pi-server@latest\n");
|
|
200
|
+
return result.status ?? 1;
|
|
201
|
+
}
|
|
202
|
+
stdout.write("pi-server update complete\n");
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function runPiServerUpdate(_args = [], options = {}) {
|
|
207
|
+
const packageRoot = options.packageRoot ?? defaultPackageRoot();
|
|
208
|
+
const repoRoot = options.repoRoot ?? defaultRepoRoot();
|
|
209
|
+
const runner = options.runner ?? spawnSync;
|
|
210
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
211
|
+
const stdout = options.stdout ?? process.stdout;
|
|
212
|
+
const stderr = options.stderr ?? process.stderr;
|
|
213
|
+
const pkg = readPackageMetadata(packageRoot);
|
|
214
|
+
|
|
215
|
+
stdout.write(`pi-server ${pkg.version}\n`);
|
|
216
|
+
stdout.write(`Updating checkout: ${repoRoot}\n`);
|
|
217
|
+
|
|
218
|
+
const status = runStep(runner, "git", ["status", "--porcelain"], repoRoot, "pipe");
|
|
219
|
+
if (status.status !== 0) {
|
|
220
|
+
if (isMissingGitCheckout(status)) {
|
|
221
|
+
return runNpmGlobalUpdate(runner, repoRoot, stdout, stderr, fetchImpl);
|
|
222
|
+
}
|
|
223
|
+
stderr.write("pi-server update failed: unable to inspect git status\n");
|
|
224
|
+
return status.status ?? 1;
|
|
225
|
+
}
|
|
226
|
+
if (String(status.stdout ?? "").trim().length > 0) {
|
|
227
|
+
stderr.write("pi-server update failed: working tree has uncommitted changes\n");
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await approveUpdateProxyTargets(runner, repoRoot, stdout, fetchImpl);
|
|
232
|
+
|
|
233
|
+
const steps = [
|
|
234
|
+
["git", ["pull", "--ff-only"]],
|
|
235
|
+
["npm", ["install", "--ignore-scripts"]],
|
|
236
|
+
["npm", ["run", "install:pi-client"]],
|
|
237
|
+
["npm", ["run", "install:pi-server"]],
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
for (const [command, args] of steps) {
|
|
241
|
+
const result = runStep(runner, command, args, repoRoot);
|
|
242
|
+
if (result.status !== 0) {
|
|
243
|
+
stderr.write(`pi-server update failed: ${command} ${args.join(" ")}\n`);
|
|
244
|
+
return result.status ?? 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
stdout.write("pi-server update complete\n");
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@averyyy/pi-server",
|
|
3
|
-
"version": "0.80.3-piclient.
|
|
3
|
+
"version": "0.80.3-piclient.5",
|
|
4
4
|
"description": "Proxy server for pi-client that stores session state and forwards incremental requests to upstream LLM providers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"pi-server": "
|
|
7
|
+
"pi-server": "bin/pi-server.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
+
"bin",
|
|
18
19
|
"dist",
|
|
19
20
|
"README.md",
|
|
20
21
|
"CHANGELOG.md"
|
|
@@ -26,8 +27,8 @@
|
|
|
26
27
|
"prepublishOnly": "npm run clean && npm run build"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@earendil-works/pi-ai": "npm:@averyyy/pi-ai@0.80.3-piclient.
|
|
30
|
-
"@earendil-works/pi-agent-core": "npm:@averyyy/pi-agent-core@0.80.3-piclient.
|
|
30
|
+
"@earendil-works/pi-ai": "npm:@averyyy/pi-ai@0.80.3-piclient.5",
|
|
31
|
+
"@earendil-works/pi-agent-core": "npm:@averyyy/pi-agent-core@0.80.3-piclient.5"
|
|
31
32
|
},
|
|
32
33
|
"keywords": [
|
|
33
34
|
"pi-server",
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"license": "MIT",
|
|
40
41
|
"repository": {
|
|
41
42
|
"type": "git",
|
|
42
|
-
"url": "
|
|
43
|
+
"url": "https://github.com/Averyyy/pi-client",
|
|
43
44
|
"directory": "packages/pi-server"
|
|
44
45
|
},
|
|
45
46
|
"engines": {
|
|
@@ -50,5 +51,8 @@
|
|
|
50
51
|
"shx": "0.4.0",
|
|
51
52
|
"typescript": "5.9.3",
|
|
52
53
|
"vitest": "4.1.9"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
53
57
|
}
|
|
54
58
|
}
|