@cephalization/phoenix-insight 0.4.0 → 1.0.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.
Files changed (56) hide show
  1. package/README.md +195 -1
  2. package/dist/agent/index.js +9 -4
  3. package/dist/cli.js +172 -0
  4. package/dist/commands/index.js +1 -0
  5. package/dist/commands/report-tool.js +239 -0
  6. package/dist/config/schema.js +2 -2
  7. package/dist/modes/local.js +7 -0
  8. package/dist/modes/sandbox.js +8 -0
  9. package/dist/prompts/index.js +1 -1
  10. package/dist/prompts/system.js +10 -3
  11. package/dist/server/session.js +357 -0
  12. package/dist/server/ui.js +232 -0
  13. package/dist/server/websocket.js +212 -0
  14. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  15. package/dist/ui/assets/code-block-F6WJLWQG-BTdTzfvl.js +154 -0
  16. package/dist/ui/assets/code-block-F6WJLWQG-BTdTzfvl.js.map +1 -0
  17. package/dist/ui/assets/index-CX8aDatf.css +1 -0
  18. package/dist/ui/assets/index-DjZuAW6Y.js +63 -0
  19. package/dist/ui/assets/index-DjZuAW6Y.js.map +1 -0
  20. package/dist/ui/assets/vendor-data-r1ZEkUds.js +40 -0
  21. package/dist/ui/assets/vendor-data-r1ZEkUds.js.map +1 -0
  22. package/dist/ui/assets/vendor-react-Cgg2GOmP.js +2 -0
  23. package/dist/ui/assets/vendor-react-Cgg2GOmP.js.map +1 -0
  24. package/dist/ui/assets/vendor-render-DoMl5bum.js +381 -0
  25. package/dist/ui/assets/vendor-render-DoMl5bum.js.map +1 -0
  26. package/dist/ui/assets/vendor-ui-Cg-YC4hK.js +46 -0
  27. package/dist/ui/assets/vendor-ui-Cg-YC4hK.js.map +1 -0
  28. package/dist/ui/index.html +18 -0
  29. package/dist/ui/vite.svg +1 -0
  30. package/package.json +11 -14
  31. package/src/agent/index.ts +0 -323
  32. package/src/cli.ts +0 -854
  33. package/src/commands/index.ts +0 -8
  34. package/src/commands/px-fetch-more-spans.ts +0 -174
  35. package/src/commands/px-fetch-more-trace.ts +0 -183
  36. package/src/config/index.ts +0 -225
  37. package/src/config/loader.ts +0 -173
  38. package/src/config/schema.ts +0 -66
  39. package/src/index.ts +0 -1
  40. package/src/modes/index.ts +0 -21
  41. package/src/modes/local.ts +0 -163
  42. package/src/modes/sandbox.ts +0 -144
  43. package/src/modes/types.ts +0 -31
  44. package/src/observability/index.ts +0 -90
  45. package/src/progress.ts +0 -239
  46. package/src/prompts/index.ts +0 -1
  47. package/src/prompts/system.ts +0 -31
  48. package/src/snapshot/client.ts +0 -129
  49. package/src/snapshot/context.ts +0 -587
  50. package/src/snapshot/datasets.ts +0 -132
  51. package/src/snapshot/experiments.ts +0 -246
  52. package/src/snapshot/index.ts +0 -403
  53. package/src/snapshot/projects.ts +0 -58
  54. package/src/snapshot/prompts.ts +0 -267
  55. package/src/snapshot/spans.ts +0 -163
  56. package/src/snapshot/utils.ts +0 -140
