oomi-ai 0.2.29 → 0.2.38
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.
- package/README.md +258 -158
- package/bin/oomi-ai.js +2130 -1365
- package/lib/openclawDevGateway.js +384 -0
- package/lib/openclawPaths.js +78 -0
- package/lib/openclawProfile.js +265 -0
- package/lib/personaApiClient.js +304 -253
- package/lib/personaJobExecutor.js +35 -11
- package/lib/personaPortAllocator.js +36 -0
- package/lib/personaRuntimeManager.js +364 -0
- package/lib/personaRuntimeProcess.js +378 -121
- package/lib/personaRuntimeRegistry.js +67 -0
- package/lib/personaRuntimeSupervisor.js +193 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { findAvailablePort } from './personaPortAllocator.js';
|
|
5
|
+
import {
|
|
6
|
+
buildLocalPersonaRuntime,
|
|
7
|
+
defaultPersonaWorkspaceRoot,
|
|
8
|
+
installPersonaWorkspace,
|
|
9
|
+
isPersonaWorkspaceProcessRunning,
|
|
10
|
+
resolvePersonaDevCommand,
|
|
11
|
+
startPersonaWorkspace,
|
|
12
|
+
stopPersonaWorkspace,
|
|
13
|
+
waitForPersonaRuntime,
|
|
14
|
+
} from './personaRuntimeProcess.js';
|
|
15
|
+
import {
|
|
16
|
+
readPersonaRuntimeState,
|
|
17
|
+
resolvePersonaRuntimeLogPath,
|
|
18
|
+
resolvePersonaRuntimeStatePath,
|
|
19
|
+
resolvePersonaWorkspacePath,
|
|
20
|
+
updatePersonaRuntimeState,
|
|
21
|
+
} from './personaRuntimeRegistry.js';
|
|
22
|
+
import { scaffoldPersonaApp } from './scaffold.js';
|
|
23
|
+
|
|
24
|
+
function trimString(value) {
|
|
25
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function slugifyPersonaName(name) {
|
|
29
|
+
const normalized = trimString(name)
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-+|-+$/g, '')
|
|
33
|
+
.replace(/-{2,}/g, '-');
|
|
34
|
+
|
|
35
|
+
if (!normalized) {
|
|
36
|
+
throw new Error('Persona name must include at least one letter or number.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveHealthPath(workspacePath) {
|
|
43
|
+
const runtimeConfigPath = path.join(workspacePath, 'oomi.runtime.json');
|
|
44
|
+
if (!fs.existsSync(runtimeConfigPath)) {
|
|
45
|
+
return '/oomi.health.json';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(fs.readFileSync(runtimeConfigPath, 'utf8'));
|
|
50
|
+
const healthPath = trimString(parsed?.healthPath);
|
|
51
|
+
return healthPath || '/oomi.health.json';
|
|
52
|
+
} catch {
|
|
53
|
+
return '/oomi.health.json';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function workspaceNeedsScaffold(workspacePath) {
|
|
58
|
+
return !fs.existsSync(path.join(workspacePath, 'package.json'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function ensureWorkspaceScaffold({
|
|
62
|
+
slug,
|
|
63
|
+
name,
|
|
64
|
+
description,
|
|
65
|
+
workspacePath,
|
|
66
|
+
templateVersion,
|
|
67
|
+
}) {
|
|
68
|
+
if (!workspaceNeedsScaffold(workspacePath)) {
|
|
69
|
+
return {
|
|
70
|
+
scaffolded: false,
|
|
71
|
+
workspacePath,
|
|
72
|
+
healthPath: resolveHealthPath(workspacePath),
|
|
73
|
+
defaultPort: 4789,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const scaffoldResult = scaffoldPersonaApp({
|
|
78
|
+
slug,
|
|
79
|
+
name,
|
|
80
|
+
description,
|
|
81
|
+
outDir: workspacePath,
|
|
82
|
+
templateVersion,
|
|
83
|
+
force: false,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
scaffolded: true,
|
|
88
|
+
workspacePath,
|
|
89
|
+
healthPath: scaffoldResult.healthPath,
|
|
90
|
+
defaultPort: scaffoldResult.defaultPort,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function ensureWorkspaceInstall({
|
|
95
|
+
workspacePath,
|
|
96
|
+
forceInstall = false,
|
|
97
|
+
}) {
|
|
98
|
+
const nodeModulesPath = path.join(workspacePath, 'node_modules');
|
|
99
|
+
if (!forceInstall && fs.existsSync(nodeModulesPath)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await installPersonaWorkspace({ workspacePath });
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildRuntimeRegistration({
|
|
108
|
+
localRuntime,
|
|
109
|
+
entryUrl,
|
|
110
|
+
transport,
|
|
111
|
+
}) {
|
|
112
|
+
const safeEntryUrl = trimString(entryUrl);
|
|
113
|
+
if (safeEntryUrl) {
|
|
114
|
+
return {
|
|
115
|
+
endpoint: safeEntryUrl,
|
|
116
|
+
transport: trimString(transport) || 'relay',
|
|
117
|
+
healthcheckUrl: localRuntime.healthcheckUrl,
|
|
118
|
+
localPort: localRuntime.localPort,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
endpoint: localRuntime.endpoint,
|
|
124
|
+
transport: trimString(transport) || localRuntime.transport,
|
|
125
|
+
healthcheckUrl: localRuntime.healthcheckUrl,
|
|
126
|
+
localPort: localRuntime.localPort,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function launchManagedPersonaRuntime({
|
|
131
|
+
slug,
|
|
132
|
+
name,
|
|
133
|
+
description,
|
|
134
|
+
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
135
|
+
templateVersion = 'v1',
|
|
136
|
+
forceInstall = false,
|
|
137
|
+
restart = false,
|
|
138
|
+
logFilePath = '',
|
|
139
|
+
entryUrl = '',
|
|
140
|
+
transport = '',
|
|
141
|
+
} = {}) {
|
|
142
|
+
const safeName = trimString(name);
|
|
143
|
+
const safeDescription = trimString(description) || safeName;
|
|
144
|
+
if (!safeName) {
|
|
145
|
+
throw new Error('Persona name is required.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const safeSlug = trimString(slug) || slugifyPersonaName(safeName);
|
|
149
|
+
const workspacePath = resolvePersonaWorkspacePath({
|
|
150
|
+
workspaceRoot,
|
|
151
|
+
slug: safeSlug,
|
|
152
|
+
});
|
|
153
|
+
const previousState = readPersonaRuntimeState(workspacePath);
|
|
154
|
+
|
|
155
|
+
const scaffoldInfo = await ensureWorkspaceScaffold({
|
|
156
|
+
slug: safeSlug,
|
|
157
|
+
name: safeName,
|
|
158
|
+
description: safeDescription,
|
|
159
|
+
workspacePath,
|
|
160
|
+
templateVersion,
|
|
161
|
+
});
|
|
162
|
+
const installed = await ensureWorkspaceInstall({
|
|
163
|
+
workspacePath,
|
|
164
|
+
forceInstall,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const healthPath = scaffoldInfo.healthPath || resolveHealthPath(workspacePath);
|
|
168
|
+
const preferredPort = previousState.localPort || scaffoldInfo.defaultPort;
|
|
169
|
+
|
|
170
|
+
let reusingRunningProcess = false;
|
|
171
|
+
if (!restart && Number.isInteger(previousState.pid) && isPersonaWorkspaceProcessRunning(previousState.pid)) {
|
|
172
|
+
try {
|
|
173
|
+
await waitForPersonaRuntime({
|
|
174
|
+
healthcheckUrl: previousState.healthcheckUrl || buildLocalPersonaRuntime({
|
|
175
|
+
localPort: preferredPort,
|
|
176
|
+
healthPath,
|
|
177
|
+
}).healthcheckUrl,
|
|
178
|
+
timeoutMs: 4000,
|
|
179
|
+
intervalMs: 500,
|
|
180
|
+
});
|
|
181
|
+
reusingRunningProcess = true;
|
|
182
|
+
} catch {
|
|
183
|
+
reusingRunningProcess = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (restart && Number.isInteger(previousState.pid) && isPersonaWorkspaceProcessRunning(previousState.pid)) {
|
|
188
|
+
await stopPersonaWorkspace({ pid: previousState.pid });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const localPort = reusingRunningProcess
|
|
192
|
+
? previousState.localPort
|
|
193
|
+
: await findAvailablePort({
|
|
194
|
+
preferredPort,
|
|
195
|
+
});
|
|
196
|
+
const localRuntime = buildLocalPersonaRuntime({
|
|
197
|
+
localPort,
|
|
198
|
+
healthPath,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let processInfo = {
|
|
202
|
+
pid: Number.isInteger(previousState.pid) ? previousState.pid : null,
|
|
203
|
+
logFilePath: trimString(previousState.logFilePath) || resolvePersonaRuntimeLogPath(workspacePath),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (!reusingRunningProcess) {
|
|
207
|
+
processInfo = startPersonaWorkspace({
|
|
208
|
+
workspacePath,
|
|
209
|
+
logFilePath: logFilePath || resolvePersonaRuntimeLogPath(workspacePath),
|
|
210
|
+
localPort,
|
|
211
|
+
});
|
|
212
|
+
await waitForPersonaRuntime({
|
|
213
|
+
healthcheckUrl: localRuntime.healthcheckUrl,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const registration = buildRuntimeRegistration({
|
|
218
|
+
localRuntime,
|
|
219
|
+
entryUrl,
|
|
220
|
+
transport,
|
|
221
|
+
});
|
|
222
|
+
const runtimeState = updatePersonaRuntimeState(workspacePath, {
|
|
223
|
+
slug: safeSlug,
|
|
224
|
+
name: safeName,
|
|
225
|
+
description: safeDescription,
|
|
226
|
+
workspacePath,
|
|
227
|
+
templateVersion,
|
|
228
|
+
localPort: localRuntime.localPort,
|
|
229
|
+
localEndpoint: localRuntime.endpoint,
|
|
230
|
+
endpoint: registration.endpoint,
|
|
231
|
+
entryUrl: registration.endpoint,
|
|
232
|
+
transport: registration.transport,
|
|
233
|
+
healthcheckUrl: localRuntime.healthcheckUrl,
|
|
234
|
+
pid: processInfo.pid,
|
|
235
|
+
logFilePath: processInfo.logFilePath,
|
|
236
|
+
status: 'running',
|
|
237
|
+
lastStartedAt: new Date().toISOString(),
|
|
238
|
+
devCommand: resolvePersonaDevCommand({ workspacePath, localPort }),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
ok: true,
|
|
243
|
+
slug: safeSlug,
|
|
244
|
+
workspacePath,
|
|
245
|
+
scaffolded: scaffoldInfo.scaffolded,
|
|
246
|
+
installed,
|
|
247
|
+
reusedRunningProcess: reusingRunningProcess,
|
|
248
|
+
runtime: registration,
|
|
249
|
+
localRuntime,
|
|
250
|
+
state: runtimeState,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function getManagedPersonaRuntimeStatus({
|
|
255
|
+
slug,
|
|
256
|
+
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
257
|
+
}) {
|
|
258
|
+
const safeSlug = trimString(slug);
|
|
259
|
+
if (!safeSlug) {
|
|
260
|
+
throw new Error('Persona slug is required.');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const workspacePath = resolvePersonaWorkspacePath({
|
|
264
|
+
workspaceRoot,
|
|
265
|
+
slug: safeSlug,
|
|
266
|
+
});
|
|
267
|
+
const state = readPersonaRuntimeState(workspacePath);
|
|
268
|
+
const pid = Number.isInteger(state.pid) ? state.pid : null;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
slug: safeSlug,
|
|
272
|
+
workspacePath,
|
|
273
|
+
workspaceExists: fs.existsSync(workspacePath),
|
|
274
|
+
runtimeStatePath: resolvePersonaRuntimeStatePath(workspacePath),
|
|
275
|
+
processRunning: pid ? isPersonaWorkspaceProcessRunning(pid) : false,
|
|
276
|
+
state,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function stopManagedPersonaRuntime({
|
|
281
|
+
slug,
|
|
282
|
+
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
283
|
+
}) {
|
|
284
|
+
const status = getManagedPersonaRuntimeStatus({ slug, workspaceRoot });
|
|
285
|
+
if (!status.workspaceExists) {
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
stopped: false,
|
|
289
|
+
missingWorkspace: true,
|
|
290
|
+
workspacePath: status.workspacePath,
|
|
291
|
+
state: status.state,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const pid = Number.isInteger(status.state.pid) ? status.state.pid : null;
|
|
296
|
+
if (!pid) {
|
|
297
|
+
const nextState = updatePersonaRuntimeState(status.workspacePath, {
|
|
298
|
+
status: 'stopped',
|
|
299
|
+
stoppedAt: new Date().toISOString(),
|
|
300
|
+
});
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
stopped: false,
|
|
304
|
+
state: nextState,
|
|
305
|
+
workspacePath: status.workspacePath,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await stopPersonaWorkspace({ pid });
|
|
310
|
+
const nextState = updatePersonaRuntimeState(status.workspacePath, {
|
|
311
|
+
status: 'stopped',
|
|
312
|
+
stoppedAt: new Date().toISOString(),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
ok: true,
|
|
317
|
+
stopped: true,
|
|
318
|
+
state: nextState,
|
|
319
|
+
workspacePath: status.workspacePath,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function ensureWorkspacePathWithinRoot(workspacePath, workspaceRoot) {
|
|
324
|
+
const resolvedWorkspacePath = path.resolve(workspacePath);
|
|
325
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
326
|
+
const relative = path.relative(resolvedRoot, resolvedWorkspacePath);
|
|
327
|
+
if (
|
|
328
|
+
relative === '' ||
|
|
329
|
+
relative.startsWith('..') ||
|
|
330
|
+
path.isAbsolute(relative)
|
|
331
|
+
) {
|
|
332
|
+
throw new Error(`Refusing to delete workspace outside persona root: ${resolvedWorkspacePath}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function destroyManagedPersonaRuntime({
|
|
337
|
+
slug,
|
|
338
|
+
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
339
|
+
}) {
|
|
340
|
+
const status = getManagedPersonaRuntimeStatus({ slug, workspaceRoot });
|
|
341
|
+
if (!status.workspaceExists) {
|
|
342
|
+
return {
|
|
343
|
+
ok: true,
|
|
344
|
+
deleted: false,
|
|
345
|
+
missingWorkspace: true,
|
|
346
|
+
workspacePath: status.workspacePath,
|
|
347
|
+
state: status.state,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const stopResult = await stopManagedPersonaRuntime({
|
|
352
|
+
slug,
|
|
353
|
+
workspaceRoot,
|
|
354
|
+
});
|
|
355
|
+
ensureWorkspacePathWithinRoot(status.workspacePath, workspaceRoot);
|
|
356
|
+
fs.rmSync(status.workspacePath, { recursive: true, force: true });
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
ok: true,
|
|
360
|
+
deleted: true,
|
|
361
|
+
stopped: Boolean(stopResult.stopped),
|
|
362
|
+
workspacePath: status.workspacePath,
|
|
363
|
+
};
|
|
364
|
+
}
|