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/bin/oomi-ai.js CHANGED
@@ -5,28 +5,59 @@ import path from 'path';
5
5
  import { spawn, spawnSync } from 'child_process';
6
6
  import { createPrivateKey, createPublicKey, randomUUID, sign as cryptoSign } from 'crypto';
7
7
  import net from 'net';
8
- import { lookup as dnsLookup } from 'dns/promises';
9
- import { fileURLToPath } from 'url';
10
- import WebSocket from 'ws';
11
- import { scaffoldPersonaApp } from '../lib/scaffold.js';
8
+ import { lookup as dnsLookup } from 'dns/promises';
9
+ import { fileURLToPath } from 'url';
10
+ import WebSocket from 'ws';
11
+ import { scaffoldPersonaApp } from '../lib/scaffold.js';
12
12
  import { createPersonaApiClient } from '../lib/personaApiClient.js';
13
13
  import { startPersonaJobPoller } from '../lib/personaJobPoller.js';
14
- import { executePersonaJob } from '../lib/personaJobExecutor.js';
15
- import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
14
+ import { startPersonaRuntimeSupervisor } from '../lib/personaRuntimeSupervisor.js';
15
+ import { executePersonaJob, extractPersonaJobPayload } from '../lib/personaJobExecutor.js';
16
+ import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
17
+ import {
18
+ resolveOpenclawBridgeLiveLogPath,
19
+ resolveOpenclawBridgeLockPath,
20
+ resolveOpenclawBridgeStatePath,
21
+ resolveOpenclawBridgeStatusPath,
22
+ resolveOpenclawConfigCandidates,
23
+ resolveOpenclawHome,
24
+ resolveOpenclawIdentityPath,
25
+ resolveOpenclawProfilePath,
26
+ resolveOpenclawSkillsDir,
27
+ resolveOpenclawUpdateStatePath,
28
+ resolveOpenclawWorkspaceRoot,
29
+ } from '../lib/openclawPaths.js';
30
+ import {
31
+ applyOpenclawProfile,
32
+ buildOomiDevLocalProfile,
33
+ readOpenclawProfile,
34
+ writeOpenclawProfile,
35
+ } from '../lib/openclawProfile.js';
36
+ import {
37
+ buildLocalPersonaRuntime,
38
+ defaultPersonaWorkspaceRoot,
39
+ installPersonaWorkspace,
40
+ isPersonaWorkspaceProcessRunning,
41
+ resolvePersonaDevCommand,
42
+ startPersonaWorkspace,
43
+ stopPersonaWorkspace,
44
+ waitForPersonaRuntime,
45
+ } from '../lib/personaRuntimeProcess.js';
16
46
  import {
17
- buildLocalPersonaRuntime,
18
- defaultPersonaWorkspaceRoot,
19
- installPersonaWorkspace,
20
- startPersonaWorkspace,
21
- waitForPersonaRuntime,
22
- } from '../lib/personaRuntimeProcess.js';
23
- import { ensureSessionBridge, flushSessionQueue, flushWaitingForConnect, forwardFrameToSession } from './sessionBridgeState.js';
47
+ destroyManagedPersonaRuntime,
48
+ getManagedPersonaRuntimeStatus,
49
+ launchManagedPersonaRuntime,
50
+ slugifyPersonaName,
51
+ stopManagedPersonaRuntime,
52
+ } from '../lib/personaRuntimeManager.js';
53
+ import { startLocalGatewayAgentServer } from '../lib/openclawDevGateway.js';
54
+ import { ensureSessionBridge, flushSessionQueue, flushWaitingForConnect, forwardFrameToSession } from './sessionBridgeState.js';
24
55
 
25
56
  const MARKER_START = '<oomi-agent-instructions>';
26
57
  const MARKER_END = '</oomi-agent-instructions>';
27
58
 
28
59
  const PACKAGE_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
29
- const UPDATE_STATE_FILE = path.join(os.homedir(), '.openclaw', 'oomi-ai-update-check.json');
60
+ const UPDATE_STATE_FILE = resolveOpenclawUpdateStatePath();
30
61
  const DEFAULT_UPDATE_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
31
62
  const DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 1200;
32
63
  const BRIDGE_RECONNECT_BASE_MS = 2000;
@@ -39,32 +70,32 @@ const BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS = parsePositiveInteger(
39
70
  process.env.OOMI_BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS,
40
71
  3000
41
72
  );
42
- const BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS = parsePositiveInteger(
43
- process.env.OOMI_BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS,
44
- 30000
45
- );
46
- const BRIDGE_LAUNCHD_LABEL = 'ai.oomi.bridge';
47
- const DEBUG_PROVIDER_ENV_KEYS = [
48
- 'QWEN_REALTIME_API_KEY',
49
- 'QWEN_REALTIME_BASE_URL',
50
- 'QWEN_REALTIME_ASR_MODEL',
51
- 'QWEN_REALTIME_TTS_MODEL',
52
- 'QWEN_REALTIME_TTS_VOICE',
53
- 'QWEN_REALTIME_LANGUAGE',
54
- ];
55
- const DEVICE_IDENTITY_PATH = path.join(os.homedir(), '.openclaw', 'identity', 'device.json');
56
- const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
57
- const BRIDGE_DEBUG_ENABLED = process.env.OOMI_BRIDGE_DEBUG === '1';
58
-
59
- function bridgeDebugLog(...args) {
60
- if (!BRIDGE_DEBUG_ENABLED) return;
61
- console.log(...args);
62
- }
63
-
64
- function bridgeDebugWarn(...args) {
65
- if (!BRIDGE_DEBUG_ENABLED) return;
66
- console.warn(...args);
67
- }
73
+ const BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS = parsePositiveInteger(
74
+ process.env.OOMI_BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS,
75
+ 30000
76
+ );
77
+ const BRIDGE_LAUNCHD_LABEL = 'ai.oomi.bridge';
78
+ const DEBUG_PROVIDER_ENV_KEYS = [
79
+ 'QWEN_REALTIME_API_KEY',
80
+ 'QWEN_REALTIME_BASE_URL',
81
+ 'QWEN_REALTIME_ASR_MODEL',
82
+ 'QWEN_REALTIME_TTS_MODEL',
83
+ 'QWEN_REALTIME_TTS_VOICE',
84
+ 'QWEN_REALTIME_LANGUAGE',
85
+ ];
86
+ const DEVICE_IDENTITY_PATH = resolveOpenclawIdentityPath();
87
+ const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
88
+ const BRIDGE_DEBUG_ENABLED = process.env.OOMI_BRIDGE_DEBUG === '1';
89
+
90
+ function bridgeDebugLog(...args) {
91
+ if (!BRIDGE_DEBUG_ENABLED) return;
92
+ console.log(...args);
93
+ }
94
+
95
+ function bridgeDebugWarn(...args) {
96
+ if (!BRIDGE_DEBUG_ENABLED) return;
97
+ console.warn(...args);
98
+ }
68
99
 
69
100
  function parsePositiveInteger(value, fallback) {
70
101
  const num = Number(value);
@@ -188,14 +219,22 @@ Commands:
188
219
  openclaw install
189
220
  Install agent instructions and the Oomi skill into OpenClaw.
190
221
 
191
- openclaw bridge [start|ensure|stop|restart|ps]
192
- Manage local OpenClaw-to-Oomi bridge lifecycle (singleton).
193
- openclaw bridge service [install|start|stop|restart|status|uninstall]
194
- Manage macOS launchd bridge supervision.
195
- openclaw debug assistant-final
196
- Replay an assistant chat.final frame through spoken-metadata normalization.
197
- openclaw debug tts-pipeline
198
- Replay an assistant chat.final through local backend voice handling.
222
+ openclaw bridge [start|ensure|stop|restart|ps]
223
+ Manage local OpenClaw-to-Oomi bridge lifecycle (singleton).
224
+ openclaw bridge service [install|start|stop|restart|status|uninstall]
225
+ Manage macOS launchd bridge supervision.
226
+ openclaw profile init
227
+ Write a deterministic OpenClaw profile for local/dev or hosted setup flows.
228
+ openclaw profile apply
229
+ Apply an OpenClaw profile into the current OpenClaw home/config.
230
+ openclaw debug assistant-final
231
+ Replay an assistant chat.final frame through spoken-metadata normalization.
232
+ openclaw debug tts-pipeline
233
+ Replay an assistant chat.final through local backend voice handling.
234
+ openclaw debug local-gateway-agent
235
+ Run a tiny local OpenClaw gateway/agent for Docker dev testing.
236
+ openclaw debug persona-runtime
237
+ Scaffold, launch, and stop a managed persona runtime locally.
199
238
 
200
239
  openclaw pair
201
240
  Pair this OpenClaw host with Oomi and start bridge (single command).
@@ -212,26 +251,34 @@ Commands:
212
251
  personas sync
213
252
  Sync personas from the repo into the Oomi backend registry.
214
253
 
215
- personas create <id>
216
- Create a new persona manifest and optionally sync it to the backend.
217
- personas create-managed [slug]
218
- Create a managed persona in Oomi and enqueue its build job for the linked device.
219
- personas scaffold <slug>
220
- Create an Oomi-managed persona app scaffold for agent customization.
254
+ personas create <id>
255
+ Create a new persona manifest and optionally sync it to the backend.
256
+ personas create-managed [slug]
257
+ Create a managed persona in Oomi and enqueue its build job for the linked device.
258
+ personas launch-managed [slug]
259
+ Launch or reuse a managed persona runtime on this OpenClaw machine.
260
+ personas scaffold <slug>
261
+ Create an Oomi-managed persona app scaffold for agent customization.
262
+ personas status <slug>
263
+ Show local managed persona runtime state for a persona slug.
264
+ personas stop <slug>
265
+ Stop a locally running managed persona runtime.
266
+ personas delete <slug>
267
+ Stop a managed persona runtime and remove its workspace from this OpenClaw machine.
221
268
  personas runtime-register <slug>
222
269
  Register a running persona runtime with the Oomi backend.
223
- personas heartbeat <slug>
224
- Send a persona runtime heartbeat to the Oomi backend.
225
- personas runtime-fail <slug>
226
- Report persona runtime failure to the Oomi backend.
227
- persona-jobs start <jobId>
228
- Mark a persona job as running.
229
- persona-jobs succeed <jobId>
230
- Mark a persona job as succeeded.
231
- persona-jobs fail <jobId>
232
- Mark a persona job as failed.
233
- persona-jobs execute
234
- Execute a structured persona job payload end to end.
270
+ personas heartbeat <slug>
271
+ Send a persona runtime heartbeat to the Oomi backend.
272
+ personas runtime-fail <slug>
273
+ Report persona runtime failure to the Oomi backend.
274
+ persona-jobs start <jobId>
275
+ Mark a persona job as running.
276
+ persona-jobs succeed <jobId>
277
+ Mark a persona job as succeeded.
278
+ persona-jobs fail <jobId>
279
+ Mark a persona job as failed.
280
+ persona-jobs execute
281
+ Execute a structured persona job payload end to end.
235
282
 
236
283
  Common flags:
237
284
  --agents-file PATH Override AGENTS.md path
@@ -244,94 +291,100 @@ Common flags:
244
291
  --label TEXT Pairing label shown in broker logs
245
292
  --session-key KEY Session key used in generated connect URL
246
293
  --detach Start bridge in background and exit
247
- --no-start Do not start the bridge or persona runtime
294
+ --no-start Do not start the bridge or persona runtime
248
295
  --device-id ID Bridge device identifier (default: host name)
249
296
  --device-token TOKEN Existing bridge device token
250
297
  --show-secrets Print full token values in diagnostic output
251
- --json Print pairing result as JSON (for automation)
252
- --text TEXT Assistant text for local debug frame replay
253
- --frame-file PATH Read a raw gateway frame from disk for local debug replay
254
- --frame-json JSON Use raw gateway frame JSON text for local debug replay
255
- --session-id ID Debug session id override (default: ms_debug_local)
256
- --user-text TEXT User utterance text used for backend voice replay
257
- --live-provider Use the real Qwen TTS provider in local debug replay
258
- --env-file PATH Load provider env vars from a specific env file (default: <repo>/.env.local)
259
- --provider-timeout-ms N
260
- Timeout in ms for live provider audio during local debug replay
261
- --backend-url URL Override Oomi backend URL
262
- --root PATH Override repo root path for persona discovery
263
- --role ROLE Message role override for local debug frame replay
264
- --omit-role Omit message.role in the generated local debug frame
265
- --name NAME Persona display name (for create)
266
- --description TEXT Persona description (for scaffold)
267
- --slug SLUG Explicit slug override (for create-managed)
268
- --summary TEXT Persona summary (for create)
298
+ --json Print pairing result as JSON (for automation)
299
+ --text TEXT Assistant text for local debug frame replay
300
+ --frame-file PATH Read a raw gateway frame from disk for local debug replay
301
+ --frame-json JSON Use raw gateway frame JSON text for local debug replay
302
+ --session-id ID Debug session id override (default: ms_debug_local)
303
+ --user-text TEXT User utterance text used for backend voice replay
304
+ --live-provider Use the real Qwen TTS provider in local debug replay
305
+ --env-file PATH Load provider env vars from a specific env file (default: <repo>/.env.local)
306
+ --provider-timeout-ms N
307
+ Timeout in ms for live provider audio during local debug replay
308
+ --backend-url URL Override Oomi backend URL
309
+ --root PATH Override repo root path for persona discovery
310
+ --role ROLE Message role override for local debug frame replay
311
+ --omit-role Omit message.role in the generated local debug frame
312
+ --name NAME Persona display name (for create)
313
+ --description TEXT Persona description (for scaffold)
314
+ --slug SLUG Explicit slug override (for create-managed)
315
+ --summary TEXT Persona summary (for create)
269
316
  --status STATUS Persona status (for create)
270
317
  --type TYPE Persona type (for create)
271
318
  --tags a,b,c Persona tags (for create)
272
319
  --chat-session KEY Persona chat session key (for create)
273
- --out PATH Output directory for scaffolded persona app
274
- --template-version V Scaffold template version (default: v1)
275
- --force Overwrite files in an existing output directory
276
- --no-sync Skip backend sync (for create)
277
- --local-port N Local runtime port for persona runtime callbacks
278
- --endpoint URL Runtime endpoint for persona runtime callbacks
279
- --health-path PATH Runtime health path (default: /oomi.health.json)
280
- --healthcheck-url URL Runtime healthcheck URL override
281
- --started-at ISO Start timestamp override
282
- --observed-at ISO Heartbeat timestamp override
283
- --completed-at ISO Completion timestamp override
284
- --code TEXT Error code for fail callbacks
285
- --message TEXT Error message for fail callbacks
286
- --message-file PATH Structured persona job message JSON file
287
- --message-json JSON Structured persona job message JSON text
288
- --log-file PATH Runtime log file path override
289
- --no-install Skip npm install during persona job execution
290
- --no-register Skip persona runtime registration during persona job execution
291
- `);
292
- }
320
+ --out PATH Output directory for scaffolded persona app
321
+ --template-version V Scaffold template version (default: v1)
322
+ --force Overwrite files in an existing output directory
323
+ --force-install Reinstall persona workspace dependencies before launch
324
+ --no-sync Skip backend sync (for create)
325
+ --no-create Do not create a managed persona record if one does not already exist
326
+ --local-port N Local runtime port for persona runtime callbacks
327
+ --endpoint URL Runtime endpoint for persona runtime callbacks
328
+ --entry-url URL Viewer URL to register for a launched persona runtime
329
+ --health-path PATH Runtime health path (default: /oomi.health.json)
330
+ --healthcheck-url URL Runtime healthcheck URL override
331
+ --transport TEXT Runtime transport label (default: local, relay when --entry-url is used)
332
+ --workspace-root PATH Persona workspace root (default: ~/.openclaw/personas)
333
+ --restart Restart an existing managed persona runtime before launch
334
+ --started-at ISO Start timestamp override
335
+ --observed-at ISO Heartbeat timestamp override
336
+ --completed-at ISO Completion timestamp override
337
+ --code TEXT Error code for fail callbacks
338
+ --message TEXT Error message for fail callbacks
339
+ --message-file PATH Structured persona job message JSON file
340
+ --message-json JSON Structured persona job message JSON text
341
+ --log-file PATH Runtime log file path override
342
+ --no-install Skip npm install during persona job execution
343
+ --no-register Skip persona runtime registration during persona job execution
344
+ `);
345
+ }
293
346
 
294
347
  function readFile(filePath) {
295
348
  return fs.readFileSync(filePath, 'utf-8');
296
349
  }
297
350
 
