diff --git a/.icons/plandex.svg b/.icons/plandex.svg
new file mode 100644
index 000000000..5e4a5ffa3
--- /dev/null
+++ b/.icons/plandex.svg
@@ -0,0 +1 @@
+
diff --git a/registry/fran-mora/.images/avatar.svg b/registry/fran-mora/.images/avatar.svg
new file mode 100644
index 000000000..cc2acd09e
--- /dev/null
+++ b/registry/fran-mora/.images/avatar.svg
@@ -0,0 +1,4 @@
+
diff --git a/registry/fran-mora/README.md b/registry/fran-mora/README.md
new file mode 100644
index 000000000..6da294324
--- /dev/null
+++ b/registry/fran-mora/README.md
@@ -0,0 +1,19 @@
+---
+display_name: Francesco Moramarco
+bio: AI/ML engineer shipping modules for AI coding agents and developer tooling.
+github: fran-mora
+avatar: ./.images/avatar.svg
+status: community
+---
+
+# Francesco Moramarco
+
+AI/ML engineer focused on AI coding agents, developer tooling, and dev-environment ergonomics.
+
+## Modules
+
+- **plandex**: Install and run the [Plandex](https://plandex.ai) CLI AI coding agent in your Coder workspace.
+
+## Contributing
+
+If you'd like to contribute to this namespace, please [open an issue](https://github.com/coder/registry/issues) or submit a pull request.
diff --git a/registry/fran-mora/modules/plandex/README.md b/registry/fran-mora/modules/plandex/README.md
new file mode 100644
index 000000000..5899914ce
--- /dev/null
+++ b/registry/fran-mora/modules/plandex/README.md
@@ -0,0 +1,157 @@
+---
+display_name: Plandex
+description: Install and configure the Plandex CLI AI coding agent in your workspace.
+icon: ../../../../.icons/plandex.svg
+verified: false
+tags: [agent, plandex, ai, cli]
+---
+
+# Plandex
+
+Install and configure the [Plandex](https://plandex.ai) CLI AI coding agent in your workspace. Starting Plandex is left to the caller (template command, IDE launcher, or a custom `coder_script`) — the same pattern used by the official `claude-code` module.
+
+```tf
+module "plandex" {
+ source = "registry.coder.com/fran-mora/plandex/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ openai_api_key = "sk-..."
+}
+```
+
+## Prerequisites
+
+Plandex needs at least one upstream LLM provider key. Set whichever the user prefers:
+
+- `openai_api_key` — passed to Plandex via `OPENAI_API_KEY`. Default provider.
+- `anthropic_api_key` — passed via `ANTHROPIC_API_KEY`.
+- `google_api_key` — passed via `GOOGLE_API_KEY`.
+- `openrouter_api_key` — passed via `OPENROUTER_API_KEY`.
+
+For a self-hosted Plandex server, also set `plandex_api_host` to the server URL.
+
+## Examples
+
+### Standalone mode with a launcher app
+
+Install Plandex against the user's OpenAI key and add a `coder_app` that opens a Plandex REPL in the workspace from the dashboard.
+
+```tf
+locals {
+ plandex_workdir = "/home/coder/project"
+}
+
+module "plandex" {
+ source = "registry.coder.com/fran-mora/plandex/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ workdir = local.plandex_workdir
+ openai_api_key = "sk-..."
+}
+
+resource "coder_app" "plandex" {
+ agent_id = coder_agent.main.id
+ slug = "plandex"
+ display_name = "Plandex"
+ icon = "/icon/plandex.svg"
+ open_in = "slim-window"
+ command = <<-EOT
+ #!/bin/bash
+ set -e
+ cd ${local.plandex_workdir}
+ plandex
+ EOT
+}
+```
+
+> [!NOTE]
+> `coder_app.command` runs when the user clicks the app tile. The module sets the relevant API-key env vars on the agent so the CLI starts pre-authenticated.
+
+### Pin a specific Plandex version
+
+```tf
+module "plandex" {
+ source = "registry.coder.com/fran-mora/plandex/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ workdir = "/home/coder/project"
+ plandex_version = "2.2.1"
+ openai_api_key = "sk-..."
+}
+```
+
+### Self-hosted Plandex server
+
+Point the CLI at a self-hosted Plandex server instead of Plandex Cloud.
+
+```tf
+module "plandex" {
+ source = "registry.coder.com/fran-mora/plandex/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ workdir = "/home/coder/project"
+ plandex_api_host = "https://plandex.example.com"
+ anthropic_api_key = "sk-ant-..."
+}
+```
+
+### Skip the installer (Plandex pre-installed in the image)
+
+If Plandex is already baked into the workspace image, set `install_plandex = false` so the module only configures env vars.
+
+```tf
+module "plandex" {
+ source = "registry.coder.com/fran-mora/plandex/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ install_plandex = false
+ openai_api_key = "sk-..."
+}
+```
+
+### Serialize a downstream `coder_script` after the install pipeline
+
+The module exposes the `coder exp sync` name of each script it creates via the `scripts` output: an ordered list (`pre_install`, `install`, `post_install`) of names for scripts this module actually creates. Scripts that were not configured are absent from the list.
+
+Downstream `coder_script` resources can wait for this module's install pipeline to finish using `coder exp sync want `:
+
+```tf
+module "plandex" {
+ source = "registry.coder.com/fran-mora/plandex/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ workdir = "/home/coder/project"
+ openai_api_key = "sk-..."
+}
+
+resource "coder_script" "post_plandex" {
+ agent_id = coder_agent.main.id
+ display_name = "Run after Plandex install"
+ run_on_start = true
+ script = <<-EOT
+ #!/bin/bash
+ set -euo pipefail
+ trap 'coder exp sync complete post-plandex' EXIT
+ coder exp sync want post-plandex ${join(" ", module.plandex.scripts)}
+ coder exp sync start post-plandex
+
+ # Your work here runs after plandex finishes installing.
+ plandex version
+ EOT
+}
+```
+
+## Troubleshooting
+
+If Plandex doesn't appear on the workspace `PATH` after install, check the install log:
+
+```bash
+cat ~/.coder-modules/fran-mora/plandex/logs/install.log
+```
+
+The Plandex installer writes the binary to `/usr/local/bin/plandex` if `sudo` is available, otherwise to `$HOME/.local/bin/plandex`. The module ensures the latter is on `PATH` by appending it to the user's shell profiles.
+
+## References
+
+- [Plandex documentation](https://docs.plandex.ai)
+- [Plandex GitHub](https://github.com/plandex-ai/plandex)
diff --git a/registry/fran-mora/modules/plandex/main.tf b/registry/fran-mora/modules/plandex/main.tf
new file mode 100644
index 000000000..207d84e7b
--- /dev/null
+++ b/registry/fran-mora/modules/plandex/main.tf
@@ -0,0 +1,155 @@
+terraform {
+ required_version = ">= 1.9"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.12"
+ }
+ }
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+variable "icon" {
+ type = string
+ description = "The icon to use for the app."
+ default = "/icon/plandex.svg"
+}
+
+variable "workdir" {
+ type = string
+ description = "Optional project directory. When set, the module pre-creates it if missing so Plandex can be opened in it directly."
+ default = null
+}
+
+variable "pre_install_script" {
+ type = string
+ description = "Custom script to run before installing Plandex. Useful for dependency ordering between modules."
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Custom script to run after installing Plandex."
+ default = null
+}
+
+variable "install_plandex" {
+ type = bool
+ description = "Whether to install Plandex."
+ default = true
+}
+
+variable "plandex_version" {
+ type = string
+ description = "The version of Plandex to install. Use 'latest' for the most recent release, or pin a specific version like '2.2.1'."
+ default = "latest"
+}
+
+variable "openai_api_key" {
+ type = string
+ description = "OpenAI API key passed to Plandex via the OPENAI_API_KEY env var."
+ sensitive = true
+ default = ""
+}
+
+variable "anthropic_api_key" {
+ type = string
+ description = "Anthropic API key passed to Plandex via the ANTHROPIC_API_KEY env var."
+ sensitive = true
+ default = ""
+}
+
+variable "google_api_key" {
+ type = string
+ description = "Google API key passed to Plandex via the GOOGLE_API_KEY env var."
+ sensitive = true
+ default = ""
+}
+
+variable "openrouter_api_key" {
+ type = string
+ description = "OpenRouter API key passed to Plandex via the OPENROUTER_API_KEY env var."
+ sensitive = true
+ default = ""
+}
+
+variable "plandex_api_host" {
+ type = string
+ description = "Optional Plandex server host. Set this to your self-hosted Plandex server URL (e.g. https://plandex.example.com). Leave empty to use the public Plandex Cloud or BYO-key local mode."
+ default = ""
+}
+
+resource "coder_env" "openai_api_key" {
+ count = var.openai_api_key != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "OPENAI_API_KEY"
+ value = var.openai_api_key
+}
+
+resource "coder_env" "anthropic_api_key" {
+ count = var.anthropic_api_key != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "ANTHROPIC_API_KEY"
+ value = var.anthropic_api_key
+}
+
+resource "coder_env" "google_api_key" {
+ count = var.google_api_key != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "GOOGLE_API_KEY"
+ value = var.google_api_key
+}
+
+resource "coder_env" "openrouter_api_key" {
+ count = var.openrouter_api_key != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "OPENROUTER_API_KEY"
+ value = var.openrouter_api_key
+}
+
+resource "coder_env" "plandex_api_host" {
+ count = var.plandex_api_host != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "PLANDEX_API_HOST"
+ value = var.plandex_api_host
+}
+
+locals {
+ workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : ""
+ install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
+ ARG_PLANDEX_VERSION = var.plandex_version
+ ARG_INSTALL_PLANDEX = tostring(var.install_plandex)
+ ARG_WORKDIR = local.workdir
+ })
+ module_dir_name = ".coder-modules/fran-mora/plandex"
+}
+
+module "coder_utils" {
+ source = "registry.coder.com/coder/coder-utils/coder"
+ version = "0.0.1"
+
+ agent_id = var.agent_id
+ module_directory = "$HOME/${local.module_dir_name}"
+ display_name_prefix = "Plandex"
+ icon = var.icon
+ pre_install_script = var.pre_install_script
+ post_install_script = var.post_install_script
+ install_script = local.install_script
+}
+
+# Pass-through of coder-utils script outputs so upstream modules can serialize
+# their coder_script resources behind this module's install pipeline using
+# `coder exp sync want `.
+output "scripts" {
+ description = "Ordered list of coder exp sync names for the coder_script resources this module actually creates, in run order (pre_install, install, post_install). Scripts that were not configured are absent from the list."
+ value = module.coder_utils.scripts
+}
diff --git a/registry/fran-mora/modules/plandex/main.tftest.hcl b/registry/fran-mora/modules/plandex/main.tftest.hcl
new file mode 100644
index 000000000..ad4500de6
--- /dev/null
+++ b/registry/fran-mora/modules/plandex/main.tftest.hcl
@@ -0,0 +1,255 @@
+run "defaults_are_correct" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ }
+
+ assert {
+ condition = var.install_plandex == true
+ error_message = "Plandex installation should be enabled by default"
+ }
+
+ assert {
+ condition = var.plandex_version == "latest"
+ error_message = "Default Plandex version should be 'latest'"
+ }
+
+ assert {
+ condition = var.icon == "/icon/plandex.svg"
+ error_message = "Default icon should be '/icon/plandex.svg'"
+ }
+
+ assert {
+ condition = var.workdir == null
+ error_message = "Workdir should be null by default"
+ }
+
+ assert {
+ condition = var.openai_api_key == ""
+ error_message = "OpenAI API key should default to empty"
+ }
+
+ assert {
+ condition = var.anthropic_api_key == ""
+ error_message = "Anthropic API key should default to empty"
+ }
+
+ assert {
+ condition = var.google_api_key == ""
+ error_message = "Google API key should default to empty"
+ }
+
+ assert {
+ condition = var.openrouter_api_key == ""
+ error_message = "OpenRouter API key should default to empty"
+ }
+
+ assert {
+ condition = var.plandex_api_host == ""
+ error_message = "Plandex API host should default to empty"
+ }
+
+ assert {
+ condition = local.module_dir_name == ".coder-modules/fran-mora/plandex"
+ error_message = "Module dir name should follow the .coder-modules// convention"
+ }
+
+ assert {
+ condition = local.workdir == ""
+ error_message = "Workdir local should be empty string when var.workdir is null"
+ }
+}
+
+run "workdir_trailing_slash_trimmed" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project/"
+ }
+
+ assert {
+ condition = local.workdir == "/home/coder/project"
+ error_message = "Trailing slash on workdir should be trimmed"
+ }
+}
+
+run "workdir_without_trailing_slash" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ }
+
+ assert {
+ condition = local.workdir == "/home/coder/project"
+ error_message = "Workdir without trailing slash should pass through unchanged"
+ }
+}
+
+run "plandex_version_pinning" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ plandex_version = "2.2.1"
+ }
+
+ assert {
+ condition = var.plandex_version == "2.2.1"
+ error_message = "Plandex version should be settable"
+ }
+}
+
+run "install_disabled" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ install_plandex = false
+ }
+
+ assert {
+ condition = var.install_plandex == false
+ error_message = "Installation should be skippable"
+ }
+}
+
+run "openai_api_key_creates_env_resource" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ openai_api_key = "sk-test-key"
+ }
+
+ assert {
+ condition = length(coder_env.openai_api_key) == 1
+ error_message = "Setting openai_api_key should create exactly one coder_env resource"
+ }
+
+ assert {
+ condition = length(coder_env.anthropic_api_key) == 0
+ error_message = "Anthropic env should not be created when only OpenAI key is set"
+ }
+}
+
+run "multiple_provider_keys_create_multiple_env_resources" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ openai_api_key = "sk-openai"
+ anthropic_api_key = "sk-ant"
+ google_api_key = "google-key"
+ openrouter_api_key = "or-key"
+ }
+
+ assert {
+ condition = length(coder_env.openai_api_key) == 1
+ error_message = "OpenAI env should be created when key is set"
+ }
+
+ assert {
+ condition = length(coder_env.anthropic_api_key) == 1
+ error_message = "Anthropic env should be created when key is set"
+ }
+
+ assert {
+ condition = length(coder_env.google_api_key) == 1
+ error_message = "Google env should be created when key is set"
+ }
+
+ assert {
+ condition = length(coder_env.openrouter_api_key) == 1
+ error_message = "OpenRouter env should be created when key is set"
+ }
+}
+
+run "no_api_keys_creates_no_env_resources" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ }
+
+ assert {
+ condition = length(coder_env.openai_api_key) == 0
+ error_message = "OpenAI env should not be created without a key"
+ }
+
+ assert {
+ condition = length(coder_env.anthropic_api_key) == 0
+ error_message = "Anthropic env should not be created without a key"
+ }
+
+ assert {
+ condition = length(coder_env.google_api_key) == 0
+ error_message = "Google env should not be created without a key"
+ }
+
+ assert {
+ condition = length(coder_env.openrouter_api_key) == 0
+ error_message = "OpenRouter env should not be created without a key"
+ }
+
+ assert {
+ condition = length(coder_env.plandex_api_host) == 0
+ error_message = "Plandex API host env should not be created without a value"
+ }
+}
+
+run "self_hosted_api_host" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ plandex_api_host = "https://plandex.example.com"
+ }
+
+ assert {
+ condition = length(coder_env.plandex_api_host) == 1
+ error_message = "Setting plandex_api_host should create the PLANDEX_API_HOST env resource"
+ }
+
+ assert {
+ condition = coder_env.plandex_api_host[0].value == "https://plandex.example.com"
+ error_message = "PLANDEX_API_HOST should pass through the configured URL"
+ }
+}
+
+run "custom_scripts_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ pre_install_script = "#!/bin/bash\necho 'pre-install'"
+ post_install_script = "#!/bin/bash\necho 'post-install'"
+ }
+
+ assert {
+ condition = var.pre_install_script != null
+ error_message = "Pre-install script should be settable"
+ }
+
+ assert {
+ condition = var.post_install_script != null
+ error_message = "Post-install script should be settable"
+ }
+}
+
+run "custom_icon" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ icon = "/custom/icon.svg"
+ }
+
+ assert {
+ condition = var.icon == "/custom/icon.svg"
+ error_message = "Custom icon should be settable"
+ }
+}
diff --git a/registry/fran-mora/modules/plandex/scripts/install.sh.tftpl b/registry/fran-mora/modules/plandex/scripts/install.sh.tftpl
new file mode 100644
index 000000000..6a79bfa5f
--- /dev/null
+++ b/registry/fran-mora/modules/plandex/scripts/install.sh.tftpl
@@ -0,0 +1,117 @@
+#!/bin/bash
+
+set -euo pipefail
+
+BOLD='\033[0;1m'
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+ARG_PLANDEX_VERSION='${ARG_PLANDEX_VERSION}'
+ARG_INSTALL_PLANDEX='${ARG_INSTALL_PLANDEX}'
+ARG_WORKDIR='${ARG_WORKDIR}'
+
+# Plandex's official installer drops the binary in /usr/local/bin (with sudo)
+# or $HOME/.local/bin (without). Prepend the latter so the binary is on PATH
+# regardless of which path the installer chose.
+export PATH="$HOME/.local/bin:$PATH"
+
+echo "--------------------------------"
+printf "ARG_PLANDEX_VERSION: %s\n" "$ARG_PLANDEX_VERSION"
+printf "ARG_INSTALL_PLANDEX: %s\n" "$ARG_INSTALL_PLANDEX"
+printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
+echo "--------------------------------"
+
+add_path_to_shell_profiles() {
+ local path_dir="$1"
+
+ for profile in "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.zprofile" "$HOME/.zshrc"; do
+ if [ -f "$profile" ]; then
+ if ! grep -q "$path_dir" "$profile" 2> /dev/null; then
+ echo "export PATH=\"\$PATH:$path_dir\"" >> "$profile"
+ echo "Added $path_dir to $profile"
+ fi
+ fi
+ done
+
+ local fish_config="$HOME/.config/fish/config.fish"
+ if [ -f "$fish_config" ]; then
+ if ! grep -q "$path_dir" "$fish_config" 2> /dev/null; then
+ echo "fish_add_path $path_dir" >> "$fish_config"
+ echo "Added $path_dir to $fish_config"
+ fi
+ fi
+}
+
+ensure_plandex_in_path() {
+ local PLANDEX_BIN=""
+ if command -v plandex > /dev/null 2>&1; then
+ PLANDEX_BIN=$(command -v plandex)
+ elif [ -x "$HOME/.local/bin/plandex" ]; then
+ PLANDEX_BIN="$HOME/.local/bin/plandex"
+ elif [ -x "/usr/local/bin/plandex" ]; then
+ PLANDEX_BIN="/usr/local/bin/plandex"
+ fi
+
+ if [ -z "$PLANDEX_BIN" ] || [ ! -x "$PLANDEX_BIN" ]; then
+ echo "Warning: Could not find plandex binary on PATH after install."
+ return
+ fi
+
+ local PLANDEX_DIR
+ PLANDEX_DIR=$(dirname "$PLANDEX_BIN")
+
+ if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$CODER_SCRIPT_BIN_DIR/plandex" ]; then
+ ln -s "$PLANDEX_BIN" "$CODER_SCRIPT_BIN_DIR/plandex"
+ echo "Created symlink: $CODER_SCRIPT_BIN_DIR/plandex -> $PLANDEX_BIN"
+ fi
+
+ add_path_to_shell_profiles "$PLANDEX_DIR"
+}
+
+install_plandex_cli() {
+ if [ "$ARG_INSTALL_PLANDEX" != "true" ]; then
+ echo "Skipping Plandex installation as per configuration."
+ ensure_plandex_in_path
+ return
+ fi
+
+ if command_exists plandex; then
+ echo "Plandex already installed: $(plandex version 2> /dev/null || echo 'unknown version')"
+ ensure_plandex_in_path
+ return
+ fi
+
+ printf "$${BOLD}Installing Plandex CLI (version: %s)...\n" "$ARG_PLANDEX_VERSION"
+
+ if [ "$ARG_PLANDEX_VERSION" = "latest" ]; then
+ curl -sL https://plandex.ai/install.sh | bash
+ else
+ # The Plandex installer reads the PLANDEX_VERSION env var to pin a specific
+ # version. Strip a leading 'v' so users can pass either '2.2.1' or 'v2.2.1'.
+ local pinned_version="$${ARG_PLANDEX_VERSION#v}"
+ curl -sL https://plandex.ai/install.sh | PLANDEX_VERSION="$pinned_version" bash
+ fi
+
+ ensure_plandex_in_path
+
+ if ! command_exists plandex; then
+ echo "ERROR: Plandex installation appears to have failed."
+ exit 1
+ fi
+
+ echo "Installed Plandex successfully: $(plandex version 2> /dev/null || echo 'unknown version')"
+}
+
+setup_workdir() {
+ if [ -n "$ARG_WORKDIR" ] && [ ! -d "$ARG_WORKDIR" ]; then
+ echo "Workdir '$ARG_WORKDIR' does not exist; creating it."
+ mkdir -p "$ARG_WORKDIR"
+ fi
+}
+
+install_plandex_cli
+setup_workdir
+
+echo "Plandex module setup completed."