oomi-ai 0.2.29 → 0.2.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -158
- package/bin/oomi-ai.js +2130 -1365
- package/lib/openclawDevGateway.js +384 -0
- package/lib/openclawPaths.js +78 -0
- package/lib/openclawProfile.js +265 -0
- package/lib/personaApiClient.js +304 -253
- package/lib/personaJobExecutor.js +35 -11
- package/lib/personaPortAllocator.js +36 -0
- package/lib/personaRuntimeManager.js +364 -0
- package/lib/personaRuntimeProcess.js +378 -121
- package/lib/personaRuntimeRegistry.js +67 -0
- package/lib/personaRuntimeSupervisor.js +193 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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 {
|
|
15
|
-
import {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from '../lib/
|
|
23
|
-
import {
|
|
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 =
|
|
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 =
|
|
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
|
|
196
|
-
|
|
197
|
-
openclaw
|
|
198
|
-
|
|
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
|
|
220
|
-
|
|
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
|
-
--
|
|
277
|
-
--
|
|
278
|
-
--
|
|
279
|
-
--
|
|
280
|
-
--
|
|
281
|
-
--
|
|
282
|
-
--
|
|
283
|
-
--
|
|
284
|
-
--
|
|
285
|
-
--
|
|
286
|
-
--
|
|
287
|
-
--
|
|
288
|
-
--
|
|
289
|
-
--
|
|
290
|
-
--
|
|
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('&', '&')
|
|
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('&', '&')
|
|
335
388
|
.replaceAll('<', '<')
|
|
336
389
|
.replaceAll('>', '>')
|
|
337
390
|
.replaceAll('"', '"')
|
|
@@ -339,11 +392,7 @@ function xmlEscape(value) {
|
|
|
339
392
|
}
|
|
340
393
|
|
|
341
394
|
function resolveWorkspace() {
|
|
342
|
-
|
|
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 =
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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 (!
|
|
749
|
-
throw new Error('Missing
|
|
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
|
|
775
|
+
return backendUrl.replace(/\/$/, '');
|
|
1071
776
|
}
|
|
1072
777
|
|
|
1073
|
-
function
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
|
1096
|
-
|
|
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
|
|
1100
|
-
return
|
|
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
|
|
1104
|
-
return
|
|
812
|
+
function isHttpErrorStatus(error, status) {
|
|
813
|
+
return Number(error?.status) === Number(status);
|
|
1105
814
|
}
|
|
1106
815
|
|
|
1107
|
-
function
|
|
1108
|
-
|
|
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
|
|
1112
|
-
return
|
|
824
|
+
function resolvePersonaTemplateVersion(flags = {}, fallback = 'v1') {
|
|
825
|
+
return String(flags['template-version'] || fallback || 'v1').trim() || 'v1';
|
|
1113
826
|
}
|
|
1114
827
|
|
|
1115
|
-
function
|
|
1116
|
-
|
|
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
|
|
1121
|
-
const
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
|
1129
|
-
const statePath = resolveBridgeStatePath();
|
|
1130
|
-
if (!fs.existsSync(statePath)) return {};
|
|
841
|
+
async function findExistingManagedPersona(client, slug) {
|
|
1131
842
|
try {
|
|
1132
|
-
return
|
|
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
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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(
|
|
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
|
-
|
|
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 &&
|
|
3652
|
+
personaJobPollEnabled && personaBackendUrl && deviceToken
|
|
2939
3653
|
? startPersonaJobPoller({
|
|
2940
|
-
backendUrl:
|
|
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:
|
|
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 &&
|
|
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 === '
|
|
4612
|
-
await
|
|
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 === '
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
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 === '
|
|
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
|
|
5409
|
+
throw new Error('Persona slug is required. Usage: oomi personas stop <slug>');
|
|
4665
5410
|
}
|
|
4666
|
-
await
|
|
5411
|
+
await handlePersonaStopCommand(slug, args.flags);
|
|
4667
5412
|
return;
|
|
4668
5413
|
}
|
|
4669
5414
|
|
|
4670
|
-
if (command === 'personas' && subcommand === '
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
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
|
+
};
|