oomi-ai 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/agent_instructions.md +61 -5
- package/bin/oomi-ai.js +154 -242
- package/lib/bridgeService.js +173 -0
- package/lib/localAgent.js +64 -5
- package/lib/objectives.js +34 -0
- package/lib/personaApiClient.js +1 -1
- package/lib/routines.js +1 -1
- package/lib/spaces.js +5 -4
- package/package.json +5 -3
- package/skills/oomi/SKILL.md +81 -4
- package/skills/oomi/agent_instructions.md +74 -21
package/bin/oomi-ai.js
CHANGED
|
@@ -8,10 +8,12 @@ import net from 'net';
|
|
|
8
8
|
import { lookup as dnsLookup } from 'dns/promises';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import WebSocket from 'ws';
|
|
11
|
+
import { bridgeServiceConfig, createBridgeService, renderLaunchAgent } from '../lib/bridgeService.js';
|
|
11
12
|
import { LOCAL_PROTOCOL_MIN, LOCAL_PROTOCOL_MAX, probeGateway } from '../lib/gatewayProbe.js';
|
|
12
13
|
import { LocalAgentRuntime, localAgentConfig, runLocalAgent, writePrivateJson, startChannelWorker } from '../lib/localAgent.js';
|
|
13
14
|
import { validateAgentName, identityRequest, startIdentityWorker, handleIdentityCommand } from '../lib/agentIdentity.js';
|
|
14
15
|
import { handleSpacesCommand, compactHealth } from '../lib/spaces.js';
|
|
16
|
+
import { handleObjectivesCommand } from '../lib/objectives.js';
|
|
15
17
|
import { handleRoutinesCommand, startRoutineEventWorker } from '../lib/routines.js';
|
|
16
18
|
import { createPersonaApiClient } from '../lib/personaApiClient.js';
|
|
17
19
|
import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
|
|
@@ -59,7 +61,6 @@ const BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS = parsePositiveInteger(
|
|
|
59
61
|
process.env.OOMI_BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS,
|
|
60
62
|
30000
|
|
61
63
|
);
|
|
62
|
-
const BRIDGE_LAUNCHD_LABEL = 'ai.oomi.bridge';
|
|
63
64
|
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
64
65
|
const BRIDGE_DEBUG_ENABLED = process.env.OOMI_BRIDGE_DEBUG === '1';
|
|
65
66
|
|
|
@@ -206,6 +207,7 @@ Commands:
|
|
|
206
207
|
Hermes: --profile PROFILE and --resume SESSION select the intended agent/session.
|
|
207
208
|
Other agents: --command '["/absolute/path/to/adapter"]' (JSON stdin / JSON stdout).
|
|
208
209
|
--cwd PATH selects the agent working directory; --name isolates multiple agents.
|
|
210
|
+
Installs automatic startup on macOS/Linux. --no-service opts into external supervision.
|
|
209
211
|
status [--name NAME]
|
|
210
212
|
Show broker connection and verified local agent health separately.
|
|
211
213
|
verify [--name NAME]
|
|
@@ -217,8 +219,10 @@ Commands:
|
|
|
217
219
|
identity restore --snapshot ID --output NEW_FOLDER [--connection NAME]
|
|
218
220
|
invite [--name NAME] [--app-url URL]
|
|
219
221
|
Generate a fresh Auth Link from saved pairing without changing agents.
|
|
220
|
-
bridge [start|ensure|stop|restart|ps] [--name NAME] [--detach]
|
|
221
|
-
Manage only the named agent's bridge.
|
|
222
|
+
bridge [start|ensure|stop|restart|repair|list|ps] [--name NAME] [--detach]
|
|
223
|
+
Manage only the named agent's bridge. Repair restores supervision without re-pairing.
|
|
224
|
+
bridge service [install|start|stop|restart|status|uninstall] [--name NAME]
|
|
225
|
+
Manage automatic restart and startup (macOS launchd / Linux systemd user).
|
|
222
226
|
|
|
223
227
|
init
|
|
224
228
|
Install Oomi agent instructions into OpenClaw AGENTS.md.
|
|
@@ -229,7 +233,7 @@ Commands:
|
|
|
229
233
|
openclaw bridge [start|ensure|stop|restart|ps]
|
|
230
234
|
Manage local OpenClaw-to-Oomi bridge lifecycle (singleton).
|
|
231
235
|
openclaw bridge service [install|start|stop|restart|status|uninstall]
|
|
232
|
-
|
|
236
|
+
Legacy alias for bridge service, with the same connection isolation.
|
|
233
237
|
openclaw profile init
|
|
234
238
|
Write a deterministic OpenClaw profile for local/dev or hosted setup flows.
|
|
235
239
|
openclaw profile apply
|
|
@@ -265,12 +269,18 @@ Commands:
|
|
|
265
269
|
updates
|
|
266
270
|
Read quiet state updates; does not start a model turn.
|
|
267
271
|
spaces list|catalog|show <id>|revisions <id>
|
|
268
|
-
spaces create|update|archive|unarchive|restore [id] --file PATH
|
|
272
|
+
spaces create|update|archive|unarchive|restore|upgrade [id] --file PATH
|
|
273
|
+
[--schema-version space.v3] Required for goals/streaks; v2 remains supported for sources
|
|
274
|
+
objectives list|show <id>
|
|
275
|
+
objectives history <id> [--before YYYY-MM-DD]
|
|
276
|
+
objectives create|update|respond [id] --file PATH
|
|
277
|
+
[--source-message ID] Owner instruction required for agent-recorded progress/corrections
|
|
269
278
|
Choose components from catalog. Writes require idempotencyKey and updates require expectedRevision.
|
|
270
279
|
|
|
271
280
|
context health [--window day|week|month|year|all|range] [--date YYYY-MM-DD]
|
|
272
281
|
[--from YYYY-MM-DD --to YYYY-MM-DD] [--shape summary|series|both]
|
|
273
282
|
[--group-by day|week|month|year] [--timezone IANA-zone] [--cursor YYYY-MM-DD]
|
|
283
|
+
[--provider all|apple_health|smart_scale]
|
|
274
284
|
[--metric steps|weight|sleep|canonical.metric] [--output PATH]
|
|
275
285
|
Show the latest account-linked health context available to this paired OpenClaw device.
|
|
276
286
|
context plugins [--plugin apple_calendar|apple_reminders|apple_contacts|apple_location|apple_notes|google_gmail|google_drive|google_calendar|bluetooth_devices]
|
|
@@ -286,9 +296,10 @@ Commands:
|
|
|
286
296
|
Save user-requested routines and settings. Use the current sourceMessageId,
|
|
287
297
|
expectedRevision, and a stable idempotencyKey. No extra approval button is needed.
|
|
288
298
|
|
|
289
|
-
notify "<message>" [--title TITLE]
|
|
299
|
+
notify "<message>" [--title TITLE] [--connection NAME]
|
|
290
300
|
Send the linked user a push notification on their phone. Use it for things worth
|
|
291
|
-
interrupting them for; say everything else in chat. Limited per hour.
|
|
301
|
+
interrupting them for; say everything else in chat. Limited per hour. Turns started
|
|
302
|
+
by an Oomi bridge already use their own connection (OOMI_CONNECTION).
|
|
292
303
|
|
|
293
304
|
Common flags:
|
|
294
305
|
--agents-file PATH Override AGENTS.md path
|
|
@@ -622,6 +633,7 @@ async function handleContextHealthCommand(flags = {}) {
|
|
|
622
633
|
const payload = await client.getHealthContext({
|
|
623
634
|
window: flags.window, date: flags.date, from: flags.from, to: flags.to,
|
|
624
635
|
shape: flags.shape, groupBy: flags['group-by'], timezone: flags.timezone, cursor: flags.cursor,
|
|
636
|
+
provider: flags.provider,
|
|
625
637
|
});
|
|
626
638
|
const result = flags.metric ? compactHealth(payload, flags.metric) : payload;
|
|
627
639
|
if (flags.output) {
|
|
@@ -691,19 +703,13 @@ async function handleContextHealthSyncCommand(flags = {}) {
|
|
|
691
703
|
|
|
692
704
|
async function refreshBridgeForUpdate(flags = {}, options = {}) {
|
|
693
705
|
const logger = options.logger || null;
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
return {
|
|
702
|
-
restarted: true,
|
|
703
|
-
mode: 'service',
|
|
704
|
-
target: launchdStatus.target,
|
|
705
|
-
};
|
|
706
|
-
}
|
|
706
|
+
const service = currentBridgeService();
|
|
707
|
+
if (service.status().installed) {
|
|
708
|
+
logger?.(`Restarting supervised bridge ${service.config.target}.`);
|
|
709
|
+
await stopSupervisedBridge(service);
|
|
710
|
+
service.start({ rewrite: true });
|
|
711
|
+
incrementBridgeMetric('bridge_restart_count');
|
|
712
|
+
return { restarted: true, mode: 'service', target: service.config.target };
|
|
707
713
|
}
|
|
708
714
|
|
|
709
715
|
const running = findRunningBridgeProcess();
|
|
@@ -842,8 +848,15 @@ function resolveBridgeLiveLogPath() {
|
|
|
842
848
|
return bridgeConnectionName ? path.join(connectionDirectory(), 'logs', 'oomi-bridge-live.log') : resolveOpenclawBridgeLiveLogPath();
|
|
843
849
|
}
|
|
844
850
|
|
|
845
|
-
function
|
|
846
|
-
return
|
|
851
|
+
function currentBridgeService() {
|
|
852
|
+
return createBridgeService(bridgeServiceConfig({
|
|
853
|
+
name: bridgeConnectionName, stateDirectory: path.dirname(resolveBridgeStatePath()),
|
|
854
|
+
logPath: resolveBridgeLiveLogPath(), scriptPath: fileURLToPath(import.meta.url),
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function buildBridgeLaunchAgentPlist() {
|
|
859
|
+
return renderLaunchAgent(currentBridgeService().config);
|
|
847
860
|
}
|
|
848
861
|
|
|
849
862
|
function defaultDeviceId() {
|
|
@@ -2417,142 +2430,6 @@ async function stopBridgeProcesses() {
|
|
|
2417
2430
|
};
|
|
2418
2431
|
}
|
|
2419
2432
|
|
|
2420
|
-
function assertMacOSLaunchdAvailable() {
|
|
2421
|
-
if (process.platform !== 'darwin') {
|
|
2422
|
-
throw new Error('Bridge service manager is only supported on macOS (launchd).');
|
|
2423
|
-
}
|
|
2424
|
-
if (typeof process.getuid !== 'function') {
|
|
2425
|
-
throw new Error('Cannot resolve current UID for launchd domain.');
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
function launchctlDomain() {
|
|
2430
|
-
assertMacOSLaunchdAvailable();
|
|
2431
|
-
return `gui/${String(process.getuid())}`;
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
function launchctlServiceTarget() {
|
|
2435
|
-
return `${launchctlDomain()}/${BRIDGE_LAUNCHD_LABEL}`;
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
function runLaunchctl(args, { allowFailure = false } = {}) {
|
|
2439
|
-
const result = spawnSync('launchctl', args, {
|
|
2440
|
-
encoding: 'utf8',
|
|
2441
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2442
|
-
});
|
|
2443
|
-
const status = Number.isInteger(result.status) ? result.status : 1;
|
|
2444
|
-
const stdout = String(result.stdout || '').trim();
|
|
2445
|
-
const stderr = String(result.stderr || '').trim();
|
|
2446
|
-
if (status !== 0 && !allowFailure) {
|
|
2447
|
-
throw new Error(
|
|
2448
|
-
`launchctl ${args.join(' ')} failed (${status}): ${stderr || stdout || 'unknown launchctl error'}`
|
|
2449
|
-
);
|
|
2450
|
-
}
|
|
2451
|
-
return { status, stdout, stderr };
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
function buildBridgeLaunchAgentPlist() {
|
|
2455
|
-
const scriptPath = (() => {
|
|
2456
|
-
try {
|
|
2457
|
-
return fs.realpathSync(process.argv[1]);
|
|
2458
|
-
} catch {
|
|
2459
|
-
return process.argv[1];
|
|
2460
|
-
}
|
|
2461
|
-
})();
|
|
2462
|
-
const programArgs = [process.execPath, scriptPath, 'openclaw', 'bridge', 'start', '--service-managed'];
|
|
2463
|
-
const bridgeLogPath = resolveBridgeLiveLogPath();
|
|
2464
|
-
const argsXml = programArgs.map((arg) => `<string>${xmlEscape(arg)}</string>`).join('\n ');
|
|
2465
|
-
const openclawHome = resolveOpenclawHome();
|
|
2466
|
-
const openclawWorkspace = resolveOpenclawWorkspaceRoot();
|
|
2467
|
-
const envVars = {
|
|
2468
|
-
OOMI_SKIP_UPDATE_CHECK: '1',
|
|
2469
|
-
};
|
|
2470
|
-
if (process.env.OPENCLAW_HOME) {
|
|
2471
|
-
envVars.OPENCLAW_HOME = openclawHome;
|
|
2472
|
-
}
|
|
2473
|
-
if (process.env.OPENCLAW_WORKSPACE) {
|
|
2474
|
-
envVars.OPENCLAW_WORKSPACE = openclawWorkspace;
|
|
2475
|
-
}
|
|
2476
|
-
const envXml = Object.entries(envVars)
|
|
2477
|
-
.map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
|
|
2478
|
-
.join('\n');
|
|
2479
|
-
|
|
2480
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2481
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2482
|
-
<plist version="1.0">
|
|
2483
|
-
<dict>
|
|
2484
|
-
<key>Label</key>
|
|
2485
|
-
<string>${xmlEscape(BRIDGE_LAUNCHD_LABEL)}</string>
|
|
2486
|
-
<key>ProgramArguments</key>
|
|
2487
|
-
<array>
|
|
2488
|
-
${argsXml}
|
|
2489
|
-
</array>
|
|
2490
|
-
<key>WorkingDirectory</key>
|
|
2491
|
-
<string>${xmlEscape(openclawHome)}</string>
|
|
2492
|
-
<key>RunAtLoad</key>
|
|
2493
|
-
<true/>
|
|
2494
|
-
<key>KeepAlive</key>
|
|
2495
|
-
<true/>
|
|
2496
|
-
<key>ThrottleInterval</key>
|
|
2497
|
-
<integer>5</integer>
|
|
2498
|
-
<key>EnvironmentVariables</key>
|
|
2499
|
-
<dict>
|
|
2500
|
-
${envXml}
|
|
2501
|
-
</dict>
|
|
2502
|
-
<key>StandardOutPath</key>
|
|
2503
|
-
<string>${xmlEscape(bridgeLogPath)}</string>
|
|
2504
|
-
<key>StandardErrorPath</key>
|
|
2505
|
-
<string>${xmlEscape(bridgeLogPath)}</string>
|
|
2506
|
-
</dict>
|
|
2507
|
-
</plist>
|
|
2508
|
-
`;
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
function readBridgeLaunchdStatus() {
|
|
2512
|
-
assertMacOSLaunchdAvailable();
|
|
2513
|
-
const plistPath = resolveBridgeLaunchAgentPlistPath();
|
|
2514
|
-
const target = launchctlServiceTarget();
|
|
2515
|
-
const printResult = runLaunchctl(['print', target], { allowFailure: true });
|
|
2516
|
-
const loaded = printResult.status === 0;
|
|
2517
|
-
const output = [printResult.stdout, printResult.stderr].filter(Boolean).join('\n');
|
|
2518
|
-
const pidMatch = output.match(/\bpid\s*=\s*(\d+)/);
|
|
2519
|
-
const lastExitMatch = output.match(/\blast exit code\s*=\s*(-?\d+)/i);
|
|
2520
|
-
|
|
2521
|
-
return {
|
|
2522
|
-
plistPath,
|
|
2523
|
-
target,
|
|
2524
|
-
installed: fs.existsSync(plistPath),
|
|
2525
|
-
loaded,
|
|
2526
|
-
pid: pidMatch ? Number(pidMatch[1]) : null,
|
|
2527
|
-
running: Boolean(pidMatch && Number(pidMatch[1]) > 0),
|
|
2528
|
-
lastExitCode: lastExitMatch ? Number(lastExitMatch[1]) : null,
|
|
2529
|
-
printOutput: output,
|
|
2530
|
-
};
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
function startBridgeLaunchdService() {
|
|
2534
|
-
assertMacOSLaunchdAvailable();
|
|
2535
|
-
const plistPath = resolveBridgeLaunchAgentPlistPath();
|
|
2536
|
-
if (!fs.existsSync(plistPath)) {
|
|
2537
|
-
throw new Error('Bridge service is not installed. Run: oomi openclaw bridge service install');
|
|
2538
|
-
}
|
|
2539
|
-
writeFile(plistPath, buildBridgeLaunchAgentPlist());
|
|
2540
|
-
const domain = launchctlDomain();
|
|
2541
|
-
const target = launchctlServiceTarget();
|
|
2542
|
-
runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
|
|
2543
|
-
runLaunchctl(['bootstrap', domain, plistPath]);
|
|
2544
|
-
runLaunchctl(['enable', target], { allowFailure: true });
|
|
2545
|
-
runLaunchctl(['kickstart', '-k', target], { allowFailure: true });
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
|
-
async function stopBridgeLaunchdService() {
|
|
2549
|
-
assertMacOSLaunchdAvailable();
|
|
2550
|
-
const plistPath = resolveBridgeLaunchAgentPlistPath();
|
|
2551
|
-
const domain = launchctlDomain();
|
|
2552
|
-
runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
|
|
2553
|
-
return stopBridgeProcesses();
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
2433
|
async function resolveBridgeRuntimeConfig(flags, bridgeState) {
|
|
2557
2434
|
const explicitBrokerHttp = String(flags['broker-http'] || '').trim();
|
|
2558
2435
|
const explicitBrokerWs = String(flags['broker-ws'] || '').trim();
|
|
@@ -2641,7 +2518,7 @@ async function startOpenclawBridge(flags) {
|
|
|
2641
2518
|
}
|
|
2642
2519
|
|
|
2643
2520
|
const localRuntime = bridgeState.localAgent
|
|
2644
|
-
? new LocalAgentRuntime(bridgeState.localAgent, path.dirname(resolveBridgeStatePath()), (health) => updateBridgeStatus(health))
|
|
2521
|
+
? new LocalAgentRuntime({ ...bridgeState.localAgent, connection: bridgeConnectionName }, path.dirname(resolveBridgeStatePath()), (health) => updateBridgeStatus(health))
|
|
2645
2522
|
: null;
|
|
2646
2523
|
const gateway = localRuntime ? { gatewayUrl: 'local-agent:', configPath: resolveBridgeStatePath() } : readOpenclawGatewayConfig();
|
|
2647
2524
|
if (!localRuntime && !gateway.token && !gateway.password) {
|
|
@@ -4141,6 +4018,7 @@ function printOpenclawBridgeStatus(flags) {
|
|
|
4141
4018
|
expiresAt: bridgeState.expiresAt || null,
|
|
4142
4019
|
},
|
|
4143
4020
|
runtime: runtimeStatus,
|
|
4021
|
+
supervision: currentBridgeService().status(),
|
|
4144
4022
|
};
|
|
4145
4023
|
|
|
4146
4024
|
if (jsonOutput) {
|
|
@@ -4149,6 +4027,10 @@ function printOpenclawBridgeStatus(flags) {
|
|
|
4149
4027
|
}
|
|
4150
4028
|
|
|
4151
4029
|
console.log('Oomi Bridge Status');
|
|
4030
|
+
const supervision = payload.supervision;
|
|
4031
|
+
console.log(`Automatic startup: ${supervision.installed && supervision.enabled ? supervision.manager + ' (' + supervision.startsAt + ')' : 'off'}`);
|
|
4032
|
+
if (!supervision.installed || !supervision.enabled || !supervision.running) console.log(`Repair on this computer: oomi bridge repair${bridgeConnectionName ? ' --name ' + bridgeConnectionName : ''}`);
|
|
4033
|
+
if (supervision.reason) console.log(supervision.reason);
|
|
4152
4034
|
console.log('------------------');
|
|
4153
4035
|
console.log(`Bridge state: ${payload.bridgeStatePath}`);
|
|
4154
4036
|
console.log(`Runtime status: ${payload.bridgeStatusPath}`);
|
|
@@ -4260,82 +4142,90 @@ function printOpenclawPluginSetup(flags) {
|
|
|
4260
4142
|
}
|
|
4261
4143
|
}
|
|
4262
4144
|
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
const
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
console.log(`Installed bridge launchd plist: ${plistPath}`);
|
|
4272
|
-
if (isTruthyFlag(flags['no-start'])) {
|
|
4273
|
-
console.log('Service install complete. Start with: oomi openclaw bridge service start');
|
|
4274
|
-
return;
|
|
4145
|
+
function listLocalBridgeConnections() {
|
|
4146
|
+
const selected = bridgeConnectionName;
|
|
4147
|
+
const names = fs.existsSync(resolveOpenclawBridgeStatePath()) ? [''] : [];
|
|
4148
|
+
const directory = process.env.OOMI_CONNECTIONS_DIR || path.join(os.homedir(), '.oomi', 'connections');
|
|
4149
|
+
if (fs.existsSync(directory)) {
|
|
4150
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
4151
|
+
if (entry.isDirectory() && /^[a-z0-9][a-z0-9_-]{0,63}$/.test(entry.name)
|
|
4152
|
+
&& fs.existsSync(path.join(directory, entry.name, 'oomi-bridge.json'))) names.push(entry.name);
|
|
4275
4153
|
}
|
|
4276
|
-
startBridgeLaunchdService();
|
|
4277
|
-
incrementBridgeMetric('bridge_start_count');
|
|
4278
|
-
console.log(`Bridge service started: ${launchctlServiceTarget()}`);
|
|
4279
|
-
return;
|
|
4280
4154
|
}
|
|
4155
|
+
try {
|
|
4156
|
+
return names.sort().map((name) => {
|
|
4157
|
+
bridgeConnectionName = name;
|
|
4158
|
+
const state = readBridgeState();
|
|
4159
|
+
const running = findRunningBridgeProcess();
|
|
4160
|
+
return { name: name || null, runtime: state.runtime || 'openclaw', deviceId: state.deviceId || null,
|
|
4161
|
+
running: Boolean(running), supervision: currentBridgeService().status(),
|
|
4162
|
+
repairCommand: `oomi bridge repair${name ? ' --name ' + name : ''}` };
|
|
4163
|
+
});
|
|
4164
|
+
} finally { bridgeConnectionName = selected; }
|
|
4165
|
+
}
|
|
4281
4166
|
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
fs.unlinkSync(plistPath);
|
|
4286
|
-
}
|
|
4287
|
-
console.log(`Removed bridge launchd plist: ${plistPath}`);
|
|
4288
|
-
return;
|
|
4289
|
-
}
|
|
4167
|
+
function requireSavedBridge() {
|
|
4168
|
+
if (!readBridgeState().deviceToken) throw new Error('No saved connection. Run oomi connect with an explicit runtime first.');
|
|
4169
|
+
}
|
|
4290
4170
|
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4171
|
+
async function stopSupervisedBridge(service) {
|
|
4172
|
+
// Unload/disable before signalling the worker, otherwise KeepAlive immediately recreates it.
|
|
4173
|
+
service.stop();
|
|
4174
|
+
const stopped = await stopBridgeProcesses();
|
|
4175
|
+
if (stopped.stillAlive?.length) throw new Error('The existing bridge did not stop. Resolve it before reconnecting.');
|
|
4176
|
+
return stopped;
|
|
4177
|
+
}
|
|
4297
4178
|
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
console.log('No bridge workers running.');
|
|
4304
|
-
}
|
|
4305
|
-
console.log(`Bridge service stopped: ${launchctlServiceTarget()}`);
|
|
4179
|
+
async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
|
|
4180
|
+
const action = String(actionRaw || 'status').trim().toLowerCase();
|
|
4181
|
+
const service = currentBridgeService();
|
|
4182
|
+
if (action === 'status') {
|
|
4183
|
+
console.log(JSON.stringify(service.status(), null, 2));
|
|
4306
4184
|
return;
|
|
4307
4185
|
}
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4186
|
+
if (!['install', 'start', 'stop', 'restart', 'uninstall'].includes(action)) {
|
|
4187
|
+
throw new Error('Use: oomi bridge service [install|start|stop|restart|status|uninstall] [--name NAME]');
|
|
4188
|
+
}
|
|
4189
|
+
if (['install', 'start', 'restart'].includes(action)) requireSavedBridge();
|
|
4190
|
+
if (action === 'install') {
|
|
4191
|
+
service.install({ noStart: true });
|
|
4192
|
+
await stopSupervisedBridge(service);
|
|
4193
|
+
if (!isTruthyFlag(flags['no-start'])) service.start();
|
|
4194
|
+
} else if (action === 'start') {
|
|
4195
|
+
service.start(); // Idempotent: do not interrupt a healthy worker.
|
|
4196
|
+
} else if (action === 'restart') {
|
|
4197
|
+
await stopSupervisedBridge(service);
|
|
4198
|
+
service.start({ rewrite: true });
|
|
4312
4199
|
incrementBridgeMetric('bridge_restart_count');
|
|
4313
|
-
|
|
4314
|
-
|
|
4200
|
+
} else if (action === 'stop') {
|
|
4201
|
+
await stopSupervisedBridge(service);
|
|
4202
|
+
} else if (action === 'uninstall') {
|
|
4203
|
+
await stopSupervisedBridge(service);
|
|
4204
|
+
service.uninstall();
|
|
4315
4205
|
}
|
|
4206
|
+
const result = service.status();
|
|
4207
|
+
console.log(JSON.stringify({ action, ...result }, null, 2));
|
|
4208
|
+
}
|
|
4316
4209
|
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
console.log(
|
|
4323
|
-
console.log(`Plist: ${status.plistPath}`);
|
|
4324
|
-
console.log(`Installed: ${status.installed ? 'yes' : 'no'}`);
|
|
4325
|
-
console.log(`Loaded: ${status.loaded ? 'yes' : 'no'}`);
|
|
4326
|
-
console.log(`Running: ${status.running ? 'yes' : 'no'}`);
|
|
4327
|
-
if (status.pid) {
|
|
4328
|
-
console.log(`PID: ${status.pid}`);
|
|
4329
|
-
}
|
|
4330
|
-
if (status.lastExitCode !== null) {
|
|
4331
|
-
console.log(`Last exit code: ${status.lastExitCode}`);
|
|
4332
|
-
}
|
|
4333
|
-
return;
|
|
4210
|
+
async function repairBridge(flags = {}) {
|
|
4211
|
+
requireSavedBridge();
|
|
4212
|
+
await handleBridgeServiceCommand('install', flags);
|
|
4213
|
+
if (!isTruthyFlag(flags['no-start'])) {
|
|
4214
|
+
await waitForBridgeConnection({});
|
|
4215
|
+
console.log('Oomi bridge restored with automatic startup. Pairing and conversations preserved.');
|
|
4334
4216
|
}
|
|
4217
|
+
}
|
|
4335
4218
|
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
)
|
|
4219
|
+
async function waitForBridgeConnection(child) {
|
|
4220
|
+
const deadline = Date.now() + 20000;
|
|
4221
|
+
while (Date.now() < deadline) {
|
|
4222
|
+
const status = readBridgeStatus();
|
|
4223
|
+
if (status.pid && (!child.pid || status.pid === child.pid) && isPidAlive(status.pid) && status.status === 'connected') return;
|
|
4224
|
+
if ((!child.pid || status.pid === child.pid) && status.status === 'error') throw new Error(status.lastErrorMessage || 'Bridge startup failed.');
|
|
4225
|
+
if (child.pid && !isPidAlive(child.pid)) throw new Error(`Bridge exited. Check ${resolveBridgeLiveLogPath()}.`);
|
|
4226
|
+
await delay(200);
|
|
4227
|
+
}
|
|
4228
|
+
throw new Error(`The bridge did not connect yet. Automatic recovery may still be retrying. Check oomi status${bridgeConnectionName ? ' --name ' + bridgeConnectionName : ''} and ${resolveBridgeLiveLogPath()}.`);
|
|
4339
4229
|
}
|
|
4340
4230
|
|
|
4341
4231
|
async function startBridgeLifecycle(flags = {}) {
|
|
@@ -4344,6 +4234,16 @@ async function startBridgeLifecycle(flags = {}) {
|
|
|
4344
4234
|
throw new Error('Detached bridge mode cannot be combined with --service-managed.');
|
|
4345
4235
|
}
|
|
4346
4236
|
|
|
4237
|
+
if (!serviceManaged) {
|
|
4238
|
+
const service = currentBridgeService();
|
|
4239
|
+
if (service.status().installed) {
|
|
4240
|
+
if (isTruthyFlag(flags.detach)) throw new Error('This connection has a supervisor. Use bridge start, or uninstall its service before using --detach.');
|
|
4241
|
+
service.start();
|
|
4242
|
+
console.log(`Bridge service started: ${service.config.target}`);
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4347
4247
|
if (Boolean(flags.detach)) {
|
|
4348
4248
|
const detachedFlags = { ...flags };
|
|
4349
4249
|
delete detachedFlags.detach;
|
|
@@ -4385,6 +4285,12 @@ async function startBridgeLifecycle(flags = {}) {
|
|
|
4385
4285
|
async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
|
|
4386
4286
|
const action = String(actionRaw || 'start').trim().toLowerCase();
|
|
4387
4287
|
|
|
4288
|
+
if (action === 'list') { console.log(JSON.stringify(listLocalBridgeConnections(), null, 2)); return; }
|
|
4289
|
+
if (action === 'repair') return repairBridge(flags);
|
|
4290
|
+
if (['stop', 'restart'].includes(action) && currentBridgeService().status().installed) {
|
|
4291
|
+
return handleBridgeServiceCommand(action, flags);
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4388
4294
|
if (action === 'start' || action === 'ensure') {
|
|
4389
4295
|
await startBridgeLifecycle(flags);
|
|
4390
4296
|
return;
|
|
@@ -4435,7 +4341,7 @@ async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
|
|
|
4435
4341
|
}
|
|
4436
4342
|
|
|
4437
4343
|
throw new Error(
|
|
4438
|
-
`Unknown bridge action: ${action}. Use: oomi
|
|
4344
|
+
`Unknown bridge action: ${action}. Use: oomi bridge [start|ensure|stop|restart|repair|list|ps]`
|
|
4439
4345
|
);
|
|
4440
4346
|
}
|
|
4441
4347
|
|
|
@@ -4462,6 +4368,13 @@ async function connectLocalAgent(flags) {
|
|
|
4462
4368
|
const agentName = flags['agent-name'] === undefined ? undefined : validateAgentName(flags['agent-name']);
|
|
4463
4369
|
const runtime = String(flags.runtime || '');
|
|
4464
4370
|
if (!['openclaw', 'hermes', 'command'].includes(runtime)) throw new Error('Specify --runtime openclaw, hermes, or command. Never select a different agent just because it is installed.');
|
|
4371
|
+
const service = currentBridgeService();
|
|
4372
|
+
const unmanaged = isTruthyFlag(flags['no-service']);
|
|
4373
|
+
if (!unmanaged) {
|
|
4374
|
+
service.preflight();
|
|
4375
|
+
} else if (service.status().installed) {
|
|
4376
|
+
throw new Error('This connection already has automatic startup. Uninstall its service before using --no-service.');
|
|
4377
|
+
}
|
|
4465
4378
|
const previous = readBridgeState();
|
|
4466
4379
|
if (previous.runtime && previous.runtime !== runtime) throw new Error('This connection belongs to a different runtime. Choose a different --name.');
|
|
4467
4380
|
const appUrl = String(flags['app-url'] || previous.appUrl || 'https://www.oomi.ai').replace(/\/$/, '');
|
|
@@ -4512,23 +4425,16 @@ async function connectLocalAgent(flags) {
|
|
|
4512
4425
|
if (error.status !== 404 || agentName) throw error;
|
|
4513
4426
|
console.warn('This Oomi server does not support agent settings yet. Connecting without shared settings.');
|
|
4514
4427
|
}
|
|
4515
|
-
//
|
|
4516
|
-
|
|
4517
|
-
const stopped =
|
|
4428
|
+
// Install/update only this connection's supervisor, retaining its saved identity and session.
|
|
4429
|
+
if (!unmanaged) service.install({ noStart: true });
|
|
4430
|
+
const stopped = unmanaged ? await stopBridgeProcesses() : await stopSupervisedBridge(service);
|
|
4518
4431
|
if (stopped.stillAlive?.length) throw new Error('The existing bridge did not stop. Resolve it before reconnecting.');
|
|
4519
4432
|
writeBridgeState(state);
|
|
4520
4433
|
updateBridgeStatus({ status: 'starting', runtime, runtimeReady: true, verifiedAt: state.verifiedAt, lastErrorMessage: '' });
|
|
4521
|
-
const child =
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
const status = readBridgeStatus();
|
|
4526
|
-
if (status.pid && (!child.pid || status.pid === child.pid) && status.status === 'connected') { connected = true; break; }
|
|
4527
|
-
if ((!child.pid || status.pid === child.pid) && status.status === 'error') throw new Error(status.lastErrorMessage || 'Bridge startup failed.');
|
|
4528
|
-
if (child.pid && !isPidAlive(child.pid)) throw new Error(`Bridge exited. Check ${resolveBridgeLiveLogPath()}.`);
|
|
4529
|
-
await delay(200);
|
|
4530
|
-
}
|
|
4531
|
-
if (!connected) throw new Error('The local agent replied, but the Oomi bridge did not connect. Run oomi status for this connection.');
|
|
4434
|
+
const child = unmanaged ? startBridgeDetachedProcess({ 'app-url': appUrl }) : (service.start(), {});
|
|
4435
|
+
await waitForBridgeConnection(child);
|
|
4436
|
+
console.log(unmanaged ? 'Automatic startup: off (--no-service); your host supervisor must manage recovery.'
|
|
4437
|
+
: `Automatic startup: ${service.config.platform === 'darwin' ? 'macOS login service' : 'Linux user service'}; restarts after worker exits.`);
|
|
4532
4438
|
const invite = await requestConnectInviteLink({ backendHttp: state.brokerHttp, appUrl, sessionKey: state.sessionKey, deviceToken: state.deviceToken });
|
|
4533
4439
|
console.log(`Verified agent: ${runtime}${state.localAgent?.profile ? ` (${state.localAgent.profile})` : ''}`);
|
|
4534
4440
|
console.log('Oomi Connect Ready');
|
|
@@ -4546,16 +4452,19 @@ async function main() {
|
|
|
4546
4452
|
process.exit(0);
|
|
4547
4453
|
}
|
|
4548
4454
|
|
|
4549
|
-
|
|
4455
|
+
// Agents started by an Oomi bridge inherit OOMI_CONNECTION, so their commands reach their own account.
|
|
4456
|
+
const connection = args.flags.connection || args.flags.name || (command === 'connect' ? (args.flags.runtime !== 'openclaw' ? args.flags.runtime : '') : String(process.env.OOMI_CONNECTION || '').trim());
|
|
4550
4457
|
if (connection && !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(connection)) throw new Error('Connection name must use lowercase letters, numbers, underscores, or hyphens.');
|
|
4551
4458
|
bridgeConnectionName = connection || '';
|
|
4552
4459
|
|
|
4553
4460
|
if (command === 'instructions') { console.log(fs.readFileSync(resolveInstructionsFile(), 'utf8')); return; }
|
|
4554
4461
|
if (command === 'capabilities') {
|
|
4555
4462
|
console.log(JSON.stringify({ instructions: 'oomi instructions', skill: path.join(PACKAGE_ROOT, 'skills/oomi/SKILL.md'), connectionSelector: '--connection NAME',
|
|
4556
|
-
spaces: { catalog: 'oomi spaces catalog', list: 'oomi spaces list', create: 'oomi spaces create --file space.json', update: 'oomi spaces update ID --file change.json', revisions: 'oomi spaces revisions ID', guide: 'oomi instructions' },
|
|
4463
|
+
spaces: { catalog: 'oomi spaces catalog', list: 'oomi spaces list', create: 'oomi spaces create --file space.json', update: 'oomi spaces update ID --file change.json', revisions: 'oomi spaces revisions ID', upgrade: 'oomi spaces upgrade ID --file upgrade.json', guide: 'oomi instructions' },
|
|
4557
4464
|
health: { steps: 'oomi context health --window week --metric steps --json', calendarWeek: 'oomi context health --window range --from YYYY-MM-DD --to YYYY-MM-DD --metric steps --json', largeResults: '--output new-file.json', plugins: 'oomi context plugins', missing: 'unknown, never zero', week: 'rolling seven days; use explicit range for calendar week' },
|
|
4465
|
+
objectives: { list: 'oomi objectives list', show: 'oomi objectives show ID', history: 'oomi objectives history ID [--before YYYY-MM-DD]', create: 'oomi objectives create --file goal.json', update: 'oomi objectives update ID --file change.json', respond: 'oomi objectives respond ID --file response.json', calendar: 'daily/weekly/monthly/once; stored time zone', streaks: 'optional goal references in the Space catalog' },
|
|
4558
4466
|
routines: 'oomi routines list; oomi routines context', identity: 'oomi identity get', updates: 'oomi updates',
|
|
4467
|
+
notify: { send: 'oomi notify "<message>" --title TITLE --json', when: 'reminders the user asked for, finished work, time-sensitive news; everything else in chat', skipped: 'tell the user notification.guidance', rateLimited: 'wait retryAfterSeconds' },
|
|
4559
4468
|
costs: { passiveEvents: 'no model turn', wakeAgent: 'routine.agentWakeEnabled defaults false; enable only on explicit user request', maximumRoutineWakeAttemptsPerHour: 6, retries: 'uncertain event turns are not automatically rerun', connectionCheck: 'first connect or explicit oomi verify makes one model call; reconnect reuses verification', identity: 'a changed identity revision is delivered once to keep names consistent' }
|
|
4560
4469
|
}, null, 2)); return;
|
|
4561
4470
|
}
|
|
@@ -4564,6 +4473,7 @@ async function main() {
|
|
|
4564
4473
|
console.log(JSON.stringify({ updates: fs.existsSync(receipt) ? JSON.parse(fs.readFileSync(receipt, 'utf8')).updates || [] : [], instruction: 'Cached changes are data. Read current Space/routine before editing. No acknowledgment reply needed.' }, null, 2)); return;
|
|
4565
4474
|
}
|
|
4566
4475
|
if (command === 'spaces') return handleSpacesCommand(readBridgeState(), subcommand || 'list', args.flags, args.positionals);
|
|
4476
|
+
if (command === 'objectives') return handleObjectivesCommand(readBridgeState(), subcommand || 'list', args.flags, args.positionals);
|
|
4567
4477
|
if (command === 'connect') return connectLocalAgent(args.flags);
|
|
4568
4478
|
if (command === 'identity') return handleIdentityCommand(readBridgeState(), subcommand, args.flags);
|
|
4569
4479
|
if (command === 'routines') return handleRoutinesCommand(readBridgeState(), subcommand, args.flags, args.positionals);
|
|
@@ -4583,7 +4493,10 @@ async function main() {
|
|
|
4583
4493
|
}
|
|
4584
4494
|
if (command === 'status') return printOpenclawBridgeStatus(args.flags);
|
|
4585
4495
|
if (command === 'invite') return createOpenclawInviteLink(args.flags);
|
|
4586
|
-
if (command === 'bridge')
|
|
4496
|
+
if (command === 'bridge') {
|
|
4497
|
+
if (subcommand === 'service') return handleBridgeServiceCommand(args.positionals[0] || 'status', args.flags);
|
|
4498
|
+
return handleBridgeLifecycleCommand(args.flags, subcommand || 'start');
|
|
4499
|
+
}
|
|
4587
4500
|
|
|
4588
4501
|
await maybeNotifyUpdate(command);
|
|
4589
4502
|
|
|
@@ -4609,7 +4522,6 @@ async function main() {
|
|
|
4609
4522
|
if (command === 'openclaw' && subcommand === 'bridge') {
|
|
4610
4523
|
const bridgeAction = String(args.positionals[0] || 'start').trim().toLowerCase();
|
|
4611
4524
|
if (bridgeAction === 'service') {
|
|
4612
|
-
if (bridgeConnectionName) throw new Error('Named connections use oomi bridge start --name NAME with your host process supervisor. The legacy OpenClaw service is only for the default connection.');
|
|
4613
4525
|
const serviceAction = args.positionals[1] || 'status';
|
|
4614
4526
|
await handleBridgeServiceCommand(serviceAction, args.flags);
|
|
4615
4527
|
return;
|