@playdrop/playdrop-cli 0.12.1 → 0.12.2
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/config/client-meta.json +1 -1
- package/dist/commands/review.d.ts +0 -20
- package/dist/commands/review.js +1 -112
- package/dist/commands/worker/instrument-evidence.d.ts +17 -0
- package/dist/commands/worker/instrument-evidence.js +239 -0
- package/dist/commands/worker/runtime.d.ts +1 -0
- package/dist/commands/worker/runtime.js +11 -0
- package/dist/commands/worker.d.ts +26 -1
- package/dist/commands/worker.js +263 -10
- package/dist/index.js +19 -17
- package/node_modules/@playdrop/config/client-meta.json +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +17 -0
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
- package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.js +3 -1
- package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts +41 -0
- package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/instrument-evidence.js +215 -0
- package/package.json +1 -1
package/config/client-meta.json
CHANGED
|
@@ -33,26 +33,6 @@ export type ReviewRatingCardOptions = {
|
|
|
33
33
|
punchline?: string;
|
|
34
34
|
title?: string;
|
|
35
35
|
};
|
|
36
|
-
export type ReviewListWindowsOptions = {
|
|
37
|
-
pid: string;
|
|
38
|
-
};
|
|
39
|
-
export type ReviewScreenshotOptions = {
|
|
40
|
-
metadata?: string;
|
|
41
|
-
out: string;
|
|
42
|
-
pid: string;
|
|
43
|
-
windowId?: string;
|
|
44
|
-
};
|
|
45
|
-
export declare function buildReviewListWindowsRecorderArgs(options: ReviewListWindowsOptions): string[];
|
|
46
|
-
export declare function buildReviewScreenshotRecorderArgs(options: ReviewScreenshotOptions): {
|
|
47
|
-
args: string[];
|
|
48
|
-
metadataPath: string;
|
|
49
|
-
outputPath: string;
|
|
50
|
-
};
|
|
51
|
-
export declare function listReviewCaptureWindows(options: ReviewListWindowsOptions): Promise<void>;
|
|
52
|
-
export declare function captureReviewScreenshot(options: ReviewScreenshotOptions): Promise<{
|
|
53
|
-
metadataPath: string;
|
|
54
|
-
outputPath: string;
|
|
55
|
-
}>;
|
|
56
36
|
export declare function validateGameReviewResult(input: ValidateGameReviewResultInput): Promise<ReviewValidationResult>;
|
|
57
37
|
export declare function validateReviewResultCommand(options: ReviewValidateResultOptions): Promise<void>;
|
|
58
38
|
export declare function composeReviewEvidence(options: ReviewComposeEvidenceOptions): Promise<{
|
package/dist/commands/review.js
CHANGED
|
@@ -4,20 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.REQUIRED_REVIEW_EVIDENCE_FILES = exports.REVIEW_CRITERIA = void 0;
|
|
7
|
-
exports.buildReviewListWindowsRecorderArgs = buildReviewListWindowsRecorderArgs;
|
|
8
|
-
exports.buildReviewScreenshotRecorderArgs = buildReviewScreenshotRecorderArgs;
|
|
9
|
-
exports.listReviewCaptureWindows = listReviewCaptureWindows;
|
|
10
|
-
exports.captureReviewScreenshot = captureReviewScreenshot;
|
|
11
7
|
exports.validateGameReviewResult = validateGameReviewResult;
|
|
12
8
|
exports.validateReviewResultCommand = validateReviewResultCommand;
|
|
13
9
|
exports.composeReviewEvidence = composeReviewEvidence;
|
|
14
10
|
exports.createReviewRatingCard = createReviewRatingCard;
|
|
15
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
-
const node_child_process_1 = require("node:child_process");
|
|
17
12
|
const node_fs_1 = require("node:fs");
|
|
18
13
|
const sharp_1 = __importDefault(require("sharp"));
|
|
19
14
|
const output_1 = require("../output");
|
|
20
|
-
const captureListing_1 = require("./captureListing");
|
|
21
15
|
exports.REVIEW_CRITERIA = [
|
|
22
16
|
'Gameplay / Core Loop',
|
|
23
17
|
'Depth / Replayability',
|
|
@@ -45,6 +39,7 @@ const STATE_OUTCOME = {
|
|
|
45
39
|
PASSED: 'Passed',
|
|
46
40
|
};
|
|
47
41
|
exports.REQUIRED_REVIEW_EVIDENCE_FILES = [
|
|
42
|
+
'first-frame.png',
|
|
48
43
|
'core.png',
|
|
49
44
|
'win.png',
|
|
50
45
|
'loss.png',
|
|
@@ -52,7 +47,6 @@ exports.REQUIRED_REVIEW_EVIDENCE_FILES = [
|
|
|
52
47
|
'rating-card.png',
|
|
53
48
|
];
|
|
54
49
|
const REVIEW_EVIDENCE_MAX_BYTES = 10 * 1024 * 1024;
|
|
55
|
-
const REVIEW_RECORDER_TIMEOUT_MS = 15000;
|
|
56
50
|
function resolveFilePath(filePath, code) {
|
|
57
51
|
const normalized = typeof filePath === 'string' ? filePath.trim() : '';
|
|
58
52
|
if (!normalized) {
|
|
@@ -174,111 +168,6 @@ async function assertPngFile(filePath) {
|
|
|
174
168
|
}
|
|
175
169
|
}
|
|
176
170
|
}
|
|
177
|
-
async function assertNonBlankPngFile(filePath) {
|
|
178
|
-
await assertPngFile(filePath);
|
|
179
|
-
const image = (0, sharp_1.default)(filePath);
|
|
180
|
-
const metadata = await image.metadata();
|
|
181
|
-
if (!metadata.width || !metadata.height) {
|
|
182
|
-
throw new Error(`review_screenshot_invalid_image:${filePath}`);
|
|
183
|
-
}
|
|
184
|
-
const stats = await image.stats();
|
|
185
|
-
const colorChannels = stats.channels.slice(0, 3);
|
|
186
|
-
const brightest = Math.max(...colorChannels.map((channel) => channel.max));
|
|
187
|
-
if (brightest <= 5) {
|
|
188
|
-
throw new Error(`review_screenshot_blank:${filePath}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
function parsePositiveInteger(value, code) {
|
|
192
|
-
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
193
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
194
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0 || String(parsed) !== normalized) {
|
|
195
|
-
throw new Error(code);
|
|
196
|
-
}
|
|
197
|
-
return parsed;
|
|
198
|
-
}
|
|
199
|
-
function buildReviewListWindowsRecorderArgs(options) {
|
|
200
|
-
const pid = parsePositiveInteger(options.pid, 'invalid_review_window_pid');
|
|
201
|
-
return ['--list-windows', '--pid', String(pid)];
|
|
202
|
-
}
|
|
203
|
-
function buildReviewScreenshotRecorderArgs(options) {
|
|
204
|
-
const pid = parsePositiveInteger(options.pid, 'invalid_review_window_pid');
|
|
205
|
-
const outputPath = resolveFilePath(options.out, 'missing_out');
|
|
206
|
-
const metadataPath = options.metadata
|
|
207
|
-
? resolveFilePath(options.metadata, 'invalid_review_screenshot_metadata')
|
|
208
|
-
: outputPath.replace(/\.png$/i, '') + '.metadata.json';
|
|
209
|
-
const args = [
|
|
210
|
-
'--pid',
|
|
211
|
-
String(pid),
|
|
212
|
-
'--screenshot',
|
|
213
|
-
outputPath,
|
|
214
|
-
'--metadata',
|
|
215
|
-
metadataPath,
|
|
216
|
-
];
|
|
217
|
-
if (options.windowId !== undefined && options.windowId !== '') {
|
|
218
|
-
const windowId = parsePositiveInteger(options.windowId, 'invalid_review_window_id');
|
|
219
|
-
args.push('--window-id', String(windowId));
|
|
220
|
-
}
|
|
221
|
-
return { args, metadataPath, outputPath };
|
|
222
|
-
}
|
|
223
|
-
function runRecorder(args) {
|
|
224
|
-
return new Promise((resolve, reject) => {
|
|
225
|
-
const recorderPath = (0, captureListing_1.resolveListingRecorderPath)();
|
|
226
|
-
const child = (0, node_child_process_1.spawn)(recorderPath, args, {
|
|
227
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
228
|
-
});
|
|
229
|
-
let stdout = '';
|
|
230
|
-
let stderr = '';
|
|
231
|
-
let settled = false;
|
|
232
|
-
const timer = setTimeout(() => {
|
|
233
|
-
if (settled) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
settled = true;
|
|
237
|
-
child.kill('SIGKILL');
|
|
238
|
-
reject(new Error(`review_recorder_timeout:${REVIEW_RECORDER_TIMEOUT_MS}`));
|
|
239
|
-
}, REVIEW_RECORDER_TIMEOUT_MS);
|
|
240
|
-
child.stdout.on('data', (chunk) => {
|
|
241
|
-
stdout += chunk.toString();
|
|
242
|
-
});
|
|
243
|
-
child.stderr.on('data', (chunk) => {
|
|
244
|
-
stderr += chunk.toString();
|
|
245
|
-
});
|
|
246
|
-
child.on('error', (error) => {
|
|
247
|
-
if (settled) {
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
settled = true;
|
|
251
|
-
clearTimeout(timer);
|
|
252
|
-
reject(new Error(`review_recorder_unavailable:${recorderPath}:${error.message}`));
|
|
253
|
-
});
|
|
254
|
-
child.on('close', (code) => {
|
|
255
|
-
if (settled) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
settled = true;
|
|
259
|
-
clearTimeout(timer);
|
|
260
|
-
if (code === 0) {
|
|
261
|
-
resolve(stdout);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const detail = (stderr || stdout || `exit ${code}`).trim();
|
|
265
|
-
reject(new Error(`review_recorder_failed:${detail}`));
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
async function listReviewCaptureWindows(options) {
|
|
270
|
-
const output = await runRecorder(buildReviewListWindowsRecorderArgs(options));
|
|
271
|
-
process.stdout.write(output);
|
|
272
|
-
}
|
|
273
|
-
async function captureReviewScreenshot(options) {
|
|
274
|
-
const { args, metadataPath, outputPath } = buildReviewScreenshotRecorderArgs(options);
|
|
275
|
-
await node_fs_1.promises.mkdir(node_path_1.default.dirname(outputPath), { recursive: true });
|
|
276
|
-
await node_fs_1.promises.mkdir(node_path_1.default.dirname(metadataPath), { recursive: true });
|
|
277
|
-
await runRecorder(args);
|
|
278
|
-
await assertNonBlankPngFile(outputPath);
|
|
279
|
-
(0, output_1.printSuccess)(`Review screenshot written to ${outputPath}`);
|
|
280
|
-
return { metadataPath, outputPath };
|
|
281
|
-
}
|
|
282
171
|
async function validateGameReviewResult(input) {
|
|
283
172
|
const normalizedState = String(input.reviewState || '').trim().toUpperCase();
|
|
284
173
|
if (!TERMINAL_REVIEW_STATES.has(normalizedState)) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type InstrumentEvidenceCapture, type InstrumentEvidenceManifest } from '@playdrop/types';
|
|
2
|
+
export declare const INSTRUMENT_EVIDENCE_MANIFEST_FILE = "instrument-evidence.json";
|
|
3
|
+
export declare function createWorkerInstrumentScreenshotCollector(workspaceDir: string): {
|
|
4
|
+
push: (chunk: string) => number;
|
|
5
|
+
finish: () => number;
|
|
6
|
+
};
|
|
7
|
+
export declare function saveInstrumentEvidenceCapture(input: {
|
|
8
|
+
workspaceDir: string;
|
|
9
|
+
evidenceDir: string;
|
|
10
|
+
groupId: string;
|
|
11
|
+
name: string;
|
|
12
|
+
screenshotId: string;
|
|
13
|
+
}): Promise<InstrumentEvidenceCapture>;
|
|
14
|
+
export declare function readAndValidateInstrumentEvidenceManifest(input: {
|
|
15
|
+
workspaceDir: string;
|
|
16
|
+
evidenceDir: string;
|
|
17
|
+
}): InstrumentEvidenceManifest;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.INSTRUMENT_EVIDENCE_MANIFEST_FILE = void 0;
|
|
7
|
+
exports.createWorkerInstrumentScreenshotCollector = createWorkerInstrumentScreenshotCollector;
|
|
8
|
+
exports.saveInstrumentEvidenceCapture = saveInstrumentEvidenceCapture;
|
|
9
|
+
exports.readAndValidateInstrumentEvidenceManifest = readAndValidateInstrumentEvidenceManifest;
|
|
10
|
+
const types_1 = require("@playdrop/types");
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
15
|
+
const SCREENSHOT_STORE_DIR = '.playdrop-instrument-screenshots';
|
|
16
|
+
const SCREENSHOT_INDEX_FILE = 'index.json';
|
|
17
|
+
exports.INSTRUMENT_EVIDENCE_MANIFEST_FILE = 'instrument-evidence.json';
|
|
18
|
+
const GROUP_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
19
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
20
|
+
function sha256(buffer) {
|
|
21
|
+
return (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
function isPathInside(parent, child) {
|
|
24
|
+
const relative = node_path_1.default.relative(parent, child);
|
|
25
|
+
return relative === '' || (!!relative && !relative.startsWith('..') && !node_path_1.default.isAbsolute(relative));
|
|
26
|
+
}
|
|
27
|
+
function writeJsonAtomic(filePath, value) {
|
|
28
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(filePath), { recursive: true });
|
|
29
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
30
|
+
(0, node_fs_1.writeFileSync)(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
31
|
+
(0, node_fs_1.renameSync)(tempPath, filePath);
|
|
32
|
+
}
|
|
33
|
+
function readJsonRecord(filePath, code) {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(filePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw new Error(`${code}:${error instanceof Error ? error.message : String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
42
|
+
throw new Error(code);
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
function screenshotStorePath(workspaceDir) {
|
|
47
|
+
return node_path_1.default.join(workspaceDir, SCREENSHOT_STORE_DIR);
|
|
48
|
+
}
|
|
49
|
+
function screenshotIndexPath(workspaceDir) {
|
|
50
|
+
return node_path_1.default.join(screenshotStorePath(workspaceDir), SCREENSHOT_INDEX_FILE);
|
|
51
|
+
}
|
|
52
|
+
function readScreenshotIndex(workspaceDir) {
|
|
53
|
+
const filePath = screenshotIndexPath(workspaceDir);
|
|
54
|
+
if (!(0, node_fs_1.existsSync)(filePath)) {
|
|
55
|
+
return { version: 1, screenshots: {} };
|
|
56
|
+
}
|
|
57
|
+
const parsed = readJsonRecord(filePath, 'invalid_instrument_screenshot_index');
|
|
58
|
+
const screenshots = parsed['screenshots'];
|
|
59
|
+
if (parsed['version'] !== 1 || !screenshots || typeof screenshots !== 'object' || Array.isArray(screenshots)) {
|
|
60
|
+
throw new Error('invalid_instrument_screenshot_index');
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
function extensionForMediaType(mediaType) {
|
|
65
|
+
return mediaType === 'image/jpeg' ? 'jpg' : 'png';
|
|
66
|
+
}
|
|
67
|
+
function createWorkerInstrumentScreenshotCollector(workspaceDir) {
|
|
68
|
+
const resolvedWorkspace = node_path_1.default.resolve(workspaceDir);
|
|
69
|
+
const parser = new types_1.ClaudeChromeScreenshotStreamParser((screenshot) => {
|
|
70
|
+
const buffer = Buffer.from(screenshot.dataBase64, 'base64');
|
|
71
|
+
if (buffer.length <= 0) {
|
|
72
|
+
throw new Error(`invalid_claude_screenshot_payload:${screenshot.screenshotId}`);
|
|
73
|
+
}
|
|
74
|
+
const storeDir = screenshotStorePath(resolvedWorkspace);
|
|
75
|
+
(0, node_fs_1.mkdirSync)(storeDir, { recursive: true });
|
|
76
|
+
const fileName = `${screenshot.screenshotId}.${extensionForMediaType(screenshot.mediaType)}`;
|
|
77
|
+
const filePath = node_path_1.default.join(storeDir, fileName);
|
|
78
|
+
(0, node_fs_1.writeFileSync)(filePath, buffer);
|
|
79
|
+
const index = readScreenshotIndex(resolvedWorkspace);
|
|
80
|
+
index.screenshots[screenshot.screenshotId] = {
|
|
81
|
+
screenshotId: screenshot.screenshotId,
|
|
82
|
+
tabId: screenshot.tabId,
|
|
83
|
+
toolUseId: screenshot.toolUseId,
|
|
84
|
+
mediaType: screenshot.mediaType,
|
|
85
|
+
fileName,
|
|
86
|
+
sha256: sha256(buffer),
|
|
87
|
+
capturedAt: new Date().toISOString(),
|
|
88
|
+
width: screenshot.width,
|
|
89
|
+
height: screenshot.height,
|
|
90
|
+
};
|
|
91
|
+
writeJsonAtomic(screenshotIndexPath(resolvedWorkspace), index);
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
push: (chunk) => parser.push(chunk),
|
|
95
|
+
finish: () => parser.finish(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function normalizeEvidenceName(value) {
|
|
99
|
+
const normalized = value.trim();
|
|
100
|
+
if (!types_1.INSTRUMENT_EVIDENCE_NAMES.includes(normalized)) {
|
|
101
|
+
throw new Error(`invalid_instrument_evidence_name:${value}`);
|
|
102
|
+
}
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
function normalizeGroupId(value) {
|
|
106
|
+
const normalized = value.trim().toLowerCase();
|
|
107
|
+
if (!GROUP_ID_PATTERN.test(normalized)) {
|
|
108
|
+
throw new Error(`invalid_instrument_evidence_group:${value}`);
|
|
109
|
+
}
|
|
110
|
+
return normalized;
|
|
111
|
+
}
|
|
112
|
+
function resolveEvidenceDir(workspaceDir, evidenceDir) {
|
|
113
|
+
const resolvedWorkspace = node_path_1.default.resolve(workspaceDir);
|
|
114
|
+
const resolvedEvidenceDir = node_path_1.default.resolve(evidenceDir);
|
|
115
|
+
if (!isPathInside(resolvedWorkspace, resolvedEvidenceDir)) {
|
|
116
|
+
throw new Error('instrument_evidence_dir_outside_workspace');
|
|
117
|
+
}
|
|
118
|
+
return resolvedEvidenceDir;
|
|
119
|
+
}
|
|
120
|
+
function readManifestIfPresent(evidenceDir) {
|
|
121
|
+
const filePath = node_path_1.default.join(evidenceDir, exports.INSTRUMENT_EVIDENCE_MANIFEST_FILE);
|
|
122
|
+
if (!(0, node_fs_1.existsSync)(filePath)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return readJsonRecord(filePath, 'invalid_instrument_evidence_manifest');
|
|
126
|
+
}
|
|
127
|
+
async function saveInstrumentEvidenceCapture(input) {
|
|
128
|
+
const evidenceName = normalizeEvidenceName(input.name);
|
|
129
|
+
const groupId = normalizeGroupId(input.groupId);
|
|
130
|
+
const screenshotId = input.screenshotId.trim();
|
|
131
|
+
const index = readScreenshotIndex(input.workspaceDir);
|
|
132
|
+
const screenshot = index.screenshots[screenshotId];
|
|
133
|
+
if (!screenshot) {
|
|
134
|
+
throw new Error(`instrument_screenshot_not_found:${screenshotId}`);
|
|
135
|
+
}
|
|
136
|
+
const evidenceDir = resolveEvidenceDir(input.workspaceDir, input.evidenceDir);
|
|
137
|
+
(0, node_fs_1.mkdirSync)(evidenceDir, { recursive: true });
|
|
138
|
+
const existing = readManifestIfPresent(evidenceDir);
|
|
139
|
+
if (existing && existing.groupId !== groupId) {
|
|
140
|
+
throw new Error(`instrument_evidence_group_mismatch:${existing.groupId}:${groupId}`);
|
|
141
|
+
}
|
|
142
|
+
if (existing && existing.controlledTabId !== screenshot.tabId) {
|
|
143
|
+
throw new Error(`instrument_evidence_tab_mismatch:${existing.controlledTabId}:${screenshot.tabId}`);
|
|
144
|
+
}
|
|
145
|
+
const fileName = `${evidenceName}.png`;
|
|
146
|
+
const sourcePath = node_path_1.default.join(screenshotStorePath(input.workspaceDir), screenshot.fileName);
|
|
147
|
+
const outputPath = node_path_1.default.join(evidenceDir, fileName);
|
|
148
|
+
const sourceBuffer = (0, node_fs_1.readFileSync)(sourcePath);
|
|
149
|
+
const outputBuffer = await (0, sharp_1.default)(sourceBuffer).png().toBuffer();
|
|
150
|
+
(0, node_fs_1.writeFileSync)(outputPath, outputBuffer);
|
|
151
|
+
const outputHash = sha256(outputBuffer);
|
|
152
|
+
const capture = {
|
|
153
|
+
name: evidenceName,
|
|
154
|
+
screenshotId,
|
|
155
|
+
tabId: screenshot.tabId,
|
|
156
|
+
toolUseId: screenshot.toolUseId,
|
|
157
|
+
mediaType: screenshot.mediaType,
|
|
158
|
+
fileName,
|
|
159
|
+
sourceSha256: screenshot.sha256,
|
|
160
|
+
sha256: outputHash,
|
|
161
|
+
capturedAt: screenshot.capturedAt,
|
|
162
|
+
};
|
|
163
|
+
const manifest = existing ?? {
|
|
164
|
+
version: 1,
|
|
165
|
+
groupId,
|
|
166
|
+
controlledTabId: screenshot.tabId,
|
|
167
|
+
captures: {},
|
|
168
|
+
};
|
|
169
|
+
manifest.captures[evidenceName] = capture;
|
|
170
|
+
writeJsonAtomic(node_path_1.default.join(evidenceDir, exports.INSTRUMENT_EVIDENCE_MANIFEST_FILE), manifest);
|
|
171
|
+
return capture;
|
|
172
|
+
}
|
|
173
|
+
function normalizeCapture(value, name) {
|
|
174
|
+
const capture = value && typeof value === 'object' && !Array.isArray(value)
|
|
175
|
+
? value
|
|
176
|
+
: null;
|
|
177
|
+
const screenshotId = typeof capture?.['screenshotId'] === 'string' ? capture['screenshotId'].trim() : '';
|
|
178
|
+
const tabId = Number(capture?.['tabId']);
|
|
179
|
+
const toolUseId = typeof capture?.['toolUseId'] === 'string' ? capture['toolUseId'].trim() : '';
|
|
180
|
+
const mediaType = capture?.['mediaType'];
|
|
181
|
+
const fileName = typeof capture?.['fileName'] === 'string' ? capture['fileName'].trim() : '';
|
|
182
|
+
const hash = typeof capture?.['sha256'] === 'string' ? capture['sha256'].trim().toLowerCase() : '';
|
|
183
|
+
const sourceHash = typeof capture?.['sourceSha256'] === 'string' ? capture['sourceSha256'].trim().toLowerCase() : '';
|
|
184
|
+
const capturedAt = typeof capture?.['capturedAt'] === 'string' ? capture['capturedAt'].trim() : '';
|
|
185
|
+
if (capture?.['name'] !== name
|
|
186
|
+
|| !screenshotId
|
|
187
|
+
|| !Number.isInteger(tabId)
|
|
188
|
+
|| tabId <= 0
|
|
189
|
+
|| !toolUseId
|
|
190
|
+
|| (mediaType !== 'image/jpeg' && mediaType !== 'image/png')
|
|
191
|
+
|| node_path_1.default.basename(fileName) !== fileName
|
|
192
|
+
|| !fileName
|
|
193
|
+
|| !SHA256_PATTERN.test(sourceHash)
|
|
194
|
+
|| !SHA256_PATTERN.test(hash)
|
|
195
|
+
|| !capturedAt) {
|
|
196
|
+
throw new Error(`invalid_instrument_evidence_capture:${name}`);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
name,
|
|
200
|
+
screenshotId,
|
|
201
|
+
tabId,
|
|
202
|
+
toolUseId,
|
|
203
|
+
mediaType,
|
|
204
|
+
fileName,
|
|
205
|
+
sourceSha256: sourceHash,
|
|
206
|
+
sha256: hash,
|
|
207
|
+
capturedAt,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function readAndValidateInstrumentEvidenceManifest(input) {
|
|
211
|
+
const evidenceDir = resolveEvidenceDir(input.workspaceDir, input.evidenceDir);
|
|
212
|
+
const parsed = readManifestIfPresent(evidenceDir);
|
|
213
|
+
if (!parsed || parsed.version !== 1) {
|
|
214
|
+
throw new Error('missing_instrument_evidence_manifest');
|
|
215
|
+
}
|
|
216
|
+
const groupId = normalizeGroupId(parsed.groupId);
|
|
217
|
+
const controlledTabId = Number(parsed.controlledTabId);
|
|
218
|
+
if (!Number.isInteger(controlledTabId) || controlledTabId <= 0) {
|
|
219
|
+
throw new Error('invalid_instrument_evidence_tab');
|
|
220
|
+
}
|
|
221
|
+
const captures = {};
|
|
222
|
+
const screenshotIds = new Set();
|
|
223
|
+
for (const name of types_1.INSTRUMENT_EVIDENCE_NAMES) {
|
|
224
|
+
const capture = normalizeCapture(parsed.captures?.[name], name);
|
|
225
|
+
if (capture.tabId !== controlledTabId) {
|
|
226
|
+
throw new Error(`instrument_evidence_tab_mismatch:${controlledTabId}:${capture.tabId}`);
|
|
227
|
+
}
|
|
228
|
+
const filePath = node_path_1.default.join(evidenceDir, capture.fileName);
|
|
229
|
+
if (!(0, node_fs_1.existsSync)(filePath) || sha256((0, node_fs_1.readFileSync)(filePath)) !== capture.sha256) {
|
|
230
|
+
throw new Error(`instrument_evidence_file_hash_mismatch:${name}`);
|
|
231
|
+
}
|
|
232
|
+
if (screenshotIds.has(capture.screenshotId)) {
|
|
233
|
+
throw new Error(`duplicate_instrument_evidence_screenshot:${capture.screenshotId}`);
|
|
234
|
+
}
|
|
235
|
+
screenshotIds.add(capture.screenshotId);
|
|
236
|
+
captures[name] = capture;
|
|
237
|
+
}
|
|
238
|
+
return { version: 1, groupId, controlledTabId, captures };
|
|
239
|
+
}
|
|
@@ -50,6 +50,7 @@ export declare function runLoggedProcess(input: {
|
|
|
50
50
|
maxOutputChars: number;
|
|
51
51
|
transcriptFlushIntervalMs?: number;
|
|
52
52
|
onTranscriptChunks?: (chunks: LoggedProcessTranscriptChunk[]) => Promise<void> | void;
|
|
53
|
+
onStdoutText?: (text: string) => boolean | void;
|
|
53
54
|
onChild?: (controls: {
|
|
54
55
|
terminate: () => void;
|
|
55
56
|
}) => void;
|
|
@@ -377,6 +377,17 @@ async function runLoggedProcess(input) {
|
|
|
377
377
|
if (input.onTranscriptChunks) {
|
|
378
378
|
pendingTranscriptChunks.push({ stream, content: text });
|
|
379
379
|
}
|
|
380
|
+
if (stream === 'stdout' && input.onStdoutText) {
|
|
381
|
+
try {
|
|
382
|
+
const flushNow = input.onStdoutText(text) === true;
|
|
383
|
+
if (flushNow && input.onTranscriptChunks) {
|
|
384
|
+
queueTranscriptFlush().catch(handleTranscriptFlushError);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
handleTranscriptFlushError(error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
380
391
|
combined = tailText(combined + text, input.maxOutputChars);
|
|
381
392
|
};
|
|
382
393
|
const timeout = setTimeout(() => {
|
|
@@ -91,6 +91,16 @@ type TaskFailOptions = {
|
|
|
91
91
|
env?: string;
|
|
92
92
|
message?: string;
|
|
93
93
|
};
|
|
94
|
+
type TaskEvidenceOptions = {
|
|
95
|
+
evidenceDir?: string;
|
|
96
|
+
group?: string;
|
|
97
|
+
name?: string;
|
|
98
|
+
screenshotId?: string;
|
|
99
|
+
};
|
|
100
|
+
type TaskInstrumentErrorOptions = {
|
|
101
|
+
env?: string;
|
|
102
|
+
reason?: string;
|
|
103
|
+
};
|
|
94
104
|
type TaskSubmitReviewOptions = {
|
|
95
105
|
env?: string;
|
|
96
106
|
state?: string;
|
|
@@ -101,6 +111,7 @@ type TaskSubmitReviewOptions = {
|
|
|
101
111
|
};
|
|
102
112
|
type TaskSubmitEvalOptions = {
|
|
103
113
|
env?: string;
|
|
114
|
+
evidenceDir?: string[];
|
|
104
115
|
resultFile?: string;
|
|
105
116
|
};
|
|
106
117
|
export type WorkerHealthAlertInput = {
|
|
@@ -251,17 +262,31 @@ export declare function assertLaunchAgentPlistCompatible(input: {
|
|
|
251
262
|
}): void;
|
|
252
263
|
export declare function showWorkerStatus(options?: WorkerStatusOptions): Promise<WorkerStatusPayload>;
|
|
253
264
|
export declare function setupWorker(options?: WorkerSetupOptions): Promise<void>;
|
|
265
|
+
export declare function instrumentTaskChromeTitleMatchers(assignment: WorkerAgentTaskAssignmentResponse): string[];
|
|
266
|
+
type InstrumentBrowserHygieneRunner = (command: string, args: string[]) => Promise<{
|
|
267
|
+
stdout: string;
|
|
268
|
+
stderr: string;
|
|
269
|
+
}>;
|
|
270
|
+
export declare function enforceInstrumentBrowserHygiene(assignment: WorkerAgentTaskAssignmentResponse, options?: {
|
|
271
|
+
platform?: NodeJS.Platform;
|
|
272
|
+
runner?: InstrumentBrowserHygieneRunner;
|
|
273
|
+
}): Promise<{
|
|
274
|
+
remainingTaskTabs: number;
|
|
275
|
+
blankWindows: number;
|
|
276
|
+
}>;
|
|
254
277
|
export declare function startWorker(options?: WorkerStartOptions): Promise<void>;
|
|
255
278
|
export declare function reportTask(options: TaskReportOptions): Promise<void>;
|
|
256
279
|
export declare function reportCatalogueTask(options: TaskCatalogueReportOptions): Promise<void>;
|
|
257
280
|
export declare function uploadTask(options?: TaskUploadOptions): Promise<void>;
|
|
258
281
|
export declare function claimSlugTask(options: TaskClaimSlugOptions): Promise<void>;
|
|
259
282
|
export declare function completeTask(options: TaskCompleteOptions): Promise<void>;
|
|
260
|
-
export declare function readReviewEvidenceFiles(evidenceDir: string | undefined): Array<{
|
|
283
|
+
export declare function readReviewEvidenceFiles(evidenceDir: string | undefined, workspaceDir?: string): Array<{
|
|
261
284
|
name: string;
|
|
262
285
|
contentType: string;
|
|
263
286
|
contentBase64: string;
|
|
264
287
|
}>;
|
|
265
288
|
export declare function submitReviewTask(options: TaskSubmitReviewOptions): Promise<void>;
|
|
266
289
|
export declare function submitEvalTask(options: TaskSubmitEvalOptions): Promise<void>;
|
|
290
|
+
export declare function saveTaskEvidence(options: TaskEvidenceOptions): Promise<void>;
|
|
291
|
+
export declare function instrumentErrorTask(options: TaskInstrumentErrorOptions): Promise<void>;
|
|
267
292
|
export declare function failTask(options: TaskFailOptions): Promise<void>;
|