@cephalization/phoenix-insight 0.2.0 → 0.3.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/README.md CHANGED
@@ -87,7 +87,10 @@ phoenix> exit
87
87
  Create or update snapshots separately from queries:
88
88
 
89
89
  ```bash
90
- # Create initial snapshot
90
+ # Create initial snapshot (explicit command)
91
+ phoenix-insight snapshot create
92
+
93
+ # Create initial snapshot (shorthand, same as 'snapshot create')
91
94
  phoenix-insight snapshot
92
95
 
93
96
  # Force refresh snapshot (ignore cache)
@@ -96,6 +99,12 @@ phoenix-insight snapshot --refresh
96
99
  # Snapshot from a specific Phoenix instance
97
100
  phoenix-insight snapshot --base-url https://phoenix.example.com --api-key your-api-key
98
101
 
102
+ # Get the path to the latest snapshot
103
+ phoenix-insight snapshot latest
104
+
105
+ # List all available snapshots
106
+ phoenix-insight snapshot list
107
+
99
108
  # Clean up local snapshots
100
109
  phoenix-insight prune
101
110
 
@@ -293,8 +302,11 @@ Creates or updates a data snapshot from Phoenix without running a query.
293
302
 
294
303
  ```bash
295
304
  phoenix-insight snapshot [options]
