@preapexis/piexis 0.2.4 → 0.2.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.
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { randomBytes } from "node:crypto";
3
+ import { spawn } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import path from "node:path";
6
+
7
+ const args = process.argv.slice(2);
8
+ const value = (flag) => { const index = args.indexOf(flag); return index < 0 ? undefined : args[index + 1]; };
9
+ if (args.includes("--help") || args.includes("-h")) {
10
+ console.log("Usage: piexis-web [--workspace <directory>] [--port <port>] [--token <token>]");
11
+ process.exit(0);
12
+ }
13
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ const workspace = path.resolve(value("--workspace") || process.cwd());
15
+ const port = value("--port") || process.env.PIXIS_PORT || "4317";
16
+ const token = value("--token") || process.env.PIXIS_TOKEN || randomBytes(32).toString("base64url");
17
+ const server = path.join(root, "apps", "server", "src", "index.ts");
18
+ const child = spawn(process.execPath, ["--import", "tsx", server], {
19
+ cwd: workspace,
20
+ env: { ...process.env, PIXIS_WORKSPACE: workspace, PIXIS_PORT: port, PIXIS_TOKEN: token, PIXIS_ORIGIN: `http://127.0.0.1:${port}` },
21
+ stdio: "inherit",
22
+ windowsHide: true
23
+ });
24
+ child.on("exit", (code) => process.exitCode = code ?? 1);
@@ -0,0 +1,45 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { spawn, type ChildProcess } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createServer } from "node:net";
5
+ import path from "node:path";
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+
8
+ async function availablePort(): Promise<number> {
9
+ const server = createServer();
10
+ await new Promise<void>((resolve, reject) => {
11
+ server.once("error", reject);
12
+ server.listen(0, "127.0.0.1", () => resolve());
13
+ });
14
+ const address = server.address();
15
+ await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
16
+ if (!address || typeof address === "string") throw new Error("Could not select a local PIXIS WebUI port.");
17
+ return address.port;
18
+ }
19
+
20
+ /** Starts the separate local WebUI; Pi's TUI remains usable in this process. */
21
+ export default function webui(pi: ExtensionAPI) {
22
+ const servers = new Map<string, ChildProcess>();
23
+ const launcher = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../bin/piexis-web.mjs");
24
+
25
+ pi.registerCommand("webui", {
26
+ description: "Start the local PIXIS WebUI for this workspace",
27
+ handler: async (_args, ctx) => {
28
+ const running = servers.get(ctx.cwd);
29
+ if (running && running.exitCode === null && !running.killed) {
30
+ ctx.ui.notify("PIXIS WebUI is already running for this workspace.", "info");
31
+ return;
32
+ }
33
+ const token = randomBytes(32).toString("base64url");
34
+ const port = await availablePort();
35
+ const child = spawn(process.execPath, [launcher, "--workspace", ctx.cwd, "--port", `${port}`, "--token", token], {
36
+ detached: true,
37
+ stdio: "ignore",
38
+ windowsHide: true
39
+ });
40
+ child.unref();
41
+ servers.set(ctx.cwd, child);
42
+ ctx.ui.notify(`PIXIS WebUI started: http://127.0.0.1:${port}/#token=${token}`, "info");
43
+ }
44
+ });
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preapexis/piexis",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "Extensions, skills, prompts, and themes for pi",
6
6
  "repository": {
@@ -11,6 +11,21 @@
11
11
  "pi-package"
12
12
  ],
13
13
  "license": "MIT",
14
+ "bin": {
15
+ "piexis-web": "./bin/piexis-web.mjs"
16
+ },
17
+ "files": [
18
+ "apps/server/",
19
+ "apps/web/dist/",
20
+ "bin/",
21
+ "extensions/",
22
+ "lib/",
23
+ "packages/",
24
+ "prompts/",
25
+ "schemas/",
26
+ "skills/",
27
+ "themes/"
28
+ ],
14
29
  "engines": {
15
30
  "node": ">=22.19.0"
16
31
  },
@@ -21,7 +36,13 @@
21
36
  },
