@cephalization/phoenix-insight 0.4.0 → 1.0.2

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 (76) hide show
  1. package/README.md +195 -1
  2. package/dist/chunk-KEQDYZIE.js +237 -0
  3. package/dist/chunk-KEQDYZIE.js.map +1 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +3950 -642
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.d.ts +108 -0
  8. package/dist/index.js +13 -1
  9. package/dist/index.js.map +1 -0
  10. package/dist/ui/assets/code-block-F6WJLWQG-BTdTzfvl.js +154 -0
  11. package/dist/ui/assets/code-block-F6WJLWQG-BTdTzfvl.js.map +1 -0
  12. package/dist/ui/assets/index-CX8aDatf.css +1 -0
  13. package/dist/ui/assets/index-DjZuAW6Y.js +63 -0
  14. package/dist/ui/assets/index-DjZuAW6Y.js.map +1 -0
  15. package/dist/ui/assets/vendor-data-r1ZEkUds.js +40 -0
  16. package/dist/ui/assets/vendor-data-r1ZEkUds.js.map +1 -0
  17. package/dist/ui/assets/vendor-react-Cgg2GOmP.js +2 -0
  18. package/dist/ui/assets/vendor-react-Cgg2GOmP.js.map +1 -0
  19. package/dist/ui/assets/vendor-render-DoMl5bum.js +381 -0
  20. package/dist/ui/assets/vendor-render-DoMl5bum.js.map +1 -0
  21. package/dist/ui/assets/vendor-ui-Cg-YC4hK.js +46 -0
  22. package/dist/ui/assets/vendor-ui-Cg-YC4hK.js.map +1 -0
  23. package/dist/ui/index.html +18 -0
  24. package/dist/ui/vite.svg +1 -0
  25. package/package.json +14 -15
  26. package/dist/agent/index.js +0 -230
  27. package/dist/commands/index.js +0 -2
  28. package/dist/commands/px-fetch-more-spans.js +0 -98
  29. package/dist/commands/px-fetch-more-trace.js +0 -110
  30. package/dist/config/index.js +0 -165
  31. package/dist/config/loader.js +0 -141
  32. package/dist/config/schema.js +0 -53
  33. package/dist/modes/index.js +0 -17
  34. package/dist/modes/local.js +0 -134
  35. package/dist/modes/sandbox.js +0 -121
  36. package/dist/modes/types.js +0 -1
  37. package/dist/observability/index.js +0 -65
  38. package/dist/progress.js +0 -209
  39. package/dist/prompts/index.js +0 -1
  40. package/dist/prompts/system.js +0 -30
  41. package/dist/snapshot/client.js +0 -74
  42. package/dist/snapshot/context.js +0 -441
  43. package/dist/snapshot/datasets.js +0 -68
  44. package/dist/snapshot/experiments.js +0 -135
  45. package/dist/snapshot/index.js +0 -262
  46. package/dist/snapshot/projects.js +0 -44
  47. package/dist/snapshot/prompts.js +0 -199
  48. package/dist/snapshot/spans.js +0 -104
  49. package/dist/snapshot/utils.js +0 -112
  50. package/dist/tsconfig.esm.tsbuildinfo +0 -1
  51. package/src/agent/index.ts +0 -323
  52. package/src/cli.ts +0 -854
  53. package/src/commands/index.ts +0 -8
  54. package/src/commands/px-fetch-more-spans.ts +0 -174
  55. package/src/commands/px-fetch-more-trace.ts +0 -183
  56. package/src/config/index.ts +0 -225
  57. package/src/config/loader.ts +0 -173
  58. package/src/config/schema.ts +0 -66
  59. package/src/index.ts +0 -1
  60. package/src/modes/index.ts +0 -21
  61. package/src/modes/local.ts +0 -163
  62. package/src/modes/sandbox.ts +0 -144
  63. package/src/modes/types.ts +0 -31
  64. package/src/observability/index.ts +0 -90
  65. package/src/progress.ts +0 -239
  66. package/src/prompts/index.ts +0 -1
  67. package/src/prompts/system.ts +0 -31
  68. package/src/snapshot/client.ts +0 -129
  69. package/src/snapshot/context.ts +0 -587
  70. package/src/snapshot/datasets.ts +0 -132
  71. package/src/snapshot/experiments.ts +0 -246
  72. package/src/snapshot/index.ts +0 -403
  73. package/src/snapshot/projects.ts +0 -58
  74. package/src/snapshot/prompts.ts +0 -267
  75. package/src/snapshot/spans.ts +0 -163
  76. package/src/snapshot/utils.ts +0 -140