305
+ phoenix-insight snapshot create [options]
296
306
  ```
297
307
 
308
+ Note: `phoenix-insight snapshot` (without subcommand) is equivalent to `phoenix-insight snapshot create` for backward compatibility.
309
+
298
310
  | Option | Description | Default | Example |
299
311
  | ------------------ | --------------------------------------------- | ----------------------- | ----------------------------------------------- |
300
312
  | `--config <path>` | Custom config file path | `~/.phoenix-insight/config.json` | `--config ./my-config.json` |
@@ -304,6 +316,76 @@ phoenix-insight snapshot [options]
304
316
  | `--limit <n>` | Maximum spans to fetch per project | `1000` | `phoenix-insight snapshot --limit 5000` |
305
317
  | `--trace` | Enable tracing of snapshot operations | `false` | `phoenix-insight snapshot --trace` |
306
318
 
319
+ ### Snapshot Create Subcommand
320
+
321
+ Explicitly creates a new snapshot from Phoenix data. This is the preferred way to create snapshots.
322
+
323
+ ```bash
324
+ phoenix-insight snapshot create
325
+ ```
326
+
327
+ Same options as `phoenix-insight snapshot`. Use `snapshot create` for clarity in scripts and automation.
328
+
329
+ ### Snapshot Latest Command
330
+
331
+ Prints the absolute path to the latest snapshot directory.
332
+
333
+ ```bash
334
+ phoenix-insight snapshot latest
335
+ ```
336
+
337
+ Outputs the path to stdout with no decoration (just the path). Exit code 0 on success, exit code 1 if no snapshots exist.
338
+
339
+ **Example usage:**
340
+
341
+ ```bash
342
+ # Get the latest snapshot path
343
+ phoenix-insight snapshot latest
344
+ # Output: /Users/you/.phoenix-insight/snapshots/1704067200000-abc123/phoenix
345
+
346
+ # Use in scripts
347
+ SNAPSHOT_PATH=$(phoenix-insight snapshot latest)
348
+ ls "$SNAPSHOT_PATH"
349
+
350
+ # Check if snapshots exist
351
+ if phoenix-insight snapshot latest > /dev/null 2>&1; then
352
+ echo "Snapshots available"
353
+ else
354
+ echo "No snapshots found"
355
+ fi
356
+ ```
357
+
358
+ ### Snapshot List Command
359
+
360
+ Lists all available snapshots with their timestamps.
361
+
362
+ ```bash
363
+ phoenix-insight snapshot list
364
+ ```
365
+
366
+ Outputs one snapshot per line in the format `<timestamp> <path>` where timestamp is ISO 8601. Most recent first. Exit code 0 even if empty (just prints nothing).
367
+
368
+ **Example usage:**
369
+
370
+ ```bash
371
+ # List all snapshots
372
+ phoenix-insight snapshot list
373
+ # Output:
374
+ # 2024-01-01T12:30:00.000Z /Users/you/.phoenix-insight/snapshots/1704113400000-abc123/phoenix
375
+ # 2024-01-01T10:00:00.000Z /Users/you/.phoenix-insight/snapshots/1704104400000-def456/phoenix
376
+
377
+ # Count snapshots
378
+ phoenix-insight snapshot list | wc -l
379
+
380
+ # Get oldest snapshot path
381
+ phoenix-insight snapshot list | tail -1 | cut -d' ' -f2
382
+
383
+ # Process snapshots in a script
384
+ phoenix-insight snapshot list | while read timestamp path; do
385
+ echo "Snapshot from $timestamp at $path"
386
+ done
387
+ ```
388
+
307
389
  ### Prune Command
308
390
 
309
391
  Deletes the local snapshot directory to free up disk space.
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import * as os from "node:os";
7
7
  import { createSandboxMode, createLocalMode } from "./modes/index.js";
8
8
  import { createInsightAgent, runOneShotQuery } from "./agent/index.js";
9
9
  import { createSnapshot, createIncrementalSnapshot, createPhoenixClient, PhoenixClientError, } from "./snapshot/index.js";
10
+ import { getLatestSnapshot, listSnapshots } from "./snapshot/utils.js";
10
11
  import { AgentProgress } from "./progress.js";
11
12
  import { initializeObservability, shutdownObservability, } from "./observability/index.js";
12
13
  import { initializeConfig, getConfig } from "./config/index.js";
@@ -182,10 +183,11 @@ Examples:
182
183
  // Initialize config singleton before any command runs
183
184
  await initializeConfig(cliArgs);
184
185
  });
185
- program
186
- .command("snapshot")
187
- .description("Create a snapshot of Phoenix data")
188
- .action(async () => {
186
+ /**
187
+ * Shared logic for creating a snapshot.
188
+ * Used by both 'phoenix-insight snapshot' and 'phoenix-insight snapshot create'.
189
+ */
190
+ async function executeSnapshotCreate() {
189
191
  const config = getConfig();
190
192
  // Initialize observability if trace is enabled in config
191
193
  if (config.trace) {
@@ -216,6 +218,61 @@ program
216
218
  catch (error) {
217
219
  handleError(error, "creating snapshot");
218
220
  }
221
+ }
222
+ // Create snapshot command group
223
+ const snapshotCmd = program
224
+ .command("snapshot")
225
+ .description("Snapshot management commands");
226
+ // Default action for 'phoenix-insight snapshot' (backward compatibility alias for 'snapshot create')
227
+ snapshotCmd.action(async () => {
228
+ await executeSnapshotCreate();
229
+ });
230
+ // Subcommand: snapshot create (explicit create command)
231
+ snapshotCmd
232
+ .command("create")
233
+ .description("Create a new snapshot from Phoenix data")
234
+ .action(async () => {
235
+ await executeSnapshotCreate();
236
+ });
237
+ // Subcommand: snapshot latest
238
+ snapshotCmd
239
+ .command("latest")
240
+ .description("Print the absolute path to the latest snapshot directory")
241
+ .action(async () => {
242
+ try {
243
+ const latestSnapshot = await getLatestSnapshot();
244
+ if (!latestSnapshot) {
245
+ console.error("No snapshots found");
246
+ process.exit(1);
247
+ }
248
+ // Print only the path to stdout, no decoration
249
+ console.log(latestSnapshot.path);
250
+ }
251
+ catch (error) {
252
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
253
+ process.exit(1);
254
+ }
255
+ });
256
+ // Subcommand: snapshot list
257
+ snapshotCmd
258
+ .command("list")
259
+ .description("List all available snapshots with their timestamps")
260
+ .action(async () => {
261
+ try {
262
+ const snapshots = await listSnapshots();
263
+ // Print each snapshot: <timestamp> <path>
264
+ // Most recent first (already sorted by listSnapshots)
265
+ for (const snapshot of snapshots) {
266
+ // Format timestamp as ISO 8601
267
+ const isoTimestamp = snapshot.timestamp.toISOString();
268
+ console.log(`${isoTimestamp} ${snapshot.path}`);
269
+ }
270
+ // Exit code 0 even if empty (just print nothing)
271
+ }
272
+ catch (error) {
273
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
274
+ process.exit(1);
275
+ }
219
276
  });
220
277
  program
221
278
  .command("help")
@@ -1,16 +1,160 @@
1
+ // =============================================================================
2
+ // Static Section Templates
3
+ // =============================================================================
4
+ /**
5
+ * Quick Start section for external agents - appears at the top for discoverability
6
+ */
7
+ const QUICK_START_SECTION = `## Quick Start for External Agents
8
+
9
+ This is a **read-only snapshot** of Phoenix observability data. You cannot modify this data.
10
+
11
+ ### Key Files to Start With
12
+
13
+ | File | Description |
14
+ |------|-------------|
15
+ | \`/phoenix/projects/index.jsonl\` | List of all projects with traces |
16
+ | \`/phoenix/datasets/index.jsonl\` | List of all datasets |
17
+ | \`/phoenix/experiments/index.jsonl\` | List of all experiments |
18
+ | \`/phoenix/prompts/index.jsonl\` | List of all prompts |
19
+
20
+ ### How to Parse Each File Format
21
+
22
+ **JSONL files** (\`.jsonl\`): One JSON object per line
23
+ \`\`\`bash
24
+ # Read all lines as a JSON array
25
+ cat /phoenix/projects/index.jsonl | jq -s '.'
26
+
27
+ # Process each line individually
28
+ while read -r line; do echo "$line" | jq '.name'; done < /phoenix/projects/index.jsonl
29
+
30
+ # Get first N items
31
+ head -n 5 /phoenix/projects/index.jsonl | jq -s '.'
32
+ \`\`\`
33
+
34
+ **JSON files** (\`.json\`): Standard JSON format
35
+ \`\`\`bash
36
+ # Read and pretty-print
37
+ cat /phoenix/projects/my-project/metadata.json | jq '.'
38
+
39
+ # Extract specific field
40
+ cat /phoenix/projects/my-project/metadata.json | jq '.name'
41
+ \`\`\`
42
+
43
+ **Markdown files** (\`.md\`): Plain text prompt templates
44
+ \`\`\`bash
45
+ # Read prompt template
46
+ cat /phoenix/prompts/my-prompt/versions/v1.md
47
+ \`\`\`
48
+
49
+ ### Common Operations
50
+
51
+ \`\`\`bash
52
+ # List all project names
53
+ cat /phoenix/projects/index.jsonl | jq -r '.name'
54
+
55
+ # Count spans in a project
56
+ wc -l < /phoenix/projects/my-project/spans/index.jsonl
57
+
58
+ # Find spans with errors
59
+ cat /phoenix/projects/my-project/spans/index.jsonl | jq 'select(.status_code == "ERROR")'
60
+
61
+ # Get dataset examples
62
+ cat /phoenix/datasets/my-dataset/examples.jsonl | jq -s '.' | head -n 100
63
+
64
+ # Search across all files
65
+ grep -r "error" /phoenix/
66
+ \`\`\``;
67
+ /**
68
+ * Directory Structure section showing the snapshot layout
69
+ */
70
+ const DIRECTORY_STRUCTURE_SECTION = `## Directory Structure
71
+
72
+ \`\`\`
73
+ /phoenix/
74
+ _context.md # This file - start here!
75
+ /projects/
76
+ index.jsonl # List of all projects
77
+ /{project_name}/
78
+ metadata.json # Project details
79
+ /spans/
80
+ index.jsonl # Span data (may be sampled)
81
+ metadata.json # Span snapshot metadata
82
+ /datasets/
83
+ index.jsonl # List of all datasets
84
+ /{dataset_name}/
85
+ metadata.json # Dataset details
86
+ examples.jsonl # Dataset examples
87
+ /experiments/
88
+ index.jsonl # List of all experiments
89
+ /{experiment_id}/
90
+ metadata.json # Experiment details
91
+ runs.jsonl # Experiment runs
92
+ /prompts/
93
+ index.jsonl # List of all prompts
94
+ /{prompt_name}/
95
+ metadata.json # Prompt details
96
+ /versions/
97
+ index.jsonl # Version list
98
+ /{version_id}.md # Version template
99
+ /_meta/
100
+ snapshot.json # Snapshot metadata
101
+ \`\`\``;
102
+ /**
103
+ * What You Can Do section describing available operations
104
+ */
105
+ const WHAT_YOU_CAN_DO_SECTION = `## What You Can Do
106
+
107
+ - **Explore**: ls, cat, grep, find, jq, awk, sed
108
+ - **Fetch more data**: \`px-fetch-more spans --project <name> --limit 500\`
109
+ - **Fetch specific trace**: \`px-fetch-more trace --trace-id <id>\``;
110
+ /**
111
+ * Data Freshness section with refresh instructions
112
+ */
113
+ const DATA_FRESHNESS_SECTION = `## Data Freshness
114
+
115
+ This is a **read-only snapshot**. Data may have changed since capture.
116
+ Run with \`--refresh\` to get latest data.`;
117
+ // =============================================================================
118
+ // Main Context Generation
119
+ // =============================================================================
1
120
  /**
2
121
  * Generates a _context.md summary file for the Phoenix snapshot
3
122
  * This provides human and agent-readable context about what data is available
4
123
  */
