diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b3290ff --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# Repo conventions for Claude + +## Pull requests +- Keep PR descriptions short: 2-4 bullets covering what changed and why. No "Summary / Key Changes / Implementation Details" sections, no restating the diff. The diff is the record of what changed; the description explains intent. +- Title should be a single short imperative sentence (<70 chars). +- When a PR is opened externally (e.g. from the Claude Code UI) with a longer description, rewrite it to match the convention above before doing further work on the PR. diff --git a/README.md b/README.md index 813182c..ad919cf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ๐Ÿงช Apify AI Sandbox +# ๐Ÿงช Apify AI Code Sandbox A suite of Apify Actors providing secure, containerized environments for AI coding agents. diff --git a/claude-code/.actor/actor.json b/claude-code/.actor/actor.json index 727ff72..79bb59b 100644 --- a/claude-code/.actor/actor.json +++ b/claude-code/.actor/actor.json @@ -15,28 +15,25 @@ "type": "object", "schemaVersion": 1, "properties": { - "skills": { + "agentSkills": { "title": "Skills", - "type": "array", - "description": "List of skill packages to install for the AI coding agent. Skills are SKILLS.md files that provide specialized instructions. For more info see https://skills.sh/. Example: apify/agent-skills", - "editor": "stringList", - "items": { - "type": "string" - }, - "default": ["apify/agent-skills"], - "prefill": ["apify/agent-skills"] + "type": "string", + "description": "Skill packages to install for the AI coding agent โ€” SKILLS.md files with specialized instructions (see https://skills.sh/). One skill per line, e.g. `anthropics/skills` (blank lines and `#` comments are ignored), or a JSON array like `[\"apify/agent-skills\", \"anthropics/skills\"]`.", + "editor": "textarea", + "default": "apify/agent-skills", + "prefill": "apify/agent-skills\n# One skill per line, e.g. anthropics/skills" }, - "initShellScript": { + "initBashScript": { "title": "Initialization script", "type": "string", "description": "Optional bash script to run on startup for installing packages or setting up the environment.", "editor": "textarea", - "prefill": "apt-get update\n# apt-get install -y curl" + "prefill": "#!/bin/bash\n# Runs on startup. Uncomment or add your own:\n# apt-get update && apt-get install -y jq\n# mkdir -p /sandbox/data" }, "envVars": { "title": "Environment variables", "type": "string", - "description": "Secret environment variables exposed to the sandbox shell, init script, and code execution. Encrypted at rest by Apify and decrypted only at runtime.\n\nAccepts either format:\n- **KEY=VALUE per line** (dotenv-style):\n ```\n OPENAI_API_KEY=sk-...\n MY_VAR=hello\n ```\n- **JSON object**:\n ```\n {\"OPENAI_API_KEY\": \"sk-...\", \"MY_VAR\": \"hello\"}\n ```", + "description": "Secret environment variables exposed to the sandbox shell, init script, and code execution. Encrypted at rest by Apify, decrypted only at runtime. Accepts `KEY=VALUE` per line or a JSON object like `{\"OPENAI_API_KEY\": \"sk-...\", \"MY_VAR\": \"hello\"}`.", "editor": "textarea", "isSecret": true }, @@ -44,9 +41,9 @@ "title": "Idle timeout seconds", "type": "integer", "description": "Automatically shut down after this many seconds of inactivity. Set to 0 to disable. Recommended: set Actor timeout to 0 (infinite) when using this.", - "default": 600, + "default": 900, "editor": "number", - "prefill": 600 + "prefill": 900 }, "proxyMappings": { "title": "Proxy mappings", @@ -87,7 +84,7 @@ "url": { "type": "string", "title": "Main page", - "template": "{{run.containerUrl}}/shell?arg=-c&arg=source%20/app/sandbox_bashrc%3B%20claude" + "template": "{{run.containerUrl}}/shell?launch=claude" } } }, diff --git a/claude-code/README.md b/claude-code/README.md index af57e43..de766ac 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -25,9 +25,9 @@ This Actor [metamorphs](https://docs.apify.com/platform/actors/development/progr | Input | Description | Default | |-------|-------------|---------| -| `skills` | Skill packages to install (SKILLS.md files) | `["apify/agent-skills"]` | -| `initShellScript` | Bash script to run before Claude Code starts | - | -| `idleTimeoutSeconds` | Shutdown after inactivity | 600 | +| `agentSkills` | Skill packages to install (SKILLS.md files), one per line or JSON array | `apify/agent-skills` | +| `initBashScript` | Bash script to run before Claude Code starts | - | +| `idleTimeoutSeconds` | Shutdown after inactivity | 900 | ## ๐Ÿ“š Skills Support diff --git a/claude-code/package-lock.json b/claude-code/package-lock.json index 863bfe5..9461247 100644 --- a/claude-code/package-lock.json +++ b/claude-code/package-lock.json @@ -8,21 +8,21 @@ "name": "claude-code-sandbox", "version": "0.0.1", "dependencies": { - "apify": "^3.4.2" + "apify": "^3.7.2" }, "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.2", - "typescript": "^5.7.3" + "@types/node": "^25.9.2", + "tsx": "^4.22.4", + "typescript": "^6.0.3" }, "engines": { "node": ">=18" } }, "node_modules/@apify/consts": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@apify/consts/-/consts-2.48.1.tgz", - "integrity": "sha512-JPa8E10edotvuGDyVfqdVwRVvQl7wathOLNTL/UjO3ELorC+RLh95b+4dwn9ydCICjU7rDEzKbZ57xCQGXx6ZA==", + "version": "2.53.3", + "resolved": "https://registry.npmjs.org/@apify/consts/-/consts-2.53.3.tgz", + "integrity": "sha512-X26SC8d6kLMSWzBcYldKgzKFJXGnTAka/bRmvL+iYHJEabFzVYupj64kGhar6QTPeGKNt6WR/hNE+dToNqHZQA==", "license": "Apache-2.0" }, "node_modules/@apify/datastructures": { @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -209,9 +209,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -226,9 +226,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -243,9 +243,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -260,9 +260,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -277,9 +277,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -294,9 +294,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -311,9 +311,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -328,9 +328,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -345,9 +345,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -362,9 +362,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -379,9 +379,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -396,9 +396,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -413,9 +413,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -430,9 +430,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -447,9 +447,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -464,9 +464,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -481,9 +481,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -515,9 +515,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -532,9 +532,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -549,9 +549,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], @@ -566,9 +566,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -583,9 +583,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -600,9 +600,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -617,9 +617,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -717,12 +717,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/sax": { @@ -772,12 +772,12 @@ } }, "node_modules/apify": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/apify/-/apify-3.5.3.tgz", - "integrity": "sha512-279q+c6Rz2RjQrDzhjrA8pyy40lQBoMnZBl3Ei+zP/gSh0WEVmTH5D/VbHH4TOQpOHv3CaP2Mwg/O2CaMNB4JQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/apify/-/apify-3.7.2.tgz", + "integrity": "sha512-I4R5Qd1X11vk5/1U0CxKSsMhNwER/3MntePEQb9Pbtf942OhQLh5IURrJZufqtpimkqRy44SlQMU6UhPdoFXhA==", "license": "Apache-2.0", "dependencies": { - "@apify/consts": "^2.47.1", + "@apify/consts": "^2.51.0", "@apify/input_secrets": "^1.2.0", "@apify/log": "^2.4.3", "@apify/timeout": "^0.3.0", @@ -1318,9 +1318,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1331,32 +1331,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -1617,19 +1617,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/get-uri": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", @@ -2320,16 +2307,6 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "license": "MIT" }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/responselike": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", @@ -2551,14 +2528,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -2583,9 +2559,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2609,9 +2585,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/universalify": { diff --git a/claude-code/package.json b/claude-code/package.json index 6d81acd..4ff364d 100644 --- a/claude-code/package.json +++ b/claude-code/package.json @@ -11,11 +11,11 @@ "build": "tsc -p tsconfig.build.json" }, "dependencies": { - "apify": "^3.4.2" + "apify": "^3.7.2" }, "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.2", - "typescript": "^5.7.3" + "@types/node": "^25.9.2", + "tsx": "^4.22.4", + "typescript": "^6.0.3" } } diff --git a/claude-code/tsconfig.build.json b/claude-code/tsconfig.build.json index 2dc9176..f78a908 100644 --- a/claude-code/tsconfig.build.json +++ b/claude-code/tsconfig.build.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "rootDir": "./src" } } diff --git a/openclaw/.actor/actor.json b/openclaw/.actor/actor.json index e246764..eb4389d 100644 --- a/openclaw/.actor/actor.json +++ b/openclaw/.actor/actor.json @@ -15,28 +15,25 @@ "type": "object", "schemaVersion": 1, "properties": { - "skills": { + "agentSkills": { "title": "Skills", - "type": "array", - "description": "List of skill packages to install for the AI coding agent. Skills are SKILLS.md files that provide specialized instructions. For more info see https://skills.sh/. Example: apify/agent-skills", - "editor": "stringList", - "items": { - "type": "string" - }, - "default": ["apify/agent-skills"], - "prefill": ["apify/agent-skills"] + "type": "string", + "description": "Skill packages to install for the AI coding agent โ€” SKILLS.md files with specialized instructions (see https://skills.sh/). One skill per line, e.g. `anthropics/skills` (blank lines and `#` comments are ignored), or a JSON array like `[\"apify/agent-skills\", \"anthropics/skills\"]`.", + "editor": "textarea", + "default": "apify/agent-skills", + "prefill": "apify/agent-skills\n# One skill per line, e.g. anthropics/skills" }, - "initShellScript": { + "initBashScript": { "title": "Initialization script", "type": "string", "description": "Optional bash script to run on startup for installing packages or setting up the environment.", "editor": "textarea", - "prefill": "apt-get update\n# apt-get install -y curl" + "prefill": "#!/bin/bash\n# Runs on startup. Uncomment or add your own:\n# apt-get update && apt-get install -y jq\n# mkdir -p /sandbox/data" }, "envVars": { "title": "Environment variables", "type": "string", - "description": "Secret environment variables exposed to the sandbox shell, init script, and code execution. Encrypted at rest by Apify and decrypted only at runtime.\n\nAccepts either format:\n- **KEY=VALUE per line** (dotenv-style):\n ```\n OPENAI_API_KEY=sk-...\n MY_VAR=hello\n ```\n- **JSON object**:\n ```\n {\"OPENAI_API_KEY\": \"sk-...\", \"MY_VAR\": \"hello\"}\n ```", + "description": "Secret environment variables exposed to the sandbox shell, init script, and code execution. Encrypted at rest by Apify, decrypted only at runtime. Accepts `KEY=VALUE` per line or a JSON object like `{\"OPENAI_API_KEY\": \"sk-...\", \"MY_VAR\": \"hello\"}`.", "editor": "textarea", "isSecret": true }, @@ -63,7 +60,7 @@ "url": { "type": "string", "title": "Main page", - "template": "{{run.containerUrl}}/shell?arg=-c&arg=source%20/app/sandbox_bashrc%3B%20openclaw%20tui" + "template": "{{run.containerUrl}}/shell?launch=openclaw%20tui" } } }, diff --git a/openclaw/README.md b/openclaw/README.md index 9549b2a..d20d7ef 100644 --- a/openclaw/README.md +++ b/openclaw/README.md @@ -27,8 +27,8 @@ On startup, it: | Input | Description | Default | |-------|-------------|---------| -| `skills` | Skill packages to install (SKILLS.md files) | `["apify/agent-skills"]` | -| `initShellScript` | Bash script to run before OpenClaw starts | - | +| `agentSkills` | Skill packages to install (SKILLS.md files), one per line or JSON array | `apify/agent-skills` | +| `initBashScript` | Bash script to run before OpenClaw starts | - | | `idleTimeoutSeconds` | Shutdown after inactivity (0 = disabled) | 0 | ## ๐Ÿ“š Skills Support diff --git a/openclaw/package-lock.json b/openclaw/package-lock.json index 8c59e6f..8342319 100644 --- a/openclaw/package-lock.json +++ b/openclaw/package-lock.json @@ -8,12 +8,12 @@ "name": "openclaw-sandbox", "version": "0.0.1", "dependencies": { - "apify": "^3.4.2" + "apify": "^3.7.2" }, "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.2", - "typescript": "^5.7.3" + "@types/node": "^25.9.2", + "tsx": "^4.22.4", + "typescript": "^6.0.3" }, "engines": { "node": ">=18" @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -209,9 +209,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -226,9 +226,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -243,9 +243,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -260,9 +260,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -277,9 +277,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -294,9 +294,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -311,9 +311,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -328,9 +328,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -345,9 +345,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -362,9 +362,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -379,9 +379,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -396,9 +396,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -413,9 +413,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -430,9 +430,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -447,9 +447,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -464,9 +464,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -481,9 +481,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -515,9 +515,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -532,9 +532,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -549,9 +549,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], @@ -566,9 +566,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -583,9 +583,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -600,9 +600,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -617,9 +617,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -717,12 +717,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/sax": { @@ -772,12 +772,12 @@ } }, "node_modules/apify": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/apify/-/apify-3.5.3.tgz", - "integrity": "sha512-279q+c6Rz2RjQrDzhjrA8pyy40lQBoMnZBl3Ei+zP/gSh0WEVmTH5D/VbHH4TOQpOHv3CaP2Mwg/O2CaMNB4JQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/apify/-/apify-3.7.2.tgz", + "integrity": "sha512-I4R5Qd1X11vk5/1U0CxKSsMhNwER/3MntePEQb9Pbtf942OhQLh5IURrJZufqtpimkqRy44SlQMU6UhPdoFXhA==", "license": "Apache-2.0", "dependencies": { - "@apify/consts": "^2.47.1", + "@apify/consts": "^2.51.0", "@apify/input_secrets": "^1.2.0", "@apify/log": "^2.4.3", "@apify/timeout": "^0.3.0", @@ -912,7 +912,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1319,9 +1318,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1332,32 +1331,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -1618,19 +1617,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/get-uri": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", @@ -2321,16 +2307,6 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "license": "MIT" }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/responselike": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", @@ -2552,14 +2528,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -2584,9 +2559,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2610,9 +2585,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/universalify": { diff --git a/openclaw/package.json b/openclaw/package.json index be214ee..2dd8148 100644 --- a/openclaw/package.json +++ b/openclaw/package.json @@ -11,11 +11,11 @@ "build": "tsc -p tsconfig.build.json" }, "dependencies": { - "apify": "^3.4.2" + "apify": "^3.7.2" }, "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.2", - "typescript": "^5.7.3" + "@types/node": "^25.9.2", + "tsx": "^4.22.4", + "typescript": "^6.0.3" } } diff --git a/openclaw/src/main.ts b/openclaw/src/main.ts index 7d5392d..b14f2df 100644 --- a/openclaw/src/main.ts +++ b/openclaw/src/main.ts @@ -40,6 +40,13 @@ cat > ~/.openclaw/openclaw.json << 'CLAWEOF' "workspace": "~/.openclaw/workspace" } }, + "tools": { + "exec": { + "host": "gateway", + "security": "full", + "ask": "off" + } + }, "models": { "mode": "merge", "providers": { @@ -73,12 +80,22 @@ openclaw onboard \\ --gateway-port 18789 \\ --skip-daemon \\ --skip-health +# Auto-approve all tool actions (YOLO) โ€” safe inside the sandbox. The effective +# policy is the stricter of two layers, so open both: the requested policy in +# openclaw.json (tools.exec, set above) and the host-local approvals file below. +openclaw exec-policy preset yolo || true +cat > ~/.openclaw/exec-approvals.json << 'APPROVEEOF' +{ + "version": 1, + "defaults": { "security": "full", "ask": "off", "askFallback": "full" } +} +APPROVEEOF nohup openclaw gateway run --bind loopback --port 18789 > /tmp/openclaw-gateway.log 2>&1 & sleep 3 echo "OpenClaw started: http://127.0.0.1:18789/openclaw/"`; // Combine OpenClaw init script with any user-provided init script -const userInitScript = (input as Record)?.initShellScript as string | undefined; +const userInitScript = (input as Record)?.initBashScript as string | undefined; const combinedInitScript = userInitScript ? `${openclawInitScript}\n\n# User-provided init script\n${userInitScript}` : openclawInitScript; @@ -86,7 +103,7 @@ const combinedInitScript = userInitScript // Build the merged input, injecting the OpenClaw init script const mergedInput = { ...((input as Record) || {}), - initShellScript: combinedInitScript, + initBashScript: combinedInitScript, }; console.log(`๐Ÿ”„ Metamorphing into: ${sandboxActorName}`); diff --git a/openclaw/tsconfig.build.json b/openclaw/tsconfig.build.json index 2dc9176..f78a908 100644 --- a/openclaw/tsconfig.build.json +++ b/openclaw/tsconfig.build.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "rootDir": "./src" } } diff --git a/opencode/.actor/actor.json b/opencode/.actor/actor.json index 39b504d..6e31d39 100644 --- a/opencode/.actor/actor.json +++ b/opencode/.actor/actor.json @@ -15,28 +15,25 @@ "type": "object", "schemaVersion": 1, "properties": { - "skills": { + "agentSkills": { "title": "Skills", - "type": "array", - "description": "List of skill packages to install for the AI coding agent. Skills are SKILLS.md files that provide specialized instructions. For more info see https://skills.sh/. Example: apify/agent-skills", - "editor": "stringList", - "items": { - "type": "string" - }, - "default": ["apify/agent-skills"], - "prefill": ["apify/agent-skills"] + "type": "string", + "description": "Skill packages to install for the AI coding agent โ€” SKILLS.md files with specialized instructions (see https://skills.sh/). One skill per line, e.g. `anthropics/skills` (blank lines and `#` comments are ignored), or a JSON array like `[\"apify/agent-skills\", \"anthropics/skills\"]`.", + "editor": "textarea", + "default": "apify/agent-skills", + "prefill": "apify/agent-skills\n# One skill per line, e.g. anthropics/skills" }, - "initShellScript": { + "initBashScript": { "title": "Initialization script", "type": "string", "description": "Optional bash script to run on startup for installing packages or setting up the environment.", "editor": "textarea", - "prefill": "apt-get update\n# apt-get install -y curl" + "prefill": "#!/bin/bash\n# Runs on startup. Uncomment or add your own:\n# apt-get update && apt-get install -y jq\n# mkdir -p /sandbox/data" }, "envVars": { "title": "Environment variables", "type": "string", - "description": "Secret environment variables exposed to the sandbox shell, init script, and code execution. Encrypted at rest by Apify and decrypted only at runtime.\n\nAccepts either format:\n- **KEY=VALUE per line** (dotenv-style):\n ```\n OPENAI_API_KEY=sk-...\n MY_VAR=hello\n ```\n- **JSON object**:\n ```\n {\"OPENAI_API_KEY\": \"sk-...\", \"MY_VAR\": \"hello\"}\n ```", + "description": "Secret environment variables exposed to the sandbox shell, init script, and code execution. Encrypted at rest by Apify, decrypted only at runtime. Accepts `KEY=VALUE` per line or a JSON object like `{\"OPENAI_API_KEY\": \"sk-...\", \"MY_VAR\": \"hello\"}`.", "editor": "textarea", "isSecret": true }, @@ -44,9 +41,9 @@ "title": "Idle timeout seconds", "type": "integer", "description": "Automatically shut down after this many seconds of inactivity. Set to 0 to disable. Recommended: set Actor timeout to 0 (infinite) when using this.", - "default": 600, + "default": 900, "editor": "number", - "prefill": 600 + "prefill": 900 } }, @@ -63,7 +60,7 @@ "url": { "type": "string", "title": "Main page", - "template": "{{run.containerUrl}}/shell?arg=-c&arg=source%20/app/sandbox_bashrc%3B%20opencode" + "template": "{{run.containerUrl}}/shell?launch=opencode" } } }, diff --git a/opencode/README.md b/opencode/README.md index da3c86e..a98b691 100644 --- a/opencode/README.md +++ b/opencode/README.md @@ -24,9 +24,9 @@ This Actor [metamorphs](https://docs.apify.com/platform/actors/development/progr | Input | Description | Default | |-------|-------------|---------| -| `skills` | Skill packages to install (SKILLS.md files) | `["apify/agent-skills"]` | -| `initShellScript` | Bash script to run before OpenCode starts | - | -| `idleTimeoutSeconds` | Shutdown after inactivity | 600 | +| `agentSkills` | Skill packages to install (SKILLS.md files), one per line or JSON array | `apify/agent-skills` | +| `initBashScript` | Bash script to run before OpenCode starts | - | +| `idleTimeoutSeconds` | Shutdown after inactivity | 900 | ## ๐Ÿ“š Skills Support diff --git a/opencode/package.json b/opencode/package.json index 7abe3cb..c2e15e6 100644 --- a/opencode/package.json +++ b/opencode/package.json @@ -11,11 +11,11 @@ "build": "tsc -p tsconfig.build.json" }, "dependencies": { - "apify": "^3.4.2" + "apify": "^3.7.2" }, "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.2", - "typescript": "^5.7.3" + "@types/node": "^25.9.2", + "tsx": "^4.22.4", + "typescript": "^6.0.3" } } diff --git a/opencode/tsconfig.build.json b/opencode/tsconfig.build.json index 2dc9176..f78a908 100644 --- a/opencode/tsconfig.build.json +++ b/opencode/tsconfig.build.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "rootDir": "./src" } } diff --git a/sandbox/.actor/actor.json b/sandbox/.actor/actor.json index 54806ff..1570c3b 100644 --- a/sandbox/.actor/actor.json +++ b/sandbox/.actor/actor.json @@ -1,72 +1,80 @@ { "actorSpecification": 1, "name": "apify-ai-sandbox", - "title": "Apify AI Sandbox", - "description": "Apify AI Sandbox Actor that allows execution of code and operations in a containerized environment for AI coding agents", + "title": "Apify AI Code Sandbox", + "description": "Actor that allows execution of code and operations in a secure containerized environment for AI coding agents", "version": "0.0", "buildTag": "latest", "usesStandbyMode": false, "meta": { "templateId": "ts-standby", - "generatedBy": "claude-haiku-4.5" + "generatedBy": "claude-opus-4.8" }, "input": { - "title": "Actor input schema", - "description": "AI Sandbox for running AI coding operations in a containerized environment. ๐Ÿš€\n\n## How to use\n* **Connect with MCP client:** Use a Model Context Protocol (MCP) client to interact with this sandbox. See https://modelcontextprotocol.io/clients\n* **Use Apify SDK:** Connect programmatically using the Apify SDK for direct integration\n* **REST API:** Access the sandbox directly via REST API endpoints\n\n## Configuration\n* **Memory & timeout:** Configure run options below to set memory allocation and execution timeout\n* **Check logs:** Open the Actor run log console to view connection details and operation output", + "title": "Sandbox input settings", + "description": "Sandbox for secure execution of AI-generated code accessible via MCP, REST API, and interactive shell.\n\nThe sandbox provides pre-configured Claude Code, Codex CLI, and OpenCode AI coding agents. Beware that you will be charged for tokens they use through OpenRouter API pricing.", "type": "object", "schemaVersion": 1, "properties": { - "skills": { - "title": "Skills", - "type": "array", - "description": "List of skill packages to install for the AI coding agent. Skills are SKILLS.md files that provide specialized instructions. For more info see https://skills.sh/. Example: apify/agent-skills", - "editor": "stringList", - "items": { - "type": "string" - }, - "default": ["apify/agent-skills"], - "prefill": ["apify/agent-skills"] + "agentSkills": { + "title": "Agent skills", + "type": "string", + "description": "Skill packages to install for the AI coding agent โ€” SKILLS.md files with specialized instructions (see https://skills.sh/). One skill per line โ€” a GitHub `owner/repo` (e.g. `anthropics/skills`) or repo URL (e.g. `https://github.com/anthropics/skills`); blank lines and `#` comments are ignored. Also accepts a JSON array like `[\"apify/agent-skills\", \"anthropics/skills\"]`.", + "editor": "textarea", + "default": "apify/agent-skills", + "prefill": "apify/agent-skills\n# One skill per line โ€” owner/repo or a GitHub repo URL:\n# anthropics/skills\n# https://github.com/anthropics/skills" }, "nodeDependencies": { "title": "Node.js dependencies", - "type": "object", - "description": "npm package.json dependencies object for JavaScript and TypeScript code execution (/sandbox/js-ts). Paste your dependencies in npm format. Example: {\"zod\": \"^3.0\", \"axios\": \"latest\"}", - "editor": "json", - "prefill": {} + "type": "string", + "description": "npm packages for JavaScript/TypeScript code execution (/sandbox/js-ts). One `package@version` per line โ€” same syntax `npm install ` accepts (omit `@version` for `latest`; blank lines and `#` comments are ignored). Also accepts a package.json-style JSON object like `{\"zod\": \"^3.0\", \"axios\": \"latest\"}`.", + "editor": "textarea", + "prefill": "# One package@version per line (omit @version for latest):\n# zod@^3.0\n# axios" }, - "pythonRequirementsTxt": { + "pythonRequirements": { "title": "Python requirements", "type": "string", - "description": "Python requirements in requirements.txt format for Python code execution (/sandbox/py). Paste your dependencies one per line. Example:\n requests==2.31.0\npandas>=2.0.0", + "description": "Python packages for Python code execution (/sandbox/py), in requirements.txt format โ€” one package per line, blank lines and `#` comments are ignored.", "editor": "textarea", - "prefill": "" + "prefill": "# One package per line:\n# requests==2.31.0\n# pandas" + }, + "mcpConnectors": { + "title": "MCP connectors", + "type": "array", + "description": "One or more MCP connectors that will be available to the sandbox. They are pre-configured in Claude Code, Codex, and OpenCode, as well as written to `/sandbox/mcp.json` so tools like `mcpc connect` pick them up immediately.", + "resourceType": "mcpConnector", + "mcpServers": [{ "url": "*" }], + "editor": "resourcePicker", + "default": [] }, - "initShellScript": { - "title": "Initialization script", + "initBashScript": { + "title": "Setup script", "type": "string", - "description": "Optional bash script to customize the sandbox environment. Runs after library installation in /sandbox directory. Use for installing system packages, creating directories, or other setup tasks.", + "description": "Bash script execute run on startup, after Node and Python dependencies are installed. Use it to install system packages, create directories, or set up the environment.", "editor": "textarea", - "prefill": "apt-get update\n# apt-get install -y curl\n# mkdir /sandbox/data" + "prefill": "#!/bin/bash\n# Runs on startup. Uncomment or add your own:\n# apt-get update && apt-get install -y jq\n# mkdir -p /sandbox/data" }, "envVars": { "title": "Environment variables", "type": "string", - "description": "Secret env vars exposed **only to the init bash script**, then deleted before the shell, MCP server, and code execution start. Encrypted at rest by Apify.\n\nAccepts `KEY=VALUE` per line or a JSON object, e.g. `{\"GITHUB_TOKEN\": \"ghp_...\"}`.", + "description": "Secret environment variables that are exposed **only to the setup script**, then deleted before the shell, MCP server, and code execution start. Encrypted at rest by Apify.\n\nAccepts dotenv format (`KEY=VALUE` per line with # comments), or a JSON object, e.g. `{\"KEY\": \"VALUE\"}`.", "editor": "textarea", "isSecret": true }, - "idleTimeoutSeconds": { - "title": "Idle timeout seconds", + "idleTimeoutSecs": { + "title": "Idle timeout", "type": "integer", - "description": "The container will automatically shut down after this period of inactivity (no requests, no shell interaction). Set to 0 to disable. Recommended to set Actor execution timeout to 0 (infinite) when using this.", - "default": 600, + "description": "The container shuts down automatically after this many seconds of inactivity (no requests or shell interaction). Set to 0 to disable. Recommended: set the standard Actor timeout to 0 (infinite) when using this setting.", + "default": 900, "editor": "number", - "prefill": 600 + "unit": "seconds", + "minimum": 0, + "prefill": 900 }, - "proxyMappings": { - "title": "Proxy mappings", + "bridges": { + "title": "Bridges", "type": "array", - "description": "Map external paths to local web servers. Example: [{\"path\": \"/myapp\", \"target\": \"http://127.0.0.1:3000/myapp\"}]", + "description": "Expose web servers that you start **inside** the sandbox at a public URL path on this container, so they're reachable from the browser or other services. Each bridge forwards `/` to `http://127.0.0.1:/...` over HTTP and WebSocket. Use for your own dev server, a TUI gateway, an admin UI, etc.\n\nExample: `[{\"path\": \"/myapp\", \"target\": \"http://127.0.0.1:3000/myapp\"}]` โ€” visiting `/myapp` forwards to the local server on port 3000.\n\nBridges can also be added/removed at runtime via the `/bridges` API.", "editor": "json", "items": { "type": "object", @@ -94,7 +102,7 @@ "output": { "actorOutputSchemaVersion": 1, - "title": "Apify AI Sandbox", + "title": "Apify AI Code Sandbox", "description": "", "type": "object", diff --git a/sandbox/AGENTS.md b/sandbox/AGENTS.md index 4e2596a..e97870d 100644 --- a/sandbox/AGENTS.md +++ b/sandbox/AGENTS.md @@ -1,6 +1,6 @@ -# Apify AI Sandbox - Development Guide +# Apify AI Code Sandbox - Development Guide -This guide is for AI coding agents working on the Apify AI Sandbox Actor codebase. +This guide is for AI coding agents working on the Apify AI Code Sandbox Actor codebase. Important: Before you begin, fill in the `generatedBy` property in the meta section of `.actor/actor.json`. Replace it with the model you're currently using. This helps Apify monitor and improve AGENTS.md for specific LLM models. @@ -34,7 +34,8 @@ tsc -p tsconfig.build.json # Full build with output # Building is slow and unnecessary for development/testing - tsx runs TypeScript directly # Testing -npm test # Run tests (placeholder - no unit tests) +npm test # Run unit + e2e (full suite, local dev) +npm run test:unit # Run unit tests only (tests/unit/*.test.ts via node --test + tsx) npm run test:e2e # Run E2E platform tests (deploys to Apify) # Apify CLI @@ -45,11 +46,20 @@ apify push # Deploy to Apify platform ### Running a Single Test -Currently there are no unit tests. The E2E test suite deploys to Apify platform and tests all endpoints: +Unit tests live in `tests/unit/*.test.ts` and run via Node's built-in test runner with `tsx` as the loader (no extra deps). The Dockerfile runs `npm run test:unit` after `npm run build`, so a regression in pure-logic code fails the build. ```bash +# Run all unit tests +npm run test:unit + +# Run a single unit test file +node --import tsx --test tests/unit/node-deps.test.ts + # Run E2E platform tests (deploys Actor, runs tests, cleans up) npm run test:e2e + +# Run the full suite (unit + e2e) +npm test ``` ## Code Style Guidelines @@ -120,7 +130,32 @@ app.post('/endpoint', async (req: Request, res: Response) => { ### Testing - Tests in `tests/` directory with `.ts` extension, executable with `tsx` -- E2E platform test suite: `tests/e2e.ts` (deploys to Apify, runs 47 endpoint tests, cleans up) +- **Unit tests:** files matching `tests/unit/*.test.ts`, run via `npm run test:unit` (Node's built-in `node:test` runner via `node --import tsx`, no extra deps). Also run during the Docker build. +- **E2E platform test suite:** `tests/e2e.ts` (deploys to Apify, runs endpoint tests, cleans up) + +**When to add a unit test:** any new pure function (parser, validator, transform) โ€” anything you can exercise without spinning up the Actor, Express, or external services. Prefer unit tests for I/O-free logic; reserve E2E for endpoint behavior. Bug fixes in pure logic should land with a regression test. + +**How to add a unit test:** + +1. Create `tests/unit/.test.ts` next to existing test files. +2. Use `node:test` (`describe`/`it`) and `node:assert/strict`: + + ```typescript + /* eslint-disable @typescript-eslint/no-floating-promises -- node:test's describe/it return promises by design */ + import assert from 'node:assert/strict'; + import { describe, it } from 'node:test'; + + import { myFn } from '../../src/my-module.js'; // .js extension required + + describe('myFn', () => { + it('does the thing', () => { + assert.deepEqual(myFn('input'), { ok: true }); + }); + }); + ``` + +3. Run `npm run test:unit` to verify. Test files are picked up by the `tests/unit/*.test.ts` glob โ€” no further wiring needed. +4. Keep tests pure: no network, no filesystem writes, no `apify`/`Actor.init()`. If you need fixtures, inline them in the test file. ## What are Apify Actors? diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index 1d65304..eed2798 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -12,8 +12,14 @@ RUN apt-get update && apt-get install -y \ ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Build ttyd from source -RUN git clone --depth 1 https://github.com/tsl0922/ttyd.git /tmp/ttyd \ +# Build ttyd from source, pinned for reproducible builds. The terminal page +# relabels ttyd's own reconnect overlay client-side (see RECONNECT_OVERLAY_SCRIPT +# in sandbox/src/templates/shell.ts), keyed off ttyd's exact overlay strings +# ("Press โŽ to Reconnect", "Reconnecting...", "Reconnected"). Before bumping this +# version, confirm those strings are unchanged in ttyd's client: +# https://github.com/tsl0922/ttyd/blob/main/html/src/components/terminal/xterm/index.ts +ARG TTYD_VERSION=1.7.7 +RUN git clone --branch "${TTYD_VERSION}" --depth 1 https://github.com/tsl0922/ttyd.git /tmp/ttyd \ && cd /tmp/ttyd && mkdir build && cd build \ && cmake .. \ && make && make install @@ -40,39 +46,64 @@ RUN [ -n "$APIFY_IS_AT_HOME" ] || npm run test:unit # Stage 2: Runtime - Node.js image FROM node:24-trixie-slim -# Install required dependencies for sandbox operations -RUN apt-get update && apt-get install -y \ +# Install required dependencies for sandbox operations. +# ttyd is compiled from source in the builder stage and copied into this stage +# as a finished binary (see `COPY --from=builder .../ttyd` below), so the runtime +# only needs the shared libraries it uses โ€” not the `-dev` packages, headers, and +# build toolchain that the compile step required: +# - libjson-c5, libwebsockets19t64, libuv1t64: libraries ttyd links against +# (the `t64` suffix reflects Debian trixie's 64-bit time_t transition). +# - libwebsockets-evlib-uv: the libuv event-loop plugin that libwebsockets +# dlopen()s when ttyd creates its context. It is NOT a link-time dependency, +# so `ldd ttyd` and `ttyd --version` won't reveal it's missing โ€” but without +# it ttyd dies at runtime with "lws_create_context: failed to load evlib_uv" +# and the interactive shell never starts. Keep it when trimming this list. +# We pass --no-install-recommends to drop the recommended-but-unused packages +# that otherwise bloated this layer. build-essential and openssh-client are +# recommends we DO want, so they are listed explicitly: build-essential lets +# sandboxed code compile native node-gyp / C modules at runtime, and +# openssh-client enables git-over-SSH. We intentionally omit python3-dev, so +# pip relies on prebuilt wheels and cannot build Python C extensions from +# source โ€” add python3-dev (plus any package-specific -dev libs) if that ever +# becomes a requirement. +RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ curl \ wget \ git \ + openssh-client \ python3 \ python3-venv \ python3-pip \ + build-essential \ ca-certificates \ - libjson-c-dev \ - libwebsockets-dev \ + libjson-c5 \ + libwebsockets19t64 \ + libuv1t64 \ + libwebsockets-evlib-uv \ libsecret-1-0 \ procps \ jq \ && rm -rf /var/lib/apt/lists/* -# Install tsx globally for TypeScript execution in execute-code endpoint -RUN npm install -g tsx - -# Install Apify CLI globally -# --ignore-scripts works around apify-client's `only-allow pnpm` preinstall hook -RUN npm install -g --ignore-scripts apify-cli - -# Install Apify MCP CLI globally (package: @apify/mcpc, binary: mcpc) -# Using beta for named connection support (mcpc connect ... @session) -RUN npm install -g @apify/mcpc@beta - -# Install Claude Code CLI -RUN curl -fsSL https://claude.ai/install.sh | bash - -# Install OpenCode CLI -RUN curl -fsSL https://opencode.ai/install | bash +# Install global CLI tooling in a single layer, then drop the npm cache so it +# doesn't bloat the image: +# - tsx: TypeScript execution in the execute-code endpoint +# - apify-cli: --ignore-scripts works around apify-client's `only-allow pnpm` +# preinstall hook +# - @apify/mcpc: MCP CLI (binary: mcpc) +RUN npm install -g tsx \ + && npm install -g --ignore-scripts apify-cli \ + && npm install -g @apify/mcpc \ + && npm cache clean --force + +# NOTE: The AI coding agent CLIs (Claude Code, OpenCode, Codex) are deliberately +# NOT installed here. They are installed lazily on first use โ€” when the user +# clicks a launch button on the landing page or types `claude`/`opencode`/`codex` +# in the shell โ€” by the functions in agent-launchers.sh (copied in below and +# sourced from the shell rcfile). This keeps the image small and always pulls +# the latest agent release. Their configuration overrides are still baked in +# below, so each agent starts pre-configured the moment it is installed. # Create sandbox directory for operations RUN mkdir -p /sandbox && chmod 755 /sandbox @@ -80,8 +111,8 @@ RUN mkdir -p /sandbox && chmod 755 /sandbox # Create Python sandbox directory with venv and pre-install apify-client RUN mkdir -p /sandbox/py && chmod 755 /sandbox/py && \ python3 -m venv /sandbox/py/venv && \ - /sandbox/py/venv/bin/pip install --upgrade pip && \ - /sandbox/py/venv/bin/pip install apify-client && \ + /sandbox/py/venv/bin/pip install --no-cache-dir --upgrade pip && \ + /sandbox/py/venv/bin/pip install --no-cache-dir apify-client && \ echo "apify-client pre-installed in Python venv" # Create JS/TS sandbox directory with proper package.json and pre-install apify-client @@ -89,6 +120,7 @@ RUN mkdir -p /sandbox/js-ts && chmod 755 /sandbox/js-ts && \ cd /sandbox/js-ts && \ echo '{"name":"apify-sandbox-js-ts","version":"1.0.0","description":"Sandbox for JS/TS code execution","type":"module","dependencies":{"apify-client":"*"}}' > package.json && \ npm install --ignore-scripts && \ + npm cache clean --force && \ echo "apify-client pre-installed in Node.js environment" # Copy AGENTS.md to sandbox for AI coding agents @@ -110,6 +142,16 @@ COPY --from=builder /build/dist /app/dist # Copy ttyd binary from builder COPY --from=builder /usr/local/bin/ttyd /usr/local/bin/ttyd +# Smoke-test that ttyd actually starts its server โ€” not just that it loads +# (`ttyd --version`). Creating the libwebsockets context dlopen()s the libuv +# event-loop plugin (libwebsockets-evlib-uv), which `--version` never exercises, +# so a missing plugin or runtime lib fails the build here instead of breaking the +# shell at run time. ttyd is a long-running server, so cap it with `timeout` and +# assert it reached "Listening on port". +RUN OUTPUT="$(timeout 5 ttyd -p 7681 -a -W bash 2>&1 || true)"; \ + echo "$OUTPUT"; \ + echo "$OUTPUT" | grep -q "Listening on port" + # Copy package.json and production node_modules from builder COPY --from=builder /build/package.json /app/package.json COPY --from=builder /build/package-lock.json /app/package-lock.json @@ -119,7 +161,7 @@ RUN mkdir -p /root/.config/opencode COPY --from=builder /build/artifacts/opencode.json /root/.config/opencode/opencode.json # Install production dependencies only -RUN npm install --production +RUN npm install --production && npm cache clean --force # Add local bin to PATH for CLI tools (Claude at ~/.local/bin, OpenCode at ~/.opencode/bin) ENV PATH="/root/.local/bin:/root/.opencode/bin:$PATH" @@ -138,6 +180,10 @@ ENV CLAUDE_CODE_SANDBOXED=1 # Set Claude Code max output tokens ENV CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000 +# Skip Claude Code's first-run onboarding wizard (theme/login picker) so the CLI +# starts straight into a session. +RUN echo '{"hasCompletedOnboarding": true}' > /root/.claude.json + # Pre-configure Claude Code with sandbox-friendly defaults so the user never has # to answer interactive prompts on launch: # - permissions.defaultMode=bypassPermissions -> run tools without asking @@ -148,6 +194,54 @@ ENV CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000 RUN mkdir -p /root/.claude && \ echo '{"permissions":{"defaultMode":"bypassPermissions"},"skipDangerousModePermissionPrompt":true}' > /root/.claude/settings.json +# Configure Codex CLI to use the Apify OpenRouter proxy โ€” the same proxy that backs +# Claude Code and OpenCode โ€” and to auto-approve everything (safe inside the sandbox). +# Codex reads the bearer token from APIFY_TOKEN at runtime, and custom providers +# default to requires_openai_auth=false, so Codex starts straight into a session with +# no "Sign in with ChatGPT" prompt. Bare keys must precede the [table] headers so the +# MCP-connector block appended at runtime (see src/mcp-agent-config.ts) stays valid TOML. +# wire_api MUST be "responses": Codex removed support for the older "chat" +# (chat/completions) wire protocol, and the Apify OpenRouter proxy speaks the +# OpenAI Responses API, so Codex talks to it at /responses. +# model is pinned to OpenRouter's "openai/gpt-5.5", OpenAI's current flagship. +# Pinning the concrete version rather than the "~openai/gpt-latest" floating +# alias lets Codex resolve its bundled model metadata (context window etc.) by +# matching the "gpt-5.5" slug suffix, so it no longer warns that metadata is +# "not found" and silently falls back to defaults that mis-size the context +# window. Bump this when a newer flagship ships. +# [projects."/sandbox"] pre-trusts the sandbox working dir (ttyd starts shells +# there) so Codex skips its interactive "Do you trust this directory?" prompt on +# first launch; Codex has no global trust-all switch. +RUN mkdir -p /root/.codex && \ + printf '%s\n' \ + 'model = "openai/gpt-5.5"' \ + 'model_provider = "apify-openrouter"' \ + 'approval_policy = "never"' \ + 'sandbox_mode = "danger-full-access"' \ + '' \ + '[model_providers.apify-openrouter]' \ + 'name = "apify/openrouter"' \ + 'base_url = "https://openrouter.apify.actor/api/v1"' \ + 'env_key = "APIFY_TOKEN"' \ + 'requires_openai_auth = false' \ + 'wire_api = "responses"' \ + '' \ + '[projects."/sandbox"]' \ + 'trust_level = "trusted"' \ + > /root/.codex/config.toml + +# Lazy-install launchers for the coding agents. Sourced by the shell rcfile (see +# SANDBOX_BASHRC in src/templates/shell.ts) so a launch button or a plain +# `claude`/`opencode`/`codex` installs the agent on first use, then runs it. +# Validate the script at build time โ€” both that it parses and that it actually +# defines the three functions โ€” so a broken launcher fails the build instead of +# the user's first launch. +COPY scripts/agent-launchers.sh /app/agent-launchers.sh +RUN bash -n /app/agent-launchers.sh \ + && bash -c '. /app/agent-launchers.sh; for f in claude opencode codex; do \ + [ "$(type -t "$f")" = function ] || { echo "agent-launchers.sh did not define function: $f"; exit 1; }; \ + done' + # Capture tool versions at build time for fast shell startup COPY scripts/capture-versions.sh /tmp/capture-versions.sh RUN chmod +x /tmp/capture-versions.sh && /tmp/capture-versions.sh && rm /tmp/capture-versions.sh diff --git a/sandbox/README.md b/sandbox/README.md index a0ac5fc..d146891 100644 --- a/sandbox/README.md +++ b/sandbox/README.md @@ -1,421 +1,166 @@ -# Apify AI Sandbox +# Apify AI Code Sandbox -Isolated sandbox for running AI coding operations in a containerized environment. ๐Ÿš€ +Secure, isolated container for executing arbitrary code, built for AI coding agents and untrusted code. +Connect over **MCP**, a **REST API**, or an **interactive browser shell**. +Ships with **Claude Code**, **Codex CLI**, and **OpenCode** pre-configured and ready to launch. -## Use cases - -- **๐Ÿ”’ Execute untrusted code safely:** Run potentially unsafe code in an isolated container with controlled resources and security boundaries -- **๐Ÿค– AI agent development:** Provide isolated and managed development environments where AI agents can code, test, and execute operations securely -- **๐Ÿ“ฆ Sandboxed operations:** Execute system commands, file operations, and custom scripts in a contained environment -- **๐Ÿ–ฅ๏ธ Interactive debugging:** Access the sandbox via browser-based shell terminal for real-time exploration and troubleshooting -- **๐Ÿ”€ Dynamic reverse proxy:** Expose local services (web servers, APIs, dashboards) running inside the container to external URL paths - accessible from outside the container -- **๐Ÿ”— Apify Actor orchestration:** Agents can access the limited permissions Apify token (available as `APIFY_TOKEN` env var) to run other [limited permissions Actors](https://docs.apify.com/platform/actors/development/permissions), process or analyze their output, and build complex data pipelines by combining results from multiple Actors - -## Quickstart - -### Start the Actor - -1. Run it on the Apify platform through the [Console](https://console.apify.com/) -2. Check the Actor run log console for connection details (host, port, MCP endpoint URL) -3. Open the landing page link from the run logs for connection details, quick links (shell + health), and endpoint URLs for the current run. - -## Ways to connect - -Start the Actor (see Quickstart above), then choose how to interact: - -- MCP client: Agent-driven access to run code or develop with LLM tooling. -- REST API: Endpoints to run code or shell commands. -- Interactive shell: Browser terminal for manual exploration. - -### MCP client - -Use a Model Context Protocol (MCP) client to interact with this sandbox. See [modelcontextprotocol.io/clients](https://modelcontextprotocol.io/clients). - -**Connect with Claude Code:** - -```bash -claude mcp add --transport http sandbox https://UNIQUE-ID.runs.apify.net/mcp -``` - -Replace `UNIQUE-ID` with the run ID from your Actor execution (URL is also in the landing page and logs). Then prompt your agent; it will use the sandbox tools automatically over MCP. - -### REST API - -Available endpoints (all URLs come from the run logs/landing page): - -#### Core endpoints - -- `POST /mcp` - - Body: JSON-RPC over HTTP per MCP client - - Returns: JSON-RPC response - -- `POST /exec` - - Execute shell commands OR code snippets (JavaScript, TypeScript, Python) - - Body: `{ command: string; language?: string; cwd?: string; timeoutSecs?: number }` - - Language options: `"js"`, `"javascript"`, `"ts"`, `"typescript"`, `"py"`, `"python"`, `"bash"`, `"sh"` (omit for shell) - - Returns (200 on success, 500 on error): `{ stdout: string; stderr: string; exitCode: number; language: string }` - - The `language` field in response is always present: `"shell"` for shell commands, `"js"`/`"ts"`/`"py"` for code - -- `GET /health` - - Health check endpoint - - Returns (200/503): `{ status: 'healthy' | 'initializing' | 'unhealthy'; message?: string }` - -- `GET /shell/` - - Interactive browser terminal - - Returns: Interactive terminal powered by ttyd - -- `GET /llms.txt` - - Markdown documentation for LLMs (same usage info as landing page) - - Returns (200): Plain text Markdown with all endpoint documentation - -#### Dynamic proxy endpoints - -Expose local services running inside the container to external URL paths. This allows you to start a web server (e.g., on port 3000) inside the sandbox and access it from outside via a mapped path. - -- `GET /proxy-config` - - List current proxy mappings - - Returns (200): `{ mappings: [{ path: string, target: string }] }` - -- `PUT /proxy-config` - - Replace all proxy mappings - - Body: `{ mappings: [{ path: "/myapp", target: "http://127.0.0.1:3000/myapp" }] }` - - Returns (200): `{ success: true, mappings: [...] }` - -- `POST /proxy-config` - - Add a single proxy mapping - - Body: `{ path: "/myapp", target: "http://127.0.0.1:3000/myapp" }` - - Returns (200): `{ success: true, mappings: [...] }` - -- `DELETE /proxy-config/{path}` - - Remove a proxy mapping by path - - Returns (200): `{ success: true, removed: string, mappings: [...] }` - -Once configured, all HTTP requests and WebSocket connections to the mapped path (e.g., `/myapp/*`) are transparently proxied to the local service. Mappings can also be configured via Actor input or by writing to `/sandbox/.proxy-mappings.json`. - -**Health status:** - -- `status: "initializing"` (503) โ€“ dependencies/setup still running -- `status: "unhealthy"` (503) โ€“ init script failed; check logs -- `status: "healthy"` (200) โ€“ ready for requests - -#### RESTful filesystem endpoints - -Direct filesystem access using standard HTTP methods. All paths are relative to `/sandbox`. +This Actor launches a web server on the Actor container URL that provides interface to the sandbox. -- `GET /fs/{path}` - - **Read file**: Returns raw file bytes with appropriate `Content-Type` header - - **List directory**: Returns JSON with directory contents (files and subdirectories with sizes) - - Query params: - - `?download=1`: Download file as attachment (or directory as ZIP) - - Returns (200): File content or directory JSON, (404): Path not found +## Use cases -- `PUT /fs/{path}` - - **Write/replace file**: Create or replace file with request body content - - Accepts raw bytes or text in request body - - Automatically creates parent directories if they don't exist - - Returns (200): `{ success: true, path: string, size: number }` +- ๐Ÿ”’ **Run untrusted or AI-generated code safely** in an isolated container with controlled resources. +- ๐Ÿค– **Give AI agents a managed workspace** to write, run, and test code โ€” with state that survives container migrations. +- ๐Ÿ”Œ **Drop in over MCP** so any MCP client gains code-execution and filesystem tools, no glue code. +- ๐Ÿ’ป **Pair with coding agents** (Claude Code, Codex CLI, OpenCode) right in the browser shell. +- ๐ŸŒ **Expose internal services** (dev servers, dashboards, TUIs) at a public URL with bridges. +- ๐ŸŽญ **Orchestrate Apify Actors** using the limited-permission `APIFY_TOKEN` available inside the sandbox to run other [limited-permission Actors](https://docs.apify.com/platform/actors/development/permissions) and build data pipelines. -- `POST /fs/{path}?mkdir=1` - - **Create directory**: Create directory at specified path (recursive by default) - - Returns (201): `{ success: true, path: string, type: "directory" }` -- `POST /fs/{path}?append=1` - - **Append to file**: Append request body to existing file (creates file if it doesn't exist) - - Returns (200): `{ success: true, path: string, size: number }` +## Quickstart -- `DELETE /fs/{path}` - - **Delete file or directory** - - Query params: - - `?recursive=1`: Enable recursive deletion for non-empty directories - - Returns (200): `{ success: true, path: string, deleted: true }`, (409): Directory not empty +1. Run the Actor on the [Apify platform](https://console.apify.com/) (Console or API). +2. Open the sandbox **landing page** (the container URL shown in the Actor output) for live links and connection details. +3. Connect with an MCP client, call the REST API, or open the shell. -- `HEAD /fs/{path}` - - **Get metadata**: Returns file/directory metadata in response headers - - Headers: `Content-Type`, `Content-Length`, `X-File-Type`, `Last-Modified`, `X-Path` - - Returns (200): Headers only, (404): Path not found +Examples below use `https://UNIQUE-ID.runs.apify.net` as the container URL โ€” replace it with your run's URL. -**Path Resolution**: All `/fs/*` paths are resolved relative to `/sandbox`: -- `/fs/app/main.py` โ†’ `/sandbox/app/main.py` -- `/fs/tmp/test.txt` โ†’ `/sandbox/tmp/test.txt` +## ๐Ÿ–ฅ๏ธ Interactive shell โ€” `/shell` -**Security**: Paths are validated to prevent escaping the `/sandbox` directory. Symlinks are followed but validated to stay within `/sandbox`. +Browser terminal (powered by ttyd) for hands-on work inside the sandbox. -**Filesystem examples (curl):** +- `https://UNIQUE-ID.runs.apify.net/shell` โ€” plain Bash shell. +- `โ€ฆ/shell?launch=claude` โ€” launch **Claude Code**. +- `โ€ฆ/shell?launch=codex` โ€” launch **Codex CLI**. +- `โ€ฆ/shell?launch=opencode` โ€” launch **OpenCode**. +- `โ€ฆ/shell?launch=` โ€” run any command, then drop into a shell. -```bash -# Read a file -curl https://UNIQUE-ID.runs.apify.net/fs/app/config.json +The coding agents are installed on first use and start pre-configured against the [Apify OpenRouter proxy](https://apify.com/apify/openrouter), +billed to your Apify account. -# List directory contents -curl https://UNIQUE-ID.runs.apify.net/fs/app -# Download directory as ZIP -curl https://UNIQUE-ID.runs.apify.net/fs/app?download=1 -o app.zip +## ๐Ÿค– AI agent instructions -# Upload a file -curl -X PUT https://UNIQUE-ID.runs.apify.net/fs/app/config.json \ - -H "Content-Type: application/json" \ - -d '{"key": "value"}' +The sandbox landing page is also available as Markdown as the `/llms.txt` file: -# Create a directory -curl -X POST https://UNIQUE-ID.runs.apify.net/fs/app/data?mkdir=1 +``` +https://UNIQUE-ID.runs.apify.net/llms.txt +``` -# Append to a log file -curl -X POST https://UNIQUE-ID.runs.apify.net/fs/app/log.txt?append=1 \ - -H "Content-Type: text/plain" \ - -d "New log entry" -# Delete a file -curl -X DELETE https://UNIQUE-ID.runs.apify.net/fs/app/temp.txt +## ๐Ÿ“ก Connect with MCP โ€” `/mcp` -# Delete directory recursively -curl -X DELETE https://UNIQUE-ID.runs.apify.net/fs/app/temp?recursive=1 +Streamable-HTTP MCP endpoint, no authentication required: -# Get file metadata -curl -I https://UNIQUE-ID.runs.apify.net/fs/app/data.json ``` - -**Upload/download files (TypeScript):** - -```ts -const baseUrl = 'https://UNIQUE-ID.runs.apify.net'; - -// Upload a file -const uploadResponse = await fetch(`${baseUrl}/fs/app/document.pdf`, { - method: 'PUT', - headers: { 'Content-Type': 'application/pdf' }, - body: pdfBuffer, // File buffer or Blob -}); - -// Download a file -const downloadResponse = await fetch(`${baseUrl}/fs/app/document.pdf`); -const fileBlob = await downloadResponse.blob(); - -// Download directory as ZIP -const zipResponse = await fetch(`${baseUrl}/fs/app?download=1`); -const zipBlob = await zipResponse.blob(); - -// List directory -const listResponse = await fetch(`${baseUrl}/fs/app`); -const { entries } = await listResponse.json(); -console.log(entries); // [{ name, type, size }, ...] - -// Create project structure -await fetch(`${baseUrl}/fs/project/src?mkdir=1`, { method: 'POST' }); -await fetch(`${baseUrl}/fs/project/tests?mkdir=1`, { method: 'POST' }); -await fetch(`${baseUrl}/fs/project/README.md`, { - method: 'PUT', - body: '# My Project', -}); +https://UNIQUE-ID.runs.apify.net/mcp ``` -**Upload/download files (Python):** +Add it to an MCP client: -```python -import requests - -base_url = "https://UNIQUE-ID.runs.apify.net" +```bash +claude mcp add --transport http sandbox https://UNIQUE-ID.runs.apify.net/mcp +codex mcp add sandbox --url https://UNIQUE-ID.runs.apify.net/mcp +mcpc connect https://UNIQUE-ID.runs.apify.net/mcp @sandbox +``` -# Upload a file -with open('document.pdf', 'rb') as f: - resp = requests.put(f"{base_url}/fs/app/document.pdf", - data=f, - headers={'Content-Type': 'application/pdf'}) - resp.raise_for_status() +Tools exposed: `execute` (shell / JS / TS / Python), `read-file`, `write-file`, `list-files`. -# Download a file -resp = requests.get(f"{base_url}/fs/app/document.pdf") -with open('downloaded.pdf', 'wb') as f: - f.write(resp.content) +## โšก Code execution API โ€” `/exec` -# Download directory as ZIP -resp = requests.get(f"{base_url}/fs/app?download=1") -with open('app.zip', 'wb') as f: - f.write(resp.content) +`POST /exec` runs a shell command or a code snippet. -# List directory -resp = requests.get(f"{base_url}/fs/app") -data = resp.json() -for entry in data['entries']: - print(f"{entry['name']} ({entry['type']}) - {entry.get('size', 'N/A')} bytes") +- Body: `{ command: string; language?: string; cwd?: string; timeoutSecs?: number }` +- `language`: `bash`/`sh` (or omit) for shell; `js`/`javascript`, `ts`/`typescript`, `py`/`python` for code. +- Returns `{ stdout, stderr, exitCode, language }` โ€” `200` on success, `500` on a non-zero exit or error. -# Create project structure -requests.post(f"{base_url}/fs/project/src?mkdir=1") -requests.post(f"{base_url}/fs/project/tests?mkdir=1") -requests.put(f"{base_url}/fs/project/README.md", data=b"# My Project") +```bash +curl -X POST https://UNIQUE-ID.runs.apify.net/exec \ + -H "Content-Type: application/json" \ + -d '{"command": "print(\"hi\")", "language": "py", "timeoutSecs": 10}' ``` -**Code execution examples (TypeScript/Node):** - -```ts -const baseUrl = 'https://UNIQUE-ID.runs.apify.net'; - -// Execute Python code -const codeRes = await fetch(`${baseUrl}/exec`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - command: 'print("hello from python")', - language: 'py', - timeoutSecs: 10, - }), -}); -console.log(await codeRes.json()); - -// Execute shell command -const shellRes = await fetch(`${baseUrl}/exec`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - command: 'ls -la', - cwd: '/sandbox', - timeoutSecs: 5, - }), -}); -console.log(await shellRes.json()); -``` +Default working directories: shell โ†’ `/sandbox`, JS/TS โ†’ `/sandbox/js-ts`, Python โ†’ `/sandbox/py`. Override with `cwd` (must stay within `/sandbox`). -**Code execution examples (Python):** -```python -import requests +## ๐Ÿ“ Filesystem API โ€” `/fs` -base_url = "https://UNIQUE-ID.runs.apify.net" +Direct file operations over HTTP. All paths are relative to `/sandbox` and validated to stay inside it. -# Execute Python code -payload = {"command": "print('hello from python')", "language": "py", "timeoutSecs": 10} -resp = requests.post(f"{base_url}/exec", json=payload, timeout=15) -resp.raise_for_status() -print(resp.json()) +- `GET /fs/{path}` โ€” read a file (raw bytes) or list a directory (JSON `{ path, entries }`). Add `?download=1` to get a file as an attachment or a directory as a ZIP. +- `PUT /fs/{path}` โ€” write/replace a file (creates parent dirs; up to 500 MB). +- `POST /fs/{path}?mkdir=1` โ€” create a directory; `?append=1` โ€” append to a file. +- `DELETE /fs/{path}` โ€” delete; add `?recursive=1` for non-empty directories. +- `HEAD /fs/{path}` โ€” return metadata in the response headers. -# Execute shell command -payload = {"command": "ls -la", "cwd": "/sandbox", "timeoutSecs": 5} -resp = requests.post(f"{base_url}/exec", json=payload, timeout=15) -resp.raise_for_status() -print(resp.json()) +```bash +curl https://UNIQUE-ID.runs.apify.net/fs/app/log.txt # read +curl -X PUT https://UNIQUE-ID.runs.apify.net/fs/config.json -d '{"key":"value"}' # write +curl -X POST "https://UNIQUE-ID.runs.apify.net/fs/project/src?mkdir=1" # mkdir +curl -X DELETE "https://UNIQUE-ID.runs.apify.net/fs/temp?recursive=1" # delete ``` -### Interactive shell terminal +Prefer a UI? Browse the filesystem at `/browse`. -Open the interactive shell terminal URL from the run logs (also linked on the landing page) to work directly in the browser. -### Dynamic reverse proxy +## ๐Ÿ”€ Bridges โ€” `/bridges` -Expose local services running inside the container to external URL paths. Start a web server inside the sandbox and access it from outside the container. +Expose a web server you start **inside** the sandbox at a public URL path on the container, reachable over HTTP and WebSocket. Each bridge forwards `โ€ฆ/{path}` โ†’ `http://127.0.0.1:{port}/โ€ฆ`. -**Example workflow:** +- `GET /bridges` โ€” list current bridges. +- `POST /bridges` โ€” add one: `{ "path": "/myapp", "target": "http://127.0.0.1:3000/myapp" }`. +- `PUT /bridges` โ€” replace all: `{ "bridges": [ โ€ฆ ] }`. +- `DELETE /bridges/{path}` โ€” remove one. ```bash -# 1. Start a local web server inside the sandbox -npx http-server /sandbox/myapp -p 8080 - -# 2. Add a proxy mapping (via REST API) -curl -X POST https://UNIQUE-ID.runs.apify.net/proxy-config \ +# Start a server inside the sandbox, then expose it: +curl -X POST https://UNIQUE-ID.runs.apify.net/bridges \ -H "Content-Type: application/json" \ -d '{"path": "/myapp", "target": "http://127.0.0.1:8080"}' - -# 3. Access the app from outside -# https://UNIQUE-ID.runs.apify.net/myapp/ +# Now reachable at https://UNIQUE-ID.runs.apify.net/myapp/ ``` -**Via Actor input:** +Bridges can also be set via the `bridges` input or by writing `/sandbox/.bridges.json` (changes are picked up live). Longest-path matching and `Location`-header rewriting are automatic, and bridges persist across restarts. -Provide proxy mappings at startup via the `proxyMappings` input parameter: -```json -[{"path": "/myapp", "target": "http://127.0.0.1:3000/myapp"}] -``` +## Health & status โ€” `/health` -**Via config file:** +`GET /health` reports the service state: -Mappings can also be modified by writing JSON to `/sandbox/.proxy-mappings.json`. Changes are detected automatically via file watching. +- `200 { status: "healthy", idleTimeoutSecs, remainingSecs? }` +- `503 { status: "initializing" }` โ€” dependencies / setup script still running. +- `503 { status: "unhealthy", message }` โ€” setup failed; check the run log. -**Features:** -- Supports both HTTP and WebSocket connections -- Longest-path matching for overlapping routes -- Automatic redirect rewriting (Location headers) -- Live reload on config file changes +`remainingSecs` counts down to idle shutdown and is present only while an idle timeout is active. ## Configuration -- **Memory & timeout:** Configure run options to set memory allocation and execution timeout -- **Idle timeout:** The container automatically shuts down after a period of inactivity (default: 10 minutes). Activity includes HTTP requests and shell interaction. You can adjust this via the `idleTimeoutSeconds` input. -- **Recommendation:** For cost efficiency, set the standard Actor **Execution Timeout to 0 (infinite)** in the Apify Console. The internal idle logic will then manage the lifecycle based on your usage. -- **Request timeout:** All requests to the Actor have a 5-minute timeout ceiling. All operations (code execution, commands, file operations) must complete within this time limit. The `timeout` parameter in requests cannot exceed this 5-minute window -- **Check logs:** Open the Actor run log console to view connection details and operation output +All inputs are optional. Set them in the Actor input form or via the API. -## Sandbox environment structure - -The sandbox runs on a **Debian Trixie** container image with **Node.js 24**, **Python 3**, and essential development tools pre-installed. - -The sandbox provides isolated execution environments for different code languages: - -### Code execution directories - -- **Python**: `/sandbox/py` - - Python code executes in this isolated directory - - Has access to Python virtual environment at `/sandbox/py/venv` - - All pip packages installed in the venv - -- **JavaScript/TypeScript**: `/sandbox/js-ts` - - JS/TS code executes in this isolated directory - - Has access to node_modules at `/sandbox/js-ts/node_modules` - - All npm packages installed in node_modules - -- **General Commands**: `/sandbox` (root) - - Shell commands via `/exec` endpoint run from sandbox root - - Can access all subdirectories - -### Dependency installation - -Specify dependencies to install via Actor input: - -- **Node.js Dependencies**: npm packages for JS/TS code execution in native npm format - - Input as a JSON object: `{"package-name": "version", ...}` - - Example: `{"zod": "^3.0", "axios": "latest", "lodash": "4.17.21"}` -- **Python Requirements**: pip packages for Python code execution in requirements.txt format - - Input as multi-line text: one package per line with optional version specifiers - - Example: - ``` - requests==2.31.0 - pandas>=2.0.0 - numpy - ``` - -Dependencies are installed during Actor startup before any code execution, allowing your code to immediately use them. - -### Customization with init script - -Provide a bash script via the "Initialization Script" input to customize the sandbox: - -- Runs **after** library installation -- Executes in `/sandbox` directory -- Can install system packages, create directories, set permissions, etc. -- Errors are logged but don't prevent Actor from starting -- **Note:** Init scripts have a 5-minute execution timeout - -**Example init scripts:** - -```bash -# Install system package -apt-get update && apt-get install -y curl - -# Create custom directory with permissions -mkdir -p /sandbox/custom-data && chmod 755 /sandbox/custom-data -``` +| Input | Description | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Agent skills** (`agentSkills`) | SKILLS.md packages for the coding agents โ€” `owner/repo` or a repo URL per line, or a JSON array. Defaults to `apify/agent-skills`. See [skills.sh](https://skills.sh/). | +| **Node.js dependencies** (`nodeDependencies`) | npm packages for JS/TS execution. One `package@version` per line (npm-style), or a `package.json`-style JSON object. | +| **Python requirements** (`pythonRequirements`) | pip packages for Python execution, in `requirements.txt` format. | +| **MCP connectors** (`mcpConnectors`) | MCP connectors to pre-load into Claude Code, Codex, and OpenCode, and write to `/sandbox/mcp.json` for `mcpc`. | +| **Setup script** (`initBashScript`) | Bash script run on startup after dependencies install. Output streams to the log (tagged `[init]`) with a progress heartbeat; 5-minute timeout. | +| **Environment variables** (`envVars`) | Secret variables exposed **only to the setup script**, then removed before the shell, MCP server, and code execution start. dotenv or JSON; encrypted at rest. | +| **Idle timeout** (`idleTimeoutSecs`) | Seconds of inactivity before automatic shutdown (default `900`; `0` disables). Activity includes HTTP requests and shell interaction. | +| **Bridges** (`bridges`) | Bridges to create at startup (see above). | -### Skills support (SKILLS.md) +Dependencies install at startup before any code runs. For cost efficiency, set the Actor's **Execution Timeout to 0 (infinite)** and let the idle timeout manage the lifecycle. Note that every request to the Actor has a 5-minute ceiling, so each operation must finish within that window. -Install skill packages that provide specialized instructions for AI coding agents. Skills are SKILLS.md files that enhance agent capabilities. +## Sandbox environment -- Specify skills via the "Skills" input (array of package names) -- Example: `["apify/agent-skills"]` -- Skills are installed globally during Actor startup -- For more info see [skills.sh](https://skills.sh/) +- **Base image:** [Debian Trixie](https://www.debian.org/releases/trixie/) with [Node.js 24](https://nodejs.org/) and [Python 3](https://www.python.org/) (`venv` at `/sandbox/py/venv`). +- **Pre-installed tools:** [git](https://git-scm.com/), [openssh-client](https://www.openssh.com/), [curl](https://curl.se/), [wget](https://www.gnu.org/software/wget/), [jq](https://jqlang.org/), [build-essential](https://packages.debian.org/trixie/build-essential), [`tsx`](https://tsx.is/), [`apify-cli`](https://docs.apify.com/cli/), [`mcpc`](https://github.com/apify/mcpc), and [`ttyd`](https://github.com/tsl0922/ttyd). The [`apify-client`](https://docs.apify.com/api/client/js/) library is preinstalled in the Node environment, and the [Python `apify-client`](https://docs.apify.com/api/client/python/) in the venv. +- **Pre-configured coding agents:** [Claude Code](https://code.claude.com/), [Codex CLI](https://github.com/openai/codex), and [OpenCode](https://opencode.ai/) โ€” installed on first launch and wired to the [Apify OpenRouter proxy](https://apify.com/apify/openrouter) (authenticated with `APIFY_TOKEN`), with confirmation prompts auto-approved (safe inside the sandbox). +- **Working directories:** `/sandbox` (shell), `/sandbox/js-ts` (npm packages in `node_modules`), `/sandbox/py` (Python venv). +- **Persistence:** filesystem changes are backed up to the Actor's key-value store and restored after a container migration, so work survives restarts (dependency directories are excluded and reinstalled). +- **Agent context:** [`AGENTS.md`](https://agents.md/) and `CLAUDE.md` are placed in `/sandbox` to guide the coding agents. ## Learn more -- [Apify Actor documentation](https://docs.apify.com/platform/actors) +- [Apify Actors documentation](https://docs.apify.com/platform/actors) - [Model Context Protocol](https://modelcontextprotocol.io/) - [Apify SDK reference](https://docs.apify.com/sdk) diff --git a/sandbox/artifacts/AGENTS.md b/sandbox/artifacts/AGENTS.md index 2672416..f73cad8 100644 --- a/sandbox/artifacts/AGENTS.md +++ b/sandbox/artifacts/AGENTS.md @@ -1,12 +1,12 @@ -# Agent Instructions for Apify AI Sandbox +# Agent Instructions for Apify AI Code Sandbox -This document contains instructions for AI coding agents working inside the Apify AI Sandbox Actor. +This document contains instructions for AI coding agents working inside the Apify AI Code Sandbox Actor. ## Sharing Files and Data with Users ### ๐Ÿšจ CRITICAL: Always Generate Signed Public URLs -When sharing data with users, **NEVER return just storage IDs or raw API URLs** โ€” they require authentication. +When sharing data with users, **NEVER return just storage IDs or raw API URLs** โ€” they require authentication. **ALWAYS generate signed public URLs** that work without authentication. ### Key-Value Stores (Files & Binary Data) @@ -144,6 +144,39 @@ mcpc @apify tools-list --json Access Apify platform features and thousands of Actors via `mcpc` tool. +### User-provided MCP Connections (`/sandbox/mcp.json`) + +If the user picked **MCP Connectors** in the Actor input (e.g. Slack, Notion, GitHub), the sandbox writes them to `/sandbox/mcp.json` on startup. Each entry is a ready-to-use HTTP MCP proxy, authenticated with the user's credentials server-side: + +> **If you are Claude Code, Codex, or OpenCode**, these connectors are already registered as native MCP tools on startup โ€” just call them directly, no `mcpc connect` needed. The steps below are for the `mcpc` CLI or any other client. + +```bash +cat /sandbox/mcp.json +# { +# "mcpServers": { +# "apify": { # always present (hosted Apify MCP) +# "url": "https://mcp.apify.com", +# "headers": { "Authorization": "Bearer ${APIFY_TOKEN}" } +# }, +# "conn_abc123": { # one per user-picked Connector +# "url": "https://api.apify.com/v2/mcp-proxy/conn_abc123", +# "headers": { "Authorization": "Bearer ${APIFY_TOKEN}" } +# } +# } +# } +``` + +`/sandbox/mcp.json` always contains the hosted Apify MCP server under the `apify` key, plus one entry per user-picked Connector. + +To connect with `mcpc`, read a server entry from the file and pass it to `mcpc connect`: + +```bash +# Connect to the first user-provided Connector (skipping the always-on "apify" entry) as @user1 +URL=$(jq -r '.mcpServers | to_entries | map(select(.key != "apify"))[0].value.url // empty' /sandbox/mcp.json) +mcpc connect "$URL" @user1 --header "Authorization: Bearer $APIFY_TOKEN" +mcpc @user1 tools-list --json +``` + ### ๐Ÿšจ CRITICAL: Connect to Apify MCP Server First **Before using any MCP commands, you MUST create a named connection.** Direct URL connections are deprecated โ€” always use named sessions with `@apify`. @@ -524,7 +557,7 @@ cat /sandbox/py/report.pdf | apify actor set-value report.pdf --content-type app ## Resources -- [Apify CLI Documentation](https://docs.apify.com/cli) -- [Apify MCP CLI Repository](https://github.com/apify/mcp-cli) +- [Apify CLI documentation](https://docs.apify.com/cli) +- [mcpc MCP CLI repository](https://github.com/apify/mcpc) - [Key-Value Store API](https://docs.apify.com/api/v2#/reference/key-value-stores) - [Dataset API](https://docs.apify.com/api/v2#/reference/datasets) diff --git a/sandbox/artifacts/opencode.json b/sandbox/artifacts/opencode.json index b151ecd..b17e841 100644 --- a/sandbox/artifacts/opencode.json +++ b/sandbox/artifacts/opencode.json @@ -1,14 +1,10 @@ { "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": "allow", - "edit": "allow", - "webfetch": "allow" - }, + "permission": "allow", "provider": { "apify-openrouter": { "npm": "@ai-sdk/openai-compatible", - "name": "Apify OpenRouter Proxy", + "name": "apify/openrouter Actor", "options": { "baseURL": "https://openrouter.apify.actor/api/v1", "apiKey": "dummy-api-key", @@ -17,14 +13,14 @@ } }, "models": { - "anthropic/claude-haiku-4.5": { - "name": "Claude 4.5 Haiku" + "~anthropic/claude-haiku-latest": { + "name": "Claude Haiku (latest)" }, - "anthropic/claude-sonnet-4.5": { - "name": "Claude 4.5 Sonnet" + "~anthropic/claude-sonnet-latest": { + "name": "Claude Sonnet (latest)" }, - "anthropic/claude-opus-4.5": { - "name": "Claude 4.5 Opus" + "~anthropic/claude-opus-latest": { + "name": "Claude Opus (latest)" }, "x-ai/grok-code-fast-1": { "name": "Grok Code Fast 1" @@ -38,5 +34,5 @@ } } }, - "model": "apify-openrouter/anthropic/claude-sonnet-4.5" + "model": "apify-openrouter/~anthropic/claude-opus-latest" } diff --git a/sandbox/package-lock.json b/sandbox/package-lock.json index fc95af0..9bcb29e 100644 --- a/sandbox/package-lock.json +++ b/sandbox/package-lock.json @@ -9,31 +9,33 @@ "version": "0.0.1", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.27.1", - "@types/archiver": "^7.0.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/archiver": "^8.0.0", "@types/mime-types": "^3.0.1", - "apify": "^3.7.0", - "archiver": "^7.0.1", - "ejs": "^5.0.1", + "apify": "^3.7.2", + "archiver": "^8.0.0", + "ejs": "^6.0.1", "express": "^5.2.1", "http-proxy": "^1.18.1", "mime-types": "^3.0.2", - "zod": "^4.3.6" + "node-html-markdown": "^2.0.0", + "node-html-parser": "^7.1.0", + "zod": "^4.4.3" }, "devDependencies": { - "@apify/eslint-config": "^1.1.0", - "@apify/tsconfig": "^0.1.1", + "@apify/eslint-config": "^2.0.6", + "@apify/tsconfig": "^0.1.2", "@types/ejs": "^3.1.5", "@types/express": "^5.0.6", "@types/http-proxy": "^1.17.17", - "@types/node": "^25.5.0", - "eslint": "^9.29.0", + "@types/node": "^25.9.2", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", - "globals": "^17.4.0", - "prettier": "^3.8.1", - "tsx": "^4.21.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.0" + "globals": "^17.6.0", + "prettier": "^3.8.3", + "tsx": "^4.22.4", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.0" }, "engines": { "node": ">=20.0.0" @@ -52,24 +54,32 @@ "license": "Apache-2.0" }, "node_modules/@apify/eslint-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@apify/eslint-config/-/eslint-config-1.1.0.tgz", - "integrity": "sha512-3rVBzGcRP13aN8pcYz8qkbJ7ji4oJ/haabaiRiI43ytKBwasMA4rAcH0ZifVKm2qOAOj5Zd0e9YSuiqlTPDoDg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@apify/eslint-config/-/eslint-config-2.0.6.tgz", + "integrity": "sha512-8Ai9bHAB3wtVOupZO0TwrM3ZaEUdh//764DEw0uOxfp71xOApS3NdAYzy/6B22jnpd/cnOXqyz80kXLQiqicAQ==", "dev": true, "license": "ISC", "dependencies": { - "@eslint/compat": "^1.2.6", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-plugin-import": "^2.32.0", + "eslint-import-resolver-node": "^0.3.10", + "eslint-plugin-import-x": "^4.16.2", "eslint-plugin-simple-import-sort": "^12.1.1", "globals": "^15.14.0" }, "peerDependencies": { + "@eslint/js": "^9.19.0", + "@stylistic/eslint-plugin": "^5.0.0", + "@vitest/eslint-plugin": "^1.6.14", "eslint": "^9.19.0", "eslint-plugin-jest": "^28.11.0", "typescript-eslint": "^8.23.0" }, "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@vitest/eslint-plugin": { + "optional": true + }, "eslint-plugin-jest": { "optional": true }, @@ -78,103 +88,6 @@ } } }, - "node_modules/@apify/eslint-config/node_modules/@eslint/compat": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz", - "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^8.40 || 9" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@apify/eslint-config/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@apify/eslint-config/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/@apify/eslint-config/node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", - "dev": true, - "license": "MIT", - "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" - } - }, - "node_modules/@apify/eslint-config/node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, "node_modules/@apify/eslint-config/node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -188,29 +101,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@apify/eslint-config/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@apify/eslint-config/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@apify/input_secrets": { "version": "1.2.19", "resolved": "https://registry.npmjs.org/@apify/input_secrets/-/input_secrets-1.2.19.tgz", @@ -263,9 +153,9 @@ "license": "Apache-2.0" }, "node_modules/@apify/tsconfig": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@apify/tsconfig/-/tsconfig-0.1.1.tgz", - "integrity": "sha512-cS7mwN2UW1UXcluGXRDHH0Vr2VsSLkw2DwLTwoSBkcJSe8fvCr3MPryTSq0uod4MashpMURxJ7CsLKxs82VmOQ==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@apify/tsconfig/-/tsconfig-0.1.2.tgz", + "integrity": "sha512-9dzEI1ZQ5+iM0k0fmPJrpdSSPUolVdeI1nDGFZMjD9UabTmIvjQrzui+1a25uy913AUEBrKTojEPj87pU9/Ekg==", "dev": true, "license": "Apache-2.0" }, @@ -399,10 +289,44 @@ "node": ">=16.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -417,9 +341,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -434,9 +358,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -451,9 +375,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -468,9 +392,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -485,9 +409,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -502,9 +426,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -519,9 +443,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -536,9 +460,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -553,9 +477,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -570,9 +494,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -587,9 +511,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -604,9 +528,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -621,9 +545,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -638,9 +562,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -655,9 +579,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -672,9 +596,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -689,9 +613,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -706,9 +630,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -723,9 +647,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -740,9 +664,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -757,9 +681,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], @@ -774,9 +698,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -791,9 +715,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -808,9 +732,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -825,9 +749,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -1110,23 +1034,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@keyv/serialize": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", @@ -1134,9 +1041,9 @@ "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", - "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -1195,20 +1102,29 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, "license": "MIT", "optional": true, - "engines": { - "node": ">=14" + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "node_modules/@package-json/types": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@package-json/types/-/types-0.0.12.tgz", + "integrity": "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==", "dev": true, "license": "MIT" }, @@ -1277,12 +1193,24 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/archiver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", - "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-8.0.0.tgz", + "integrity": "sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A==", "license": "MIT", "dependencies": { + "@types/node": "*", "@types/readdir-glob": "*" } }, @@ -1376,13 +1304,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mime-types": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz", @@ -1390,12 +1311,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/qs": { @@ -1452,20 +1373,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1475,9 +1396,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1491,17 +1412,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "engines": { @@ -1513,18 +1433,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "engines": { @@ -1535,18 +1455,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1557,9 +1477,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", "dev": true, "license": "MIT", "engines": { @@ -1570,21 +1490,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1595,13 +1515,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", "dev": true, "license": "MIT", "engines": { @@ -1613,21 +1533,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1637,7 +1557,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { @@ -1651,9 +1571,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1664,13 +1584,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -1680,16 +1600,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1700,17 +1620,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1721,6 +1641,319 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vladfrangu/async_event_emitter": { "version": "2.4.7", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", @@ -1762,7 +1995,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1854,22 +2086,11 @@ "node": ">=6" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1882,9 +2103,9 @@ } }, "node_modules/apify": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/apify/-/apify-3.7.0.tgz", - "integrity": "sha512-2WeMeI6BIH2ql5dtnsreKcWxE+OGqsj9LKEe1c0b7kaQpqlEHXN63OqPfDmX1EFjuGo5Z1I3uVYBx3BTQZ9+Ag==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/apify/-/apify-3.7.2.tgz", + "integrity": "sha512-I4R5Qd1X11vk5/1U0CxKSsMhNwER/3MntePEQb9Pbtf942OhQLh5IURrJZufqtpimkqRy44SlQMU6UhPdoFXhA==", "license": "Apache-2.0", "dependencies": { "@apify/consts": "^2.51.0", @@ -1926,51 +2147,23 @@ } }, "node_modules/archiver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", - "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-8.0.0.tgz", + "integrity": "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==", "license": "MIT", "dependencies": { - "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", - "readable-stream": "^4.0.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^6.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", - "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", - "license": "MIT", - "dependencies": { - "glob": "^10.0.0", - "graceful-fs": "^4.2.0", - "is-stream": "^2.0.1", + "is-stream": "^4.0.0", "lazystream": "^1.0.0", - "lodash": "^4.17.15", "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" + "readable-stream": "^4.0.0", + "readdir-glob": "^3.0.0", + "tar-stream": "^3.0.0", + "zip-stream": "^7.0.2" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, "node_modules/argparse": { @@ -1987,72 +2180,8 @@ "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -2281,7 +2410,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2387,15 +2515,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -2522,6 +2650,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2534,6 +2663,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -2548,32 +2678,30 @@ "node": ">= 0.8" } }, + "node_modules/comment-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.7.tgz", + "integrity": "sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/compress-commons": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", - "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-7.0.1.tgz", + "integrity": "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==", "license": "MIT", "dependencies": { "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", + "crc32-stream": "^7.0.1", + "is-stream": "^4.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/compress-commons/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, "node_modules/concat-map": { @@ -2583,13 +2711,6 @@ "dev": true, "license": "MIT" }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -2662,16 +2783,16 @@ } }, "node_modules/crc32-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", - "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-7.0.1.tgz", + "integrity": "sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==", "license": "MIT", "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 14" + "node": ">=18" } }, "node_modules/cross-spawn": { @@ -2869,19 +2990,6 @@ "node": ">= 0.8" } }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -2972,12 +3080,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "license": "MIT" }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2985,9 +3087,9 @@ "license": "MIT" }, "node_modules/ejs": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", - "integrity": "sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-6.0.1.tgz", + "integrity": "sha512-UaaM14yby8U3k02ihS1Bmj5Kz2d7CCQM1scxpgs4Mhkq8F1wR2gl3+Ts4h5Ne4Mnt7M9m4Dw7jsuMr3+xO4vZA==", "license": "Apache-2.0", "bin": { "ejs": "bin/cli.js" @@ -3002,12 +3104,6 @@ "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3030,9 +3126,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -3117,9 +3213,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3175,9 +3271,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3188,32 +3284,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -3250,7 +3346,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3321,16 +3416,41 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -3343,34 +3463,44 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "node_modules/eslint-plugin-import-x": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.2.tgz", + "integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.2.7" + "@package-json/types": "^0.0.12", + "@typescript-eslint/types": "^8.56.0", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.1.2", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" }, "engines": { - "node": ">=4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-import-x" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint-import-resolver-node": "*" }, "peerDependenciesMeta": { - "eslint": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { "optional": true } } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/eslint-plugin-simple-import-sort": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", @@ -3608,7 +3738,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3857,34 +3986,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -4116,9 +4217,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -4128,26 +4229,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4162,9 +4243,9 @@ } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -4412,9 +4493,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4423,6 +4504,15 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/header-generator": { "version": "2.1.78", "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.78.tgz", @@ -4443,7 +4533,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -4729,13 +4818,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -4805,15 +4894,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -5075,21 +5155,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -5283,12 +5348,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/map-stream": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", @@ -5376,31 +5435,28 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5417,6 +5473,67 @@ "node": ">= 0.6" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-html-markdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-html-markdown/-/node-html-markdown-2.0.0.tgz", + "integrity": "sha512-DqUC3GGP7pwSYxS93SwHoP+qCw78xcMP6C6H2DuC8rPD2AweJRjBzQb5SdXpKtDlqAQ7hVotJcfhgU7hU5Gthw==", + "license": "MIT", + "dependencies": { + "node-html-parser": "^6.1.13" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/node-html-markdown/node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-html-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -5487,94 +5604,41 @@ "node": ">= 0.4" } }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/on-finished": { @@ -5706,12 +5770,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5797,22 +5855,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -5832,12 +5874,11 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5875,9 +5916,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -6022,24 +6063,54 @@ } }, "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-3.0.0.tgz", + "integrity": "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==", "license": "Apache-2.0", "dependencies": { - "minimatch": "^5.1.0" + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/yqnn" + } + }, + "node_modules/readdir-glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/reflect.getprototypeof": { @@ -6102,13 +6173,16 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.1", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -6208,15 +6282,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -6517,6 +6591,16 @@ "node": "*" } }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -6584,79 +6668,21 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -6666,16 +6692,16 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.2" }, "engines": { "node": ">= 0.4" @@ -6702,53 +6728,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6831,14 +6810,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -6905,9 +6884,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -6917,32 +6896,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6950,14 +6903,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -7066,18 +7018,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" }, "engines": { "node": ">= 0.4" @@ -7087,12 +7039,11 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7102,17 +7053,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", - "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.0", - "@typescript-eslint/parser": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0" + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7123,7 +7073,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/uint8array-extras": { @@ -7158,9 +7108,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/universalify": { @@ -7181,6 +7131,44 @@ "node": ">= 0.8" } }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", @@ -7337,14 +7325,14 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", @@ -7368,94 +7356,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7497,25 +7397,24 @@ } }, "node_modules/zip-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", - "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz", + "integrity": "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==", "license": "MIT", "dependencies": { - "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.2", + "compress-commons": "^7.0.0", + "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 14" + "node": ">=18" } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/sandbox/package.json b/sandbox/package.json index 2f16e69..28268a8 100644 --- a/sandbox/package.json +++ b/sandbox/package.json @@ -2,36 +2,38 @@ "name": "apify-ai-sandbox", "version": "0.0.1", "type": "module", - "description": "Apify AI Sandbox for running AI coding operations in a containerized environment.", + "description": "Apify AI Code Sandbox for running AI coding operations in a containerized environment.", "engines": { "node": ">=20.0.0" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.27.1", - "@types/archiver": "^7.0.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/archiver": "^8.0.0", "@types/mime-types": "^3.0.1", - "apify": "^3.7.0", - "archiver": "^7.0.1", - "ejs": "^5.0.1", + "apify": "^3.7.2", + "archiver": "^8.0.0", + "ejs": "^6.0.1", "express": "^5.2.1", "http-proxy": "^1.18.1", "mime-types": "^3.0.2", - "zod": "^4.3.6" + "node-html-markdown": "^2.0.0", + "node-html-parser": "^7.1.0", + "zod": "^4.4.3" }, "devDependencies": { - "@apify/eslint-config": "^1.1.0", - "@apify/tsconfig": "^0.1.1", + "@apify/eslint-config": "^2.0.6", + "@apify/tsconfig": "^0.1.2", "@types/ejs": "^3.1.5", "@types/express": "^5.0.6", "@types/http-proxy": "^1.17.17", - "@types/node": "^25.5.0", - "eslint": "^9.29.0", + "@types/node": "^25.9.2", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", - "globals": "^17.4.0", - "prettier": "^3.8.1", - "tsx": "^4.21.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.0" + "globals": "^17.6.0", + "prettier": "^3.8.3", + "tsx": "^4.22.4", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.0" }, "scripts": { "start": "npm run start:dev", @@ -40,7 +42,7 @@ "start:dev": "tsx src/main.ts", "start:standby": "APIFY_META_ORIGIN=\"STANDBY\" npm run start:dev", "build": "tsc -p tsconfig.build.json && npm run copy-templates", - "copy-templates": "mkdir -p dist/templates && cp src/templates/*.ejs src/templates/*.md src/templates/*.ico dist/templates/", + "copy-templates": "mkdir -p dist/templates && cp src/templates/*.ejs src/templates/*.css dist/templates/", "lint": "eslint", "lint:fix": "eslint --fix", "format": "prettier --write .", diff --git a/sandbox/scripts/agent-launchers.sh b/sandbox/scripts/agent-launchers.sh new file mode 100644 index 0000000..09d4c52 --- /dev/null +++ b/sandbox/scripts/agent-launchers.sh @@ -0,0 +1,85 @@ +# shellcheck shell=bash +# +# Lazy installers for the bundled AI coding agents (Claude Code, OpenCode, Codex). +# +# The sandbox image ships each agent's *configuration* (provider/proxy settings, +# auto-approve flags, MCP connectors โ€” see the Dockerfile and mcp-agent-config.ts) +# but no longer bundles the CLIs themselves. Each agent is instead installed on +# first use with its official installer, then exec'd. +# +# They are defined as shell functions (rather than wrapper scripts on PATH) so +# both entry points take the identical install-then-run path: +# - the landing-page launch buttons -> /shell?launch=claude (etc.), and +# - a plain `claude` / `opencode` / `codex` typed at the prompt. +# +# This file defines functions only and has no top-level side effects, so the +# shell rcfile (SANDBOX_BASHRC) can source it cheaply on every shell start. + +# Ensure $1 (a binary name) is runnable, installing it via the command in $3 if +# it is missing. $2 is a human-readable label for messages. Returns non-zero โ€” +# without running anything else โ€” if the agent cannot be made available, so the +# caller can bail out instead of printing a confusing "command not found". +__sandbox_ensure_agent() { + local bin=$1 label=$2 installer=$3 + + # Already installed (from a previous launch, persisted across migrations)? + # `type -P` ignores this very function and looks only at PATH executables. + if type -P "$bin" >/dev/null 2>&1; then + return 0 + fi + + if [ -t 2 ]; then + printf '\033[0;34mInstalling %s (one-time setup on first launch)...\033[0m\n' "$label" >&2 + else + printf 'Installing %s (one-time setup on first launch)...\n' "$label" >&2 + fi + + # `set -o pipefail` so a failed download (curl) fails the install instead of + # piping an empty script to the interpreter, which would exit 0 and look + # like success. + if ! bash -c "set -o pipefail; $installer"; then + printf 'Failed to install %s. Check the network connection and try again.\n' "$label" >&2 + return 1 + fi + + # Drop bash's cached "not found" result, then re-check. The official + # installers place their binaries in directories already on PATH + # (~/.local/bin for Claude Code and Codex, ~/.opencode/bin for OpenCode); + # the loop is a fallback that prepends the install dir to PATH should an + # installer ever change its default location. + hash -r 2>/dev/null || true + if type -P "$bin" >/dev/null 2>&1; then + return 0 + fi + local dir + for dir in "$HOME/.local/bin" "$HOME/.opencode/bin" "$HOME/.codex/bin" "$HOME/bin" /usr/local/bin; do + if [ -x "$dir/$bin" ]; then + export PATH="$dir:$PATH" + hash -r 2>/dev/null || true + return 0 + fi + done + + printf '%s was installed but "%s" was not found on PATH.\n' "$label" "$bin" >&2 + return 1 +} + +# `command ` bypasses these functions and runs the installed binary, so the +# wrappers don't recurse into themselves. +claude() { + __sandbox_ensure_agent claude 'Claude Code' \ + 'curl -fsSL https://claude.ai/install.sh | bash' || return + command claude "$@" +} + +opencode() { + __sandbox_ensure_agent opencode 'OpenCode' \ + 'curl -fsSL https://opencode.ai/install | OPENCODE_INSTALL_DIR=$HOME/.opencode/bin bash' || return + command opencode "$@" +} + +codex() { + __sandbox_ensure_agent codex 'Codex' \ + 'curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh' || return + command codex "$@" +} diff --git a/sandbox/scripts/capture-versions.sh b/sandbox/scripts/capture-versions.sh index 639239e..b9a2592 100644 --- a/sandbox/scripts/capture-versions.sh +++ b/sandbox/scripts/capture-versions.sh @@ -34,29 +34,20 @@ else echo "โš ๏ธ Apify CLI: not installed" fi -# Capture MCP CLI version (optional) +# Capture mcpc version (optional) if mcpc --version > "$VERSION_DIR/mcpc.txt" 2>/dev/null; then - echo "โœ… MCP CLI: $(cat "$VERSION_DIR/mcpc.txt")" + echo "โœ… mcpc: $(cat "$VERSION_DIR/mcpc.txt")" else echo "not installed" > "$VERSION_DIR/mcpc.txt" - echo "โš ๏ธ MCP CLI: not installed" + echo "โš ๏ธ mcpc: not installed" fi -# Capture Claude CLI version (optional) -if claude --version > "$VERSION_DIR/claude.txt" 2>/dev/null; then - echo "โœ… Claude: $(cat "$VERSION_DIR/claude.txt")" -else - echo "not installed" > "$VERSION_DIR/claude.txt" - echo "โš ๏ธ Claude: not installed" -fi - -# Capture OpenCode CLI version (optional) -if opencode --version > "$VERSION_DIR/opencode.txt" 2>/dev/null; then - echo "โœ… OpenCode: $(cat "$VERSION_DIR/opencode.txt")" -else - echo "not installed" > "$VERSION_DIR/opencode.txt" - echo "โš ๏ธ OpenCode: not installed" -fi +# Claude Code, OpenCode, and Codex are NOT captured here: they are installed +# lazily on first use (see agent-launchers.sh), so there is nothing to probe at +# build time. The shell welcome message detects them at runtime instead โ€” it +# falls back to ` --version` when the cached version file is absent โ€” so +# it shows the real version once an agent has been installed, and "not installed" +# before that. echo "" echo "๐ŸŽ‰ Version capture complete! Files stored in $VERSION_DIR" diff --git a/sandbox/src/bridges.ts b/sandbox/src/bridges.ts new file mode 100644 index 0000000..6072dce --- /dev/null +++ b/sandbox/src/bridges.ts @@ -0,0 +1,203 @@ +/** + * Bridges Module + * + * Manages bridge configuration (exposed path โ†’ local service) with file + * watching for live updates. A bridge exposes a local server running inside + * the sandbox at a public URL path on the container. + */ + +import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; + +import { log } from 'apify'; + +import { BRIDGES_PATH } from './consts.js'; +import type { Bridge } from './types.js'; + +// Current bridges (in-memory cache) +let currentBridges: Bridge[] = []; + +// Callbacks to notify when bridges change +const changeListeners: Array<(bridges: Bridge[]) => void> = []; + +/** + * Initialize bridges from input and start file watching + * @param initialBridges - Initial bridges from Actor input + */ +export const initializeBridges = (initialBridges?: Bridge[]): void => { + // First try to load from config file (persisted state) + if (existsSync(BRIDGES_PATH)) { + try { + const fileContent = readFileSync(BRIDGES_PATH, 'utf-8'); + currentBridges = JSON.parse(fileContent); + log.info('Loaded bridges from config file', { + count: currentBridges.length, + bridges: currentBridges, + }); + } catch (error) { + log.warning('Failed to load bridges config file, using input bridges', { + error: (error as Error).message, + }); + currentBridges = initialBridges || []; + } + } else if (initialBridges && initialBridges.length > 0) { + // Use input bridges and save to config file + currentBridges = initialBridges; + saveBridges(currentBridges); + log.info('Initialized bridges from Actor input', { + count: currentBridges.length, + bridges: currentBridges, + }); + } + + // Start watching config file for changes + startBridgesWatcher(); +}; + +/** + * Save bridges to config file + * @param bridges - Bridges to save + */ +export const saveBridges = (bridges: Bridge[]): void => { + try { + // Ensure directory exists + const dir = dirname(BRIDGES_PATH); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + writeFileSync(BRIDGES_PATH, JSON.stringify(bridges, null, 2)); + currentBridges = bridges; + log.info('Saved bridges to config file', { + count: bridges.length, + path: BRIDGES_PATH, + }); + + // Notify listeners + notifyListeners(); + } catch (error) { + log.error('Failed to save bridges', { error: (error as Error).message }); + throw error; + } +}; + +/** + * Get current bridges + */ +export const getBridges = (): Bridge[] => { + return [...currentBridges]; +}; + +/** + * Add a bridge + * @param bridge - Bridge with path and target URL + */ +export const addBridge = (bridge: Bridge): void => { + // Normalize path to ensure it starts with / + const normalizedPath = bridge.path.startsWith('/') ? bridge.path : `/${bridge.path}`; + + // Normalize target URL - add http:// if no protocol + let normalizedTarget = bridge.target.trim(); + if (!normalizedTarget.startsWith('http://') && !normalizedTarget.startsWith('https://')) { + normalizedTarget = `http://${normalizedTarget}`; + } + + const normalizedBridge = { path: normalizedPath, target: normalizedTarget }; + + // Check for duplicate paths + const existingIndex = currentBridges.findIndex((b) => b.path === normalizedPath); + if (existingIndex >= 0) { + // Update existing bridge + currentBridges[existingIndex] = normalizedBridge; + } else { + currentBridges.push(normalizedBridge); + } + + saveBridges(currentBridges); +}; + +/** + * Remove a bridge by path + * @param path - Path to remove + */ +export const removeBridge = (path: string): boolean => { + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + const initialLength = currentBridges.length; + currentBridges = currentBridges.filter((b) => b.path !== normalizedPath); + + if (currentBridges.length < initialLength) { + saveBridges(currentBridges); + return true; + } + return false; +}; + +/** + * Register a callback to be called when bridges change + * @param callback - Function to call with new bridges + */ +export const onBridgesChange = (callback: (bridges: Bridge[]) => void): void => { + changeListeners.push(callback); +}; + +/** + * Notify all listeners of bridge changes + */ +const notifyListeners = (): void => { + for (const listener of changeListeners) { + try { + listener([...currentBridges]); + } catch (error) { + log.error('Error in bridges change listener', { error: (error as Error).message }); + } + } +}; + +/** + * Start watching the config file for external changes + */ +const startBridgesWatcher = (): void => { + // Ensure directory exists before watching + const dir = dirname(BRIDGES_PATH); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + // Debounce timeout + let debounceTimer: NodeJS.Timeout | null = null; + + try { + watch(dir, (eventType, filename) => { + if (filename === '.bridges.json' && eventType === 'change') { + // Debounce rapid changes + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + debounceTimer = setTimeout(() => { + try { + if (existsSync(BRIDGES_PATH)) { + const fileContent = readFileSync(BRIDGES_PATH, 'utf-8'); + const newBridges = JSON.parse(fileContent); + + // Only update if actually changed + if (JSON.stringify(newBridges) !== JSON.stringify(currentBridges)) { + currentBridges = newBridges; + log.info('Bridges config file changed, reloading', { + count: currentBridges.length, + }); + notifyListeners(); + } + } + } catch (error) { + log.error('Error reloading bridges config', { error: (error as Error).message }); + } + }, 100); + } + }); + + log.info('Started watching bridges config file for changes', { path: BRIDGES_PATH }); + } catch (error) { + log.warning('Failed to start bridges config file watcher', { error: (error as Error).message }); + } +}; diff --git a/sandbox/src/consts.ts b/sandbox/src/consts.ts index 73c6460..fbc1424 100644 --- a/sandbox/src/consts.ts +++ b/sandbox/src/consts.ts @@ -32,10 +32,23 @@ export const PYTHON_VENV_DIR = '/sandbox/py/venv'; */ export const PYTHON_BIN_DIR = '/sandbox/py/venv/bin'; +/** + * Default idle timeout in seconds (15 minutes). The container shuts down + * automatically after this much inactivity unless overridden via the + * `idleTimeoutSecs` input. Set to 0 to disable. + */ +export const DEFAULT_IDLE_TIMEOUT_SECS = 900; + /** * Init script execution timeout (5 minutes) */ -export const INIT_SCRIPT_TIMEOUT = 300000; +export const INIT_SCRIPT_TIMEOUT_MS = 300000; + +/** + * How often to log a heartbeat while the init script is running (30 seconds), + * so long, quiet steps (e.g. `npm install`) don't look like a hang. + */ +export const INIT_SCRIPT_HEARTBEAT_INTERVAL_MS = 30000; /** * Migration persistence constants @@ -75,6 +88,6 @@ export const MIGRATION_EXCLUDED_PATHS = [ ]; /** - * Proxy mappings configuration file path + * Bridges configuration file path */ -export const PROXY_CONFIG_PATH = '/sandbox/.proxy-mappings.json'; +export const BRIDGES_PATH = '/sandbox/.bridges.json'; diff --git a/sandbox/src/env-vars.ts b/sandbox/src/env-vars.ts index e36679c..bccb5b6 100644 --- a/sandbox/src/env-vars.ts +++ b/sandbox/src/env-vars.ts @@ -1,24 +1,20 @@ import { log } from 'apify'; +import { isFlatJsonObject, safeParseJson } from './safe-json.js'; + const VALID_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/; const parseJsonObject = (raw: string): Record => { - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch (error) { - const err = error as Error; - log.warning('envVars: failed to parse JSON input, ignoring', { error: err.message }); - return {}; - } - - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - log.warning('envVars: JSON must be a flat object of string keys and string values'); - return {}; - } + const parsed = safeParseJson( + raw, + 'envVars', + isFlatJsonObject, + 'JSON must be a flat object of string keys and string values', + ); + if (!parsed) return {}; const out: Record = {}; - for (const [key, value] of Object.entries(parsed as Record)) { + for (const [key, value] of Object.entries(parsed)) { if (!VALID_KEY.test(key)) { log.warning('envVars: skipping invalid key', { key }); continue; diff --git a/sandbox/src/environment.ts b/sandbox/src/environment.ts index 4d2216b..beb6269 100644 --- a/sandbox/src/environment.ts +++ b/sandbox/src/environment.ts @@ -1,12 +1,19 @@ // Environment setup for code execution (Node.js and Python) -import { exec } from 'node:child_process'; +import { exec, spawn } from 'node:child_process'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import { promisify } from 'node:util'; import { log } from 'apify'; -import { INIT_SCRIPT_TIMEOUT, JS_TS_CODE_DIR, PYTHON_CODE_DIR, SANDBOX_DIR } from './consts.js'; +import { + INIT_SCRIPT_HEARTBEAT_INTERVAL_MS, + INIT_SCRIPT_TIMEOUT_MS, + JS_TS_CODE_DIR, + PYTHON_CODE_DIR, + SANDBOX_DIR, +} from './consts.js'; +import { setStatusMessage } from './status.js'; const execAsync = promisify(exec); @@ -163,6 +170,7 @@ export const installNodeLibraries = async ( const packageSpecs = Object.entries(dependencies).map(([pkg, version]) => `${pkg}@${version}`); log.info('Installing Node.js dependencies', { count: packageSpecs.length, packages: packageSpecs }); + await setStatusMessage('Installing Node.js dependencies'); const installed: string[] = []; const failed: { library: string; error: string }[] = []; @@ -226,6 +234,7 @@ export const installPythonLibraries = async ( } log.info('Installing Python requirements', { count: requirements.length, requirements }); + await setStatusMessage('Installing Python dependencies'); const installed: string[] = []; const failed: { library: string; error: string }[] = []; @@ -310,7 +319,7 @@ export const installSkills = async ( export const setupExecutionEnvironment = async (input: { skills?: string[]; nodeDependencies?: Record; - pythonRequirementsTxt?: string; + pythonRequirements?: string; }): Promise<{ success: boolean; skillsSetup: { success: boolean; installed: string[]; failed: { skill: string; error: string }[] }; @@ -349,7 +358,7 @@ export const setupExecutionEnvironment = async (input: { const [skillsSetup, nodeSetup, pythonSetup] = await Promise.all([ installSkills(input.skills), installNodeLibraries(input.nodeDependencies), - installPythonLibraries(input.pythonRequirementsTxt), + installPythonLibraries(input.pythonRequirements), ]); const success = errors.length === 0 && skillsSetup.success && nodeSetup.success && pythonSetup.success; @@ -425,6 +434,102 @@ export const getExecutionEnvironment = (): NodeJS.ProcessEnv => { return env; }; +/** + * Buffers a text stream and forwards it to the log one complete line at a time, + * each tagged with the given prefix. Partial lines are held until the newline + * arrives; call flush() at the end to emit any trailing text. + */ +const createLineStreamer = (prefix: string) => { + let buffer = ''; + return { + push(text: string): void { + buffer += text; + const lines = buffer.split('\n'); + buffer = lines.pop() ?? ''; + for (const line of lines) { + log.info(`${prefix} ${line}`); + } + }, + flush(): void { + if (buffer.length > 0) { + log.info(`${prefix} ${buffer}`); + buffer = ''; + } + }, + }; +}; + +/** + * Run a bash script, streaming its stdout/stderr to the log line-by-line as it + * runs so progress is visible and failures are easy to pinpoint (instead of one + * silent gap followed by a dump at the end). A heartbeat is logged periodically + * so long, quiet steps (e.g. `npm install`) don't look like a hang. The full + * output is still collected and returned for programmatic callers. + */ +const runScriptStreaming = async ( + scriptPath: string, +): Promise<{ stdout: string; stderr: string; exitCode: number | null; timedOut: boolean }> => { + return new Promise((resolve) => { + const child = spawn('bash', [scriptPath], { + cwd: SANDBOX_DIR, + env: getExecutionEnvironment(), + }); + + let stdout = ''; + let stderr = ''; + let timedOut = false; + let settled = false; + + // stderr is streamed at info level too: tools like npm/apt/git write + // normal progress there, so the exit code (not the stream) decides + // success. Output is tagged so it's distinguishable from harness logs. + const stdoutStreamer = createLineStreamer('[init]'); + const stderrStreamer = createLineStreamer('[init]'); + + child.stdout?.on('data', (chunk: Buffer) => { + const text = chunk.toString(); + stdout += text; + stdoutStreamer.push(text); + }); + child.stderr?.on('data', (chunk: Buffer) => { + const text = chunk.toString(); + stderr += text; + stderrStreamer.push(text); + }); + + const startedAt = Date.now(); + const heartbeat = setInterval(() => { + log.info('Init script still running...', { + elapsedSeconds: Math.round((Date.now() - startedAt) / 1000), + }); + }, INIT_SCRIPT_HEARTBEAT_INTERVAL_MS); + + // spawn() has no built-in rejection on timeout (unlike the old exec + // call), so enforce it here and kill the process if it overruns. + const timer = setTimeout(() => { + timedOut = true; + child.kill('SIGKILL'); + }, INIT_SCRIPT_TIMEOUT_MS); + + const finish = (exitCode: number | null): void => { + if (settled) return; + settled = true; + clearInterval(heartbeat); + clearTimeout(timer); + stdoutStreamer.flush(); + stderrStreamer.flush(); + resolve({ stdout, stderr, exitCode, timedOut }); + }; + + // 'error' fires when bash itself can't be spawned (no 'close' follows). + child.on('error', (err) => { + stderr += err.message; + finish(1); + }); + child.on('close', (code) => finish(code)); + }); +}; + /** * Execute initialization bash script * Runs custom bash script in /sandbox directory to setup environment @@ -478,49 +583,48 @@ export const executeInitScript = async ( log.debug('Init script written to temp file', { path: tempFile }); - // Execute script - const execOptions: { cwd?: string; timeout?: number; env?: NodeJS.ProcessEnv } = { - cwd: SANDBOX_DIR, - timeout: INIT_SCRIPT_TIMEOUT, - env: getExecutionEnvironment(), - }; + // Execute the script, streaming output live so progress is visible and + // failures are easy to locate (rather than dumping everything at the end). + const startedAt = Date.now(); + const { stdout, stderr, exitCode, timedOut } = await runScriptStreaming(tempFile); + const elapsedSeconds = ((Date.now() - startedAt) / 1000).toFixed(1); - const { stdout, stderr } = await execAsync(`bash ${tempFile}`, execOptions); + if (timedOut) { + log.error('Init script timed out', { + timeoutSeconds: INIT_SCRIPT_TIMEOUT_MS / 1000, + elapsedSeconds, + }); + return { + success: false, + stdout, + stderr: stderr || `Init script timed out after ${INIT_SCRIPT_TIMEOUT_MS / 1000}s`, + exitCode: exitCode ?? 1, + }; + } - log.info('Init script execution completed'); - log.info('-----------------------------------------'); - log.info(stdout || '(no output)'); - if (stderr) { - log.warning(stderr); + if (exitCode === 0) { + log.info('Init script execution completed', { elapsedSeconds }); + return { success: true, stdout, stderr, exitCode: 0 }; } - log.info('-----------------------------------------'); + // Output was already streamed live above, so just summarise the failure. + log.error('Init script execution failed', { exitCode, elapsedSeconds }); return { - success: true, + success: false, stdout, - stderr, - exitCode: 0, + stderr: stderr || 'Init script execution failed', + exitCode: exitCode ?? 1, }; } catch (error) { - const err = error as { message: string; stdout?: string; stderr?: string; code?: number }; - log.error('Init script execution failed', { exitCode: err.code || 1 }); - log.error('-----------------------------------------'); - if (err.stdout) { - log.error(err.stdout); - } - if (err.stderr) { - log.error(err.stderr); - } - if (!err.stdout && !err.stderr) { - log.error(err.message); - } - log.error('-----------------------------------------'); - + // Reaches here only if setup (e.g. writing the temp file) fails; the + // script run itself reports failures via its exit code above. + const err = error as Error; + log.error('Failed to run init script', { error: err.message }); return { success: false, - stdout: err.stdout || '', - stderr: err.stderr || err.message || 'Init script execution failed', - exitCode: err.code || 1, + stdout: '', + stderr: err.message || 'Failed to run init script', + exitCode: 1, }; } finally { // Clean up temporary files diff --git a/sandbox/src/main.ts b/sandbox/src/main.ts index e87be03..2dd86dd 100644 --- a/sandbox/src/main.ts +++ b/sandbox/src/main.ts @@ -1,9 +1,7 @@ import { spawn } from 'node:child_process'; import { chmodSync, mkdirSync, writeFileSync } from 'node:fs'; import http, { createServer } from 'node:http'; -import { dirname, join } from 'node:path'; import type { Duplex } from 'node:stream'; -import { fileURLToPath } from 'node:url'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { Actor, log } from 'apify'; @@ -11,10 +9,15 @@ import type { Request, Response } from 'express'; import express from 'express'; import httpProxy from 'http-proxy'; -import { SANDBOX_DIR } from './consts.js'; +import { DEFAULT_IDLE_TIMEOUT_SECS, SANDBOX_DIR } from './consts.js'; import { parseEnvVars } from './env-vars.js'; import { executeInitScript, setupExecutionEnvironment, setUserEnvVars } from './environment.js'; import { createMcpServer } from './mcp.js'; +import { configureAgentMcpServers } from './mcp-agent-config.js'; +import { writeMcpConfig } from './mcp-connections.js'; +import { translateLaunchParam } from './shell-launch.js'; +import { parseNodeDependencies } from './node-deps.js'; +import { setStatusMessage } from './status.js'; import { appendFile, createDirectory, @@ -29,21 +32,33 @@ import { } from './operations.js'; import { initializePersistence, restoreMigrationState, saveMigrationState } from './persistence.js'; import { - initializeProxyConfig, - getProxyMappings, - saveProxyConfig, - addProxyMapping, - removeProxyMapping, - onMappingsChange, -} from './proxy-config.js'; + addBridge, + getBridges, + initializeBridges, + onBridgesChange, + removeBridge, + saveBridges, +} from './bridges.js'; +import { parseSkills } from './skills.js'; +import { getBrowsePageHTML } from './templates/browse.js'; import { getLandingPageHTML, getLLMsMarkdown } from './templates/landing.js'; -import { SANDBOX_BASHRC, WELCOME_SCRIPT } from './templates/shell.js'; -import type { ActorInput, ProxyMapping } from './types.js'; +import { injectTerminalReconnectScript, SANDBOX_BASHRC, WELCOME_SCRIPT } from './templates/shell.js'; +import { + appendTtydOutput, + buildShellUnavailableMessage, + isTtydStartupCrash, + nextTtydRestartDelayMs, + TTYD_RESTART_MIN_MS, +} from './ttyd.js'; +import type { ActorInput, Bridge } from './types.js'; // Track initialization state let initializationComplete = false; let initializationError: string | null = null; let lastActivityAt = Date.now(); +// Seconds of inactivity before auto-shutdown (0 = disabled). Set from input at +// startup; kept module-level so the landing page and /health can report it. +let idleTimeoutSecs = DEFAULT_IDLE_TIMEOUT_SECS; // Check if running in local mode const isLocalMode = process.env.MODE === 'local'; @@ -68,14 +83,28 @@ const input = await Actor.getInput(); const userEnvVars = parseEnvVars(input?.envVars); setUserEnvVars(userEnvVars); +const nodeDependencies = parseNodeDependencies(input?.nodeDependencies); +const skills = parseSkills(input?.agentSkills); + log.info('Actor input retrieved', { mode: isLocalMode ? 'local' : 'production', - hasNodeDependencies: !!input?.nodeDependencies && Object.keys(input.nodeDependencies).length > 0, - hasPythonRequirements: !!input?.pythonRequirementsTxt?.trim().length, - hasInitScript: !!input?.initShellScript?.trim().length, + hasSkills: skills.length > 0, + hasNodeDependencies: Object.keys(nodeDependencies).length > 0, + hasPythonRequirements: !!input?.pythonRequirements?.trim().length, + hasInitScript: !!input?.initBashScript?.trim().length, envVarKeys: Object.keys(userEnvVars), + mcpConnectorCount: input?.mcpConnectors?.length ?? 0, }); +// Write /sandbox/mcp.json with the configured MCP Connector proxies so +// tools like `mcpc connect` find them as soon as the shell opens, then load the +// same servers into Claude Code, Codex, and OpenCode so they appear as tools the +// moment an agent launches (no `claude mcp add` / `mcpc connect` step needed). +if (!isLocalMode) { + const mcpConfig = writeMcpConfig(input?.mcpConnectors); + configureAgentMcpServers(mcpConfig); +} + // Check for migration state and restore if available let restoredFromMigration = false; if (!isLocalMode) { @@ -101,9 +130,9 @@ if (restoredFromMigration) { } else { log.info('Setting up execution environment...'); setupResult = await setupExecutionEnvironment({ - skills: input?.skills, - nodeDependencies: input?.nodeDependencies, - pythonRequirementsTxt: input?.pythonRequirementsTxt, + skills, + nodeDependencies, + pythonRequirements: input?.pythonRequirements, }); } @@ -121,15 +150,13 @@ if (!setupResult.success) { } // Execute init script if provided and not empty -if (input?.initShellScript && input.initShellScript.trim().length > 0) { +if (input?.initBashScript && input.initBashScript.trim().length > 0) { log.info('Executing init script...'); - const initResult = await executeInitScript(input.initShellScript); + await setStatusMessage('Running setup script'); + const initResult = await executeInitScript(input.initBashScript); if (initResult.exitCode !== 0) { - log.error('Init script failed', { - exitCode: initResult.exitCode, - stderr: initResult.stderr, - stdout: initResult.stdout, - }); + // The output and failure summary were already streamed by executeInitScript; + // record the reason so the /health endpoint can report it. initializationError = `Init script failed with exit code ${initResult.exitCode}`; } } else { @@ -187,9 +214,10 @@ if (!isLocalMode) { initializationComplete = true; lastActivityAt = Date.now(); log.info('Actor startup complete - ready for requests'); +await setStatusMessage('Sandbox is live'); -// Initialize proxy configuration -initializeProxyConfig(input?.proxyMappings); +// Initialize bridges +initializeBridges(input?.bridges); // Create Express app const app = express(); @@ -208,6 +236,9 @@ app.use((req, _res, next) => { }); } + // Any non-health request โ€” including doc fetches like /llms.txt โ€” counts as + // activity and pushes back the idle-shutdown timer. Only /health and the + // readiness probe are excluded, since they fire automatically. if (!isHealth && !isProbe) { lastActivityAt = Date.now(); } @@ -630,72 +661,75 @@ app.get('/', (_req: Request, res: Response) => { getLandingPageHTML({ serverUrl, isLocalMode, + idleTimeoutSecs, }), ); }); -// Favicon endpoint -app.get('/favicon.ico', (_req: Request, res: Response) => { - res.setHeader('Content-Type', 'image/x-icon'); - res.setHeader('Cache-Control', 'public, max-age=86400'); - const faviconPath = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'favicon.ico'); - res.sendFile(faviconPath); -}); +// Interactive filesystem browser. SPA fetches the /fs/* JSON endpoints to +// render directory listings and file previews. +const handleBrowse = (_req: Request, res: Response) => { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.send(getBrowsePageHTML()); +}; +app.get('/browse', handleBrowse); +app.get('/browse/', handleBrowse); +app.get('/browse/*path', handleBrowse); // LLMs.txt endpoint (Markdown documentation for LLMs) app.get('/llms.txt', (_req: Request, res: Response) => { res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.send(getLLMsMarkdown({ serverUrl })); + res.send(getLLMsMarkdown({ serverUrl, idleTimeoutSecs })); }); // ============================================================================ -// Proxy Mappings Configuration Endpoints +// Bridges configuration endpoints // ============================================================================ -// GET /proxy-config - Get current proxy mappings -app.get('/proxy-config', (_req: Request, res: Response) => { +// GET /bridges - Get current bridges +app.get('/bridges', (_req: Request, res: Response) => { try { - const mappings = getProxyMappings(); - res.json({ mappings }); + const bridges = getBridges(); + res.json({ bridges }); } catch (error) { - log.error('Failed to get proxy config', { error: (error as Error).message }); + log.error('Failed to get bridges', { error: (error as Error).message }); res.status(500).json({ error: (error as Error).message }); } }); -// PUT /proxy-config - Replace all proxy mappings -app.put('/proxy-config', (req: Request, res: Response) => { +// PUT /bridges - Replace all bridges +app.put('/bridges', (req: Request, res: Response) => { try { - const { mappings } = req.body; + const { bridges } = req.body; - if (!Array.isArray(mappings)) { - res.status(400).json({ error: 'mappings must be an array' }); + if (!Array.isArray(bridges)) { + res.status(400).json({ error: 'bridges must be an array' }); return; } - // Validate each mapping - for (const mapping of mappings) { - if (!mapping.path || typeof mapping.path !== 'string') { - res.status(400).json({ error: 'Each mapping must have a path string' }); + // Validate each bridge + for (const bridge of bridges) { + if (!bridge.path || typeof bridge.path !== 'string') { + res.status(400).json({ error: 'Each bridge must have a path string' }); return; } - if (!mapping.target || typeof mapping.target !== 'string') { - res.status(400).json({ error: 'Each mapping must have a target string (full URL like http://127.0.0.1:3000/myapp)' }); + if (!bridge.target || typeof bridge.target !== 'string') { + res.status(400).json({ error: 'Each bridge must have a target string (full URL like http://127.0.0.1:3000/myapp)' }); return; } } - saveProxyConfig(mappings); - log.info('Proxy config updated via API', { count: mappings.length }); - res.json({ success: true, mappings: getProxyMappings() }); + saveBridges(bridges); + log.info('Bridges updated via API', { count: bridges.length }); + res.json({ success: true, bridges: getBridges() }); } catch (error) { - log.error('Failed to update proxy config', { error: (error as Error).message }); + log.error('Failed to update bridges', { error: (error as Error).message }); res.status(500).json({ error: (error as Error).message }); } }); -// POST /proxy-config - Add a single proxy mapping -app.post('/proxy-config', (req: Request, res: Response) => { +// POST /bridges - Add a single bridge +app.post('/bridges', (req: Request, res: Response) => { try { const { path, target } = req.body; @@ -708,29 +742,29 @@ app.post('/proxy-config', (req: Request, res: Response) => { return; } - addProxyMapping({ path, target }); - log.info('Proxy mapping added via API', { path, target }); - res.json({ success: true, mappings: getProxyMappings() }); + addBridge({ path, target }); + log.info('Bridge added via API', { path, target }); + res.json({ success: true, bridges: getBridges() }); } catch (error) { - log.error('Failed to add proxy mapping', { error: (error as Error).message }); + log.error('Failed to add bridge', { error: (error as Error).message }); res.status(500).json({ error: (error as Error).message }); } }); -// DELETE /proxy-config/:path - Remove a proxy mapping -app.delete('/proxy-config/*path', (req: Request, res: Response) => { +// DELETE /bridges/:path - Remove a bridge +app.delete('/bridges/*path', (req: Request, res: Response) => { try { - const pathToRemove = '/' + String(req.params.path || ''); + const pathToRemove = `/${ String(req.params.path || '')}`; - const removed = removeProxyMapping(pathToRemove); + const removed = removeBridge(pathToRemove); if (removed) { - log.info('Proxy mapping removed via API', { path: pathToRemove }); - res.json({ success: true, removed: pathToRemove, mappings: getProxyMappings() }); + log.info('Bridge removed via API', { path: pathToRemove }); + res.json({ success: true, removed: pathToRemove, bridges: getBridges() }); } else { - res.status(404).json({ error: 'Mapping not found', path: pathToRemove }); + res.status(404).json({ error: 'Bridge not found', path: pathToRemove }); } } catch (error) { - log.error('Failed to remove proxy mapping', { error: (error as Error).message }); + log.error('Failed to remove bridge', { error: (error as Error).message }); res.status(500).json({ error: (error as Error).message }); } }); @@ -753,7 +787,12 @@ app.get('/health', (_req: Request, res: Response) => { return; } - res.json({ status: 'healthy' }); + const body: Record = { status: 'healthy', idleTimeoutSecs }; + if (idleTimeoutSecs > 0) { + const elapsedSecs = Math.floor((Date.now() - lastActivityAt) / 1000); + body.remainingSecs = Math.max(0, idleTimeoutSecs - elapsedSecs); + } + res.json(body); }); // MCP endpoint using proper StreamableHTTPServerTransport @@ -858,24 +897,71 @@ app.post('/exec', async (req: Request, res: Response) => { // ============================================================================ const shellPort = 7681; +// ttyd's last words: the tail of its stdout/stderr (or a spawn error). Recorded +// so a crash โ€” e.g. a missing shared library โ€” shows up in the Actor log and in +// the /shell proxy response instead of a bare exit code. The delay grows as ttyd +// keeps failing to start (see nextTtydRestartDelayMs). +let lastTtydError = ''; +let ttydRestartDelayMs = TTYD_RESTART_MIN_MS; + // Spawn ttyd process const spawnTtyd = () => { log.info('Spawning ttyd process...', { port: shellPort }); + const startedAt = Date.now(); - // Run ttyd with custom bashrc for better UX and environment alignment + // Run ttyd with custom bashrc for better UX and environment alignment. Pipe + // its stdio (rather than ignoring it) so a startup failure is captured. const ttyd = spawn('ttyd', ['-p', shellPort.toString(), '-a', '-W', 'bash', '--rcfile', '/app/sandbox_bashrc'], { - stdio: 'ignore', + stdio: ['ignore', 'pipe', 'pipe'], cwd: SANDBOX_DIR, env: { ...process.env }, }); + // Keep only the tail of ttyd's output โ€” its startup/error messages are short. + let recentOutput = ''; + const capture = (chunk: Buffer): void => { + recentOutput = appendTtydOutput(recentOutput, chunk.toString()); + }; + ttyd.stdout?.on('data', capture); + ttyd.stderr?.on('data', capture); + + // Schedule exactly one restart per spawn: a failed spawn emits 'error' (no + // 'exit'), a started process emits 'exit'; guard against both firing. + let settled = false; + const restartAfter = (crashed: boolean): void => { + if (settled) return; + settled = true; + const delay = crashed ? ttydRestartDelayMs : TTYD_RESTART_MIN_MS; + ttydRestartDelayMs = nextTtydRestartDelayMs(ttydRestartDelayMs, crashed); + setTimeout(spawnTtyd, delay); + }; + ttyd.on('error', (err) => { + lastTtydError = err.message; log.error('Failed to start ttyd', { error: err.message }); + restartAfter(true); }); - ttyd.on('exit', (code) => { - log.warning('ttyd process exited', { code }); - setTimeout(spawnTtyd, 5000); + ttyd.on('exit', (code, signal) => { + const aliveMs = Date.now() - startedAt; + const output = recentOutput.trim(); + if (output) lastTtydError = output; + + // A fast exit means ttyd never really came up (missing shared library, + // port already bound, bad args). Shout, since the old fixed-5s retry with + // no detail produced an invisible crash loop behind "Shell Proxy Error". + const crashed = isTtydStartupCrash(aliveMs); + if (crashed) { + log.error('ttyd exited immediately โ€” interactive shell is unavailable', { + code, + signal, + aliveMs, + output: output || '(no output captured)', + }); + } else { + log.warning('ttyd process exited; restarting', { code, signal, aliveMs }); + } + restartAfter(crashed); }); }; @@ -883,6 +969,18 @@ if (!isLocalMode) { spawnTtyd(); } +// ============================================================================ +// Terminal disconnect messaging +// ============================================================================ +// +// There is deliberately no server-side shutdown banner. Most stops (run timeout, +// hard abort, platform scale-down) kill the container with a signal and no +// advance event โ€” the Apify SDK installs no SIGTERM/SIGINT handlers, it's driven +// by the platform events WebSocket โ€” and a dying process can't reliably flush a +// message to the browser anyway. Instead the terminal page relabels ttyd's own +// reconnect overlay client-side; see injectTerminalReconnectScript in +// templates/shell.ts (it shows "Actor probably finished" when a retry fails). + // Manual HTTP Proxy for ttyd app.all('/shell{*rest}', (req, res) => { let path = req.url.replace(/^\/shell/, '') || '/'; @@ -890,25 +988,64 @@ app.all('/shell{*rest}', (req, res) => { if (path.startsWith('?')) { path = `/${ path}`; } + path = translateLaunchParam(path); + // Ask ttyd for an uncompressed response so the terminal HTML can be rewritten + // (see injectTerminalReconnectScript below). ttyd's assets are tiny, so losing + // gzip here is negligible. + const headers = { ...req.headers, 'accept-encoding': 'identity' }; const options = { hostname: '127.0.0.1', port: shellPort, path, method: req.method, - headers: req.headers, + headers, }; const proxyReq = http.request(options, (proxyRes) => { - if (proxyRes.statusCode) { - res.writeHead(proxyRes.statusCode, proxyRes.headers); + // Inject the client-side reconnect notice into ttyd's terminal page. The + // server can't reliably push a shutdown message as the container is killed, + // so the browser relabels ttyd's reconnect overlay instead. Only the HTML + // document is rewritten; every other asset and status is piped through. + const isHtml = (proxyRes.headers['content-type'] || '').includes('text/html'); + if (!isHtml) { + if (proxyRes.statusCode) { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + } + proxyRes.pipe(res); + return; } - proxyRes.pipe(res); + + const chunks: Buffer[] = []; + proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk)); + proxyRes.on('end', () => { + const html = injectTerminalReconnectScript(Buffer.concat(chunks).toString('utf8')); + const outHeaders = { ...proxyRes.headers }; + // The body length changed and is now fixed: drop any stale length/ + // encoding framing and set the real one. + delete outHeaders['content-encoding']; + delete outHeaders['transfer-encoding']; + outHeaders['content-length'] = Buffer.byteLength(html).toString(); + res.writeHead(proxyRes.statusCode || 200, outHeaders); + res.end(html); + }); + proxyRes.on('error', (err) => { + log.error('Shell proxy response error', { error: err.message }); + if (!res.headersSent) res.status(502).type('text/plain').send('Shell proxy error'); + }); }); proxyReq.on('error', (err) => { - log.error('Manual proxy error', { error: err.message }); + // ECONNREFUSED means ttyd isn't listening โ€” it almost always crashed on + // startup (see spawnTtyd, which records its last output in lastTtydError). + // Surface that as a 503 instead of an opaque 500 so the cause is visible. + const ttydDown = (err as NodeJS.ErrnoException).code === 'ECONNREFUSED'; + log.error('Manual proxy error', { error: err.message, ttydDown }); if (!res.headersSent) { - res.status(500).send('Shell Proxy Error'); + if (ttydDown) { + res.status(503).type('text/plain').send(buildShellUnavailableMessage(lastTtydError)); + } else { + res.status(502).type('text/plain').send(`Shell proxy error: ${err.message}`); + } } }); @@ -921,9 +1058,22 @@ const wsProxy = httpProxy.createProxyServer({ ws: true, }); +// Without this handler a WebSocket upgrade to a down ttyd emits an 'error' with +// no listener, which Node escalates to an uncaught exception that can take down +// the whole server. ttyd is restarted by spawnTtyd; just close the browser +// socket so the terminal shows its reconnect overlay and retries. +wsProxy.on('error', (err, _req, resOrSocket) => { + log.warning('Shell WebSocket proxy error', { error: (err as Error).message }); + const socket = resOrSocket as Duplex | undefined; + if (socket && typeof socket.destroy === 'function' && !socket.destroyed) { + socket.destroy(); + } +}); + server.on('upgrade', (req, socket, head) => { if (req.url?.startsWith('/shell')) { req.url = req.url.replace(/^\/shell/, '') || '/'; + req.url = translateLaunchParam(req.url); log.info('Proxying shell WebSocket upgrade', { url: req.url }); // Track activity on WebSocket data @@ -933,28 +1083,28 @@ server.on('upgrade', (req, socket, head) => { wsProxy.ws(req, socket as Duplex, head); } else { - // Check dynamic proxy mappings - let matchedMapping: ProxyMapping | null = null; + // Check bridges + let matchedBridge: Bridge | null = null; let matchedPath = ''; const reqPath = req.url || '/'; // Extract just the path without query string for matching const pathOnly = reqPath.split('?')[0]; - - for (const mapping of getProxyMappings()) { - if (pathOnly.startsWith(mapping.path) && mapping.path.length > matchedPath.length) { - matchedMapping = mapping; - matchedPath = mapping.path; + + for (const bridge of getBridges()) { + if (pathOnly.startsWith(bridge.path) && bridge.path.length > matchedPath.length) { + matchedBridge = bridge; + matchedPath = bridge.path; } } - if (matchedMapping && dynamicProxies.has(matchedMapping.path)) { - const entry = dynamicProxies.get(matchedMapping.path)!; + if (matchedBridge && bridgeProxies.has(matchedBridge.path)) { + const entry = bridgeProxies.get(matchedBridge.path)!; // Get the extra path after the exposed path - let extraPath = pathOnly.slice(matchedMapping.path.length) || ''; + let extraPath = pathOnly.slice(matchedBridge.path.length) || ''; const queryString = reqPath.includes('?') ? reqPath.slice(reqPath.indexOf('?')) : ''; - + // Build the new URL: targetPath + extraPath + query string // Avoid double slashes when joining paths let finalPath = entry.targetPath; @@ -967,14 +1117,14 @@ server.on('upgrade', (req, socket, head) => { } finalPath += extraPath; } - + req.url = finalPath + queryString; if (!req.url.startsWith('/')) { - req.url = '/' + req.url; + req.url = `/${ req.url}`; } log.info('Proxying dynamic WebSocket upgrade', { - exposedPath: matchedMapping.path, + exposedPath: matchedBridge.path, targetUrl: entry.targetOrigin + req.url, }); @@ -989,23 +1139,23 @@ server.on('upgrade', (req, socket, head) => { }); // ============================================================================ -// Dynamic Proxy Mappings for Local Servers +// Bridges: dynamic reverse proxies for local servers // ============================================================================ -// Store for dynamic proxy instances - stores { proxy, targetOrigin, targetPath } -interface ProxyEntry { +// Live reverse-proxy instances backing each bridge +interface BridgeProxy { proxy: ReturnType; targetOrigin: string; targetPath: string; } -const dynamicProxies = new Map(); +const bridgeProxies = new Map(); /** - * Create or update proxy for a mapping + * Create or update the reverse proxy backing a bridge */ -const setupProxyForMapping = (mapping: ProxyMapping): void => { +const setupBridge = (bridge: Bridge): void => { // Normalize target URL - let targetUrl = mapping.target; + let targetUrl = bridge.target; if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) { targetUrl = `http://${targetUrl}`; } @@ -1024,8 +1174,8 @@ const setupProxyForMapping = (mapping: ProxyMapping): void => { } // Remove existing proxy if any - if (dynamicProxies.has(mapping.path)) { - const oldEntry = dynamicProxies.get(mapping.path); + if (bridgeProxies.has(bridge.path)) { + const oldEntry = bridgeProxies.get(bridge.path); oldEntry?.proxy.close(); } @@ -1033,12 +1183,12 @@ const setupProxyForMapping = (mapping: ProxyMapping): void => { const proxy = httpProxy.createProxyServer({ target: targetOrigin, changeOrigin: true, - // Don't rewrite redirects - we handle path mapping ourselves + // Don't rewrite redirects - we remap paths ourselves autoRewrite: false, }); proxy.on('error', (err, _req, res) => { - log.error('Dynamic proxy error', { path: mapping.path, target: targetUrl, error: err.message }); + log.error('Dynamic proxy error', { path: bridge.path, target: targetUrl, error: err.message }); if (res && 'writeHead' in res && !res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end(`Proxy error: target server at ${targetUrl} not available`); @@ -1047,86 +1197,86 @@ const setupProxyForMapping = (mapping: ProxyMapping): void => { // Rewrite Location headers in redirects to map target paths back to exposed paths proxy.on('proxyRes', (proxyRes) => { - const location = proxyRes.headers['location']; + const {location} = proxyRes.headers; if (location && typeof location === 'string') { - log.info('Proxy response with Location header', { + log.info('Proxy response with Location header', { statusCode: proxyRes.statusCode, location, targetPath, - exposedPath: mapping.path + exposedPath: bridge.path }); // If the location starts with the target path, rewrite it to the exposed path if (location.startsWith(targetPath)) { - const newLocation = mapping.path + location.slice(targetPath.length); - proxyRes.headers['location'] = newLocation; - log.info('Rewrote redirect Location header', { - original: location, + const newLocation = bridge.path + location.slice(targetPath.length); + proxyRes.headers.location = newLocation; + log.info('Rewrote redirect Location header', { + original: location, rewritten: newLocation, }); } } }); - dynamicProxies.set(mapping.path, { proxy, targetOrigin, targetPath }); - log.info('Proxy configured', { exposedPath: mapping.path, targetOrigin, targetPath }); + bridgeProxies.set(bridge.path, { proxy, targetOrigin, targetPath }); + log.info('Proxy configured', { exposedPath: bridge.path, targetOrigin, targetPath }); }; /** - * Remove proxy for a path + * Tear down the reverse proxy for a bridge path */ -const removeProxyForPath = (path: string): void => { - if (dynamicProxies.has(path)) { - const entry = dynamicProxies.get(path); +const removeBridgeProxy = (path: string): void => { + if (bridgeProxies.has(path)) { + const entry = bridgeProxies.get(path); entry?.proxy.close(); - dynamicProxies.delete(path); + bridgeProxies.delete(path); log.info('Proxy removed', { path }); } }; // Initialize proxies from current config -for (const mapping of getProxyMappings()) { - setupProxyForMapping(mapping); +for (const bridge of getBridges()) { + setupBridge(bridge); } // Listen for config changes and update proxies -onMappingsChange((newMappings) => { - // Find removed mappings - const newPaths = new Set(newMappings.map((m) => m.path)); - for (const path of dynamicProxies.keys()) { +onBridgesChange((newBridges) => { + // Find removed bridges + const newPaths = new Set(newBridges.map((m) => m.path)); + for (const path of bridgeProxies.keys()) { if (!newPaths.has(path)) { - removeProxyForPath(path); + removeBridgeProxy(path); } } - // Add/update mappings - for (const mapping of newMappings) { - setupProxyForMapping(mapping); + // Add/update bridges + for (const bridge of newBridges) { + setupBridge(bridge); } }); -// Dynamic proxy route handler - must be added BEFORE the 404 handler +// Bridge route handler - must be added BEFORE the 404 handler // This catches all requests to mapped paths app.use((req: Request, res: Response, next) => { - // Find matching proxy mapping (longest path match) - let matchedMapping: ProxyMapping | null = null; + // Find the matching bridge (longest path match) + let matchedBridge: Bridge | null = null; let matchedPath = ''; - for (const mapping of getProxyMappings()) { - if (req.path.startsWith(mapping.path) && mapping.path.length > matchedPath.length) { - matchedMapping = mapping; - matchedPath = mapping.path; + for (const bridge of getBridges()) { + if (req.path.startsWith(bridge.path) && bridge.path.length > matchedPath.length) { + matchedBridge = bridge; + matchedPath = bridge.path; } } - if (matchedMapping && dynamicProxies.has(matchedMapping.path)) { - const entry = dynamicProxies.get(matchedMapping.path)!; + if (matchedBridge && bridgeProxies.has(matchedBridge.path)) { + const entry = bridgeProxies.get(matchedBridge.path)!; // Get the extra path after the exposed path (e.g., /openclaw/foo -> /foo) - let extraPath = req.path.slice(matchedMapping.path.length) || ''; - + let extraPath = req.path.slice(matchedBridge.path.length) || ''; + // Build the new URL: targetPath + extraPath + query string const queryString = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : ''; - + // Avoid double slashes when joining paths let finalPath = entry.targetPath; if (extraPath) { @@ -1140,17 +1290,17 @@ app.use((req: Request, res: Response, next) => { } finalPath += extraPath; } - + req.url = finalPath + queryString; - + // Ensure URL starts with / if (!req.url.startsWith('/')) { - req.url = '/' + req.url; + req.url = `/${ req.url}`; } log.info('Proxying request', { originalPath: req.path, - exposedPath: matchedMapping.path, + exposedPath: matchedBridge.path, extraPath, targetPath: entry.targetPath, finalUrl: req.url, @@ -1168,12 +1318,12 @@ app.use((req: Request, res: Response, next) => { // Start server server.listen(port, () => { - log.info(`Apify AI Sandbox listening on port ${port}`); + log.info(`Apify AI Code Sandbox listening on port ${port}`); log.info(`Server URL: ${serverUrl}`); // Print startup information console.log('\n====================================='); - console.log('๐Ÿš€ Apify AI Sandbox Started'); + console.log('๐Ÿš€ Apify AI Code Sandbox Started'); console.log('=====================================\n'); console.log('๐Ÿ  Sandbox home page:'); @@ -1183,6 +1333,10 @@ server.listen(port, () => { console.log('๐Ÿ“„ Documentation for LLMs in Markdown:'); console.log(` ${serverUrl}/llms.txt\n`); + console.log('๐Ÿ—‚ File browser:'); + console.log(` GET ${serverUrl}/browse`); + console.log(' Interactive web UI for navigating /sandbox\n'); + // Shell terminal endpoint console.log('๐Ÿ–ฅ๏ธ Shell terminal:'); console.log(` ${serverUrl}/shell\n`); @@ -1190,14 +1344,15 @@ server.listen(port, () => { console.log('=====================================\n'); // Start idle timeout check - const idleTimeoutSecs = input?.idleTimeoutSeconds ?? 600; + idleTimeoutSecs = input?.idleTimeoutSecs ?? DEFAULT_IDLE_TIMEOUT_SECS; if (idleTimeoutSecs > 0) { log.info(`Idle timeout monitor started (${idleTimeoutSecs}s)`); setInterval(async () => { const idleTimeMs = Date.now() - lastActivityAt; if (idleTimeMs > idleTimeoutSecs * 1000) { - const message = `Actor shut down after ${Math.floor(idleTimeoutSecs / 60)} minutes of inactivity.`; + const message = `Sandbox shut down after ${Math.round(idleTimeoutSecs)} seconds of inactivity.`; log.warning(message); + await setStatusMessage('Sandbox is shutting down'); await Actor.exit({ statusMessage: message }); } }, 30000); // Check every 30 seconds diff --git a/sandbox/src/mcp-agent-config.ts b/sandbox/src/mcp-agent-config.ts new file mode 100644 index 0000000..3441f61 --- /dev/null +++ b/sandbox/src/mcp-agent-config.ts @@ -0,0 +1,282 @@ +/** + * MCP Agent Configuration Module + * + * Translates the user's MCP Connectors (the same list written to + * /sandbox/mcp.json by mcp-connections.ts) into the native config format of + * each supported coding agent โ€” Claude Code, Codex, and OpenCode โ€” so the + * connectors are available as tools the moment the agent starts (installed on + * first use; see agent-launchers.sh), with no `claude mcp add` / `mcpc connect` + * step. + * + * Why a translation step rather than pointing every agent at /sandbox/mcp.json: + * the three agents disagree on both the config schema and the env-var syntax. + * - Claude Code: ~/.claude.json top-level `mcpServers`, entries need + * `"type": "http"`, and `${VAR}` is expanded natively in header values. + * - Codex: ~/.codex/config.toml `[mcp_servers.]` with `url`; TOML strings + * are NOT interpolated, so a bearer token is referenced by env-var NAME via + * `bearer_token_env_var`. HTTP transport is gated behind the top-level + * `experimental_use_rmcp_client` flag. + * - OpenCode: ~/.config/opencode/opencode.json top-level `mcp`, entries use + * `"type": "remote"` and the `{env:VAR}` substitution syntax. + * + * Each agent's existing config (onboarding flags, provider/model defaults, + * approval policy) is preserved โ€” we only add/replace the MCP server entries. + * All writes log and swallow errors so a failure never aborts sandbox startup. + */ + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { dirname, join } from 'node:path'; + +import { log } from 'apify'; + +import type { McpConfig, McpServerEntry } from './mcp-connections.js'; + +const HOME = homedir(); + +/** Config file each agent reads its MCP servers from (all under the user's home). */ +export const CLAUDE_CONFIG_PATH = join(HOME, '.claude.json'); +export const CODEX_CONFIG_PATH = join(HOME, '.codex', 'config.toml'); +export const OPENCODE_CONFIG_PATH = join(HOME, '.config', 'opencode', 'opencode.json'); + +/** Markers around the Codex block we own, so re-runs replace rather than duplicate it. */ +const CODEX_BLOCK_START = '# >>> apify mcp connectors (auto-generated) >>>'; +const CODEX_BLOCK_END = '# <<< apify mcp connectors (auto-generated) <<<'; + +// --------------------------------------------------------------------------- +// Claude Code โ€” ~/.claude.json top-level `mcpServers` +// --------------------------------------------------------------------------- + +export interface ClaudeHttpServer { + type: 'http'; + url: string; + headers: Record; +} + +/** + * Build Claude Code's `mcpServers` map. Claude expands `${VAR}` in header + * values itself, so the entries are used verbatim apart from the required + * `"type": "http"` discriminator. + */ +export const buildClaudeMcpServers = (config: McpConfig): Record => { + const servers: Record = {}; + for (const [key, entry] of Object.entries(config.mcpServers)) { + servers[key] = { type: 'http', url: entry.url, headers: { ...entry.headers } }; + } + return servers; +}; + +/** + * Merge the MCP servers into an existing parsed ~/.claude.json object, leaving + * every other key (e.g. `hasCompletedOnboarding`) untouched. + */ +export const mergeClaudeConfig = (existing: Record, config: McpConfig): Record => { + const prior = (existing.mcpServers as Record) ?? {}; + return { ...existing, mcpServers: { ...prior, ...buildClaudeMcpServers(config) } }; +}; + +// --------------------------------------------------------------------------- +// OpenCode โ€” ~/.config/opencode/opencode.json top-level `mcp` +// --------------------------------------------------------------------------- + +export interface OpenCodeRemoteServer { + type: 'remote'; + url: string; + enabled: true; + /** Disable OAuth discovery so the static Authorization header is used as-is. */ + oauth: false; + headers: Record; +} + +/** Rewrite shell-style `${VAR}` references into OpenCode's `{env:VAR}` syntax. */ +export const toOpenCodeEnvSyntax = (value: string): string => + value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, '{env:$1}'); + +/** + * Build OpenCode's `mcp` map. Each connector becomes a remote server with + * header values converted to `{env:VAR}` so OpenCode resolves the token at + * launch time. + */ +export const buildOpenCodeMcp = (config: McpConfig): Record => { + const servers: Record = {}; + for (const [key, entry] of Object.entries(config.mcpServers)) { + const headers: Record = {}; + for (const [name, val] of Object.entries(entry.headers)) { + headers[name] = toOpenCodeEnvSyntax(val); + } + servers[key] = { type: 'remote', url: entry.url, enabled: true, oauth: false, headers }; + } + return servers; +}; + +/** Merge the MCP servers into an existing parsed opencode.json, preserving provider/model config. */ +export const mergeOpenCodeConfig = (existing: Record, config: McpConfig): Record => { + const prior = (existing.mcp as Record) ?? {}; + return { ...existing, mcp: { ...prior, ...buildOpenCodeMcp(config) } }; +}; + +// --------------------------------------------------------------------------- +// Codex โ€” ~/.codex/config.toml `[mcp_servers.]` +// --------------------------------------------------------------------------- + +/** Quote a value as a TOML basic string, escaping backslashes and double quotes. */ +const tomlString = (value: string): string => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + +/** Match a bearer token whose entire value is a single `${VAR}` reference. */ +const BEARER_ENV_REF = /^Bearer \$\{([A-Za-z_][A-Za-z0-9_]*)\}$/; +/** Match a header whose entire value is a single `${VAR}` reference. */ +const WHOLE_ENV_REF = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/; + +/** + * Render one Codex `[mcp_servers.]` table. Codex does not interpolate env + * vars in TOML strings, so it exposes dedicated keys that take an env-var NAME: + * - an `Authorization: Bearer ${VAR}` header maps to `bearer_token_env_var`, + * - any other whole-value `${VAR}` header maps to `env_http_headers`, + * - anything else is written literally under `http_headers`. + * The connector list only ever produces the bearer case, but the fallbacks keep + * the translation faithful for any header shape. + */ +const codexServerTable = (key: string, entry: McpServerEntry): string => { + const lines = [`[mcp_servers.${key}]`, `url = ${tomlString(entry.url)}`]; + + let bearerEnvVar: string | undefined; + const envHeaders: [string, string][] = []; + const literalHeaders: [string, string][] = []; + + for (const [name, value] of Object.entries(entry.headers)) { + const bearer = name.toLowerCase() === 'authorization' ? BEARER_ENV_REF.exec(value) : null; + const whole = WHOLE_ENV_REF.exec(value); + if (bearer) { + bearerEnvVar = bearer[1]; + } else if (whole) { + envHeaders.push([name, whole[1]]); + } else { + literalHeaders.push([name, value]); + } + } + + if (bearerEnvVar) lines.push(`bearer_token_env_var = ${tomlString(bearerEnvVar)}`); + if (literalHeaders.length > 0) { + const inline = literalHeaders.map(([n, v]) => `${tomlString(n)} = ${tomlString(v)}`).join(', '); + lines.push(`http_headers = { ${inline} }`); + } + if (envHeaders.length > 0) { + const inline = envHeaders.map(([n, v]) => `${tomlString(n)} = ${tomlString(v)}`).join(', '); + lines.push(`env_http_headers = { ${inline} }`); + } + // Remote servers reach Codex through a proxy that may cold-start; the 10s + // default startup timeout is tight, so give the handshake more room. + lines.push('startup_timeout_sec = 20'); + + return lines.join('\n'); +}; + +/** Build the marker-delimited Codex block holding every connector's server table. */ +export const buildCodexBlock = (config: McpConfig): string => { + const tables = Object.entries(config.mcpServers).map(([key, entry]) => codexServerTable(key, entry)); + return [CODEX_BLOCK_START, ...tables, CODEX_BLOCK_END].join('\n\n'); +}; + +const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** + * Splice the connector block into existing config.toml text. Replaces a prior + * auto-generated block if present (idempotent across restarts/migrations), and + * ensures the top-level `experimental_use_rmcp_client` flag that gates HTTP MCP + * transport is set โ€” prepended so it stays ahead of any `[table]` headers, as + * TOML requires of bare top-level keys. + */ +export const applyCodexConfig = (existing: string, config: McpConfig): string => { + // Drop any block we wrote previously, plus the blank lines hugging it. + const blockPattern = new RegExp( + `\\n*${escapeRegExp(CODEX_BLOCK_START)}[\\s\\S]*?${escapeRegExp(CODEX_BLOCK_END)}\\n*`, + ); + let text = existing.replace(blockPattern, '\n').replace(/\s+$/, ''); + + if (!/^\s*experimental_use_rmcp_client\s*=/m.test(text)) { + text = `experimental_use_rmcp_client = true\n${text}`; + } + + return `${text}\n\n${buildCodexBlock(config)}\n`; +}; + +// --------------------------------------------------------------------------- +// Side effects: read each agent's config, merge in the connectors, write it back +// --------------------------------------------------------------------------- + +/** Read and JSON-parse a config file. Returns `{}` if absent, `null` if unreadable/corrupt. */ +const readJsonConfig = (path: string): Record | null => { + if (!existsSync(path)) return {}; + try { + const parsed = JSON.parse(readFileSync(path, 'utf8')); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + log.warning('Existing agent config is not a JSON object; leaving it unchanged', { path }); + return null; + } + return parsed as Record; + } catch (error) { + // Don't clobber a config we can't understand โ€” better to keep the + // working baked-in defaults than overwrite them with a partial file. + log.warning('Failed to parse existing agent config; leaving it unchanged', { + path, + error: (error as Error).message, + }); + return null; + } +}; + +const writeFileEnsuringDir = (path: string, contents: string): void => { + const dir = dirname(path); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(path, contents); +}; + +const configureClaude = (config: McpConfig): void => { + const existing = readJsonConfig(CLAUDE_CONFIG_PATH); + if (!existing) return; + const merged = mergeClaudeConfig(existing, config); + writeFileEnsuringDir(CLAUDE_CONFIG_PATH, `${JSON.stringify(merged, null, 2)}\n`); +}; + +const configureOpenCode = (config: McpConfig): void => { + const existing = readJsonConfig(OPENCODE_CONFIG_PATH); + if (!existing) return; + const merged = mergeOpenCodeConfig(existing, config); + writeFileEnsuringDir(OPENCODE_CONFIG_PATH, `${JSON.stringify(merged, null, 2)}\n`); +}; + +const configureCodex = (config: McpConfig): void => { + const existing = existsSync(CODEX_CONFIG_PATH) ? readFileSync(CODEX_CONFIG_PATH, 'utf8') : ''; + writeFileEnsuringDir(CODEX_CONFIG_PATH, applyCodexConfig(existing, config)); +}; + +/** + * Load the user's MCP Connectors into Claude Code, Codex, and OpenCode so they + * appear as tools on launch. No-op when no connectors are configured (the + * agents keep their baked-in defaults). Each agent is configured independently + * and failures are logged but never thrown. + */ +export const configureAgentMcpServers = (config: McpConfig): void => { + const count = Object.keys(config.mcpServers).length; + if (count === 0) { + log.debug('No MCP connectors configured; leaving agent configs untouched'); + return; + } + + const agents: [string, (config: McpConfig) => void][] = [ + ['Claude Code', configureClaude], + ['Codex', configureCodex], + ['OpenCode', configureOpenCode], + ]; + + for (const [name, configure] of agents) { + try { + configure(config); + log.info(`Configured ${name} with MCP connectors`, { count }); + } catch (error) { + log.error(`Failed to configure ${name} with MCP connectors`, { + error: (error as Error).message, + }); + } + } +}; diff --git a/sandbox/src/mcp-connections.ts b/sandbox/src/mcp-connections.ts new file mode 100644 index 0000000..3bb07fd --- /dev/null +++ b/sandbox/src/mcp-connections.ts @@ -0,0 +1,116 @@ +/** + * MCP Connections Module + * + * Writes /sandbox/mcp.json on sandbox start so tools like `mcpc connect` + * can immediately find the MCP Connector proxies provided via Actor input. + * + * Each input value is a Connector ID (e.g. "conn_abc123"). At runtime the + * platform exposes the matching MCP server as a proxy at + * `${APIFY_MCP_PROXY_URL}/`, authenticated with `APIFY_TOKEN`. + */ + +/* eslint-disable no-template-curly-in-string -- ${APIFY_TOKEN} is a literal placeholder we write to mcp.json, not a JS template expression */ + +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; + +import { log } from 'apify'; + +import { SANDBOX_DIR } from './consts.js'; + +export const MCP_CONFIG_PATH = `${SANDBOX_DIR}/mcp.json`; + +/** Apify's hosted MCP server, always available via the run's APIFY_TOKEN. */ +export const APIFY_MCP_URL = 'https://mcp.apify.com'; + +export interface McpServerEntry { + url: string; + headers: Record; +} + +export interface McpConfig { + mcpServers: Record; +} + +/** The always-on Apify MCP server entry seeded into every config. */ +const apifyMcpServer = (): McpServerEntry => ({ + url: APIFY_MCP_URL, + headers: { Authorization: 'Bearer ${APIFY_TOKEN}' }, +}); + +/** + * Sanitize a Connector ID for use as a JSON object key. We keep the ID as the + * key when it's already safe to read in a shell (alphanumeric, `_`, `-`); if + * not, we fall back to a sanitized version so the file is still well-formed. + */ +const toServerKey = (id: string): string => { + if (/^[A-Za-z0-9_-]+$/.test(id)) return id; + return id.replace(/[^A-Za-z0-9_-]/g, '_'); +}; + +/** + * Build the MCP config object from a list of Connector IDs. + * + * Always seeds the hosted Apify MCP server (key `apify`) so agents have it + * available out of the box, then appends one entry per user-provided Connector. + * Returns just the Apify entry when the input is empty/invalid, so the file is + * never empty and downstream tools can rely on its shape. + */ +export const buildMcpConfig = (connectionIds: string[] | undefined, proxyUrl: string | undefined): McpConfig => { + const config: McpConfig = { mcpServers: { apify: apifyMcpServer() } }; + + if (!connectionIds || connectionIds.length === 0) return config; + + const base = (proxyUrl || '').replace(/\/+$/, ''); + + for (const raw of connectionIds) { + if (typeof raw !== 'string') continue; + const id = raw.trim(); + if (!id) continue; + + const key = toServerKey(id); + config.mcpServers[key] = { + url: `${base}/${id}`, + headers: { + Authorization: 'Bearer ${APIFY_TOKEN}', + }, + }; + } + + return config; +}; + +/** + * Write the MCP config to /sandbox/mcp.json. Always writes a file (even when + * the connection list is empty) so consumers can rely on its presence. + * Logs and swallows errors โ€” a failed write must not abort sandbox startup. + * + * Returns the config it built so the caller can hand the same server list to + * the agent configurators (see mcp-agent-config.ts) without rebuilding it. The + * config is returned even if the write fails, since the agent configs are + * independent of the /sandbox/mcp.json file. + */ +export const writeMcpConfig = (connectionIds: string[] | undefined): McpConfig => { + const proxyUrl = process.env.APIFY_MCP_PROXY_URL; + const config = buildMcpConfig(connectionIds, proxyUrl); + + try { + const dir = dirname(MCP_CONFIG_PATH); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + + writeFileSync(MCP_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`); + + const count = Object.keys(config.mcpServers).length; + log.info('Wrote MCP connections config', { path: MCP_CONFIG_PATH, count }); + if (count > 0 && !proxyUrl) { + log.warning('APIFY_MCP_PROXY_URL is not set; mcp.json entries point to an empty proxy base URL'); + } + } catch (error) { + log.error('Failed to write MCP connections config', { + path: MCP_CONFIG_PATH, + error: (error as Error).message, + }); + } + + return config; +}; diff --git a/sandbox/src/node-deps.ts b/sandbox/src/node-deps.ts new file mode 100644 index 0000000..b7ac320 --- /dev/null +++ b/sandbox/src/node-deps.ts @@ -0,0 +1,86 @@ +import { log } from 'apify'; + +import { isFlatJsonObject, safeParseJson } from './safe-json.js'; + +/** + * Parse a `{ "package": "version" }` object. Coerces numeric versions to + * strings and null/empty values to `latest`; malformed JSON degrades to `{}` + * with a warning so a single bad character does not abort the run. + */ +const parseJsonObject = (raw: string): Record => { + const parsed = safeParseJson( + raw, + 'nodeDependencies', + isFlatJsonObject, + 'JSON must be a flat object of package names to version strings', + ); + if (!parsed) return {}; + + const out: Record = {}; + for (const [name, value] of Object.entries(parsed)) { + const pkg = name.trim(); + if (!pkg) continue; + if (value === null || value === undefined || value === '') { + out[pkg] = 'latest'; + continue; + } + if (typeof value !== 'string' && typeof value !== 'number') { + log.warning('nodeDependencies: skipping non-string version', { package: pkg }); + continue; + } + out[pkg] = String(value); + } + return out; +}; + +/** + * Split a `package@version` line on the version separator. Returns `[name, version]`. + * + * Handles scoped packages (`@scope/name@version`) by splitting on the last `@`, + * not the first. A bare `package` (no `@version`) returns `version = 'latest'`. + */ +const splitSpec = (spec: string): [string, string] | null => { + const trimmed = spec.trim(); + if (!trimmed) return null; + + const isScoped = trimmed.startsWith('@'); + const versionAt = isScoped ? trimmed.indexOf('@', 1) : trimmed.indexOf('@'); + + if (versionAt < 0) return [trimmed, 'latest']; + + const name = trimmed.slice(0, versionAt).trim(); + const version = trimmed.slice(versionAt + 1).trim(); + if (!name) return null; + return [name, version || 'latest']; +}; + +const parseLines = (raw: string): Record => { + const out: Record = {}; + for (const rawLine of raw.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + + const split = splitSpec(line); + if (!split) { + log.warning('nodeDependencies: skipping malformed line', { line: rawLine }); + continue; + } + out[split[0]] = split[1]; + } + return out; +}; + +/** + * Parse the user-supplied `nodeDependencies` input. Accepts either: + * - npm CLI-style lines: one `package@version` per line (`#` comments ignored, + * missing `@version` defaults to `latest`, scoped packages supported), or + * - a JSON object (input starts with `{`): `{ "package-name": "version", ... }`. + * + * Returns a `{ name: version }` object suitable for `installNodeLibraries`. + */ +export const parseNodeDependencies = (raw: string | undefined | null): Record => { + if (!raw) return {}; + const trimmed = raw.trim(); + if (!trimmed) return {}; + return trimmed.startsWith('{') ? parseJsonObject(trimmed) : parseLines(trimmed); +}; diff --git a/sandbox/src/operations.ts b/sandbox/src/operations.ts index eb5282a..3d9324b 100644 --- a/sandbox/src/operations.ts +++ b/sandbox/src/operations.ts @@ -7,7 +7,7 @@ import type { Readable } from 'node:stream'; import { promisify } from 'node:util'; import { log } from 'apify'; -import archiver from 'archiver'; +import { ZipArchive } from 'archiver'; import mime from 'mime-types'; import { JS_TS_CODE_DIR, PYTHON_CODE_DIR, SANDBOX_DIR } from './consts.js'; @@ -709,7 +709,7 @@ export const createZipArchive = async ( } // Create archive - const archive = archiver('zip', { + const archive = new ZipArchive({ zlib: { level: 6 }, // Compression level }); diff --git a/sandbox/src/proxy-config.ts b/sandbox/src/proxy-config.ts deleted file mode 100644 index 98832fd..0000000 --- a/sandbox/src/proxy-config.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Proxy Configuration Module - * - * Manages proxy mapping configuration with file watching for live updates. - */ - -import { existsSync, readFileSync, watch, writeFileSync } from 'node:fs'; -import { dirname } from 'node:path'; -import { mkdirSync } from 'node:fs'; - -import { log } from 'apify'; - -import { PROXY_CONFIG_PATH } from './consts.js'; -import type { ProxyMapping } from './types.js'; - -// Current proxy mappings (in-memory cache) -let currentMappings: ProxyMapping[] = []; - -// Callbacks to notify when mappings change -const changeListeners: Array<(mappings: ProxyMapping[]) => void> = []; - -/** - * Initialize proxy configuration from input and start file watching - * @param initialMappings - Initial mappings from Actor input - */ -export const initializeProxyConfig = (initialMappings?: ProxyMapping[]): void => { - // First try to load from config file (persisted state) - if (existsSync(PROXY_CONFIG_PATH)) { - try { - const fileContent = readFileSync(PROXY_CONFIG_PATH, 'utf-8'); - currentMappings = JSON.parse(fileContent); - log.info('Loaded proxy mappings from config file', { - count: currentMappings.length, - mappings: currentMappings, - }); - } catch (error) { - log.warning('Failed to load proxy config file, using input mappings', { - error: (error as Error).message, - }); - currentMappings = initialMappings || []; - } - } else if (initialMappings && initialMappings.length > 0) { - // Use input mappings and save to config file - currentMappings = initialMappings; - saveProxyConfig(currentMappings); - log.info('Initialized proxy mappings from Actor input', { - count: currentMappings.length, - mappings: currentMappings, - }); - } - - // Start watching config file for changes - startConfigWatcher(); -}; - -/** - * Save proxy mappings to config file - * @param mappings - Mappings to save - */ -export const saveProxyConfig = (mappings: ProxyMapping[]): void => { - try { - // Ensure directory exists - const dir = dirname(PROXY_CONFIG_PATH); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - - writeFileSync(PROXY_CONFIG_PATH, JSON.stringify(mappings, null, 2)); - currentMappings = mappings; - log.info('Saved proxy mappings to config file', { - count: mappings.length, - path: PROXY_CONFIG_PATH, - }); - - // Notify listeners - notifyListeners(); - } catch (error) { - log.error('Failed to save proxy config', { error: (error as Error).message }); - throw error; - } -}; - -/** - * Get current proxy mappings - */ -export const getProxyMappings = (): ProxyMapping[] => { - return [...currentMappings]; -}; - -/** - * Add a proxy mapping - * @param mapping - Mapping with path and target URL - */ -export const addProxyMapping = (mapping: ProxyMapping): void => { - // Normalize path to ensure it starts with / - const normalizedPath = mapping.path.startsWith('/') ? mapping.path : `/${mapping.path}`; - - // Normalize target URL - add http:// if no protocol - let normalizedTarget = mapping.target.trim(); - if (!normalizedTarget.startsWith('http://') && !normalizedTarget.startsWith('https://')) { - normalizedTarget = `http://${normalizedTarget}`; - } - - const normalizedMapping = { path: normalizedPath, target: normalizedTarget }; - - // Check for duplicate paths - const existingIndex = currentMappings.findIndex((m) => m.path === normalizedPath); - if (existingIndex >= 0) { - // Update existing mapping - currentMappings[existingIndex] = normalizedMapping; - } else { - currentMappings.push(normalizedMapping); - } - - saveProxyConfig(currentMappings); -}; - -/** - * Remove a proxy mapping by path - * @param path - Path to remove - */ -export const removeProxyMapping = (path: string): boolean => { - const normalizedPath = path.startsWith('/') ? path : `/${path}`; - const initialLength = currentMappings.length; - currentMappings = currentMappings.filter((m) => m.path !== normalizedPath); - - if (currentMappings.length < initialLength) { - saveProxyConfig(currentMappings); - return true; - } - return false; -}; - -/** - * Register a callback to be called when mappings change - * @param callback - Function to call with new mappings - */ -export const onMappingsChange = (callback: (mappings: ProxyMapping[]) => void): void => { - changeListeners.push(callback); -}; - -/** - * Notify all listeners of mapping changes - */ -const notifyListeners = (): void => { - for (const listener of changeListeners) { - try { - listener([...currentMappings]); - } catch (error) { - log.error('Error in proxy config change listener', { error: (error as Error).message }); - } - } -}; - -/** - * Start watching the config file for external changes - */ -const startConfigWatcher = (): void => { - // Ensure directory exists before watching - const dir = dirname(PROXY_CONFIG_PATH); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - - // Debounce timeout - let debounceTimer: NodeJS.Timeout | null = null; - - try { - watch(dir, (eventType, filename) => { - if (filename === '.proxy-mappings.json' && eventType === 'change') { - // Debounce rapid changes - if (debounceTimer) { - clearTimeout(debounceTimer); - } - - debounceTimer = setTimeout(() => { - try { - if (existsSync(PROXY_CONFIG_PATH)) { - const fileContent = readFileSync(PROXY_CONFIG_PATH, 'utf-8'); - const newMappings = JSON.parse(fileContent); - - // Only update if actually changed - if (JSON.stringify(newMappings) !== JSON.stringify(currentMappings)) { - currentMappings = newMappings; - log.info('Proxy config file changed, reloading mappings', { - count: currentMappings.length, - }); - notifyListeners(); - } - } - } catch (error) { - log.error('Error reloading proxy config', { error: (error as Error).message }); - } - }, 100); - } - }); - - log.info('Started watching proxy config file for changes', { path: PROXY_CONFIG_PATH }); - } catch (error) { - log.warning('Failed to start config file watcher', { error: (error as Error).message }); - } -}; diff --git a/sandbox/src/safe-json.ts b/sandbox/src/safe-json.ts new file mode 100644 index 0000000..c5b61c4 --- /dev/null +++ b/sandbox/src/safe-json.ts @@ -0,0 +1,34 @@ +import { log } from 'apify'; + +/** Type guard for a non-null, non-array JSON object. */ +export const isFlatJsonObject = (value: unknown): value is Record => + !!value && typeof value === 'object' && !Array.isArray(value); + +/** + * Safely parse user-supplied JSON and verify it matches an expected shape. + * + * Returns the parsed value on success, or `null` if `JSON.parse` threw or the + * `isValid` predicate rejected the result. Both failure modes log a warning + * labeled with `fieldName` so callers can degrade to an empty result without + * aborting the run. + */ +export const safeParseJson = ( + raw: string, + fieldName: string, + isValid: (parsed: unknown) => parsed is T, + shapeDescription: string, +): T | null => { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (error) { + const err = error as Error; + log.warning(`${fieldName}: failed to parse JSON input, ignoring`, { error: err.message }); + return null; + } + if (!isValid(parsed)) { + log.warning(`${fieldName}: ${shapeDescription}`); + return null; + } + return parsed; +}; diff --git a/sandbox/src/shell-launch.ts b/sandbox/src/shell-launch.ts new file mode 100644 index 0000000..1ac954b --- /dev/null +++ b/sandbox/src/shell-launch.ts @@ -0,0 +1,52 @@ +/** + * Translate `?launch=` on a /shell URL into the `?arg=-c&arg=...` form + * ttyd expects, running inside a persistent interactive shell. + * + * Why the interactive wrapper: ttyd has no `--once`, so when the spawned + * process exits the browser reconnects and ttyd respawns it. A bare + * `bash -c "...; "` exits the moment finishes (or fails to start), + * which produced a restart loop. Instead we: + * 1. source the sandbox rcfile (so sees the same env + wrappers), + * 2. echo the command so it's visible (as if typed at the prompt), + * 3. run it, surfacing a non-zero exit status, + * 4. `exec` an interactive shell so the terminal stays alive afterwards โ€” + * keeping any output or errors on screen instead of looping away. + * + * Idempotent: if `launch` is absent, the path is returned unchanged. Other + * query params are preserved in order. + */ +const BASHRC = '/app/sandbox_bashrc'; +const INTERACTIVE_SHELL = `exec bash --rcfile ${BASHRC}`; + +/** Build the `bash -c` payload for a launched command. */ +const buildLaunchCommand = (launch: string): string => { + if (!launch.trim()) return INTERACTIVE_SHELL; + return [ + `source ${BASHRC};`, + `echo "$ ${launch}";`, + `${launch} || echo "[command exited with status $?]";`, + INTERACTIVE_SHELL, + ].join(' '); +}; + +export const translateLaunchParam = (path: string): string => { + const queryIdx = path.indexOf('?'); + if (queryIdx < 0) return path; + + const basePath = path.slice(0, queryIdx); + const params = new URLSearchParams(path.slice(queryIdx + 1)); + const launch = params.get('launch'); + if (launch === null) return path; + + params.delete('launch'); + const cmd = buildLaunchCommand(launch); + + const parts: string[] = [ + `arg=${encodeURIComponent('-c')}`, + `arg=${encodeURIComponent(cmd)}`, + ]; + const rest = params.toString(); + if (rest) parts.push(rest); + + return `${basePath}?${parts.join('&')}`; +}; diff --git a/sandbox/src/skills.ts b/sandbox/src/skills.ts new file mode 100644 index 0000000..d14197e --- /dev/null +++ b/sandbox/src/skills.ts @@ -0,0 +1,60 @@ +import { log } from 'apify'; + +import { safeParseJson } from './safe-json.js'; + +/** + * Trim entries, drop blanks and non-strings, and de-duplicate while preserving + * the original order. + */ +const cleanList = (values: unknown[]): string[] => { + const out: string[] = []; + const seen = new Set(); + for (const value of values) { + if (typeof value !== 'string') { + log.warning('skills: skipping non-string entry'); + continue; + } + const skill = value.trim(); + if (!skill || seen.has(skill)) continue; + seen.add(skill); + out.push(skill); + } + return out; +}; + +const parseJsonArray = (raw: string): string[] => { + const parsed = safeParseJson(raw, 'skills', Array.isArray, 'JSON must be an array of skill name strings'); + return parsed ? cleanList(parsed) : []; +}; + +const parseLines = (raw: string): string[] => { + const out: string[] = []; + for (const rawLine of raw.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + out.push(line); + } + return out; +}; + +/** + * Parse the user-supplied `skills` input into a de-duplicated list of skill + * identifiers for `installSkills`. Each identifier is passed through unchanged to + * the `skills` CLI, which accepts a GitHub `owner/repo` (e.g. `anthropics/skills`) + * or a repo URL (e.g. `https://github.com/anthropics/skills`). Accepts either: + * - one skill per line (blank lines and `#` comments ignored), or + * - a JSON array of skill name strings (input starting with `[` or `{` is parsed + * as JSON; any non-array JSON yields no skills). + * + * Also accepts an actual string array, which task inputs saved while `skills` + * was a `stringList` (and programmatic callers) may still send. + */ +export const parseSkills = (raw: string | string[] | undefined | null): string[] => { + if (!raw) return []; + if (Array.isArray(raw)) return cleanList(raw); + + const trimmed = raw.trim(); + if (!trimmed) return []; + const looksLikeJson = trimmed.startsWith('[') || trimmed.startsWith('{'); + return looksLikeJson ? parseJsonArray(trimmed) : cleanList(parseLines(trimmed)); +}; diff --git a/sandbox/src/status.ts b/sandbox/src/status.ts new file mode 100644 index 0000000..c14119a --- /dev/null +++ b/sandbox/src/status.ts @@ -0,0 +1,22 @@ +// Run status message reporting for the Apify Console. +import { Actor, log } from 'apify'; + +const isLocalMode = process.env.MODE === 'local'; + +/** + * Update the Actor run's status message shown in the Apify Console so each + * lifecycle stage is visible at a glance (installing dependencies, running the + * setup script, live, shutting down). + * + * Best-effort by design: a failed status update โ€” or running locally with no + * platform run to report to โ€” must never interrupt startup or shutdown, so the + * call is swallowed and only logged. + */ +export const setStatusMessage = async (message: string): Promise => { + if (isLocalMode) return; + try { + await Actor.setStatusMessage(message); + } catch (err) { + log.warning('Failed to set Actor status message', { message, error: (err as Error).message }); + } +}; diff --git a/sandbox/src/templates/browse.ts b/sandbox/src/templates/browse.ts new file mode 100644 index 0000000..4316822 --- /dev/null +++ b/sandbox/src/templates/browse.ts @@ -0,0 +1,344 @@ +/** + * Filesystem browser page served at /browse and /browse/. + * + * Client-side SPA that fetches the existing /fs/* JSON endpoints to render + * directory listings and inline file previews. All path validation happens + * server-side in the /fs/* handlers โ€” this page is purely a viewer. + */ +export function getBrowsePageHTML(): string { + return ` + + + + + Sandbox | Browse + + + + +
+
+
+

