// Where the app is, and the only place that knows. // // ── Why this file is .mjs ── // // Every process is bun except `officer-pty`, which pm2 launches with node (see // ecosystem.config.cjs). Node cannot import TypeScript, so a .ts module here would have left the pty // sidecar with its own copy — which is exactly the thing this file exists to end. Plain JS is // importable by both, and `allowJs` in tsconfig.json means the TS callers still get types. // // ── Why there is no default ── // // There were twenty-two, and they disagreed: 5000 in the app and nineteen sidecars, 9010 in // user-instance.ts, 9000 in .env.example. Each was defensible where it was written and none was // visible from the others. // // A default is a guess at a value that .env always supplies. The one case it covers — PORT genuinely // unset — is not a machine anyone wants running: it means .env was not loaded, so POSTGRES_URL is // missing too and nothing works anyway. What a default buys there is a process that starts, binds // somewhere unexpected and fails later for a reason that does not name the cause. // // So: no default anywhere, and this throws. Same posture as jwt.ts with JWT_SECRET. const raw = process.env.PORT; if (!raw) { throw new Error( [ 'PORT is not set.', '', 'Officer reads it from .env, which bun auto-loads from the working directory. If this is a pm2', 'process, the `cwd` pin in ecosystem.profile.cjs is what puts it there — a PORT this empty', 'usually means .env was never loaded at all, and POSTGRES_URL is missing too.', '', 'Set PORT in .env (officer-setup writes 9000), or start from the repo root.', ].join('\n'), ); } const parsed = Number(raw); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) { throw new Error(`PORT must be an integer between 1 and 65535, but it is ${JSON.stringify(raw)}`); } /** The port the app binds. Everything else addresses it; nothing else binds. */ export const PORT = parsed; // The app serves HTTP and WebSocket on ONE listener, so these are the same port in two protocols. // Sidecars append their own path — `/api/sidecar/register` for the registration socket. // // The env overrides are kept because they are explicit escape hatches rather than defaults: nothing // sets either today, and a value that is present is a deliberate act rather than a guess. export const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${PORT}`; export const OFFICER_API_URL = process.env.OFFICER_API_URL ?? `http://127.0.0.1:${PORT}`; // ── The Anthropic proxy: PORT + 1, derived, not configured ── // // This is the ONE sidecar that binds a fixed port. Every other one binds `port: 0`, lets the kernel // choose, and reports what it got back over the registration socket — which works because their // consumer is the platform. // // The proxy cannot do that. Its consumer is `claude`, spawned by a DIFFERENT pm2 process // (officer-agent), which needs ANTHROPIC_BASE_URL at spawn time and has no channel to ask the proxy // what port it landed on. Two independent processes with nothing between them have to agree in // advance, so the number has to be predictable rather than discovered. // // It was ANTHROPIC_PROXY_PORT, defaulting to 5051 in four separate files: the one that binds it and // three that guessed the same constant to find it. 5051 was chosen against nothing and could collide // with anything the owner installs later, with the symptom being chat failing while the rest of the // platform looked healthy. // // PORT + 1 keeps the predictability and removes both problems. There is no variable to set, no second // number to keep in agreement with the first, and the pair moves together when the install moves. export const ANTHROPIC_PROXY_PORT = PORT + 1; export const ANTHROPIC_PROXY_URL = `http://127.0.0.1:${ANTHROPIC_PROXY_PORT}`;