@averyyy/pi-client 0.80.3 → 0.80.6-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 CHANGED
@@ -5,5 +5,14 @@
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
- - `pi-client web` command that starts the PI WEB client GUI on port `1838` by default.
9
- - Pi Server status, URL settings, client actions, project visibility, and global `AGENTS.md` editing inside `pi-client web`.
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.
14
+
15
+ ### Changed
16
+
17
+ - Rebased the client on upstream Pi `0.80.6`, including GPT-5.6 model metadata and `max` thinking support.
18
+ - Changed `pi-client update` to update global packages without reinstalling the active checkout or stopping sessions; updated sessions require `/reload` to restart on the new runtime with their existing history.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @averyyy/pi-client
2
+
3
+ Client CLI for connecting Pi to a `pi-server` instance.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -g --ignore-scripts --legacy-peer-deps @averyyy/pi-client
9
+ ```
10
+
11
+ `--legacy-peer-deps` avoids npm peer override warnings when upstream Pi is already installed globally.
12
+
13
+ ## Use
14
+
15
+ Connect to the hosted server:
16
+
17
+ ```bash
18
+ PI_SERVER_URL=https://pi.yreva.asia pi-client
19
+ ```
20
+
21
+ Send one prompt and exit:
22
+
23
+ ```bash
24
+ PI_SERVER_URL=https://pi.yreva.asia pi-client -p "Say exactly: ok"
25
+ ```
26
+
27
+ Start the browser UI:
28
+
29
+ ```bash
30
+ pi install npm:tau-mirror
31
+ PI_SERVER_URL=https://pi.yreva.asia pi-client web
32
+ ```
33
+
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.
35
+
36
+ ## Server Auth
37
+
38
+ If your server uses an auth token, set it on the client:
39
+
40
+ ```bash
41
+ PI_SERVER_AUTH_TOKEN=your-token PI_SERVER_URL=http://127.0.0.1:4217 pi-client
42
+ ```
43
+
44
+ ## Update
45
+
46
+ `pi-client update` installs the latest client and server packages without stopping active client sessions. Existing sessions block new prompts until you run `/reload`; `/reload` restarts that session on the new runtime and resumes its persisted history.
47
+
48
+ ## Related Package
49
+
50
+ Install the server separately:
51
+
52
+ ```bash
53
+ npm i -g --ignore-scripts @averyyy/pi-server
54
+ ```
package/bin/pi-client.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
3
5
  import { dirname, join } from "node:path";
4
6
  import { fileURLToPath } from "node:url";
5
7
 
6
8
  const args = process.argv.slice(2);
7
9
  const modulePromise =
8
10
  args[0] === "update"
9
- ? import("./update.js").then(({ runPiClientUpdate }) => {
10
- process.exitCode = runPiClientUpdate(args.slice(1));
11
+ ? import("./update.js").then(async ({ runPiClientUpdate }) => {
12
+ process.exitCode = await runPiClientUpdate(args.slice(1));
11
13
  })
12
14
  : args[0] === "web"
13
15
  ? import("./web.js").then(async ({ runPiClientWeb }) => {
@@ -21,15 +23,66 @@ Promise.resolve(modulePromise).catch((e) => {
21
23
  });
22
24
 
23
25
  function runPiClientCli(args) {
26
+ const reloadDir = mkdtempSync(join(tmpdir(), "pi-client-reload-"));
27
+ const reloadStatePath = join(reloadDir, "state.json");
28
+ let cliPath = getLocalCliPath();
29
+ let childArgs = args;
30
+
31
+ try {
32
+ for (;;) {
33
+ const result = spawnSync(process.execPath, [cliPath, ...childArgs], {
34
+ env: {
35
+ ...process.env,
36
+ PI_CODING_AGENT: "true",
37
+ PI_SERVER_MODE: "true",
38
+ PI_CLIENT_RELOAD_STATE_PATH: reloadStatePath,
39
+ },
40
+ stdio: "inherit",
41
+ });
42
+ if (result.error) throw result.error;
43
+ if (result.status !== 75) {
44
+ process.exitCode = result.status ?? (result.signal === "SIGINT" ? 130 : 1);
45
+ return;
46
+ }
47
+
48
+ const state = readReloadState(reloadStatePath);
49
+ if (!state) {
50
+ console.error("pi-client reload failed: missing session state from the previous runtime.");
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+ rmSync(state.updateMarkerPath, { force: true });
55
+ childArgs = state.sessionDir ? ["--session-dir", state.sessionDir, "--session", state.sessionId] : ["--session", state.sessionId];
56
+ cliPath = getUpdatedGlobalCliPath() ?? getLocalCliPath();
57
+ }
58
+ } finally {
59
+ rmSync(reloadDir, { recursive: true, force: true });
60
+ }
61
+ }
62
+
63
+ function getLocalCliPath() {
24
64
  const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
25
- const result = spawnSync(process.execPath, [join(dirname(entry), "cli.js"), ...args], {
26
- env: {
27
- ...process.env,
28
- PI_CODING_AGENT: "true",
29
- PI_SERVER_MODE: "true",
30
- },
31
- stdio: "inherit",
32
- });
33
- if (result.error) throw result.error;
34
- process.exitCode = result.status ?? (result.signal === "SIGINT" ? 130 : 1);
65
+ return join(dirname(entry), "cli.js");
66
+ }
67
+
68
+ function getUpdatedGlobalCliPath() {
69
+ const npmCli = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
70
+ const result = existsSync(npmCli)
71
+ ? spawnSync(process.execPath, [npmCli, "root", "-g"], { encoding: "utf-8" })
72
+ : spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
73
+ if (result.status !== 0) return undefined;
74
+ const root = result.stdout.trim();
75
+ const cliPath = join(root, "@averyyy", "pi-client", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js");
76
+ return existsSync(cliPath) ? cliPath : undefined;
77
+ }
78
+
79
+ function readReloadState(path) {
80
+ try {
81
+ const value = JSON.parse(readFileSync(path, "utf-8"));
82
+ if (typeof value.sessionId !== "string" || typeof value.updateMarkerPath !== "string") return undefined;
83
+ if (value.sessionDir !== undefined && typeof value.sessionDir !== "string") return undefined;
84
+ return value;
85
+ } catch {
86
+ return undefined;
87
+ }
35
88
  }
package/bin/update.js CHANGED
@@ -1,16 +1,13 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { readFileSync } from "node:fs";
3
- import { dirname, resolve } from "node:path";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
6
7
  function defaultPackageRoot() {
7
8
  return dirname(dirname(fileURLToPath(import.meta.url)));
8
9
  }
9
10
 
10
- function defaultRepoRoot() {
11
- return resolve(defaultPackageRoot(), "..", "..");
12
- }
13
-
14
11
  function readPackageMetadata(packageRoot) {
15
12
  return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf-8"));
16
13
  }
@@ -19,44 +16,53 @@ function runStep(runner, command, args, cwd, stdio = "inherit") {
19
16
  return runner(command, args, { cwd, stdio, encoding: "utf-8" });
20
17
  }
21
18
 
22
- export function runPiClientUpdate(_args = [], options = {}) {
19
+ function defaultUpdateMarkerPath() {
20
+ return join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "pi-client-update.json");
21
+ }
22
+
23
+ function writeUpdateMarker(path) {
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ writeFileSync(path, `${JSON.stringify({ version: "latest", updatedAt: new Date().toISOString() })}\n`);
26
+ }
27
+
28
+ function runNpmGlobalUpdate(runner, cwd, stdout, stderr, updateMarkerPath) {
29
+ stdout.write("Updating npm packages: @averyyy/pi-client@latest @averyyy/pi-server@latest\n");
30
+ const result = runStep(
31
+ runner,
32
+ "npm",
33
+ [
34
+ "install",
35
+ "-g",
36
+ "--ignore-scripts",
37
+ "--legacy-peer-deps",
38
+ "--force",
39
+ "@averyyy/pi-client@latest",
40
+ "@averyyy/pi-server@latest",
41
+ ],
42
+ cwd,
43
+ );
44
+ if (result.status !== 0) {
45
+ stderr.write(
46
+ "pi-client update failed: npm install -g --ignore-scripts --legacy-peer-deps --force @averyyy/pi-client@latest @averyyy/pi-server@latest\n",
47
+ );
48
+ return result.status ?? 1;
49
+ }
50
+ writeUpdateMarker(updateMarkerPath);
51
+ stdout.write("Updated package files without stopping active pi-client sessions. Run /reload in each session to switch runtimes.\n");
52
+ stdout.write("pi-client update complete\n");
53
+ return 0;
54
+ }
55
+
56
+ export async function runPiClientUpdate(_args = [], options = {}) {
23
57
  const packageRoot = options.packageRoot ?? defaultPackageRoot();
24
- const repoRoot = options.repoRoot ?? defaultRepoRoot();
25
58
  const runner = options.runner ?? spawnSync;
26
59
  const stdout = options.stdout ?? process.stdout;
27
60
  const stderr = options.stderr ?? process.stderr;
61
+ const updateMarkerPath = options.updateMarkerPath ?? defaultUpdateMarkerPath();
28
62
  const pkg = readPackageMetadata(packageRoot);
29
63
  const baseVersion = pkg.piClient?.basePiVersion ?? "unknown";
30
64
  const baseCommit = pkg.piClient?.basePiCommit ?? "unknown";
31
65
 
32
66
  stdout.write(`pi-client ${pkg.version} (based on pi ${baseVersion}, upstream ${baseCommit})\n`);
33
- stdout.write(`Updating checkout: ${repoRoot}\n`);
34
-
35
- const status = runStep(runner, "git", ["status", "--porcelain"], repoRoot, "pipe");
36
- if (status.status !== 0) {
37
- stderr.write("pi-client update failed: unable to inspect git status\n");
38
- return status.status ?? 1;
39
- }
40
- if (String(status.stdout ?? "").trim().length > 0) {
41
- stderr.write("pi-client update failed: working tree has uncommitted changes\n");
42
- return 1;
43
- }
44
-
45
- const steps = [
46
- ["git", ["pull", "--ff-only"]],
47
- ["npm", ["install", "--ignore-scripts"]],
48
- ["npm", ["run", "install:pi-client"]],
49
- ["npm", ["run", "install:pi-server"]],
50
- ];
51
-
52
- for (const [command, args] of steps) {
53
- const result = runStep(runner, command, args, repoRoot);
54
- if (result.status !== 0) {
55
- stderr.write(`pi-client update failed: ${command} ${args.join(" ")}\n`);
56
- return result.status ?? 1;
57
- }
58
- }
59
-
60
- stdout.write("pi-client update complete\n");
61
- return 0;
67
+ return runNpmGlobalUpdate(runner, process.cwd(), stdout, stderr, updateMarkerPath);
62
68
  }
package/bin/web.js CHANGED
@@ -1,23 +1,16 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
6
4
  import { homedir } from "node:os";
7
5
  import { dirname, join } from "node:path";
6
+ import { createInterface } from "node:readline/promises";
8
7
  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
8
 
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
9
  const defaultPort = "1838";
20
10
  const defaultPiServerUrl = "http://127.0.0.1:4217";
11
+ const tauCodexPackage = "@averyyy/pi-tau-codex";
12
+ const tauCodexInstallTarget = "npm:@averyyy/pi-tau-codex";
13
+ const tauCodexGitTarget = "git:github.com/Averyyy/pi-tau-codex";
21
14
 
22
15
  export async function runPiClientWeb(args = process.argv.slice(2)) {
23
16
  const parsed = parseArgs(args);
@@ -26,256 +19,97 @@ export async function runPiClientWeb(args = process.argv.slice(2)) {
26
19
  return 0;
27
20
  }
28
21
 
22
+ if (!hasTauCodexExtensionInstalled(process.env)) {
23
+ if (!await offerTauCodexInstall()) return 1;
24
+ }
25
+
29
26
  process.title = "pi-client web";
30
- const piServer = await effectivePiServerSettings(process.env);
31
- const childEnv = {
32
- ...process.env,
33
- PI_CODING_AGENT: "true",
34
- PI_SERVER_MODE: "true",
35
- PI_SERVER_URL: piServer.serverUrl,
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
- }),
27
+ const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
28
+ const port = parsed.port;
29
+ const host = process.env.TAU_HOST ?? "127.0.0.1";
30
+ const child = spawn(process.execPath, [join(dirname(entry), "cli.js"), ...parsed.clientArgs], {
31
+ env: piClientWebEnv(process.env, { port, host }),
32
+ stdio: "inherit",
52
33
  });
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
34
 
57
- let shuttingDown = false;
58
- return await new Promise((resolve) => {
59
- const shutdown = (exitCode) => {
60
- if (shuttingDown) return;
61
- shuttingDown = true;
62
- sessiond.kill();
63
- void app.close().finally(() => {
64
- resolve(exitCode);
65
- });
66
- };
35
+ console.log(`pi-client web uses Tau at http://${host}:${port}`);
67
36
 
