diff --git a/src/cli.ts b/src/cli.ts index 98244be0..f67775c2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -41,6 +41,7 @@ import { import { renderSplash } from "./cli/splash.js"; import { isFirstRun, readPrefs, resetPrefs, writePrefs } from "./cli/preferences.js"; import { runOnboarding } from "./cli/onboarding.js"; +import { applyPortFlag, renderRuntimeIiiConfig } from "./cli/runtime-ports.js"; import { setBootVerbose } from "./logger.js"; import { VERSION } from "./version.js"; @@ -180,10 +181,7 @@ if (toolsIdx !== -1 && args[toolsIdx + 1]) { process.env["AGENTMEMORY_TOOLS"] = args[toolsIdx + 1]; } -const portIdx = args.indexOf("--port"); -if (portIdx !== -1 && args[portIdx + 1]) { - process.env["III_REST_PORT"] = args[portIdx + 1]; -} +applyPortFlag(args); const skipEngine = args.includes("--no-engine"); @@ -237,11 +235,13 @@ function getViewerUrl(): string { try { const u = new URL(getBaseUrl()); const vPort = + parseInt(process.env["AGENTMEMORY_VIEWER_PORT"] || "", 10) || parseInt(process.env["III_VIEWER_PORT"] || "", 10) || (parseInt(u.port || "3111", 10) || 3111) + 2; return `${u.protocol}//${u.hostname}:${vPort}`; } catch { const vPort = + parseInt(process.env["AGENTMEMORY_VIEWER_PORT"] || "", 10) || parseInt(process.env["III_VIEWER_PORT"] || "", 10) || getRestPort() + 2; return `http://localhost:${vPort}`; @@ -267,6 +267,8 @@ function getStreamPort(): number { function getEnginePort(): number { const explicit = parseInt(process.env["III_ENGINE_PORT"] || "", 10); if (explicit) return explicit; + const iiiPort = parseInt(process.env["III_PORT"] || "", 10); + if (iiiPort) return iiiPort; const url = process.env["III_ENGINE_URL"]; if (url) { try { @@ -330,6 +332,19 @@ function findIiiConfig(): string { return ""; } +function prepareRuntimeIiiConfig(configPath: string): string { + if (!configPath) return configPath; + const raw = readFileSync(configPath, "utf-8"); + const rendered = renderRuntimeIiiConfig(raw); + if (rendered === raw) return configPath; + + const dir = join(homedir(), ".agentmemory"); + mkdirSync(dir, { recursive: true }); + const target = join(dir, "iii-runtime-config.yaml"); + writeFileSync(target, rendered); + return target; +} + function whichBinary(name: string): string | null { const cmd = IS_WINDOWS ? "where" : "which"; try { @@ -830,7 +845,7 @@ function pickCompatibleIii(candidates: Array): string } async function startEngine(): Promise { - const configPath = findIiiConfig(); + const configPath = prepareRuntimeIiiConfig(findIiiConfig()); const pathIii = whichBinary("iii"); vlog(`iii binary: ${pathIii ?? "(not on PATH)"}, config: ${configPath || "(not found)"}`); diff --git a/src/cli/runtime-ports.ts b/src/cli/runtime-ports.ts new file mode 100644 index 00000000..faed9213 --- /dev/null +++ b/src/cli/runtime-ports.ts @@ -0,0 +1,95 @@ +const DEFAULT_REST_PORT = 3111; + +function parsePort(value: string | undefined): number | null { + if (!value) return null; + if (!/^\d+$/.test(value)) return null; + const port = Number(value); + if (!Number.isInteger(port) || port < 1 || port > 65535) return null; + return port; +} + +function setIfUnset(env: NodeJS.ProcessEnv, key: string, value: number | string): void { + if (!env[key]) env[key] = String(value); +} + +export function configuredRuntimePorts(env: NodeJS.ProcessEnv = process.env): { + restPort: number; + streamPort: number; + enginePort: number; +} { + const restPort = parsePort(env["III_REST_PORT"]) ?? DEFAULT_REST_PORT; + return { + restPort, + streamPort: + parsePort(env["III_STREAMS_PORT"]) ?? + parsePort(env["III_STREAM_PORT"]) ?? + restPort + 1, + enginePort: + parsePort(env["III_ENGINE_PORT"]) ?? + parsePort(env["III_PORT"]) ?? + (() => { + try { + const port = new URL(env["III_ENGINE_URL"] || "").port; + return parsePort(port) ?? restPort + 3; + } catch { + return restPort + 3; + } + })(), + }; +} + +export function applyPortFlag(args: string[], env: NodeJS.ProcessEnv = process.env): void { + const portIdx = args.indexOf("--port"); + if (portIdx === -1 || !args[portIdx + 1]) return; + + const restPort = parsePort(args[portIdx + 1]); + if (!restPort) return; + + if (restPort === DEFAULT_REST_PORT) return; + + const streamPort = restPort + 1; + const viewerPort = restPort + 2; + const enginePort = restPort + 3; + if (enginePort > 65535) return; + + env["III_REST_PORT"] = String(restPort); + setIfUnset(env, "III_STREAMS_PORT", streamPort); + setIfUnset(env, "III_STREAM_PORT", streamPort); + setIfUnset(env, "AGENTMEMORY_VIEWER_PORT", viewerPort); + setIfUnset(env, "III_VIEWER_PORT", viewerPort); + setIfUnset(env, "III_PORT", enginePort); + setIfUnset(env, "III_ENGINE_PORT", enginePort); + setIfUnset(env, "III_ENGINE_URL", `ws://localhost:${enginePort}`); +} + +export function renderRuntimeIiiConfig( + config: string, + env: NodeJS.ProcessEnv = process.env, +): string { + const { restPort, streamPort, enginePort } = configuredRuntimePorts(env); + let currentWorker = ""; + let sawTopLevelPort = false; + + const lines = config.split(/\r?\n/).map((line) => { + const worker = line.match(/^\s*-\s+name:\s*([A-Za-z0-9_-]+)\s*$/); + if (worker) currentWorker = worker[1]; + + if (/^port:\s*\d+\s*$/.test(line)) { + sawTopLevelPort = true; + return `port: ${enginePort}`; + } + + if (/^\s+port:\s*\d+\s*$/.test(line)) { + if (currentWorker === "iii-http") { + return line.replace(/\d+/, String(restPort)); + } + if (currentWorker === "iii-stream") { + return line.replace(/\d+/, String(streamPort)); + } + } + return line; + }); + + if (!sawTopLevelPort) lines.unshift(`port: ${enginePort}`, ""); + return lines.join("\n"); +} diff --git a/src/config.ts b/src/config.ts index 1fe70465..dc844121 100644 --- a/src/config.ts +++ b/src/config.ts @@ -162,7 +162,9 @@ export function loadConfig(): AgentMemoryConfig { const provider = detectProvider(env); return { - engineUrl: env["III_ENGINE_URL"] || "ws://localhost:49134", + engineUrl: + env["III_ENGINE_URL"] || + `ws://localhost:${env["III_ENGINE_PORT"] || env["III_PORT"] || "49134"}`, restPort: parseInt(env["III_REST_PORT"] || "3111", 10) || 3111, streamsPort: parseInt(env["III_STREAMS_PORT"] || "3112", 10) || 3112, provider, diff --git a/src/index.ts b/src/index.ts index d20d4693..7d90e736 100644 --- a/src/index.ts +++ b/src/index.ts @@ -522,7 +522,10 @@ async function main() { `MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`, ); - const viewerPort = config.restPort + 2; + const viewerPort = + parseInt(process.env.AGENTMEMORY_VIEWER_PORT || "", 10) || + parseInt(process.env.III_VIEWER_PORT || "", 10) || + config.restPort + 2; const viewerServer = startViewerServer( viewerPort, kv, diff --git a/test/runtime-ports.test.ts b/test/runtime-ports.test.ts new file mode 100644 index 00000000..3ee6a730 --- /dev/null +++ b/test/runtime-ports.test.ts @@ -0,0 +1,61 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { applyPortFlag, renderRuntimeIiiConfig } from "../src/cli/runtime-ports.js"; + +describe("runtime port derivation (#750)", () => { + it("derives sibling ports when --port targets a non-default instance", () => { + const env: NodeJS.ProcessEnv = {}; + applyPortFlag(["--port", "3211"], env); + + expect(env.III_REST_PORT).toBe("3211"); + expect(env.III_STREAMS_PORT).toBe("3212"); + expect(env.III_STREAM_PORT).toBe("3212"); + expect(env.AGENTMEMORY_VIEWER_PORT).toBe("3213"); + expect(env.III_VIEWER_PORT).toBe("3213"); + expect(env.III_PORT).toBe("3214"); + expect(env.III_ENGINE_PORT).toBe("3214"); + expect(env.III_ENGINE_URL).toBe("ws://localhost:3214"); + }); + + it("respects explicit sibling port overrides", () => { + const env: NodeJS.ProcessEnv = { + III_STREAMS_PORT: "4300", + III_PORT: "49000", + III_ENGINE_URL: "ws://127.0.0.1:49000", + AGENTMEMORY_VIEWER_PORT: "4400", + }; + applyPortFlag(["--port", "3211"], env); + + expect(env.III_REST_PORT).toBe("3211"); + expect(env.III_STREAMS_PORT).toBe("4300"); + expect(env.III_PORT).toBe("49000"); + expect(env.III_ENGINE_URL).toBe("ws://127.0.0.1:49000"); + expect(env.AGENTMEMORY_VIEWER_PORT).toBe("4400"); + }); + + it("ignores --port values that would overflow derived sibling ports", () => { + const env: NodeJS.ProcessEnv = {}; + applyPortFlag(["--port", "65533"], env); + + expect(env.III_REST_PORT).toBeUndefined(); + expect(env.III_STREAMS_PORT).toBeUndefined(); + expect(env.AGENTMEMORY_VIEWER_PORT).toBeUndefined(); + expect(env.III_ENGINE_PORT).toBeUndefined(); + expect(env.III_ENGINE_URL).toBeUndefined(); + }); + + it("renders a runtime iii config with derived ports without changing bundled defaults", () => { + const nativeConfig = readFileSync("iii-config.yaml", "utf-8"); + const rendered = renderRuntimeIiiConfig(nativeConfig, { + III_REST_PORT: "3211", + III_STREAMS_PORT: "3212", + III_PORT: "3214", + }); + + expect(nativeConfig).toContain("port: 3111"); + expect(nativeConfig).toContain("port: 3112"); + expect(rendered).toContain("port: 3214"); + expect(rendered).toContain("port: 3211"); + expect(rendered).toContain("port: 3212"); + }); +});