diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63a0f1a..b91c99b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,25 @@ jobs: - run: bun run build + - name: Upload sourcemaps to Sentry + if: startsWith(github.ref, 'refs/tags/v') + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: pickforge + SENTRY_PROJECT: picklab + run: | + if [ -z "$SENTRY_AUTH_TOKEN" ]; then + echo "SENTRY_AUTH_TOKEN not set; skipping" + exit 0 + fi + VERSION=$(node -p "require('./packages/cli/package.json').version") + if ! { + npx --yes @sentry/cli sourcemaps inject packages/cli/dist && + npx --yes @sentry/cli sourcemaps upload --release "picklab@$VERSION" packages/cli/dist + }; then + echo "::warning::Sentry sourcemap upload failed; continuing release" + fi + - name: Publish @pickforge/picklab if: startsWith(github.ref, 'refs/tags/v') working-directory: packages/cli diff --git a/INSTALL.md b/INSTALL.md index ecd99d4..0b8f9ad 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -24,6 +24,8 @@ command -v picklab && picklab --version If `picklab` is not on PATH, the global bin dir (`npm prefix -g`/bin or `~/.bun/bin`) is missing from PATH — fix the user's shell profile or tell them, don't fall back to a local install. +On fatal errors the CLI and MCP server report the error message and stack trace — the message can reference the failing command and its output, with secrets redacted — plus OS, Node.js, and app versions to Sentry; nothing else is collected. Tell the user they can disable this with `PICKLAB_TELEMETRY=0`. + ## 2. Register the MCP server with the agent the user uses That is probably you. Built-in support: diff --git a/README.md b/README.md index 1e29ee6..61be2ee 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,10 @@ Each session gets its own isolated display or emulator, so several agents and pr PICKLAB · RUN LAB — desktop session, Android emulator, live screenshots, logs, and agent terminal