package/README.md CHANGED
@@ -148,10 +148,33 @@ phoenix-insight --refresh "show me the latest errors"
148
148
  phoenix-insight --no-stream "generate report" > report.txt
149
149
  ```
150
150
 
151
+ ### Web UI
152
+
153
+ Start the web-based UI for a visual chat interface and structured report display:
154
+
155
+ ```bash
156
+ # Start web UI (opens browser automatically)
157
+ phoenix-insight ui
158
+
159
+ # Start on a custom port
160
+ phoenix-insight ui --port 8080
161
+
162
+ # Start without opening browser
163
+ phoenix-insight ui --no-open
164
+ ```
165
+
166
+ The web UI provides:
167
+
168
+ - **Split-pane interface**: Chat panel on the left, report display on the right
169
+ - **Real-time streaming**: See agent responses and tool usage as they happen
170
+ - **Structured reports**: AI-generated reports with tables, metrics, and formatted content
171
+ - **Session history**: Persist and browse previous conversations and reports
172
+ - **WebSocket connection**: Real-time bidirectional communication with the agent
173
+
151
174
  ### Observability
152
175
 
153
176
  ```bash
154
- # Enable tracing of agent operations to Phoenix
177
+ # Enable tracing of agent operations to Phoenix (enabled by default)
155
178
  phoenix-insight --trace "analyze performance"
