diff --git a/.rubocop.yml b/.rubocop.yml index 021d4aae..5e623b1f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,6 +15,7 @@ AllCops: - "test/dummy/tmp/**/*" - "node_modules/**/*" - "vendor/**/*" + - "lib/generators/**/templates/**/*" Style/StringLiterals: EnforcedStyle: double_quotes diff --git a/.tool-versions b/.tool-versions index 9e57e968..489e0527 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ nodejs 24.7.0 +ruby 3.4.8 diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 02262704..cf41e49a 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -99,6 +99,7 @@ export default defineConfig({ { text: 'Providers', link: '/providers' }, { text: 'Configuration', link: '/framework/configuration' }, { text: 'Instrumentation', link: '/framework/instrumentation' }, + { text: 'Telemetry', link: '/framework/telemetry' }, { text: 'Retries', link: '/framework/retries' }, { text: 'Rails Integration', link: '/framework/rails' }, { text: 'Testing', link: '/framework/testing' }, diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 887caed4..364b8824 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -102,7 +102,7 @@ rgb(255, 100, 100) 20%, rgb(250, 52, 59) 40%, rgb(255, 100, 100) 60%, - rgb(250, 52, 59) 100% + rgb(250, 52, 59) 80% ); --vp-home-hero-image-background-image: radial-gradient( diff --git a/docs/framework/telemetry.md b/docs/framework/telemetry.md new file mode 100644 index 00000000..296ab674 --- /dev/null +++ b/docs/framework/telemetry.md @@ -0,0 +1,380 @@ +--- +title: Telemetry & Observability +description: Collect and report agent traces to monitor AI operations, track costs, and debug generation flows with hosted or self-hosted observability. +--- + +# {{ $frontmatter.title }} + +ActiveAgent includes built-in telemetry for collecting and reporting agent traces. Monitor your AI operations, track token usage and costs, and debug generation flows with comprehensive observability. + +## Overview + +The telemetry system captures: +- **Generation Traces**: Full lifecycle of agent generations +- **Token Usage**: Input, output, and thinking tokens per request +- **Tool Calls**: Invocations with timing and results +- **Errors**: Exceptions with backtraces for debugging +- **Performance Metrics**: Response times and latencies + +## Quick Start + +### Hosted Service (ActiveAgents.ai) + +The fastest way to get started is with the hosted observability service: + +```yaml +# config/active_agent.yml +development: + openai: + service: "OpenAI" + access_token: <%= Rails.application.credentials.dig(:openai, :access_token) %> + + telemetry: + enabled: true + endpoint: https://api.activeagents.ai/v1/traces + api_key: <%= Rails.application.credentials.dig(:activeagents, :api_key) %> +``` + +### Self-Hosted + +For complete control, run your own telemetry endpoint: + +```yaml +# config/active_agent.yml +production: + telemetry: + enabled: true + endpoint: https://observability.mycompany.com/v1/traces + api_key: <%= ENV["TELEMETRY_API_KEY"] %> + service_name: my-rails-app +``` + +## Configuration + +### YAML Configuration + +Configure telemetry in your `config/active_agent.yml`: + +```yaml +telemetry: + enabled: true + endpoint: https://api.activeagents.ai/v1/traces + api_key: <%= Rails.application.credentials.dig(:activeagents, :api_key) %> + sample_rate: 1.0 # 1.0 = 100%, 0.5 = 50% + batch_size: 100 # Traces per batch + flush_interval: 5 # Seconds between flushes + service_name: my-app # Override app name + capture_bodies: false # Include request/response bodies + resource_attributes: # Custom attributes for all traces + deployment: production + team: ai-platform +``` + +### Programmatic Configuration + +Configure in an initializer for dynamic settings: + +```ruby +# config/initializers/active_agent.rb +ActiveAgent::Telemetry.configure do |config| + config.enabled = Rails.env.production? + config.endpoint = ENV.fetch("TELEMETRY_ENDPOINT", "https://api.activeagents.ai/v1/traces") + config.api_key = Rails.application.credentials.dig(:activeagents, :api_key) + config.sample_rate = 1.0 + config.service_name = Rails.application.class.module_parent_name.underscore +end +``` + +### Rails Configuration + +You can also configure via Rails config: + +```ruby +# config/application.rb +config.active_agent.telemetry = { + enabled: true, + endpoint: "https://api.activeagents.ai/v1/traces", + api_key: Rails.application.credentials.dig(:activeagents, :api_key) +} +``` + +## Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | Boolean | `false` | Enable telemetry collection | +| `endpoint` | String | `https://api.activeagents.ai/v1/traces` | Telemetry receiver URL | +| `api_key` | String | `nil` | Authentication token | +| `sample_rate` | Float | `1.0` | Sampling rate (0.0 - 1.0) | +| `batch_size` | Integer | `100` | Traces per batch before flush | +| `flush_interval` | Integer | `5` | Seconds between auto-flushes | +| `timeout` | Integer | `10` | HTTP request timeout | +| `capture_bodies` | Boolean | `false` | Include message bodies | +| `service_name` | String | App name | Service identifier | +| `environment` | String | `Rails.env` | Environment name | +| `resource_attributes` | Hash | `{}` | Custom trace attributes | +| `redact_attributes` | Array | `["password", "secret", ...]` | Keys to redact | + +## Trace Structure + +Each trace captures the complete generation lifecycle: + +``` +Trace: WeatherAgent.forecast +├── Span: agent.prompt (prompt preparation) +├── Span: llm.generate (API call) +│ ├── tokens: { input: 150, output: 75, total: 225 } +│ └── model: "gpt-4o" +└── Span: tool.get_weather (tool invocation) + └── duration: 234ms +``` + +### Span Types + +| Type | Description | +|------|-------------| +| `root` | Root span for the entire generation | +| `prompt` | Prompt preparation and rendering | +| `llm` | LLM API call | +| `tool` | Tool/function invocation | +| `thinking` | Extended thinking (Anthropic) | +| `embedding` | Embedding generation | +| `error` | Error handling | + +## Manual Tracing + +Add custom spans to your traces: + +```ruby +class WeatherAgent < ApplicationAgent + def forecast(location:) + @location = location + + # Add custom span + if ActiveAgent::Telemetry.enabled? + span = ActiveAgent::Telemetry.span("geocode.lookup") + span.set_attribute("location", location) + coordinates = geocode_location(location) + span.finish + end + + prompt + end +end +``` + +### Trace Block + +Use the trace block for automatic timing and error handling: + +```ruby +ActiveAgent::Telemetry.trace("custom.operation") do |span| + span.set_attribute("user_id", current_user.id) + span.set_attribute("operation", "data_enrichment") + + result = perform_operation + + span.set_tokens(input: 100, output: 50) + result +end +``` + +## Sampling + +Control trace volume with sampling: + +```ruby +ActiveAgent::Telemetry.configure do |config| + # Sample 10% of production traffic + config.sample_rate = Rails.env.production? ? 0.1 : 1.0 +end +``` + +Sampling is deterministic per-trace, so all spans within a trace are included or excluded together. + +## Flushing & Shutdown + +Traces are batched and sent asynchronously. Force flush when needed: + +```ruby +# Flush buffered traces immediately +ActiveAgent::Telemetry.flush + +# Graceful shutdown (flush and wait) +ActiveAgent::Telemetry.shutdown +``` + +### Rails Integration + +Telemetry automatically flushes on Rails shutdown: + +```ruby +# config/initializers/active_agent.rb +at_exit { ActiveAgent::Telemetry.shutdown } +``` + +## Self-Hosting + +### Endpoint Requirements + +Your telemetry endpoint must accept POST requests with: + +**Headers:** +- `Content-Type: application/json` +- `Authorization: Bearer ` +- `X-Service-Name: ` +- `X-Environment: ` + +**Payload:** +```json +{ + "traces": [ + { + "trace_id": "abc123...", + "service_name": "my-app", + "environment": "production", + "timestamp": "2024-01-15T10:30:00.123456Z", + "resource_attributes": {}, + "spans": [ + { + "span_id": "def456", + "trace_id": "abc123...", + "parent_span_id": null, + "name": "WeatherAgent.forecast", + "type": "root", + "start_time": "2024-01-15T10:30:00.123456Z", + "end_time": "2024-01-15T10:30:01.234567Z", + "duration_ms": 1111.11, + "status": "OK", + "attributes": { + "agent.class": "WeatherAgent", + "agent.action": "forecast" + }, + "tokens": { + "input": 150, + "output": 75, + "total": 225 + }, + "events": [] + } + ] + } + ], + "sdk": { + "name": "activeagent", + "version": "0.5.0", + "language": "ruby", + "runtime_version": "3.3.0" + } +} +``` + +### Example Rails Endpoint + +```ruby +# app/controllers/api/traces_controller.rb +class Api::TracesController < ApplicationController + skip_before_action :verify_authenticity_token + before_action :authenticate_api_key! + + def create + traces = params[:traces] + + traces.each do |trace| + Trace.create!( + trace_id: trace[:trace_id], + service_name: trace[:service_name], + environment: trace[:environment], + spans: trace[:spans], + timestamp: trace[:timestamp] + ) + end + + head :accepted + end + + private + + def authenticate_api_key! + api_key = request.headers["Authorization"]&.gsub(/^Bearer /, "") + head :unauthorized unless ApiKey.exists?(key: api_key) + end +end +``` + +## Comparison with Instrumentation + +ActiveAgent provides two complementary observability systems: + +| Feature | Instrumentation | Telemetry | +|---------|-----------------|-----------| +| **Purpose** | Local logging & metrics | Distributed tracing | +| **Transport** | ActiveSupport::Notifications | HTTP POST | +| **Destination** | Rails logs, local metrics | Remote endpoint | +| **Use Case** | Debugging, local monitoring | Production observability | +| **Overhead** | Minimal | Async, batched | + +Use **Instrumentation** for local development and debugging. Use **Telemetry** for production observability and analytics. + +## Security Considerations + +### Sensitive Data + +By default, telemetry redacts common sensitive attributes: +- `password`, `secret`, `token`, `key`, `credential`, `api_key` + +Add custom redactions: + +```ruby +ActiveAgent::Telemetry.configure do |config| + config.redact_attributes += ["ssn", "credit_card"] +end +``` + +### Message Bodies + +Message bodies are **not captured by default**. Enable with caution: + +```ruby +ActiveAgent::Telemetry.configure do |config| + config.capture_bodies = true # Only in controlled environments +end +``` + +## Troubleshooting + +### Traces Not Appearing + +1. **Check enabled status:** + ```ruby + puts ActiveAgent::Telemetry.enabled? # Should be true + ``` + +2. **Verify configuration:** + ```ruby + puts ActiveAgent::Telemetry.configuration.to_h + ``` + +3. **Check logs for errors:** + ```ruby + ActiveAgent::Telemetry.configure do |config| + config.logger = Rails.logger + end + ``` + +### High Memory Usage + +Reduce batch size or increase flush frequency: + +```ruby +ActiveAgent::Telemetry.configure do |config| + config.batch_size = 25 + config.flush_interval = 2 +end +``` + +## Related Documentation + +- **[Instrumentation](/framework/instrumentation)** - Local logging with ActiveSupport::Notifications +- **[Usage Statistics](/actions/usage)** - Token usage and cost tracking +- **[Configuration](/framework/configuration)** - General framework configuration diff --git a/docs/public/activeagent-logo-dark.svg b/docs/public/activeagent-logo-dark.svg index 49e42c81..295af47c 100644 --- a/docs/public/activeagent-logo-dark.svg +++ b/docs/public/activeagent-logo-dark.svg @@ -1,85 +1,386 @@ - - - ActiveAgent Dark - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + ActiveAgent Dark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ActiveAgent Dark + + + diff --git a/docs/public/activeagent-logo.svg b/docs/public/activeagent-logo.svg index ba862d22..657ce3d5 100644 --- a/docs/public/activeagent-logo.svg +++ b/docs/public/activeagent-logo.svg @@ -1,72 +1,386 @@ - - - ActiveAgent - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + ActiveAgent Dark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + + + + + ActiveAgent Dark + + + + diff --git a/lib/active_agent.rb b/lib/active_agent.rb index f39a0164..adc56b5d 100644 --- a/lib/active_agent.rb +++ b/lib/active_agent.rb @@ -106,6 +106,8 @@ module ActiveAgent autoload :Rescue, "active_agent/concerns/rescue" autoload :Tooling, "active_agent/concerns/tooling" autoload :View, "active_agent/concerns/view" + autoload :Telemetry + autoload :Dashboard class << self # Eagerly loads all ActiveAgent components and descendant agent classes. diff --git a/lib/active_agent/dashboard.rb b/lib/active_agent/dashboard.rb new file mode 100644 index 00000000..b7977661 --- /dev/null +++ b/lib/active_agent/dashboard.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require "active_agent/dashboard/engine" + +module ActiveAgent + # Dashboard engine for visualizing telemetry data and managing agents. + # + # Mount the engine in your routes to access the full dashboard: + # + # # config/routes.rb + # mount ActiveAgent::Dashboard::Engine => "/active_agent" + # + # The dashboard provides: + # - Agent management: Create, edit, version, and execute agents + # - Traces view: See all agent invocations with spans, timing, and token usage + # - Metrics view: Aggregate statistics and charts + # - Sandbox execution: Run agents in isolated environments + # - Session recordings: Capture and replay browser sessions + # + # = Configuration Modes + # + # == Local Mode (default) + # For self-hosted, single-tenant deployments: + # + # ActiveAgent::Dashboard.configure do |config| + # config.authentication_method = ->(controller) { controller.authenticate_admin! } + # config.sandbox_service = :local # Docker/Incus + # end + # + # == Multi-tenant Mode + # For SaaS platforms with multiple accounts: + # + # ActiveAgent::Dashboard.configure do |config| + # config.multi_tenant = true + # config.account_class = "Account" + # config.user_class = "User" + # config.current_account_method = :current_account + # config.current_user_method = :current_user + # config.authentication_method = ->(controller) { controller.authenticate_user! } + # config.sandbox_service = :cloud_run # Managed + # config.use_inertia = true + # end + # + module Dashboard + class << self + # Authentication method to call on controllers + # @return [Proc, nil] A proc that receives the controller instance + attr_accessor :authentication_method + + # Enable multi-tenant mode (requires account association) + # @return [Boolean] + attr_accessor :multi_tenant + + # Class name for the Account model (multi-tenant mode) + # @return [String, nil] + attr_accessor :account_class + + # Class name for the User model + # @return [String, nil] + attr_accessor :user_class + + # Method to call on controller to get current account (multi-tenant mode) + # @return [Symbol, nil] + attr_accessor :current_account_method + + # Method to call on controller to get current user + # @return [Symbol, nil] + attr_accessor :current_user_method + + # Custom trace model class (for host app overrides) + # @return [String, nil] + attr_accessor :trace_model_class + + # Enable React/Inertia frontend instead of ERB + # @return [Boolean] + attr_accessor :use_inertia + + # Custom layout for the dashboard + # @return [String, nil] + attr_accessor :layout + + # Sandbox service type (:local, :cloud_run, :kubernetes) + # @return [Symbol] + attr_accessor :sandbox_service + + # Custom sandbox limits (overrides defaults) + # @return [Hash, nil] + attr_accessor :sandbox_limits + + # Storage service for screenshots/snapshots + # @return [Object, nil] Object responding to #signed_url_for and #fetch_snapshot + attr_accessor :storage_service + + # Base controller class for dashboard controllers + # @return [String] + attr_accessor :base_controller_class + + # Returns whether multi-tenant mode is enabled. + # + # @return [Boolean] + def multi_tenant? + @multi_tenant == true + end + + # Returns the trace model class to use. + # + # @return [Class] The trace model class + def trace_model + if trace_model_class + trace_model_class.constantize + else + ActiveAgent::TelemetryTrace + end + end + + # Returns the agent model class to use. + # + # @return [Class] The agent model class + def agent_model + ActiveAgent::Dashboard::Agent + end + + # Configures the dashboard. + # + # @yield [config] Configuration block + def configure + yield self + end + + # Reset configuration to defaults + def reset! + @authentication_method = nil + @multi_tenant = false + @account_class = nil + @user_class = nil + @current_account_method = nil + @current_user_method = nil + @trace_model_class = nil + @use_inertia = false + @layout = nil + @sandbox_service = :local + @sandbox_limits = nil + @storage_service = nil + @base_controller_class = "ActionController::Base" + end + end + + # Set defaults + reset! + end +end diff --git a/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb new file mode 100644 index 00000000..f6889140 --- /dev/null +++ b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + module Api + # Telemetry ingestion endpoint. + # + # Receives traces from ActiveAgent::Telemetry::Reporter and stores them + # for analysis and visualization in the dashboard. + # + # Supports two modes: + # - Local mode: No authentication, synchronous processing + # - Multi-tenant mode: Bearer token auth, async processing via job + # + # @example Local mode request + # POST /active_agent/api/traces + # Content-Type: application/json + # + # { + # "traces": [...], + # "sdk": { "name": "activeagent", "version": "0.5.0" } + # } + # + # @example Multi-tenant mode request + # POST /active_agent/api/traces + # Authorization: Bearer + # Content-Type: application/json + # + # { + # "traces": [...], + # "sdk": { "name": "activeagent", "version": "0.5.0" } + # } + # + class TracesController < ActionController::API + before_action :authenticate_api_key!, if: -> { ActiveAgent::Dashboard.multi_tenant? } + + # POST /active_agent/api/traces + def create + traces = params[:traces] || [] + sdk_info = params[:sdk] || {} + + return head :accepted if traces.empty? + + if ActiveAgent::Dashboard.multi_tenant? + # Multi-tenant mode: process in background + ActiveAgent::ProcessTelemetryTracesJob.perform_later( + account_id: @account&.id, + traces: traces.as_json, + sdk_info: sdk_info.as_json, + received_at: Time.current.iso8601(6) + ) + else + # Local mode: process synchronously + process_traces_synchronously(traces, sdk_info) + end + + head :accepted + rescue ActionController::ParameterMissing => e + render json: { error: e.message }, status: :bad_request + rescue StandardError => e + Rails.logger.error("[ActiveAgent::Dashboard] Trace ingestion error: #{e.message}") + render json: { error: "Internal server error" }, status: :internal_server_error + end + + private + + # Authenticates the request using Bearer token from Authorization header. + # Only used in multi-tenant mode. + def authenticate_api_key! + token = extract_bearer_token + + if token.blank? + render json: { error: "Missing Authorization header" }, status: :unauthorized + return + end + + account_class = ActiveAgent::Dashboard.account_class.constantize + @account = account_class.find_by(telemetry_api_key: token) + + if @account.nil? + render json: { error: "Invalid API key" }, status: :unauthorized + return + end + + # Track usage for rate limiting (if the account responds to it) + @account.increment_telemetry_usage! if @account.respond_to?(:increment_telemetry_usage!) + end + + # Extracts Bearer token from Authorization header. + def extract_bearer_token + auth_header = request.headers["Authorization"] + return nil if auth_header.blank? + + match = auth_header.match(/^Bearer\s+(.+)$/i) + match[1] if match + end + + # Process traces synchronously for local development. + def process_traces_synchronously(traces, sdk_info) + model = ActiveAgent::Dashboard.trace_model + + traces.each do |trace| + # Skip if trace already exists (idempotency) + next if model.exists?(trace_id: trace["trace_id"]) + + model.create_from_payload(trace, sdk_info) + rescue StandardError => e + Rails.logger.error( + "[ActiveAgent::Dashboard] Failed to process trace #{trace['trace_id']}: " \ + "#{e.class} - #{e.message}" + ) + end + end + end + end + end +end diff --git a/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb new file mode 100644 index 00000000..9c7c19be --- /dev/null +++ b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Base controller for the ActiveAgent Dashboard. + # + # Handles authentication and provides helper methods for multi-tenant mode. + class ApplicationController < ActionController::Base + protect_from_forgery with: :exception + + before_action :authenticate_dashboard! + + # Add the engine's view path + prepend_view_path ActiveAgent::Dashboard::Engine.dashboard_root.join("app", "views").to_s + + # Use custom layout if configured, otherwise use engine layout + layout -> { ActiveAgent::Dashboard.layout || "active_agent/dashboard/application" } + + helper_method :current_user, :current_owner + + private + + def authenticate_dashboard! + return if ActiveAgent::Dashboard.authentication_method.nil? + + result = ActiveAgent::Dashboard.authentication_method.call(self) + head :unauthorized unless result + rescue StandardError => e + Rails.logger.error("[ActiveAgent::Dashboard] Authentication error: #{e.message}") + head :unauthorized + end + + # Returns the current user from the host application. + def current_user + return nil unless ActiveAgent::Dashboard.current_user_method + + send(ActiveAgent::Dashboard.current_user_method) + rescue NoMethodError + nil + end + + # Returns the current owner (account in multi-tenant, user otherwise). + def current_owner + if ActiveAgent::Dashboard.multi_tenant? && ActiveAgent::Dashboard.current_account_method + send(ActiveAgent::Dashboard.current_account_method) + else + current_user + end + rescue NoMethodError + nil + end + end + end +end diff --git a/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb new file mode 100644 index 00000000..1e794107 --- /dev/null +++ b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Main dashboard controller showing overview metrics and recent activity. + # + class DashboardController < ApplicationController + def index + @agents = fetch_agents.limit(10) + @recent_runs = fetch_recent_runs.limit(10) + @recent_traces = fetch_recent_traces.limit(10) + @metrics = calculate_metrics + + if ActiveAgent::Dashboard.use_inertia && defined?(InertiaRails) + render inertia: "Dashboard", props: { + agents: serialize_agents(@agents), + recentRuns: serialize_runs(@recent_runs), + recentTraces: serialize_traces(@recent_traces), + metrics: @metrics, + user: current_user_props, + account: current_account_props + } + else + render :index + end + end + + private + + def fetch_agents + agents = Agent.order(updated_at: :desc) + agents = agents.for_owner(current_owner) if current_owner + agents + end + + def fetch_recent_runs + runs = AgentRun.includes(:agent).recent + if current_owner && ActiveAgent::Dashboard.multi_tenant? + runs = runs.joins(:agent).where(agents: { account_id: current_owner.id }) + end + runs + end + + def fetch_recent_traces + traces = ActiveAgent::Dashboard.trace_model.recent + traces = traces.for_account(current_owner) if current_owner && ActiveAgent::Dashboard.multi_tenant? + traces + end + + def calculate_metrics + traces_24h = ActiveAgent::Dashboard.trace_model.where("created_at > ?", 24.hours.ago) + runs_24h = AgentRun.where("created_at > ?", 24.hours.ago) + + if current_owner && ActiveAgent::Dashboard.multi_tenant? + traces_24h = traces_24h.for_account(current_owner) + runs_24h = runs_24h.joins(:agent).where(agents: { account_id: current_owner.id }) + end + + { + total_agents: fetch_agents.count, + active_agents: fetch_agents.active_agents.count, + total_runs_24h: runs_24h.count, + successful_runs_24h: runs_24h.successful.count, + failed_runs_24h: runs_24h.failed_runs.count, + total_traces_24h: traces_24h.count, + total_tokens_24h: traces_24h.sum(:total_input_tokens).to_i + traces_24h.sum(:total_output_tokens).to_i, + avg_duration_ms: runs_24h.where.not(duration_ms: nil).average(:duration_ms)&.round || 0 + } + end + + def serialize_agents(agents) + agents.map do |agent| + { + id: agent.id, + name: agent.name, + slug: agent.slug, + description: agent.description, + provider: agent.provider, + model: agent.model, + status: agent.status, + presetType: agent.preset_type, + versionCount: agent.version_count, + updatedAt: agent.updated_at.iso8601 + } + end + end + + def serialize_runs(runs) + runs.map(&:summary) + end + + def serialize_traces(traces) + traces.map do |trace| + { + id: trace.id, + traceId: trace.trace_id, + displayName: trace.display_name, + status: trace.status, + duration: trace.formatted_duration, + tokens: trace.formatted_tokens, + timestamp: trace.timestamp&.iso8601 + } + end + end + + def current_user_props + return nil unless current_user + + { + id: current_user.id, + name: current_user.try(:display_name) || current_user.try(:name) || current_user.try(:email), + email: current_user.try(:email) + } + end + + def current_account_props + return nil unless current_owner && ActiveAgent::Dashboard.multi_tenant? + + { + id: current_owner.id, + name: current_owner.try(:name) + } + end + end + end +end diff --git a/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb new file mode 100644 index 00000000..f72cc9be --- /dev/null +++ b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Controller for viewing telemetry traces. + # + # Provides: + # - List view with filtering and pagination + # - Detail view with span timeline + # - Metrics overview + # - Live updates via Turbo Streams + class TracesController < ApplicationController + def index + @traces = fetch_traces + @metrics = calculate_metrics + + respond_to do |format| + format.html + format.turbo_stream + end + end + + def show + @trace = ActiveAgent::TelemetryTrace.find(params[:id]) + end + + def metrics + @metrics = calculate_metrics + @agent_stats = agent_statistics + @time_series = time_series_data + + respond_to do |format| + format.html + format.turbo_stream + end + end + + private + + def fetch_traces + traces = ActiveAgent::TelemetryTrace.recent + + traces = traces.for_agent(params[:agent]) if params[:agent].present? + traces = traces.with_errors if params[:status] == "error" + traces = traces.for_service(params[:service]) if params[:service].present? + + if params[:start_date].present? && params[:end_date].present? + traces = traces.for_date_range( + Time.parse(params[:start_date]), + Time.parse(params[:end_date]) + ) + end + + traces.limit(params[:limit] || 50) + end + + def calculate_metrics + traces = ActiveAgent::TelemetryTrace.where( + "created_at > ?", 24.hours.ago + ) + + { + total_traces: traces.count, + total_tokens: traces.sum(:total_input_tokens) + traces.sum(:total_output_tokens), + avg_duration_ms: traces.average(:total_duration_ms)&.round(2) || 0, + error_rate: calculate_error_rate(traces), + unique_agents: traces.distinct.count(:agent_class) + } + end + + def calculate_error_rate(traces) + total = traces.count + return 0.0 if total.zero? + + errors = traces.with_errors.count + ((errors.to_f / total) * 100).round(2) + end + + def agent_statistics + ActiveAgent::TelemetryTrace + .where("created_at > ?", 24.hours.ago) + .group(:agent_class) + .select( + "agent_class", + "COUNT(*) as trace_count", + "SUM(total_input_tokens + total_output_tokens) as total_tokens", + "AVG(total_duration_ms) as avg_duration", + "SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END) as error_count" + ) + end + + def time_series_data + ActiveAgent::TelemetryTrace + .where("created_at > ?", 1.hour.ago) + .group_by_minute(:created_at) + .count + rescue NoMethodError + # Fallback if groupdate gem not available + {} + end + end + end +end diff --git a/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb b/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb new file mode 100644 index 00000000..776d8df6 --- /dev/null +++ b/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Background job for executing agent runs. + # + # Handles the actual execution of an agent with the given input, + # updating the AgentRun record with results. + # + class AgentExecutionJob < ApplicationJob + queue_as :default + + def perform(agent_run_id) + run = AgentRun.find(agent_run_id) + return if run.finished? + + run.update!(status: :running, started_at: Time.current) + + begin + agent = run.agent + result = execute_agent(agent, run.input_prompt, **(run.input_params || {})) + + run.update!( + output: result[:output], + output_metadata: result[:metadata], + status: :complete, + completed_at: Time.current, + duration_ms: ((Time.current - run.started_at) * 1000).to_i, + input_tokens: result.dig(:usage, :input_tokens), + output_tokens: result.dig(:usage, :output_tokens), + total_tokens: result.dig(:usage, :total_tokens) + ) + rescue => e + run.update!( + status: :failed, + completed_at: Time.current, + error_message: e.message, + error_backtrace: e.backtrace&.first(10)&.join("\n") + ) + end + end + + private + + def execute_agent(agent, input_prompt, **params) + # TODO: Build and execute actual ActiveAgent::Base subclass + # For now, return mock data + { + output: "Executed: #{input_prompt}", + metadata: { provider: agent.provider, model: agent.model }, + usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300 } + } + end + end + end +end diff --git a/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb b/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb new file mode 100644 index 00000000..7303cd85 --- /dev/null +++ b/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Base class for all Dashboard engine jobs. + class ApplicationJob < ActiveJob::Base + # Retry failed jobs + retry_on StandardError, wait: :polynomially_longer, attempts: 3 + + # Discard jobs for records that no longer exist + discard_on ActiveRecord::RecordNotFound + end + end +end diff --git a/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb b/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb new file mode 100644 index 00000000..6900cc11 --- /dev/null +++ b/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Background job for cleaning up sandbox environments. + # + # Removes containers, Cloud Run jobs, or other resources + # when a sandbox session expires. + # + class SandboxCleanupJob < ApplicationJob + queue_as :default + + def perform(sandbox_session_id) + session = SandboxSession.find_by(id: sandbox_session_id) + return unless session + + cleanup_sandbox(session) + end + + private + + def cleanup_sandbox(session) + case ActiveAgent::Dashboard.sandbox_service + when :cloud_run + cleanup_cloud_run(session) + when :kubernetes + cleanup_kubernetes(session) + else + cleanup_local(session) + end + end + + def cleanup_local(session) + # Local mode: Nothing to clean up + Rails.logger.info "[ActiveAgent::Dashboard] Cleaned up local sandbox: #{session.session_id}" + end + + def cleanup_cloud_run(session) + # TODO: Implement Cloud Run cleanup + Rails.logger.info "[ActiveAgent::Dashboard] Would clean up Cloud Run job: #{session.cloud_run_job_id}" + end + + def cleanup_kubernetes(session) + # TODO: Implement Kubernetes cleanup + Rails.logger.info "[ActiveAgent::Dashboard] Would clean up Kubernetes pod: #{session.cloud_run_job_id}" + end + end + end +end diff --git a/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb b/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb new file mode 100644 index 00000000..e311a093 --- /dev/null +++ b/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Background job for provisioning sandbox environments. + # + # Creates isolated execution environments (Docker, Cloud Run, etc.) + # for running agents with tools. + # + class SandboxProvisionJob < ApplicationJob + queue_as :default + + def perform(sandbox_session_id) + session = SandboxSession.find(sandbox_session_id) + return unless session.provisioning? + + begin + result = provision_sandbox(session) + + session.mark_ready!( + sandbox_url: result[:url], + sandbox_job_id: result[:job_id] + ) + rescue => e + session.update!( + status: :failed, + error_message: e.message + ) + end + end + + private + + def provision_sandbox(session) + case ActiveAgent::Dashboard.sandbox_service + when :cloud_run + provision_cloud_run(session) + when :kubernetes + provision_kubernetes(session) + else + provision_local(session) + end + end + + def provision_local(session) + # Local mode: No actual provisioning needed + # The sandbox runs in the same process or via Docker + { + url: "http://localhost:#{3000 + session.id}", + job_id: "local-#{session.session_id}" + } + end + + def provision_cloud_run(session) + # TODO: Implement Cloud Run provisioning + raise NotImplementedError, "Cloud Run provisioning not yet implemented in engine" + end + + def provision_kubernetes(session) + # TODO: Implement Kubernetes provisioning + raise NotImplementedError, "Kubernetes provisioning not yet implemented in engine" + end + end + end +end diff --git a/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb b/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb new file mode 100644 index 00000000..0456e8dc --- /dev/null +++ b/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module ActiveAgent + # Processes telemetry traces received from ActiveAgent clients. + # + # This job handles the asynchronous processing of trace data to avoid + # blocking the ingestion endpoint. It: + # - Creates TelemetryTrace records for each trace + # - Updates aggregate statistics + # - Handles any errors gracefully + # + # @example Local mode + # ActiveAgent::ProcessTelemetryTracesJob.perform_later( + # traces: [...], + # sdk_info: { name: "activeagent", version: "0.5.0" }, + # received_at: "2024-01-15T10:30:00Z" + # ) + # + # @example Multi-tenant mode + # ActiveAgent::ProcessTelemetryTracesJob.perform_later( + # account_id: 1, + # traces: [...], + # sdk_info: { name: "activeagent", version: "0.5.0" }, + # received_at: "2024-01-15T10:30:00Z" + # ) + # + class ProcessTelemetryTracesJob < ::ActiveJob::Base + queue_as :default + + # Maximum traces to process in a single job to avoid memory issues + MAX_TRACES_PER_JOB = 100 + + def perform(account_id: nil, traces:, sdk_info:, received_at:) + account = resolve_account(account_id) + + # In multi-tenant mode, require an account + if ActiveAgent::Dashboard.multi_tenant? && account.nil? + Rails.logger.warn("[ProcessTelemetryTracesJob] Skipping traces - no valid account") + return + end + + traces = traces.take(MAX_TRACES_PER_JOB) + + traces.each do |trace| + process_trace(trace, sdk_info, account) + rescue StandardError => e + Rails.logger.error( + "[ProcessTelemetryTracesJob] Failed to process trace #{trace['trace_id']}: " \ + "#{e.class} - #{e.message}" + ) + end + end + + private + + def resolve_account(account_id) + return nil unless ActiveAgent::Dashboard.multi_tenant? + return nil unless account_id + + account_class = ActiveAgent::Dashboard.account_class.constantize + account_class.find_by(id: account_id) + end + + def process_trace(trace, sdk_info, account) + model = ActiveAgent::Dashboard.trace_model + + # Build uniqueness scope + scope = model.where(trace_id: trace["trace_id"]) + scope = scope.where(account: account) if ActiveAgent::Dashboard.multi_tenant? && account + + # Skip if trace already exists (idempotency) + return if scope.exists? + + model.create_from_payload(trace, sdk_info, account: account) + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb new file mode 100644 index 00000000..d27e253b --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Represents an AI agent configuration. + # + # Agents are the core entity in the dashboard, storing all configuration + # needed to execute AI interactions including provider settings, instructions, + # tools, and appearance. + # + # Supports both local (single-user) and multi-tenant (account-scoped) modes. + # + # @example Creating an agent + # agent = ActiveAgent::Dashboard::Agent.create!( + # name: "Code Assistant", + # provider: "openai", + # model: "gpt-4o" + # ) + # + # @example Executing an agent + # run = agent.execute("Explain this code", code: "def foo; end") + # + class Agent < ApplicationRecord + # Associations - owner is optional to support both modes + belongs_to :user, class_name: ActiveAgent::Dashboard.user_class, optional: true if ActiveAgent::Dashboard.user_class + belongs_to :account, class_name: ActiveAgent::Dashboard.account_class, optional: true if ActiveAgent::Dashboard.multi_tenant? + + has_many :agent_versions, class_name: "ActiveAgent::Dashboard::AgentVersion", dependent: :destroy + has_many :agent_runs, class_name: "ActiveAgent::Dashboard::AgentRun", dependent: :destroy + + # Validations + validates :name, presence: true, length: { minimum: 2, maximum: 100 } + validates :slug, presence: true, format: { with: /\A[a-z0-9\-_]+\z/ } + validates :provider, presence: true + validates :model, presence: true + + # Ensure slug uniqueness within scope + if ActiveAgent::Dashboard.multi_tenant? + validates :slug, uniqueness: { scope: :account_id } + else + validates :slug, uniqueness: { scope: :user_id } + end + + # Status enum + enum :status, { draft: 0, active: 1, archived: 2 } + + # Callbacks + before_validation :generate_slug, on: :create + after_create :create_initial_version + after_update :create_version_on_config_change, if: :configuration_changed? + + # Scopes + scope :active_agents, -> { where(status: :active) } + scope :by_provider, ->(provider) { where(provider: provider) } + scope :with_tool, ->(tool) { where("tools @> ?", [ tool ].to_json) } + + # Available presets matching AgentAvatar component + PRESET_TYPES = %w[ + terminal webDeveloper documentAnalysis writing translation + playwright research imageAnalysis computerUse productDesign + ].freeze + + # Available instruction sets + INSTRUCTION_SETS = %w[ + github ruby rails aws gcp python typescript docker kubernetes + ].freeze + + # Available tools/MCPs + AVAILABLE_TOOLS = %w[ + terminal playwright filesystem code database slack fetch search edit translate memory + ].freeze + + # Available providers + PROVIDERS = %w[openai anthropic ollama openrouter].freeze + + # Returns the configuration as a hash for versioning + def configuration_snapshot + { + name: name, + description: description, + provider: provider, + model: model, + instructions: instructions, + preset_type: preset_type, + appearance: appearance, + instruction_sets: instruction_sets, + tools: tools, + mcp_servers: mcp_servers, + model_config: model_config, + response_format: response_format + } + end + + # Restore from a version + def restore_from_version!(version) + config = version.configuration_snapshot + update!( + instructions: config["instructions"], + preset_type: config["preset_type"], + appearance: config["appearance"], + instruction_sets: config["instruction_sets"], + tools: config["tools"], + mcp_servers: config["mcp_servers"], + model_config: config["model_config"], + response_format: config["response_format"] + ) + end + + # Get the latest version + def latest_version + agent_versions.order(version_number: :desc).first + end + + # Get version count + def version_count + agent_versions.count + end + + # Generate Ruby agent class code + def to_agent_class_code + <<~RUBY + class #{agent_class_name || name.camelize}Agent < ApplicationAgent + generate_with :#{provider}, model: "#{model}"#{model_config_code} + + def perform + prompt#{instructions_code} + end + end + RUBY + end + + # Execute a run with this agent + def execute(input_prompt, **params) + run = agent_runs.create!( + input_prompt: input_prompt, + input_params: params, + status: :pending, + trace_id: SecureRandom.uuid + ) + + # Queue the execution job + ActiveAgent::Dashboard::AgentExecutionJob.perform_later(run.id) + + run + end + + # Quick test execution (synchronous) + def test_execute(input_prompt, **params) + run = agent_runs.create!( + input_prompt: input_prompt, + input_params: params, + status: :running, + trace_id: SecureRandom.uuid, + started_at: Time.current + ) + + begin + result = build_and_execute_agent(input_prompt, **params) + + run.update!( + output: result[:output], + output_metadata: result[:metadata], + status: :complete, + completed_at: Time.current, + duration_ms: ((Time.current - run.started_at) * 1000).to_i, + input_tokens: result.dig(:usage, :input_tokens), + output_tokens: result.dig(:usage, :output_tokens), + total_tokens: result.dig(:usage, :total_tokens) + ) + rescue => e + run.update!( + status: :failed, + completed_at: Time.current, + error_message: e.message, + error_backtrace: e.backtrace&.first(10)&.join("\n") + ) + end + + run + end + + private + + def generate_slug + return if slug.present? + + base_slug = name.to_s.parameterize + self.slug = base_slug + + # Ensure uniqueness within scope + counter = 1 + scope = self.class.where(slug: slug) + scope = scope.where(account_id: account_id) if respond_to?(:account_id) && account_id + scope = scope.where(user_id: user_id) if respond_to?(:user_id) && user_id + + while scope.exists? + self.slug = "#{base_slug}-#{counter}" + scope = self.class.where(slug: slug) + scope = scope.where(account_id: account_id) if respond_to?(:account_id) && account_id + scope = scope.where(user_id: user_id) if respond_to?(:user_id) && user_id + counter += 1 + end + end + + def create_initial_version + agent_versions.create!( + version_number: 1, + change_summary: "Initial creation", + configuration_snapshot: configuration_snapshot + ) + end + + def configuration_changed? + saved_changes.keys.any? do |key| + %w[instructions preset_type appearance instruction_sets tools mcp_servers model_config response_format].include?(key) + end + end + + def create_version_on_config_change + next_version = (latest_version&.version_number || 0) + 1 + changed_fields = saved_changes.keys.select do |key| + %w[instructions preset_type appearance instruction_sets tools mcp_servers model_config response_format].include?(key) + end + + agent_versions.create!( + version_number: next_version, + change_summary: "Updated: #{changed_fields.join(', ')}", + configuration_snapshot: configuration_snapshot + ) + end + + def model_config_code + return "" if model_config.blank? + + configs = model_config.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") + ", #{configs}" + end + + def instructions_code + return "" if instructions.blank? + + "\n prompt instructions: <<~INSTRUCTIONS\n #{instructions.gsub("\n", "\n ")}\n INSTRUCTIONS" + end + + def build_and_execute_agent(input_prompt, **params) + # TODO: Implement actual ActiveAgent execution + # This will create a dynamic agent class and execute it + { + output: "Mock response for: #{input_prompt}", + metadata: { provider: provider, model: model }, + usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 } + } + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb new file mode 100644 index 00000000..7960414a --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Tracks individual agent execution runs. + # + # Each run captures input, output, timing, token usage, and any errors + # that occurred during execution. + # + # @example Creating a run + # run = agent.execute("Analyze this code", code: code) + # run.status # => "pending" + # + # @example Monitoring a run + # run.in_progress? # => true + # run.finished? # => false + # + class AgentRun < ApplicationRecord + belongs_to :agent, class_name: "ActiveAgent::Dashboard::Agent" + has_one :session_recording, class_name: "ActiveAgent::Dashboard::SessionRecording", dependent: :nullify + + # Status enum + enum :status, { pending: 0, running: 1, complete: 2, failed: 3, cancelled: 4 } + + # Validations + validates :trace_id, presence: true + + # Scopes + scope :recent, -> { order(created_at: :desc) } + scope :successful, -> { where(status: :complete) } + scope :failed_runs, -> { where(status: :failed) } + scope :today, -> { where("created_at >= ?", Time.current.beginning_of_day) } + + # Callbacks + before_validation :set_trace_id, on: :create + after_update_commit :broadcast_update, if: :saved_change_to_status? + + # Add a log entry + def add_log(message, level: :info) + new_logs = logs || [] + new_logs << { + timestamp: Time.current.iso8601, + level: level.to_s, + message: message + } + update!(logs: new_logs) + end + + # Calculate duration if not set + def calculated_duration_ms + return duration_ms if duration_ms.present? + return nil unless started_at && completed_at + + ((completed_at - started_at) * 1000).to_i + end + + # Check if run is still in progress + def in_progress? + pending? || running? + end + + # Check if run is finished + def finished? + complete? || failed? || cancelled? + end + + # Get a summary for display + def summary + { + id: id, + status: status, + input_preview: input_prompt&.truncate(100), + output_preview: output&.truncate(200), + duration_ms: calculated_duration_ms, + tokens: total_tokens, + created_at: created_at, + error: error_message + } + end + + # Stream output updates via ActionCable + def broadcast_update + return unless defined?(ActionCable) + + ActionCable.server.broadcast( + "agent_run_#{id}", + { + type: "update", + run: summary + } + ) + end + + # Cancel a running execution + def cancel! + return unless in_progress? + + update!( + status: :cancelled, + completed_at: Time.current, + error_message: "Cancelled by user" + ) + broadcast_update + end + + private + + def set_trace_id + self.trace_id ||= SecureRandom.uuid + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb new file mode 100644 index 00000000..d5e690be --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Pre-built agent templates for quick agent creation. + # + # Templates provide starting configurations for common use cases like + # code assistance, research, writing, and browser automation. + # + # @example Creating an agent from a template + # template = ActiveAgent::Dashboard::AgentTemplate.find_by(slug: "code-assistant") + # agent = template.create_agent_for(user) + # + class AgentTemplate < ApplicationRecord + # Validations + validates :name, presence: true + validates :slug, presence: true, uniqueness: true + validates :category, presence: true + + # Scopes + scope :featured, -> { where(featured: true) } + scope :by_category, ->(cat) { where(category: cat) } + scope :popular, -> { order(usage_count: :desc) } + scope :public_templates, -> { where(public: true) } + scope :free_tier, -> { where(free_tier: true) } + + # Categories + CATEGORIES = %w[ + productivity + development + research + creative + data + automation + ].freeze + + # Create an agent from this template for a user/account + def create_agent_for(owner, name: nil) + agent_class = ActiveAgent::Dashboard::Agent + + agent = agent_class.new( + name: name || self.name, + description: description, + provider: provider, + model: model, + instructions: instructions, + preset_type: preset_type, + appearance: appearance, + instruction_sets: instruction_sets, + tools: tools, + mcp_servers: mcp_servers, + model_config: model_config, + status: :draft + ) + + # Set owner based on mode + if ActiveAgent::Dashboard.multi_tenant? && owner.respond_to?(:id) + agent.account = owner + elsif owner.respond_to?(:id) + agent.user = owner if agent.respond_to?(:user=) + end + + if agent.save + increment!(:usage_count) + end + + agent + end + + # Seed default templates + def self.seed_defaults! + templates = [ + { + name: "Code Assistant", + slug: "code-assistant", + description: "A helpful coding assistant that can explain code, suggest improvements, and help debug issues.", + category: "development", + provider: "openai", + model: "gpt-4o", + preset_type: "terminal", + appearance: { hat: "fedora", heldItem: "terminal" }, + instruction_sets: %w[github ruby rails typescript], + tools: %w[terminal code filesystem], + model_config: { temperature: 0.3 }, + instructions: "You are a senior software engineer with expertise in multiple programming languages. Help users with:\n- Code explanations and reviews\n- Debugging issues\n- Suggesting best practices\n- Writing tests\n\nAlways explain your reasoning and provide examples when helpful.", + icon: "💻", + featured: true, + free_tier: true + }, + { + name: "Research Assistant", + slug: "research-assistant", + description: "Helps research topics, summarize information, and organize findings.", + category: "research", + provider: "anthropic", + model: "claude-sonnet-4-20250514", + preset_type: "research", + appearance: { hat: "safari", heldItem: "magnifyingGlass" }, + instruction_sets: %w[github python], + tools: %w[fetch search memory], + model_config: { temperature: 0.5 }, + instructions: "You are a thorough research assistant. Help users by:\n- Searching for relevant information\n- Summarizing complex topics\n- Organizing findings into clear reports\n- Identifying key insights and patterns\n\nAlways cite sources when available and distinguish between facts and opinions.", + icon: "🔍", + featured: true, + free_tier: true + }, + { + name: "Writing Assistant", + slug: "writing-assistant", + description: "Helps with writing, editing, and improving text content.", + category: "creative", + provider: "openai", + model: "gpt-4o", + preset_type: "writing", + appearance: { hat: "fedora", hatAccessory: "feather", heldItem: "scroll" }, + instruction_sets: [], + tools: %w[edit translate], + model_config: { temperature: 0.7 }, + instructions: "You are a skilled writer and editor. Help users with:\n- Writing and editing content\n- Improving clarity and flow\n- Adjusting tone for different audiences\n- Grammar and style corrections\n\nMaintain the author's voice while suggesting improvements.", + icon: "✍️", + featured: true, + free_tier: true + }, + { + name: "Browser Automation", + slug: "browser-automation", + description: "Automates web browsing tasks like form filling, data extraction, and testing.", + category: "automation", + provider: "anthropic", + model: "claude-sonnet-4-20250514", + preset_type: "playwright", + appearance: { hat: "fedora", hatAccessory: "theaterMasks", heldItem: "browser" }, + instruction_sets: %w[typescript], + tools: %w[playwright filesystem], + model_config: { temperature: 0.2 }, + instructions: "You are a browser automation specialist. Help users by:\n- Navigating web pages\n- Filling out forms\n- Extracting data from websites\n- Testing web applications\n\nAlways wait for page loads and handle errors gracefully.", + icon: "🎭", + featured: false, + free_tier: true + }, + { + name: "Data Analyst", + slug: "data-analyst", + description: "Analyzes data, creates visualizations, and provides insights.", + category: "data", + provider: "openai", + model: "gpt-4o", + preset_type: "documentAnalysis", + appearance: { hat: "fedora", heldItem: "document" }, + instruction_sets: %w[python], + tools: %w[code database filesystem], + model_config: { temperature: 0.3 }, + instructions: "You are a data analyst. Help users by:\n- Analyzing datasets\n- Creating visualizations\n- Finding patterns and insights\n- Generating reports\n\nExplain your methodology and provide clear interpretations of results.", + icon: "📊", + featured: true, + free_tier: true + }, + { + name: "DevOps Assistant", + slug: "devops-assistant", + description: "Helps with infrastructure, deployments, and system administration.", + category: "development", + provider: "openai", + model: "gpt-4o", + preset_type: "terminal", + appearance: { hat: "fedora", heldItem: "terminal" }, + instruction_sets: %w[docker kubernetes aws gcp], + tools: %w[terminal filesystem code], + model_config: { temperature: 0.2 }, + instructions: "You are a DevOps engineer. Help users with:\n- Infrastructure setup and management\n- CI/CD pipeline configuration\n- Container orchestration\n- Cloud resource management\n\nAlways prioritize security and follow best practices.", + icon: "🚀", + featured: false, + free_tier: true + }, + { + name: "PlaywrightMCP Demo", + slug: "playwright-mcp-demo", + description: "Free browser automation demo using Playwright MCP. Navigate sites, take screenshots, and extract content.", + category: "automation", + provider: "anthropic", + model: "claude-sonnet-4-20250514", + preset_type: "playwright", + appearance: { hat: "fedora", hatAccessory: "theaterMasks", heldItem: "browser" }, + instruction_sets: [], + tools: %w[playwright], + mcp_servers: { + playwright: { + command: "npx", + args: [ "-y", "@anthropic/mcp-server-playwright" ] + } + }, + model_config: { temperature: 0.2, max_tokens: 4096 }, + instructions: "You are a browser automation assistant using Playwright MCP.\n\nAvailable actions:\n- browser_navigate: Go to a URL\n- browser_snapshot: Get the accessibility tree\n- browser_click: Click on an element\n- browser_type: Type text into an input\n- browser_take_screenshot: Capture the page\n- browser_wait_for: Wait for text or element\n\nGuidelines:\n1. Always take a snapshot first to understand the page\n2. Use element refs from snapshots for interactions\n3. Wait for page loads before taking actions\n4. Handle errors gracefully\n5. Limit yourself to 10 steps maximum\n\nAlways describe what you see and what actions you're taking.", + icon: "🎭", + featured: true, + free_tier: true + } + ] + + templates.each do |template_attrs| + find_or_create_by!(slug: template_attrs[:slug]) do |t| + t.assign_attributes(template_attrs) + end + end + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb new file mode 100644 index 00000000..b4d65414 --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Tracks version history for agent configurations. + # + # Each time an agent's configuration changes, a new version is created + # with a snapshot of the configuration at that point in time. + # + # @example Comparing versions + # v1 = agent.agent_versions.find_by(version_number: 1) + # v2 = agent.agent_versions.find_by(version_number: 2) + # changes = v2.diff(v1) + # + class AgentVersion < ApplicationRecord + belongs_to :agent, class_name: "ActiveAgent::Dashboard::Agent" + + validates :version_number, presence: true, uniqueness: { scope: :agent_id } + validates :configuration_snapshot, presence: true + + # Scopes + scope :recent, -> { order(version_number: :desc) } + scope :by_version, ->(num) { where(version_number: num) } + + # Compare two versions + def diff(other_version) + return {} unless other_version + + changes = {} + configuration_snapshot.each do |key, value| + other_value = other_version.configuration_snapshot[key] + if value != other_value + changes[key] = { from: other_value, to: value } + end + end + changes + end + + # Get previous version + def previous + agent.agent_versions.where("version_number < ?", version_number).order(version_number: :desc).first + end + + # Get next version + def next_version + agent.agent_versions.where("version_number > ?", version_number).order(version_number: :asc).first + end + + # Check if this is the latest version + def latest? + agent.latest_version&.id == id + end + + # Check if this is the initial version + def initial? + version_number == 1 + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb new file mode 100644 index 00000000..dd2b9f47 --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Base class for all Dashboard engine models. + # + # Provides multi-tenant support when configured, allowing the same models + # to work in both local (single-tenant) and platform (multi-tenant) modes. + class ApplicationRecord < ::ActiveRecord::Base + self.abstract_class = true + + # Override table name calculation to use active_agent_ prefix + # without the "dashboard_" from the module namespace + def self.table_name + @table_name ||= "active_agent_#{name.demodulize.underscore.pluralize}" + end + + class << self + # Returns the owner association name based on configuration. + # In multi-tenant mode, this returns :account. + # In local mode, this returns :user (optional). + def owner_association + if ActiveAgent::Dashboard.multi_tenant? + :account + else + :user + end + end + + # Scopes records to the current owner (account or user). + # No-op in local mode without owner configuration. + def for_owner(owner) + return all if owner.nil? + + if ActiveAgent::Dashboard.multi_tenant? + where(account: owner) + elsif column_names.include?("user_id") + where(user: owner) + else + all + end + end + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb new file mode 100644 index 00000000..518a31f3 --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Represents a single browser action within a session recording. + # + # Actions capture user and agent interactions like clicks, typing, + # navigation, and form submissions. + # + class RecordingAction < ApplicationRecord + belongs_to :session_recording, class_name: "ActiveAgent::Dashboard::SessionRecording" + has_one :snapshot, class_name: "ActiveAgent::Dashboard::RecordingSnapshot", dependent: :nullify + + ACTION_TYPES = %w[ + navigate + click + type + scroll + snapshot + select + hover + drag + file_upload + dialog + evaluate + wait + form_fill + key_press + focus + submit + handoff + user_action + completion + ].freeze + + validates :action_type, presence: true, inclusion: { in: ACTION_TYPES } + validates :sequence, presence: true, uniqueness: { scope: :session_recording_id } + validates :timestamp_ms, presence: true + + scope :ordered, -> { order(:sequence) } + scope :with_screenshots, -> { where.not(screenshot_key: nil) } + + # Get the screenshot URL (signed URL from storage) + def screenshot_url(expires_in: 15.minutes) + return nil unless screenshot_key.present? + + ActiveAgent::Dashboard.storage_service&.signed_url_for(screenshot_key, expires_in: expires_in) + end + + # Get the DOM snapshot content + def dom_snapshot_content + return nil unless dom_snapshot_key.present? + + ActiveAgent::Dashboard.storage_service&.fetch_snapshot(dom_snapshot_key) + end + + # Format for API response + def as_json_for_api + { + id: id, + action_type: action_type, + sequence: sequence, + timestamp_ms: timestamp_ms, + selector: selector, + value: redacted_value, + screenshot_url: screenshot_url, + has_dom_snapshot: dom_snapshot_key.present?, + metadata: safe_metadata, + created_at: created_at.iso8601 + } + end + + # Get browser state at this action (for handoff) + def browser_state + { + url: extract_url, + form_values: metadata["form_values"], + scroll_position: metadata["scroll_position"], + active_element: selector, + action_type: action_type + } + end + + private + + def redacted_value + return value unless should_redact? + + "[REDACTED]" + end + + def should_redact? + return false unless value.present? + + sensitive_patterns = [ + /password/i, + /credit.?card/i, + /cvv/i, + /ssn/i, + /social.?security/i, + /\b\d{16}\b/, + /\b\d{3}-\d{2}-\d{4}\b/ + ] + + selector_is_sensitive = sensitive_patterns.any? { |p| selector&.match?(p) } + value_is_sensitive = sensitive_patterns.any? { |p| value.match?(p) } + + selector_is_sensitive || value_is_sensitive + end + + def safe_metadata + metadata.except("password", "credit_card", "cvv", "ssn") + end + + def extract_url + case action_type + when "navigate" + value + else + metadata["url"] || metadata["page_url"] + end + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb new file mode 100644 index 00000000..8c4e852d --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Stores screenshots and DOM snapshots from session recordings. + # + # Supports Active Storage for file attachment and provides + # signed URLs for secure access. + # + class RecordingSnapshot < ApplicationRecord + belongs_to :session_recording, class_name: "ActiveAgent::Dashboard::SessionRecording" + belongs_to :recording_action, class_name: "ActiveAgent::Dashboard::RecordingAction", optional: true + + has_one_attached :file if defined?(ActiveStorage) + + SNAPSHOT_TYPES = %w[screenshot dom full_page].freeze + + validates :storage_key, presence: true, uniqueness: true + validates :snapshot_type, presence: true, inclusion: { in: SNAPSHOT_TYPES } + + scope :screenshots, -> { where(snapshot_type: "screenshot") } + scope :dom_snapshots, -> { where(snapshot_type: "dom") } + scope :ordered, -> { order(:created_at) } + + # Get a signed URL for the file + def signed_url(expires_in: 15.minutes) + return nil unless respond_to?(:file) && file.attached? + + file.url(expires_in: expires_in) + rescue StandardError + nil + end + + # Store file data + def store!(data, filename: nil, content_type: nil) + return unless respond_to?(:file) + + content_type ||= infer_content_type + filename ||= generate_filename + + file.attach( + io: StringIO.new(data), + filename: filename, + content_type: content_type + ) + + update!(file_size_bytes: data.bytesize) + end + + # For API response + def as_json_for_api + { + id: id, + storage_key: storage_key, + snapshot_type: snapshot_type, + width: width, + height: height, + file_size_bytes: file_size_bytes, + url: signed_url, + created_at: created_at.iso8601 + } + end + + private + + def infer_content_type + case snapshot_type + when "screenshot", "full_page" + "image/png" + when "dom" + "text/html" + else + "application/octet-stream" + end + end + + def generate_filename + ext = snapshot_type == "dom" ? "html" : "png" + "#{storage_key.tr('/', '_')}.#{ext}" + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb new file mode 100644 index 00000000..ae268fde --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Tracks individual task executions within a sandbox session. + # + # Each sandbox run represents a single agent task execution, + # capturing input, output, timing, and any errors. + # + class SandboxRun < ApplicationRecord + belongs_to :sandbox_session, class_name: "ActiveAgent::Dashboard::SandboxSession", optional: true + + enum :status, { + pending: 0, + running: 1, + completed: 2, + failed: 3, + cancelled: 4 + } + + validates :task, presence: true + validates :status, presence: true + + scope :recent, -> { order(created_at: :desc) } + scope :completed_runs, -> { where(status: [ :completed, :failed ]) } + + # Summary for API responses + def summary + { + id: id, + task: task.truncate(100), + status: status, + duration_ms: duration_ms, + tokens_used: tokens_used, + created_at: created_at&.iso8601, + completed_at: completed_at&.iso8601 + } + end + + # Detailed info including full result + def details + summary.merge( + task: task, + result: result, + error: error, + screenshots: screenshots || [], + started_at: started_at&.iso8601 + ) + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb new file mode 100644 index 00000000..4e52eb6c --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Manages sandbox execution sessions for agent runs. + # + # Sandbox sessions provide isolated execution environments for running + # agents with tools like browser automation, file system access, etc. + # + # Supports both local (Docker/Incus) and cloud (Cloud Run) sandbox providers. + # + # @example Creating a sandbox session + # session = ActiveAgent::Dashboard::SandboxSession.create!( + # sandbox_type: "playwright_mcp" + # ) + # session.provision! + # + class SandboxSession < ApplicationRecord + # Owner associations - optional to support anonymous users + belongs_to :user, class_name: ActiveAgent::Dashboard.user_class, optional: true if ActiveAgent::Dashboard.user_class + belongs_to :account, class_name: ActiveAgent::Dashboard.account_class, optional: true if ActiveAgent::Dashboard.multi_tenant? + belongs_to :agent_template, class_name: "ActiveAgent::Dashboard::AgentTemplate", optional: true + + has_many :sandbox_runs, class_name: "ActiveAgent::Dashboard::SandboxRun", dependent: :destroy + has_one :session_recording, class_name: "ActiveAgent::Dashboard::SessionRecording", dependent: :nullify + + # Session statuses + enum :status, { + pending: 0, + provisioning: 1, + ready: 2, + running: 3, + completed: 4, + expired: 5, + failed: 6 + } + + # Sandbox types + SANDBOX_TYPES = %w[playwright_mcp terminal research].freeze + + # Default limits (can be overridden by platform tier limits) + DEFAULT_LIMITS = { + max_runs: 10, + timeout_seconds: 300, + max_tokens: 50_000, + session_duration_minutes: 15 + }.freeze + + # Validations + validates :session_id, presence: true, uniqueness: true + validates :sandbox_type, inclusion: { in: SANDBOX_TYPES } + + # Callbacks + before_validation :generate_session_id, on: :create + before_create :set_defaults + + # Scopes + scope :active, -> { where(status: [ :pending, :provisioning, :ready, :running ]) } + scope :expired_sessions, -> { where("expires_at < ?", Time.current) } + scope :by_type, ->(type) { where(sandbox_type: type) } + scope :anonymous, -> { where(user_id: nil) } + scope :recent, -> { order(created_at: :desc) } + + # Check if session is still valid + def active? + !expired? && !failed? && !completed? && expires_at > Time.current + end + + # Check if can run more tasks + def can_run? + active? && runs_count < max_runs + end + + # Record a new run (thread-safe for parallel execution) + def record_run!(task:, result:, duration_ms:, tokens:, screenshots: [], provider: nil) + run = { + id: SecureRandom.uuid, + task: task, + result: result, + duration_ms: duration_ms, + tokens: tokens, + screenshots: screenshots, + provider: provider, + status: "completed", + created_at: Time.current.iso8601 + } + + with_lock do + reload + self.runs = runs + [ run ] + self.runs_count = runs.size + self.total_tokens += tokens + self.total_duration_ms += duration_ms + self.last_activity_at = Time.current + save! + end + + run + end + + # Provision the sandbox + def provision! + return if provisioning? || ready? + + update!(status: :provisioning) + + # Use configured sandbox service + if Rails.env.development? || Rails.env.test? + ActiveAgent::Dashboard::SandboxProvisionJob.perform_now(id) + else + ActiveAgent::Dashboard::SandboxProvisionJob.perform_later(id) + end + end + + # Mark as ready with sandbox URL + def mark_ready!(sandbox_url:, sandbox_job_id: nil) + update!( + status: :ready, + cloud_run_url: sandbox_url, + cloud_run_job_id: sandbox_job_id + ) + end + + # Expire the session + def expire! + update!(status: :expired) + ActiveAgent::Dashboard::SandboxCleanupJob.perform_later(id) if cloud_run_job_id.present? + end + + # Summary for API responses + def summary + { + id: id, + session_id: session_id, + sandbox_type: sandbox_type, + status: status, + runs_count: runs_count, + max_runs: max_runs, + total_tokens: total_tokens, + expires_at: expires_at&.iso8601, + created_at: created_at.iso8601, + cloud_run_url: cloud_run_url + } + end + + # Detailed info including runs + def details + summary.merge( + runs: runs, + total_duration_ms: total_duration_ms, + last_activity_at: last_activity_at&.iso8601 + ) + end + + private + + def generate_session_id + self.session_id ||= SecureRandom.uuid + end + + def set_defaults + limits = ActiveAgent::Dashboard.sandbox_limits || DEFAULT_LIMITS + self.expires_at ||= limits[:session_duration_minutes].minutes.from_now + self.max_runs ||= limits[:max_runs] + self.timeout_seconds ||= limits[:timeout_seconds] + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb new file mode 100644 index 00000000..3b58ae15 --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Records browser sessions for playback and analysis. + # + # Session recordings capture the sequence of actions taken during + # an agent run or sandbox session, including screenshots, DOM snapshots, + # and timing information. + # + # @example Starting a recording + # recording = ActiveAgent::Dashboard::SessionRecording.start!( + # agent_run: run, + # name: "checkout_flow" + # ) + # + # @example Recording an action + # recording.record_action!( + # action_type: "click", + # selector: "button.submit", + # screenshot: screenshot_data + # ) + # + class SessionRecording < ApplicationRecord + belongs_to :agent_run, class_name: "ActiveAgent::Dashboard::AgentRun", optional: true + belongs_to :sandbox_session, class_name: "ActiveAgent::Dashboard::SandboxSession", optional: true + + has_many :recording_actions, class_name: "ActiveAgent::Dashboard::RecordingAction", dependent: :destroy + has_many :recording_snapshots, class_name: "ActiveAgent::Dashboard::RecordingSnapshot", dependent: :destroy + + enum :status, { recording: 0, completed: 1, failed: 2 } + + validates :status, presence: true + validate :must_have_parent, unless: -> { demo_recording? || user_session? } + + scope :recent, -> { order(created_at: :desc) } + scope :for_agent, ->(agent_id) { joins(:agent_run).where(agent_runs: { agent_id: agent_id }) } + scope :demo, -> { where(name: "lander_demo") } + scope :user_sessions, -> { where("name LIKE ?", "user_takeover_%") } + + # Check if this is a demo recording (doesn't require parent) + def demo_recording? + name&.start_with?("lander_") || name == "demo" + end + + # Check if this is a user takeover session (doesn't require parent) + def user_session? + name&.start_with?("user_takeover_") + end + + # Start a new recording session + def self.start!(agent_run: nil, sandbox_session: nil, name: nil) + create!( + agent_run: agent_run, + sandbox_session: sandbox_session, + name: name || generate_name(agent_run, sandbox_session), + status: :recording, + metadata: { started_at: Time.current.iso8601 } + ) + end + + # Start a user takeover session (for lander demo analytics) + def self.start_user_session!(visitor_id: nil, parent_demo_id: nil, page_url: nil) + create!( + name: "user_takeover_#{Time.current.strftime('%Y%m%d_%H%M%S')}_#{SecureRandom.hex(4)}", + status: :recording, + metadata: { + started_at: Time.current.iso8601, + session_type: "user_takeover", + visitor_id: visitor_id, + parent_demo_id: parent_demo_id, + page_url: page_url, + user_agent: nil + } + ) + end + + # Record a browser action + def record_action!(action_type:, selector: nil, value: nil, screenshot: nil, dom_snapshot: nil, metadata: {}) + raise "Recording already completed" unless recording? + + action = recording_actions.create!( + action_type: action_type, + sequence: next_sequence, + timestamp_ms: elapsed_ms, + selector: selector, + value: value, + metadata: metadata + ) + + if screenshot.present? + snapshot = store_snapshot(screenshot, :screenshot, action) + action.update!(screenshot_key: snapshot.storage_key) + end + + if dom_snapshot.present? + snapshot = store_snapshot(dom_snapshot, :dom, action) + action.update!(dom_snapshot_key: snapshot.storage_key) + end + + increment!(:action_count) + action + end + + # Complete the recording + def complete! + return unless recording? + + update!( + status: :completed, + duration_ms: elapsed_ms, + metadata: metadata.merge(completed_at: Time.current.iso8601) + ) + end + + # Mark recording as failed + def fail!(error_message = nil) + return unless recording? + + update!( + status: :failed, + duration_ms: elapsed_ms, + metadata: metadata.merge( + failed_at: Time.current.iso8601, + error: error_message + ) + ) + end + + # Get timeline data for playback + def timeline + recording_actions.order(:sequence).map do |action| + { + id: action.id, + type: action.action_type, + sequence: action.sequence, + timestamp_ms: action.timestamp_ms, + selector: action.selector, + value: action.value, + screenshot_key: action.screenshot_key, + metadata: action.metadata + } + end + end + + private + + def must_have_parent + return if agent_run.present? || sandbox_session.present? + + errors.add(:base, "must belong to an agent_run or sandbox_session") + end + + def self.generate_name(agent_run, sandbox_session) + prefix = if agent_run&.agent + agent_run.agent.name.parameterize + elsif sandbox_session&.agent_template + sandbox_session.agent_template.name.parameterize + else + "session" + end + + "#{prefix}_#{Time.current.strftime('%Y%m%d_%H%M%S')}" + end + + def next_sequence + (recording_actions.maximum(:sequence) || 0) + 1 + end + + def elapsed_ms + ((Time.current - created_at) * 1000).to_i + end + + def store_snapshot(data, snapshot_type, action = nil) + storage_key = generate_storage_key(snapshot_type, action&.sequence) + + recording_snapshots.create!( + recording_action: action, + storage_key: storage_key, + snapshot_type: snapshot_type, + file_size_bytes: data.bytesize + ) + end + + def generate_storage_key(snapshot_type, sequence = nil) + parts = [ "recordings", id, snapshot_type.to_s ] + parts << sequence.to_s if sequence + parts << SecureRandom.hex(8) + parts.join("/") + end + end + end +end diff --git a/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb b/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb new file mode 100644 index 00000000..b178e724 --- /dev/null +++ b/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +module ActiveAgent + # Stores telemetry traces from ActiveAgent clients. + # + # Each trace represents a complete generation lifecycle, including prompt + # preparation, LLM calls, tool invocations, and error handling. + # + # This model supports two modes: + # - Local mode: No account association (single-tenant, self-hosted) + # - Multi-tenant mode: With account association (for activeagents.ai platform) + # + # @example Creating a trace from ingested data (local mode) + # ActiveAgent::TelemetryTrace.create_from_payload(trace_payload, sdk_info) + # + # @example Creating a trace with account (multi-tenant mode) + # ActiveAgent::TelemetryTrace.create_from_payload(trace_payload, sdk_info, account: account) + # + class TelemetryTrace < ::ActiveRecord::Base + self.table_name = "active_agent_telemetry_traces" + + # Optional account association for multi-tenant mode + # The host app can add: belongs_to :account if needed + if ActiveAgent::Dashboard.multi_tenant? + belongs_to :account, class_name: ActiveAgent::Dashboard.account_class + end + + # Status values for traces + STATUS_OK = "OK" + STATUS_ERROR = "ERROR" + STATUS_UNSET = "UNSET" + + validates :trace_id, presence: true + + # Scopes + scope :recent, -> { order(timestamp: :desc) } + scope :with_errors, -> { where(status: STATUS_ERROR) } + scope :for_service, ->(name) { where(service_name: name) } + scope :for_environment, ->(env) { where(environment: env) } + scope :for_agent, ->(agent_class) { where(agent_class: agent_class) } + scope :for_date_range, ->(start_date, end_date) { where(timestamp: start_date..end_date) } + scope :for_account, ->(account) { where(account: account) if ActiveAgent::Dashboard.multi_tenant? } + + # Creates a TelemetryTrace from an ingested trace payload. + # + # Extracts relevant data from the trace payload and stores it in a + # normalized format for querying and analysis. + # + # @param trace [Hash] The trace payload from ActiveAgent::Telemetry + # @param sdk_info [Hash] SDK metadata + # @param account [Object, nil] Optional account for multi-tenant mode + # @return [TelemetryTrace] The created trace + def self.create_from_payload(trace, sdk_info = {}, account: nil) + spans = trace["spans"] || [] + root_span = spans.find { |s| s["parent_span_id"].nil? } || spans.first || {} + + # Calculate totals from all spans + total_duration = root_span["duration_ms"] + total_input = 0 + total_output = 0 + total_thinking = 0 + + spans.each do |span| + tokens = span["tokens"] || {} + total_input += (tokens["input"] || 0) + total_output += (tokens["output"] || 0) + total_thinking += (tokens["thinking"] || 0) + end + + # Extract agent info from root span attributes + attributes = root_span["attributes"] || {} + agent_class = attributes["agent.class"] + agent_action = attributes["agent.action"] + + # Find any error message + error_span = spans.find { |s| s["status"] == STATUS_ERROR } + error_message = error_span&.dig("attributes", "error.message") + + attrs = { + trace_id: trace["trace_id"], + service_name: trace["service_name"], + environment: trace["environment"], + timestamp: Time.parse(trace["timestamp"]), + spans: spans, + resource_attributes: trace["resource_attributes"], + sdk_info: sdk_info, + total_duration_ms: total_duration, + total_input_tokens: total_input, + total_output_tokens: total_output, + total_thinking_tokens: total_thinking, + status: root_span["status"] || STATUS_UNSET, + agent_class: agent_class, + agent_action: agent_action, + error_message: error_message + } + + # Add account if in multi-tenant mode + attrs[:account] = account if ActiveAgent::Dashboard.multi_tenant? && account + + create!(attrs) + end + + # Returns the root span of this trace. + # + # @return [Hash, nil] The root span or nil + def root_span + spans&.find { |s| s["parent_span_id"].nil? } + end + + # Returns all LLM spans in this trace. + # + # @return [Array] LLM spans + def llm_spans + spans&.select { |s| s["type"] == "llm" } || [] + end + + # Returns all tool call spans in this trace. + # + # @return [Array] Tool spans + def tool_spans + spans&.select { |s| s["type"] == "tool" } || [] + end + + # Returns total token count. + # + # @return [Integer] Total tokens used + def total_tokens + (total_input_tokens || 0) + (total_output_tokens || 0) + (total_thinking_tokens || 0) + end + + # Returns whether this trace had an error. + # + # @return [Boolean] + def error? + status == STATUS_ERROR + end + + # Returns display name for the trace. + # + # @return [String] Display name (e.g., "WeatherAgent.forecast") + def display_name + if agent_class && agent_action + "#{agent_class}.#{agent_action}" + elsif agent_class + agent_class + else + trace_id&.first(8) + end + end + + # Returns formatted duration. + # + # @return [String] Duration in ms or s + def formatted_duration + return "—" unless total_duration_ms + + if total_duration_ms >= 1000 + "#{(total_duration_ms / 1000.0).round(2)}s" + else + "#{total_duration_ms.round(0)}ms" + end + end + + # Returns formatted token count. + # + # @return [String] Token count with K suffix for large numbers + def formatted_tokens + count = total_tokens + return "0" if count.zero? + + if count >= 1000 + "#{(count / 1000.0).round(1)}K" + else + count.to_s + end + end + + # Returns the provider used (from LLM spans). + # + # @return [String, nil] Provider name + def provider + llm_span = llm_spans.first + return nil unless llm_span + + llm_span.dig("attributes", "llm.provider") + end + + # Returns the model used (from LLM spans). + # + # @return [String, nil] Model name + def model + llm_span = llm_spans.first + return nil unless llm_span + + llm_span.dig("attributes", "llm.model") + end + end +end diff --git a/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb new file mode 100644 index 00000000..68787a2b --- /dev/null +++ b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb @@ -0,0 +1,105 @@ +
+ +
+
+ Trace ID +
<%= trace.trace_id %>
+
+
+ Service +
<%= trace.service_name || "—" %>
+
+
+ Provider +
<%= trace.provider || "—" %>
+
+
+ Model +
<%= trace.model || "—" %>
+
+
+ + <% if trace.error? && trace.error_message.present? %> +
+
+ + + +
+

Error

+

<%= trace.error_message %>

+
+
+
+ <% end %> + + +
+
+
+ Input: + <%= number_with_delimiter(trace.total_input_tokens || 0) %> +
+
+
+ Output: + <%= number_with_delimiter(trace.total_output_tokens || 0) %> +
+ <% if trace.total_thinking_tokens.to_i > 0 %> +
+
+ Thinking: + <%= number_with_delimiter(trace.total_thinking_tokens) %> +
+ <% end %> +
+ + + <% if trace.spans.present? %> +
+

Span Timeline

+
+ <% total_duration = trace.total_duration_ms || 1 %> + <% trace.spans.each_with_index do |span, index| %> + <% + start_pct = ((span["start_time_relative_ms"] || 0) / total_duration * 100).clamp(0, 100) + width_pct = ((span["duration_ms"] || 0) / total_duration * 100).clamp(0.5, 100 - start_pct) + span_type = span["type"] || "unknown" + colors = { + "root" => "bg-gray-400", + "prompt" => "bg-blue-400", + "llm" => "bg-indigo-500", + "generate" => "bg-indigo-400", + "tool" => "bg-amber-500", + "thinking" => "bg-purple-500", + "response" => "bg-green-400" + } + bg_color = colors[span_type] || "bg-gray-400" + %> +
+
"> + <%= span["name"]&.split(".")&.last || span_type %> +
+
+
" + style="left: <%= start_pct %>%; width: <%= width_pct %>%;" + title="<%= span["name"] %>: <%= span["duration_ms"]&.round(1) %>ms"> +
+
+
+ <%= span["duration_ms"] ? "#{span["duration_ms"].round(1)}ms" : "—" %> +
+
+ <% end %> +
+
+ <% end %> + + +
+ + View raw trace data + +
<%= JSON.pretty_generate(trace.as_json(except: [:id, :created_at, :updated_at])) %>
+
+
diff --git a/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb new file mode 100644 index 00000000..1b68ba4c --- /dev/null +++ b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb @@ -0,0 +1,135 @@ +
+ +
+
+
Total Traces
+
+ <%= number_with_delimiter(@metrics[:total_traces]) %> +
+
Last 24h
+
+ +
+
Total Tokens
+
+ <%= number_to_human(@metrics[:total_tokens], precision: 2) %> +
+
Last 24h
+
+ +
+
Avg Duration
+
+ <%= @metrics[:avg_duration_ms] >= 1000 ? "#{(@metrics[:avg_duration_ms] / 1000.0).round(2)}s" : "#{@metrics[:avg_duration_ms].round(0)}ms" %> +
+
Last 24h
+
+ +
+
Error Rate
+
+ <%= @metrics[:error_rate] %>% +
+
Last 24h
+
+ +
+
Active Agents
+
+ <%= @metrics[:unique_agents] %> +
+
Last 24h
+
+
+ + +
+
+

Recent Traces

+
+ <%= link_to "All", traces_path, class: "px-3 py-1 text-sm rounded #{params[:status].blank? ? 'bg-indigo-100 text-indigo-700' : 'text-gray-600 hover:bg-gray-100'}" %> + <%= link_to "Errors", traces_path(status: "error"), class: "px-3 py-1 text-sm rounded #{params[:status] == 'error' ? 'bg-red-100 text-red-700' : 'text-gray-600 hover:bg-gray-100'}" %> +
+
+ + <% if @traces.any? %> + + + + + + + + + + + + <% @traces.each do |trace| %> + + + + + + + + + + + + + + <% end %> + +
AgentStatusDurationTokensTime
+
+ + <%= trace.agent_class || "Unknown" %> + + <% if trace.agent_action %> + + .<%= trace.agent_action %> + + <% end %> +
+
<%= trace.trace_id&.first(8) %>
+
+ <% if trace.error? %> + + Error + + <% else %> + + OK + + <% end %> + + <%= trace.formatted_duration %> + +
+ <%= trace.formatted_tokens %> + <% if trace.total_thinking_tokens.to_i > 0 %> + + +<%= number_to_human(trace.total_thinking_tokens, precision: 1) %> thinking + + <% end %> +
+
+ <%= time_ago_in_words(trace.timestamp || trace.created_at) %> ago +
+ <% else %> +
+ + + +

No traces yet

+

+ Traces will appear here when your agents generate responses. +

+
+ <% end %> +
+
diff --git a/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb new file mode 100644 index 00000000..9b05bf0f --- /dev/null +++ b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb @@ -0,0 +1,143 @@ +
+

Metrics Overview

+ + +
+
+
+
+ + + +
+
+

Total Traces

+

+ <%= number_with_delimiter(@metrics[:total_traces]) %> +

+
+
+
+ +
+
+
+ + + +
+
+

Total Tokens

+

+ <%= number_to_human(@metrics[:total_tokens], precision: 2) %> +

+
+
+
+ +
+
+
+ + + +
+
+

Avg Duration

+

+ <%= @metrics[:avg_duration_ms] >= 1000 ? "#{(@metrics[:avg_duration_ms] / 1000.0).round(2)}s" : "#{@metrics[:avg_duration_ms].round(0)}ms" %> +

+
+
+
+ +
+
+
+ + + +
+
+

Error Rate

+

+ <%= @metrics[:error_rate] %>% +

+
+
+
+ +
+
+
+ + + +
+
+

Active Agents

+

+ <%= @metrics[:unique_agents] %> +

+
+
+
+
+ + +
+
+

Agent Statistics

+

Performance breakdown by agent class (last 24h)

+
+ + <% if @agent_stats.any? %> + + + + + + + + + + + + <% @agent_stats.each do |stat| %> + + + + + + + + <% end %> + +
AgentTracesTokensAvg DurationErrors
+ + <%= stat.agent_class || "Unknown" %> + + + <%= number_with_delimiter(stat.trace_count) %> + + <%= number_to_human(stat.total_tokens || 0, precision: 1) %> + + <% avg = stat.avg_duration.to_f %> + <%= avg >= 1000 ? "#{(avg / 1000.0).round(2)}s" : "#{avg.round(0)}ms" %> + + <% error_count = stat.error_count.to_i %> + <% if error_count > 0 %> + + <%= error_count %> + + <% else %> + 0 + <% end %> +
+ <% else %> +
+ No agent data available yet. +
+ <% end %> +
+
diff --git a/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb new file mode 100644 index 00000000..434730a7 --- /dev/null +++ b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb @@ -0,0 +1,36 @@ +
+ <%= link_to "← Back to traces", traces_path, class: "text-indigo-600 hover:text-indigo-800 text-sm" %> +
+ +
+
+
+
+

+ <%= @trace.display_name %> +

+

+ <%= @trace.trace_id %> +

+
+
+ <% if @trace.error? %> + + Error + + <% else %> + + Success + + <% end %> + + <%= @trace.timestamp&.strftime("%Y-%m-%d %H:%M:%S") %> + +
+
+
+ +
+ <%= render partial: "trace_detail", locals: { trace: @trace } %> +
+
diff --git a/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb b/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb new file mode 100644 index 00000000..5c15d9e1 --- /dev/null +++ b/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb @@ -0,0 +1,94 @@ + + + + + + ActiveAgent Dashboard + + + + + + + +
+ + + + +
+
+ <%= yield %> +
+
+
+ + diff --git a/lib/active_agent/dashboard/config/routes.rb b/lib/active_agent/dashboard/config/routes.rb new file mode 100644 index 00000000..0820ff69 --- /dev/null +++ b/lib/active_agent/dashboard/config/routes.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +ActiveAgent::Dashboard::Engine.routes.draw do + # Dashboard root + root to: "dashboard#index" + + # Dashboard + get "dashboard", to: "dashboard#index" + + # Traces + resources :traces, only: [ :index, :show ] do + collection do + get :metrics + end + end + + # Agents + resources :agents do + member do + post :execute + post :test + get :versions + post :restore_version + end + resources :runs, controller: "agent_runs", only: [ :index, :show ] do + member do + post :cancel + end + end + end + + # Agent Templates + resources :templates, only: [ :index, :show ] do + member do + post :create_agent + end + end + + # Sandbox Sessions + resources :sandboxes, controller: "sandbox_sessions" do + member do + post :provision + post :execute + post :expire + end + resources :runs, controller: "sandbox_runs", only: [ :index, :show ] + end + + # Session Recordings + resources :recordings, controller: "session_recordings", only: [ :index, :show ] do + member do + get :timeline + get :playback + end + end + + # API namespace + namespace :api do + namespace :v1 do + # Telemetry ingestion + resources :traces, only: [ :create ] + + # Agent execution API + resources :agents, only: [ :index, :show ] do + member do + post :execute + end + end + + # Sandbox API + resources :sandboxes, only: [ :create, :show ] do + member do + post :execute + end + end + end + end +end diff --git a/lib/active_agent/dashboard/engine.rb b/lib/active_agent/dashboard/engine.rb new file mode 100644 index 00000000..7fac7feb --- /dev/null +++ b/lib/active_agent/dashboard/engine.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module ActiveAgent + module Dashboard + # Rails Engine for the ActiveAgent Dashboard. + # + # Provides a full-featured dashboard for managing agents, viewing telemetry, + # running sandboxes, and recording sessions. + # + # Mount in your routes: + # mount ActiveAgent::Dashboard::Engine => "/active_agent" + # + class Engine < ::Rails::Engine + isolate_namespace ActiveAgent::Dashboard + + # Use a unique engine name to avoid conflicts + engine_name "active_agent" + + # Override engine root to point to the dashboard directory + # This must be set before paths are computed + def self.root + @root ||= Pathname.new(File.expand_path("..", __FILE__)) + end + + # Alias for consistency with existing code + def self.dashboard_root + root + end + + config.active_agent_dashboard = ActiveSupport::OrderedOptions.new + + initializer "active_agent.dashboard.append_view_paths" do + ActiveSupport.on_load(:action_controller) do + prepend_view_path ActiveAgent::Dashboard::Engine.root.join("app", "views").to_s + end + end + end + end +end diff --git a/lib/active_agent/railtie.rb b/lib/active_agent/railtie.rb index 76477662..baba8f9e 100644 --- a/lib/active_agent/railtie.rb +++ b/lib/active_agent/railtie.rb @@ -10,6 +10,7 @@ module ActiveAgent class Railtie < Rails::Railtie # :nodoc: config.active_agent = ActiveSupport::OrderedOptions.new config.active_agent.preview_paths = [] + config.active_agent.telemetry = ActiveSupport::OrderedOptions.new config.eager_load_namespaces << ActiveAgent initializer "active_agent.deprecator", before: :load_environment_config do |app| @@ -40,6 +41,35 @@ class Railtie < Rails::Railtie # :nodoc: ActiveAgent.configuration_load(Rails.root.join("config", "active_agent.yml")) # endregion configuration_load + # region telemetry_configuration + # Load telemetry configuration from activeagent.yml or Rails config + telemetry_config = ActiveAgent.configuration[:telemetry] + if telemetry_config.is_a?(Hash) + ActiveAgent::Telemetry.configure do |config| + config.load_from_hash(telemetry_config) + end + end + + # Also support Rails config.active_agent.telemetry + if options.telemetry.present? + ActiveAgent::Telemetry.configure do |config| + config.enabled = options.telemetry[:enabled] if options.telemetry.key?(:enabled) + config.endpoint = options.telemetry[:endpoint] if options.telemetry.key?(:endpoint) + config.api_key = options.telemetry[:api_key] if options.telemetry.key?(:api_key) + config.sample_rate = options.telemetry[:sample_rate] if options.telemetry.key?(:sample_rate) + config.service_name = options.telemetry[:service_name] if options.telemetry.key?(:service_name) + end + end + + # Apply instrumentation to ActiveAgent::Base when telemetry is enabled + if ActiveAgent::Telemetry.enabled? + ActiveSupport.on_load(:active_agent) do + include ActiveAgent::Telemetry::Instrumentation + instrument_telemetry! + end + end + # endregion telemetry_configuration + ActiveSupport.on_load(:active_agent) do include AbstractController::UrlFor extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false) @@ -55,7 +85,8 @@ class Railtie < Rails::Railtie # :nodoc: self.generation_job = generation_job.constantize end - options.each { |k, v| send(:"#{k}=", v) } + # Skip telemetry config - it's handled separately above + options.except(:telemetry).each { |k, v| send(:"#{k}=", v) } end ActiveSupport.on_load(:action_dispatch_integration_test) do diff --git a/lib/active_agent/telemetry.rb b/lib/active_agent/telemetry.rb new file mode 100644 index 00000000..51a0d170 --- /dev/null +++ b/lib/active_agent/telemetry.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require "net/http" +require "json" +require "securerandom" + +module ActiveAgent + # Telemetry module for collecting and reporting agent traces. + # + # Provides optional observability by capturing agent generation traces, + # tool calls, token usage, and errors. Reports to a configured endpoint + # (self-hosted or ActiveAgents.ai hosted service). + # + # = Features + # + # * **Trace Collection**: Captures full generation lifecycle with spans + # * **Token Tracking**: Records input/output/thinking tokens per generation + # * **Tool Call Tracing**: Captures tool invocations with arguments and results + # * **Error Tracking**: Records errors with backtraces + # * **Async Reporting**: Non-blocking HTTP reporting with background thread + # + # = Configuration + # + # Configure in your Rails initializer or activeagent.yml: + # + # @example Basic configuration + # ActiveAgent::Telemetry.configure do |config| + # config.enabled = true + # config.endpoint = "https://api.activeagents.ai/v1/traces" + # config.api_key = Rails.application.credentials.dig(:activeagents, :api_key) + # end + # + # @example YAML configuration (config/activeagent.yml) + # telemetry: + # enabled: true + # endpoint: https://api.activeagents.ai/v1/traces + # api_key: <%= Rails.application.credentials.dig(:activeagents, :api_key) %> + # sample_rate: 1.0 + # batch_size: 100 + # flush_interval: 5 + # + # @example Self-hosted endpoint + # ActiveAgent::Telemetry.configure do |config| + # config.endpoint = "https://observability.mycompany.com/v1/traces" + # config.api_key = ENV["TELEMETRY_API_KEY"] + # end + # + # @see ActiveAgent::Telemetry::Configuration + # @see ActiveAgent::Telemetry::Tracer + module Telemetry + extend ActiveSupport::Autoload + + autoload :Configuration + autoload :Tracer + autoload :Span + autoload :Reporter + autoload :Instrumentation + + class << self + # Returns the telemetry configuration instance. + # + # @return [Configuration] The configuration instance + def configuration + @configuration ||= Configuration.new + end + + # Configures telemetry with a block. + # + # @yield [config] Yields the configuration instance + # @yieldparam config [Configuration] The configuration to modify + # @return [Configuration] The modified configuration + # + # @example + # ActiveAgent::Telemetry.configure do |config| + # config.enabled = true + # config.endpoint = "https://api.activeagents.ai/v1/traces" + # config.api_key = "your-api-key" + # end + def configure + yield configuration if block_given? + configuration + end + + # Resets the configuration to defaults. + # + # @return [Configuration] New default configuration + def reset_configuration! + @configuration = Configuration.new + end + + # Returns whether telemetry is enabled and configured. + # + # @return [Boolean] True if telemetry should collect and report + def enabled? + configuration.enabled? && configuration.configured? + end + + # Returns the global tracer instance. + # + # @return [Tracer] The tracer instance + def tracer + @tracer ||= Tracer.new(configuration) + end + + # Starts a new trace for an agent generation. + # + # @param name [String] Name of the trace (typically agent.action) + # @param attributes [Hash] Additional trace attributes + # @yield [trace] Yields the trace for adding spans + # @return [Span] The root span of the trace + # + # @example + # ActiveAgent::Telemetry.trace("WeatherAgent.forecast") do |trace| + # trace.add_span("llm.generate", provider: "anthropic") + # trace.set_tokens(input: 100, output: 50) + # end + def trace(name, **attributes, &block) + return yield(NullSpan.new) unless enabled? + + tracer.trace(name, **attributes, &block) + end + + # Records a standalone span (outside of a trace context). + # + # @param name [String] Span name + # @param attributes [Hash] Span attributes + # @return [Span] The created span + def span(name, **attributes) + return NullSpan.new unless enabled? + + tracer.span(name, **attributes) + end + + # Flushes any buffered traces immediately. + # + # @return [void] + def flush + tracer.flush if enabled? + end + + # Shuts down telemetry, flushing remaining traces. + # + # @return [void] + def shutdown + tracer.shutdown if @tracer + end + end + + # Null span implementation for when telemetry is disabled. + # + # Provides no-op methods that match Span interface to avoid + # nil checks throughout the codebase. + class NullSpan + def add_span(name, **attributes); self; end + def set_attribute(key, value); self; end + def set_tokens(input: 0, output: 0, thinking: 0); self; end + def set_status(status, message = nil); self; end + def record_error(error); self; end + def finish; self; end + end + end +end diff --git a/lib/active_agent/telemetry/configuration.rb b/lib/active_agent/telemetry/configuration.rb new file mode 100644 index 00000000..89e7b032 --- /dev/null +++ b/lib/active_agent/telemetry/configuration.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +module ActiveAgent + module Telemetry + # Configuration for telemetry collection and reporting. + # + # Stores settings for endpoint, authentication, sampling, and batching. + # Configuration can be set programmatically or loaded from YAML. + # + # @example Programmatic configuration + # ActiveAgent::Telemetry.configure do |config| + # config.enabled = true + # config.endpoint = "https://api.activeagents.ai/v1/traces" + # config.api_key = "your-api-key" + # config.sample_rate = 1.0 + # end + # + # @example YAML configuration (config/activeagent.yml) + # telemetry: + # enabled: true + # endpoint: https://api.activeagents.ai/v1/traces + # api_key: <%= ENV["ACTIVEAGENTS_API_KEY"] %> + # sample_rate: 1.0 + # batch_size: 100 + # flush_interval: 5 + # + class Configuration + # @return [Boolean] Whether telemetry is enabled (default: false) + attr_accessor :enabled + + # @return [String] The endpoint URL for sending traces + attr_accessor :endpoint + + # @return [String] API key for authentication + attr_accessor :api_key + + # @return [Float] Sampling rate from 0.0 to 1.0 (default: 1.0) + attr_accessor :sample_rate + + # @return [Integer] Number of traces to batch before sending (default: 100) + attr_accessor :batch_size + + # @return [Integer] Seconds between automatic flushes (default: 5) + attr_accessor :flush_interval + + # @return [Integer] HTTP timeout in seconds (default: 10) + attr_accessor :timeout + + # @return [Boolean] Whether to capture request/response bodies (default: false) + attr_accessor :capture_bodies + + # @return [Array] Attributes to redact from traces + attr_accessor :redact_attributes + + # @return [String] Service name for trace attribution + attr_accessor :service_name + + # @return [String] Environment name (development, staging, production) + attr_accessor :environment + + # @return [Hash] Additional resource attributes to include in all traces + attr_accessor :resource_attributes + + # @return [Logger] Logger for telemetry operations + attr_accessor :logger + + # @return [Boolean] Whether to store traces locally in the app's database + attr_accessor :local_storage + + # Default ActiveAgents.ai endpoint for hosted observability. + DEFAULT_ENDPOINT = "https://api.activeagents.ai/v1/traces" + + # Local dashboard endpoint path (relative to app root) + LOCAL_ENDPOINT_PATH = "/active_agent/api/traces" + + def initialize + @enabled = false + @endpoint = DEFAULT_ENDPOINT + @api_key = nil + @sample_rate = 1.0 + @batch_size = 100 + @flush_interval = 5 + @timeout = 10 + @capture_bodies = false + @redact_attributes = %w[password secret token key credential api_key] + @service_name = nil + @environment = Rails.env if defined?(Rails) + @resource_attributes = {} + @logger = nil + @local_storage = false + end + + # Returns whether telemetry collection is enabled. + # + # @return [Boolean] + def enabled? + @enabled == true + end + + # Returns whether telemetry is properly configured. + # + # Checks that endpoint and api_key are present, or local_storage is enabled. + # + # @return [Boolean] + def configured? + local_storage? || (endpoint.present? && api_key.present?) + end + + # Returns whether local storage mode is enabled. + # + # @return [Boolean] + def local_storage? + @local_storage == true + end + + # Returns the resolved endpoint for trace reporting. + # + # Uses local endpoint when local_storage is enabled. + # + # @return [String] + def resolved_endpoint + if local_storage? + LOCAL_ENDPOINT_PATH + else + endpoint + end + end + + # Returns whether a trace should be sampled. + # + # Uses sample_rate to determine if trace should be collected. + # + # @return [Boolean] + def should_sample? + return true if sample_rate >= 1.0 + return false if sample_rate <= 0.0 + + rand < sample_rate + end + + # Resolves the service name for traces. + # + # Falls back to Rails application name or "activeagent". + # + # @return [String] + def resolved_service_name + @service_name || rails_app_name || "activeagent" + end + + # Returns the logger for telemetry operations. + # + # Falls back to Rails.logger or a null logger. + # + # @return [Logger] + def resolved_logger + @logger || (defined?(Rails) && Rails.logger) || Logger.new(File::NULL) + end + + # Loads configuration from a hash (typically from YAML). + # + # @param hash [Hash] Configuration hash + # @return [self] + def load_from_hash(hash) + hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access) + + @enabled = hash[:enabled] if hash.key?(:enabled) + @endpoint = hash[:endpoint] if hash.key?(:endpoint) + @api_key = hash[:api_key] if hash.key?(:api_key) + @sample_rate = hash[:sample_rate].to_f if hash.key?(:sample_rate) + @batch_size = hash[:batch_size].to_i if hash.key?(:batch_size) + @flush_interval = hash[:flush_interval].to_i if hash.key?(:flush_interval) + @timeout = hash[:timeout].to_i if hash.key?(:timeout) + @capture_bodies = hash[:capture_bodies] if hash.key?(:capture_bodies) + @redact_attributes = hash[:redact_attributes] if hash.key?(:redact_attributes) + @service_name = hash[:service_name] if hash.key?(:service_name) + @environment = hash[:environment] if hash.key?(:environment) + @resource_attributes = hash[:resource_attributes] if hash.key?(:resource_attributes) + @local_storage = hash[:local_storage] if hash.key?(:local_storage) + + self + end + + # Returns configuration as a hash for serialization. + # + # @return [Hash] + def to_h + { + enabled: enabled, + endpoint: endpoint, + api_key: api_key ? "[REDACTED]" : nil, + sample_rate: sample_rate, + batch_size: batch_size, + flush_interval: flush_interval, + timeout: timeout, + capture_bodies: capture_bodies, + service_name: resolved_service_name, + environment: environment, + local_storage: local_storage + } + end + + private + + def rails_app_name + return nil unless defined?(Rails) && Rails.application + + Rails.application.class.module_parent_name.underscore + rescue StandardError + nil + end + end + end +end diff --git a/lib/active_agent/telemetry/instrumentation.rb b/lib/active_agent/telemetry/instrumentation.rb new file mode 100644 index 00000000..f9ddb53a --- /dev/null +++ b/lib/active_agent/telemetry/instrumentation.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +module ActiveAgent + module Telemetry + # Auto-instrumentation for ActiveAgent generation lifecycle. + # + # When included in ActiveAgent::Base, automatically traces: + # - Agent generation (prompt_now, generate_now) + # - Tool calls + # - Streaming events + # - Errors + # + # @example Enabling instrumentation + # # In config/initializers/activeagent.rb + # ActiveAgent::Telemetry.configure do |config| + # config.enabled = true + # config.endpoint = "https://api.activeagents.ai/v1/traces" + # config.api_key = Rails.application.credentials.activeagents_api_key + # end + # + # # Instrumentation is automatically applied when telemetry is enabled + # + module Instrumentation + extend ActiveSupport::Concern + + included do + # Hook into generation lifecycle + around_generate :trace_generation if respond_to?(:around_generate) + end + + class_methods do + # Installs instrumentation on the agent class. + # + # Called automatically when telemetry is enabled. + def instrument_telemetry! + return if @telemetry_instrumented + + prepend GenerationInstrumentation + @telemetry_instrumented = true + end + end + + # Module prepended to intercept generation methods. + module GenerationInstrumentation + # Wraps process_prompt with telemetry tracing. + def process_prompt + return super unless Telemetry.enabled? + + Telemetry.trace("#{self.class.name}.#{action_name}", span_type: :root) do |span| + span.set_attribute("agent.class", self.class.name) + span.set_attribute("agent.action", action_name.to_s) + span.set_attribute("agent.provider", provider_name) if respond_to?(:provider_name) + span.set_attribute("agent.model", model_name) if respond_to?(:model_name) + + # Add prompt span + prompt_span = span.add_span("agent.prompt", span_type: :prompt) + prompt_span.set_attribute("messages.count", messages.size) if respond_to?(:messages) + prompt_span.finish + + # Execute generation with LLM span + llm_span = span.add_span("llm.generate", span_type: :llm) + llm_span.set_attribute("llm.provider", provider_name) if respond_to?(:provider_name) + llm_span.set_attribute("llm.model", model_name) if respond_to?(:model_name) + + begin + result = super + + # Record token usage from response + if result.respond_to?(:usage) && result.usage.present? + usage = result.usage + # Usage model uses methods, not hash access + input_tokens = (usage.input_tokens rescue 0) || 0 + output_tokens = (usage.output_tokens rescue 0) || 0 + reasoning_tokens = (usage.reasoning_tokens rescue 0) || 0 + + llm_span.set_tokens( + input: input_tokens.to_i, + output: output_tokens.to_i, + thinking: reasoning_tokens.to_i + ) + span.set_tokens( + input: input_tokens.to_i, + output: output_tokens.to_i, + thinking: reasoning_tokens.to_i + ) + end + + # Record tool calls if present + if result.respond_to?(:tool_calls) && result.tool_calls.present? + result.tool_calls.each do |tool_call| + tool_span = span.add_span("tool.#{tool_call[:name]}", span_type: :tool) + tool_span.set_attribute("tool.name", tool_call[:name]) + tool_span.set_attribute("tool.id", tool_call[:id]) if tool_call[:id] + tool_span.finish + end + end + + llm_span.set_status(:ok) + llm_span.finish + span.set_status(:ok) + + result + rescue StandardError => e + llm_span.record_error(e) + llm_span.finish + span.record_error(e) + raise + end + end + end + + # Wraps process_embed with telemetry tracing. + def process_embed + return super unless Telemetry.enabled? + + Telemetry.trace("#{self.class.name}.embed", span_type: :embedding) do |span| + span.set_attribute("agent.class", self.class.name) + span.set_attribute("agent.action", "embed") + span.set_attribute("agent.provider", provider_name) if respond_to?(:provider_name) + + begin + result = super + + if result.respond_to?(:usage) && result.usage.present? + usage = result.usage + input_tokens = (usage.input_tokens rescue 0) || 0 + span.set_tokens(input: input_tokens.to_i) + end + + span.set_status(:ok) + result + rescue StandardError => e + span.record_error(e) + raise + end + end + end + + private + + def provider_name + self.class.generation_provider&.to_s || "unknown" + rescue StandardError + "unknown" + end + + def model_name + prompt_options[:model] || "unknown" + rescue StandardError + "unknown" + end + end + end + end +end diff --git a/lib/active_agent/telemetry/reporter.rb b/lib/active_agent/telemetry/reporter.rb new file mode 100644 index 00000000..11679f84 --- /dev/null +++ b/lib/active_agent/telemetry/reporter.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +require "net/http" +require "json" +require "uri" + +module ActiveAgent + module Telemetry + # Asynchronously reports traces to the telemetry endpoint. + # + # Buffers traces and sends them in batches to reduce network overhead. + # Uses a background thread for non-blocking transmission. + # + # @example + # reporter = Reporter.new(configuration) + # reporter.report(trace_payload) + # reporter.flush # Send immediately + # reporter.shutdown # Clean shutdown + # + class Reporter + # @return [Configuration] Telemetry configuration + attr_reader :configuration + + def initialize(configuration) + @configuration = configuration + @buffer = [] + @mutex = Mutex.new + @running = false + @thread = nil + @shutdown = false + + start_flush_thread if configuration.enabled? + end + + # Adds a trace to the buffer for transmission. + # + # @param trace [Hash] Trace payload + # @return [void] + def report(trace) + return unless configuration.enabled? + + @mutex.synchronize do + @buffer << trace + + # Flush immediately if buffer is full + if @buffer.size >= configuration.batch_size + flush_buffer + end + end + end + + # Flushes all buffered traces immediately. + # + # @return [void] + def flush + @mutex.synchronize do + flush_buffer + end + end + + # Shuts down the reporter, flushing remaining traces. + # + # @return [void] + def shutdown + @shutdown = true + flush + @thread&.join(5) # Wait up to 5 seconds for thread to finish + end + + private + + # Starts the background flush thread. + def start_flush_thread + @running = true + @thread = Thread.new do + Thread.current.name = "activeagent-telemetry-reporter" + + while @running && !@shutdown + sleep(configuration.flush_interval) + + @mutex.synchronize do + flush_buffer if @buffer.any? + end + end + end + end + + # Flushes the buffer by sending traces to the endpoint. + # + # Must be called within @mutex synchronization. + def flush_buffer + return if @buffer.empty? + + traces = @buffer.dup + @buffer.clear + + Thread.new { send_traces(traces) } + end + + # Sends traces to the configured endpoint. + # + # @param traces [Array] Traces to send + def send_traces(traces) + # Use direct database storage for local mode + if configuration.local_storage? + store_traces_locally(traces) + return + end + + uri = URI.parse(configuration.endpoint) + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == "https" + http.open_timeout = configuration.timeout + http.read_timeout = configuration.timeout + + request = Net::HTTP::Post.new(uri.request_uri) + request["Content-Type"] = "application/json" + request["Authorization"] = "Bearer #{configuration.api_key}" + request["User-Agent"] = "ActiveAgent/#{ActiveAgent::VERSION} Ruby/#{RUBY_VERSION}" + request["X-Service-Name"] = configuration.resolved_service_name + request["X-Environment"] = configuration.environment + + payload = { + traces: traces, + sdk: { + name: "activeagent", + version: ActiveAgent::VERSION, + language: "ruby", + runtime_version: RUBY_VERSION + } + } + + request.body = JSON.generate(payload) + + response = http.request(request) + + unless response.is_a?(Net::HTTPSuccess) + log_error("Failed to send traces: #{response.code} #{response.message}") + end + rescue StandardError => e + log_error("Error sending traces: #{e.class} - #{e.message}") + end + + # Stores traces directly in the local database. + # + # @param traces [Array] Traces to store + def store_traces_locally(traces) + sdk_info = { + name: "activeagent", + version: ActiveAgent::VERSION, + language: "ruby", + runtime_version: RUBY_VERSION + } + + traces.each do |trace| + # Skip if trace already exists (idempotency) + next if ActiveAgent::TelemetryTrace.exists?(trace_id: trace["trace_id"]) + + ActiveAgent::TelemetryTrace.create_from_payload(trace, sdk_info) + rescue StandardError => e + log_error("Failed to store trace locally: #{e.class} - #{e.message}") + end + rescue StandardError => e + log_error("Error storing traces locally: #{e.class} - #{e.message}") + end + + # Logs an error message. + # + # @param message [String] Error message + def log_error(message) + configuration.resolved_logger.error("[ActiveAgent::Telemetry] #{message}") + end + end + end +end diff --git a/lib/active_agent/telemetry/span.rb b/lib/active_agent/telemetry/span.rb new file mode 100644 index 00000000..f8b8d282 --- /dev/null +++ b/lib/active_agent/telemetry/span.rb @@ -0,0 +1,267 @@ +# frozen_string_literal: true + +module ActiveAgent + module Telemetry + # Represents a single span in a trace. + # + # Spans capture discrete operations within a trace, such as LLM calls, + # tool invocations, or prompt rendering. Each span has timing, attributes, + # and can have child spans. + # + # @example Creating a span + # span = Span.new("llm.generate", trace_id: trace.trace_id) + # span.set_attribute("provider", "anthropic") + # span.set_attribute("model", "claude-3-5-sonnet") + # span.set_tokens(input: 100, output: 50) + # span.finish + # + class Span + # Span types for categorization + TYPES = { + root: "root", # Root span for entire generation + prompt: "prompt", # Prompt preparation/rendering + llm: "llm", # LLM API call + tool: "tool", # Tool invocation + thinking: "thinking", # Extended thinking (Anthropic) + embedding: "embedding", # Embedding generation + error: "error" # Error handling + }.freeze + + # Span status codes + STATUS = { + unset: "UNSET", + ok: "OK", + error: "ERROR" + }.freeze + + # @return [String] Unique identifier for this span + attr_reader :span_id + + # @return [String] Trace ID this span belongs to + attr_reader :trace_id + + # @return [String, nil] Parent span ID + attr_reader :parent_span_id + + # @return [String] Span name (e.g., "llm.generate", "tool.get_weather") + attr_reader :name + + # @return [String] Span type from TYPES + attr_reader :span_type + + # @return [Time] When the span started + attr_reader :start_time + + # @return [Time, nil] When the span ended + attr_reader :end_time + + # @return [Hash] Span attributes + attr_reader :attributes + + # @return [Array] Child spans + attr_reader :children + + # @return [String] Status code from STATUS + attr_reader :status + + # @return [String, nil] Status message + attr_reader :status_message + + # @return [Array] Events recorded during the span + attr_reader :events + + # Creates a new span. + # + # @param name [String] Span name + # @param trace_id [String] Parent trace ID + # @param parent_span_id [String, nil] Parent span ID + # @param span_type [Symbol] Type of span + # @param attributes [Hash] Initial attributes + def initialize(name, trace_id:, parent_span_id: nil, span_type: :root, **attributes) + @span_id = SecureRandom.hex(8) + @trace_id = trace_id + @parent_span_id = parent_span_id + @name = name + @span_type = TYPES[span_type] || span_type.to_s + @start_time = Time.current + @end_time = nil + @attributes = attributes.transform_keys(&:to_s) + @children = [] + @status = STATUS[:unset] + @status_message = nil + @events = [] + @tokens = { input: 0, output: 0, thinking: 0, total: 0 } + end + + # Creates a child span. + # + # @param name [String] Child span name + # @param span_type [Symbol] Type of span + # @param attributes [Hash] Span attributes + # @return [Span] The child span + def add_span(name, span_type: :root, **attributes) + child = Span.new( + name, + trace_id: trace_id, + parent_span_id: span_id, + span_type: span_type, + **attributes + ) + @children << child + child + end + + # Sets a single attribute. + # + # @param key [String, Symbol] Attribute key + # @param value [Object] Attribute value + # @return [self] + def set_attribute(key, value) + @attributes[key.to_s] = value + self + end + + # Sets multiple attributes at once. + # + # @param attrs [Hash] Attributes to set + # @return [self] + def set_attributes(attrs) + attrs.each { |k, v| set_attribute(k, v) } + self + end + + # Sets token usage for LLM spans. + # + # @param input [Integer] Input token count + # @param output [Integer] Output token count + # @param thinking [Integer] Thinking token count (Anthropic extended thinking) + # @return [self] + def set_tokens(input: 0, output: 0, thinking: 0) + @tokens = { + input: input, + output: output, + thinking: thinking, + total: input + output + thinking + } + set_attribute("tokens.input", input) + set_attribute("tokens.output", output) + set_attribute("tokens.thinking", thinking) if thinking > 0 + set_attribute("tokens.total", @tokens[:total]) + self + end + + # Returns token usage. + # + # @return [Hash] Token counts + def tokens + @tokens.dup + end + + # Sets the span status. + # + # @param code [Symbol] Status code (:ok, :error, :unset) + # @param message [String, nil] Optional status message + # @return [self] + def set_status(code, message = nil) + @status = STATUS[code] || STATUS[:unset] + @status_message = message + self + end + + # Records an error on the span. + # + # @param error [Exception] The error to record + # @return [self] + def record_error(error) + set_status(:error, error.message) + set_attribute("error.type", error.class.name) + set_attribute("error.message", error.message) + set_attribute("error.backtrace", error.backtrace&.first(10)&.join("\n")) + + add_event("exception", { + "exception.type" => error.class.name, + "exception.message" => error.message, + "exception.stacktrace" => error.backtrace&.join("\n") + }) + + self + end + + # Adds an event to the span. + # + # @param name [String] Event name + # @param attributes [Hash] Event attributes + # @return [self] + def add_event(name, attributes = {}) + @events << { + name: name, + timestamp: Time.current.iso8601(6), + attributes: attributes.transform_keys(&:to_s) + } + self + end + + # Marks the span as finished. + # + # @return [self] + def finish + @end_time = Time.current + set_status(:ok) if @status == STATUS[:unset] + self + end + + # Returns whether the span is finished. + # + # @return [Boolean] + def finished? + !@end_time.nil? + end + + # Returns the duration in milliseconds. + # + # @return [Float, nil] Duration or nil if not finished + def duration_ms + return nil unless finished? + + ((@end_time - @start_time) * 1000).round(2) + end + + # Serializes the span for transmission. + # + # @return [Hash] Serialized span data + def to_h + { + span_id: span_id, + trace_id: trace_id, + parent_span_id: parent_span_id, + name: name, + type: span_type, + start_time: start_time.iso8601(6), + end_time: end_time&.iso8601(6), + duration_ms: duration_ms, + status: status, + status_message: status_message, + attributes: attributes, + tokens: tokens, + events: events, + children: children.map(&:to_h) + } + end + + # Executes a block and records timing/errors. + # + # @yield Block to execute within the span + # @return [Object] Result of the block + def measure + result = yield + set_status(:ok) + result + rescue StandardError => e + record_error(e) + raise + ensure + finish + end + end + end +end diff --git a/lib/active_agent/telemetry/tracer.rb b/lib/active_agent/telemetry/tracer.rb new file mode 100644 index 00000000..a9665ca1 --- /dev/null +++ b/lib/active_agent/telemetry/tracer.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +module ActiveAgent + module Telemetry + # Manages trace creation and lifecycle. + # + # The Tracer creates traces, manages the current trace context, + # and coordinates with the Reporter for async transmission. + # + # @example Basic usage + # tracer = Tracer.new(configuration) + # tracer.trace("MyAgent.greet") do |span| + # span.set_attribute("user_id", 123) + # span.add_span("llm.generate", span_type: :llm) + # end + # + class Tracer + # @return [Configuration] Telemetry configuration + attr_reader :configuration + + # @return [Reporter] The reporter for sending traces + attr_reader :reporter + + # Thread-local storage for current trace context + CURRENT_SPAN_KEY = :active_agent_telemetry_current_span + + def initialize(configuration) + @configuration = configuration + @reporter = Reporter.new(configuration) + @mutex = Mutex.new + end + + # Creates and executes a new trace. + # + # @param name [String] Trace name (typically "AgentClass.action") + # @param attributes [Hash] Root span attributes + # @yield [span] Yields the root span for adding child spans + # @return [Object] Result of the block + # + # @example + # tracer.trace("WeatherAgent.forecast") do |span| + # span.set_attribute("location", "Seattle") + # result = do_llm_call + # span.set_tokens(input: 100, output: 50) + # result + # end + def trace(name, **attributes, &block) + return yield(Telemetry::NullSpan.new) unless should_trace? + + trace_id = generate_trace_id + root_span = Span.new( + name, + trace_id: trace_id, + span_type: :root, + **default_attributes.merge(attributes) + ) + + with_span(root_span) do + result = yield(root_span) + root_span.finish + report_trace(root_span) + result + end + rescue StandardError => e + root_span&.record_error(e) + root_span&.finish + report_trace(root_span) if root_span + raise + end + + # Creates a standalone span (not within a trace block). + # + # @param name [String] Span name + # @param attributes [Hash] Span attributes + # @return [Span] The created span + def span(name, **attributes) + return Telemetry::NullSpan.new unless should_trace? + + current = current_span + if current + current.add_span(name, **attributes) + else + Span.new(name, trace_id: generate_trace_id, **default_attributes.merge(attributes)) + end + end + + # Returns the current span from thread-local storage. + # + # @return [Span, nil] Current span or nil + def current_span + Thread.current[CURRENT_SPAN_KEY] + end + + # Flushes buffered traces immediately. + # + # @return [void] + def flush + reporter.flush + end + + # Shuts down the tracer and reporter. + # + # @return [void] + def shutdown + reporter.shutdown + end + + private + + # Executes block with span as current context. + # + # @param span [Span] Span to set as current + # @yield Block to execute + # @return [Object] Result of block + def with_span(span) + previous = Thread.current[CURRENT_SPAN_KEY] + Thread.current[CURRENT_SPAN_KEY] = span + yield + ensure + Thread.current[CURRENT_SPAN_KEY] = previous + end + + # Reports a completed trace to the reporter. + # + # @param span [Span] Root span of the trace + def report_trace(span) + reporter.report(build_trace_payload(span)) + end + + # Builds the trace payload for transmission. + # + # @param root_span [Span] Root span + # @return [Hash] Trace payload + def build_trace_payload(root_span) + { + trace_id: root_span.trace_id, + service_name: configuration.resolved_service_name, + environment: configuration.environment, + timestamp: Time.current.iso8601(6), + resource_attributes: configuration.resource_attributes, + spans: flatten_spans(root_span) + } + end + + # Flattens span hierarchy into array. + # + # @param span [Span] Root span + # @return [Array] Flattened span data + def flatten_spans(span) + result = [ span.to_h.except(:children) ] + span.children.each do |child| + result.concat(flatten_spans(child)) + end + result + end + + # Returns whether this trace should be sampled. + # + # @return [Boolean] + def should_trace? + configuration.enabled? && configuration.configured? && configuration.should_sample? + end + + # Generates a unique trace ID. + # + # @return [String] 32-character hex trace ID + def generate_trace_id + SecureRandom.hex(16) + end + + # Returns default attributes for all spans. + # + # @return [Hash] Default attributes + def default_attributes + { + "service.name" => configuration.resolved_service_name, + "service.environment" => configuration.environment, + "telemetry.sdk.name" => "activeagent", + "telemetry.sdk.version" => ActiveAgent::VERSION + } + end + end + end +end diff --git a/lib/generators/active_agent/dashboard/install/install_generator.rb b/lib/generators/active_agent/dashboard/install/install_generator.rb new file mode 100644 index 00000000..ede09e99 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/install_generator.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/active_record" + +module ActiveAgent + module Dashboard + module Generators + # Generator for installing the ActiveAgent Dashboard engine. + # + # Usage: + # rails generate active_agent:dashboard:install + # + # This will: + # - Copy migration files for all dashboard models + # - Create an initializer for configuration + # - Add the engine mount to routes.rb + # - Seed default agent templates + # + class InstallGenerator < Rails::Generators::Base + include ActiveRecord::Generators::Migration + + source_root File.expand_path("templates", __dir__) + + class_option :multi_tenant, type: :boolean, default: false, + desc: "Configure for multi-tenant mode with account association" + + class_option :skip_migrations, type: :boolean, default: false, + desc: "Skip copying migration files" + + class_option :skip_routes, type: :boolean, default: false, + desc: "Skip adding route mount" + + def copy_migrations + return if options[:skip_migrations] + + migration_template "migrations/create_active_agent_agents.rb", + "db/migrate/create_active_agent_agents.rb" + + migration_template "migrations/create_active_agent_agent_versions.rb", + "db/migrate/create_active_agent_agent_versions.rb" + + migration_template "migrations/create_active_agent_agent_runs.rb", + "db/migrate/create_active_agent_agent_runs.rb" + + migration_template "migrations/create_active_agent_agent_templates.rb", + "db/migrate/create_active_agent_agent_templates.rb" + + migration_template "migrations/create_active_agent_sandbox_sessions.rb", + "db/migrate/create_active_agent_sandbox_sessions.rb" + + migration_template "migrations/create_active_agent_sandbox_runs.rb", + "db/migrate/create_active_agent_sandbox_runs.rb" + + migration_template "migrations/create_active_agent_session_recordings.rb", + "db/migrate/create_active_agent_session_recordings.rb" + + migration_template "migrations/create_active_agent_telemetry_traces.rb", + "db/migrate/create_active_agent_telemetry_traces.rb" + end + + def create_initializer + template "initializer.rb", "config/initializers/active_agent_dashboard.rb" + end + + def mount_engine + return if options[:skip_routes] + + route 'mount ActiveAgent::Dashboard::Engine => "/active_agent"' + end + + def show_post_install + say "" + say "ActiveAgent Dashboard installed!", :green + say "" + say "Next steps:" + say " 1. Run migrations: rails db:migrate" + say " 2. Seed templates: rails active_agent:dashboard:seed" + say " 3. Configure authentication in config/initializers/active_agent_dashboard.rb" + say " 4. Visit /active_agent to access the dashboard" + say "" + end + + private + + def migration_version + "[#{ActiveRecord::Migration.current_version}]" + end + + def multi_tenant? + options[:multi_tenant] + end + end + end + end +end diff --git a/lib/generators/active_agent/dashboard/install/templates/initializer.rb b/lib/generators/active_agent/dashboard/install/templates/initializer.rb new file mode 100644 index 00000000..e8d3a279 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/initializer.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# ActiveAgent Dashboard Configuration +# +# This initializer configures the ActiveAgent Dashboard engine. +# See https://docs.activeagents.ai/dashboard for full documentation. + +ActiveAgent::Dashboard.configure do |config| + # ========================================================================== + # Authentication + # ========================================================================== + + # Set an authentication method that will be called on all dashboard controllers. + # This should authenticate the user and redirect/raise if unauthorized. + # + # Examples: + # config.authentication_method = ->(controller) { controller.authenticate_admin! } + # config.authentication_method = ->(controller) { controller.authenticate_user! } + # + # config.authentication_method = nil + +<% if multi_tenant? -%> + # ========================================================================== + # Multi-tenant Mode + # ========================================================================== + + # Enable multi-tenant mode for SaaS deployments with multiple accounts. + config.multi_tenant = true + + # The Account model class name + config.account_class = "Account" + + # The User model class name + config.user_class = "User" + + # Method to call on controllers to get the current account + config.current_account_method = :current_account + + # Method to call on controllers to get the current user + config.current_user_method = :current_user + +<% else -%> + # ========================================================================== + # Local Mode (default) + # ========================================================================== + + # Multi-tenant mode is disabled by default. + # Set to true if you're building a SaaS platform with multiple accounts. + # config.multi_tenant = false + + # Optional: Associate agents with users + # config.user_class = "User" + # config.current_user_method = :current_user + +<% end -%> + # ========================================================================== + # Sandbox Configuration + # ========================================================================== + + # Sandbox service type for agent execution environments. + # Options: :local (Docker/Incus), :cloud_run, :kubernetes + config.sandbox_service = :local + + # Custom sandbox limits (optional) + # config.sandbox_limits = { + # max_runs: 10, + # timeout_seconds: 300, + # max_tokens: 50_000, + # session_duration_minutes: 15 + # } + + # ========================================================================== + # UI Configuration + # ========================================================================== + + # Use Inertia.js with React for the frontend (requires additional setup) + # config.use_inertia = false + + # Custom layout for dashboard views + # config.layout = "application" + + # ========================================================================== + # Storage Configuration + # ========================================================================== + + # Storage service for screenshots and snapshots. + # Must respond to #signed_url_for(key, expires_in:) and #fetch_snapshot(key) + # config.storage_service = MyStorageService.new +end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb new file mode 100644 index 00000000..ae754c27 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class CreateActiveAgentAgentRuns < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_agent_runs do |t| + t.references :agent, null: false, foreign_key: { to_table: :active_agent_agents } + + # Input + t.text :input_prompt + t.jsonb :input_params, default: {} + + # Output + t.text :output + t.jsonb :output_metadata, default: {} + + # Execution details + t.integer :status, default: 0, null: false + t.integer :duration_ms + t.datetime :started_at + t.datetime :completed_at + + # Token usage + t.integer :input_tokens + t.integer :output_tokens + t.integer :total_tokens + + # Error tracking + t.text :error_message + t.text :error_backtrace + + # Trace for debugging + t.string :trace_id + t.jsonb :logs, default: [] + + t.timestamps + end + + add_index :active_agent_agent_runs, :status + add_index :active_agent_agent_runs, :trace_id + add_index :active_agent_agent_runs, :created_at + end +end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb new file mode 100644 index 00000000..71b5e121 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class CreateActiveAgentAgentTemplates < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_agent_templates do |t| + t.string :name, null: false + t.string :slug, null: false + t.text :description + t.string :category + + # Template configuration (same as agents) + t.string :provider, default: "openai" + t.string :model, default: "gpt-4o-mini" + t.text :instructions + t.string :preset_type + t.jsonb :appearance, default: {} + t.jsonb :instruction_sets, default: [] + t.jsonb :tools, default: [] + t.jsonb :mcp_servers, default: {} + t.jsonb :model_config, default: {} + + # Metadata + t.string :icon + t.integer :usage_count, default: 0 + t.boolean :featured, default: false + t.boolean :public, default: true + t.boolean :free_tier, default: true + + t.timestamps + end + + add_index :active_agent_agent_templates, :slug, unique: true + add_index :active_agent_agent_templates, :category + add_index :active_agent_agent_templates, :featured + add_index :active_agent_agent_templates, :usage_count + add_index :active_agent_agent_templates, :free_tier + end +end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb new file mode 100644 index 00000000..6f1dcb63 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class CreateActiveAgentAgentVersions < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_agent_versions do |t| + t.references :agent, null: false, foreign_key: { to_table: :active_agent_agents } + + t.integer :version_number, null: false, default: 1 + t.string :change_summary + + # Snapshot of agent configuration at this version + t.jsonb :configuration_snapshot, null: false, default: {} + + # Who made the change + t.string :created_by + + t.timestamps + end + + add_index :active_agent_agent_versions, [:agent_id, :version_number], unique: true + end +end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb new file mode 100644 index 00000000..83c79055 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class CreateActiveAgentAgents < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_agents do |t| + t.string :name, null: false + t.text :description + t.string :slug, null: false + + # Agent class configuration + t.string :agent_class_name + t.string :provider, default: "openai" + t.string :model, default: "gpt-4o-mini" + + # Instructions and system prompt + t.text :instructions + + # Avatar/appearance configuration + t.string :preset_type + t.jsonb :appearance, default: {} + + # Capabilities + t.jsonb :instruction_sets, default: [] + t.jsonb :tools, default: [] + t.jsonb :mcp_servers, default: [] + + # Model configuration + t.jsonb :model_config, default: {} + + # Response format + t.jsonb :response_format, default: {} + + # Status + t.integer :status, default: 0, null: false + + # Owner associations (optional) + t.references :user, foreign_key: true, null: true +<% if multi_tenant? -%> + t.references :account, foreign_key: true, null: true +<% end -%> + + t.timestamps + end + +<% if multi_tenant? -%> + add_index :active_agent_agents, [:account_id, :slug], unique: true +<% else -%> + add_index :active_agent_agents, [:user_id, :slug], unique: true +<% end -%> + add_index :active_agent_agents, :status + add_index :active_agent_agents, :provider + end +end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb new file mode 100644 index 00000000..f03ea9f1 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class CreateActiveAgentSandboxRuns < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_sandbox_runs do |t| + t.references :sandbox_session, foreign_key: { to_table: :active_agent_sandbox_sessions }, null: true + + t.text :task, null: false + t.integer :status, default: 0, null: false + + # Execution details + t.text :result + t.text :error + t.integer :duration_ms + t.integer :tokens_used + t.datetime :started_at + t.datetime :completed_at + + # Screenshots + t.jsonb :screenshots, default: [] + + t.timestamps + end + + add_index :active_agent_sandbox_runs, :status + add_index :active_agent_sandbox_runs, :created_at + end +end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb new file mode 100644 index 00000000..23888727 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class CreateActiveAgentSandboxSessions < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_sandbox_sessions do |t| + t.string :session_id, null: false + t.references :user, foreign_key: true, null: true +<% if multi_tenant? -%> + t.references :account, foreign_key: true, null: true +<% end -%> + t.references :agent_template, foreign_key: { to_table: :active_agent_agent_templates }, null: true + + # Session metadata + t.string :sandbox_type, default: "playwright_mcp" + t.integer :status, default: 0 + t.string :cloud_run_job_id + t.string :cloud_run_url + + # Execution tracking + t.integer :runs_count, default: 0 + t.integer :max_runs, default: 10 + t.integer :timeout_seconds, default: 300 + t.datetime :expires_at + t.datetime :last_activity_at + + # Resource usage + t.integer :total_tokens, default: 0 + t.integer :total_duration_ms, default: 0 + + # Results + t.jsonb :runs, default: [] + t.text :error_message + + t.timestamps + end + + add_index :active_agent_sandbox_sessions, :session_id, unique: true + add_index :active_agent_sandbox_sessions, :status + add_index :active_agent_sandbox_sessions, :sandbox_type + add_index :active_agent_sandbox_sessions, :expires_at + add_index :active_agent_sandbox_sessions, :cloud_run_job_id + end +end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb new file mode 100644 index 00000000..170cfd3e --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class CreateActiveAgentSessionRecordings < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_session_recordings do |t| + t.references :agent_run, null: true, foreign_key: { to_table: :active_agent_agent_runs } + t.references :sandbox_session, null: true, foreign_key: { to_table: :active_agent_sandbox_sessions } + t.string :name + t.integer :status, default: 0, null: false + t.integer :duration_ms + t.integer :action_count, default: 0 + t.jsonb :metadata, default: {} + t.timestamps + end + + create_table :active_agent_recording_actions do |t| + t.references :session_recording, null: false, foreign_key: { to_table: :active_agent_session_recordings } + t.string :action_type, null: false + t.integer :sequence, null: false + t.integer :timestamp_ms, null: false + t.string :selector + t.text :value + t.string :screenshot_key + t.string :dom_snapshot_key + t.jsonb :metadata, default: {} + t.timestamps + end + + add_index :active_agent_recording_actions, [:session_recording_id, :sequence], unique: true, name: "idx_recording_actions_on_recording_and_sequence" + + create_table :active_agent_recording_snapshots do |t| + t.references :session_recording, null: false, foreign_key: { to_table: :active_agent_session_recordings } + t.references :recording_action, null: true, foreign_key: { to_table: :active_agent_recording_actions } + t.string :storage_key, null: false + t.string :snapshot_type, null: false + t.integer :width + t.integer :height + t.integer :file_size_bytes + t.timestamps + end + + add_index :active_agent_recording_snapshots, :storage_key, unique: true + end +end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb new file mode 100644 index 00000000..a06d96e3 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class CreateActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_telemetry_traces do |t| +<% if multi_tenant? -%> + t.references :account, foreign_key: true, null: true +<% end -%> + + # Trace identification + t.string :trace_id, null: false + t.string :service_name + t.string :environment + + # Timing + t.datetime :timestamp, null: false + + # Span data (JSON array of spans) + t.jsonb :spans, default: [] + + # Resource attributes + t.jsonb :resource_attributes, default: {} + + # SDK info + t.jsonb :sdk_info, default: {} + + # Aggregated metrics (for quick queries) + t.integer :total_duration_ms + t.integer :total_input_tokens, default: 0 + t.integer :total_output_tokens, default: 0 + t.integer :total_thinking_tokens, default: 0 + + # Status + t.string :status, default: "UNSET" + + # Agent info (denormalized for queries) + t.string :agent_class + t.string :agent_action + + # Error info + t.text :error_message + + t.timestamps + end + + add_index :active_agent_telemetry_traces, :trace_id, unique: true + add_index :active_agent_telemetry_traces, :timestamp + add_index :active_agent_telemetry_traces, :service_name + add_index :active_agent_telemetry_traces, :environment + add_index :active_agent_telemetry_traces, :agent_class + add_index :active_agent_telemetry_traces, :status +<% if multi_tenant? -%> + add_index :active_agent_telemetry_traces, [:account_id, :timestamp] +<% end -%> + end +end diff --git a/lib/generators/active_agent/dashboard/install_generator.rb b/lib/generators/active_agent/dashboard/install_generator.rb new file mode 100644 index 00000000..db34f809 --- /dev/null +++ b/lib/generators/active_agent/dashboard/install_generator.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/active_record" + +module ActiveAgent + module Dashboard + # Generator for installing the ActiveAgent Dashboard. + # + # @example Run the generator + # rails generate active_agent:dashboard:install + # + # This will: + # - Create the telemetry_traces table migration + # - Add mount directive to routes + # - Create initializer for dashboard configuration + # + class InstallGenerator < Rails::Generators::Base + include ActiveRecord::Generators::Migration + + source_root File.expand_path("templates", __dir__) + + desc "Installs the ActiveAgent Dashboard with telemetry storage" + + def copy_migrations + migration_template( + "create_active_agent_telemetry_traces.rb.erb", + "db/migrate/create_active_agent_telemetry_traces.rb" + ) + end + + def add_route + route 'mount ActiveAgent::Dashboard::Engine => "/active_agent"' + end + + def create_initializer + template( + "active_agent_dashboard.rb.erb", + "config/initializers/active_agent_dashboard.rb" + ) + end + + def show_readme + say "\n" + say "ActiveAgent Dashboard installed successfully!", :green + say "\n" + say "Next steps:" + say " 1. Run migrations: rails db:migrate" + say " 2. Configure telemetry in config/active_agent.yml:" + say " telemetry:" + say " enabled: true" + say " local_storage: true" + say " 3. Visit /active_agent to view the dashboard" + say "\n" + end + + private + + def migration_version + "[#{ActiveRecord::Migration.current_version}]" + end + end + end +end diff --git a/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb b/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb new file mode 100644 index 00000000..02a41897 --- /dev/null +++ b/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# ActiveAgent Dashboard Configuration +# +# This file configures the ActiveAgent telemetry dashboard. +# The dashboard is mounted at /active_agent by default. + +ActiveAgent::Dashboard.configure do |config| + # Authentication method - provide a lambda that receives the controller + # and performs authentication. Return false or raise to deny access. + # + # Examples: + # + # Basic auth: + # config.authentication_method = ->(controller) { + # controller.authenticate_or_request_with_http_basic do |username, password| + # username == "admin" && password == Rails.application.credentials.dashboard_password + # end + # } + # + # Devise: + # config.authentication_method = ->(controller) { + # controller.authenticate_admin! + # } + # + # No authentication (development only!): + # config.authentication_method = nil + # + config.authentication_method = nil +end diff --git a/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb b/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb new file mode 100644 index 00000000..803e4df7 --- /dev/null +++ b/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class CreateActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_version %> + def change + create_table :active_agent_telemetry_traces do |t| + t.string :trace_id, null: false, index: true + t.string :service_name + t.string :environment + t.datetime :timestamp, index: true + t.jsonb :spans, default: [] + t.jsonb :resource_attributes, default: {} + t.jsonb :sdk_info, default: {} + t.decimal :total_duration_ms, precision: 12, scale: 3 + t.integer :total_input_tokens, default: 0 + t.integer :total_output_tokens, default: 0 + t.integer :total_thinking_tokens, default: 0 + t.string :status + t.string :agent_class, index: true + t.string :agent_action + t.text :error_message + + t.timestamps + end + + add_index :active_agent_telemetry_traces, :status + add_index :active_agent_telemetry_traces, [:agent_class, :agent_action] + add_index :active_agent_telemetry_traces, [:service_name, :environment] + add_index :active_agent_telemetry_traces, :created_at + end +end diff --git a/yarn.lock b/yarn.lock index b7b33199..0e41ccc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,628 +2,644 @@ # yarn lockfile v1 +"@algolia/abtesting@1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.2.0.tgz" + integrity sha512-Z6Liq7US5CpdHExZLfPMBPxQHHUObV587kGvCLniLr1UTx0fGFIeGNWd005WIqQXqEda9GyAi7T2e7DUupVv0g== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + "@algolia/autocomplete-core@1.17.7": - "integrity" "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==" - "resolved" "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz" - "version" "1.17.7" + version "1.17.7" + resolved "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz" + integrity sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q== dependencies: "@algolia/autocomplete-plugin-algolia-insights" "1.17.7" "@algolia/autocomplete-shared" "1.17.7" "@algolia/autocomplete-plugin-algolia-insights@1.17.7": - "integrity" "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==" - "resolved" "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz" - "version" "1.17.7" + version "1.17.7" + resolved "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz" + integrity sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A== dependencies: "@algolia/autocomplete-shared" "1.17.7" "@algolia/autocomplete-preset-algolia@1.17.7": - "integrity" "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==" - "resolved" "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz" - "version" "1.17.7" + version "1.17.7" + resolved "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz" + integrity sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA== dependencies: "@algolia/autocomplete-shared" "1.17.7" "@algolia/autocomplete-shared@1.17.7": - "integrity" "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==" - "resolved" "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz" - "version" "1.17.7" - -"@algolia/client-abtesting@5.27.0": - "integrity" "sha512-SITU5umoknxETtw67TxJu9njyMkWiH8pM+Bvw4dzfuIrIAT6Y1rmwV4y0A0didWoT+6xVuammIykbtBMolBcmg==" - "resolved" "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/client-analytics@5.27.0": - "integrity" "sha512-go1b9qIZK5vYEQ7jD2bsfhhhVsoh9cFxQ5xF8TzTsg2WOCZR3O92oXCkq15SOK0ngJfqDU6a/k0oZ4KuEnih1Q==" - "resolved" "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/client-common@5.27.0": - "integrity" "sha512-tnFOzdNuMzsz93kOClj3fKfuYoF3oYaEB5bggULSj075GJ7HUNedBEm7a6ScrjtnOaOtipbnT7veUpHA4o4wEQ==" - "resolved" "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.27.0.tgz" - "version" "5.27.0" - -"@algolia/client-insights@5.27.0": - "integrity" "sha512-y1qgw39qZijjQBXrqZTiwK1cWgWGRiLpJNWBv9w36nVMKfl9kInrfsYmdBAfmlhVgF/+Woe0y1jQ7pa4HyShAw==" - "resolved" "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/client-personalization@5.27.0": - "integrity" "sha512-XluG9qPZKEbiLoIfXTKbABsWDNOMPx0t6T2ImJTTeuX+U/zBdmfcqqgcgkqXp+vbXof/XX/4of9Eqo1JaqEmKw==" - "resolved" "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/client-query-suggestions@5.27.0": - "integrity" "sha512-V8/To+SsAl2sdw2AAjeLJuCW1L+xpz+LAGerJK7HKqHzE5yQhWmIWZTzqYQcojkii4iBMYn0y3+uReWqT8XVSQ==" - "resolved" "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/client-search@>= 4.9.1 < 6", "@algolia/client-search@5.27.0": - "integrity" "sha512-EJJ7WmvmUXZdchueKFCK8UZFyLqy4Hz64snNp0cTc7c0MKaSeDGYEDxVsIJKp15r7ORaoGxSyS4y6BGZMXYuCg==" - "resolved" "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/ingestion@1.27.0": - "integrity" "sha512-xNCyWeqpmEo4EdmpG57Fs1fJIQcPwt5NnJ6MBdXnUdMVXF4f5PHgza+HQWQQcYpCsune96jfmR0v7us6gRIlCw==" - "resolved" "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.27.0.tgz" - "version" "1.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/monitoring@1.27.0": - "integrity" "sha512-P0NDiEFyt9UYQLBI0IQocIT7xHpjMpoFN3UDeerbztlkH9HdqT0GGh1SHYmNWpbMWIGWhSJTtz6kSIWvFu4+pw==" - "resolved" "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.27.0.tgz" - "version" "1.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/recommend@5.27.0": - "integrity" "sha512-cqfTMF1d1cc7hg0vITNAFxJZas7MJ4Obc36WwkKpY23NOtGb+4tH9X7UKlQa2PmTgbXIANoJ/DAQTeiVlD2I4Q==" - "resolved" "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"@algolia/requester-browser-xhr@5.27.0": - "integrity" "sha512-ErenYTcXl16wYXtf0pxLl9KLVxIztuehqXHfW9nNsD8mz9OX42HbXuPzT7y6JcPiWJpc/UU/LY5wBTB65vsEUg==" - "resolved" "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - -"@algolia/requester-fetch@5.27.0": - "integrity" "sha512-CNOvmXsVi+IvT7z1d+6X7FveVkgEQwTNgipjQCHTIbF9KSMfZR7tUsJC+NpELrm10ALdOMauah84ybs9rw1cKQ==" - "resolved" "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - -"@algolia/requester-node-http@5.27.0": - "integrity" "sha512-Nx9EdLYZDsaYFTthqmc0XcVvsx6jqeEX8fNiYOB5i2HboQwl8pJPj1jFhGqoGd0KG7KFR+sdPO5/e0EDDAru2Q==" - "resolved" "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-common" "5.27.0" - -"@antfu/install-pkg@^1.0.0": - "integrity" "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==" - "resolved" "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz" - "version" "1.1.0" - dependencies: - "package-manager-detector" "^1.3.0" - "tinyexec" "^1.0.1" - -"@antfu/utils@^8.1.0": - "integrity" "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==" - "resolved" "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz" - "version" "8.1.1" + version "1.17.7" + resolved "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz" + integrity sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg== + +"@algolia/client-abtesting@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.36.0.tgz" + integrity sha512-uGr57O1UqDDeZHYXr1VnUomtdgQMxb6fS8yC/LXCMOn5ucN4k6FlcCRqXQnUyiiFZNG/rVK3zpRiyomq4JWXdQ== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/client-analytics@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.36.0.tgz" + integrity sha512-/zrf0NMxcvBBQ4r9lIqM7rMt7oI7gY7bZ+bNcgpZAQMvzXbKJVla3MqKGuPC/bfOthKvAcAr0mCZ8/7GwBmkVw== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/client-common@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.36.0.tgz" + integrity sha512-fDsg9w6xXWQyNkm/VfiWF2D9wnpTPv0fRVei7lWtz7cXJewhOmP1kKE2GaDTI4QDxVxgDkoPJ1+3UVMIzTcjjQ== + +"@algolia/client-insights@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.36.0.tgz" + integrity sha512-x6ZICyIN3BZjja47lqlMLG+AZwfx9wrYWttd6Daxp+wX/fFGxha6gdqxeoi5J44BmFqK8CUU4u8vpwHqGOCl4g== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/client-personalization@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.36.0.tgz" + integrity sha512-gnH9VHrC+/9OuaumbgxNXzzEq1AY2j3tm00ymNXNz35T7RQ2AK/x4T5b2UnjOUJejuXaSJ88gFyPk3nM5OhJZQ== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/client-query-suggestions@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.36.0.tgz" + integrity sha512-GkWIS+cAMoxsNPHEp3j7iywO9JJMVHVCWHzPPHFXIe0iNIOfsnZy5MqC1T9sifjqoU9b0GGbzzdxB3TEdwfiFA== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/client-search@>= 4.9.1 < 6", "@algolia/client-search@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.36.0.tgz" + integrity sha512-MLx32nSeDSNxfx28IfvwfHEfeo3AYe9JgEj0rLeYtJGmt0W30K6tCNokxhWGUUKrggQTH6H1lnohWsoj2OC2bw== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/ingestion@1.36.0": + version "1.36.0" + resolved "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.36.0.tgz" + integrity sha512-6zmlPLCsyzShOsfs1G1uqxwLTojte3NLyukwyUmJFfa46DSq3wkIOE9hFtqAoV951dXp4sZd2KCFYJmgRjcYbA== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/monitoring@1.36.0": + version "1.36.0" + resolved "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.36.0.tgz" + integrity sha512-SjJeDqlzAKJiWhquqfDWLEu5X/PIM+5KvUH65c4LBvt8T+USOVJbijtzA9UHZ1eUIfFSDBmbzEH0YvlS6Di2mg== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/recommend@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.36.0.tgz" + integrity sha512-FalJm3h9fwoZZpkkMpA0r4Grcvjk32FzmC4CXvlpyF/gBvu6pXE01yygjJBU20zGVLGsXU+Ad8nYPf+oGD7Zkg== + dependencies: + "@algolia/client-common" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +"@algolia/requester-browser-xhr@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.36.0.tgz" + integrity sha512-weE9SImWIDmQrfGLb1pSPEfP3mioKQ84GaQRpUmjFxlxG/4nW2bSsmkV+kNp1s+iomL2gnxFknSmcQuuAy+kPA== + dependencies: + "@algolia/client-common" "5.36.0" + +"@algolia/requester-fetch@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.36.0.tgz" + integrity sha512-zGPI2sgzvOwCHTVMmDvc301iirOKCtJ+Egh+HQB/+DG0zTGUT1DpdwQVT25A7Yin/twnO8CkFpI/S+74FVYNjg== + dependencies: + "@algolia/client-common" "5.36.0" + +"@algolia/requester-node-http@5.36.0": + version "5.36.0" + resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.36.0.tgz" + integrity sha512-dNbBGE/O6VG/6vFhv3CFm5za4rubAVrhQf/ef0YWiDqPMmalPxGEzIijw4xV1mU1JmX2ffyp/x8Kdtz24sDkOQ== + dependencies: + "@algolia/client-common" "5.36.0" + +"@antfu/install-pkg@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz" + integrity sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ== + dependencies: + package-manager-detector "^1.3.0" + tinyexec "^1.0.1" + +"@antfu/utils@^9.2.0": + version "9.2.0" + resolved "https://registry.npmjs.org/@antfu/utils/-/utils-9.2.0.tgz" + integrity sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw== "@babel/helper-string-parser@^7.27.1": - "integrity" "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" - "resolved" "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" - "version" "7.27.1" + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== "@babel/helper-validator-identifier@^7.27.1": - "integrity" "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" - "resolved" "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz" - "version" "7.27.1" + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== -"@babel/parser@^7.27.2": - "integrity" "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==" - "resolved" "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz" - "version" "7.27.5" +"@babel/parser@^7.28.3": + version "7.28.3" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz" + integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== dependencies: - "@babel/types" "^7.27.3" + "@babel/types" "^7.28.2" -"@babel/types@^7.27.3": - "integrity" "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==" - "resolved" "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz" - "version" "7.27.6" +"@babel/types@^7.28.2": + version "7.28.2" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz" + integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" "@docsearch/css@3.8.2": - "integrity" "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==" - "resolved" "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz" - "version" "3.8.2" + version "3.8.2" + resolved "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz" + integrity sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ== "@docsearch/js@3.8.2": - "integrity" "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==" - "resolved" "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz" - "version" "3.8.2" + version "3.8.2" + resolved "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz" + integrity sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ== dependencies: "@docsearch/react" "3.8.2" - "preact" "^10.0.0" + preact "^10.0.0" "@docsearch/react@3.8.2": - "integrity" "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==" - "resolved" "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz" - "version" "3.8.2" + version "3.8.2" + resolved "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz" + integrity sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg== dependencies: "@algolia/autocomplete-core" "1.17.7" "@algolia/autocomplete-preset-algolia" "1.17.7" "@docsearch/css" "3.8.2" - "algoliasearch" "^5.14.2" + algoliasearch "^5.14.2" "@esbuild/darwin-arm64@0.21.5": - "integrity" "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==" - "resolved" "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz" - "version" "0.21.5" + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== "@iconify-json/logos@^1.2.4": - "integrity" "sha512-XC4If5D/hbaZvUkTV8iaZuGlQCyG6CNOlaAaJaGa13V5QMYwYjgtKk3vPP8wz3wtTVNVEVk3LRx1fOJz+YnSMw==" - "resolved" "https://registry.npmjs.org/@iconify-json/logos/-/logos-1.2.4.tgz" - "version" "1.2.4" + version "1.2.9" + resolved "https://registry.npmjs.org/@iconify-json/logos/-/logos-1.2.9.tgz" + integrity sha512-G6VCdFnwZcrT6Eveq3m43oJfLw/CX8plwFcE+2jgv3fiGB64pTmnU7Yd1MNZ/eA+/Re2iEDhuCfSNOWTHwwK8w== dependencies: "@iconify/types" "*" "@iconify-json/simple-icons@^1.2.21": - "integrity" "sha512-jZwTBznpYVDYKWyAuRpepPpCiHScVrX6f8WRX8ReX6pdii99LYVHwJywKcH2excWQrWmBomC9nkxGlEKzXZ/wQ==" - "resolved" "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.37.tgz" - "version" "1.2.37" + version "1.2.49" + resolved "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.49.tgz" + integrity sha512-nRLwrHzz+cTAQYBNQrcr4eWOmQIcHObTj/QSi7nj0SFwVh5MvBsgx8OhoDC/R8iGklNmMpmoE/NKU0cPXMlOZw== dependencies: "@iconify/types" "*" -"@iconify-json/vscode-icons@^1.2.18": - "integrity" "sha512-xuWqr/SrckUoFi6kpSH/NrNGK+CuZ8LNnBY8qkRdkQvHmhirXvwsLfTKHoFndTsOlxfsHahlOLVCCb523kdqMA==" - "resolved" "https://registry.npmjs.org/@iconify-json/vscode-icons/-/vscode-icons-1.2.20.tgz" - "version" "1.2.20" +"@iconify-json/vscode-icons@^1.2.29": + version "1.2.30" + resolved "https://registry.npmjs.org/@iconify-json/vscode-icons/-/vscode-icons-1.2.30.tgz" + integrity sha512-dlTOc8w4a8/QNumZzMve+APJa6xQVXPZwo8qBk/MaYfY42NPrQT83QXkbTWKDkuEu/xgHPXvKZZBL7Yy12vYQw== dependencies: "@iconify/types" "*" "@iconify/types@*", "@iconify/types@^2.0.0": - "integrity" "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" - "resolved" "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz" - "version" "2.0.0" + version "2.0.0" + resolved "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz" + integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== -"@iconify/utils@^2.3.0": - "integrity" "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==" - "resolved" "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz" - "version" "2.3.0" +"@iconify/utils@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.1.tgz" + integrity sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw== dependencies: - "@antfu/install-pkg" "^1.0.0" - "@antfu/utils" "^8.1.0" + "@antfu/install-pkg" "^1.1.0" + "@antfu/utils" "^9.2.0" "@iconify/types" "^2.0.0" - "debug" "^4.4.0" - "globals" "^15.14.0" - "kolorist" "^1.8.0" - "local-pkg" "^1.0.0" - "mlly" "^1.7.4" - -"@jridgewell/sourcemap-codec@^1.5.0": - "integrity" "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - "resolved" "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" - "version" "1.5.0" - -"@rollup/rollup-darwin-arm64@4.42.0": - "integrity" "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==" - "resolved" "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz" - "version" "4.42.0" + debug "^4.4.1" + globals "^15.15.0" + kolorist "^1.8.0" + local-pkg "^1.1.1" + mlly "^1.7.4" + +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@rollup/rollup-darwin-arm64@4.49.0": + version "4.49.0" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz" + integrity sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw== "@shikijs/core@^2.1.0", "@shikijs/core@2.5.0": - "integrity" "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==" - "resolved" "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz" - "version" "2.5.0" + version "2.5.0" + resolved "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz" + integrity sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg== dependencies: "@shikijs/engine-javascript" "2.5.0" "@shikijs/engine-oniguruma" "2.5.0" "@shikijs/types" "2.5.0" "@shikijs/vscode-textmate" "^10.0.2" "@types/hast" "^3.0.4" - "hast-util-to-html" "^9.0.4" + hast-util-to-html "^9.0.4" "@shikijs/engine-javascript@2.5.0": - "integrity" "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==" - "resolved" "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz" - "version" "2.5.0" + version "2.5.0" + resolved "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz" + integrity sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w== dependencies: "@shikijs/types" "2.5.0" "@shikijs/vscode-textmate" "^10.0.2" - "oniguruma-to-es" "^3.1.0" + oniguruma-to-es "^3.1.0" "@shikijs/engine-oniguruma@2.5.0": - "integrity" "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==" - "resolved" "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz" - "version" "2.5.0" + version "2.5.0" + resolved "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz" + integrity sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw== dependencies: "@shikijs/types" "2.5.0" "@shikijs/vscode-textmate" "^10.0.2" "@shikijs/langs@2.5.0": - "integrity" "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==" - "resolved" "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz" - "version" "2.5.0" + version "2.5.0" + resolved "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz" + integrity sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w== dependencies: "@shikijs/types" "2.5.0" "@shikijs/themes@2.5.0": - "integrity" "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==" - "resolved" "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz" - "version" "2.5.0" + version "2.5.0" + resolved "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz" + integrity sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw== dependencies: "@shikijs/types" "2.5.0" "@shikijs/transformers@^2.1.0": - "integrity" "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==" - "resolved" "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz" - "version" "2.5.0" + version "2.5.0" + resolved "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz" + integrity sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg== dependencies: "@shikijs/core" "2.5.0" "@shikijs/types" "2.5.0" "@shikijs/types@^2.1.0", "@shikijs/types@2.5.0": - "integrity" "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==" - "resolved" "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz" - "version" "2.5.0" + version "2.5.0" + resolved "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz" + integrity sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw== dependencies: "@shikijs/vscode-textmate" "^10.0.2" "@types/hast" "^3.0.4" "@shikijs/vscode-textmate@^10.0.2": - "integrity" "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" - "resolved" "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz" - "version" "10.0.2" + version "10.0.2" + resolved "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz" + integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== -"@types/estree@1.0.7": - "integrity" "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" - "resolved" "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz" - "version" "1.0.7" +"@types/estree@1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== "@types/hast@^3.0.0", "@types/hast@^3.0.4": - "integrity" "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==" - "resolved" "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz" - "version" "3.0.4" + version "3.0.4" + resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== dependencies: "@types/unist" "*" "@types/linkify-it@^5": - "integrity" "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==" - "resolved" "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz" - "version" "5.0.0" + version "5.0.0" + resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== "@types/markdown-it@^14.1.2": - "integrity" "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==" - "resolved" "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz" - "version" "14.1.2" + version "14.1.2" + resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== dependencies: "@types/linkify-it" "^5" "@types/mdurl" "^2" "@types/mdast@^4.0.0": - "integrity" "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==" - "resolved" "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz" - "version" "4.0.4" + version "4.0.4" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== dependencies: "@types/unist" "*" "@types/mdurl@^2": - "integrity" "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==" - "resolved" "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz" - "version" "2.0.0" + version "2.0.0" + resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== "@types/unist@*", "@types/unist@^3.0.0": - "integrity" "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" - "resolved" "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz" - "version" "3.0.3" + version "3.0.3" + resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== "@types/web-bluetooth@^0.0.21": - "integrity" "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==" - "resolved" "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz" - "version" "0.0.21" + version "0.0.21" + resolved "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz" + integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA== "@ungap/structured-clone@^1.0.0": - "integrity" "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" - "resolved" "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" - "version" "1.3.0" + version "1.3.0" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== "@vitejs/plugin-vue@^5.2.1": - "integrity" "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==" - "resolved" "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz" - "version" "5.2.4" - -"@vue/compiler-core@3.5.16": - "integrity" "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==" - "resolved" "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@babel/parser" "^7.27.2" - "@vue/shared" "3.5.16" - "entities" "^4.5.0" - "estree-walker" "^2.0.2" - "source-map-js" "^1.2.1" - -"@vue/compiler-dom@3.5.16": - "integrity" "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==" - "resolved" "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@vue/compiler-core" "3.5.16" - "@vue/shared" "3.5.16" - -"@vue/compiler-sfc@3.5.16": - "integrity" "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==" - "resolved" "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@babel/parser" "^7.27.2" - "@vue/compiler-core" "3.5.16" - "@vue/compiler-dom" "3.5.16" - "@vue/compiler-ssr" "3.5.16" - "@vue/shared" "3.5.16" - "estree-walker" "^2.0.2" - "magic-string" "^0.30.17" - "postcss" "^8.5.3" - "source-map-js" "^1.2.1" - -"@vue/compiler-ssr@3.5.16": - "integrity" "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==" - "resolved" "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@vue/compiler-dom" "3.5.16" - "@vue/shared" "3.5.16" + version "5.2.4" + resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz" + integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA== + +"@vue/compiler-core@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.20.tgz" + integrity sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg== + dependencies: + "@babel/parser" "^7.28.3" + "@vue/shared" "3.5.20" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.1" + +"@vue/compiler-dom@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz" + integrity sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ== + dependencies: + "@vue/compiler-core" "3.5.20" + "@vue/shared" "3.5.20" + +"@vue/compiler-sfc@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz" + integrity sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw== + dependencies: + "@babel/parser" "^7.28.3" + "@vue/compiler-core" "3.5.20" + "@vue/compiler-dom" "3.5.20" + "@vue/compiler-ssr" "3.5.20" + "@vue/shared" "3.5.20" + estree-walker "^2.0.2" + magic-string "^0.30.17" + postcss "^8.5.6" + source-map-js "^1.2.1" + +"@vue/compiler-ssr@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz" + integrity sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA== + dependencies: + "@vue/compiler-dom" "3.5.20" + "@vue/shared" "3.5.20" "@vue/devtools-api@^7.7.0": - "integrity" "sha512-b2Xx0KvXZObePpXPYHvBRRJLDQn5nhKjXh7vUhMEtWxz1AYNFOVIsh5+HLP8xDGL7sy+Q7hXeUxPHB/KgbtsPw==" - "resolved" "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.6.tgz" - "version" "7.7.6" - dependencies: - "@vue/devtools-kit" "^7.7.6" - -"@vue/devtools-kit@^7.7.6": - "integrity" "sha512-geu7ds7tem2Y7Wz+WgbnbZ6T5eadOvozHZ23Atk/8tksHMFOFylKi1xgGlQlVn0wlkEf4hu+vd5ctj1G4kFtwA==" - "resolved" "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.6.tgz" - "version" "7.7.6" - dependencies: - "@vue/devtools-shared" "^7.7.6" - "birpc" "^2.3.0" - "hookable" "^5.5.3" - "mitt" "^3.0.1" - "perfect-debounce" "^1.0.0" - "speakingurl" "^14.0.1" - "superjson" "^2.2.2" - -"@vue/devtools-shared@^7.7.6": - "integrity" "sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==" - "resolved" "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.6.tgz" - "version" "7.7.6" - dependencies: - "rfdc" "^1.4.1" - -"@vue/reactivity@3.5.16": - "integrity" "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==" - "resolved" "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@vue/shared" "3.5.16" - -"@vue/runtime-core@3.5.16": - "integrity" "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==" - "resolved" "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@vue/reactivity" "3.5.16" - "@vue/shared" "3.5.16" - -"@vue/runtime-dom@3.5.16": - "integrity" "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==" - "resolved" "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@vue/reactivity" "3.5.16" - "@vue/runtime-core" "3.5.16" - "@vue/shared" "3.5.16" - "csstype" "^3.1.3" - -"@vue/server-renderer@3.5.16": - "integrity" "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==" - "resolved" "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@vue/compiler-ssr" "3.5.16" - "@vue/shared" "3.5.16" - -"@vue/shared@^3.5.13", "@vue/shared@3.5.16": - "integrity" "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==" - "resolved" "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz" - "version" "3.5.16" + version "7.7.7" + resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz" + integrity sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg== + dependencies: + "@vue/devtools-kit" "^7.7.7" + +"@vue/devtools-kit@^7.7.7": + version "7.7.7" + resolved "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz" + integrity sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA== + dependencies: + "@vue/devtools-shared" "^7.7.7" + birpc "^2.3.0" + hookable "^5.5.3" + mitt "^3.0.1" + perfect-debounce "^1.0.0" + speakingurl "^14.0.1" + superjson "^2.2.2" + +"@vue/devtools-shared@^7.7.7": + version "7.7.7" + resolved "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz" + integrity sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw== + dependencies: + rfdc "^1.4.1" + +"@vue/reactivity@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.20.tgz" + integrity sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ== + dependencies: + "@vue/shared" "3.5.20" + +"@vue/runtime-core@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.20.tgz" + integrity sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw== + dependencies: + "@vue/reactivity" "3.5.20" + "@vue/shared" "3.5.20" + +"@vue/runtime-dom@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.20.tgz" + integrity sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw== + dependencies: + "@vue/reactivity" "3.5.20" + "@vue/runtime-core" "3.5.20" + "@vue/shared" "3.5.20" + csstype "^3.1.3" + +"@vue/server-renderer@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.20.tgz" + integrity sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg== + dependencies: + "@vue/compiler-ssr" "3.5.20" + "@vue/shared" "3.5.20" + +"@vue/shared@^3.5.13", "@vue/shared@3.5.20": + version "3.5.20" + resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.5.20.tgz" + integrity sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA== "@vueuse/core@^12.4.0", "@vueuse/core@12.8.2": - "integrity" "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==" - "resolved" "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz" - "version" "12.8.2" + version "12.8.2" + resolved "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz" + integrity sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ== dependencies: "@types/web-bluetooth" "^0.0.21" "@vueuse/metadata" "12.8.2" "@vueuse/shared" "12.8.2" - "vue" "^3.5.13" + vue "^3.5.13" "@vueuse/integrations@^12.4.0": - "integrity" "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==" - "resolved" "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz" - "version" "12.8.2" + version "12.8.2" + resolved "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz" + integrity sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g== dependencies: "@vueuse/core" "12.8.2" "@vueuse/shared" "12.8.2" - "vue" "^3.5.13" + vue "^3.5.13" "@vueuse/metadata@12.8.2": - "integrity" "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==" - "resolved" "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz" - "version" "12.8.2" + version "12.8.2" + resolved "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz" + integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A== "@vueuse/shared@12.8.2": - "integrity" "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==" - "resolved" "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz" - "version" "12.8.2" - dependencies: - "vue" "^3.5.13" - -"acorn@^8.14.0": - "integrity" "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" - "resolved" "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz" - "version" "8.14.1" - -"algoliasearch@^5.14.2", "algoliasearch@>= 4.9.1 < 6": - "integrity" "sha512-2PvAgvxxJzA3+dB+ERfS2JPdvUsxNf89Cc2GF5iCcFupTULOwmbfinvqrC4Qj9nHJJDNf494NqEN/1f9177ZTQ==" - "resolved" "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.27.0.tgz" - "version" "5.27.0" - dependencies: - "@algolia/client-abtesting" "5.27.0" - "@algolia/client-analytics" "5.27.0" - "@algolia/client-common" "5.27.0" - "@algolia/client-insights" "5.27.0" - "@algolia/client-personalization" "5.27.0" - "@algolia/client-query-suggestions" "5.27.0" - "@algolia/client-search" "5.27.0" - "@algolia/ingestion" "1.27.0" - "@algolia/monitoring" "1.27.0" - "@algolia/recommend" "5.27.0" - "@algolia/requester-browser-xhr" "5.27.0" - "@algolia/requester-fetch" "5.27.0" - "@algolia/requester-node-http" "5.27.0" - -"birpc@^2.3.0": - "integrity" "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==" - "resolved" "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz" - "version" "2.3.0" - -"ccount@^2.0.0": - "integrity" "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" - "resolved" "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" - "version" "2.0.1" - -"character-entities-html4@^2.0.0": - "integrity" "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==" - "resolved" "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz" - "version" "2.1.0" - -"character-entities-legacy@^3.0.0": - "integrity" "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" - "resolved" "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz" - "version" "3.0.0" - -"comma-separated-tokens@^2.0.0": - "integrity" "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" - "resolved" "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" - "version" "2.0.3" - -"confbox@^0.1.8": - "integrity" "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==" - "resolved" "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz" - "version" "0.1.8" - -"confbox@^0.2.1": - "integrity" "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==" - "resolved" "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz" - "version" "0.2.2" - -"copy-anything@^3.0.2": - "integrity" "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==" - "resolved" "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz" - "version" "3.0.5" - dependencies: - "is-what" "^4.1.8" - -"csstype@^3.1.3": - "integrity" "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - "resolved" "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" - "version" "3.1.3" - -"debug@^4.4.0": - "integrity" "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==" - "resolved" "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" - "version" "4.4.0" - dependencies: - "ms" "^2.1.3" - -"dequal@^2.0.0": - "integrity" "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" - "resolved" "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" - "version" "2.0.3" - -"devlop@^1.0.0": - "integrity" "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==" - "resolved" "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz" - "version" "1.1.0" - dependencies: - "dequal" "^2.0.0" - -"emoji-regex-xs@^1.0.0": - "integrity" "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==" - "resolved" "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz" - "version" "1.0.0" - -"entities@^4.5.0": - "integrity" "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" - "resolved" "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" - "version" "4.5.0" - -"esbuild@^0.21.3": - "integrity" "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==" - "resolved" "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz" - "version" "0.21.5" + version "12.8.2" + resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz" + integrity sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w== + dependencies: + vue "^3.5.13" + +acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +algoliasearch@^5.14.2, "algoliasearch@>= 4.9.1 < 6": + version "5.36.0" + resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.36.0.tgz" + integrity sha512-FpwQ+p4x4RIsWnPj2z9idOC70T90ga7Oeh8BURSFKpqp5lITRsgkIj/bwYj2bY5xbyD7uBuP9AZRnM5EV20WOw== + dependencies: + "@algolia/abtesting" "1.2.0" + "@algolia/client-abtesting" "5.36.0" + "@algolia/client-analytics" "5.36.0" + "@algolia/client-common" "5.36.0" + "@algolia/client-insights" "5.36.0" + "@algolia/client-personalization" "5.36.0" + "@algolia/client-query-suggestions" "5.36.0" + "@algolia/client-search" "5.36.0" + "@algolia/ingestion" "1.36.0" + "@algolia/monitoring" "1.36.0" + "@algolia/recommend" "5.36.0" + "@algolia/requester-browser-xhr" "5.36.0" + "@algolia/requester-fetch" "5.36.0" + "@algolia/requester-node-http" "5.36.0" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +birpc@^2.3.0: + version "2.5.0" + resolved "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz" + integrity sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +confbox@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz" + integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ== + +copy-anything@^3.0.2: + version "3.0.5" + resolved "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz" + integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w== + dependencies: + is-what "^4.1.8" + +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +debug@^4.4.1: + version "4.4.1" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +devlop@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + +emoji-regex-xs@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz" + integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg== + +entities@^4.4.0, entities@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== optionalDependencies: "@esbuild/aix-ppc64" "0.21.5" "@esbuild/android-arm" "0.21.5" @@ -649,308 +665,337 @@ "@esbuild/win32-ia32" "0.21.5" "@esbuild/win32-x64" "0.21.5" -"estree-walker@^2.0.2": - "integrity" "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - "resolved" "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" - "version" "2.0.2" +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== -"exsolve@^1.0.1": - "integrity" "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==" - "resolved" "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz" - "version" "1.0.5" +exsolve@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz" + integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw== -"focus-trap@^7", "focus-trap@^7.6.4": - "integrity" "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==" - "resolved" "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz" - "version" "7.6.5" +focus-trap@^7, focus-trap@^7.6.4: + version "7.6.5" + resolved "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz" + integrity sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg== dependencies: - "tabbable" "^6.2.0" + tabbable "^6.2.0" -"fsevents@~2.3.2", "fsevents@~2.3.3": - "integrity" "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" - "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - "version" "2.3.3" +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -"globals@^15.14.0": - "integrity" "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==" - "resolved" "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz" - "version" "15.15.0" +globals@^15.15.0: + version "15.15.0" + resolved "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz" + integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== -"hast-util-to-html@^9.0.4": - "integrity" "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==" - "resolved" "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz" - "version" "9.0.5" +hast-util-to-html@^9.0.4: + version "9.0.5" + resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz" + integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw== dependencies: "@types/hast" "^3.0.0" "@types/unist" "^3.0.0" - "ccount" "^2.0.0" - "comma-separated-tokens" "^2.0.0" - "hast-util-whitespace" "^3.0.0" - "html-void-elements" "^3.0.0" - "mdast-util-to-hast" "^13.0.0" - "property-information" "^7.0.0" - "space-separated-tokens" "^2.0.0" - "stringify-entities" "^4.0.0" - "zwitch" "^2.0.4" - -"hast-util-whitespace@^3.0.0": - "integrity" "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==" - "resolved" "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz" - "version" "3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== dependencies: "@types/hast" "^3.0.0" -"hookable@^5.5.3": - "integrity" "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" - "resolved" "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz" - "version" "5.5.3" - -"html-void-elements@^3.0.0": - "integrity" "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==" - "resolved" "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz" - "version" "3.0.0" - -"is-what@^4.1.8": - "integrity" "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==" - "resolved" "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz" - "version" "4.1.16" - -"kolorist@^1.8.0": - "integrity" "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" - "resolved" "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz" - "version" "1.8.0" - -"local-pkg@^1.0.0": - "integrity" "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==" - "resolved" "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz" - "version" "1.1.1" - dependencies: - "mlly" "^1.7.4" - "pkg-types" "^2.0.1" - "quansync" "^0.2.8" - -"magic-string@^0.30.17": - "integrity" "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==" - "resolved" "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz" - "version" "0.30.17" - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.0" - -"mark.js@8.11.1": - "integrity" "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==" - "resolved" "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz" - "version" "8.11.1" - -"mdast-util-to-hast@^13.0.0": - "integrity" "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==" - "resolved" "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz" - "version" "13.2.0" +hookable@^5.5.3: + version "5.5.3" + resolved "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz" + integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +is-what@^4.1.8: + version "4.1.16" + resolved "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz" + integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== + +kolorist@^1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz" + integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== + +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + +local-pkg@^1.1.1: + version "1.1.2" + resolved "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz" + integrity sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A== + dependencies: + mlly "^1.7.4" + pkg-types "^2.3.0" + quansync "^0.2.11" + +magic-string@^0.30.17: + version "0.30.18" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz" + integrity sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +mark.js@8.11.1: + version "8.11.1" + resolved "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz" + integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ== + +markdown-it@>=14: + version "14.1.0" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + +mdast-util-to-hast@^13.0.0: + version "13.2.0" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz" + integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== dependencies: "@types/hast" "^3.0.0" "@types/mdast" "^4.0.0" "@ungap/structured-clone" "^1.0.0" - "devlop" "^1.0.0" - "micromark-util-sanitize-uri" "^2.0.0" - "trim-lines" "^3.0.0" - "unist-util-position" "^5.0.0" - "unist-util-visit" "^5.0.0" - "vfile" "^6.0.0" - -"micromark-util-character@^2.0.0": - "integrity" "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==" - "resolved" "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz" - "version" "2.1.1" - dependencies: - "micromark-util-symbol" "^2.0.0" - "micromark-util-types" "^2.0.0" - -"micromark-util-encode@^2.0.0": - "integrity" "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" - "resolved" "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz" - "version" "2.0.1" - -"micromark-util-sanitize-uri@^2.0.0": - "integrity" "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==" - "resolved" "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz" - "version" "2.0.1" - dependencies: - "micromark-util-character" "^2.0.0" - "micromark-util-encode" "^2.0.0" - "micromark-util-symbol" "^2.0.0" - -"micromark-util-symbol@^2.0.0": - "integrity" "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" - "resolved" "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz" - "version" "2.0.1" - -"micromark-util-types@^2.0.0": - "integrity" "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" - "resolved" "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz" - "version" "2.0.2" - -"minisearch@^7.1.1": - "integrity" "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==" - "resolved" "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz" - "version" "7.1.2" - -"mitt@^3.0.1": - "integrity" "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" - "resolved" "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz" - "version" "3.0.1" - -"mlly@^1.7.4": - "integrity" "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==" - "resolved" "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz" - "version" "1.7.4" - dependencies: - "acorn" "^8.14.0" - "pathe" "^2.0.1" - "pkg-types" "^1.3.0" - "ufo" "^1.5.4" - -"ms@^2.1.3": - "integrity" "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - "version" "2.1.3" - -"nanoid@^3.3.11": - "integrity" "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" - "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" - "version" "3.3.11" - -"oniguruma-to-es@^3.1.0": - "integrity" "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==" - "resolved" "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz" - "version" "3.1.1" - dependencies: - "emoji-regex-xs" "^1.0.0" - "regex" "^6.0.1" - "regex-recursion" "^6.0.2" - -"package-manager-detector@^1.3.0": - "integrity" "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==" - "resolved" "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz" - "version" "1.3.0" - -"pathe@^2.0.1", "pathe@^2.0.3": - "integrity" "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" - "resolved" "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" - "version" "2.0.3" - -"perfect-debounce@^1.0.0": - "integrity" "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==" - "resolved" "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz" - "version" "1.0.0" - -"picocolors@^1.1.1": - "integrity" "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - "resolved" "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" - "version" "1.1.1" - -"pkg-types@^1.3.0": - "integrity" "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==" - "resolved" "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz" - "version" "1.3.1" - dependencies: - "confbox" "^0.1.8" - "mlly" "^1.7.4" - "pathe" "^2.0.1" - -"pkg-types@^2.0.1": - "integrity" "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==" - "resolved" "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz" - "version" "2.1.0" - dependencies: - "confbox" "^0.2.1" - "exsolve" "^1.0.1" - "pathe" "^2.0.3" - -"postcss@^8", "postcss@^8.4.43", "postcss@^8.5.3": - "integrity" "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==" - "resolved" "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz" - "version" "8.5.4" - dependencies: - "nanoid" "^3.3.11" - "picocolors" "^1.1.1" - "source-map-js" "^1.2.1" - -"preact@^10.0.0": - "integrity" "sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ==" - "resolved" "https://registry.npmjs.org/preact/-/preact-10.26.8.tgz" - "version" "10.26.8" - -"property-information@^7.0.0": - "integrity" "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" - "resolved" "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz" - "version" "7.1.0" - -"quansync@^0.2.8": - "integrity" "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==" - "resolved" "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz" - "version" "0.2.10" - -"regex-recursion@^6.0.2": - "integrity" "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==" - "resolved" "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz" - "version" "6.0.2" - dependencies: - "regex-utilities" "^2.3.0" - -"regex-utilities@^2.3.0": - "integrity" "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==" - "resolved" "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz" - "version" "2.3.0" - -"regex@^6.0.1": - "integrity" "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==" - "resolved" "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz" - "version" "6.0.1" - dependencies: - "regex-utilities" "^2.3.0" - -"rfdc@^1.4.1": - "integrity" "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" - "resolved" "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" - "version" "1.4.1" - -"rollup@^4.20.0": - "integrity" "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==" - "resolved" "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz" - "version" "4.42.0" - dependencies: - "@types/estree" "1.0.7" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + +minisearch@^7.1.1: + version "7.1.2" + resolved "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz" + integrity sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA== + +mitt@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + +mlly@^1.7.4: + version "1.8.0" + resolved "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz" + integrity sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g== + dependencies: + acorn "^8.15.0" + pathe "^2.0.3" + pkg-types "^1.3.1" + ufo "^1.6.1" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +oniguruma-to-es@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz" + integrity sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ== + dependencies: + emoji-regex-xs "^1.0.0" + regex "^6.0.1" + regex-recursion "^6.0.2" + +package-manager-detector@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz" + integrity sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ== + +pathe@^2.0.1, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +pkg-types@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== + dependencies: + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +pkg-types@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz" + integrity sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig== + dependencies: + confbox "^0.2.2" + exsolve "^1.0.7" + pathe "^2.0.3" + +postcss@^8, postcss@^8.4.43, postcss@^8.5.6: + version "8.5.6" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +preact@^10.0.0: + version "10.27.1" + resolved "https://registry.npmjs.org/preact/-/preact-10.27.1.tgz" + integrity sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ== + +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + +quansync@^0.2.11: + version "0.2.11" + resolved "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz" + integrity sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA== + +regex-recursion@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz" + integrity sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg== + dependencies: + regex-utilities "^2.3.0" + +regex-utilities@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz" + integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== + +regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz" + integrity sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA== + dependencies: + regex-utilities "^2.3.0" + +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rollup@^4.20.0: + version "4.49.0" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz" + integrity sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA== + dependencies: + "@types/estree" "1.0.8" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.42.0" - "@rollup/rollup-android-arm64" "4.42.0" - "@rollup/rollup-darwin-arm64" "4.42.0" - "@rollup/rollup-darwin-x64" "4.42.0" - "@rollup/rollup-freebsd-arm64" "4.42.0" - "@rollup/rollup-freebsd-x64" "4.42.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.42.0" - "@rollup/rollup-linux-arm-musleabihf" "4.42.0" - "@rollup/rollup-linux-arm64-gnu" "4.42.0" - "@rollup/rollup-linux-arm64-musl" "4.42.0" - "@rollup/rollup-linux-loongarch64-gnu" "4.42.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.42.0" - "@rollup/rollup-linux-riscv64-gnu" "4.42.0" - "@rollup/rollup-linux-riscv64-musl" "4.42.0" - "@rollup/rollup-linux-s390x-gnu" "4.42.0" - "@rollup/rollup-linux-x64-gnu" "4.42.0" - "@rollup/rollup-linux-x64-musl" "4.42.0" - "@rollup/rollup-win32-arm64-msvc" "4.42.0" - "@rollup/rollup-win32-ia32-msvc" "4.42.0" - "@rollup/rollup-win32-x64-msvc" "4.42.0" - "fsevents" "~2.3.2" + "@rollup/rollup-android-arm-eabi" "4.49.0" + "@rollup/rollup-android-arm64" "4.49.0" + "@rollup/rollup-darwin-arm64" "4.49.0" + "@rollup/rollup-darwin-x64" "4.49.0" + "@rollup/rollup-freebsd-arm64" "4.49.0" + "@rollup/rollup-freebsd-x64" "4.49.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.49.0" + "@rollup/rollup-linux-arm-musleabihf" "4.49.0" + "@rollup/rollup-linux-arm64-gnu" "4.49.0" + "@rollup/rollup-linux-arm64-musl" "4.49.0" + "@rollup/rollup-linux-loongarch64-gnu" "4.49.0" + "@rollup/rollup-linux-ppc64-gnu" "4.49.0" + "@rollup/rollup-linux-riscv64-gnu" "4.49.0" + "@rollup/rollup-linux-riscv64-musl" "4.49.0" + "@rollup/rollup-linux-s390x-gnu" "4.49.0" + "@rollup/rollup-linux-x64-gnu" "4.49.0" + "@rollup/rollup-linux-x64-musl" "4.49.0" + "@rollup/rollup-win32-arm64-msvc" "4.49.0" + "@rollup/rollup-win32-ia32-msvc" "4.49.0" + "@rollup/rollup-win32-x64-msvc" "4.49.0" + fsevents "~2.3.2" "search-insights@>= 1 < 3": - "integrity" "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==" - "resolved" "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz" - "version" "2.17.3" + version "2.17.3" + resolved "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz" + integrity sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ== -"shiki@^2.1.0": - "integrity" "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==" - "resolved" "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz" - "version" "2.5.0" +shiki@^2.1.0: + version "2.5.0" + resolved "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz" + integrity sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ== dependencies: "@shikijs/core" "2.5.0" "@shikijs/engine-javascript" "2.5.0" @@ -961,139 +1006,144 @@ "@shikijs/vscode-textmate" "^10.0.2" "@types/hast" "^3.0.4" -"source-map-js@^1.2.1": - "integrity" "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" - "resolved" "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" - "version" "1.2.1" - -"space-separated-tokens@^2.0.0": - "integrity" "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==" - "resolved" "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" - "version" "2.0.2" - -"speakingurl@^14.0.1": - "integrity" "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==" - "resolved" "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz" - "version" "14.0.1" - -"stringify-entities@^4.0.0": - "integrity" "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==" - "resolved" "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz" - "version" "4.0.4" - dependencies: - "character-entities-html4" "^2.0.0" - "character-entities-legacy" "^3.0.0" - -"superjson@^2.2.2": - "integrity" "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==" - "resolved" "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz" - "version" "2.2.2" - dependencies: - "copy-anything" "^3.0.2" - -"tabbable@^6.2.0": - "integrity" "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" - "resolved" "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz" - "version" "6.2.0" - -"tinyexec@^1.0.1": - "integrity" "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==" - "resolved" "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz" - "version" "1.0.1" - -"trim-lines@^3.0.0": - "integrity" "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" - "resolved" "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz" - "version" "3.0.1" - -"ufo@^1.5.4": - "integrity" "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==" - "resolved" "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz" - "version" "1.6.1" - -"unist-util-is@^6.0.0": - "integrity" "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==" - "resolved" "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz" - "version" "6.0.0" +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +speakingurl@^14.0.1: + version "14.0.1" + resolved "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz" + integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +superjson@^2.2.2: + version "2.2.2" + resolved "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz" + integrity sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q== + dependencies: + copy-anything "^3.0.2" + +tabbable@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== + +tinyexec@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz" + integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw== + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + +ufo@^1.6.1: + version "1.6.1" + resolved "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== + +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== dependencies: "@types/unist" "^3.0.0" -"unist-util-position@^5.0.0": - "integrity" "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==" - "resolved" "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz" - "version" "5.0.0" +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== dependencies: "@types/unist" "^3.0.0" -"unist-util-stringify-position@^4.0.0": - "integrity" "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==" - "resolved" "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz" - "version" "4.0.0" +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== dependencies: "@types/unist" "^3.0.0" -"unist-util-visit-parents@^6.0.0": - "integrity" "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==" - "resolved" "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz" - "version" "6.0.1" +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== dependencies: "@types/unist" "^3.0.0" - "unist-util-is" "^6.0.0" + unist-util-is "^6.0.0" -"unist-util-visit@^5.0.0": - "integrity" "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==" - "resolved" "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz" - "version" "5.0.0" +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== dependencies: "@types/unist" "^3.0.0" - "unist-util-is" "^6.0.0" - "unist-util-visit-parents" "^6.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" -"vfile-message@^4.0.0": - "integrity" "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==" - "resolved" "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz" - "version" "4.0.2" +vfile-message@^4.0.0: + version "4.0.3" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== dependencies: "@types/unist" "^3.0.0" - "unist-util-stringify-position" "^4.0.0" + unist-util-stringify-position "^4.0.0" -"vfile@^6.0.0": - "integrity" "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==" - "resolved" "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz" - "version" "6.0.3" +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== dependencies: "@types/unist" "^3.0.0" - "vfile-message" "^4.0.0" + vfile-message "^4.0.0" -"vite@^5.0.0 || ^6.0.0", "vite@^5.4.14": - "integrity" "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==" - "resolved" "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz" - "version" "5.4.19" +"vite@^5.0.0 || ^6.0.0", vite@^5.4.14, vite@>=3: + version "5.4.19" + resolved "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz" + integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA== dependencies: - "esbuild" "^0.21.3" - "postcss" "^8.4.43" - "rollup" "^4.20.0" + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" optionalDependencies: - "fsevents" "~2.3.3" + fsevents "~2.3.3" -"vitepress-plugin-group-icons@^1.5.2": - "integrity" "sha512-zen07KxZ83y3eecou4EraaEgwIriwHaB5Q0cHAmS4yO1UZEQvbljTylHPqiJ7LNkV39U8VehfcyquAJXg/26LA==" - "resolved" "https://registry.npmjs.org/vitepress-plugin-group-icons/-/vitepress-plugin-group-icons-1.5.2.tgz" - "version" "1.5.2" +vitepress-plugin-group-icons@^1.5.2: + version "1.6.3" + resolved "https://registry.npmjs.org/vitepress-plugin-group-icons/-/vitepress-plugin-group-icons-1.6.3.tgz" + integrity sha512-bvPD4lhraLJw3rPtLhUIVsOvNfnHnF+F1LH7BKHekEzeZ4uqdTdqnwEyaT580AoKjjT6/F8En6hVJj7takPKDA== dependencies: "@iconify-json/logos" "^1.2.4" - "@iconify-json/vscode-icons" "^1.2.18" - "@iconify/utils" "^2.3.0" + "@iconify-json/vscode-icons" "^1.2.29" + "@iconify/utils" "^3.0.0" -"vitepress-plugin-tabs@^0.7.1": - "integrity" "sha512-jxJvsicxnMSIYX9b8mAFLD2nwyKUcMO10dEt4nDSbinZhM8cGvAmMFOHPdf6TBX6gYZRl+/++/iYHtoM14fERQ==" - "resolved" "https://registry.npmjs.org/vitepress-plugin-tabs/-/vitepress-plugin-tabs-0.7.1.tgz" - "version" "0.7.1" +vitepress-plugin-tabs@^0.7.1: + version "0.7.1" + resolved "https://registry.npmjs.org/vitepress-plugin-tabs/-/vitepress-plugin-tabs-0.7.1.tgz" + integrity sha512-jxJvsicxnMSIYX9b8mAFLD2nwyKUcMO10dEt4nDSbinZhM8cGvAmMFOHPdf6TBX6gYZRl+/++/iYHtoM14fERQ== -"vitepress@^1.0.0", "vitepress@^1.6.3": - "integrity" "sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==" - "resolved" "https://registry.npmjs.org/vitepress/-/vitepress-1.6.3.tgz" - "version" "1.6.3" +vitepress@^1.0.0, vitepress@^1.6.3: + version "1.6.4" + resolved "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz" + integrity sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg== dependencies: "@docsearch/css" "3.8.2" "@docsearch/js" "3.8.2" @@ -1107,25 +1157,25 @@ "@vue/shared" "^3.5.13" "@vueuse/core" "^12.4.0" "@vueuse/integrations" "^12.4.0" - "focus-trap" "^7.6.4" - "mark.js" "8.11.1" - "minisearch" "^7.1.1" - "shiki" "^2.1.0" - "vite" "^5.4.14" - "vue" "^3.5.13" - -"vue@^3.2.25", "vue@^3.5.0", "vue@^3.5.13", "vue@3.5.16": - "integrity" "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==" - "resolved" "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz" - "version" "3.5.16" - dependencies: - "@vue/compiler-dom" "3.5.16" - "@vue/compiler-sfc" "3.5.16" - "@vue/runtime-dom" "3.5.16" - "@vue/server-renderer" "3.5.16" - "@vue/shared" "3.5.16" - -"zwitch@^2.0.4": - "integrity" "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" - "resolved" "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz" - "version" "2.0.4" + focus-trap "^7.6.4" + mark.js "8.11.1" + minisearch "^7.1.1" + shiki "^2.1.0" + vite "^5.4.14" + vue "^3.5.13" + +vue@^3.2.25, vue@^3.5.0, vue@^3.5.13, vue@3.5.20: + version "3.5.20" + resolved "https://registry.npmjs.org/vue/-/vue-3.5.20.tgz" + integrity sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw== + dependencies: + "@vue/compiler-dom" "3.5.20" + "@vue/compiler-sfc" "3.5.20" + "@vue/runtime-dom" "3.5.20" + "@vue/server-renderer" "3.5.20" + "@vue/shared" "3.5.20" + +zwitch@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==