Skip to content

Commit 0504394

Browse files
committed
feat: env-var-first port pipeline for agentclick runtime
1 parent 2824b13 commit 0504394

File tree

5 files changed

+99
-47
lines changed

5 files changed

+99
-47
lines changed

SKILL.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@ cd /Users/hm/.openclaw/workspace/AgentClick
1111
npm start
1212
```
1313

14-
Default server: `http://localhost:38173` (legacy deployments may still use `:3001`)
14+
Default server: `http://localhost:38173`
1515

16-
Containerized clients may use `http://host.docker.internal:38173` (or your overridden `PORT`).
16+
Containerized clients may use `http://host.docker.internal:38173` (or `AGENTCLICK_PORT` / `AGENTCLICK_URL` overrides).
17+
18+
## Port Pipeline
19+
20+
Use this runtime pipeline before review calls:
21+
22+
1. Read `AGENTCLICK_URL` (if set) and use it directly.
23+
2. Else read `AGENTCLICK_PORT` (fallback `PORT`, then `38173`) and target `http://localhost:<port>`.
24+
3. Verify identity with `GET /api/identity`.
25+
4. If identity check fails, start `agentclick` and set `AGENTCLICK_PORT` for the running process chain.
26+
5. Then create review sessions and keep an active `/wait` loop.
1727

1828
## Supported Review Types
1929

@@ -32,7 +42,7 @@ Canonical types accepted by `POST /api/review`:
3242
### 1) Create session
3343

3444
```bash
35-
curl -s -X POST http://localhost:38173/api/review \
45+
curl -s -X POST "${AGENTCLICK_URL:-http://localhost:${AGENTCLICK_PORT:-38173}}/api/review" \
3646
-H 'Content-Type: application/json' \
3747
-d '{
3848
"type": "action_approval",
@@ -50,7 +60,7 @@ Response:
5060
### 2) Wait for human decision (required active polling)
5161

5262
```bash
53-
curl -s "http://localhost:38173/api/sessions/<sessionId>/wait"
63+
curl -s "${AGENTCLICK_URL:-http://localhost:${AGENTCLICK_PORT:-38173}}/api/sessions/<sessionId>/wait"
5464
```
5565

5666
`/wait` blocks up to 5 minutes. Returns the session when status becomes `completed` or `rewriting`.
@@ -69,7 +79,7 @@ Important:
6979
### 4) Rewrite payload (when requested)
7080

7181
```bash
72-
curl -s -X PUT "http://localhost:38173/api/sessions/<sessionId>/payload" \
82+
curl -s -X PUT "${AGENTCLICK_URL:-http://localhost:${AGENTCLICK_PORT:-38173}}/api/sessions/<sessionId>/payload" \
7383
-H 'Content-Type: application/json' \
7484
-d '{ "payload": { "...": "updated" } }'
7585
```

bin/agentclick.mjs

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
66
import { spawnSync } from 'node:child_process'
77
import net from 'node:net'
88

9+
const DEFAULT_PORT = 38173
910
const __filename = fileURLToPath(import.meta.url)
1011
const rootDir = dirname(dirname(__filename))
1112
const webDistIndex = join(rootDir, 'packages', 'web', 'dist', 'index.html')
@@ -35,10 +36,11 @@ Options:
3536
3637
Examples:
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
4041
Environment:
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+
110134
async 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

126150
async 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

171170
const childEnv = { ...process.env }
@@ -174,6 +173,9 @@ if (!resolvedPort) {
174173
process.exit(0)
175174
}
176175
childEnv.PORT = resolvedPort
176+
childEnv.AGENTCLICK_PORT = resolvedPort
177+
process.env.AGENTCLICK_PORT = resolvedPort
178+
console.log(`[agentclick] Using AGENTCLICK_PORT=${resolvedPort}`)
177179

178180
const result = spawnSync(process.execPath, [serverDistEntry], {
179181
cwd: rootDir,

lib/agentclick.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const AGENTCLICK_PORT = process.env.AGENTCLICK_PORT || process.env.PORT || '38173';
2+
const AGENTCLICK_URL = process.env.AGENTCLICK_URL || `http://localhost:${AGENTCLICK_PORT}`;
3+
4+
/**
5+
* Send an action for human review and wait for approval.
6+
* @param {Object} options
7+
* @param {string} options.type - 'code_review' | 'email_review' | 'action_approval' | 'plan_review' | 'trajectory_review'
8+
* @param {string} options.sessionKey - OpenClaw session key
9+
* @param {Object} options.payload - Review-specific payload
10+
* @returns {Promise<Object>} User's decision
11+
*/
12+
async function reviewAndWait({ type, sessionKey, payload }) {
13+
// Create review session
14+
const res = await fetch(`${AGENTCLICK_URL}/api/review`, {
15+
method: 'POST',
16+
headers: { 'Content-Type': 'application/json' },
17+
body: JSON.stringify({ type, sessionKey, payload })
18+
});
19+
20+
if (!res.ok) {
21+
throw new Error(`AgentClick error: ${res.status} ${res.statusText}`);
22+
}
23+
24+
const { sessionId } = await res.json();
25+
console.log(`[agentclick] Waiting for review: ${sessionId}`);
26+
27+
// Long-poll for result (up to 5 min)
28+
const waitRes = await fetch(`${AGENTCLICK_URL}/api/sessions/${sessionId}/wait`);
29+
30+
if (!waitRes.ok) {
31+
throw new Error(`AgentClick wait failed: ${waitRes.status}`);
32+
}
33+
34+
const session = await waitRes.json();
35+
console.log(`[agentclick] Review complete: ${session.status}`);
36+
37+
return session.result || session;
38+
}
39+
40+
module.exports = { reviewAndWait };

packages/server/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { createSession, getSession, listSessions, completeSession, setSessionRew
1010

1111
const app = express()
1212
const DEFAULT_PORT = 38173
13-
const PORT = Number(process.env.PORT || DEFAULT_PORT)
13+
const PORT = Number(process.env.AGENTCLICK_PORT || process.env.PORT || DEFAULT_PORT)
1414
const OPENCLAW_WEBHOOK = process.env.OPENCLAW_WEBHOOK || 'http://localhost:18789/hooks/agent'
1515
const __filename = fileURLToPath(import.meta.url)
1616
const __dirname = dirname(__filename)

skills/clawui-plan/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Submit a multi-step execution plan to AgentClick for human review. The human can
137137
## Submitting a Plan
138138

139139
```bash
140-
RESPONSE=$(curl -s -X POST http://localhost:38173/api/review \
140+
RESPONSE=$(curl -s -X POST "${AGENTCLICK_URL:-http://localhost:${AGENTCLICK_PORT:-38173}}/api/review" \
141141
-H 'Content-Type: application/json' \
142142
-d '{
143143
"type": "plan_review",
@@ -160,7 +160,7 @@ echo "Session: $SESSION_ID"
160160
After creating the session, immediately block on `/wait` for that same session. Do not continue execution before `/wait` returns a decision.
161161

162162
```bash
163-
curl -s "http://localhost:38173/api/sessions/${SESSION_ID}/wait"
163+
curl -s "${AGENTCLICK_URL:-http://localhost:${AGENTCLICK_PORT:-38173}}/api/sessions/${SESSION_ID}/wait"
164164
```
165165

166166
Rules:

0 commit comments

Comments
 (0)