5
124
  export async function generateContext(mode, metadata) {
6
- const lines = [];
7
- // Header
8
- lines.push("# Phoenix Snapshot Context");
9
- lines.push("");
10
125
  // Collect stats from the snapshot
11
126
  const stats = await collectSnapshotStats(mode);
12
- // What's Here section
127
+ // Build the dynamic "What's Here" section
128
+ const whatsHereSection = buildWhatsHereSection(stats, metadata);
129
+ // Build the dynamic "Recent Activity" section (may be empty)
130
+ const recentActivitySection = buildRecentActivitySection(stats);
131
+ // Compose the full context document
132
+ const content = [
133
+ "# Phoenix Snapshot Context",
134
+ "",
135
+ QUICK_START_SECTION,
136
+ "",
137
+ whatsHereSection,
138
+ recentActivitySection,
139
+ DIRECTORY_STRUCTURE_SECTION,
140
+ "",
141
+ WHAT_YOU_CAN_DO_SECTION,
142
+ "",
143
+ DATA_FRESHNESS_SECTION,
144
+ ].join("\n");
145
+ // Write the context file
146
+ await mode.writeFile("/phoenix/_context.md", content);
147
+ }
148
+ // =============================================================================
149
+ // Dynamic Section Builders
150
+ // =============================================================================
151
+ /**
152
+ * Builds the "What's Here" section with project/dataset/experiment/prompt summaries
153
+ */
154
+ function buildWhatsHereSection(stats, metadata) {
155
+ const lines = [];
13
156
  lines.push("## What's Here");
157
+ lines.push("");
14
158
  // Projects summary
15
159
  if (stats.projects.length > 0) {
16
160
  const projectSummary = stats.projects
@@ -57,67 +201,29 @@ export async function generateContext(mode, metadata) {
57
201
  // Snapshot metadata
58
202
  lines.push(`- **Snapshot**: Created ${formatRelativeTime(metadata.snapshotTime)} from ${metadata.phoenixUrl}`);
59
203
  lines.push("");
60
- // Recent Activity section (if we have recent data)
61
- const recentActivity = getRecentActivity(stats);
62
- if (recentActivity.length > 0) {
63
- lines.push("## Recent Activity");
64
- for (const activity of recentActivity) {
65
- lines.push(`- ${activity}`);
66
- }
67
- lines.push("");
204
+ return lines.join("\n");
205
+ }
206
+ /**
207
+ * Builds the "Recent Activity" section if there are recent updates
208
+ * Returns an empty string if no recent activity
209
+ */
210
+ function buildRecentActivitySection(stats) {
211
+ const activities = getRecentActivity(stats);
212
+ if (activities.length === 0) {
213
+ return "";
68
214
  }
69
- // What You Can Do section
70
- lines.push("## What You Can Do");
71
- lines.push("- **Explore**: ls, cat, grep, find, jq, awk, sed");
72
- lines.push("- **Fetch more data**: `px-fetch-more spans --project <name> --limit 500`");
73
- lines.push("- **Fetch specific trace**: `px-fetch-more trace --trace-id <id>`");
74
- lines.push("");
75
- // Data Freshness section
76
- lines.push("## Data Freshness");
77
- lines.push("This is a **read-only snapshot**. Data may have changed since capture.");
78
- lines.push("Run with `--refresh` to get latest data.");
215
+ const lines = [];
216
+ lines.push("## Recent Activity");
79
217
  lines.push("");
80
- // File Formats section
81
- lines.push("## File Formats");
82
- lines.push("- `.jsonl` files: One JSON object per line, use `jq -s` to parse as array");
83
- lines.push("- `.json` files: Standard JSON");
84
- lines.push("- `.md` files: Markdown (prompt templates)");
218
+ for (const activity of activities) {
219
+ lines.push(`- ${activity}`);
220
+ }
85
221
  lines.push("");
86
- // Directory Structure section
87
- lines.push("## Directory Structure");
88
- lines.push("```");
89
- lines.push("/phoenix/");
90
- lines.push(" _context.md # This file");
91
- lines.push(" /projects/");
92
- lines.push(" index.jsonl # List of all projects");
93
- lines.push(" /{project_name}/");
94
- lines.push(" metadata.json # Project details");
95
- lines.push(" /spans/");
96
- lines.push(" index.jsonl # Span data (may be sampled)");
97
- lines.push(" metadata.json # Span snapshot metadata");
98
- lines.push(" /datasets/");
99
- lines.push(" index.jsonl # List of all datasets");
100
- lines.push(" /{dataset_name}/");
101
- lines.push(" metadata.json # Dataset details");
102
- lines.push(" examples.jsonl # Dataset examples");
103
- lines.push(" /experiments/");
104
- lines.push(" index.jsonl # List of all experiments");
105
- lines.push(" /{experiment_id}/");
106
- lines.push(" metadata.json # Experiment details");
107
- lines.push(" runs.jsonl # Experiment runs");
108
- lines.push(" /prompts/");
109
- lines.push(" index.jsonl # List of all prompts");
110
- lines.push(" /{prompt_name}/");
111
- lines.push(" metadata.json # Prompt details");
112
- lines.push(" /versions/");
113
- lines.push(" index.jsonl # Version list");
114
- lines.push(" /{version_id}.md # Version template");
115
- lines.push(" /_meta/");
116
- lines.push(" snapshot.json # Snapshot metadata");
117
- lines.push("```");
118
- // Write the context file
119
- await mode.writeFile("/phoenix/_context.md", lines.join("\n"));
222
+ return lines.join("\n");
120
223
  }
224
+ // =============================================================================
225
+ // Data Collection
226
+ // =============================================================================
121
227
  /**
122
228
  * Collects statistics from the snapshot filesystem
123
229
  */
@@ -259,6 +365,9 @@ async function collectSnapshotStats(mode) {
259
365
  }
260
366
  return result;
261
367
  }
368
+ // =============================================================================
369
+ // Helper Functions
370
+ // =============================================================================
262
371
  /**
263
372
  * Determines the status of an experiment based on its run counts
264
373
  */
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Snapshot discovery utilities
3
+ *
4
+ * Functions for listing and finding snapshots in the local filesystem.
5
+ */
6
+ import * as fs from "node:fs/promises";
7
+ import * as path from "node:path";
8
+ import * as os from "node:os";
9
+ /**
10
+ * Get the base snapshots directory path
11
+ */
12
+ export function getSnapshotsDir() {
13
+ return path.join(os.homedir(), ".phoenix-insight", "snapshots");
14
+ }
15
+ /**
16
+ * Parse a snapshot directory name to extract timestamp
17
+ *
18
+ * Directory names are in format: `<timestamp>-<random>` where timestamp is Date.now()
19
+ * Example: "1704067200000-abc123" -> Date(2024-01-01T00:00:00.000Z)
20
+ *
21
+ * @param dirName - The directory name to parse
22
+ * @returns The parsed timestamp as Date, or null if invalid
23
+ */
24
+ function parseSnapshotDirName(dirName) {
25
+ // Format: <timestamp>-<random>
26
+ const match = dirName.match(/^(\d+)-[\w]+$/);
27
+ if (!match || !match[1]) {
28
+ return null;
29
+ }
30
+ const timestamp = parseInt(match[1], 10);
31
+ if (isNaN(timestamp) || timestamp <= 0) {
32
+ return null;
33
+ }
34
+ const date = new Date(timestamp);
35
+ // Validate the date is reasonable (between year 2000 and year 3000)
36
+ // Use UTC year to avoid timezone issues
37
+ const year = date.getUTCFullYear();
38
+ if (year < 2000 || year > 3000) {
39
+ return null;
40
+ }
41
+ return date;
42
+ }
43
+ /**
44
+ * List all available snapshots
45
+ *
46
+ * Scans the snapshots directory and returns information about each valid snapshot.
47
+ * Results are sorted by timestamp descending (most recent first).
48
+ *
49
+ * @returns Array of snapshot info objects, sorted by timestamp descending
50
+ */
51
+ export async function listSnapshots() {
52
+ const snapshotsDir = getSnapshotsDir();
53
+ // Check if snapshots directory exists
54
+ try {
55
+ await fs.access(snapshotsDir);
56
+ }
57
+ catch {
58
+ // Directory doesn't exist - return empty array
59
+ return [];
60
+ }
61
+ // Read directory contents
62
+ let entries;
63
+ try {
64
+ entries = await fs.readdir(snapshotsDir);
65
+ }
66
+ catch {
67
+ // Cannot read directory - return empty array
68
+ return [];
69
+ }
70
+ // Filter and parse valid snapshot directories
71
+ const snapshots = [];
72
+ for (const entry of entries) {
73
+ const timestamp = parseSnapshotDirName(entry);
74
+ if (!timestamp) {
75
+ // Invalid directory name format - skip
76
+ continue;
77
+ }
78
+ const snapshotPath = path.join(snapshotsDir, entry, "phoenix");
79
+ // Verify the phoenix subdirectory exists
80
+ try {
81
+ const stat = await fs.stat(snapshotPath);
82
+ if (!stat.isDirectory()) {
83
+ continue;
84
+ }
85
+ }
86
+ catch {
87
+ // Phoenix subdirectory doesn't exist or can't be accessed - skip
88
+ continue;
89
+ }
90
+ snapshots.push({
91
+ path: snapshotPath,
92
+ timestamp,
93
+ id: entry,
94
+ });
95
+ }
96
+ // Sort by timestamp descending (most recent first)
97
+ snapshots.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
98
+ return snapshots;
99
+ }
100
+ /**
101
+ * Get the latest (most recent) snapshot
102
+ *
103
+ * @returns The most recent snapshot info, or null if no snapshots exist
104
+ */
105
+ export async function getLatestSnapshot() {
106
+ const snapshots = await listSnapshots();
107
+ if (snapshots.length === 0) {
108
+ return null;
109
+ }
110
+ // First element is the most recent due to descending sort
111
+ return snapshots[0] ?? null;
112
+ }
@@ -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"],"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/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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cephalization/phoenix-insight",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A CLI for Arize AI Phoenix data analysis with AI agents",
5
5
  "type": "module",
6
6
  "exports": {
package/src/cli.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  createPhoenixClient,
14
14
  PhoenixClientError,
15
15
  } from "./snapshot/index.js";
16
+ import { getLatestSnapshot, listSnapshots } from "./snapshot/utils.js";
16
17
  import type { ExecutionMode } from "./modes/types.js";
17
18
  import type { PhoenixInsightAgentConfig } from "./agent/index.js";
18
19
  import { AgentProgress } from "./progress.js";
@@ -218,44 +219,111 @@ Examples:
218
219
  await initializeConfig(cliArgs);
219
220
  });
