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