oomi-ai 0.2.41 → 0.2.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -210,28 +210,32 @@ async function ensureWorkspaceInstall({
210
210
  return true;
211
211
  }
212
212
 
213
- function buildRuntimeRegistration({
214
- localRuntime,
215
- entryUrl,
216
- transport,
217
- }) {
218
- const safeEntryUrl = trimString(entryUrl);
219
- if (safeEntryUrl) {
220
- return {
221
- endpoint: safeEntryUrl,
222
- transport: trimString(transport) || 'relay',
223
- healthcheckUrl: localRuntime.healthcheckUrl,
224
- localPort: localRuntime.localPort,
225
- };
226
- }
227
-
228
- return {
229
- endpoint: localRuntime.endpoint,
230
- transport: trimString(transport) || localRuntime.transport,
231
- healthcheckUrl: localRuntime.healthcheckUrl,
232
- localPort: localRuntime.localPort,
233
- };
234
- }
213
+ function buildRuntimeRegistration({
214
+ localRuntime,
215
+ entryUrl,
216
+ transport,
217
+ }) {
218
+ const safeEntryUrl = trimString(entryUrl);
219
+ if (safeEntryUrl) {
220
+ return {
221
+ endpoint: safeEntryUrl,
222
+ transport: trimString(transport) || 'relay',
223
+ healthcheckUrl: localRuntime.healthcheckUrl,
224
+ localPort: localRuntime.localPort,
225
+ localEndpoint: localRuntime.endpoint,
226
+ reachableEndpoint: localRuntime.reachableEndpoint,
227
+ };
228
+ }
229
+
230
+ return {
231
+ endpoint: localRuntime.reachableEndpoint || localRuntime.endpoint,
232
+ transport: trimString(transport) || localRuntime.transport,
233
+ healthcheckUrl: localRuntime.healthcheckUrl,
234
+ localPort: localRuntime.localPort,
235
+ localEndpoint: localRuntime.endpoint,
236
+ reachableEndpoint: localRuntime.reachableEndpoint,
237
+ };
238
+ }
235
239
 
236
240
  export async function launchManagedPersonaRuntime({
237
241
  slug,
@@ -272,13 +276,25 @@ export async function launchManagedPersonaRuntime({
272
276
  });
273
277
 
274
278
  const healthPath = scaffoldInfo.healthPath || resolveHealthPath(workspacePath);
275
- const preferredPort = previousState.localPort || scaffoldInfo.defaultPort;
276
-
277
- let reusingRunningProcess = false;
278
- if (!restart && Number.isInteger(previousState.pid) && isPersonaWorkspaceProcessRunning(previousState.pid)) {
279
- try {
280
- await waitForPersonaRuntime({
281
- healthcheckUrl: previousState.healthcheckUrl || buildLocalPersonaRuntime({
279
+ const preferredPort = previousState.localPort || scaffoldInfo.defaultPort;
280
+ const expectedDevCommand = resolvePersonaDevCommand({
281
+ workspacePath,
282
+ localPort: preferredPort,
283
+ });
284
+
285
+ let reusingRunningProcess = false;
286
+ if (
287
+ !restart &&
288
+ Number.isInteger(previousState.pid) &&
289
+ isPersonaWorkspaceProcessRunning(previousState.pid, {
290
+ workspacePath,
291
+ expectedCommand: expectedDevCommand,
292
+ localPort: preferredPort,
293
+ })
294
+ ) {
295
+ try {
296
+ await waitForPersonaRuntime({
297
+ healthcheckUrl: previousState.healthcheckUrl || buildLocalPersonaRuntime({
282
298
  localPort: preferredPort,
283
299
  healthPath,
284
300
  }).healthcheckUrl,
@@ -326,24 +342,27 @@ export async function launchManagedPersonaRuntime({
326
342
  entryUrl,
327
343
  transport,
328
344
  });
329
- const runtimeState = updatePersonaRuntimeState(workspacePath, {
345
+ const runtimeState = updatePersonaRuntimeState(workspacePath, {
330
346
  slug: safeSlug,
331
347
  name: safeName,
332
348
  description: safeDescription,
333
349
  workspacePath,
334
350
  templateVersion,
335
- localPort: localRuntime.localPort,
336
- localEndpoint: localRuntime.endpoint,
337
- endpoint: registration.endpoint,
338
- entryUrl: registration.endpoint,
351
+ localPort: localRuntime.localPort,
352
+ localEndpoint: localRuntime.endpoint,
353
+ reachableEndpoint: localRuntime.reachableEndpoint,
354
+ bindHost: localRuntime.bindHost,
355
+ reachableHost: localRuntime.reachableHost,
356
+ endpoint: registration.endpoint,
357
+ entryUrl: registration.endpoint,
339
358
  transport: registration.transport,
340
359
  healthcheckUrl: localRuntime.healthcheckUrl,
341
360
  pid: processInfo.pid,
342
361
  logFilePath: processInfo.logFilePath,
343
- status: 'running',
344
- lastStartedAt: new Date().toISOString(),
345
- devCommand: resolvePersonaDevCommand({ workspacePath, localPort }),
346
- });
362
+ status: 'running',
363
+ lastStartedAt: new Date().toISOString(),
364
+ devCommand: resolvePersonaDevCommand({ workspacePath, localPort }),
365
+ });
347
366
 
348
367
  return {
349
368
  ok: true,
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { spawn, spawnSync } from 'node:child_process';
4
5
  import { fileURLToPath } from 'node:url';
@@ -198,9 +199,96 @@ function normalizePositiveInteger(value) {
198
199
  return Math.floor(parsed);
199
200
  }
200
201
 
201
- function resolvePersonaBindHost() {
202
+ function isWildcardHost(host) {
203
+ const normalized = String(host || '').trim().toLowerCase();
204
+ return normalized === '0.0.0.0' || normalized === '::' || normalized === '[::]';
205
+ }
206
+
207
+ function isLoopbackHost(host) {
208
+ const normalized = String(host || '').trim().toLowerCase();
209
+ return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1' || normalized === '[::1]';
210
+ }
211
+
212
+ function isPrivateIpv4(address) {
213
+ if (!/^\d+\.\d+\.\d+\.\d+$/u.test(address)) {
214
+ return false;
215
+ }
216
+
217
+ const [first, second] = address.split('.').map((segment) => Number(segment));
218
+ if (first === 10) return true;
219
+ if (first === 192 && second === 168) return true;
220
+ if (first === 172 && second >= 16 && second <= 31) return true;
221
+ return false;
222
+ }
223
+
224
+ function scorePersonaNetworkCandidate(candidate) {
225
+ let score = 0;
226
+ const name = String(candidate.name || '').toLowerCase();
227
+ const address = String(candidate.address || '');
228
+
229
+ if (isPrivateIpv4(address)) score += 40;
230
+ if (address.startsWith('192.168.')) score += 8;
231
+ if (address.startsWith('10.')) score += 6;
232
+ if (/ethernet|wi-?fi|wlan|en\d|eth\d/iu.test(name)) score += 12;
233
+ if (/hyper-v|vethernet|wsl|docker|vmware|virtualbox|tailscale|loopback|bridge/iu.test(name)) score -= 30;
234
+ if (address.startsWith('169.254.')) score -= 100;
235
+ return score;
236
+ }
237
+
238
+ function formatPersonaRuntimeHostForUrl(host) {
239
+ const safeHost = String(host || '').trim();
240
+ if (!safeHost) {
241
+ return '127.0.0.1';
242
+ }
243
+ if (safeHost.includes(':') && !safeHost.startsWith('[')) {
244
+ return `[${safeHost}]`;
245
+ }
246
+ return safeHost;
247
+ }
248
+
249
+ export function resolvePersonaBindHost() {
202
250
  const value = String(process.env.OOMI_PERSONA_BIND_HOST || '').trim();
203
- return value || '127.0.0.1';
251
+ return value || '0.0.0.0';
252
+ }
253
+
254
+ export function resolvePersonaReachableHost({
255
+ bindHost = resolvePersonaBindHost(),
256
+ env = process.env,
257
+ networkInterfaces = os.networkInterfaces(),
258
+ } = {}) {
259
+ const explicit = String(env.OOMI_PERSONA_PUBLIC_HOST || '').trim();
260
+ if (explicit) {
261
+ return explicit;
262
+ }
263
+
264
+ const safeBindHost = String(bindHost || '').trim();
265
+ if (safeBindHost && !isWildcardHost(safeBindHost) && !isLoopbackHost(safeBindHost)) {
266
+ return safeBindHost;
267
+ }
268
+
269
+ const candidates = [];
270
+ for (const [name, entries] of Object.entries(networkInterfaces || {})) {
271
+ for (const entry of Array.isArray(entries) ? entries : []) {
272
+ if (!entry || entry.internal || entry.family !== 'IPv4') {
273
+ continue;
274
+ }
275
+
276
+ const address = String(entry.address || '').trim();
277
+ if (!address || isLoopbackHost(address) || address.startsWith('169.254.')) {
278
+ continue;
279
+ }
280
+
281
+ candidates.push({
282
+ name,
283
+ address,
284
+ });
285
+ }
286
+ }
287
+
288
+ const winner = candidates
289
+ .sort((left, right) => scorePersonaNetworkCandidate(right) - scorePersonaNetworkCandidate(left))[0];
290
+
291
+ return winner?.address || '127.0.0.1';
204
292
  }
205
293
 
206
294
  function trimString(value) {
@@ -754,23 +842,29 @@ export async function waitForPersonaRuntime({
754
842
  throw new Error(`Timed out waiting for persona runtime healthcheck: ${message}`);
755
843
  }
756
844
 
757
- export function buildLocalPersonaRuntime({
758
- localPort,
759
- healthPath,
760
- }) {
761
- const port = Number(localPort);
762
- if (!Number.isFinite(port) || port <= 0) {
763
- throw new Error('Local port is required.');
764
- }
765
-
766
- const endpoint = `http://127.0.0.1:${port}`;
767
- return {
768
- transport: 'local',
769
- endpoint,
770
- localPort: port,
771
- healthcheckUrl: `${endpoint}${healthPath}`,
772
- };
773
- }
845
+ export function buildLocalPersonaRuntime({
846
+ localPort,
847
+ healthPath,
848
+ }) {
849
+ const port = Number(localPort);
850
+ if (!Number.isFinite(port) || port <= 0) {
851
+ throw new Error('Local port is required.');
852
+ }
853
+
854
+ const bindHost = resolvePersonaBindHost();
855
+ const reachableHost = resolvePersonaReachableHost({ bindHost });
856
+ const endpoint = `http://127.0.0.1:${port}`;
857
+ const reachableEndpoint = `http://${formatPersonaRuntimeHostForUrl(reachableHost)}:${port}`;
858
+ return {
859
+ transport: 'local',
860
+ endpoint,
861
+ reachableEndpoint,
862
+ bindHost,
863
+ reachableHost,
864
+ localPort: port,
865
+ healthcheckUrl: `${endpoint}${healthPath}`,
866
+ };
867
+ }
774
868
 
775
869
  export function defaultPersonaWorkspaceRoot() {
776
870
  return resolveOpenclawPersonasDir();
@@ -4,8 +4,8 @@ import path from 'node:path';
4
4
  import { resolveOpenclawLegacyPersonasDir } from './openclawPaths.js';
5
5
  import { createPersonaApiClient } from './personaApiClient.js';
6
6
  import { launchManagedPersonaRuntime } from './personaRuntimeManager.js';
7
- import { readPersonaRuntimeState } from './personaRuntimeRegistry.js';
8
- import { isPersonaWorkspaceProcessRunning } from './personaRuntimeProcess.js';
7
+ import { readPersonaRuntimeState, updatePersonaRuntimeState } from './personaRuntimeRegistry.js';
8
+ import { buildLocalPersonaRuntime, isPersonaWorkspaceProcessRunning, resolvePersonaDevCommand } from './personaRuntimeProcess.js';
9
9
 
10
10
  function trimString(value) {
11
11
  return typeof value === 'string' ? value.trim() : '';
@@ -17,6 +17,20 @@ function wait(ms) {
17
17
  });
18
18
  }
19
19
 
20
+ function resolveHealthPath(healthcheckUrl) {
21
+ const safeUrl = trimString(healthcheckUrl);
22
+ if (!safeUrl) {
23
+ return '/oomi.health.json';
24
+ }
25
+
26
+ try {
27
+ const parsed = new URL(safeUrl);
28
+ return `${parsed.pathname || '/oomi.health.json'}${parsed.search || ''}`;
29
+ } catch {
30
+ return '/oomi.health.json';
31
+ }
32
+ }
33
+
20
34
  function listWorkspacePaths(workspaceRoot) {
21
35
  const roots = [trimString(workspaceRoot), trimString(resolveOpenclawLegacyPersonasDir())]
22
36
  .filter(Boolean)
@@ -76,14 +90,29 @@ async function reconcileWorkspace({
76
90
  const runtime = {
77
91
  slug,
78
92
  endpoint: trimString(state.endpoint || state.entryUrl || state.localEndpoint),
93
+ localEndpoint: trimString(state.localEndpoint),
94
+ reachableEndpoint: trimString(state.reachableEndpoint),
79
95
  healthcheckUrl: trimString(state.healthcheckUrl),
80
96
  transport: trimString(state.transport) || 'local',
81
97
  localPort: Number.isFinite(Number(state.localPort)) ? Number(state.localPort) : null,
82
98
  };
83
99
 
100
+ const localRuntime = runtime.localPort
101
+ ? buildLocalPersonaRuntime({
102
+ localPort: runtime.localPort,
103
+ healthPath: resolveHealthPath(runtime.healthcheckUrl),
104
+ })
105
+ : null;
106
+
107
+ const expectedDevCommand = runtime.localPort
108
+ ? resolvePersonaDevCommand({
109
+ workspacePath,
110
+ localPort: runtime.localPort,
111
+ })
112
+ : state.devCommand;
84
113
  const processRunning = isPersonaWorkspaceProcessRunning(state.pid, {
85
114
  workspacePath,
86
- expectedCommand: state.devCommand,
115
+ expectedCommand: expectedDevCommand,
87
116
  localPort: runtime.localPort,
88
117
  });
89
118
 
@@ -110,6 +139,8 @@ async function reconcileWorkspace({
110
139
  effectiveRuntime = {
111
140
  slug,
112
141
  endpoint: launchResult.runtime.endpoint,
142
+ localEndpoint: launchResult.runtime.localEndpoint || launchResult.localRuntime.endpoint,
143
+ reachableEndpoint: launchResult.runtime.reachableEndpoint || launchResult.localRuntime.reachableEndpoint,
113
144
  healthcheckUrl: launchResult.runtime.healthcheckUrl,
114
145
  transport: launchResult.runtime.transport,
115
146
  localPort: launchResult.runtime.localPort,
@@ -138,6 +169,48 @@ async function reconcileWorkspace({
138
169
  return;
139
170
  }
140
171
 
172
+ if (localRuntime) {
173
+ const desiredEndpoint = localRuntime.reachableEndpoint || runtime.endpoint;
174
+ const endpointChanged = desiredEndpoint && desiredEndpoint !== effectiveRuntime.endpoint;
175
+ const localEndpointChanged = localRuntime.endpoint !== effectiveRuntime.localEndpoint;
176
+ const reachableEndpointChanged = localRuntime.reachableEndpoint !== effectiveRuntime.reachableEndpoint;
177
+
178
+ if (endpointChanged || localEndpointChanged || reachableEndpointChanged) {
179
+ effectiveRuntime = {
180
+ ...effectiveRuntime,
181
+ endpoint: desiredEndpoint,
182
+ localEndpoint: localRuntime.endpoint,
183
+ reachableEndpoint: localRuntime.reachableEndpoint,
184
+ };
185
+ updatePersonaRuntimeState(workspacePath, {
186
+ endpoint: desiredEndpoint,
187
+ entryUrl: desiredEndpoint,
188
+ localEndpoint: localRuntime.endpoint,
189
+ reachableEndpoint: localRuntime.reachableEndpoint,
190
+ bindHost: localRuntime.bindHost,
191
+ reachableHost: localRuntime.reachableHost,
192
+ });
193
+
194
+ try {
195
+ await client.registerRuntime({
196
+ slug,
197
+ endpoint: effectiveRuntime.endpoint,
198
+ healthcheckUrl: effectiveRuntime.healthcheckUrl,
199
+ localPort: effectiveRuntime.localPort,
200
+ transport: effectiveRuntime.transport,
201
+ startedAt: new Date().toISOString(),
202
+ });
203
+ } catch (error) {
204
+ logger.warn?.(
205
+ `[persona-runtime] registration refresh failed for ${slug}: ${
206
+ error instanceof Error ? error.message : String(error)
207
+ }`
208
+ );
209
+ return;
210
+ }
211
+ }
212
+ }
213
+
141
214
  try {
142
215
  await client.heartbeatRuntime({
143
216
  slug,
@@ -2,7 +2,7 @@
2
2
  "id": "oomi-ai",
3
3
  "name": "Oomi Channel Plugin",
4
4
  "description": "Managed Oomi channel integration for OpenClaw.",
5
- "version": "0.2.41",
5
+ "version": "0.2.42",
6
6
  "author": "Oomi",
7
7
  "license": "MIT",
8
8
  "openclawVersion": ">=0.5.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.2.41",
3
+ "version": "0.2.42",
4
4
  "description": "Oomi OpenClaw channel plugin and bridge tooling",
5
5
  "bin": {
6
6
  "oomi": "bin/oomi-ai.js"