220
221
 
221
- program
222
+ /**
223
+ * Shared logic for creating a snapshot.
224
+ * Used by both 'phoenix-insight snapshot' and 'phoenix-insight snapshot create'.
225
+ */
226
+ async function executeSnapshotCreate(): Promise<void> {
227
+ const config = getConfig();
228
+
229
+ // Initialize observability if trace is enabled in config
230
+ if (config.trace) {
231
+ initializeObservability({
232
+ enabled: true,
233
+ baseUrl: config.baseUrl,
234
+ apiKey: config.apiKey,
235
+ projectName: "phoenix-insight-snapshot",
236
+ debug: !!process.env.DEBUG,
237
+ });
238
+ }
239
+
240
+ try {
241
+ // Determine the execution mode
242
+ const mode: ExecutionMode = await createLocalMode();
243
+
244
+ // Create snapshot with config values
245
+ const snapshotOptions = {
246
+ baseURL: config.baseUrl,
247
+ apiKey: config.apiKey,
248
+ spansPerProject: config.limit,
249
+ showProgress: true,
250
+ };
251
+
252
+ await createSnapshot(mode, snapshotOptions);
253
+
254
+ // Cleanup
255
+ await mode.cleanup();
256
+
257
+ // Shutdown observability if enabled
258
+ await shutdownObservability();
259
+ } catch (error) {
260
+ handleError(error, "creating snapshot");
261
+ }
262
+ }
263
+
264
+ // Create snapshot command group
265
+ const snapshotCmd = program
222
266
  .command("snapshot")
