@playdrop/playdrop-cli 0.10.18 → 0.10.20
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/apps/index.js +2 -2
- package/dist/apps/registration.js +1 -0
- package/dist/captureRuntime.d.ts +7 -0
- package/dist/captureRuntime.js +36 -0
- package/dist/commands/captureRemote.d.ts +2 -0
- package/dist/commands/captureRemote.js +42 -2
- package/dist/commands/worker/runtime.d.ts +7 -0
- package/dist/commands/worker/runtime.js +171 -31
- package/dist/commands/worker.d.ts +16 -3
- package/dist/commands/worker.js +459 -115
- package/dist/index.js +18 -3
- package/node_modules/@playdrop/api-client/dist/client.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +28 -1
- package/node_modules/@playdrop/api-client/dist/index.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +10 -0
- package/node_modules/@playdrop/config/client-meta.json +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +64 -3
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +28 -1
- package/package.json +1 -1
package/config/client-meta.json
CHANGED
package/dist/apps/index.js
CHANGED
|
@@ -19,8 +19,8 @@ async function runAppPipeline(client, task, options) {
|
|
|
19
19
|
await (0, validate_1.validateAppTask)(task);
|
|
20
20
|
// External apps don't need to be built - they're hosted elsewhere
|
|
21
21
|
const isExternal = task.hostingMode === 'EXTERNAL' || !!task.externalUrl;
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
22
|
+
const shouldEnsureRegisteredShell = !isExternal && options?.ensureRegisteredAppShell;
|
|
23
|
+
if (shouldEnsureRegisteredShell) {
|
|
24
24
|
const creatorUsername = typeof options.creatorUsername === 'string' ? options.creatorUsername.trim() : '';
|
|
25
25
|
if (!creatorUsername) {
|
|
26
26
|
throw new Error(`Missing creator username for required hosted app registration of "${task.name}".`);
|
|
@@ -28,6 +28,7 @@ async function ensureRegisteredHostedAppShell(input) {
|
|
|
28
28
|
name: input.task.name,
|
|
29
29
|
displayName,
|
|
30
30
|
...(input.agentTaskId !== undefined ? { agentTaskId: input.agentTaskId } : {}),
|
|
31
|
+
...(input.task.remix ? { remixRef: input.task.remix } : {}),
|
|
31
32
|
});
|
|
32
33
|
app = createdResponse.app;
|
|
33
34
|
created = true;
|
package/dist/captureRuntime.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type RunCaptureOptions = {
|
|
|
13
13
|
expectedUrl?: string | null;
|
|
14
14
|
timeoutMs: number;
|
|
15
15
|
settleAfterReadyMs?: number;
|
|
16
|
+
frameRateSampleMs?: number;
|
|
16
17
|
minimumLogLevel: CaptureLogLevel;
|
|
17
18
|
screenshotPath?: string | null;
|
|
18
19
|
logPath?: string | null;
|
|
@@ -59,6 +60,12 @@ export type CaptureRunResult = {
|
|
|
59
60
|
warnings: string[];
|
|
60
61
|
finalUrl: string;
|
|
61
62
|
hostedLaunchState: CaptureHostedLaunchState | null;
|
|
63
|
+
frameRate: CaptureFrameRateSample | null;
|
|
64
|
+
};
|
|
65
|
+
export type CaptureFrameRateSample = {
|
|
66
|
+
fps: number;
|
|
67
|
+
sampleMs: number;
|
|
68
|
+
frameCount: number;
|
|
62
69
|
};
|
|
63
70
|
export declare function resolveCaptureLogLevel(value: string | undefined): CaptureLogLevel;
|
|
64
71
|
export declare function validateCaptureTimeout(value: number | undefined): number;
|
package/dist/captureRuntime.js
CHANGED
|
@@ -526,6 +526,36 @@ async function runCaptureActions(page, actions) {
|
|
|
526
526
|
throw new Error('capture_action_type_unknown');
|
|
527
527
|
}
|
|
528
528
|
}
|
|
529
|
+
async function sampleFrameRate(page, sampleMs) {
|
|
530
|
+
if (!Number.isInteger(sampleMs) || sampleMs <= 0) {
|
|
531
|
+
throw new Error('capture_fps_sample_invalid');
|
|
532
|
+
}
|
|
533
|
+
const childFrames = page.frames().filter(frame => frame.parentFrame() !== null);
|
|
534
|
+
const target = childFrames[0] ?? page;
|
|
535
|
+
const sample = await target.evaluate((durationMs) => new Promise((resolve) => {
|
|
536
|
+
const startedAt = performance.now();
|
|
537
|
+
let frameCount = 0;
|
|
538
|
+
const tick = (timestamp) => {
|
|
539
|
+
frameCount += 1;
|
|
540
|
+
const elapsedMs = Math.max(timestamp - startedAt, 0);
|
|
541
|
+
if (elapsedMs >= durationMs) {
|
|
542
|
+
resolve({
|
|
543
|
+
fps: frameCount > 0 && elapsedMs > 0 ? frameCount * 1000 / elapsedMs : 0,
|
|
544
|
+
sampleMs: elapsedMs,
|
|
545
|
+
frameCount,
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
requestAnimationFrame(tick);
|
|
550
|
+
};
|
|
551
|
+
requestAnimationFrame(tick);
|
|
552
|
+
}), sampleMs);
|
|
553
|
+
return {
|
|
554
|
+
fps: Number(sample.fps.toFixed(2)),
|
|
555
|
+
sampleMs: Math.round(sample.sampleMs),
|
|
556
|
+
frameCount: sample.frameCount,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
529
559
|
async function assertHostedFrameVisualReadiness(page) {
|
|
530
560
|
const childFrames = page.frames().filter(frame => frame.parentFrame() !== null);
|
|
531
561
|
if (childFrames.length === 0) {
|
|
@@ -596,6 +626,7 @@ async function runCapture(options) {
|
|
|
596
626
|
const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
|
|
597
627
|
let finalUrl = options.targetUrl;
|
|
598
628
|
let hostedLaunchState = null;
|
|
629
|
+
let frameRate = null;
|
|
599
630
|
const record = (level, message) => {
|
|
600
631
|
const line = `[capture][${level}] ${message}`;
|
|
601
632
|
outputLines.push(line);
|
|
@@ -959,6 +990,10 @@ async function runCapture(options) {
|
|
|
959
990
|
await page.screenshot({ path: options.screenshotPath, fullPage: true });
|
|
960
991
|
console.log(`[capture] Saved screenshot to ${(0, node_path_1.relative)(process.cwd(), options.screenshotPath) || options.screenshotPath}`);
|
|
961
992
|
}
|
|
993
|
+
if (options.frameRateSampleMs && options.frameRateSampleMs > 0) {
|
|
994
|
+
frameRate = await sampleFrameRate(page, options.frameRateSampleMs);
|
|
995
|
+
record('info', `Frame-rate sample: ${frameRate.fps.toFixed(2)} fps over ${frameRate.sampleMs}ms (${frameRate.frameCount} frames).`);
|
|
996
|
+
}
|
|
962
997
|
}, {
|
|
963
998
|
...(options.contextOptions ?? {}),
|
|
964
999
|
automationOrigin: resolveAutomationOrigin(options.targetUrl),
|
|
@@ -974,5 +1009,6 @@ async function runCapture(options) {
|
|
|
974
1009
|
warnings,
|
|
975
1010
|
finalUrl,
|
|
976
1011
|
hostedLaunchState,
|
|
1012
|
+
frameRate,
|
|
977
1013
|
};
|
|
978
1014
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type CaptureRemoteOptions = {
|
|
2
2
|
timeout?: string | number;
|
|
3
|
+
settle?: string | number;
|
|
3
4
|
screenshot?: string;
|
|
4
5
|
log?: string;
|
|
5
6
|
expectedUrl?: string;
|
|
@@ -8,6 +9,7 @@ type CaptureRemoteOptions = {
|
|
|
8
9
|
loginUrl?: string;
|
|
9
10
|
actions?: string;
|
|
10
11
|
viewport?: string;
|
|
12
|
+
fpsSampleMs?: string | number;
|
|
11
13
|
};
|
|
12
14
|
export declare function buildCaptureRemoteCommandArgs(url: string | undefined, options?: CaptureRemoteOptions): string[] | null;
|
|
13
15
|
export declare function captureRemote(url: string | undefined, options?: CaptureRemoteOptions): Promise<void>;
|
|
@@ -43,6 +43,16 @@ function parseTimeout(raw) {
|
|
|
43
43
|
}
|
|
44
44
|
return parsed;
|
|
45
45
|
}
|
|
46
|
+
function parseSettle(raw) {
|
|
47
|
+
if (raw === undefined) {
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw));
|
|
51
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 600) {
|
|
52
|
+
throw new Error('invalid_settle');
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
46
56
|
function parseViewport(raw) {
|
|
47
57
|
const value = raw?.trim();
|
|
48
58
|
if (!value) {
|
|
@@ -59,6 +69,16 @@ function parseViewport(raw) {
|
|
|
59
69
|
}
|
|
60
70
|
return { width, height };
|
|
61
71
|
}
|
|
72
|
+
function parseFpsSampleMs(raw) {
|
|
73
|
+
if (raw === undefined) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw));
|
|
77
|
+
if (!Number.isInteger(parsed) || parsed < 100 || parsed > 60000) {
|
|
78
|
+
throw new Error('invalid_fps_sample_ms');
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
62
82
|
function normalizeCaptureAction(raw, index) {
|
|
63
83
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
64
84
|
throw new Error(`invalid_action:${index}`);
|
|
@@ -144,11 +164,17 @@ function buildCaptureRemoteCommandArgs(url, options = {}) {
|
|
|
144
164
|
if (options.timeout !== undefined) {
|
|
145
165
|
args.push('--timeout', String(parseTimeout(options.timeout)));
|
|
146
166
|
}
|
|
167
|
+
if (options.settle !== undefined) {
|
|
168
|
+
args.push('--settle', String(parseSettle(options.settle)));
|
|
169
|
+
}
|
|
147
170
|
pushOptionalArg(args, '--screenshot', options.screenshot);
|
|
148
171
|
pushOptionalArg(args, '--log', options.log);
|
|
149
172
|
pushOptionalArg(args, '--expected-url', options.expectedUrl);
|
|
150
173
|
pushOptionalArg(args, '--actions', options.actions);
|
|
151
174
|
pushOptionalArg(args, '--viewport', options.viewport);
|
|
175
|
+
if (options.fpsSampleMs !== undefined) {
|
|
176
|
+
args.push('--fps-sample-ms', String(parseFpsSampleMs(options.fpsSampleMs)));
|
|
177
|
+
}
|
|
152
178
|
if (username) {
|
|
153
179
|
args.push('--username', username, '--password', password);
|
|
154
180
|
}
|
|
@@ -174,10 +200,12 @@ function parseOptions(url, options = {}) {
|
|
|
174
200
|
targetUrl: trimmedUrl,
|
|
175
201
|
expectedUrl,
|
|
176
202
|
timeoutSeconds: parseTimeout(options.timeout),
|
|
203
|
+
settleSeconds: parseSettle(options.settle),
|
|
177
204
|
screenshotPath: (0, node_path_1.resolve)(process.cwd(), options.screenshot?.trim() || 'output/playwright/capture-url.png'),
|
|
178
205
|
logPath: options.log?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.log.trim()) : null,
|
|
179
206
|
actionsPath: options.actions?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.actions.trim()) : null,
|
|
180
207
|
viewport: parseViewport(options.viewport),
|
|
208
|
+
fpsSampleMs: parseFpsSampleMs(options.fpsSampleMs),
|
|
181
209
|
login: username
|
|
182
210
|
? {
|
|
183
211
|
username,
|
|
@@ -209,11 +237,21 @@ async function captureRemote(url, options = {}) {
|
|
|
209
237
|
process.exitCode = 1;
|
|
210
238
|
return;
|
|
211
239
|
}
|
|
240
|
+
if (error instanceof Error && error.message === 'invalid_settle') {
|
|
241
|
+
(0, messages_1.printErrorWithHelp)('The --settle value must be a number between 0 and 600 seconds.', [], { command: 'project capture remote' });
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
212
245
|
if (error instanceof Error && error.message === 'invalid_viewport') {
|
|
213
246
|
(0, messages_1.printErrorWithHelp)('The --viewport value must be WIDTHxHEIGHT with each side between 320 and 4096 pixels.', [], { command: 'project capture remote' });
|
|
214
247
|
process.exitCode = 1;
|
|
215
248
|
return;
|
|
216
249
|
}
|
|
250
|
+
if (error instanceof Error && error.message === 'invalid_fps_sample_ms') {
|
|
251
|
+
(0, messages_1.printErrorWithHelp)('The --fps-sample-ms value must be an integer between 100 and 60000 milliseconds.', [], { command: 'project capture remote' });
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
217
255
|
if (error instanceof Error && error.message === 'invalid_credentials_pair') {
|
|
218
256
|
(0, messages_1.printErrorWithHelp)('Use --username and --password together.', [], { command: 'project capture remote' });
|
|
219
257
|
process.exitCode = 1;
|
|
@@ -272,6 +310,7 @@ async function captureRemote(url, options = {}) {
|
|
|
272
310
|
targetUrl: parsed.targetUrl,
|
|
273
311
|
expectedUrl: parsed.expectedUrl ?? parsed.targetUrl,
|
|
274
312
|
timeoutMs: Math.round(parsed.timeoutSeconds * 1000),
|
|
313
|
+
settleAfterReadyMs: Math.round(parsed.settleSeconds * 1000),
|
|
275
314
|
minimumLogLevel: 'info',
|
|
276
315
|
screenshotPath: parsed.screenshotPath,
|
|
277
316
|
logPath: parsed.logPath,
|
|
@@ -280,13 +319,14 @@ async function captureRemote(url, options = {}) {
|
|
|
280
319
|
login: parsed.login,
|
|
281
320
|
actions: actions ?? undefined,
|
|
282
321
|
contextOptions: parsed.viewport ? { viewport: parsed.viewport } : undefined,
|
|
322
|
+
frameRateSampleMs: parsed.fpsSampleMs ?? undefined,
|
|
283
323
|
});
|
|
284
324
|
if (result.errorCount > 0) {
|
|
285
|
-
console.error(`[capture] Completed with ${result.errorCount} error(s) after
|
|
325
|
+
console.error(`[capture] Completed with ${result.errorCount} error(s) after timeout=${parsed.timeoutSeconds}s settle=${parsed.settleSeconds}s.`);
|
|
286
326
|
process.exitCode = 1;
|
|
287
327
|
}
|
|
288
328
|
else {
|
|
289
|
-
console.log(`[capture] Completed without console errors after
|
|
329
|
+
console.log(`[capture] Completed without console errors after timeout=${parsed.timeoutSeconds}s settle=${parsed.settleSeconds}s.`);
|
|
290
330
|
}
|
|
291
331
|
}
|
|
292
332
|
catch (error) {
|
|
@@ -28,6 +28,13 @@ export type LoggedProcessTranscriptChunk = {
|
|
|
28
28
|
export declare function readPositiveEnvInt(name: string, fallback: number): number;
|
|
29
29
|
export declare function readEnvBoolean(name: string, fallback: boolean): boolean;
|
|
30
30
|
export declare function extractAgentTokenUsage(output: string): AgentTokenUsage;
|
|
31
|
+
export declare function extractCodexRolloutTokenUsage(output: string): AgentTokenUsage;
|
|
32
|
+
export declare function extractCodexThreadId(output: string): string | null;
|
|
33
|
+
export type CodexRolloutTokenUsageReadOptions = {
|
|
34
|
+
sessionsRoot?: string;
|
|
35
|
+
deleteAfterRead?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare function readCodexRolloutTokenUsageForThread(threadId: string, options?: CodexRolloutTokenUsageReadOptions): Promise<AgentTokenUsage>;
|
|
31
38
|
export declare function extractCodexTokensUsed(output: string): number | null;
|
|
32
39
|
export declare function assertWorkerTokenUsageWithinCap(input: {
|
|
33
40
|
tokensUsed: number | null;
|
|
@@ -7,6 +7,9 @@ exports.DEFAULT_TRANSCRIPT_FLUSH_INTERVAL_MS = exports.DEFAULT_CODEX_LOG_TAIL_CH
|
|
|
7
7
|
exports.readPositiveEnvInt = readPositiveEnvInt;
|
|
8
8
|
exports.readEnvBoolean = readEnvBoolean;
|
|
9
9
|
exports.extractAgentTokenUsage = extractAgentTokenUsage;
|
|
10
|
+
exports.extractCodexRolloutTokenUsage = extractCodexRolloutTokenUsage;
|
|
11
|
+
exports.extractCodexThreadId = extractCodexThreadId;
|
|
12
|
+
exports.readCodexRolloutTokenUsageForThread = readCodexRolloutTokenUsageForThread;
|
|
10
13
|
exports.extractCodexTokensUsed = extractCodexTokensUsed;
|
|
11
14
|
exports.assertWorkerTokenUsageWithinCap = assertWorkerTokenUsageWithinCap;
|
|
12
15
|
exports.runLoggedProcess = runLoggedProcess;
|
|
@@ -17,6 +20,8 @@ exports.buildClaudePermissionSettings = buildClaudePermissionSettings;
|
|
|
17
20
|
exports.buildClaudeDeniedPermissionRules = buildClaudeDeniedPermissionRules;
|
|
18
21
|
exports.buildWorkerChildEnv = buildWorkerChildEnv;
|
|
19
22
|
const node_child_process_1 = require("node:child_process");
|
|
23
|
+
const promises_1 = require("node:fs/promises");
|
|
24
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
20
25
|
const node_path_1 = __importDefault(require("node:path"));
|
|
21
26
|
const CHILD_KILL_GRACE_MS = 10000;
|
|
22
27
|
exports.DEFAULT_CODEX_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
@@ -59,6 +64,15 @@ function normalizeTokenCount(value) {
|
|
|
59
64
|
}
|
|
60
65
|
return Math.ceil(value);
|
|
61
66
|
}
|
|
67
|
+
function normalizeFirstTokenCount(...values) {
|
|
68
|
+
for (const value of values) {
|
|
69
|
+
const normalized = normalizeTokenCount(value);
|
|
70
|
+
if (normalized !== null) {
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
62
76
|
function coerceRecord(value) {
|
|
63
77
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
64
78
|
return null;
|
|
@@ -91,6 +105,55 @@ function readStructuredUsage(parsed) {
|
|
|
91
105
|
}
|
|
92
106
|
return null;
|
|
93
107
|
}
|
|
108
|
+
function readCodexRolloutStructuredUsage(parsed) {
|
|
109
|
+
const root = coerceRecord(parsed);
|
|
110
|
+
if (!root || root.type !== 'token_count') {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const info = coerceRecord(root.info);
|
|
114
|
+
return coerceRecord(info?.total_token_usage);
|
|
115
|
+
}
|
|
116
|
+
function normalizeAgentTokenUsageFromRecord(usage) {
|
|
117
|
+
const outputTokens = normalizeFirstTokenCount(usage.output_tokens, usage.outputTokens);
|
|
118
|
+
const cacheCreationInputTokens = normalizeFirstTokenCount(usage.cache_creation_input_tokens, usage.cacheCreationInputTokens);
|
|
119
|
+
const cacheReadInputTokens = normalizeFirstTokenCount(usage.cache_read_input_tokens, usage.cached_input_tokens, usage.cacheReadInputTokens);
|
|
120
|
+
const rawInputTokens = normalizeFirstTokenCount(usage.input_tokens, usage.inputTokens);
|
|
121
|
+
const inputTokens = rawInputTokens !== null
|
|
122
|
+
&& cacheReadInputTokens !== null
|
|
123
|
+
&& usage.cached_input_tokens !== undefined
|
|
124
|
+
? Math.max(0, rawInputTokens - cacheReadInputTokens)
|
|
125
|
+
: rawInputTokens;
|
|
126
|
+
const tokenParts = [
|
|
127
|
+
inputTokens,
|
|
128
|
+
outputTokens,
|
|
129
|
+
cacheCreationInputTokens,
|
|
130
|
+
cacheReadInputTokens,
|
|
131
|
+
];
|
|
132
|
+
const sawTokenValue = tokenParts.some((value) => value !== null);
|
|
133
|
+
if (!sawTokenValue) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
inputTokens,
|
|
138
|
+
outputTokens,
|
|
139
|
+
cacheCreationInputTokens,
|
|
140
|
+
cacheReadInputTokens,
|
|
141
|
+
totalTokens: tokenParts.reduce((sum, value) => sum + (value ?? 0), 0),
|
|
142
|
+
rawProviderUsage: usage,
|
|
143
|
+
usageParseError: null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function missingAgentTokenUsage(reason = 'agent_usage_not_reported') {
|
|
147
|
+
return {
|
|
148
|
+
inputTokens: null,
|
|
149
|
+
outputTokens: null,
|
|
150
|
+
cacheCreationInputTokens: null,
|
|
151
|
+
cacheReadInputTokens: null,
|
|
152
|
+
totalTokens: null,
|
|
153
|
+
rawProviderUsage: null,
|
|
154
|
+
usageParseError: reason,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
94
157
|
function extractAgentTokenUsage(output) {
|
|
95
158
|
for (const line of output.split(/\r?\n/).reverse()) {
|
|
96
159
|
const trimmed = line.trim();
|
|
@@ -103,42 +166,119 @@ function extractAgentTokenUsage(output) {
|
|
|
103
166
|
if (!usage) {
|
|
104
167
|
continue;
|
|
105
168
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const cacheReadInputTokens = normalizeTokenCount(usage.cache_read_input_tokens);
|
|
110
|
-
const tokenParts = [
|
|
111
|
-
inputTokens,
|
|
112
|
-
outputTokens,
|
|
113
|
-
cacheCreationInputTokens,
|
|
114
|
-
cacheReadInputTokens,
|
|
115
|
-
];
|
|
116
|
-
const sawTokenValue = tokenParts.some((value) => value !== null);
|
|
117
|
-
if (sawTokenValue) {
|
|
118
|
-
return {
|
|
119
|
-
inputTokens,
|
|
120
|
-
outputTokens,
|
|
121
|
-
cacheCreationInputTokens,
|
|
122
|
-
cacheReadInputTokens,
|
|
123
|
-
totalTokens: tokenParts.reduce((sum, value) => sum + (value ?? 0), 0),
|
|
124
|
-
rawProviderUsage: usage,
|
|
125
|
-
usageParseError: null,
|
|
126
|
-
};
|
|
169
|
+
const tokenUsage = normalizeAgentTokenUsageFromRecord(usage);
|
|
170
|
+
if (tokenUsage) {
|
|
171
|
+
return tokenUsage;
|
|
127
172
|
}
|
|
128
173
|
}
|
|
129
174
|
catch {
|
|
130
175
|
continue;
|
|
131
176
|
}
|
|
132
177
|
}
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
178
|
+
return missingAgentTokenUsage();
|
|
179
|
+
}
|
|
180
|
+
function extractCodexRolloutTokenUsage(output) {
|
|
181
|
+
for (const line of output.split(/\r?\n/).reverse()) {
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(trimmed);
|
|
188
|
+
const usage = readCodexRolloutStructuredUsage(parsed);
|
|
189
|
+
if (!usage) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const tokenUsage = normalizeAgentTokenUsageFromRecord(usage);
|
|
193
|
+
if (tokenUsage) {
|
|
194
|
+
return tokenUsage;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return missingAgentTokenUsage();
|
|
202
|
+
}
|
|
203
|
+
function extractCodexThreadId(output) {
|
|
204
|
+
for (const line of output.split(/\r?\n/)) {
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const root = coerceRecord(JSON.parse(trimmed));
|
|
211
|
+
if (!root) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const type = typeof root.type === 'string' ? root.type : '';
|
|
215
|
+
const rawThreadId = type === 'thread.started'
|
|
216
|
+
? root.thread_id ?? root.threadId
|
|
217
|
+
: type === 'session_meta'
|
|
218
|
+
? root.session_id ?? root.sessionId
|
|
219
|
+
: null;
|
|
220
|
+
if (typeof rawThreadId === 'string' && rawThreadId.trim()) {
|
|
221
|
+
return rawThreadId.trim();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
async function findCodexRolloutFileForThread(threadId, sessionsRoot) {
|
|
231
|
+
const stack = [{ dir: sessionsRoot, depth: 0 }];
|
|
232
|
+
const matches = [];
|
|
233
|
+
while (stack.length > 0) {
|
|
234
|
+
const current = stack.pop();
|
|
235
|
+
let entries;
|
|
236
|
+
try {
|
|
237
|
+
entries = await (0, promises_1.readdir)(current.dir, { withFileTypes: true });
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error.code === 'ENOENT') {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
const entryPath = node_path_1.default.join(current.dir, entry.name);
|
|
247
|
+
if (entry.isDirectory()) {
|
|
248
|
+
if (current.depth < 6) {
|
|
249
|
+
stack.push({ dir: entryPath, depth: current.depth + 1 });
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (entry.isFile()
|
|
254
|
+
&& entry.name.startsWith('rollout-')
|
|
255
|
+
&& entry.name.endsWith('.jsonl')
|
|
256
|
+
&& entry.name.includes(threadId)) {
|
|
257
|
+
matches.push(entryPath);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (matches.length <= 0) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
matches.sort();
|
|
265
|
+
return matches[matches.length - 1];
|
|
266
|
+
}
|
|
267
|
+
async function readCodexRolloutTokenUsageForThread(threadId, options = {}) {
|
|
268
|
+
const normalizedThreadId = threadId.trim();
|
|
269
|
+
if (!normalizedThreadId) {
|
|
270
|
+
return missingAgentTokenUsage('codex_thread_id_missing');
|
|
271
|
+
}
|
|
272
|
+
const sessionsRoot = options.sessionsRoot ?? node_path_1.default.join(node_os_1.default.homedir(), '.codex', 'sessions');
|
|
273
|
+
const rolloutPath = await findCodexRolloutFileForThread(normalizedThreadId, sessionsRoot);
|
|
274
|
+
if (!rolloutPath) {
|
|
275
|
+
return missingAgentTokenUsage('codex_rollout_file_not_found');
|
|
276
|
+
}
|
|
277
|
+
const output = await (0, promises_1.readFile)(rolloutPath, 'utf8');
|
|
278
|
+
if (options.deleteAfterRead !== false) {
|
|
279
|
+
await (0, promises_1.rm)(rolloutPath, { force: true });
|
|
280
|
+
}
|
|
281
|
+
return extractCodexRolloutTokenUsage(output);
|
|
142
282
|
}
|
|
143
283
|
function extractCodexTokensUsed(output) {
|
|
144
284
|
return extractAgentTokenUsage(output).totalTokens;
|
|
@@ -285,12 +425,12 @@ function buildCodexExecArgs(input) {
|
|
|
285
425
|
const sandboxMode = input.sandboxMode ?? 'danger-full-access';
|
|
286
426
|
const args = [
|
|
287
427
|
'exec',
|
|
428
|
+
'--json',
|
|
288
429
|
'--sandbox',
|
|
289
430
|
sandboxMode,
|
|
290
431
|
'--cd',
|
|
291
432
|
input.workspaceDir,
|
|
292
433
|
'--skip-git-repo-check',
|
|
293
|
-
'--ephemeral',
|
|
294
434
|
'--model',
|
|
295
435
|
input.model,
|
|
296
436
|
'-c',
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ApiClient } from '@playdrop/api-client';
|
|
2
|
-
import { type AgentExecutionTarget, type AgentRuntime, type AgentTaskKind, type AgentTaskResponse, type AgentWorkerCapabilities, type WorkerAgentTaskAssignmentResponse, type WorkerAgentTaskBaseSourceResponse, type WorkerAgentTaskWorkspaceFileResponse, type WorkerClaimAgentTaskResponse } from '@playdrop/types';
|
|
2
|
+
import { type AgentTaskNextStepSuggestion, type AgentExecutionTarget, type AgentRuntime, type AgentTaskKind, type AgentTaskResponse, type AgentWorkerCapabilities, type WorkerAgentTaskAssignmentResponse, type WorkerAgentTaskBaseSourceResponse, type WorkerAgentTaskWorkspaceFileResponse, type WorkerClaimAgentTaskResponse } from '@playdrop/types';
|
|
3
3
|
import { type WorkerPlaydropAssetRequirement } from './upload';
|
|
4
4
|
import { type LoggedProcessResult, type LoggedProcessTranscriptChunk } from './worker/runtime';
|
|
5
|
-
export { DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, assertWorkerTokenUsageWithinCap, buildCodexExecArgs, buildClaudeExecArgs, buildClaudePermissionSettings, buildWorkerChildEnv, extractAgentTokenUsage, extractCodexTokensUsed, readEnvBoolean, readCodexSandboxMode, runLoggedProcess, } from './worker/runtime';
|
|
5
|
+
export { DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, assertWorkerTokenUsageWithinCap, buildCodexExecArgs, buildClaudeExecArgs, buildClaudePermissionSettings, buildWorkerChildEnv, extractAgentTokenUsage, extractCodexRolloutTokenUsage, extractCodexThreadId, extractCodexTokensUsed, readEnvBoolean, readCodexSandboxMode, readCodexRolloutTokenUsageForThread, runLoggedProcess, } from './worker/runtime';
|
|
6
6
|
export type { AgentTokenUsage, LoggedProcessResult, LoggedProcessTranscriptChunk, } from './worker/runtime';
|
|
7
7
|
export declare const WORKER_SESSION_EXPIRED_MESSAGE = "worker session expired: run \"playdrop auth login\" and start the worker again";
|
|
8
|
-
export declare const WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = "worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task upload/done/fail, assigned
|
|
8
|
+
export declare const WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = "worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task claim-slug, task upload/done/fail, assigned app scaffolding, project validation/build/dev/capture, read-only catalogue/documentation lookup, and PlayDrop AI generation are permitted.";
|
|
9
9
|
type WorkerStartOptions = {
|
|
10
10
|
env?: string;
|
|
11
11
|
once?: boolean;
|
|
@@ -57,6 +57,8 @@ type TaskReportOptions = {
|
|
|
57
57
|
phase?: string;
|
|
58
58
|
pct?: string | number;
|
|
59
59
|
message?: string;
|
|
60
|
+
done?: string;
|
|
61
|
+
current?: string;
|
|
60
62
|
kind?: string;
|
|
61
63
|
};
|
|
62
64
|
type TaskCatalogueReportOptions = {
|
|
@@ -66,6 +68,12 @@ type TaskCatalogueReportOptions = {
|
|
|
66
68
|
};
|
|
67
69
|
type TaskCompleteOptions = {
|
|
68
70
|
env?: string;
|
|
71
|
+
nextSteps?: string;
|
|
72
|
+
};
|
|
73
|
+
type TaskClaimSlugOptions = {
|
|
74
|
+
appName?: string;
|
|
75
|
+
displayName?: string;
|
|
76
|
+
env?: string;
|
|
69
77
|
};
|
|
70
78
|
type TaskUploadOptions = {
|
|
71
79
|
env?: string;
|
|
@@ -80,6 +88,7 @@ type TaskSubmitReviewOptions = {
|
|
|
80
88
|
messageFile?: string;
|
|
81
89
|
creatorFeedbackFile?: string;
|
|
82
90
|
evidenceDir?: string;
|
|
91
|
+
nextSteps?: string;
|
|
83
92
|
};
|
|
84
93
|
export type WorkerHealthAlertInput = {
|
|
85
94
|
state: 'started' | 'stopped' | 'crashed';
|
|
@@ -115,6 +124,7 @@ export declare function appendWorkerTranscriptChunks(input: {
|
|
|
115
124
|
chunks: LoggedProcessTranscriptChunk[];
|
|
116
125
|
batchSize?: number;
|
|
117
126
|
}): Promise<void>;
|
|
127
|
+
export declare function readTaskNextStepsFile(filePath: string | undefined): AgentTaskNextStepSuggestion[] | undefined;
|
|
118
128
|
export declare function assertTaskUploadResultMatchesContext(input: {
|
|
119
129
|
taskContext: {
|
|
120
130
|
taskId: number;
|
|
@@ -144,6 +154,8 @@ export declare function readWorkerTaskState(): WorkerTaskState | null;
|
|
|
144
154
|
export declare function buildWorkerHealthAlertText(input: WorkerHealthAlertInput): string;
|
|
145
155
|
export declare function isWorkerAuthFailureError(error: unknown): boolean;
|
|
146
156
|
export declare function classifyWorkerEventDrainError(error: unknown): 'session_expired' | 'lease_invalid' | null;
|
|
157
|
+
export declare function isWorkerTransientNetworkError(error: unknown): boolean;
|
|
158
|
+
export declare function classifyWorkerHeartbeatError(error: unknown): 'session_expired' | 'lease_invalid' | 'transient_network' | null;
|
|
147
159
|
export declare function assertWorkerClaimErrorRetryable(error: unknown): void;
|
|
148
160
|
export declare function resolvePlaydropAssetRequirementFromText(value: unknown): WorkerPlaydropAssetRequirement | null;
|
|
149
161
|
export declare function loadWorkerEnvFile(envName: string | undefined, cwd?: string): string | null;
|
|
@@ -218,6 +230,7 @@ export declare function startWorker(options?: WorkerStartOptions): Promise<void>
|
|
|
218
230
|
export declare function reportTask(options: TaskReportOptions): Promise<void>;
|
|
219
231
|
export declare function reportCatalogueTask(options: TaskCatalogueReportOptions): Promise<void>;
|
|
220
232
|
export declare function uploadTask(options?: TaskUploadOptions): Promise<void>;
|
|
233
|
+
export declare function claimSlugTask(options: TaskClaimSlugOptions): Promise<void>;
|
|
221
234
|
export declare function completeTask(options: TaskCompleteOptions): Promise<void>;
|
|
222
235
|
export declare function readReviewEvidenceFiles(evidenceDir: string | undefined): Array<{
|
|
223
236
|
name: string;
|