oomi-ai 0.2.39 → 0.2.41

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 (45) hide show
  1. package/README.md +5 -3
  2. package/lib/personaRuntimeManager.js +21 -17
  3. package/lib/personaRuntimeProcess.js +417 -49
  4. package/lib/scaffold.js +14 -14
  5. package/openclaw.plugin.json +1 -1
  6. package/package.json +10 -8
  7. package/templates/persona-app/package.json +6 -4
  8. package/templates/persona-app/src/App.css +562 -81
  9. package/templates/persona-app/src/App.tsx +2 -2
  10. package/templates/persona-app/src/main.tsx +13 -0
  11. package/templates/persona-app/src/pages/HomePage.tsx +117 -36
  12. package/templates/persona-app/src/pages/ScenePage.tsx +2 -15
  13. package/templates/persona-app/src/persona/notes.ts +3 -1
  14. package/templates/persona-app/src/spatial.ts +82 -0
  15. package/templates/persona-app/template.json +1 -1
  16. package/templates/persona-app/vendor/webspatial/FORK.md +6 -0
  17. package/templates/persona-app/vendor/webspatial/core-sdk/LICENSE +21 -0
  18. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.d.ts +906 -0
  19. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.global.js +75 -0
  20. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.global.js.map +1 -0
  21. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.d.ts +906 -0
  22. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.js +3131 -0
  23. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.js.map +1 -0
  24. package/templates/persona-app/vendor/webspatial/core-sdk/package.json +45 -0
  25. package/templates/persona-app/vendor/webspatial/react-sdk/LICENSE +21 -0
  26. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.d.ts +365 -0
  27. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js +4167 -0
  28. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js.map +1 -0
  29. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.d.ts +82 -0
  30. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.js +66 -0
  31. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.js.map +1 -0
  32. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.d.ts +2 -0
  33. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.js +18 -0
  34. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.js.map +1 -0
  35. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.d.ts +5 -0
  36. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.js +66 -0
  37. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.js.map +1 -0
  38. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.d.ts +1 -0
  39. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.js +18 -0
  40. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.js.map +1 -0
  41. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.d.ts +365 -0
  42. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js +4207 -0
  43. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js.map +1 -0
  44. package/templates/persona-app/vendor/webspatial/react-sdk/package.json +94 -0
  45. package/templates/persona-app/vite.config.ts +13 -0
package/README.md CHANGED
@@ -4,9 +4,11 @@ OpenClaw channel plugin and bridge tooling for Oomi managed chat and voice.
4
4
 
5
5
  ## Current Focus
6
6
 
7
- `0.2.39` keeps the persona automation lane, adds a stable local persona runtime manager, upgrades the Docker dev harness from a package simulator to a real OpenClaw runtime, and introduces a shared OpenClaw profile contract so local onboarding, Docker bootstrap, and future hosted agents use the same setup model:
8
- - WebSpatial-based persona scaffolding for generated Oomi apps
9
- - a high-level `oomi personas create-managed` command for agent-driven persona creation
7
+ `0.2.40` keeps the persona automation lane, adds a stable local persona runtime manager, upgrades the Docker dev harness from a package simulator to a real OpenClaw runtime, and introduces a shared OpenClaw profile contract so local onboarding, Docker bootstrap, and future hosted agents use the same setup model:
8
+ - WebSpatial-based persona scaffolding for generated Oomi apps
9
+ - vendored AndroidXR WebSpatial fork sync for persona runtimes, including the npm-safe package metadata needed by OpenClaw installs
10
+ - managed persona runtime installs now detect vendored WebSpatial drift and reinstall automatically so existing workspaces pick up XR runtime fixes
11
+ - a high-level `oomi personas create-managed` command for agent-driven persona creation
10
12
  - a stable `oomi personas launch-managed` flow for local persona hosting under `OPENCLAW_WORKSPACE/personas`
11
13
  - a matching `oomi personas delete` flow that stops managed runtimes and removes the persona workspace from the OpenClaw machine
