sentienceapi 0.96.1 → 0.98.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,767 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.FailureArtifactBuffer = void 0;
40
+ const child_process_1 = require("child_process");
41
+ const fs_1 = __importDefault(require("fs"));
42
+ const http = __importStar(require("http"));
43
+ const https = __importStar(require("https"));
44
+ const os_1 = __importDefault(require("os"));
45
+ const path_1 = __importDefault(require("path"));
46
+ const url_1 = require("url");
47
+ const zlib = __importStar(require("zlib"));
48
+ const SENTIENCE_API_URL = 'https://api.sentienceapi.com';
49
+ async function writeJsonAtomic(filePath, data) {
50
+ const tmpPath = `${filePath}.tmp`;
51
+ await fs_1.default.promises.writeFile(tmpPath, JSON.stringify(data, null, 2));
52
+ await fs_1.default.promises.rename(tmpPath, filePath);
53
+ }
54
+ /**
55
+ * Check if ffmpeg is available on the system PATH.
56
+ */
57
+ function isFfmpegAvailable() {
58
+ try {
59
+ const result = (0, child_process_1.spawnSync)('ffmpeg', ['-version'], {
60
+ timeout: 5000,
61
+ stdio: 'pipe',
62
+ });
63
+ return result.status === 0;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ /**
70
+ * Generate an MP4 video clip from a directory of frames using ffmpeg.
71
+ */
72
+ function generateClipFromFrames(framesDir, outputPath, fps = 8) {
73
+ // Find all frame files and sort them
74
+ const files = fs_1.default
75
+ .readdirSync(framesDir)
76
+ .filter(f => f.startsWith('frame_') && (f.endsWith('.png') || f.endsWith('.jpeg') || f.endsWith('.jpg')))
77
+ .sort();
78
+ if (files.length === 0) {
79
+ console.warn('No frame files found for clip generation');
80
+ return false;
81
+ }
82
+ // Create a temporary file list for ffmpeg concat demuxer
83
+ const listFile = path_1.default.join(framesDir, 'frames_list.txt');
84
+ const frameDuration = 1.0 / fps;
85
+ try {
86
+ // Write the frames list file
87
+ const listContent = files.map(f => `file '${f}'\nduration ${frameDuration}`).join('\n') +
88
+ `\nfile '${files[files.length - 1]}'`; // ffmpeg concat quirk
89
+ fs_1.default.writeFileSync(listFile, listContent);
90
+ // Run ffmpeg to generate the clip
91
+ const result = (0, child_process_1.spawnSync)('ffmpeg', [
92
+ '-y',
93
+ '-f',
94
+ 'concat',
95
+ '-safe',
96
+ '0',
97
+ '-i',
98
+ listFile,
99
+ '-vsync',
100
+ 'vfr',
101
+ '-pix_fmt',
102
+ 'yuv420p',
103
+ '-c:v',
104
+ 'libx264',
105
+ '-crf',
106
+ '23',
107
+ outputPath,
108
+ ], {
109
+ timeout: 60000, // 1 minute timeout
110
+ cwd: framesDir,
111
+ stdio: 'pipe',
112
+ });
113
+ if (result.status !== 0) {
114
+ const stderr = result.stderr?.toString('utf-8').slice(0, 500) ?? '';
115
+ console.warn(`ffmpeg failed with return code ${result.status}: ${stderr}`);
116
+ return false;
117
+ }
118
+ return fs_1.default.existsSync(outputPath);
119
+ }
120
+ catch (err) {
121
+ console.warn(`Error generating clip: ${err}`);
122
+ return false;
123
+ }
124
+ finally {
125
+ // Clean up the list file
126
+ try {
127
+ fs_1.default.unlinkSync(listFile);
128
+ }
129
+ catch {
130
+ // ignore
131
+ }
132
+ }
133
+ }
134
+ function redactSnapshotDefaults(payload) {
135
+ if (!payload || typeof payload !== 'object') {
136
+ return payload;
137
+ }
138
+ const elements = Array.isArray(payload.elements) ? payload.elements : null;
139
+ if (!elements) {
140
+ return payload;
141
+ }
142
+ const redactedElements = elements.map((el) => {
143
+ if (!el || typeof el !== 'object')
144
+ return el;
145
+ const inputType = String(el.input_type || '').toLowerCase();
146
+ if (['password', 'email', 'tel'].includes(inputType) && 'value' in el) {
147
+ return { ...el, value: null, value_redacted: true };
148
+ }
149
+ return el;
150
+ });
151
+ return { ...payload, elements: redactedElements };
152
+ }
153
+ class FailureArtifactBuffer {
154
+ constructor(runId, options = {}, timeNow = () => Date.now()) {
155
+ this.frames = [];
156
+ this.steps = [];
157
+ this.persisted = false;
158
+ this.runId = runId;
159
+ this.options = {
160
+ bufferSeconds: options.bufferSeconds ?? 15,
161
+ captureOnAction: options.captureOnAction ?? true,
162
+ fps: options.fps ?? 0,
163
+ persistMode: options.persistMode ?? 'onFail',
164
+ outputDir: options.outputDir ?? '.sentience/artifacts',
165
+ onBeforePersist: options.onBeforePersist ?? null,
166
+ redactSnapshotValues: options.redactSnapshotValues ?? true,
167
+ clip: {
168
+ mode: options.clip?.mode ?? 'auto',
169
+ fps: options.clip?.fps ?? 8,
170
+ seconds: options.clip?.seconds,
171
+ },
172
+ };
173
+ this.timeNow = timeNow;
174
+ this.tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'sentience-artifacts-'));
175
+ this.framesDir = path_1.default.join(this.tempDir, 'frames');
176
+ fs_1.default.mkdirSync(this.framesDir, { recursive: true });
177
+ }
178
+ getOptions() {
179
+ return this.options;
180
+ }
181
+ recordStep(action, stepId, stepIndex, url) {
182
+ this.steps.push({
183
+ ts: this.timeNow(),
184
+ action,
185
+ step_id: stepId,
186
+ step_index: stepIndex,
187
+ url,
188
+ });
189
+ }
190
+ async addFrame(image, fmt = 'jpeg') {
191
+ const ts = this.timeNow();
192
+ const fileName = `frame_${ts}.${fmt}`;
193
+ const filePath = path_1.default.join(this.framesDir, fileName);
194
+ await fs_1.default.promises.writeFile(filePath, image);
195
+ this.frames.push({ ts, fileName, filePath });
196
+ this.prune();
197
+ }
198
+ frameCount() {
199
+ return this.frames.length;
200
+ }
201
+ prune() {
202
+ const cutoff = this.timeNow() - this.options.bufferSeconds * 1000;
203
+ const keep = [];
204
+ for (const frame of this.frames) {
205
+ if (frame.ts >= cutoff) {
206
+ keep.push(frame);
207
+ }
208
+ else {
209
+ try {
210
+ fs_1.default.unlinkSync(frame.filePath);
211
+ }
212
+ catch {
213
+ // ignore
214
+ }
215
+ }
216
+ }
217
+ this.frames = keep;
218
+ }
219
+ async persist(reason, status, snapshot, diagnostics, metadata) {
220
+ if (this.persisted) {
221
+ return null;
222
+ }
223
+ const outDir = this.options.outputDir;
224
+ await fs_1.default.promises.mkdir(outDir, { recursive: true });
225
+ const ts = this.timeNow();
226
+ const runDir = path_1.default.join(outDir, `${this.runId}-${ts}`);
227
+ const framesOut = path_1.default.join(runDir, 'frames');
228
+ await fs_1.default.promises.mkdir(framesOut, { recursive: true });
229
+ for (const frame of this.frames) {
230
+ await fs_1.default.promises.copyFile(frame.filePath, path_1.default.join(framesOut, frame.fileName));
231
+ }
232
+ await writeJsonAtomic(path_1.default.join(runDir, 'steps.json'), this.steps);
233
+ let snapshotPayload = snapshot;
234
+ if (snapshotPayload && this.options.redactSnapshotValues) {
235
+ snapshotPayload = redactSnapshotDefaults(snapshotPayload);
236
+ }
237
+ let diagnosticsPayload = diagnostics;
238
+ let framePaths = this.frames.map(frame => frame.filePath);
239
+ let dropFrames = false;
240
+ if (this.options.onBeforePersist) {
241
+ try {
242
+ const result = this.options.onBeforePersist({
243
+ runId: this.runId,
244
+ reason,
245
+ status,
246
+ snapshot: snapshotPayload,
247
+ diagnostics: diagnosticsPayload,
248
+ framePaths,
249
+ metadata: metadata ?? {},
250
+ });
251
+ if (result.snapshot !== undefined) {
252
+ snapshotPayload = result.snapshot;
253
+ }
254
+ if (result.diagnostics !== undefined) {
255
+ diagnosticsPayload = result.diagnostics;
256
+ }
257
+ if (result.framePaths) {
258
+ framePaths = result.framePaths;
259
+ }
260
+ dropFrames = Boolean(result.dropFrames);
261
+ }
262
+ catch {
263
+ dropFrames = true;
264
+ }
265
+ }
266
+ if (!dropFrames) {
267
+ for (const framePath of framePaths) {
268
+ if (!fs_1.default.existsSync(framePath)) {
269
+ continue;
270
+ }
271
+ const fileName = path_1.default.basename(framePath);
272
+ await fs_1.default.promises.copyFile(framePath, path_1.default.join(framesOut, fileName));
273
+ }
274
+ }
275
+ let snapshotWritten = false;
276
+ if (snapshotPayload) {
277
+ await writeJsonAtomic(path_1.default.join(runDir, 'snapshot.json'), snapshotPayload);
278
+ snapshotWritten = true;
279
+ }
280
+ let diagnosticsWritten = false;
281
+ if (diagnosticsPayload) {
282
+ await writeJsonAtomic(path_1.default.join(runDir, 'diagnostics.json'), diagnosticsPayload);
283
+ diagnosticsWritten = true;
284
+ }
285
+ // Generate video clip from frames (optional, requires ffmpeg)
286
+ let clipGenerated = false;
287
+ const clipOptions = this.options.clip;
288
+ if (!dropFrames && framePaths.length > 0 && clipOptions.mode !== 'off') {
289
+ let shouldGenerate = false;
290
+ if (clipOptions.mode === 'auto') {
291
+ // Only generate if ffmpeg is available
292
+ shouldGenerate = isFfmpegAvailable();
293
+ if (!shouldGenerate) {
294
+ // Silent in auto mode - just skip
295
+ }
296
+ }
297
+ else if (clipOptions.mode === 'on') {
298
+ // Always attempt to generate
299
+ shouldGenerate = true;
300
+ if (!isFfmpegAvailable()) {
301
+ console.warn("ffmpeg not found on PATH but clip.mode='on'. " +
302
+ 'Install ffmpeg to generate video clips.');
303
+ shouldGenerate = false;
304
+ }
305
+ }
306
+ if (shouldGenerate) {
307
+ const clipPath = path_1.default.join(runDir, 'failure.mp4');
308
+ clipGenerated = generateClipFromFrames(framesOut, clipPath, clipOptions.fps ?? 8);
309
+ if (clipGenerated) {
310
+ console.log(`Generated failure clip: ${clipPath}`);
311
+ }
312
+ else {
313
+ console.warn('Failed to generate video clip');
314
+ }
315
+ }
316
+ }
317
+ const manifest = {
318
+ run_id: this.runId,
319
+ created_at_ms: ts,
320
+ status,
321
+ reason,
322
+ buffer_seconds: this.options.bufferSeconds,
323
+ frame_count: dropFrames ? 0 : framePaths.length,
324
+ frames: dropFrames ? [] : framePaths.map(p => ({ file: path_1.default.basename(p), ts: null })),
325
+ snapshot: snapshotWritten ? 'snapshot.json' : null,
326
+ diagnostics: diagnosticsWritten ? 'diagnostics.json' : null,
327
+ clip: clipGenerated ? 'failure.mp4' : null,
328
+ clip_fps: clipGenerated ? (clipOptions.fps ?? 8) : null,
329
+ metadata: metadata ?? {},
330
+ frames_redacted: !dropFrames && Boolean(this.options.onBeforePersist),
331
+ frames_dropped: dropFrames,
332
+ };
333
+ await writeJsonAtomic(path_1.default.join(runDir, 'manifest.json'), manifest);
334
+ this.persisted = true;
335
+ return runDir;
336
+ }
337
+ async cleanup() {
338
+ await fs_1.default.promises.rm(this.tempDir, { recursive: true, force: true });
339
+ }
340
+ /**
341
+ * Upload persisted artifacts to cloud storage.
342
+ *
343
+ * This method uploads all artifacts from a persisted directory to cloud storage
344
+ * using presigned URLs from the gateway. It follows the same pattern as trace
345
+ * screenshot uploads.
346
+ *
347
+ * @param apiKey - Sentience API key for authentication
348
+ * @param apiUrl - Sentience API base URL (default: https://api.sentienceapi.com)
349
+ * @param persistedDir - Path to persisted artifacts directory. If undefined, uses the
350
+ * most recent persist() output directory.
351
+ * @param logger - Optional logger for progress/error messages
352
+ * @returns artifact_index_key on success, null on failure
353
+ *
354
+ * @example
355
+ * const buf = new FailureArtifactBuffer('run-123', options);
356
+ * await buf.addFrame(screenshotBytes);
357
+ * const runDir = await buf.persist('assertion failed', 'failure');
358
+ * const artifactKey = await buf.uploadToCloud('sk-...');
359
+ * // artifactKey can be passed to /v1/traces/complete
360
+ */
361
+ async uploadToCloud(apiKey, apiUrl, persistedDir, logger) {
362
+ const baseUrl = apiUrl || SENTIENCE_API_URL;
363
+ // Determine which directory to upload
364
+ let targetDir = persistedDir;
365
+ if (!targetDir) {
366
+ // Find most recent persisted directory
367
+ const outputDir = this.options.outputDir;
368
+ if (!fs_1.default.existsSync(outputDir)) {
369
+ logger?.warn('No artifacts directory found');
370
+ return null;
371
+ }
372
+ // Look for directories matching runId pattern
373
+ const entries = fs_1.default.readdirSync(outputDir, { withFileTypes: true });
374
+ const matchingDirs = entries
375
+ .filter(e => e.isDirectory() && e.name.startsWith(this.runId))
376
+ .map(e => ({
377
+ name: e.name,
378
+ path: path_1.default.join(outputDir, e.name),
379
+ mtime: fs_1.default.statSync(path_1.default.join(outputDir, e.name)).mtimeMs,
380
+ }))
381
+ .sort((a, b) => b.mtime - a.mtime);
382
+ if (matchingDirs.length === 0) {
383
+ logger?.warn(`No persisted artifacts found for runId=${this.runId}`);
384
+ return null;
385
+ }
386
+ targetDir = matchingDirs[0].path;
387
+ }
388
+ if (!fs_1.default.existsSync(targetDir)) {
389
+ logger?.warn(`Artifacts directory not found: ${targetDir}`);
390
+ return null;
391
+ }
392
+ // Read manifest to understand what files need uploading
393
+ const manifestPath = path_1.default.join(targetDir, 'manifest.json');
394
+ if (!fs_1.default.existsSync(manifestPath)) {
395
+ logger?.warn('manifest.json not found in artifacts directory');
396
+ return null;
397
+ }
398
+ const manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
399
+ // Build list of artifacts to upload
400
+ const artifacts = this.collectArtifactsForUpload(targetDir, manifest);
401
+ if (artifacts.length === 0) {
402
+ logger?.warn('No artifacts to upload');
403
+ return null;
404
+ }
405
+ logger?.info(`Uploading ${artifacts.length} artifact(s) to cloud`);
406
+ // Request presigned URLs from gateway
407
+ const uploadUrls = await this.requestArtifactUrls(apiKey, baseUrl, artifacts, logger);
408
+ if (!uploadUrls) {
409
+ return null;
410
+ }
411
+ // Upload artifacts in parallel
412
+ const artifactIndexKey = await this.uploadArtifacts(artifacts, uploadUrls, logger);
413
+ if (artifactIndexKey) {
414
+ // Report completion to gateway
415
+ await this.completeArtifacts(apiKey, baseUrl, artifactIndexKey, artifacts, logger);
416
+ }
417
+ return artifactIndexKey;
418
+ }
419
+ collectArtifactsForUpload(persistedDir, manifest) {
420
+ const artifacts = [];
421
+ // Core JSON artifacts
422
+ const jsonFiles = ['manifest.json', 'steps.json'];
423
+ if (manifest.snapshot) {
424
+ jsonFiles.push('snapshot.json');
425
+ }
426
+ if (manifest.diagnostics) {
427
+ jsonFiles.push('diagnostics.json');
428
+ }
429
+ for (const filename of jsonFiles) {
430
+ const filePath = path_1.default.join(persistedDir, filename);
431
+ if (fs_1.default.existsSync(filePath)) {
432
+ artifacts.push({
433
+ name: filename,
434
+ sizeBytes: fs_1.default.statSync(filePath).size,
435
+ contentType: 'application/json',
436
+ filePath,
437
+ });
438
+ }
439
+ }
440
+ // Video clip
441
+ if (manifest.clip) {
442
+ const clipPath = path_1.default.join(persistedDir, 'failure.mp4');
443
+ if (fs_1.default.existsSync(clipPath)) {
444
+ artifacts.push({
445
+ name: 'failure.mp4',
446
+ sizeBytes: fs_1.default.statSync(clipPath).size,
447
+ contentType: 'video/mp4',
448
+ filePath: clipPath,
449
+ });
450
+ }
451
+ }
452
+ // Frames
453
+ const framesDir = path_1.default.join(persistedDir, 'frames');
454
+ if (fs_1.default.existsSync(framesDir)) {
455
+ const frameFiles = fs_1.default.readdirSync(framesDir).sort();
456
+ for (const frameFile of frameFiles) {
457
+ const ext = path_1.default.extname(frameFile).toLowerCase();
458
+ if (['.jpeg', '.jpg', '.png'].includes(ext)) {
459
+ const framePath = path_1.default.join(framesDir, frameFile);
460
+ const contentType = ext === '.png' ? 'image/png' : 'image/jpeg';
461
+ artifacts.push({
462
+ name: `frames/${frameFile}`,
463
+ sizeBytes: fs_1.default.statSync(framePath).size,
464
+ contentType,
465
+ filePath: framePath,
466
+ });
467
+ }
468
+ }
469
+ }
470
+ return artifacts;
471
+ }
472
+ async requestArtifactUrls(apiKey, apiUrl, artifacts, logger) {
473
+ try {
474
+ // Prepare request payload (exclude local path)
475
+ const artifactsPayload = artifacts.map(a => ({
476
+ name: a.name,
477
+ size_bytes: a.sizeBytes,
478
+ content_type: a.contentType,
479
+ }));
480
+ const body = JSON.stringify({
481
+ run_id: this.runId,
482
+ artifacts: artifactsPayload,
483
+ });
484
+ return new Promise(resolve => {
485
+ const url = new url_1.URL(`${apiUrl}/v1/traces/artifacts/init`);
486
+ const protocol = url.protocol === 'https:' ? https : http;
487
+ const options = {
488
+ hostname: url.hostname,
489
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
490
+ path: url.pathname + url.search,
491
+ method: 'POST',
492
+ headers: {
493
+ 'Content-Type': 'application/json',
494
+ 'Content-Length': Buffer.byteLength(body),
495
+ Authorization: `Bearer ${apiKey}`,
496
+ },
497
+ timeout: 30000,
498
+ };
499
+ const req = protocol.request(options, res => {
500
+ let data = '';
501
+ res.on('data', chunk => {
502
+ data += chunk;
503
+ });
504
+ res.on('end', () => {
505
+ if (res.statusCode === 200) {
506
+ try {
507
+ resolve(JSON.parse(data));
508
+ }
509
+ catch {
510
+ logger?.warn('Failed to parse artifact upload URLs response');
511
+ resolve(null);
512
+ }
513
+ }
514
+ else {
515
+ logger?.warn(`Failed to get artifact upload URLs: HTTP ${res.statusCode}`);
516
+ resolve(null);
517
+ }
518
+ });
519
+ });
520
+ req.on('error', error => {
521
+ logger?.error(`Error requesting artifact upload URLs: ${error.message}`);
522
+ resolve(null);
523
+ });
524
+ req.on('timeout', () => {
525
+ req.destroy();
526
+ logger?.warn('Artifact URLs request timeout');
527
+ resolve(null);
528
+ });
529
+ req.write(body);
530
+ req.end();
531
+ });
532
+ }
533
+ catch (error) {
534
+ logger?.error(`Error requesting artifact upload URLs: ${error.message}`);
535
+ return null;
536
+ }
537
+ }
538
+ async uploadArtifacts(artifacts, uploadUrls, logger) {
539
+ const urlMap = new Map();
540
+ for (const item of uploadUrls.upload_urls) {
541
+ urlMap.set(item.name, item);
542
+ }
543
+ const indexUpload = uploadUrls.artifact_index_upload;
544
+ const storageKeys = new Map();
545
+ const uploadPromises = [];
546
+ for (const artifact of artifacts) {
547
+ const urlInfo = urlMap.get(artifact.name);
548
+ if (!urlInfo) {
549
+ continue;
550
+ }
551
+ const uploadPromise = this.uploadSingleArtifact(artifact, urlInfo, logger).then(success => ({
552
+ name: artifact.name,
553
+ success,
554
+ }));
555
+ uploadPromises.push(uploadPromise);
556
+ }
557
+ // Wait for all uploads
558
+ const results = await Promise.all(uploadPromises);
559
+ let uploadedCount = 0;
560
+ const failedNames = [];
561
+ for (const result of results) {
562
+ if (result.success) {
563
+ uploadedCount++;
564
+ const urlInfo = urlMap.get(result.name);
565
+ if (urlInfo?.storage_key) {
566
+ storageKeys.set(result.name, urlInfo.storage_key);
567
+ }
568
+ }
569
+ else {
570
+ failedNames.push(result.name);
571
+ }
572
+ }
573
+ if (uploadedCount === artifacts.length) {
574
+ logger?.info(`All ${uploadedCount} artifacts uploaded successfully`);
575
+ }
576
+ else {
577
+ logger?.warn(`Uploaded ${uploadedCount}/${artifacts.length} artifacts. Failed: ${failedNames.join(', ')}`);
578
+ }
579
+ // Upload artifact index file
580
+ if (indexUpload && uploadedCount > 0) {
581
+ return this.uploadArtifactIndex(artifacts, storageKeys, indexUpload, logger);
582
+ }
583
+ return null;
584
+ }
585
+ async uploadSingleArtifact(artifact, urlInfo, logger) {
586
+ try {
587
+ const data = fs_1.default.readFileSync(artifact.filePath);
588
+ return new Promise(resolve => {
589
+ const url = new url_1.URL(urlInfo.upload_url);
590
+ const protocol = url.protocol === 'https:' ? https : http;
591
+ const options = {
592
+ hostname: url.hostname,
593
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
594
+ path: url.pathname + url.search,
595
+ method: 'PUT',
596
+ headers: {
597
+ 'Content-Type': artifact.contentType,
598
+ 'Content-Length': data.length,
599
+ },
600
+ timeout: 60000,
601
+ };
602
+ const req = protocol.request(options, res => {
603
+ res.on('data', () => { });
604
+ res.on('end', () => {
605
+ if (res.statusCode === 200) {
606
+ resolve(true);
607
+ }
608
+ else {
609
+ logger?.warn(`Artifact ${artifact.name} upload failed: HTTP ${res.statusCode}`);
610
+ resolve(false);
611
+ }
612
+ });
613
+ });
614
+ req.on('error', error => {
615
+ logger?.warn(`Artifact ${artifact.name} upload error: ${error.message}`);
616
+ resolve(false);
617
+ });
618
+ req.on('timeout', () => {
619
+ req.destroy();
620
+ logger?.warn(`Artifact ${artifact.name} upload timeout`);
621
+ resolve(false);
622
+ });
623
+ req.write(data);
624
+ req.end();
625
+ });
626
+ }
627
+ catch (error) {
628
+ logger?.warn(`Artifact ${artifact.name} upload error: ${error.message}`);
629
+ return false;
630
+ }
631
+ }
632
+ async uploadArtifactIndex(artifacts, storageKeys, indexUpload, logger) {
633
+ try {
634
+ // Build index content
635
+ const indexData = {
636
+ run_id: this.runId,
637
+ created_at_ms: Date.now(),
638
+ artifacts: artifacts
639
+ .filter(a => storageKeys.has(a.name))
640
+ .map(a => ({
641
+ name: a.name,
642
+ storage_key: storageKeys.get(a.name) || '',
643
+ content_type: a.contentType,
644
+ })),
645
+ };
646
+ // Compress and upload
647
+ const indexJson = Buffer.from(JSON.stringify(indexData, null, 2), 'utf-8');
648
+ const compressed = zlib.gzipSync(indexJson);
649
+ return new Promise(resolve => {
650
+ const url = new url_1.URL(indexUpload.upload_url);
651
+ const protocol = url.protocol === 'https:' ? https : http;
652
+ const options = {
653
+ hostname: url.hostname,
654
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
655
+ path: url.pathname + url.search,
656
+ method: 'PUT',
657
+ headers: {
658
+ 'Content-Type': 'application/json',
659
+ 'Content-Encoding': 'gzip',
660
+ 'Content-Length': compressed.length,
661
+ },
662
+ timeout: 30000,
663
+ };
664
+ const req = protocol.request(options, res => {
665
+ res.on('data', () => { });
666
+ res.on('end', () => {
667
+ if (res.statusCode === 200) {
668
+ logger?.info('Artifact index uploaded successfully');
669
+ resolve(indexUpload.storage_key || '');
670
+ }
671
+ else {
672
+ logger?.warn(`Artifact index upload failed: HTTP ${res.statusCode}`);
673
+ resolve(null);
674
+ }
675
+ });
676
+ });
677
+ req.on('error', error => {
678
+ logger?.warn(`Error uploading artifact index: ${error.message}`);
679
+ resolve(null);
680
+ });
681
+ req.on('timeout', () => {
682
+ req.destroy();
683
+ logger?.warn('Artifact index upload timeout');
684
+ resolve(null);
685
+ });
686
+ req.write(compressed);
687
+ req.end();
688
+ });
689
+ }
690
+ catch (error) {
691
+ logger?.warn(`Error uploading artifact index: ${error.message}`);
692
+ return null;
693
+ }
694
+ }
695
+ async completeArtifacts(apiKey, apiUrl, artifactIndexKey, artifacts, logger) {
696
+ try {
697
+ // Calculate stats
698
+ const totalSize = artifacts.reduce((sum, a) => sum + a.sizeBytes, 0);
699
+ const framesArtifacts = artifacts.filter(a => a.name.startsWith('frames/'));
700
+ const framesTotal = framesArtifacts.reduce((sum, a) => sum + a.sizeBytes, 0);
701
+ // Get individual file sizes
702
+ const manifestSize = artifacts.find(a => a.name === 'manifest.json')?.sizeBytes || 0;
703
+ const snapshotSize = artifacts.find(a => a.name === 'snapshot.json')?.sizeBytes || 0;
704
+ const diagnosticsSize = artifacts.find(a => a.name === 'diagnostics.json')?.sizeBytes || 0;
705
+ const stepsSize = artifacts.find(a => a.name === 'steps.json')?.sizeBytes || 0;
706
+ const clipSize = artifacts.find(a => a.name === 'failure.mp4')?.sizeBytes || 0;
707
+ const body = JSON.stringify({
708
+ run_id: this.runId,
709
+ artifact_index_key: artifactIndexKey,
710
+ stats: {
711
+ manifest_size_bytes: manifestSize,
712
+ snapshot_size_bytes: snapshotSize,
713
+ diagnostics_size_bytes: diagnosticsSize,
714
+ steps_size_bytes: stepsSize,
715
+ clip_size_bytes: clipSize,
716
+ frames_total_size_bytes: framesTotal,
717
+ frames_count: framesArtifacts.length,
718
+ total_artifact_size_bytes: totalSize,
719
+ },
720
+ });
721
+ return new Promise(resolve => {
722
+ const url = new url_1.URL(`${apiUrl}/v1/traces/artifacts/complete`);
723
+ const protocol = url.protocol === 'https:' ? https : http;
724
+ const options = {
725
+ hostname: url.hostname,
726
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
727
+ path: url.pathname + url.search,
728
+ method: 'POST',
729
+ headers: {
730
+ 'Content-Type': 'application/json',
731
+ 'Content-Length': Buffer.byteLength(body),
732
+ Authorization: `Bearer ${apiKey}`,
733
+ },
734
+ timeout: 10000,
735
+ };
736
+ const req = protocol.request(options, res => {
737
+ res.on('data', () => { });
738
+ res.on('end', () => {
739
+ if (res.statusCode === 200) {
740
+ logger?.info('Artifact completion reported to gateway');
741
+ }
742
+ else {
743
+ logger?.warn(`Failed to report artifact completion: HTTP ${res.statusCode}`);
744
+ }
745
+ resolve();
746
+ });
747
+ });
748
+ req.on('error', error => {
749
+ logger?.warn(`Error reporting artifact completion: ${error.message}`);
750
+ resolve();
751
+ });
752
+ req.on('timeout', () => {
753
+ req.destroy();
754
+ logger?.warn('Artifact completion request timeout');
755
+ resolve();
756
+ });
757
+ req.write(body);
758
+ req.end();
759
+ });
760
+ }
761
+ catch (error) {
762
+ logger?.warn(`Error reporting artifact completion: ${error.message}`);
763
+ }
764
+ }
765
+ }
766
+ exports.FailureArtifactBuffer = FailureArtifactBuffer;
767
+ //# sourceMappingURL=failure-artifacts.js.map