298
- function writeFile(filePath, content, options = undefined) {
299
- fs.writeFileSync(filePath, content, options);
300
- }
301
-
302
- function parseDotEnvLine(line) {
303
- const trimmed = String(line || '').trim();
304
- if (!trimmed || trimmed.startsWith('#')) return null;
305
- const separatorIndex = trimmed.indexOf('=');
306
- if (separatorIndex <= 0) return null;
307
- const key = trimmed.slice(0, separatorIndex).trim();
308
- if (!key) return null;
309
- let value = trimmed.slice(separatorIndex + 1).trim();
310
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
311
- value = value.slice(1, -1);
312
- }
313
- return { key, value };
314
- }
315
-
316
- function loadEnvFile(filePath, keys = []) {
317
- if (!filePath || !fs.existsSync(filePath)) {
318
- throw new Error(`Environment file not found: ${filePath}`);
319
- }
320
- const selectedKeys = Array.isArray(keys) && keys.length ? new Set(keys) : null;
321
- const entries = {};
322
- const lines = readFile(filePath).split(/\r?\n/);
323
- for (const line of lines) {
324
- const parsed = parseDotEnvLine(line);
325
- if (!parsed) continue;
326
- if (selectedKeys && !selectedKeys.has(parsed.key)) continue;
327
- entries[parsed.key] = parsed.value;
328
- }
329
- return entries;
330
- }
331
-
332
- function xmlEscape(value) {
333
- return String(value)
334
- .replaceAll('&', '&amp;')
351
+ function writeFile(filePath, content, options = undefined) {
352
+ fs.writeFileSync(filePath, content, options);
353
+ }
354
+
355
+ function parseDotEnvLine(line) {
356
+ const trimmed = String(line || '').trim();
357
+ if (!trimmed || trimmed.startsWith('#')) return null;
358
+ const separatorIndex = trimmed.indexOf('=');
359
+ if (separatorIndex <= 0) return null;
360
+ const key = trimmed.slice(0, separatorIndex).trim();
361
+ if (!key) return null;
362
+ let value = trimmed.slice(separatorIndex + 1).trim();
363
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
364
+ value = value.slice(1, -1);
365
+ }
366
+ return { key, value };
367
+ }
368
+
369
+ function loadEnvFile(filePath, keys = []) {
370
+ if (!filePath || !fs.existsSync(filePath)) {
371
+ throw new Error(`Environment file not found: ${filePath}`);
372
+ }
373
+ const selectedKeys = Array.isArray(keys) && keys.length ? new Set(keys) : null;
374
+ const entries = {};
375
+ const lines = readFile(filePath).split(/\r?\n/);
376
+ for (const line of lines) {
377
+ const parsed = parseDotEnvLine(line);
378
+ if (!parsed) continue;
379
+ if (selectedKeys && !selectedKeys.has(parsed.key)) continue;
380
+ entries[parsed.key] = parsed.value;
381
+ }
382
+ return entries;
383
+ }
384
+
385
+ function xmlEscape(value) {
386
+ return String(value)
387
+ .replaceAll('&', '&amp;')
335
388
  .replaceAll('<', '&lt;')
336
389
  .replaceAll('>', '&gt;')
337
390
  .replaceAll('"', '&quot;')
@@ -339,11 +392,7 @@ function xmlEscape(value) {
339
392
  }
340
393
 
341
394
  function resolveWorkspace() {
342
- const envWorkspace = process.env.OPENCLAW_WORKSPACE || process.env.OPENCLAW_HOME;
343
- if (envWorkspace) return envWorkspace;
344
- const defaultWorkspace = path.join(os.homedir(), '.openclaw', 'workspace');
345
- if (fs.existsSync(defaultWorkspace)) return defaultWorkspace;
346
- return path.join(os.homedir(), '.openclaw');
395
+ return resolveOpenclawWorkspaceRoot();
347
396
  }
348
397
 
349
398
  function resolveAgentsFile(cliAgentsFile, cliWorkspace) {
@@ -420,9 +469,9 @@ function ensureDir(dirPath) {
420
469
  }
421
470
  }
422
471
 
423
- function findRepoRoot(startDir) {
424
- let current = startDir;
425
- for (let i = 0; i < 6; i += 1) {
472
+ function findRepoRoot(startDir) {
473
+ let current = startDir;
474
+ for (let i = 0; i < 6; i += 1) {
426
475
  const personasDir = path.join(current, 'personas');
427
476
  const skillsDir = path.join(current, 'skills', 'oomi');
428
477
  if (fs.existsSync(personasDir) || fs.existsSync(skillsDir)) {
@@ -431,23 +480,23 @@ function findRepoRoot(startDir) {
431
480
  const parent = path.dirname(current);
432
481
  if (parent === current) break;
433
482
  current = parent;
434
- }
435
- return null;
436
- }
437
-
438
- function resolveRepoRoot(rootFlag) {
439
- const explicitRoot =
440
- typeof rootFlag === 'string' && rootFlag.trim()
441
- ? path.resolve(rootFlag.trim())
442
- : '';
443
- const repoRoot = explicitRoot || findRepoRoot(process.cwd()) || findRepoRoot(PACKAGE_ROOT);
444
- if (!repoRoot) {
445
- throw new Error('Could not locate repo root. Use --root <repo root>.');
446
- }
447
- return repoRoot;
448
- }
449
-
450
- function resolveSkillSource(cliRoot) {
483
+ }
484
+ return null;
485
+ }
486
+
487
+ function resolveRepoRoot(rootFlag) {
488
+ const explicitRoot =
489
+ typeof rootFlag === 'string' && rootFlag.trim()
490
+ ? path.resolve(rootFlag.trim())
491
+ : '';
492
+ const repoRoot = explicitRoot || findRepoRoot(process.cwd()) || findRepoRoot(PACKAGE_ROOT);
493
+ if (!repoRoot) {
494
+ throw new Error('Could not locate repo root. Use --root <repo root>.');
495
+ }
496
+ return repoRoot;
497
+ }
498
+
499
+ function resolveSkillSource(cliRoot) {
451
500
  const packaged = path.join(PACKAGE_ROOT, 'skills', 'oomi');
452
501
  if (fs.existsSync(packaged)) {
453
502
  return packaged;
@@ -473,7 +522,7 @@ function resolveSkillTargets(cliSkillsDir) {
473
522
  }
474
523
 
475
524
  const targets = [];
476
- const openclaw = path.join(os.homedir(), '.openclaw', 'skills');
525
+ const openclaw = resolveOpenclawSkillsDir();
477
526
  const clawd = path.join(os.homedir(), 'clawd', 'skills');
478
527
 
479
528
  targets.push(openclaw);
@@ -659,7 +708,7 @@ async function createPersona({ id, root, flags }) {
659
708
  console.log(`Synced persona ${payload?.persona?.slug || id}`);
660
709
  }
661
710
 
662
- function printPersonaScaffoldResult(result, asJson = false) {
711
+ function printPersonaScaffoldResult(result, asJson = false) {
663
712
  if (asJson) {
664
713
  console.log(JSON.stringify(result, null, 2));
665
714
  return;
@@ -672,465 +721,764 @@ function printPersonaScaffoldResult(result, asJson = false) {
672
721
  console.log(`Start: ${result.startCommand}`);
673
722
  if (Array.isArray(result.editableZones) && result.editableZones.length > 0) {
674
723
  console.log(`Editable zones: ${result.editableZones.join(', ')}`);
675
- }
676
- }
677
-
678
- function printManagedPersonaCreateResult(result, asJson = false) {
679
- if (asJson) {
680
- console.log(JSON.stringify(result, null, 2));
681
- return;
682
- }
683
-
684
- const persona = result?.persona && typeof result.persona === 'object' ? result.persona : {};
685
- const personaJob = result?.personaJob && typeof result.personaJob === 'object' ? result.personaJob : {};
686
- console.log(`Managed persona created: ${String(persona.name || persona.slug || 'unknown')}`);
687
- if (persona.slug) {
688
- console.log(`Slug: ${persona.slug}`);
689
- }
690
- if (persona.lifecycle) {
691
- console.log(`Lifecycle: ${persona.lifecycle}`);
692
- }
693
- if (personaJob.jobId) {
694
- console.log(`Persona job: ${personaJob.jobId}`);
695
- }
696
- if (personaJob.status) {
697
- console.log(`Job status: ${personaJob.status}`);
698
- }
699
- if (personaJob.deviceId) {
700
- console.log(`Assigned device: ${personaJob.deviceId}`);
701
- }
702
- }
703
-
704
- function parseOptionalPositiveInteger(value) {
705
- if (value === undefined || value === null || value === '') return null;
706
- const parsed = Number(value);
707
- if (!Number.isFinite(parsed) || parsed <= 0) {
708
- throw new Error(`Expected a positive integer, received: ${value}`);
709
- }
710
- return Math.floor(parsed);
711
- }
712
-
724
+ }
725
+ }
726
+
727
+ function printManagedPersonaCreateResult(result, asJson = false) {
728
+ if (asJson) {
729
+ console.log(JSON.stringify(result, null, 2));
730
+ return;
731
+ }
732
+
733
+ const persona = result?.persona && typeof result.persona === 'object' ? result.persona : {};
734
+ const personaJob = result?.personaJob && typeof result.personaJob === 'object' ? result.personaJob : {};
735
+ console.log(`Managed persona created: ${String(persona.name || persona.slug || 'unknown')}`);
736
+ if (persona.slug) {
737
+ console.log(`Slug: ${persona.slug}`);
738
+ }
739
+ if (persona.lifecycle) {
740
+ console.log(`Lifecycle: ${persona.lifecycle}`);
741
+ }
742
+ if (personaJob.jobId) {
743
+ console.log(`Persona job: ${personaJob.jobId}`);
744
+ }
745
+ if (personaJob.status) {
746
+ console.log(`Job status: ${personaJob.status}`);
747
+ }
748
+ if (personaJob.deviceId) {
749
+ console.log(`Assigned device: ${personaJob.deviceId}`);
750
+ }
751
+ }
752
+
753
+ function parseOptionalPositiveInteger(value) {
754
+ if (value === undefined || value === null || value === '') return null;
755
+ const parsed = Number(value);
756
+ if (!Number.isFinite(parsed) || parsed <= 0) {
757
+ throw new Error(`Expected a positive integer, received: ${value}`);
758
+ }
759
+ return Math.floor(parsed);
760
+ }
761
+
713
762
  function resolvePersonaBackendUrl(flags = {}) {
714
763
  const bridgeState = readBridgeState();
715
764
  const backendUrl = String(
716
765
  flags['backend-url'] ||
717
- process.env.OOMI_BACKEND_URL ||
718
- process.env.NEXT_PUBLIC_BACKEND_URL ||
719
- bridgeState.brokerHttp ||
720
- ''
721
- ).trim();
722
- if (!backendUrl) {
723
- throw new Error('Missing backend URL. Use --backend-url or pair the device first.');
724
- }
725
- return backendUrl.replace(/\/$/, '');
726
- }
727
-
728
- function resolvePersonaDeviceToken(flags = {}) {
729
- const bridgeState = readBridgeState();
730
- const deviceToken = String(
731
- flags['device-token'] ||
732
- bridgeState.deviceToken ||
733
- ''
734
- ).trim();
735
- if (!deviceToken) {
736
- throw new Error('Missing device token. Use --device-token or pair the device first.');
737
- }
738
- return deviceToken;
739
- }
740
-
741
- function resolvePersonaDeviceId(flags = {}) {
742
- const bridgeState = readBridgeState();
743
- const deviceId = String(
744
- flags['device-id'] ||
745
- bridgeState.deviceId ||
746
- ''
766
+ process.env.OOMI_DEV_BACKEND_URL ||
767
+ process.env.OOMI_BACKEND_URL ||
768
+ process.env.NEXT_PUBLIC_BACKEND_URL ||
769
+ bridgeState.brokerHttp ||
770
+ ''
747
771
  ).trim();
748
- if (!deviceId) {
749
- throw new Error('Missing device id. Use --device-id or pair the device first.');
750
- }
751
- return deviceId;
752
- }
753
-
754
- function createCliPersonaApiClient(flags = {}) {
755
- return createPersonaApiClient({
756
- backendUrl: resolvePersonaBackendUrl(flags),
757
- deviceToken: resolvePersonaDeviceToken(flags),
758
- deviceId: resolvePersonaDeviceId(flags),
759
- });
760
- }
761
-
762
- function ensurePersonaJobWorkspace(message, workspaceRoot = defaultPersonaWorkspaceRoot()) {
763
- const metadata = message && typeof message === 'object' ? message.metadata : null;
764
- const payload = metadata && typeof metadata === 'object' ? metadata.payload : null;
765
- if (!payload || typeof payload !== 'object') {
766
- return message;
767
- }
768
-
769
- const persona = payload.persona && typeof payload.persona === 'object' ? payload.persona : {};
770
- const scaffold = payload.scaffold && typeof payload.scaffold === 'object' ? payload.scaffold : {};
771
- if (!scaffold.outDir && typeof persona.slug === 'string' && persona.slug.trim()) {
772
- scaffold.outDir = path.join(workspaceRoot, persona.slug.trim());
773
- payload.scaffold = scaffold;
774
- metadata.payload = payload;
775
- message.metadata = metadata;
776
- }
777
-
778
- return message;
779
- }
780
-
781
- async function runManagedPersonaJobExecution({
782
- message,
783
- backendUrl,
784
- deviceToken,
785
- deviceId,
786
- workspaceRoot = defaultPersonaWorkspaceRoot(),
787
- shouldInstall = true,
788
- shouldStart = true,
789
- shouldRegister = true,
790
- logFilePath = '',
791
- }) {
792
- const normalizedMessage = ensurePersonaJobWorkspace(
793
- structuredClone(message),
794
- workspaceRoot,
795
- );
796
- const client = createPersonaApiClient({
797
- backendUrl,
798
- deviceToken,
799
- deviceId,
800
- });
801
-
802
- return executePersonaJob({
803
- message: normalizedMessage,
804
- installWorkspace: shouldInstall
805
- ? async ({ workspacePath }) => {
806
- await installPersonaWorkspace({ workspacePath });
807
- }
808
- : async () => {},
809
- startWorkspace: shouldStart
810
- ? async ({ workspacePath }) =>
811
- startPersonaWorkspace({
812
- workspacePath,
813
- logFilePath,
814
- })
815
- : async () => ({ pid: null, logFilePath }),
816
- waitForRuntime: shouldStart
817
- ? async ({ runtime }) => {
818
- await waitForPersonaRuntime({
819
- healthcheckUrl: runtime.healthcheckUrl,
820
- });
821
- }
822
- : async () => {},
823
- registerRuntime: shouldRegister
824
- ? async ({ payload: jobPayload, result: runtimeResult }) => {
825
- const jobPersona = jobPayload.persona && typeof jobPayload.persona === 'object' ? jobPayload.persona : {};
826
- await client.registerRuntime({
827
- slug: String(jobPersona.slug || '').trim(),
828
- endpoint: runtimeResult.endpoint,
829
- healthcheckUrl: runtimeResult.healthcheckUrl,
830
- transport: runtimeResult.transport,
831
- localPort: runtimeResult.localPort,
832
- startedAt: new Date().toISOString(),
833
- });
834
- }
835
- : async () => {},
836
- onJobStart: async ({ jobId }) => {
837
- await client.startJob({
838
- jobId,
839
- startedAt: new Date().toISOString(),
840
- });
841
- },
842
- onJobSuccess: async ({ jobId, result: runtimeResult }) => {
843
- await client.succeedJob({
844
- jobId,
845
- workspacePath: runtimeResult.workspacePath,
846
- localPort: runtimeResult.localPort,
847
- transport: runtimeResult.transport,
848
- endpoint: runtimeResult.endpoint,
849
- healthcheckUrl: runtimeResult.healthcheckUrl,
850
- completedAt: new Date().toISOString(),
851
- });
852
- },
853
- onJobFailure: async ({ jobId, error }) => {
854
- await client.failJob({
855
- jobId,
856
- code: String(error?.code || 'PERSONA_JOB_EXECUTION_FAILED').trim(),
857
- message: String(error?.message || 'Persona job execution failed.').trim(),
858
- completedAt: new Date().toISOString(),
859
- });
860
- },
861
- });
862
- }
863
-
864
- function resolvePersonaRuntimeInput(flags = {}, defaults = {}) {
865
- const localPort = parseOptionalPositiveInteger(flags['local-port'] || flags.localPort || defaults.localPort);
866
- const endpoint = String(flags.endpoint || defaults.endpoint || '').trim();
867
- const healthPath = String(flags['health-path'] || defaults.healthPath || '/oomi.health.json').trim() || '/oomi.health.json';
868
- const healthcheckUrl = String(flags['healthcheck-url'] || defaults.healthcheckUrl || '').trim();
869
- const transport = String(flags.transport || defaults.transport || 'local').trim() || 'local';
870
-
871
- if (endpoint) {
872
- return {
873
- endpoint,
874
- healthcheckUrl: healthcheckUrl || `${endpoint.replace(/\/$/, '')}${healthPath}`,
875
- localPort,
876
- transport,
877
- };
878
- }
879
-
880
- if (!localPort) {
881
- throw new Error('Runtime endpoint or local port is required.');
882
- }
883
-
884
- return buildLocalPersonaRuntime({
885
- localPort,
886
- healthPath,
887
- });
888
- }
889
-
890
- function parseIsoTimestamp(rawValue, label) {
891
- const value = String(rawValue || '').trim();
892
- if (!value) return undefined;
893
- const timestamp = new Date(value);
894
- if (Number.isNaN(timestamp.getTime())) {
895
- throw new Error(`${label} must be a valid ISO timestamp.`);
896
- }
897
- return timestamp.toISOString();
898
- }
899
-
900
- function readStructuredPersonaJobMessage(flags = {}) {
901
- const filePath = String(flags['message-file'] || '').trim();
902
- const inlineJson = String(flags['message-json'] || '').trim();
903
-
904
- if (inlineJson) {
905
- return JSON.parse(inlineJson);
906
- }
907
- if (filePath) {
908
- return JSON.parse(readFile(path.resolve(filePath)));
909
- }
910
-
911
- throw new Error('Persona job message is required. Use --message-file or --message-json.');
912
- }
913
-
914
- function printStructuredResult(result, asJson = false) {
915
- if (asJson) {
916
- console.log(JSON.stringify(result, null, 2));
917
- return;
918
- }
919
-
920
- for (const [key, value] of Object.entries(result)) {
921
- console.log(`${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`);
922
- }
923
- }
924
-
925
- async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
926
- const client = createCliPersonaApiClient(flags);
927
- const runtime = resolvePersonaRuntimeInput(flags);
928
- const payload = await client.registerRuntime({
929
- slug,
930
- endpoint: runtime.endpoint,
931
- healthcheckUrl: runtime.healthcheckUrl,
932
- localPort: runtime.localPort,
933
- transport: runtime.transport,
934
- startedAt: parseIsoTimestamp(flags['started-at'], 'started-at'),
935
- });
936
- printStructuredResult(payload, isTruthyFlag(flags.json));
937
- }
938
-
939
- async function handlePersonaHeartbeatCommand(slug, flags = {}) {
940
- const client = createCliPersonaApiClient(flags);
941
- const runtime = resolvePersonaRuntimeInput(flags);
942
- const payload = await client.heartbeatRuntime({
943
- slug,
944
- endpoint: runtime.endpoint,
945
- healthcheckUrl: runtime.healthcheckUrl,
946
- localPort: runtime.localPort,
947
- transport: runtime.transport,
948
- observedAt: parseIsoTimestamp(flags['observed-at'], 'observed-at'),
949
- });
950
- printStructuredResult(payload, isTruthyFlag(flags.json));
951
- }
952
-
953
- async function handlePersonaRuntimeFailCommand(slug, flags = {}) {
954
- const code = String(flags.code || '').trim();
955
- const message = String(flags.message || '').trim();
956
- if (!code) {
957
- throw new Error('Error code is required. Use --code.');
958
- }
959
- if (!message) {
960
- throw new Error('Error message is required. Use --message.');
961
- }
962
-
963
- const client = createCliPersonaApiClient(flags);
964
- const payload = await client.failRuntime({
965
- slug,
966
- code,
967
- message,
968
- });
969
- printStructuredResult(payload, isTruthyFlag(flags.json));
970
- }
971
-
972
- async function handlePersonaJobStartCommand(jobId, flags = {}) {
973
- const client = createCliPersonaApiClient(flags);
974
- const payload = await client.startJob({
975
- jobId,
976
- startedAt: parseIsoTimestamp(flags['started-at'], 'started-at'),
977
- });
978
- printStructuredResult(payload, isTruthyFlag(flags.json));
979
- }
980
-
981
- async function handlePersonaJobSucceedCommand(jobId, flags = {}) {
982
- const client = createCliPersonaApiClient(flags);
983
- const runtime = resolvePersonaRuntimeInput(flags);
984
- const workspacePath = String(flags['workspace-path'] || flags.workspacePath || '').trim();
985
- if (!workspacePath) {
986
- throw new Error('Workspace path is required. Use --workspace-path.');
987
- }
988
-
989
- const payload = await client.succeedJob({
990
- jobId,
991
- workspacePath,
992
- localPort: runtime.localPort,
993
- transport: runtime.transport,
994
- endpoint: runtime.endpoint,
995
- healthcheckUrl: runtime.healthcheckUrl,
996
- completedAt: parseIsoTimestamp(flags['completed-at'], 'completed-at'),
997
- });
998
- printStructuredResult(payload, isTruthyFlag(flags.json));
999
- }
1000
-
1001
- async function handlePersonaJobFailCommand(jobId, flags = {}) {
1002
- const code = String(flags.code || '').trim();
1003
- const message = String(flags.message || '').trim();
1004
- if (!code) {
1005
- throw new Error('Error code is required. Use --code.');
1006
- }
1007
- if (!message) {
1008
- throw new Error('Error message is required. Use --message.');
1009
- }
1010
-
1011
- const client = createCliPersonaApiClient(flags);
1012
- const payload = await client.failJob({
1013
- jobId,
1014
- code,
1015
- message,
1016
- completedAt: parseIsoTimestamp(flags['completed-at'], 'completed-at'),
1017
- });
1018
- printStructuredResult(payload, isTruthyFlag(flags.json));
1019
- }
1020
-
1021
- async function handlePersonaJobExecuteCommand(flags = {}) {
1022
- const message = readStructuredPersonaJobMessage(flags);
1023
- const shouldInstall = !isTruthyFlag(flags['no-install']);
1024
- const shouldStart = !isTruthyFlag(flags['no-start']);
1025
- const shouldRegister = !isTruthyFlag(flags['no-register']) && shouldStart;
1026
- const logFilePath = String(flags['log-file'] || '').trim();
1027
- const result = await runManagedPersonaJobExecution({
1028
- message,
1029
- backendUrl: resolvePersonaBackendUrl(flags),
1030
- deviceToken: resolvePersonaDeviceToken(flags),
1031
- deviceId: resolvePersonaDeviceId(flags),
1032
- workspaceRoot: String(flags['workspace-root'] || defaultPersonaWorkspaceRoot()).trim(),
1033
- shouldInstall,
1034
- shouldStart,
1035
- shouldRegister,
1036
- logFilePath,
1037
- });
1038
-
1039
- printStructuredResult(result, isTruthyFlag(flags.json));
1040
- }
1041
-
1042
- async function handlePersonaCreateManagedCommand(flags = {}, positionalSlug = '') {
1043
- const name = String(flags.name || '').trim();
1044
- if (!name) {
1045
- throw new Error('Persona name is required. Usage: oomi personas create-managed [slug] --name "<name>" --description "<description>"');
1046
- }
1047
-
1048
- const description = String(flags.description || '').trim() || name;
1049
- const explicitSlug = String(flags.slug || positionalSlug || '').trim();
1050
- const client = createCliPersonaApiClient(flags);
1051
- const result = await client.createManagedPersona({
1052
- slug: explicitSlug,
1053
- name,
1054
- description,
1055
- templateType: String(flags['template-type'] || 'persona-app').trim() || 'persona-app',
1056
- promptTemplateVersion: String(flags['template-version'] || 'v1').trim() || 'v1',
1057
- });
1058
-
1059
- printManagedPersonaCreateResult(result, isTruthyFlag(flags.json));
1060
- }
1061
-
1062
- function resolveOpenclawConfigPath() {
1063
- const candidates = [
1064
- path.join(os.homedir(), '.openclaw', 'clawdbot.json'),
1065
- path.join(os.homedir(), '.openclaw', 'openclaw.json'),
1066
- ];
1067
- for (const candidate of candidates) {
1068
- if (fs.existsSync(candidate)) return candidate;
772
+ if (!backendUrl) {
773
+ throw new Error('Missing backend URL. Use --backend-url or pair the device first.');
1069
774
  }
1070
- return null;
775
+ return backendUrl.replace(/\/$/, '');
1071
776
  }
1072
777
 
1073
- function readOpenclawGatewayConfig() {
1074
- const configPath = resolveOpenclawConfigPath();
1075
- if (!configPath) {
1076
- throw new Error('OpenClaw config not found (~/.openclaw/clawdbot.json or ~/.openclaw/openclaw.json).');
778
+ function resolvePersonaDeviceToken(flags = {}) {
779
+ const bridgeState = readBridgeState();
780
+ const deviceToken = String(
781
+ flags['device-token'] ||
782
+ bridgeState.deviceToken ||
783
+ ''
784
+ ).trim();
785
+ if (!deviceToken) {
786
+ throw new Error('Missing device token. Use --device-token or pair the device first.');
1077
787
  }
1078
-
1079
- const parsed = JSON.parse(readFile(configPath));
1080
- const gateway = parsed.gateway || {};
1081
- const auth = gateway.auth || {};
1082
- const port = gateway.port || 18789;
1083
- const bind = gateway.bind || 'loopback';
1084
- const host = bind === 'all' ? '127.0.0.1' : '127.0.0.1';
1085
- const gatewayUrl = `ws://${host}:${port}`;
1086
-
1087
- return {
1088
- gatewayUrl,
1089
- token: typeof auth.token === 'string' ? auth.token.trim() : '',
1090
- password: typeof auth.password === 'string' ? auth.password.trim() : '',
1091
- configPath,
1092
- };
788
+ return deviceToken;
1093
789
  }
1094
790
 
1095
- function resolveBridgeStatePath() {
1096
- return path.join(os.homedir(), '.openclaw', 'oomi-bridge.json');
791
+ function resolvePersonaDeviceId(flags = {}) {
792
+ const bridgeState = readBridgeState();
793
+ const deviceId = String(
794
+ flags['device-id'] ||
795
+ bridgeState.deviceId ||
796
+ ''
797
+ ).trim();
798
+ if (!deviceId) {
799
+ throw new Error('Missing device id. Use --device-id or pair the device first.');
800
+ }
801
+ return deviceId;
1097
802
  }
1098
803
 
1099
- function resolveBridgeStatusPath() {
1100
- return path.join(os.homedir(), '.openclaw', 'oomi-bridge-status.json');
804
+ function createCliPersonaApiClient(flags = {}) {
805
+ return createPersonaApiClient({
806
+ backendUrl: resolvePersonaBackendUrl(flags),
807
+ deviceToken: resolvePersonaDeviceToken(flags),
808
+ deviceId: resolvePersonaDeviceId(flags),
809
+ });
1101
810
  }
1102
811
 
1103
- function resolveBridgeLockPath() {
1104
- return path.join(os.homedir(), '.openclaw', 'oomi-bridge.lock');
812
+ function isHttpErrorStatus(error, status) {
813
+ return Number(error?.status) === Number(status);
1105
814
  }
1106
815
 
1107
- function resolveBridgeLiveLogPath() {
1108
- return path.join(os.homedir(), '.openclaw', 'logs', 'oomi-bridge-live.log');
816
+ function resolvePersonaWorkspaceRoot(flags = {}) {
817
+ const workspaceRoot = String(flags['workspace-root'] || defaultPersonaWorkspaceRoot()).trim();
818
+ if (!workspaceRoot) {
819
+ throw new Error('Persona workspace root is required.');
820
+ }
821
+ return workspaceRoot;
1109
822
  }
1110
823
 
1111
- function resolveBridgeLaunchAgentPlistPath() {
1112
- return path.join(os.homedir(), 'Library', 'LaunchAgents', `${BRIDGE_LAUNCHD_LABEL}.plist`);
824
+ function resolvePersonaTemplateVersion(flags = {}, fallback = 'v1') {
825
+ return String(flags['template-version'] || fallback || 'v1').trim() || 'v1';
1113
826
  }
1114
827
 
1115
- function defaultDeviceId() {
1116
- const host = os.hostname().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'openclaw';
1117
- return `oomi-${host}-${randomUUID().slice(0, 8)}`;
828
+ function resolvePersonaEntryUrl(flags = {}) {
829
+ return String(flags['entry-url'] || '').trim();
1118
830
  }
1119
831
 
1120
- function resolveDeviceId(flags, bridgeState) {
1121
- const explicit = String(flags['device-id'] || process.env.OOMI_MANAGED_DEVICE_ID || '').trim();
1122
- if (explicit) return explicit;
1123
- const existing = String(bridgeState.deviceId || '').trim();
1124
- if (existing) return existing;
1125
- return defaultDeviceId();
832
+ function resolvePersonaLaunchTransport(flags = {}) {
833
+ const explicitTransport = String(flags.transport || '').trim();
834
+ if (explicitTransport) {
835
+ return explicitTransport;
836
+ }
837
+
838
+ return resolvePersonaEntryUrl(flags) ? 'relay' : 'local';
1126
839
  }
1127
840
 
1128
- function readBridgeState() {
1129
- const statePath = resolveBridgeStatePath();
1130
- if (!fs.existsSync(statePath)) return {};
841
+ async function findExistingManagedPersona(client, slug) {
1131
842
  try {
1132
- return JSON.parse(readFile(statePath));
1133
- } catch {
843
+ return await client.getPersona({ slug });
844
+ } catch (error) {
845
+ if (isHttpErrorStatus(error, 404)) {
846
+ return null;
847
+ }
848
+ throw error;
849
+ }
850
+ }
851
+
852
+ function ensurePersonaJobWorkspace(message, workspaceRoot = defaultPersonaWorkspaceRoot()) {
853
+ const metadata = message && typeof message === 'object' ? message.metadata : null;
854
+ const payload = metadata && typeof metadata === 'object' ? metadata.payload : null;
855
+ if (!payload || typeof payload !== 'object') {
856
+ return message;
857
+ }
858
+
859
+ const persona = payload.persona && typeof payload.persona === 'object' ? payload.persona : {};
860
+ const scaffold = payload.scaffold && typeof payload.scaffold === 'object' ? payload.scaffold : {};
861
+ if (!scaffold.outDir && typeof persona.slug === 'string' && persona.slug.trim()) {
862
+ scaffold.outDir = path.join(workspaceRoot, persona.slug.trim());
863
+ payload.scaffold = scaffold;
864
+ metadata.payload = payload;
865
+ message.metadata = metadata;
866
+ }
867
+
868
+ return message;
869
+ }
870
+
871
+ async function runLegacyManagedPersonaJobExecution({
872
+ message,
873
+ backendUrl,
874
+ deviceToken,
875
+ deviceId,
876
+ workspaceRoot = defaultPersonaWorkspaceRoot(),
877
+ shouldInstall = true,
878
+ shouldStart = true,
879
+ shouldRegister = true,
880
+ logFilePath = '',
881
+ }) {
882
+ const normalizedMessage = ensurePersonaJobWorkspace(
883
+ structuredClone(message),
884
+ workspaceRoot,
885
+ );
886
+ const client = createPersonaApiClient({
887
+ backendUrl,
888
+ deviceToken,
889
+ deviceId,
890
+ });
891
+
892
+ return executePersonaJob({
893
+ message: normalizedMessage,
894
+ installWorkspace: shouldInstall
895
+ ? async ({ workspacePath }) => {
896
+ await installPersonaWorkspace({ workspacePath });
897
+ }
898
+ : async () => {},
899
+ startWorkspace: shouldStart
900
+ ? async ({ workspacePath }) =>
901
+ startPersonaWorkspace({
902
+ workspacePath,
903
+ logFilePath,
904
+ })
905
+ : async () => ({ pid: null, logFilePath }),
906
+ waitForRuntime: shouldStart
907
+ ? async ({ runtime }) => {
908
+ await waitForPersonaRuntime({
909
+ healthcheckUrl: runtime.healthcheckUrl,
910
+ });
911
+ }
912
+ : async () => {},
913
+ registerRuntime: shouldRegister
914
+ ? async ({ payload: jobPayload, result: runtimeResult }) => {
915
+ const jobPersona = jobPayload.persona && typeof jobPayload.persona === 'object' ? jobPayload.persona : {};
916
+ await client.registerRuntime({
917
+ slug: String(jobPersona.slug || '').trim(),
918
+ endpoint: runtimeResult.endpoint,
919
+ healthcheckUrl: runtimeResult.healthcheckUrl,
920
+ transport: runtimeResult.transport,
921
+ localPort: runtimeResult.localPort,
922
+ startedAt: new Date().toISOString(),
923
+ });
924
+ }
925
+ : async () => {},
926
+ destroyWorkspace: async ({ payload: jobPayload }) => {
927
+ const jobPersona = jobPayload.persona && typeof jobPayload.persona === 'object' ? jobPayload.persona : {};
928
+ const safeSlug = String(jobPersona.slug || '').trim();
929
+ if (!safeSlug) {
930
+ throw new Error('Destroy persona job payload is missing persona.slug.');
931
+ }
932
+
933
+ return destroyManagedPersonaRuntime({
934
+ slug: safeSlug,
935
+ workspaceRoot,
936
+ });
937
+ },
938
+ onJobStart: async ({ jobId }) => {
939
+ await client.startJob({
940
+ jobId,
941
+ startedAt: new Date().toISOString(),
942
+ });
943
+ },
944
+ onJobSuccess: async ({ jobId, result: runtimeResult }) => {
945
+ await client.succeedJob({
946
+ jobId,
947
+ workspacePath: runtimeResult.workspacePath,
948
+ localPort: runtimeResult.localPort,
949
+ transport: runtimeResult.transport,
950
+ endpoint: runtimeResult.endpoint,
951
+ healthcheckUrl: runtimeResult.healthcheckUrl,
952
+ completedAt: new Date().toISOString(),
953
+ });
954
+ },
955
+ onJobFailure: async ({ jobId, error }) => {
956
+ await client.failJob({
957
+ jobId,
958
+ code: String(error?.code || 'PERSONA_JOB_EXECUTION_FAILED').trim(),
959
+ message: String(error?.message || 'Persona job execution failed.').trim(),
960
+ completedAt: new Date().toISOString(),
961
+ });
962
+ },
963
+ });
964
+ }
965
+
966
+ async function runManagedPersonaJobExecution({
967
+ message,
968
+ backendUrl,
969
+ deviceToken,
970
+ deviceId,
971
+ workspaceRoot = defaultPersonaWorkspaceRoot(),
972
+ shouldInstall = true,
973
+ shouldStart = true,
974
+ shouldRegister = true,
975
+ logFilePath = '',
976
+ }) {
977
+ if (!shouldStart) {
978
+ return runLegacyManagedPersonaJobExecution({
979
+ message,
980
+ backendUrl,
981
+ deviceToken,
982
+ deviceId,
983
+ workspaceRoot,
984
+ shouldInstall,
985
+ shouldStart,
986
+ shouldRegister,
987
+ logFilePath,
988
+ });
989
+ }
990
+
991
+ const normalizedMessage = ensurePersonaJobWorkspace(
992
+ structuredClone(message),
993
+ workspaceRoot,
994
+ );
995
+ const client = createPersonaApiClient({
996
+ backendUrl,
997
+ deviceToken,
998
+ deviceId,
999
+ });
1000
+ const payload = extractPersonaJobPayload(normalizedMessage);
1001
+ const jobId = String(payload.jobId || normalizedMessage?.metadata?.jobId || '').trim();
1002
+ if (!jobId) {
1003
+ throw new Error('Persona job payload is missing jobId.');
1004
+ }
1005
+ if (!['create_persona_runtime', 'destroy_persona_runtime'].includes(payload.jobType)) {
1006
+ throw new Error(`Unsupported persona job type: ${payload.jobType || 'unknown'}`);
1007
+ }
1008
+
1009
+ const persona = payload.persona && typeof payload.persona === 'object' ? payload.persona : {};
1010
+ const slug = String(persona.slug || '').trim();
1011
+ const name = String(persona.name || '').trim();
1012
+ const description = String(persona.description || '').trim() || name;
1013
+ if (!slug || !name) {
1014
+ throw new Error('Persona job payload is missing persona slug or name.');
1015
+ }
1016
+
1017
+ await client.startJob({
1018
+ jobId,
1019
+ startedAt: new Date().toISOString(),
1020
+ });
1021
+
1022
+ try {
1023
+ if (payload.jobType === 'destroy_persona_runtime') {
1024
+ const destroyResult = await destroyManagedPersonaRuntime({
1025
+ slug,
1026
+ workspaceRoot,
1027
+ });
1028
+
1029
+ await client.succeedJob({
1030
+ jobId,
1031
+ workspacePath: destroyResult.workspacePath,
1032
+ completedAt: new Date().toISOString(),
1033
+ });
1034
+
1035
+ return {
1036
+ ok: true,
1037
+ jobId,
1038
+ result: destroyResult,
1039
+ };
1040
+ }
1041
+
1042
+ const launchResult = await launchManagedPersonaRuntime({
1043
+ slug,
1044
+ name,
1045
+ description,
1046
+ workspaceRoot,
1047
+ templateVersion: String(persona.templateVersion || 'v1').trim() || 'v1',
1048
+ forceInstall: shouldInstall,
1049
+ restart: false,
1050
+ logFilePath,
1051
+ entryUrl: '',
1052
+ transport: 'local',
1053
+ });
1054
+
1055
+ let registrationPayload = null;
1056
+ if (shouldRegister) {
1057
+ registrationPayload = await client.registerRuntime({
1058
+ slug,
1059
+ endpoint: launchResult.runtime.endpoint,
1060
+ healthcheckUrl: launchResult.runtime.healthcheckUrl,
1061
+ transport: launchResult.runtime.transport,
1062
+ localPort: launchResult.runtime.localPort,
1063
+ startedAt: new Date().toISOString(),
1064
+ });
1065
+ }
1066
+
1067
+ const result = {
1068
+ workspacePath: launchResult.workspacePath,
1069
+ localPort: launchResult.runtime.localPort,
1070
+ transport: launchResult.runtime.transport,
1071
+ endpoint: launchResult.runtime.endpoint,
1072
+ healthcheckUrl: launchResult.runtime.healthcheckUrl,
1073
+ pid: launchResult.state?.pid || null,
1074
+ logFilePath: launchResult.state?.logFilePath || '',
1075
+ templateVersion: launchResult.state?.templateVersion || String(persona.templateVersion || 'v1').trim() || 'v1',
1076
+ reusedRunningProcess: launchResult.reusedRunningProcess,
1077
+ scaffolded: launchResult.scaffolded,
1078
+ installed: launchResult.installed,
1079
+ registration: registrationPayload,
1080
+ };
1081
+
1082
+ await client.succeedJob({
1083
+ jobId,
1084
+ workspacePath: result.workspacePath,
1085
+ localPort: result.localPort,
1086
+ transport: result.transport,
1087
+ endpoint: result.endpoint,
1088
+ healthcheckUrl: result.healthcheckUrl,
1089
+ completedAt: new Date().toISOString(),
1090
+ });
1091
+
1092
+ return {
1093
+ ok: true,
1094
+ jobId,
1095
+ result,
1096
+ };
1097
+ } catch (error) {
1098
+ const messageText = error instanceof Error ? error.message : 'Persona job execution failed.';
1099
+ await client.failJob({
1100
+ jobId,
1101
+ code: 'PERSONA_JOB_EXECUTION_FAILED',
1102
+ message: messageText,
1103
+ completedAt: new Date().toISOString(),
1104
+ });
1105
+
1106
+ return {
1107
+ ok: false,
1108
+ jobId,
1109
+ error: {
1110
+ code: 'PERSONA_JOB_EXECUTION_FAILED',
1111
+ message: messageText,
1112
+ },
1113
+ };
1114
+ }
1115
+ }
1116
+
1117
+ function resolvePersonaRuntimeInput(flags = {}, defaults = {}) {
1118
+ const localPort = parseOptionalPositiveInteger(flags['local-port'] || flags.localPort || defaults.localPort);
1119
+ const endpoint = String(flags.endpoint || defaults.endpoint || '').trim();
1120
+ const healthPath = String(flags['health-path'] || defaults.healthPath || '/oomi.health.json').trim() || '/oomi.health.json';
1121
+ const healthcheckUrl = String(flags['healthcheck-url'] || defaults.healthcheckUrl || '').trim();
1122
+ const transport = String(flags.transport || defaults.transport || 'local').trim() || 'local';
1123
+
1124
+ if (endpoint) {
1125
+ return {
1126
+ endpoint,
1127
+ healthcheckUrl: healthcheckUrl || `${endpoint.replace(/\/$/, '')}${healthPath}`,
1128
+ localPort,
1129
+ transport,
1130
+ };
1131
+ }
1132
+
1133
+ if (!localPort) {
1134
+ throw new Error('Runtime endpoint or local port is required.');
1135
+ }
1136
+
1137
+ return buildLocalPersonaRuntime({
1138
+ localPort,
1139
+ healthPath,
1140
+ });
1141
+ }
1142
+
1143
+ function parseIsoTimestamp(rawValue, label) {
1144
+ const value = String(rawValue || '').trim();
1145
+ if (!value) return undefined;
1146
+ const timestamp = new Date(value);
1147
+ if (Number.isNaN(timestamp.getTime())) {
1148
+ throw new Error(`${label} must be a valid ISO timestamp.`);
1149
+ }
1150
+ return timestamp.toISOString();
1151
+ }
1152
+
1153
+ function readStructuredPersonaJobMessage(flags = {}) {
1154
+ const filePath = String(flags['message-file'] || '').trim();
1155
+ const inlineJson = String(flags['message-json'] || '').trim();
1156
+
1157
+ if (inlineJson) {
1158
+ return JSON.parse(inlineJson);
1159
+ }
1160
+ if (filePath) {
1161
+ return JSON.parse(readFile(path.resolve(filePath)));
1162
+ }
1163
+
1164
+ throw new Error('Persona job message is required. Use --message-file or --message-json.');
1165
+ }
1166
+
1167
+ function printStructuredResult(result, asJson = false) {
1168
+ if (asJson) {
1169
+ console.log(JSON.stringify(result, null, 2));
1170
+ return;
1171
+ }
1172
+
1173
+ for (const [key, value] of Object.entries(result)) {
1174
+ console.log(`${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`);
1175
+ }
1176
+ }
1177
+
1178
+ async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
1179
+ const client = createCliPersonaApiClient(flags);
1180
+ const runtime = resolvePersonaRuntimeInput(flags);
1181
+ const payload = await client.registerRuntime({
1182
+ slug,
1183
+ endpoint: runtime.endpoint,
1184
+ healthcheckUrl: runtime.healthcheckUrl,
1185
+ localPort: runtime.localPort,
1186
+ transport: runtime.transport,
1187
+ startedAt: parseIsoTimestamp(flags['started-at'], 'started-at'),
1188
+ });
1189
+ printStructuredResult(payload, isTruthyFlag(flags.json));
1190
+ }
1191
+
1192
+ async function handlePersonaHeartbeatCommand(slug, flags = {}) {
1193
+ const client = createCliPersonaApiClient(flags);
1194
+ const runtime = resolvePersonaRuntimeInput(flags);
1195
+ const payload = await client.heartbeatRuntime({
1196
+ slug,
1197
+ endpoint: runtime.endpoint,
1198
+ healthcheckUrl: runtime.healthcheckUrl,
1199
+ localPort: runtime.localPort,
1200
+ transport: runtime.transport,
1201
+ observedAt: parseIsoTimestamp(flags['observed-at'], 'observed-at'),
1202
+ });
1203
+ printStructuredResult(payload, isTruthyFlag(flags.json));
1204
+ }
1205
+
1206
+ async function handlePersonaRuntimeFailCommand(slug, flags = {}) {
1207
+ const code = String(flags.code || '').trim();
1208
+ const message = String(flags.message || '').trim();
1209
+ if (!code) {
1210
+ throw new Error('Error code is required. Use --code.');
1211
+ }
1212
+ if (!message) {
1213
+ throw new Error('Error message is required. Use --message.');
1214
+ }
1215
+
1216
+ const client = createCliPersonaApiClient(flags);
1217
+ const payload = await client.failRuntime({
1218
+ slug,
1219
+ code,
1220
+ message,
1221
+ });
1222
+ printStructuredResult(payload, isTruthyFlag(flags.json));
1223
+ }
1224
+
1225
+ async function handlePersonaJobStartCommand(jobId, flags = {}) {
1226
+ const client = createCliPersonaApiClient(flags);
1227
+ const payload = await client.startJob({
1228
+ jobId,
1229
+ startedAt: parseIsoTimestamp(flags['started-at'], 'started-at'),
1230
+ });
1231
+ printStructuredResult(payload, isTruthyFlag(flags.json));
1232
+ }
1233
+
1234
+ async function handlePersonaJobSucceedCommand(jobId, flags = {}) {
1235
+ const client = createCliPersonaApiClient(flags);
1236
+ const runtime = resolvePersonaRuntimeInput(flags);
1237
+ const workspacePath = String(flags['workspace-path'] || flags.workspacePath || '').trim();
1238
+ if (!workspacePath) {
1239
+ throw new Error('Workspace path is required. Use --workspace-path.');
1240
+ }
1241
+
1242
+ const payload = await client.succeedJob({
1243
+ jobId,
1244
+ workspacePath,
1245
+ localPort: runtime.localPort,
1246
+ transport: runtime.transport,
1247
+ endpoint: runtime.endpoint,
1248
+ healthcheckUrl: runtime.healthcheckUrl,
1249
+ completedAt: parseIsoTimestamp(flags['completed-at'], 'completed-at'),
1250
+ });
1251
+ printStructuredResult(payload, isTruthyFlag(flags.json));
1252
+ }
1253
+
1254
+ async function handlePersonaJobFailCommand(jobId, flags = {}) {
1255
+ const code = String(flags.code || '').trim();
1256
+ const message = String(flags.message || '').trim();
1257
+ if (!code) {
1258
+ throw new Error('Error code is required. Use --code.');
1259
+ }
1260
+ if (!message) {
1261
+ throw new Error('Error message is required. Use --message.');
1262
+ }
1263
+
1264
+ const client = createCliPersonaApiClient(flags);
1265
+ const payload = await client.failJob({
1266
+ jobId,
1267
+ code,
1268
+ message,
1269
+ completedAt: parseIsoTimestamp(flags['completed-at'], 'completed-at'),
1270
+ });
1271
+ printStructuredResult(payload, isTruthyFlag(flags.json));
1272
+ }
1273
+
1274
+ async function handlePersonaJobExecuteCommand(flags = {}) {
1275
+ const message = readStructuredPersonaJobMessage(flags);
1276
+ const shouldInstall = !isTruthyFlag(flags['no-install']);
1277
+ const shouldStart = !isTruthyFlag(flags['no-start']);
1278
+ const shouldRegister = !isTruthyFlag(flags['no-register']) && shouldStart;
1279
+ const logFilePath = String(flags['log-file'] || '').trim();
1280
+ const result = await runManagedPersonaJobExecution({
1281
+ message,
1282
+ backendUrl: resolvePersonaBackendUrl(flags),
1283
+ deviceToken: resolvePersonaDeviceToken(flags),
1284
+ deviceId: resolvePersonaDeviceId(flags),
1285
+ workspaceRoot: String(flags['workspace-root'] || defaultPersonaWorkspaceRoot()).trim(),
1286
+ shouldInstall,
1287
+ shouldStart,
1288
+ shouldRegister,
1289
+ logFilePath,
1290
+ });
1291
+
1292
+ printStructuredResult(result, isTruthyFlag(flags.json));
1293
+ }
1294
+
1295
+ async function handlePersonaCreateManagedCommand(flags = {}, positionalSlug = '') {
1296
+ const name = String(flags.name || '').trim();
1297
+ if (!name) {
1298
+ throw new Error('Persona name is required. Usage: oomi personas create-managed [slug] --name "<name>" --description "<description>"');
1299
+ }
1300
+
1301
+ const description = String(flags.description || '').trim() || name;
1302
+ const explicitSlug = String(flags.slug || positionalSlug || '').trim();
1303
+ const client = createCliPersonaApiClient(flags);
1304
+ const result = await client.createManagedPersona({
1305
+ slug: explicitSlug,
1306
+ name,
1307
+ description,
1308
+ templateType: String(flags['template-type'] || 'persona-app').trim() || 'persona-app',
1309
+ promptTemplateVersion: String(flags['template-version'] || 'v1').trim() || 'v1',
1310
+ });
1311
+
1312
+ printManagedPersonaCreateResult(result, isTruthyFlag(flags.json));
1313
+ }
1314
+
1315
+ async function handlePersonaLaunchManagedCommand(flags = {}, positionalSlug = '') {
1316
+ const client = createCliPersonaApiClient(flags);
1317
+ const safeName = String(flags.name || '').trim();
1318
+ const safeDescription = String(flags.description || '').trim() || safeName;
1319
+ const safeSlug = String(flags.slug || positionalSlug || (safeName ? slugifyPersonaName(safeName) : '')).trim();
1320
+ if (!safeSlug) {
1321
+ throw new Error('Persona slug or name is required. Usage: oomi personas launch-managed [slug] --name "<name>"');
1322
+ }
1323
+
1324
+ const shouldCreate = !isTruthyFlag(flags['no-create']);
1325
+ let createdPersona = false;
1326
+ let personaPayload = await findExistingManagedPersona(client, safeSlug);
1327
+ if (!personaPayload) {
1328
+ if (!shouldCreate) {
1329
+ throw new Error(`Managed persona ${safeSlug} does not exist in Oomi. Remove --no-create or create it first.`);
1330
+ }
1331
+ if (!safeName) {
1332
+ throw new Error('Persona name is required when creating a new managed persona.');
1333
+ }
1334
+ personaPayload = await client.createManagedPersona({
1335
+ slug: safeSlug,
1336
+ name: safeName,
1337
+ description: safeDescription,
1338
+ templateType: 'persona-app',
1339
+ promptTemplateVersion: resolvePersonaTemplateVersion(flags),
1340
+ });
1341
+ createdPersona = true;
1342
+ }
1343
+
1344
+ const persona = personaPayload?.persona && typeof personaPayload.persona === 'object' ? personaPayload.persona : {};
1345
+ const launchResult = await launchManagedPersonaRuntime({
1346
+ slug: String(persona.slug || safeSlug).trim(),
1347
+ name: String(persona.name || safeName || safeSlug).trim(),
1348
+ description: String(persona.description || safeDescription || safeName || safeSlug).trim(),
1349
+ workspaceRoot: resolvePersonaWorkspaceRoot(flags),
1350
+ templateVersion: String(persona.promptTemplateVersion || resolvePersonaTemplateVersion(flags)).trim() || 'v1',
1351
+ forceInstall: isTruthyFlag(flags['force-install']),
1352
+ restart: isTruthyFlag(flags.restart),
1353
+ logFilePath: String(flags['log-file'] || '').trim(),
1354
+ entryUrl: resolvePersonaEntryUrl(flags),
1355
+ transport: resolvePersonaLaunchTransport(flags),
1356
+ });
1357
+
1358
+ let registrationPayload = null;
1359
+ if (!isTruthyFlag(flags['no-register'])) {
1360
+ registrationPayload = await client.registerRuntime({
1361
+ slug: launchResult.slug,
1362
+ endpoint: launchResult.runtime.endpoint,
1363
+ healthcheckUrl: launchResult.runtime.healthcheckUrl,
1364
+ localPort: launchResult.runtime.localPort,
1365
+ transport: launchResult.runtime.transport,
1366
+ startedAt: parseIsoTimestamp(flags['started-at'], 'started-at') || new Date().toISOString(),
1367
+ });
1368
+ }
1369
+
1370
+ printStructuredResult({
1371
+ ok: true,
1372
+ persona,
1373
+ createdPersona,
1374
+ launch: {
1375
+ slug: launchResult.slug,
1376
+ workspacePath: launchResult.workspacePath,
1377
+ scaffolded: launchResult.scaffolded,
1378
+ installed: launchResult.installed,
1379
+ reusedRunningProcess: launchResult.reusedRunningProcess,
1380
+ },
1381
+ runtime: launchResult.runtime,
1382
+ localRuntime: launchResult.localRuntime,
1383
+ state: launchResult.state,
1384
+ registration: registrationPayload,
1385
+ }, isTruthyFlag(flags.json));
1386
+ }
1387
+
1388
+ async function handlePersonaStatusCommand(slug, flags = {}) {
1389
+ const result = getManagedPersonaRuntimeStatus({
1390
+ slug,
1391
+ workspaceRoot: resolvePersonaWorkspaceRoot(flags),
1392
+ });
1393
+ printStructuredResult(result, isTruthyFlag(flags.json));
1394
+ }
1395
+
1396
+ async function handlePersonaStopCommand(slug, flags = {}) {
1397
+ const result = await stopManagedPersonaRuntime({
1398
+ slug,
1399
+ workspaceRoot: resolvePersonaWorkspaceRoot(flags),
1400
+ });
1401
+ printStructuredResult(result, isTruthyFlag(flags.json));
1402
+ }
1403
+
1404
+ async function handlePersonaDeleteCommand(slug, flags = {}) {
1405
+ const result = await destroyManagedPersonaRuntime({
1406
+ slug,
1407
+ workspaceRoot: resolvePersonaWorkspaceRoot(flags),
1408
+ });
1409
+ printStructuredResult(result, isTruthyFlag(flags.json));
1410
+ }
1411
+
1412
+ function resolveOpenclawConfigPath() {
1413
+ const candidates = resolveOpenclawConfigCandidates();
1414
+ for (const candidate of candidates) {
1415
+ if (fs.existsSync(candidate)) return candidate;
1416
+ }
1417
+ return null;
1418
+ }
1419
+
1420
+ function readOpenclawGatewayConfig() {
1421
+ const configPath = resolveOpenclawConfigPath();
1422
+ if (!configPath) {
1423
+ const openclawHome = resolveOpenclawHome();
1424
+ throw new Error(`OpenClaw config not found (${path.join(openclawHome, 'clawdbot.json')} or ${path.join(openclawHome, 'openclaw.json')}).`);
1425
+ }
1426
+
1427
+ const parsed = JSON.parse(readFile(configPath));
1428
+ const gateway = parsed.gateway || {};
1429
+ const auth = gateway.auth || {};
1430
+ const port = gateway.port || 18789;
1431
+ const bind = gateway.bind || 'loopback';
1432
+ const host = bind === 'all' ? '127.0.0.1' : '127.0.0.1';
1433
+ const gatewayUrl = `ws://${host}:${port}`;
1434
+
1435
+ return {
1436
+ gatewayUrl,
1437
+ token: typeof auth.token === 'string' ? auth.token.trim() : '',
1438
+ password: typeof auth.password === 'string' ? auth.password.trim() : '',
1439
+ configPath,
1440
+ };
1441
+ }
1442
+
1443
+ function resolveBridgeStatePath() {
1444
+ return resolveOpenclawBridgeStatePath();
1445
+ }
1446
+
1447
+ function resolveBridgeStatusPath() {
1448
+ return resolveOpenclawBridgeStatusPath();
1449
+ }
1450
+
1451
+ function resolveBridgeLockPath() {
1452
+ return resolveOpenclawBridgeLockPath();
1453
+ }
1454
+
1455
+ function resolveBridgeLiveLogPath() {
1456
+ return resolveOpenclawBridgeLiveLogPath();
1457
+ }
1458
+
1459
+ function resolveBridgeLaunchAgentPlistPath() {
1460
+ return path.join(os.homedir(), 'Library', 'LaunchAgents', `${BRIDGE_LAUNCHD_LABEL}.plist`);
1461
+ }
1462
+
1463
+ function defaultDeviceId() {
1464
+ const host = os.hostname().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'openclaw';
1465
+ return `oomi-${host}-${randomUUID().slice(0, 8)}`;
1466
+ }
1467
+
1468
+ function resolveDeviceId(flags, bridgeState) {
1469
+ const explicit = String(flags['device-id'] || process.env.OOMI_MANAGED_DEVICE_ID || '').trim();
1470
+ if (explicit) return explicit;
1471
+ const existing = String(bridgeState.deviceId || '').trim();
1472
+ if (existing) return existing;
1473
+ return defaultDeviceId();
1474
+ }
1475
+
1476
+ function readBridgeState() {
1477
+ const statePath = resolveBridgeStatePath();
1478
+ if (!fs.existsSync(statePath)) return {};
1479
+ try {
1480
+ return JSON.parse(readFile(statePath));
1481
+ } catch {
1134
1482
  return {};
1135
1483
  }
1136
1484
  }
@@ -1725,499 +2073,847 @@ function prepareGatewayFrameForLocalGateway(frameText, gatewayAuth, options = {}
1725
2073
  }
1726
2074
  }
1727
2075
 
1728
- function parseJsonPayload(raw) {
1729
- try {
1730
- return JSON.parse(raw);
1731
- } catch {
1732
- return null;
1733
- }
1734
- }
1735
-
1736
- function extractTextFromGatewayMessage(message) {
1737
- if (!message || typeof message !== 'object') return '';
1738
-
1739
- if (typeof message.content === 'string' && message.content.trim()) {
1740
- return message.content.trim();
1741
- }
1742
-
1743
- if (!Array.isArray(message.content)) return '';
1744
-
1745
- return message.content
1746
- .filter((block) => block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
1747
- .map((block) => block.text.trim())
1748
- .filter(Boolean)
1749
- .join(' ');
1750
- }
1751
-
1752
- function summarizeVoiceFrameContract(frameText) {
1753
- const frame = parseJsonPayload(frameText);
1754
- if (!frame || typeof frame !== 'object') {
1755
- return { parseable: false };
1756
- }
1757
- const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : {};
1758
- const message = payload.message && typeof payload.message === 'object' ? payload.message : {};
1759
- const metadata = message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
1760
- const spokenRaw = Object.prototype.hasOwnProperty.call(metadata, 'spoken') ? metadata.spoken : undefined;
1761
- const spokenNormalized = normalizeSpokenMetadata(spokenRaw);
1762
- const text = extractTextFromGatewayMessage(message);
1763
- return {
1764
- parseable: true,
1765
- event: typeof frame.event === 'string' ? frame.event : '',
1766
- state: typeof payload.state === 'string' ? payload.state : '',
1767
- role: typeof message.role === 'string' ? message.role : '',
1768
- contentLength: text.length,
1769
- hasMetadata: Object.keys(metadata).length > 0,
1770
- hasSpokenKey: Object.prototype.hasOwnProperty.call(metadata, 'spoken'),
1771
- spokenRawType: spokenRaw === undefined ? 'missing' : Array.isArray(spokenRaw) ? 'array' : typeof spokenRaw,
1772
- spokenNormalized: Boolean(spokenNormalized),
1773
- spokenSegmentCount: Array.isArray(spokenNormalized?.segments) ? spokenNormalized.segments.length : 0,
1774
- };
1775
- }
1776
-
1777
- function ensureAssistantSpokenMetadata(frameText) {
1778
- const frame = parseJsonPayload(frameText);
1779
- if (!frame || typeof frame !== 'object') {
1780
- return { frameText, changed: false, reason: '' };
1781
- }
1782
- if (frame.type !== 'event' || frame.event !== 'chat') {
1783
- return { frameText, changed: false, reason: '' };
1784
- }
1785
-
1786
- const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : null;
1787
- if (!payload || payload.state !== 'final') {
1788
- return { frameText, changed: false, reason: '' };
1789
- }
1790
-
1791
- const message = payload.message && typeof payload.message === 'object' ? payload.message : null;
1792
- if (!message) {
1793
- return { frameText, changed: false, reason: '' };
1794
- }
1795
-
1796
- const messageRole = typeof message.role === 'string' ? message.role.trim() : '';
1797
- if (messageRole && messageRole !== 'assistant') {
1798
- return { frameText, changed: false, reason: '' };
1799
- }
1800
-
1801
- const originalMetadata =
1802
- message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata)
1803
- ? message.metadata
1804
- : {};
1805
- const metadata = { ...originalMetadata };
1806
- const normalizedExplicitSpoken = normalizeSpokenMetadata(originalMetadata.spoken);
1807
- const spoken =
1808
- normalizedExplicitSpoken ||
1809
- inferSpokenMetadataFromContent(extractTextFromGatewayMessage(message));
1810
- if (!spoken) {
1811
- return { frameText, changed: false, reason: '' };
1812
- }
1813
-
1814
- metadata.spoken = spoken;
1815
- const nextFrame = JSON.stringify({
1816
- ...frame,
1817
- payload: {
1818
- ...payload,
1819
- message: {
1820
- ...message,
1821
- metadata,
1822
- },
1823
- },
1824
- });
1825
-
1826
- return {
1827
- frameText: nextFrame,
1828
- changed: nextFrame !== frameText,
1829
- reason: normalizedExplicitSpoken ? 'normalized' : (messageRole ? 'synthesized' : 'synthesized_missing_role'),
1830
- };
1831
- }
1832
-
1833
- function normalizeAssistantGatewayFrame(sessionId, frameText) {
1834
- const scope = classifyBridgeSessionScope(sessionId);
1835
- const summary = summarizeVoiceFrameContract(frameText);
1836
- if (!summary.parseable || summary.event !== 'chat' || summary.state !== 'final') {
1837
- return {
1838
- frameText,
1839
- changed: false,
1840
- reason: '',
1841
- scope,
1842
- summary,
1843
- };
1844
- }
1845
-
1846
- const normalized = ensureAssistantSpokenMetadata(frameText);
1847
- return {
1848
- ...normalized,
1849
- scope,
1850
- summary,
1851
- };
1852
- }
1853
-
1854
- function buildAssistantFinalDebugFrame({ sessionKey, text, role }) {
1855
- const trimmedSessionKey =
1856
- typeof sessionKey === 'string' && sessionKey.trim()
1857
- ? sessionKey.trim()
1858
- : 'agent:main:webchat:channel:oomi';
1859
- const message = {
1860
- content: String(text || ''),
1861
- };
1862
- if (typeof role === 'string' && role.trim()) {
1863
- message.role = role.trim();
1864
- }
1865
- return JSON.stringify({
1866
- type: 'event',
1867
- event: 'chat',
1868
- payload: {
1869
- sessionKey: trimmedSessionKey,
1870
- state: 'final',
1871
- message,
1872
- },
1873
- });
1874
- }
1875
-
1876
- function extractSpokenMetadata(frameText) {
1877
- const payload = parseJsonPayload(frameText);
1878
- const message =
1879
- payload &&
1880
- payload.payload &&
1881
- typeof payload.payload === 'object' &&
1882
- payload.payload.message &&
1883
- typeof payload.payload.message === 'object'
1884
- ? payload.payload.message
1885
- : null;
1886
- const metadata =
1887
- message &&
1888
- message.metadata &&
1889
- typeof message.metadata === 'object' &&
1890
- !Array.isArray(message.metadata)
1891
- ? message.metadata
1892
- : {};
1893
- return normalizeSpokenMetadata(metadata.spoken);
1894
- }
1895
-
1896
- function runAssistantFinalDebugCheck(options = {}) {
1897
- const sessionId =
1898
- typeof options.sessionId === 'string' && options.sessionId.trim()
1899
- ? options.sessionId.trim()
1900
- : 'ms_debug_local';
1901
- const sessionKey =
1902
- typeof options.sessionKey === 'string' && options.sessionKey.trim()
1903
- ? options.sessionKey.trim()
1904
- : 'agent:main:webchat:channel:oomi';
1905
- const role =
1906
- options.omitRole
1907
- ? ''
1908
- : (typeof options.role === 'string' && options.role.trim() ? options.role.trim() : 'assistant');
1909
-
1910
- const rawFrameText =
1911
- typeof options.frameText === 'string' && options.frameText.trim()
1912
- ? options.frameText
1913
- : buildAssistantFinalDebugFrame({
1914
- sessionKey,
1915
- text: options.text,
1916
- role,
1917
- });
1918
-
1919
- const before = summarizeVoiceFrameContract(rawFrameText);
1920
- const normalized = normalizeAssistantGatewayFrame(sessionId, rawFrameText);
1921
- const after = summarizeVoiceFrameContract(normalized.frameText);
1922
- const spoken = extractSpokenMetadata(normalized.frameText);
1923
-
1924
- return {
1925
- sessionId,
1926
- sessionKey,
1927
- scope: normalized.scope,
1928
- changed: normalized.changed,
1929
- reason: normalized.reason,
1930
- before,
1931
- after,
1932
- spoken,
1933
- frameText: normalized.frameText,
1934
- };
1935
- }
1936
-
1937
- function printAssistantFinalDebugResult(result, asJson) {
1938
- if (asJson) {
1939
- console.log(JSON.stringify(result, null, 2));
1940
- return;
1941
- }
1942
-
1943
- console.log(`Session id: ${result.sessionId}`);
1944
- console.log(`Session key: ${result.sessionKey}`);
1945
- console.log(`Scope: ${result.scope}`);
1946
- console.log(`Changed: ${result.changed ? 'yes' : 'no'}${result.reason ? ` (${result.reason})` : ''}`);
1947
- console.log(
1948
- `Before: event=${result.before.event || '<none>'} state=${result.before.state || '<none>'} role=${result.before.role || '<none>'} spoken=${result.before.spokenNormalized ? 'yes' : 'no'}`
1949
- );
1950
- console.log(
1951
- `After: event=${result.after.event || '<none>'} state=${result.after.state || '<none>'} role=${result.after.role || '<none>'} spoken=${result.after.spokenNormalized ? 'yes' : 'no'}`
1952
- );
1953
- if (result.spoken) {
1954
- console.log(`Spoken text: ${result.spoken.text}`);
1955
- console.log(`Segments: ${Array.isArray(result.spoken.segments) ? result.spoken.segments.length : 0}`);
1956
- if (typeof result.spoken.instructions === 'string' && result.spoken.instructions.trim()) {
1957
- console.log(`Instructions: ${result.spoken.instructions}`);
1958
- }
1959
- } else {
1960
- console.log('Spoken text: <missing>');
1961
- }
1962
- }
1963
-
1964
- function resolveCommandFromPath(commandName) {
1965
- const normalized = String(commandName || '').trim();
1966
- if (!normalized) return '';
1967
- try {
1968
- const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', [normalized], {
1969
- encoding: 'utf8',
1970
- stdio: ['ignore', 'pipe', 'ignore'],
1971
- });
1972
- if (probe.status !== 0) return '';
1973
- const firstLine = String(probe.stdout || '')
1974
- .split(/\r?\n/)
1975
- .map((line) => line.trim())
1976
- .find(Boolean);
1977
- return firstLine || '';
1978
- } catch {
1979
- return '';
1980
- }
1981
- }
1982
-
1983
- function resolveExecutable(candidates = []) {
1984
- for (const candidate of candidates) {
1985
- if (!candidate) continue;
1986
- const value = String(candidate).trim();
1987
- if (!value) continue;
1988
- if (path.isAbsolute(value) && fs.existsSync(value)) {
1989
- return value;
1990
- }
1991
- if (value.includes(path.sep) || value.includes('/')) {
1992
- const resolved = path.resolve(value);
1993
- if (fs.existsSync(resolved)) {
1994
- return resolved;
1995
- }
1996
- continue;
1997
- }
1998
- const fromPath = resolveCommandFromPath(value);
1999
- if (fromPath) {
2000
- return fromPath;
2001
- }
2002
- }
2003
- return '';
2004
- }
2005
-
2006
- function resolveBackendRoot(rootFlag) {
2007
- const repoRoot = resolveRepoRoot(rootFlag);
2008
- const backendRoot = path.join(repoRoot, 'apps', 'backend');
2009
- if (!fs.existsSync(backendRoot)) {
2010
- throw new Error(`Could not locate backend app at ${backendRoot}`);
2011
- }
2012
- return backendRoot;
2013
- }
2014
-
2015
- function resolveRubyExecutable() {
2016
- const candidates = [
2017
- process.env.OOMI_RUBY_BIN,
2018
- process.env.RUBY,
2019
- process.platform === 'win32' ? 'ruby.exe' : 'ruby',
2020
- process.platform === 'win32' ? 'ruby' : '',
2021
- process.platform === 'win32' ? 'C:\\Ruby33-x64\\bin\\ruby.exe' : '',
2022
- ];
2023
- const executable = resolveExecutable(candidates);
2024
- if (!executable) {
2025
- throw new Error('Ruby executable not found. Set OOMI_RUBY_BIN or install Ruby locally.');
2026
- }
2027
- return executable;
2028
- }
2029
-
2030
- function resolveBundleExecutable() {
2031
- const candidates = [
2032
- process.env.OOMI_BUNDLE_BIN,
2033
- process.platform === 'win32' ? 'bundle.bat' : 'bundle',
2034
- 'bundle',
2035
- process.platform === 'win32' ? 'C:\\Ruby33-x64\\bin\\bundle.bat' : '',
2036
- ];
2037
- const executable = resolveExecutable(candidates);
2038
- if (!executable) {
2039
- throw new Error('Bundler executable not found. Set OOMI_BUNDLE_BIN or install Bundler locally.');
2040
- }
2041
- return executable;
2042
- }
2043
-
2044
- function shellQuote(value) {
2045
- const text = String(value);
2046
- if (process.platform === 'win32') {
2047
- return `"${text.replace(/"/g, '""')}"`;
2048
- }
2049
- return `'${text.replace(/'/g, `'\\''`)}'`;
2050
- }
2051
-
2052
- async function runBundledRubyScript({ backendRoot, scriptPath, inputFile, env = undefined }) {
2053
- const rubyExecutable = resolveRubyExecutable();
2054
- const bundleExecutable = resolveBundleExecutable();
2055
- const commandText = process.platform === 'win32'
2056
- ? [bundleExecutable, 'exec', rubyExecutable, scriptPath, '--input-file', inputFile].map(shellQuote).join(' ')
2057
- : '';
2058
- const childEnv = env ? { ...process.env, ...env } : process.env;
2059
-
2060
- return await new Promise((resolve, reject) => {
2061
- const child = process.platform === 'win32'
2062
- ? spawn(commandText, [], {
2063
- cwd: backendRoot,
2064
- shell: true,
2065
- env: childEnv,
2066
- stdio: ['ignore', 'pipe', 'pipe'],
2067
- })
2068
- : spawn(bundleExecutable, ['exec', rubyExecutable, scriptPath, '--input-file', inputFile], {
2069
- cwd: backendRoot,
2070
- env: childEnv,
2071
- stdio: ['ignore', 'pipe', 'pipe'],
2072
- });
2073
-
2074
- let stdout = '';
2075
- let stderr = '';
2076
- child.stdout.on('data', (chunk) => {
2077
- stdout += chunk.toString();
2078
- });
2079
- child.stderr.on('data', (chunk) => {
2080
- stderr += chunk.toString();
2081
- });
2082
- child.on('error', reject);
2083
- child.on('close', (code) => {
2084
- resolve({ code: Number(code || 0), stdout, stderr });
2085
- });
2076
+ function parseJsonPayload(raw) {
2077
+ try {
2078
+ return JSON.parse(raw);
2079
+ } catch {
2080
+ return null;
2081
+ }
2082
+ }
2083
+
2084
+ function extractTextFromGatewayMessage(message) {
2085
+ if (!message || typeof message !== 'object') return '';
2086
+
2087
+ if (typeof message.content === 'string' && message.content.trim()) {
2088
+ return message.content.trim();
2089
+ }
2090
+
2091
+ if (!Array.isArray(message.content)) return '';
2092
+
2093
+ return message.content
2094
+ .filter((block) => block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
2095
+ .map((block) => block.text.trim())
2096
+ .filter(Boolean)
2097
+ .join(' ');
2098
+ }
2099
+
2100
+ function summarizeVoiceFrameContract(frameText) {
2101
+ const frame = parseJsonPayload(frameText);
2102
+ if (!frame || typeof frame !== 'object') {
2103
+ return { parseable: false };
2104
+ }
2105
+ const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : {};
2106
+ const message = payload.message && typeof payload.message === 'object' ? payload.message : {};
2107
+ const metadata = message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
2108
+ const spokenRaw = Object.prototype.hasOwnProperty.call(metadata, 'spoken') ? metadata.spoken : undefined;
2109
+ const spokenNormalized = normalizeSpokenMetadata(spokenRaw);
2110
+ const text = extractTextFromGatewayMessage(message);
2111
+ return {
2112
+ parseable: true,
2113
+ event: typeof frame.event === 'string' ? frame.event : '',
2114
+ state: typeof payload.state === 'string' ? payload.state : '',
2115
+ role: typeof message.role === 'string' ? message.role : '',
2116
+ contentLength: text.length,
2117
+ hasMetadata: Object.keys(metadata).length > 0,
2118
+ hasSpokenKey: Object.prototype.hasOwnProperty.call(metadata, 'spoken'),
2119
+ spokenRawType: spokenRaw === undefined ? 'missing' : Array.isArray(spokenRaw) ? 'array' : typeof spokenRaw,
2120
+ spokenNormalized: Boolean(spokenNormalized),
2121
+ spokenSegmentCount: Array.isArray(spokenNormalized?.segments) ? spokenNormalized.segments.length : 0,
2122
+ };
2123
+ }
2124
+
2125
+ function ensureAssistantSpokenMetadata(frameText) {
2126
+ const frame = parseJsonPayload(frameText);
2127
+ if (!frame || typeof frame !== 'object') {
2128
+ return { frameText, changed: false, reason: '' };
2129
+ }
2130
+ if (frame.type !== 'event' || frame.event !== 'chat') {
2131
+ return { frameText, changed: false, reason: '' };
2132
+ }
2133
+
2134
+ const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : null;
2135
+ if (!payload || payload.state !== 'final') {
2136
+ return { frameText, changed: false, reason: '' };
2137
+ }
2138
+
2139
+ const message = payload.message && typeof payload.message === 'object' ? payload.message : null;
2140
+ if (!message) {
2141
+ return { frameText, changed: false, reason: '' };
2142
+ }
2143
+
2144
+ const messageRole = typeof message.role === 'string' ? message.role.trim() : '';
2145
+ if (messageRole && messageRole !== 'assistant') {
2146
+ return { frameText, changed: false, reason: '' };
2147
+ }
2148
+
2149
+ const originalMetadata =
2150
+ message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata)
2151
+ ? message.metadata
2152
+ : {};
2153
+ const metadata = { ...originalMetadata };
2154
+ const normalizedExplicitSpoken = normalizeSpokenMetadata(originalMetadata.spoken);
2155
+ const spoken =
2156
+ normalizedExplicitSpoken ||
2157
+ inferSpokenMetadataFromContent(extractTextFromGatewayMessage(message));
2158
+ if (!spoken) {
2159
+ return { frameText, changed: false, reason: '' };
2160
+ }
2161
+
2162
+ metadata.spoken = spoken;
2163
+ const nextFrame = JSON.stringify({
2164
+ ...frame,
2165
+ payload: {
2166
+ ...payload,
2167
+ message: {
2168
+ ...message,
2169
+ metadata,
2170
+ },
2171
+ },
2172
+ });
2173
+
2174
+ return {
2175
+ frameText: nextFrame,
2176
+ changed: nextFrame !== frameText,
2177
+ reason: normalizedExplicitSpoken ? 'normalized' : (messageRole ? 'synthesized' : 'synthesized_missing_role'),
2178
+ };
2179
+ }
2180
+
2181
+ function normalizeAssistantGatewayFrame(sessionId, frameText) {
2182
+ const scope = classifyBridgeSessionScope(sessionId);
2183
+ const summary = summarizeVoiceFrameContract(frameText);
2184
+ if (!summary.parseable || summary.event !== 'chat' || summary.state !== 'final') {
2185
+ return {
2186
+ frameText,
2187
+ changed: false,
2188
+ reason: '',
2189
+ scope,
2190
+ summary,
2191
+ };
2192
+ }
2193
+
2194
+ const normalized = ensureAssistantSpokenMetadata(frameText);
2195
+ return {
2196
+ ...normalized,
2197
+ scope,
2198
+ summary,
2199
+ };
2200
+ }
2201
+
2202
+ function buildAssistantFinalDebugFrame({ sessionKey, text, role }) {
2203
+ const trimmedSessionKey =
2204
+ typeof sessionKey === 'string' && sessionKey.trim()
2205
+ ? sessionKey.trim()
2206
+ : 'agent:main:webchat:channel:oomi';
2207
+ const message = {
2208
+ content: String(text || ''),
2209
+ };
2210
+ if (typeof role === 'string' && role.trim()) {
2211
+ message.role = role.trim();
2212
+ }
2213
+ return JSON.stringify({
2214
+ type: 'event',
2215
+ event: 'chat',
2216
+ payload: {
2217
+ sessionKey: trimmedSessionKey,
2218
+ state: 'final',
2219
+ message,
2220
+ },
2221
+ });
2222
+ }
2223
+
2224
+ function extractSpokenMetadata(frameText) {
2225
+ const payload = parseJsonPayload(frameText);
2226
+ const message =
2227
+ payload &&
2228
+ payload.payload &&
2229
+ typeof payload.payload === 'object' &&
2230
+ payload.payload.message &&
2231
+ typeof payload.payload.message === 'object'
2232
+ ? payload.payload.message
2233
+ : null;
2234
+ const metadata =
2235
+ message &&
2236
+ message.metadata &&
2237
+ typeof message.metadata === 'object' &&
2238
+ !Array.isArray(message.metadata)
2239
+ ? message.metadata
2240
+ : {};
2241
+ return normalizeSpokenMetadata(metadata.spoken);
2242
+ }
2243
+
2244
+ function runAssistantFinalDebugCheck(options = {}) {
2245
+ const sessionId =
2246
+ typeof options.sessionId === 'string' && options.sessionId.trim()
2247
+ ? options.sessionId.trim()
2248
+ : 'ms_debug_local';
2249
+ const sessionKey =
2250
+ typeof options.sessionKey === 'string' && options.sessionKey.trim()
2251
+ ? options.sessionKey.trim()
2252
+ : 'agent:main:webchat:channel:oomi';
2253
+ const role =
2254
+ options.omitRole
2255
+ ? ''
2256
+ : (typeof options.role === 'string' && options.role.trim() ? options.role.trim() : 'assistant');
2257
+
2258
+ const rawFrameText =
2259
+ typeof options.frameText === 'string' && options.frameText.trim()
2260
+ ? options.frameText
2261
+ : buildAssistantFinalDebugFrame({
2262
+ sessionKey,
2263
+ text: options.text,
2264
+ role,
2265
+ });
2266
+
2267
+ const before = summarizeVoiceFrameContract(rawFrameText);
2268
+ const normalized = normalizeAssistantGatewayFrame(sessionId, rawFrameText);
2269
+ const after = summarizeVoiceFrameContract(normalized.frameText);
2270
+ const spoken = extractSpokenMetadata(normalized.frameText);
2271
+
2272
+ return {
2273
+ sessionId,
2274
+ sessionKey,
2275
+ scope: normalized.scope,
2276
+ changed: normalized.changed,
2277
+ reason: normalized.reason,
2278
+ before,
2279
+ after,
2280
+ spoken,
2281
+ frameText: normalized.frameText,
2282
+ };
2283
+ }
2284
+
2285
+ function printAssistantFinalDebugResult(result, asJson) {
2286
+ if (asJson) {
2287
+ console.log(JSON.stringify(result, null, 2));
2288
+ return;
2289
+ }
2290
+
2291
+ console.log(`Session id: ${result.sessionId}`);
2292
+ console.log(`Session key: ${result.sessionKey}`);
2293
+ console.log(`Scope: ${result.scope}`);
2294
+ console.log(`Changed: ${result.changed ? 'yes' : 'no'}${result.reason ? ` (${result.reason})` : ''}`);
2295
+ console.log(
2296
+ `Before: event=${result.before.event || '<none>'} state=${result.before.state || '<none>'} role=${result.before.role || '<none>'} spoken=${result.before.spokenNormalized ? 'yes' : 'no'}`
2297
+ );
2298
+ console.log(
2299
+ `After: event=${result.after.event || '<none>'} state=${result.after.state || '<none>'} role=${result.after.role || '<none>'} spoken=${result.after.spokenNormalized ? 'yes' : 'no'}`
2300
+ );
2301
+ if (result.spoken) {
2302
+ console.log(`Spoken text: ${result.spoken.text}`);
2303
+ console.log(`Segments: ${Array.isArray(result.spoken.segments) ? result.spoken.segments.length : 0}`);
2304
+ if (typeof result.spoken.instructions === 'string' && result.spoken.instructions.trim()) {
2305
+ console.log(`Instructions: ${result.spoken.instructions}`);
2306
+ }
2307
+ } else {
2308
+ console.log('Spoken text: <missing>');
2309
+ }
2310
+ }
2311
+
2312
+ function resolveCommandFromPath(commandName) {
2313
+ const normalized = String(commandName || '').trim();
2314
+ if (!normalized) return '';
2315
+ try {
2316
+ const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', [normalized], {
2317
+ encoding: 'utf8',
2318
+ stdio: ['ignore', 'pipe', 'ignore'],
2319
+ });
2320
+ if (probe.status !== 0) return '';
2321
+ const firstLine = String(probe.stdout || '')
2322
+ .split(/\r?\n/)
2323
+ .map((line) => line.trim())
2324
+ .find(Boolean);
2325
+ return firstLine || '';
2326
+ } catch {
2327
+ return '';
2328
+ }
2329
+ }
2330
+
2331
+ function resolveExecutable(candidates = []) {
2332
+ for (const candidate of candidates) {
2333
+ if (!candidate) continue;
2334
+ const value = String(candidate).trim();
2335
+ if (!value) continue;
2336
+ if (path.isAbsolute(value) && fs.existsSync(value)) {
2337
+ return value;
2338
+ }
2339
+ if (value.includes(path.sep) || value.includes('/')) {
2340
+ const resolved = path.resolve(value);
2341
+ if (fs.existsSync(resolved)) {
2342
+ return resolved;
2343
+ }
2344
+ continue;
2345
+ }
2346
+ const fromPath = resolveCommandFromPath(value);
2347
+ if (fromPath) {
2348
+ return fromPath;
2349
+ }
2350
+ }
2351
+ return '';
2352
+ }
2353
+
2354
+ function resolveBackendRoot(rootFlag) {
2355
+ const repoRoot = resolveRepoRoot(rootFlag);
2356
+ const backendRoot = path.join(repoRoot, 'apps', 'backend');
2357
+ if (!fs.existsSync(backendRoot)) {
2358
+ throw new Error(`Could not locate backend app at ${backendRoot}`);
2359
+ }
2360
+ return backendRoot;
2361
+ }
2362
+
2363
+ function resolveRubyExecutable() {
2364
+ const candidates = [
2365
+ process.env.OOMI_RUBY_BIN,
2366
+ process.env.RUBY,
2367
+ process.platform === 'win32' ? 'ruby.exe' : 'ruby',
2368
+ process.platform === 'win32' ? 'ruby' : '',
2369
+ process.platform === 'win32' ? 'C:\\Ruby33-x64\\bin\\ruby.exe' : '',
2370
+ ];
2371
+ const executable = resolveExecutable(candidates);
2372
+ if (!executable) {
2373
+ throw new Error('Ruby executable not found. Set OOMI_RUBY_BIN or install Ruby locally.');
2374
+ }
2375
+ return executable;
2376
+ }
2377
+
2378
+ function resolveBundleExecutable() {
2379
+ const candidates = [
2380
+ process.env.OOMI_BUNDLE_BIN,
2381
+ process.platform === 'win32' ? 'bundle.bat' : 'bundle',
2382
+ 'bundle',
2383
+ process.platform === 'win32' ? 'C:\\Ruby33-x64\\bin\\bundle.bat' : '',
2384
+ ];
2385
+ const executable = resolveExecutable(candidates);
2386
+ if (!executable) {
2387
+ throw new Error('Bundler executable not found. Set OOMI_BUNDLE_BIN or install Bundler locally.');
2388
+ }
2389
+ return executable;
2390
+ }
2391
+
2392
+ function shellQuote(value) {
2393
+ const text = String(value);
2394
+ if (process.platform === 'win32') {
2395
+ return `"${text.replace(/"/g, '""')}"`;
2396
+ }
2397
+ return `'${text.replace(/'/g, `'\\''`)}'`;
2398
+ }
2399
+
2400
+ async function runBundledRubyScript({ backendRoot, scriptPath, inputFile, env = undefined }) {
2401
+ const rubyExecutable = resolveRubyExecutable();
2402
+ const bundleExecutable = resolveBundleExecutable();
2403
+ const commandText = process.platform === 'win32'
2404
+ ? [bundleExecutable, 'exec', rubyExecutable, scriptPath, '--input-file', inputFile].map(shellQuote).join(' ')
2405
+ : '';
2406
+ const childEnv = env ? { ...process.env, ...env } : process.env;
2407
+
2408
+ return await new Promise((resolve, reject) => {
2409
+ const child = process.platform === 'win32'
2410
+ ? spawn(commandText, [], {
2411
+ cwd: backendRoot,
2412
+ shell: true,
2413
+ env: childEnv,
2414
+ stdio: ['ignore', 'pipe', 'pipe'],
2415
+ })
2416
+ : spawn(bundleExecutable, ['exec', rubyExecutable, scriptPath, '--input-file', inputFile], {
2417
+ cwd: backendRoot,
2418
+ env: childEnv,
2419
+ stdio: ['ignore', 'pipe', 'pipe'],
2420
+ });
2421
+
2422
+ let stdout = '';
2423
+ let stderr = '';
2424
+ child.stdout.on('data', (chunk) => {
2425
+ stdout += chunk.toString();
2426
+ });
2427
+ child.stderr.on('data', (chunk) => {
2428
+ stderr += chunk.toString();
2429
+ });
2430
+ child.on('error', reject);
2431
+ child.on('close', (code) => {
2432
+ resolve({ code: Number(code || 0), stdout, stderr });
2433
+ });
2434
+ });
2435
+ }
2436
+
2437
+ async function runLocalTtsPipelineDebugCheck(options = {}) {
2438
+ const assistant = runAssistantFinalDebugCheck(options);
2439
+ const repoRoot = resolveRepoRoot(options.root);
2440
+ const backendRoot = resolveBackendRoot(options.root);
2441
+ const scriptPath = path.join(backendRoot, 'bin', 'voice_tts_replay.rb');
2442
+ if (!fs.existsSync(scriptPath)) {
2443
+ throw new Error(`Backend replay script not found: ${scriptPath}`);
2444
+ }
2445
+
2446
+ const inputPayload = {
2447
+ repoRoot,
2448
+ sessionId: assistant.sessionId,
2449
+ sessionKey: assistant.sessionKey,
2450
+ frameText: assistant.frameText,
2451
+ userText:
2452
+ typeof options.userText === 'string' && options.userText.trim()
2453
+ ? options.userText.trim()
2454
+ : 'local debug utterance',
2455
+ liveProvider: Boolean(options.liveProvider),
2456
+ providerTimeoutMs: parsePositiveInteger(options.providerTimeoutMs, 15000),
2457
+ };
2458
+ let childEnv = undefined;
2459
+ let resolvedEnvFile = '';
2460
+ if (options.liveProvider) {
2461
+ resolvedEnvFile =
2462
+ typeof options.envFile === 'string' && options.envFile.trim()
2463
+ ? path.resolve(options.envFile.trim())
2464
+ : path.join(repoRoot, '.env.local');
2465
+ childEnv = loadEnvFile(resolvedEnvFile, DEBUG_PROVIDER_ENV_KEYS);
2466
+ }
2467
+ const inputFile = path.join(os.tmpdir(), `oomi-voice-replay-${randomUUID()}.json`);
2468
+ writeFile(inputFile, JSON.stringify(inputPayload, null, 2) + '\n');
2469
+
2470
+ try {
2471
+ const backend = await runBundledRubyScript({ backendRoot, scriptPath, inputFile, env: childEnv });
2472
+ const parsed = backend.stdout.trim() ? JSON.parse(backend.stdout) : null;
2473
+ return {
2474
+ assistant,
2475
+ backend: parsed,
2476
+ backendExitCode: backend.code,
2477
+ backendStderr: backend.stderr.trim(),
2478
+ liveProvider: Boolean(options.liveProvider),
2479
+ envFile: resolvedEnvFile || null,
2480
+ };
2481
+ } finally {
2482
+ try {
2483
+ fs.unlinkSync(inputFile);
2484
+ } catch {
2485
+ // no-op
2486
+ }
2487
+ }
2488
+ }
2489
+
2490
+ function printTtsPipelineDebugResult(result, asJson) {
2491
+ if (asJson) {
2492
+ console.log(JSON.stringify(result, null, 2));
2493
+ return;
2494
+ }
2495
+
2496
+ console.log(`Assistant normalization: ${result.assistant.changed ? 'changed' : 'unchanged'}${result.assistant.reason ? ` (${result.assistant.reason})` : ''}`);
2497
+ console.log(`Assistant spoken segments: ${Array.isArray(result.assistant.spoken?.segments) ? result.assistant.spoken.segments.length : 0}`);
2498
+ if (!result.backend) {
2499
+ console.log('Backend replay: <no output>');
2500
+ return;
2501
+ }
2502
+ console.log(`Backend replay success: ${result.backend.success ? 'yes' : 'no'}`);
2503
+ console.log(`Managed speech sidecar: ${result.backend.managed?.assistantSpeechFinal?.present ? 'yes' : 'no'}`);
2504
+ console.log(`Backend final text: ${result.backend.qwen?.assistantTextFinal || '<missing>'}`);
2505
+ console.log(`Backend TTS appends: ${Array.isArray(result.backend.qwen?.ttsAppends) ? result.backend.qwen.ttsAppends.length : 0}`);
2506
+ console.log(`Backend TTS commits: ${Number(result.backend.qwen?.commitCount || 0)}`);
2507
+ if (result.liveProvider) {
2508
+ console.log(`Live provider audio deltas: ${Number(result.backend.qwen?.audioDeltaCount || 0)}`);
2509
+ console.log(`Live provider audio bytes (base64): ${Number(result.backend.qwen?.audioDeltaBytes || 0)}`);
2510
+ console.log(`Live provider timeout: ${result.backend.qwen?.providerTimedOut ? 'yes' : 'no'}`);
2511
+ }
2512
+ if (result.backend.qwen?.errorCode) {
2513
+ console.log(`Backend error: ${result.backend.qwen.errorCode}`);
2514
+ }
2515
+ if (result.backendStderr) {
2516
+ console.log(`Backend stderr: ${result.backendStderr}`);
2517
+ }
2518
+ }
2519
+
2520
+ async function runPersonaRuntimeDebugCheck(options = {}) {
2521
+ const generatedWorkspaceRoot = !(
2522
+ typeof options.workspaceRoot === 'string' && options.workspaceRoot.trim()
2523
+ );
2524
+ const workspaceRoot = generatedWorkspaceRoot
2525
+ ? path.join(os.tmpdir(), `oomi-openclaw-dev-${randomUUID()}`, 'personas')
2526
+ : path.resolve(String(options.workspaceRoot).trim());
2527
+ const safeName =
2528
+ typeof options.name === 'string' && options.name.trim()
2529
+ ? options.name.trim()
2530
+ : 'Persona Dev Smoke';
2531
+ const safeDescription =
2532
+ typeof options.description === 'string' && options.description.trim()
2533
+ ? options.description.trim()
2534
+ : 'Local OpenClaw persona runtime smoke test.';
2535
+ const safeSlug =
2536
+ typeof options.slug === 'string' && options.slug.trim()
2537
+ ? options.slug.trim()
2538
+ : slugifyPersonaName(safeName);
2539
+ const leaveRunning = Boolean(options.leaveRunning);
2540
+ const cleanup = Boolean(options.cleanup);
2541
+
2542
+ const launch = await launchManagedPersonaRuntime({
2543
+ slug: safeSlug,
2544
+ name: safeName,
2545
+ description: safeDescription,
2546
+ workspaceRoot,
2547
+ forceInstall: Boolean(options.forceInstall),
2548
+ restart: Boolean(options.restart),
2549
+ entryUrl: '',
2550
+ transport: 'local',
2551
+ });
2552
+ const statusAfterLaunch = getManagedPersonaRuntimeStatus({
2553
+ slug: safeSlug,
2554
+ workspaceRoot,
2555
+ });
2556
+
2557
+ let stop = null;
2558
+ let statusAfterStop = null;
2559
+ if (!leaveRunning) {
2560
+ stop = await stopManagedPersonaRuntime({
2561
+ slug: safeSlug,
2562
+ workspaceRoot,
2563
+ });
2564
+ statusAfterStop = getManagedPersonaRuntimeStatus({
2565
+ slug: safeSlug,
2566
+ workspaceRoot,
2567
+ });
2568
+ }
2569
+
2570
+ let cleanedUp = false;
2571
+ if (cleanup && !leaveRunning && generatedWorkspaceRoot) {
2572
+ cleanedUp = await cleanupPersonaRuntimeDebugWorkspace(path.resolve(workspaceRoot, '..'));
2573
+ }
2574
+
2575
+ return {
2576
+ ok: true,
2577
+ workspaceRoot,
2578
+ generatedWorkspaceRoot,
2579
+ cleanedUp,
2580
+ slug: safeSlug,
2581
+ name: safeName,
2582
+ description: safeDescription,
2583
+ launch: {
2584
+ slug: launch.slug,
2585
+ workspacePath: launch.workspacePath,
2586
+ scaffolded: launch.scaffolded,
2587
+ installed: launch.installed,
2588
+ reusedRunningProcess: launch.reusedRunningProcess,
2589
+ },
2590
+ runtime: launch.runtime,
2591
+ localRuntime: launch.localRuntime,
2592
+ state: launch.state,
2593
+ statusAfterLaunch,
2594
+ stop,
2595
+ statusAfterStop,
2596
+ };
2597
+ }
2598
+
2599
+ async function cleanupPersonaRuntimeDebugWorkspace(rootPath) {
2600
+ const targetPath = path.resolve(rootPath);
2601
+ for (let attempt = 0; attempt < 5; attempt += 1) {
2602
+ try {
2603
+ fs.rmSync(targetPath, { recursive: true, force: true });
2604
+ return true;
2605
+ } catch {
2606
+ await new Promise((resolve) => setTimeout(resolve, 200));
2607
+ }
2608
+ }
2609
+ return false;
2610
+ }
2611
+
2612
+ function printPersonaRuntimeDebugResult(result, asJson) {
2613
+ if (asJson) {
2614
+ console.log(JSON.stringify(result, null, 2));
2615
+ return;
2616
+ }
2617
+
2618
+ console.log(`Persona slug: ${result.slug}`);
2619
+ console.log(`Workspace root: ${result.workspaceRoot}`);
2620
+ console.log(`Workspace: ${result.launch.workspacePath}`);
2621
+ console.log(`Scaffolded: ${result.launch.scaffolded ? 'yes' : 'no'}`);
2622
+ console.log(`Installed dependencies: ${result.launch.installed ? 'yes' : 'no'}`);
2623
+ console.log(`Reused running process: ${result.launch.reusedRunningProcess ? 'yes' : 'no'}`);
2624
+ console.log(`Local endpoint: ${result.localRuntime.endpoint}`);
2625
+ console.log(`Local port: ${result.localRuntime.localPort}`);
2626
+ console.log(`Healthcheck: ${result.localRuntime.healthcheckUrl}`);
2627
+ console.log(`Process running after launch: ${result.statusAfterLaunch.processRunning ? 'yes' : 'no'}`);
2628
+ if (result.stop) {
2629
+ console.log(`Stopped runtime: ${result.stop.stopped ? 'yes' : 'no'}`);
2630
+ console.log(`Process running after stop: ${result.statusAfterStop?.processRunning ? 'yes' : 'no'}`);
2631
+ } else {
2632
+ console.log('Runtime left running for inspection.');
2633
+ }
2634
+ if (result.cleanedUp) {
2635
+ console.log('Temporary workspace cleaned up.');
2636
+ }
2637
+ }
2638
+
2639
+ async function runLocalGatewayAgentDebug(flags) {
2640
+ const logger = isTruthyFlag(flags.json)
2641
+ ? () => {}
2642
+ : (...args) => {
2643
+ console.log(...args);
2644
+ };
2645
+
2646
+ const server = await startLocalGatewayAgentServer({
2647
+ host: flags.host,
2648
+ port: flags.port,
2649
+ token: flags.token,
2650
+ password: flags.password,
2651
+ logger,
2652
+ });
2653
+
2654
+ const readyPayload = {
2655
+ ok: true,
2656
+ host: server.host,
2657
+ port: server.port,
2658
+ url: `ws://${server.host}:${server.port}`,
2659
+ tokenConfigured: Boolean(server.token),
2660
+ passwordConfigured: Boolean(server.password),
2661
+ };
2662
+
2663
+ if (isTruthyFlag(flags.json)) {
2664
+ console.log(JSON.stringify(readyPayload, null, 2));
2665
+ } else {
2666
+ console.log(`Local OpenClaw dev gateway listening on ${readyPayload.url}`);
2667
+ }
2668
+
2669
+ let closing = false;
2670
+ const shutdown = async () => {
2671
+ if (closing) return;
2672
+ closing = true;
2673
+ await server.close();
2674
+ process.exit(0);
2675
+ };
2676
+
2677
+ process.once('SIGINT', () => {
2678
+ void shutdown();
2679
+ });
2680
+ process.once('SIGTERM', () => {
2681
+ void shutdown();
2682
+ });
2683
+
2684
+ await new Promise(() => {});
2685
+ }
2686
+
2687
+ async function handleOpenclawDebugCommand(action, flags) {
2688
+ const normalizedAction = String(action || '').trim().toLowerCase();
2689
+ if (normalizedAction === 'local-gateway-agent') {
2690
+ await runLocalGatewayAgentDebug(flags);
2691
+ return;
2692
+ }
2693
+
2694
+ if (normalizedAction === 'persona-runtime') {
2695
+ const result = await runPersonaRuntimeDebugCheck({
2696
+ slug: flags.slug,
2697
+ name: flags.name,
2698
+ description: flags.description,
2699
+ workspaceRoot: flags['workspace-root'] || flags['openclaw-home'],
2700
+ forceInstall: isTruthyFlag(flags['force-install']),
2701
+ restart: isTruthyFlag(flags.restart),
2702
+ leaveRunning: isTruthyFlag(flags['leave-running']),
2703
+ cleanup: !isTruthyFlag(flags['no-cleanup']),
2704
+ });
2705
+ printPersonaRuntimeDebugResult(result, isTruthyFlag(flags.json));
2706
+ if (!result.statusAfterLaunch.processRunning) {
2707
+ throw new Error('Persona runtime smoke check failed to keep the process running after launch.');
2708
+ }
2709
+ if (result.stop && result.statusAfterStop?.processRunning) {
2710
+ throw new Error('Persona runtime smoke check failed to stop the process cleanly.');
2711
+ }
2712
+ return;
2713
+ }
2714
+
2715
+ const frameFile =
2716
+ typeof flags['frame-file'] === 'string' && flags['frame-file'].trim()
2717
+ ? path.resolve(flags['frame-file'])
2718
+ : '';
2719
+ const frameText =
2720
+ frameFile
2721
+ ? readFile(frameFile)
2722
+ : (typeof flags['frame-json'] === 'string' && flags['frame-json'].trim() ? flags['frame-json'] : '');
2723
+ const text = typeof flags.text === 'string' ? flags.text : '';
2724
+
2725
+ if (!frameText && !text.trim()) {
2726
+ throw new Error(
2727
+ 'Assistant text or frame input is required. Usage: oomi openclaw debug assistant-final --text "<assistant text>"'
2728
+ );
2729
+ }
2730
+
2731
+ const debugOptions = {
2732
+ sessionId: flags['session-id'],
2733
+ sessionKey: flags['session-key'],
2734
+ role: flags.role,
2735
+ omitRole: isTruthyFlag(flags['omit-role']),
2736
+ text,
2737
+ frameText,
2738
+ root: flags.root,
2739
+ userText: flags['user-text'],
2740
+ liveProvider: isTruthyFlag(flags['live-provider']),
2741
+ envFile: flags['env-file'],
2742
+ providerTimeoutMs: flags['provider-timeout-ms'],
2743
+ };
2744
+
2745
+ if (normalizedAction === 'assistant-final') {
2746
+ const result = runAssistantFinalDebugCheck(debugOptions);
2747
+ printAssistantFinalDebugResult(result, isTruthyFlag(flags.json));
2748
+ return;
2749
+ }
2750
+
2751
+ if (normalizedAction === 'tts-pipeline') {
2752
+ const result = await runLocalTtsPipelineDebugCheck(debugOptions);
2753
+ printTtsPipelineDebugResult(result, isTruthyFlag(flags.json));
2754
+ if (!result.backend?.success) {
2755
+ throw new Error(result.backend?.qwen?.errorCode || 'Local backend TTS replay failed.');
2756
+ }
2757
+ return;
2758
+ }
2759
+
2760
+ throw new Error('Unknown debug action: ' + normalizedAction + '. Use: oomi openclaw debug assistant-final|tts-pipeline|local-gateway-agent|persona-runtime');
2761
+ }
2762
+
2763
+ function buildOpenclawProfileFromFlags(flags) {
2764
+ const defaultProfilePath = resolveOpenclawProfilePath();
2765
+ const defaultOpenclawHome = path.dirname(defaultProfilePath);
2766
+ const defaultWorkspaceRoot = String(
2767
+ flags['workspace-root'] ||
2768
+ flags.workspace ||
2769
+ process.env.OPENCLAW_WORKSPACE ||
2770
+ resolveOpenclawWorkspaceRoot()
2771
+ ).trim();
2772
+ const defaultGatewayPort = parsePositiveInteger(
2773
+ flags['gateway-port'] || process.env.OPENCLAW_GATEWAY_PORT,
2774
+ 18789
2775
+ );
2776
+ const pluginTrustMode =
2777
+ String(flags['plugin-trust-mode'] || '').trim() === 'plugins.allow' ||
2778
+ isTruthyFlag(flags['strict-plugin-allow'])
2779
+ ? 'plugins.allow'
2780
+ : 'auto-discovery';
2781
+
2782
+ return buildOomiDevLocalProfile({
2783
+ profileId: flags['profile-id'] || flags.id || 'oomi-dev-local',
2784
+ label: flags.label || 'Oomi Local Dev',
2785
+ workspaceRoot: defaultWorkspaceRoot,
2786
+ deviceId: flags['device-id'] || process.env.OOMI_DEV_DEVICE_ID || '',
2787
+ gatewayPort: defaultGatewayPort,
2788
+ gatewayToken:
2789
+ flags['gateway-token'] ||
2790
+ process.env.OPENCLAW_GATEWAY_TOKEN ||
2791
+ process.env.OOMI_DEV_GATEWAY_TOKEN ||
2792
+ 'dev-gateway-token',
2793
+ backendUrl:
2794
+ flags['backend-url'] ||
2795
+ process.env.OOMI_DEV_BACKEND_URL ||
2796
+ process.env.OOMI_BACKEND_URL ||
2797
+ '',
2798
+ deviceToken:
2799
+ flags['device-token'] ||
2800
+ process.env.OOMI_DEV_DEVICE_TOKEN ||
2801
+ process.env.OOMI_DEVICE_TOKEN ||
2802
+ '',
2803
+ defaultSessionKey:
2804
+ flags['session-key'] ||
2805
+ flags['default-session-key'] ||
2806
+ process.env.OOMI_DEV_DEFAULT_SESSION_KEY ||
2807
+ process.env.OOMI_DEV_SESSION_KEY ||
2808
+ 'agent:main:webchat:channel:oomi',
2809
+ enableOomiChannel:
2810
+ isTruthyFlag(flags['enable-channel']) ||
2811
+ (!isTruthyFlag(flags['disable-channel']) &&
2812
+ Boolean(
2813
+ String(
2814
+ flags['device-token'] ||
2815
+ process.env.OOMI_DEV_DEVICE_TOKEN ||
2816
+ process.env.OOMI_DEVICE_TOKEN ||
2817
+ ''
2818
+ ).trim()
2819
+ )),
2820
+ requestTimeoutMs: parsePositiveInteger(flags['request-timeout-ms'], 15000),
2821
+ pluginTrustMode,
2822
+ modelPreset: flags['model-preset'] || 'openrouter-free',
2823
+ modelAuthMode: flags['model-auth-mode'] || 'oomi-managed',
2824
+ openclawHome: flags['openclaw-home'] || defaultOpenclawHome,
2086
2825
  });
2087
2826
  }
2088
-
2089
- async function runLocalTtsPipelineDebugCheck(options = {}) {
2090
- const assistant = runAssistantFinalDebugCheck(options);
2091
- const repoRoot = resolveRepoRoot(options.root);
2092
- const backendRoot = resolveBackendRoot(options.root);
2093
- const scriptPath = path.join(backendRoot, 'bin', 'voice_tts_replay.rb');
2094
- if (!fs.existsSync(scriptPath)) {
2095
- throw new Error(`Backend replay script not found: ${scriptPath}`);
2096
- }
2097
-
2098
- const inputPayload = {
2099
- repoRoot,
2100
- sessionId: assistant.sessionId,
2101
- sessionKey: assistant.sessionKey,
2102
- frameText: assistant.frameText,
2103
- userText:
2104
- typeof options.userText === 'string' && options.userText.trim()
2105
- ? options.userText.trim()
2106
- : 'local debug utterance',
2107
- liveProvider: Boolean(options.liveProvider),
2108
- providerTimeoutMs: parsePositiveInteger(options.providerTimeoutMs, 15000),
2109
- };
2110
- let childEnv = undefined;
2111
- let resolvedEnvFile = '';
2112
- if (options.liveProvider) {
2113
- resolvedEnvFile =
2114
- typeof options.envFile === 'string' && options.envFile.trim()
2115
- ? path.resolve(options.envFile.trim())
2116
- : path.join(repoRoot, '.env.local');
2117
- childEnv = loadEnvFile(resolvedEnvFile, DEBUG_PROVIDER_ENV_KEYS);
2118
- }
2119
- const inputFile = path.join(os.tmpdir(), `oomi-voice-replay-${randomUUID()}.json`);
2120
- writeFile(inputFile, JSON.stringify(inputPayload, null, 2) + '\n');
2121
-
2122
- try {
2123
- const backend = await runBundledRubyScript({ backendRoot, scriptPath, inputFile, env: childEnv });
2124
- const parsed = backend.stdout.trim() ? JSON.parse(backend.stdout) : null;
2125
- return {
2126
- assistant,
2127
- backend: parsed,
2128
- backendExitCode: backend.code,
2129
- backendStderr: backend.stderr.trim(),
2130
- liveProvider: Boolean(options.liveProvider),
2131
- envFile: resolvedEnvFile || null,
2132
- };
2133
- } finally {
2134
- try {
2135
- fs.unlinkSync(inputFile);
2136
- } catch {
2137
- // no-op
2138
- }
2139
- }
2140
- }
2141
-
2142
- function printTtsPipelineDebugResult(result, asJson) {
2143
- if (asJson) {
2144
- console.log(JSON.stringify(result, null, 2));
2145
- return;
2146
- }
2147
-
2148
- console.log(`Assistant normalization: ${result.assistant.changed ? 'changed' : 'unchanged'}${result.assistant.reason ? ` (${result.assistant.reason})` : ''}`);
2149
- console.log(`Assistant spoken segments: ${Array.isArray(result.assistant.spoken?.segments) ? result.assistant.spoken.segments.length : 0}`);
2150
- if (!result.backend) {
2151
- console.log('Backend replay: <no output>');
2152
- return;
2153
- }
2154
- console.log(`Backend replay success: ${result.backend.success ? 'yes' : 'no'}`);
2155
- console.log(`Managed speech sidecar: ${result.backend.managed?.assistantSpeechFinal?.present ? 'yes' : 'no'}`);
2156
- console.log(`Backend final text: ${result.backend.qwen?.assistantTextFinal || '<missing>'}`);
2157
- console.log(`Backend TTS appends: ${Array.isArray(result.backend.qwen?.ttsAppends) ? result.backend.qwen.ttsAppends.length : 0}`);
2158
- console.log(`Backend TTS commits: ${Number(result.backend.qwen?.commitCount || 0)}`);
2159
- if (result.liveProvider) {
2160
- console.log(`Live provider audio deltas: ${Number(result.backend.qwen?.audioDeltaCount || 0)}`);
2161
- console.log(`Live provider audio bytes (base64): ${Number(result.backend.qwen?.audioDeltaBytes || 0)}`);
2162
- console.log(`Live provider timeout: ${result.backend.qwen?.providerTimedOut ? 'yes' : 'no'}`);
2163
- }
2164
- if (result.backend.qwen?.errorCode) {
2165
- console.log(`Backend error: ${result.backend.qwen.errorCode}`);
2166
- }
2167
- if (result.backendStderr) {
2168
- console.log(`Backend stderr: ${result.backendStderr}`);
2169
- }
2170
- }
2171
-
2172
- async function handleOpenclawDebugCommand(action, flags) {
2173
- const normalizedAction = String(action || '').trim().toLowerCase();
2174
- const frameFile =
2175
- typeof flags['frame-file'] === 'string' && flags['frame-file'].trim()
2176
- ? path.resolve(flags['frame-file'])
2177
- : '';
2178
- const frameText =
2179
- frameFile
2180
- ? readFile(frameFile)
2181
- : (typeof flags['frame-json'] === 'string' && flags['frame-json'].trim() ? flags['frame-json'] : '');
2182
- const text = typeof flags.text === 'string' ? flags.text : '';
2183
-
2184
- if (!frameText && !text.trim()) {
2185
- throw new Error(
2186
- 'Assistant text or frame input is required. Usage: oomi openclaw debug assistant-final --text "<assistant text>"'
2187
- );
2188
- }
2189
-
2190
- const debugOptions = {
2191
- sessionId: flags['session-id'],
2192
- sessionKey: flags['session-key'],
2193
- role: flags.role,
2194
- omitRole: isTruthyFlag(flags['omit-role']),
2195
- text,
2196
- frameText,
2197
- root: flags.root,
2198
- userText: flags['user-text'],
2199
- liveProvider: isTruthyFlag(flags['live-provider']),
2200
- envFile: flags['env-file'],
2201
- providerTimeoutMs: flags['provider-timeout-ms'],
2202
- };
2203
-
2204
- if (normalizedAction === 'assistant-final') {
2205
- const result = runAssistantFinalDebugCheck(debugOptions);
2206
- printAssistantFinalDebugResult(result, isTruthyFlag(flags.json));
2207
- return;
2208
- }
2209
-
2210
- if (normalizedAction === 'tts-pipeline') {
2211
- const result = await runLocalTtsPipelineDebugCheck(debugOptions);
2212
- printTtsPipelineDebugResult(result, isTruthyFlag(flags.json));
2213
- if (!result.backend?.success) {
2214
- throw new Error(result.backend?.qwen?.errorCode || 'Local backend TTS replay failed.');
2215
- }
2216
- return;
2217
- }
2218
-
2219
- throw new Error('Unknown debug action: ' + normalizedAction + '. Use: oomi openclaw debug assistant-final|tts-pipeline');
2220
- }
2827
+
2828
+ function printOpenclawProfileResult(payload, asJson) {
2829
+ if (asJson) {
2830
+ console.log(JSON.stringify(payload, null, 2));
2831
+ return;
2832
+ }
2833
+
2834
+ if (payload.profilePath) {
2835
+ console.log(`Profile written: ${payload.profilePath}`);
2836
+ }
2837
+ if (payload.configPath) {
2838
+ console.log(`Config updated: ${payload.configPath}`);
2839
+ }
2840
+ if (payload.identityPath) {
2841
+ console.log(`Identity path: ${payload.identityPath}`);
2842
+ }
2843
+ if (payload.profile?.profileId) {
2844
+ console.log(`Profile id: ${payload.profile.profileId}`);
2845
+ }
2846
+ if (payload.profile?.preset) {
2847
+ console.log(`Preset: ${payload.profile.preset}`);
2848
+ }
2849
+ if (payload.pluginTrustMode) {
2850
+ console.log(`Plugin trust mode: ${payload.pluginTrustMode}`);
2851
+ }
2852
+ if (typeof payload.oomiChannelEnabled === 'boolean') {
2853
+ console.log(`Oomi channel enabled: ${payload.oomiChannelEnabled ? 'yes' : 'no'}`);
2854
+ }
2855
+ if (typeof payload.identityCreated === 'boolean') {
2856
+ console.log(`Device identity created: ${payload.identityCreated ? 'yes' : 'no'}`);
2857
+ }
2858
+ }
2859
+
2860
+ async function handleOpenclawProfileCommand(action, flags) {
2861
+ const normalizedAction = String(action || '').trim().toLowerCase();
2862
+ if (!normalizedAction || normalizedAction === 'help') {
2863
+ throw new Error('OpenClaw profile action is required. Use: oomi openclaw profile init|apply');
2864
+ }
2865
+
2866
+ const profilePath = path.resolve(
2867
+ String(flags.profile || flags['profile-file'] || resolveOpenclawProfilePath()).trim()
2868
+ );
2869
+
2870
+ if (normalizedAction === 'init') {
2871
+ const profile = buildOpenclawProfileFromFlags(flags);
2872
+ writeOpenclawProfile(profilePath, profile);
2873
+ printOpenclawProfileResult(
2874
+ {
2875
+ ok: true,
2876
+ profilePath,
2877
+ profile,
2878
+ },
2879
+ isTruthyFlag(flags.json)
2880
+ );
2881
+ return;
2882
+ }
2883
+
2884
+ if (normalizedAction === 'apply') {
2885
+ const profile = readOpenclawProfile(profilePath);
2886
+ if (!profile) {
2887
+ throw new Error(`OpenClaw profile not found or unreadable: ${profilePath}`);
2888
+ }
2889
+ const openclawHome = path.resolve(String(flags['openclaw-home'] || resolveOpenclawHome()).trim());
2890
+ const defaultIdentityPath = path.join(openclawHome, 'identity', 'device.json');
2891
+ const applyResult = applyOpenclawProfile({
2892
+ profile,
2893
+ openclawHome,
2894
+ configPath:
2895
+ typeof flags['config-path'] === 'string' && flags['config-path'].trim()
2896
+ ? path.resolve(flags['config-path'])
2897
+ : '',
2898
+ identityPath:
2899
+ typeof flags['identity-path'] === 'string' && flags['identity-path'].trim()
2900
+ ? path.resolve(flags['identity-path'])
2901
+ : defaultIdentityPath,
2902
+ ensureIdentity: !isTruthyFlag(flags['skip-identity']),
2903
+ });
2904
+ printOpenclawProfileResult(
2905
+ {
2906
+ ...applyResult,
2907
+ profilePath,
2908
+ profile,
2909
+ },
2910
+ isTruthyFlag(flags.json)
2911
+ );
2912
+ return;
2913
+ }
2914
+
2915
+ throw new Error(`Unknown profile action: ${normalizedAction}. Use: oomi openclaw profile init|apply`);
2916
+ }
2221
2917
 
2222
2918
  function extractCorrelationId(params) {
2223
2919
  if (!params || typeof params !== 'object') return '';
@@ -2396,11 +3092,11 @@ async function runBridgePreflight({ brokerWs, gatewayUrl, gatewayConfigPath }) {
2396
3092
  await assertTcpReachable(parsedGatewayUrl.toString());
2397
3093
  }
2398
3094
 
2399
- function buildBridgeDetachArgs(rawFlags = {}) {
2400
- const orderedKeys = [
2401
- 'broker-http',
2402
- 'broker-ws',
2403
- 'pair-code',
3095
+ function buildBridgeDetachArgs(rawFlags = {}) {
3096
+ const orderedKeys = [
3097
+ 'broker-http',
3098
+ 'broker-ws',
3099
+ 'pair-code',
2404
3100
  'app-url',
2405
3101
  'device-id',
2406
3102
  'device-token',
@@ -2418,13 +3114,13 @@ function buildBridgeDetachArgs(rawFlags = {}) {
2418
3114
  if (!text) continue;
2419
3115
  args.push(`--${key}`, text);
2420
3116
  }
2421
-
2422
- return args;
2423
- }
2424
-
2425
- function isServiceManagedBridgeStart(flags = {}) {
2426
- return isTruthyFlag(flags['service-managed']);
2427
- }
3117
+
3118
+ return args;
3119
+ }
3120
+
3121
+ function isServiceManagedBridgeStart(flags = {}) {
3122
+ return isTruthyFlag(flags['service-managed']);
3123
+ }
2428
3124
 
2429
3125
  function startBridgeDetachedProcess(rawFlags = {}) {
2430
3126
  const existing = findRunningBridgeProcess();
@@ -2595,17 +3291,31 @@ function runLaunchctl(args, { allowFailure = false } = {}) {
2595
3291
  return { status, stdout, stderr };
2596
3292
  }
2597
3293
 
2598
- function buildBridgeLaunchAgentPlist() {
2599
- const scriptPath = (() => {
2600
- try {
2601
- return fs.realpathSync(process.argv[1]);
2602
- } catch {
2603
- return process.argv[1];
2604
- }
2605
- })();
2606
- const programArgs = [process.execPath, scriptPath, 'openclaw', 'bridge', 'start', '--service-managed'];
2607
- const bridgeLogPath = resolveBridgeLiveLogPath();
2608
- const argsXml = programArgs.map((arg) => `<string>${xmlEscape(arg)}</string>`).join('\n ');
3294
+ function buildBridgeLaunchAgentPlist() {
3295
+ const scriptPath = (() => {
3296
+ try {
3297
+ return fs.realpathSync(process.argv[1]);
3298
+ } catch {
3299
+ return process.argv[1];
3300
+ }
3301
+ })();
3302
+ const programArgs = [process.execPath, scriptPath, 'openclaw', 'bridge', 'start', '--service-managed'];
3303
+ const bridgeLogPath = resolveBridgeLiveLogPath();
3304
+ const argsXml = programArgs.map((arg) => `<string>${xmlEscape(arg)}</string>`).join('\n ');
3305
+ const openclawHome = resolveOpenclawHome();
3306
+ const openclawWorkspace = resolveOpenclawWorkspaceRoot();
3307
+ const envVars = {
3308
+ OOMI_SKIP_UPDATE_CHECK: '1',
3309
+ };
3310
+ if (process.env.OPENCLAW_HOME) {
3311
+ envVars.OPENCLAW_HOME = openclawHome;
3312
+ }
3313
+ if (process.env.OPENCLAW_WORKSPACE) {
3314
+ envVars.OPENCLAW_WORKSPACE = openclawWorkspace;
3315
+ }
3316
+ const envXml = Object.entries(envVars)
3317
+ .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
3318
+ .join('\n');
2609
3319
 
2610
3320
  return `<?xml version="1.0" encoding="UTF-8"?>
2611
3321
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -2618,7 +3328,7 @@ function buildBridgeLaunchAgentPlist() {
2618
3328
  ${argsXml}
2619
3329
  </array>
2620
3330
  <key>WorkingDirectory</key>
2621
- <string>${xmlEscape(os.homedir())}</string>
3331
+ <string>${xmlEscape(openclawHome)}</string>
2622
3332
  <key>RunAtLoad</key>
2623
3333
  <true/>
2624
3334
  <key>KeepAlive</key>
@@ -2627,8 +3337,7 @@ function buildBridgeLaunchAgentPlist() {
2627
3337
  <integer>5</integer>
2628
3338
  <key>EnvironmentVariables</key>
2629
3339
  <dict>
2630
- <key>OOMI_SKIP_UPDATE_CHECK</key>
2631
- <string>1</string>
3340
+ ${envXml}
2632
3341
  </dict>
2633
3342
  <key>StandardOutPath</key>
2634
3343
  <string>${xmlEscape(bridgeLogPath)}</string>
@@ -2661,18 +3370,18 @@ function readBridgeLaunchdStatus() {
2661
3370
  };
2662
3371
  }
2663
3372
 
2664
- function startBridgeLaunchdService() {
2665
- assertMacOSLaunchdAvailable();
2666
- const plistPath = resolveBridgeLaunchAgentPlistPath();
2667
- if (!fs.existsSync(plistPath)) {
2668
- throw new Error('Bridge service is not installed. Run: oomi openclaw bridge service install');
2669
- }
2670
- writeFile(plistPath, buildBridgeLaunchAgentPlist());
2671
- const domain = launchctlDomain();
2672
- const target = launchctlServiceTarget();
2673
- runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
2674
- runLaunchctl(['bootstrap', domain, plistPath]);
2675
- runLaunchctl(['enable', target], { allowFailure: true });
3373
+ function startBridgeLaunchdService() {
3374
+ assertMacOSLaunchdAvailable();
3375
+ const plistPath = resolveBridgeLaunchAgentPlistPath();
3376
+ if (!fs.existsSync(plistPath)) {
3377
+ throw new Error('Bridge service is not installed. Run: oomi openclaw bridge service install');
3378
+ }
3379
+ writeFile(plistPath, buildBridgeLaunchAgentPlist());
3380
+ const domain = launchctlDomain();
3381
+ const target = launchctlServiceTarget();
3382
+ runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
3383
+ runLaunchctl(['bootstrap', domain, plistPath]);
3384
+ runLaunchctl(['enable', target], { allowFailure: true });
2676
3385
  runLaunchctl(['kickstart', '-k', target], { allowFailure: true });
2677
3386
  }
2678
3387
 
@@ -2831,22 +3540,22 @@ async function startOpenclawBridge(flags) {
2831
3540
  console.log(`Broker WS: ${brokerWs}`);
2832
3541
 
2833
3542
  const activeGatewaySockets = new Map();
2834
- const reconnectState = {
2835
- attempt: 0,
2836
- timer: null,
2837
- stopped: false,
2838
- lastFailure: null,
2839
- };
2840
- const personaJobPollEnabled = !isTruthyFlag(process.env.OOMI_DISABLE_PERSONA_JOB_POLL);
2841
- const personaJobPollIntervalMs = parsePositiveInteger(
2842
- process.env.OOMI_PERSONA_JOB_POLL_INTERVAL_MS,
2843
- 3000,
2844
- );
2845
- const personaJobIdlePollIntervalMs = parsePositiveInteger(
2846
- process.env.OOMI_PERSONA_JOB_IDLE_POLL_INTERVAL_MS,
2847
- 3000,
2848
- );
2849
- const personaWorkspaceRoot = defaultPersonaWorkspaceRoot();
3543
+ const reconnectState = {
3544
+ attempt: 0,
3545
+ timer: null,
3546
+ stopped: false,
3547
+ lastFailure: null,
3548
+ };
3549
+ const personaJobPollEnabled = !isTruthyFlag(process.env.OOMI_DISABLE_PERSONA_JOB_POLL);
3550
+ const personaJobPollIntervalMs = parsePositiveInteger(
3551
+ process.env.OOMI_PERSONA_JOB_POLL_INTERVAL_MS,
3552
+ 3000,
3553
+ );
3554
+ const personaJobIdlePollIntervalMs = parsePositiveInteger(
3555
+ process.env.OOMI_PERSONA_JOB_IDLE_POLL_INTERVAL_MS,
3556
+ 3000,
3557
+ );
3558
+ const personaWorkspaceRoot = defaultPersonaWorkspaceRoot();
2850
3559
  const brokerPath = (() => {
2851
3560
  try {
2852
3561
  return new URL(brokerWs).pathname || '';
@@ -2911,33 +3620,38 @@ async function startOpenclawBridge(flags) {
2911
3620
  onReport: ({ phase, status, error }) => {
2912
3621
  reportBridgeProcessFault({ phase, status, error });
2913
3622
  },
2914
- onExit: (code) => {
2915
- reconnectState.stopped = true;
2916
- if (reconnectState.timer) {
2917
- clearTimeout(reconnectState.timer);
2918
- reconnectState.timer = null;
2919
- }
2920
- personaJobPoller?.stop();
2921
- releaseBridgeLock();
2922
- process.exit(code);
2923
- },
2924
- });
3623
+ onExit: (code) => {
3624
+ reconnectState.stopped = true;
3625
+ if (reconnectState.timer) {
3626
+ clearTimeout(reconnectState.timer);
3627
+ reconnectState.timer = null;
3628
+ }
3629
+ personaJobPoller?.stop();
3630
+ releaseBridgeLock();
3631
+ process.exit(code);
3632
+ },
3633
+ });
2925
3634
 
2926
3635
  const uncaughtExceptionHandler = (error) => {
2927
3636
  handleBridgeProcessFault({ phase: 'process.uncaughtException', error });
2928
3637
  };
2929
3638
 
2930
- const unhandledRejectionHandler = (reason) => {
2931
- handleBridgeProcessFault({ phase: 'process.unhandledRejection', error: reason });
2932
- };
2933
-
2934
- process.on('uncaughtException', uncaughtExceptionHandler);
2935
- process.on('unhandledRejection', unhandledRejectionHandler);
3639
+ const unhandledRejectionHandler = (reason) => {
3640
+ handleBridgeProcessFault({ phase: 'process.unhandledRejection', error: reason });
3641
+ };
3642
+
3643
+ process.on('uncaughtException', uncaughtExceptionHandler);
3644
+ process.on('unhandledRejection', unhandledRejectionHandler);
3645
+
3646
+ const personaBackendUrl =
3647
+ deviceToken
3648
+ ? resolvePersonaBackendUrl({ 'backend-url': process.env.OOMI_DEV_BACKEND_URL || process.env.OOMI_BACKEND_URL || brokerHttp })
3649
+ : '';
2936
3650
 
2937
3651
  const personaJobPoller =
2938
- personaJobPollEnabled && brokerHttp && deviceToken
3652
+ personaJobPollEnabled && personaBackendUrl && deviceToken
2939
3653
  ? startPersonaJobPoller({
2940
- backendUrl: brokerHttp,
3654
+ backendUrl: personaBackendUrl,
2941
3655
  deviceToken,
2942
3656
  pollIntervalMs: personaJobPollIntervalMs,
2943
3657
  idleIntervalMs: personaJobIdlePollIntervalMs,
@@ -2945,38 +3659,49 @@ async function startOpenclawBridge(flags) {
2945
3659
  onMessage: async (message) => {
2946
3660
  const result = await runManagedPersonaJobExecution({
2947
3661
  message,
2948
- backendUrl: brokerHttp,
3662
+ backendUrl: personaBackendUrl,
2949
3663
  deviceToken,
2950
3664
  deviceId,
2951
3665
  workspaceRoot: personaWorkspaceRoot,
2952
3666
  shouldInstall: true,
2953
3667
  shouldStart: true,
2954
- shouldRegister: true,
2955
- });
2956
-
2957
- if (result && result.ok) {
2958
- console.log(
2959
- `[persona-jobs] completed ${result.jobId} on port ${result.result?.localPort || 'unknown'}`
2960
- );
2961
- return;
2962
- }
2963
-
2964
- if (result) {
2965
- console.warn(
2966
- `[persona-jobs] job ${result.jobId} completed with failure: ${result.error?.message || 'unknown error'}`
2967
- );
2968
- }
2969
- },
3668
+ shouldRegister: true,
3669
+ });
3670
+
3671
+ if (result && result.ok) {
3672
+ console.log(
3673
+ `[persona-jobs] completed ${result.jobId} on port ${result.result?.localPort || 'unknown'}`
3674
+ );
3675
+ return;
3676
+ }
3677
+
3678
+ if (result) {
3679
+ console.warn(
3680
+ `[persona-jobs] job ${result.jobId} completed with failure: ${result.error?.message || 'unknown error'}`
3681
+ );
3682
+ }
3683
+ },
3684
+ })
3685
+ : null;
3686
+ const personaRuntimeSupervisor =
3687
+ personaBackendUrl && deviceToken
3688
+ ? startPersonaRuntimeSupervisor({
3689
+ backendUrl: personaBackendUrl,
3690
+ deviceToken,
3691
+ workspaceRoot: personaWorkspaceRoot,
3692
+ intervalMs: 30000,
3693
+ logger: console,
3694
+ autoRestart: true,
2970
3695
  })
2971
3696
  : null;
2972
3697
 
2973
- if (personaJobPollEnabled && brokerHttp && deviceToken) {
3698
+ if (personaJobPollEnabled && personaBackendUrl && deviceToken) {
2974
3699
  bridgeDebugLog('[persona-jobs] polling filtered control queue for persona_job messages.');
2975
3700
  } else if (personaJobPollEnabled) {
2976
3701
  console.warn('[persona-jobs] disabled because broker HTTP URL or device token is unavailable.');
2977
3702
  }
2978
-
2979
- const sendBrokerPayload = (brokerSocket, payload) => {
3703
+
3704
+ const sendBrokerPayload = (brokerSocket, payload) => {
2980
3705
  if (brokerSocket.readyState !== WebSocket.OPEN) return;
2981
3706
  if (!actionCableMode) {
2982
3707
  brokerSocket.send(JSON.stringify(payload));
@@ -3206,9 +3931,9 @@ async function startOpenclawBridge(flags) {
3206
3931
  classifyBridgeFailure({ reason: 'connection closed without classified error' });
3207
3932
  const delayMs = computeReconnectDelayMs(reconnectState.attempt, failure.baseDelayMs);
3208
3933
 
3209
- bridgeDebugWarn(
3210
- `[bridge] reconnect scheduled in ${delayMs}ms (attempt ${reconnectState.attempt}, class=${failure.failureClass}, code=${failure.errorCode})`
3211
- );
3934
+ bridgeDebugWarn(
3935
+ `[bridge] reconnect scheduled in ${delayMs}ms (attempt ${reconnectState.attempt}, class=${failure.failureClass}, code=${failure.errorCode})`
3936
+ );
3212
3937
 
3213
3938
  updateBridgeStatus({
3214
3939
  status: 'reconnecting',
@@ -3334,7 +4059,7 @@ async function startOpenclawBridge(flags) {
3334
4059
  }
3335
4060
  const result = forwardFrameToSession(sessionBridge, prepared.frameText);
3336
4061
  if (result === 'queued') {
3337
- bridgeDebugLog(`[bridge] client.frame queued after challenge ${sessionId}`);
4062
+ bridgeDebugLog(`[bridge] client.frame queued after challenge ${sessionId}`);
3338
4063
  if (requestMeta) {
3339
4064
  startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
3340
4065
  }
@@ -3348,7 +4073,7 @@ async function startOpenclawBridge(flags) {
3348
4073
  });
3349
4074
  }
3350
4075
  } else if (result === 'dropped') {
3351
- bridgeDebugLog(`[bridge] client.frame dropped after challenge ${sessionId}`);
4076
+ bridgeDebugLog(`[bridge] client.frame dropped after challenge ${sessionId}`);
3352
4077
  incrementBridgeMetric('bridge_drop_count');
3353
4078
  if (requestMeta) {
3354
4079
  const pending = sessionBridge.pendingRequests instanceof Map
@@ -3380,7 +4105,7 @@ async function startOpenclawBridge(flags) {
3380
4105
  });
3381
4106
  }
3382
4107
  } else {
3383
- bridgeDebugLog(`[bridge] client.frame sent after challenge ${sessionId}`);
4108
+ bridgeDebugLog(`[bridge] client.frame sent after challenge ${sessionId}`);
3384
4109
  if (requestMeta) {
3385
4110
  startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
3386
4111
  }
@@ -3446,27 +4171,27 @@ async function startOpenclawBridge(flags) {
3446
4171
  clearTimeout(connectTimeout);
3447
4172
  connectTimeout = null;
3448
4173
  }
3449
- bridgeDebugLog(`[bridge] gateway.open ${sessionId}`);
4174
+ bridgeDebugLog(`[bridge] gateway.open ${sessionId}`);
3450
4175
  flushSessionQueue(sessionBridge);
3451
4176
  });
3452
4177
 
3453
- gatewaySocket.on('message', runBridgeCallbackSafely((gatewayRaw) => {
3454
- let frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
3455
- const spokenNormalized = normalizeAssistantGatewayFrame(sessionId, frame);
3456
- if (spokenNormalized.changed) {
3457
- frame = spokenNormalized.frameText;
3458
- if (spokenNormalized.scope === 'voice') {
3459
- bridgeDebugLog(`[bridge] voice.spoken_metadata.${spokenNormalized.reason} ${sessionId} ${JSON.stringify({
3460
- before: spokenNormalized.summary,
3461
- after: summarizeVoiceFrameContract(frame),
3462
- })}`);
3463
- }
3464
- } else if (spokenNormalized.scope === 'voice' && spokenNormalized.summary.event === 'chat' && spokenNormalized.summary.state === 'final') {
3465
- bridgeDebugLog(`[bridge] voice.chat.final ${sessionId} ${JSON.stringify(spokenNormalized.summary)}`);
3466
- }
3467
- const gatewayPayload = parseJsonPayload(frame);
3468
- if (gatewayPayload?.event === 'connect.challenge') {
3469
- bridgeDebugLog(`[bridge] gateway.connect.challenge ${sessionId}`);
4178
+ gatewaySocket.on('message', runBridgeCallbackSafely((gatewayRaw) => {
4179
+ let frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
4180
+ const spokenNormalized = normalizeAssistantGatewayFrame(sessionId, frame);
4181
+ if (spokenNormalized.changed) {
4182
+ frame = spokenNormalized.frameText;
4183
+ if (spokenNormalized.scope === 'voice') {
4184
+ bridgeDebugLog(`[bridge] voice.spoken_metadata.${spokenNormalized.reason} ${sessionId} ${JSON.stringify({
4185
+ before: spokenNormalized.summary,
4186
+ after: summarizeVoiceFrameContract(frame),
4187
+ })}`);
4188
+ }
4189
+ } else if (spokenNormalized.scope === 'voice' && spokenNormalized.summary.event === 'chat' && spokenNormalized.summary.state === 'final') {
4190
+ bridgeDebugLog(`[bridge] voice.chat.final ${sessionId} ${JSON.stringify(spokenNormalized.summary)}`);
4191
+ }
4192
+ const gatewayPayload = parseJsonPayload(frame);
4193
+ if (gatewayPayload?.event === 'connect.challenge') {
4194
+ bridgeDebugLog(`[bridge] gateway.connect.challenge ${sessionId}`);
3470
4195
  const nonce =
3471
4196
  gatewayPayload.payload && typeof gatewayPayload.payload.nonce === 'string'
3472
4197
  ? gatewayPayload.payload.nonce.trim()
@@ -3612,9 +4337,9 @@ async function startOpenclawBridge(flags) {
3612
4337
  clearChallengeTimer(sessionBridge);
3613
4338
  const reasonText = reason ? reason.toString() : '';
3614
4339
  const closeMeta = classifyGatewayClose(code, reasonText);
3615
- bridgeDebugLog(
3616
- `[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
3617
- );
4340
+ bridgeDebugLog(
4341
+ `[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
4342
+ );
3618
4343
  if (sessionBridge.pendingRequests instanceof Map) {
3619
4344
  for (const requestMeta of sessionBridge.pendingRequests.values()) {
3620
4345
  if (!requestMeta || typeof requestMeta !== 'object') continue;
@@ -3689,7 +4414,7 @@ async function startOpenclawBridge(flags) {
3689
4414
  };
3690
4415
 
3691
4416
  brokerSocket.on('open', () => {
3692
- bridgeDebugLog('[bridge] Connected to managed broker.');
4417
+ bridgeDebugLog('[bridge] Connected to managed broker.');
3693
4418
  reconnectState.attempt = 0;
3694
4419
  reconnectState.lastFailure = null;
3695
4420
  if (actionCableMode) {
@@ -3806,29 +4531,29 @@ async function startOpenclawBridge(flags) {
3806
4531
  }
3807
4532
 
3808
4533
  if (payload.type === 'device.ready') {
3809
- bridgeDebugLog(`[bridge] Broker ready for device ${payload.deviceId || deviceId}.`);
4534
+ bridgeDebugLog(`[bridge] Broker ready for device ${payload.deviceId || deviceId}.`);
3810
4535
  return;
3811
4536
  }
3812
4537
 
3813
4538
  if (payload.type === 'client.open') {
3814
4539
  const sessionId = String(payload.sessionId || '').trim();
3815
4540
  if (!sessionId) return;
3816
- bridgeDebugLog(`[bridge] client.open ${sessionId}`);
4541
+ bridgeDebugLog(`[bridge] client.open ${sessionId}`);
3817
4542
  getOrCreateGatewaySession(sessionId);
3818
4543
  return;
3819
4544
  }
3820
4545
 
3821
- if (payload.type === 'client.frame') {
3822
- const sessionId = String(payload.sessionId || '').trim();
3823
- const frame = typeof payload.frame === 'string' ? payload.frame : '';
3824
- if (!sessionId || !frame) return;
3825
- if (classifyBridgeSessionScope(sessionId) === 'voice') {
3826
- bridgeDebugLog(`[bridge] client.frame ${sessionId} ${JSON.stringify(summarizeVoiceFrameContract(frame))}`);
3827
- } else {
3828
- bridgeDebugLog(`[bridge] client.frame ${sessionId}`);
3829
- }
3830
- const sessionBridge = getOrCreateGatewaySession(sessionId);
3831
- if (!sessionBridge) return;
4546
+ if (payload.type === 'client.frame') {
4547
+ const sessionId = String(payload.sessionId || '').trim();
4548
+ const frame = typeof payload.frame === 'string' ? payload.frame : '';
4549
+ if (!sessionId || !frame) return;
4550
+ if (classifyBridgeSessionScope(sessionId) === 'voice') {
4551
+ bridgeDebugLog(`[bridge] client.frame ${sessionId} ${JSON.stringify(summarizeVoiceFrameContract(frame))}`);
4552
+ } else {
4553
+ bridgeDebugLog(`[bridge] client.frame ${sessionId}`);
4554
+ }
4555
+ const sessionBridge = getOrCreateGatewaySession(sessionId);
4556
+ if (!sessionBridge) return;
3832
4557
  const requestMeta = extractGatewayRequestMeta(frame);
3833
4558
  if (requestMeta) {
3834
4559
  if (!(sessionBridge.pendingRequests instanceof Map)) {
@@ -3851,7 +4576,7 @@ async function startOpenclawBridge(flags) {
3851
4576
  });
3852
4577
  if (prepared.waitForChallenge) {
3853
4578
  queueConnectUntilChallenge(sessionId, sessionBridge, frame);
3854
- bridgeDebugLog(`[bridge] client.frame waiting for challenge ${sessionId}`);
4579
+ bridgeDebugLog(`[bridge] client.frame waiting for challenge ${sessionId}`);
3855
4580
  if (requestMeta) {
3856
4581
  sendGatewayAck(brokerSocket, {
3857
4582
  sessionId,
@@ -3897,7 +4622,7 @@ async function startOpenclawBridge(flags) {
3897
4622
  requiresConnectAccepted: Boolean(requestMeta && requestMeta.method !== 'connect'),
3898
4623
  });
3899
4624
  if (result === 'waiting_for_connect') {
3900
- bridgeDebugLog(`[bridge] client.frame waiting for connect ${sessionId}`);
4625
+ bridgeDebugLog(`[bridge] client.frame waiting for connect ${sessionId}`);
3901
4626
  if (requestMeta) {
3902
4627
  sendGatewayAck(brokerSocket, {
3903
4628
  sessionId,
@@ -3910,7 +4635,7 @@ async function startOpenclawBridge(flags) {
3910
4635
  return;
3911
4636
  }
3912
4637
  if (result === 'queued') {
3913
- bridgeDebugLog(`[bridge] client.frame queued ${sessionId}`);
4638
+ bridgeDebugLog(`[bridge] client.frame queued ${sessionId}`);
3914
4639
  if (requestMeta) {
3915
4640
  startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
3916
4641
  }
@@ -3924,7 +4649,7 @@ async function startOpenclawBridge(flags) {
3924
4649
  });
3925
4650
  }
3926
4651
  } else if (result === 'dropped') {
3927
- bridgeDebugLog(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
4652
+ bridgeDebugLog(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
3928
4653
  incrementBridgeMetric('bridge_drop_count');
3929
4654
  if (requestMeta) {
3930
4655
  const pending = sessionBridge.pendingRequests instanceof Map
@@ -3970,7 +4695,7 @@ async function startOpenclawBridge(flags) {
3970
4695
 
3971
4696
  if (payload.type === 'client.close') {
3972
4697
  const sessionId = String(payload.sessionId || '').trim();
3973
- bridgeDebugLog(`[bridge] client.close ${sessionId}`);
4698
+ bridgeDebugLog(`[bridge] client.close ${sessionId}`);
3974
4699
  const sessionBridge = activeGatewaySockets.get(sessionId);
3975
4700
  if (sessionBridge && sessionBridge.socket) {
3976
4701
  clearChallengeTimer(sessionBridge);
@@ -3993,7 +4718,7 @@ async function startOpenclawBridge(flags) {
3993
4718
  actionCableHeartbeat = null;
3994
4719
  }
3995
4720
  const reasonText = reason ? reason.toString() : '';
3996
- bridgeDebugLog(`[bridge] Broker disconnected (${code}) ${reasonText}`);
4721
+ bridgeDebugLog(`[bridge] Broker disconnected (${code}) ${reasonText}`);
3997
4722
  incrementBridgeMetric('bridge_disconnect_count');
3998
4723
  for (const [sessionId, sessionBridge] of activeGatewaySockets.entries()) {
3999
4724
  clearChallengeTimer(sessionBridge);
@@ -4035,16 +4760,17 @@ async function startOpenclawBridge(flags) {
4035
4760
  }));
4036
4761
  };
4037
4762
 
4038
- const markStopped = (signal) => {
4039
- reconnectState.stopped = true;
4763
+ const markStopped = (signal) => {
4764
+ reconnectState.stopped = true;
4040
4765
  if (reconnectState.timer) {
4041
4766
  clearTimeout(reconnectState.timer);
4042
4767
  reconnectState.timer = null;
4043
4768
  }
4044
4769
  personaJobPoller?.stop();
4770
+ personaRuntimeSupervisor?.stop();
4045
4771
  process.off('uncaughtException', uncaughtExceptionHandler);
4046
4772
  process.off('unhandledRejection', unhandledRejectionHandler);
4047
- updateBridgeStatus({
4773
+ updateBridgeStatus({
4048
4774
  status: 'stopped',
4049
4775
  deviceId,
4050
4776
  brokerWs,
@@ -4445,17 +5171,17 @@ async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
4445
5171
  );
4446
5172
  }
4447
5173
 
4448
- async function startBridgeLifecycle(flags = {}) {
4449
- const serviceManaged = isServiceManagedBridgeStart(flags);
4450
- if (serviceManaged && Boolean(flags.detach)) {
4451
- throw new Error('Detached bridge mode cannot be combined with --service-managed.');
4452
- }
4453
-
4454
- if (Boolean(flags.detach)) {
4455
- const detachedFlags = { ...flags };
4456
- delete detachedFlags.detach;
4457
- const result = startBridgeDetachedProcess(detachedFlags);
4458
- if (result.alreadyRunning) {
5174
+ async function startBridgeLifecycle(flags = {}) {
5175
+ const serviceManaged = isServiceManagedBridgeStart(flags);
5176
+ if (serviceManaged && Boolean(flags.detach)) {
5177
+ throw new Error('Detached bridge mode cannot be combined with --service-managed.');
5178
+ }
5179
+
5180
+ if (Boolean(flags.detach)) {
5181
+ const detachedFlags = { ...flags };
5182
+ delete detachedFlags.detach;
5183
+ const result = startBridgeDetachedProcess(detachedFlags);
5184
+ if (result.alreadyRunning) {
4459
5185
  incrementBridgeMetric('duplicate_start_attempt_count');
4460
5186
  console.log(`Bridge already running (pid: ${result.pid}).`);
4461
5187
  return;
@@ -4464,30 +5190,30 @@ async function startBridgeLifecycle(flags = {}) {
4464
5190
  console.log(`Bridge started in background (pid: ${result.pid}).`);
4465
5191
  return;
4466
5192
  }
4467
-
4468
- const running = findRunningBridgeProcess();
4469
- if (running) {
4470
- if (!serviceManaged) {
4471
- incrementBridgeMetric('duplicate_start_attempt_count');
4472
- console.log(
4473
- `Bridge already running (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}.`
4474
- );
4475
- return;
4476
- }
4477
-
4478
- incrementBridgeMetric('bridge_restart_count');
4479
- console.log(
4480
- `Service-managed bridge start detected existing bridge (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}; reclaiming ownership.`
4481
- );
4482
- const result = await stopBridgeProcesses();
4483
- if (Array.isArray(result.stillAlive) && result.stillAlive.length > 0) {
4484
- throw new Error(`Failed to stop bridge processes: ${result.stillAlive.join(', ')}`);
4485
- }
4486
- }
4487
-
4488
- incrementBridgeMetric('bridge_start_count');
4489
- await startOpenclawBridge(flags);
4490
- }
5193
+
5194
+ const running = findRunningBridgeProcess();
5195
+ if (running) {
5196
+ if (!serviceManaged) {
5197
+ incrementBridgeMetric('duplicate_start_attempt_count');
5198
+ console.log(
5199
+ `Bridge already running (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}.`
5200
+ );
5201
+ return;
5202
+ }
5203
+
5204
+ incrementBridgeMetric('bridge_restart_count');
5205
+ console.log(
5206
+ `Service-managed bridge start detected existing bridge (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}; reclaiming ownership.`
5207
+ );
5208
+ const result = await stopBridgeProcesses();
5209
+ if (Array.isArray(result.stillAlive) && result.stillAlive.length > 0) {
5210
+ throw new Error(`Failed to stop bridge processes: ${result.stillAlive.join(', ')}`);
5211
+ }
5212
+ }
5213
+
5214
+ incrementBridgeMetric('bridge_start_count');
5215
+ await startOpenclawBridge(flags);
5216
+ }
4491
5217
 
4492
5218
  async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
4493
5219
  const action = String(actionRaw || 'start').trim().toLowerCase();
@@ -4603,40 +5329,50 @@ async function main() {
4603
5329
  return;
4604
5330
  }
4605
5331
 
4606
- if (command === 'openclaw' && subcommand === 'plugin') {
4607
- printOpenclawPluginSetup(args.flags);
4608
- return;
4609
- }
4610
-
4611
- if (command === 'openclaw' && subcommand === 'debug') {
4612
- await handleOpenclawDebugCommand(args.positionals[0], args.flags);
4613
- return;
4614
- }
5332
+ if (command === 'openclaw' && subcommand === 'plugin') {
5333
+ printOpenclawPluginSetup(args.flags);
5334
+ return;
5335
+ }
5336
+
5337
+ if (command === 'openclaw' && subcommand === 'profile') {
5338
+ await handleOpenclawProfileCommand(args.positionals[0], args.flags);
5339
+ return;
5340
+ }
5341
+
5342
+ if (command === 'openclaw' && subcommand === 'debug') {
5343
+ await handleOpenclawDebugCommand(args.positionals[0], args.flags);
5344
+ return;
5345
+ }
4615
5346
 
4616
5347
  if (command === 'personas' && subcommand === 'sync') {
4617
5348
  await syncPersonas({ backendUrl: args.flags['backend-url'], root: args.flags.root });
4618
5349
  return;
4619
5350
  }
4620
5351
 
4621
- if (command === 'personas' && subcommand === 'create') {
5352
+ if (command === 'personas' && subcommand === 'create') {
4622
5353
  const id = args.positionals[0];
4623
5354
  if (!id) {
4624
5355
  throw new Error('Persona id is required. Usage: oomi personas create <id>');
4625
5356
  }
4626
5357
  await createPersona({ id, root: args.flags.root, flags: args.flags });
4627
5358
  return;
4628
- }
4629
-
4630
- if (command === 'personas' && subcommand === 'create-managed') {
4631
- await handlePersonaCreateManagedCommand(args.flags, args.positionals[0]);
4632
- return;
4633
- }
4634
-
4635
- if (command === 'personas' && subcommand === 'scaffold') {
4636
- const slug = args.positionals[0];
4637
- if (!slug) {
4638
- throw new Error('Persona slug is required. Usage: oomi personas scaffold <slug> --name "<name>" --description "<description>" --out <path>');
4639
- }
5359
+ }
5360
+
5361
+ if (command === 'personas' && subcommand === 'create-managed') {
5362
+ await handlePersonaCreateManagedCommand(args.flags, args.positionals[0]);
5363
+ return;
5364
+ }
5365
+
5366
+ if (command === 'personas' && subcommand === 'launch-managed') {
5367
+ await handlePersonaLaunchManagedCommand(args.flags, args.positionals[0]);
5368
+ return;
5369
+ }
5370
+
5371
+ if (command === 'personas' && subcommand === 'scaffold') {
5372
+ const slug = args.positionals[0];
5373
+ if (!slug) {
5374
+ throw new Error('Persona slug is required. Usage: oomi personas scaffold <slug> --name "<name>" --description "<description>" --out <path>');
5375
+ }
4640
5376
  const result = scaffoldPersonaApp({
4641
5377
  slug,
4642
5378
  name: args.flags.name,
@@ -4645,70 +5381,97 @@ async function main() {
4645
5381
  templateVersion: args.flags['template-version'],
4646
5382
  force: isTruthyFlag(args.flags.force),
4647
5383
  });
4648
- printPersonaScaffoldResult(result, isTruthyFlag(args.flags.json));
4649
- return;
4650
- }
4651
-
4652
- if (command === 'personas' && subcommand === 'runtime-register') {
4653
- const slug = args.positionals[0];
4654
- if (!slug) {
4655
- throw new Error('Persona slug is required. Usage: oomi personas runtime-register <slug> --local-port 4789');
4656
- }
4657
- await handlePersonaRuntimeRegisterCommand(slug, args.flags);
4658
- return;
4659
- }
4660
-
4661
- if (command === 'personas' && subcommand === 'heartbeat') {
5384
+ printPersonaScaffoldResult(result, isTruthyFlag(args.flags.json));
5385
+ return;
5386
+ }
5387
+
5388
+ if (command === 'personas' && subcommand === 'runtime-register') {
5389
+ const slug = args.positionals[0];
5390
+ if (!slug) {
5391
+ throw new Error('Persona slug is required. Usage: oomi personas runtime-register <slug> --local-port 4789');
5392
+ }
5393
+ await handlePersonaRuntimeRegisterCommand(slug, args.flags);
5394
+ return;
5395
+ }
5396
+
5397
+ if (command === 'personas' && subcommand === 'status') {
5398
+ const slug = args.positionals[0];
5399
+ if (!slug) {
5400
+ throw new Error('Persona slug is required. Usage: oomi personas status <slug>');
5401
+ }
5402
+ await handlePersonaStatusCommand(slug, args.flags);
5403
+ return;
5404
+ }
5405
+
5406
+ if (command === 'personas' && subcommand === 'stop') {
4662
5407
  const slug = args.positionals[0];
4663
5408
  if (!slug) {
4664
- throw new Error('Persona slug is required. Usage: oomi personas heartbeat <slug> --local-port 4789');
5409
+ throw new Error('Persona slug is required. Usage: oomi personas stop <slug>');
4665
5410
  }
4666
- await handlePersonaHeartbeatCommand(slug, args.flags);
5411
+ await handlePersonaStopCommand(slug, args.flags);
4667
5412
  return;
4668
5413
  }
4669
5414
 
4670
- if (command === 'personas' && subcommand === 'runtime-fail') {
5415
+ if (command === 'personas' && subcommand === 'delete') {
4671
5416
  const slug = args.positionals[0];
4672
5417
  if (!slug) {
4673
- throw new Error('Persona slug is required. Usage: oomi personas runtime-fail <slug> --code RUNTIME_FAILED --message "<text>"');
4674
- }
4675
- await handlePersonaRuntimeFailCommand(slug, args.flags);
4676
- return;
4677
- }
4678
-
4679
- if (command === 'persona-jobs' && subcommand === 'start') {
4680
- const jobId = args.positionals[0];
4681
- if (!jobId) {
4682
- throw new Error('Persona job id is required. Usage: oomi persona-jobs start <jobId>');
4683
- }
4684
- await handlePersonaJobStartCommand(jobId, args.flags);
4685
- return;
4686
- }
4687
-
4688
- if (command === 'persona-jobs' && subcommand === 'succeed') {
4689
- const jobId = args.positionals[0];
4690
- if (!jobId) {
4691
- throw new Error('Persona job id is required. Usage: oomi persona-jobs succeed <jobId> --workspace-path <path> --local-port 4789');
4692
- }
4693
- await handlePersonaJobSucceedCommand(jobId, args.flags);
4694
- return;
4695
- }
4696
-
4697
- if (command === 'persona-jobs' && subcommand === 'fail') {
4698
- const jobId = args.positionals[0];
4699
- if (!jobId) {
4700
- throw new Error('Persona job id is required. Usage: oomi persona-jobs fail <jobId> --code JOB_FAILED --message "<text>"');
5418
+ throw new Error('Persona slug is required. Usage: oomi personas delete <slug>');
4701
5419
  }
4702
- await handlePersonaJobFailCommand(jobId, args.flags);
4703
- return;
4704
- }
4705
-
4706
- if (command === 'persona-jobs' && subcommand === 'execute') {
4707
- await handlePersonaJobExecuteCommand(args.flags);
5420
+ await handlePersonaDeleteCommand(slug, args.flags);
4708
5421
  return;
4709
5422
  }
4710
5423
 
4711
- console.error(`Unknown command: ${command} ${subcommand || ''}`.trim());
5424
+ if (command === 'personas' && subcommand === 'heartbeat') {
5425
+ const slug = args.positionals[0];
5426
+ if (!slug) {
5427
+ throw new Error('Persona slug is required. Usage: oomi personas heartbeat <slug> --local-port 4789');
5428
+ }
5429
+ await handlePersonaHeartbeatCommand(slug, args.flags);
5430
+ return;
5431
+ }
5432
+
5433
+ if (command === 'personas' && subcommand === 'runtime-fail') {
5434
+ const slug = args.positionals[0];
5435
+ if (!slug) {
5436
+ throw new Error('Persona slug is required. Usage: oomi personas runtime-fail <slug> --code RUNTIME_FAILED --message "<text>"');
5437
+ }
5438
+ await handlePersonaRuntimeFailCommand(slug, args.flags);
5439
+ return;
5440
+ }
5441
+
5442
+ if (command === 'persona-jobs' && subcommand === 'start') {
5443
+ const jobId = args.positionals[0];
5444
+ if (!jobId) {
5445
+ throw new Error('Persona job id is required. Usage: oomi persona-jobs start <jobId>');
5446
+ }
5447
+ await handlePersonaJobStartCommand(jobId, args.flags);
5448
+ return;
5449
+ }
5450
+
5451
+ if (command === 'persona-jobs' && subcommand === 'succeed') {
5452
+ const jobId = args.positionals[0];
5453
+ if (!jobId) {
5454
+ throw new Error('Persona job id is required. Usage: oomi persona-jobs succeed <jobId> --workspace-path <path> --local-port 4789');
5455
+ }
5456
+ await handlePersonaJobSucceedCommand(jobId, args.flags);
5457
+ return;
5458
+ }
5459
+
5460
+ if (command === 'persona-jobs' && subcommand === 'fail') {
5461
+ const jobId = args.positionals[0];
5462
+ if (!jobId) {
5463
+ throw new Error('Persona job id is required. Usage: oomi persona-jobs fail <jobId> --code JOB_FAILED --message "<text>"');
5464
+ }
5465
+ await handlePersonaJobFailCommand(jobId, args.flags);
5466
+ return;
5467
+ }
5468
+
5469
+ if (command === 'persona-jobs' && subcommand === 'execute') {
5470
+ await handlePersonaJobExecuteCommand(args.flags);
5471
+ return;
5472
+ }
5473
+
5474
+ console.error(`Unknown command: ${command} ${subcommand || ''}`.trim());
4712
5475
  usage();
4713
5476
  process.exit(1);
4714
5477
  }
@@ -4724,23 +5487,25 @@ if (__isDirectExecution) {
4724
5487
  });
4725
5488
  }
4726
5489
 
4727
- export {
4728
- prepareGatewayFrameForLocalGateway,
4729
- ensureAssistantSpokenMetadata,
4730
- normalizeAssistantGatewayFrame,
4731
- runAssistantFinalDebugCheck,
4732
- buildBridgeLaunchAgentPlist,
4733
- classifyBridgeFailure,
4734
- classifyBridgeSessionScope,
4735
- createBridgeProcessFaultHandler,
4736
- computeReconnectDelayMs,
4737
- resolveBridgeStatusForBrokerOpen,
4738
- resolveBridgeStatusForRuntimeFault,
4739
- runBridgeCallbackSafely,
4740
- extractGatewayRequestMeta,
4741
- extractGatewayResponseMeta,
4742
- isServiceManagedBridgeStart,
4743
- isGatewayRunStartedFrame,
4744
- isBridgeWorkerCommand,
4745
- parsePositiveInteger,
4746
- };
5490
+ export {
5491
+ prepareGatewayFrameForLocalGateway,
5492
+ ensureAssistantSpokenMetadata,
5493
+ normalizeAssistantGatewayFrame,
5494
+ runAssistantFinalDebugCheck,
5495
+ buildOpenclawProfileFromFlags,
5496
+ handleOpenclawProfileCommand,
5497
+ buildBridgeLaunchAgentPlist,
5498
+ classifyBridgeFailure,
5499
+ classifyBridgeSessionScope,
5500
+ createBridgeProcessFaultHandler,
5501
+ computeReconnectDelayMs,
5502
+ resolveBridgeStatusForBrokerOpen,
5503
+ resolveBridgeStatusForRuntimeFault,
5504
+ runBridgeCallbackSafely,
5505
+ extractGatewayRequestMeta,
5506
+ extractGatewayResponseMeta,
5507
+ isServiceManagedBridgeStart,
5508
+ isGatewayRunStartedFrame,
5509
+ isBridgeWorkerCommand,
5510
+ parsePositiveInteger,
5511
+ };