12
14
  - shared OpenClaw path handling for isolated local or containerized dev roots
@@ -6,13 +6,15 @@ import { resolveOpenclawLegacyPersonasDir } from './openclawPaths.js';
6
6
  import {
7
7
  buildLocalPersonaRuntime,
8
8
  defaultPersonaWorkspaceRoot,
9
- installPersonaWorkspace,
10
- isPersonaWorkspaceProcessRunning,
11
- resolvePersonaDevCommand,
12
- startPersonaWorkspace,
13
- stopPersonaWorkspace,
14
- waitForPersonaRuntime,
15
- } from './personaRuntimeProcess.js';
9
+ installPersonaWorkspace,
10
+ isPersonaWorkspaceProcessRunning,
11
+ resolvePersonaDevCommand,
12
+ syncLegacyWebSpatialScaffoldFiles,
13
+ syncVendoredWebSpatialPackages,
14
+ startPersonaWorkspace,
15
+ stopPersonaWorkspace,
16
+ waitForPersonaRuntime,
17
+ } from './personaRuntimeProcess.js';
16
18
  import {
17
19
  readPersonaRuntimeState,
18
20
  resolvePersonaRuntimeLogPath,
@@ -193,16 +195,18 @@ async function ensureWorkspaceScaffold({
193
195
  };
194
196
  }
195
197
 
196
- async function ensureWorkspaceInstall({
197
- workspacePath,
198
- forceInstall = false,
199
- }) {
200
- const nodeModulesPath = path.join(workspacePath, 'node_modules');
201
- if (!forceInstall && fs.existsSync(nodeModulesPath)) {
202
- return false;
203
- }
204
-
205
- await installPersonaWorkspace({ workspacePath });
198
+ async function ensureWorkspaceInstall({
199
+ workspacePath,
200
+ forceInstall = false,
201
+ }) {
202
+ const packageSyncChanged = syncVendoredWebSpatialPackages({ workspacePath });
203
+ syncLegacyWebSpatialScaffoldFiles({ workspacePath });
204
+ const nodeModulesPath = path.join(workspacePath, 'node_modules');
205
+ if (!forceInstall && fs.existsSync(nodeModulesPath) && !packageSyncChanged) {
206
+ return false;
207
+ }
208
+
209
+ await installPersonaWorkspace({ workspacePath });
206
210
  return true;
207
211
  }
208
212
 
@@ -1,16 +1,180 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { spawn, spawnSync } from 'node:child_process';
4
-
5
- import { resolveOpenclawPersonasDir } from './openclawPaths.js';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { resolveOpenclawPersonasDir } from './openclawPaths.js';
7
+ import {
8
+ DEFAULT_TEMPLATE_ID,
9
+ DEFAULT_TEMPLATE_VERSION,
10
+ renderTemplateFile,
11
+ resolveTemplateRoot,
12
+ } from './template.js';
13
+
14
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+ const PERSONA_TEMPLATE_ROOT = resolveTemplateRoot(
16
+ DEFAULT_TEMPLATE_ID,
17
+ DEFAULT_TEMPLATE_VERSION,
18
+ );
19
+ const PERSONA_TEMPLATE_PACKAGE_JSON_PATH = path.join(PERSONA_TEMPLATE_ROOT, 'package.json');
20
+ const PERSONA_TEMPLATE_PACKAGE_JSON = fs.existsSync(PERSONA_TEMPLATE_PACKAGE_JSON_PATH)
21
+ ? JSON.parse(fs.readFileSync(PERSONA_TEMPLATE_PACKAGE_JSON_PATH, 'utf8'))
22
+ : {};
23
+ const WEBSPATIAL_VENDOR_ROOT = path.join(
24
+ PACKAGE_ROOT,
25
+ 'templates',
26
+ 'persona-app',
27
+ 'vendor',
28
+ 'webspatial',
29
+ );
30
+ const VENDORED_WEBSPATIAL_CORE_SPEC = 'file:./vendor/webspatial/core-sdk';
31
+ const VENDORED_WEBSPATIAL_REACT_SPEC = 'file:./vendor/webspatial/react-sdk';
32
+ const WEBSPATIAL_TEMPLATE_DEV_DEPENDENCIES = [
33
+ '@webspatial/builder',
34
+ '@webspatial/platform-visionos',
35
+ '@webspatial/vite-plugin',
36
+ ];
37
+ const LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES = [
38
+ {
39
+ relativePath: path.join('src', 'spatial.ts'),
40
+ shouldReplace: (content) =>
41
+ !content.includes('WEBSPATIAL_FORK_REPOSITORY') ||
42
+ !content.includes('configurePersonaScene') ||
43
+ content.includes('WEBSPATIAL_FORK_COMMIT = "b2746721e4fe6b4f86dac0ea55938074eea00cda"') ||
44
+ content.includes('WEBSPATIAL_FORK_COMMIT = "8904ac8fec48fe49ee14d1739237bd1afb2894fe"'),
45
+ },
46
+ {
47
+ relativePath: path.join('src', 'main.tsx'),
48
+ shouldReplace: (content) =>
49
+ !content.includes('snapdom') &&
50
+ !content.includes('html2canvas') &&
51
+ content.includes('createRoot(document.getElementById("root")!)'),
52
+ },
53
+ {
54
+ relativePath: path.join('src', 'App.tsx'),
55
+ shouldReplace: (content) =>
56
+ content.includes('<Route path="/" element={<HomePage />} />') ||
57
+ content.includes('<Route path="/scene" element={<ScenePage />} />'),
58
+ },
59
+ {
60
+ relativePath: path.join('src', 'pages', 'HomePage.tsx'),
61
+ shouldReplace: (content) =>
62
+ (content.includes('Open Spatial Scene') &&
63
+ content.includes('Open Scene Route')) ||
64
+ (content.includes('Launch Spatial Surface') &&
65
+ content.includes('Open Browser Preview') &&
66
+ content.includes('Focused surface') &&
67
+ !content.includes('sceneMode')) ||
68
+ content.includes('enable-xr-monitor={sceneMode || undefined}') ||
69
+ content.includes('enable-xr={sceneMode || undefined}') ||
70
+ (
71
+ content.includes('persona-panel persona-runtime" enable-xr') ||
72
+ content.includes('persona-button" onClick={openPersonaScene} enable-xr') ||
73
+ content.includes('persona-link" to="/scene" target="_blank" enable-xr') ||
74
+ content.includes('persona-card" enable-xr style={xrStyle(')
75
+ ),
76
+ },
77
+ {
78
+ relativePath: path.join('src', 'pages', 'ScenePage.tsx'),
79
+ shouldReplace: (content) =>
80
+ (
81
+ !content.includes('enable-xr-monitor') &&
82
+ content.includes(
83
+ 'This route is intentionally separate so WebSpatial scene launching has a dedicated',
84
+ )
85
+ ) ||
86
+ (
87
+ content.includes('Awaiting AndroidXR interaction') &&
88
+ content.includes('Interaction Console') &&
89
+ content.includes('Fork-backed proof points')
90
+ ),
91
+ },
92
+ {
93
+ relativePath: path.join('src', 'App.css'),
94
+ shouldReplace: (content) =>
95
+ (content.includes('.scene-panel') && !content.includes('.scene-interaction-grid')) ||
96
+ content.includes('html.is-spatial .persona-runtime {') ||
97
+ content.includes('html.is-spatial .persona-scene-root {') ||
98
+ content.includes('html.is-spatial .persona-button,') ||
99
+ content.includes('html.is-spatial .persona-card,'),
100
+ },
101
+ {
102
+ relativePath: 'vite.config.ts',
103
+ shouldReplace: (content) =>
104
+ content.includes('webSpatial()') && !content.includes('optimizeDeps'),
105
+ },
106
+ ];
6
107
 
7
108
  function resolveNpmCommand() {
8
109
  return process.platform === 'win32' ? 'npm.cmd' : 'npm';
9
110
  }
10
111
 
11
- function ensureDir(dirPath) {
12
- fs.mkdirSync(dirPath, { recursive: true });
13
- }
112
+ function ensureDir(dirPath) {
113
+ fs.mkdirSync(dirPath, { recursive: true });
114
+ }
115
+
116
+ function readJsonFile(filePath) {
117
+ if (!fs.existsSync(filePath)) {
118
+ return null;
119
+ }
120
+
121
+ try {
122
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ function copyDirectory(sourcePath, targetPath) {
129
+ ensureDir(targetPath);
130
+ let changed = false;
131
+ const sourceEntries = fs.readdirSync(sourcePath, { withFileTypes: true });
132
+ const sourceEntryNames = new Set(sourceEntries.map((entry) => entry.name));
133
+ const existingTargetEntries = fs.readdirSync(targetPath, { withFileTypes: true });
134
+
135
+ for (const entry of existingTargetEntries) {
136
+ if (sourceEntryNames.has(entry.name)) {
137
+ continue;
138
+ }
139
+ fs.rmSync(path.join(targetPath, entry.name), { recursive: true, force: true });
140
+ changed = true;
141
+ }
142
+
143
+ for (const entry of sourceEntries) {
144
+ const sourceEntryPath = path.join(sourcePath, entry.name);
145
+ const targetEntryPath = path.join(targetPath, entry.name);
146
+ const targetExists = fs.existsSync(targetEntryPath);
147
+
148
+ if (entry.isDirectory()) {
149
+ if (targetExists && !fs.statSync(targetEntryPath).isDirectory()) {
150
+ fs.rmSync(targetEntryPath, { recursive: true, force: true });
151
+ changed = true;
152
+ }
153
+ if (copyDirectory(sourceEntryPath, targetEntryPath)) {
154
+ changed = true;
155
+ }
156
+ continue;
157
+ }
158
+
159
+ if (targetExists && fs.statSync(targetEntryPath).isDirectory()) {
160
+ fs.rmSync(targetEntryPath, { recursive: true, force: true });
161
+ changed = true;
162
+ }
163
+
164
+ const sourceBuffer = fs.readFileSync(sourceEntryPath);
165
+ const targetBuffer = fs.existsSync(targetEntryPath)
166
+ ? fs.readFileSync(targetEntryPath)
167
+ : null;
168
+ if (targetBuffer && sourceBuffer.equals(targetBuffer)) {
169
+ continue;
170
+ }
171
+
172
+ fs.copyFileSync(sourceEntryPath, targetEntryPath);
173
+ changed = true;
174
+ }
175
+
176
+ return changed;
177
+ }
14
178
 
15
179
  function quoteWindowsCommandPart(value) {
16
180
  const text = String(value);
@@ -43,6 +207,191 @@ function trimString(value) {
43
207
  return typeof value === 'string' ? value.trim() : '';
44
208
  }
45
209
 
210
+ function readPersonaConfigLiteral(source, key) {
211
+ if (!source) {
212
+ return '';
213
+ }
214
+
215
+ const match = source.match(new RegExp(`${key}:\\s*"([^"]*)"`, 'u'));
216
+ return trimString(match?.[1] || '');
217
+ }
218
+
219
+ function resolvePersonaTemplateVariables(workspacePath) {
220
+ const personaConfigSource = fs.existsSync(path.join(workspacePath, 'src', 'persona', 'config.ts'))
221
+ ? fs.readFileSync(path.join(workspacePath, 'src', 'persona', 'config.ts'), 'utf8')
222
+ : '';
223
+ const personaJson = readJsonFile(path.join(workspacePath, 'persona.json')) || {};
224
+ const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
225
+ const slug =
226
+ readPersonaConfigLiteral(personaConfigSource, 'slug') ||
227
+ trimString(personaJson.id) ||
228
+ path.basename(path.resolve(workspacePath));
229
+ const name =
230
+ readPersonaConfigLiteral(personaConfigSource, 'name') ||
231
+ trimString(personaJson.name) ||
232
+ slug;
233
+ const description =
234
+ readPersonaConfigLiteral(personaConfigSource, 'description') ||
235
+ trimString(personaJson.summary) ||
236
+ name;
237
+ const templateVersion =
238
+ readPersonaConfigLiteral(personaConfigSource, 'templateVersion') ||
239
+ trimString(personaJson.promptTemplateVersion) ||
240
+ trimString(runtimeConfig.templateVersion) ||
241
+ DEFAULT_TEMPLATE_VERSION;
242
+
243
+ return {
244
+ __OOMI_PERSONA_SLUG__: slug,
245
+ __OOMI_PERSONA_NAME__: name,
246
+ __OOMI_PERSONA_DESCRIPTION__: description,
247
+ __OOMI_TEMPLATE_VERSION__: templateVersion,
248
+ };
249
+ }
250
+
251
+ function renderPersonaTemplateFile({ workspacePath, relativePath }) {
252
+ const sourcePath = path.join(PERSONA_TEMPLATE_ROOT, relativePath);
253
+ const content = fs.readFileSync(sourcePath, 'utf8');
254
+ return renderTemplateFile(content, resolvePersonaTemplateVariables(workspacePath));
255
+ }
256
+
257
+ function readPersonaRuntimeConfig(workspacePath) {
258
+ if (!workspacePath) {
259
+ return {};
260
+ }
261
+
262
+ const runtimeConfigPath = path.join(workspacePath, 'oomi.runtime.json');
263
+ if (!fs.existsSync(runtimeConfigPath)) {
264
+ return {};
265
+ }
266
+
267
+ try {
268
+ const parsed = JSON.parse(fs.readFileSync(runtimeConfigPath, 'utf8'));
269
+ return parsed && typeof parsed === 'object' ? parsed : {};
270
+ } catch {
271
+ return {};
272
+ }
273
+ }
274
+
275
+ function isWebSpatialRuntime(workspacePath) {
276
+ const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
277
+ return trimString(runtimeConfig?.renderMode).toLowerCase() === 'webspatial';
278
+ }
279
+
280
+ export function syncVendoredWebSpatialPackages({
281
+ workspacePath,
282
+ } = {}) {
283
+ if (!workspacePath || !isWebSpatialRuntime(workspacePath)) {
284
+ return false;
285
+ }
286
+
287
+ const packageJsonPath = path.join(workspacePath, 'package.json');
288
+ if (!fs.existsSync(packageJsonPath)) {
289
+ return false;
290
+ }
291
+
292
+ const vendorTargetRoot = path.join(workspacePath, 'vendor', 'webspatial');
293
+ let changed = false;
294
+ if (fs.existsSync(WEBSPATIAL_VENDOR_ROOT)) {
295
+ changed = copyDirectory(WEBSPATIAL_VENDOR_ROOT, vendorTargetRoot) || changed;
296
+ }
297
+
298
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
299
+ const dependencies =
300
+ packageJson.dependencies && typeof packageJson.dependencies === 'object'
301
+ ? packageJson.dependencies
302
+ : {};
303
+ const devDependencies =
304
+ packageJson.devDependencies && typeof packageJson.devDependencies === 'object'
305
+ ? packageJson.devDependencies
306
+ : {};
307
+ const scripts =
308
+ packageJson.scripts && typeof packageJson.scripts === 'object'
309
+ ? packageJson.scripts
310
+ : {};
311
+ if (dependencies['@webspatial/core-sdk'] !== VENDORED_WEBSPATIAL_CORE_SPEC) {
312
+ dependencies['@webspatial/core-sdk'] = VENDORED_WEBSPATIAL_CORE_SPEC;
313
+ changed = true;
314
+ }
315
+
316
+ if (dependencies['@webspatial/react-sdk'] !== VENDORED_WEBSPATIAL_REACT_SPEC) {
317
+ dependencies['@webspatial/react-sdk'] = VENDORED_WEBSPATIAL_REACT_SPEC;
318
+ changed = true;
319
+ }
320
+
321
+ if (!dependencies['@zumer/snapdom']) {
322
+ dependencies['@zumer/snapdom'] = '^1.9.14';
323
+ changed = true;
324
+ }
325
+
326
+ if (!dependencies['html2canvas']) {
327
+ dependencies['html2canvas'] = '^1.4.1';
328
+ changed = true;
329
+ }
330
+
331
+ for (const dependencyName of WEBSPATIAL_TEMPLATE_DEV_DEPENDENCIES) {
332
+ const expectedVersion = trimString(
333
+ PERSONA_TEMPLATE_PACKAGE_JSON?.devDependencies?.[dependencyName],
334
+ );
335
+ if (!expectedVersion) {
336
+ continue;
337
+ }
338
+ if (devDependencies[dependencyName] !== expectedVersion) {
339
+ devDependencies[dependencyName] = expectedVersion;
340
+ changed = true;
341
+ }
342
+ }
343
+
344
+ for (const scriptName of ['dev:avp', 'build']) {
345
+ const expectedScript = trimString(PERSONA_TEMPLATE_PACKAGE_JSON?.scripts?.[scriptName]);
346
+ if (!expectedScript || trimString(scripts[scriptName])) {
347
+ continue;
348
+ }
349
+ scripts[scriptName] = expectedScript;
350
+ changed = true;
351
+ }
352
+
353
+ if (changed) {
354
+ packageJson.dependencies = dependencies;
355
+ packageJson.devDependencies = devDependencies;
356
+ packageJson.scripts = scripts;
357
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
358
+ }
359
+
360
+ return changed;
361
+ }
362
+
363
+ export function syncLegacyWebSpatialScaffoldFiles({
364
+ workspacePath,
365
+ } = {}) {
366
+ if (!workspacePath || !isWebSpatialRuntime(workspacePath)) {
367
+ return false;
368
+ }
369
+
370
+ let changed = false;
371
+ for (const rule of LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES) {
372
+ const targetPath = path.join(workspacePath, rule.relativePath);
373
+ const targetExists = fs.existsSync(targetPath);
374
+ const currentContent = targetExists ? fs.readFileSync(targetPath, 'utf8') : '';
375
+ if (targetExists && !rule.shouldReplace(currentContent)) {
376
+ continue;
377
+ }
378
+
379
+ const renderedContent = renderPersonaTemplateFile({
380
+ workspacePath,
381
+ relativePath: rule.relativePath,
382
+ });
383
+ if (currentContent === renderedContent) {
384
+ continue;
385
+ }
386
+
387
+ ensureDir(path.dirname(targetPath));
388
+ fs.writeFileSync(targetPath, renderedContent, 'utf8');
389
+ changed = true;
390
+ }
391
+
392
+ return changed;
393
+ }
394
+
46
395
  function readProcessCommandLine(pid) {
47
396
  const safePid = normalizePositiveInteger(pid);
48
397
  if (!safePid) {
@@ -176,25 +525,28 @@ function runProcess({ command, args, cwd }) {
176
525
  });
177
526
  }
178
527
 
179
- export async function installPersonaWorkspace({
180
- workspacePath,
181
- }) {
182
- if (!workspacePath) {
183
- throw new Error('Workspace path is required.');
184
- }
185
-
186
- await runProcess({
187
- command: resolveNpmCommand(),
188
- args: ['install', '--silent', '--no-fund', '--no-audit'],
528
+ export async function installPersonaWorkspace({
529
+ workspacePath,
530
+ }) {
531
+ if (!workspacePath) {
532
+ throw new Error('Workspace path is required.');
533
+ }
534
+
535
+ syncVendoredWebSpatialPackages({ workspacePath });
536
+ syncLegacyWebSpatialScaffoldFiles({ workspacePath });
537
+
538
+ await runProcess({
539
+ command: resolveNpmCommand(),
540
+ args: ['install', '--silent', '--no-fund', '--no-audit'],
189
541
  cwd: workspacePath,
190
542
  });
191
543
  }
192
544
 
193
- export function resolvePersonaDevCommand({
194
- workspacePath,
195
- localPort,
196
- }) {
197
- const directCommand = resolveDirectViteCommand({ workspacePath, localPort });
545
+ export function resolvePersonaDevCommand({
546
+ workspacePath,
547
+ localPort,
548
+ }) {
549
+ const directCommand = resolveDirectViteCommand({ workspacePath, localPort });
198
550
  if (directCommand) {
199
551
  return directCommand;
200
552
  }
@@ -206,15 +558,29 @@ export function resolvePersonaDevCommand({
206
558
  }
207
559
  return {
208
560
  command: resolveNpmCommand(),
209
- args,
210
- };
211
- }
212
-
213
- export function startPersonaWorkspace({
214
- workspacePath,
215
- logFilePath,
216
- env = {},
217
- localPort,
561
+ args,
562
+ };
563
+ }
564
+
565
+ export function resolvePersonaDevEnvironment({
566
+ workspacePath,
567
+ } = {}) {
568
+ const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
569
+ const renderMode = trimString(runtimeConfig?.renderMode).toLowerCase();
570
+ if (renderMode === 'webspatial') {
571
+ return {
572
+ XR_ENV: 'avp',
573
+ };
574
+ }
575
+
576
+ return {};
577
+ }
578
+
579
+ export function startPersonaWorkspace({
580
+ workspacePath,
581
+ logFilePath,
582
+ env = {},
583
+ localPort,
218
584
  }) {
219
585
  if (!workspacePath) {
220
586
  throw new Error('Workspace path is required.');
@@ -238,15 +604,16 @@ export function startPersonaWorkspace({
238
604
  )} >> "${resolvedLogFilePath.replace(/"/g, '""')}" 2>&1`;
239
605
  const child = spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', shellCommand], {
240
606
  cwd: workspacePath,
241
- detached: true,
242
- stdio: 'ignore',
243
- shell: false,
244
- windowsHide: true,
245
- env: {
246
- ...process.env,
247
- ...env,
248
- },
249
- });
607
+ detached: true,
608
+ stdio: 'ignore',
609
+ shell: false,
610
+ windowsHide: true,
611
+ env: {
612
+ ...process.env,
613
+ ...resolvePersonaDevEnvironment({ workspacePath }),
614
+ ...env,
615
+ },
616
+ });
250
617
 
251
618
  child.unref();
252
619
 
@@ -265,15 +632,16 @@ export function startPersonaWorkspace({
265
632
  try {
266
633
  child = spawn(devCommand.command, devCommand.args, {
267
634
  cwd: workspacePath,
268
- detached: true,
269
- stdio: ['ignore', output, output],
270
- shell: false,
271
- windowsHide: true,
272
- env: {
273
- ...process.env,
274
- ...env,
275
- },
276
- });
635
+ detached: true,
636
+ stdio: ['ignore', output, output],
637
+ shell: false,
638
+ windowsHide: true,
639
+ env: {
640
+ ...process.env,
641
+ ...resolvePersonaDevEnvironment({ workspacePath }),
642
+ ...env,
643
+ },
644
+ });
277
645
  } finally {
278
646
  fs.closeSync(output);
279
647
  }
package/lib/scaffold.js CHANGED
@@ -82,9 +82,9 @@ export function scaffoldPersonaApp({
82
82
  }
83
83
 
84
84
  const resolvedOutDir = path.resolve(outDir);
85
- const templateRoot = resolveTemplateRoot(templateId, templateVersion);
86
- const descriptor = readTemplateDescriptor(templateRoot);
87
- const variables = {
85
+ const templateRoot = resolveTemplateRoot(templateId, templateVersion);
86
+ const descriptor = readTemplateDescriptor(templateRoot);
87
+ const variables = {
88
88
  __OOMI_PERSONA_SLUG__: safeSlug,
89
89
  __OOMI_PERSONA_NAME__: safeName,
90
90
  __OOMI_PERSONA_DESCRIPTION__: safeDescription,
@@ -95,14 +95,14 @@ export function scaffoldPersonaApp({
95
95
  copyTemplateTree(templateRoot, resolvedOutDir, variables);
96
96
 
97
97
  return {
98
- ok: true,
99
- templateId,
100
- templateVersion,
101
- slug: safeSlug,
102
- outDir: resolvedOutDir,
103
- startCommand: `cd ${resolvedOutDir} && npm install && npm run dev`,
104
- healthPath: descriptor.healthPath,
105
- editableZones: descriptor.editableZones,
106
- defaultPort: descriptor.defaultPort,
107
- };
108
- }
98
+ ok: true,
99
+ templateId,
100
+ templateVersion,
101
+ slug: safeSlug,
102
+ outDir: resolvedOutDir,
103
+ startCommand: `cd ${resolvedOutDir} && npm install && ${descriptor.startCommand || 'npm run dev'}`,
104
+ healthPath: descriptor.healthPath,
105
+ editableZones: descriptor.editableZones,
106
+ defaultPort: descriptor.defaultPort,
107
+ };
108
+ }
@@ -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.39",
5
+ "version": "0.2.41",
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.39",
3
+ "version": "0.2.41",
4
4
  "description": "Oomi OpenClaw channel plugin and bridge tooling",
5
5
  "bin": {
6
6
  "oomi": "bin/oomi-ai.js"
@@ -58,10 +58,12 @@
58
58
  "bin/sessionBridgeState.js",
59
59
  "lib",
60
60
  "openclaw.plugin.json",
61
- "openclaw.extension.js",
62
- "agent_instructions.md",
63
- "README.md",
64
- "skills/oomi",
65
- "templates/persona-app"
66
- ]
67
- }
61
+ "openclaw.extension.js",
62
+ "agent_instructions.md",
63
+ "README.md",
64
+ "skills/oomi",
65
+ "templates/persona-app",
66
+ "templates/persona-app/vendor/webspatial/core-sdk/dist",
67
+ "templates/persona-app/vendor/webspatial/react-sdk/dist"
68
+ ]
69
+ }
@@ -12,8 +12,10 @@
12
12
  "lint": "eslint ."
13
13
  },
14
14
  "dependencies": {
15
- "@webspatial/core-sdk": "^0.1.16",
16
- "@webspatial/react-sdk": "^0.1.16",
15
+ "@webspatial/core-sdk": "file:./vendor/webspatial/core-sdk",
16
+ "@webspatial/react-sdk": "file:./vendor/webspatial/react-sdk",
17
+ "@zumer/snapdom": "^1.9.14",
18
+ "html2canvas": "^1.4.1",
17
19
  "react": "^19.0.0",
18
20
  "react-dom": "^19.0.0",
19
21
  "react-router-dom": "^7.4.0",
@@ -24,8 +26,8 @@
24
26
  "@types/react": "^19.0.10",
25
27
  "@types/react-dom": "^19.0.4",
26
28
  "@vitejs/plugin-react": "^4.3.4",
27
- "@webspatial/builder": "^0.1.16",
28
- "@webspatial/platform-visionos": "^0.1.16",
29
+ "@webspatial/builder": "^1.2.1",
30
+ "@webspatial/platform-visionos": "^1.2.1",
29
31
  "@webspatial/vite-plugin": "^0.1.7",
30
32
  "cross-env": "^7.0.3",
31
33
  "dotenv-cli": "^8.0.0",