From 1b5e38950c78f4fe1bdae091d73712c851883b69 Mon Sep 17 00:00:00 2001 From: Emmanuel Roux <15956441+fesaille@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:30:23 +0100 Subject: [PATCH 1/3] feat(server): support remote server connection and authentication Enables connecting to opencode servers on non-localhost hosts and authenticating via OPENCODE_SERVER_PASSWORD env var --- lua/opencode/cli/client.lua | 70 +++++++++++++++++++++++++++++++------ lua/opencode/config.lua | 14 ++++++++ 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index c4b19133..5d1b9628 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -10,6 +10,39 @@ local sse_state = { job_id = nil, } +---Get authentication credentials from config or environment variables. +---@return { username: string, password: string }|nil +local function get_auth() + local config = require("opencode.config") + local auth = config.opts.auth + + -- Check config first + if auth and auth.password then + return { + username = auth.username or "opencode", + password = auth.password, + } + end + + -- Fall back to environment variables + local env_password = vim.env.OPENCODE_SERVER_PASSWORD + if env_password and env_password ~= "" then + return { + username = vim.env.OPENCODE_SERVER_USERNAME or "opencode", + password = env_password, + } + end + + return nil +end + +---Get the configured hostname for the opencode server. +---@return string +local function get_hostname() + local config = require("opencode.config") + return config.opts.hostname or "127.0.0.1" +end + ---Generate a UUID v4 (cross-platform, no external dependencies) ---@return string UUID in format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx local function generate_uuid() @@ -70,6 +103,13 @@ local function curl(url, method, body, callback) "-N", -- No buffering, for streaming SSEs } + -- Add basic auth if credentials are available + local auth = get_auth() + if auth then + table.insert(command, "-u") + table.insert(command, auth.username .. ":" .. auth.password) + end + if body then table.insert(command, "-d") table.insert(command, vim.fn.json_encode(body)) @@ -151,7 +191,8 @@ end ---@param callback fun(response: table)|nil ---@return number job_id function M.call(port, path, method, body, callback) - return curl("http://localhost:" .. port .. path, method, body, callback) + local hostname = get_hostname() + return curl("http://" .. hostname .. ":" .. port .. path, method, body, callback) end ---@param text string @@ -260,15 +301,24 @@ end function M.get_path(port) -- Query each port synchronously for working directory -- TODO: Migrate to align with async paradigm used elsewhere - local curl_result = vim - .system({ - "curl", - "-s", - "--connect-timeout", - "1", - "http://localhost:" .. port .. "/path", - }) - :wait() + local hostname = get_hostname() + local curl_cmd = { + "curl", + "-s", + "--connect-timeout", + "1", + } + + -- Add basic auth if credentials are available + local auth = get_auth() + if auth then + table.insert(curl_cmd, "-u") + table.insert(curl_cmd, auth.username .. ":" .. auth.password) + end + + table.insert(curl_cmd, "http://" .. hostname .. ":" .. port .. "/path") + + local curl_result = vim.system(curl_cmd):wait() require("opencode.util").check_system_call(curl_result, "curl") local path_ok, path_data = pcall(vim.fn.json_decode, curl_result.stdout) diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index ca4ed22c..a4a2112e 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -18,6 +18,14 @@ vim.g.opencode_opts = vim.g.opencode_opts ---If set, `opencode.nvim` will append `--port ` to `provider.cmd`. ---@field port? number --- +---The hostname `opencode` server is running on. +---Defaults to `127.0.0.1`. +---@field hostname? string +--- +---Authentication configuration for the `opencode` server. +---If not set, will check for `OPENCODE_SERVER_PASSWORD` and `OPENCODE_SERVER_USERNAME` environment variables. +---@field auth? opencode.auth.Opts +--- ---Contexts to inject into prompts, keyed by their placeholder. ---@field contexts? table --- @@ -38,6 +46,10 @@ vim.g.opencode_opts = vim.g.opencode_opts ---Provide an integrated `opencode` when one is not found. ---@field provider? opencode.Provider|opencode.provider.Opts +---@class opencode.auth.Opts +---@field username? string The username for HTTP basic auth. Defaults to `opencode` if password is set. +---@field password? string The password for HTTP basic auth. + ---@class opencode.Prompt : opencode.api.prompt.Opts ---@field prompt string The prompt to send to `opencode`. ---@field ask? boolean Call `ask(prompt)` instead of `prompt(prompt)`. Useful for prompts that expect additional user input. @@ -45,6 +57,8 @@ vim.g.opencode_opts = vim.g.opencode_opts ---@type opencode.Opts local defaults = { port = nil, + hostname = "127.0.0.1", + auth = nil, -- Will check env vars if not set -- stylua: ignore contexts = { ["@this"] = function(context) return context:this() end, From 9dedea167508f3069e72e8980cd64f903583252a Mon Sep 17 00:00:00 2001 From: Emmanuel Roux <15956441+fesaille@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:51:13 +0100 Subject: [PATCH 2/3] refactor(cli/client): move computed opts at runtime to config --- lua/opencode/cli/client.lua | 47 +++++++------------------------------ lua/opencode/config.lua | 13 +++++++++- 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index 5d1b9628..77c7ba3a 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -3,6 +3,8 @@ --- - [implementation](https://github.com/sst/opencode/blob/dev/packages/opencode/src/server/server.ts) local M = {} +local config = require("opencode.config") + local sse_state = { -- Track the port - `opencode` may have restarted, usually on a new port port = nil, @@ -10,39 +12,6 @@ local sse_state = { job_id = nil, } ----Get authentication credentials from config or environment variables. ----@return { username: string, password: string }|nil -local function get_auth() - local config = require("opencode.config") - local auth = config.opts.auth - - -- Check config first - if auth and auth.password then - return { - username = auth.username or "opencode", - password = auth.password, - } - end - - -- Fall back to environment variables - local env_password = vim.env.OPENCODE_SERVER_PASSWORD - if env_password and env_password ~= "" then - return { - username = vim.env.OPENCODE_SERVER_USERNAME or "opencode", - password = env_password, - } - end - - return nil -end - ----Get the configured hostname for the opencode server. ----@return string -local function get_hostname() - local config = require("opencode.config") - return config.opts.hostname or "127.0.0.1" -end - ---Generate a UUID v4 (cross-platform, no external dependencies) ---@return string UUID in format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx local function generate_uuid() @@ -104,8 +73,8 @@ local function curl(url, method, body, callback) } -- Add basic auth if credentials are available - local auth = get_auth() - if auth then + local auth = config.opts.auth + if auth and auth.username and auth.password and auth.password ~= "" then table.insert(command, "-u") table.insert(command, auth.username .. ":" .. auth.password) end @@ -191,7 +160,7 @@ end ---@param callback fun(response: table)|nil ---@return number job_id function M.call(port, path, method, body, callback) - local hostname = get_hostname() + local hostname = config.opts.hostname return curl("http://" .. hostname .. ":" .. port .. path, method, body, callback) end @@ -301,7 +270,7 @@ end function M.get_path(port) -- Query each port synchronously for working directory -- TODO: Migrate to align with async paradigm used elsewhere - local hostname = get_hostname() + local hostname = config.opts.hostname local curl_cmd = { "curl", "-s", @@ -310,8 +279,8 @@ function M.get_path(port) } -- Add basic auth if credentials are available - local auth = get_auth() - if auth then + local auth = config.opts.auth + if auth and auth.username and auth.password and auth.password ~= "" then table.insert(curl_cmd, "-u") table.insert(curl_cmd, auth.username .. ":" .. auth.password) end diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index a4a2112e..8b5f7034 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -58,7 +58,10 @@ vim.g.opencode_opts = vim.g.opencode_opts local defaults = { port = nil, hostname = "127.0.0.1", - auth = nil, -- Will check env vars if not set + auth = vim.env.OPENCODE_SERVER_PASSWORD and { + username = vim.env.OPENCODE_SERVER_USERNAME or "opencode", + password = vim.env.OPENCODE_SERVER_PASSWORD, + } or nil, -- stylua: ignore contexts = { ["@this"] = function(context) return context:this() end, @@ -184,6 +187,14 @@ local defaults = { ---@type opencode.Opts M.opts = vim.tbl_deep_extend("force", vim.deepcopy(defaults), vim.g.opencode_opts or {}) +if M.opts.auth then + if not M.opts.auth.password or M.opts.auth.password == "" then + M.opts.auth = nil + elseif not M.opts.auth.username then + M.opts.auth.username = "opencode" + end +end + -- Allow removing default `contexts` and `prompts` by setting them to `false` in your user config. -- TODO: Add to type definition, and apply to `opts.select.commands`. local user_opts = vim.g.opencode_opts or {} From 866e9b1d820959dca1d6887f5ac2e7d83de02ec1 Mon Sep 17 00:00:00 2001 From: Emmanuel Roux <15956441+fesaille@users.noreply.github.com> Date: Tue, 3 Feb 2026 06:40:01 +0100 Subject: [PATCH 3/3] refactor(cli/client): inline require calls to opencode.config --- lua/opencode/cli/client.lua | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index 77c7ba3a..486c29fc 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -3,8 +3,6 @@ --- - [implementation](https://github.com/sst/opencode/blob/dev/packages/opencode/src/server/server.ts) local M = {} -local config = require("opencode.config") - local sse_state = { -- Track the port - `opencode` may have restarted, usually on a new port port = nil, @@ -50,6 +48,23 @@ local function generate_uuid() ) end +---Resolve the full URL for a given port and path, ensuring a valid hostname. +---@param port number +---@param path string +---@return string +local function resolve_server_url(port, path) + local config = require("opencode.config") + local hostname = config.opts.hostname + -- fallback to localhost as config is basically not validated + if not hostname or hostname == "" then + hostname = "localhost" + end + if not vim.startswith(path, "/") then + path = "/" .. path + end + return string.format("http://%s:%d%s", hostname, port, path) +end + ---@param url string ---@param method string ---@param body table|nil @@ -73,7 +88,7 @@ local function curl(url, method, body, callback) } -- Add basic auth if credentials are available - local auth = config.opts.auth + local auth = require("opencode.config").opts.auth if auth and auth.username and auth.password and auth.password ~= "" then table.insert(command, "-u") table.insert(command, auth.username .. ":" .. auth.password) @@ -160,8 +175,8 @@ end ---@param callback fun(response: table)|nil ---@return number job_id function M.call(port, path, method, body, callback) - local hostname = config.opts.hostname - return curl("http://" .. hostname .. ":" .. port .. path, method, body, callback) + local url = resolve_server_url(port, path) + return curl(url, method, body, callback) end ---@param text string @@ -270,7 +285,6 @@ end function M.get_path(port) -- Query each port synchronously for working directory -- TODO: Migrate to align with async paradigm used elsewhere - local hostname = config.opts.hostname local curl_cmd = { "curl", "-s", @@ -278,6 +292,8 @@ function M.get_path(port) "1", } + local config = require("opencode.config") + -- Add basic auth if credentials are available local auth = config.opts.auth if auth and auth.username and auth.password and auth.password ~= "" then @@ -285,7 +301,8 @@ function M.get_path(port) table.insert(curl_cmd, auth.username .. ":" .. auth.password) end - table.insert(curl_cmd, "http://" .. hostname .. ":" .. port .. "/path") + local url = resolve_server_url(port, "/path") + table.insert(curl_cmd, url) local curl_result = vim.system(curl_cmd):wait() require("opencode.util").check_system_call(curl_result, "curl")