oomi-ai 0.2.28 → 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 +257 -145
- package/bin/oomi-ai.js +2130 -1354
- 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,21 +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');
|
|
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
|
+
}
|
|
57
99
|
|
|
58
100
|
function parsePositiveInteger(value, fallback) {
|
|
59
101
|
const num = Number(value);
|
|
@@ -177,14 +219,22 @@ Commands:
|
|
|
177
219
|
openclaw install
|
|
178
220
|
Install agent instructions and the Oomi skill into OpenClaw.
|
|
179
221
|
|
|
180
|
-
openclaw bridge [start|ensure|stop|restart|ps]
|
|
181
|
-
Manage local OpenClaw-to-Oomi bridge lifecycle (singleton).
|
|
182
|
-
openclaw bridge service [install|start|stop|restart|status|uninstall]
|
|
183
|
-
Manage macOS launchd bridge supervision.
|
|
184
|
-
openclaw
|
|
185
|
-
|
|
186
|
-
openclaw
|
|
187
|
-
|
|
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.
|
|
188
238
|
|
|
189
239
|
openclaw pair
|
|
190
240
|
Pair this OpenClaw host with Oomi and start bridge (single command).
|
|
@@ -201,26 +251,34 @@ Commands:
|
|
|
201
251
|
personas sync
|
|
202
252
|
Sync personas from the repo into the Oomi backend registry.
|
|
203
253
|
|
|
204
|
-
personas create <id>
|
|
205
|
-
Create a new persona manifest and optionally sync it to the backend.
|
|
206
|
-
personas create-managed [slug]
|
|
207
|
-
Create a managed persona in Oomi and enqueue its build job for the linked device.
|
|
208
|
-
personas
|
|
209
|
-
|
|
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.
|
|
210
268
|
personas runtime-register <slug>
|
|
211
269
|
Register a running persona runtime with the Oomi backend.
|
|
212
|
-
personas heartbeat <slug>
|
|
213
|
-
Send a persona runtime heartbeat to the Oomi backend.
|
|
214
|
-
personas runtime-fail <slug>
|
|
215
|
-
Report persona runtime failure to the Oomi backend.
|
|
216
|
-
persona-jobs start <jobId>
|
|
217
|
-
Mark a persona job as running.
|
|
218
|
-
persona-jobs succeed <jobId>
|
|
219
|
-
Mark a persona job as succeeded.
|
|
220
|
-
persona-jobs fail <jobId>
|
|
221
|
-
Mark a persona job as failed.
|
|
222
|
-
persona-jobs execute
|
|
223
|
-
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.
|
|
224
282
|
|
|
225
283
|
Common flags:
|
|
226
284
|
--agents-file PATH Override AGENTS.md path
|
|
@@ -233,94 +291,100 @@ Common flags:
|
|
|
233
291
|
--label TEXT Pairing label shown in broker logs
|
|
234
292
|
--session-key KEY Session key used in generated connect URL
|
|
235
293
|
--detach Start bridge in background and exit
|
|
236
|
-
--no-start Do not start the bridge or persona runtime
|
|
294
|
+
--no-start Do not start the bridge or persona runtime
|
|
237
295
|
--device-id ID Bridge device identifier (default: host name)
|
|
238
296
|
--device-token TOKEN Existing bridge device token
|
|
239
297
|
--show-secrets Print full token values in diagnostic output
|
|
240
|
-
--json Print pairing result as JSON (for automation)
|
|
241
|
-
--text TEXT Assistant text for local debug frame replay
|
|
242
|
-
--frame-file PATH Read a raw gateway frame from disk for local debug replay
|
|
243
|
-
--frame-json JSON Use raw gateway frame JSON text for local debug replay
|
|
244
|
-
--session-id ID Debug session id override (default: ms_debug_local)
|
|
245
|
-
--user-text TEXT User utterance text used for backend voice replay
|
|
246
|
-
--live-provider Use the real Qwen TTS provider in local debug replay
|
|
247
|
-
--env-file PATH Load provider env vars from a specific env file (default: <repo>/.env.local)
|
|
248
|
-
--provider-timeout-ms N
|
|
249
|
-
Timeout in ms for live provider audio during local debug replay
|
|
250
|
-
--backend-url URL Override Oomi backend URL
|
|
251
|
-
--root PATH Override repo root path for persona discovery
|
|
252
|
-
--role ROLE Message role override for local debug frame replay
|
|
253
|
-
--omit-role Omit message.role in the generated local debug frame
|
|
254
|
-
--name NAME Persona display name (for create)
|
|
255
|
-
--description TEXT Persona description (for scaffold)
|
|
256
|
-
--slug SLUG Explicit slug override (for create-managed)
|
|
257
|
-
--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)
|
|
258
316
|
--status STATUS Persona status (for create)
|
|
259
317
|
--type TYPE Persona type (for create)
|
|
260
318
|
--tags a,b,c Persona tags (for create)
|
|
261
319
|
--chat-session KEY Persona chat session key (for create)
|
|
262
|
-
--out PATH Output directory for scaffolded persona app
|
|
263
|
-
--template-version V Scaffold template version (default: v1)
|
|
264
|
-
--force Overwrite files in an existing output directory
|
|
265
|
-
--
|
|
266
|
-
--
|
|
267
|
-
--
|
|
268
|
-
--
|
|
269
|
-
--
|
|
270
|
-
--
|
|
271
|
-
--
|
|
272
|
-
--
|
|
273
|
-
--
|
|
274
|
-
--
|
|
275
|
-
--
|
|
276
|
-
--
|
|
277
|
-
--
|
|
278
|
-
--
|
|
279
|
-
--
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
}
|
|
282
346
|
|
|
283
347
|
function readFile(filePath) {
|
|
284
348
|
return fs.readFileSync(filePath, 'utf-8');
|
|
285
349
|
}
|
|
286
350
|
|
|
287
|
-
function writeFile(filePath, content, options = undefined) {
|
|
288
|
-
fs.writeFileSync(filePath, content, options);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function parseDotEnvLine(line) {
|
|
292
|
-
const trimmed = String(line || '').trim();
|
|
293
|
-
if (!trimmed || trimmed.startsWith('#')) return null;
|
|
294
|
-
const separatorIndex = trimmed.indexOf('=');
|
|
295
|
-
if (separatorIndex <= 0) return null;
|
|
296
|
-
const key = trimmed.slice(0, separatorIndex).trim();
|
|
297
|
-
if (!key) return null;
|
|
298
|
-
let value = trimmed.slice(separatorIndex + 1).trim();
|
|
299
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
300
|
-
value = value.slice(1, -1);
|
|
301
|
-
}
|
|
302
|
-
return { key, value };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function loadEnvFile(filePath, keys = []) {
|
|
306
|
-
if (!filePath || !fs.existsSync(filePath)) {
|
|
307
|
-
throw new Error(`Environment file not found: ${filePath}`);
|
|
308
|
-
}
|
|
309
|
-
const selectedKeys = Array.isArray(keys) && keys.length ? new Set(keys) : null;
|
|
310
|
-
const entries = {};
|
|
311
|
-
const lines = readFile(filePath).split(/\r?\n/);
|
|
312
|
-
for (const line of lines) {
|
|
313
|
-
const parsed = parseDotEnvLine(line);
|
|
314
|
-
if (!parsed) continue;
|
|
315
|
-
if (selectedKeys && !selectedKeys.has(parsed.key)) continue;
|
|
316
|
-
entries[parsed.key] = parsed.value;
|
|
317
|
-
}
|
|
318
|
-
return entries;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function xmlEscape(value) {
|
|
322
|
-
return String(value)
|
|
323
|
-
.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('&', '&')
|
|
324
388
|
.replaceAll('<', '<')
|
|
325
389
|
.replaceAll('>', '>')
|
|
326
390
|
.replaceAll('"', '"')
|
|
@@ -328,11 +392,7 @@ function xmlEscape(value) {
|
|
|
328
392
|
}
|
|
329
393
|
|
|
330
394
|
function resolveWorkspace() {
|
|
331
|
-
|
|
332
|
-
if (envWorkspace) return envWorkspace;
|
|
333
|
-
const defaultWorkspace = path.join(os.homedir(), '.openclaw', 'workspace');
|
|
334
|
-
if (fs.existsSync(defaultWorkspace)) return defaultWorkspace;
|
|
335
|
-
return path.join(os.homedir(), '.openclaw');
|
|
395
|
+
return resolveOpenclawWorkspaceRoot();
|
|
336
396
|
}
|
|
337
397
|
|
|
338
398
|
function resolveAgentsFile(cliAgentsFile, cliWorkspace) {
|
|
@@ -409,9 +469,9 @@ function ensureDir(dirPath) {
|
|
|
409
469
|
}
|
|
410
470
|
}
|
|
411
471
|
|
|
412
|
-
function findRepoRoot(startDir) {
|
|
413
|
-
let current = startDir;
|
|
414
|
-
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) {
|
|
415
475
|
const personasDir = path.join(current, 'personas');
|
|
416
476
|
const skillsDir = path.join(current, 'skills', 'oomi');
|
|
417
477
|
if (fs.existsSync(personasDir) || fs.existsSync(skillsDir)) {
|
|
@@ -420,23 +480,23 @@ function findRepoRoot(startDir) {
|
|
|
420
480
|
const parent = path.dirname(current);
|
|
421
481
|
if (parent === current) break;
|
|
422
482
|
current = parent;
|
|
423
|
-
}
|
|
424
|
-
return null;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function resolveRepoRoot(rootFlag) {
|
|
428
|
-
const explicitRoot =
|
|
429
|
-
typeof rootFlag === 'string' && rootFlag.trim()
|
|
430
|
-
? path.resolve(rootFlag.trim())
|
|
431
|
-
: '';
|
|
432
|
-
const repoRoot = explicitRoot || findRepoRoot(process.cwd()) || findRepoRoot(PACKAGE_ROOT);
|
|
433
|
-
if (!repoRoot) {
|
|
434
|
-
throw new Error('Could not locate repo root. Use --root <repo root>.');
|
|
435
|
-
}
|
|
436
|
-
return repoRoot;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
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) {
|
|
440
500
|
const packaged = path.join(PACKAGE_ROOT, 'skills', 'oomi');
|
|
441
501
|
if (fs.existsSync(packaged)) {
|
|
442
502
|
return packaged;
|
|
@@ -462,7 +522,7 @@ function resolveSkillTargets(cliSkillsDir) {
|
|
|
462
522
|
}
|
|
463
523
|
|
|
464
524
|
const targets = [];
|
|
465
|
-
const openclaw =
|
|
525
|
+
const openclaw = resolveOpenclawSkillsDir();
|
|
466
526
|
const clawd = path.join(os.homedir(), 'clawd', 'skills');
|
|
467
527
|
|
|
468
528
|
targets.push(openclaw);
|
|
@@ -648,7 +708,7 @@ async function createPersona({ id, root, flags }) {
|
|
|
648
708
|
console.log(`Synced persona ${payload?.persona?.slug || id}`);
|
|
649
709
|
}
|
|
650
710
|
|
|
651
|
-
function printPersonaScaffoldResult(result, asJson = false) {
|
|
711
|
+
function printPersonaScaffoldResult(result, asJson = false) {
|
|
652
712
|
if (asJson) {
|
|
653
713
|
console.log(JSON.stringify(result, null, 2));
|
|
654
714
|
return;
|
|
@@ -661,471 +721,770 @@ function printPersonaScaffoldResult(result, asJson = false) {
|
|
|
661
721
|
console.log(`Start: ${result.startCommand}`);
|
|
662
722
|
if (Array.isArray(result.editableZones) && result.editableZones.length > 0) {
|
|
663
723
|
console.log(`Editable zones: ${result.editableZones.join(', ')}`);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function printManagedPersonaCreateResult(result, asJson = false) {
|
|
668
|
-
if (asJson) {
|
|
669
|
-
console.log(JSON.stringify(result, null, 2));
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const persona = result?.persona && typeof result.persona === 'object' ? result.persona : {};
|
|
674
|
-
const personaJob = result?.personaJob && typeof result.personaJob === 'object' ? result.personaJob : {};
|
|
675
|
-
console.log(`Managed persona created: ${String(persona.name || persona.slug || 'unknown')}`);
|
|
676
|
-
if (persona.slug) {
|
|
677
|
-
console.log(`Slug: ${persona.slug}`);
|
|
678
|
-
}
|
|
679
|
-
if (persona.lifecycle) {
|
|
680
|
-
console.log(`Lifecycle: ${persona.lifecycle}`);
|
|
681
|
-
}
|
|
682
|
-
if (personaJob.jobId) {
|
|
683
|
-
console.log(`Persona job: ${personaJob.jobId}`);
|
|
684
|
-
}
|
|
685
|
-
if (personaJob.status) {
|
|
686
|
-
console.log(`Job status: ${personaJob.status}`);
|
|
687
|
-
}
|
|
688
|
-
if (personaJob.deviceId) {
|
|
689
|
-
console.log(`Assigned device: ${personaJob.deviceId}`);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function parseOptionalPositiveInteger(value) {
|
|
694
|
-
if (value === undefined || value === null || value === '') return null;
|
|
695
|
-
const parsed = Number(value);
|
|
696
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
697
|
-
throw new Error(`Expected a positive integer, received: ${value}`);
|
|
698
|
-
}
|
|
699
|
-
return Math.floor(parsed);
|
|
700
|
-
}
|
|
701
|
-
|
|
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
|
+
|
|
702
762
|
function resolvePersonaBackendUrl(flags = {}) {
|
|
703
763
|
const bridgeState = readBridgeState();
|
|
704
764
|
const backendUrl = String(
|
|
705
765
|
flags['backend-url'] ||
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (!backendUrl) {
|
|
712
|
-
throw new Error('Missing backend URL. Use --backend-url or pair the device first.');
|
|
713
|
-
}
|
|
714
|
-
return backendUrl.replace(/\/$/, '');
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function resolvePersonaDeviceToken(flags = {}) {
|
|
718
|
-
const bridgeState = readBridgeState();
|
|
719
|
-
const deviceToken = String(
|
|
720
|
-
flags['device-token'] ||
|
|
721
|
-
bridgeState.deviceToken ||
|
|
722
|
-
''
|
|
723
|
-
).trim();
|
|
724
|
-
if (!deviceToken) {
|
|
725
|
-
throw new Error('Missing device token. Use --device-token or pair the device first.');
|
|
726
|
-
}
|
|
727
|
-
return deviceToken;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function resolvePersonaDeviceId(flags = {}) {
|
|
731
|
-
const bridgeState = readBridgeState();
|
|
732
|
-
const deviceId = String(
|
|
733
|
-
flags['device-id'] ||
|
|
734
|
-
bridgeState.deviceId ||
|
|
735
|
-
''
|
|
766
|
+
process.env.OOMI_DEV_BACKEND_URL ||
|
|
767
|
+
process.env.OOMI_BACKEND_URL ||
|
|
768
|
+
process.env.NEXT_PUBLIC_BACKEND_URL ||
|
|
769
|
+
bridgeState.brokerHttp ||
|
|
770
|
+
''
|
|
736
771
|
).trim();
|
|
737
|
-
if (!
|
|
738
|
-
throw new Error('Missing
|
|
739
|
-
}
|
|
740
|
-
return deviceId;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function createCliPersonaApiClient(flags = {}) {
|
|
744
|
-
return createPersonaApiClient({
|
|
745
|
-
backendUrl: resolvePersonaBackendUrl(flags),
|
|
746
|
-
deviceToken: resolvePersonaDeviceToken(flags),
|
|
747
|
-
deviceId: resolvePersonaDeviceId(flags),
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function ensurePersonaJobWorkspace(message, workspaceRoot = defaultPersonaWorkspaceRoot()) {
|
|
752
|
-
const metadata = message && typeof message === 'object' ? message.metadata : null;
|
|
753
|
-
const payload = metadata && typeof metadata === 'object' ? metadata.payload : null;
|
|
754
|
-
if (!payload || typeof payload !== 'object') {
|
|
755
|
-
return message;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const persona = payload.persona && typeof payload.persona === 'object' ? payload.persona : {};
|
|
759
|
-
const scaffold = payload.scaffold && typeof payload.scaffold === 'object' ? payload.scaffold : {};
|
|
760
|
-
if (!scaffold.outDir && typeof persona.slug === 'string' && persona.slug.trim()) {
|
|
761
|
-
scaffold.outDir = path.join(workspaceRoot, persona.slug.trim());
|
|
762
|
-
payload.scaffold = scaffold;
|
|
763
|
-
metadata.payload = payload;
|
|
764
|
-
message.metadata = metadata;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return message;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
async function runManagedPersonaJobExecution({
|
|
771
|
-
message,
|
|
772
|
-
backendUrl,
|
|
773
|
-
deviceToken,
|
|
774
|
-
deviceId,
|
|
775
|
-
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
776
|
-
shouldInstall = true,
|
|
777
|
-
shouldStart = true,
|
|
778
|
-
shouldRegister = true,
|
|
779
|
-
logFilePath = '',
|
|
780
|
-
}) {
|
|
781
|
-
const normalizedMessage = ensurePersonaJobWorkspace(
|
|
782
|
-
structuredClone(message),
|
|
783
|
-
workspaceRoot,
|
|
784
|
-
);
|
|
785
|
-
const client = createPersonaApiClient({
|
|
786
|
-
backendUrl,
|
|
787
|
-
deviceToken,
|
|
788
|
-
deviceId,
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
return executePersonaJob({
|
|
792
|
-
message: normalizedMessage,
|
|
793
|
-
installWorkspace: shouldInstall
|
|
794
|
-
? async ({ workspacePath }) => {
|
|
795
|
-
await installPersonaWorkspace({ workspacePath });
|
|
796
|
-
}
|
|
797
|
-
: async () => {},
|
|
798
|
-
startWorkspace: shouldStart
|
|
799
|
-
? async ({ workspacePath }) =>
|
|
800
|
-
startPersonaWorkspace({
|
|
801
|
-
workspacePath,
|
|
802
|
-
logFilePath,
|
|
803
|
-
})
|
|
804
|
-
: async () => ({ pid: null, logFilePath }),
|
|
805
|
-
waitForRuntime: shouldStart
|
|
806
|
-
? async ({ runtime }) => {
|
|
807
|
-
await waitForPersonaRuntime({
|
|
808
|
-
healthcheckUrl: runtime.healthcheckUrl,
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
: async () => {},
|
|
812
|
-
registerRuntime: shouldRegister
|
|
813
|
-
? async ({ payload: jobPayload, result: runtimeResult }) => {
|
|
814
|
-
const jobPersona = jobPayload.persona && typeof jobPayload.persona === 'object' ? jobPayload.persona : {};
|
|
815
|
-
await client.registerRuntime({
|
|
816
|
-
slug: String(jobPersona.slug || '').trim(),
|
|
817
|
-
endpoint: runtimeResult.endpoint,
|
|
818
|
-
healthcheckUrl: runtimeResult.healthcheckUrl,
|
|
819
|
-
transport: runtimeResult.transport,
|
|
820
|
-
localPort: runtimeResult.localPort,
|
|
821
|
-
startedAt: new Date().toISOString(),
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
: async () => {},
|
|
825
|
-
onJobStart: async ({ jobId }) => {
|
|
826
|
-
await client.startJob({
|
|
827
|
-
jobId,
|
|
828
|
-
startedAt: new Date().toISOString(),
|
|
829
|
-
});
|
|
830
|
-
},
|
|
831
|
-
onJobSuccess: async ({ jobId, result: runtimeResult }) => {
|
|
832
|
-
await client.succeedJob({
|
|
833
|
-
jobId,
|
|
834
|
-
workspacePath: runtimeResult.workspacePath,
|
|
835
|
-
localPort: runtimeResult.localPort,
|
|
836
|
-
transport: runtimeResult.transport,
|
|
837
|
-
endpoint: runtimeResult.endpoint,
|
|
838
|
-
healthcheckUrl: runtimeResult.healthcheckUrl,
|
|
839
|
-
completedAt: new Date().toISOString(),
|
|
840
|
-
});
|
|
841
|
-
},
|
|
842
|
-
onJobFailure: async ({ jobId, error }) => {
|
|
843
|
-
await client.failJob({
|
|
844
|
-
jobId,
|
|
845
|
-
code: String(error?.code || 'PERSONA_JOB_EXECUTION_FAILED').trim(),
|
|
846
|
-
message: String(error?.message || 'Persona job execution failed.').trim(),
|
|
847
|
-
completedAt: new Date().toISOString(),
|
|
848
|
-
});
|
|
849
|
-
},
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function resolvePersonaRuntimeInput(flags = {}, defaults = {}) {
|
|
854
|
-
const localPort = parseOptionalPositiveInteger(flags['local-port'] || flags.localPort || defaults.localPort);
|
|
855
|
-
const endpoint = String(flags.endpoint || defaults.endpoint || '').trim();
|
|
856
|
-
const healthPath = String(flags['health-path'] || defaults.healthPath || '/oomi.health.json').trim() || '/oomi.health.json';
|
|
857
|
-
const healthcheckUrl = String(flags['healthcheck-url'] || defaults.healthcheckUrl || '').trim();
|
|
858
|
-
const transport = String(flags.transport || defaults.transport || 'local').trim() || 'local';
|
|
859
|
-
|
|
860
|
-
if (endpoint) {
|
|
861
|
-
return {
|
|
862
|
-
endpoint,
|
|
863
|
-
healthcheckUrl: healthcheckUrl || `${endpoint.replace(/\/$/, '')}${healthPath}`,
|
|
864
|
-
localPort,
|
|
865
|
-
transport,
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if (!localPort) {
|
|
870
|
-
throw new Error('Runtime endpoint or local port is required.');
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return buildLocalPersonaRuntime({
|
|
874
|
-
localPort,
|
|
875
|
-
healthPath,
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function parseIsoTimestamp(rawValue, label) {
|
|
880
|
-
const value = String(rawValue || '').trim();
|
|
881
|
-
if (!value) return undefined;
|
|
882
|
-
const timestamp = new Date(value);
|
|
883
|
-
if (Number.isNaN(timestamp.getTime())) {
|
|
884
|
-
throw new Error(`${label} must be a valid ISO timestamp.`);
|
|
885
|
-
}
|
|
886
|
-
return timestamp.toISOString();
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function readStructuredPersonaJobMessage(flags = {}) {
|
|
890
|
-
const filePath = String(flags['message-file'] || '').trim();
|
|
891
|
-
const inlineJson = String(flags['message-json'] || '').trim();
|
|
892
|
-
|
|
893
|
-
if (inlineJson) {
|
|
894
|
-
return JSON.parse(inlineJson);
|
|
895
|
-
}
|
|
896
|
-
if (filePath) {
|
|
897
|
-
return JSON.parse(readFile(path.resolve(filePath)));
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
throw new Error('Persona job message is required. Use --message-file or --message-json.');
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function printStructuredResult(result, asJson = false) {
|
|
904
|
-
if (asJson) {
|
|
905
|
-
console.log(JSON.stringify(result, null, 2));
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
for (const [key, value] of Object.entries(result)) {
|
|
910
|
-
console.log(`${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
|
|
915
|
-
const client = createCliPersonaApiClient(flags);
|
|
916
|
-
const runtime = resolvePersonaRuntimeInput(flags);
|
|
917
|
-
const payload = await client.registerRuntime({
|
|
918
|
-
slug,
|
|
919
|
-
endpoint: runtime.endpoint,
|
|
920
|
-
healthcheckUrl: runtime.healthcheckUrl,
|
|
921
|
-
localPort: runtime.localPort,
|
|
922
|
-
transport: runtime.transport,
|
|
923
|
-
startedAt: parseIsoTimestamp(flags['started-at'], 'started-at'),
|
|
924
|
-
});
|
|
925
|
-
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
async function handlePersonaHeartbeatCommand(slug, flags = {}) {
|
|
929
|
-
const client = createCliPersonaApiClient(flags);
|
|
930
|
-
const runtime = resolvePersonaRuntimeInput(flags);
|
|
931
|
-
const payload = await client.heartbeatRuntime({
|
|
932
|
-
slug,
|
|
933
|
-
endpoint: runtime.endpoint,
|
|
934
|
-
healthcheckUrl: runtime.healthcheckUrl,
|
|
935
|
-
localPort: runtime.localPort,
|
|
936
|
-
transport: runtime.transport,
|
|
937
|
-
observedAt: parseIsoTimestamp(flags['observed-at'], 'observed-at'),
|
|
938
|
-
});
|
|
939
|
-
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
async function handlePersonaRuntimeFailCommand(slug, flags = {}) {
|
|
943
|
-
const code = String(flags.code || '').trim();
|
|
944
|
-
const message = String(flags.message || '').trim();
|
|
945
|
-
if (!code) {
|
|
946
|
-
throw new Error('Error code is required. Use --code.');
|
|
947
|
-
}
|
|
948
|
-
if (!message) {
|
|
949
|
-
throw new Error('Error message is required. Use --message.');
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
const client = createCliPersonaApiClient(flags);
|
|
953
|
-
const payload = await client.failRuntime({
|
|
954
|
-
slug,
|
|
955
|
-
code,
|
|
956
|
-
message,
|
|
957
|
-
});
|
|
958
|
-
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
async function handlePersonaJobStartCommand(jobId, flags = {}) {
|
|
962
|
-
const client = createCliPersonaApiClient(flags);
|
|
963
|
-
const payload = await client.startJob({
|
|
964
|
-
jobId,
|
|
965
|
-
startedAt: parseIsoTimestamp(flags['started-at'], 'started-at'),
|
|
966
|
-
});
|
|
967
|
-
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
async function handlePersonaJobSucceedCommand(jobId, flags = {}) {
|
|
971
|
-
const client = createCliPersonaApiClient(flags);
|
|
972
|
-
const runtime = resolvePersonaRuntimeInput(flags);
|
|
973
|
-
const workspacePath = String(flags['workspace-path'] || flags.workspacePath || '').trim();
|
|
974
|
-
if (!workspacePath) {
|
|
975
|
-
throw new Error('Workspace path is required. Use --workspace-path.');
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
const payload = await client.succeedJob({
|
|
979
|
-
jobId,
|
|
980
|
-
workspacePath,
|
|
981
|
-
localPort: runtime.localPort,
|
|
982
|
-
transport: runtime.transport,
|
|
983
|
-
endpoint: runtime.endpoint,
|
|
984
|
-
healthcheckUrl: runtime.healthcheckUrl,
|
|
985
|
-
completedAt: parseIsoTimestamp(flags['completed-at'], 'completed-at'),
|
|
986
|
-
});
|
|
987
|
-
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
async function handlePersonaJobFailCommand(jobId, flags = {}) {
|
|
991
|
-
const code = String(flags.code || '').trim();
|
|
992
|
-
const message = String(flags.message || '').trim();
|
|
993
|
-
if (!code) {
|
|
994
|
-
throw new Error('Error code is required. Use --code.');
|
|
995
|
-
}
|
|
996
|
-
if (!message) {
|
|
997
|
-
throw new Error('Error message is required. Use --message.');
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const client = createCliPersonaApiClient(flags);
|
|
1001
|
-
const payload = await client.failJob({
|
|
1002
|
-
jobId,
|
|
1003
|
-
code,
|
|
1004
|
-
message,
|
|
1005
|
-
completedAt: parseIsoTimestamp(flags['completed-at'], 'completed-at'),
|
|
1006
|
-
});
|
|
1007
|
-
printStructuredResult(payload, isTruthyFlag(flags.json));
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
async function handlePersonaJobExecuteCommand(flags = {}) {
|
|
1011
|
-
const message = readStructuredPersonaJobMessage(flags);
|
|
1012
|
-
const shouldInstall = !isTruthyFlag(flags['no-install']);
|
|
1013
|
-
const shouldStart = !isTruthyFlag(flags['no-start']);
|
|
1014
|
-
const shouldRegister = !isTruthyFlag(flags['no-register']) && shouldStart;
|
|
1015
|
-
const logFilePath = String(flags['log-file'] || '').trim();
|
|
1016
|
-
const result = await runManagedPersonaJobExecution({
|
|
1017
|
-
message,
|
|
1018
|
-
backendUrl: resolvePersonaBackendUrl(flags),
|
|
1019
|
-
deviceToken: resolvePersonaDeviceToken(flags),
|
|
1020
|
-
deviceId: resolvePersonaDeviceId(flags),
|
|
1021
|
-
workspaceRoot: String(flags['workspace-root'] || defaultPersonaWorkspaceRoot()).trim(),
|
|
1022
|
-
shouldInstall,
|
|
1023
|
-
shouldStart,
|
|
1024
|
-
shouldRegister,
|
|
1025
|
-
logFilePath,
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
printStructuredResult(result, isTruthyFlag(flags.json));
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async function handlePersonaCreateManagedCommand(flags = {}, positionalSlug = '') {
|
|
1032
|
-
const name = String(flags.name || '').trim();
|
|
1033
|
-
if (!name) {
|
|
1034
|
-
throw new Error('Persona name is required. Usage: oomi personas create-managed [slug] --name "<name>" --description "<description>"');
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const description = String(flags.description || '').trim() || name;
|
|
1038
|
-
const explicitSlug = String(flags.slug || positionalSlug || '').trim();
|
|
1039
|
-
const client = createCliPersonaApiClient(flags);
|
|
1040
|
-
const result = await client.createManagedPersona({
|
|
1041
|
-
slug: explicitSlug,
|
|
1042
|
-
name,
|
|
1043
|
-
description,
|
|
1044
|
-
templateType: String(flags['template-type'] || 'persona-app').trim() || 'persona-app',
|
|
1045
|
-
promptTemplateVersion: String(flags['template-version'] || 'v1').trim() || 'v1',
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
printManagedPersonaCreateResult(result, isTruthyFlag(flags.json));
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
function resolveOpenclawConfigPath() {
|
|
1052
|
-
const candidates = [
|
|
1053
|
-
path.join(os.homedir(), '.openclaw', 'clawdbot.json'),
|
|
1054
|
-
path.join(os.homedir(), '.openclaw', 'openclaw.json'),
|
|
1055
|
-
];
|
|
1056
|
-
for (const candidate of candidates) {
|
|
1057
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
772
|
+
if (!backendUrl) {
|
|
773
|
+
throw new Error('Missing backend URL. Use --backend-url or pair the device first.');
|
|
1058
774
|
}
|
|
1059
|
-
return
|
|
775
|
+
return backendUrl.replace(/\/$/, '');
|
|
1060
776
|
}
|
|
1061
777
|
|
|
1062
|
-
function
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
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.');
|
|
1066
787
|
}
|
|
1067
|
-
|
|
1068
|
-
const parsed = JSON.parse(readFile(configPath));
|
|
1069
|
-
const gateway = parsed.gateway || {};
|
|
1070
|
-
const auth = gateway.auth || {};
|
|
1071
|
-
const port = gateway.port || 18789;
|
|
1072
|
-
const bind = gateway.bind || 'loopback';
|
|
1073
|
-
const host = bind === 'all' ? '127.0.0.1' : '127.0.0.1';
|
|
1074
|
-
const gatewayUrl = `ws://${host}:${port}`;
|
|
1075
|
-
|
|
1076
|
-
return {
|
|
1077
|
-
gatewayUrl,
|
|
1078
|
-
token: typeof auth.token === 'string' ? auth.token.trim() : '',
|
|
1079
|
-
password: typeof auth.password === 'string' ? auth.password.trim() : '',
|
|
1080
|
-
configPath,
|
|
1081
|
-
};
|
|
788
|
+
return deviceToken;
|
|
1082
789
|
}
|
|
1083
790
|
|
|
1084
|
-
function
|
|
1085
|
-
|
|
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;
|
|
1086
802
|
}
|
|
1087
803
|
|
|
1088
|
-
function
|
|
1089
|
-
return
|
|
804
|
+
function createCliPersonaApiClient(flags = {}) {
|
|
805
|
+
return createPersonaApiClient({
|
|
806
|
+
backendUrl: resolvePersonaBackendUrl(flags),
|
|
807
|
+
deviceToken: resolvePersonaDeviceToken(flags),
|
|
808
|
+
deviceId: resolvePersonaDeviceId(flags),
|
|
809
|
+
});
|
|
1090
810
|
}
|
|
1091
811
|
|
|
1092
|
-
function
|
|
1093
|
-
return
|
|
812
|
+
function isHttpErrorStatus(error, status) {
|
|
813
|
+
return Number(error?.status) === Number(status);
|
|
1094
814
|
}
|
|
1095
815
|
|
|
1096
|
-
function
|
|
1097
|
-
|
|
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;
|
|
1098
822
|
}
|
|
1099
823
|
|
|
1100
|
-
function
|
|
1101
|
-
return
|
|
824
|
+
function resolvePersonaTemplateVersion(flags = {}, fallback = 'v1') {
|
|
825
|
+
return String(flags['template-version'] || fallback || 'v1').trim() || 'v1';
|
|
1102
826
|
}
|
|
1103
827
|
|
|
1104
|
-
function
|
|
1105
|
-
|
|
1106
|
-
return `oomi-${host}-${randomUUID().slice(0, 8)}`;
|
|
828
|
+
function resolvePersonaEntryUrl(flags = {}) {
|
|
829
|
+
return String(flags['entry-url'] || '').trim();
|
|
1107
830
|
}
|
|
1108
831
|
|
|
1109
|
-
function
|
|
1110
|
-
const
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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';
|
|
1115
839
|
}
|
|
1116
840
|
|
|
1117
|
-
function
|
|
1118
|
-
const statePath = resolveBridgeStatePath();
|
|
1119
|
-
if (!fs.existsSync(statePath)) return {};
|
|
841
|
+
async function findExistingManagedPersona(client, slug) {
|
|
1120
842
|
try {
|
|
1121
|
-
return
|
|
1122
|
-
} catch {
|
|
1123
|
-
|
|
843
|
+
return await client.getPersona({ slug });
|
|
844
|
+
} catch (error) {
|
|
845
|
+
if (isHttpErrorStatus(error, 404)) {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
throw error;
|
|
1124
849
|
}
|
|
1125
850
|
}
|
|
1126
851
|
|
|
1127
|
-
function
|
|
1128
|
-
const
|
|
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 {
|
|
1482
|
+
return {};
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function writeBridgeState(nextState) {
|
|
1487
|
+
const statePath = resolveBridgeStatePath();
|
|
1129
1488
|
ensureDir(path.dirname(statePath));
|
|
1130
1489
|
writeFile(statePath, JSON.stringify(nextState, null, 2) + '\n');
|
|
1131
1490
|
}
|
|
@@ -1714,499 +2073,847 @@ function prepareGatewayFrameForLocalGateway(frameText, gatewayAuth, options = {}
|
|
|
1714
2073
|
}
|
|
1715
2074
|
}
|
|
1716
2075
|
|
|
1717
|
-
function parseJsonPayload(raw) {
|
|
1718
|
-
try {
|
|
1719
|
-
return JSON.parse(raw);
|
|
1720
|
-
} catch {
|
|
1721
|
-
return null;
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
function extractTextFromGatewayMessage(message) {
|
|
1726
|
-
if (!message || typeof message !== 'object') return '';
|
|
1727
|
-
|
|
1728
|
-
if (typeof message.content === 'string' && message.content.trim()) {
|
|
1729
|
-
return message.content.trim();
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
if (!Array.isArray(message.content)) return '';
|
|
1733
|
-
|
|
1734
|
-
return message.content
|
|
1735
|
-
.filter((block) => block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
|
|
1736
|
-
.map((block) => block.text.trim())
|
|
1737
|
-
.filter(Boolean)
|
|
1738
|
-
.join(' ');
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
function summarizeVoiceFrameContract(frameText) {
|
|
1742
|
-
const frame = parseJsonPayload(frameText);
|
|
1743
|
-
if (!frame || typeof frame !== 'object') {
|
|
1744
|
-
return { parseable: false };
|
|
1745
|
-
}
|
|
1746
|
-
const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : {};
|
|
1747
|
-
const message = payload.message && typeof payload.message === 'object' ? payload.message : {};
|
|
1748
|
-
const metadata = message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
|
|
1749
|
-
const spokenRaw = Object.prototype.hasOwnProperty.call(metadata, 'spoken') ? metadata.spoken : undefined;
|
|
1750
|
-
const spokenNormalized = normalizeSpokenMetadata(spokenRaw);
|
|
1751
|
-
const text = extractTextFromGatewayMessage(message);
|
|
1752
|
-
return {
|
|
1753
|
-
parseable: true,
|
|
1754
|
-
event: typeof frame.event === 'string' ? frame.event : '',
|
|
1755
|
-
state: typeof payload.state === 'string' ? payload.state : '',
|
|
1756
|
-
role: typeof message.role === 'string' ? message.role : '',
|
|
1757
|
-
contentLength: text.length,
|
|
1758
|
-
hasMetadata: Object.keys(metadata).length > 0,
|
|
1759
|
-
hasSpokenKey: Object.prototype.hasOwnProperty.call(metadata, 'spoken'),
|
|
1760
|
-
spokenRawType: spokenRaw === undefined ? 'missing' : Array.isArray(spokenRaw) ? 'array' : typeof spokenRaw,
|
|
1761
|
-
spokenNormalized: Boolean(spokenNormalized),
|
|
1762
|
-
spokenSegmentCount: Array.isArray(spokenNormalized?.segments) ? spokenNormalized.segments.length : 0,
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
function ensureAssistantSpokenMetadata(frameText) {
|
|
1767
|
-
const frame = parseJsonPayload(frameText);
|
|
1768
|
-
if (!frame || typeof frame !== 'object') {
|
|
1769
|
-
return { frameText, changed: false, reason: '' };
|
|
1770
|
-
}
|
|
1771
|
-
if (frame.type !== 'event' || frame.event !== 'chat') {
|
|
1772
|
-
return { frameText, changed: false, reason: '' };
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : null;
|
|
1776
|
-
if (!payload || payload.state !== 'final') {
|
|
1777
|
-
return { frameText, changed: false, reason: '' };
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
const message = payload.message && typeof payload.message === 'object' ? payload.message : null;
|
|
1781
|
-
if (!message) {
|
|
1782
|
-
return { frameText, changed: false, reason: '' };
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
const messageRole = typeof message.role === 'string' ? message.role.trim() : '';
|
|
1786
|
-
if (messageRole && messageRole !== 'assistant') {
|
|
1787
|
-
return { frameText, changed: false, reason: '' };
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
const originalMetadata =
|
|
1791
|
-
message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata)
|
|
1792
|
-
? message.metadata
|
|
1793
|
-
: {};
|
|
1794
|
-
const metadata = { ...originalMetadata };
|
|
1795
|
-
const normalizedExplicitSpoken = normalizeSpokenMetadata(originalMetadata.spoken);
|
|
1796
|
-
const spoken =
|
|
1797
|
-
normalizedExplicitSpoken ||
|
|
1798
|
-
inferSpokenMetadataFromContent(extractTextFromGatewayMessage(message));
|
|
1799
|
-
if (!spoken) {
|
|
1800
|
-
return { frameText, changed: false, reason: '' };
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
metadata.spoken = spoken;
|
|
1804
|
-
const nextFrame = JSON.stringify({
|
|
1805
|
-
...frame,
|
|
1806
|
-
payload: {
|
|
1807
|
-
...payload,
|
|
1808
|
-
message: {
|
|
1809
|
-
...message,
|
|
1810
|
-
metadata,
|
|
1811
|
-
},
|
|
1812
|
-
},
|
|
1813
|
-
});
|
|
1814
|
-
|
|
1815
|
-
return {
|
|
1816
|
-
frameText: nextFrame,
|
|
1817
|
-
changed: nextFrame !== frameText,
|
|
1818
|
-
reason: normalizedExplicitSpoken ? 'normalized' : (messageRole ? 'synthesized' : 'synthesized_missing_role'),
|
|
1819
|
-
};
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
function normalizeAssistantGatewayFrame(sessionId, frameText) {
|
|
1823
|
-
const scope = classifyBridgeSessionScope(sessionId);
|
|
1824
|
-
const summary = summarizeVoiceFrameContract(frameText);
|
|
1825
|
-
if (!summary.parseable || summary.event !== 'chat' || summary.state !== 'final') {
|
|
1826
|
-
return {
|
|
1827
|
-
frameText,
|
|
1828
|
-
changed: false,
|
|
1829
|
-
reason: '',
|
|
1830
|
-
scope,
|
|
1831
|
-
summary,
|
|
1832
|
-
};
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
const normalized = ensureAssistantSpokenMetadata(frameText);
|
|
1836
|
-
return {
|
|
1837
|
-
...normalized,
|
|
1838
|
-
scope,
|
|
1839
|
-
summary,
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
function buildAssistantFinalDebugFrame({ sessionKey, text, role }) {
|
|
1844
|
-
const trimmedSessionKey =
|
|
1845
|
-
typeof sessionKey === 'string' && sessionKey.trim()
|
|
1846
|
-
? sessionKey.trim()
|
|
1847
|
-
: 'agent:main:webchat:channel:oomi';
|
|
1848
|
-
const message = {
|
|
1849
|
-
content: String(text || ''),
|
|
1850
|
-
};
|
|
1851
|
-
if (typeof role === 'string' && role.trim()) {
|
|
1852
|
-
message.role = role.trim();
|
|
1853
|
-
}
|
|
1854
|
-
return JSON.stringify({
|
|
1855
|
-
type: 'event',
|
|
1856
|
-
event: 'chat',
|
|
1857
|
-
payload: {
|
|
1858
|
-
sessionKey: trimmedSessionKey,
|
|
1859
|
-
state: 'final',
|
|
1860
|
-
message,
|
|
1861
|
-
},
|
|
1862
|
-
});
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
function extractSpokenMetadata(frameText) {
|
|
1866
|
-
const payload = parseJsonPayload(frameText);
|
|
1867
|
-
const message =
|
|
1868
|
-
payload &&
|
|
1869
|
-
payload.payload &&
|
|
1870
|
-
typeof payload.payload === 'object' &&
|
|
1871
|
-
payload.payload.message &&
|
|
1872
|
-
typeof payload.payload.message === 'object'
|
|
1873
|
-
? payload.payload.message
|
|
1874
|
-
: null;
|
|
1875
|
-
const metadata =
|
|
1876
|
-
message &&
|
|
1877
|
-
message.metadata &&
|
|
1878
|
-
typeof message.metadata === 'object' &&
|
|
1879
|
-
!Array.isArray(message.metadata)
|
|
1880
|
-
? message.metadata
|
|
1881
|
-
: {};
|
|
1882
|
-
return normalizeSpokenMetadata(metadata.spoken);
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
function runAssistantFinalDebugCheck(options = {}) {
|
|
1886
|
-
const sessionId =
|
|
1887
|
-
typeof options.sessionId === 'string' && options.sessionId.trim()
|
|
1888
|
-
? options.sessionId.trim()
|
|
1889
|
-
: 'ms_debug_local';
|
|
1890
|
-
const sessionKey =
|
|
1891
|
-
typeof options.sessionKey === 'string' && options.sessionKey.trim()
|
|
1892
|
-
? options.sessionKey.trim()
|
|
1893
|
-
: 'agent:main:webchat:channel:oomi';
|
|
1894
|
-
const role =
|
|
1895
|
-
options.omitRole
|
|
1896
|
-
? ''
|
|
1897
|
-
: (typeof options.role === 'string' && options.role.trim() ? options.role.trim() : 'assistant');
|
|
1898
|
-
|
|
1899
|
-
const rawFrameText =
|
|
1900
|
-
typeof options.frameText === 'string' && options.frameText.trim()
|
|
1901
|
-
? options.frameText
|
|
1902
|
-
: buildAssistantFinalDebugFrame({
|
|
1903
|
-
sessionKey,
|
|
1904
|
-
text: options.text,
|
|
1905
|
-
role,
|
|
1906
|
-
});
|
|
1907
|
-
|
|
1908
|
-
const before = summarizeVoiceFrameContract(rawFrameText);
|
|
1909
|
-
const normalized = normalizeAssistantGatewayFrame(sessionId, rawFrameText);
|
|
1910
|
-
const after = summarizeVoiceFrameContract(normalized.frameText);
|
|
1911
|
-
const spoken = extractSpokenMetadata(normalized.frameText);
|
|
1912
|
-
|
|
1913
|
-
return {
|
|
1914
|
-
sessionId,
|
|
1915
|
-
sessionKey,
|
|
1916
|
-
scope: normalized.scope,
|
|
1917
|
-
changed: normalized.changed,
|
|
1918
|
-
reason: normalized.reason,
|
|
1919
|
-
before,
|
|
1920
|
-
after,
|
|
1921
|
-
spoken,
|
|
1922
|
-
frameText: normalized.frameText,
|
|
1923
|
-
};
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
function printAssistantFinalDebugResult(result, asJson) {
|
|
1927
|
-
if (asJson) {
|
|
1928
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1929
|
-
return;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
console.log(`Session id: ${result.sessionId}`);
|
|
1933
|
-
console.log(`Session key: ${result.sessionKey}`);
|
|
1934
|
-
console.log(`Scope: ${result.scope}`);
|
|
1935
|
-
console.log(`Changed: ${result.changed ? 'yes' : 'no'}${result.reason ? ` (${result.reason})` : ''}`);
|
|
1936
|
-
console.log(
|
|
1937
|
-
`Before: event=${result.before.event || '<none>'} state=${result.before.state || '<none>'} role=${result.before.role || '<none>'} spoken=${result.before.spokenNormalized ? 'yes' : 'no'}`
|
|
1938
|
-
);
|
|
1939
|
-
console.log(
|
|
1940
|
-
`After: event=${result.after.event || '<none>'} state=${result.after.state || '<none>'} role=${result.after.role || '<none>'} spoken=${result.after.spokenNormalized ? 'yes' : 'no'}`
|
|
1941
|
-
);
|
|
1942
|
-
if (result.spoken) {
|
|
1943
|
-
console.log(`Spoken text: ${result.spoken.text}`);
|
|
1944
|
-
console.log(`Segments: ${Array.isArray(result.spoken.segments) ? result.spoken.segments.length : 0}`);
|
|
1945
|
-
if (typeof result.spoken.instructions === 'string' && result.spoken.instructions.trim()) {
|
|
1946
|
-
console.log(`Instructions: ${result.spoken.instructions}`);
|
|
1947
|
-
}
|
|
1948
|
-
} else {
|
|
1949
|
-
console.log('Spoken text: <missing>');
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
function resolveCommandFromPath(commandName) {
|
|
1954
|
-
const normalized = String(commandName || '').trim();
|
|
1955
|
-
if (!normalized) return '';
|
|
1956
|
-
try {
|
|
1957
|
-
const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', [normalized], {
|
|
1958
|
-
encoding: 'utf8',
|
|
1959
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1960
|
-
});
|
|
1961
|
-
if (probe.status !== 0) return '';
|
|
1962
|
-
const firstLine = String(probe.stdout || '')
|
|
1963
|
-
.split(/\r?\n/)
|
|
1964
|
-
.map((line) => line.trim())
|
|
1965
|
-
.find(Boolean);
|
|
1966
|
-
return firstLine || '';
|
|
1967
|
-
} catch {
|
|
1968
|
-
return '';
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
function resolveExecutable(candidates = []) {
|
|
1973
|
-
for (const candidate of candidates) {
|
|
1974
|
-
if (!candidate) continue;
|
|
1975
|
-
const value = String(candidate).trim();
|
|
1976
|
-
if (!value) continue;
|
|
1977
|
-
if (path.isAbsolute(value) && fs.existsSync(value)) {
|
|
1978
|
-
return value;
|
|
1979
|
-
}
|
|
1980
|
-
if (value.includes(path.sep) || value.includes('/')) {
|
|
1981
|
-
const resolved = path.resolve(value);
|
|
1982
|
-
if (fs.existsSync(resolved)) {
|
|
1983
|
-
return resolved;
|
|
1984
|
-
}
|
|
1985
|
-
continue;
|
|
1986
|
-
}
|
|
1987
|
-
const fromPath = resolveCommandFromPath(value);
|
|
1988
|
-
if (fromPath) {
|
|
1989
|
-
return fromPath;
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
return '';
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
function resolveBackendRoot(rootFlag) {
|
|
1996
|
-
const repoRoot = resolveRepoRoot(rootFlag);
|
|
1997
|
-
const backendRoot = path.join(repoRoot, 'apps', 'backend');
|
|
1998
|
-
if (!fs.existsSync(backendRoot)) {
|
|
1999
|
-
throw new Error(`Could not locate backend app at ${backendRoot}`);
|
|
2000
|
-
}
|
|
2001
|
-
return backendRoot;
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
function resolveRubyExecutable() {
|
|
2005
|
-
const candidates = [
|
|
2006
|
-
process.env.OOMI_RUBY_BIN,
|
|
2007
|
-
process.env.RUBY,
|
|
2008
|
-
process.platform === 'win32' ? 'ruby.exe' : 'ruby',
|
|
2009
|
-
process.platform === 'win32' ? 'ruby' : '',
|
|
2010
|
-
process.platform === 'win32' ? 'C:\\Ruby33-x64\\bin\\ruby.exe' : '',
|
|
2011
|
-
];
|
|
2012
|
-
const executable = resolveExecutable(candidates);
|
|
2013
|
-
if (!executable) {
|
|
2014
|
-
throw new Error('Ruby executable not found. Set OOMI_RUBY_BIN or install Ruby locally.');
|
|
2015
|
-
}
|
|
2016
|
-
return executable;
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
function resolveBundleExecutable() {
|
|
2020
|
-
const candidates = [
|
|
2021
|
-
process.env.OOMI_BUNDLE_BIN,
|
|
2022
|
-
process.platform === 'win32' ? 'bundle.bat' : 'bundle',
|
|
2023
|
-
'bundle',
|
|
2024
|
-
process.platform === 'win32' ? 'C:\\Ruby33-x64\\bin\\bundle.bat' : '',
|
|
2025
|
-
];
|
|
2026
|
-
const executable = resolveExecutable(candidates);
|
|
2027
|
-
if (!executable) {
|
|
2028
|
-
throw new Error('Bundler executable not found. Set OOMI_BUNDLE_BIN or install Bundler locally.');
|
|
2029
|
-
}
|
|
2030
|
-
return executable;
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
function shellQuote(value) {
|
|
2034
|
-
const text = String(value);
|
|
2035
|
-
if (process.platform === 'win32') {
|
|
2036
|
-
return `"${text.replace(/"/g, '""')}"`;
|
|
2037
|
-
}
|
|
2038
|
-
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
async function runBundledRubyScript({ backendRoot, scriptPath, inputFile, env = undefined }) {
|
|
2042
|
-
const rubyExecutable = resolveRubyExecutable();
|
|
2043
|
-
const bundleExecutable = resolveBundleExecutable();
|
|
2044
|
-
const commandText = process.platform === 'win32'
|
|
2045
|
-
? [bundleExecutable, 'exec', rubyExecutable, scriptPath, '--input-file', inputFile].map(shellQuote).join(' ')
|
|
2046
|
-
: '';
|
|
2047
|
-
const childEnv = env ? { ...process.env, ...env } : process.env;
|
|
2048
|
-
|
|
2049
|
-
return await new Promise((resolve, reject) => {
|
|
2050
|
-
const child = process.platform === 'win32'
|
|
2051
|
-
? spawn(commandText, [], {
|
|
2052
|
-
cwd: backendRoot,
|
|
2053
|
-
shell: true,
|
|
2054
|
-
env: childEnv,
|
|
2055
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2056
|
-
})
|
|
2057
|
-
: spawn(bundleExecutable, ['exec', rubyExecutable, scriptPath, '--input-file', inputFile], {
|
|
2058
|
-
cwd: backendRoot,
|
|
2059
|
-
env: childEnv,
|
|
2060
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2061
|
-
});
|
|
2062
|
-
|
|
2063
|
-
let stdout = '';
|
|
2064
|
-
let stderr = '';
|
|
2065
|
-
child.stdout.on('data', (chunk) => {
|
|
2066
|
-
stdout += chunk.toString();
|
|
2067
|
-
});
|
|
2068
|
-
child.stderr.on('data', (chunk) => {
|
|
2069
|
-
stderr += chunk.toString();
|
|
2070
|
-
});
|
|
2071
|
-
child.on('error', reject);
|
|
2072
|
-
child.on('close', (code) => {
|
|
2073
|
-
resolve({ code: Number(code || 0), stdout, stderr });
|
|
2074
|
-
});
|
|
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,
|
|
2075
2825
|
});
|
|
2076
2826
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
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
|
-
const frameText =
|
|
2168
|
-
frameFile
|
|
2169
|
-
? readFile(frameFile)
|
|
2170
|
-
: (typeof flags['frame-json'] === 'string' && flags['frame-json'].trim() ? flags['frame-json'] : '');
|
|
2171
|
-
const text = typeof flags.text === 'string' ? flags.text : '';
|
|
2172
|
-
|
|
2173
|
-
if (!frameText && !text.trim()) {
|
|
2174
|
-
throw new Error(
|
|
2175
|
-
'Assistant text or frame input is required. Usage: oomi openclaw debug assistant-final --text "<assistant text>"'
|
|
2176
|
-
);
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
const debugOptions = {
|
|
2180
|
-
sessionId: flags['session-id'],
|
|
2181
|
-
sessionKey: flags['session-key'],
|
|
2182
|
-
role: flags.role,
|
|
2183
|
-
omitRole: isTruthyFlag(flags['omit-role']),
|
|
2184
|
-
text,
|
|
2185
|
-
frameText,
|
|
2186
|
-
root: flags.root,
|
|
2187
|
-
userText: flags['user-text'],
|
|
2188
|
-
liveProvider: isTruthyFlag(flags['live-provider']),
|
|
2189
|
-
envFile: flags['env-file'],
|
|
2190
|
-
providerTimeoutMs: flags['provider-timeout-ms'],
|
|
2191
|
-
};
|
|
2192
|
-
|
|
2193
|
-
if (normalizedAction === 'assistant-final') {
|
|
2194
|
-
const result = runAssistantFinalDebugCheck(debugOptions);
|
|
2195
|
-
printAssistantFinalDebugResult(result, isTruthyFlag(flags.json));
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
if (normalizedAction === 'tts-pipeline') {
|
|
2200
|
-
const result = await runLocalTtsPipelineDebugCheck(debugOptions);
|
|
2201
|
-
printTtsPipelineDebugResult(result, isTruthyFlag(flags.json));
|
|
2202
|
-
if (!result.backend?.success) {
|
|
2203
|
-
throw new Error(result.backend?.qwen?.errorCode || 'Local backend TTS replay failed.');
|
|
2204
|
-
}
|
|
2205
|
-
return;
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
throw new Error('Unknown debug action: ' + normalizedAction + '. Use: oomi openclaw debug assistant-final|tts-pipeline');
|
|
2209
|
-
}
|
|
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
|
+
}
|
|
2210
2917
|
|
|
2211
2918
|
function extractCorrelationId(params) {
|
|
2212
2919
|
if (!params || typeof params !== 'object') return '';
|
|
@@ -2385,11 +3092,11 @@ async function runBridgePreflight({ brokerWs, gatewayUrl, gatewayConfigPath }) {
|
|
|
2385
3092
|
await assertTcpReachable(parsedGatewayUrl.toString());
|
|
2386
3093
|
}
|
|
2387
3094
|
|
|
2388
|
-
function buildBridgeDetachArgs(rawFlags = {}) {
|
|
2389
|
-
const orderedKeys = [
|
|
2390
|
-
'broker-http',
|
|
2391
|
-
'broker-ws',
|
|
2392
|
-
'pair-code',
|
|
3095
|
+
function buildBridgeDetachArgs(rawFlags = {}) {
|
|
3096
|
+
const orderedKeys = [
|
|
3097
|
+
'broker-http',
|
|
3098
|
+
'broker-ws',
|
|
3099
|
+
'pair-code',
|
|
2393
3100
|
'app-url',
|
|
2394
3101
|
'device-id',
|
|
2395
3102
|
'device-token',
|
|
@@ -2407,13 +3114,13 @@ function buildBridgeDetachArgs(rawFlags = {}) {
|
|
|
2407
3114
|
if (!text) continue;
|
|
2408
3115
|
args.push(`--${key}`, text);
|
|
2409
3116
|
}
|
|
2410
|
-
|
|
2411
|
-
return args;
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
function isServiceManagedBridgeStart(flags = {}) {
|
|
2415
|
-
return isTruthyFlag(flags['service-managed']);
|
|
2416
|
-
}
|
|
3117
|
+
|
|
3118
|
+
return args;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
function isServiceManagedBridgeStart(flags = {}) {
|
|
3122
|
+
return isTruthyFlag(flags['service-managed']);
|
|
3123
|
+
}
|
|
2417
3124
|
|
|
2418
3125
|
function startBridgeDetachedProcess(rawFlags = {}) {
|
|
2419
3126
|
const existing = findRunningBridgeProcess();
|
|
@@ -2584,17 +3291,31 @@ function runLaunchctl(args, { allowFailure = false } = {}) {
|
|
|
2584
3291
|
return { status, stdout, stderr };
|
|
2585
3292
|
}
|
|
2586
3293
|
|
|
2587
|
-
function buildBridgeLaunchAgentPlist() {
|
|
2588
|
-
const scriptPath = (() => {
|
|
2589
|
-
try {
|
|
2590
|
-
return fs.realpathSync(process.argv[1]);
|
|
2591
|
-
} catch {
|
|
2592
|
-
return process.argv[1];
|
|
2593
|
-
}
|
|
2594
|
-
})();
|
|
2595
|
-
const programArgs = [process.execPath, scriptPath, 'openclaw', 'bridge', 'start', '--service-managed'];
|
|
2596
|
-
const bridgeLogPath = resolveBridgeLiveLogPath();
|
|
2597
|
-
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');
|
|
2598
3319
|
|
|
2599
3320
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2600
3321
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -2607,7 +3328,7 @@ function buildBridgeLaunchAgentPlist() {
|
|
|
2607
3328
|
${argsXml}
|
|
2608
3329
|
</array>
|
|
2609
3330
|
<key>WorkingDirectory</key>
|
|
2610
|
-
<string>${xmlEscape(
|
|
3331
|
+
<string>${xmlEscape(openclawHome)}</string>
|
|
2611
3332
|
<key>RunAtLoad</key>
|
|
2612
3333
|
<true/>
|
|
2613
3334
|
<key>KeepAlive</key>
|
|
@@ -2616,8 +3337,7 @@ function buildBridgeLaunchAgentPlist() {
|
|
|
2616
3337
|
<integer>5</integer>
|
|
2617
3338
|
<key>EnvironmentVariables</key>
|
|
2618
3339
|
<dict>
|
|
2619
|
-
|
|
2620
|
-
<string>1</string>
|
|
3340
|
+
${envXml}
|
|
2621
3341
|
</dict>
|
|
2622
3342
|
<key>StandardOutPath</key>
|
|
2623
3343
|
<string>${xmlEscape(bridgeLogPath)}</string>
|
|
@@ -2650,18 +3370,18 @@ function readBridgeLaunchdStatus() {
|
|
|
2650
3370
|
};
|
|
2651
3371
|
}
|
|
2652
3372
|
|
|
2653
|
-
function startBridgeLaunchdService() {
|
|
2654
|
-
assertMacOSLaunchdAvailable();
|
|
2655
|
-
const plistPath = resolveBridgeLaunchAgentPlistPath();
|
|
2656
|
-
if (!fs.existsSync(plistPath)) {
|
|
2657
|
-
throw new Error('Bridge service is not installed. Run: oomi openclaw bridge service install');
|
|
2658
|
-
}
|
|
2659
|
-
writeFile(plistPath, buildBridgeLaunchAgentPlist());
|
|
2660
|
-
const domain = launchctlDomain();
|
|
2661
|
-
const target = launchctlServiceTarget();
|
|
2662
|
-
runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
|
|
2663
|
-
runLaunchctl(['bootstrap', domain, plistPath]);
|
|
2664
|
-
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 });
|
|
2665
3385
|
runLaunchctl(['kickstart', '-k', target], { allowFailure: true });
|
|
2666
3386
|
}
|
|
2667
3387
|
|
|
@@ -2820,22 +3540,22 @@ async function startOpenclawBridge(flags) {
|
|
|
2820
3540
|
console.log(`Broker WS: ${brokerWs}`);
|
|
2821
3541
|
|
|
2822
3542
|
const activeGatewaySockets = new Map();
|
|
2823
|
-
const reconnectState = {
|
|
2824
|
-
attempt: 0,
|
|
2825
|
-
timer: null,
|
|
2826
|
-
stopped: false,
|
|
2827
|
-
lastFailure: null,
|
|
2828
|
-
};
|
|
2829
|
-
const personaJobPollEnabled = !isTruthyFlag(process.env.OOMI_DISABLE_PERSONA_JOB_POLL);
|
|
2830
|
-
const personaJobPollIntervalMs = parsePositiveInteger(
|
|
2831
|
-
process.env.OOMI_PERSONA_JOB_POLL_INTERVAL_MS,
|
|
2832
|
-
3000,
|
|
2833
|
-
);
|
|
2834
|
-
const personaJobIdlePollIntervalMs = parsePositiveInteger(
|
|
2835
|
-
process.env.OOMI_PERSONA_JOB_IDLE_POLL_INTERVAL_MS,
|
|
2836
|
-
3000,
|
|
2837
|
-
);
|
|
2838
|
-
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();
|
|
2839
3559
|
const brokerPath = (() => {
|
|
2840
3560
|
try {
|
|
2841
3561
|
return new URL(brokerWs).pathname || '';
|
|
@@ -2900,33 +3620,38 @@ async function startOpenclawBridge(flags) {
|
|
|
2900
3620
|
onReport: ({ phase, status, error }) => {
|
|
2901
3621
|
reportBridgeProcessFault({ phase, status, error });
|
|
2902
3622
|
},
|
|
2903
|
-
onExit: (code) => {
|
|
2904
|
-
reconnectState.stopped = true;
|
|
2905
|
-
if (reconnectState.timer) {
|
|
2906
|
-
clearTimeout(reconnectState.timer);
|
|
2907
|
-
reconnectState.timer = null;
|
|
2908
|
-
}
|
|
2909
|
-
personaJobPoller?.stop();
|
|
2910
|
-
releaseBridgeLock();
|
|
2911
|
-
process.exit(code);
|
|
2912
|
-
},
|
|
2913
|
-
});
|
|
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
|
+
});
|
|
2914
3634
|
|
|
2915
3635
|
const uncaughtExceptionHandler = (error) => {
|
|
2916
3636
|
handleBridgeProcessFault({ phase: 'process.uncaughtException', error });
|
|
2917
3637
|
};
|
|
2918
3638
|
|
|
2919
|
-
const unhandledRejectionHandler = (reason) => {
|
|
2920
|
-
handleBridgeProcessFault({ phase: 'process.unhandledRejection', error: reason });
|
|
2921
|
-
};
|
|
2922
|
-
|
|
2923
|
-
process.on('uncaughtException', uncaughtExceptionHandler);
|
|
2924
|
-
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
|
+
: '';
|
|
2925
3650
|
|
|
2926
3651
|
const personaJobPoller =
|
|
2927
|
-
personaJobPollEnabled &&
|
|
3652
|
+
personaJobPollEnabled && personaBackendUrl && deviceToken
|
|
2928
3653
|
? startPersonaJobPoller({
|
|
2929
|
-
backendUrl:
|
|
3654
|
+
backendUrl: personaBackendUrl,
|
|
2930
3655
|
deviceToken,
|
|
2931
3656
|
pollIntervalMs: personaJobPollIntervalMs,
|
|
2932
3657
|
idleIntervalMs: personaJobIdlePollIntervalMs,
|
|
@@ -2934,38 +3659,49 @@ async function startOpenclawBridge(flags) {
|
|
|
2934
3659
|
onMessage: async (message) => {
|
|
2935
3660
|
const result = await runManagedPersonaJobExecution({
|
|
2936
3661
|
message,
|
|
2937
|
-
backendUrl:
|
|
3662
|
+
backendUrl: personaBackendUrl,
|
|
2938
3663
|
deviceToken,
|
|
2939
3664
|
deviceId,
|
|
2940
3665
|
workspaceRoot: personaWorkspaceRoot,
|
|
2941
3666
|
shouldInstall: true,
|
|
2942
3667
|
shouldStart: true,
|
|
2943
|
-
shouldRegister: true,
|
|
2944
|
-
});
|
|
2945
|
-
|
|
2946
|
-
if (result && result.ok) {
|
|
2947
|
-
console.log(
|
|
2948
|
-
`[persona-jobs] completed ${result.jobId} on port ${result.result?.localPort || 'unknown'}`
|
|
2949
|
-
);
|
|
2950
|
-
return;
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
if (result) {
|
|
2954
|
-
console.warn(
|
|
2955
|
-
`[persona-jobs] job ${result.jobId} completed with failure: ${result.error?.message || 'unknown error'}`
|
|
2956
|
-
);
|
|
2957
|
-
}
|
|
2958
|
-
},
|
|
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,
|
|
2959
3695
|
})
|
|
2960
3696
|
: null;
|
|
2961
3697
|
|
|
2962
|
-
if (personaJobPollEnabled &&
|
|
2963
|
-
|
|
3698
|
+
if (personaJobPollEnabled && personaBackendUrl && deviceToken) {
|
|
3699
|
+
bridgeDebugLog('[persona-jobs] polling filtered control queue for persona_job messages.');
|
|
2964
3700
|
} else if (personaJobPollEnabled) {
|
|
2965
3701
|
console.warn('[persona-jobs] disabled because broker HTTP URL or device token is unavailable.');
|
|
2966
3702
|
}
|
|
2967
|
-
|
|
2968
|
-
const sendBrokerPayload = (brokerSocket, payload) => {
|
|
3703
|
+
|
|
3704
|
+
const sendBrokerPayload = (brokerSocket, payload) => {
|
|
2969
3705
|
if (brokerSocket.readyState !== WebSocket.OPEN) return;
|
|
2970
3706
|
if (!actionCableMode) {
|
|
2971
3707
|
brokerSocket.send(JSON.stringify(payload));
|
|
@@ -3195,7 +3931,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3195
3931
|
classifyBridgeFailure({ reason: 'connection closed without classified error' });
|
|
3196
3932
|
const delayMs = computeReconnectDelayMs(reconnectState.attempt, failure.baseDelayMs);
|
|
3197
3933
|
|
|
3198
|
-
|
|
3934
|
+
bridgeDebugWarn(
|
|
3199
3935
|
`[bridge] reconnect scheduled in ${delayMs}ms (attempt ${reconnectState.attempt}, class=${failure.failureClass}, code=${failure.errorCode})`
|
|
3200
3936
|
);
|
|
3201
3937
|
|
|
@@ -3323,7 +4059,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3323
4059
|
}
|
|
3324
4060
|
const result = forwardFrameToSession(sessionBridge, prepared.frameText);
|
|
3325
4061
|
if (result === 'queued') {
|
|
3326
|
-
|
|
4062
|
+
bridgeDebugLog(`[bridge] client.frame queued after challenge ${sessionId}`);
|
|
3327
4063
|
if (requestMeta) {
|
|
3328
4064
|
startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
|
|
3329
4065
|
}
|
|
@@ -3337,7 +4073,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3337
4073
|
});
|
|
3338
4074
|
}
|
|
3339
4075
|
} else if (result === 'dropped') {
|
|
3340
|
-
|
|
4076
|
+
bridgeDebugLog(`[bridge] client.frame dropped after challenge ${sessionId}`);
|
|
3341
4077
|
incrementBridgeMetric('bridge_drop_count');
|
|
3342
4078
|
if (requestMeta) {
|
|
3343
4079
|
const pending = sessionBridge.pendingRequests instanceof Map
|
|
@@ -3369,7 +4105,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3369
4105
|
});
|
|
3370
4106
|
}
|
|
3371
4107
|
} else {
|
|
3372
|
-
|
|
4108
|
+
bridgeDebugLog(`[bridge] client.frame sent after challenge ${sessionId}`);
|
|
3373
4109
|
if (requestMeta) {
|
|
3374
4110
|
startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
|
|
3375
4111
|
}
|
|
@@ -3435,27 +4171,27 @@ async function startOpenclawBridge(flags) {
|
|
|
3435
4171
|
clearTimeout(connectTimeout);
|
|
3436
4172
|
connectTimeout = null;
|
|
3437
4173
|
}
|
|
3438
|
-
|
|
4174
|
+
bridgeDebugLog(`[bridge] gateway.open ${sessionId}`);
|
|
3439
4175
|
flushSessionQueue(sessionBridge);
|
|
3440
4176
|
});
|
|
3441
4177
|
|
|
3442
|
-
gatewaySocket.on('message', runBridgeCallbackSafely((gatewayRaw) => {
|
|
3443
|
-
let frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
3444
|
-
const spokenNormalized = normalizeAssistantGatewayFrame(sessionId, frame);
|
|
3445
|
-
if (spokenNormalized.changed) {
|
|
3446
|
-
frame = spokenNormalized.frameText;
|
|
3447
|
-
if (spokenNormalized.scope === 'voice') {
|
|
3448
|
-
|
|
3449
|
-
before: spokenNormalized.summary,
|
|
3450
|
-
after: summarizeVoiceFrameContract(frame),
|
|
3451
|
-
})}`);
|
|
3452
|
-
}
|
|
3453
|
-
} else if (spokenNormalized.scope === 'voice' && spokenNormalized.summary.event === 'chat' && spokenNormalized.summary.state === 'final') {
|
|
3454
|
-
|
|
3455
|
-
}
|
|
3456
|
-
const gatewayPayload = parseJsonPayload(frame);
|
|
3457
|
-
if (gatewayPayload?.event === 'connect.challenge') {
|
|
3458
|
-
|
|
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}`);
|
|
3459
4195
|
const nonce =
|
|
3460
4196
|
gatewayPayload.payload && typeof gatewayPayload.payload.nonce === 'string'
|
|
3461
4197
|
? gatewayPayload.payload.nonce.trim()
|
|
@@ -3601,7 +4337,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3601
4337
|
clearChallengeTimer(sessionBridge);
|
|
3602
4338
|
const reasonText = reason ? reason.toString() : '';
|
|
3603
4339
|
const closeMeta = classifyGatewayClose(code, reasonText);
|
|
3604
|
-
|
|
4340
|
+
bridgeDebugLog(
|
|
3605
4341
|
`[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
|
|
3606
4342
|
);
|
|
3607
4343
|
if (sessionBridge.pendingRequests instanceof Map) {
|
|
@@ -3678,7 +4414,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3678
4414
|
};
|
|
3679
4415
|
|
|
3680
4416
|
brokerSocket.on('open', () => {
|
|
3681
|
-
|
|
4417
|
+
bridgeDebugLog('[bridge] Connected to managed broker.');
|
|
3682
4418
|
reconnectState.attempt = 0;
|
|
3683
4419
|
reconnectState.lastFailure = null;
|
|
3684
4420
|
if (actionCableMode) {
|
|
@@ -3795,29 +4531,29 @@ async function startOpenclawBridge(flags) {
|
|
|
3795
4531
|
}
|
|
3796
4532
|
|
|
3797
4533
|
if (payload.type === 'device.ready') {
|
|
3798
|
-
|
|
4534
|
+
bridgeDebugLog(`[bridge] Broker ready for device ${payload.deviceId || deviceId}.`);
|
|
3799
4535
|
return;
|
|
3800
4536
|
}
|
|
3801
4537
|
|
|
3802
4538
|
if (payload.type === 'client.open') {
|
|
3803
4539
|
const sessionId = String(payload.sessionId || '').trim();
|
|
3804
4540
|
if (!sessionId) return;
|
|
3805
|
-
|
|
4541
|
+
bridgeDebugLog(`[bridge] client.open ${sessionId}`);
|
|
3806
4542
|
getOrCreateGatewaySession(sessionId);
|
|
3807
4543
|
return;
|
|
3808
4544
|
}
|
|
3809
4545
|
|
|
3810
|
-
if (payload.type === 'client.frame') {
|
|
3811
|
-
const sessionId = String(payload.sessionId || '').trim();
|
|
3812
|
-
const frame = typeof payload.frame === 'string' ? payload.frame : '';
|
|
3813
|
-
if (!sessionId || !frame) return;
|
|
3814
|
-
if (classifyBridgeSessionScope(sessionId) === 'voice') {
|
|
3815
|
-
|
|
3816
|
-
} else {
|
|
3817
|
-
|
|
3818
|
-
}
|
|
3819
|
-
const sessionBridge = getOrCreateGatewaySession(sessionId);
|
|
3820
|
-
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;
|
|
3821
4557
|
const requestMeta = extractGatewayRequestMeta(frame);
|
|
3822
4558
|
if (requestMeta) {
|
|
3823
4559
|
if (!(sessionBridge.pendingRequests instanceof Map)) {
|
|
@@ -3840,7 +4576,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3840
4576
|
});
|
|
3841
4577
|
if (prepared.waitForChallenge) {
|
|
3842
4578
|
queueConnectUntilChallenge(sessionId, sessionBridge, frame);
|
|
3843
|
-
|
|
4579
|
+
bridgeDebugLog(`[bridge] client.frame waiting for challenge ${sessionId}`);
|
|
3844
4580
|
if (requestMeta) {
|
|
3845
4581
|
sendGatewayAck(brokerSocket, {
|
|
3846
4582
|
sessionId,
|
|
@@ -3886,7 +4622,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3886
4622
|
requiresConnectAccepted: Boolean(requestMeta && requestMeta.method !== 'connect'),
|
|
3887
4623
|
});
|
|
3888
4624
|
if (result === 'waiting_for_connect') {
|
|
3889
|
-
|
|
4625
|
+
bridgeDebugLog(`[bridge] client.frame waiting for connect ${sessionId}`);
|
|
3890
4626
|
if (requestMeta) {
|
|
3891
4627
|
sendGatewayAck(brokerSocket, {
|
|
3892
4628
|
sessionId,
|
|
@@ -3899,7 +4635,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3899
4635
|
return;
|
|
3900
4636
|
}
|
|
3901
4637
|
if (result === 'queued') {
|
|
3902
|
-
|
|
4638
|
+
bridgeDebugLog(`[bridge] client.frame queued ${sessionId}`);
|
|
3903
4639
|
if (requestMeta) {
|
|
3904
4640
|
startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
|
|
3905
4641
|
}
|
|
@@ -3913,7 +4649,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3913
4649
|
});
|
|
3914
4650
|
}
|
|
3915
4651
|
} else if (result === 'dropped') {
|
|
3916
|
-
|
|
4652
|
+
bridgeDebugLog(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
|
|
3917
4653
|
incrementBridgeMetric('bridge_drop_count');
|
|
3918
4654
|
if (requestMeta) {
|
|
3919
4655
|
const pending = sessionBridge.pendingRequests instanceof Map
|
|
@@ -3959,7 +4695,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3959
4695
|
|
|
3960
4696
|
if (payload.type === 'client.close') {
|
|
3961
4697
|
const sessionId = String(payload.sessionId || '').trim();
|
|
3962
|
-
|
|
4698
|
+
bridgeDebugLog(`[bridge] client.close ${sessionId}`);
|
|
3963
4699
|
const sessionBridge = activeGatewaySockets.get(sessionId);
|
|
3964
4700
|
if (sessionBridge && sessionBridge.socket) {
|
|
3965
4701
|
clearChallengeTimer(sessionBridge);
|
|
@@ -3982,7 +4718,7 @@ async function startOpenclawBridge(flags) {
|
|
|
3982
4718
|
actionCableHeartbeat = null;
|
|
3983
4719
|
}
|
|
3984
4720
|
const reasonText = reason ? reason.toString() : '';
|
|
3985
|
-
|
|
4721
|
+
bridgeDebugLog(`[bridge] Broker disconnected (${code}) ${reasonText}`);
|
|
3986
4722
|
incrementBridgeMetric('bridge_disconnect_count');
|
|
3987
4723
|
for (const [sessionId, sessionBridge] of activeGatewaySockets.entries()) {
|
|
3988
4724
|
clearChallengeTimer(sessionBridge);
|
|
@@ -4024,16 +4760,17 @@ async function startOpenclawBridge(flags) {
|
|
|
4024
4760
|
}));
|
|
4025
4761
|
};
|
|
4026
4762
|
|
|
4027
|
-
const markStopped = (signal) => {
|
|
4028
|
-
reconnectState.stopped = true;
|
|
4763
|
+
const markStopped = (signal) => {
|
|
4764
|
+
reconnectState.stopped = true;
|
|
4029
4765
|
if (reconnectState.timer) {
|
|
4030
4766
|
clearTimeout(reconnectState.timer);
|
|
4031
4767
|
reconnectState.timer = null;
|
|
4032
4768
|
}
|
|
4033
4769
|
personaJobPoller?.stop();
|
|
4770
|
+
personaRuntimeSupervisor?.stop();
|
|
4034
4771
|
process.off('uncaughtException', uncaughtExceptionHandler);
|
|
4035
4772
|
process.off('unhandledRejection', unhandledRejectionHandler);
|
|
4036
|
-
updateBridgeStatus({
|
|
4773
|
+
updateBridgeStatus({
|
|
4037
4774
|
status: 'stopped',
|
|
4038
4775
|
deviceId,
|
|
4039
4776
|
brokerWs,
|
|
@@ -4434,17 +5171,17 @@ async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
|
|
|
4434
5171
|
);
|
|
4435
5172
|
}
|
|
4436
5173
|
|
|
4437
|
-
async function startBridgeLifecycle(flags = {}) {
|
|
4438
|
-
const serviceManaged = isServiceManagedBridgeStart(flags);
|
|
4439
|
-
if (serviceManaged && Boolean(flags.detach)) {
|
|
4440
|
-
throw new Error('Detached bridge mode cannot be combined with --service-managed.');
|
|
4441
|
-
}
|
|
4442
|
-
|
|
4443
|
-
if (Boolean(flags.detach)) {
|
|
4444
|
-
const detachedFlags = { ...flags };
|
|
4445
|
-
delete detachedFlags.detach;
|
|
4446
|
-
const result = startBridgeDetachedProcess(detachedFlags);
|
|
4447
|
-
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) {
|
|
4448
5185
|
incrementBridgeMetric('duplicate_start_attempt_count');
|
|
4449
5186
|
console.log(`Bridge already running (pid: ${result.pid}).`);
|
|
4450
5187
|
return;
|
|
@@ -4453,30 +5190,30 @@ async function startBridgeLifecycle(flags = {}) {
|
|
|
4453
5190
|
console.log(`Bridge started in background (pid: ${result.pid}).`);
|
|
4454
5191
|
return;
|
|
4455
5192
|
}
|
|
4456
|
-
|
|
4457
|
-
const running = findRunningBridgeProcess();
|
|
4458
|
-
if (running) {
|
|
4459
|
-
if (!serviceManaged) {
|
|
4460
|
-
incrementBridgeMetric('duplicate_start_attempt_count');
|
|
4461
|
-
console.log(
|
|
4462
|
-
`Bridge already running (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}.`
|
|
4463
|
-
);
|
|
4464
|
-
return;
|
|
4465
|
-
}
|
|
4466
|
-
|
|
4467
|
-
incrementBridgeMetric('bridge_restart_count');
|
|
4468
|
-
console.log(
|
|
4469
|
-
`Service-managed bridge start detected existing bridge (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}; reclaiming ownership.`
|
|
4470
|
-
);
|
|
4471
|
-
const result = await stopBridgeProcesses();
|
|
4472
|
-
if (Array.isArray(result.stillAlive) && result.stillAlive.length > 0) {
|
|
4473
|
-
throw new Error(`Failed to stop bridge processes: ${result.stillAlive.join(', ')}`);
|
|
4474
|
-
}
|
|
4475
|
-
}
|
|
4476
|
-
|
|
4477
|
-
incrementBridgeMetric('bridge_start_count');
|
|
4478
|
-
await startOpenclawBridge(flags);
|
|
4479
|
-
}
|
|
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
|
+
}
|
|
4480
5217
|
|
|
4481
5218
|
async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
|
|
4482
5219
|
const action = String(actionRaw || 'start').trim().toLowerCase();
|
|
@@ -4592,40 +5329,50 @@ async function main() {
|
|
|
4592
5329
|
return;
|
|
4593
5330
|
}
|
|
4594
5331
|
|
|
4595
|
-
if (command === 'openclaw' && subcommand === 'plugin') {
|
|
4596
|
-
printOpenclawPluginSetup(args.flags);
|
|
4597
|
-
return;
|
|
4598
|
-
}
|
|
4599
|
-
|
|
4600
|
-
if (command === 'openclaw' && subcommand === '
|
|
4601
|
-
await
|
|
4602
|
-
return;
|
|
4603
|
-
}
|
|
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
|
+
}
|
|
4604
5346
|
|
|
4605
5347
|
if (command === 'personas' && subcommand === 'sync') {
|
|
4606
5348
|
await syncPersonas({ backendUrl: args.flags['backend-url'], root: args.flags.root });
|
|
4607
5349
|
return;
|
|
4608
5350
|
}
|
|
4609
5351
|
|
|
4610
|
-
if (command === 'personas' && subcommand === 'create') {
|
|
5352
|
+
if (command === 'personas' && subcommand === 'create') {
|
|
4611
5353
|
const id = args.positionals[0];
|
|
4612
5354
|
if (!id) {
|
|
4613
5355
|
throw new Error('Persona id is required. Usage: oomi personas create <id>');
|
|
4614
5356
|
}
|
|
4615
5357
|
await createPersona({ id, root: args.flags.root, flags: args.flags });
|
|
4616
5358
|
return;
|
|
4617
|
-
}
|
|
4618
|
-
|
|
4619
|
-
if (command === 'personas' && subcommand === 'create-managed') {
|
|
4620
|
-
await handlePersonaCreateManagedCommand(args.flags, args.positionals[0]);
|
|
4621
|
-
return;
|
|
4622
|
-
}
|
|
4623
|
-
|
|
4624
|
-
if (command === 'personas' && subcommand === '
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
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
|
+
}
|
|
4629
5376
|
const result = scaffoldPersonaApp({
|
|
4630
5377
|
slug,
|
|
4631
5378
|
name: args.flags.name,
|
|
@@ -4634,70 +5381,97 @@ async function main() {
|
|
|
4634
5381
|
templateVersion: args.flags['template-version'],
|
|
4635
5382
|
force: isTruthyFlag(args.flags.force),
|
|
4636
5383
|
});
|
|
4637
|
-
printPersonaScaffoldResult(result, isTruthyFlag(args.flags.json));
|
|
4638
|
-
return;
|
|
4639
|
-
}
|
|
4640
|
-
|
|
4641
|
-
if (command === 'personas' && subcommand === 'runtime-register') {
|
|
4642
|
-
const slug = args.positionals[0];
|
|
4643
|
-
if (!slug) {
|
|
4644
|
-
throw new Error('Persona slug is required. Usage: oomi personas runtime-register <slug> --local-port 4789');
|
|
4645
|
-
}
|
|
4646
|
-
await handlePersonaRuntimeRegisterCommand(slug, args.flags);
|
|
4647
|
-
return;
|
|
4648
|
-
}
|
|
4649
|
-
|
|
4650
|
-
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') {
|
|
4651
5407
|
const slug = args.positionals[0];
|
|
4652
5408
|
if (!slug) {
|
|
4653
|
-
throw new Error('Persona slug is required. Usage: oomi personas
|
|
5409
|
+
throw new Error('Persona slug is required. Usage: oomi personas stop <slug>');
|
|
4654
5410
|
}
|
|
4655
|
-
await
|
|
5411
|
+
await handlePersonaStopCommand(slug, args.flags);
|
|
4656
5412
|
return;
|
|
4657
5413
|
}
|
|
4658
5414
|
|
|
4659
|
-
if (command === 'personas' && subcommand === '
|
|
5415
|
+
if (command === 'personas' && subcommand === 'delete') {
|
|
4660
5416
|
const slug = args.positionals[0];
|
|
4661
5417
|
if (!slug) {
|
|
4662
|
-
throw new Error('Persona slug is required. Usage: oomi personas
|
|
4663
|
-
}
|
|
4664
|
-
await handlePersonaRuntimeFailCommand(slug, args.flags);
|
|
4665
|
-
return;
|
|
4666
|
-
}
|
|
4667
|
-
|
|
4668
|
-
if (command === 'persona-jobs' && subcommand === 'start') {
|
|
4669
|
-
const jobId = args.positionals[0];
|
|
4670
|
-
if (!jobId) {
|
|
4671
|
-
throw new Error('Persona job id is required. Usage: oomi persona-jobs start <jobId>');
|
|
4672
|
-
}
|
|
4673
|
-
await handlePersonaJobStartCommand(jobId, args.flags);
|
|
4674
|
-
return;
|
|
4675
|
-
}
|
|
4676
|
-
|
|
4677
|
-
if (command === 'persona-jobs' && subcommand === 'succeed') {
|
|
4678
|
-
const jobId = args.positionals[0];
|
|
4679
|
-
if (!jobId) {
|
|
4680
|
-
throw new Error('Persona job id is required. Usage: oomi persona-jobs succeed <jobId> --workspace-path <path> --local-port 4789');
|
|
4681
|
-
}
|
|
4682
|
-
await handlePersonaJobSucceedCommand(jobId, args.flags);
|
|
4683
|
-
return;
|
|
4684
|
-
}
|
|
4685
|
-
|
|
4686
|
-
if (command === 'persona-jobs' && subcommand === 'fail') {
|
|
4687
|
-
const jobId = args.positionals[0];
|
|
4688
|
-
if (!jobId) {
|
|
4689
|
-
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>');
|
|
4690
5419
|
}
|
|
4691
|
-
await
|
|
4692
|
-
return;
|
|
4693
|
-
}
|
|
4694
|
-
|
|
4695
|
-
if (command === 'persona-jobs' && subcommand === 'execute') {
|
|
4696
|
-
await handlePersonaJobExecuteCommand(args.flags);
|
|
5420
|
+
await handlePersonaDeleteCommand(slug, args.flags);
|
|
4697
5421
|
return;
|
|
4698
5422
|
}
|
|
4699
5423
|
|
|
4700
|
-
|
|
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());
|
|
4701
5475
|
usage();
|
|
4702
5476
|
process.exit(1);
|
|
4703
5477
|
}
|
|
@@ -4713,23 +5487,25 @@ if (__isDirectExecution) {
|
|
|
4713
5487
|
});
|
|
4714
5488
|
}
|
|
4715
5489
|
|
|
4716
|
-
export {
|
|
4717
|
-
prepareGatewayFrameForLocalGateway,
|
|
4718
|
-
ensureAssistantSpokenMetadata,
|
|
4719
|
-
normalizeAssistantGatewayFrame,
|
|
4720
|
-
runAssistantFinalDebugCheck,
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
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
|
+
};
|