@output.ai/cli 0.3.0-dev.pr156.c8e7f40 → 0.3.1-dev.pr156.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.
@@ -0,0 +1,154 @@
1
+ import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import { existsSync } from 'node:fs';
5
+ export class S3Downloader {
6
+ s3Client = null;
7
+ cacheDir;
8
+ constructor(options = {}) {
9
+ this.cacheDir = options.cacheDir || path.join(process.cwd(), '.output', 'traces');
10
+ this.initializeS3Client();
11
+ }
12
+ initializeS3Client() {
13
+ const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
14
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
15
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
16
+ if (!accessKeyId || !secretAccessKey) {
17
+ // S3 client not available without credentials
18
+ this.s3Client = null;
19
+ return;
20
+ }
21
+ this.s3Client = new S3Client({
22
+ region,
23
+ credentials: {
24
+ accessKeyId,
25
+ secretAccessKey
26
+ }
27
+ });
28
+ }
29
+ /**
30
+ * Parse S3 URL to extract bucket and key
31
+ * Supports formats:
32
+ * - https://bucket.s3.amazonaws.com/path/to/file
33
+ * - https://bucket.s3.region.amazonaws.com/path/to/file
34
+ * - s3://bucket/path/to/file
35
+ */
36
+ parseS3Url(url) {
37
+ // Handle s3:// protocol
38
+ if (url.startsWith('s3://')) {
39
+ const withoutProtocol = url.slice(5);
40
+ const firstSlash = withoutProtocol.indexOf('/');
41
+ if (firstSlash === -1) {
42
+ throw new Error(`Invalid S3 URL format: ${url}`);
43
+ }
44
+ return {
45
+ bucket: withoutProtocol.substring(0, firstSlash),
46
+ key: withoutProtocol.substring(firstSlash + 1)
47
+ };
48
+ }
49
+ // Handle https:// URLs
50
+ if (url.startsWith('https://')) {
51
+ const urlObj = new URL(url);
52
+ const hostname = urlObj.hostname;
53
+ // Extract bucket from hostname
54
+ // Format: bucket.s3.amazonaws.com or bucket.s3.region.amazonaws.com
55
+ const s3Match = hostname.match(/^([^.]+)\.s3(?:\.[^.]+)?\.amazonaws\.com$/);
56
+ if (!s3Match) {
57
+ throw new Error(`Invalid S3 URL format: ${url}`);
58
+ }
59
+ const bucket = s3Match[1];
60
+ const key = urlObj.pathname.startsWith('/') ? urlObj.pathname.slice(1) : urlObj.pathname;
61
+ return { bucket, key };
62
+ }
63
+ throw new Error(`Unsupported S3 URL format: ${url}`);
64
+ }
65
+ /**
66
+ * Get cached file path for a given S3 URL
67
+ */
68
+ getCachePath(url) {
69
+ const { key } = this.parseS3Url(url);
70
+ // Use the last part of the key as filename to preserve the original name
71
+ const filename = path.basename(key);
72
+ return path.join(this.cacheDir, filename);
73
+ }
74
+ /**
75
+ * Download a file from S3
76
+ */
77
+ async download(s3Url, options = {}) {
78
+ if (!this.s3Client) {
79
+ throw new Error('AWS credentials not configured. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.');
80
+ }
81
+ // Check cache first
82
+ const cachePath = this.getCachePath(s3Url);
83
+ if (!options.forceDownload && existsSync(cachePath)) {
84
+ const content = await fs.readFile(cachePath, 'utf-8');
85
+ return content;
86
+ }
87
+ // Parse S3 URL
88
+ const { bucket, key } = this.parseS3Url(s3Url);
89
+ try {
90
+ // Download from S3
91
+ const command = new GetObjectCommand({
92
+ Bucket: bucket,
93
+ Key: key
94
+ });
95
+ const response = await this.s3Client.send(command);
96
+ if (!response.Body) {
97
+ throw new Error('Empty response from S3');
98
+ }
99
+ // Convert stream to string
100
+ const bodyContents = await response.Body.transformToString();
101
+ // Cache the file
102
+ await this.ensureCacheDir();
103
+ await fs.writeFile(cachePath, bodyContents, 'utf-8');
104
+ return bodyContents;
105
+ }
106
+ catch (error) {
107
+ if (error instanceof Error) {
108
+ throw new Error(`Failed to download from S3: ${error.message}`);
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+ /**
114
+ * Check if S3 client is available (credentials configured)
115
+ */
116
+ isAvailable() {
117
+ return this.s3Client !== null;
118
+ }
119
+ /**
120
+ * Ensure cache directory exists
121
+ */
122
+ async ensureCacheDir() {
123
+ await fs.mkdir(this.cacheDir, { recursive: true });
124
+ }
125
+ /**
126
+ * Clear the cache directory
127
+ */
128
+ async clearCache() {
129
+ if (existsSync(this.cacheDir)) {
130
+ await fs.rm(this.cacheDir, { recursive: true, force: true });
131
+ }
132
+ }
133
+ /**
134
+ * Get the size of the cache directory in bytes
135
+ */
136
+ async getCacheSize() {
137
+ if (!existsSync(this.cacheDir)) {
138
+ return 0;
139
+ }
140
+ const files = await fs.readdir(this.cacheDir);
141
+ // eslint-disable-next-line no-restricted-syntax
142
+ let totalSize = 0;
143
+ for (const file of files) {
144
+ const filePath = path.join(this.cacheDir, file);
145
+ const stats = await fs.stat(filePath);
146
+ totalSize += stats.size;
147
+ }
148
+ return totalSize;
149
+ }
150
+ }
151
+ /**
152
+ * Create a singleton instance for convenience
153
+ */
154
+ export const s3Downloader = new S3Downloader();
@@ -1,27 +1,61 @@
1
- /**
2
- * Format duration in human-readable format
3
- */
4
- export declare function formatDuration(ms: number): string;
5
- /**
6
- * Format trace data based on the requested format
7
- */
8
- export declare function format(traceData: string | object, outputFormat?: 'json' | 'text'): string;
9
- /**
10
- * Get summary statistics from trace
11
- */
12
- export declare function getSummary(traceData: string | object): {
13
- totalDuration: number;
14
- totalEvents: number;
15
- totalSteps: number;
16
- totalActivities: number;
17
- hasErrors: boolean;
18
- };
19
- /**
20
- * Display trace tree with debug command formatting style
21
- */
22
- export declare function displayDebugTree(node: any): string;
23
- export declare const traceFormatter: {
24
- format: typeof format;
25
- getSummary: typeof getSummary;
26
- displayDebugTree: typeof displayDebugTree;
27
- };
1
+ export declare class TraceFormatter {
2
+ /**
3
+ * Format trace data based on the requested format
4
+ */
5
+ format(traceData: string | object, format?: 'json' | 'text'): string;
6
+ /**
7
+ * Format trace as human-readable text with tree structure
8
+ */
9
+ private formatAsText;
10
+ /**
11
+ * Format the header with workflow information
12
+ */
13
+ private formatHeader;
14
+ /**
15
+ * Format events as a timeline table
16
+ */
17
+ private formatEventsTable;
18
+ /**
19
+ * Format trace as a tree structure
20
+ */
21
+ private formatTree;
22
+ /**
23
+ * Get a readable name for an event
24
+ */
25
+ private getEventName;
26
+ /**
27
+ * Format the phase with icons
28
+ */
29
+ private formatPhase;
30
+ /**
31
+ * Format duration in human-readable format
32
+ */
33
+ private formatDuration;
34
+ /**
35
+ * Format error for display
36
+ */
37
+ private formatError;
38
+ /**
39
+ * Format details for table display
40
+ */
41
+ private formatDetails;
42
+ /**
43
+ * Format details for tree display
44
+ */
45
+ private formatTreeDetails;
46
+ /**
47
+ * Truncate long values for display
48
+ */
49
+ private truncateValue;
50
+ /**
51
+ * Get summary statistics from trace
52
+ */
53
+ getSummary(traceData: string | object): {
54
+ totalDuration: number;
55
+ totalEvents: number;
56
+ totalSteps: number;
57
+ totalActivities: number;
58
+ hasErrors: boolean;
59
+ };
60
+ }
61
+ export declare const traceFormatter: TraceFormatter;