+## Telemetry + +When the `picklab` CLI or `picklab-mcp` server hits a fatal error, it reports the error message and stack trace — the message can reference the failing command and its output, with secrets redacted — plus OS, Node.js, and app versions to Sentry so we can fix it. Nothing else is collected. Disable with `PICKLAB_TELEMETRY=0`. + ## MCP setup for agents Register the MCP server with your coding agent: diff --git a/bun.lock b/bun.lock index dd2e6f5..d309606 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", + "@sentry/node": "^10.64.0", "commander": "^14.0.0", "zod": "^4.0.0", }, @@ -72,6 +73,12 @@ "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.15.0", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-XmXYVs8CzJ1Aj79noVbn2weUO/XWtRyURpGqx7aU7DOXlUQhR0WKOQNF0okh7PCeY37vxf7kU3v57OAkEPm3ww=="], + + "@apm-js-collab/code-transformer-bundler-plugins": ["@apm-js-collab/code-transformer-bundler-plugins@0.5.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", "es-module-lexer": "^2.1.0", "magic-string": "^0.30.21", "module-details-from-path": "^1.0.4" } }, "sha512-YxLBY5nGlurL7QeJLq6e5g0ouBpAp0pwgyA/5rHXEXwhiPLn9ZHbT+Y2LlP90GT872cSocfjWRYu/fnpuBudNQ=="], + + "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.10.1", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-w2OWXR7FWrKqSziuE9+QclaZrStxO/8+OwbXM635s/zs0Eez1Qo3ivSPdB2WsaPY/iznKTytONPx/PitD7IXcA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], @@ -150,6 +157,22 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.9.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + + "@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-trace": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.42.0", "", {}, "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw=="], + "@pickforge/picklab": ["@pickforge/picklab@workspace:packages/cli"], "@pickforge/picklab-agent-installers": ["@pickforge/picklab-agent-installers@workspace:packages/agent-installers"], @@ -214,6 +237,18 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="], + "@sentry/conventions": ["@sentry/conventions@0.15.1", "", {}, "sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA=="], + + "@sentry/core": ["@sentry/core@10.64.0", "", { "dependencies": { "@sentry/conventions": "^0.15.1" } }, "sha512-HjojJcXD1l2qZ1AXje2s0XY/nYsaUt00wsM1HMBImA8vAClyPisFE/CC0/UD6pEvsGFhVgi8Dcxo7EN41uyeFw=="], + + "@sentry/node": ["@sentry/node@10.64.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.220.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@sentry/conventions": "^0.15.1", "@sentry/core": "10.64.0", "@sentry/node-core": "10.64.0", "@sentry/opentelemetry": "10.64.0", "@sentry/server-utils": "10.64.0", "import-in-the-middle": "^3.0.0" } }, "sha512-rhNZ3CTqwdTQHo9Zd+NsoLyW69VIzbMcGMRX9y8W1A6runT4YH/MDhmT0U9oWfNZKN8ngqR/gfVMTnlYVQliCA=="], + + "@sentry/node-core": ["@sentry/node-core@10.64.0", "", { "dependencies": { "@sentry/conventions": "^0.15.1", "@sentry/core": "10.64.0", "@sentry/opentelemetry": "10.64.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base"] }, "sha512-dcma5uEWl0SXywY4vzrlOwuz4NBPyPhrzqL1NjlU6Di/OVbyYAZJPCwsR8XYDz73PGc5sU3qKSRoXWX56Ip0wA=="], + + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.64.0", "", { "dependencies": { "@sentry/conventions": "^0.15.1", "@sentry/core": "10.64.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" } }, "sha512-hXw8dwSSA9/9r3VGy0PO2SJp7g5/yH2+7Bak60EkOT21AT1BwFnVEsVQyZb+UHjG7UWne/jDbwdWPSUm/rtovw=="], + + "@sentry/server-utils": ["@sentry/server-utils@10.64.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", "@apm-js-collab/tracing-hooks": "^0.10.1", "@sentry/conventions": "^0.15.1", "@sentry/core": "10.64.0", "magic-string": "~0.30.0" } }, "sha512-d568Jhn3WBfm07dsdPsLh0q+qRj71xZEDZFsA33kizD0UuSCR8axtc2YW6aKdPhtuqgm9tHjSX35Q9vK/XmujA=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], @@ -256,6 +291,8 @@ "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.12", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], @@ -278,6 +315,8 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -328,6 +367,10 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -384,6 +427,8 @@ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "import-in-the-middle": ["import-in-the-middle@3.3.1", "", { "dependencies": { "cjs-module-lexer": "^2.2.0", "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], @@ -438,6 +483,8 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "meriyah": ["meriyah@6.1.4", "", {}, "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -448,6 +495,8 @@ "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -504,6 +553,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="], @@ -512,6 +563,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semifies": ["semifies@1.0.0", "", {}, "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -618,8 +671,14 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@apm-js-collab/code-transformer/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "@apm-js-collab/code-transformer-bundler-plugins/es-module-lexer": ["es-module-lexer@2.3.0", "", {}, "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw=="], + "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "import-in-the-middle/es-module-lexer": ["es-module-lexer@2.3.0", "", {}, "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/docs/releases/UNRELEASED.md b/docs/releases/UNRELEASED.md index 5f45c01..39dedb8 100644 --- a/docs/releases/UNRELEASED.md +++ b/docs/releases/UNRELEASED.md @@ -6,23 +6,25 @@ then reset this file. ## User-facing changes -- None yet. +- Fatal errors from the `picklab` CLI and `picklab-mcp` server are now reported to Sentry (error message, stack trace, OS and app version only; breadcrumbs dropped, hostname stripped, messages run through `redactSecrets`). Opt out with `PICKLAB_TELEMETRY=0`; documented in README and INSTALL. ## Internal/release changes - Added repo-local release tracking in `docs/releases/UNRELEASED.md`. +- CLI bundles now emit sourcemaps; the release workflow injects debug IDs and uploads sourcemaps to Sentry on tag builds (skips cleanly until `SENTRY_AUTH_TOKEN` secret exists). ## Validation ### Tested - Reviewed the release tracking docs. +- `bun run typecheck`, `bun run test` (556 passed), `bun run build`. +- MCP stdio smoke: initialize handshake over the built `picklab-mcp` with telemetry enabled — stdout is pure JSON-RPC. ### Not tested yet -- CLI build. -- MCP server smoke test. -- npm publish flow. +- Sentry event delivery end-to-end (needs a real fatal in a released build). +- npm publish flow with the new sourcemap upload step. ### Release blockers diff --git a/packages/cli/package.json b/packages/cli/package.json index eeb6122..655fcdd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,6 +41,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", + "@sentry/node": "^10.64.0", "commander": "^14.0.0", "zod": "^4.0.0" }, diff --git a/packages/cli/src/picklab-mcp.ts b/packages/cli/src/picklab-mcp.ts index f010af8..21f6891 100644 --- a/packages/cli/src/picklab-mcp.ts +++ b/packages/cli/src/picklab-mcp.ts @@ -1,4 +1,15 @@ #!/usr/bin/env node -import { runMcpServe } from "./commands/mcp.js"; +import { captureFatal, initTelemetry } from "./telemetry.js"; -process.exitCode = await runMcpServe(); +initTelemetry(); + +try { + const { runMcpServe } = await import("./commands/mcp.js"); + process.exitCode = await runMcpServe(); +} catch (error) { + await captureFatal(error); + console.error( + `error: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exitCode = 1; +} diff --git a/packages/cli/src/picklab.ts b/packages/cli/src/picklab.ts index 9922fe6..215782c 100644 --- a/packages/cli/src/picklab.ts +++ b/packages/cli/src/picklab.ts @@ -1,9 +1,15 @@ #!/usr/bin/env node -import { buildProgram } from "./program.js"; +import { captureFatal, initTelemetry } from "./telemetry.js"; + +initTelemetry(); try { + const { buildProgram } = await import("./program.js"); await buildProgram().parseAsync(); } catch (error) { - console.error(`error: ${(error as Error).message}`); + await captureFatal(error); + console.error( + `error: ${error instanceof Error ? error.message : String(error)}`, + ); process.exitCode = 1; } diff --git a/packages/cli/src/telemetry.ts b/packages/cli/src/telemetry.ts new file mode 100644 index 0000000..e6955a0 --- /dev/null +++ b/packages/cli/src/telemetry.ts @@ -0,0 +1,74 @@ +import { createRequire } from "node:module"; +import * as Sentry from "@sentry/node"; +import type { ErrorEvent } from "@sentry/node"; +import { redactSecrets } from "@pickforge/picklab-core"; + +export type EnvLike = Record; + +const DSN = + "https://25cc6307aeca0d1d454e0af21bee5498@o4511699702317056.ingest.us.sentry.io/4511699813990400"; + +const DISABLE_VALUES = new Set(["0", "false", "off"]); + +export function telemetryEnabled(env: EnvLike = process.env): boolean { + const value = env.PICKLAB_TELEMETRY?.trim(); + if (value === undefined || value === "") { + return true; + } + return !DISABLE_VALUES.has(value.toLowerCase()); +} + +export function initTelemetry(env: EnvLike = process.env): void { + if (!telemetryEnabled(env)) { + return; + } + const require = createRequire(import.meta.url); + const { version } = require("../package.json") as { version: string }; + Sentry.init({ + dsn: DSN, + release: `picklab@${version}`, + tracesSampleRate: 0, + defaultIntegrations: false, + integrations: [ + Sentry.inboundFiltersIntegration(), + Sentry.functionToStringIntegration(), + Sentry.linkedErrorsIntegration(), + Sentry.dedupeIntegration(), + Sentry.onUncaughtExceptionIntegration(), + Sentry.onUnhandledRejectionIntegration({ mode: "strict" }), + Sentry.nodeContextIntegration(), + ], + beforeBreadcrumb: dropBreadcrumb, + beforeSend: scrubEvent, + }); +} + +export function dropBreadcrumb(): null { + return null; +} + +export function scrubEvent(event: ErrorEvent): ErrorEvent { + delete event.server_name; + delete event.modules; + if (event.contexts) { + for (const key of Object.keys(event.contexts)) { + if (key !== "os" && key !== "runtime") { + delete event.contexts[key]; + } + } + } + if (typeof event.message === "string") { + event.message = redactSecrets(event.message); + } + for (const exception of event.exception?.values ?? []) { + if (exception.value !== undefined) { + exception.value = redactSecrets(exception.value); + } + } + return event; +} + +export async function captureFatal(err: unknown): Promise { + Sentry.captureException(err); + await Sentry.flush(2000); +} diff --git a/packages/cli/test/telemetry.test.ts b/packages/cli/test/telemetry.test.ts new file mode 100644 index 0000000..4e416a9 --- /dev/null +++ b/packages/cli/test/telemetry.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from "vitest"; +import * as Sentry from "@sentry/node"; +import type { ErrorEvent } from "@sentry/node"; +import { + captureFatal, + dropBreadcrumb, + initTelemetry, + scrubEvent, + telemetryEnabled, +} from "../src/telemetry.js"; + +describe("telemetryEnabled", () => { + it("defaults to on when unset", () => { + expect(telemetryEnabled({})).toBe(true); + }); + + it("defaults to on when trimmed-empty", () => { + expect(telemetryEnabled({ PICKLAB_TELEMETRY: " " })).toBe(true); + }); + + it("defaults to on for unrelated values", () => { + expect(telemetryEnabled({ PICKLAB_TELEMETRY: "yes" })).toBe(true); + }); + + it("is off for '0'", () => { + expect(telemetryEnabled({ PICKLAB_TELEMETRY: "0" })).toBe(false); + }); + + it("is off for 'false'", () => { + expect(telemetryEnabled({ PICKLAB_TELEMETRY: "false" })).toBe(false); + }); + + it("is off for 'OFF' case-insensitively", () => { + expect(telemetryEnabled({ PICKLAB_TELEMETRY: "OFF" })).toBe(false); + }); + + it("is off for ' off ' with surrounding whitespace", () => { + expect(telemetryEnabled({ PICKLAB_TELEMETRY: " off " })).toBe(false); + }); +}); + +describe("initTelemetry", () => { + it("does not initialize Sentry when opted out", () => { + initTelemetry({ PICKLAB_TELEMETRY: "0" }); + expect(Sentry.isInitialized()).toBe(false); + }); +}); + +describe("scrubEvent", () => { + it("deletes server_name and modules", () => { + const event: ErrorEvent = { + type: undefined, + server_name: "my-hostname", + modules: { commander: "14.0.0" }, + }; + const scrubbed = scrubEvent(event); + expect(scrubbed.server_name).toBeUndefined(); + expect(scrubbed.modules).toBeUndefined(); + }); + + it("prunes contexts down to os and runtime", () => { + const event: ErrorEvent = { + type: undefined, + contexts: { + os: { name: "linux" }, + runtime: { name: "node", version: "v20" }, + device: { arch: "x64" }, + app: { app_start_time: "now" }, + culture: { locale: "en-US" }, + trace: { trace_id: "abc", span_id: "def" }, + }, + }; + const scrubbed = scrubEvent(event); + expect(Object.keys(scrubbed.contexts ?? {}).sort()).toEqual([ + "os", + "runtime", + ]); + }); + + it("redacts secrets in the message and exception values", () => { + const event: ErrorEvent = { + type: undefined, + message: "failed with token=ghp_0123456789012345678901234567890abcde", + exception: { + values: [ + { type: "Error", value: "adb failed: API_KEY=super-secret-value" }, + { type: "Error" }, + ], + }, + }; + const scrubbed = scrubEvent(event); + expect(scrubbed.message).not.toContain("ghp_"); + expect(scrubbed.exception?.values?.[0].value).toBe( + "adb failed: API_KEY=[REDACTED]", + ); + expect(scrubbed.exception?.values?.[1].value).toBeUndefined(); + }); +}); + +describe("dropBreadcrumb", () => { + it("always returns null", () => { + expect(dropBreadcrumb()).toBeNull(); + }); +}); + +describe("captureFatal", () => { + it("resolves without throwing when Sentry is uninitialized", async () => { + expect(Sentry.isInitialized()).toBe(false); + await expect(captureFatal(new Error("boom"))).resolves.toBeUndefined(); + }); +}); diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index 0eb52a8..be6bccb 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -5,5 +5,6 @@ export default defineConfig({ format: ["esm"], platform: "node", clean: true, + sourcemap: true, noExternal: [/^@pickforge\//], }); diff --git a/test/security/security-no-sudo.test.ts b/test/security/security-no-sudo.test.ts index 98b6a16..49d22db 100644 --- a/test/security/security-no-sudo.test.ts +++ b/test/security/security-no-sudo.test.ts @@ -230,7 +230,7 @@ describe("bundle: built picklab-mcp entrypoint", () => { const content = fs.readFileSync(path.join(distDir, name), "utf8"); contents.set(name, content); for (const match of content.matchAll( - /\bfrom\s*["'](\.\.?\/[^"']+)["']/g, + /(?:\bfrom\s*|\bimport\s*\(\s*)["'](\.\.?\/[^"']+)["']/g, )) { queue.push(path.normalize(path.join(path.dirname(name), match[1]))); }