223
- .description("Create a snapshot of Phoenix data")
224
- .action(async () => {
225
- const config = getConfig();
267
+ .description("Snapshot management commands");
226
268
 
227
- // Initialize observability if trace is enabled in config
228
- if (config.trace) {
229
- initializeObservability({
230
- enabled: true,
231
- baseUrl: config.baseUrl,
232
- apiKey: config.apiKey,
233
- projectName: "phoenix-insight-snapshot",
234
- debug: !!process.env.DEBUG,
235
- });
236
- }
269
+ // Default action for 'phoenix-insight snapshot' (backward compatibility alias for 'snapshot create')
270
+ snapshotCmd.action(async () => {
271
+ await executeSnapshotCreate();
272
+ });
237
273
 
274
+ // Subcommand: snapshot create (explicit create command)
275
+ snapshotCmd
276
+ .command("create")
277
+ .description("Create a new snapshot from Phoenix data")
278
+ .action(async () => {
279
+ await executeSnapshotCreate();
280
+ });
281
+
282
+ // Subcommand: snapshot latest
283
+ snapshotCmd
284
+ .command("latest")
285
+ .description("Print the absolute path to the latest snapshot directory")
286
+ .action(async () => {
238
287
  try {
239
- // Determine the execution mode
240
- const mode: ExecutionMode = await createLocalMode();
288
+ const latestSnapshot = await getLatestSnapshot();
241
289
 
242
- // Create snapshot with config values
243
- const snapshotOptions = {
244
- baseURL: config.baseUrl,
245
- apiKey: config.apiKey,
246
- spansPerProject: config.limit,
247
- showProgress: true,
248
- };
290
+ if (!latestSnapshot) {
291
+ console.error("No snapshots found");
292
+ process.exit(1);
293
+ }
249
294
 
250
- await createSnapshot(mode, snapshotOptions);
295
+ // Print only the path to stdout, no decoration
296
+ console.log(latestSnapshot.path);
297
+ } catch (error) {
298
+ console.error(
299
+ `Error: ${error instanceof Error ? error.message : String(error)}`
300
+ );
301
+ process.exit(1);
302
+ }
303
+ });
251
304
 
252
- // Cleanup
253
- await mode.cleanup();
305
+ // Subcommand: snapshot list
306
+ snapshotCmd
307
+ .command("list")
308
+ .description("List all available snapshots with their timestamps")
309
+ .action(async () => {
310
+ try {
311
+ const snapshots = await listSnapshots();
312
+
313
+ // Print each snapshot: <timestamp> <path>
314
+ // Most recent first (already sorted by listSnapshots)
315
+ for (const snapshot of snapshots) {
316
+ // Format timestamp as ISO 8601
317
+ const isoTimestamp = snapshot.timestamp.toISOString();
318
+ console.log(`${isoTimestamp} ${snapshot.path}`);
319
+ }
254
320
 
255
- // Shutdown observability if enabled
256
- await shutdownObservability();
321
+ // Exit code 0 even if empty (just print nothing)
257
322
  } catch (error) {
258
- handleError(error, "creating snapshot");
323
+ console.error(
324
+ `Error: ${error instanceof Error ? error.message : String(error)}`
325
+ );
326
+ process.exit(1);
259
327
  }
260
328
  });
261
329
 
@@ -39,6 +39,131 @@ interface PromptInfo {
39
39
  updatedAt?: string;
40
40
  }
41
41
 
42
+ // =============================================================================
43
+ // Static Section Templates
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Quick Start section for external agents - appears at the top for discoverability
48
+ */
49
+ const QUICK_START_SECTION = `## Quick Start for External Agents
50
+
51
+ This is a **read-only snapshot** of Phoenix observability data. You cannot modify this data.
52
+
53
+ ### Key Files to Start With
54
+
55
+ | File | Description |
56
+ |------|-------------|
57
+ | \`/phoenix/projects/index.jsonl\` | List of all projects with traces |
58
+ | \`/phoenix/datasets/index.jsonl\` | List of all datasets |
59
+ | \`/phoenix/experiments/index.jsonl\` | List of all experiments |
60
+ | \`/phoenix/prompts/index.jsonl\` | List of all prompts |
61
+
62
+ ### How to Parse Each File Format
63
+
64
+ **JSONL files** (\`.jsonl\`): One JSON object per line
65
+ \`\`\`bash
66
+ # Read all lines as a JSON array
67
+ cat /phoenix/projects/index.jsonl | jq -s '.'
68
+
69
+ # Process each line individually
70
+ while read -r line; do echo "$line" | jq '.name'; done < /phoenix/projects/index.jsonl
71
+
72
+ # Get first N items
73
+ head -n 5 /phoenix/projects/index.jsonl | jq -s '.'
74
+ \`\`\`
75
+
76
+ **JSON files** (\`.json\`): Standard JSON format
77
+ \`\`\`bash
78
+ # Read and pretty-print
79
+ cat /phoenix/projects/my-project/metadata.json | jq '.'
80
+
81
+ # Extract specific field
82
+ cat /phoenix/projects/my-project/metadata.json | jq '.name'
83
+ \`\`\`
84
+
85
+ **Markdown files** (\`.md\`): Plain text prompt templates
86
+ \`\`\`bash
87
+ # Read prompt template
88
+ cat /phoenix/prompts/my-prompt/versions/v1.md
89
+ \`\`\`
90
+
91
+ ### Common Operations
92
+
93
+ \`\`\`bash
94
+ # List all project names
95
+ cat /phoenix/projects/index.jsonl | jq -r '.name'
96
+
97
+ # Count spans in a project
98
+ wc -l < /phoenix/projects/my-project/spans/index.jsonl
99
+
100
+ # Find spans with errors
101
+ cat /phoenix/projects/my-project/spans/index.jsonl | jq 'select(.status_code == "ERROR")'
102
+
103
+ # Get dataset examples
104
+ cat /phoenix/datasets/my-dataset/examples.jsonl | jq -s '.' | head -n 100
105
+
106
+ # Search across all files
107
+ grep -r "error" /phoenix/
108
+ \`\`\``;
109
+
110
+ /**
111
+ * Directory Structure section showing the snapshot layout
112
+ */
113
+ const DIRECTORY_STRUCTURE_SECTION = `## Directory Structure
114
+
115
+ \`\`\`
116
+ /phoenix/
117
+ _context.md # This file - start here!
118
+ /projects/
119
+ index.jsonl # List of all projects
120
+ /{project_name}/
121
+ metadata.json # Project details
122
+ /spans/
123
+ index.jsonl # Span data (may be sampled)
124
+ metadata.json # Span snapshot metadata
125
+ /datasets/
126
+ index.jsonl # List of all datasets
127
+ /{dataset_name}/
128
+ metadata.json # Dataset details
129
+ examples.jsonl # Dataset examples
130
+ /experiments/
131
+ index.jsonl # List of all experiments
132
+ /{experiment_id}/
133
+ metadata.json # Experiment details
134
+ runs.jsonl # Experiment runs
135
+ /prompts/
136
+ index.jsonl # List of all prompts
137
+ /{prompt_name}/
138
+ metadata.json # Prompt details
139
+ /versions/
140
+ index.jsonl # Version list
141
+ /{version_id}.md # Version template
142
+ /_meta/
143
+ snapshot.json # Snapshot metadata
144
+ \`\`\``;
145
+
146
+ /**
147
+ * What You Can Do section describing available operations
148
+ */
149
+ const WHAT_YOU_CAN_DO_SECTION = `## What You Can Do
150
+
151
+ - **Explore**: ls, cat, grep, find, jq, awk, sed
152
+ - **Fetch more data**: \`px-fetch-more spans --project <name> --limit 500\`
153
+ - **Fetch specific trace**: \`px-fetch-more trace --trace-id <id>\``;
154
+
155
+ /**
156
+ * Data Freshness section with refresh instructions
157
+ */
158
+ const DATA_FRESHNESS_SECTION = `## Data Freshness
159
+
160
+ This is a **read-only snapshot**. Data may have changed since capture.
161
+ Run with \`--refresh\` to get latest data.`;
162
+
163
+ // =============================================================================
164
+ // Main Context Generation
165
+ // =============================================================================
166
+
42
167
  /**
43
168
  * Generates a _context.md summary file for the Phoenix snapshot
44
169
  * This provides human and agent-readable context about what data is available
@@ -47,17 +172,54 @@ export async function generateContext(
47
172
  mode: ExecutionMode,
48
173
  metadata: ContextMetadata
49
174
  ): Promise<void> {
50
- const lines: string[] = [];
51
-
52
- // Header
53
- lines.push("# Phoenix Snapshot Context");
54
- lines.push("");
55
-
56
175
  // Collect stats from the snapshot
57
176
  const stats = await collectSnapshotStats(mode);
58
177
 
59
- // What's Here section
178
+ // Build the dynamic "What's Here" section
179
+ const whatsHereSection = buildWhatsHereSection(stats, metadata);
180
+
181
+ // Build the dynamic "Recent Activity" section (may be empty)
182
+ const recentActivitySection = buildRecentActivitySection(stats);
183
+
184
+ // Compose the full context document
185
+ const content = [
186
+ "# Phoenix Snapshot Context",
187
+ "",
188
+ QUICK_START_SECTION,
189
+ "",
190
+ whatsHereSection,
191
+ recentActivitySection,
192
+ DIRECTORY_STRUCTURE_SECTION,
193
+ "",
194
+ WHAT_YOU_CAN_DO_SECTION,
195
+ "",
196
+ DATA_FRESHNESS_SECTION,
197
+ ].join("\n");
198
+
199
+ // Write the context file
200
+ await mode.writeFile("/phoenix/_context.md", content);
201
+ }
202
+
203
+ // =============================================================================
204
+ // Dynamic Section Builders
205
+ // =============================================================================
206
+
207
+ /**
208
+ * Builds the "What's Here" section with project/dataset/experiment/prompt summaries
209
+ */
210
+ function buildWhatsHereSection(
211
+ stats: {
212
+ projects: ProjectStats[];
213
+ datasets: DatasetInfo[];
214
+ experiments: ExperimentInfo[];
215
+ prompts: PromptInfo[];
216
+ },
217
+ metadata: ContextMetadata
218
+ ): string {
219
+ const lines: string[] = [];
220
+
60
221
  lines.push("## What's Here");
222
+ lines.push("");
61
223
 
62
224
  // Projects summary
63
225
  if (stats.projects.length > 0) {
@@ -115,81 +277,40 @@ export async function generateContext(
115
277
  );
116
278
  lines.push("");
117
279
 
118
- // Recent Activity section (if we have recent data)
119
- const recentActivity = getRecentActivity(stats);
120
- if (recentActivity.length > 0) {
121
- lines.push("## Recent Activity");
122
- for (const activity of recentActivity) {
123
- lines.push(`- ${activity}`);
124
- }
125
- lines.push("");
126
- }
280
+ return lines.join("\n");
281
+ }
127
282
 
128
- // What You Can Do section
129
- lines.push("## What You Can Do");
130
- lines.push("- **Explore**: ls, cat, grep, find, jq, awk, sed");
131
- lines.push(
132
- "- **Fetch more data**: `px-fetch-more spans --project <name> --limit 500`"
133
- );
134
- lines.push(
135
- "- **Fetch specific trace**: `px-fetch-more trace --trace-id <id>`"
136
- );
137
- lines.push("");
283
+ /**
284
+ * Builds the "Recent Activity" section if there are recent updates
285
+ * Returns an empty string if no recent activity
286
+ */
287
+ function buildRecentActivitySection(stats: {
288
+ projects: ProjectStats[];
289
+ datasets: DatasetInfo[];
290
+ experiments: ExperimentInfo[];
291
+ prompts: PromptInfo[];
292
+ }): string {
293
+ const activities = getRecentActivity(stats);
138
294
 
139
- // Data Freshness section
140
- lines.push("## Data Freshness");
141
- lines.push(
142
- "This is a **read-only snapshot**. Data may have changed since capture."
143
- );
144
- lines.push("Run with `--refresh` to get latest data.");
145
- lines.push("");
295
+ if (activities.length === 0) {
296
+ return "";
297
+ }
146
298
 
147
- // File Formats section
148
- lines.push("## File Formats");
149
- lines.push(
150
- "- `.jsonl` files: One JSON object per line, use `jq -s` to parse as array"
151
- );
152
- lines.push("- `.json` files: Standard JSON");
153
- lines.push("- `.md` files: Markdown (prompt templates)");
299
+ const lines: string[] = [];
300
+ lines.push("## Recent Activity");
301
+ lines.push("");
302
+ for (const activity of activities) {
303
+ lines.push(`- ${activity}`);
304
+ }
154
305
  lines.push("");
155
306
 
156
- // Directory Structure section
157
- lines.push("## Directory Structure");
158
- lines.push("```");
159
- lines.push("/phoenix/");
160
- lines.push(" _context.md # This file");
161
- lines.push(" /projects/");
162
- lines.push(" index.jsonl # List of all projects");
163
- lines.push(" /{project_name}/");
164
- lines.push(" metadata.json # Project details");
165
- lines.push(" /spans/");
166
- lines.push(" index.jsonl # Span data (may be sampled)");
167
- lines.push(" metadata.json # Span snapshot metadata");
168
- lines.push(" /datasets/");
169
- lines.push(" index.jsonl # List of all datasets");
170
- lines.push(" /{dataset_name}/");
171
- lines.push(" metadata.json # Dataset details");
172
- lines.push(" examples.jsonl # Dataset examples");
173
- lines.push(" /experiments/");
174
- lines.push(" index.jsonl # List of all experiments");
175
- lines.push(" /{experiment_id}/");
176
- lines.push(" metadata.json # Experiment details");
177
- lines.push(" runs.jsonl # Experiment runs");
178
- lines.push(" /prompts/");
179
- lines.push(" index.jsonl # List of all prompts");
180
- lines.push(" /{prompt_name}/");
181
- lines.push(" metadata.json # Prompt details");
182
- lines.push(" /versions/");
183
- lines.push(" index.jsonl # Version list");
184
- lines.push(" /{version_id}.md # Version template");
185
- lines.push(" /_meta/");
186
- lines.push(" snapshot.json # Snapshot metadata");
187
- lines.push("```");
188
-
189
- // Write the context file
190
- await mode.writeFile("/phoenix/_context.md", lines.join("\n"));
307
+ return lines.join("\n");
191
308
  }
192
309
 
310
+ // =============================================================================
311
+ // Data Collection
312
+ // =============================================================================
313
+
193
314
  /**
194
315
  * Collects statistics from the snapshot filesystem
195
316
  */
@@ -358,6 +479,10 @@ async function collectSnapshotStats(mode: ExecutionMode): Promise<{
358
479
  return result;
359
480
  }
360
481
 
482
+ // =============================================================================
483
+ // Helper Functions
484
+ // =============================================================================
485
+
361
486
  /**
362
487
  * Determines the status of an experiment based on its run counts
363
488
  */
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Snapshot discovery utilities
3
+ *
4
+ * Functions for listing and finding snapshots in the local filesystem.
5
+ */
6
+
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+ import * as os from "node:os";
10
+
11
+ /**
12
+ * Information about a single snapshot
13
+ */
14
+ export interface SnapshotInfo {
15
+ /** Absolute path to the snapshot directory (the 'phoenix' subdirectory) */
16
+ path: string;
17
+ /** Timestamp when the snapshot was created (from directory name) */
18
+ timestamp: Date;
19
+ /** Unique identifier for the snapshot (directory name) */
20
+ id: string;
21
+ }
22
+
23
+ /**
24
+ * Get the base snapshots directory path
25
+ */
26
+ export function getSnapshotsDir(): string {
27
+ return path.join(os.homedir(), ".phoenix-insight", "snapshots");
28
+ }
29
+
30
+ /**
31
+ * Parse a snapshot directory name to extract timestamp
32
+ *
33
+ * Directory names are in format: `<timestamp>-<random>` where timestamp is Date.now()
34
+ * Example: "1704067200000-abc123" -> Date(2024-01-01T00:00:00.000Z)
35
+ *
36
+ * @param dirName - The directory name to parse
37
+ * @returns The parsed timestamp as Date, or null if invalid
38
+ */
39
+ function parseSnapshotDirName(dirName: string): Date | null {
40
+ // Format: <timestamp>-<random>
41
+ const match = dirName.match(/^(\d+)-[\w]+$/);
42
+ if (!match || !match[1]) {
43
+ return null;
44
+ }
45
+
46
+ const timestamp = parseInt(match[1], 10);
47
+ if (isNaN(timestamp) || timestamp <= 0) {
48
+ return null;
49
+ }
50
+
51
+ const date = new Date(timestamp);
52
+ // Validate the date is reasonable (between year 2000 and year 3000)
53
+ // Use UTC year to avoid timezone issues
54
+ const year = date.getUTCFullYear();
55
+ if (year < 2000 || year > 3000) {
56
+ return null;
57
+ }
58
+
59
+ return date;
60
+ }
61
+
62
+ /**
63
+ * List all available snapshots
64
+ *
65
+ * Scans the snapshots directory and returns information about each valid snapshot.
66
+ * Results are sorted by timestamp descending (most recent first).
67
+ *
68
+ * @returns Array of snapshot info objects, sorted by timestamp descending
69
+ */
70
+ export async function listSnapshots(): Promise<SnapshotInfo[]> {
71
+ const snapshotsDir = getSnapshotsDir();
72
+
73
+ // Check if snapshots directory exists
74
+ try {
75
+ await fs.access(snapshotsDir);
76
+ } catch {
77
+ // Directory doesn't exist - return empty array
78
+ return [];
79
+ }
80
+
81
+ // Read directory contents
82
+ let entries: string[];
83
+ try {
84
+ entries = await fs.readdir(snapshotsDir);
85
+ } catch {
86
+ // Cannot read directory - return empty array
87
+ return [];
88
+ }
89
+
90
+ // Filter and parse valid snapshot directories
91
+ const snapshots: SnapshotInfo[] = [];
92
+
93
+ for (const entry of entries) {
94
+ const timestamp = parseSnapshotDirName(entry);
95
+ if (!timestamp) {
96
+ // Invalid directory name format - skip
97
+ continue;
98
+ }
99
+
100
+ const snapshotPath = path.join(snapshotsDir, entry, "phoenix");
101
+
102
+ // Verify the phoenix subdirectory exists
103
+ try {
104
+ const stat = await fs.stat(snapshotPath);
105
+ if (!stat.isDirectory()) {
106
+ continue;
107
+ }
108
+ } catch {
109
+ // Phoenix subdirectory doesn't exist or can't be accessed - skip
110
+ continue;
111
+ }
112
+
113
+ snapshots.push({
114
+ path: snapshotPath,
115
+ timestamp,
116
+ id: entry,
117
+ });
118
+ }
119
+
120
+ // Sort by timestamp descending (most recent first)
121
+ snapshots.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
122
+
123
+ return snapshots;
124
+ }
125
+
126
+ /**
127
+ * Get the latest (most recent) snapshot
128
+ *
129
+ * @returns The most recent snapshot info, or null if no snapshots exist
130
+ */
131
+ export async function getLatestSnapshot(): Promise<SnapshotInfo | null> {
132
+ const snapshots = await listSnapshots();
133
+
134
+ if (snapshots.length === 0) {
135
+ return null;
136
+ }
137
+
138
+ // First element is the most recent due to descending sort
139
+ return snapshots[0] ?? null;
140
+ }