diff --git a/CHANGELOG.md b/CHANGELOG.md index 361be2528..200ccc412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Features +- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app. + ### Fixes - Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm. diff --git a/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift b/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift index e0b3532ad..e9a262010 100644 --- a/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift +++ b/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift @@ -106,4 +106,20 @@ enum ClawdisConfigFile { return remote["password"] as? String } + static func gatewayPort() -> Int? { + let root = self.loadDict() + guard let gateway = root["gateway"] as? [String: Any] else { return nil } + if let port = gateway["port"] as? Int, port > 0 { return port } + if let number = gateway["port"] as? NSNumber, number.intValue > 0 { + return number.intValue + } + if let raw = gateway["port"] as? String, + let parsed = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)), + parsed > 0 + { + return parsed + } + return nil + } + } diff --git a/apps/macos/Sources/Clawdis/ControlChannel.swift b/apps/macos/Sources/Clawdis/ControlChannel.swift index e01cd2b1b..0c5c20bd5 100644 --- a/apps/macos/Sources/Clawdis/ControlChannel.swift +++ b/apps/macos/Sources/Clawdis/ControlChannel.swift @@ -185,7 +185,7 @@ final class ControlChannel { "Reason: \(reason)" } - // Common misfire: we connected to localhost:18789 but the port is occupied + // Common misfire: we connected to the configured localhost port but it is occupied // by some other process (e.g. a local dev gateway or a stuck SSH forward). // The gateway handshake returns something we can't parse, which currently // surfaces as "hello failed (unexpected response)". Give the user a pointer diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index 037a17855..2e2887665 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -306,7 +306,7 @@ struct DebugSettings: View { } if self.portReports.isEmpty, !self.portCheckInFlight { - Text("Check which process owns 18789 and suggest fixes.") + Text("Check which process owns \(GatewayEnvironment.gatewayPort()) and suggest fixes.") .font(.caption2) .foregroundStyle(.secondary) } else { @@ -946,7 +946,7 @@ extension DebugSettings { view.portCheckInFlight = true view.portReports = [ DebugActions.PortReport( - port: 18789, + port: GatewayEnvironment.gatewayPort(), expected: "Gateway websocket (node/tsx)", status: .missing("Missing"), listeners: []), diff --git a/apps/macos/Sources/Clawdis/GatewayEnvironment.swift b/apps/macos/Sources/Clawdis/GatewayEnvironment.swift index d3196439e..67d9b3572 100644 --- a/apps/macos/Sources/Clawdis/GatewayEnvironment.swift +++ b/apps/macos/Sources/Clawdis/GatewayEnvironment.swift @@ -72,6 +72,13 @@ enum GatewayEnvironment { } static func gatewayPort() -> Int { + if let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PORT"] { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + if let parsed = Int(trimmed), parsed > 0 { return parsed } + } + if let configPort = ClawdisConfigFile.gatewayPort(), configPort > 0 { + return configPort + } let stored = UserDefaults.standard.integer(forKey: "gatewayPort") return stored > 0 ? stored : 18789 } diff --git a/apps/macos/Sources/Clawdis/HealthStore.swift b/apps/macos/Sources/Clawdis/HealthStore.swift index 117cde4e7..dee277f74 100644 --- a/apps/macos/Sources/Clawdis/HealthStore.swift +++ b/apps/macos/Sources/Clawdis/HealthStore.swift @@ -188,7 +188,8 @@ final class HealthStore { if let error = self.lastError, !error.isEmpty { let lower = error.lowercased() if lower.contains("connection refused") { - return "The gateway control port (127.0.0.1:18789) isn’t listening — restart Clawdis to bring it back." + let port = GatewayEnvironment.gatewayPort() + return "The gateway control port (127.0.0.1:\(port)) isn’t listening — restart Clawdis to bring it back." } if lower.contains("timeout") { return "Timed out waiting for the control server; the gateway may be crashed or still starting." diff --git a/apps/macos/Sources/Clawdis/OnboardingView+Testing.swift b/apps/macos/Sources/Clawdis/OnboardingView+Testing.swift index 002b7c123..fa515e697 100644 --- a/apps/macos/Sources/Clawdis/OnboardingView+Testing.swift +++ b/apps/macos/Sources/Clawdis/OnboardingView+Testing.swift @@ -24,7 +24,7 @@ extension OnboardingView { discoveryModel: discovery) view.needsBootstrap = true view.localGatewayProbe = LocalGatewayProbe( - port: 18789, + port: GatewayEnvironment.gatewayPort(), pid: 123, command: "clawdis-gateway", expected: true) diff --git a/apps/macos/Sources/Clawdis/PortGuardian.swift b/apps/macos/Sources/Clawdis/PortGuardian.swift index 26535b260..dae5328ef 100644 --- a/apps/macos/Sources/Clawdis/PortGuardian.swift +++ b/apps/macos/Sources/Clawdis/PortGuardian.swift @@ -42,7 +42,7 @@ actor PortGuardian { self.logger.info("port sweep skipped (mode=unconfigured)") return } - let ports = [18789] + let ports = [GatewayEnvironment.gatewayPort()] for port in ports { let listeners = await self.listeners(on: port) guard !listeners.isEmpty else { continue } @@ -148,7 +148,7 @@ actor PortGuardian { if mode == .unconfigured { return [] } - let ports = [18789] + let ports = [GatewayEnvironment.gatewayPort()] var reports: [PortReport] = [] for port in ports { @@ -279,7 +279,8 @@ actor PortGuardian { return .init(port: port, expected: expectedDesc, status: .missing(text), listeners: []) } - let tunnelUnhealthy = mode == .remote && port == 18789 && tunnelHealthy == false + let tunnelUnhealthy = + mode == .remote && port == GatewayEnvironment.gatewayPort() && tunnelHealthy == false let reportListeners = listeners.map { listener in var expected = okPredicate(listener) if tunnelUnhealthy, expected { expected = false } @@ -347,7 +348,7 @@ actor PortGuardian { switch mode { case .remote: // Remote mode expects an SSH tunnel for the gateway WebSocket port. - if port == 18789 { return cmd.contains("ssh") } + if port == GatewayEnvironment.gatewayPort() { return cmd.contains("ssh") } return false case .local: return expectedCommands.contains { cmd.contains($0) } @@ -361,7 +362,7 @@ actor PortGuardian { mode: AppState.ConnectionMode, listeners: [Listener]) async -> Bool? { - guard mode == .remote, port == 18789, !listeners.isEmpty else { return nil } + guard mode == .remote, port == GatewayEnvironment.gatewayPort(), !listeners.isEmpty else { return nil } let hasSsh = listeners.contains { $0.command.lowercased().contains("ssh") } guard hasSsh else { return nil } return await self.probeGatewayHealth(port: port) diff --git a/apps/macos/Sources/Clawdis/RemoteTunnelManager.swift b/apps/macos/Sources/Clawdis/RemoteTunnelManager.swift index 9351b57a4..b5287a6b4 100644 --- a/apps/macos/Sources/Clawdis/RemoteTunnelManager.swift +++ b/apps/macos/Sources/Clawdis/RemoteTunnelManager.swift @@ -38,7 +38,7 @@ actor RemoteTunnelManager { } /// Ensure an SSH tunnel is running for the gateway control port. - /// Returns the local forwarded port (usually 18789). + /// Returns the local forwarded port (usually the configured gateway port). func ensureControlTunnel() async throws -> UInt16 { let settings = CommandResolver.connectionSettings() guard settings.mode == .remote else { diff --git a/src/cli/gateway-cli.ts b/src/cli/gateway-cli.ts index 7d68b7998..8fee5c421 100644 --- a/src/cli/gateway-cli.ts +++ b/src/cli/gateway-cli.ts @@ -1,7 +1,11 @@ import fs from "node:fs"; import type { Command } from "commander"; -import { CONFIG_PATH_CLAWDIS, loadConfig } from "../config/config.js"; +import { + CONFIG_PATH_CLAWDIS, + loadConfig, + resolveGatewayPort, +} from "../config/config.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { startGatewayServer } from "../gateway/server.js"; import { @@ -27,6 +31,13 @@ const gatewayLog = createSubsystemLogger("gateway"); type GatewayRunSignalAction = "stop" | "restart"; +function parsePort(raw: unknown): number | null { + if (raw === undefined || raw === null) return null; + const parsed = Number.parseInt(String(raw), 10); + if (!Number.isFinite(parsed) || parsed <= 0) return null; + return parsed; +} + async function runGatewayLoop(params: { start: () => Promise>>; runtime: typeof defaultRuntime; @@ -186,8 +197,15 @@ export function registerGatewayCli(program: Command) { } setGatewayWsLogStyle(wsLogStyle); - const port = Number.parseInt(String(opts.port ?? "18789"), 10); - if (Number.isNaN(port) || port <= 0) { + const cfg = loadConfig(); + const portOverride = parsePort(opts.port); + if (opts.port !== undefined && portOverride === null) { + defaultRuntime.error("Invalid port"); + defaultRuntime.exit(1); + return; + } + const port = portOverride ?? resolveGatewayPort(cfg); + if (!Number.isFinite(port) || port <= 0) { defaultRuntime.error("Invalid port"); defaultRuntime.exit(1); return; @@ -219,7 +237,6 @@ export function registerGatewayCli(program: Command) { defaultRuntime.exit(1); return; } - const cfg = loadConfig(); const bindRaw = String(opts.bind ?? cfg.gateway?.bind ?? "loopback"); const bind = bindRaw === "loopback" || @@ -335,8 +352,14 @@ export function registerGatewayCli(program: Command) { } setGatewayWsLogStyle(wsLogStyle); - const port = Number.parseInt(String(opts.port ?? "18789"), 10); - if (Number.isNaN(port) || port <= 0) { + const cfg = loadConfig(); + const portOverride = parsePort(opts.port); + if (opts.port !== undefined && portOverride === null) { + defaultRuntime.error("Invalid port"); + defaultRuntime.exit(1); + } + const port = portOverride ?? resolveGatewayPort(cfg); + if (!Number.isFinite(port) || port <= 0) { defaultRuntime.error("Invalid port"); defaultRuntime.exit(1); } @@ -400,7 +423,6 @@ export function registerGatewayCli(program: Command) { defaultRuntime.exit(1); return; } - const cfg = loadConfig(); const configExists = fs.existsSync(CONFIG_PATH_CLAWDIS); const mode = cfg.gateway?.mode; if (!opts.allowUnconfigured && mode !== "local") { diff --git a/src/cli/program.ts b/src/cli/program.ts index e3bcd3a01..8e28c8f22 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -166,7 +166,7 @@ export function buildProgram() { .option("--mode ", "Wizard mode: local|remote") .option("--auth-choice ", "Auth: oauth|apiKey|minimax|skip") .option("--anthropic-api-key ", "Anthropic API key") - .option("--gateway-port ", "Gateway port", "18789") + .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|lan|tailnet|auto") .option("--gateway-auth ", "Gateway auth: off|token|password") .option("--gateway-token ", "Gateway token (token auth)") @@ -194,10 +194,10 @@ export function buildProgram() { | "skip" | undefined, anthropicApiKey: opts.anthropicApiKey as string | undefined, - gatewayPort: Number.parseInt( - String(opts.gatewayPort ?? "18789"), - 10, - ), + gatewayPort: + typeof opts.gatewayPort === "string" + ? Number.parseInt(opts.gatewayPort, 10) + : undefined, gatewayBind: opts.gatewayBind as | "loopback" | "lan" diff --git a/src/commands/configure.ts b/src/commands/configure.ts index 9ac9853a0..3d59e5efa 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -15,6 +15,7 @@ import type { ClawdisConfig } from "../config/config.js"; import { CONFIG_PATH_CLAWDIS, readConfigFileSnapshot, + resolveGatewayPort, writeConfigFile, } from "../config/config.js"; import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js"; @@ -74,7 +75,7 @@ async function promptGatewayConfig( const portRaw = guardCancel( await text({ message: "Gateway port", - initialValue: "18789", + initialValue: String(resolveGatewayPort(cfg)), validate: (value) => Number.isFinite(Number(value)) ? undefined : "Invalid port", }), @@ -205,6 +206,7 @@ async function promptGatewayConfig( gateway: { ...next.gateway, mode: "local", + port, bind, tailscale: { ...next.gateway?.tailscale, @@ -527,7 +529,7 @@ export async function runConfigureWizard( nextConfig.agent?.workspace ?? baseConfig.agent?.workspace ?? DEFAULT_WORKSPACE; - let gatewayPort = 18789; + let gatewayPort = resolveGatewayPort(baseConfig); let gatewayToken: string | undefined; if (selected.includes("workspace")) { diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 024be2646..3674631b0 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -34,6 +34,9 @@ export function summarizeExistingConfig(config: ClawdisConfig): string { rows.push(`workspace: ${config.agent.workspace}`); if (config.agent?.model) rows.push(`model: ${config.agent.model}`); if (config.gateway?.mode) rows.push(`gateway.mode: ${config.gateway.mode}`); + if (typeof config.gateway?.port === "number") { + rows.push(`gateway.port: ${config.gateway.port}`); + } if (config.gateway?.bind) rows.push(`gateway.bind: ${config.gateway.bind}`); if (config.gateway?.remote?.url) { rows.push(`gateway.remote.url: ${config.gateway.remote.url}`); diff --git a/src/commands/onboard-interactive.ts b/src/commands/onboard-interactive.ts index 9a2e2ce8d..dfc2c22a3 100644 --- a/src/commands/onboard-interactive.ts +++ b/src/commands/onboard-interactive.ts @@ -14,6 +14,7 @@ import type { ClawdisConfig } from "../config/config.js"; import { CONFIG_PATH_CLAWDIS, readConfigFileSnapshot, + resolveGatewayPort, writeConfigFile, } from "../config/config.js"; import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js"; @@ -118,7 +119,8 @@ export async function runInteractiveOnboarding( } } - const localUrl = "ws://127.0.0.1:18789"; + const localPort = resolveGatewayPort(baseConfig); + const localUrl = `ws://127.0.0.1:${localPort}`; const localProbe = await probeGatewayReachable({ url: localUrl, token: process.env.CLAWDIS_GATEWAY_TOKEN, @@ -315,7 +317,7 @@ export async function runInteractiveOnboarding( const portRaw = guardCancel( await text({ message: "Gateway port", - initialValue: "18789", + initialValue: String(localPort), validate: (value) => Number.isFinite(Number(value)) ? undefined : "Invalid port", }), @@ -457,6 +459,7 @@ export async function runInteractiveOnboarding( ...nextConfig, gateway: { ...nextConfig.gateway, + port, bind, tailscale: { ...nextConfig.gateway?.tailscale, diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index a06aa8cc6..4e7456104 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -4,6 +4,7 @@ import { type ClawdisConfig, CONFIG_PATH_CLAWDIS, readConfigFileSnapshot, + resolveGatewayPort, writeConfigFile, } from "../config/config.js"; import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js"; @@ -106,12 +107,18 @@ export async function runNonInteractiveOnboarding( return; } - const port = opts.gatewayPort ?? 18789; - if (!Number.isFinite(port) || port <= 0) { + const hasGatewayPort = opts.gatewayPort !== undefined; + if ( + hasGatewayPort && + (!Number.isFinite(opts.gatewayPort) || (opts.gatewayPort ?? 0) <= 0) + ) { runtime.error("Invalid --gateway-port"); runtime.exit(1); return; } + const port = hasGatewayPort + ? (opts.gatewayPort as number) + : resolveGatewayPort(baseConfig); let bind = opts.gatewayBind ?? "loopback"; let authMode = opts.gatewayAuth ?? "off"; const tailscaleMode = opts.tailscale ?? "off"; @@ -162,6 +169,7 @@ export async function runNonInteractiveOnboarding( ...nextConfig, gateway: { ...nextConfig.gateway, + port, bind, tailscale: { ...nextConfig.gateway?.tailscale, diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 659b32e8b..384f23682 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -390,6 +390,30 @@ describe("Nix integration (U3, U5, U9)", () => { }); }); + describe("U6: gateway port resolution", () => { + it("uses default when env and config are unset", async () => { + await withEnvOverride({ CLAWDIS_GATEWAY_PORT: undefined }, async () => { + const { DEFAULT_GATEWAY_PORT, resolveGatewayPort } = + await import("./config.js"); + expect(resolveGatewayPort({})).toBe(DEFAULT_GATEWAY_PORT); + }); + }); + + it("prefers CLAWDIS_GATEWAY_PORT over config", async () => { + await withEnvOverride({ CLAWDIS_GATEWAY_PORT: "19001" }, async () => { + const { resolveGatewayPort } = await import("./config.js"); + expect(resolveGatewayPort({ gateway: { port: 19002 } })).toBe(19001); + }); + }); + + it("falls back to config when env is invalid", async () => { + await withEnvOverride({ CLAWDIS_GATEWAY_PORT: "nope" }, async () => { + const { resolveGatewayPort } = await import("./config.js"); + expect(resolveGatewayPort({ gateway: { port: 19003 } })).toBe(19003); + }); + }); + }); + describe("U9: telegram.tokenFile schema validation", () => { it("accepts config with only botToken", async () => { await withTempHome(async (home) => { diff --git a/src/config/config.ts b/src/config/config.ts index dfa8c707a..8a774ae4a 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -438,6 +438,8 @@ export type GatewayRemoteConfig = { }; export type GatewayConfig = { + /** Single multiplexed port for Gateway WS + HTTP (default: 18789). */ + port?: number; /** * Explicit gateway mode. When set to "remote", local gateway start is disabled. * When set to "local", the CLI may start the gateway locally. @@ -642,6 +644,24 @@ export const CONFIG_PATH_CLAWDIS = process.env.CLAWDIS_CONFIG_PATH ?? path.join(STATE_DIR_CLAWDIS, "clawdis.json"); +export const DEFAULT_GATEWAY_PORT = 18789; + +export function resolveGatewayPort( + cfg?: ClawdisConfig, + env: NodeJS.ProcessEnv = process.env, +): number { + const envRaw = env.CLAWDIS_GATEWAY_PORT?.trim(); + if (envRaw) { + const parsed = Number.parseInt(envRaw, 10); + if (Number.isFinite(parsed) && parsed > 0) return parsed; + } + const configPort = cfg?.gateway?.port; + if (typeof configPort === "number" && Number.isFinite(configPort)) { + if (configPort > 0) return configPort; + } + return DEFAULT_GATEWAY_PORT; +} + const ModelApiSchema = z.union([ z.literal("openai-completions"), z.literal("openai-responses"), @@ -1217,6 +1237,7 @@ const ClawdisSchema = z.object({ .optional(), gateway: z .object({ + port: z.number().int().positive().optional(), mode: z.union([z.literal("local"), z.literal("remote")]).optional(), bind: z .union([ diff --git a/src/gateway/call.ts b/src/gateway/call.ts index 9f2989523..04a9f2497 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { loadConfig } from "../config/config.js"; +import { loadConfig, resolveGatewayPort } from "../config/config.js"; import { GatewayClient } from "./client.js"; import { PROTOCOL_VERSION } from "./protocol/index.js"; @@ -28,6 +28,7 @@ export async function callGateway( const isRemoteMode = config.gateway?.mode === "remote"; const remote = isRemoteMode ? config.gateway?.remote : undefined; const authToken = config.gateway?.auth?.token; + const localPort = resolveGatewayPort(config); const url = (typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() @@ -35,7 +36,7 @@ export async function callGateway( (typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : undefined) || - "ws://127.0.0.1:18789"; + `ws://127.0.0.1:${localPort}`; const token = (typeof opts.token === "string" && opts.token.trim().length > 0 ? opts.token.trim() diff --git a/src/hooks/gmail-ops.ts b/src/hooks/gmail-ops.ts index edc14194e..ed1649fc8 100644 --- a/src/hooks/gmail-ops.ts +++ b/src/hooks/gmail-ops.ts @@ -5,6 +5,7 @@ import { CONFIG_PATH_CLAWDIS, loadConfig, readConfigFileSnapshot, + resolveGatewayPort, validateConfigObject, writeConfigFile, } from "../config/config.js"; @@ -128,7 +129,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) { const hookUrl = opts.hookUrl ?? baseConfig.hooks?.gmail?.hookUrl ?? - buildDefaultHookUrl(hooksPath); + buildDefaultHookUrl(hooksPath, resolveGatewayPort(baseConfig)); const serveBind = opts.bind ?? DEFAULT_GMAIL_SERVE_BIND; const servePort = opts.port ?? DEFAULT_GMAIL_SERVE_PORT; diff --git a/src/hooks/gmail.test.ts b/src/hooks/gmail.test.ts index e496d81b4..fb8041b57 100644 --- a/src/hooks/gmail.test.ts +++ b/src/hooks/gmail.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { ClawdisConfig } from "../config/config.js"; +import { type ClawdisConfig, DEFAULT_GATEWAY_PORT } from "../config/config.js"; import { buildDefaultHookUrl, buildTopicPath, @@ -20,8 +20,8 @@ const baseConfig = { describe("gmail hook config", () => { it("builds default hook url", () => { - expect(buildDefaultHookUrl("/hooks")).toBe( - "http://127.0.0.1:18789/hooks/gmail", + expect(buildDefaultHookUrl("/hooks", DEFAULT_GATEWAY_PORT)).toBe( + `http://127.0.0.1:${DEFAULT_GATEWAY_PORT}/hooks/gmail`, ); }); @@ -41,7 +41,9 @@ describe("gmail hook config", () => { expect(result.value.label).toBe("INBOX"); expect(result.value.includeBody).toBe(true); expect(result.value.serve.port).toBe(8788); - expect(result.value.hookUrl).toBe("http://127.0.0.1:18789/hooks/gmail"); + expect(result.value.hookUrl).toBe( + `http://127.0.0.1:${DEFAULT_GATEWAY_PORT}/hooks/gmail`, + ); } }); diff --git a/src/hooks/gmail.ts b/src/hooks/gmail.ts index bb64412d0..96b62f092 100644 --- a/src/hooks/gmail.ts +++ b/src/hooks/gmail.ts @@ -1,8 +1,10 @@ import { randomBytes } from "node:crypto"; -import type { - ClawdisConfig, +import { + type ClawdisConfig, + DEFAULT_GATEWAY_PORT, HooksGmailTailscaleMode, + resolveGatewayPort, } from "../config/config.js"; export const DEFAULT_GMAIL_LABEL = "INBOX"; @@ -14,7 +16,6 @@ export const DEFAULT_GMAIL_SERVE_PATH = "/gmail-pubsub"; export const DEFAULT_GMAIL_MAX_BYTES = 20_000; export const DEFAULT_GMAIL_RENEW_MINUTES = 12 * 60; export const DEFAULT_HOOKS_PATH = "/hooks"; -export const DEFAULT_HOOKS_BASE_URL = "http://127.0.0.1:18789"; export type GmailHookOverrides = { account?: string; @@ -87,9 +88,13 @@ export function normalizeServePath(raw?: string): string { return withSlash.replace(/\/+$/, ""); } -export function buildDefaultHookUrl(hooksPath?: string): string { +export function buildDefaultHookUrl( + hooksPath?: string, + port: number = DEFAULT_GATEWAY_PORT, +): string { const basePath = normalizeHooksPath(hooksPath); - return joinUrl(DEFAULT_HOOKS_BASE_URL, `${basePath}/gmail`); + const baseUrl = `http://127.0.0.1:${port}`; + return joinUrl(baseUrl, `${basePath}/gmail`); } export function resolveGmailHookRuntimeConfig( @@ -122,7 +127,9 @@ export function resolveGmailHookRuntimeConfig( } const hookUrl = - overrides.hookUrl ?? gmail?.hookUrl ?? buildDefaultHookUrl(hooks?.path); + overrides.hookUrl ?? + gmail?.hookUrl ?? + buildDefaultHookUrl(hooks?.path, resolveGatewayPort(cfg)); const includeBody = overrides.includeBody ?? gmail?.includeBody ?? true; diff --git a/src/macos/gateway-daemon.ts b/src/macos/gateway-daemon.ts index 1c046b7b3..922243e05 100644 --- a/src/macos/gateway-daemon.ts +++ b/src/macos/gateway-daemon.ts @@ -61,15 +61,18 @@ async function main() { wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto"; setGatewayWsLogStyle(wsLogStyle); + const cfg = loadConfig(); const portRaw = - argValue(args, "--port") ?? process.env.CLAWDIS_GATEWAY_PORT ?? "18789"; + argValue(args, "--port") ?? + process.env.CLAWDIS_GATEWAY_PORT ?? + (typeof cfg.gateway?.port === "number" ? String(cfg.gateway.port) : "") ?? + "18789"; const port = Number.parseInt(portRaw, 10); if (Number.isNaN(port) || port <= 0) { defaultRuntime.error(`Invalid --port (${portRaw})`); process.exit(1); } - const cfg = loadConfig(); const bindRaw = argValue(args, "--bind") ?? process.env.CLAWDIS_GATEWAY_BIND ?? diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index 2ae0145c6..4d81f5d69 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { loadConfig } from "../config/config.js"; +import { loadConfig, resolveGatewayPort } from "../config/config.js"; import { GatewayClient } from "../gateway/client.js"; import { type HelloOk, @@ -183,6 +183,7 @@ export function resolveGatewayConnection(opts: GatewayConnectionOptions) { const remote = isRemoteMode ? config.gateway?.remote : undefined; const authToken = config.gateway?.auth?.token; + const localPort = resolveGatewayPort(config); const url = (typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() @@ -190,7 +191,7 @@ export function resolveGatewayConnection(opts: GatewayConnectionOptions) { (typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : undefined) || - "ws://127.0.0.1:18789"; + `ws://127.0.0.1:${localPort}`; const token = (typeof opts.token === "string" && opts.token.trim().length > 0