@rotriz/pi-web-ui 1.2.0 → 1.4.0
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/extension.mjs +47 -136
- package/package.json +1 -1
- package/server.mjs +66 -18
package/extension.mjs
CHANGED
|
@@ -1,153 +1,64 @@
|
|
|
1
1
|
// pi-web-ui extension entry point for the Pi coding agent.
|
|
2
|
-
//
|
|
2
|
+
// Runs the HTTP/SSE gateway in-process (no subprocess needed).
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { exec, execFile } from "node:child_process";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
|
-
import { request } from "node:http";
|
|
4
|
+
import { exec } from "node:child_process";
|
|
10
5
|
|
|
11
6
|
const PORT = Number(process.env.PI_WEB_PORT || process.env.PORT || 3123);
|
|
12
7
|
|
|
13
|
-
// HTTP GET helper — works on Node 14+ without global fetch
|
|
14
|
-
function httpGet(url) {
|
|
15
|
-
return new Promise((resolve) => {
|
|
16
|
-
const req = request(url, (res) => {
|
|
17
|
-
let data = "";
|
|
18
|
-
res.on("data", (chunk) => (data += chunk));
|
|
19
|
-
res.on("end", () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data }));
|
|
20
|
-
});
|
|
21
|
-
req.on("error", () => resolve({ ok: false, status: 0, data: "" }));
|
|
22
|
-
req.setTimeout(3000, () => { req.destroy(); resolve({ ok: false, status: 0, data: "" }); });
|
|
23
|
-
req.end();
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
8
|
function openBrowser(url) {
|
|
28
9
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
29
10
|
exec(`${cmd} ${JSON.stringify(url)}`, () => {});
|
|
30
11
|
}
|
|
31
12
|
|
|
32
|
-
// Resolve the pi-coding-agent module path from within Pi's process context
|
|
33
|
-
function findPiModulePath() {
|
|
34
|
-
try {
|
|
35
|
-
// When running inside Pi, the module is already loaded — find it via require.resolve or import.meta
|
|
36
|
-
const candidates = [];
|
|
37
|
-
// Check if pi's entry script reveals the path
|
|
38
|
-
const entry = process.argv[1] || "";
|
|
39
|
-
if (entry) {
|
|
40
|
-
const dir = dirname(entry);
|
|
41
|
-
candidates.push(join(dir, "..", "dist", "index.js"));
|
|
42
|
-
candidates.push(join(dir, "index.js"));
|
|
43
|
-
// Go up to find the package
|
|
44
|
-
let d = dir;
|
|
45
|
-
for (let i = 0; i < 5; i++) {
|
|
46
|
-
const candidate = join(d, "dist", "index.js");
|
|
47
|
-
if (existsSync(candidate)) candidates.push(candidate);
|
|
48
|
-
const pkgCandidate = join(d, "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js");
|
|
49
|
-
if (existsSync(pkgCandidate)) candidates.push(pkgCandidate);
|
|
50
|
-
d = dirname(d);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// Check common global locations
|
|
54
|
-
const globalDirs = [
|
|
55
|
-
join(homedir(), ".local", "share", "fnm"),
|
|
56
|
-
"/usr/local/lib/node_modules",
|
|
57
|
-
"/usr/lib/node_modules",
|
|
58
|
-
join(homedir(), ".nvm", "versions", "node"),
|
|
59
|
-
join(homedir(), ".volta", "tools", "image", "packages"),
|
|
60
|
-
];
|
|
61
|
-
for (const base of globalDirs) {
|
|
62
|
-
if (!existsSync(base)) continue;
|
|
63
|
-
// Recursively look for the package
|
|
64
|
-
const piPkg = "@earendil-works/pi-coding-agent/dist/index.js";
|
|
65
|
-
// Check direct path
|
|
66
|
-
const direct = join(base, piPkg);
|
|
67
|
-
if (existsSync(direct)) { candidates.push(direct); continue; }
|
|
68
|
-
// For version managers, search one level deep
|
|
69
|
-
}
|
|
70
|
-
// Also check NODE_PATH
|
|
71
|
-
if (process.env.NODE_PATH) {
|
|
72
|
-
for (const p of process.env.NODE_PATH.split(":")) {
|
|
73
|
-
const candidate = join(p, "@earendil-works", "pi-coding-agent", "dist", "index.js");
|
|
74
|
-
if (existsSync(candidate)) candidates.push(candidate);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
for (const c of candidates) {
|
|
78
|
-
if (existsSync(c)) return c;
|
|
79
|
-
}
|
|
80
|
-
} catch {}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
13
|
export default function (pi) {
|
|
85
|
-
let
|
|
86
|
-
let
|
|
87
|
-
|
|
88
|
-
const extDir = (() => {
|
|
89
|
-
try {
|
|
90
|
-
return dirname(fileURLToPath(import.meta.url));
|
|
91
|
-
} catch {
|
|
92
|
-
return join(homedir(), ".pi", "agent", "extensions", "pi-web-ui");
|
|
93
|
-
}
|
|
94
|
-
})();
|
|
95
|
-
|
|
96
|
-
function findNode() {
|
|
97
|
-
return process.execPath;
|
|
98
|
-
}
|
|
14
|
+
let serverStarted = false;
|
|
15
|
+
let startPromise = null;
|
|
16
|
+
const origin = `http://localhost:${PORT}`;
|
|
99
17
|
|
|
100
18
|
async function ensureServer() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (res.ok) return;
|
|
105
|
-
} catch {}
|
|
106
|
-
|
|
107
|
-
if (serverProcess && !serverProcess.killed) return;
|
|
108
|
-
const serverPath = join(extDir, "server.mjs");
|
|
109
|
-
if (!existsSync(serverPath)) {
|
|
110
|
-
throw new Error(`pi-web-ui server not found at ${serverPath}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Resolve the pi module path and pass it to the server
|
|
114
|
-
const piModulePath = findPiModulePath();
|
|
115
|
-
const env = { ...process.env, PORT: String(PORT) };
|
|
116
|
-
if (piModulePath) env.PI_WEB_PI_MODULE = piModulePath;
|
|
117
|
-
|
|
118
|
-
// Also pass NODE_PATH so the subprocess can find global modules
|
|
119
|
-
const nodePath = [];
|
|
120
|
-
if (process.env.NODE_PATH) nodePath.push(process.env.NODE_PATH);
|
|
121
|
-
// Add the global node_modules from the current node binary
|
|
122
|
-
const nodeDir = dirname(dirname(process.execPath));
|
|
123
|
-
const globalNM = join(nodeDir, "lib", "node_modules");
|
|
124
|
-
if (existsSync(globalNM)) nodePath.push(globalNM);
|
|
125
|
-
if (nodePath.length) env.NODE_PATH = nodePath.join(":");
|
|
126
|
-
|
|
127
|
-
serverProcess = execFile(findNode(), [serverPath], {
|
|
128
|
-
cwd: process.cwd(),
|
|
129
|
-
env,
|
|
130
|
-
stdio: "ignore",
|
|
131
|
-
});
|
|
132
|
-
serverProcess.unref();
|
|
133
|
-
serverProcess.on("exit", () => { serverProcess = null; });
|
|
134
|
-
|
|
135
|
-
// Wait for the server to be ready (up to 30 seconds)
|
|
136
|
-
for (let i = 0; i < 60; i++) {
|
|
137
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
19
|
+
if (serverStarted) return;
|
|
20
|
+
if (startPromise) return startPromise;
|
|
21
|
+
startPromise = (async () => {
|
|
138
22
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
23
|
+
// Make @earendil-works/pi-coding-agent resolvable by the server module.
|
|
24
|
+
// Since we're in Pi's process, the module is already loaded — expose it globally.
|
|
25
|
+
try {
|
|
26
|
+
const piMod = await import("@earendil-works/pi-coding-agent");
|
|
27
|
+
globalThis.__PI_MODULE__ = piMod;
|
|
28
|
+
} catch {
|
|
29
|
+
// Fallback: resolve relative to Pi's entry script
|
|
30
|
+
const entry = process.argv[1] || "";
|
|
31
|
+
if (entry) {
|
|
32
|
+
const { dirname, join } = await import("node:path");
|
|
33
|
+
const dir = dirname(entry);
|
|
34
|
+
for (const c of [join(dir, "..", "dist", "index.js"), join(dir, "index.js")]) {
|
|
35
|
+
try { globalThis.__PI_MODULE__ = await import(c); break; } catch {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const { startServer } = await import("./server.mjs");
|
|
40
|
+
await startServer(PORT);
|
|
41
|
+
serverStarted = true;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err?.code === "EADDRINUSE") {
|
|
44
|
+
serverStarted = true;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
} finally {
|
|
49
|
+
startPromise = null;
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
return startPromise;
|
|
144
53
|
}
|
|
145
54
|
|
|
146
|
-
async function
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
55
|
+
async function stopServerFn() {
|
|
56
|
+
if (!serverStarted) return;
|
|
57
|
+
try {
|
|
58
|
+
const { stopServer } = await import("./server.mjs");
|
|
59
|
+
await stopServer();
|
|
60
|
+
serverStarted = false;
|
|
61
|
+
} catch {}
|
|
151
62
|
}
|
|
152
63
|
|
|
153
64
|
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
@@ -159,7 +70,7 @@ export default function (pi) {
|
|
|
159
70
|
|
|
160
71
|
pi.on("session_shutdown", async (event) => {
|
|
161
72
|
if (event.reason === "reload" || event.reason === "quit") {
|
|
162
|
-
await
|
|
73
|
+
await stopServerFn();
|
|
163
74
|
}
|
|
164
75
|
});
|
|
165
76
|
|
|
@@ -173,7 +84,7 @@ export default function (pi) {
|
|
|
173
84
|
handler: async (args, ctx) => {
|
|
174
85
|
const arg = String(args ?? "").trim().toLowerCase();
|
|
175
86
|
if (arg === "stop") {
|
|
176
|
-
await
|
|
87
|
+
await stopServerFn();
|
|
177
88
|
ctx.ui?.notify?.("pi-web-ui stopped", "info");
|
|
178
89
|
return;
|
|
179
90
|
}
|
package/package.json
CHANGED
package/server.mjs
CHANGED
|
@@ -19,8 +19,17 @@ const PORT = process.env.PORT || 3123;
|
|
|
19
19
|
const CWD = process.cwd();
|
|
20
20
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// ─── Pi module resolution ─────────────────────────────────────────────────
|
|
23
|
+
let _piModule = null;
|
|
24
|
+
|
|
25
|
+
export function initPiModule(mod) {
|
|
26
|
+
_piModule = mod;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
async function resolvePiModule() {
|
|
30
|
+
if (_piModule) return _piModule;
|
|
31
|
+
// Check if the extension pre-loaded the module via globalThis
|
|
32
|
+
if (globalThis.__PI_MODULE__) { _piModule = globalThis.__PI_MODULE__; return _piModule; }
|
|
24
33
|
const attempts = [];
|
|
25
34
|
if (process.env.PI_WEB_PI_MODULE) attempts.push(process.env.PI_WEB_PI_MODULE);
|
|
26
35
|
attempts.push("@earendil-works/pi-coding-agent");
|
|
@@ -30,35 +39,51 @@ async function resolvePiModule() {
|
|
|
30
39
|
attempts.push(join(dir, "..", "dist", "index.js"));
|
|
31
40
|
attempts.push(join(dir, "index.js"));
|
|
32
41
|
}
|
|
33
|
-
// Try NODE_PATH entries
|
|
34
42
|
if (process.env.NODE_PATH) {
|
|
35
43
|
for (const p of process.env.NODE_PATH.split(":")) {
|
|
36
44
|
attempts.push(join(p, "@earendil-works", "pi-coding-agent", "dist", "index.js"));
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
|
-
// Common global install locations
|
|
40
47
|
const nodeDir = dirname(dirname(process.execPath));
|
|
41
48
|
attempts.push(join(nodeDir, "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"));
|
|
42
|
-
// npm global prefix
|
|
43
49
|
attempts.push(join(homedir(), ".local", "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"));
|
|
44
50
|
for (const attempt of attempts) {
|
|
45
|
-
try {
|
|
51
|
+
try { _piModule = await import(attempt); return _piModule; } catch {}
|
|
46
52
|
}
|
|
47
53
|
throw new Error("Could not resolve @earendil-works/pi-coding-agent. Install it globally or set PI_WEB_PI_MODULE env var.");
|
|
48
54
|
}
|
|
49
|
-
const pi = await resolvePiModule();
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
// Only auto-initialize when run directly (node server.mjs)
|
|
57
|
+
const _isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
|
58
|
+
if (_isDirectRun) {
|
|
59
|
+
await resolvePiModule();
|
|
60
|
+
} else if (globalThis.__PI_MODULE__) {
|
|
61
|
+
_piModule = globalThis.__PI_MODULE__;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let createAgentSessionRuntime, createAgentSessionFromServices, createAgentSessionServices,
|
|
65
|
+
DefaultResourceLoader, SessionManager, SettingsManager, getAgentDir;
|
|
66
|
+
|
|
67
|
+
if (_piModule) {
|
|
68
|
+
({ createAgentSessionRuntime, createAgentSessionFromServices, createAgentSessionServices,
|
|
69
|
+
DefaultResourceLoader, SessionManager, SettingsManager, getAgentDir } = _piModule);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let AGENT_DIR = null;
|
|
73
|
+
|
|
74
|
+
let server = null;
|
|
75
|
+
let _booted = false;
|
|
76
|
+
|
|
77
|
+
async function boot() {
|
|
78
|
+
if (_booted) return;
|
|
79
|
+
if (!_piModule) {
|
|
80
|
+
if (globalThis.__PI_MODULE__) _piModule = globalThis.__PI_MODULE__;
|
|
81
|
+
else await resolvePiModule();
|
|
82
|
+
({ createAgentSessionRuntime, createAgentSessionFromServices, createAgentSessionServices,
|
|
83
|
+
DefaultResourceLoader, SessionManager, SettingsManager, getAgentDir } = _piModule);
|
|
84
|
+
}
|
|
85
|
+
AGENT_DIR = getAgentDir();
|
|
60
86
|
|
|
61
|
-
const AGENT_DIR = getAgentDir();
|
|
62
87
|
|
|
63
88
|
const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
64
89
|
const services = await createAgentSessionServices({ cwd });
|
|
@@ -868,7 +893,7 @@ function readBody(req) {
|
|
|
868
893
|
});
|
|
869
894
|
}
|
|
870
895
|
|
|
871
|
-
|
|
896
|
+
server = createServer(async (req, res) => {
|
|
872
897
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
873
898
|
const path = url.pathname;
|
|
874
899
|
|
|
@@ -1403,4 +1428,27 @@ const server = createServer(async (req, res) => {
|
|
|
1403
1428
|
}
|
|
1404
1429
|
});
|
|
1405
1430
|
|
|
1406
|
-
|
|
1431
|
+
_booted = true;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// When run directly (node server.mjs), boot and listen immediately.
|
|
1435
|
+
const _isDirect = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
|
1436
|
+
if (_isDirect) {
|
|
1437
|
+
await boot();
|
|
1438
|
+
server.listen(PORT, () => console.log(`[pi-web-ui] listening on http://localhost:${PORT}`));
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
export { PORT };
|
|
1442
|
+
export async function startServer(port) {
|
|
1443
|
+
await boot();
|
|
1444
|
+
return new Promise((res, rej) => {
|
|
1445
|
+
server.listen(port || PORT, () => {
|
|
1446
|
+
console.log(`[pi-web-ui] listening on http://localhost:${port || PORT}`);
|
|
1447
|
+
res();
|
|
1448
|
+
});
|
|
1449
|
+
server.on("error", rej);
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
export async function stopServer() {
|
|
1453
|
+
if (server) return new Promise((res) => server.close(() => res()));
|
|
1454
|
+
}
|