156
179
  ```
157
180
 
@@ -398,6 +421,134 @@ phoenix-insight prune [options]
398
421
  | ----------- | ---------------------------------------- | ------- | ------------------------------- |
399
422
  | `--dry-run` | Preview what would be deleted without actually deleting | `false` | `phoenix-insight prune --dry-run` |
400
423
 
424
+ ### UI Command
425
+
426
+ Starts a web-based UI for visual interaction with the Phoenix Insight agent.
427
+
428
+ ```bash
429
+ phoenix-insight ui [options]
430
+ ```
431
+
432
+ | Option | Description | Default | Example |
433
+ | --------------- | ------------------------------------------------ | ------- | -------------------------------- |
434
+ | `--port <n>` | Port to run the UI server on | `6007` | `phoenix-insight ui --port 8080` |
435
+ | `--no-open` | Do not automatically open browser | `false` | `phoenix-insight ui --no-open` |
436
+
437
+ The UI server provides:
438
+
439
+ - **HTTP server**: Serves the web UI on `http://localhost:6007` (configurable port)
440
+ - **WebSocket**: Real-time bidirectional communication at `/ws`
441
+ - **Session management**: Multiple concurrent sessions with conversation history
442
+ - **Report generation**: Agent can generate structured reports displayed in the UI
443
+
444
+ **Usage notes:**
445
+
446
+ - The server binds to localhost only (127.0.0.1) for security
447
+ - Press Ctrl+C to stop the server gracefully
448
+ - Configuration is loaded from the standard config file and environment variables
449
+
450
+ #### Web UI Features
451
+
452
+ The web UI provides a rich interface for interacting with Phoenix Insight:
453
+
454
+ **Split-pane Layout:**
455
+ - Left panel: Chat interface with message history
456
+ - Right panel: Structured report display
457
+ - Resizable panels with drag handle
458
+ - Responsive design: stacked tabs on mobile devices
459
+
460
+ **Chat Interface:**
461
+ - Real-time streaming of agent responses
462
+ - Markdown rendering optimized for streaming content
463
+ - Session history with dropdown to switch between conversations
464
+ - Create, switch, and delete sessions
465
+ - Visual indicators for agent tool usage (tool_call/tool_result events)
466
+
467
+ **Report Panel:**
468
+ - Structured reports generated by the agent using `generate_report` tool
469
+ - Components: Cards, Tables, Metrics, Alerts, Badges, Lists, Code blocks, and more
470
+ - Download reports as Markdown
471
+ - Browse and restore previous reports from history
472
+
473
+ **Connection Management:**
474
+ - Automatic reconnection with exponential backoff (via partysocket)
475
+ - Visual connection status indicator (green/yellow/red)
476
+ - Toast notifications for connection state changes
477
+ - Message buffering during temporary disconnections
478
+
479
+ **Data Persistence:**
480
+ - Sessions and reports stored in browser's IndexedDB
481
+ - Survives page refreshes and browser restarts
482
+ - Export reports as Markdown files
483
+
484
+ #### WebSocket Protocol
485
+
486
+ The UI communicates with the CLI server over WebSocket at the `/ws` endpoint. All messages are JSON-encoded.
487
+
488
+ **Client Messages (UI to Server):**
489
+
490
+ ```typescript
491
+ // Send a query to the agent
492
+ { type: "query", payload: { content: string, sessionId?: string } }
493
+
494
+ // Cancel the current query
495
+ { type: "cancel", payload: { sessionId?: string } }
496
+ ```
497
+
498
+ **Server Messages (Server to UI):**
499
+
500
+ ```typescript
501
+ // Streaming text content from the agent
502
+ { type: "text", payload: { content: string, sessionId: string } }
503
+
504
+ // Agent is calling a tool
505
+ { type: "tool_call", payload: { toolName: string, args: unknown, sessionId: string } }
506
+
507
+ // Tool execution result
508
+ { type: "tool_result", payload: { toolName: string, result: unknown, sessionId: string } }
509
+
510
+ // Agent generated a structured report
511
+ { type: "report", payload: { content: UITree, sessionId: string } }
512
+
513
+ // Error occurred during processing
514
+ { type: "error", payload: { message: string, sessionId?: string } }
515
+
516
+ // Agent finished processing the query
517
+ { type: "done", payload: { sessionId: string } }
518
+ ```
519
+
520
+ **Report Content Structure (UITree):**
521
+
522
+ The `report` message contains a `UITree` structure compatible with [json-render](https://github.com/vercel-labs/json-render):
523
+
524
+ ```typescript
525
+ interface UITree {
526
+ root: string; // Key of the root element
527
+ elements: Record<string, UIElement>; // Flat map of all elements
528
+ }
529
+
530
+ interface UIElement {
531
+ key: string;
532
+ type: "Card" | "Text" | "Heading" | "List" | "Table" | "Metric" | "Badge" | "Alert" | "Separator" | "Code";
533
+ props: Record<string, unknown>; // Component-specific props
534
+ children?: string[]; // Keys of child elements
535
+ parentKey?: string;
536
+ }
537
+ ```
538
+
539
+ **Example WebSocket Session:**
540
+
541
+ ```
542
+ Client: {"type":"query","payload":{"content":"Show error patterns","sessionId":"session-1"}}
543
+ Server: {"type":"text","payload":{"content":"I'll analyze","sessionId":"session-1"}}
544
+ Server: {"type":"text","payload":{"content":" the error patterns...","sessionId":"session-1"}}
545
+ Server: {"type":"tool_call","payload":{"toolName":"bash","args":{"command":"grep -c 'error' spans.jsonl"},"sessionId":"session-1"}}
546
+ Server: {"type":"tool_result","payload":{"toolName":"bash","result":"42","sessionId":"session-1"}}
547
+ Server: {"type":"text","payload":{"content":"Found 42 errors.","sessionId":"session-1"}}
548
+ Server: {"type":"report","payload":{"content":{"root":"card-1","elements":{...}},"sessionId":"session-1"}}
549
+ Server: {"type":"done","payload":{"sessionId":"session-1"}}
550
+ ```
551
+
401
552
  ### Help Command
402
553
 
403
554
  Displays help information and available options.
@@ -691,6 +842,49 @@ pnpm test test/modes/sandbox.test.ts
691
842
  pnpm typecheck
692
843
  ```
693
844
 
845
+ #### UI Integration Testing
846
+
847
+ The web UI has manual integration tests using [agent-browser](https://github.com/vercel-labs/agent-browser) for browser automation. These tests are NOT run in CI because they require a live Phoenix server with data.
848
+
849
+ **Prerequisites:**
850
+
851
+ 1. Phoenix server running on `localhost:6006` with data
852
+ 2. Run from the monorepo root
853
+
854
+ **Running UI tests:**
855
+
856
+ ```bash
857
+ # From monorepo root - builds packages and runs UI integration tests
858
+ pnpm test:ui
859
+ ```
860
+
861
+ The test:ui script will:
862
+ 1. Build the UI package
863
+ 2. Build the CLI package (which bundles the UI)
864
+ 3. Expect a running Phoenix server at localhost:6006
865
+ 4. Expect the UI server to be running at localhost:6007 (start with `phoenix-insight ui`)
866
+ 5. Run browser automation tests that verify:
867
+ - Layout renders correctly (chat panel, report panel)
868
+ - Chat input is functional
869
+ - WebSocket connection works
870
+ - Session management works
871
+ - Report panel displays correctly
872
+
873
+ **Manual testing workflow:**
874
+
875
+ ```bash
876
+ # Terminal 1: Start Phoenix
877
+ phoenix serve
878
+
879
+ # Terminal 2: Start the UI server
880
+ cd packages/cli && pnpm dev ui
881
+
882
+ # Terminal 3: Run UI tests
883
+ pnpm test:ui
884
+ ```
885
+
886
+ **Note:** If tests skip with "Phoenix server not running" or "UI server not running", ensure both servers are accessible before running tests.
887
+
694
888
  ### Contributing & Releases
695
889
 
696
890
  Contributions are welcome! This project uses [changesets](https://github.com/changesets/changesets) for version management and automated releases.
@@ -0,0 +1,237 @@
1
+ // src/modes/sandbox.ts
2
+ import { tool } from "ai";
3
+ import { z } from "zod";
4
+ var SandboxMode = class {
5
+ bash;
6
+ // Will be typed as Bash from just-bash
7
+ initialized = false;
8
+ bashToolPromise = null;
9
+ snapshotRoot = "/phoenix/";
10
+ constructor() {
11
+ }
12
+ /**
13
+ * Get the absolute root path of the Phoenix snapshot directory
14
+ * For sandbox mode, this is always the virtual path "/phoenix/"
15
+ */
16
+ getSnapshotRoot() {
17
+ return this.snapshotRoot;
18
+ }
19
+ async init() {
20
+ if (this.initialized) return;
21
+ try {
22
+ const { Bash } = await import("just-bash");
23
+ this.bash = new Bash({ cwd: "/phoenix" });
24
+ this.initialized = true;
25
+ } catch (error) {
26
+ throw new Error(
27
+ `Failed to initialize sandbox mode: ${error instanceof Error ? error.message : String(error)}`
28
+ );
29
+ }
30
+ }
31
+ async writeFile(path2, content) {
32
+ await this.init();
33
+ try {
34
+ const fullPath = path2.startsWith("/phoenix") ? path2 : `/phoenix${path2.startsWith("/") ? "" : "/"}${path2}`;
35
+ const dirname2 = fullPath.substring(0, fullPath.lastIndexOf("/"));
36
+ if (dirname2) {
37
+ await this.bash.exec(`mkdir -p ${dirname2}`);
38
+ }
39
+ this.bash.fs.writeFileSync(fullPath, content);
40
+ } catch (error) {
41
+ throw new Error(
42
+ `Failed to write file ${path2}: ${error instanceof Error ? error.message : String(error)}`
43
+ );
44
+ }
45
+ }
46
+ async exec(command) {
47
+ await this.init();
48
+ try {
49
+ const result = await this.bash.exec(command);
50
+ return {
51
+ stdout: result.stdout || "",
52
+ stderr: result.stderr || "",
53
+ exitCode: result.exitCode || 0
54
+ };
55
+ } catch (error) {
56
+ if (error && typeof error === "object" && "exitCode" in error) {
57
+ return {
58
+ stdout: error.stdout || "",
59
+ stderr: error.stderr || error.toString(),
60
+ exitCode: error.exitCode || 1
61
+ };
62
+ }
63
+ return {
64
+ stdout: "",
65
+ stderr: error?.toString() || "Unknown error",
66
+ exitCode: 1
67
+ };
68
+ }
69
+ }
70
+ async getBashTool() {
71
+ if (!this.bashToolPromise) {
72
+ this.bashToolPromise = this.createBashTool();
73
+ }
74
+ return this.bashToolPromise;
75
+ }
76
+ async createBashTool() {
77
+ await this.init();
78
+ return tool({
79
+ description: "Execute bash commands in the sandbox filesystem",
80
+ inputSchema: z.object({
81
+ command: z.string().describe("The bash command to execute")
82
+ }),
83
+ execute: async ({ command }) => {
84
+ const result = await this.exec(command);
85
+ if (result.exitCode !== 0) {
86
+ return {
87
+ success: false,
88
+ stdout: result.stdout,
89
+ stderr: result.stderr,
90
+ exitCode: result.exitCode,
91
+ error: `Command failed with exit code ${result.exitCode}`
92
+ };
93
+ }
94
+ return {
95
+ success: true,
96
+ stdout: result.stdout,
97
+ stderr: result.stderr,
98
+ exitCode: result.exitCode
99
+ };
100
+ }
101
+ });
102
+ }
103
+ async cleanup() {
104
+ }
105
+ };
106
+
107
+ // src/modes/local.ts
108
+ import { exec as execCallback } from "child_process";
109
+ import { promisify } from "util";
110
+ import * as fs from "fs/promises";
111
+ import * as path from "path";
112
+ import * as os from "os";
113
+ import { tool as tool2 } from "ai";
114
+ import { z as z2 } from "zod";
115
+ var execAsync = promisify(execCallback);
116
+ var LocalMode = class {
117
+ workDir;
118
+ toolCreated = false;
119
+ bashToolPromise = null;
120
+ constructor() {
121
+ const timestamp = Date.now().toString() + "-" + Math.random().toString(36).substring(7);
122
+ this.workDir = path.join(
123
+ os.homedir(),
124
+ ".phoenix-insight",
125
+ "snapshots",
126
+ timestamp,
127
+ "phoenix"
128
+ );
129
+ }
130
+ /**
131
+ * Get the absolute root path of the Phoenix snapshot directory
132
+ * For local mode, this is the actual filesystem path
133
+ */
134
+ getSnapshotRoot() {
135
+ return this.workDir;
136
+ }
137
+ /**
138
+ * Initialize the working directory
139
+ */
140
+ async init() {
141
+ try {
142
+ await fs.mkdir(this.workDir, { recursive: true });
143
+ } catch (error) {
144
+ throw new Error(
145
+ `Failed to initialize local mode directory at ${this.workDir}: ${error instanceof Error ? error.message : String(error)}`
146
+ );
147
+ }
148
+ }
149
+ async writeFile(filePath, content) {
150
+ await this.init();
151
+ const cleanPath = filePath.startsWith("/phoenix") ? filePath.substring(8) : filePath.startsWith("/") ? filePath.substring(1) : filePath;
152
+ const fullPath = path.join(this.workDir, cleanPath);
153
+ const dirname2 = path.dirname(fullPath);
154
+ await fs.mkdir(dirname2, { recursive: true });
155
+ await fs.writeFile(fullPath, content, "utf-8");
156
+ }
157
+ async exec(command) {
158
+ await this.init();
159
+ try {
160
+ const { stdout, stderr } = await execAsync(command, {
161
+ cwd: this.workDir,
162
+ shell: "/bin/bash",
163
+ encoding: "utf-8",
164
+ timeout: 6e4
165
+ // 60 second timeout for bash commands
166
+ });
167
+ return {
168
+ stdout: stdout || "",
169
+ stderr: stderr || "",
170
+ exitCode: 0
171
+ };
172
+ } catch (error) {
173
+ if (error.code !== void 0) {
174
+ return {
175
+ stdout: error.stdout || "",
176
+ stderr: error.stderr || error.message || "Command failed",
177
+ exitCode: error.code || 1
178
+ };
179
+ }
180
+ return {
181
+ stdout: "",
182
+ stderr: error.message || "Unknown error",
183
+ exitCode: 1
184
+ };
185
+ }
186
+ }
187
+ async getBashTool() {
188
+ if (!this.bashToolPromise) {
189
+ this.bashToolPromise = this.createBashTool();
190
+ }
191
+ return this.bashToolPromise;
192
+ }
193
+ async createBashTool() {
194
+ return tool2({
195
+ description: "Execute bash commands in the local filesystem",
196
+ inputSchema: z2.object({
197
+ command: z2.string().describe("The bash command to execute")
198
+ }),
199
+ execute: async ({ command }) => {
200
+ const result = await this.exec(command);
201
+ if (result.exitCode !== 0) {
202
+ return {
203
+ success: false,
204
+ stdout: result.stdout,
205
+ stderr: result.stderr,
206
+ exitCode: result.exitCode,
207
+ error: `Command failed with exit code ${result.exitCode}`
208
+ };
209
+ }
210
+ return {
211
+ success: true,
212
+ stdout: result.stdout,
213
+ stderr: result.stderr,
214
+ exitCode: result.exitCode
215
+ };
216
+ }
217
+ });
218
+ }
219
+ async cleanup() {
220
+ }
221
+ };
222
+
223
+ // src/modes/index.ts
224
+ function createSandboxMode() {
225
+ return new SandboxMode();
226
+ }
227
+ async function createLocalMode() {
228
+ return new LocalMode();
229
+ }
230
+
231
+ export {
232
+ SandboxMode,
233
+ LocalMode,
234
+ createSandboxMode,
235
+ createLocalMode
236
+ };
237
+ //# sourceMappingURL=chunk-KEQDYZIE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/modes/sandbox.ts","../src/modes/local.ts","../src/modes/index.ts"],"sourcesContent":["import type { ExecutionMode } from \"./types.js\";\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\n\n/**\n * Sandbox execution mode using just-bash for isolated execution\n * - In-memory filesystem\n * - Simulated bash commands\n * - No disk or network access\n */\nexport class SandboxMode implements ExecutionMode {\n private bash: any; // Will be typed as Bash from just-bash\n private initialized = false;\n private bashToolPromise: Promise<any> | null = null;\n private readonly snapshotRoot = \"/phoenix/\";\n\n constructor() {\n // We'll initialize in the init method since we need async imports\n }\n\n /**\n * Get the absolute root path of the Phoenix snapshot directory\n * For sandbox mode, this is always the virtual path \"/phoenix/\"\n */\n getSnapshotRoot(): string {\n return this.snapshotRoot;\n }\n\n private async init() {\n if (this.initialized) return;\n\n try {\n // Dynamic imports for ESM modules\n const { Bash } = await import(\"just-bash\");\n\n // Initialize just-bash with /phoenix as the working directory\n this.bash = new Bash({ cwd: \"/phoenix\" });\n\n this.initialized = true;\n } catch (error) {\n throw new Error(\n `Failed to initialize sandbox mode: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n async writeFile(path: string, content: string): Promise<void> {\n await this.init();\n\n try {\n // Ensure the path starts with /phoenix\n const fullPath = path.startsWith(\"/phoenix\")\n ? path\n : `/phoenix${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n\n // Create parent directories if they don't exist\n const dirname = fullPath.substring(0, fullPath.lastIndexOf(\"/\"));\n if (dirname) {\n await this.bash.exec(`mkdir -p ${dirname}`);\n }\n\n // Write the file using just-bash's filesystem\n // We'll use the InMemoryFs directly for better performance\n this.bash.fs.writeFileSync(fullPath, content);\n } catch (error) {\n throw new Error(\n `Failed to write file ${path}: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n async exec(\n command: string\n ): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n await this.init();\n\n try {\n const result = await this.bash.exec(command);\n\n // just-bash returns a different structure, so we need to normalize it\n return {\n stdout: result.stdout || \"\",\n stderr: result.stderr || \"\",\n exitCode: result.exitCode || 0,\n };\n } catch (error) {\n // If the command fails, just-bash throws an error\n // Extract what we can from the error\n if (error && typeof error === \"object\" && \"exitCode\" in error) {\n return {\n stdout: (error as any).stdout || \"\",\n stderr: (error as any).stderr || error.toString(),\n exitCode: (error as any).exitCode || 1,\n };\n }\n\n // Fallback for unexpected errors\n return {\n stdout: \"\",\n stderr: error?.toString() || \"Unknown error\",\n exitCode: 1,\n };\n }\n }\n\n async getBashTool(): Promise<any> {\n // Only create the tool once and cache it\n if (!this.bashToolPromise) {\n this.bashToolPromise = this.createBashTool();\n }\n return this.bashToolPromise;\n }\n\n private async createBashTool(): Promise<any> {\n await this.init();\n\n // Create a bash tool compatible with the AI SDK\n // Similar to local mode, we'll create it directly using the tool function\n return tool({\n description: \"Execute bash commands in the sandbox filesystem\",\n inputSchema: z.object({\n command: z.string().describe(\"The bash command to execute\"),\n }),\n execute: async ({ command }) => {\n const result = await this.exec(command);\n\n // Return result in a format similar to bash-tool\n if (result.exitCode !== 0) {\n // Include error details in the response\n return {\n success: false,\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode,\n error: `Command failed with exit code ${result.exitCode}`,\n };\n }\n\n return {\n success: true,\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode,\n };\n },\n });\n }\n\n async cleanup(): Promise<void> {\n // No-op for in-memory mode - garbage collection will handle cleanup\n // We could optionally clear the filesystem here if needed\n }\n}\n","import type { ExecutionMode } from \"./types.js\";\nimport { exec as execCallback } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\n\nconst execAsync = promisify(execCallback);\n\n/**\n * Local execution mode using real bash and persistent filesystem\n * - Real bash execution via child_process\n * - Persistent storage in ~/.phoenix-insight/\n * - Full system access\n */\nexport class LocalMode implements ExecutionMode {\n private workDir: string;\n private toolCreated = false;\n private bashToolPromise: Promise<any> | null = null;\n\n constructor() {\n // Create a timestamped directory for this snapshot\n // Add a small random component to ensure uniqueness even if created at the same millisecond\n const timestamp =\n Date.now().toString() + \"-\" + Math.random().toString(36).substring(7);\n this.workDir = path.join(\n os.homedir(),\n \".phoenix-insight\",\n \"snapshots\",\n timestamp,\n \"phoenix\"\n );\n }\n\n /**\n * Get the absolute root path of the Phoenix snapshot directory\n * For local mode, this is the actual filesystem path\n */\n getSnapshotRoot(): string {\n return this.workDir;\n }\n\n /**\n * Initialize the working directory\n */\n private async init() {\n try {\n // Create the directory structure if it doesn't exist\n await fs.mkdir(this.workDir, { recursive: true });\n } catch (error) {\n throw new Error(\n `Failed to initialize local mode directory at ${this.workDir}: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n async writeFile(filePath: string, content: string): Promise<void> {\n await this.init();\n\n // Ensure the path is relative to phoenix root\n const cleanPath = filePath.startsWith(\"/phoenix\")\n ? filePath.substring(8) // Remove /phoenix prefix\n : filePath.startsWith(\"/\")\n ? filePath.substring(1) // Remove leading slash\n : filePath;\n\n const fullPath = path.join(this.workDir, cleanPath);\n\n // Create parent directories if they don't exist\n const dirname = path.dirname(fullPath);\n await fs.mkdir(dirname, { recursive: true });\n\n // Write the file\n await fs.writeFile(fullPath, content, \"utf-8\");\n }\n\n async exec(\n command: string\n ): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n await this.init();\n\n try {\n // Execute the command in the phoenix directory with a timeout\n const { stdout, stderr } = await execAsync(command, {\n cwd: this.workDir,\n shell: \"/bin/bash\",\n encoding: \"utf-8\",\n timeout: 60000, // 60 second timeout for bash commands\n });\n\n return {\n stdout: stdout || \"\",\n stderr: stderr || \"\",\n exitCode: 0,\n };\n } catch (error: any) {\n // Handle command execution errors\n if (error.code !== undefined) {\n return {\n stdout: error.stdout || \"\",\n stderr: error.stderr || error.message || \"Command failed\",\n exitCode: error.code || 1,\n };\n }\n\n // Handle other errors\n return {\n stdout: \"\",\n stderr: error.message || \"Unknown error\",\n exitCode: 1,\n };\n }\n }\n\n async getBashTool(): Promise<any> {\n // Only create the tool once and cache it\n if (!this.bashToolPromise) {\n this.bashToolPromise = this.createBashTool();\n }\n return this.bashToolPromise;\n }\n\n private async createBashTool(): Promise<any> {\n // We can't use bash-tool directly for local mode since it's designed for just-bash\n // Instead, we'll create a tool using the AI SDK's tool function\n // This ensures compatibility with the AI SDK\n\n // Return a bash tool that executes real bash commands\n return tool({\n description: \"Execute bash commands in the local filesystem\",\n inputSchema: z.object({\n command: z.string().describe(\"The bash command to execute\"),\n }),\n execute: async ({ command }) => {\n const result = await this.exec(command);\n\n // Return result in a format similar to bash-tool\n if (result.exitCode !== 0) {\n // Include error details in the response\n return {\n success: false,\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode,\n error: `Command failed with exit code ${result.exitCode}`,\n };\n }\n\n return {\n success: true,\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode,\n };\n },\n });\n }\n\n async cleanup(): Promise<void> {\n // Optional: Clean up old snapshots\n // For now, we'll keep all snapshots for user reference\n // Users can manually clean ~/.phoenix-insight/ if needed\n // We could implement logic to:\n // 1. Keep only the last N snapshots\n // 2. Delete snapshots older than X days\n // 3. Provide a separate cleanup command\n // For this implementation, we do nothing\n }\n}\n","export * from \"./types.js\";\nexport * from \"./sandbox.js\";\nexport * from \"./local.js\";\n\nimport { SandboxMode } from \"./sandbox.js\";\nimport { LocalMode } from \"./local.js\";\nimport type { ExecutionMode } from \"./types.js\";\n\n/**\n * Creates a new sandbox execution mode\n */\nexport function createSandboxMode(): ExecutionMode {\n return new SandboxMode();\n}\n\n/**\n * Creates a new local execution mode\n */\nexport async function createLocalMode(): Promise<ExecutionMode> {\n return new LocalMode();\n}\n"],"mappings":"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","names":["path","dirname","tool","z","dirname"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node