diff --git a/bun.lock b/bun.lock index 9bb91c0b520..cf892b1a40c 100644 --- a/bun.lock +++ b/bun.lock @@ -285,6 +285,14 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.2", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.208.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.208.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-node": "0.208.0", + "@opentelemetry/semantic-conventions": "1.37.0", "@opentui/core": "0.1.69", "@opentui/solid": "0.1.69", "@parcel/watcher": "2.5.1", @@ -473,6 +481,7 @@ }, }, "trustedDependencies": [ + "protobufjs", "esbuild", "web-tree-sitter", "tree-sitter-bash", @@ -901,6 +910,10 @@ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="], @@ -1037,6 +1050,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], @@ -1197,6 +1212,60 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.2.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.208.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-grpc-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-AmZDKFzbq/idME/yq68M155CJW1y056MNBekH9OZewiZKaqgwYN4VYfn3mXVPftYsfrCM2r4V6tS8H2LmfiDCg=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="], + + "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wy8dZm16AOfM7yddEzSFzutHZDZ6HspKUODSUJVjyhnZFMBojWDjSNgduyCMlw6qaxJYz0dlb0OEcb4Eme+BfQ=="], + + "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.208.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.208.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-grpc-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-YbEnk7jjYmvhIwp2xJGkEvdgnayrA2QSr28R1LR1klDPvCxsoQPxE6TokDbQpoCEhD3+KmJVEXfb4EeEQxjymg=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-QZ3TrI90Y0i1ezWQdvreryjY0a5TK4J9gyDLIyhLBwV+EQUvyp5wR7TFPKCAexD4TDSWM0t3ulQDbYYjVtzTyA=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.208.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CvvVD5kRDmRB/uSMalvEF6kiamY02pB46YAqclHtfjJccNZFxbkkXkMMmcJ7NgBFa5THmQBNVQ2AHyX29nRxOw=="], + + "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Rgws8GfIfq2iNWCD3G1dTD9xwYsCof1+tc5S5X0Ahdb5CrAPE+k5P70XCWHqrFFurVCcKaHLJ/6DjIBHWVfLiw=="], + + "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.208.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-grpc-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E/eNdcqVUTAT7BC+e8VOw/krqb+5rjzYkztMZ/o+eyJl+iEY6PfczPXpwWuICwvsm0SIhBoh9hmYED5Vh5RwIw=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA=="], + + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-q844Jc3ApkZVdWYd5OAl+an3n1XXf3RWHa3Zgmnhw3HpsM3VluEKHckUUEqHPzbwDUx2lhPRVkqK7LsJ/CbDzA=="], + + "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], + + "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.208.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], + + "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="], + + "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.208.0", "@opentelemetry/exporter-logs-otlp-http": "0.208.0", "@opentelemetry/exporter-logs-otlp-proto": "0.208.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.208.0", "@opentelemetry/exporter-metrics-otlp-http": "0.208.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.208.0", "@opentelemetry/exporter-prometheus": "0.208.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.208.0", "@opentelemetry/exporter-trace-otlp-http": "0.208.0", "@opentelemetry/exporter-trace-otlp-proto": "0.208.0", "@opentelemetry/exporter-zipkin": "2.2.0", "@opentelemetry/instrumentation": "0.208.0", "@opentelemetry/propagator-b3": "2.2.0", "@opentelemetry/propagator-jaeger": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/sdk-trace-node": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-pbAqpZ7zTMFuTf3YecYsecsto/mheuvnK2a/jgstsE5ynWotBjgF5bnz5500W9Xl2LeUfg04WMt63TWtAgzRMw=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.2.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.2.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.37.0", "", {}, "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA=="], + "@opentui/core": ["@opentui/core@0.1.69", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.69", "@opentui/core-darwin-x64": "0.1.69", "@opentui/core-linux-arm64": "0.1.69", "@opentui/core-linux-x64": "0.1.69", "@opentui/core-win32-arm64": "0.1.69", "@opentui/core-win32-x64": "0.1.69", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-BcEFnAuMq4vgfb+zxOP/l+NO1AS3fVHkYjn+E8Wpmaxr0AzWNTi2NPAMtQf+Wqufxo0NYh0gY4c9B6n8OxTjGw=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.69", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d9RPAh84O2XIyMw+7+X0fEyi+4KH5sPk9AxLze8GHRBGOzkRunqagFCLBrN5VFs2e2nbhIYtjMszo7gcpWyh7g=="], @@ -1347,6 +1416,26 @@ "@protobuf-ts/runtime-rpc": ["@protobuf-ts/runtime-rpc@2.11.1", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1" } }, "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], @@ -1891,6 +1980,8 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], @@ -2111,6 +2202,8 @@ "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + "classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="], "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], @@ -2633,6 +2726,8 @@ "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], + "import-in-the-middle": ["import-in-the-middle@2.0.1", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bruMpJ7xz+9jwGzrwEhWgvRrlKRYCRDBrfU+ur3FcasYXLJDxTruJ//8g2Noj+QFyRBeqbpj8Bhn4Fbw6HjvhA=="], + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -2843,6 +2938,8 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], @@ -3041,6 +3138,8 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -3279,6 +3378,8 @@ "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], @@ -3383,6 +3484,10 @@ "remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "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=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -4003,6 +4108,8 @@ "@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], + "@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], @@ -4537,6 +4644,10 @@ "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], + "@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -4989,6 +5100,14 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5173,6 +5292,10 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0d2a6d2b230..384ed64dc74 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -10,6 +10,9 @@ "test": "bun test", "build": "bun run script/build.ts", "dev": "bun run --conditions=browser ./src/index.ts", + "aspire:start": "docker run --rm -d -p 18888:18888 -p 4317:18889 -e ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true --name aspire-dashboard mcr.microsoft.com/dotnet/aspire-dashboard:latest && echo 'Aspire Dashboard: http://localhost:18888'", + "aspire:stop": "docker stop aspire-dashboard 2>/dev/null || true", + "dev:otel": "bun run aspire:start; OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true bun dev", "random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'", "clean": "echo 'Cleaning up...' && rm -rf node_modules dist", "lint": "echo 'Running lint checks...' && bun test --coverage", @@ -81,6 +84,14 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.2", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-node": "0.208.0", + "@opentelemetry/semantic-conventions": "1.37.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.208.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.208.0", "@opentui/core": "0.1.69", "@opentui/solid": "0.1.69", "@parcel/watcher": "2.5.1", diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index c683727dfa3..dd0417828a9 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -12,6 +12,7 @@ import PROMPT_SUMMARY from "./prompt/summary.txt" import PROMPT_TITLE from "./prompt/title.txt" import { PermissionNext } from "@/permission/next" import { mergeDeep, pipe, sortBy, values } from "remeda" +import { Telemetry } from "@/telemetry" export namespace Agent { export const Info = z @@ -214,8 +215,12 @@ export namespace Agent { } export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) { - const cfg = await Config.get() const defaultModel = input.model ?? (await Provider.defaultModel()) + using _ = Telemetry.span("agent.generate", { + "llm.provider_id": defaultModel.providerID, + "llm.model_id": defaultModel.modelID, + }) + const cfg = await Config.get() const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID) const language = await Provider.getLanguage(model) const system = SystemPrompt.header(defaultModel.providerID) @@ -223,9 +228,11 @@ export namespace Agent { const existing = await list() const result = await generateObject({ experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, + isEnabled: Telemetry.isEnabled(), + functionId: "opencode.agent.generate", metadata: { - userId: cfg.username ?? "unknown", + "llm.provider_id": defaultModel.providerID, + "llm.model_id": defaultModel.modelID, }, }, temperature: 0.3, diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index d32612dd59b..111a7db2152 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -17,6 +17,12 @@ await Log.init({ })(), }) +const globalConfig = await Config.global() +if (globalConfig?.experimental?.openTelemetry) { + const { Telemetry } = await import("@/telemetry") + Telemetry.init(Telemetry.resolveConfig("opencode-server", true)) +} + process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { e: e instanceof Error ? e.message : e, @@ -58,7 +64,10 @@ export const rpc = { }, async shutdown() { Log.Default.info("worker shutting down") + Log.Default.info("disposing all instances") await Instance.disposeAll() + const { Telemetry } = await import("@/telemetry") + await Telemetry.shutdown() // TODO: this should be awaited, but ws connections are // causing this to hang, need to revisit this server.stop(true) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index a91c91cf0a0..a40aec01fbf 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -954,7 +954,7 @@ export namespace Config { openTelemetry: z .boolean() .optional() - .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"), + .describe("Enable OpenTelemetry tracing. Set OTEL_EXPORTER_OTLP_ENDPOINT env var for endpoint."), primary_tools: z .array(z.string()) .optional() diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 805da33cc7a..3d1582ffa6d 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -15,6 +15,7 @@ export namespace Flag { export const OPENCODE_DISABLE_MODELS_FETCH = truthy("OPENCODE_DISABLE_MODELS_FETCH") export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli" + export const OTEL_EXPORTER_OTLP_ENDPOINT = process.env["OTEL_EXPORTER_OTLP_ENDPOINT"] // Experimental export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL") diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 03ccf76042f..4d8ca43d97b 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -27,6 +27,8 @@ import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" +import { Config } from "./config/config" +import { Telemetry } from "./telemetry" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -40,6 +42,14 @@ process.on("uncaughtException", (e) => { }) }) +process.on("SIGTERM", async () => { + await Telemetry.shutdown() +}) + +process.on("SIGINT", async () => { + await Telemetry.shutdown() +}) + const cli = yargs(hideBin(process.argv)) .parserConfiguration({ "populate--": true }) .scriptName("opencode") @@ -75,6 +85,11 @@ const cli = yargs(hideBin(process.argv)) version: Installation.VERSION, args: process.argv.slice(2), }) + + const globalConfig = await Config.global() + if (globalConfig?.experimental?.openTelemetry) { + Telemetry.init(Telemetry.resolveConfig("opencode-cli", true)) + } }) .usage("\n" + UI.logo()) .completion("completion", "generate shell completion script") @@ -151,6 +166,8 @@ try { } process.exitCode = 1 } finally { + // Shutdown telemetry before exit + await Telemetry.shutdown() // Some subprocesses don't react properly to SIGTERM and similar signals. // Most notably, some docker-container-based MCP servers don't handle such signals unless // run using `docker run --init`. diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 8704b65acb5..87bd3ffeba6 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -12,6 +12,7 @@ import { NamedError } from "@opencode-ai/util/error" import { withTimeout } from "../util/timeout" import { Instance } from "../project/instance" import { Filesystem } from "../util/filesystem" +import { Telemetry } from "@/telemetry" const DIAGNOSTICS_DEBOUNCE_MS = 150 @@ -40,6 +41,10 @@ export namespace LSPClient { } export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) { + using _span = Telemetry.span("lsp.client.create", { + "lsp.server_id": input.serverID, + "lsp.root": input.root, + }) const l = log.clone().tag("serverID", input.serverID) l.info("starting client") @@ -79,50 +84,58 @@ export namespace LSPClient { connection.listen() l.info("sending initialize") - await withTimeout( - connection.sendRequest("initialize", { - rootUri: pathToFileURL(input.root).href, - processId: input.server.process.pid, - workspaceFolders: [ - { - name: "workspace", - uri: pathToFileURL(input.root).href, - }, - ], - initializationOptions: { - ...input.server.initialization, - }, - capabilities: { - window: { - workDoneProgress: true, - }, - workspace: { - configuration: true, - didChangeWatchedFiles: { - dynamicRegistration: true, + await Telemetry.withSpan( + "lsp.request.initialize", + { + "lsp.server_id": input.serverID, + }, + async () => { + await withTimeout( + connection.sendRequest("initialize", { + rootUri: pathToFileURL(input.root).href, + processId: input.server.process.pid, + workspaceFolders: [ + { + name: "workspace", + uri: pathToFileURL(input.root).href, + }, + ], + initializationOptions: { + ...input.server.initialization, }, - }, - textDocument: { - synchronization: { - didOpen: true, - didChange: true, + capabilities: { + window: { + workDoneProgress: true, + }, + workspace: { + configuration: true, + didChangeWatchedFiles: { + dynamicRegistration: true, + }, + }, + textDocument: { + synchronization: { + didOpen: true, + didChange: true, + }, + publishDiagnostics: { + versionSupport: true, + }, + }, }, - publishDiagnostics: { - versionSupport: true, + }), + 45_000, + ).catch((err) => { + l.error("initialize error", { error: err }) + throw new InitializeError( + { serverID: input.serverID }, + { + cause: err, }, - }, - }, - }), - 45_000, - ).catch((err) => { - l.error("initialize error", { error: err }) - throw new InitializeError( - { serverID: input.serverID }, - { - cause: err, - }, - ) - }) + ) + }) + }, + ) await connection.sendNotification("initialized", {}) diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 0fd3b69dfcd..828510a8430 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -10,6 +10,7 @@ import { Config } from "../config/config" import { spawn } from "child_process" import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" +import { Telemetry, traced } from "@/telemetry" export namespace LSP { const log = Log.create({ service: "lsp" }) @@ -275,6 +276,7 @@ export namespace LSP { } export async function touchFile(input: string, waitForDiagnostics?: boolean) { + using _span = Telemetry.span("lsp.touch_file", { "lsp.file": input }) log.info("touching file", { file: input }) const clients = await getClients(input) await Promise.all( @@ -300,7 +302,14 @@ export namespace LSP { return results } - export async function hover(input: { file: string; line: number; character: number }) { + export const hover = traced<{ file: string; line: number; character: number }, (unknown | null)[]>( + "lsp.request.hover", + (input) => ({ + "lsp.file": input.file, + "lsp.line": input.line, + "lsp.character": input.character, + }), + )(async (input) => { return run(input.file, (client) => { return client.connection .sendRequest("textDocument/hover", { @@ -314,7 +323,7 @@ export namespace LSP { }) .catch(() => null) }) - } + }) enum SymbolKind { File = 1, @@ -383,7 +392,14 @@ export namespace LSP { .then((result) => result.filter(Boolean)) } - export async function definition(input: { file: string; line: number; character: number }) { + export const definition = traced<{ file: string; line: number; character: number }, unknown[]>( + "lsp.request.definition", + (input) => ({ + "lsp.file": input.file, + "lsp.line": input.line, + "lsp.character": input.character, + }), + )(async (input) => { return run(input.file, (client) => client.connection .sendRequest("textDocument/definition", { @@ -392,9 +408,16 @@ export namespace LSP { }) .catch(() => null), ).then((result) => result.flat().filter(Boolean)) - } - - export async function references(input: { file: string; line: number; character: number }) { + }) + + export const references = traced<{ file: string; line: number; character: number }, unknown[]>( + "lsp.request.references", + (input) => ({ + "lsp.file": input.file, + "lsp.line": input.line, + "lsp.character": input.character, + }), + )(async (input) => { return run(input.file, (client) => client.connection .sendRequest("textDocument/references", { @@ -404,7 +427,7 @@ export namespace LSP { }) .catch(() => []), ).then((result) => result.flat().filter(Boolean)) - } + }) export async function implementation(input: { file: string; line: number; character: number }) { return run(input.file, (client) => diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index aca0c663152..820b5ef3738 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -23,6 +23,7 @@ import { BusEvent } from "../bus/bus-event" import { Bus } from "@/bus" import { TuiEvent } from "@/cli/cmd/tui/event" import open from "open" +import { Telemetry } from "@/telemetry" export namespace MCP { const log = Log.create({ service: "mcp" }) @@ -109,7 +110,7 @@ export namespace MCP { } // Convert MCP tool definition to AI SDK Tool type - async function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient): Promise { + async function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, serverName: string): Promise { const inputSchema = mcpTool.inputSchema // Spread first, then override type to ensure it's always "object" @@ -125,15 +126,24 @@ export namespace MCP { description: mcpTool.description ?? "", inputSchema: jsonSchema(schema), execute: async (args: unknown) => { - return client.callTool( + return Telemetry.withSpan( + "mcp.tool.call", { - name: mcpTool.name, - arguments: args as Record, + "mcp.server_name": serverName, + "mcp.tool_name": mcpTool.name, }, - CallToolResultSchema, - { - resetTimeoutOnProgress: true, - timeout: config.experimental?.mcp_timeout, + async () => { + return client.callTool( + { + name: mcpTool.name, + arguments: args as Record, + }, + CallToolResultSchema, + { + resetTimeoutOnProgress: true, + timeout: config.experimental?.mcp_timeout, + }, + ) }, ) }, @@ -204,6 +214,8 @@ export namespace MCP { // Helper function to fetch prompts for a specific client async function fetchPromptsForClient(clientName: string, client: Client) { + using span = Telemetry.span("mcp.prompts.list", { "mcp.server_name": clientName }) + const prompts = await client.listPrompts().catch((e) => { log.error("failed to get prompts", { clientName, error: e.message }) return undefined @@ -213,6 +225,8 @@ export namespace MCP { return } + span.setAttributes({ "mcp.prompt_count": prompts.prompts.length }) + const commands: Record = {} for (const prompt of prompts.prompts) { @@ -336,7 +350,14 @@ export namespace MCP { name: "opencode", version: Installation.VERSION, }) - await withTimeout(client.connect(transport), connectTimeout) + { + using _span = Telemetry.span("mcp.client.connect", { + "mcp.server_name": key, + "mcp.type": "remote", + "mcp.transport": name, + }) + await withTimeout(client.connect(transport), connectTimeout) + } registerNotificationHandlers(client, key) mcpClient = client log.info("connected", { key, transport: name }) @@ -412,7 +433,13 @@ export namespace MCP { name: "opencode", version: Installation.VERSION, }) - await withTimeout(client.connect(transport), connectTimeout) + { + using _span = Telemetry.span("mcp.client.connect", { + "mcp.server_name": key, + "mcp.type": "local", + }) + await withTimeout(client.connect(transport), connectTimeout) + } registerNotificationHandlers(client, key) mcpClient = client status = { @@ -446,10 +473,24 @@ export namespace MCP { } } - const result = await withTimeout(mcpClient.listTools(), mcp.timeout ?? DEFAULT_TIMEOUT).catch((err) => { - log.error("failed to get tools from client", { key, error: err }) - return undefined - }) + const result = await Telemetry.withSpan( + "mcp.tools.list", + { + "mcp.server_name": key, + }, + async (span) => { + const tools = await withTimeout(mcpClient!.listTools(), mcp.timeout ?? DEFAULT_TIMEOUT).catch((err) => { + log.error("failed to get tools from client", { key, error: err }) + return undefined + }) + if (tools) { + span.setAttributes({ + "mcp.tool_count": tools.tools.length, + }) + } + return tools + }, + ) if (!result) { await mcpClient.close().catch((error) => { log.error("Failed to close MCP client", { @@ -550,6 +591,7 @@ export namespace MCP { continue } + using span = Telemetry.span("mcp.tools.list", { "mcp.server_name": clientName }) const toolsResult = await client.listTools().catch((e) => { log.error("failed to get tools", { clientName, error: e.message }) const failedStatus = { @@ -560,13 +602,18 @@ export namespace MCP { delete s.clients[clientName] return undefined }) + if (toolsResult) { + span.setAttributes({ + "mcp.tool_count": toolsResult.tools.length, + }) + } if (!toolsResult) { continue } for (const mcpTool of toolsResult.tools) { const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_") const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_") - result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, client) + result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, client, clientName) } } return result @@ -615,6 +662,11 @@ export namespace MCP { } export async function getPrompt(clientName: string, name: string, args?: Record) { + using _span = Telemetry.span("mcp.prompt.get", { + "mcp.server_name": clientName, + "mcp.prompt_name": name, + }) + const clientsSnapshot = await clients() const client = clientsSnapshot[clientName] diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 6868836f0b0..e72c0666c17 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -7,6 +7,7 @@ import { Server } from "../server/server" import { BunProc } from "../bun" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" +import { Telemetry } from "@/telemetry" export namespace Plugin { const log = Log.create({ service: "plugin" }) @@ -71,7 +72,12 @@ export namespace Plugin { Output = Parameters[Name]>[1], >(name: Name, input: Input, output: Output): Promise { if (!name) return output - for (const hook of await state().then((x) => x.hooks)) { + const hooks = await state().then((x) => x.hooks) + using _ = Telemetry.span("plugin.trigger", { + "plugin.hook_name": name, + "plugin.hooks_count": hooks.length, + }) + for (const hook of hooks) { const fn = hook[name] if (!fn) continue // @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 42bab2eb975..131f1afc5ff 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -14,6 +14,7 @@ import { fn } from "@/util/fn" import { Agent } from "@/agent/agent" import { Plugin } from "@/plugin" import { Config } from "@/config/config" +import { traced } from "@/telemetry/traced" export namespace SessionCompaction { const log = Log.create({ service: "session.compaction" }) @@ -89,13 +90,21 @@ export namespace SessionCompaction { } } - export async function process(input: { + type ProcessInput = { parentID: string messages: MessageV2.WithParts[] sessionID: string abort: AbortSignal auto: boolean - }) { + } + + type ProcessOutput = "stop" | "continue" + + export const process = traced("session.compaction.process", (input) => ({ + "session.id": input.sessionID, + "session.auto": input.auto, + "session.message_count": input.messages.length, + }))(async (input) => { const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User const agent = await Agent.get("compaction") const model = agent.model @@ -190,7 +199,7 @@ export namespace SessionCompaction { if (processor.message.error) return "stop" Bus.publish(Event.Compacted, { sessionID: input.sessionID }) return "continue" - } + }) export const create = fn( z.object({ diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 7e2967e31ba..d3798cccf51 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -11,7 +11,6 @@ import { } from "ai" import { clone, mergeDeep, pipe } from "remeda" import { ProviderTransform } from "@/provider/transform" -import { Config } from "@/config/config" import { Instance } from "@/project/instance" import type { Agent } from "@/agent/agent" import type { MessageV2 } from "./message-v2" @@ -19,6 +18,8 @@ import { Plugin } from "@/plugin" import { SystemPrompt } from "./system" import { Flag } from "@/flag/flag" import { PermissionNext } from "@/permission/next" +import { traced } from "@/telemetry/traced" +import { Telemetry } from "@/telemetry" export namespace LLM { const log = Log.create({ service: "llm" }) @@ -40,7 +41,13 @@ export namespace LLM { export type StreamOutput = StreamTextResult - export async function stream(input: StreamInput) { + export const stream = traced("llm.stream", (input) => ({ + "llm.provider_id": input.model.providerID, + "llm.model_id": input.model.id, + "session.id": input.sessionID, + "llm.agent": input.agent.name, + "llm.tools_count": Object.keys(input.tools).length, + }))(async (input) => { const l = log .clone() .tag("providerID", input.model.providerID) @@ -52,7 +59,7 @@ export namespace LLM { modelID: input.model.id, providerID: input.model.providerID, }) - const [language, cfg] = await Promise.all([Provider.getLanguage(input.model), Config.get()]) + const language = await Provider.getLanguage(input.model) const system = SystemPrompt.header(input.model.providerID) system.push( @@ -196,9 +203,22 @@ export namespace LLM { extractReasoningMiddleware({ tagName: "think", startWithReasoning: false }), ], }), - experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, + experimental_telemetry: { + isEnabled: Telemetry.isEnabled(), + functionId: `${input.agent.name}.chat`, + recordInputs: true, + recordOutputs: true, + metadata: { + "session.id": input.sessionID, + "llm.provider_id": input.model.providerID, + "llm.model_id": input.model.id, + "llm.agent": input.agent.name, + "llm.small": input.small ?? false, + "llm.tools_count": Object.keys(input.tools).length, + }, + }, }) - } + }) async function resolveTools(input: Pick) { const disabled = PermissionNext.disabled(Object.keys(input.tools), input.agent.permission) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 227ca64bb9b..d8e3a3a0dff 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -14,6 +14,7 @@ import { LLM } from "./llm" import { Config } from "@/config/config" import { SessionCompaction } from "./compaction" import { PermissionNext } from "@/permission/next" +import { Telemetry } from "@/telemetry" export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 @@ -42,6 +43,12 @@ export namespace SessionProcessor { return toolcalls[toolCallID] }, async process(streamInput: LLM.StreamInput) { + using _ = Telemetry.span("session.processor.process", { + "session.id": input.sessionID, + "session.message_id": input.assistantMessage.id, + "llm.model_id": input.model.id, + "llm.provider_id": input.model.providerID, + }) log.info("process") needsCompaction = false const shouldBreak = (await Config.get()).experimental?.continue_loop_on_deny !== true diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 87b53f526bb..ac81fc64916 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -44,6 +44,8 @@ import { SessionStatus } from "./status" import { LLM } from "./llm" import { iife } from "@/util/iife" import { Shell } from "@/shell/shell" +import { Telemetry } from "@/telemetry" +import { traced } from "@/telemetry/traced" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -147,36 +149,44 @@ export namespace SessionPrompt { }) export type PromptInput = z.infer - export const prompt = fn(PromptInput, async (input) => { - const session = await Session.get(input.sessionID) - await SessionRevert.cleanup(session) - - const message = await createUserMessage(input) - await Session.touch(input.sessionID) - - // this is backwards compatibility for allowing `tools` to be specified when - // prompting - const permissions: PermissionNext.Ruleset = [] - for (const [tool, enabled] of Object.entries(input.tools ?? {})) { - permissions.push({ - permission: tool, - action: enabled ? "allow" : "deny", - pattern: "*", - }) - } - if (permissions.length > 0) { - session.permission = permissions - await Session.update(session.id, (draft) => { - draft.permission = permissions - }) - } + export const prompt = fn( + PromptInput, + traced("session.prompt", (input) => ({ + "session.id": input.sessionID, + "session.agent": input.agent ?? "", + "llm.provider_id": input.model?.providerID ?? "", + "llm.model_id": input.model?.modelID ?? "", + }))(async (input) => { + const session = await Session.get(input.sessionID) + await SessionRevert.cleanup(session) + + const message = await createUserMessage(input) + await Session.touch(input.sessionID) + + // this is backwards compatibility for allowing `tools` to be specified when + // prompting + const permissions: PermissionNext.Ruleset = [] + for (const [tool, enabled] of Object.entries(input.tools ?? {})) { + permissions.push({ + permission: tool, + action: enabled ? "allow" : "deny", + pattern: "*", + }) + } + if (permissions.length > 0) { + session.permission = permissions + await Session.update(session.id, (draft) => { + draft.permission = permissions + }) + } - if (input.noReply === true) { - return message - } + if (input.noReply === true) { + return message + } - return loop(input.sessionID) - }) + return loop(input.sessionID) + }), + ) export async function resolvePromptParts(template: string): Promise { const parts: PromptInput["parts"] = [ @@ -263,6 +273,11 @@ export namespace SessionPrompt { }) } + using loopSpan = Telemetry.span("session.prompt.loop", { + "session.id": sessionID, + "session.step": 0, + "session.agent": "", + }) using _ = defer(() => cancel(sessionID)) let step = 0 @@ -301,6 +316,11 @@ export namespace SessionPrompt { } step++ + using stepSpan = Telemetry.span("session.prompt.step", { + "session.id": sessionID, + "session.step": step, + "session.agent": lastUser.agent, + }) if (step === 1) ensureTitle({ session, diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 83519307a32..ef5484087b3 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -16,22 +16,31 @@ import { Bus } from "@/bus" import { LLM } from "./llm" import { Agent } from "@/agent/agent" +import { traced } from "@/telemetry/traced" export namespace SessionSummary { const log = Log.create({ service: "session.summary" }) + type SummarizeInput = { + sessionID: string + messageID: string + } + export const summarize = fn( z.object({ sessionID: z.string(), messageID: z.string(), }), - async (input) => { + traced("session.summary", (input) => ({ + "session.id": input.sessionID, + "session.message_id": input.messageID, + }))(async (input) => { const all = await Session.messages({ sessionID: input.sessionID }) await Promise.all([ summarizeSession({ sessionID: input.sessionID, messages: all }), summarizeMessage({ messageID: input.messageID, messages: all }), ]) - }, + }), ) async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) { diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 69f2abc7903..1c204d7c0e5 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -6,6 +6,7 @@ import { Global } from "../global" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" +import { Telemetry, traced } from "@/telemetry" export namespace Snapshot { const log = Log.create({ service: "snapshot" }) @@ -14,6 +15,9 @@ export namespace Snapshot { if (Instance.project.vcs !== "git") return const cfg = await Config.get() if (cfg.snapshot === false) return + using span = Telemetry.span("snapshot.track", { + "snapshot.vcs": Instance.project.vcs, + }) const git = gitdir() if (await fs.mkdir(git, { recursive: true })) { await $`git init` @@ -35,7 +39,11 @@ export namespace Snapshot { .nothrow() .text() log.info("tracking", { hash, cwd: Instance.directory, git }) - return hash.trim() + const trimmedHash = hash.trim() + span.setAttributes({ + "snapshot.hash": trimmedHash, + }) + return trimmedHash } export const Patch = z.object({ @@ -71,7 +79,9 @@ export namespace Snapshot { } } - export async function restore(snapshot: string) { + export const restore = traced("snapshot.restore", (snapshot) => ({ "snapshot.hash": snapshot }))(async ( + snapshot, + ) => { log.info("restore", { commit: snapshot }) const git = gitdir() const result = @@ -88,7 +98,7 @@ export namespace Snapshot { stdout: result.stdout.toString(), }) } - } + }) export async function revert(patches: Patch[]) { const files = new Set() diff --git a/packages/opencode/src/telemetry/index.ts b/packages/opencode/src/telemetry/index.ts new file mode 100644 index 00000000000..696b2992858 --- /dev/null +++ b/packages/opencode/src/telemetry/index.ts @@ -0,0 +1,216 @@ +import { trace, type Span, SpanStatusCode, type AttributeValue } from "@opentelemetry/api" +export { traced } from "./traced.ts" +import { logs, SeverityNumber } from "@opentelemetry/api-logs" +import { resourceFromAttributes } from "@opentelemetry/resources" +import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions" +import { NodeSDK } from "@opentelemetry/sdk-node" +import { LoggerProvider, BatchLogRecordProcessor } from "@opentelemetry/sdk-logs" +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc" +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-grpc" +import { Installation } from "@/installation" +import { Log } from "@/util/log" +import { Flag } from "@/flag/flag" + +export namespace Telemetry { + const log = Log.create({ service: "telemetry" }) + + export interface Config { + enabled: boolean + endpoint: string + serviceName: string + } + + let sdk: NodeSDK | undefined + let loggerProvider: LoggerProvider | undefined + let initialized = false + + export function resolveConfig(serviceName: string, enabled?: boolean): Config { + return { + enabled: enabled ?? false, + endpoint: Flag.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4317", + serviceName, + } + } + + export function init(config: Config): void { + if (initialized) return + if (!config.enabled) return + + log.info("initializing", { endpoint: config.endpoint }) + + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: config.serviceName, + [ATTR_SERVICE_VERSION]: Installation.VERSION, + }) + + const traceExporter = new OTLPTraceExporter({ + url: config.endpoint, + }) + + const logExporter = new OTLPLogExporter({ + url: config.endpoint, + }) + + loggerProvider = new LoggerProvider({ + resource, + processors: [new BatchLogRecordProcessor(logExporter)], + }) + logs.setGlobalLoggerProvider(loggerProvider) + + sdk = new NodeSDK({ + resource, + traceExporter, + }) + + sdk.start() + initialized = true + log.info("initialized") + } + + export async function shutdown(): Promise { + if (!initialized) return + + log.info("shutting down") + await Promise.all([ + sdk?.shutdown().catch((e) => log.error("sdk shutdown error", { error: e })), + loggerProvider?.shutdown().catch((e) => log.error("logger shutdown error", { error: e })), + ]) + initialized = false + log.info("shutdown complete") + } + + export function isEnabled(): boolean { + return initialized + } + + export function getTracer(name: string) { + return trace.getTracer(name) + } + + export function getLogger(name: string) { + return logs.getLogger(name) + } + + export const NOOP_SPAN: Span = { + spanContext: () => ({ traceId: "", spanId: "", traceFlags: 0 }), + setAttribute: () => NOOP_SPAN, + setAttributes: () => NOOP_SPAN, + addEvent: () => NOOP_SPAN, + addLink: () => NOOP_SPAN, + addLinks: () => NOOP_SPAN, + setStatus: () => NOOP_SPAN, + updateName: () => NOOP_SPAN, + end: () => {}, + isRecording: () => false, + recordException: () => {}, + } + + export async function withSpan( + name: string, + attributes: Record, + fn: (span: Span) => Promise, + ): Promise { + if (!initialized) { + return fn(NOOP_SPAN) + } + + const tracer = getTracer("opencode") + return tracer.startActiveSpan(name, { attributes }, async (span) => { + try { + const result = await fn(span) + return result + } catch (error) { + if (error instanceof Error) { + span.recordException(error) + } + span.setStatus({ code: SpanStatusCode.ERROR }) + throw error + } finally { + span.end() + } + }) + } + + export const SeverityMap: Record = { + DEBUG: SeverityNumber.DEBUG, + INFO: SeverityNumber.INFO, + WARN: SeverityNumber.WARN, + ERROR: SeverityNumber.ERROR, + } + + /** + * Flattens an object into OpenTelemetry span attributes with a prefix. + * Only captures primitives (string, number, boolean), skips undefined/null. + * Truncates strings longer than 200 characters. + */ + export function flattenAttributes(prefix: string, obj: Record): Record { + const result: Record = {} + for (const key in obj) { + const value = obj[key] + if (value === undefined || value === null) continue + if (typeof value === "string") { + result[`${prefix}${key}`] = value.length > 200 ? value.slice(0, 200) + "..." : value + } else if (typeof value === "number" || typeof value === "boolean") { + result[`${prefix}${key}`] = value + } + } + return result + } + + export type DisposableSpan = Span & Disposable + + // Create a self-referential NOOP disposable span + const NOOP_DISPOSABLE_SPAN: DisposableSpan = { + spanContext: () => ({ traceId: "", spanId: "", traceFlags: 0 }), + setAttribute: function () { + return this + }, + setAttributes: function () { + return this + }, + addEvent: function () { + return this + }, + addLink: function () { + return this + }, + addLinks: function () { + return this + }, + setStatus: function () { + return this + }, + updateName: function () { + return this + }, + end: () => {}, + isRecording: () => false, + recordException: () => {}, + [Symbol.dispose]: () => {}, + } + + /** + * Creates a span that can be used with the `using` keyword for automatic cleanup. + * Returns a NOOP span if telemetry is not initialized. + * + * @example + * ```ts + * using span = Telemetry.span("my.operation", { "attr.key": "value" }) + * // span.end() is automatically called when scope exits + * ``` + */ + export function span(name: string, attrs: Record = {}): DisposableSpan { + if (!initialized) { + return NOOP_DISPOSABLE_SPAN + } + + const tracer = getTracer("opencode") + const activeSpan = tracer.startSpan(name, { attributes: attrs }) + + return Object.assign(activeSpan, { + [Symbol.dispose]: () => { + activeSpan.end() + }, + }) + } +} diff --git a/packages/opencode/src/telemetry/traced.ts b/packages/opencode/src/telemetry/traced.ts new file mode 100644 index 00000000000..91d52809170 --- /dev/null +++ b/packages/opencode/src/telemetry/traced.ts @@ -0,0 +1,33 @@ +import type { AttributeValue } from "@opentelemetry/api" +import { Telemetry } from "./index.ts" + +/** + * Higher-order function that wraps a function with OpenTelemetry tracing. + * Preserves the original function's return type and handles errors appropriately. + * + * @param name - The span name (e.g., "session.prompt", "llm.stream") + * @param attributesFn - Function that extracts span attributes from the input + * @returns A function that takes the target function and returns a traced version + * + * @example + * ```ts + * export const myFunction = traced( + * "my.operation", + * (input) => ({ "input.key": input.key }) + * )(async (input) => { + * // function body + * return result + * }) + * ``` + */ +export function traced( + name: string, + attributesFn: (input: TInput) => Record, +): (fn: (input: TInput) => Promise) => (input: TInput) => Promise { + return (fn) => { + return (input) => { + const attributes = attributesFn(input) + return Telemetry.withSpan(name, attributes, () => fn(input)) + } + } +} diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index b9e0f8a1c3e..b9090c541ae 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -228,10 +228,14 @@ export const BashTool = Tool.define("bash", async () => { }) }) + const truncated = output.length > MAX_OUTPUT_LENGTH + if (truncated) { + output = output.slice(0, MAX_OUTPUT_LENGTH) + } + let resultMetadata: String[] = [""] - if (output.length > MAX_OUTPUT_LENGTH) { - output = output.slice(0, MAX_OUTPUT_LENGTH) + if (truncated) { resultMetadata.push(`bash tool truncated output as it exceeded ${MAX_OUTPUT_LENGTH} char limit`) } @@ -254,6 +258,9 @@ export const BashTool = Tool.define("bash", async () => { output, exit: proc.exitCode, description: params.description, + aborted, + truncated, + timedOut, }, output, } diff --git a/packages/opencode/src/tool/codesearch.ts b/packages/opencode/src/tool/codesearch.ts index 369cdb45048..5a13acdc341 100644 --- a/packages/opencode/src/tool/codesearch.ts +++ b/packages/opencode/src/tool/codesearch.ts @@ -91,9 +91,10 @@ export const CodeSearchTool = Tool.define("codesearch", { clearTimeout(timeoutId) + const statusCode = response.status if (!response.ok) { const errorText = await response.text() - throw new Error(`Code search error (${response.status}): ${errorText}`) + throw new Error(`Code search error (${statusCode}): ${errorText}`) } const responseText = await response.text() @@ -107,7 +108,12 @@ export const CodeSearchTool = Tool.define("codesearch", { return { output: data.result.content[0].text, title: `Code search: ${params.query}`, - metadata: {}, + metadata: { + query: params.query, + tokensNum: params.tokensNum || 5000, + hasResults: true, + statusCode, + }, } } } @@ -117,7 +123,12 @@ export const CodeSearchTool = Tool.define("codesearch", { output: "No code snippets or documentation found. Please try a different query, be more specific about the library or programming concept, or check the spelling of framework names.", title: `Code search: ${params.query}`, - metadata: {}, + metadata: { + query: params.query, + tokensNum: params.tokensNum || 5000, + hasResults: false, + statusCode, + }, } } catch (error) { clearTimeout(timeoutId) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 787282ecd04..f1d2ff7982a 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -56,8 +56,10 @@ export const EditTool = Tool.define("edit", { let diff = "" let contentOld = "" let contentNew = "" + let fileExisted = true await FileTime.withLock(filePath, async () => { if (params.oldString === "") { + fileExisted = false contentNew = params.newString diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) await ctx.ask({ @@ -147,6 +149,8 @@ export const EditTool = Tool.define("edit", { diagnostics, diff, filediff, + errorCount: errors.length, + fileExisted, }, title: `${path.relative(Instance.worktree, filePath)}`, output, diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 4cbc5347f57..c0bd964a9fc 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -51,7 +51,7 @@ export const GrepTool = Tool.define("grep", { if (exitCode === 1) { return { title: params.pattern, - metadata: { matches: 0, truncated: false }, + metadata: { matches: 0, truncated: false, uniqueFiles: 0 }, output: "No files found", } } @@ -94,11 +94,12 @@ export const GrepTool = Tool.define("grep", { if (finalMatches.length === 0) { return { title: params.pattern, - metadata: { matches: 0, truncated: false }, + metadata: { matches: 0, truncated: false, uniqueFiles: 0 }, output: "No files found", } } + const uniqueFiles = new Set(finalMatches.map((m) => m.path)).size const outputLines = [`Found ${finalMatches.length} matches`] let currentFile = "" @@ -125,6 +126,7 @@ export const GrepTool = Tool.define("grep", { metadata: { matches: finalMatches.length, truncated, + uniqueFiles, }, output: outputLines.join("\n"), } diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts index b8638b3e904..acb8383239f 100644 --- a/packages/opencode/src/tool/ls.ts +++ b/packages/opencode/src/tool/ls.ts @@ -112,6 +112,7 @@ export const ListTool = Tool.define("list", { metadata: { count: files.length, truncated: files.length >= LIMIT, + directories: dirs.size, }, output, } diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index df4692bf6db..3066bccd0e8 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -87,7 +87,11 @@ export const LspTool = Tool.define("lsp", { return { title, - metadata: { result }, + metadata: { + result, + operation: args.operation, + resultCount: result.length, + }, output, } }, diff --git a/packages/opencode/src/tool/multiedit.ts b/packages/opencode/src/tool/multiedit.ts index 7f562f4737a..71720cb458b 100644 --- a/packages/opencode/src/tool/multiedit.ts +++ b/packages/opencode/src/tool/multiedit.ts @@ -22,25 +22,44 @@ export const MultiEditTool = Tool.define("multiedit", { }), async execute(params, ctx) { const tool = await EditTool.init() - const results = [] + const results: Array<{ + success: boolean + metadata?: Awaited>["metadata"] + output?: string + }> = [] for (const [, edit] of params.edits.entries()) { - const result = await tool.execute( - { - filePath: params.filePath, - oldString: edit.oldString, - newString: edit.newString, - replaceAll: edit.replaceAll, - }, - ctx, - ) - results.push(result) + try { + const result = await tool.execute( + { + filePath: params.filePath, + oldString: edit.oldString, + newString: edit.newString, + replaceAll: edit.replaceAll, + }, + ctx, + ) + results.push({ success: true, metadata: result.metadata, output: result.output }) + } catch { + results.push({ success: false }) + } } + + const successfulEdits = results.filter((r) => r.success).length + const failedEdits = results.filter((r) => !r.success).length + const totalAdditions = results.reduce((sum, r) => sum + (r.metadata?.filediff?.additions ?? 0), 0) + const totalDeletions = results.reduce((sum, r) => sum + (r.metadata?.filediff?.deletions ?? 0), 0) + + const successfulResults = results.filter((r) => r.success) return { title: path.relative(Instance.worktree, params.filePath), metadata: { - results: results.map((r) => r.metadata), + results: successfulResults.map((r) => r.metadata), + successfulEdits, + failedEdits, + totalAdditions, + totalDeletions, }, - output: results.at(-1)!.output, + output: successfulResults.at(-1)?.output ?? "", } }, }) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index a0f50129e50..1994a447515 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -77,6 +77,11 @@ export const ReadTool = Tool.define("read", { output: msg, metadata: { preview: msg, + isImage, + isBinary: false, + linesRead: 0, + totalLines: 0, + truncated: false, }, attachments: [ { @@ -128,6 +133,11 @@ export const ReadTool = Tool.define("read", { output, metadata: { preview, + isImage: false, + isBinary: false, + linesRead: content.length, + totalLines, + truncated: hasMoreLines, }, } }, diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 00a081eaca0..4ff15b64b9d 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -70,6 +70,7 @@ export const SkillTool = Tool.define("skill", async () => { metadata: { name: skill.name, dir, + skillFound: true, }, } }, diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 112edc3dc88..81f28b949dd 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -42,10 +42,14 @@ export const TaskTool = Tool.define("task", async () => { const agent = await Agent.get(params.subagent_type) if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) + let isNewSession = true const session = await iife(async () => { if (params.session_id) { const found = await Session.get(params.session_id).catch(() => {}) - if (found) return found + if (found) { + isNewSession = false + return found + } } return await Session.create({ @@ -159,6 +163,8 @@ export const TaskTool = Tool.define("task", async () => { metadata: { summary, sessionId: session.id, + toolCallsCount: summary.length, + isNewSession, }, output, } diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index 440f1563c70..80f28c81b00 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -20,11 +20,15 @@ export const TodoWriteTool = Tool.define("todowrite", { sessionID: ctx.sessionID, todos: params.todos, }) + const completedCount = params.todos.filter((x) => x.status === "completed").length + const pendingCount = params.todos.length - completedCount return { - title: `${params.todos.filter((x) => x.status !== "completed").length} todos`, + title: `${pendingCount} todos`, output: JSON.stringify(params.todos, null, 2), metadata: { todos: params.todos, + completedCount, + pendingCount, }, } }, @@ -42,10 +46,13 @@ export const TodoReadTool = Tool.define("todoread", { }) const todos = await Todo.get(ctx.sessionID) + const completedCount = todos.filter((x) => x.status === "completed").length return { - title: `${todos.filter((x) => x.status !== "completed").length} todos`, + title: `${todos.length - completedCount} todos`, metadata: { todos, + todoCount: todos.length, + completedCount, }, output: JSON.stringify(todos, null, 2), } diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 434a3d42660..1be885f31be 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -2,6 +2,7 @@ import z from "zod" import type { MessageV2 } from "../session/message-v2" import type { Agent } from "../agent/agent" import type { PermissionNext } from "../permission/next" +import { Telemetry } from "../telemetry" export namespace Tool { interface Metadata { @@ -64,7 +65,19 @@ export namespace Tool { { cause: error }, ) } - return execute(args, ctx) + return Telemetry.withSpan( + `tool.${id}.execute`, + { + "tool.name": id, + "session.id": ctx.sessionID, + ...Telemetry.flattenAttributes("tool.param.", args as Record), + }, + async (span) => { + const result = await execute(args, ctx) + span.setAttributes(Telemetry.flattenAttributes("tool.", result.metadata as Record)) + return result + }, + ) } return toolInfo }, diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts index 634c68f4eea..05f45b7b62a 100644 --- a/packages/opencode/src/tool/webfetch.ts +++ b/packages/opencode/src/tool/webfetch.ts @@ -85,8 +85,15 @@ export const WebFetchTool = Tool.define("webfetch", { const content = new TextDecoder().decode(arrayBuffer) const contentType = response.headers.get("content-type") || "" + const statusCode = response.status + const responseSize = arrayBuffer.byteLength const title = `${params.url} (${contentType})` + const metadata = { + statusCode, + contentType, + responseSize, + } // Handle content based on requested format and actual content type switch (params.format) { @@ -96,13 +103,13 @@ export const WebFetchTool = Tool.define("webfetch", { return { output: markdown, title, - metadata: {}, + metadata, } } return { output: content, title, - metadata: {}, + metadata, } case "text": @@ -111,27 +118,27 @@ export const WebFetchTool = Tool.define("webfetch", { return { output: text, title, - metadata: {}, + metadata, } } return { output: content, title, - metadata: {}, + metadata, } case "html": return { output: content, title, - metadata: {}, + metadata, } default: return { output: content, title, - metadata: {}, + metadata, } } }, diff --git a/packages/opencode/src/tool/websearch.ts b/packages/opencode/src/tool/websearch.ts index f6df36f10f9..3dbf2ed1b9b 100644 --- a/packages/opencode/src/tool/websearch.ts +++ b/packages/opencode/src/tool/websearch.ts @@ -111,16 +111,24 @@ export const WebSearchTool = Tool.define("websearch", { const responseText = await response.text() + const searchType = params.type || "auto" + // Parse SSE response const lines = responseText.split("\n") for (const line of lines) { if (line.startsWith("data: ")) { const data: McpSearchResponse = JSON.parse(line.substring(6)) if (data.result && data.result.content && data.result.content.length > 0) { + const resultCount = data.result.content.length return { output: data.result.content[0].text, title: `Web search: ${params.query}`, - metadata: {}, + metadata: { + statusCode: response.status, + resultCount, + hasResults: true, + searchType, + }, } } } @@ -129,7 +137,12 @@ export const WebSearchTool = Tool.define("websearch", { return { output: "No search results found. Please try a different query.", title: `Web search: ${params.query}`, - metadata: {}, + metadata: { + statusCode: response.status, + resultCount: 0, + hasResults: false, + searchType, + }, } } catch (error) { clearTimeout(timeoutId) diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index a0ca6b14f7c..fb4f40946fc 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -56,8 +56,10 @@ export const WriteTool = Tool.define("write", { const diagnostics = await LSP.diagnostics() const normalizedFilepath = Filesystem.normalizePath(filepath) let projectDiagnosticsCount = 0 + let errorCount = 0 for (const [file, issues] of Object.entries(diagnostics)) { const errors = issues.filter((item) => item.severity === 1) + errorCount += errors.length if (errors.length === 0) continue const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE) const suffix = @@ -77,6 +79,8 @@ export const WriteTool = Tool.define("write", { diagnostics, filepath, exists: exists, + errorCount, + fileCreated: !exists, }, output, } diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 6941310bbbd..ec66226a722 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -3,6 +3,84 @@ import fs from "fs/promises" import { Global } from "../global" import z from "zod" +// Lazy-loaded telemetry to avoid circular dependency +let telemetryModule: typeof import("@/telemetry") | undefined +let telemetryLoading = false + +function emitOtelLog(level: string, message: string, attributes: Record) { + // Prevent recursive calls during module loading + if (telemetryLoading) return + if (!telemetryModule) { + telemetryLoading = true + import("@/telemetry") + .then((mod) => { + telemetryModule = mod + telemetryLoading = false + doEmit(mod.Telemetry, level, message, attributes) + }) + .catch(() => { + telemetryLoading = false + }) + return + } + doEmit(telemetryModule.Telemetry, level, message, attributes) +} + +function doEmit( + Telemetry: (typeof import("@/telemetry"))["Telemetry"], + level: string, + message: string, + attributes: Record, +) { + if (!Telemetry.isEnabled()) return + + const logger = Telemetry.getLogger("opencode") + const severityNumber = Telemetry.SeverityMap[level] + if (!severityNumber) return + + // Build body with key=value pairs like file logs, service first + const service = attributes.service + const otherAttrs = Object.entries(attributes).filter(([key]) => key !== "service") + + const formatValue = (key: string, value: any): string => { + if (value instanceof Error) return `${key}=${value.message}` + if (typeof value === "object") return `${key}=${JSON.stringify(value)}` + return `${key}=${value}` + } + + const parts: string[] = [] + if (service) parts.push(`service=${service}`) + for (const [key, value] of otherAttrs) { + if (value !== undefined && value !== null) { + parts.push(formatValue(key, value)) + } + } + parts.push(message) + + const body = parts.join(" ") + + // Find any Error in attributes and extract for OTEL exception semantic conventions + const errorEntry = Object.entries(attributes).find(([_, v]) => v instanceof Error) + const finalAttributes = { ...attributes } + + if (errorEntry) { + const error = errorEntry[1] as Error + // Add OTEL semantic convention attributes for exceptions + finalAttributes["exception.type"] = error.name || "Error" + finalAttributes["exception.message"] = error.message + if (error.stack) { + finalAttributes["exception.stacktrace"] = error.stack + } + } + + logger.emit({ + severityNumber, + severityText: level, + body, + attributes: finalAttributes, + }) +} + export namespace Log { export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) export type Level = z.infer @@ -128,21 +206,25 @@ export namespace Log { debug(message?: any, extra?: Record) { if (shouldLog("DEBUG")) { write("DEBUG " + build(message, extra)) + emitOtelLog("DEBUG", String(message ?? ""), { ...tags, ...extra }) } }, info(message?: any, extra?: Record) { if (shouldLog("INFO")) { write("INFO " + build(message, extra)) + emitOtelLog("INFO", String(message ?? ""), { ...tags, ...extra }) } }, error(message?: any, extra?: Record) { if (shouldLog("ERROR")) { write("ERROR " + build(message, extra)) + emitOtelLog("ERROR", String(message ?? ""), { ...tags, ...extra }) } }, warn(message?: any, extra?: Record) { if (shouldLog("WARN")) { write("WARN " + build(message, extra)) + emitOtelLog("WARN", String(message ?? ""), { ...tags, ...extra }) } }, tag(key: string, value: string) { diff --git a/packages/opencode/test/telemetry/telemetry.test.ts b/packages/opencode/test/telemetry/telemetry.test.ts new file mode 100644 index 00000000000..c6a169d550f --- /dev/null +++ b/packages/opencode/test/telemetry/telemetry.test.ts @@ -0,0 +1,44 @@ +import { test, expect, describe } from "bun:test" +import { Telemetry } from "../../src/telemetry" + +describe("Telemetry.resolveConfig", () => { + const defaultEndpoint = "http://localhost:4317" + + test("returns disabled config when not enabled", () => { + const config = Telemetry.resolveConfig("test-service", undefined) + expect(config).toEqual({ + enabled: false, + endpoint: defaultEndpoint, + serviceName: "test-service", + }) + }) + + test("returns enabled config when true", () => { + const config = Telemetry.resolveConfig("test-service", true) + expect(config).toEqual({ + enabled: true, + endpoint: defaultEndpoint, + serviceName: "test-service", + }) + }) + + test("returns disabled config when false", () => { + const config = Telemetry.resolveConfig("test-service", false) + expect(config).toEqual({ + enabled: false, + endpoint: defaultEndpoint, + serviceName: "test-service", + }) + }) + + test("uses custom service name", () => { + const config = Telemetry.resolveConfig("opencode-cli", true) + expect(config.serviceName).toBe("opencode-cli") + }) +}) + +describe("Telemetry.isEnabled", () => { + test("returns false before initialization", () => { + expect(typeof Telemetry.isEnabled).toBe("function") + }) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index fb3be8208ac..b43ec265694 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1664,7 +1664,7 @@ export type Config = { */ batch_tool?: boolean /** - * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) + * Enable OpenTelemetry tracing. Set OTEL_EXPORTER_OTLP_ENDPOINT env var for endpoint. */ openTelemetry?: boolean /** diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index c3af722451d..482914edcdd 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -9191,7 +9191,7 @@ "type": "boolean" }, "openTelemetry": { - "description": "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)", + "description": "Enable OpenTelemetry tracing. Set OTEL_EXPORTER_OTLP_ENDPOINT env var for endpoint.", "type": "boolean" }, "primary_tools": { diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index d9076e13a36..53aee3a93c8 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -564,6 +564,25 @@ The `experimental` key contains options that are under active development. Experimental options are not stable. They may change or be removed without notice. ::: +#### OpenTelemetry + +Enable OpenTelemetry tracing for monitoring and debugging. This exports traces and structured logs to an OTLP-compatible collector like [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview). + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "experimental": { + "openTelemetry": true + } +} +``` + +Set the endpoint using the standard `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable (defaults to `http://localhost:4317`). + +```bash title="Terminal" +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 opencode +``` + --- ## Variables diff --git a/temp.md b/temp.md new file mode 100644 index 00000000000..ffdfda03d04 --- /dev/null +++ b/temp.md @@ -0,0 +1,24 @@ +## Enabling OpenTelemetry + +1. Add to your **global** config (`~/.config/opencode/opencode.json`): + +```json +{ + "experimental": { + "openTelemetry": true + } +} +``` + +> Note: Project-level config (`.opencode/opencode.jsonc`) does not work for this setting. + +2. Run with Aspire Dashboard: + +```bash +cd packages/opencode +bun run dev:otel +``` + +3. Open dashboard at http://localhost:18888 + +The `OTEL_EXPORTER_OTLP_ENDPOINT` env var controls the endpoint (defaults to `http://localhost:4317`).