@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.
- package/LICENSE +201 -0
- package/README.md +620 -0
- package/dist/agent/index.js +230 -0
- package/dist/cli.js +640 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/px-fetch-more-spans.js +98 -0
- package/dist/commands/px-fetch-more-trace.js +110 -0
- package/dist/config/index.js +165 -0
- package/dist/config/loader.js +141 -0
- package/dist/config/schema.js +53 -0
- package/dist/index.js +1 -0
- package/dist/modes/index.js +17 -0
- package/dist/modes/local.js +134 -0
- package/dist/modes/sandbox.js +121 -0
- package/dist/modes/types.js +1 -0
- package/dist/observability/index.js +65 -0
- package/dist/progress.js +209 -0
- package/dist/prompts/index.js +1 -0
- package/dist/prompts/system.js +30 -0
- package/dist/snapshot/client.js +74 -0
- package/dist/snapshot/context.js +332 -0
- package/dist/snapshot/datasets.js +68 -0
- package/dist/snapshot/experiments.js +135 -0
- package/dist/snapshot/index.js +262 -0
- package/dist/snapshot/projects.js +44 -0
- package/dist/snapshot/prompts.js +199 -0
- package/dist/snapshot/spans.js +80 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +75 -0
- package/src/agent/index.ts +323 -0
- package/src/cli.ts +782 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/px-fetch-more-spans.ts +174 -0
- package/src/commands/px-fetch-more-trace.ts +183 -0
- package/src/config/index.ts +225 -0
- package/src/config/loader.ts +173 -0
- package/src/config/schema.ts +66 -0
- package/src/index.ts +1 -0
- package/src/modes/index.ts +21 -0
- package/src/modes/local.ts +163 -0
- package/src/modes/sandbox.ts +144 -0
- package/src/modes/types.ts +31 -0
- package/src/observability/index.ts +90 -0
- package/src/progress.ts +239 -0
- package/src/prompts/index.ts +1 -0
- package/src/prompts/system.ts +31 -0
- package/src/snapshot/client.ts +129 -0
- package/src/snapshot/context.ts +462 -0
- package/src/snapshot/datasets.ts +132 -0
- package/src/snapshot/experiments.ts +246 -0
- package/src/snapshot/index.ts +403 -0
- package/src/snapshot/projects.ts +58 -0
- package/src/snapshot/prompts.ts +267 -0
- package/src/snapshot/spans.ts +142 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { withErrorHandling } from "../snapshot/client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetches additional spans for a specific project on-demand
|
|
4
|
+
*
|
|
5
|
+
* @param client - Phoenix client instance
|
|
6
|
+
* @param mode - Execution mode for file operations
|
|
7
|
+
* @param options - Options for fetching spans
|
|
8
|
+
*/
|
|
9
|
+
export async function fetchMoreSpans(client, mode, options) {
|
|
10
|
+
const { project, limit, startTime, endTime } = options;
|
|
11
|
+
await withErrorHandling(async () => {
|
|
12
|
+
// Try to read existing metadata to get the last cursor
|
|
13
|
+
let existingMetadata = null;
|
|
14
|
+
let existingSpans = [];
|
|
15
|
+
try {
|
|
16
|
+
const metadataResult = await mode.exec(`cat /phoenix/projects/${project}/spans/metadata.json`);
|
|
17
|
+
if (metadataResult.exitCode === 0 && metadataResult.stdout) {
|
|
18
|
+
existingMetadata = JSON.parse(metadataResult.stdout);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// Metadata doesn't exist, that's okay
|
|
23
|
+
}
|
|
24
|
+
// Try to read existing spans
|
|
25
|
+
try {
|
|
26
|
+
const spansResult = await mode.exec(`cat /phoenix/projects/${project}/spans/index.jsonl`);
|
|
27
|
+
if (spansResult.exitCode === 0 && spansResult.stdout) {
|
|
28
|
+
existingSpans = spansResult.stdout
|
|
29
|
+
.trim()
|
|
30
|
+
.split("\n")
|
|
31
|
+
.filter((line) => line.length > 0)
|
|
32
|
+
.map((line) => JSON.parse(line));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
// Spans file doesn't exist, that's okay
|
|
37
|
+
}
|
|
38
|
+
// Fetch new spans
|
|
39
|
+
const newSpans = [];
|
|
40
|
+
let cursor = existingMetadata?.lastCursor ?? null;
|
|
41
|
+
let totalFetched = 0;
|
|
42
|
+
while (totalFetched < limit) {
|
|
43
|
+
const query = {
|
|
44
|
+
limit: Math.min(100, limit - totalFetched), // Fetch in chunks of 100
|
|
45
|
+
};
|
|
46
|
+
if (cursor) {
|
|
47
|
+
query.cursor = cursor;
|
|
48
|
+
}
|
|
49
|
+
if (startTime) {
|
|
50
|
+
query.start_time =
|
|
51
|
+
startTime instanceof Date ? startTime.toISOString() : startTime;
|
|
52
|
+
}
|
|
53
|
+
if (endTime) {
|
|
54
|
+
query.end_time =
|
|
55
|
+
endTime instanceof Date ? endTime.toISOString() : endTime;
|
|
56
|
+
}
|
|
57
|
+
const response = await client.GET("/v1/projects/{project_identifier}/spans", {
|
|
58
|
+
params: {
|
|
59
|
+
path: {
|
|
60
|
+
project_identifier: project,
|
|
61
|
+
},
|
|
62
|
+
query,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (response.error)
|
|
66
|
+
throw response.error;
|
|
67
|
+
const data = response.data?.data ?? [];
|
|
68
|
+
newSpans.push(...data);
|
|
69
|
+
totalFetched += data.length;
|
|
70
|
+
cursor = response.data?.next_cursor ?? null;
|
|
71
|
+
// Stop if there's no more data
|
|
72
|
+
if (!cursor || data.length === 0) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Combine existing and new spans
|
|
77
|
+
const allSpans = [...existingSpans, ...newSpans];
|
|
78
|
+
// Write updated spans to JSONL file
|
|
79
|
+
const jsonlContent = allSpans
|
|
80
|
+
.map((span) => JSON.stringify(span))
|
|
81
|
+
.join("\n");
|
|
82
|
+
await mode.writeFile(`/phoenix/projects/${project}/spans/index.jsonl`, jsonlContent);
|
|
83
|
+
// Update metadata
|
|
84
|
+
const metadata = {
|
|
85
|
+
project,
|
|
86
|
+
spanCount: allSpans.length,
|
|
87
|
+
startTime: startTime instanceof Date
|
|
88
|
+
? startTime.toISOString()
|
|
89
|
+
: (startTime ?? null),
|
|
90
|
+
endTime: endTime instanceof Date ? endTime.toISOString() : (endTime ?? null),
|
|
91
|
+
snapshotTime: new Date().toISOString(),
|
|
92
|
+
lastCursor: cursor,
|
|
93
|
+
};
|
|
94
|
+
await mode.writeFile(`/phoenix/projects/${project}/spans/metadata.json`, JSON.stringify(metadata, null, 2));
|
|
95
|
+
console.log(`Fetched ${newSpans.length} additional spans for project "${project}"`);
|
|
96
|
+
console.log(`Total spans for project: ${allSpans.length}`);
|
|
97
|
+
}, `fetching more spans for project ${project}`);
|
|
98
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { withErrorHandling } from "../snapshot/client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetches all spans for a specific trace ID
|
|
4
|
+
*
|
|
5
|
+
* @param client - Phoenix client instance
|
|
6
|
+
* @param mode - Execution mode for file operations
|
|
7
|
+
* @param options - Options for fetching trace
|
|
8
|
+
*/
|
|
9
|
+
export async function fetchMoreTrace(client, mode, options) {
|
|
10
|
+
const { traceId, project } = options;
|
|
11
|
+
await withErrorHandling(async () => {
|
|
12
|
+
// First, check if the project exists in our snapshot
|
|
13
|
+
const projectsResult = await mode.exec("cat /phoenix/projects/index.jsonl");
|
|
14
|
+
if (projectsResult.exitCode !== 0 || !projectsResult.stdout) {
|
|
15
|
+
console.error("No projects found in snapshot. Run a snapshot first.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const projectNames = projectsResult.stdout
|
|
19
|
+
.trim()
|
|
20
|
+
.split("\n")
|
|
21
|
+
.filter((line) => line.length > 0)
|
|
22
|
+
.map((line) => JSON.parse(line).name);
|
|
23
|
+
if (!projectNames.includes(project)) {
|
|
24
|
+
console.error(`Project "${project}" not found. Available projects: ${projectNames.join(", ")}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Fetch all spans that belong to this trace
|
|
28
|
+
const traceSpans = [];
|
|
29
|
+
let cursor = null;
|
|
30
|
+
let totalFetched = 0;
|
|
31
|
+
console.log(`Fetching trace ${traceId} from project "${project}"...`);
|
|
32
|
+
// We need to fetch spans in batches and filter by trace_id
|
|
33
|
+
// Since the API doesn't support direct trace_id filtering
|
|
34
|
+
while (true) {
|
|
35
|
+
const query = {
|
|
36
|
+
limit: 100, // Fetch in chunks
|
|
37
|
+
};
|
|
38
|
+
if (cursor) {
|
|
39
|
+
query.cursor = cursor;
|
|
40
|
+
}
|
|
41
|
+
const response = await client.GET("/v1/projects/{project_identifier}/spans", {
|
|
42
|
+
params: {
|
|
43
|
+
path: {
|
|
44
|
+
project_identifier: project,
|
|
45
|
+
},
|
|
46
|
+
query,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
if (response.error)
|
|
50
|
+
throw response.error;
|
|
51
|
+
const data = response.data?.data ?? [];
|
|
52
|
+
totalFetched += data.length;
|
|
53
|
+
// Filter spans that belong to our trace
|
|
54
|
+
const matchingSpans = data.filter((span) => span.context.trace_id === traceId);
|
|
55
|
+
traceSpans.push(...matchingSpans);
|
|
56
|
+
cursor = response.data?.next_cursor ?? null;
|
|
57
|
+
// Stop if we found spans for the trace or no more data
|
|
58
|
+
if (traceSpans.length > 0 || !cursor || data.length === 0) {
|
|
59
|
+
// If we found some spans, continue until we have all spans from the trace
|
|
60
|
+
// This is because trace spans might be spread across multiple pages
|
|
61
|
+
if (traceSpans.length > 0 && cursor && data.length > 0) {
|
|
62
|
+
console.log(`Found ${traceSpans.length} spans so far, continuing search...`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
// Show progress for large datasets
|
|
68
|
+
if (totalFetched % 1000 === 0) {
|
|
69
|
+
console.log(`Searched ${totalFetched} spans so far...`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (traceSpans.length === 0) {
|
|
73
|
+
console.log(`No spans found for trace ${traceId} in project "${project}"`);
|
|
74
|
+
console.log(`Searched through ${totalFetched} spans. The trace might not exist or might be in a different project.`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Sort spans by start_time to show them in order
|
|
78
|
+
traceSpans.sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime());
|
|
79
|
+
// Write trace spans to a dedicated file
|
|
80
|
+
const jsonlContent = traceSpans
|
|
81
|
+
.map((span) => JSON.stringify(span))
|
|
82
|
+
.join("\n");
|
|
83
|
+
const traceDir = `/phoenix/traces/${traceId}`;
|
|
84
|
+
await mode.writeFile(`${traceDir}/spans.jsonl`, jsonlContent);
|
|
85
|
+
// Create trace metadata
|
|
86
|
+
const rootSpan = traceSpans.find((span) => !span.parent_id);
|
|
87
|
+
const firstSpan = traceSpans[0];
|
|
88
|
+
const lastSpan = traceSpans[traceSpans.length - 1];
|
|
89
|
+
const metadata = {
|
|
90
|
+
traceId,
|
|
91
|
+
project,
|
|
92
|
+
spanCount: traceSpans.length,
|
|
93
|
+
rootSpan: rootSpan ? { id: rootSpan.id, name: rootSpan.name } : null,
|
|
94
|
+
startTime: firstSpan?.start_time || null,
|
|
95
|
+
endTime: lastSpan?.end_time || null,
|
|
96
|
+
duration: firstSpan && lastSpan
|
|
97
|
+
? new Date(lastSpan.end_time).getTime() -
|
|
98
|
+
new Date(firstSpan.start_time).getTime()
|
|
99
|
+
: 0,
|
|
100
|
+
snapshotTime: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
await mode.writeFile(`${traceDir}/metadata.json`, JSON.stringify(metadata, null, 2));
|
|
103
|
+
console.log(`\nSuccessfully fetched trace ${traceId}:`);
|
|
104
|
+
console.log(`- Project: ${project}`);
|
|
105
|
+
console.log(`- Spans: ${traceSpans.length}`);
|
|
106
|
+
console.log(`- Root span: ${rootSpan?.name || "Unknown"}`);
|
|
107
|
+
console.log(`- Duration: ${(metadata.duration / 1000).toFixed(2)} seconds`);
|
|
108
|
+
console.log(`\nTrace data saved to: ${traceDir}/`);
|
|
109
|
+
}, `fetching trace ${traceId}`);
|
|
110
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config singleton module for Phoenix Insight CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides centralized configuration management with priority-based merging:
|
|
5
|
+
* 1. Config file (lowest priority)
|
|
6
|
+
* 2. Environment variables
|
|
7
|
+
* 3. CLI arguments (highest priority)
|
|
8
|
+
*/
|
|
9
|
+
import { configSchema, getDefaultConfig } from "./schema.js";
|
|
10
|
+
import { getConfigPath, loadConfigFile, validateConfig, createDefaultConfig, setCliConfigPath, } from "./loader.js";
|
|
11
|
+
/**
|
|
12
|
+
* Module-level storage for the initialized config singleton
|
|
13
|
+
*/
|
|
14
|
+
let configInstance = null;
|
|
15
|
+
/**
|
|
16
|
+
* Environment variable mappings to config keys
|
|
17
|
+
*/
|
|
18
|
+
const ENV_VAR_MAPPINGS = {
|
|
19
|
+
PHOENIX_BASE_URL: "baseUrl",
|
|
20
|
+
PHOENIX_API_KEY: "apiKey",
|
|
21
|
+
PHOENIX_INSIGHT_LIMIT: "limit",
|
|
22
|
+
PHOENIX_INSIGHT_STREAM: "stream",
|
|
23
|
+
PHOENIX_INSIGHT_MODE: "mode",
|
|
24
|
+
PHOENIX_INSIGHT_REFRESH: "refresh",
|
|
25
|
+
PHOENIX_INSIGHT_TRACE: "trace",
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parse environment variable value to appropriate type
|
|
29
|
+
*/
|
|
30
|
+
function parseEnvValue(key, value) {
|
|
31
|
+
switch (key) {
|
|
32
|
+
case "baseUrl":
|
|
33
|
+
case "apiKey":
|
|
34
|
+
return value;
|
|
35
|
+
case "limit":
|
|
36
|
+
const num = parseInt(value, 10);
|
|
37
|
+
return isNaN(num) ? undefined : num;
|
|
38
|
+
case "stream":
|
|
39
|
+
case "refresh":
|
|
40
|
+
case "trace":
|
|
41
|
+
return value.toLowerCase() === "true" || value === "1";
|
|
42
|
+
case "mode":
|
|
43
|
+
if (value === "sandbox" || value === "local") {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
default:
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get config values from environment variables
|
|
53
|
+
*/
|
|
54
|
+
function getEnvConfig() {
|
|
55
|
+
const envConfig = {};
|
|
56
|
+
for (const [envVar, configKey] of Object.entries(ENV_VAR_MAPPINGS)) {
|
|
57
|
+
const value = process.env[envVar];
|
|
58
|
+
if (value !== undefined) {
|
|
59
|
+
const parsed = parseEnvValue(configKey, value);
|
|
60
|
+
if (parsed !== undefined) {
|
|
61
|
+
envConfig[configKey] = parsed;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return envConfig;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Convert CLI args to config format
|
|
69
|
+
*/
|
|
70
|
+
function cliArgsToConfig(cliArgs) {
|
|
71
|
+
const config = {};
|
|
72
|
+
if (cliArgs.baseUrl !== undefined) {
|
|
73
|
+
config.baseUrl = cliArgs.baseUrl;
|
|
74
|
+
}
|
|
75
|
+
if (cliArgs.apiKey !== undefined) {
|
|
76
|
+
config.apiKey = cliArgs.apiKey;
|
|
77
|
+
}
|
|
78
|
+
if (cliArgs.limit !== undefined) {
|
|
79
|
+
config.limit = cliArgs.limit;
|
|
80
|
+
}
|
|
81
|
+
if (cliArgs.stream !== undefined) {
|
|
82
|
+
config.stream = cliArgs.stream;
|
|
83
|
+
}
|
|
84
|
+
if (cliArgs.local !== undefined) {
|
|
85
|
+
// CLI uses --local flag, config uses mode
|
|
86
|
+
config.mode = cliArgs.local ? "local" : "sandbox";
|
|
87
|
+
}
|
|
88
|
+
if (cliArgs.refresh !== undefined) {
|
|
89
|
+
config.refresh = cliArgs.refresh;
|
|
90
|
+
}
|
|
91
|
+
if (cliArgs.trace !== undefined) {
|
|
92
|
+
config.trace = cliArgs.trace;
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Initialize the configuration singleton
|
|
98
|
+
*
|
|
99
|
+
* Merges configuration from multiple sources with the following priority:
|
|
100
|
+
* 1. Config file (lowest priority)
|
|
101
|
+
* 2. Environment variables
|
|
102
|
+
* 3. CLI arguments (highest priority)
|
|
103
|
+
*
|
|
104
|
+
* @param cliArgs - CLI arguments from Commander
|
|
105
|
+
* @returns The initialized configuration
|
|
106
|
+
*/
|
|
107
|
+
export async function initializeConfig(cliArgs = {}) {
|
|
108
|
+
// Set CLI config path if provided (for getConfigPath to use)
|
|
109
|
+
if (cliArgs.config) {
|
|
110
|
+
setCliConfigPath(cliArgs.config);
|
|
111
|
+
}
|
|
112
|
+
// Get config file path
|
|
113
|
+
const { path: configPath, isDefault } = getConfigPath();
|
|
114
|
+
// Try to create default config if it doesn't exist (only for default path)
|
|
115
|
+
if (isDefault) {
|
|
116
|
+
await createDefaultConfig(configPath, isDefault);
|
|
117
|
+
}
|
|
118
|
+
// Load config file
|
|
119
|
+
const fileConfig = await loadConfigFile(configPath);
|
|
120
|
+
// Validate file config (returns defaults if null/invalid)
|
|
121
|
+
const validatedFileConfig = validateConfig(fileConfig);
|
|
122
|
+
// Get environment config
|
|
123
|
+
const envConfig = getEnvConfig();
|
|
124
|
+
// Get CLI config
|
|
125
|
+
const cliConfig = cliArgsToConfig(cliArgs);
|
|
126
|
+
// Merge configs: file < env < cli
|
|
127
|
+
const mergedConfig = {
|
|
128
|
+
...validatedFileConfig,
|
|
129
|
+
...envConfig,
|
|
130
|
+
...cliConfig,
|
|
131
|
+
};
|
|
132
|
+
// Final validation with Zod
|
|
133
|
+
const result = configSchema.safeParse(mergedConfig);
|
|
134
|
+
if (result.success) {
|
|
135
|
+
configInstance = result.data;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Log validation issues as warnings
|
|
139
|
+
result.error.issues.forEach((issue) => {
|
|
140
|
+
console.warn(`Warning: Config validation error at '${issue.path.join(".")}': ${issue.message}`);
|
|
141
|
+
});
|
|
142
|
+
// Fall back to defaults
|
|
143
|
+
configInstance = getDefaultConfig();
|
|
144
|
+
}
|
|
145
|
+
return configInstance;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get the initialized configuration
|
|
149
|
+
*
|
|
150
|
+
* @throws Error if config has not been initialized via initializeConfig()
|
|
151
|
+
* @returns The configuration object
|
|
152
|
+
*/
|
|
153
|
+
export function getConfig() {
|
|
154
|
+
if (configInstance === null) {
|
|
155
|
+
throw new Error("Config not initialized. Call initializeConfig() first before using getConfig().");
|
|
156
|
+
}
|
|
157
|
+
return configInstance;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Reset the config singleton (useful for testing)
|
|
161
|
+
*/
|
|
162
|
+
export function resetConfig() {
|
|
163
|
+
configInstance = null;
|
|
164
|
+
setCliConfigPath(undefined);
|
|
165
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { configSchema, getDefaultConfig } from "./schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* Default config directory and file path
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".phoenix-insight");
|
|
9
|
+
const DEFAULT_CONFIG_FILE = path.join(DEFAULT_CONFIG_DIR, "config.json");
|
|
10
|
+
/**
|
|
11
|
+
* Module-level storage for CLI args passed from commander
|
|
12
|
+
* This is set externally before getConfigPath is called
|
|
13
|
+
*/
|
|
14
|
+
let cliConfigPath;
|
|
15
|
+
/**
|
|
16
|
+
* Set the CLI config path (called from CLI parsing)
|
|
17
|
+
*/
|
|
18
|
+
export function setCliConfigPath(configPath) {
|
|
19
|
+
cliConfigPath = configPath;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the config file path based on priority:
|
|
23
|
+
* 1. CLI argument (--config)
|
|
24
|
+
* 2. Environment variable (PHOENIX_INSIGHT_CONFIG)
|
|
25
|
+
* 3. Default path (~/.phoenix-insight/config.json)
|
|
26
|
+
*
|
|
27
|
+
* @returns The path to the config file and whether it's the default path
|
|
28
|
+
*/
|
|
29
|
+
export function getConfigPath() {
|
|
30
|
+
// Priority 1: CLI argument
|
|
31
|
+
if (cliConfigPath) {
|
|
32
|
+
return { path: cliConfigPath, isDefault: false };
|
|
33
|
+
}
|
|
34
|
+
// Priority 2: Environment variable
|
|
35
|
+
const envConfigPath = process.env.PHOENIX_INSIGHT_CONFIG;
|
|
36
|
+
if (envConfigPath) {
|
|
37
|
+
return { path: envConfigPath, isDefault: false };
|
|
38
|
+
}
|
|
39
|
+
// Priority 3: Default path
|
|
40
|
+
return { path: DEFAULT_CONFIG_FILE, isDefault: true };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load and parse a config file from disk
|
|
44
|
+
*
|
|
45
|
+
* @param configPath - Path to the config file
|
|
46
|
+
* @returns Parsed JSON object or null if file not found
|
|
47
|
+
* @throws Error if file exists but cannot be parsed as JSON
|
|
48
|
+
*/
|
|
49
|
+
export async function loadConfigFile(configPath) {
|
|
50
|
+
try {
|
|
51
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
52
|
+
return JSON.parse(content);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// File not found is expected - return null
|
|
56
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// JSON parse errors should be reported
|
|
60
|
+
if (error instanceof SyntaxError) {
|
|
61
|
+
console.warn(`Warning: Config file at ${configPath} contains invalid JSON: ${error.message}`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Other errors (permissions, etc.) - warn and return null
|
|
65
|
+
console.warn(`Warning: Could not read config file at ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate a raw config object against the schema
|
|
71
|
+
* Returns validated config or defaults if validation fails
|
|
72
|
+
*
|
|
73
|
+
* @param raw - Raw config object (or null/undefined)
|
|
74
|
+
* @returns Validated config with defaults applied
|
|
75
|
+
*/
|
|
76
|
+
export function validateConfig(raw) {
|
|
77
|
+
// If no raw config, return defaults
|
|
78
|
+
if (!raw) {
|
|
79
|
+
return getDefaultConfig();
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
// Parse with Zod schema - this applies defaults for missing fields
|
|
83
|
+
return configSchema.parse(raw);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Log validation errors as warnings
|
|
87
|
+
if (error instanceof Error && "issues" in error) {
|
|
88
|
+
// Zod error with issues array
|
|
89
|
+
const zodError = error;
|
|
90
|
+
zodError.issues.forEach((issue) => {
|
|
91
|
+
console.warn(`Warning: Config validation error at '${issue.path.join(".")}': ${issue.message}`);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.warn(`Warning: Config validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
}
|
|
97
|
+
// Return defaults on validation failure
|
|
98
|
+
return getDefaultConfig();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create a default config file at the given path
|
|
103
|
+
* Only creates the file if it doesn't already exist
|
|
104
|
+
* Only triggers for the default path, not custom paths
|
|
105
|
+
*
|
|
106
|
+
* @param configPath - Path where to create the config file
|
|
107
|
+
* @param isDefault - Whether this is the default path (only create if true)
|
|
108
|
+
* @returns true if file was created, false otherwise
|
|
109
|
+
*/
|
|
110
|
+
export async function createDefaultConfig(configPath, isDefault) {
|
|
111
|
+
// Only create default config for the default path
|
|
112
|
+
if (!isDefault) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
// Check if file already exists
|
|
117
|
+
await fs.access(configPath);
|
|
118
|
+
// File exists, don't overwrite
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// File doesn't exist, create it
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
// Create directory if needed
|
|
126
|
+
const configDir = path.dirname(configPath);
|
|
127
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
128
|
+
// Get default config and write it
|
|
129
|
+
const defaultConfig = getDefaultConfig();
|
|
130
|
+
const content = JSON.stringify(defaultConfig, null, 2);
|
|
131
|
+
await fs.writeFile(configPath, content, "utf-8");
|
|
132
|
+
// Log informational message to stderr
|
|
133
|
+
console.error(`Created default config at ${configPath}`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// Log warning but don't fail - config will use defaults
|
|
138
|
+
console.warn(`Warning: Could not create default config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for Phoenix Insight CLI configuration
|
|
4
|
+
*
|
|
5
|
+
* Configuration values can be set via:
|
|
6
|
+
* 1. Config file (~/.phoenix-insight/config.json or custom path)
|
|
7
|
+
* 2. Environment variables (PHOENIX_BASE_URL, PHOENIX_API_KEY, etc.)
|
|
8
|
+
* 3. CLI arguments (--base-url, --api-key, etc.)
|
|
9
|
+
*
|
|
10
|
+
* Priority: config file < env vars < CLI args
|
|
11
|
+
*/
|
|
12
|
+
export const configSchema = z.object({
|
|
13
|
+
/**
|
|
14
|
+
* Phoenix server base URL
|
|
15
|
+
* @default "http://localhost:6006"
|
|
16
|
+
*/
|
|
17
|
+
baseUrl: z.string().default("http://localhost:6006"),
|
|
18
|
+
/**
|
|
19
|
+
* Phoenix API key for authentication (optional)
|
|
20
|
+
*/
|
|
21
|
+
apiKey: z.string().optional(),
|
|
22
|
+
/**
|
|
23
|
+
* Maximum number of spans to fetch per project
|
|
24
|
+
* @default 1000
|
|
25
|
+
*/
|
|
26
|
+
limit: z.number().int().positive().default(1000),
|
|
27
|
+
/**
|
|
28
|
+
* Enable streaming responses from the agent
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
stream: z.boolean().default(true),
|
|
32
|
+
/**
|
|
33
|
+
* Execution mode: "sandbox" for in-memory filesystem, "local" for real filesystem
|
|
34
|
+
* @default "sandbox"
|
|
35
|
+
*/
|
|
36
|
+
mode: z.enum(["sandbox", "local"]).default("sandbox"),
|
|
37
|
+
/**
|
|
38
|
+
* Force refresh of snapshot data
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
refresh: z.boolean().default(false),
|
|
42
|
+
/**
|
|
43
|
+
* Enable tracing of the agent to Phoenix
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
trace: z.boolean().default(false),
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Get default configuration values
|
|
50
|
+
*/
|
|
51
|
+
export function getDefaultConfig() {
|
|
52
|
+
return configSchema.parse({});
|
|
53
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./modes/index.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./sandbox.js";
|
|
3
|
+
export * from "./local.js";
|
|
4
|
+
import { SandboxMode } from "./sandbox.js";
|
|
5
|
+
import { LocalMode } from "./local.js";
|
|
6
|
+
/**
|
|
7
|
+
* Creates a new sandbox execution mode
|
|
8
|
+
*/
|
|
9
|
+
export function createSandboxMode() {
|
|
10
|
+
return new SandboxMode();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new local execution mode
|
|
14
|
+
*/
|
|
15
|
+
export async function createLocalMode() {
|
|
16
|
+
return new LocalMode();
|
|
17
|
+
}
|