Sandbox browser

+
/sandbox
+
+
+ Docs + Shell +
+
+ +
+
+ +
+
+
Loadingโ€ฆ
+
+
+ + + + +`; +} diff --git a/sandbox/src/templates/favicon.ico b/sandbox/src/templates/favicon.ico deleted file mode 100644 index e9fad85..0000000 Binary files a/sandbox/src/templates/favicon.ico and /dev/null differ diff --git a/sandbox/src/templates/landing.css b/sandbox/src/templates/landing.css new file mode 100644 index 0000000..ea6a9c2 --- /dev/null +++ b/sandbox/src/templates/landing.css @@ -0,0 +1,493 @@ +:root { + color-scheme: dark; + --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --mono: 'SFMono-Regular', ui-monospace, 'Cascadia Code', Consolas, 'Liberation Mono', Menlo, monospace; + + --bg: + radial-gradient(1100px 560px at 86% -8%, rgba(56, 189, 248, 0.1), transparent 60%), + radial-gradient(900px 520px at -6% 4%, rgba(99, 102, 241, 0.16), transparent 55%), #0b1120; + --hero-bg: linear-gradient(180deg, rgba(30, 41, 59, 0.55), rgba(15, 23, 42, 0.3)); + --accent-bar: linear-gradient(90deg, #6366f1, #22d3ee); + --card-bg: rgba(17, 24, 39, 0.55); + --blur: blur(8px); + --border: rgba(148, 163, 184, 0.16); + --shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 12px 32px rgba(2, 6, 23, 0.35); + + --text: #d7dee9; + --text-strong: #f8fafc; + --text-muted: #93a1b5; + --text-faint: #5b6679; + + --accent: #6366f1; + --accent-contrast: #ffffff; + --ghost-bg: transparent; + --hover: rgba(148, 163, 184, 0.12); + --chip: rgba(148, 163, 184, 0.1); + + --icode-bg: rgba(99, 102, 241, 0.12); + --icode-border: rgba(129, 140, 248, 0.22); + --icode-text: #c7d2fe; + --code-bg: #070b16; + --code-border: rgba(148, 163, 184, 0.14); + --code-text: #e2e8f0; + --input-bg: #070b16; + + --radius: 16px; + --btn-radius: 9px; + + --ok: #22c55e; + --ok-glow: rgba(34, 197, 94, 0.16); + --warn: #eab308; + --danger: #f87171; + --danger-bg: rgba(248, 113, 113, 0.14); + + --hl-command: #7dd3fc; + --hl-flag: #c4b5fd; + --hl-url: #86efac; + --hl-string: #fde68a; + --hl-number: #fdba74; + --hl-key: #d8b4fe; + --hl-curl: #67e8f9; + --hl-method: #f0abfc; + --hl-comment: #64748b; +} +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} +html { + scroll-behavior: smooth; +} +body { + font-family: var(--font); + background: var(--bg); + background-attachment: fixed; + color: var(--text); + padding: 40px 24px 64px; + min-height: 100vh; + font-size: 14px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} +.page { + max-width: 940px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 18px; +} +a { + color: var(--accent); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +.hero { + display: flex; + flex-direction: column; + gap: 14px; + padding: 30px; + background: var(--hero-bg); + border: 1px solid var(--border); + border-radius: var(--radius); + position: relative; + overflow: hidden; +} +.hero::after { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 3px; + background: var(--accent-bar); +} +.hero-bar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + flex-wrap: wrap; +} +.hero-badges { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + flex-shrink: 0; +} +h1 { + font-size: 30px; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-strong); +} +.lead { + color: var(--text-muted); + font-size: 15px; +} +.badge { + background: var(--ok); + color: #04140b; + padding: 6px 12px; + border-radius: 999px; + font-weight: 600; + white-space: nowrap; + font-size: 12px; +} + +.card { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 22px 24px; + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: var(--shadow); + backdrop-filter: var(--blur); +} +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} +h2 { + font-size: 17px; + font-weight: 650; + color: var(--text-strong); + letter-spacing: -0.01em; + display: inline-flex; + align-items: center; + gap: 9px; +} +h3 { + font-size: 15px; + font-weight: 650; + color: var(--text-strong); +} +.section-icon { + font-size: 16px; + line-height: 1; +} + +.actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.button { + display: inline-flex; + align-items: center; + gap: 7px; + background: var(--accent); + color: var(--accent-contrast); + padding: 9px 14px; + border-radius: var(--btn-radius); + font-weight: 600; + font-size: 13.5px; + border: 1px solid transparent; + text-decoration: none; + white-space: nowrap; + transition: + transform 0.12s ease, + filter 0.12s ease, + background 0.12s ease, + border-color 0.12s ease; +} +.button:hover { + filter: brightness(1.08); + text-decoration: none; + transform: translateY(-1px); +} +.button .dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: currentColor; + opacity: 0.85; +} +.button.ghost { + background: var(--ghost-bg); + color: var(--text); + border-color: var(--border); +} +.button.ghost:hover { + background: var(--hover); + filter: none; +} + +.list { + list-style: none; + display: grid; + gap: 9px; +} +.list li { + color: var(--text-muted); +} +.list strong { + color: var(--text); + font-weight: 600; +} +.bullets { + padding-left: 1.4em; +} +.bullets li { + color: var(--text-muted); + margin-top: 9px; +} +.bullets li:first-child { + margin-top: 0; +} + +/* Inline code chips render truly inline so they sit on the text baseline. */ +code { + font-family: var(--mono); + display: inline; + padding: 2px 6px; + border-radius: 5px; + font-size: 0.88em; + background: var(--icode-bg); + border: 1px solid var(--icode-border); + color: var(--icode-text); + vertical-align: baseline; + white-space: nowrap; +} +/* Route chip shown next to a section title, e.g. /mcp */ +.section-path { + font-size: 12.5px; + font-weight: 500; + color: var(--text-muted); + background: var(--chip); + border-color: var(--border); +} +.muted { + color: var(--text-muted); +} +.example-label { + font-size: 15px; + font-weight: 650; + color: var(--text-strong); + margin-top: 4px; + line-height: 1.4; +} + +.code-block-wrapper { + position: relative; +} +.code-block { + font-family: var(--mono); + display: block; + background: var(--code-bg); + border: 1px solid var(--code-border); + color: var(--code-text); + padding: 14px 16px; + border-radius: 10px; + font-size: 12.5px; + overflow-x: auto; + white-space: pre; + line-height: 1.65; +} +.copy-btn { + position: absolute; + top: 10px; + right: 10px; + background: var(--chip); + color: var(--text-muted); + border: 1px solid var(--border); + padding: 5px 10px; + border-radius: 6px; + font-size: 11.5px; + font-weight: 600; + cursor: pointer; + transition: + color 0.12s ease, + background 0.12s ease; +} +.copy-btn:hover { + color: var(--text-strong); + background: var(--hover); +} +.copy-btn.copied { + background: var(--ok); + color: #04140b; + border-color: transparent; +} + +/* Syntax highlighting */ +.hl-command { + color: var(--hl-command); +} +.hl-flag { + color: var(--hl-flag); +} +.hl-url { + color: var(--hl-url); +} +.hl-string { + color: var(--hl-string); +} +.hl-number { + color: var(--hl-number); +} +.hl-key { + color: var(--hl-key); +} +.hl-curl { + color: var(--hl-curl); + font-weight: 600; +} +.hl-method { + color: var(--hl-method); + font-weight: 600; +} +.hl-comment { + color: var(--hl-comment); + font-style: italic; +} + +/* Status badge */ +.status-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 13px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + background: var(--chip); + color: var(--text); + border: 1px solid var(--border); + white-space: nowrap; +} +a.status-badge:hover { + text-decoration: none; + border-color: var(--text-faint); +} +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-faint); + animation: pulse 2s infinite; +} +.status-icon { + font-size: 12px; + line-height: 1; +} +/* Rendered only in the generated /llms.txt Markdown (where CSS doesn't apply); + hidden in the live page, which shows the JS-driven status text instead. */ +.md-only { + display: none; +} +/* Keep countdown digits fixed-width so the badge doesn't jiggle as it ticks */ +.countdown { + font-variant-numeric: tabular-nums; + font-feature-settings: 'tnum'; +} +.status-badge.healthy .status-dot { + background: var(--ok); + box-shadow: 0 0 0 3px var(--ok-glow); +} +.status-badge.unhealthy .status-dot { + background: #ef4444; + animation: none; +} +.status-badge.loading .status-dot { + background: var(--warn); +} +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +/* Bridges */ +.field-label { + display: block; + font-size: 12px; + color: var(--text-muted); + margin-bottom: 6px; +} +.input { + width: 100%; + padding: 9px 12px; + border-radius: 8px; + background: var(--input-bg); + border: 1px solid var(--border); + color: var(--text); + font-family: inherit; + font-size: 13.5px; +} +.input::placeholder { + color: var(--text-faint); +} +.bridge-form { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: flex-end; +} +.bridge-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: var(--input-bg); + border: 1px solid var(--border); + border-radius: 8px; + margin-bottom: 8px; +} +.bridge-row code { + background: none; + border: none; + padding: 0; +} +.bridge-path { + flex: 1; +} +.bridge-target { + flex: 2; +} +.bridge-open { + font-size: 13px; +} +.tag { + color: var(--text-faint); +} +.btn-danger { + background: var(--danger-bg); + color: var(--danger); + border: none; + padding: 5px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 12px; + font-weight: 600; +} +.bridge-empty { + color: var(--text-faint); + font-style: italic; +} + +@media (max-width: 640px) { + body { + padding: 22px 14px 40px; + } + .hero-bar { + flex-direction: column; + align-items: flex-start; + } + .card-header { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/sandbox/src/templates/landing.ejs b/sandbox/src/templates/landing.ejs index bce83e9..fa188cc 100644 --- a/sandbox/src/templates/landing.ejs +++ b/sandbox/src/templates/landing.ejs @@ -4,332 +4,201 @@ Sandbox | Home - - + +
-
-

Apify AI Sandbox

-

Landing page

-

Connect through HTTP, MCP, or the interactive shell.

-
-
-
- - Checking... +
+

Apify AI Code Sandbox

+
+ + ๐Ÿ“„ + /llms.txt + + + <%# The status-badge link is kept in /llms.txt so the /health URL stays %> + <%# discoverable by LLMs. The live "Checking..." text is page-only %> + <%# (data-no-md, swapped out by JS); the Markdown shows a static %> + <%# "Health check" label via the .md-only span instead. %> + Checking...Health check + <% if (isLocalMode) { %> +
<%= modeLabel %>
+ <% } %>
- <% if (isLocalMode) { %> -
<%= modeLabel %>
- <% } %>
+

An Actor for secure execution of arbitrary code. Connect using MCP, REST API, or interactive shell.<% if (idleTimeoutSecs > 0) { %> The Actor shuts down automatically after <%= idleTimeoutLabel %> of inactivity.<% } %>

-

๐Ÿ”— Quick links

-
- Interactive shell - Claude Code - OpenCode - LLMs.txt +

๐Ÿ–ฅ๏ธ Interactive shell /shell

+
+ +

URL

+
+ +
<%= serverUrl %>/shell
+
+ +

Launch a specific command

+
+ +
<%= serverUrl %>/shell?launch=ls
+
-

๐Ÿ“ก Connect with MCP

-

URL

+

๐Ÿ“ก Connect with MCP /mcp

+

MCP server URL (no authentication required)

<%= serverUrl %>/mcp
- -

Claude Code

+ +

Set up examples

-
claude mcp add --transport http sandbox <%= serverUrl %>/mcp
+
+claude mcp add --transport http sandbox <%= serverUrl %>/mcp
+codex mcp add sandbox --url <%= serverUrl %>/mcp
+mcpc connect <%= serverUrl %>/mcp @sandbox
-
-

โšก Code execution

- -
- -
-

Run bash command

+

โšก Code execution API /exec

+ +

Run Bash command

-
curl -X POST <%= serverUrl %>/exec \
+                
curl -X POST <%= serverUrl %>/exec \
   -H "Content-Type: application/json" \
   -d '{"command": "ls -la", "language": "bash", "cwd": "/sandbox", "timeoutSecs": 5}'
-

Run Python code

+

Run Python code

-
curl -X POST <%= serverUrl %>/exec \
+                
curl -X POST <%= serverUrl %>/exec \
   -H "Content-Type: application/json" \
   -d '{"command": "print(\"hello\")", "language": "py", "timeoutSecs": 10}'
-

Run TypeScript code

+

Run TypeScript code

-
curl -X POST <%= serverUrl %>/exec \
+                
curl -X POST <%= serverUrl %>/exec \
   -H "Content-Type: application/json" \
   -d '{"command": "console.log(\"hello\")", "language": "ts", "timeoutSecs": 10}'
+ +

Response format

+

All /exec requests return:

+
+ +
{
+    "stdout": "string",
+    "stderr": "string",
+    "exitCode": 0,
+    "language": "shell|js|ts|py"
+}
+ +

Working directories

+
    +
  • Shell commands: /sandbox (default)
  • +
  • JavaScript/TypeScript: /sandbox/js-ts (default)
  • +
  • Python: /sandbox/py (default)
  • +
+

Override with cwd parameter (must be within /sandbox).

-
-

๐Ÿ“ Filesystem endpoints

- -
- -
-

Direct file operations using HTTP methods. All paths relative to /sandbox.

- -

Read file or list directory

+

๐Ÿ“ Filesystem API /fs

+

Direct file operations using HTTP methods. All paths relative to /sandbox.

+ +

Read file or list directory

-
curl <%= serverUrl %>/fs/app/log.txt
+
curl <%= serverUrl %>/fs/app/log.txt
-

Write or replace file

+

Write or replace file

-
curl -X PUT <%= serverUrl %>/fs/config.json \
+                
curl -X PUT <%= serverUrl %>/fs/config.json \
   -H "Content-Type: application/json" \
   -d '{"key": "value"}'
-

Create directory

+

Create directory

-
curl -X POST <%= serverUrl %>/fs/project/src?mkdir=1
+
curl -X POST <%= serverUrl %>/fs/project/src?mkdir=1
-

Append to file

+

Append to file

-
curl -X POST <%= serverUrl %>/fs/log.txt?append=1 \
+                
curl -X POST <%= serverUrl %>/fs/log.txt?append=1 \
   -d "New log entry"
-

Delete file or directory

+

Delete file or directory (to delete non-empty directory add recursive=1)

-
curl -X DELETE <%= serverUrl %>/fs/temp?recursive=1
+
curl -X DELETE <%= serverUrl %>/fs/temp?recursive=1
-

Get file metadata

+

Get file metadata

-
curl -I <%= serverUrl %>/fs/data.json
-
+
curl -I <%= serverUrl %>/fs/data.json
-
-

๐Ÿ”€ Proxy Mappings

- -
+

๐Ÿ”€ Bridges /bridges

+

Expose web servers that you start inside the sandbox (your own dev server, a TUI gateway, etc.) at a public URL path on this container, so they're reachable from the browser or other services. Each bridge forwards <%= serverUrl %>/<path> to http://127.0.0.1:<port>/... over HTTP and WebSocket. Changes apply immediately and persist across restarts.

-
-

Map local web servers to paths. Changes are applied immediately and persist across restarts.

+
-
- -
-
- - +
+
+ +
-
- - +
+ +
- +
-

API Examples

+

API examples

-
# Get current mappings
-curl <%= serverUrl %>/proxy-config
+                
# List bridges
+curl <%= serverUrl %>/bridges
 
-# Add a mapping
-curl -X POST <%= serverUrl %>/proxy-config \
+# Add a bridge
+curl -X POST <%= serverUrl %>/bridges \
   -H "Content-Type: application/json" \
   -d '{"path": "/openclaw", "target": "http://127.0.0.1:18789/openclaw"}'
 
-# Remove a mapping
-curl -X DELETE <%= serverUrl %>/proxy-config/openclaw
-
+# Remove a bridge +curl -X DELETE <%= serverUrl %>/bridges/openclaw
@@ -338,12 +207,12 @@ function copyCode(button) { const codeBlock = button.nextElementSibling; const code = codeBlock.textContent; - + navigator.clipboard.writeText(code).then(() => { const originalText = button.textContent; button.textContent = 'Copied!'; button.classList.add('copied'); - + setTimeout(() => { button.textContent = originalText; button.classList.remove('copied'); @@ -352,28 +221,44 @@ console.error('Failed to copy:', err); }); } - - function toggleCollapse(id) { - const content = document.getElementById(id); - const button = document.getElementById(id + 'Btn'); - - if (content.classList.contains('collapsed')) { - content.classList.remove('collapsed'); - button.textContent = 'Hide'; - } else { - content.classList.add('collapsed'); - button.textContent = 'Show'; + + // Client-clock timestamp (ms) when idle shutdown will fire, re-synced + // from /health. null means no active idle timeout (disabled/offline). + let shutdownDeadline = null; + + function formatDuration(totalSecs) { + const s = Math.max(0, totalSecs); + const pad = (n) => String(n).padStart(2, '0'); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + return h > 0 ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`; + } + + function renderCountdown() { + const badge = document.getElementById('shutdownBadge'); + const text = document.getElementById('shutdownText'); + + if (shutdownDeadline === null) { + badge.style.display = 'none'; + return; } + + const remainingSecs = Math.round((shutdownDeadline - Date.now()) / 1000); + badge.style.display = ''; + text.textContent = remainingSecs <= 0 + ? 'Shutting downโ€ฆ' + : `Idle shutdown in ${formatDuration(remainingSecs)}`; } - + async function checkHealth() { const badge = document.getElementById('statusBadge'); const text = document.getElementById('statusText'); - + try { const response = await fetch('/health'); const data = await response.json(); - + if (data.status === 'healthy') { badge.className = 'status-badge healthy'; text.textContent = 'Healthy'; @@ -381,117 +266,117 @@ badge.className = 'status-badge unhealthy'; text.textContent = data.status || 'Unhealthy'; } + + // remainingSecs is only present while an idle timeout is active. + shutdownDeadline = typeof data.remainingSecs === 'number' + ? Date.now() + data.remainingSecs * 1000 + : null; } catch (error) { badge.className = 'status-badge unhealthy'; text.textContent = 'Offline'; + shutdownDeadline = null; } + + renderCountdown(); } - + // Check health on page load checkHealth(); - + // Refresh health status every 30 seconds setInterval(checkHealth, 30000); - // Proxy Mappings Management - async function loadProxyMappings() { + // Tick the idle-shutdown countdown locally between health syncs + setInterval(renderCountdown, 1000); + + // Bridges management + async function loadBridges() { try { - const response = await fetch('/proxy-config'); + const response = await fetch('/bridges'); const data = await response.json(); - renderProxyMappings(data.mappings || []); + renderBridges(data.bridges || []); } catch (error) { - console.error('Failed to load proxy mappings:', error); + console.error('Failed to load bridges:', error); } } - - function renderProxyMappings(mappings) { - const container = document.getElementById('proxyMappingsList'); - - if (mappings.length === 0) { - container.innerHTML = '

No proxy mappings configured

'; + + function renderBridges(bridges) { + const container = document.getElementById('bridgesList'); + + if (bridges.length === 0) { + container.innerHTML = '

No bridges configured

'; return; } - - container.innerHTML = mappings.map(m => ` -
- ${m.path} - โ†’ - ${m.target} - Open - + + container.innerHTML = bridges.map(b => ` +
+ ${b.path} + โ†’ + ${b.target} + Open +
`).join(''); } - - async function addProxyMapping() { - const pathInput = document.getElementById('newProxyPath'); - const targetInput = document.getElementById('newProxyTarget'); - + + async function addBridge() { + const pathInput = document.getElementById('newBridgePath'); + const targetInput = document.getElementById('newBridgeTarget'); + const path = pathInput.value.trim(); const target = targetInput.value.trim(); - + if (!path || !target) { alert('Both path and target are required'); return; } - + try { - const response = await fetch('/proxy-config', { + const response = await fetch('/bridges', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path, target }) }); - + if (response.ok) { const data = await response.json(); - renderProxyMappings(data.mappings); + renderBridges(data.bridges); pathInput.value = ''; targetInput.value = ''; } else { const error = await response.json(); - alert('Failed to add mapping: ' + error.error); + alert('Failed to add bridge: ' + error.error); } } catch (error) { - alert('Failed to add mapping: ' + error.message); + alert('Failed to add bridge: ' + error.message); } } - - async function deleteProxyMapping(path) { - if (!confirm(`Remove proxy mapping for ${path}?`)) return; - + + async function deleteBridge(path) { + if (!confirm(`Remove bridge for ${path}?`)) return; + try { - const response = await fetch('/proxy-config' + path, { + const response = await fetch('/bridges' + path, { method: 'DELETE' }); - + if (response.ok) { const data = await response.json(); - renderProxyMappings(data.mappings); + renderBridges(data.bridges); } else { const error = await response.json(); - alert('Failed to remove mapping: ' + error.error); + alert('Failed to remove bridge: ' + error.error); } } catch (error) { - alert('Failed to remove mapping: ' + error.message); + alert('Failed to remove bridge: ' + error.message); } } - - // Load proxy mappings on page load - loadProxyMappings(); - - // Refresh proxy mappings every 10 seconds to catch file-based changes - setInterval(loadProxyMappings, 10000); + + // Load bridges on page load + loadBridges(); + + // Refresh bridges every 10 seconds to catch file-based changes + setInterval(loadBridges, 10000); - \ No newline at end of file + diff --git a/sandbox/src/templates/landing.ts b/sandbox/src/templates/landing.ts index 2e210c4..8afe2c1 100644 --- a/sandbox/src/templates/landing.ts +++ b/sandbox/src/templates/landing.ts @@ -3,28 +3,82 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import ejs from 'ejs'; +import { NodeHtmlMarkdown } from 'node-html-markdown'; +import { parse as parseHtml } from 'node-html-parser'; interface LandingPageOptions { serverUrl: string; isLocalMode: boolean; + idleTimeoutSecs: number; +} + +/** Human-readable duration for the idle-timeout sentence, e.g. "15 minutes". */ +function humanizeDuration(secs: number): string { + if (secs % 3600 === 0) { + const hours = secs / 3600; + return `${hours} hour${hours === 1 ? '' : 's'}`; + } + if (secs >= 60) { + const mins = Math.round(secs / 60); + return `${mins} minute${mins === 1 ? '' : 's'}`; + } + return `${secs} second${secs === 1 ? '' : 's'}`; } const templatePath = join(dirname(fileURLToPath(import.meta.url)), 'landing.ejs'); const landingTemplate = readFileSync(templatePath, 'utf8'); -const llmsTemplatePath = join(dirname(fileURLToPath(import.meta.url)), 'llms.md'); -const llmsTemplate = readFileSync(llmsTemplatePath, 'utf8'); +const stylesPath = join(dirname(fileURLToPath(import.meta.url)), 'landing.css'); +const landingStyles = readFileSync(stylesPath, 'utf8'); + +// `.status-badge` is intentionally not stripped here: the health-status badge is +// kept in /llms.txt so the /health URL stays discoverable. Badges that should not +// appear in the Markdown (the /llms.txt link, the idle countdown) are tagged +// data-no-md in the template instead. +const STRIP_SELECTOR = 'script, style, [data-no-md], .copy-btn'; -export function getLandingPageHTML({ serverUrl, isLocalMode }: LandingPageOptions): string { +const nhm = new NodeHtmlMarkdown( + { + bulletMarker: '-', + codeFence: '```', + codeBlockStyle: 'fenced', + useInlineLinks: true, + }, + { + pre: { + noEscape: true, + postprocess: ({ node }) => { + const text = (node.textContent ?? '').replace(/\n+$/, ''); + const lang = node.getAttribute('data-lang') ?? ''; + return `\`\`\`${lang}\n${text}\n\`\`\``; + }, + surroundingNewlines: 2, + }, + }, +); + +export function getLandingPageHTML({ serverUrl, isLocalMode, idleTimeoutSecs }: LandingPageOptions): string { const modeLabel = isLocalMode ? 'Local mode (deps skipped)' : 'Production mode'; return ejs.render(landingTemplate, { serverUrl, modeLabel, isLocalMode, + idleTimeoutSecs, + idleTimeoutLabel: humanizeDuration(idleTimeoutSecs), + styles: landingStyles, }); } -export function getLLMsMarkdown({ serverUrl }: { serverUrl: string }): string { - return ejs.render(llmsTemplate, { serverUrl }); +export function getLLMsMarkdown({ + serverUrl, + idleTimeoutSecs, +}: { + serverUrl: string; + idleTimeoutSecs: number; +}): string { + const html = getLandingPageHTML({ serverUrl, isLocalMode: false, idleTimeoutSecs }); + const root = parseHtml(html); + root.querySelectorAll(STRIP_SELECTOR).forEach((el) => el.remove()); + return `${nhm.translate(root.toString()).trim()}\n`; } diff --git a/sandbox/src/templates/llms.md b/sandbox/src/templates/llms.md deleted file mode 100644 index 7fabf1b..0000000 --- a/sandbox/src/templates/llms.md +++ /dev/null @@ -1,130 +0,0 @@ -# Apify AI Sandbox - -Containerized sandbox environment for AI coding operations. Connect through HTTP, MCP, or the interactive shell. - -## Quick links - -- **Shell terminal**: <%= serverUrl %>/shell/ -- **Health check**: <%= serverUrl %>/health -- **MCP endpoint**: <%= serverUrl %>/mcp - -## Connect with MCP - -```bash -claude mcp add --transport http sandbox <%= serverUrl %>/mcp -``` - -## Core HTTP endpoints - -### Execute commands or code - -**POST** `<%= serverUrl %>/exec` - -Run shell commands or execute code snippets. - -**Shell command:** - -```json -{ - "command": "ls -la", - "cwd": "/sandbox", - "timeoutSecs": 5 -} -``` - -**Code execution:** - -```json -{ - "command": "print('hello')", - "language": "py", - "timeoutSecs": 10 -} -``` - -**Supported languages:** `js`, `javascript`, `ts`, `typescript`, `py`, `python`, `bash`, `sh` (or omit for shell) - -## Filesystem endpoints - -Direct file operations using HTTP methods. All paths relative to `/sandbox`. - -- **GET** `/fs/{path}` - Read file or list directory - - Query: `?download=1` for ZIP download of directories - - Example: `GET /fs/app/log.txt` or `GET /fs/app?download=1` - -- **PUT** `/fs/{path}` - Write/replace file (raw body) - - Example: `PUT /fs/config.json` with JSON body - -- **POST** `/fs/{path}?mkdir=1` - Create directory - - Example: `POST /fs/project/src?mkdir=1` - -- **POST** `/fs/{path}?append=1` - Append to file (raw body) - - Example: `POST /fs/log.txt?append=1` with text body - -- **DELETE** `/fs/{path}` - Delete file or directory - - Query: `?recursive=1` for directories - - Example: `DELETE /fs/temp?recursive=1` - -- **HEAD** `/fs/{path}` - Get file metadata (headers only) - - Returns: Content-Type, Content-Length, Last-Modified, etc. - - Example: `HEAD /fs/data.json` - -## Code examples - -### TypeScript/Node.js - -```typescript -const baseUrl = '<%= serverUrl %>'; -const res = await fetch(`${baseUrl}/exec`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - command: 'print("hello")', - language: 'py', - timeoutSecs: 10, - }), -}); -const json = await res.json(); -console.log(json); -``` - -### Python - -```python -import requests - -base_url = "<%= serverUrl %>" -payload = { - "command": "print('hello')", - "language": "py", - "timeoutSecs": 10 -} -resp = requests.post(f"{base_url}/exec", json=payload, timeout=15) -resp.raise_for_status() -print(resp.json()) -``` - -## Response format - -All `/exec` requests return: - -```json -{ - "stdout": "string", - "stderr": "string", - "exitCode": 0, - "language": "shell|js|ts|py" -} -``` - -## Working directories - -- Shell commands: `/sandbox` (default) -- JavaScript/TypeScript: `/sandbox/js-ts` (default) -- Python: `/sandbox/py` (default) -- Override with `cwd` parameter (must be within `/sandbox`) - -## Configuration - -- **Idle Timeout**: The container automatically shuts down after inactivity (default 10m). -- **Execution Timeout**: Recommended to set to 0 (infinite) on the platform; use the `idleTimeoutSeconds` input to control lifecycle. diff --git a/sandbox/src/templates/shell.ts b/sandbox/src/templates/shell.ts index 16f5f43..2814481 100644 --- a/sandbox/src/templates/shell.ts +++ b/sandbox/src/templates/shell.ts @@ -13,42 +13,28 @@ export const WELCOME_SCRIPT = `#!/bin/bash GREEN='\\033[0;32m' BLUE='\\033[0;34m' ORANGE='\\033[38;5;208m' +WHITE='\\033[0;97m' NC='\\033[0m' # No Color BOLD='\\033[1m' -# Print ASCII Art -echo -e "\${GREEN} *+++++++++++++++++++++ \${BLUE}**********************\${NC}" -echo -e "\${GREEN} *++++++++++++++++++++ \${BLUE}*********************\${NC}" -echo -e "\${GREEN} *+++++++++++++++++++ \${BLUE}********************\${NC}" -echo -e "\${GREEN} *++++++++++++++++++ \${BLUE}*******************\${NC}" -echo -e "\${GREEN} *++++++++++++++++ \${BLUE}*****************\${NC}" -echo -e "\${GREEN} *+++++++++++++++ \${BLUE}****************\${NC}" -echo -e "\${GREEN} *++++++++++++++ \${BLUE}***************\${NC}" -echo -e "\${GREEN} *+++++++++++++ \${BLUE}**************\${NC}" -echo -e "\${GREEN} *++++++++++++ \${BLUE}*************\${NC}" -echo -e "\${GREEN} *+++++++++++ \${BLUE}************\${NC}" -echo -e "\${GREEN} *+++++++++ \${BLUE}**********\${NC}" -echo -e "\${GREEN} *++++++++ \${BLUE}*********\${NC}" -echo -e "\${GREEN} *+++++++ \${BLUE}********\${NC}" -echo -e " \${GREEN}*++++++ \${ORANGE}+\${BLUE} *******" -echo -e " \${GREEN}*+++++ \${ORANGE}++++\${BLUE} ******" -echo -e " \${GREEN}*+++ \${ORANGE}++++++++\${BLUE} ****" -echo -e " \${GREEN}*++ \${ORANGE}++++++++++++\${BLUE} ***" -echo -e " \${GREEN}++ \${ORANGE}+++++++++++++++\${BLUE} **" -echo -e "\${ORANGE} +++++++++++++++++++ " -echo -e " ++++++++++++++++++++++ " -echo -e " ++++++++++++++++++++++++++ " -echo -e " ++++++++++++++++++++++++++++++ " -echo -e " +++++++++++++++++++++++++++++++++ " -echo -e " ++++++++++++++++++++++++++++++++++++ " -echo -e " ++++++++++++++++++++++++++++++++++++++++ " -echo -e " ++++++++++++++++++++++++++++++++++++++++++++ " -echo -e " +++++++++++++++++++++++++++++++++++++++++++++++\${NC}" +# Print ASCII Art (Apify logo) +echo -e "\${GREEN}GGGGGGGGGGG.\${NC} \${BLUE}BBBBBBBBBBb\${NC} \${WHITE}++++\${NC} \${WHITE}.++###+#\${NC}" +echo -e "\${GREEN}GGGGGGGGGg\${NC} \${BLUE}.BBBBBBBBBb\${NC} \${WHITE}####\${NC} \${WHITE}#########\${NC}" +echo -e "\${GREEN}GGGGGGGGg\${NC} \${BLUE}.BBBBBBBb\${NC} \${WHITE}..\${NC} \${WHITE}###\${NC}" +echo -e "\${GREEN}GGGGGGG.\${NC} \${BLUE}bBBBBBBb\${NC} \${WHITE}+#######+\${NC} \${WHITE}.##+.+#####+\${NC} \${WHITE}+##+\${NC} \${WHITE}.############+\${NC} \${WHITE}+##+\${NC}" +echo -e "\${GREEN}GGGGGG\${NC} \${BLUE}bBBBBb\${NC} \${WHITE}+##+.\${NC} \${WHITE}+###.\${NC} \${WHITE}+#####+++####.\${NC} \${WHITE}+###\${NC} \${WHITE}.+++###++++###+\${NC} \${WHITE}###+\${NC}" +echo -e "\${GREEN}GGGG.\${NC} \${BLUE}bBBBb\${NC} \${WHITE}.++++++####\${NC} \${WHITE}+###\${NC} \${WHITE}.###.\${NC}\${WHITE}+##+\${NC} \${WHITE}###\${NC} \${WHITE}####\${NC} \${WHITE}.###\${NC}" +echo -e "\${GREEN}GGG\${NC} \${ORANGE}.OOo\${NC} \${BLUE}bBb\${NC} \${WHITE}####+++++###\${NC} \${WHITE}+###\${NC} \${WHITE}###.\${NC}\${WHITE}+##+\${NC} \${WHITE}###\${NC} \${WHITE}###\${NC} \${WHITE}+###\${NC}" +echo -e "\${GREEN}Gg\${NC} \${ORANGE}oOOOOOOo\${NC} \${BLUE}Bb\${NC} \${WHITE}.###\${NC} \${WHITE}.####\${NC} \${WHITE}+####+\${NC} \${WHITE}+###+\${NC} \${WHITE}+##+\${NC} \${WHITE}###\${NC} \${WHITE}.#####+\${NC}" +echo -e " \${ORANGE}oOOOOOOOOOO.\${NC} \${WHITE}.#######+####\${NC}\${WHITE}+###+#######.\${NC} \${WHITE}+##+\${NC} \${WHITE}###\${NC} \${WHITE}####\${NC}" +echo -e " \${ORANGE}.OOOOOOOOOOOOOOo\${NC} \${WHITE}..\${NC} \${WHITE}+###\${NC} \${WHITE}..\${NC} \${WHITE}###.\${NC}" +echo -e " \${ORANGE}oOOOOOOOOOOOOOOOOOOo.\${NC} \${WHITE}+###\${NC} \${WHITE}.###.\${NC}" +echo -e " \${ORANGE}oOOOOOOOOOOOOOOOOOOOOOO.\${NC} \${WHITE}.+++\${NC} \${WHITE}+++\${NC}" echo "" -echo -e "\${BOLD}Welcome to Apify AI Sandbox!\${NC}" +echo -e "\${BOLD}Welcome to Apify AI Code Sandbox Actor!\${NC}" echo "" -echo -e "\${GREEN}System Info:\${NC}" +echo -e "\${GREEN}System info:\${NC}" # Read versions from cached files (fallback to runtime check if not found) VERSION_DIR="/app/.versions" @@ -56,25 +42,33 @@ NODE_VER=\$(cat "\$VERSION_DIR/node.txt" 2>/dev/null || node -v 2>/dev/null || e PYTHON_VER=\$(cat "\$VERSION_DIR/python.txt" 2>/dev/null || python3 --version 2>&1 || echo 'not installed') APIFY_VER=\$(cat "\$VERSION_DIR/apify.txt" 2>/dev/null || apify --version 2>/dev/null || echo 'not installed') MCPC_VER=\$(cat "\$VERSION_DIR/mcpc.txt" 2>/dev/null || mcpc --version 2>/dev/null || echo 'not installed') -CLAUDE_VER=\$(cat "\$VERSION_DIR/claude.txt" 2>/dev/null || claude --version 2>/dev/null || echo 'not installed') +CLAUDE_CODE_VER=\$(cat "\$VERSION_DIR/claude.txt" 2>/dev/null || claude --version 2>/dev/null || echo 'not installed') OPENCODE_VER=\$(cat "\$VERSION_DIR/opencode.txt" 2>/dev/null || opencode --version 2>/dev/null || echo 'not installed') +CODEX_VER=\$(cat "\$VERSION_DIR/codex.txt" 2>/dev/null || codex --version 2>/dev/null || echo 'not installed') -echo -e " - Node.js: \$NODE_VER" -echo -e " - Python: \$PYTHON_VER" -echo -e " - Apify CLI: \$APIFY_VER" -echo -e " - MCP CLI: \$MCPC_VER (https://github.com/apify/mcp-cli)" -echo -e " - Claude: \$CLAUDE_VER" -echo -e " - OpenCode: \$OPENCODE_VER" -echo -e " - CWD: \$(pwd)" +echo -e " - Node.js: \$NODE_VER" +echo -e " - Python: \$PYTHON_VER" if [ -n "\$VIRTUAL_ENV" ]; then - echo -e " - Venv: Active (\$VIRTUAL_ENV)" + echo -e " - Venv: Active (\$VIRTUAL_ENV)" fi +echo -e " - Apify CLI: \$APIFY_VER" +echo -e " - mcpc: \$MCPC_VER (https://github.com/apify/mcpc)" +echo -e " - Claude Code: \$CLAUDE_CODE_VER" +echo -e " - Codex CLI: \$CODEX_VER" +echo -e " - OpenCode: \$OPENCODE_VER" +echo -e " - LLM API: https://apify.com/apify/openrouter" +echo -e " - Working dir: \$(pwd)" + echo "" -echo -e "\${BLUE}Documentation:\${NC}" -echo -e " - Homepage: https://apify.com/apify/ai-sandbox" -echo -e " - Git repo: https://github.com/apify/actor-ai-sandbox" -echo -e " - AI tools: Claude Code & OpenCode configured with Apify OpenRouter" -echo -e " - Apify OpenRouter: https://apify.com/apify/openrouter" +echo -e "\${BLUE}Links:\${NC}" +if [ -n "\$ACTOR_WEB_SERVER_URL" ]; then + echo -e " - Sandbox home: \$ACTOR_WEB_SERVER_URL" +fi +if [ -n "\$ACTOR_RUN_ID" ]; then + echo -e " - Actor run: https://console.apify.com/view/runs/\$ACTOR_RUN_ID" +fi +echo -e " - Actor page: https://apify.com/apify/ai-code-sandbox" +echo -e " - Git repo: https://github.com/apify/actor-ai-code-sandbox" echo "" `; @@ -104,8 +98,113 @@ alias ll='ls -alF' alias la='ls -A' alias l='ls -CF' -# Print welcome message -if [ -f /app/welcome.sh ]; then +# Claude Code, OpenCode, and Codex are installed on first use โ€” the image ships +# only their config, not the CLIs. agent-launchers.sh defines a function per +# agent that installs it (if missing) then runs it, so the landing-page launch +# buttons and a plain \`claude\`/\`opencode\`/\`codex\` share one install-then-run path. +[ -f /app/agent-launchers.sh ] && . /app/agent-launchers.sh + +# AI coding agents auto-approve all confirmations โ€” safe inside the sandbox. +# Claude Code: bypass mode + prompt suppression are baked into the image via +# settings.json (see the Dockerfile). Codex and OpenCode auto-approve via their +# own config files. + +# Print welcome message (once per session; the launch wrapper sources this +# rcfile twice โ€” to set up the env, then again for the persistent shell). +if [ -z "$SANDBOX_WELCOME_SHOWN" ] && [ -f /app/welcome.sh ]; then + export SANDBOX_WELCOME_SHOWN=1 bash /app/welcome.sh fi `; + +/** + * Message shown when the terminal WebSocket drops but a reconnect may still work + * (replaces ttyd's bare "Press โŽ to Reconnect"). + */ +export const TERMINAL_DISCONNECT_MESSAGE = 'Connection lost โ€” press โŽ to reconnect'; + +/** + * Message shown when a reconnect attempt fails โ€” the Actor run has most likely + * stopped (idle timeout, abort, run timeout, migration). Pressing โŽ still retries. + */ +export const TERMINAL_FINISHED_MESSAGE = 'Actor run probably finished โ€” press โŽ to retry'; + +/** + * Browser script injected into ttyd's terminal page to explain *why* the session + * ended, client-side. + * + * Why client-side: the terminal is proxied (browser โ†” Actor โ†” ttyd), and most + * stops (run timeout, hard abort, platform scale-down) kill the container with a + * signal โ€” there is no advance Actor event and no time to flush a banner before + * the process dies. The browser, however, can always see the socket drop and any + * failed reconnect, so we relabel ttyd's own reconnect overlay here. + * + * ttyd (1.7.7) drives a single overlay
via `overlayAddon.showOverlay(text)` + * (html/src/components/terminal/xterm/index.ts). On a drop it shows + * "Press โŽ to Reconnect"; a retry shows "Reconnecting..."; a fresh connection + * shows "Reconnected". We watch those exact strings: the first prompt means the + * link dropped (recoverable), but a prompt that follows a "Reconnecting..." means + * the retry failed โ€” so the Actor is probably gone. The relabel is cosmetic; + * ttyd's Enter-to-reconnect handler stays intact, so retry still works. + */ +export const RECONNECT_OVERLAY_SCRIPT = `(function () { + var LOST = ${JSON.stringify(TERMINAL_DISCONNECT_MESSAGE)}; + var FINISHED = ${JSON.stringify(TERMINAL_FINISHED_MESSAGE)}; + + // ttyd's exact overlay strings; \\u23ce is the โŽ glyph it uses. + var PRESS_ENTER = 'Press \\u23ce to Reconnect'; + var RECONNECTING = 'Reconnecting...'; + var RECONNECTED = 'Reconnected'; + + // True once a reconnect has been attempted since the last live connection. If + // we then fall back to the reconnect prompt, the attempt failed. + var triedReconnect = false; + + function relabel(el) { + var text = el.textContent; + if (text === RECONNECTING) { + triedReconnect = true; + } else if (text === RECONNECTED) { + triedReconnect = false; + } else if (text === PRESS_ENTER) { + var msg = triedReconnect ? FINISHED : LOST; + triedReconnect = false; + if (el.textContent !== msg) el.textContent = msg; + } + } + + // The overlay text is set via textContent, so a change shows up as a childList + // mutation on the overlay element (or as the element being (re)attached). + function elementOf(node) { + if (!node) return null; + return node.nodeType === 3 ? node.parentNode : node; + } + + var observer = new MutationObserver(function (records) { + for (var i = 0; i < records.length; i++) { + var r = records[i]; + var target = elementOf(r.target); + if (target && target.nodeType === 1) relabel(target); + for (var j = 0; j < r.addedNodes.length; j++) { + var added = elementOf(r.addedNodes[j]); + if (added && added.nodeType === 1) relabel(added); + } + } + }); + observer.observe(document.documentElement, { childList: true, subtree: true }); +})();`; + +/** + * Inject the reconnect-overlay script into ttyd's HTML page. The script only + * reacts to overlay text that appears long after load, so placement isn't + * critical โ€” prefer right after , falling back to , then end of doc. + * + * @param html - The HTML document served by ttyd. + * @returns The HTML with the script injected. + */ +export const injectTerminalReconnectScript = (html: string): string => { + const tag = ``; + if (/]*>/i.test(html)) return html.replace(/]*>/i, (match) => match + tag); + if (/]*>/i.test(html)) return html.replace(/]*>/i, (match) => match + tag); + return html + tag; +}; diff --git a/sandbox/src/ttyd.ts b/sandbox/src/ttyd.ts new file mode 100644 index 0000000..f832f3d --- /dev/null +++ b/sandbox/src/ttyd.ts @@ -0,0 +1,65 @@ +/** + * Supervision helpers for the ttyd process that backs the interactive /shell + * terminal. The process is spawned and restarted in main.ts; the pure decision + * logic lives here so it can be unit-tested without spawning a real process. + * + * Why this exists: ttyd used to be spawned with `stdio: 'ignore'` and restarted + * on a fixed 5s timer, so a startup failure (e.g. a missing shared library โ€” + * the libuv regression that broke the shell) surfaced only as a repeating + * "ttyd process exited {code:1}" with no reason, while the /shell proxy returned + * an opaque 500 "Shell Proxy Error". These helpers support capturing ttyd's + * output, backing off between restarts, and telling the user why the shell is + * down. + */ + +/** Restart backoff bounds (ms). */ +export const TTYD_RESTART_MIN_MS = 1000; +export const TTYD_RESTART_MAX_MS = 30000; + +/** + * An exit within this window of startup means ttyd never really came up (bad + * args, missing shared library, port already in use) rather than a long-lived + * server that later died. We back off and log loudly in that case. + */ +export const TTYD_CRASH_WINDOW_MS = 2000; + +/** How many characters of ttyd's recent stdout/stderr to retain for diagnostics. */ +export const TTYD_OUTPUT_LIMIT = 2048; + +/** True if ttyd exited fast enough to count as a startup crash rather than a normal exit. */ +export const isTtydStartupCrash = (aliveMs: number): boolean => aliveMs < TTYD_CRASH_WINDOW_MS; + +/** + * The delay to use the next time ttyd needs restarting. Startup crashes back off + * exponentially up to the cap, so a permanently broken ttyd doesn't spam the log; + * a process that ran for a while before exiting resets to the minimum so it + * recovers promptly. + */ +export const nextTtydRestartDelayMs = (currentDelayMs: number, isCrash: boolean): number => { + if (!isCrash) return TTYD_RESTART_MIN_MS; + const doubled = Math.max(currentDelayMs, TTYD_RESTART_MIN_MS) * 2; + return Math.min(doubled, TTYD_RESTART_MAX_MS); +}; + +/** Append a chunk to the rolling output buffer, keeping only the last `limit` characters. */ +export const appendTtydOutput = (buffer: string, chunk: string, limit = TTYD_OUTPUT_LIMIT): string => + (buffer + chunk).slice(-limit); + +/** + * Plain-text body returned by the /shell proxy when ttyd isn't reachable. Includes + * ttyd's last output when we have it, so the cause (e.g. a missing shared library) + * is visible in the browser instead of an opaque error. + */ +export const buildShellUnavailableMessage = (lastTtydOutput: string): string => { + const lines = ['Interactive shell is not available โ€” the terminal backend (ttyd) is not running.']; + const detail = lastTtydOutput.trim(); + if (detail) { + lines.push('', 'Last ttyd output:', detail); + } + lines.push( + '', + 'The sandbox keeps trying to restart it โ€” reload this page in a few seconds.', + 'If the problem persists, check the Actor run log.', + ); + return `${lines.join('\n')}\n`; +}; diff --git a/sandbox/src/types.ts b/sandbox/src/types.ts index 7fae7a9..b0df6e1 100644 --- a/sandbox/src/types.ts +++ b/sandbox/src/types.ts @@ -1,45 +1,46 @@ /** - * Type definitions for the Apify AI Sandbox Actor + * Type definitions for the Apify AI Code Sandbox Actor */ /** - * Proxy mapping configuration for routing requests to local servers + * A bridge exposes a local server (running inside the sandbox) at a public + * URL path on the container. */ -export interface ProxyMapping { +export interface Bridge { /** Exposed URL path on the container (e.g., /openclaw) */ path: string; - /** Full URL of the local service to proxy to (e.g., http://127.0.0.1:18789/openclaw) */ + /** Full URL of the local service to forward to (e.g., http://127.0.0.1:18789/openclaw) */ target: string; } export interface ActorInput { /** - * Skill packages to install for the AI coding agent - * Skills are SKILL.md files that provide specialized instructions - * Format: array of skill package names or URLs - * Example: ["apify/agent-skills"] + * Skill packages to install for the AI coding agent (SKILLS.md files). + * Accepts one skill per line โ€” a GitHub `owner/repo` (e.g. `anthropics/skills`) + * or repo URL (e.g. `https://github.com/anthropics/skills`); blank lines and + * `#` comments ignored โ€” or a JSON array of skill name strings. */ - skills?: string[]; + agentSkills?: string; /** - * Node.js dependencies object for JavaScript and TypeScript code execution - * Format: { "package-name": "version", ... } - * Example: { "zod": "^3.0", "axios": "latest" } + * Node.js dependencies for JavaScript and TypeScript code execution. + * Accepts either npm CLI-style lines (one `package@version` per line, missing + * `@version` defaults to `latest`) or a JSON object (`{ "pkg": "version" }`). */ - nodeDependencies?: Record; + nodeDependencies?: string; /** * Python requirements in requirements.txt format for Python code execution * Format: one package per line with optional version specifiers * Example: "requests==2.31.0\npandas>=2.0.0\nnumpy" */ - pythonRequirementsTxt?: string; + pythonRequirements?: string; /** * Optional bash script to customize the sandbox environment * Runs after dependency installation in /sandbox directory */ - initShellScript?: string; + initBashScript?: string; /** * Secret environment variables exposed to the sandbox shell, init script, @@ -51,14 +52,22 @@ export interface ActorInput { /** * Graceful shutdown timeout in seconds if no activity is detected. * Activity includes HTTP requests and shell interaction. - * @default 600 (10 minutes) + * @default 900 (15 minutes) */ - idleTimeoutSeconds?: number; + idleTimeoutSecs?: number; /** - * Proxy mappings for routing requests to local servers - * Maps exposed paths to local service URLs + * Bridges exposing local servers at public URL paths on the container. + * Maps exposed paths to local service URLs. * Example: [{ "path": "/openclaw", "target": "http://127.0.0.1:18789/openclaw" }] */ - proxyMappings?: ProxyMapping[]; + bridges?: Bridge[]; + + /** + * MCP Connector IDs the Actor can use. At runtime the platform exposes + * each Connector as a proxy at `${APIFY_MCP_PROXY_URL}/`, + * and the sandbox writes the list to `/sandbox/mcp.json` on startup + * so tools like `mcpc connect` can pick them up. + */ + mcpConnectors?: string[]; } diff --git a/sandbox/tests/e2e.ts b/sandbox/tests/e2e.ts index dd69d62..e29b8c8 100644 --- a/sandbox/tests/e2e.ts +++ b/sandbox/tests/e2e.ts @@ -1,5 +1,5 @@ /** - * E2E Platform Test for Apify AI Sandbox Actor + * E2E Platform Test for Apify AI Code Sandbox Actor * * This script: * 1. Deploys and starts the Actor on Apify platform @@ -141,7 +141,7 @@ async function waitForHealth(runId: string, timeoutSeconds = 180): Promise { - setTimeout(resolve, pollInterval); + setTimeout(resolve, pollIntervalMs); }); } @@ -433,7 +433,7 @@ async function testFsEndpoint( // ============================================================================ async function runAllTests(baseUrl: string): Promise { - console.log(`\n${colors.blue}Testing Apify AI Sandbox REST Endpoints${colors.reset}`); + console.log(`\n${colors.blue}Testing Apify AI Code Sandbox REST Endpoints${colors.reset}`); console.log(`Base URL: ${baseUrl}\n`); // Health check @@ -922,7 +922,7 @@ async function runAllTests(baseUrl: string): Promise { async function main(): Promise { console.log('==================================='); - console.log('๐Ÿš€ Apify AI Sandbox E2E Platform Test'); + console.log('๐Ÿš€ Apify AI Code Sandbox E2E Platform Test'); console.log('===================================\n'); let runId: string | null = null; @@ -933,12 +933,10 @@ async function main(): Promise { console.log(`${colors.green}โ„น${colors.reset} Step 1: Preparing Actor input with dependencies...`); const input = { - nodeDependencies: { - zod: '^3.22.0', - }, - pythonRequirementsTxt: 'numpy>=1.24.0', + nodeDependencies: 'zod@^3.22.0', + pythonRequirements: 'numpy>=1.24.0', envVars: 'TEST_E2E_SECRET=hunter2-do-not-leak', - initShellScript: [ + initBashScript: [ '#!/bin/bash', 'mkdir -p /sandbox/test-e2e-init', "echo 'E2E test init script executed' > /sandbox/test-e2e-init/status.txt", diff --git a/sandbox/tests/unit/mcp-agent-config.test.ts b/sandbox/tests/unit/mcp-agent-config.test.ts new file mode 100644 index 0000000..13f7bc9 --- /dev/null +++ b/sandbox/tests/unit/mcp-agent-config.test.ts @@ -0,0 +1,145 @@ +/* eslint-disable @typescript-eslint/no-floating-promises -- node:test's describe/it return promises by design */ +/* eslint-disable no-template-curly-in-string -- ${APIFY_TOKEN} is a literal placeholder from mcp.json, not a JS template expression */ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { + applyCodexConfig, + buildClaudeMcpServers, + buildCodexBlock, + buildOpenCodeMcp, + mergeClaudeConfig, + mergeOpenCodeConfig, + toOpenCodeEnvSyntax, +} from '../../src/mcp-agent-config.js'; +import type { McpConfig } from '../../src/mcp-connections.js'; + +const CONFIG: McpConfig = { + mcpServers: { + conn_abc123: { + url: 'https://api.apify.com/v2/mcp-proxy/conn_abc123', + headers: { Authorization: 'Bearer ${APIFY_TOKEN}' }, + }, + 'conn-def': { + url: 'https://api.apify.com/v2/mcp-proxy/conn-def', + headers: { Authorization: 'Bearer ${APIFY_TOKEN}' }, + }, + }, +}; + +describe('buildClaudeMcpServers', () => { + it('adds type:http and keeps url + headers verbatim (Claude expands ${VAR} itself)', () => { + const servers = buildClaudeMcpServers(CONFIG); + assert.deepEqual(servers.conn_abc123, { + type: 'http', + url: 'https://api.apify.com/v2/mcp-proxy/conn_abc123', + headers: { Authorization: 'Bearer ${APIFY_TOKEN}' }, + }); + }); + + it('produces one entry per connector', () => { + assert.deepEqual(Object.keys(buildClaudeMcpServers(CONFIG)), ['conn_abc123', 'conn-def']); + }); +}); + +describe('mergeClaudeConfig', () => { + it('preserves existing keys like hasCompletedOnboarding', () => { + const merged = mergeClaudeConfig({ hasCompletedOnboarding: true }, CONFIG); + assert.equal(merged.hasCompletedOnboarding, true); + assert.ok((merged.mcpServers as Record).conn_abc123); + }); + + it('is idempotent โ€” re-merging yields the same servers', () => { + const once = mergeClaudeConfig({ hasCompletedOnboarding: true }, CONFIG); + const twice = mergeClaudeConfig(once, CONFIG); + assert.deepEqual(twice, once); + }); +}); + +describe('toOpenCodeEnvSyntax', () => { + it('rewrites ${VAR} into {env:VAR}', () => { + assert.equal(toOpenCodeEnvSyntax('Bearer ${APIFY_TOKEN}'), 'Bearer {env:APIFY_TOKEN}'); + }); + + it('leaves strings without ${VAR} unchanged', () => { + assert.equal(toOpenCodeEnvSyntax('Bearer static-token'), 'Bearer static-token'); + }); +}); + +describe('buildOpenCodeMcp', () => { + it('builds remote servers with oauth:false and {env:VAR} headers', () => { + const mcp = buildOpenCodeMcp(CONFIG); + assert.deepEqual(mcp.conn_abc123, { + type: 'remote', + url: 'https://api.apify.com/v2/mcp-proxy/conn_abc123', + enabled: true, + oauth: false, + headers: { Authorization: 'Bearer {env:APIFY_TOKEN}' }, + }); + }); +}); + +describe('mergeOpenCodeConfig', () => { + it('preserves provider/model config and adds the mcp block', () => { + const merged = mergeOpenCodeConfig( + { model: 'apify-openrouter/anthropic/claude-sonnet-4.5', provider: { x: 1 } }, + CONFIG, + ); + assert.equal(merged.model, 'apify-openrouter/anthropic/claude-sonnet-4.5'); + assert.deepEqual(merged.provider, { x: 1 }); + assert.ok((merged.mcp as Record)['conn-def']); + }); +}); + +describe('buildCodexBlock', () => { + it('emits one [mcp_servers.] table per connector with url + bearer_token_env_var', () => { + const block = buildCodexBlock(CONFIG); + assert.match(block, /\[mcp_servers\.conn_abc123\]/); + assert.match(block, /\[mcp_servers\.conn-def\]/); + assert.match(block, /url = "https:\/\/api\.apify\.com\/v2\/mcp-proxy\/conn_abc123"/); + // The literal ${APIFY_TOKEN} must NOT leak into TOML โ€” Codex resolves the + // env var by name instead. + assert.match(block, /bearer_token_env_var = "APIFY_TOKEN"/); + assert.doesNotMatch(block, /\$\{APIFY_TOKEN\}/); + }); + + it('is wrapped in the auto-generated markers', () => { + const block = buildCodexBlock(CONFIG); + assert.match(block, /# >>> apify mcp connectors \(auto-generated\) >>>/); + assert.match(block, /# <<< apify mcp connectors \(auto-generated\) << { + const BASE = 'approval_policy = "never"\nsandbox_mode = "danger-full-access"\n'; + + it('preserves the base config and prepends the rmcp flag before any table', () => { + const out = applyCodexConfig(BASE, CONFIG); + assert.match(out, /approval_policy = "never"/); + assert.match(out, /sandbox_mode = "danger-full-access"/); + // The flag is a bare top-level key, so it must appear before the first table. + const flagIdx = out.indexOf('experimental_use_rmcp_client'); + const tableIdx = out.indexOf('[mcp_servers.'); + assert.ok(flagIdx >= 0 && flagIdx < tableIdx, 'rmcp flag must precede the first [mcp_servers] table'); + }); + + it('does not duplicate the rmcp flag when it is already present', () => { + const withFlag = `experimental_use_rmcp_client = true\n${BASE}`; + const out = applyCodexConfig(withFlag, CONFIG); + assert.equal(out.match(/experimental_use_rmcp_client/g)?.length, 1); + }); + + it('replaces a prior generated block instead of appending a second one (idempotent)', () => { + const once = applyCodexConfig(BASE, CONFIG); + const twice = applyCodexConfig(once, CONFIG); + assert.equal(twice, once); + assert.equal(twice.match(/# >>> apify mcp connectors/g)?.length, 1); + }); + + it('reflects a changed connector list on a re-run', () => { + const first = applyCodexConfig(BASE, CONFIG); + const second = applyCodexConfig(first, { mcpServers: { only: CONFIG.mcpServers.conn_abc123 } }); + assert.match(second, /\[mcp_servers\.only\]/); + assert.doesNotMatch(second, /\[mcp_servers\.conn-def\]/); + }); +}); diff --git a/sandbox/tests/unit/mcp-connections.test.ts b/sandbox/tests/unit/mcp-connections.test.ts new file mode 100644 index 0000000..e6e5b87 --- /dev/null +++ b/sandbox/tests/unit/mcp-connections.test.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-floating-promises -- node:test's describe/it return promises by design */ +/* eslint-disable no-template-curly-in-string -- ${APIFY_TOKEN} is a literal placeholder we write to mcp.json, not a JS template expression */ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { APIFY_MCP_URL, buildMcpConfig } from '../../src/mcp-connections.js'; + +const PROXY = 'https://api.apify.com/v2/mcp-proxy'; + +/** The Apify MCP server is always seeded into the config under the `apify` key. */ +const APIFY_ENTRY = { url: APIFY_MCP_URL, headers: { Authorization: 'Bearer ${APIFY_TOKEN}' } }; + +describe('buildMcpConfig', () => { + describe('always-on Apify MCP server', () => { + it('seeds the Apify MCP server even with no connectors', () => { + assert.deepEqual(buildMcpConfig(undefined, PROXY), { mcpServers: { apify: APIFY_ENTRY } }); + assert.deepEqual(buildMcpConfig([], PROXY), { mcpServers: { apify: APIFY_ENTRY } }); + }); + + it('points the Apify entry at https://mcp.apify.com with the APIFY_TOKEN bearer', () => { + const { apify } = buildMcpConfig(['conn_abc'], PROXY).mcpServers; + assert.equal(apify.url, 'https://mcp.apify.com'); + assert.equal(apify.headers.Authorization, 'Bearer ${APIFY_TOKEN}'); + }); + }); + + describe('empty / nullish input', () => { + it('returns only the Apify server for an empty array', () => { + assert.deepEqual(Object.keys(buildMcpConfig([], PROXY).mcpServers), ['apify']); + }); + + it('skips blank / non-string entries', () => { + // @ts-expect-error - intentionally passing non-string to exercise runtime guard + const config = buildMcpConfig(['', ' ', null, 42, 'conn_ok'], PROXY); + assert.deepEqual(Object.keys(config.mcpServers), ['apify', 'conn_ok']); + }); + }); + + describe('valid Connector IDs', () => { + it('builds one server entry per Connector ID (alongside the Apify server)', () => { + const config = buildMcpConfig(['conn_abc123', 'conn_def456'], PROXY); + assert.deepEqual(config, { + mcpServers: { + apify: APIFY_ENTRY, + conn_abc123: { + url: `${PROXY}/conn_abc123`, + headers: { Authorization: 'Bearer ${APIFY_TOKEN}' }, + }, + conn_def456: { + url: `${PROXY}/conn_def456`, + headers: { Authorization: 'Bearer ${APIFY_TOKEN}' }, + }, + }, + }); + }); + + it('uses ${APIFY_TOKEN} placeholder in Authorization header', () => { + const config = buildMcpConfig(['conn_abc'], PROXY); + assert.equal(config.mcpServers.conn_abc.headers.Authorization, 'Bearer ${APIFY_TOKEN}'); + }); + + it('strips trailing slashes from the proxy base URL', () => { + const config = buildMcpConfig(['conn_abc'], `${PROXY}//`); + assert.equal(config.mcpServers.conn_abc.url, `${PROXY}/conn_abc`); + }); + + it('trims whitespace around Connector IDs', () => { + const config = buildMcpConfig([' conn_abc '], PROXY); + assert.deepEqual(Object.keys(config.mcpServers), ['apify', 'conn_abc']); + assert.equal(config.mcpServers.conn_abc.url, `${PROXY}/conn_abc`); + }); + }); + + describe('proxy URL handling', () => { + it('falls back to an empty base when proxy URL is undefined', () => { + const config = buildMcpConfig(['conn_abc'], undefined); + assert.equal(config.mcpServers.conn_abc.url, '/conn_abc'); + }); + + it('falls back to an empty base when proxy URL is empty string', () => { + const config = buildMcpConfig(['conn_abc'], ''); + assert.equal(config.mcpServers.conn_abc.url, '/conn_abc'); + }); + }); + + describe('key sanitization', () => { + it('uses the raw ID as the key when it is safe', () => { + const config = buildMcpConfig(['Conn-123_abc'], PROXY); + assert.ok('Conn-123_abc' in config.mcpServers); + }); + + it('sanitizes IDs that contain unsafe characters', () => { + const config = buildMcpConfig(['conn@abc/xyz'], PROXY); + assert.ok('conn_abc_xyz' in config.mcpServers); + // URL preserves the original ID so the proxy can route correctly + assert.equal(config.mcpServers.conn_abc_xyz.url, `${PROXY}/conn@abc/xyz`); + }); + }); +}); diff --git a/sandbox/tests/unit/node-deps.test.ts b/sandbox/tests/unit/node-deps.test.ts new file mode 100644 index 0000000..97ad0dd --- /dev/null +++ b/sandbox/tests/unit/node-deps.test.ts @@ -0,0 +1,181 @@ +/* eslint-disable @typescript-eslint/no-floating-promises -- node:test's describe/it return promises by design */ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { parseNodeDependencies } from '../../src/node-deps.js'; + +describe('parseNodeDependencies', () => { + describe('empty input', () => { + it('returns {} for undefined', () => { + assert.deepEqual(parseNodeDependencies(undefined), {}); + }); + + it('returns {} for null', () => { + assert.deepEqual(parseNodeDependencies(null), {}); + }); + + it('returns {} for empty string', () => { + assert.deepEqual(parseNodeDependencies(''), {}); + }); + + it('returns {} for whitespace-only input', () => { + assert.deepEqual(parseNodeDependencies(' \n \t \n'), {}); + }); + }); + + describe('npm CLI-style line format', () => { + it('parses a single package@version line', () => { + assert.deepEqual(parseNodeDependencies('zod@^3.0'), { zod: '^3.0' }); + }); + + it('parses multiple packages, one per line', () => { + const input = 'zod@^3.0\naxios@latest\nlodash@4.17.21'; + assert.deepEqual(parseNodeDependencies(input), { + zod: '^3.0', + axios: 'latest', + lodash: '4.17.21', + }); + }); + + it('defaults bare package names to latest', () => { + assert.deepEqual(parseNodeDependencies('lodash'), { lodash: 'latest' }); + }); + + it('defaults bare names to latest in mixed input', () => { + const input = 'zod@^3.0\nlodash\naxios@latest'; + assert.deepEqual(parseNodeDependencies(input), { + zod: '^3.0', + lodash: 'latest', + axios: 'latest', + }); + }); + + it('handles scoped packages without version', () => { + assert.deepEqual(parseNodeDependencies('@types/node'), { '@types/node': 'latest' }); + }); + + it('handles scoped packages with version (splits on last @)', () => { + assert.deepEqual(parseNodeDependencies('@types/node@^20'), { '@types/node': '^20' }); + }); + + it('handles mixed scoped and unscoped packages', () => { + const input = '@types/node@^20\nzod@^3.0\n@apify/sdk'; + assert.deepEqual(parseNodeDependencies(input), { + '@types/node': '^20', + zod: '^3.0', + '@apify/sdk': 'latest', + }); + }); + + it('ignores blank lines', () => { + const input = '\n\nzod@^3.0\n\n\naxios@latest\n\n'; + assert.deepEqual(parseNodeDependencies(input), { + zod: '^3.0', + axios: 'latest', + }); + }); + + it('ignores # comment lines', () => { + const input = '# my deps\nzod@^3.0\n# another comment\naxios@latest'; + assert.deepEqual(parseNodeDependencies(input), { + zod: '^3.0', + axios: 'latest', + }); + }); + + it('trims whitespace around package specs', () => { + const input = ' zod@^3.0 \n\t axios@latest\t'; + assert.deepEqual(parseNodeDependencies(input), { + zod: '^3.0', + axios: 'latest', + }); + }); + + it('handles \\r\\n line endings', () => { + assert.deepEqual(parseNodeDependencies('zod@^3.0\r\naxios@latest'), { + zod: '^3.0', + axios: 'latest', + }); + }); + + it('treats trailing @ as latest', () => { + assert.deepEqual(parseNodeDependencies('zod@'), { zod: 'latest' }); + }); + + it('lets later duplicate entries override earlier ones', () => { + assert.deepEqual(parseNodeDependencies('zod@^3.0\nzod@^4.0'), { zod: '^4.0' }); + }); + }); + + describe('JSON object format', () => { + it('parses a JSON object', () => { + const input = '{"zod": "^3.0", "axios": "latest"}'; + assert.deepEqual(parseNodeDependencies(input), { + zod: '^3.0', + axios: 'latest', + }); + }); + + it('parses pretty-printed JSON', () => { + const input = '{\n "zod": "^3.0",\n "axios": "latest"\n}'; + assert.deepEqual(parseNodeDependencies(input), { + zod: '^3.0', + axios: 'latest', + }); + }); + + it('parses JSON with leading whitespace', () => { + assert.deepEqual(parseNodeDependencies(' {"zod": "^3.0"} '), { zod: '^3.0' }); + }); + + it('coerces numeric versions to strings', () => { + const input = '{"lodash": 4}'; + assert.deepEqual(parseNodeDependencies(input), { lodash: '4' }); + }); + + it('treats null/empty values as latest', () => { + const input = '{"zod": null, "axios": ""}'; + assert.deepEqual(parseNodeDependencies(input), { + zod: 'latest', + axios: 'latest', + }); + }); + + it('skips object/array values', () => { + const input = '{"zod": "^3.0", "bad": {"nested": true}, "alsobad": [1, 2]}'; + assert.deepEqual(parseNodeDependencies(input), { zod: '^3.0' }); + }); + + it('returns {} for malformed JSON', () => { + assert.deepEqual(parseNodeDependencies('{not valid json'), {}); + }); + + it('returns {} for top-level JSON array (must be a flat object)', () => { + // Wrapped in `{}` so it routes to JSON parsing; bare `[...]` would be + // treated as line-format input by design. + assert.deepEqual(parseNodeDependencies('{"deps": ["zod", "axios"]}'), {}); + }); + + it('parses an empty JSON object as {}', () => { + assert.deepEqual(parseNodeDependencies('{}'), {}); + }); + + it('handles scoped packages in JSON', () => { + const input = '{"@types/node": "^20", "@apify/sdk": "latest"}'; + assert.deepEqual(parseNodeDependencies(input), { + '@types/node': '^20', + '@apify/sdk': 'latest', + }); + }); + }); + + describe('format detection', () => { + it('treats input starting with { as JSON', () => { + assert.deepEqual(parseNodeDependencies('{"zod": "^3.0"}'), { zod: '^3.0' }); + }); + + it('treats input not starting with { as line format', () => { + assert.deepEqual(parseNodeDependencies('zod@^3.0'), { zod: '^3.0' }); + }); + }); +}); diff --git a/sandbox/tests/unit/safe-json.test.ts b/sandbox/tests/unit/safe-json.test.ts new file mode 100644 index 0000000..ef79cee --- /dev/null +++ b/sandbox/tests/unit/safe-json.test.ts @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/no-floating-promises -- node:test's describe/it return promises by design */ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { isFlatJsonObject, safeParseJson } from '../../src/safe-json.js'; + +describe('isFlatJsonObject', () => { + it('returns true for an empty object', () => { + assert.equal(isFlatJsonObject({}), true); + }); + + it('returns true for a non-empty object', () => { + assert.equal(isFlatJsonObject({ foo: 1 }), true); + }); + + it('returns true for an object with nested values (only the top-level is checked)', () => { + assert.equal(isFlatJsonObject({ foo: { bar: 1 } }), true); + }); + + it('returns false for an empty array', () => { + assert.equal(isFlatJsonObject([]), false); + }); + + it('returns false for a non-empty array', () => { + assert.equal(isFlatJsonObject([1, 2, 3]), false); + }); + + it('returns false for null', () => { + assert.equal(isFlatJsonObject(null), false); + }); + + it('returns false for primitive values', () => { + assert.equal(isFlatJsonObject('string'), false); + assert.equal(isFlatJsonObject(42), false); + assert.equal(isFlatJsonObject(true), false); + assert.equal(isFlatJsonObject(undefined), false); + }); +}); + +describe('safeParseJson', () => { + const shapeMsg = 'must be a flat object'; + const arrayMsg = 'must be an array'; + + describe('with isFlatJsonObject predicate', () => { + it('returns the parsed object on success', () => { + assert.deepEqual(safeParseJson('{"foo": "bar"}', 'test', isFlatJsonObject, shapeMsg), { foo: 'bar' }); + }); + + it('parses a pretty-printed object', () => { + const input = '{\n "foo": "bar",\n "baz": 1\n}'; + assert.deepEqual(safeParseJson(input, 'test', isFlatJsonObject, shapeMsg), { foo: 'bar', baz: 1 }); + }); + + it('returns null for an array', () => { + assert.equal(safeParseJson('["foo"]', 'test', isFlatJsonObject, shapeMsg), null); + }); + + it('returns null for a JSON null literal', () => { + assert.equal(safeParseJson('null', 'test', isFlatJsonObject, shapeMsg), null); + }); + + it('returns null for a primitive value', () => { + assert.equal(safeParseJson('42', 'test', isFlatJsonObject, shapeMsg), null); + assert.equal(safeParseJson('"hello"', 'test', isFlatJsonObject, shapeMsg), null); + }); + }); + + describe('with Array.isArray predicate', () => { + it('returns the parsed array on success', () => { + assert.deepEqual(safeParseJson('["a", "b"]', 'test', Array.isArray, arrayMsg), ['a', 'b']); + }); + + it('parses an empty array', () => { + assert.deepEqual(safeParseJson('[]', 'test', Array.isArray, arrayMsg), []); + }); + + it('returns null for an object', () => { + assert.equal(safeParseJson('{"foo": 1}', 'test', Array.isArray, arrayMsg), null); + }); + }); + + describe('malformed JSON', () => { + it('returns null for syntactically invalid JSON', () => { + assert.equal(safeParseJson('{not valid json', 'test', isFlatJsonObject, shapeMsg), null); + }); + + it('returns null for an unterminated string', () => { + assert.equal(safeParseJson('"unterminated', 'test', isFlatJsonObject, shapeMsg), null); + }); + + it('returns null for empty input', () => { + assert.equal(safeParseJson('', 'test', isFlatJsonObject, shapeMsg), null); + }); + + it('returns null for whitespace-only input', () => { + assert.equal(safeParseJson(' ', 'test', isFlatJsonObject, shapeMsg), null); + }); + }); + + describe('custom predicates', () => { + it('honors a stricter type guard', () => { + const isStringArray = (v: unknown): v is string[] => + Array.isArray(v) && v.every((x) => typeof x === 'string'); + assert.deepEqual(safeParseJson('["a", "b"]', 'test', isStringArray, 'string[]'), ['a', 'b']); + assert.equal(safeParseJson('[1, 2]', 'test', isStringArray, 'string[]'), null); + }); + }); +}); diff --git a/sandbox/tests/unit/shell-launch.test.ts b/sandbox/tests/unit/shell-launch.test.ts new file mode 100644 index 0000000..d689991 --- /dev/null +++ b/sandbox/tests/unit/shell-launch.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-floating-promises -- node:test's describe/it return promises by design */ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { translateLaunchParam } from '../../src/shell-launch.js'; + +describe('translateLaunchParam', () => { + it('returns path unchanged when no query string', () => { + assert.equal(translateLaunchParam('/'), '/'); + assert.equal(translateLaunchParam('/ws'), '/ws'); + }); + + it('returns path unchanged when launch is absent', () => { + assert.equal(translateLaunchParam('/?arg=-c&arg=foo'), '/?arg=-c&arg=foo'); + }); + + it('runs launch= in a persistent shell, echoing the command and any error', () => { + const out = translateLaunchParam('/?launch=claude'); + assert.equal( + out, + '/?arg=-c&arg=source%20%2Fapp%2Fsandbox_bashrc%3B%20echo%20%22%24%20claude%22%3B%20claude%20%7C%7C%20echo%20%22%5Bcommand%20exited%20with%20status%20%24%3F%5D%22%3B%20exec%20bash%20--rcfile%20%2Fapp%2Fsandbox_bashrc', + ); + }); + + it('handles commands with spaces and special chars', () => { + const out = translateLaunchParam('/?launch=opencode%20tui'); + assert.equal( + out, + '/?arg=-c&arg=source%20%2Fapp%2Fsandbox_bashrc%3B%20echo%20%22%24%20opencode%20tui%22%3B%20opencode%20tui%20%7C%7C%20echo%20%22%5Bcommand%20exited%20with%20status%20%24%3F%5D%22%3B%20exec%20bash%20--rcfile%20%2Fapp%2Fsandbox_bashrc', + ); + }); + + it('preserves other query params (after the injected args)', () => { + const out = translateLaunchParam('/ws?launch=claude&token=abc'); + assert.equal( + out, + '/ws?arg=-c&arg=source%20%2Fapp%2Fsandbox_bashrc%3B%20echo%20%22%24%20claude%22%3B%20claude%20%7C%7C%20echo%20%22%5Bcommand%20exited%20with%20status%20%24%3F%5D%22%3B%20exec%20bash%20--rcfile%20%2Fapp%2Fsandbox_bashrc&token=abc', + ); + }); + + it('opens a persistent interactive shell when launch is empty', () => { + const out = translateLaunchParam('/?launch='); + assert.equal(out, '/?arg=-c&arg=exec%20bash%20--rcfile%20%2Fapp%2Fsandbox_bashrc'); + }); +}); diff --git a/sandbox/tests/unit/shell.test.ts b/sandbox/tests/unit/shell.test.ts new file mode 100644 index 0000000..1855c25 --- /dev/null +++ b/sandbox/tests/unit/shell.test.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-floating-promises -- node:test's describe/it return promises by design */ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { + injectTerminalReconnectScript, + RECONNECT_OVERLAY_SCRIPT, + TERMINAL_DISCONNECT_MESSAGE, + TERMINAL_FINISHED_MESSAGE, +} from '../../src/templates/shell.js'; + +describe('injectTerminalReconnectScript', () => { + it('injects the script right after ', () => { + const html = 'ttyd'; + const out = injectTerminalReconnectScript(html); + assert.match(out, /')); + }); + + it('injects only once (single match)', () => { + const out = injectTerminalReconnectScript(''); + assert.equal(out.match(/