autokap 1.0.8 → 1.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/assets/skill/OPCODE-REFERENCE.md +29 -1
- package/assets/skill/SKILL.md +2 -1
- package/dist/auth-capture.js +35 -2
- package/dist/billing-operation-logging.d.ts +4 -3
- package/dist/billing-operation-logging.js +3 -2
- package/dist/browser.d.ts +10 -10
- package/dist/browser.js +32 -28
- package/dist/capture-encryption.d.ts +3 -1
- package/dist/capture-encryption.js +21 -6
- package/dist/capture-strategy.js +3 -2
- package/dist/cli-config.d.ts +2 -1
- package/dist/cli-config.js +51 -2
- package/dist/cli-contract.d.ts +5 -1
- package/dist/cli-contract.js +7 -1
- package/dist/cli-runner-local.js +16 -3
- package/dist/cli-runner.js +165 -18
- package/dist/cli.js +25 -19
- package/dist/clip-begin-frame-recorder.d.ts +44 -0
- package/dist/clip-begin-frame-recorder.js +250 -0
- package/dist/clip-capture-backend.d.ts +25 -0
- package/dist/clip-capture-backend.js +189 -0
- package/dist/clip-capture-loop.d.ts +61 -0
- package/dist/clip-capture-loop.js +111 -0
- package/dist/clip-frame-recorder.d.ts +63 -0
- package/dist/clip-frame-recorder.js +305 -0
- package/dist/clip-postprocess.d.ts +31 -2
- package/dist/clip-postprocess.js +174 -57
- package/dist/clip-runtime.d.ts +18 -0
- package/dist/clip-runtime.js +67 -0
- package/dist/clip-scale.d.ts +10 -0
- package/dist/clip-scale.js +21 -0
- package/dist/clip-screencast-recorder.d.ts +42 -0
- package/dist/clip-screencast-recorder.js +242 -0
- package/dist/clip-sidecar.d.ts +54 -0
- package/dist/clip-sidecar.js +208 -0
- package/dist/cost-logging.d.ts +1 -1
- package/dist/env-validation.js +38 -4
- package/dist/execution-schema.d.ts +690 -360
- package/dist/execution-schema.js +98 -42
- package/dist/execution-types.d.ts +53 -3
- package/dist/execution-types.js +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/llm-healer.d.ts +2 -10
- package/dist/llm-healer.js +109 -62
- package/dist/llm-provider.js +3 -0
- package/dist/opcode-actions.js +13 -0
- package/dist/opcode-runner.js +21 -12
- package/dist/program-signing.d.ts +1094 -0
- package/dist/program-signing.js +140 -0
- package/dist/provider-config.d.ts +5 -0
- package/dist/provider-config.js +28 -1
- package/dist/recovery-chain.js +40 -16
- package/dist/server-credit-usage.d.ts +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/web-playwright-local.d.ts +31 -1
- package/dist/web-playwright-local.js +207 -37
- package/package.json +12 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const CLIP_BACKEND_ENV_VAR = 'AUTOKAP_CLIP_BACKEND';
|
|
2
|
+
export const CLIP_HEADLESS_POLICY_ENV_VAR = 'AUTOKAP_CLIP_HEADLESS_POLICY';
|
|
3
|
+
const BEGIN_FRAME_LAUNCH_ARGS = [
|
|
4
|
+
'--enable-begin-frame-control',
|
|
5
|
+
'--run-all-compositor-stages-before-draw',
|
|
6
|
+
];
|
|
7
|
+
function normalizeClipCaptureMethod(raw) {
|
|
8
|
+
switch (raw?.trim().toLowerCase()) {
|
|
9
|
+
case 'begin_frame':
|
|
10
|
+
case 'frame_sequence':
|
|
11
|
+
case 'playwright_video':
|
|
12
|
+
return raw.trim().toLowerCase();
|
|
13
|
+
default:
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function normalizeHeadlessPolicy(raw) {
|
|
18
|
+
switch (raw?.trim().toLowerCase()) {
|
|
19
|
+
case 'headless':
|
|
20
|
+
return 'headless';
|
|
21
|
+
case 'headed':
|
|
22
|
+
return 'headed';
|
|
23
|
+
default:
|
|
24
|
+
return 'auto';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function resolveRequestedClipCaptureMethod() {
|
|
28
|
+
return normalizeClipCaptureMethod(process.env[CLIP_BACKEND_ENV_VAR]) ?? 'begin_frame';
|
|
29
|
+
}
|
|
30
|
+
export function resolveClipHeadlessPolicy() {
|
|
31
|
+
return normalizeHeadlessPolicy(process.env[CLIP_HEADLESS_POLICY_ENV_VAR]);
|
|
32
|
+
}
|
|
33
|
+
export async function resolveClipRuntimeSelection(options = {}) {
|
|
34
|
+
const requestedMethod = resolveRequestedClipCaptureMethod();
|
|
35
|
+
const headlessPolicy = resolveClipHeadlessPolicy();
|
|
36
|
+
const platform = options.platform ?? process.platform;
|
|
37
|
+
const browserHeaded = headlessPolicy === 'headed'
|
|
38
|
+
? true
|
|
39
|
+
: headlessPolicy === 'headless'
|
|
40
|
+
? false
|
|
41
|
+
: options.headed ?? false;
|
|
42
|
+
let captureMethod = requestedMethod;
|
|
43
|
+
let fallbackReason;
|
|
44
|
+
let browserLaunchArgs;
|
|
45
|
+
if (requestedMethod === 'begin_frame') {
|
|
46
|
+
if (platform === 'darwin') {
|
|
47
|
+
captureMethod = 'frame_sequence';
|
|
48
|
+
fallbackReason = 'begin_frame is unsupported on macOS Chromium targets; falling back to frame sequence capture';
|
|
49
|
+
}
|
|
50
|
+
else if (browserHeaded) {
|
|
51
|
+
captureMethod = 'frame_sequence';
|
|
52
|
+
fallbackReason = 'begin_frame requires headless Chromium; falling back to frame sequence capture';
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
browserLaunchArgs = [...BEGIN_FRAME_LAUNCH_ARGS];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
requestedMethod,
|
|
60
|
+
captureMethod,
|
|
61
|
+
browserHeaded,
|
|
62
|
+
headlessPolicy,
|
|
63
|
+
fallbackReason,
|
|
64
|
+
browserLaunchArgs,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=clip-runtime.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const DEFAULT_CAPTURE_OUTPUT_SCALE = 2;
|
|
2
|
+
export declare function normalizeCaptureOutputScale(value?: number | null): number | null;
|
|
3
|
+
export declare function resolveEffectiveCaptureOutputScale(requestedScale?: number | null, fallbackScale?: number): number;
|
|
4
|
+
export declare function resolvePhysicalCaptureSize(viewport: {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
} | null, scale: number | null | undefined): {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
} | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const DEFAULT_CAPTURE_OUTPUT_SCALE = 2;
|
|
2
|
+
export function normalizeCaptureOutputScale(value) {
|
|
3
|
+
if (!Number.isFinite(value))
|
|
4
|
+
return null;
|
|
5
|
+
return Math.max(0.5, Math.min(4, Number(value)));
|
|
6
|
+
}
|
|
7
|
+
export function resolveEffectiveCaptureOutputScale(requestedScale, fallbackScale = DEFAULT_CAPTURE_OUTPUT_SCALE) {
|
|
8
|
+
return normalizeCaptureOutputScale(requestedScale)
|
|
9
|
+
?? normalizeCaptureOutputScale(fallbackScale)
|
|
10
|
+
?? DEFAULT_CAPTURE_OUTPUT_SCALE;
|
|
11
|
+
}
|
|
12
|
+
export function resolvePhysicalCaptureSize(viewport, scale) {
|
|
13
|
+
if (!viewport)
|
|
14
|
+
return null;
|
|
15
|
+
const effectiveScale = resolveEffectiveCaptureOutputScale(scale);
|
|
16
|
+
return {
|
|
17
|
+
width: Math.max(2, Math.round(viewport.width * effectiveScale)) & ~1,
|
|
18
|
+
height: Math.max(2, Math.round(viewport.height * effectiveScale)) & ~1,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=clip-scale.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import type { RecordingResult } from './execution-types.js';
|
|
3
|
+
import type { ClipOptions } from './types.js';
|
|
4
|
+
export interface CdpScreencastClipRecorderOptions {
|
|
5
|
+
baseDir?: string;
|
|
6
|
+
viewport: {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
};
|
|
10
|
+
requestedScale?: number;
|
|
11
|
+
effectiveScale: number;
|
|
12
|
+
clipOptions?: ClipOptions;
|
|
13
|
+
sourceFps?: number;
|
|
14
|
+
outputFps?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class CdpScreencastClipRecorder {
|
|
17
|
+
private readonly page;
|
|
18
|
+
private readonly options;
|
|
19
|
+
private readonly sourceFps;
|
|
20
|
+
private readonly outputFps;
|
|
21
|
+
private readonly workingDirPromise;
|
|
22
|
+
private readonly frames;
|
|
23
|
+
private readonly startedAt;
|
|
24
|
+
private finalResult;
|
|
25
|
+
private frameDimensions;
|
|
26
|
+
private pendingWriteError;
|
|
27
|
+
private readonly pendingWrites;
|
|
28
|
+
private cdpSession;
|
|
29
|
+
private stopRequested;
|
|
30
|
+
private started;
|
|
31
|
+
constructor(page: Page, options: CdpScreencastClipRecorderOptions);
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
stop(): Promise<RecordingResult>;
|
|
34
|
+
abort(): Promise<void>;
|
|
35
|
+
private captureInitialFrame;
|
|
36
|
+
private handleScreencastFrame;
|
|
37
|
+
private consumeFrame;
|
|
38
|
+
private queueFrameWrite;
|
|
39
|
+
private waitForPendingWriteCapacity;
|
|
40
|
+
private flushPendingWrites;
|
|
41
|
+
private cleanup;
|
|
42
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { postProcessClipFrames } from './clip-postprocess.js';
|
|
5
|
+
import { buildCdpViewportClip } from './clip-frame-recorder.js';
|
|
6
|
+
import { resolvePhysicalCaptureSize } from './clip-scale.js';
|
|
7
|
+
const DEFAULT_CLIP_SOURCE_FPS = 30;
|
|
8
|
+
const DEFAULT_CLIP_OUTPUT_FPS = 30;
|
|
9
|
+
const SCREENCAST_FRAME_FORMAT = 'jpeg';
|
|
10
|
+
const SCREENCAST_FRAME_QUALITY = 92;
|
|
11
|
+
const MAX_PENDING_FRAME_WRITES = 8;
|
|
12
|
+
export class CdpScreencastClipRecorder {
|
|
13
|
+
page;
|
|
14
|
+
options;
|
|
15
|
+
sourceFps;
|
|
16
|
+
outputFps;
|
|
17
|
+
workingDirPromise;
|
|
18
|
+
frames = [];
|
|
19
|
+
startedAt = Date.now();
|
|
20
|
+
finalResult = null;
|
|
21
|
+
frameDimensions = null;
|
|
22
|
+
pendingWriteError = null;
|
|
23
|
+
pendingWrites = new Set();
|
|
24
|
+
cdpSession = null;
|
|
25
|
+
stopRequested = false;
|
|
26
|
+
started = false;
|
|
27
|
+
constructor(page, options) {
|
|
28
|
+
this.page = page;
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.sourceFps = Math.max(1, Math.round(options.sourceFps ?? DEFAULT_CLIP_SOURCE_FPS));
|
|
31
|
+
this.outputFps = Math.max(1, Math.round(options.outputFps ?? DEFAULT_CLIP_OUTPUT_FPS));
|
|
32
|
+
this.workingDirPromise = options.baseDir
|
|
33
|
+
? Promise.resolve(options.baseDir)
|
|
34
|
+
: fs.mkdtemp(path.join(os.tmpdir(), 'autokap-clip-screencast-'));
|
|
35
|
+
this.frameDimensions = resolvePhysicalCaptureSize(options.viewport, options.effectiveScale)
|
|
36
|
+
?? {
|
|
37
|
+
width: Math.max(2, Math.round(options.viewport.width)) & ~1,
|
|
38
|
+
height: Math.max(2, Math.round(options.viewport.height)) & ~1,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async start() {
|
|
42
|
+
const workingDir = await this.workingDirPromise;
|
|
43
|
+
await fs.mkdir(path.join(workingDir, 'frames'), { recursive: true });
|
|
44
|
+
const context = this.page.context();
|
|
45
|
+
if (typeof context.newCDPSession !== 'function') {
|
|
46
|
+
throw new Error('CDP screencast is not available on this browser context');
|
|
47
|
+
}
|
|
48
|
+
this.cdpSession = await context.newCDPSession(this.page);
|
|
49
|
+
this.cdpSession.on('Page.screencastFrame', this.handleScreencastFrame);
|
|
50
|
+
await this.cdpSession.send('Page.enable').catch(() => undefined);
|
|
51
|
+
await this.captureInitialFrame();
|
|
52
|
+
await this.cdpSession.send('Page.startScreencast', {
|
|
53
|
+
format: SCREENCAST_FRAME_FORMAT,
|
|
54
|
+
quality: SCREENCAST_FRAME_QUALITY,
|
|
55
|
+
maxWidth: this.frameDimensions?.width,
|
|
56
|
+
maxHeight: this.frameDimensions?.height,
|
|
57
|
+
everyNthFrame: 1,
|
|
58
|
+
});
|
|
59
|
+
this.started = true;
|
|
60
|
+
}
|
|
61
|
+
async stop() {
|
|
62
|
+
if (this.finalResult) {
|
|
63
|
+
return this.finalResult;
|
|
64
|
+
}
|
|
65
|
+
if (!this.started || !this.cdpSession) {
|
|
66
|
+
throw new Error('CDP screencast recording was not started');
|
|
67
|
+
}
|
|
68
|
+
this.stopRequested = true;
|
|
69
|
+
const stopAt = Date.now();
|
|
70
|
+
try {
|
|
71
|
+
await this.cdpSession.send('Page.stopScreencast').catch(() => undefined);
|
|
72
|
+
await sleep(100);
|
|
73
|
+
await this.flushPendingWrites();
|
|
74
|
+
if (this.frames.length === 0) {
|
|
75
|
+
throw this.pendingWriteError ?? new Error('clip screencast ended before any frame was recorded');
|
|
76
|
+
}
|
|
77
|
+
if (this.pendingWriteError) {
|
|
78
|
+
throw this.pendingWriteError;
|
|
79
|
+
}
|
|
80
|
+
const workingDir = await this.workingDirPromise;
|
|
81
|
+
const outputDimensions = this.frameDimensions
|
|
82
|
+
?? resolvePhysicalCaptureSize(this.options.viewport, this.options.effectiveScale)
|
|
83
|
+
?? this.options.viewport;
|
|
84
|
+
const frameEntries = buildFrameEntries(this.frames, stopAt, this.sourceFps, this.outputFps);
|
|
85
|
+
const actualSourceFps = resolveActualSourceFps(this.frames, stopAt);
|
|
86
|
+
const processed = await postProcessClipFrames(frameEntries, workingDir, 'clip', {
|
|
87
|
+
...this.options.clipOptions,
|
|
88
|
+
mp4Width: outputDimensions.width,
|
|
89
|
+
mp4Height: outputDimensions.height,
|
|
90
|
+
});
|
|
91
|
+
if (!processed.mp4Path || !processed.thumbnailPath) {
|
|
92
|
+
throw new Error('clip packaging failed: missing MP4 or thumbnail output');
|
|
93
|
+
}
|
|
94
|
+
const [mp4Buffer, gifBuffer, thumbnailBuffer] = await Promise.all([
|
|
95
|
+
fs.readFile(processed.mp4Path),
|
|
96
|
+
processed.gifPath ? fs.readFile(processed.gifPath) : Promise.resolve(null),
|
|
97
|
+
fs.readFile(processed.thumbnailPath),
|
|
98
|
+
]);
|
|
99
|
+
const clipPackage = {
|
|
100
|
+
mp4: {
|
|
101
|
+
buffer: mp4Buffer,
|
|
102
|
+
mimeType: 'video/mp4',
|
|
103
|
+
},
|
|
104
|
+
...(gifBuffer
|
|
105
|
+
? {
|
|
106
|
+
gif: {
|
|
107
|
+
buffer: gifBuffer,
|
|
108
|
+
mimeType: 'image/gif',
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
thumbnail: {
|
|
113
|
+
buffer: thumbnailBuffer,
|
|
114
|
+
mimeType: 'image/png',
|
|
115
|
+
},
|
|
116
|
+
captureMethod: 'cdp_screencast',
|
|
117
|
+
requestedScale: this.options.requestedScale,
|
|
118
|
+
effectiveScale: this.options.effectiveScale,
|
|
119
|
+
sourceFps: this.sourceFps,
|
|
120
|
+
actualSourceFps,
|
|
121
|
+
outputFps: this.outputFps,
|
|
122
|
+
frameCount: this.frames.length,
|
|
123
|
+
dimensions: outputDimensions,
|
|
124
|
+
};
|
|
125
|
+
this.finalResult = {
|
|
126
|
+
buffer: mp4Buffer,
|
|
127
|
+
mimeType: 'video/mp4',
|
|
128
|
+
durationMs: processed.durationMs,
|
|
129
|
+
trimStartMs: 0,
|
|
130
|
+
clipPackage,
|
|
131
|
+
};
|
|
132
|
+
return this.finalResult;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await this.cleanup();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async abort() {
|
|
139
|
+
this.stopRequested = true;
|
|
140
|
+
await this.cleanup();
|
|
141
|
+
}
|
|
142
|
+
async captureInitialFrame() {
|
|
143
|
+
const session = this.cdpSession;
|
|
144
|
+
if (!session) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const scrollOffset = await this.page.evaluate(() => ({
|
|
148
|
+
x: window.scrollX || window.pageXOffset || 0,
|
|
149
|
+
y: window.scrollY || window.pageYOffset || 0,
|
|
150
|
+
})).catch(() => ({ x: 0, y: 0 }));
|
|
151
|
+
const screenshot = await session.send('Page.captureScreenshot', {
|
|
152
|
+
format: SCREENCAST_FRAME_FORMAT,
|
|
153
|
+
quality: SCREENCAST_FRAME_QUALITY,
|
|
154
|
+
clip: buildCdpViewportClip(this.options.viewport, this.options.effectiveScale, scrollOffset),
|
|
155
|
+
}).catch(() => null);
|
|
156
|
+
if (!screenshot?.data) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await this.consumeFrame(Buffer.from(screenshot.data, 'base64'), this.startedAt);
|
|
160
|
+
}
|
|
161
|
+
handleScreencastFrame = (payload) => {
|
|
162
|
+
const session = this.cdpSession;
|
|
163
|
+
if (!session || this.stopRequested) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const ack = session.send('Page.screencastFrameAck', { sessionId: payload.sessionId }).catch(() => undefined);
|
|
167
|
+
void ack;
|
|
168
|
+
void this.consumeFrame(Buffer.from(payload.data, 'base64'), Date.now());
|
|
169
|
+
};
|
|
170
|
+
async consumeFrame(frameBuffer, capturedAt) {
|
|
171
|
+
if (this.pendingWriteError) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const workingDir = await this.workingDirPromise;
|
|
175
|
+
const frameIndex = this.frames.length;
|
|
176
|
+
const framePath = path.join(workingDir, 'frames', `frame-${String(frameIndex).padStart(6, '0')}.jpg`);
|
|
177
|
+
this.frames.push({ path: framePath, capturedAt });
|
|
178
|
+
await this.queueFrameWrite(framePath, frameBuffer);
|
|
179
|
+
}
|
|
180
|
+
async queueFrameWrite(framePath, frameBuffer) {
|
|
181
|
+
await this.waitForPendingWriteCapacity();
|
|
182
|
+
if (this.pendingWriteError) {
|
|
183
|
+
throw this.pendingWriteError;
|
|
184
|
+
}
|
|
185
|
+
let writePromise;
|
|
186
|
+
writePromise = fs.writeFile(framePath, frameBuffer)
|
|
187
|
+
.catch((error) => {
|
|
188
|
+
this.pendingWriteError = error instanceof Error ? error : new Error(String(error));
|
|
189
|
+
throw this.pendingWriteError;
|
|
190
|
+
})
|
|
191
|
+
.finally(() => {
|
|
192
|
+
this.pendingWrites.delete(writePromise);
|
|
193
|
+
});
|
|
194
|
+
this.pendingWrites.add(writePromise);
|
|
195
|
+
}
|
|
196
|
+
async waitForPendingWriteCapacity() {
|
|
197
|
+
while (this.pendingWrites.size >= MAX_PENDING_FRAME_WRITES) {
|
|
198
|
+
await Promise.race(this.pendingWrites);
|
|
199
|
+
if (this.pendingWriteError) {
|
|
200
|
+
throw this.pendingWriteError;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async flushPendingWrites() {
|
|
205
|
+
while (this.pendingWrites.size > 0) {
|
|
206
|
+
await Promise.allSettled(Array.from(this.pendingWrites));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async cleanup() {
|
|
210
|
+
const workingDir = await this.workingDirPromise;
|
|
211
|
+
this.cdpSession?.off?.('Page.screencastFrame', this.handleScreencastFrame);
|
|
212
|
+
await this.cdpSession?.detach().catch(() => undefined);
|
|
213
|
+
this.cdpSession = null;
|
|
214
|
+
this.started = false;
|
|
215
|
+
await fs.rm(workingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function buildFrameEntries(frames, stopAt, sourceFps, outputFps) {
|
|
219
|
+
const entries = [];
|
|
220
|
+
const minFrameDurationMs = Math.max(1, Math.min(Math.round(1000 / sourceFps), Math.round(1000 / outputFps)));
|
|
221
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
222
|
+
const frame = frames[index];
|
|
223
|
+
const nextTimestamp = frames[index + 1]?.capturedAt ?? stopAt;
|
|
224
|
+
entries.push({
|
|
225
|
+
path: frame.path,
|
|
226
|
+
durationMs: Math.max(minFrameDurationMs, nextTimestamp - frame.capturedAt),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return entries;
|
|
230
|
+
}
|
|
231
|
+
function resolveActualSourceFps(frames, stopAt) {
|
|
232
|
+
if (frames.length <= 1) {
|
|
233
|
+
return frames.length;
|
|
234
|
+
}
|
|
235
|
+
const firstTimestamp = frames[0].capturedAt;
|
|
236
|
+
const durationSec = Math.max(0.001, (stopAt - firstTimestamp) / 1000);
|
|
237
|
+
return Math.round((frames.length / durationSec) * 100) / 100;
|
|
238
|
+
}
|
|
239
|
+
function sleep(ms) {
|
|
240
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=clip-screencast-recorder.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface ClipSidecarProbeResult {
|
|
2
|
+
supported: boolean;
|
|
3
|
+
supportsHeadless: boolean;
|
|
4
|
+
permissionGranted?: boolean;
|
|
5
|
+
executablePath?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
backend?: string;
|
|
8
|
+
reason?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface NativeClipSidecarStartParams {
|
|
11
|
+
targetId: string;
|
|
12
|
+
outputPath: string;
|
|
13
|
+
viewport: {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
effectiveScale: number;
|
|
18
|
+
outputWidth: number;
|
|
19
|
+
outputHeight: number;
|
|
20
|
+
targetFps: number;
|
|
21
|
+
includeCursor: boolean;
|
|
22
|
+
headed: boolean;
|
|
23
|
+
windowTitle: string;
|
|
24
|
+
cropRect?: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface NativeClipSidecarStopResult {
|
|
32
|
+
mp4Path: string;
|
|
33
|
+
actualSourceFps?: number;
|
|
34
|
+
frameCount?: number;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
durationMs?: number;
|
|
38
|
+
}
|
|
39
|
+
interface NativeClipSidecarSessionOptions {
|
|
40
|
+
executablePath: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class NativeClipSidecarSession {
|
|
43
|
+
private readonly child;
|
|
44
|
+
private readonly pending;
|
|
45
|
+
private stderr;
|
|
46
|
+
private closed;
|
|
47
|
+
constructor(options: NativeClipSidecarSessionOptions);
|
|
48
|
+
request<T>(command: string, payload?: object): Promise<T>;
|
|
49
|
+
close(): Promise<void>;
|
|
50
|
+
private handleEnvelope;
|
|
51
|
+
private rejectAll;
|
|
52
|
+
}
|
|
53
|
+
export declare function probeNativeClipSidecar(): Promise<ClipSidecarProbeResult>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { getConfigDir } from './cli-config.js';
|
|
6
|
+
const SIDECAR_BINARY_ENV_VAR = 'AUTOKAP_CLIP_SIDECAR_PATH';
|
|
7
|
+
const SIDECAR_BASE_URL_ENV_VAR = 'AUTOKAP_CLIP_SIDECAR_BASE_URL';
|
|
8
|
+
const DEFAULT_SIDECAR_BASE_URL = 'https://github.com/mangue-dev/screenshot-agent/releases/latest/download';
|
|
9
|
+
const SIDECAR_NAME = 'autokap-clip-capture-sidecar';
|
|
10
|
+
function resolveSidecarAssetSpec() {
|
|
11
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
12
|
+
const platform = process.platform;
|
|
13
|
+
const arch = process.arch;
|
|
14
|
+
if (!['darwin', 'linux', 'win32'].includes(platform)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (!['arm64', 'x64'].includes(arch)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const fileName = `${SIDECAR_NAME}-${platform}-${arch}${ext}`;
|
|
21
|
+
return {
|
|
22
|
+
fileName,
|
|
23
|
+
executableName: `${SIDECAR_NAME}${ext}`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function fileExists(filePath) {
|
|
27
|
+
try {
|
|
28
|
+
await fs.stat(filePath);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function ensureDownloadedSidecar() {
|
|
36
|
+
const explicitPath = process.env[SIDECAR_BINARY_ENV_VAR]?.trim();
|
|
37
|
+
if (explicitPath) {
|
|
38
|
+
return explicitPath;
|
|
39
|
+
}
|
|
40
|
+
const workspaceBinary = path.resolve(process.cwd(), 'native', 'clip-capture-sidecar', 'target', 'release', resolveSidecarAssetSpec()?.executableName ?? SIDECAR_NAME);
|
|
41
|
+
if (await fileExists(workspaceBinary)) {
|
|
42
|
+
return workspaceBinary;
|
|
43
|
+
}
|
|
44
|
+
const asset = resolveSidecarAssetSpec();
|
|
45
|
+
if (!asset) {
|
|
46
|
+
throw new Error(`native clip sidecar is not available on ${process.platform}/${process.arch}`);
|
|
47
|
+
}
|
|
48
|
+
const sidecarDir = path.join(getConfigDir(), 'sidecars', 'clip-capture');
|
|
49
|
+
const binaryPath = path.join(sidecarDir, asset.executableName);
|
|
50
|
+
if (await fileExists(binaryPath)) {
|
|
51
|
+
return binaryPath;
|
|
52
|
+
}
|
|
53
|
+
const baseUrl = process.env[SIDECAR_BASE_URL_ENV_VAR]?.trim() || DEFAULT_SIDECAR_BASE_URL;
|
|
54
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, '')}/${asset.fileName}`);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`failed to download native clip sidecar (${response.status})`);
|
|
57
|
+
}
|
|
58
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
59
|
+
await fs.mkdir(sidecarDir, { recursive: true });
|
|
60
|
+
await fs.writeFile(binaryPath, bytes);
|
|
61
|
+
if (process.platform !== 'win32') {
|
|
62
|
+
await fs.chmod(binaryPath, 0o755);
|
|
63
|
+
}
|
|
64
|
+
return binaryPath;
|
|
65
|
+
}
|
|
66
|
+
function parseLineDelimitedJson(stream, onMessage, onInvalid) {
|
|
67
|
+
let buffer = '';
|
|
68
|
+
stream.on('data', (chunk) => {
|
|
69
|
+
buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
70
|
+
while (true) {
|
|
71
|
+
const newlineIndex = buffer.indexOf('\n');
|
|
72
|
+
if (newlineIndex === -1) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
76
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
77
|
+
if (!line) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
onMessage(JSON.parse(line));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
onInvalid(line);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export class NativeClipSidecarSession {
|
|
90
|
+
child;
|
|
91
|
+
pending = new Map();
|
|
92
|
+
stderr = '';
|
|
93
|
+
closed = false;
|
|
94
|
+
constructor(options) {
|
|
95
|
+
this.child = spawn(options.executablePath, [], {
|
|
96
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
windowsHide: true,
|
|
98
|
+
});
|
|
99
|
+
parseLineDelimitedJson(this.child.stdout, (payload) => this.handleEnvelope(payload), (raw) => this.rejectAll(new Error(`native sidecar emitted invalid JSON: ${raw}`)));
|
|
100
|
+
this.child.stderr.on('data', (chunk) => {
|
|
101
|
+
this.stderr += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
102
|
+
});
|
|
103
|
+
this.child.on('error', (error) => {
|
|
104
|
+
this.rejectAll(error instanceof Error ? error : new Error(String(error)));
|
|
105
|
+
});
|
|
106
|
+
this.child.on('exit', (code, signal) => {
|
|
107
|
+
this.closed = true;
|
|
108
|
+
if (this.pending.size === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const suffix = this.stderr.trim() ? ` — ${this.stderr.trim()}` : '';
|
|
112
|
+
this.rejectAll(new Error(`native sidecar exited before completing a request (code=${code ?? 'null'}, signal=${signal ?? 'null'})${suffix}`));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async request(command, payload = {}) {
|
|
116
|
+
if (this.closed) {
|
|
117
|
+
throw new Error('native sidecar session is already closed');
|
|
118
|
+
}
|
|
119
|
+
const id = randomUUID();
|
|
120
|
+
const request = JSON.stringify({
|
|
121
|
+
id,
|
|
122
|
+
command,
|
|
123
|
+
...payload,
|
|
124
|
+
});
|
|
125
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
126
|
+
this.pending.set(id, {
|
|
127
|
+
resolve: (value) => resolve(value),
|
|
128
|
+
reject,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
this.child.stdin.write(`${request}\n`);
|
|
132
|
+
return responsePromise;
|
|
133
|
+
}
|
|
134
|
+
async close() {
|
|
135
|
+
if (this.closed) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.closed = true;
|
|
139
|
+
this.child.kill();
|
|
140
|
+
}
|
|
141
|
+
handleEnvelope(payload) {
|
|
142
|
+
if (!payload.id) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const pending = this.pending.get(payload.id);
|
|
146
|
+
if (!pending) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.pending.delete(payload.id);
|
|
150
|
+
if (payload.ok) {
|
|
151
|
+
pending.resolve(payload.result);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
pending.reject(new Error(payload.error?.message ?? 'native sidecar request failed'));
|
|
155
|
+
}
|
|
156
|
+
rejectAll(error) {
|
|
157
|
+
for (const pending of this.pending.values()) {
|
|
158
|
+
pending.reject(error);
|
|
159
|
+
}
|
|
160
|
+
this.pending.clear();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function probeNativeClipSidecar() {
|
|
164
|
+
let executablePath;
|
|
165
|
+
try {
|
|
166
|
+
executablePath = await ensureDownloadedSidecar();
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
supported: false,
|
|
171
|
+
supportsHeadless: false,
|
|
172
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const session = new NativeClipSidecarSession({ executablePath });
|
|
176
|
+
try {
|
|
177
|
+
const result = await session.request('probe');
|
|
178
|
+
if (result.supported !== true || typeof result.supportsHeadless !== 'boolean') {
|
|
179
|
+
return {
|
|
180
|
+
supported: false,
|
|
181
|
+
supportsHeadless: false,
|
|
182
|
+
executablePath,
|
|
183
|
+
reason: 'native clip sidecar returned an invalid probe response',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
supported: true,
|
|
188
|
+
supportsHeadless: result.supportsHeadless,
|
|
189
|
+
permissionGranted: result.permissionGranted !== false,
|
|
190
|
+
executablePath,
|
|
191
|
+
version: result.version,
|
|
192
|
+
backend: result.backend,
|
|
193
|
+
reason: result.reason,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return {
|
|
198
|
+
supported: false,
|
|
199
|
+
supportsHeadless: false,
|
|
200
|
+
executablePath,
|
|
201
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
await session.close();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=clip-sidecar.js.map
|
package/dist/cost-logging.d.ts
CHANGED