@@ -0,0 +1,232 @@
1
+ /**
2
+ * HTTP server for Phoenix Insight UI.
3
+ * Serves the static build of @cephalization/phoenix-insight-ui package.
4
+ *
5
+ * Binds to localhost only for security (no external network access).
6
+ * Handles SPA routing by serving index.html for non-asset routes.
7
+ */
8
+ import { createServer } from "node:http";
9
+ import { createReadStream, existsSync, statSync } from "node:fs";
10
+ import { extname, join, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ // ============================================================================
13
+ // MIME Type Mapping
14
+ // ============================================================================
15
+ const MIME_TYPES = {
16
+ ".html": "text/html; charset=utf-8",
17
+ ".css": "text/css; charset=utf-8",
18
+ ".js": "text/javascript; charset=utf-8",
19
+ ".mjs": "text/javascript; charset=utf-8",
20
+ ".json": "application/json; charset=utf-8",
21
+ ".svg": "image/svg+xml",
22
+ ".png": "image/png",
23
+ ".jpg": "image/jpeg",
24
+ ".jpeg": "image/jpeg",
25
+ ".gif": "image/gif",
26
+ ".ico": "image/x-icon",
27
+ ".webp": "image/webp",
28
+ ".woff": "font/woff",
29
+ ".woff2": "font/woff2",
30
+ ".ttf": "font/ttf",
31
+ ".eot": "application/vnd.ms-fontobject",
32
+ ".map": "application/json",
33
+ };
34
+ /**
35
+ * Get MIME type for a file extension
36
+ */
37
+ function getMimeType(filePath) {
38
+ const ext = extname(filePath).toLowerCase();
39
+ return MIME_TYPES[ext] ?? "application/octet-stream";
40
+ }
41
+ // ============================================================================
42
+ // Path Resolution
43
+ // ============================================================================
44
+ /**
45
+ * Resolve the UI package dist directory path.
46
+ *
47
+ * Resolution order:
48
+ * 1. Bundled dist/ui (copied during build, used for npm published package)
49
+ * 2. import.meta.resolve (finds package in node_modules, used for development)
50
+ * 3. Relative path fallback (for development before packages are linked)
51
+ */
52
+ export function resolveUIDistPath() {
53
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
54
+ // First check for bundled UI dist (copied during CLI build)
55
+ // This is the path when installed from npm
56
+ const bundledPath = resolve(__dirname, "../ui");
57
+ if (existsSync(bundledPath) && existsSync(join(bundledPath, "index.html"))) {
58
+ return bundledPath;
59
+ }
60
+ try {
61
+ // Resolve the UI package's package.json using import.meta.resolve
62
+ // import.meta.resolve returns a file URL (file:///...)
63
+ const packageJsonUrl = import.meta.resolve("@cephalization/phoenix-insight-ui/package.json");
64
+ const packageJsonPath = fileURLToPath(packageJsonUrl);
65
+ const packageDir = resolve(packageJsonPath, "..");
66
+ return join(packageDir, "dist");
67
+ }
68
+ catch {
69
+ // Fallback: try to resolve relative to this file (for development)
70
+ return resolve(__dirname, "../../../ui/dist");
71
+ }
72
+ }
73
+ // ============================================================================
74
+ // Static File Server
75
+ // ============================================================================
76
+ /**
77
+ * Determine if a path should be served as a static asset.
78
+ * Asset paths have file extensions (e.g., .js, .css, .png).
79
+ * Non-asset paths (routes) should get the SPA index.html.
80
+ */
81
+ function isAssetPath(urlPath) {
82
+ // Check for file extension in the last segment
83
+ const lastSegment = urlPath.split("/").pop() ?? "";
84
+ return lastSegment.includes(".");
85
+ }
86
+ /**
87
+ * Sanitize URL path to prevent directory traversal attacks.
88
+ * Returns null if the path is invalid.
89
+ */
90
+ function sanitizePath(urlPath, basePath) {
91
+ // Decode URI and normalize slashes
92
+ let decoded;
93
+ try {
94
+ decoded = decodeURIComponent(urlPath);
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ // Remove query string and hash
100
+ const withoutQuery = decoded.split("?")[0] ?? decoded;
101
+ decoded = withoutQuery.split("#")[0] ?? withoutQuery;
102
+ // Resolve the full path
103
+ const fullPath = resolve(basePath, "." + decoded);
104
+ // Ensure the resolved path is within the base directory
105
+ if (!fullPath.startsWith(basePath)) {
106
+ return null;
107
+ }
108
+ return fullPath;
109
+ }
110
+ /**
111
+ * Create an HTTP server that serves the Phoenix Insight UI.
112
+ *
113
+ * Features:
114
+ * - Serves static files from the UI package dist directory
115
+ * - SPA fallback: non-asset routes serve index.html
116
+ * - Binds to localhost only (127.0.0.1) for security
117
+ * - Proper MIME types for all common web assets
118
+ *
119
+ * @param options - Server configuration options
120
+ * @returns Promise resolving to UIServer instance
121
+ */
122
+ export function createUIServer(options = {}) {
123
+ const port = options.port ?? 6007;
124
+ const host = options.host ?? "127.0.0.1";
125
+ const distPath = options.distPath ?? resolveUIDistPath();
126
+ // Verify dist directory exists
127
+ if (!existsSync(distPath)) {
128
+ return Promise.reject(new Error(`UI dist directory not found at: ${distPath}\n` +
129
+ "Make sure to build the UI package first: pnpm --filter @cephalization/phoenix-insight-ui build"));
130
+ }
131
+ // Verify index.html exists
132
+ const indexPath = join(distPath, "index.html");
133
+ if (!existsSync(indexPath)) {
134
+ return Promise.reject(new Error(`UI index.html not found at: ${indexPath}\n` +
135
+ "Make sure to build the UI package first: pnpm --filter @cephalization/phoenix-insight-ui build"));
136
+ }
137
+ return new Promise((resolve, reject) => {
138
+ const httpServer = createServer((req, res) => {
139
+ const urlPath = req.url ?? "/";
140
+ // Sanitize the path to prevent directory traversal
141
+ let filePath = sanitizePath(urlPath, distPath);
142
+ if (!filePath) {
143
+ res.writeHead(400, { "Content-Type": "text/plain" });
144
+ res.end("Bad Request");
145
+ return;
146
+ }
147
+ // SPA fallback: if not an asset path, serve index.html
148
+ if (!isAssetPath(urlPath)) {
149
+ filePath = indexPath;
150
+ }
151
+ // Check if file exists
152
+ if (!existsSync(filePath)) {
153
+ // For missing assets, return 404
154
+ // For missing routes, serve index.html (SPA)
155
+ if (isAssetPath(urlPath)) {
156
+ res.writeHead(404, { "Content-Type": "text/plain" });
157
+ res.end("Not Found");
158
+ return;
159
+ }
160
+ filePath = indexPath;
161
+ }
162
+ // Get file stats
163
+ let stats;
164
+ try {
165
+ stats = statSync(filePath);
166
+ }
167
+ catch {
168
+ res.writeHead(500, { "Content-Type": "text/plain" });
169
+ res.end("Internal Server Error");
170
+ return;
171
+ }
172
+ // If it's a directory, serve index.html
173
+ if (stats.isDirectory()) {
174
+ filePath = indexPath;
175
+ try {
176
+ stats = statSync(filePath);
177
+ }
178
+ catch {
179
+ res.writeHead(500, { "Content-Type": "text/plain" });
180
+ res.end("Internal Server Error");
181
+ return;
182
+ }
183
+ }
184
+ // Set response headers
185
+ const mimeType = getMimeType(filePath);
186
+ res.writeHead(200, {
187
+ "Content-Type": mimeType,
188
+ "Content-Length": stats.size,
189
+ "Cache-Control": filePath === indexPath
190
+ ? "no-cache" // Don't cache index.html for SPA
191
+ : "public, max-age=31536000", // Cache assets for 1 year
192
+ });
193
+ // Stream the file
194
+ const stream = createReadStream(filePath);
195
+ stream.pipe(res);
196
+ stream.on("error", () => {
197
+ res.writeHead(500, { "Content-Type": "text/plain" });
198
+ res.end("Internal Server Error");
199
+ });
200
+ });
201
+ // Handle server errors
202
+ httpServer.on("error", (err) => {
203
+ reject(err);
204
+ });
205
+ // Start listening
206
+ httpServer.listen(port, host, () => {
207
+ // Get the actual assigned port (important when port 0 is specified)
208
+ const address = httpServer.address();
209
+ const actualPort = typeof address === "object" && address !== null
210
+ ? address.port
211
+ : port;
212
+ resolve({
213
+ httpServer,
214
+ port: actualPort,
215
+ host,
216
+ distPath,
217
+ close() {
218
+ return new Promise((resolveClose, rejectClose) => {
219
+ httpServer.close((err) => {
220
+ if (err) {
221
+ rejectClose(err);
222
+ }
223
+ else {
224
+ resolveClose();
225
+ }
226
+ });
227
+ });
228
+ },
229
+ });
230
+ });
231
+ });
232
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * WebSocket server for Phoenix Insight CLI.
3
+ * Provides bidirectional communication between the CLI agent and the web UI.
4
+ *
5
+ * Binds to localhost only for security (no external network access).
6
+ * Handles HTTP upgrade requests, manages client connections, and broadcasts messages.
7
+ */
8
+ import { WebSocketServer, WebSocket } from "ws";
9
+ // ============================================================================
10
+ // Phoenix WebSocket Server
11
+ // ============================================================================
12
+ /**
13
+ * Phoenix WebSocket server wrapper providing typed message handling
14
+ * and connection management.
15
+ */
16
+ export class PhoenixWebSocketServer {
17
+ wss = null;
18
+ clients = new Set();
19
+ options;
20
+ constructor(options = {}) {
21
+ this.options = {
22
+ path: "/ws",
23
+ onMessage: () => { },
24
+ onConnection: () => { },
25
+ onDisconnection: () => { },
26
+ onError: () => { },
27
+ ...options,
28
+ };
29
+ }
30
+ /**
31
+ * Attach the WebSocket server to an existing HTTP server.
32
+ * Uses the upgrade event to handle WebSocket handshakes.
33
+ */
34
+ attach(httpServer) {
35
+ if (this.wss) {
36
+ throw new Error("WebSocket server is already attached");
37
+ }
38
+ // Create WebSocket server with noServer mode to handle upgrade ourselves
39
+ this.wss = new WebSocketServer({ noServer: true });
40
+ // Handle HTTP upgrade requests
41
+ httpServer.on("upgrade", (request, socket, head) => {
42
+ this.handleUpgrade(request, socket, head);
43
+ });
44
+ // Set up WebSocket server event handlers
45
+ this.setupEventHandlers();
46
+ }
47
+ /**
48
+ * Handle HTTP upgrade request for WebSocket connection.
49
+ * Only accepts connections on the configured path and from localhost.
50
+ */
51
+ handleUpgrade(request, socket, head) {
52
+ if (!this.wss) {
53
+ socket.destroy();
54
+ return;
55
+ }
56
+ // Parse the request URL
57
+ const url = new URL(request.url ?? "/", `http://${request.headers.host}`);
58
+ // Only accept connections on the configured path
59
+ if (url.pathname !== this.options.path) {
60
+ socket.destroy();
61
+ return;
62
+ }
63
+ // Complete the WebSocket handshake
64
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
65
+ this.wss?.emit("connection", ws, request);
66
+ });
67
+ }
68
+ /**
69
+ * Set up event handlers for the WebSocket server.
70
+ */
71
+ setupEventHandlers() {
72
+ if (!this.wss)
73
+ return;
74
+ this.wss.on("connection", (ws) => {
75
+ this.clients.add(ws);
76
+ this.options.onConnection(ws);
77
+ ws.on("message", (data) => {
78
+ this.handleMessage(data, ws);
79
+ });
80
+ ws.on("close", (code, reason) => {
81
+ this.clients.delete(ws);
82
+ this.options.onDisconnection(ws, code, reason.toString());
83
+ });
84
+ ws.on("error", (error) => {
85
+ this.options.onError(error, ws);
86
+ });
87
+ });
88
+ this.wss.on("error", (error) => {
89
+ this.options.onError(error);
90
+ });
91
+ }
92
+ /**
93
+ * Handle incoming message from a client.
94
+ * Parses JSON and validates message structure before calling handler.
95
+ */
96
+ handleMessage(data, client) {
97
+ try {
98
+ const rawMessage = data.toString();
99
+ const parsed = JSON.parse(rawMessage);
100
+ // Basic validation of message structure
101
+ if (!parsed || typeof parsed !== "object") {
102
+ throw new Error("Invalid message structure: expected object");
103
+ }
104
+ const obj = parsed;
105
+ if (!("type" in obj) || typeof obj.type !== "string") {
106
+ throw new Error("Invalid message structure: missing type field");
107
+ }
108
+ const messageType = obj.type;
109
+ if (messageType !== "query" && messageType !== "cancel") {
110
+ throw new Error(`Unknown message type: ${messageType}`);
111
+ }
112
+ // Now we know this is a valid ClientMessage
113
+ const message = parsed;
114
+ this.options.onMessage(message, client);
115
+ }
116
+ catch (error) {
117
+ // Send error message back to the client
118
+ const errorMessage = {
119
+ type: "error",
120
+ payload: {
121
+ message: error instanceof Error
122
+ ? error.message
123
+ : "Failed to parse message",
124
+ },
125
+ };
126
+ this.sendToClient(client, errorMessage);
127
+ }
128
+ }
129
+ /**
130
+ * Send a message to a specific client.
131
+ */
132
+ sendToClient(client, message) {
133
+ if (client.readyState === WebSocket.OPEN) {
134
+ client.send(JSON.stringify(message));
135
+ }
136
+ }
137
+ /**
138
+ * Broadcast a message to all connected clients.
139
+ */
140
+ broadcast(message) {
141
+ const data = JSON.stringify(message);
142
+ for (const client of this.clients) {
143
+ if (client.readyState === WebSocket.OPEN) {
144
+ client.send(data);
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * Broadcast a message to all clients with a specific session ID.
150
+ * This requires tracking session-to-client mapping externally.
151
+ * For now, it broadcasts to all clients (to be refined in cli-agent-session).
152
+ */
153
+ broadcastToSession(sessionId, message) {
154
+ // For now, broadcast to all clients.
155
+ // Session-to-client mapping will be implemented in cli-agent-session task.
156
+ this.broadcast(message);
157
+ }
158
+ /**
159
+ * Get the number of connected clients.
160
+ */
161
+ get clientCount() {
162
+ return this.clients.size;
163
+ }
164
+ /**
165
+ * Get all connected clients.
166
+ */
167
+ getClients() {
168
+ return new Set(this.clients);
169
+ }
170
+ /**
171
+ * Close the WebSocket server and disconnect all clients.
172
+ */
173
+ close() {
174
+ return new Promise((resolve, reject) => {
175
+ if (!this.wss) {
176
+ resolve();
177
+ return;
178
+ }
179
+ // Close all client connections
180
+ for (const client of this.clients) {
181
+ client.close(1000, "Server shutting down");
182
+ }
183
+ this.clients.clear();
184
+ // Close the WebSocket server
185
+ this.wss.close((err) => {
186
+ this.wss = null;
187
+ if (err) {
188
+ reject(err);
189
+ }
190
+ else {
191
+ resolve();
192
+ }
193
+ });
194
+ });
195
+ }
196
+ }
197
+ // ============================================================================
198
+ // Factory Function
199
+ // ============================================================================
200
+ /**
201
+ * Create and attach a WebSocket server to an HTTP server.
202
+ * The WebSocket server binds to localhost only (through the HTTP server).
203
+ *
204
+ * @param httpServer - HTTP server to attach WebSocket handling to
205
+ * @param options - WebSocket server options
206
+ * @returns PhoenixWebSocketServer instance
207
+ */
208
+ export function createWebSocketServer(httpServer, options) {
209
+ const server = new PhoenixWebSocketServer(options);
210
+ server.attach(httpServer);
211
+ return server;
212
+ }
@@ -1 +1 @@
1
- {"root":["../src/cli.ts","../src/index.ts","../src/progress.ts","../src/agent/index.ts","../src/commands/index.ts","../src/commands/px-fetch-more-spans.ts","../src/commands/px-fetch-more-trace.ts","../src/config/index.ts","../src/config/loader.ts","../src/config/schema.ts","../src/modes/index.ts","../src/modes/local.ts","../src/modes/sandbox.ts","../src/modes/types.ts","../src/observability/index.ts","../src/prompts/index.ts","../src/prompts/system.ts","../src/snapshot/client.ts","../src/snapshot/context.ts","../src/snapshot/datasets.ts","../src/snapshot/experiments.ts","../src/snapshot/index.ts","../src/snapshot/projects.ts","../src/snapshot/prompts.ts","../src/snapshot/spans.ts","../src/snapshot/utils.ts"],"version":"5.9.3"}
1
+ {"root":["../src/cli.ts","../src/index.ts","../src/progress.ts","../src/agent/index.ts","../src/commands/index.ts","../src/commands/px-fetch-more-spans.ts","../src/commands/px-fetch-more-trace.ts","../src/commands/report-tool.ts","../src/config/index.ts","../src/config/loader.ts","../src/config/schema.ts","../src/modes/index.ts","../src/modes/local.ts","../src/modes/sandbox.ts","../src/modes/types.ts","../src/observability/index.ts","../src/prompts/index.ts","../src/prompts/system.ts","../src/server/session.ts","../src/server/ui.ts","../src/server/websocket.ts","../src/snapshot/client.ts","../src/snapshot/context.ts","../src/snapshot/datasets.ts","../src/snapshot/experiments.ts","../src/snapshot/index.ts","../src/snapshot/projects.ts","../src/snapshot/prompts.ts","../src/snapshot/spans.ts","../src/snapshot/utils.ts"],"version":"5.9.3"}