oomi-ai 0.2.49 → 0.3.0

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.
Files changed (88) hide show
  1. package/README.md +227 -463
  2. package/agent_instructions.md +244 -234
  3. package/bin/oomi-ai.js +4028 -5797
  4. package/bin/sessionBridgeState.js +78 -78
  5. package/lib/openclawPaths.js +70 -71
  6. package/lib/openclawProfile.js +216 -216
  7. package/lib/personaApiClient.js +133 -303
  8. package/lib/spokenMetadata.js +137 -137
  9. package/openclaw.extension.js +341 -341
  10. package/openclaw.plugin.json +17 -17
  11. package/package.json +59 -59
  12. package/persona-app/README.md +27 -0
  13. package/persona-app/registry/v1.json +63 -0
  14. package/persona-app/schema/persona-app.v1.schema.json +90 -0
  15. package/skills/oomi/SKILL.md +165 -182
  16. package/skills/oomi/agent_instructions.md +99 -80
  17. package/lib/channelPluginClient.js +0 -119
  18. package/lib/openclawDevGateway.js +0 -384
  19. package/lib/personaJobExecutor.js +0 -139
  20. package/lib/personaJobPoller.js +0 -112
  21. package/lib/personaPortAllocator.js +0 -36
  22. package/lib/personaRuntimeManager.js +0 -496
  23. package/lib/personaRuntimeProcess.js +0 -924
  24. package/lib/personaRuntimeRegistry.js +0 -67
  25. package/lib/personaRuntimeSupervisor.js +0 -330
  26. package/lib/scaffold.js +0 -108
  27. package/lib/template.js +0 -45
  28. package/skills/oomi/config.json +0 -3
  29. package/skills/oomi/scripts/get_avatar_capabilities.py +0 -40
  30. package/skills/oomi/scripts/get_data.py +0 -49
  31. package/skills/oomi/scripts/install_agent_instructions.py +0 -78
  32. package/skills/oomi/scripts/send_goal.py +0 -53
  33. package/skills/oomi/scripts/sync.py +0 -46
  34. package/skills/oomi/setup.py +0 -41
  35. package/templates/persona-app/.env.example +0 -8
  36. package/templates/persona-app/README.md +0 -47
  37. package/templates/persona-app/eslint.config.js +0 -28
  38. package/templates/persona-app/index.html +0 -18
  39. package/templates/persona-app/oomi.runtime.json +0 -13
  40. package/templates/persona-app/package.json +0 -44
  41. package/templates/persona-app/persona/brief.md +0 -14
  42. package/templates/persona-app/persona.json +0 -14
  43. package/templates/persona-app/public/manifest.webmanifest +0 -8
  44. package/templates/persona-app/public/oomi.health.json +0 -6
  45. package/templates/persona-app/src/App.css +0 -379
  46. package/templates/persona-app/src/App.tsx +0 -17
  47. package/templates/persona-app/src/index.css +0 -53
  48. package/templates/persona-app/src/main.tsx +0 -23
  49. package/templates/persona-app/src/pages/HomePage.tsx +0 -127
  50. package/templates/persona-app/src/pages/ScenePage.tsx +0 -158
  51. package/templates/persona-app/src/persona/config.ts +0 -6
  52. package/templates/persona-app/src/persona/notes.ts +0 -9
  53. package/templates/persona-app/src/spatial.ts +0 -82
  54. package/templates/persona-app/src/vite-env.d.ts +0 -3
  55. package/templates/persona-app/template.json +0 -13
  56. package/templates/persona-app/tsconfig.app.json +0 -23
  57. package/templates/persona-app/tsconfig.json +0 -7
  58. package/templates/persona-app/tsconfig.node.json +0 -21
  59. package/templates/persona-app/vendor/webspatial/FORK.md +0 -6
  60. package/templates/persona-app/vendor/webspatial/core-sdk/LICENSE +0 -21
  61. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.d.ts +0 -906
  62. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.global.js +0 -75
  63. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.global.js.map +0 -1
  64. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.d.ts +0 -906
  65. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.js +0 -3131
  66. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.js.map +0 -1
  67. package/templates/persona-app/vendor/webspatial/core-sdk/package.json +0 -45
  68. package/templates/persona-app/vendor/webspatial/react-sdk/LICENSE +0 -21
  69. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.d.ts +0 -365
  70. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js +0 -4167
  71. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js.map +0 -1
  72. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.d.ts +0 -82
  73. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.js +0 -66
  74. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.js.map +0 -1
  75. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.d.ts +0 -2
  76. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.js +0 -18
  77. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.js.map +0 -1
  78. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.d.ts +0 -5
  79. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.js +0 -66
  80. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.js.map +0 -1
  81. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.d.ts +0 -1
  82. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.js +0 -18
  83. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.js.map +0 -1
  84. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.d.ts +0 -365
  85. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js +0 -4207
  86. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js.map +0 -1
  87. package/templates/persona-app/vendor/webspatial/react-sdk/package.json +0 -94
  88. package/templates/persona-app/vite.config.ts +0 -31
