diff --git a/.claude/hooks/pre-commit.sh b/.claude/hooks/pre-commit.sh index 1d56daf..7e80e74 100755 --- a/.claude/hooks/pre-commit.sh +++ b/.claude/hooks/pre-commit.sh @@ -12,6 +12,11 @@ fi cd "$CLAUDE_PROJECT_DIR" || exit 0 +# Skip checks if mix is not available (e.g., in CI or sandboxed environments) +if ! command -v mix &> /dev/null; then + exit 0 +fi + echo "Running pre-commit checks..." >&2 if ! mix compile --warnings-as-errors 2>&1; then diff --git a/.envrc b/.envrc deleted file mode 100644 index fe7c01a..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -dotenv diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8d49fd3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + name: Elixir ${{ matrix.elixir }} / OTP ${{ matrix.otp }} + runs-on: ubuntu-latest + strategy: + matrix: + include: + - elixir: "1.19" + otp: "27" + env: + MIX_ENV: test + steps: + - name: Install system dependencies + run: sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep + + - uses: actions/checkout@v4 + + - uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + + - name: Cache deps + uses: actions/cache@v4 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}- + + - name: Install dependencies + run: mix deps.get + + - name: Compile (warnings as errors) + run: mix compile --warnings-as-errors + + - name: Check formatting + run: mix format --check-formatted + + - name: Run tests + run: mix test diff --git a/.gitignore b/.gitignore index 86e7a58..8a931c8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ erl_crash.dump # Environment files with secrets .env .env.* +.envrc # macOS metadata .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 5437e22..a6b8a77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,25 @@ All notable changes to this project are documented here. GitHub API via `curl` + `jq`, parses with `Jason`, and generates summaries via parallel schema queries. Run with `mix rlm.examples web_fetch`. +- GitHub Actions CI workflow (compile, format, test) +- Security notice in README documenting the trust boundary + +### Changed + +- API key log output now shows only last 4 characters instead of first 12 +- `.envrc` untracked and added to `.gitignore` (local direnv preference) +- Added `@moduledoc` to `RLMWeb.Endpoint`, `RLMWeb.Router`, `RLMWeb.RunListLive`, + `RLMWeb.RunDetailLive`, and `RLMWeb.Telemetry` +- Pre-commit hook skips gracefully when `mix` is not available + +### Removed + +- `demo_run.exs` — referenced missing `priv/foodlab.epub`; would crash on clone +- `RLMWeb.PageController`, `RLMWeb.PageHTML` — unused Phoenix generator leftovers +- `RLMWeb.channel/0` helper — no channels in the application +- Swoosh dependency and `RLMWeb.Mailer` — no email functionality exists +- `/dev/mailbox` route — removed with Swoosh + --- ## [0.3.0] — 2026-02-24 diff --git a/CLAUDE.md b/CLAUDE.md index 4e81cf2..2673d95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,10 +35,9 @@ rlm/ │ │ ├── router.ex │ │ ├── telemetry.ex │ │ ├── gettext.ex -│ │ ├── mailer.ex │ │ ├── live/ # RunListLive (/), RunDetailLive (/runs/:run_id) │ │ ├── components/ # CoreComponents, Layouts, TraceComponents -│ │ └── controllers/ # ErrorHTML, ErrorJSON, PageController, TraceDebugController +│ │ └── controllers/ # ErrorHTML, ErrorJSON, TraceDebugController │ └── mix/tasks/ # mix rlm.smoke, mix rlm.examples ├── test/ │ ├── test_helper.exs @@ -227,7 +226,7 @@ Read-only Phoenix LiveView dashboard. Serves on `http://localhost:4000`. |---|---| | `RLMWeb` | Phoenix web module (verified routes, component imports) | | `RLMWeb.Endpoint` | Phoenix endpoint using `RLM.PubSub` as pubsub_server | -| `RLMWeb.Router` | Routes: `/` (RunListLive), `/runs/:run_id` (RunDetailLive), `/dev/dashboard`, `/dev/mailbox` | +| `RLMWeb.Router` | Routes: `/` (RunListLive), `/runs/:run_id` (RunDetailLive), `/dev/dashboard` | | `RLMWeb.RunListLive` | `/` — list of all runs (from TraceStore + live PubSub updates) | | `RLMWeb.RunDetailLive` | `/runs/:run_id` — recursive span tree with expandable iterations | | `RLMWeb.TraceComponents` | HEEx components: `span_node/1`, `iteration_card/1` | @@ -236,7 +235,7 @@ Read-only Phoenix LiveView dashboard. Serves on `http://localhost:4000`. | `RLMWeb.CoreComponents` | Core UI components (flash, button, input, table, icon, etc.) | | `RLMWeb.Layouts` | App layout, flash group, theme toggle | | `RLMWeb.Gettext` | Internationalization backend | -| `RLMWeb.Mailer` | Swoosh mailer | + ## Config Fields diff --git a/README.md b/README.md index 60e9906..907975e 100644 --- a/README.md +++ b/README.md @@ -306,6 +306,16 @@ Tools live inside the sandbox — the eval'd code calls `read_file/1`, `bash/1`, --- +## Security + +RLM executes LLM-generated Elixir code via `Code.eval_string` with full access to the +host filesystem, network, and shell. **Do not expose RLM to untrusted users or untrusted +LLM providers.** It is designed for local development, trusted API backends (Anthropic), +and controlled environments. There is no sandboxing beyond process-level isolation and +configurable timeouts. + +--- + ## Further reading For a comprehensive architecture reference — OTP supervision tree, async-eval pattern, diff --git a/config/config.exs b/config/config.exs index 637bfd6..7984104 100644 --- a/config/config.exs +++ b/config/config.exs @@ -27,15 +27,6 @@ config :rlm, RLMWeb.Endpoint, pubsub_server: RLM.PubSub, live_view: [signing_salt: "G4RzK1j2"] -# Configure the mailer -# -# By default it uses the "Local" adapter which stores the emails -# locally. You can see the emails in your browser, at "/dev/mailbox". -# -# For production it's recommended to configure a different adapter -# at the `config/runtime.exs`. -config :rlm, RLMWeb.Mailer, adapter: Swoosh.Adapters.Local - # Configure esbuild (the version is required) config :esbuild, version: "0.25.4", diff --git a/config/dev.exs b/config/dev.exs index 781c152..10d283f 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -57,7 +57,7 @@ config :rlm, RLMWeb.Endpoint, ] ] -# Enable dev routes for dashboard and mailbox +# Enable dev routes for dashboard config :rlm, dev_routes: true # Do not include metadata nor timestamps in development logs @@ -77,6 +77,3 @@ config :phoenix_live_view, debug_attributes: true, # Enable helpful, but potentially expensive runtime checks enable_expensive_runtime_checks: true - -# Disable swoosh api client as it is only required for production adapters. -config :swoosh, :api_client, false diff --git a/config/prod.exs b/config/prod.exs index d6cd726..0c67efd 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -17,12 +17,6 @@ config :rlm, RLMWeb.Endpoint, hosts: ["localhost", "127.0.0.1"] ] -# Configure Swoosh API Client -config :swoosh, api_client: Swoosh.ApiClient.Req - -# Disable Swoosh Local Memory Storage -config :swoosh, local: false - # Do not print debug messages in production config :logger, level: :info diff --git a/config/runtime.exs b/config/runtime.exs index e0dfca1..76d8b37 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -88,22 +88,4 @@ if config_env() == :prod do # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. - - # ## Configuring the mailer - # - # In production you need to configure the mailer to use a different adapter. - # Here is an example configuration for Mailgun: - # - # config :rlm, RLMWeb.Mailer, - # adapter: Swoosh.Adapters.Mailgun, - # api_key: System.get_env("MAILGUN_API_KEY"), - # domain: System.get_env("MAILGUN_DOMAIN") - # - # Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney, - # and Finch out-of-the-box. This configuration is typically done at - # compile-time in your config/prod.exs: - # - # config :swoosh, :api_client, Swoosh.ApiClient.Req - # - # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. end diff --git a/config/test.exs b/config/test.exs index c4c8650..5baf0bd 100644 --- a/config/test.exs +++ b/config/test.exs @@ -7,12 +7,6 @@ config :rlm, RLMWeb.Endpoint, secret_key_base: "wGMmiWW+w1iw84uJuC13MGBtRyTSECbvCuOgLhxi2fgyC3qgc/n+iJQXmqYAdAMo", server: false -# In test we don't send emails -config :rlm, RLMWeb.Mailer, adapter: Swoosh.Adapters.Test - -# Disable swoosh api client as it is only required for production adapters -config :swoosh, :api_client, false - # Print only warnings and errors during test config :logger, level: :warning diff --git a/demo_run.exs b/demo_run.exs deleted file mode 100644 index b5bccce..0000000 --- a/demo_run.exs +++ /dev/null @@ -1,52 +0,0 @@ -# Load API key from .env -env_path = Path.join(__DIR__, ".env") - -if File.exists?(env_path) do - env_path - |> File.stream!() - |> Enum.each(fn line -> - line = String.trim(line) - - if line != "" and not String.starts_with?(line, "#") do - case String.split(line, "=", parts: 2) do - [key, value] -> - value = value |> String.trim() |> String.trim("\"") |> String.trim("'") - System.put_env(String.trim(key), value) - - _ -> - :ok - end - end - end) -end - -# Load a cross-section of the cookbook HTML (files 3-45: ~200 KB, rich recipe content) -path = Application.app_dir(:rlm, "priv/foodlab.epub") -{:ok, zip_files} = :zip.unzip(String.to_charlist(path), [:memory]) - -html = - zip_files - |> Enum.map(fn {name, content} -> {to_string(name), content} end) - |> Enum.filter(fn {k, _} -> String.ends_with?(k, ".html") or String.ends_with?(k, ".xhtml") end) - |> Enum.sort_by(fn {k, _} -> k end) - |> Enum.slice(3, 42) - |> Enum.map_join("\n\n", fn {name, content} -> "=== #{name} ===\n#{content}" end) - -IO.puts("Input size: #{div(byte_size(html), 1024)} KB") - -query = """ -This is HTML from a cookbook. Identify the top 5 most frequently mentioned -main proteins (meats, fish, legumes) across all recipes in the content. - -The input is large — split it into 3 roughly equal chunks and use -parallel_query to extract protein mentions from each chunk concurrently, -then aggregate and rank across all results. -""" - -IO.puts("Starting RLM run...\n") - -{:ok, answer, run_id} = RLM.run(html, query) - -IO.puts("\n=== Answer ===") -IO.puts(answer) -IO.puts("\nRun ID: #{run_id}") diff --git a/examples/smoke_test.exs b/examples/smoke_test.exs index 75ab007..abf53f6 100644 --- a/examples/smoke_test.exs +++ b/examples/smoke_test.exs @@ -160,7 +160,7 @@ defmodule RLM.SmokeTest do key -> IO.puts("\nRLM Smoke Test") IO.puts("==============") - IO.puts("API key: #{String.slice(key, 0, 12)}...") + IO.puts("API key: ...#{String.slice(key, -4, 4)}") IO.puts("") end end diff --git a/lib/mix/tasks/rlm.examples.ex b/lib/mix/tasks/rlm.examples.ex index 41b456d..a7dd370 100644 --- a/lib/mix/tasks/rlm.examples.ex +++ b/lib/mix/tasks/rlm.examples.ex @@ -137,7 +137,7 @@ defmodule Mix.Tasks.Rlm.Examples do System.halt(1) key -> - IO.puts(" API key: #{String.slice(key, 0, 12)}...") + IO.puts(" API key: ...#{String.slice(key, -4, 4)}") end end diff --git a/lib/rlm_web.ex b/lib/rlm_web.ex index 2052e9d..5f29b53 100644 --- a/lib/rlm_web.ex +++ b/lib/rlm_web.ex @@ -1,7 +1,7 @@ defmodule RLMWeb do @moduledoc """ The entrypoint for defining your web interface, such - as controllers, components, channels, and so on. + as controllers, components, and so on. This can be used in your application as: @@ -34,12 +34,6 @@ defmodule RLMWeb do end end - def channel do - quote do - use Phoenix.Channel - end - end - def controller do quote do use Phoenix.Controller, formats: [:html, :json] diff --git a/lib/rlm_web/controllers/page_controller.ex b/lib/rlm_web/controllers/page_controller.ex deleted file mode 100644 index 7f0bbd1..0000000 --- a/lib/rlm_web/controllers/page_controller.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule RLMWeb.PageController do - use RLMWeb, :controller - - def home(conn, _params) do - render(conn, :home) - end -end diff --git a/lib/rlm_web/controllers/page_html.ex b/lib/rlm_web/controllers/page_html.ex deleted file mode 100644 index 11da15a..0000000 --- a/lib/rlm_web/controllers/page_html.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule RLMWeb.PageHTML do - @moduledoc """ - This module contains pages rendered by PageController. - - See the `page_html` directory for all templates available. - """ - use RLMWeb, :html - - embed_templates "page_html/*" -end diff --git a/lib/rlm_web/controllers/page_html/home.html.heex b/lib/rlm_web/controllers/page_html/home.html.heex deleted file mode 100644 index b107fd0..0000000 --- a/lib/rlm_web/controllers/page_html/home.html.heex +++ /dev/null @@ -1,202 +0,0 @@ - - -
-
- -
-

