feat: auto-discover server port via ~/.browseros/server.json (#504)

* feat: auto-discover server port via ~/.browseros/server.json

Server writes its port to ~/.browseros/server.json on startup so the CLI
can auto-discover the server URL without requiring `browseros-cli init`.

Discovery chain: BROWSEROS_URL env > config.yaml > server.json > error

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback for PR #504

- Use synchronous unlinkSync in stop() since process.exit() fires
  immediately after, abandoning any pending async operations
- Wrap writeServerConfig in try/catch so a write failure doesn't crash
  a healthy server for a convenience feature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: type server discovery config and add version metadata

Add ServerDiscoveryConfig interface to @browseros/shared and enrich
server.json with server_version, browseros_version, and chromium_version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: normalize URL from server.json for consistency

All other URL sources (env var, config.yaml) pass through
normalizeServerURL; apply the same to the server.json path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nikhil
2026-03-20 11:37:00 -07:00
committed by GitHub
parent 2271277b4d
commit 9bc5e666c4
6 changed files with 99 additions and 3 deletions

View File

@@ -1,8 +1,10 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -170,11 +172,44 @@ func defaultServerURL() string {
}
cfg, err := config.Load()
if err == nil {
if url := normalizeServerURL(cfg.ServerURL); url != "" {
return url
}
}
if url := loadBrowserosServerURL(); url != "" {
return url
}
return ""
}
type serverDiscoveryConfig struct {
ServerPort int `json:"server_port"`
URL string `json:"url"`
ServerVersion string `json:"server_version"`
BrowserOSVersion string `json:"browseros_version,omitempty"`
ChromiumVersion string `json:"chromium_version,omitempty"`
}
func loadBrowserosServerURL() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return normalizeServerURL(cfg.ServerURL)
data, err := os.ReadFile(filepath.Join(home, ".browseros", "server.json"))
if err != nil {
return ""
}
var sc serverDiscoveryConfig
if err := json.Unmarshal(data, &sc); err != nil {
return ""
}
return normalizeServerURL(sc.URL)
}
func normalizeServerURL(raw string) string {

View File

@@ -1,7 +1,9 @@
import { mkdir, readdir, rm, stat } from 'node:fs/promises'
import { unlinkSync } from 'node:fs'
import { mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { PATHS } from '@browseros/shared/constants/paths'
import type { ServerDiscoveryConfig } from '@browseros/shared/types/server-config'
import { logger } from './logger'
export function getBrowserosDir(): string {
@@ -32,6 +34,24 @@ export function getBuiltinSkillsDir(): string {
return join(getSkillsDir(), PATHS.BUILTIN_DIR_NAME)
}
export function getServerConfigPath(): string {
return join(getBrowserosDir(), PATHS.SERVER_CONFIG_FILE_NAME)
}
export async function writeServerConfig(
config: ServerDiscoveryConfig,
): Promise<void> {
await writeFile(getServerConfigPath(), `${JSON.stringify(config, null, 2)}\n`)
}
export function removeServerConfigSync(): void {
try {
unlinkSync(getServerConfigPath())
} catch {
// File may not exist or already be removed
}
}
export async function ensureBrowserosDir(): Promise<void> {
await mkdir(getMemoryDir(), { recursive: true })
await mkdir(getSkillsDir(), { recursive: true })

View File

@@ -18,7 +18,12 @@ import { ControllerBackend } from './browser/backends/controller'
import { Browser } from './browser/browser'
import type { ServerConfig } from './config'
import { INLINED_ENV } from './env'
import { cleanOldSessions, ensureBrowserosDir } from './lib/browseros-dir'
import {
cleanOldSessions,
ensureBrowserosDir,
removeServerConfigSync,
writeServerConfig,
} from './lib/browseros-dir'
import { initializeDb } from './lib/db'
import { identity } from './lib/identity'
import { logger } from './lib/logger'
@@ -109,6 +114,20 @@ export class Application {
this.handleStartupError('HTTP server', this.config.serverPort, error)
}
try {
await writeServerConfig({
server_port: this.config.serverPort,
url: `http://127.0.0.1:${this.config.serverPort}`,
server_version: VERSION,
browseros_version: this.config.instanceBrowserosVersion,
chromium_version: this.config.instanceChromiumVersion,
})
} catch (error) {
logger.warn('Failed to write server config for auto-discovery', {
error: error instanceof Error ? error.message : String(error),
})
}
logger.info(
`HTTP server listening on http://127.0.0.1:${this.config.serverPort}`,
)
@@ -125,6 +144,7 @@ export class Application {
stop(reason?: string): void {
logger.info('Shutting down server...', { reason })
stopSkillSync()
removeServerConfigSync()
// Immediate exit without graceful shutdown. Chromium may kill us on update/restart,
// and we need to free the port instantly so the HTTP port doesn't keep switching.

View File

@@ -37,6 +37,10 @@
"types": "./src/types/logger.ts",
"default": "./src/types/logger.ts"
},
"./types/server-config": {
"types": "./src/types/server-config.ts",
"default": "./src/types/server-config.ts"
},
"./schemas/llm": {
"types": "./src/schemas/llm.ts",
"default": "./src/schemas/llm.ts"

View File

@@ -16,6 +16,7 @@ export const PATHS = {
CORE_MEMORY_FILE_NAME: 'CORE.md',
SKILLS_DIR_NAME: 'skills',
BUILTIN_DIR_NAME: 'builtin',
SERVER_CONFIG_FILE_NAME: 'server.json',
SOUL_MAX_LINES: 150,
MEMORY_RETENTION_DAYS: 30,
SESSION_RETENTION_DAYS: 30,

View File

@@ -0,0 +1,16 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Shape of ~/.browseros/server.json written by the server on startup.
* The CLI reads this file for auto-discovery of the server URL.
*/
export interface ServerDiscoveryConfig {
server_port: number
url: string
server_version: string
browseros_version?: string
chromium_version?: string
}