@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
66import { spawnSync } from 'node:child_process'
77import net from 'node:net'
88
9+ const DEFAULT_PORT = 38173
910const __filename = fileURLToPath ( import . meta. url )
1011const rootDir = dirname ( dirname ( __filename ) )
1112const webDistIndex = join ( rootDir , 'packages' , 'web' , 'dist' , 'index.html' )
@@ -35,10 +36,11 @@ Options:
3536
3637Examples:
3738 agentclick Start the server (auto-detects port)
38- PORT =4000 agentclick Start on a specific port
39+ AGENTCLICK_PORT =4000 agentclick Start on a specific port
3940
4041Environment:
41- PORT Server port (default: 38173, auto-fallback if busy)
42+ AGENTCLICK_PORT Preferred server port (default: 38173)
43+ PORT Backward-compatible server port override
4244 OPENCLAW_WEBHOOK Webhook URL for agent callbacks
4345` )
4446}
@@ -107,6 +109,28 @@ async function canListen(port) {
107109 } )
108110}
109111
112+ async function getClosestAvailablePort ( preferredPort ) {
113+ const nextPort = preferredPort + 1
114+ if ( await canListen ( nextPort ) ) return nextPort
115+ // Fallback to OS-assigned free port (no range scan).
116+ return await new Promise ( ( resolve , reject ) => {
117+ const server = net . createServer ( )
118+ server . once ( 'error' , reject )
119+ server . listen ( 0 , ( ) => {
120+ const address = server . address ( )
121+ if ( ! address || typeof address === 'string' ) {
122+ server . close ( ( ) => reject ( new Error ( 'Failed to resolve ephemeral port' ) ) )
123+ return
124+ }
125+ const freePort = address . port
126+ server . close ( err => {
127+ if ( err ) reject ( err )
128+ else resolve ( freePort )
129+ } )
130+ } )
131+ } )
132+ }
133+
110134async function isAgentClickServer ( port ) {
111135 const url = `http://localhost:${ port } /api/identity`
112136 const controller = new AbortController ( )
@@ -124,48 +148,23 @@ async function isAgentClickServer(port) {
124148}
125149
126150async function resolvePort ( ) {
127- const explicitPort = process . env . PORT ? Number ( process . env . PORT ) : null
128- if ( explicitPort && Number . isFinite ( explicitPort ) && explicitPort > 0 ) {
129- const free = await canListen ( explicitPort )
130- if ( free ) return String ( explicitPort )
131- if ( await isAgentClickServer ( explicitPort ) ) {
132- console . log ( `[agentclick] AgentClick already running at http://localhost:${ explicitPort } ; reusing existing server.` )
133- return null
134- }
135- console . log ( `[agentclick] Port ${ explicitPort } is occupied by another service; searching for fallback port...` )
136- let fallback = explicitPort + 1
137- while ( true ) {
138- const available = await canListen ( fallback )
139- if ( available ) {
140- console . log ( `[agentclick] Using fallback port ${ fallback } ` )
141- return String ( fallback )
142- }
143- if ( await isAgentClickServer ( fallback ) ) {
144- console . log ( `[agentclick] AgentClick already running at http://localhost:${ fallback } ; reusing existing server.` )
145- return null
146- }
147- fallback += 1
148- }
151+ const configuredPort = Number ( process . env . AGENTCLICK_PORT || process . env . PORT || DEFAULT_PORT )
152+ if ( ! Number . isFinite ( configuredPort ) || configuredPort <= 0 ) {
153+ console . error ( '[agentclick] Invalid AGENTCLICK_PORT/PORT configuration.' )
154+ process . exit ( 1 )
149155 }
150156
151- // Backward compatibility: if legacy default is running AgentClick, reuse it.
152- const legacyPort = 3001
153- if ( await isAgentClickServer ( legacyPort ) ) {
154- console . log ( `[agentclick] AgentClick already running at legacy default http://localhost:${ legacyPort } ; reusing existing server.` )
157+ const free = await canListen ( configuredPort )
158+ if ( free ) return String ( configuredPort )
159+
160+ if ( await isAgentClickServer ( configuredPort ) ) {
161+ console . log ( `[agentclick] AgentClick already running at http://localhost:${ configuredPort } ; reusing existing server.` )
155162 return null
156163 }
157164
158- let port = 38173
159- while ( true ) {
160- const available = await canListen ( port )
161- if ( available ) return String ( port )
162- if ( await isAgentClickServer ( port ) ) {
163- console . log ( `[agentclick] AgentClick already running at http://localhost:${ port } ; reusing existing server.` )
164- return null
165- }
166- console . log ( `[agentclick] Port ${ port } in use by another service, trying ${ port + 1 } ...` )
167- port += 1
168- }
165+ const fallbackPort = await getClosestAvailablePort ( configuredPort )
166+ console . log ( `[agentclick] Port ${ configuredPort } is occupied by another service. Starting AgentClick on ${ fallbackPort } .` )
167+ return String ( fallbackPort )
169168}
170169
171170const childEnv = { ...process . env }
@@ -174,6 +173,9 @@ if (!resolvedPort) {
174173 process . exit ( 0 )
175174}
176175childEnv . PORT = resolvedPort
176+ childEnv . AGENTCLICK_PORT = resolvedPort
177+ process . env . AGENTCLICK_PORT = resolvedPort
178+ console . log ( `[agentclick] Using AGENTCLICK_PORT=${ resolvedPort } ` )
177179
178180const result = spawnSync ( process . execPath , [ serverDistEntry ] , {
179181 cwd : rootDir ,
0 commit comments