@playdrop/playdrop-cli 0.12.17 → 0.12.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.12.8",
3
- "build": 10,
3
+ "build": 13,
4
4
  "runtimeSdkVersion": "0.12.5",
5
5
  "runtimeSdkBuild": 2,
6
6
  "clients": {
@@ -96,6 +96,7 @@ export declare function buildWorkerChildEnv(input: {
96
96
  homeDir?: string;
97
97
  codexHomeDir?: string;
98
98
  claudeConfigDir?: string;
99
+ playdropConfigPath?: string;
99
100
  playwrightBrowsersPath?: string;
100
101
  baseEnv?: NodeJS.ProcessEnv;
101
102
  }): NodeJS.ProcessEnv;
@@ -712,6 +712,17 @@ function buildWorkerChildEnv(input) {
712
712
  }
713
713
  child.CLAUDE_CONFIG_DIR = claudeConfigDir;
714
714
  }
715
+ // Claude sessions inherit the host's real HOME and Claude config on purpose: the machine is
716
+ // logged in with the Claude CLI and we call it exactly as a human does. The worker never
717
+ // stages, copies, or refreshes Claude credentials. The agent's `playdrop` CLI is still scoped
718
+ // to a task session through PLAYDROP_CONFIG_PATH rather than by hijacking HOME.
719
+ if (input.playdropConfigPath !== undefined) {
720
+ const playdropConfigPath = input.playdropConfigPath.trim();
721
+ if (!playdropConfigPath || !node_path_1.default.isAbsolute(playdropConfigPath)) {
722
+ throw new Error('invalid_worker_child_playdrop_config_path');
723
+ }
724
+ child.PLAYDROP_CONFIG_PATH = playdropConfigPath;
725
+ }
715
726
  if (input.playwrightBrowsersPath !== undefined) {
716
727
  const playwrightBrowsersPath = input.playwrightBrowsersPath.trim();
717
728
  if (!playwrightBrowsersPath || !node_path_1.default.isAbsolute(playwrightBrowsersPath)) {
@@ -137,6 +137,8 @@ export declare function resolveWorkerClaimTaskAssignment(claim: WorkerClaimAgent
137
137
  export declare function nextClaimBackoffMs(currentMs: number): number;
138
138
  export declare function claimBackoffDelayMs(currentMs: number, random?: () => number): number;
139
139
  export declare function resolveWorkerHomeDir(): string;
140
+ export declare const WORKER_TASK_WORKSPACE_RETENTION = 20;
141
+ export declare function pruneWorkerTaskWorkspaces(incomingWorkspaceDir: string, retention?: number): Promise<string[]>;
140
142
  export declare function hasAgentTaskUploadedArtifact(task: Pick<AgentTaskResponse, 'appId' | 'appVersionId'>): boolean;
141
143
  export declare function appendWorkerTranscriptChunks(input: {
142
144
  client: Pick<ApiClient, 'workerAppendAgentTaskTranscriptChunks'>;
@@ -247,11 +249,9 @@ export declare function prepareClaudeRunConfig(input: {
247
249
  workspaceDir: string;
248
250
  workerUsername: string;
249
251
  envName: string;
250
- sourceCredentialsPath?: string;
251
252
  playdropConfig?: ReturnType<typeof loadConfig>;
252
253
  }): Promise<{
253
- homeDir: string;
254
- claudeConfigDir: string;
254
+ playdropConfigPath: string;
255
255
  }>;
256
256
  export declare function removeAgentRunCredentials(workspaceDir: string): Promise<void>;
257
257
  export declare function removeStaleAgentRunCredentials(workerHomeDir?: string): Promise<void>;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readEnvBoolean = exports.readCodexSandboxMode = exports.readCodexRolloutTokenUsageForThread = exports.extractCodexTokensUsed = exports.extractCodexThreadId = exports.extractCodexRolloutTokenUsage = exports.extractAgentTokenUsage = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = exports.buildWorkerChildEnv = exports.buildCodexExecArgs = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.assertWorkerTokenUsageWithinCap = void 0;
6
+ exports.WORKER_TASK_WORKSPACE_RETENTION = exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readEnvBoolean = exports.readCodexSandboxMode = exports.readCodexRolloutTokenUsageForThread = exports.extractCodexTokensUsed = exports.extractCodexThreadId = exports.extractCodexRolloutTokenUsage = exports.extractAgentTokenUsage = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = exports.buildWorkerChildEnv = exports.buildCodexExecArgs = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.assertWorkerTokenUsageWithinCap = void 0;
7
7
  exports.allocateWorkerDevPort = allocateWorkerDevPort;
8
8
  exports.isWorkerContextCommandAllowed = isWorkerContextCommandAllowed;
9
9
  exports.resolveWorkerExecutionTargetFromRole = resolveWorkerExecutionTargetFromRole;
@@ -11,6 +11,7 @@ exports.resolveWorkerClaimTaskAssignment = resolveWorkerClaimTaskAssignment;
11
11
  exports.nextClaimBackoffMs = nextClaimBackoffMs;
12
12
  exports.claimBackoffDelayMs = claimBackoffDelayMs;
13
13
  exports.resolveWorkerHomeDir = resolveWorkerHomeDir;
14
+ exports.pruneWorkerTaskWorkspaces = pruneWorkerTaskWorkspaces;
14
15
  exports.hasAgentTaskUploadedArtifact = hasAgentTaskUploadedArtifact;
15
16
  exports.appendWorkerTranscriptChunks = appendWorkerTranscriptChunks;
16
17
  exports.readTaskNextStepsFile = readTaskNextStepsFile;
@@ -412,6 +413,33 @@ function workerStateFilePath() {
412
413
  function workerTaskWorkspaceDirFromAssignment(assignment) {
413
414
  return node_path_1.default.join(resolveWorkerHomeDir(), 'tasks', assignment.workspace.folderName);
414
415
  }
416
+ exports.WORKER_TASK_WORKSPACE_RETENTION = 20;
417
+ // Task workspaces (including retained failed ones) otherwise grow without bound on a long-lived
418
+ // worker host. Keep only the most recently touched ones; the incoming task's own directory is
419
+ // never a prune candidate.
420
+ async function pruneWorkerTaskWorkspaces(incomingWorkspaceDir, retention = exports.WORKER_TASK_WORKSPACE_RETENTION) {
421
+ const tasksRoot = node_path_1.default.dirname(node_path_1.default.resolve(incomingWorkspaceDir));
422
+ if (!(0, node_fs_1.existsSync)(tasksRoot)) {
423
+ return [];
424
+ }
425
+ const incoming = node_path_1.default.resolve(incomingWorkspaceDir);
426
+ const entries = (0, node_fs_1.readdirSync)(tasksRoot, { withFileTypes: true })
427
+ .filter((entry) => entry.isDirectory())
428
+ .map((entry) => node_path_1.default.join(tasksRoot, entry.name))
429
+ .filter((dir) => dir !== incoming)
430
+ .map((dir) => ({ dir, mtimeMs: (0, node_fs_1.statSync)(dir).mtimeMs }))
431
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
432
+ const stale = entries.slice(Math.max(0, retention - 1));
433
+ const removed = [];
434
+ for (const entry of stale) {
435
+ await (0, promises_1.rm)(entry.dir, { recursive: true, force: true });
436
+ removed.push(entry.dir);
437
+ }
438
+ if (removed.length > 0) {
439
+ console.error(`pruned ${removed.length} stale task workspace(s), keeping the ${retention} most recent`);
440
+ }
441
+ return removed;
442
+ }
415
443
  function buildWorkerTaskContext(claim, assignment) {
416
444
  const task = claim.task;
417
445
  if (!task) {
@@ -1930,78 +1958,32 @@ async function stageWorkerPlaydropSession(input) {
1930
1958
  },
1931
1959
  }, null, 2), { mode: 0o600 });
1932
1960
  await (0, promises_1.chmod)(playdropConfigPath, 0o600);
1961
+ return playdropConfigPath;
1933
1962
  }
1934
1963
  async function prepareClaudeRunConfig(input) {
1935
1964
  const runRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'claude-run');
1936
1965
  const homeDir = node_path_1.default.join(runRoot, 'home');
1937
- const claudeConfigDir = node_path_1.default.join(runRoot, 'config');
1938
1966
  await (0, promises_1.rm)(runRoot, { recursive: true, force: true });
1939
1967
  await (0, promises_1.mkdir)(runRoot, { recursive: true, mode: 0o700 });
1940
1968
  await (0, promises_1.chmod)(runRoot, 0o700);
1941
1969
  await (0, promises_1.mkdir)(homeDir, { mode: 0o700 });
1942
- await (0, promises_1.mkdir)(claudeConfigDir, { mode: 0o700 });
1943
- const configuredCredentialsPath = input.sourceCredentialsPath?.trim()
1944
- || node_process_1.default.env.PLAYDROP_WORKER_CLAUDE_CREDENTIALS_FILE?.trim();
1945
- const defaultCredentialsPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', '.credentials.json');
1946
- let rawCredentials;
1947
- if (configuredCredentialsPath) {
1948
- const sourceCredentialsPath = node_path_1.default.resolve(configuredCredentialsPath);
1949
- if (!isFile(sourceCredentialsPath)) {
1950
- throw new Error(`worker_claude_credentials_missing:${sourceCredentialsPath}`);
1951
- }
1952
- rawCredentials = (0, node_fs_1.readFileSync)(sourceCredentialsPath, 'utf8');
1953
- }
1954
- else if (isFile(defaultCredentialsPath)) {
1955
- rawCredentials = (0, node_fs_1.readFileSync)(defaultCredentialsPath, 'utf8');
1956
- }
1957
- else if (node_process_1.default.platform === 'darwin') {
1958
- try {
1959
- const result = await execFileAsync('security', [
1960
- 'find-generic-password',
1961
- '-s',
1962
- 'Claude Code-credentials',
1963
- '-w',
1964
- ], { maxBuffer: 1024 * 1024 });
1965
- rawCredentials = result.stdout;
1966
- }
1967
- catch {
1968
- throw new Error('worker_claude_credentials_missing:macos_keychain');
1969
- }
1970
- }
1971
- else {
1972
- throw new Error(`worker_claude_credentials_missing:${defaultCredentialsPath}`);
1973
- }
1974
- let credentials;
1975
- try {
1976
- credentials = JSON.parse(rawCredentials);
1977
- }
1978
- catch {
1979
- throw new Error('worker_claude_credentials_invalid_json');
1980
- }
1981
- const credentialObject = typeof credentials === 'object' && credentials !== null && !Array.isArray(credentials)
1982
- ? credentials
1983
- : null;
1984
- const oauthValue = credentialObject?.claudeAiOauth;
1985
- const claudeAiOauth = typeof oauthValue === 'object' && oauthValue !== null && !Array.isArray(oauthValue)
1986
- ? oauthValue
1987
- : null;
1988
- if (!claudeAiOauth
1989
- || typeof claudeAiOauth.accessToken !== 'string'
1990
- || !claudeAiOauth.accessToken.trim()
1991
- || typeof claudeAiOauth.refreshToken !== 'string'
1992
- || !claudeAiOauth.refreshToken.trim()) {
1993
- throw new Error('worker_claude_oauth_credentials_missing');
1994
- }
1995
- const stagedCredentialsPath = node_path_1.default.join(claudeConfigDir, '.credentials.json');
1996
- await (0, promises_1.writeFile)(stagedCredentialsPath, `${JSON.stringify({ claudeAiOauth })}\n`, { mode: 0o600 });
1997
- await (0, promises_1.chmod)(stagedCredentialsPath, 0o600);
1998
- await stageWorkerPlaydropSession({
1999
- homeDir,
2000
- workerUsername: input.workerUsername,
2001
- envName: input.envName,
2002
- playdropConfig: input.playdropConfig,
2003
- });
2004
- return { homeDir, claudeConfigDir };
1970
+ // The worker does NOT manage Claude authentication. The host is logged in with the Claude CLI;
1971
+ // we call `claude` and let it use the machine's own login exactly as a human would.
1972
+ //
1973
+ // We used to copy the OAuth credential into this per-task config dir. That is what logged the
1974
+ // machine out: the copy refreshed on first use after the 8h access-token expiry, Anthropic
1975
+ // rotated the refresh token (invalidating the one still in the keychain), and the refreshed
1976
+ // credential was thrown away with the task workspace. The host was then permanently logged out.
1977
+ // Observed twice in production (2026-07-13 19:50, 2026-07-14 13:00:23, the latter three seconds
1978
+ // after task 449's Claude exited). Never stage, copy, or write Claude credentials here.
1979
+ return {
1980
+ playdropConfigPath: await stageWorkerPlaydropSession({
1981
+ homeDir,
1982
+ workerUsername: input.workerUsername,
1983
+ envName: input.envName,
1984
+ playdropConfig: input.playdropConfig,
1985
+ }),
1986
+ };
2005
1987
  }
2006
1988
  async function removeAgentRunCredentials(workspaceDir) {
2007
1989
  await Promise.all([
@@ -2096,7 +2078,7 @@ async function runCodex(input) {
2096
2078
  };
2097
2079
  }
2098
2080
  async function runClaude(input) {
2099
- const { homeDir, claudeConfigDir } = await prepareClaudeRunConfig({
2081
+ const { playdropConfigPath } = await prepareClaudeRunConfig({
2100
2082
  workspaceDir: input.workspaceDir,
2101
2083
  workerUsername: input.workerUsername,
2102
2084
  envName: input.envName,
@@ -2107,7 +2089,8 @@ async function runClaude(input) {
2107
2089
  : null;
2108
2090
  let playwrightMcpConfigPath = null;
2109
2091
  if (input.enablePlaywrightMcp) {
2110
- playwrightMcpConfigPath = node_path_1.default.join(claudeConfigDir, 'playwright-mcp.json');
2092
+ playwrightMcpConfigPath = node_path_1.default.join(input.workspaceDir, '.playdrop', 'claude-run', 'playwright-mcp.json');
2093
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(playwrightMcpConfigPath), { recursive: true });
2111
2094
  (0, node_fs_1.writeFileSync)(playwrightMcpConfigPath, `${JSON.stringify((0, runtime_1.buildPlaywrightMcpServerConfig)(), null, 2)}\n`, 'utf8');
2112
2095
  }
2113
2096
  const result = await (0, runtime_1.runLoggedProcess)({
@@ -2129,8 +2112,7 @@ async function runClaude(input) {
2129
2112
  envName: input.envName,
2130
2113
  eventDir: input.eventDir,
2131
2114
  devPort: input.devPort,
2132
- homeDir,
2133
- claudeConfigDir,
2115
+ playdropConfigPath,
2134
2116
  playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
2135
2117
  }),
2136
2118
  stdin: input.prompt,
@@ -3870,6 +3852,7 @@ async function startWorker(options = {}) {
3870
3852
  reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
3871
3853
  const taskContext = buildWorkerTaskContext(claim, assignment);
3872
3854
  workspaceDir = workerTaskWorkspaceDirFromAssignment(assignment);
3855
+ await pruneWorkerTaskWorkspaces(workspaceDir);
3873
3856
  await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
3874
3857
  await (0, promises_1.mkdir)(workspaceDir, { recursive: true });
3875
3858
  const eventDir = node_path_1.default.join(workspaceDir, '.playdrop-task-events');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.12.8",
3
- "build": 10,
3
+ "build": 13,
4
4
  "runtimeSdkVersion": "0.12.5",
5
5
  "runtimeSdkBuild": 2,
6
6
  "clients": {
@@ -28,7 +28,9 @@ export type InstrumentEvidenceManifest = {
28
28
  };
29
29
  export declare const PLAYWRIGHT_MCP_SCREENSHOT_TOOL = "mcp__playwright__browser_take_screenshot";
30
30
  export declare const PLAYWRIGHT_MCP_CONTROLLED_TAB_ID = 1;
31
+ export declare const PLAYWRIGHT_MCP_SCREENSHOT_BUDGET = 100;
31
32
  export declare function isInstrumentScreenshotId(value: string): boolean;
33
+ export declare function normalizePlaywrightScreenshotId(value: unknown): string | null;
32
34
  export declare class ClaudeChromeScreenshotStreamParser {
33
35
  private readonly onScreenshot;
34
36
  private buffer;
@@ -44,9 +46,9 @@ export declare class ClaudeChromeScreenshotStreamParser {
44
46
  export declare class PlaywrightMcpScreenshotStreamParser {
45
47
  private readonly onScreenshot;
46
48
  private buffer;
47
- private readonly pendingScreenshotToolUseIds;
49
+ private readonly pendingScreenshotFilenames;
48
50
  private readonly handledToolUseIds;
49
- private sequence;
51
+ private screenshotCallCount;
50
52
  constructor(onScreenshot: (screenshot: ClaudeChromeScreenshot) => void);
51
53
  push(chunk: string): number;
52
54
  finish(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"instrument-evidence.d.ts","sourceRoot":"","sources":["../src/instrument-evidence.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,yBAAyB,iDAK5B,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhF,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,YAAY,GAAG,WAAW,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,YAAY,GAAG,WAAW,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,sBAAsB,EAAE,yBAAyB,CAAC,CAAC,CAAC;CAC9E,CAAC;AAMF,eAAO,MAAM,8BAA8B,6CAA6C,CAAC;AACzF,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAGlD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AA+CD,qBAAa,kCAAkC;IAKjC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAJzC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA+B;IACzE,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqB;gBAE7B,YAAY,EAAE,CAAC,UAAU,EAAE,sBAAsB,KAAK,IAAI;IAEvF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAqB3B,MAAM,IAAI,MAAM;IAMhB,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,iBAAiB;CAiE1B;AAMD,qBAAa,mCAAmC;IAMlC,OAAO,CAAC,QAAQ,CAAC,YAAY;IALzC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAqB;IACjE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IACvD,OAAO,CAAC,QAAQ,CAAK;gBAEQ,YAAY,EAAE,CAAC,UAAU,EAAE,sBAAsB,KAAK,IAAI;IAEvF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAqB3B,MAAM,IAAI,MAAM;IAMhB,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,iBAAiB;CA8C1B"}
1
+ {"version":3,"file":"instrument-evidence.d.ts","sourceRoot":"","sources":["../src/instrument-evidence.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,yBAAyB,iDAK5B,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhF,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,YAAY,GAAG,WAAW,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,YAAY,GAAG,WAAW,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,sBAAsB,EAAE,yBAAyB,CAAC,CAAC,CAAC;CAC9E,CAAC;AAMF,eAAO,MAAM,8BAA8B,6CAA6C,CAAC;AACzF,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAGlD,eAAO,MAAM,gCAAgC,MAAM,CAAC;AAKpD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAG7E;AA+CD,qBAAa,kCAAkC;IAKjC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAJzC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA+B;IACzE,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqB;gBAE7B,YAAY,EAAE,CAAC,UAAU,EAAE,sBAAsB,KAAK,IAAI;IAEvF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAqB3B,MAAM,IAAI,MAAM;IAMhB,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,iBAAiB;CAiE1B;AAMD,qBAAa,mCAAmC;IAMlC,OAAO,CAAC,QAAQ,CAAC,YAAY;IALzC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAA6B;IACxE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IACvD,OAAO,CAAC,mBAAmB,CAAK;gBAEH,YAAY,EAAE,CAAC,UAAU,EAAE,sBAAsB,KAAK,IAAI;IAEvF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAqB3B,MAAM,IAAI,MAAM;IAMhB,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,aAAa;IAwBrB,OAAO,CAAC,iBAAiB;CA+C1B"}
@@ -20,9 +20,11 @@ __export(instrument_evidence_exports, {
20
20
  ClaudeChromeScreenshotStreamParser: () => ClaudeChromeScreenshotStreamParser,
21
21
  INSTRUMENT_EVIDENCE_NAMES: () => INSTRUMENT_EVIDENCE_NAMES,
22
22
  PLAYWRIGHT_MCP_CONTROLLED_TAB_ID: () => PLAYWRIGHT_MCP_CONTROLLED_TAB_ID,
23
+ PLAYWRIGHT_MCP_SCREENSHOT_BUDGET: () => PLAYWRIGHT_MCP_SCREENSHOT_BUDGET,
23
24
  PLAYWRIGHT_MCP_SCREENSHOT_TOOL: () => PLAYWRIGHT_MCP_SCREENSHOT_TOOL,
24
25
  PlaywrightMcpScreenshotStreamParser: () => PlaywrightMcpScreenshotStreamParser,
25
- isInstrumentScreenshotId: () => isInstrumentScreenshotId
26
+ isInstrumentScreenshotId: () => isInstrumentScreenshotId,
27
+ normalizePlaywrightScreenshotId: () => normalizePlaywrightScreenshotId
26
28
  });
27
29
  module.exports = __toCommonJS(instrument_evidence_exports);
28
30
  const INSTRUMENT_EVIDENCE_NAMES = [
@@ -36,10 +38,15 @@ const SCREENSHOT_ID_IN_TEXT_PATTERN = /\bID:\s*(ss_[A-Za-z0-9_-]+)\b/g;
36
38
  const MAX_STREAM_LINE_LENGTH = 20 * 1024 * 1024;
37
39
  const PLAYWRIGHT_MCP_SCREENSHOT_TOOL = "mcp__playwright__browser_take_screenshot";
38
40
  const PLAYWRIGHT_MCP_CONTROLLED_TAB_ID = 1;
39
- const PLAYWRIGHT_SCREENSHOT_ID_PATTERN = /^pw-[1-9][0-9]*$/;
41
+ const PLAYWRIGHT_MCP_SCREENSHOT_BUDGET = 100;
42
+ const PLAYWRIGHT_SCREENSHOT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}\.(png|jpe?g)$/;
40
43
  function isInstrumentScreenshotId(value) {
41
44
  return SCREENSHOT_ID_PATTERN.test(value) || PLAYWRIGHT_SCREENSHOT_ID_PATTERN.test(value);
42
45
  }
46
+ function normalizePlaywrightScreenshotId(value) {
47
+ const id = typeof value === "string" ? value.trim().toLowerCase() : "";
48
+ return PLAYWRIGHT_SCREENSHOT_ID_PATTERN.test(id) ? id : null;
49
+ }
43
50
  function asRecord(value) {
44
51
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
45
52
  }
@@ -221,9 +228,9 @@ class ClaudeChromeScreenshotStreamParser {
221
228
  class PlaywrightMcpScreenshotStreamParser {
222
229
  onScreenshot;
223
230
  buffer = "";
224
- pendingScreenshotToolUseIds = /* @__PURE__ */ new Set();
231
+ pendingScreenshotFilenames = /* @__PURE__ */ new Map();
225
232
  handledToolUseIds = /* @__PURE__ */ new Set();
226
- sequence = 0;
233
+ screenshotCallCount = 0;
227
234
  constructor(onScreenshot) {
228
235
  this.onScreenshot = onScreenshot;
229
236
  }
@@ -287,7 +294,16 @@ class PlaywrightMcpScreenshotStreamParser {
287
294
  if (item?.["type"] !== "tool_use" || !id || name !== PLAYWRIGHT_MCP_SCREENSHOT_TOOL) {
288
295
  continue;
289
296
  }
290
- this.pendingScreenshotToolUseIds.add(id);
297
+ this.screenshotCallCount += 1;
298
+ if (this.screenshotCallCount > PLAYWRIGHT_MCP_SCREENSHOT_BUDGET) {
299
+ throw new Error(`playwright_screenshot_budget_exceeded:${PLAYWRIGHT_MCP_SCREENSHOT_BUDGET}`);
300
+ }
301
+ const input = asRecord(item?.["input"]);
302
+ const filename = normalizePlaywrightScreenshotId(input?.["filename"]);
303
+ if (!filename) {
304
+ continue;
305
+ }
306
+ this.pendingScreenshotFilenames.set(id, filename);
291
307
  }
292
308
  }
293
309
  recordToolResults(payload) {
@@ -300,11 +316,12 @@ class PlaywrightMcpScreenshotStreamParser {
300
316
  continue;
301
317
  }
302
318
  const toolUseId = typeof toolResult["tool_use_id"] === "string" ? toolResult["tool_use_id"].trim() : "";
303
- if (!this.pendingScreenshotToolUseIds.has(toolUseId) || this.handledToolUseIds.has(toolUseId)) {
319
+ const screenshotId = this.pendingScreenshotFilenames.get(toolUseId);
320
+ if (!screenshotId || this.handledToolUseIds.has(toolUseId)) {
304
321
  continue;
305
322
  }
306
323
  this.handledToolUseIds.add(toolUseId);
307
- this.pendingScreenshotToolUseIds.delete(toolUseId);
324
+ this.pendingScreenshotFilenames.delete(toolUseId);
308
325
  const resultContent = Array.isArray(toolResult["content"]) ? toolResult["content"] : [];
309
326
  const images = resultContent.map(asRecord).filter((item) => item?.["type"] === "image");
310
327
  if (images.length === 0) {
@@ -319,9 +336,8 @@ class PlaywrightMcpScreenshotStreamParser {
319
336
  if (mediaType !== "image/jpeg" && mediaType !== "image/png" || !dataBase64) {
320
337
  throw new Error(`invalid_playwright_screenshot_payload:${toolUseId}`);
321
338
  }
322
- this.sequence += 1;
323
339
  this.onScreenshot({
324
- screenshotId: `pw-${this.sequence}`,
340
+ screenshotId,
325
341
  tabId: PLAYWRIGHT_MCP_CONTROLLED_TAB_ID,
326
342
  toolUseId,
327
343
  mediaType,
@@ -339,7 +355,9 @@ class PlaywrightMcpScreenshotStreamParser {
339
355
  ClaudeChromeScreenshotStreamParser,
340
356
  INSTRUMENT_EVIDENCE_NAMES,
341
357
  PLAYWRIGHT_MCP_CONTROLLED_TAB_ID,
358
+ PLAYWRIGHT_MCP_SCREENSHOT_BUDGET,
342
359
  PLAYWRIGHT_MCP_SCREENSHOT_TOOL,
343
360
  PlaywrightMcpScreenshotStreamParser,
344
- isInstrumentScreenshotId
361
+ isInstrumentScreenshotId,
362
+ normalizePlaywrightScreenshotId
345
363
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playdrop/playdrop-cli",
3
- "version": "0.12.17",
3
+ "version": "0.12.20",
4
4
  "description": "Official Playdrop CLI for publishing browser games, creator apps, and AI-generated game assets on playdrop.ai",
5
5
  "homepage": "https://www.playdrop.ai",
6
6
  "repository": {