68
- sessiond.once("error", (error) => {
69
- console.error(error);
70
- shutdown(1);
71
- });
72
- sessiond.once("exit", (code, signal) => {
73
- shutdown(code ?? (signal === "SIGINT" ? 130 : 1));
37
+ return await new Promise((resolve, reject) => {
38
+ child.once("error", reject);
39
+ child.once("exit", (code, signal) => {
40
+ resolve(code ?? (signal === "SIGINT" ? 130 : 1));
74
41
  });
75
- process.once("SIGINT", () => shutdown(130));
76
- process.once("SIGTERM", () => shutdown(143));
77
42
  });
78
43
  }
79
44
 
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
- });
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
- }
45
+ export async function offerTauCodexInstall(env = process.env, input = process.stdin, output = process.stdout) {
46
+ printInstallRequired();
47
+ if (!input.isTTY || !output.isTTY) return false;
151
48
 
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
- }
49
+ const rl = createInterface({ input, output });
50
+ const answer = (await rl.question("现在安装吗? [y/N] ")).trim().toLowerCase();
51
+ rl.close();
52
+ if (answer !== "y" && answer !== "yes") return false;
192
53
 
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, "");
54
+ const result = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), "pi-client.js"), "install", tauCodexInstallTarget], {
55
+ env,
56
+ stdio: "inherit",
57
+ });
58
+ if (result.error) throw result.error;
59
+ return result.status === 0 && hasTauCodexExtensionInstalled(env);
198
60
  }