22
37
  "scripts": {
23
38
  "test": "node --test tests/*.test.mjs",
39
+ "test:webui": "node --import tsx --test tests/webui/*.test.ts",
24
40
  "check": "tsc --noEmit",
41
+ "check:server": "tsc -p apps/server/tsconfig.json --noEmit",
42
+ "check:web": "tsc -p apps/web/tsconfig.json --noEmit",
43
+ "build": "vite build --config apps/web/vite.config.ts",
44
+ "prepack": "npm run build",
45
+ "dev": "node scripts/dev.mjs",
25
46
  "release": "node scripts/release.mjs"
26
47
  },
27
48
  "pi": {
@@ -29,7 +50,8 @@
29
50
  "./extensions/ask-question.ts",
30
51
  "./extensions/ui.ts",
31
52
  "./extensions/recommended-tools.ts",
32
- "./extensions/agents.ts"
53
+ "./extensions/agents.ts",
54
+ "./extensions/webui.ts"
33
55
  ],
34
56
  "skills": [
35
57
  "./skills"
@@ -43,6 +65,19 @@
43
65
  },
44
66
  "devDependencies": {
45
67
  "@types/node": "24.10.0",
46
- "typescript": "5.9.3"
68
+ "@types/react": "^19.3.0",
69
+ "@types/react-dom": "^19.3.0",
70
+ "@vitejs/plugin-react": "^6.1.2",
71
+ "typescript": "5.9.3",
72
+ "vite": "^8.3.4"
73
+ },
74
+ "dependencies": {
75
+ "@fastify/static": "^10.1.6",
76
+ "@fastify/websocket": "^11.3.4",
77
+ "fastify": "^5.12.5",
78
+ "react": "^19.3.0",
79
+ "react-dom": "^19.3.0",
80
+ "tsx": "^4.23.15",
81
+ "zod": "^4.6.5"
47
82
  }
48
83
  }
