@averyyy/pi-client 0.80.3-piclient.5 → 0.80.3-piclient.7
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 -3
- package/README.md +6 -3
- package/bin/update.js +6 -170
- package/bin/web.js +39 -254
- package/package.json +2 -3
- package/bin/pi-web-plugins/pi-client/package.json +0 -13
- package/bin/pi-web-plugins/pi-client/pi-web-plugin.js +0 -515
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
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
|
|
9
|
-
- `pi-client web` command that starts the
|
|
10
|
-
|
|
8
|
+
- npm global package updates during `pi-client update`.
|
|
9
|
+
- `pi-client web` command that starts the client backend in Tau mirror mode on port `1838` by default.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- 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.
|
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:
|
|
@@ -25,10 +27,11 @@ PI_SERVER_URL=https://pi.yreva.asia pi-client -p "Say exactly: ok"
|
|
|
25
27
|
Start the browser UI:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
30
|
+
pi install npm:tau-mirror
|
|
28
31
|
PI_SERVER_URL=https://pi.yreva.asia pi-client web
|
|
29
32
|
```
|
|
30
33
|
|
|
31
|
-
The web
|
|
34
|
+
The web command starts `pi-client` in Tau mirror mode. Tau listens on `http://127.0.0.1:1838` by default and uses the same shared `~/.pi/agent` extension install as local `pi`, so installing Tau with either `pi` or `pi-client` works.
|
|
32
35
|
|
|
33
36
|
## Server Auth
|
|
34
37
|
|
|
@@ -43,5 +46,5 @@ PI_SERVER_AUTH_TOKEN=your-token PI_SERVER_URL=http://127.0.0.1:4217 pi-client
|
|
|
43
46
|
Install the server separately:
|
|
44
47
|
|
|
45
48
|
```bash
|
|
46
|
-
npm i -g @averyyy/pi-server
|
|
49
|
+
npm i -g --ignore-scripts @averyyy/pi-server
|
|
47
50
|
```
|
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
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
-
import { createRequire } from "node:module";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
3
|
import { dirname, join } from "node:path";
|
|
8
4
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { effectivePiWebConfig, maxUploadBytes, piWebDataDir } from "@jmfederico/pi-web/dist/config.js";
|
|
11
|
-
import { buildApp } from "@jmfederico/pi-web/dist/server/app.js";
|
|
12
|
-
import { PiWebPluginService } from "@jmfederico/pi-web/dist/server/piWebPluginService.js";
|
|
13
|
-
import { ProjectService } from "@jmfederico/pi-web/dist/server/projects/projectService.js";
|
|
14
|
-
import { ProjectStore } from "@jmfederico/pi-web/dist/server/storage/projectStore.js";
|
|
15
5
|
|
|
16
|
-
const require = createRequire(import.meta.url);
|
|
17
|
-
const piWebRoot = dirname(require.resolve("@jmfederico/pi-web/package.json"));
|
|
18
|
-
const binDir = dirname(fileURLToPath(import.meta.url));
|
|
19
6
|
const defaultPort = "1838";
|
|
20
7
|
const defaultPiServerUrl = "http://127.0.0.1:4217";
|
|
21
8
|
|
|
@@ -27,255 +14,47 @@ export async function runPiClientWeb(args = process.argv.slice(2)) {
|
|
|
27
14
|
}
|
|
28
15
|
|
|
29
16
|
process.title = "pi-client web";
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
PI_WEB_HOST: process.env.PI_WEB_HOST ?? "127.0.0.1",
|
|
37
|
-
PI_WEB_PORT: parsed.port,
|
|
38
|
-
};
|
|
39
|
-
const sessiond = spawn(process.execPath, [join(piWebRoot, "dist", "server", "sessiond.js")], { env: childEnv, stdio: "inherit" });
|
|
40
|
-
const { config } = effectivePiWebConfig({ env: childEnv });
|
|
41
|
-
const projects = new PiClientProjectService(new ProjectService(new ProjectStore()), process.env);
|
|
42
|
-
const app = await buildApp({
|
|
43
|
-
bodyLimit: maxUploadBytes(childEnv, config),
|
|
44
|
-
projects,
|
|
45
|
-
piWebPlugins: new PiWebPluginService({
|
|
46
|
-
roots: [
|
|
47
|
-
{ path: join(piWebRoot, "dist", "pi-web-plugins"), source: "bundled", scope: "bundled" },
|
|
48
|
-
{ path: join(binDir, "pi-web-plugins"), source: "pi-client", scope: "bundled" },
|
|
49
|
-
{ path: join(piWebDataDir(childEnv), "plugins"), source: "local", scope: "local" },
|
|
50
|
-
],
|
|
51
|
-
}),
|
|
17
|
+
const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
18
|
+
const port = parsed.port;
|
|
19
|
+
const host = process.env.TAU_HOST ?? "127.0.0.1";
|
|
20
|
+
const child = spawn(process.execPath, [join(dirname(entry), "cli.js"), ...parsed.clientArgs], {
|
|
21
|
+
env: piClientWebEnv(process.env, { port, host }),
|
|
22
|
+
stdio: "inherit",
|
|
52
23
|
});
|
|
53
|
-
registerPiClientRoutes(app, { env: process.env, startupPiServerUrl: piServer.serverUrl, projects });
|
|
54
|
-
await app.listen({ port: config.port ?? Number.parseInt(defaultPort, 10), host: config.host ?? "127.0.0.1" });
|
|
55
|
-
console.log(`pi-client web listening on http://${config.host ?? "127.0.0.1"}:${config.port ?? defaultPort}`);
|
|
56
24
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const shutdown = (exitCode) => {
|
|
60
|
-
if (shuttingDown) return;
|
|
61
|
-
shuttingDown = true;
|
|
62
|
-
sessiond.kill();
|
|
63
|
-
void app.close().finally(() => {
|
|
64
|
-
resolve(exitCode);
|
|
65
|
-
});
|
|
66
|
-
};
|
|
25
|
+
console.log(`pi-client web uses Tau at http://${host}:${port}`);
|
|
26
|
+
console.log("If Tau is not installed, run: pi install npm:tau-mirror or pi-client install npm:tau-mirror");
|
|
67
27
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
sessiond.once("exit", (code, signal) => {
|
|
73
|
-
shutdown(code ?? (signal === "SIGINT" ? 130 : 1));
|
|
28
|
+
return await new Promise((resolve, reject) => {
|
|
29
|
+
child.once("error", reject);
|
|
30
|
+
child.once("exit", (code, signal) => {
|
|
31
|
+
resolve(code ?? (signal === "SIGINT" ? 130 : 1));
|
|
74
32
|
});
|
|
75
|
-
process.once("SIGINT", () => shutdown(130));
|
|
76
|
-
process.once("SIGTERM", () => shutdown(143));
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function registerPiClientRoutes(app, state) {
|
|
81
|
-
app.get("/api/pi-client/pi-server", async (_request, _reply) => piServerStatus(state));
|
|
82
|
-
app.put("/api/pi-client/pi-server", async (request, reply) => {
|
|
83
|
-
const body = request.body;
|
|
84
|
-
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
85
|
-
return reply.code(400).send({ error: "pi-server settings update must be an object" });
|
|
86
|
-
}
|
|
87
|
-
const piServerUrl = validatePiServerUrl(body.piServerUrl);
|
|
88
|
-
await savePiClientWebConfig({ piServerUrl }, state.env);
|
|
89
|
-
return piServerStatus(state);
|
|
90
33
|
});
|
|
91
|
-
app.get("/api/pi-client/global-agents", async () => globalAgentsFile());
|
|
92
|
-
app.put("/api/pi-client/global-agents", async (request, reply) => {
|
|
93
|
-
const body = request.body;
|
|
94
|
-
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
95
|
-
return reply.code(400).send({ error: "global AGENTS.md update must be an object" });
|
|
96
|
-
}
|
|
97
|
-
const content = validateGlobalAgentsContent(body.content);
|
|
98
|
-
await saveGlobalAgentsFile(content);
|
|
99
|
-
return globalAgentsFile();
|
|
100
|
-
});
|
|
101
|
-
app.get("/api/pi-client/projects", async () => state.projects.listWithVisibility());
|
|
102
|
-
app.put("/api/pi-client/projects/:projectId/visibility", async (request, reply) => {
|
|
103
|
-
const body = request.body;
|
|
104
|
-
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
105
|
-
return reply.code(400).send({ error: "project visibility update must be an object" });
|
|
106
|
-
}
|
|
107
|
-
if (typeof body.visible !== "boolean") return reply.code(400).send({ error: "visible must be a boolean" });
|
|
108
|
-
await state.projects.setVisibility(request.params.projectId, body.visible);
|
|
109
|
-
return state.projects.listWithVisibility();
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function piServerStatus(state) {
|
|
114
|
-
const settings = await effectivePiServerSettings(state.env);
|
|
115
|
-
const publicSettings = { ...settings };
|
|
116
|
-
delete publicSettings.authToken;
|
|
117
|
-
return {
|
|
118
|
-
...publicSettings,
|
|
119
|
-
restartRequired: settings.serverUrl !== state.startupPiServerUrl,
|
|
120
|
-
...(await checkPiServer(settings)),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function checkPiServer(settings) {
|
|
125
|
-
const headers =
|
|
126
|
-
settings.authToken === undefined || settings.authToken === ""
|
|
127
|
-
? {}
|
|
128
|
-
: { authorization: `Bearer ${settings.authToken}` };
|
|
129
|
-
const controller = new AbortController();
|
|
130
|
-
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
131
|
-
try {
|
|
132
|
-
const health = await fetch(new URL("/health", settings.serverUrl), { headers, signal: controller.signal });
|
|
133
|
-
const sessions = await fetch(new URL("/api/sessions", settings.serverUrl), { headers, signal: controller.signal });
|
|
134
|
-
return {
|
|
135
|
-
reachable: health.ok,
|
|
136
|
-
authenticated: sessions.status !== 401 && sessions.status !== 403,
|
|
137
|
-
status: sessions.status,
|
|
138
|
-
checkedAt: new Date().toISOString(),
|
|
139
|
-
};
|
|
140
|
-
} catch (error) {
|
|
141
|
-
return {
|
|
142
|
-
reachable: false,
|
|
143
|
-
authenticated: false,
|
|
144
|
-
error: error instanceof Error ? error.message : String(error),
|
|
145
|
-
checkedAt: new Date().toISOString(),
|
|
146
|
-
};
|
|
147
|
-
} finally {
|
|
148
|
-
clearTimeout(timeout);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function effectivePiServerSettings(env) {
|
|
153
|
-
const config = await loadPiClientWebConfig(env);
|
|
154
|
-
const envUrl = env.PI_SERVER_URL;
|
|
155
|
-
const configUrl = config.piServerUrl;
|
|
156
|
-
return {
|
|
157
|
-
serverUrl:
|
|
158
|
-
envUrl !== undefined && envUrl !== ""
|
|
159
|
-
? validatePiServerUrl(envUrl)
|
|
160
|
-
: configUrl !== undefined && configUrl !== ""
|
|
161
|
-
? validatePiServerUrl(configUrl)
|
|
162
|
-
: defaultPiServerUrl,
|
|
163
|
-
urlSource: envUrl !== undefined && envUrl !== "" ? "environment" : configUrl !== undefined ? "config" : "default",
|
|
164
|
-
tokenConfigured: env.PI_SERVER_AUTH_TOKEN !== undefined && env.PI_SERVER_AUTH_TOKEN !== "",
|
|
165
|
-
authToken: env.PI_SERVER_AUTH_TOKEN,
|
|
166
|
-
configPath: piClientWebConfigPath(env),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function loadPiClientWebConfig(env) {
|
|
171
|
-
const configPath = piClientWebConfigPath(env);
|
|
172
|
-
if (!existsSync(configPath)) return {};
|
|
173
|
-
const parsed = JSON.parse(await readFile(configPath, "utf-8"));
|
|
174
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
175
|
-
throw new Error(`pi-client web config must be a JSON object: ${configPath}`);
|
|
176
|
-
}
|
|
177
|
-
return parsed;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function savePiClientWebConfig(update, env) {
|
|
181
|
-
const configPath = piClientWebConfigPath(env);
|
|
182
|
-
const existing = await loadPiClientWebConfig(env);
|
|
183
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
184
|
-
await writeFile(configPath, `${JSON.stringify({ ...existing, ...update }, null, 2)}\n`, "utf-8");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function piClientWebConfigPath(env) {
|
|
188
|
-
if (env.PI_CLIENT_WEB_CONFIG !== undefined && env.PI_CLIENT_WEB_CONFIG !== "") return env.PI_CLIENT_WEB_CONFIG;
|
|
189
|
-
const xdgConfigHome = env.XDG_CONFIG_HOME;
|
|
190
|
-
return join(xdgConfigHome !== undefined && xdgConfigHome !== "" ? xdgConfigHome : join(homedir(), ".config"), "pi-client", "web.json");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function validatePiServerUrl(value) {
|
|
194
|
-
if (typeof value !== "string" || value.trim() === "") throw new Error("piServerUrl must be a non-empty string");
|
|
195
|
-
const url = new URL(value.trim());
|
|
196
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("piServerUrl must be http or https");
|
|
197
|
-
return url.toString().replace(/\/$/u, "");
|
|
198
34
|
}
|
|
199
35
|
|
|
200
|
-
|
|
201
|
-
const filePath = globalAgentsPath();
|
|
202
|
-
const exists = existsSync(filePath);
|
|
36
|
+
export function piClientWebEnv(env = process.env, options) {
|
|
203
37
|
return {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
38
|
+
...env,
|
|
39
|
+
PI_CODING_AGENT: "true",
|
|
40
|
+
PI_SERVER_MODE: "true",
|
|
41
|
+
PI_SERVER_URL: env.PI_SERVER_URL ?? defaultPiServerUrl,
|
|
42
|
+
TAU_HOST: options.host,
|
|
43
|
+
TAU_MIRROR_PORT: options.port,
|
|
207
44
|
};
|
|
208
45
|
}
|
|
209
46
|
|
|
210
|
-
async function saveGlobalAgentsFile(content) {
|
|
211
|
-
const filePath = globalAgentsPath();
|
|
212
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
213
|
-
await writeFile(filePath, content, "utf-8");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function globalAgentsPath() {
|
|
217
|
-
return join(getAgentDir(), "AGENTS.md");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function validateGlobalAgentsContent(value) {
|
|
221
|
-
if (typeof value !== "string") throw new Error("content must be a string");
|
|
222
|
-
return value;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
class PiClientProjectService {
|
|
226
|
-
constructor(projects, env) {
|
|
227
|
-
this.projects = projects;
|
|
228
|
-
this.env = env;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async list() {
|
|
232
|
-
const [projects, hiddenProjectIds] = await Promise.all([this.projects.list(), loadHiddenProjectIds(this.env)]);
|
|
233
|
-
return projects.filter((project) => !hiddenProjectIds.has(project.id));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
add(input) {
|
|
237
|
-
return this.projects.add(input);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
close(id) {
|
|
241
|
-
return this.projects.close(id);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
requireProject(id) {
|
|
245
|
-
return this.projects.requireProject(id);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async listWithVisibility() {
|
|
249
|
-
const [projects, hiddenProjectIds] = await Promise.all([this.projects.list(), loadHiddenProjectIds(this.env)]);
|
|
250
|
-
return projects.map((project) => ({ ...project, hidden: hiddenProjectIds.has(project.id) }));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async setVisibility(projectId, visible) {
|
|
254
|
-
await this.projects.requireProject(projectId);
|
|
255
|
-
const hiddenProjectIds = await loadHiddenProjectIds(this.env);
|
|
256
|
-
if (visible) {
|
|
257
|
-
hiddenProjectIds.delete(projectId);
|
|
258
|
-
} else {
|
|
259
|
-
hiddenProjectIds.add(projectId);
|
|
260
|
-
}
|
|
261
|
-
await savePiClientWebConfig({ hiddenProjectIds: [...hiddenProjectIds].sort() }, this.env);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function loadHiddenProjectIds(env) {
|
|
266
|
-
const config = await loadPiClientWebConfig(env);
|
|
267
|
-
if (config.hiddenProjectIds === undefined) return new Set();
|
|
268
|
-
if (!Array.isArray(config.hiddenProjectIds) || !config.hiddenProjectIds.every((id) => typeof id === "string")) {
|
|
269
|
-
throw new Error("hiddenProjectIds must be an array of strings");
|
|
270
|
-
}
|
|
271
|
-
return new Set(config.hiddenProjectIds);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
47
|
function parseArgs(args) {
|
|
275
|
-
let port = process.env.
|
|
48
|
+
let port = process.env.TAU_MIRROR_PORT ?? defaultPort;
|
|
49
|
+
const clientArgs = [];
|
|
50
|
+
|
|
276
51
|
for (let i = 0; i < args.length; i += 1) {
|
|
277
52
|
const arg = args[i];
|
|
278
|
-
if (arg === "--help" || arg === "-h") return { help: true, port };
|
|
53
|
+
if (arg === "--help" || arg === "-h") return { help: true, port, clientArgs };
|
|
54
|
+
if (arg === "--") {
|
|
55
|
+
clientArgs.push(...args.slice(i + 1));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
279
58
|
if (arg === "--port" || arg === "-p") {
|
|
280
59
|
port = requireValue(args, (i += 1), arg);
|
|
281
60
|
continue;
|
|
@@ -286,8 +65,9 @@ function parseArgs(args) {
|
|
|
286
65
|
}
|
|
287
66
|
throw new Error(`Unknown pi-client web argument: ${arg}`);
|
|
288
67
|
}
|
|
68
|
+
|
|
289
69
|
validatePort(port);
|
|
290
|
-
return { help: false, port };
|
|
70
|
+
return { help: false, port, clientArgs };
|
|
291
71
|
}
|
|
292
72
|
|
|
293
73
|
function requireValue(args, index, name) {
|
|
@@ -304,17 +84,22 @@ function validatePort(port) {
|
|
|
304
84
|
}
|
|
305
85
|
|
|
306
86
|
function printHelp() {
|
|
307
|
-
console.log(`Usage: pi-client web [--port <port>]
|
|
87
|
+
console.log(`Usage: pi-client web [--port <port>] [-- <pi-client args...>]
|
|
308
88
|
|
|
309
|
-
Start
|
|
89
|
+
Start pi-client in Tau mirror mode.
|
|
310
90
|
|
|
311
91
|
Options:
|
|
312
|
-
-p, --port <port>
|
|
92
|
+
-p, --port <port> Tau port to listen on (default: ${defaultPort})
|
|
313
93
|
-h, --help Show this help
|
|
314
94
|
|
|
315
95
|
Environment:
|
|
316
|
-
PI_SERVER_URL pi-server URL used by
|
|
96
|
+
PI_SERVER_URL pi-server URL used by pi-client sessions
|
|
317
97
|
PI_SERVER_AUTH_TOKEN pi-server auth token
|
|
318
|
-
|
|
98
|
+
TAU_HOST Tau bind host (default: 127.0.0.1)
|
|
99
|
+
TAU_MIRROR_PORT Tau port (default: ${defaultPort})
|
|
100
|
+
|
|
101
|
+
Tau must be installed in the shared Pi agent settings:
|
|
102
|
+
pi install npm:tau-mirror
|
|
103
|
+
# or: pi-client install npm:tau-mirror
|
|
319
104
|
`);
|
|
320
105
|
}
|
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.7",
|
|
4
4
|
"description": "Lightweight CLI wrapper that connects to a pi-server instance",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piClient": {
|
|
@@ -25,8 +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.
|
|
29
|
-
"@jmfederico/pi-web": "1.202606.7"
|
|
28
|
+
"@earendil-works/pi-coding-agent": "npm:@averyyy/pi-coding-agent@0.80.3-piclient.7"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
32
31
|
"shx": "0.4.0",
|
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
const endpoint = "/api/pi-client/pi-server";
|
|
2
|
-
const agentsEndpoint = "/api/pi-client/global-agents";
|
|
3
|
-
const projectsEndpoint = "/api/pi-client/projects";
|
|
4
|
-
|
|
5
|
-
function definePiClientServerElements() {
|
|
6
|
-
if (customElements.get("pi-client-server-panel") !== undefined) return;
|
|
7
|
-
|
|
8
|
-
class PiClientServerPanel extends HTMLElement {
|
|
9
|
-
connectedCallback() {
|
|
10
|
-
if (this.eventsBound !== true) {
|
|
11
|
-
this.eventsBound = true;
|
|
12
|
-
this.addEventListener("click", (event) => {
|
|
13
|
-
const path = event.composedPath();
|
|
14
|
-
if (path.some((node) => typeof node?.getAttribute === "function" && node.getAttribute("data-save") !== null)) void this.save(event);
|
|
15
|
-
if (path.some((node) => typeof node?.getAttribute === "function" && node.getAttribute("data-refresh") !== null)) void this.load();
|
|
16
|
-
});
|
|
17
|
-
this.addEventListener("submit", (event) => this.save(event));
|
|
18
|
-
}
|
|
19
|
-
this.load();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async load() {
|
|
23
|
-
this.state = { loading: true };
|
|
24
|
-
this.render();
|
|
25
|
-
try {
|
|
26
|
-
const response = await fetch(endpoint);
|
|
27
|
-
if (!response.ok) throw new Error(response.statusText);
|
|
28
|
-
this.state = { data: await response.json() };
|
|
29
|
-
} catch (error) {
|
|
30
|
-
this.state = { error: error instanceof Error ? error.message : String(error) };
|
|
31
|
-
}
|
|
32
|
-
this.render();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async save(event) {
|
|
36
|
-
event?.preventDefault();
|
|
37
|
-
const input = this.querySelector("input[name='piServerUrl']");
|
|
38
|
-
if (typeof input?.value !== "string") return;
|
|
39
|
-
this.state = { ...this.state, saving: true };
|
|
40
|
-
this.render();
|
|
41
|
-
try {
|
|
42
|
-
const response = await fetch(endpoint, {
|
|
43
|
-
method: "PUT",
|
|
44
|
-
headers: { "content-type": "application/json" },
|
|
45
|
-
body: JSON.stringify({ piServerUrl: input.value }),
|
|
46
|
-
});
|
|
47
|
-
if (!response.ok) throw new Error((await response.json()).error ?? response.statusText);
|
|
48
|
-
this.state = { data: await response.json(), saved: true };
|
|
49
|
-
} catch (error) {
|
|
50
|
-
this.state = { ...this.state, error: error instanceof Error ? error.message : String(error) };
|
|
51
|
-
}
|
|
52
|
-
this.render();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
render() {
|
|
56
|
-
const data = this.state?.data;
|
|
57
|
-
const ready = data?.reachable === true && data?.authenticated !== false;
|
|
58
|
-
const dot = data === undefined ? "unknown" : ready ? "ok" : "bad";
|
|
59
|
-
this.innerHTML = `
|
|
60
|
-
<style>
|
|
61
|
-
pi-client-server-panel { display: block; color: var(--pi-text); }
|
|
62
|
-
.pi-client-server-panel { display: grid; gap: 12px; padding: 12px; }
|
|
63
|
-
.pi-client-server-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
64
|
-
.pi-client-server-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--pi-muted); flex: 0 0 auto; }
|
|
65
|
-
.pi-client-server-dot.ok { background: #2ea043; }
|
|
66
|
-
.pi-client-server-dot.bad { background: #f85149; }
|
|
67
|
-
.pi-client-server-panel input { width: 100%; box-sizing: border-box; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-bg); color: var(--pi-text); padding: 8px; }
|
|
68
|
-
.pi-client-server-panel button { border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 10px; cursor: pointer; }
|
|
69
|
-
.pi-client-server-panel button.primary { border-color: var(--pi-accent-border); color: var(--pi-text-bright); }
|
|
70
|
-
.pi-client-server-panel small, .pi-client-server-panel .muted { color: var(--pi-muted); }
|
|
71
|
-
.pi-client-server-panel form { display: grid; gap: 8px; }
|
|
72
|
-
.pi-client-server-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
73
|
-
</style>
|
|
74
|
-
<div class="pi-client-server-panel">
|
|
75
|
-
<div class="pi-client-server-row"><span class="pi-client-server-dot ${dot}"></span><strong>pi-server</strong><small>${escapeHtml(statusText(this.state))}</small></div>
|
|
76
|
-
<form>
|
|
77
|
-
<label>
|
|
78
|
-
<small>Server URL</small>
|
|
79
|
-
<input name="piServerUrl" value="${escapeAttr(data?.serverUrl ?? "")}" placeholder="http://127.0.0.1:4217" />
|
|
80
|
-
</label>
|
|
81
|
-
<div class="pi-client-server-actions">
|
|
82
|
-
<button class="primary" type="button" data-save>${this.state?.saving === true ? "Saving..." : "Save"}</button>
|
|
83
|
-
<button type="button" data-refresh>Refresh</button>
|
|
84
|
-
</div>
|
|
85
|
-
</form>
|
|
86
|
-
<small>URL source: ${escapeHtml(data?.urlSource ?? "unknown")} · token: ${data?.tokenConfigured === true ? "configured" : "not configured"}</small>
|
|
87
|
-
${data?.restartRequired === true ? `<small>Restart pi-client web for saved server URL changes to affect new sessions.</small>` : ""}
|
|
88
|
-
${this.state?.saved === true ? `<small>Saved.</small>` : ""}
|
|
89
|
-
${this.state?.error === undefined ? "" : `<small>${escapeHtml(this.state.error)}</small>`}
|
|
90
|
-
</div>
|
|
91
|
-
`;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
class PiClientServerDialog extends HTMLElement {
|
|
96
|
-
connectedCallback() {
|
|
97
|
-
this.render();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
render() {
|
|
101
|
-
this.innerHTML = `
|
|
102
|
-
<style>
|
|
103
|
-
.pi-client-server-backdrop { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; background: rgb(0 0 0 / 0.5); }
|
|
104
|
-
.pi-client-server-dialog { width: min(520px, calc(100vw - 32px)); border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); box-shadow: 0 20px 60px rgb(0 0 0 / 0.35); overflow: hidden; }
|
|
105
|
-
.pi-client-server-dialog header { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid var(--pi-border); }
|
|
106
|
-
.pi-client-server-dialog button { border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 9px; cursor: pointer; }
|
|
107
|
-
</style>
|
|
108
|
-
<div class="pi-client-server-backdrop">
|
|
109
|
-
<section class="pi-client-server-dialog">
|
|
110
|
-
<header><strong>Pi Server Settings</strong><button type="button" data-close>Close</button></header>
|
|
111
|
-
<pi-client-server-panel></pi-client-server-panel>
|
|
112
|
-
</section>
|
|
113
|
-
</div>
|
|
114
|
-
`;
|
|
115
|
-
this.querySelector("[data-close]")?.addEventListener("click", () => this.remove());
|
|
116
|
-
this.querySelector(".pi-client-server-backdrop")?.addEventListener("click", (event) => {
|
|
117
|
-
if (event.target === event.currentTarget) this.remove();
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
class PiClientServerBadge extends HTMLElement {
|
|
123
|
-
connectedCallback() {
|
|
124
|
-
if (this.shadowRoot === null) this.attachShadow({ mode: "open" });
|
|
125
|
-
this.load();
|
|
126
|
-
window.addEventListener("focus", this);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
disconnectedCallback() {
|
|
130
|
-
window.removeEventListener("focus", this);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
handleEvent() {
|
|
134
|
-
this.load();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async load() {
|
|
138
|
-
try {
|
|
139
|
-
const response = await fetch(endpoint);
|
|
140
|
-
this.data = response.ok ? await response.json() : { reachable: false };
|
|
141
|
-
} catch {
|
|
142
|
-
this.data = { reachable: false };
|
|
143
|
-
}
|
|
144
|
-
this.render();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
render() {
|
|
148
|
-
const ok = this.data?.reachable === true && this.data?.authenticated !== false;
|
|
149
|
-
this.shadowRoot.innerHTML = `
|
|
150
|
-
<style>
|
|
151
|
-
:host { position: fixed; right: 12px; bottom: 10px; z-index: 1000; }
|
|
152
|
-
button { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--pi-border); border-radius: 999px; background: var(--pi-bg); color: var(--pi-muted); padding: 5px 9px; font-size: 12px; cursor: pointer; }
|
|
153
|
-
.dot { width: 8px; height: 8px; border-radius: 50%; background: ${ok ? "#2ea043" : "#f85149"}; }
|
|
154
|
-
</style>
|
|
155
|
-
<button type="button" title="Pi Server Settings"><span class="dot"></span>pi-server</button>
|
|
156
|
-
`;
|
|
157
|
-
this.shadowRoot.querySelector("button")?.addEventListener("click", () => openPiClientServerDialog());
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
class PiClientAgentsDialog extends HTMLElement {
|
|
162
|
-
connectedCallback() {
|
|
163
|
-
if (this.eventsBound !== true) {
|
|
164
|
-
this.eventsBound = true;
|
|
165
|
-
this.addEventListener("click", (event) => {
|
|
166
|
-
const path = event.composedPath();
|
|
167
|
-
if (path.some((node) => typeof node?.getAttribute === "function" && node.getAttribute("data-close") !== null)) this.remove();
|
|
168
|
-
if (path.some((node) => typeof node?.getAttribute === "function" && node.getAttribute("data-save-agents") !== null)) void this.save(event);
|
|
169
|
-
});
|
|
170
|
-
this.addEventListener("submit", (event) => this.save(event));
|
|
171
|
-
}
|
|
172
|
-
this.load();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async load() {
|
|
176
|
-
this.state = { loading: true };
|
|
177
|
-
this.render();
|
|
178
|
-
const response = await fetch(agentsEndpoint);
|
|
179
|
-
if (!response.ok) throw new Error(response.statusText);
|
|
180
|
-
this.state = { data: await response.json() };
|
|
181
|
-
this.render();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async save(event) {
|
|
185
|
-
event?.preventDefault();
|
|
186
|
-
const textarea = this.querySelector("textarea[name='content']");
|
|
187
|
-
if (typeof textarea?.value !== "string") return;
|
|
188
|
-
this.state = { ...this.state, saving: true };
|
|
189
|
-
this.render();
|
|
190
|
-
const response = await fetch(agentsEndpoint, {
|
|
191
|
-
method: "PUT",
|
|
192
|
-
headers: { "content-type": "application/json" },
|
|
193
|
-
body: JSON.stringify({ content: textarea.value }),
|
|
194
|
-
});
|
|
195
|
-
if (!response.ok) throw new Error((await response.json()).error ?? response.statusText);
|
|
196
|
-
this.state = { data: await response.json(), saved: true };
|
|
197
|
-
this.render();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
render() {
|
|
201
|
-
const data = this.state?.data;
|
|
202
|
-
this.innerHTML = `
|
|
203
|
-
<style>
|
|
204
|
-
.pi-client-agents-backdrop { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; background: rgb(0 0 0 / 0.5); }
|
|
205
|
-
.pi-client-agents-dialog { width: min(840px, calc(100vw - 32px)); max-height: calc(100vh - 32px); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); box-shadow: 0 20px 60px rgb(0 0 0 / 0.35); overflow: hidden; color: var(--pi-text); }
|
|
206
|
-
.pi-client-agents-dialog header, .pi-client-agents-dialog footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--pi-border); }
|
|
207
|
-
.pi-client-agents-dialog footer { border-top: 1px solid var(--pi-border); border-bottom: 0; }
|
|
208
|
-
.pi-client-agents-dialog button { border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-text); padding: 6px 10px; cursor: pointer; }
|
|
209
|
-
.pi-client-agents-dialog button.primary { border-color: var(--pi-accent-border); color: var(--pi-text-bright); }
|
|
210
|
-
.pi-client-agents-dialog small { color: var(--pi-muted); overflow-wrap: anywhere; }
|
|
211
|
-
.pi-client-agents-dialog textarea { width: 100%; min-height: 420px; height: min(56vh, 640px); resize: vertical; box-sizing: border-box; border: 0; border-bottom: 1px solid var(--pi-border); background: var(--pi-terminal-bg, var(--pi-bg)); color: var(--pi-terminal-text, var(--pi-text)); padding: 12px; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
|
|
212
|
-
.pi-client-agents-body { min-height: 0; }
|
|
213
|
-
.pi-client-agents-actions { display: flex; gap: 8px; align-items: center; }
|
|
214
|
-
</style>
|
|
215
|
-
<div class="pi-client-agents-backdrop">
|
|
216
|
-
<form class="pi-client-agents-dialog">
|
|
217
|
-
<header><strong>Global AGENTS.md</strong><button type="button" data-close>Close</button></header>
|
|
218
|
-
<div class="pi-client-agents-body">
|
|
219
|
-
<textarea name="content" spellcheck="false" ${this.state?.loading === true ? "disabled" : ""}>${escapeHtml(data?.content ?? "")}</textarea>
|
|
220
|
-
</div>
|
|
221
|
-
<footer>
|
|
222
|
-
<small>${escapeHtml(data?.path ?? "Loading...")}</small>
|
|
223
|
-
<div class="pi-client-agents-actions">
|
|
224
|
-
${this.state?.saved === true ? `<small>Saved.</small>` : ""}
|
|
225
|
-
<button class="primary" type="button" data-save-agents>${this.state?.saving === true ? "Saving..." : "Save"}</button>
|
|
226
|
-
</div>
|
|
227
|
-
</footer>
|
|
228
|
-
</form>
|
|
229
|
-
</div>
|
|
230
|
-
`;
|
|
231
|
-
this.querySelector(".pi-client-agents-backdrop")?.addEventListener("click", (event) => {
|
|
232
|
-
if (event.target === event.currentTarget) this.remove();
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
class PiClientProjectsDialog extends HTMLElement {
|
|
238
|
-
connectedCallback() {
|
|
239
|
-
if (this.eventsBound !== true) {
|
|
240
|
-
this.eventsBound = true;
|
|
241
|
-
this.addEventListener("click", (event) => {
|
|
242
|
-
const path = event.composedPath();
|
|
243
|
-
if (path.some((node) => typeof node?.getAttribute === "function" && node.getAttribute("data-close") !== null)) this.remove();
|
|
244
|
-
const button = path.find((node) => typeof node?.getAttribute === "function" && node.getAttribute("data-project-id") !== null);
|
|
245
|
-
if (button !== undefined) void this.setVisibility(button.getAttribute("data-project-id"), button.getAttribute("data-visible") === "true");
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
this.load();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async load() {
|
|
252
|
-
this.state = { loading: true };
|
|
253
|
-
this.render();
|
|
254
|
-
const response = await fetch(projectsEndpoint);
|
|
255
|
-
if (!response.ok) throw new Error(response.statusText);
|
|
256
|
-
this.state = { projects: await response.json() };
|
|
257
|
-
this.render();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async setVisibility(projectId, visible) {
|
|
261
|
-
this.state = { ...this.state, saving: projectId };
|
|
262
|
-
this.render();
|
|
263
|
-
const response = await fetch(`${projectsEndpoint}/${encodeURIComponent(projectId)}/visibility`, {
|
|
264
|
-
method: "PUT",
|
|
265
|
-
headers: { "content-type": "application/json" },
|
|
266
|
-
body: JSON.stringify({ visible }),
|
|
267
|
-
});
|
|
268
|
-
if (!response.ok) throw new Error((await response.json()).error ?? response.statusText);
|
|
269
|
-
this.state = { projects: await response.json(), saved: true };
|
|
270
|
-
this.render();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
render() {
|
|
274
|
-
const projects = this.state?.projects ?? [];
|
|
275
|
-
this.innerHTML = `
|
|
276
|
-
<style>
|
|
277
|
-
.pi-client-projects-backdrop { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; background: rgb(0 0 0 / 0.5); }
|
|
278
|
-
.pi-client-projects-dialog { width: min(720px, calc(100vw - 32px)); max-height: calc(100vh - 32px); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); box-shadow: 0 20px 60px rgb(0 0 0 / 0.35); overflow: hidden; color: var(--pi-text); }
|
|
279
|
-
.pi-client-projects-dialog header, .pi-client-projects-dialog footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--pi-border); }
|
|
280
|
-
.pi-client-projects-dialog footer { border-top: 1px solid var(--pi-border); border-bottom: 0; }
|
|
281
|
-
.pi-client-projects-dialog button { border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-text); padding: 6px 10px; cursor: pointer; }
|
|
282
|
-
.pi-client-projects-dialog small { color: var(--pi-muted); overflow-wrap: anywhere; }
|
|
283
|
-
.pi-client-projects-list { overflow: auto; }
|
|
284
|
-
.pi-client-project-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 10px; align-items: center; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted, var(--pi-border)); }
|
|
285
|
-
.pi-client-project-row strong { display: block; }
|
|
286
|
-
.pi-client-project-empty { padding: 18px 12px; color: var(--pi-muted); }
|
|
287
|
-
</style>
|
|
288
|
-
<div class="pi-client-projects-backdrop">
|
|
289
|
-
<section class="pi-client-projects-dialog">
|
|
290
|
-
<header><strong>Project Visibility</strong><button type="button" data-close>Close</button></header>
|
|
291
|
-
<div class="pi-client-projects-list">
|
|
292
|
-
${
|
|
293
|
-
this.state?.loading === true
|
|
294
|
-
? `<div class="pi-client-project-empty">Loading...</div>`
|
|
295
|
-
: projects.length === 0
|
|
296
|
-
? `<div class="pi-client-project-empty">No projects yet.</div>`
|
|
297
|
-
: projects.map((project) => projectRow(project, this.state?.saving)).join("")
|
|
298
|
-
}
|
|
299
|
-
</div>
|
|
300
|
-
<footer><small>Visible projects appear in the PI WEB sidebar.</small>${this.state?.saved === true ? `<small>Saved.</small>` : ""}</footer>
|
|
301
|
-
</section>
|
|
302
|
-
</div>
|
|
303
|
-
`;
|
|
304
|
-
this.querySelector(".pi-client-projects-backdrop")?.addEventListener("click", (event) => {
|
|
305
|
-
if (event.target === event.currentTarget) this.remove();
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
class PiClientQuickbar extends HTMLElement {
|
|
311
|
-
connectedCallback() {
|
|
312
|
-
if (this.shadowRoot === null) this.attachShadow({ mode: "open" });
|
|
313
|
-
this.render();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
render() {
|
|
317
|
-
this.shadowRoot.innerHTML = `
|
|
318
|
-
<style>
|
|
319
|
-
:host { display: block; border-bottom: 1px solid var(--pi-border); padding: 8px 10px; }
|
|
320
|
-
.pi-client-quickbar { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
|
|
321
|
-
button { min-width: 0; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-text); padding: 6px 4px; font-size: 12px; line-height: 1; cursor: pointer; }
|
|
322
|
-
</style>
|
|
323
|
-
<div class="pi-client-quickbar">
|
|
324
|
-
<button type="button" title="New Conversation" data-action="new">New</button>
|
|
325
|
-
<button type="button" title="Search" data-action="search">Search</button>
|
|
326
|
-
<button type="button" title="Skill Management" data-action="skills">Skills</button>
|
|
327
|
-
<button type="button" title="Global AGENTS.md" data-action="agents">AGENTS</button>
|
|
328
|
-
</div>
|
|
329
|
-
`;
|
|
330
|
-
this.shadowRoot.querySelectorAll("button").forEach((button) => {
|
|
331
|
-
button.addEventListener("click", () => runQuickbarAction(button.getAttribute("data-action")));
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
customElements.define("pi-client-server-panel", PiClientServerPanel);
|
|
337
|
-
customElements.define("pi-client-server-dialog", PiClientServerDialog);
|
|
338
|
-
customElements.define("pi-client-server-badge", PiClientServerBadge);
|
|
339
|
-
customElements.define("pi-client-agents-dialog", PiClientAgentsDialog);
|
|
340
|
-
customElements.define("pi-client-projects-dialog", PiClientProjectsDialog);
|
|
341
|
-
customElements.define("pi-client-quickbar", PiClientQuickbar);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function openPiClientServerDialog() {
|
|
345
|
-
document.querySelector("pi-client-server-dialog")?.remove();
|
|
346
|
-
document.body.append(document.createElement("pi-client-server-dialog"));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function openPiClientAgentsDialog() {
|
|
350
|
-
document.querySelector("pi-client-agents-dialog")?.remove();
|
|
351
|
-
document.body.append(document.createElement("pi-client-agents-dialog"));
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function openPiClientProjectsDialog() {
|
|
355
|
-
document.querySelector("pi-client-projects-dialog")?.remove();
|
|
356
|
-
document.body.append(document.createElement("pi-client-projects-dialog"));
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function installBadge() {
|
|
360
|
-
if (document.querySelector("pi-client-server-badge") !== null) return;
|
|
361
|
-
document.body.append(document.createElement("pi-client-server-badge"));
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function installQuickbar() {
|
|
365
|
-
const root = document.querySelector("pi-web-app")?.shadowRoot?.querySelector("app-navigation-panel")?.shadowRoot;
|
|
366
|
-
if (root === undefined || root === null) {
|
|
367
|
-
window.requestAnimationFrame(installQuickbar);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (root.querySelector("pi-client-quickbar") === null) root.querySelector("header")?.after(document.createElement("pi-client-quickbar"));
|
|
371
|
-
if (window.piClientQuickbarObserver !== undefined) return;
|
|
372
|
-
window.piClientQuickbarObserver = new MutationObserver(() => {
|
|
373
|
-
if (root.querySelector("pi-client-quickbar") === null) root.querySelector("header")?.after(document.createElement("pi-client-quickbar"));
|
|
374
|
-
});
|
|
375
|
-
window.piClientQuickbarObserver.observe(root, { childList: true });
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function runQuickbarAction(action) {
|
|
379
|
-
const context = piWebContext();
|
|
380
|
-
if (action === "new") {
|
|
381
|
-
if (context.state.selectedWorkspace === undefined) {
|
|
382
|
-
context.addProject();
|
|
383
|
-
} else {
|
|
384
|
-
void context.startSession();
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (action === "search") context.openActionPalette();
|
|
388
|
-
if (action === "skills") context.piWebUnstable.openSettings("plugins");
|
|
389
|
-
if (action === "agents") openPiClientAgentsDialog();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function piWebContext() {
|
|
393
|
-
// ponytail: PI WEB exposes runtime helpers to actions but not fixed toolbar slots yet.
|
|
394
|
-
return document.querySelector("pi-web-app").createPluginRuntimeContext();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function projectRow(project, savingProjectId) {
|
|
398
|
-
const visible = project.hidden !== true;
|
|
399
|
-
const saving = savingProjectId === project.id;
|
|
400
|
-
return `
|
|
401
|
-
<div class="pi-client-project-row">
|
|
402
|
-
<div>
|
|
403
|
-
<strong>${escapeHtml(project.name)}</strong>
|
|
404
|
-
<small>${escapeHtml(project.path)}</small>
|
|
405
|
-
</div>
|
|
406
|
-
<small>${visible ? "Visible" : "Hidden"}</small>
|
|
407
|
-
<button type="button" data-project-id="${escapeAttr(project.id)}" data-visible="${visible ? "false" : "true"}">${saving ? "Saving..." : visible ? "Hide" : "Show"}</button>
|
|
408
|
-
</div>
|
|
409
|
-
`;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function statusText(state) {
|
|
413
|
-
if (state?.loading === true) return "checking";
|
|
414
|
-
if (state?.error !== undefined) return state.error;
|
|
415
|
-
if (state?.data === undefined) return "unknown";
|
|
416
|
-
if (state.data.reachable !== true) return "offline";
|
|
417
|
-
if (state.data.authenticated === false) return "auth failed";
|
|
418
|
-
return "online";
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function escapeHtml(value) {
|
|
422
|
-
return String(value).replace(/[&<>"']/gu, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function escapeAttr(value) {
|
|
426
|
-
return escapeHtml(value);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const plugin = {
|
|
430
|
-
apiVersion: 1,
|
|
431
|
-
name: "pi-client",
|
|
432
|
-
activate: ({ html, svg }) => {
|
|
433
|
-
definePiClientServerElements();
|
|
434
|
-
queueMicrotask(installBadge);
|
|
435
|
-
queueMicrotask(installQuickbar);
|
|
436
|
-
return {
|
|
437
|
-
contributions: {
|
|
438
|
-
actions: [
|
|
439
|
-
{
|
|
440
|
-
id: "pi-client.add-project",
|
|
441
|
-
title: "Add Project",
|
|
442
|
-
description: "Add a local folder to the pi-client web sidebar.",
|
|
443
|
-
group: "Pi Client",
|
|
444
|
-
run: (context) => context.addProject(),
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
id: "pi-client.new-conversation",
|
|
448
|
-
title: "New Conversation",
|
|
449
|
-
description: "Start a new pi-client session in the selected workspace.",
|
|
450
|
-
group: "Pi Client",
|
|
451
|
-
enabled: (context) => context.state.selectedWorkspace !== undefined,
|
|
452
|
-
disabledReason: () => "Select a workspace first.",
|
|
453
|
-
run: (context) => context.startSession(),
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
id: "pi-client.search",
|
|
457
|
-
title: "Search",
|
|
458
|
-
description: "Open PI WEB's action search.",
|
|
459
|
-
group: "Pi Client",
|
|
460
|
-
run: (context) => context.openActionPalette(),
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
id: "pi-client.skill-management",
|
|
464
|
-
title: "Skill Management",
|
|
465
|
-
description: "Open PI WEB plugin management.",
|
|
466
|
-
group: "Pi Client",
|
|
467
|
-
run: (context) => context.piWebUnstable.openSettings("plugins"),
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
id: "pi-client.project-visibility",
|
|
471
|
-
title: "Project Visibility",
|
|
472
|
-
description: "Hide or show projects in the pi-client web sidebar.",
|
|
473
|
-
group: "Pi Client",
|
|
474
|
-
run: openPiClientProjectsDialog,
|
|
475
|
-
},
|
|
476
|
-
{
|
|
477
|
-
id: "pi-client.global-agents",
|
|
478
|
-
title: "Global AGENTS.md",
|
|
479
|
-
description: "Edit the shared pi-client AGENTS.md file.",
|
|
480
|
-
group: "Pi Client",
|
|
481
|
-
run: openPiClientAgentsDialog,
|
|
482
|
-
},
|
|
483
|
-
{
|
|
484
|
-
id: "pi-client.open-pi-server-settings",
|
|
485
|
-
title: "Pi Server Settings",
|
|
486
|
-
description: "Configure the pi-server URL used by pi-client web sessions.",
|
|
487
|
-
group: "Pi Client",
|
|
488
|
-
run: openPiClientServerDialog,
|
|
489
|
-
},
|
|
490
|
-
],
|
|
491
|
-
workspacePanels: [
|
|
492
|
-
{
|
|
493
|
-
id: "pi-client.server",
|
|
494
|
-
title: "Pi Server",
|
|
495
|
-
icon: svg`
|
|
496
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
497
|
-
<path d="M12 2v6"></path>
|
|
498
|
-
<path d="M12 16v6"></path>
|
|
499
|
-
<path d="M4.9 4.9l4.2 4.2"></path>
|
|
500
|
-
<path d="M14.9 14.9l4.2 4.2"></path>
|
|
501
|
-
<path d="M2 12h6"></path>
|
|
502
|
-
<path d="M16 12h6"></path>
|
|
503
|
-
<circle cx="12" cy="12" r="4"></circle>
|
|
504
|
-
</svg>
|
|
505
|
-
`,
|
|
506
|
-
order: 5,
|
|
507
|
-
render: () => html`<pi-client-server-panel></pi-client-server-panel>`,
|
|
508
|
-
},
|
|
509
|
-
],
|
|
510
|
-
},
|
|
511
|
-
};
|
|
512
|
-
},
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
export default plugin;
|