199
61
 
200
- async function globalAgentsFile() {
201
- const filePath = globalAgentsPath();
202
- const exists = existsSync(filePath);
62
+ export function piClientWebEnv(env = process.env, options) {
203
63
  return {
204
- path: filePath,
205
- exists,
206
- content: exists ? await readFile(filePath, "utf-8") : "",
64
+ ...env,
65
+ PI_CODING_AGENT: "true",
66
+ PI_SERVER_MODE: "true",
67
+ PI_SERVER_URL: env.PI_SERVER_URL ?? defaultPiServerUrl,
68
+ TAU_HOST: options.host,
69
+ TAU_MIRROR_PORT: options.port,
207
70
  };
208
71
  }
209
72
 
210
- async function saveGlobalAgentsFile(content) {
211
- const filePath = globalAgentsPath();
212
- await mkdir(dirname(filePath), { recursive: true });
213
- await writeFile(filePath, content, "utf-8");
73
+ export function hasTauCodexExtensionInstalled(env = process.env) {
74
+ if (env.TAU_STATIC_DIR) return true;
75
+ const settingsPath = env.PI_CODING_AGENT_SETTINGS_PATH ?? join(
76
+ env.PI_CODING_AGENT_DIR ?? join(env.HOME ?? homedir(), ".pi", "agent"),
77
+ "settings.json",
78
+ );
79
+ if (!existsSync(settingsPath)) return false;
80
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
81
+ const packages = Array.isArray(settings.packages) ? settings.packages : [];
82
+ return packages.some(packageSpecMatchesTauCodex);
214
83
  }
215
84
 
216
- function globalAgentsPath() {
217
- return join(getAgentDir(), "AGENTS.md");
85
+ function packageSpecMatchesTauCodex(spec) {
86
+ if (typeof spec === "string") return specMatchesTauCodex(spec);
87
+ if (!spec || typeof spec !== "object") return false;
88
+ return ["source", "package", "name", "spec"].some((key) => specMatchesTauCodex(spec[key]));
218
89
  }
219
90
 
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);
91
+ function specMatchesTauCodex(spec) {
92
+ return typeof spec === "string" && (
93
+ spec === tauCodexPackage ||
94
+ spec === tauCodexInstallTarget ||
95
+ spec.startsWith(`${tauCodexPackage}@`) ||
96
+ spec.startsWith(`${tauCodexInstallTarget}@`) ||
97
+ spec === tauCodexGitTarget ||
98
+ spec.includes("github.com/Averyyy/pi-tau-codex")
99
+ );
272
100
  }
273
101
 
274
102
  function parseArgs(args) {
275
- let port = process.env.PI_WEB_PORT ?? defaultPort;
103
+ let port = process.env.TAU_MIRROR_PORT ?? defaultPort;
104
+ const clientArgs = [];
105
+
276
106
  for (let i = 0; i < args.length; i += 1) {
277
107
  const arg = args[i];
278
- if (arg === "--help" || arg === "-h") return { help: true, port };
108
+ if (arg === "--help" || arg === "-h") return { help: true, port, clientArgs };
109
+ if (arg === "--") {
110
+ clientArgs.push(...args.slice(i + 1));
111
+ break;
112
+ }
279
113
  if (arg === "--port" || arg === "-p") {
280
114
  port = requireValue(args, (i += 1), arg);
281
115
  continue;
@@ -286,8 +120,9 @@ function parseArgs(args) {
286
120
  }
287
121
  throw new Error(`Unknown pi-client web argument: ${arg}`);
288
122
  }
123
+
289
124
  validatePort(port);
290
- return { help: false, port };
125
+ return { help: false, port, clientArgs };
291
126
  }
292
127
 
293
128
  function requireValue(args, index, name) {
@@ -304,17 +139,30 @@ function validatePort(port) {
304
139
  }
305
140
 
306
141
  function printHelp() {
307
- console.log(`Usage: pi-client web [--port <port>]
142
+ console.log(`Usage: pi-client web [--port <port>] [-- <pi-client args...>]
308
143
 
309
- Start the pi-client web UI.
144
+ Start pi-client in Tau mirror mode.
310
145
 
311
146
  Options:
312
- -p, --port <port> Port to listen on (default: ${defaultPort})
147
+ -p, --port <port> Tau port to listen on (default: ${defaultPort})
313
148
  -h, --help Show this help
314
149
 
315
150
  Environment:
316
- PI_SERVER_URL pi-server URL used by browser sessions
151
+ PI_SERVER_URL pi-server URL used by pi-client sessions
317
152
  PI_SERVER_AUTH_TOKEN pi-server auth token
318
- PI_CLIENT_WEB_CONFIG pi-client web settings file
153
+ TAU_HOST Tau bind host (default: 127.0.0.1)
154
+ TAU_MIRROR_PORT Tau port (default: ${defaultPort})
155
+
156
+ Tau must be installed in the shared Pi agent settings:
157
+ pi-client install npm:@averyyy/pi-tau-codex
158
+ # or: pi install npm:@averyyy/pi-tau-codex
159
+ # dev fallback: pi-client install git:github.com/Averyyy/pi-tau-codex
319
160
  `);
320
161
  }
162
+
163
+ function printInstallRequired() {
164
+ console.error(`请安装 ${tauCodexPackage}:`);
165
+ console.error(" pi-client install npm:@averyyy/pi-tau-codex");
166
+ console.error(" # or: pi install npm:@averyyy/pi-tau-codex");
167
+ console.error(" # dev fallback: pi-client install git:github.com/Averyyy/pi-tau-codex");
168
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@averyyy/pi-client",
3
- "version": "0.80.3",
3
+ "version": "0.80.6-piclient.5",
4
4
  "description": "Lightweight CLI wrapper that connects to a pi-server instance",
5
5
  "type": "module",
6
6
  "piClient": {
7
- "basePiVersion": "0.80.3",
8
- "basePiCommit": "85b7c247"
7
+ "basePiVersion": "0.80.6",
8
+ "basePiCommit": "2b3fda99"
9
9
  },
10
10
  "bin": {
11
11
  "pi-client": "bin/pi-client.js"
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "bin",
18
+ "README.md",
18
19
  "CHANGELOG.md"
19
20
  ],
20
21
  "scripts": {
@@ -24,8 +25,7 @@
24
25
  "prepublishOnly": "npm run build"
25
26
  },
26
27
  "dependencies": {
27
- "@earendil-works/pi-coding-agent": "0.80.3",
28
- "@jmfederico/pi-web": "1.202606.7"
28
+ "@earendil-works/pi-coding-agent": "npm:@averyyy/pi-coding-agent@0.80.6-piclient.5"
29
29
  },
30
30
  "devDependencies": {
31
31
  "shx": "0.4.0",
@@ -40,7 +40,7 @@
40
40
  "license": "MIT",
41
41
  "repository": {
42
42
  "type": "git",
43
- "url": "git+https://github.com/earendil-works/pi.git",
43
+ "url": "https://github.com/Averyyy/pi-client",
44
44
  "directory": "packages/pi-client"
45
45
  },
46
46
  "engines": {
@@ -1,13 +0,0 @@
1
- {
2
- "name": "pi-client-pi-web-plugin",
3
- "version": "0.0.0",
4
- "type": "module",
5
- "piWeb": {
6
- "plugins": [
7
- {
8
- "id": "pi-client",
9
- "module": "pi-web-plugin.js"
10
- }
11
- ]
12
- }
13
- }
@@ -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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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;