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
+## 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])));
}