@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.
- package/README.md +195 -1
- package/dist/chunk-KEQDYZIE.js +237 -0
- package/dist/chunk-KEQDYZIE.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3950 -642
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -0
- package/dist/ui/assets/code-block-F6WJLWQG-BTdTzfvl.js +154 -0
- package/dist/ui/assets/code-block-F6WJLWQG-BTdTzfvl.js.map +1 -0
- package/dist/ui/assets/index-CX8aDatf.css +1 -0
- package/dist/ui/assets/index-DjZuAW6Y.js +63 -0
- package/dist/ui/assets/index-DjZuAW6Y.js.map +1 -0
- package/dist/ui/assets/vendor-data-r1ZEkUds.js +40 -0
- package/dist/ui/assets/vendor-data-r1ZEkUds.js.map +1 -0
- package/dist/ui/assets/vendor-react-Cgg2GOmP.js +2 -0
- package/dist/ui/assets/vendor-react-Cgg2GOmP.js.map +1 -0
- package/dist/ui/assets/vendor-render-DoMl5bum.js +381 -0
- package/dist/ui/assets/vendor-render-DoMl5bum.js.map +1 -0
- package/dist/ui/assets/vendor-ui-Cg-YC4hK.js +46 -0
- package/dist/ui/assets/vendor-ui-Cg-YC4hK.js.map +1 -0
- package/dist/ui/index.html +18 -0
- package/dist/ui/vite.svg +1 -0
- package/package.json +14 -15
- package/dist/agent/index.js +0 -230
- package/dist/commands/index.js +0 -2
- package/dist/commands/px-fetch-more-spans.js +0 -98
- package/dist/commands/px-fetch-more-trace.js +0 -110
- package/dist/config/index.js +0 -165
- package/dist/config/loader.js +0 -141
- package/dist/config/schema.js +0 -53
- package/dist/modes/index.js +0 -17
- package/dist/modes/local.js +0 -134
- package/dist/modes/sandbox.js +0 -121
- package/dist/modes/types.js +0 -1
- package/dist/observability/index.js +0 -65
- package/dist/progress.js +0 -209
- package/dist/prompts/index.js +0 -1
- package/dist/prompts/system.js +0 -30
- package/dist/snapshot/client.js +0 -74
- package/dist/snapshot/context.js +0 -441
- package/dist/snapshot/datasets.js +0 -68
- package/dist/snapshot/experiments.js +0 -135
- package/dist/snapshot/index.js +0 -262
- package/dist/snapshot/projects.js +0 -44
- package/dist/snapshot/prompts.js +0 -199
- package/dist/snapshot/spans.js +0 -104
- package/dist/snapshot/utils.js +0 -112
- package/dist/tsconfig.esm.tsbuildinfo +0 -1
- package/src/agent/index.ts +0 -323
- package/src/cli.ts +0 -854
- package/src/commands/index.ts +0 -8
- package/src/commands/px-fetch-more-spans.ts +0 -174
- package/src/commands/px-fetch-more-trace.ts +0 -183
- package/src/config/index.ts +0 -225
- package/src/config/loader.ts +0 -173
- package/src/config/schema.ts +0 -66
- package/src/index.ts +0 -1
- package/src/modes/index.ts +0 -21
- package/src/modes/local.ts +0 -163
- package/src/modes/sandbox.ts +0 -144
- package/src/modes/types.ts +0 -31
- package/src/observability/index.ts +0 -90
- package/src/progress.ts +0 -239
- package/src/prompts/index.ts +0 -1
- package/src/prompts/system.ts +0 -31
- package/src/snapshot/client.ts +0 -129
- package/src/snapshot/context.ts +0 -587
- package/src/snapshot/datasets.ts +0 -132
- package/src/snapshot/experiments.ts +0 -246
- package/src/snapshot/index.ts +0 -403
- package/src/snapshot/projects.ts +0 -58
- package/src/snapshot/prompts.ts +0 -267
- package/src/snapshot/spans.ts +0 -163
- 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
|