@@ -0,0 +1,99 @@
1
+ import { ChildProcess, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { EventEmitter } from "node:events";
4
+
5
+ export interface RpcResponse {
6
+ type: "response";
7
+ id?: string;
8
+ command: string;
9
+ success: boolean;
10
+ data?: unknown;
11
+ error?: string;
12
+ }
13
+
14
+ export type RpcRecord = Record<string, unknown>;
15
+
16
+ /** Long-lived, documented Pi RPC client. It treats stdout as strict JSONL. */
17
+ export class PiRpcClient extends EventEmitter {
18
+ #child?: ChildProcess;
19
+ #buffer = "";
20
+ #pending = new Map<string, { resolve: (value: RpcResponse) => void; reject: (reason: Error) => void }>();
21
+
22
+ constructor(
23
+ private readonly cwd: string,
24
+ private readonly executable = process.env.PIXIS_PI_BIN || (process.platform === "win32" ? "pi.cmd" : "pi"),
25
+ private readonly executableArgs: string[] = []
26
+ ) {
27
+ super();
28
+ }
29
+
30
+ start(): void {
31
+ if (this.#child) return;
32
+ const child = spawn(this.executable, [...this.executableArgs, "--mode", "rpc"], { cwd: this.cwd, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
33
+ this.#child = child;
34
+ child.stdout?.on("data", (chunk: Buffer) => this.#consume(chunk.toString("utf8")));
35
+ child.stderr?.on("data", (chunk: Buffer) => this.emit("diagnostic", chunk.toString("utf8")));
36
+ child.on("error", (error) => this.#fail(error));
37
+ child.on("exit", (code, signal) => this.#fail(new Error(`Pi exited (${code ?? signal ?? "unknown"})`)));
38
+ }
39
+
40
+ async command(command: Record<string, unknown>): Promise<RpcResponse> {
41
+ this.start();
42
+ const id = randomUUID();
43
+ const record = { ...command, id };
44
+ return new Promise((resolve, reject) => {
45
+ this.#pending.set(id, { resolve, reject });
46
+ const input = this.#child?.stdin;
47
+ if (!input || input.destroyed) return reject(new Error("Pi RPC stdin is unavailable"));
48
+ input.write(`${JSON.stringify(record)}\n`, "utf8", (error) => {
49
+ if (error) {
50
+ this.#pending.delete(id);
51
+ reject(error);
52
+ }
53
+ });
54
+ });
55
+ }
56
+
57
+ send(record: Record<string, unknown>): void {
58
+ const input = this.#child?.stdin;
59
+ if (!input || input.destroyed) throw new Error("Pi RPC stdin is unavailable");
60
+ input.write(`${JSON.stringify(record)}\n`, "utf8");
61
+ }
62
+
63
+ dispose(): void {
64
+ this.#child?.stdin?.end();
65
+ this.#child = undefined;
66
+ }
67
+
68
+ #consume(chunk: string): void {
69
+ this.#buffer += chunk;
70
+ let newline: number;
71
+ while ((newline = this.#buffer.indexOf("\n")) >= 0) {
72
+ const line = this.#buffer.slice(0, newline).replace(/\r$/, "");
73
+ this.#buffer = this.#buffer.slice(newline + 1);
74
+ if (!line) continue;
75
+ try {
76
+ const record = JSON.parse(line) as RpcRecord;
77
+ if (record.type === "response" && typeof record.id === "string" && typeof record.command === "string" && typeof record.success === "boolean") {
78
+ const pending = this.#pending.get(record.id);
79
+ if (pending) {
80
+ this.#pending.delete(record.id);
81
+ pending.resolve(record as unknown as RpcResponse);
82
+ continue;
83
+ }
84
+ }
85
+ this.emit("record", record);
86
+ } catch {
87
+ this.emit("diagnostic", `Ignoring malformed Pi RPC record: ${line.slice(0, 200)}`);
88
+ }
89
+ }
90
+ }
91
+
92
+ #fail(error: Error): void {
93
+ if (!this.#child && this.#pending.size === 0) return;
94
+ this.#child = undefined;
95
+ for (const { reject } of this.#pending.values()) reject(error);
96
+ this.#pending.clear();
97
+ this.emit("exit", error);
98
+ }
99
+ }
@@ -0,0 +1,27 @@
1
+ export type SessionStatus = "starting" | "ready" | "running" | "waiting" | "cancelling" | "failed" | "stopped";
2
+
3
+ export interface SessionSummary {
4
+ id: string;
5
+ name: string;
6
+ status: SessionStatus;
7
+ model?: string;
8
+ sessionFile?: string;
9
+ }
10
+
11
+ export type ClientCommand =
12
+ | { type: "list_sessions" }
13
+ | { type: "subscribe"; sessionId: string }
14
+ | { type: "create_session" }
15
+ | { type: "prompt"; sessionId: string; message: string }
16
+ | { type: "abort"; sessionId: string }
17
+ | { type: "extension_ui_response"; sessionId: string; id: string; value?: string; confirmed?: boolean; cancelled?: boolean }
18
+ | { type: "git_status"; sessionId: string }
19
+ | { type: "git_diff"; sessionId: string; path: string };
20
+
21
+ export type ServerMessage =
22
+ | { type: "snapshot"; sessions: SessionSummary[]; sessionId: string; state: unknown; messages: unknown[] }
23
+ | { type: "session"; session: SessionSummary }
24
+ | { type: "pi_event"; sessionId: string; event: unknown }
25
+ | { type: "extension_ui"; sessionId: string; request: unknown }
26
+ | { type: "workspace"; sessionId: string; action: "git_status" | "git_diff"; data: unknown }
27
+ | { type: "error"; message: string; sessionId?: string };
@@ -0,0 +1,81 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "midnight",
4
+ "vars": {
5
+ "cyan": "#67d9ef",
6
+ "violet": "#bb9af7",
7
+ "blue": "#7aa2f7",
8
+ "ink": "#141821",
9
+ "panel": "#1c2230",
10
+ "panelRaised": "#2c3548",
11
+ "text": "#e2e8f0",
12
+ "muted": "#a9b7cc",
13
+ "dim": "#7e8da6",
14
+ "mint": "#88dfb3",
15
+ "rose": "#ff859d",
16
+ "gold": "#f5ce7b"
17
+ },
18
+ "colors": {
19
+ "accent": "cyan",
20
+ "border": "blue",
21
+ "borderAccent": "cyan",
22
+ "borderMuted": "dim",
23
+ "success": "mint",
24
+ "error": "rose",
25
+ "warning": "gold",
26
+ "muted": "muted",
27
+ "dim": "dim",
28
+ "text": "text",
29
+ "thinkingText": "muted",
30
+ "selectedBg": "panelRaised",
31
+ "scrollbarTrack": "panel",
32
+ "scrollbarThumb": "blue",
33
+ "searchMatchBg": "#3c3655",
34
+ "searchMatchText": "text",
35
+ "userMessageBg": "panel",
36
+ "userMessageText": "text",
37
+ "customMessageBg": "#232337",
38
+ "customMessageText": "text",
39
+ "customMessageLabel": "violet",
40
+ "toolPendingBg": "#232b3c",
41
+ "toolSuccessBg": "#1c302b",
42
+ "toolErrorBg": "#37242f",
43
+ "toolTitle": "cyan",
44
+ "toolOutput": "muted",
45
+ "mdHeading": "violet",
46
+ "mdLink": "cyan",
47
+ "mdLinkUrl": "muted",
48
+ "mdCode": "mint",
49
+ "mdCodeBlock": "text",
50
+ "mdCodeBlockBorder": "blue",
51
+ "mdQuote": "muted",
52
+ "mdQuoteBorder": "violet",
53
+ "mdHr": "dim",
54
+ "mdListBullet": "cyan",
55
+ "toolDiffAdded": "mint",
56
+ "toolDiffRemoved": "rose",
57
+ "toolDiffContext": "muted",
58
+ "syntaxComment": "dim",
59
+ "syntaxKeyword": "violet",
60
+ "syntaxFunction": "blue",
61
+ "syntaxVariable": "text",
62
+ "syntaxString": "mint",
63
+ "syntaxNumber": "gold",
64
+ "syntaxType": "cyan",
65
+ "syntaxOperator": "cyan",
66
+ "syntaxPunctuation": "muted",
67
+ "thinkingOff": "dim",
68
+ "thinkingMinimal": "muted",
69
+ "thinkingLow": "blue",
70
+ "thinkingMedium": "cyan",
71
+ "thinkingHigh": "violet",
72
+ "thinkingXhigh": "rose",
73
+ "thinkingMax": "gold",
74
+ "bashMode": "gold"
75
+ },
76
+ "export": {
77
+ "pageBg": "ink",
78
+ "cardBg": "panel",
79
+ "infoBg": "panelRaised"
80
+ }
81
+ }
@@ -1,35 +0,0 @@
1
- name: Publish npm package
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
-
8
- jobs:
9
- publish:
10
- runs-on: ubuntu-latest
11
-
12
- permissions:
13
- contents: read
14
- id-token: write
15
-
16
- steps:
17
- - uses: actions/checkout@v6
18
-
19
- - uses: actions/setup-node@v6
20
- with:
21
- node-version: 24
22
- registry-url: https://registry.npmjs.org
23
- package-manager-cache: false
24
-
25
- - run: npm ci --ignore-scripts
26
- - run: npm run check
27
- - run: npm test
28
-
29
- - name: Verify version tag
30
- run: |
31
- VERSION=$(node -p "require('./package.json').version")
32
- TAG=${GITHUB_REF_NAME#v}
33
- test "$VERSION" = "$TAG"
34
-
35
- - run: npm publish --access public
package/AGENTS.md DELETED
@@ -1,38 +0,0 @@
1
- # Agents Guide
2
-
3
- ## Project and layout
4
-
5
- Piexis (`@preapexis/piexis`) is a TypeScript Pi package providing interactive questions, UI extensions, skills, prompts, and themes. Node.js 22.19+ and a current Pi release are required.
6
-
7
- - `extensions/ask-question.ts`: sequential interactive `AskQuestion` tool; requires the TUI.
8
- - `extensions/agents.ts`: five agent profiles and sequential subprocess subagents; `lib/agents.ts` owns configuration/routing, and `lib/subagent-runner.ts` owns protocol/cancellation. User/project `agent.json` files configure models and effort, with legacy `agents.json` fallback. Accepted project setup creates a missing `.pi/agent.json` scaffold without overwriting existing configuration.
9
- - `extensions/ui.ts`: project header, activity footer, Git branch, model, and context usage.
10
- - `extensions/recommended-tools.ts`: automatic opt-in project setup for Pi's native `grep`, `find`, and `ls` tools.
11
- - `skills/`: task-specific `SKILL.md` guidance.
12
- - `prompts/`: bundled `/init`, `/commit`, and `/summary` prompts.
13
- - `themes/orange.json`: bundled theme.
14
- - `package.json`: resource manifest and npm scripts; `tsconfig.json`: strict TypeScript configuration.
15
-
16
- ## Development
17
-
18
- | Command | Purpose |
19
- | --- | --- |
20
- | `npm ci --ignore-scripts` | Install locked dependencies without lifecycle scripts. |
21
- | `npm run check` | Type-check with `tsc --noEmit`. |
22
- | `npm test` | Run `node --test tests/*.test.mjs`. |
23
- | `pi -e ./extensions/ask-question.ts` | Preview only the question extension. |
24
-
25
- There are no separate build, lint, or format scripts, and no CI configuration in this checkout.
26
-
27
- ## Conventions and safety
28
-
29
- - Use ESM and strict TypeScript with NodeNext module resolution and ES2022 targeting. Local imports use explicit `.ts` extensions.
30
- - Match nearby source style: two-space indentation, double-quoted strings, and semicolons. Extensions export a default registration function receiving `ExtensionAPI`.
31
- - Preserve interactive cancellation and non-TUI handling. Add or update `node:test`/`node:assert/strict` tests for behavior changes, using disposable fixtures rather than credentials.
32
- - Preserve unrelated working-tree changes and intentional deletions. Do not commit generated output, secrets, dependency directories, or local configuration. `.pi/` and `.kilo/` are ignored local state, not package resources.
33
- - Permission modes, approval/classifier logic, plan execution handoffs, and background workers have been removed. Piexis does not enforce tool-access restrictions or provide sandboxing; respect host restrictions and other installed extensions.
34
- - Do not delete existing user settings, session records, or retained task worktrees as part of routine package changes.
35
-
36
- ## Validation
37
-
38
- Run `npm run check` and relevant tests for implementation changes. Report unexecuted checks explicitly; skipped or empty runs do not establish coverage. Review the diff for credentials and unrelated changes before finishing.
@@ -1,32 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { readFileSync } from "node:fs";
3
-
4
- const bump = process.argv[2] ?? "patch";
5
- if (!["patch", "minor", "major"].includes(bump)) {
6
- console.error("Usage: npm run release -- [patch|minor|major]");
7
- process.exit(1);
8
- }
9
-
10
- function run(command, args) {
11
- const result = spawnSync(command, args, { stdio: "inherit", shell: false });
12
- if (result.error) throw result.error;
13
- if (result.status !== 0) process.exit(result.status ?? 1);
14
- }
15
-
16
- const status = spawnSync("git", ["status", "--porcelain"], { encoding: "utf8" });
17
- if (status.status !== 0) process.exit(status.status ?? 1);
18
- if (status.stdout.trim()) {
19
- console.error("Working tree is not clean. Commit or stash changes first.");
20
- process.exit(1);
21
- }
22
-
23
- console.log("Running checks...");
24
- run("npm", ["run", "check"]);
25
- run("npm", ["test"]);
26
-
27
- console.log(`Bumping ${bump} version and creating a Git tag...`);
28
- run("npm", ["version", bump]);
29
-
30
- const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
31
- console.log(`Pushing v${packageJson.version}; GitHub Actions will publish it.`);
32
- run("git", ["push", "origin", "HEAD", "--follow-tags"]);