@@ -1,924 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { spawn, spawnSync } from 'node:child_process';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- import { resolveOpenclawPersonasDir } from './openclawPaths.js';
8
- import {
9
- DEFAULT_TEMPLATE_ID,
10
- DEFAULT_TEMPLATE_VERSION,
11
- renderTemplateFile,
12
- resolveTemplateRoot,
13
- } from './template.js';
14
-
15
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16
- const PERSONA_TEMPLATE_ROOT = resolveTemplateRoot(
17
- DEFAULT_TEMPLATE_ID,
18
- DEFAULT_TEMPLATE_VERSION,
19
- );
20
- const PERSONA_TEMPLATE_PACKAGE_JSON_PATH = path.join(PERSONA_TEMPLATE_ROOT, 'package.json');
21
- const PERSONA_TEMPLATE_PACKAGE_JSON = fs.existsSync(PERSONA_TEMPLATE_PACKAGE_JSON_PATH)
22
- ? JSON.parse(fs.readFileSync(PERSONA_TEMPLATE_PACKAGE_JSON_PATH, 'utf8'))
23
- : {};
24
- const WEBSPATIAL_VENDOR_ROOT = path.join(
25
- PACKAGE_ROOT,
26
- 'templates',
27
- 'persona-app',
28
- 'vendor',
29
- 'webspatial',
30
- );
31
- const VENDORED_WEBSPATIAL_CORE_SPEC = 'file:./vendor/webspatial/core-sdk';
32
- const VENDORED_WEBSPATIAL_REACT_SPEC = 'file:./vendor/webspatial/react-sdk';
33
- const WEBSPATIAL_RUNTIME_BASE_PATH = '/webspatial/avp';
34
- const WEBSPATIAL_TEMPLATE_DEV_DEPENDENCIES = [
35
- '@webspatial/builder',
36
- '@webspatial/platform-visionos',
37
- '@webspatial/vite-plugin',
38
- ];
39
- const LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES = [
40
- {
41
- relativePath: 'oomi.runtime.json',
42
- shouldReplace: (content) =>
43
- content.includes('"renderMode": "webspatial"') &&
44
- content.includes('"healthPath": "/oomi.health.json"'),
45
- },
46
- {
47
- relativePath: 'package.json',
48
- shouldReplace: (content) =>
49
- content.includes('vite --host 127.0.0.1 --port 4789') ||
50
- content.includes('vite preview --host 127.0.0.1 --port 4789'),
51
- },
52
- {
53
- relativePath: path.join('src', 'spatial.ts'),
54
- shouldReplace: (content) =>
55
- !content.includes('WEBSPATIAL_FORK_REPOSITORY') ||
56
- !content.includes('configurePersonaScene') ||
57
- !content.includes('"--xr-back": String(back)') ||
58
- content.includes('WEBSPATIAL_FORK_COMMIT = "b2746721e4fe6b4f86dac0ea55938074eea00cda"') ||
59
- content.includes('WEBSPATIAL_FORK_COMMIT = "8904ac8fec48fe49ee14d1739237bd1afb2894fe"'),
60
- },
61
- {
62
- relativePath: path.join('src', 'main.tsx'),
63
- shouldReplace: (content) =>
64
- !content.includes('snapdom') &&
65
- !content.includes('html2canvas') &&
66
- content.includes('createRoot(document.getElementById("root")!)'),
67
- },
68
- {
69
- relativePath: path.join('src', 'App.tsx'),
70
- shouldReplace: (content) =>
71
- !content.includes('const isSpatialRuntime = __XR_ENV_BASE__.startsWith("/webspatial/avp");') ||
72
- !content.includes('<Route path="home" element={<HomePage />} />') ||
73
- content.includes('<Route path="/" element={<HomePage />} />') ||
74
- content.includes('<Route path="/scene" element={<ScenePage />} />') ||
75
- content.includes('<Route index element={<HomePage />} />'),
76
- },
77
- {
78
- relativePath: path.join('src', 'pages', 'HomePage.tsx'),
79
- shouldReplace: (content) =>
80
- (content.includes('Open Spatial Scene') &&
81
- content.includes('Open Scene Route')) ||
82
- content.includes('sceneMode') ||
83
- content.includes('configurePersonaScene();') ||
84
- content.includes('persona-preview-card') ||
85
- content.includes('Launch Spatial Surface') ||
86
- content.includes('Open Spatial Preview') ||
87
- (
88
- content.includes('persona-panel persona-runtime" enable-xr') ||
89
- content.includes('persona-button" onClick={openPersonaScene} enable-xr') ||
90
- content.includes('persona-link" to="/scene" target="_blank" enable-xr') ||
91
- content.includes('persona-card" enable-xr style={xrStyle(')
92
- ),
93
- },
94
- {
95
- relativePath: path.join('src', 'pages', 'ScenePage.tsx'),
96
- shouldReplace: (content) =>
97
- content.includes('sceneMode') ||
98
- (
99
- !content.includes('enable-xr-monitor') &&
100
- content.includes(
101
- 'This route is intentionally separate so WebSpatial scene launching has a dedicated',
102
- )
103
- ) ||
104
- (
105
- content.includes('Awaiting AndroidXR interaction') &&
106
- content.includes('Interaction Console') &&
107
- content.includes('Fork-backed proof points')
108
- ),
109
- },
110
- {
111
- relativePath: path.join('src', 'index.css'),
112
- shouldReplace: (content) =>
113
- !content.includes('html.is-spatial #root') ||
114
- content.includes('radial-gradient(circle at top, rgba(205, 183, 143, 0.32), transparent 36%)'),
115
- },
116
- {
117
- relativePath: path.join('src', 'App.css'),
118
- shouldReplace: (content) =>
119
- content.includes('.persona-shell') ||
120
- (content.includes('.scene-panel') && !content.includes('.scene-interaction-grid')) ||
121
- content.includes('html.is-spatial .persona-runtime {') ||
122
- content.includes('html.is-spatial .persona-scene-root {') ||
123
- content.includes('html.is-spatial .persona-button,') ||
124
- content.includes('html.is-spatial .persona-card,') ||
125
- !content.includes('.scene-workspace-grid') ||
126
- !content.includes('.home-grid'),
127
- },
128
- {
129
- relativePath: 'vite.config.ts',
130
- shouldReplace: (content) =>
131
- content.includes('webSpatial()') && !content.includes('optimizeDeps'),
132
- },
133
- ];
134
-
135
- function resolveNpmCommand() {
136
- return process.platform === 'win32' ? 'npm.cmd' : 'npm';
137
- }
138
-
139
- function ensureDir(dirPath) {
140
- fs.mkdirSync(dirPath, { recursive: true });
141
- }
142
-
143
- function readJsonFile(filePath) {
144
- if (!fs.existsSync(filePath)) {
145
- return null;
146
- }
147
-
148
- try {
149
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
150
- } catch {
151
- return null;
152
- }
153
- }
154
-
155
- function copyDirectory(sourcePath, targetPath) {
156
- ensureDir(targetPath);
157
- let changed = false;
158
- const sourceEntries = fs.readdirSync(sourcePath, { withFileTypes: true });
159
- const sourceEntryNames = new Set(sourceEntries.map((entry) => entry.name));
160
- const existingTargetEntries = fs.readdirSync(targetPath, { withFileTypes: true });
161
-
162
- for (const entry of existingTargetEntries) {
163
- if (sourceEntryNames.has(entry.name)) {
164
- continue;
165
- }
166
- fs.rmSync(path.join(targetPath, entry.name), { recursive: true, force: true });
167
- changed = true;
168
- }
169
-
170
- for (const entry of sourceEntries) {
171
- const sourceEntryPath = path.join(sourcePath, entry.name);
172
- const targetEntryPath = path.join(targetPath, entry.name);
173
- const targetExists = fs.existsSync(targetEntryPath);
174
-
175
- if (entry.isDirectory()) {
176
- if (targetExists && !fs.statSync(targetEntryPath).isDirectory()) {
177
- fs.rmSync(targetEntryPath, { recursive: true, force: true });
178
- changed = true;
179
- }
180
- if (copyDirectory(sourceEntryPath, targetEntryPath)) {
181
- changed = true;
182
- }
183
- continue;
184
- }
185
-
186
- if (targetExists && fs.statSync(targetEntryPath).isDirectory()) {
187
- fs.rmSync(targetEntryPath, { recursive: true, force: true });
188
- changed = true;
189
- }
190
-
191
- const sourceBuffer = fs.readFileSync(sourceEntryPath);
192
- const targetBuffer = fs.existsSync(targetEntryPath)
193
- ? fs.readFileSync(targetEntryPath)
194
- : null;
195
- if (targetBuffer && sourceBuffer.equals(targetBuffer)) {
196
- continue;
197
- }
198
-
199
- fs.copyFileSync(sourceEntryPath, targetEntryPath);
200
- changed = true;
201
- }
202
-
203
- return changed;
204
- }
205
-
206
- function quoteWindowsCommandPart(value) {
207
- const text = String(value);
208
- if (!/[\s"]/u.test(text)) {
209
- return text;
210
- }
211
- return `"${text.replace(/"/g, '""')}"`;
212
- }
213
-
214
- function buildWindowsCommandLine(command, args) {
215
- const quotedCommand = quoteWindowsCommandPart(command);
216
- const quotedArgs = args.map((value) => quoteWindowsCommandPart(value)).join(' ');
217
- return `${quotedCommand}${quotedArgs ? ` ${quotedArgs}` : ''}`;
218
- }
219
-
220
- function normalizePositiveInteger(value) {
221
- const parsed = Number(value);
222
- if (!Number.isFinite(parsed) || parsed <= 0) {
223
- return null;
224
- }
225
- return Math.floor(parsed);
226
- }
227
-
228
- function isWildcardHost(host) {
229
- const normalized = String(host || '').trim().toLowerCase();
230
- return normalized === '0.0.0.0' || normalized === '::' || normalized === '[::]';
231
- }
232
-
233
- function isLoopbackHost(host) {
234
- const normalized = String(host || '').trim().toLowerCase();
235
- return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1' || normalized === '[::1]';
236
- }
237
-
238
- function isPrivateIpv4(address) {
239
- if (!/^\d+\.\d+\.\d+\.\d+$/u.test(address)) {
240
- return false;
241
- }
242
-
243
- const [first, second] = address.split('.').map((segment) => Number(segment));
244
- if (first === 10) return true;
245
- if (first === 192 && second === 168) return true;
246
- if (first === 172 && second >= 16 && second <= 31) return true;
247
- return false;
248
- }
249
-
250
- function scorePersonaNetworkCandidate(candidate) {
251
- let score = 0;
252
- const name = String(candidate.name || '').toLowerCase();
253
- const address = String(candidate.address || '');
254
-
255
- if (isPrivateIpv4(address)) score += 40;
256
- if (address.startsWith('192.168.')) score += 8;
257
- if (address.startsWith('10.')) score += 6;
258
- if (/ethernet|wi-?fi|wlan|en\d|eth\d/iu.test(name)) score += 12;
259
- if (/hyper-v|vethernet|wsl|docker|vmware|virtualbox|tailscale|loopback|bridge/iu.test(name)) score -= 30;
260
- if (address.startsWith('169.254.')) score -= 100;
261
- return score;
262
- }
263
-
264
- function formatPersonaRuntimeHostForUrl(host) {
265
- const safeHost = String(host || '').trim();
266
- if (!safeHost) {
267
- return '127.0.0.1';
268
- }
269
- if (safeHost.includes(':') && !safeHost.startsWith('[')) {
270
- return `[${safeHost}]`;
271
- }
272
- return safeHost;
273
- }
274
-
275
- export function resolvePersonaBindHost() {
276
- const value = String(process.env.OOMI_PERSONA_BIND_HOST || '').trim();
277
- return value || '0.0.0.0';
278
- }
279
-
280
- export function resolvePersonaReachableHost({
281
- bindHost = resolvePersonaBindHost(),
282
- env = process.env,
283
- networkInterfaces = os.networkInterfaces(),
284
- } = {}) {
285
- const explicit = String(env.OOMI_PERSONA_PUBLIC_HOST || '').trim();
286
- if (explicit) {
287
- return explicit;
288
- }
289
-
290
- const safeBindHost = String(bindHost || '').trim();
291
- if (safeBindHost && !isWildcardHost(safeBindHost) && !isLoopbackHost(safeBindHost)) {
292
- return safeBindHost;
293
- }
294
-
295
- const candidates = [];
296
- for (const [name, entries] of Object.entries(networkInterfaces || {})) {
297
- for (const entry of Array.isArray(entries) ? entries : []) {
298
- if (!entry || entry.internal || entry.family !== 'IPv4') {
299
- continue;
300
- }
301
-
302
- const address = String(entry.address || '').trim();
303
- if (!address || isLoopbackHost(address) || address.startsWith('169.254.')) {
304
- continue;
305
- }
306
-
307
- candidates.push({
308
- name,
309
- address,
310
- });
311
- }
312
- }
313
-
314
- const winner = candidates
315
- .sort((left, right) => scorePersonaNetworkCandidate(right) - scorePersonaNetworkCandidate(left))[0];
316
-
317
- return winner?.address || '127.0.0.1';
318
- }
319
-
320
- function trimString(value) {
321
- return typeof value === 'string' ? value.trim() : '';
322
- }
323
-
324
- function normalizePersonaHealthPath(healthPath, runtimeConfig = {}) {
325
- const safeHealthPath = trimString(healthPath) || '/oomi.health.json';
326
- const renderMode = trimString(runtimeConfig?.renderMode).toLowerCase();
327
- if (renderMode !== 'webspatial') {
328
- return safeHealthPath;
329
- }
330
-
331
- if (safeHealthPath.startsWith(`${WEBSPATIAL_RUNTIME_BASE_PATH}/`)) {
332
- return safeHealthPath;
333
- }
334
-
335
- const normalizedSuffix = safeHealthPath.startsWith('/') ? safeHealthPath : `/${safeHealthPath}`;
336
- return `${WEBSPATIAL_RUNTIME_BASE_PATH}${normalizedSuffix}`;
337
- }
338
-
339
- function readPersonaConfigLiteral(source, key) {
340
- if (!source) {
341
- return '';
342
- }
343
-
344
- const match = source.match(new RegExp(`${key}:\\s*"([^"]*)"`, 'u'));
345
- return trimString(match?.[1] || '');
346
- }
347
-
348
- function resolvePersonaTemplateVariables(workspacePath) {
349
- const personaConfigSource = fs.existsSync(path.join(workspacePath, 'src', 'persona', 'config.ts'))
350
- ? fs.readFileSync(path.join(workspacePath, 'src', 'persona', 'config.ts'), 'utf8')
351
- : '';
352
- const personaJson = readJsonFile(path.join(workspacePath, 'persona.json')) || {};
353
- const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
354
- const slug =
355
- readPersonaConfigLiteral(personaConfigSource, 'slug') ||
356
- trimString(personaJson.id) ||
357
- path.basename(path.resolve(workspacePath));
358
- const name =
359
- readPersonaConfigLiteral(personaConfigSource, 'name') ||
360
- trimString(personaJson.name) ||
361
- slug;
362
- const description =
363
- readPersonaConfigLiteral(personaConfigSource, 'description') ||
364
- trimString(personaJson.summary) ||
365
- name;
366
- const templateVersion =
367
- readPersonaConfigLiteral(personaConfigSource, 'templateVersion') ||
368
- trimString(personaJson.promptTemplateVersion) ||
369
- trimString(runtimeConfig.templateVersion) ||
370
- DEFAULT_TEMPLATE_VERSION;
371
-
372
- return {
373
- __OOMI_PERSONA_SLUG__: slug,
374
- __OOMI_PERSONA_NAME__: name,
375
- __OOMI_PERSONA_DESCRIPTION__: description,
376
- __OOMI_TEMPLATE_VERSION__: templateVersion,
377
- };
378
- }
379
-
380
- function renderPersonaTemplateFile({ workspacePath, relativePath }) {
381
- const sourcePath = path.join(PERSONA_TEMPLATE_ROOT, relativePath);
382
- const content = fs.readFileSync(sourcePath, 'utf8');
383
- return renderTemplateFile(content, resolvePersonaTemplateVariables(workspacePath));
384
- }
385
-
386
- function readPersonaRuntimeConfig(workspacePath) {
387
- if (!workspacePath) {
388
- return {};
389
- }
390
-
391
- const runtimeConfigPath = path.join(workspacePath, 'oomi.runtime.json');
392
- if (!fs.existsSync(runtimeConfigPath)) {
393
- return {};
394
- }
395
-
396
- try {
397
- const parsed = JSON.parse(fs.readFileSync(runtimeConfigPath, 'utf8'));
398
- return parsed && typeof parsed === 'object' ? parsed : {};
399
- } catch {
400
- return {};
401
- }
402
- }
403
-
404
- function isWebSpatialRuntime(workspacePath) {
405
- const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
406
- return trimString(runtimeConfig?.renderMode).toLowerCase() === 'webspatial';
407
- }
408
-
409
- export function syncVendoredWebSpatialPackages({
410
- workspacePath,
411
- } = {}) {
412
- if (!workspacePath || !isWebSpatialRuntime(workspacePath)) {
413
- return false;
414
- }
415
-
416
- const packageJsonPath = path.join(workspacePath, 'package.json');
417
- if (!fs.existsSync(packageJsonPath)) {
418
- return false;
419
- }
420
-
421
- const vendorTargetRoot = path.join(workspacePath, 'vendor', 'webspatial');
422
- let changed = false;
423
- if (fs.existsSync(WEBSPATIAL_VENDOR_ROOT)) {
424
- changed = copyDirectory(WEBSPATIAL_VENDOR_ROOT, vendorTargetRoot) || changed;
425
- }
426
-
427
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
428
- const dependencies =
429
- packageJson.dependencies && typeof packageJson.dependencies === 'object'
430
- ? packageJson.dependencies
431
- : {};
432
- const devDependencies =
433
- packageJson.devDependencies && typeof packageJson.devDependencies === 'object'
434
- ? packageJson.devDependencies
435
- : {};
436
- const scripts =
437
- packageJson.scripts && typeof packageJson.scripts === 'object'
438
- ? packageJson.scripts
439
- : {};
440
- if (dependencies['@webspatial/core-sdk'] !== VENDORED_WEBSPATIAL_CORE_SPEC) {
441
- dependencies['@webspatial/core-sdk'] = VENDORED_WEBSPATIAL_CORE_SPEC;
442
- changed = true;
443
- }
444
-
445
- if (dependencies['@webspatial/react-sdk'] !== VENDORED_WEBSPATIAL_REACT_SPEC) {
446
- dependencies['@webspatial/react-sdk'] = VENDORED_WEBSPATIAL_REACT_SPEC;
447
- changed = true;
448
- }
449
-
450
- if (!dependencies['@zumer/snapdom']) {
451
- dependencies['@zumer/snapdom'] = '^1.9.14';
452
- changed = true;
453
- }
454
-
455
- if (!dependencies['html2canvas']) {
456
- dependencies['html2canvas'] = '^1.4.1';
457
- changed = true;
458
- }
459
-
460
- for (const dependencyName of WEBSPATIAL_TEMPLATE_DEV_DEPENDENCIES) {
461
- const expectedVersion = trimString(
462
- PERSONA_TEMPLATE_PACKAGE_JSON?.devDependencies?.[dependencyName],
463
- );
464
- if (!expectedVersion) {
465
- continue;
466
- }
467
- if (devDependencies[dependencyName] !== expectedVersion) {
468
- devDependencies[dependencyName] = expectedVersion;
469
- changed = true;
470
- }
471
- }
472
-
473
- for (const scriptName of ['dev:avp', 'build']) {
474
- const expectedScript = trimString(PERSONA_TEMPLATE_PACKAGE_JSON?.scripts?.[scriptName]);
475
- if (!expectedScript || trimString(scripts[scriptName])) {
476
- continue;
477
- }
478
- scripts[scriptName] = expectedScript;
479
- changed = true;
480
- }
481
-
482
- if (changed) {
483
- packageJson.dependencies = dependencies;
484
- packageJson.devDependencies = devDependencies;
485
- packageJson.scripts = scripts;
486
- fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
487
- }
488
-
489
- return changed;
490
- }
491
-
492
- export function syncLegacyWebSpatialScaffoldFiles({
493
- workspacePath,
494
- } = {}) {
495
- if (!workspacePath || !isWebSpatialRuntime(workspacePath)) {
496
- return false;
497
- }
498
-
499
- let changed = false;
500
- for (const rule of LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES) {
501
- const targetPath = path.join(workspacePath, rule.relativePath);
502
- const targetExists = fs.existsSync(targetPath);
503
- const currentContent = targetExists ? fs.readFileSync(targetPath, 'utf8') : '';
504
- if (targetExists && !rule.shouldReplace(currentContent)) {
505
- continue;
506
- }
507
-
508
- const renderedContent = renderPersonaTemplateFile({
509
- workspacePath,
510
- relativePath: rule.relativePath,
511
- });
512
- if (currentContent === renderedContent) {
513
- continue;
514
- }
515
-
516
- ensureDir(path.dirname(targetPath));
517
- fs.writeFileSync(targetPath, renderedContent, 'utf8');
518
- changed = true;
519
- }
520
-
521
- return changed;
522
- }
523
-
524
- function readProcessCommandLine(pid) {
525
- const safePid = normalizePositiveInteger(pid);
526
- if (!safePid) {
527
- return '';
528
- }
529
-
530
- if (process.platform === 'linux') {
531
- const procPath = `/proc/${safePid}/cmdline`;
532
- if (fs.existsSync(procPath)) {
533
- try {
534
- return fs.readFileSync(procPath).toString().replace(/\u0000/g, ' ').trim();
535
- } catch {
536
- return '';
537
- }
538
- }
539
- }
540
-
541
- if (process.platform === 'win32') {
542
- const result = spawnSync(
543
- 'powershell',
544
- [
545
- '-NoProfile',
546
- '-Command',
547
- `(Get-CimInstance Win32_Process -Filter "ProcessId = ${safePid}").CommandLine`,
548
- ],
549
- {
550
- encoding: 'utf8',
551
- stdio: ['ignore', 'pipe', 'pipe'],
552
- }
553
- );
554
- return trimString(result.stdout || '');
555
- }
556
-
557
- const result = spawnSync('ps', ['-o', 'command=', '-p', String(safePid)], {
558
- encoding: 'utf8',
559
- stdio: ['ignore', 'pipe', 'pipe'],
560
- });
561
- return trimString(result.stdout || '');
562
- }
563
-
564
- export function matchesPersonaRuntimeCommand(commandLine, options = {}) {
565
- const text = trimString(commandLine);
566
- if (!text) {
567
- return false;
568
- }
569
-
570
- const workspacePath = trimString(options.workspacePath);
571
- const command = trimString(options.expectedCommand?.command);
572
- const args = Array.isArray(options.expectedCommand?.args) ? options.expectedCommand.args : [];
573
- const firstArg = trimString(args[0]);
574
- const localPort = normalizePositiveInteger(options.localPort);
575
-
576
- if (firstArg && !text.includes(firstArg)) {
577
- return false;
578
- }
579
-
580
- if (workspacePath && !text.includes(workspacePath)) {
581
- return false;
582
- }
583
-
584
- if (localPort) {
585
- const strictPortArg = `--port ${localPort}`;
586
- if (!text.includes(strictPortArg) && !text.includes(`:${localPort}`)) {
587
- return false;
588
- }
589
- }
590
-
591
- if (command) {
592
- const commandBase = path.basename(command);
593
- if (!text.includes(command) && !text.includes(commandBase)) {
594
- return false;
595
- }
596
- }
597
-
598
- return true;
599
- }
600
-
601
- function resolveDirectViteCommand({ workspacePath, localPort }) {
602
- if (!workspacePath) {
603
- return null;
604
- }
605
-
606
- const viteScriptPath = path.join(workspacePath, 'node_modules', 'vite', 'bin', 'vite.js');
607
- if (!fs.existsSync(viteScriptPath)) {
608
- return null;
609
- }
610
-
611
- const port = normalizePositiveInteger(localPort);
612
- const args = [viteScriptPath, '--host', resolvePersonaBindHost()];
613
- if (port) {
614
- args.push('--port', String(port), '--strictPort');
615
- }
616
-
617
- return {
618
- command: process.execPath,
619
- args,
620
- };
621
- }
622
-
623
- function wait(ms) {
624
- return new Promise((resolve) => {
625
- setTimeout(resolve, ms);
626
- });
627
- }
628
-
629
- function runProcess({ command, args, cwd }) {
630
- return new Promise((resolve, reject) => {
631
- const child =
632
- process.platform === 'win32'
633
- ? spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', buildWindowsCommandLine(command, args)], {
634
- cwd,
635
- stdio: 'inherit',
636
- shell: false,
637
- windowsHide: true,
638
- })
639
- : spawn(command, args, {
640
- cwd,
641
- stdio: 'inherit',
642
- shell: false,
643
- windowsHide: true,
644
- });
645
-
646
- child.on('error', reject);
647
- child.on('exit', (code) => {
648
- if (code === 0) {
649
- resolve();
650
- return;
651
- }
652
- reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code ?? 'unknown'}.`));
653
- });
654
- });
655
- }
656
-
657
- export async function installPersonaWorkspace({
658
- workspacePath,
659
- }) {
660
- if (!workspacePath) {
661
- throw new Error('Workspace path is required.');
662
- }
663
-
664
- syncVendoredWebSpatialPackages({ workspacePath });
665
- syncLegacyWebSpatialScaffoldFiles({ workspacePath });
666
-
667
- await runProcess({
668
- command: resolveNpmCommand(),
669
- args: ['install', '--silent', '--no-fund', '--no-audit'],
670
- cwd: workspacePath,
671
- });
672
- }
673
-
674
- export function resolvePersonaDevCommand({
675
- workspacePath,
676
- localPort,
677
- }) {
678
- const directCommand = resolveDirectViteCommand({ workspacePath, localPort });
679
- if (directCommand) {
680
- return directCommand;
681
- }
682
-
683
- const port = normalizePositiveInteger(localPort);
684
- const args = ['run', 'dev'];
685
- if (port) {
686
- args.push('--', '--host', resolvePersonaBindHost(), '--port', String(port), '--strictPort');
687
- }
688
- return {
689
- command: resolveNpmCommand(),
690
- args,
691
- };
692
- }
693
-
694
- export function resolvePersonaDevEnvironment({
695
- workspacePath,
696
- } = {}) {
697
- const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
698
- const renderMode = trimString(runtimeConfig?.renderMode).toLowerCase();
699
- if (renderMode === 'webspatial') {
700
- return {
701
- XR_ENV: 'avp',
702
- };
703
- }
704
-
705
- return {};
706
- }
707
-
708
- export function resolvePersonaHealthPath({
709
- workspacePath,
710
- fallback = '/oomi.health.json',
711
- } = {}) {
712
- const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
713
- return normalizePersonaHealthPath(
714
- trimString(runtimeConfig?.healthPath) || fallback,
715
- runtimeConfig,
716
- );
717
- }
718
-
719
- export function startPersonaWorkspace({
720
- workspacePath,
721
- logFilePath,
722
- env = {},
723
- localPort,
724
- }) {
725
- if (!workspacePath) {
726
- throw new Error('Workspace path is required.');
727
- }
728
-
729
- const resolvedLogFilePath =
730
- logFilePath ||
731
- path.join(workspacePath, '.oomi', 'runtime.log');
732
- ensureDir(path.dirname(resolvedLogFilePath));
733
-
734
- const output = fs.openSync(resolvedLogFilePath, 'a');
735
- const devCommand = resolvePersonaDevCommand({ workspacePath, localPort });
736
- const needsWindowsShellWrapper =
737
- process.platform === 'win32' && /\.cmd$/iu.test(path.basename(devCommand.command));
738
-
739
- if (needsWindowsShellWrapper) {
740
- fs.closeSync(output);
741
- const shellCommand = `${buildWindowsCommandLine(
742
- devCommand.command,
743
- devCommand.args
744
- )} >> "${resolvedLogFilePath.replace(/"/g, '""')}" 2>&1`;
745
- const child = spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', shellCommand], {
746
- cwd: workspacePath,
747
- detached: true,
748
- stdio: 'ignore',
749
- shell: false,
750
- windowsHide: true,
751
- env: {
752
- ...process.env,
753
- ...resolvePersonaDevEnvironment({ workspacePath }),
754
- ...env,
755
- },
756
- });
757
-
758
- child.unref();
759
-
760
- const pid = normalizePositiveInteger(child.pid);
761
- if (!pid) {
762
- throw new Error('Failed to determine persona workspace process id on Windows.');
763
- }
764
-
765
- return {
766
- pid,
767
- logFilePath: resolvedLogFilePath,
768
- };
769
- }
770
-
771
- let child;
772
- try {
773
- child = spawn(devCommand.command, devCommand.args, {
774
- cwd: workspacePath,
775
- detached: true,
776
- stdio: ['ignore', output, output],
777
- shell: false,
778
- windowsHide: true,
779
- env: {
780
- ...process.env,
781
- ...resolvePersonaDevEnvironment({ workspacePath }),
782
- ...env,
783
- },
784
- });
785
- } finally {
786
- fs.closeSync(output);
787
- }
788
-
789
- child.unref();
790
-
791
- return {
792
- pid: child.pid,
793
- logFilePath: resolvedLogFilePath,
794
- };
795
- }
796
-
797
- export function isPersonaWorkspaceProcessRunning(pid, options = {}) {
798
- const safePid = normalizePositiveInteger(pid);
799
- if (!safePid) {
800
- return false;
801
- }
802
-
803
- try {
804
- process.kill(safePid, 0);
805
- const hasExpectations =
806
- trimString(options.workspacePath) ||
807
- trimString(options.expectedCommand?.command) ||
808
- normalizePositiveInteger(options.localPort);
809
- if (!hasExpectations) {
810
- return true;
811
- }
812
-
813
- const commandLine = readProcessCommandLine(safePid);
814
- return matchesPersonaRuntimeCommand(commandLine, options);
815
-
816
- return true;
817
- } catch {
818
- return false;
819
- }
820
- }
821
-
822
- export async function stopPersonaWorkspace({
823
- pid,
824
- waitMs = 4000,
825
- }) {
826
- const safePid = normalizePositiveInteger(pid);
827
- if (!safePid || !isPersonaWorkspaceProcessRunning(safePid)) {
828
- return false;
829
- }
830
-
831
- try {
832
- process.kill(safePid, 'SIGTERM');
833
- } catch {
834
- return false;
835
- }
836
-
837
- const startedAt = Date.now();
838
- while (Date.now() - startedAt <= waitMs) {
839
- if (!isPersonaWorkspaceProcessRunning(safePid)) {
840
- return true;
841
- }
842
- await wait(200);
843
- }
844
-
845
- try {
846
- process.kill(safePid, 'SIGKILL');
847
- } catch {
848
- return !isPersonaWorkspaceProcessRunning(safePid);
849
- }
850
-
851
- return !isPersonaWorkspaceProcessRunning(safePid);
852
- }
853
-
854
- async function fetchHealth(url) {
855
- const response = await fetch(url, {
856
- method: 'GET',
857
- headers: {
858
- Accept: 'application/json',
859
- },
860
- });
861
-
862
- if (!response.ok) {
863
- throw new Error(`Healthcheck returned ${response.status}.`);
864
- }
865
-
866
- return response;
867
- }
868
-
869
- export async function waitForPersonaRuntime({
870
- healthcheckUrl,
871
- timeoutMs = 45000,
872
- intervalMs = 1000,
873
- }) {
874
- if (!healthcheckUrl) {
875
- throw new Error('Healthcheck URL is required.');
876
- }
877
-
878
- const startedAt = Date.now();
879
- let lastError = null;
880
- while (Date.now() - startedAt <= timeoutMs) {
881
- try {
882
- await fetchHealth(healthcheckUrl);
883
- return;
884
- } catch (error) {
885
- lastError = error;
886
- await wait(intervalMs);
887
- }
888
- }
889
-
890
- const message =
891
- lastError instanceof Error
892
- ? lastError.message
893
- : 'Timed out waiting for persona runtime healthcheck.';
894
- throw new Error(`Timed out waiting for persona runtime healthcheck: ${message}`);
895
- }
896
-
897
- export function buildLocalPersonaRuntime({
898
- localPort,
899
- healthPath,
900
- }) {
901
- const port = Number(localPort);
902
- if (!Number.isFinite(port) || port <= 0) {
903
- throw new Error('Local port is required.');
904
- }
905
-
906
- const bindHost = resolvePersonaBindHost();
907
- const reachableHost = resolvePersonaReachableHost({ bindHost });
908
- const endpoint = `http://127.0.0.1:${port}`;
909
- const reachableEndpoint = `http://${formatPersonaRuntimeHostForUrl(reachableHost)}:${port}`;
910
- const normalizedHealthPath = healthPath || '/oomi.health.json';
911
- return {
912
- transport: 'local',
913
- endpoint,
914
- reachableEndpoint,
915
- bindHost,
916
- reachableHost,
917
- localPort: port,
918
- healthcheckUrl: `${endpoint}${normalizedHealthPath}`,
919
- };
920
- }
921
-
922
- export function defaultPersonaWorkspaceRoot() {
923
- return resolveOpenclawPersonasDir();
924
- }