@cephalization/phoenix-insight 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +620 -0
  3. package/dist/agent/index.js +230 -0
  4. package/dist/cli.js +640 -0
  5. package/dist/commands/index.js +2 -0
  6. package/dist/commands/px-fetch-more-spans.js +98 -0
  7. package/dist/commands/px-fetch-more-trace.js +110 -0
  8. package/dist/config/index.js +165 -0
  9. package/dist/config/loader.js +141 -0
  10. package/dist/config/schema.js +53 -0
  11. package/dist/index.js +1 -0
  12. package/dist/modes/index.js +17 -0
  13. package/dist/modes/local.js +134 -0
  14. package/dist/modes/sandbox.js +121 -0
  15. package/dist/modes/types.js +1 -0
  16. package/dist/observability/index.js +65 -0
  17. package/dist/progress.js +209 -0
  18. package/dist/prompts/index.js +1 -0
  19. package/dist/prompts/system.js +30 -0
  20. package/dist/snapshot/client.js +74 -0
  21. package/dist/snapshot/context.js +332 -0
  22. package/dist/snapshot/datasets.js +68 -0
  23. package/dist/snapshot/experiments.js +135 -0
  24. package/dist/snapshot/index.js +262 -0
  25. package/dist/snapshot/projects.js +44 -0
  26. package/dist/snapshot/prompts.js +199 -0
  27. package/dist/snapshot/spans.js +80 -0
  28. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  29. package/package.json +75 -0
  30. package/src/agent/index.ts +323 -0
  31. package/src/cli.ts +782 -0
  32. package/src/commands/index.ts +8 -0
  33. package/src/commands/px-fetch-more-spans.ts +174 -0
  34. package/src/commands/px-fetch-more-trace.ts +183 -0
  35. package/src/config/index.ts +225 -0
  36. package/src/config/loader.ts +173 -0
  37. package/src/config/schema.ts +66 -0
  38. package/src/index.ts +1 -0
  39. package/src/modes/index.ts +21 -0
  40. package/src/modes/local.ts +163 -0
  41. package/src/modes/sandbox.ts +144 -0
  42. package/src/modes/types.ts +31 -0
  43. package/src/observability/index.ts +90 -0
  44. package/src/progress.ts +239 -0
  45. package/src/prompts/index.ts +1 -0
  46. package/src/prompts/system.ts +31 -0
  47. package/src/snapshot/client.ts +129 -0
  48. package/src/snapshot/context.ts +462 -0
  49. package/src/snapshot/datasets.ts +132 -0
  50. package/src/snapshot/experiments.ts +246 -0
  51. package/src/snapshot/index.ts +403 -0
  52. package/src/snapshot/projects.ts +58 -0
  53. package/src/snapshot/prompts.ts +267 -0
  54. package/src/snapshot/spans.ts +142 -0