- Phoenix Framework - - v{Application.spec(:phoenix, :vsn)} - -

- -
- -

- Peace of mind from prototype to production. -

-

- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. -

- -
-
diff --git a/lib/rlm_web/endpoint.ex b/lib/rlm_web/endpoint.ex index ec7b7ff..4d95801 100644 --- a/lib/rlm_web/endpoint.ex +++ b/lib/rlm_web/endpoint.ex @@ -1,4 +1,5 @@ defmodule RLMWeb.Endpoint do + @moduledoc "Phoenix endpoint for the RLM web dashboard." use Phoenix.Endpoint, otp_app: :rlm # The session will be stored in the cookie and signed, diff --git a/lib/rlm_web/live/run_detail_live.ex b/lib/rlm_web/live/run_detail_live.ex index 8c4ab36..11497c2 100644 --- a/lib/rlm_web/live/run_detail_live.ex +++ b/lib/rlm_web/live/run_detail_live.ex @@ -1,4 +1,5 @@ defmodule RLMWeb.RunDetailLive do + @moduledoc "LiveView for a single run's trace at `/runs/:run_id`. Shows the recursive span tree with expandable iterations." use RLMWeb, :live_view import RLMWeb.TraceComponents diff --git a/lib/rlm_web/live/run_list_live.ex b/lib/rlm_web/live/run_list_live.ex index 4d88764..2d054cf 100644 --- a/lib/rlm_web/live/run_list_live.ex +++ b/lib/rlm_web/live/run_list_live.ex @@ -1,4 +1,5 @@ defmodule RLMWeb.RunListLive do + @moduledoc "LiveView for the run history table at `/`. Subscribes to PubSub for live updates." use RLMWeb, :live_view alias Phoenix.PubSub diff --git a/lib/rlm_web/mailer.ex b/lib/rlm_web/mailer.ex deleted file mode 100644 index f74d26e..0000000 --- a/lib/rlm_web/mailer.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule RLMWeb.Mailer do - use Swoosh.Mailer, otp_app: :rlm -end diff --git a/lib/rlm_web/router.ex b/lib/rlm_web/router.ex index 33887a8..64e16e8 100644 --- a/lib/rlm_web/router.ex +++ b/lib/rlm_web/router.ex @@ -1,4 +1,5 @@ defmodule RLMWeb.Router do + @moduledoc "Routes for the RLM web dashboard and dev-only debug API." use RLMWeb, :router pipeline :browser do @@ -28,7 +29,7 @@ defmodule RLMWeb.Router do # pipe_through :api # end - # Enable LiveDashboard, Swoosh mailbox preview, and trace debug API in development + # Enable LiveDashboard and trace debug API in development if Application.compile_env(:rlm, :dev_routes) do # If you want to use the LiveDashboard in production, you should put # it behind authentication and allow only admins to access it. @@ -41,7 +42,6 @@ defmodule RLMWeb.Router do pipe_through :browser live_dashboard "/dashboard", metrics: RLMWeb.Telemetry - forward "/mailbox", Plug.Swoosh.MailboxPreview end scope "/api/debug", RLMWeb do diff --git a/lib/rlm_web/telemetry.ex b/lib/rlm_web/telemetry.ex index cfa2e96..8d0edf3 100644 --- a/lib/rlm_web/telemetry.ex +++ b/lib/rlm_web/telemetry.ex @@ -1,4 +1,5 @@ defmodule RLMWeb.Telemetry do + @moduledoc "Phoenix telemetry metrics supervisor for the web dashboard." use Supervisor import Telemetry.Metrics diff --git a/mix.exs b/mix.exs index b6ed00c..dbb0703 100644 --- a/mix.exs +++ b/mix.exs @@ -64,7 +64,6 @@ defmodule RLM.MixProject do app: false, compile: false, depth: 1}, - {:swoosh, "~> 1.16"}, {:gettext, "~> 1.0"}, {:dns_cluster, "~> 0.2.0"}, {:bandit, "~> 1.5"}, diff --git a/mix.lock b/mix.lock index 7432a76..8628def 100644 --- a/mix.lock +++ b/mix.lock @@ -17,7 +17,6 @@ "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, @@ -41,14 +40,12 @@ "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, - "swoosh": {:hex, :swoosh, "1.22.0", "0d65a95f89aedb5011af13295742294e309b4b4aaca556858d81e3b372b58abc", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c01ced23d8786d1ee1a03e4c16574290b2ccd6267beb8c81d081c4a34574ef6e"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "tidewave": {:hex, :tidewave, "0.5.5", "a125dfc87f99daf0e2280b3a9719b874c616ead5926cdf9cdfe4fcc19a020eff", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "825ebb4fa20de005785efa21e5a88c04d81c3f57552638d12ff3def2f203dbf7"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, }