package/src/cli.ts ADDED
@@ -0,0 +1,782 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import * as readline from "node:readline";
5
+ import * as fs from "node:fs/promises";
6
+ import * as path from "node:path";
7
+ import * as os from "node:os";
8
+ import { createSandboxMode, createLocalMode } from "./modes/index.js";
9
+ import { createInsightAgent, runOneShotQuery } from "./agent/index.js";
10
+ import {
11
+ createSnapshot,
12
+ createIncrementalSnapshot,
13
+ createPhoenixClient,
14
+ PhoenixClientError,
15
+ } from "./snapshot/index.js";
16
+ import type { ExecutionMode } from "./modes/types.js";
17
+ import type { PhoenixInsightAgentConfig } from "./agent/index.js";
18
+ import { AgentProgress } from "./progress.js";
19
+ import {
20
+ initializeObservability,
21
+ shutdownObservability,
22
+ } from "./observability/index.js";
23
+ import { initializeConfig, getConfig, type CliArgs } from "./config/index.js";
24
+
25
+ // Version will be read from package.json during build
26
+ const VERSION = "0.0.1";
27
+
28
+ const program = new Command();
29
+
30
+ /**
31
+ * Format bash command for display in progress indicator
32
+ */
33
+ function formatBashCommand(command: string): string {
34
+ if (!command) return "";
35
+
36
+ // Split by newline and get first line
37
+ const lines = command.split("\n");
38
+ const firstLine = lines[0]?.trim() || "";
39
+
40
+ // Check for pipeline first (3+ commands)
41
+ if (firstLine.includes(" | ") && firstLine.split(" | ").length > 2) {
42
+ const parts = firstLine.split(" | ");
43
+ const firstCmd = parts[0]?.split(" ")[0] || "";
44
+ const lastCmd = parts[parts.length - 1]?.split(" ")[0] || "";
45
+ return `${firstCmd} | ... | ${lastCmd}`;
46
+ }
47
+
48
+ // Common command patterns to display nicely
49
+ if (firstLine.startsWith("cat ")) {
50
+ const file = firstLine.substring(4).trim();
51
+ return `cat ${file}`;
52
+ } else if (firstLine.startsWith("grep ")) {
53
+ // Extract pattern and file/directory
54
+ const match = firstLine.match(
55
+ /grep\s+(?:-[^\s]+\s+)*['"]?([^'"]+)['"]?\s+(.+)/
56
+ );
57
+ if (match && match[1] && match[2]) {
58
+ return `grep "${match[1]}" in ${match[2]}`;
59
+ }
60
+ return firstLine.substring(0, 60) + (firstLine.length > 60 ? "..." : "");
61
+ } else if (firstLine.startsWith("find ")) {
62
+ const match = firstLine.match(
63
+ /find\s+([^\s]+)(?:\s+-name\s+['"]?([^'"]+)['"]?)?/
64
+ );
65
+ if (match && match[1]) {
66
+ return match[2]
67
+ ? `find "${match[2]}" in ${match[1]}`
68
+ : `find in ${match[1]}`;
69
+ }
70
+ return firstLine.substring(0, 60) + (firstLine.length > 60 ? "..." : "");
71
+ } else if (firstLine.startsWith("ls ")) {
72
+ const path = firstLine.substring(3).trim();
73
+ return path ? `ls ${path}` : "ls";
74
+ } else if (firstLine.startsWith("ls")) {
75
+ return "ls";
76
+ } else if (firstLine.startsWith("jq ")) {
77
+ return `jq processing JSON data`;
78
+ } else if (firstLine.startsWith("head ") || firstLine.startsWith("tail ")) {
79
+ const cmd = firstLine.split(" ")[0];
80
+ const fileMatch = firstLine.match(/(?:head|tail)\s+(?:-[^\s]+\s+)*(.+)/);
81
+ if (fileMatch && fileMatch[1]) {
82
+ return `${cmd} ${fileMatch[1]}`;
83
+ }
84
+ return firstLine.substring(0, 60) + (firstLine.length > 60 ? "..." : "");
85
+ } else {
86
+ // For other commands, show up to 80 characters
87
+ return firstLine.substring(0, 80) + (firstLine.length > 80 ? "..." : "");
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Handle errors with appropriate exit codes and user-friendly messages
93
+ */
94
+ function handleError(error: unknown, context: string): never {
95
+ console.error(`\n❌ Error ${context}:`);
96
+
97
+ if (error instanceof PhoenixClientError) {
98
+ switch (error.code) {
99
+ case "NETWORK_ERROR":
100
+ console.error(
101
+ "\n🌐 Network Error: Unable to connect to Phoenix server"
102
+ );
103
+ console.error(` Make sure Phoenix is running and accessible`);
104
+ console.error(` You can specify a different URL with --base-url`);
105
+ break;
106
+ case "AUTH_ERROR":
107
+ console.error("\n🔒 Authentication Error: Invalid or missing API key");
108
+ console.error(
109
+ ` Set the PHOENIX_API_KEY environment variable or use --api-key`
110
+ );
111
+ break;
112
+ case "INVALID_RESPONSE":
113
+ console.error(
114
+ "\n⚠️ Invalid Response: Phoenix returned unexpected data"
115
+ );
116
+ console.error(` This might be a version compatibility issue`);
117
+ break;
118
+ default:
119
+ console.error("\n❓ Phoenix Client Error:", error.message);
120
+ }
121
+ if (error.originalError && process.env.DEBUG) {
122
+ console.error("\nOriginal error:", error.originalError);
123
+ }
124
+ } else if (error instanceof Error) {
125
+ // Check for specific error patterns
126
+ if (error.message.includes("ENOENT")) {
127
+ console.error(
128
+ "\n📁 File System Error: Required file or directory not found"
129
+ );
130
+ console.error(` ${error.message}`);
131
+ } else if (
132
+ error.message.includes("EACCES") ||
133
+ error.message.includes("EPERM")
134
+ ) {
135
+ console.error("\n🚫 Permission Error: Insufficient permissions");
136
+ console.error(` ${error.message}`);
137
+ if (error.message.includes(".phoenix-insight")) {
138
+ console.error(
139
+ ` Try running with appropriate permissions or check ~/.phoenix-insight/`
140
+ );
141
+ }
142
+ } else if (
143
+ error.message.includes("rate limit") ||
144
+ error.message.includes("429")
145
+ ) {
146
+ console.error("\n⏱️ Rate Limit Error: Too many requests to Phoenix");
147
+ console.error(` Please wait a moment and try again`);
148
+ } else if (error.message.includes("timeout")) {
149
+ console.error("\n⏰ Timeout Error: Request took too long");
150
+ console.error(` The Phoenix server might be slow or unresponsive`);
151
+ } else {
152
+ console.error(`\n${error.message}`);
153
+ }
154
+
155
+ if (error.stack && process.env.DEBUG) {
156
+ console.error("\nStack trace:", error.stack);
157
+ }
158
+ } else {
159
+ console.error("\nUnexpected error:", error);
160
+ }
161
+
162
+ console.error("\n💡 Tips:");
163
+ console.error(" • Run with DEBUG=1 for more detailed error information");
164
+ console.error(
165
+ " • Check your Phoenix connection with: phoenix-insight snapshot --base-url <url>"
166
+ );
167
+ console.error(" • Use --help to see all available options");
168
+
169
+ process.exit(1);
170
+ }
171
+
172
+ program
173
+ .name("phoenix-insight")
174
+ .description("A CLI for Phoenix data analysis with AI agents")
175
+ .version(VERSION)
176
+ .usage("[options] [query]")
177
+ .option(
178
+ "--config <path>",
179
+ "Path to config file (default: ~/.phoenix-insight/config.json, or set PHOENIX_INSIGHT_CONFIG env var)"
180
+ )
181
+ .addHelpText(
182
+ "after",
183
+ `
184
+ Configuration:
185
+ Config values are loaded with the following priority (highest to lowest):
186
+ 1. CLI arguments (e.g., --base-url)
187
+ 2. Environment variables (e.g., PHOENIX_BASE_URL)
188
+ 3. Config file (~/.phoenix-insight/config.json)
189
+
190
+ Use --config to specify a custom config file path.
191
+ Set PHOENIX_INSIGHT_CONFIG env var to override the default config location.
192
+
193
+ Examples:
194
+ $ phoenix-insight # Start interactive mode
195
+ $ phoenix-insight "What are the slowest traces?" # Single query (sandbox mode)
196
+ $ phoenix-insight --interactive # Explicitly start interactive mode
197
+ $ phoenix-insight --local "Show me error patterns" # Local mode with persistence
198
+ $ phoenix-insight --local --stream "Analyze recent experiments" # Local mode with streaming
199
+ $ phoenix-insight --config ./my-config.json "Analyze traces" # Use custom config file
200
+ $ phoenix-insight help # Show this help message
201
+ `
202
+ )
203
+ .hook("preAction", async (thisCommand) => {
204
+ // Get all options from the root command
205
+ const opts = thisCommand.opts();
206
+ // Build CLI args from commander options
207
+ const cliArgs: CliArgs = {
208
+ config: opts.config,
209
+ baseUrl: opts.baseUrl,
210
+ apiKey: opts.apiKey,
211
+ limit: opts.limit,
212
+ stream: opts.stream,
213
+ local: opts.local,
214
+ refresh: opts.refresh,
215
+ trace: opts.trace,
216
+ };
217
+ // Initialize config singleton before any command runs
218
+ await initializeConfig(cliArgs);
219
+ });
220
+
221
+ program
222
+ .command("snapshot")
223
+ .description("Create a snapshot of Phoenix data")
224
+ .action(async () => {
225
+ const config = getConfig();
226
+
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
+ }
237
+
238
+ try {
239
+ // Determine the execution mode
240
+ const mode: ExecutionMode = await createLocalMode();
241
+
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
+ };
249
+
250
+ await createSnapshot(mode, snapshotOptions);
251
+
252
+ // Cleanup
253
+ await mode.cleanup();
254
+
255
+ // Shutdown observability if enabled
256
+ await shutdownObservability();
257
+ } catch (error) {
258
+ handleError(error, "creating snapshot");
259
+ }
260
+ });
261
+
262
+ program
263
+ .command("help")
264
+ .description("Show help information")
265
+ .action(() => {
266
+ program.outputHelp();
267
+ });
268
+
269
+ program
270
+ .command("prune")
271
+ .description("Delete the local snapshot directory (~/.phoenix-insight/)")
272
+ .option("--dry-run", "Show what would be deleted without actually deleting")
273
+ .action(async (options) => {
274
+ const snapshotDir = path.join(os.homedir(), ".phoenix-insight");
275
+
276
+ try {
277
+ // Check if the directory exists
278
+ const stats = await fs.stat(snapshotDir).catch(() => null);
279
+
280
+ if (!stats) {
281
+ console.log("📁 No local snapshot directory found. Nothing to prune.");
282
+ return;
283
+ }
284
+
285
+ if (options.dryRun) {
286
+ console.log("🔍 Dry run mode - would delete:");
287
+ console.log(` ${snapshotDir}`);
288
+
289
+ // Show size and count of snapshots
290
+ const snapshots = await fs
291
+ .readdir(path.join(snapshotDir, "snapshots"))
292
+ .catch(() => []);
293
+ console.log(` 📊 Contains ${snapshots.length} snapshot(s)`);
294
+
295
+ return;
296
+ }
297
+
298
+ // Ask for confirmation
299
+ const rl = readline.createInterface({
300
+ input: process.stdin,
301
+ output: process.stdout,
302
+ });
303
+
304
+ const answer = await new Promise<string>((resolve) => {
305
+ rl.question(
306
+ `⚠️ This will delete all local snapshots at:\n ${snapshotDir}\n\n Are you sure? (yes/no): `,
307
+ resolve
308
+ );
309
+ });
310
+
311
+ rl.close();
312
+
313
+ if (answer.toLowerCase() !== "yes" && answer.toLowerCase() !== "y") {
314
+ console.log("❌ Prune cancelled.");
315
+ return;
316
+ }
317
+
318
+ // Delete the directory
319
+ await fs.rm(snapshotDir, { recursive: true, force: true });
320
+ console.log("✅ Local snapshot directory deleted successfully!");
321
+ } catch (error) {
322
+ console.error("❌ Error pruning snapshots:");
323
+ console.error(
324
+ ` ${error instanceof Error ? error.message : String(error)}`
325
+ );
326
+ process.exit(1);
327
+ }
328
+ });
329
+
330
+ program
331
+ .argument("[query]", "Query to run against Phoenix data")
332
+ .option(
333
+ "--sandbox",
334
+ "Run in sandbox mode with in-memory filesystem (default)"
335
+ )
336
+ .option("--local", "Run in local mode with real filesystem")
337
+ .option("--base-url <url>", "Phoenix base URL")
338
+ .option("--api-key <key>", "Phoenix API key")
339
+ .option("--refresh", "Force refresh of snapshot data")
340
+ .option("--limit <number>", "Limit number of spans to fetch", parseInt)
341
+ .option("--stream [true|false]", "Stream agent responses", (v) =>
342
+ ["f", "false"].includes(v.toLowerCase()) ? false : true
343
+ )
344
+ .option("-i, --interactive", "Run in interactive mode (REPL)")
345
+ .option("--trace", "Enable tracing of the agent to Phoenix")
346
+ .action(async (query, options) => {
347
+ const config = getConfig();
348
+ // If interactive mode is requested, ignore query argument
349
+ if (options.interactive) {
350
+ await runInteractiveMode();
351
+ return;
352
+ }
353
+
354
+ // If no query is provided and no specific flag, start interactive mode
355
+ if (!query && !options.help) {
356
+ await runInteractiveMode();
357
+ return;
358
+ }
359
+
360
+ // Initialize observability if trace is enabled in config
361
+ if (config.trace) {
362
+ initializeObservability({
363
+ enabled: true,
364
+ baseUrl: config.baseUrl,
365
+ apiKey: config.apiKey,
366
+ projectName: "phoenix-insight",
367
+ debug: !!process.env.DEBUG,
368
+ });
369
+ }
370
+
371
+ try {
372
+ // Determine the execution mode
373
+ const mode: ExecutionMode =
374
+ config.mode === "local" ? await createLocalMode() : createSandboxMode();
375
+
376
+ // Create Phoenix client
377
+ const client = createPhoenixClient({
378
+ baseURL: config.baseUrl,
379
+ apiKey: config.apiKey,
380
+ });
381
+
382
+ // Create or update snapshot
383
+ const snapshotOptions = {
384
+ baseURL: config.baseUrl,
385
+ apiKey: config.apiKey,
386
+ spansPerProject: config.limit,
387
+ showProgress: true,
388
+ };
389
+
390
+ if (config.refresh || config.mode !== "local") {
391
+ // For sandbox mode (default) or when refresh is requested, always create a fresh snapshot
392
+ await createSnapshot(mode, snapshotOptions);
393
+ } else {
394
+ // For local mode without refresh, try incremental update
395
+ await createIncrementalSnapshot(mode, snapshotOptions);
396
+ }
397
+
398
+ // Create agent configuration
399
+ const agentConfig: PhoenixInsightAgentConfig = {
400
+ mode,
401
+ client,
402
+ maxSteps: 25,
403
+ };
404
+
405
+ // Execute the query
406
+ const agentProgress = new AgentProgress(!config.stream);
407
+ agentProgress.startThinking();
408
+
409
+ if (config.stream) {
410
+ // Stream mode
411
+ const result = (await runOneShotQuery(agentConfig, query, {
412
+ stream: true,
413
+ onStepFinish: (step) => {
414
+ // Show tool usage even in stream mode
415
+ if (step.toolCalls?.length) {
416
+ step.toolCalls.forEach((toolCall: any) => {
417
+ const toolName = toolCall.toolName;
418
+ if (toolName === "bash") {
419
+ // Extract bash command for better visibility
420
+ const command = toolCall.args?.command || "";
421
+ const formattedCmd = formatBashCommand(command);
422
+ agentProgress.updateTool(toolName, formattedCmd);
423
+ } else {
424
+ agentProgress.updateTool(toolName);
425
+ }
426
+ console.log();
427
+ });
428
+ }
429
+
430
+ // Show tool results
431
+ if (step.toolResults?.length) {
432
+ step.toolResults.forEach((toolResult: any) => {
433
+ agentProgress.updateToolResult(
434
+ toolResult.toolName,
435
+ !toolResult.isError
436
+ );
437
+ });
438
+ console.log();
439
+ }
440
+ },
441
+ })) as any; // Type assertion needed due to union type
442
+
443
+ // Stop progress before streaming
444
+ agentProgress.stop();
445
+
446
+ // Handle streaming response
447
+ console.log("\n✨ Answer:\n");
448
+ for await (const chunk of result.textStream) {
449
+ process.stdout.write(chunk);
450
+ }
451
+ console.log(); // Final newline
452
+
453
+ // Wait for full response to complete
454
+ await result.response;
455
+ } else {
456
+ // Non-streaming mode
457
+ const result = (await runOneShotQuery(agentConfig, query, {
458
+ onStepFinish: (step) => {
459
+ // Show tool usage
460
+ if (step.toolCalls?.length) {
461
+ step.toolCalls.forEach((toolCall: any) => {
462
+ const toolName = toolCall.toolName;
463
+ if (toolName === "bash") {
464
+ // Extract bash command for better visibility
465
+ const command = toolCall.args?.command || "";
466
+ const formattedCmd = formatBashCommand(command);
467
+ agentProgress.updateTool(toolName, formattedCmd);
468
+ } else {
469
+ agentProgress.updateTool(toolName);
470
+ }
471
+ });
472
+ }
473
+
474
+ // Show tool results
475
+ if (step.toolResults?.length) {
476
+ step.toolResults.forEach((toolResult: any) => {
477
+ agentProgress.updateToolResult(
478
+ toolResult.toolName,
479
+ !toolResult.isError
480
+ );
481
+ });
482
+ }
483
+ },
484
+ })) as any; // Type assertion needed due to union type
485
+
486
+ // Stop progress and display the final answer
487
+ agentProgress.succeed();
488
+ console.log("\n✨ Answer:\n");
489
+ console.log(result.text);
490
+ }
491
+
492
+ // Cleanup
493
+ await mode.cleanup();
494
+
495
+ console.log("\n✅ Done!");
496
+ } catch (error) {
497
+ handleError(error, "executing query");
498
+ } finally {
499
+ // Shutdown observability if enabled
500
+ await shutdownObservability();
501
+ }
502
+ });
503
+
504
+ async function runInteractiveMode(): Promise<void> {
505
+ const config = getConfig();
506
+
507
+ console.log("🚀 Phoenix Insight Interactive Mode");
508
+ console.log(
509
+ "Type your queries below. Type 'help' for available commands or 'exit' to quit.\n"
510
+ );
511
+
512
+ // Prevent the process from exiting on unhandled promise rejections
513
+ process.on("unhandledRejection", (reason, promise) => {
514
+ console.error("\n⚠️ Unhandled promise rejection:", reason);
515
+ console.error(
516
+ "The interactive mode will continue. You can try another query."
517
+ );
518
+ });
519
+
520
+ // Initialize observability if trace is enabled in config
521
+ if (config.trace) {
522
+ initializeObservability({
523
+ enabled: true,
524
+ baseUrl: config.baseUrl,
525
+ apiKey: config.apiKey,
526
+ projectName: "phoenix-insight",
527
+ debug: !!process.env.DEBUG,
528
+ });
529
+ }
530
+
531
+ // Setup mode and snapshot once for the session
532
+ let mode: ExecutionMode;
533
+ let agent: any;
534
+
535
+ try {
536
+ // Determine the execution mode
537
+ mode =
538
+ config.mode === "local" ? await createLocalMode() : createSandboxMode();
539
+
540
+ // Create Phoenix client
541
+ const client = createPhoenixClient({
542
+ baseURL: config.baseUrl,
543
+ apiKey: config.apiKey,
544
+ });
545
+
546
+ // Create or update snapshot
547
+ const snapshotOptions = {
548
+ baseURL: config.baseUrl,
549
+ apiKey: config.apiKey,
550
+ spansPerProject: config.limit,
551
+ showProgress: true,
552
+ };
553
+
554
+ if (config.refresh || config.mode !== "local") {
555
+ await createSnapshot(mode, snapshotOptions);
556
+ } else {
557
+ await createIncrementalSnapshot(mode, snapshotOptions);
558
+ }
559
+
560
+ console.log(
561
+ "\n✅ Snapshot ready. You can now ask questions about your Phoenix data.\n"
562
+ );
563
+
564
+ // Create agent configuration
565
+ const agentConfig: PhoenixInsightAgentConfig = {
566
+ mode,
567
+ client,
568
+ maxSteps: 25,
569
+ };
570
+
571
+ // Create reusable agent
572
+ agent = await createInsightAgent(agentConfig);
573
+
574
+ // Setup readline interface
575
+ const rl = readline.createInterface({
576
+ input: process.stdin,
577
+ output: process.stdout,
578
+ prompt: "phoenix> ",
579
+ terminal: true, // Ensure terminal mode for better compatibility
580
+ });
581
+
582
+ let userExited = false;
583
+
584
+ // Handle SIGINT (Ctrl+C) gracefully
585
+ rl.on("SIGINT", () => {
586
+ if (userExited) {
587
+ process.exit(0);
588
+ }
589
+ console.log(
590
+ '\n\nUse "exit" to quit or press Ctrl+C again to force exit.'
591
+ );
592
+ userExited = true;
593
+ rl.prompt();
594
+ });
595
+
596
+ // Helper function to process a single query
597
+ const processQuery = async (query: string): Promise<boolean> => {
598
+ if (query === "exit" || query === "quit") {
599
+ return true; // Signal to exit
600
+ }
601
+
602
+ if (query === "help") {
603
+ console.log("\n📖 Interactive Mode Commands:");
604
+ console.log(" help - Show this help message");
605
+ console.log(" exit, quit - Exit interactive mode");
606
+ console.log(
607
+ " px-fetch-more - Fetch additional data (e.g., px-fetch-more spans --project <name> --limit <n>)"
608
+ );
609
+ console.log("\n💡 Usage Tips:");
610
+ console.log(
611
+ " • Ask natural language questions about your Phoenix data"
612
+ );
613
+ console.log(
614
+ " • The agent has access to bash commands to analyze the data"
615
+ );
616
+ console.log(
617
+ " • Use px-fetch-more commands to get additional data on-demand"
618
+ );
619
+ console.log("\n🔧 Options (set when starting phoenix-insight):");
620
+ console.log(
621
+ " --local - Use local mode with persistent storage"
622
+ );
623
+ console.log(
624
+ " --stream - Stream agent responses in real-time"
625
+ );
626
+ console.log(" --refresh - Force fresh snapshot data");
627
+ console.log(" --limit <n> - Set max spans per project");
628
+ console.log(" --trace - Enable observability tracing");
629
+ return false;
630
+ }
631
+
632
+ if (query === "") {
633
+ return false;
634
+ }
635
+
636
+ try {
637
+ const agentProgress = new AgentProgress(!config.stream);
638
+ agentProgress.startThinking();
639
+
640
+ if (config.stream) {
641
+ // Stream mode
642
+ const result = await agent.stream(query, {
643
+ onStepFinish: (step: any) => {
644
+ // Show tool usage even in stream mode
645
+ if (step.toolCalls?.length) {
646
+ step.toolCalls.forEach((toolCall: any) => {
647
+ const toolName = toolCall.toolName;
648
+ if (toolName === "bash") {
649
+ // Extract bash command for better visibility
650
+ const command = toolCall.args?.command || "";
651
+ const shortCmd = command.split("\n")[0].substring(0, 50);
652
+ agentProgress.updateTool(
653
+ toolName,
654
+ shortCmd + (command.length > 50 ? "..." : "")
655
+ );
656
+ } else {
657
+ agentProgress.updateTool(toolName);
658
+ }
659
+ });
660
+ }
661
+
662
+ // Show tool results
663
+ if (step.toolResults?.length) {
664
+ step.toolResults.forEach((toolResult: any) => {
665
+ agentProgress.updateToolResult(
666
+ toolResult.toolName,
667
+ !toolResult.isError
668
+ );
669
+ });
670
+ }
671
+ },
672
+ });
673
+
674
+ // Stop progress before streaming
675
+ agentProgress.stop();
676
+
677
+ // Handle streaming response
678
+ console.log("\n✨ Answer:\n");
679
+ for await (const chunk of result.textStream) {
680
+ process.stdout.write(chunk);
681
+ }
682
+ console.log(); // Final newline
683
+
684
+ // Wait for full response to complete
685
+ await result.response;
686
+ } else {
687
+ // Non-streaming mode
688
+ const result = await agent.generate(query, {
689
+ onStepFinish: (step: any) => {
690
+ // Show tool usage
691
+ if (step.toolCalls?.length) {
692
+ step.toolCalls.forEach((toolCall: any) => {
693
+ const toolName = toolCall.toolName;
694
+ if (toolName === "bash") {
695
+ // Extract bash command for better visibility
696
+ const command = toolCall.args?.command || "";
697
+ const shortCmd = command.split("\n")[0].substring(0, 50);
698
+ agentProgress.updateTool(
699
+ toolName,
700
+ shortCmd + (command.length > 50 ? "..." : "")
701
+ );
702
+ } else {
703
+ agentProgress.updateTool(toolName);
704
+ }
705
+ });
706
+ }
707
+
708
+ // Show tool results
709
+ if (step.toolResults?.length) {
710
+ step.toolResults.forEach((toolResult: any) => {
711
+ agentProgress.updateToolResult(
712
+ toolResult.toolName,
713
+ !toolResult.isError
714
+ );
715
+ });
716
+ }
717
+ },
718
+ });
719
+
720
+ // Stop progress and display the final answer
721
+ agentProgress.succeed();
722
+ console.log("\n✨ Answer:\n");
723
+ console.log(result.text);
724
+ }
725
+
726
+ console.log("\n" + "─".repeat(50) + "\n");
727
+ } catch (error) {
728
+ console.error("\n❌ Query Error:");
729
+ if (error instanceof PhoenixClientError) {
730
+ console.error(` ${error.message}`);
731
+ } else if (error instanceof Error) {
732
+ console.error(` ${error.message}`);
733
+ } else {
734
+ console.error(` ${String(error)}`);
735
+ }
736
+ console.error(" You can try again with a different query\n");
737
+ }
738
+
739
+ return false;
740
+ };
741
+
742
+ // Use event-based approach instead of async iterator to prevent
743
+ // premature exit when ora/spinners interact with stdin
744
+ await new Promise<void>((resolve) => {
745
+ rl.on("line", async (line) => {
746
+ const query = line.trim();
747
+
748
+ // Pause readline while processing to prevent queuing
749
+ rl.pause();
750
+
751
+ const shouldExit = await processQuery(query);
752
+
753
+ if (shouldExit) {
754
+ rl.close();
755
+ } else {
756
+ // Resume and show prompt for next input
757
+ rl.resume();
758
+ rl.prompt();
759
+ }
760
+ });
761
+
762
+ rl.on("close", () => {
763
+ resolve();
764
+ });
765
+
766
+ // Show initial prompt
767
+ rl.prompt();
768
+ });
769
+
770
+ console.log("\n👋 Goodbye!");
771
+
772
+ // Cleanup
773
+ await mode.cleanup();
774
+
775
+ // Shutdown observability if enabled
776
+ await shutdownObservability();
777
+ } catch (error) {
778
+ handleError(error, "setting up interactive mode");
779
+ }
780
+ }
781
+
782
+ program.parse();