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/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
- Manage macOS launchd bridge supervision.
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
- if (process.platform === 'darwin') {
695
- const launchdStatus = readBridgeLaunchdStatus();
696
- if (launchdStatus.installed) {
697
- logger?.(`Restarting launchd-managed bridge ${launchdStatus.target}.`);
698
- await stopBridgeLaunchdService();
699
- startBridgeLaunchdService();
700
- incrementBridgeMetric('bridge_restart_count');
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 resolveBridgeLaunchAgentPlistPath() {
846
- return path.join(os.homedir(), 'Library', 'LaunchAgents', `${BRIDGE_LAUNCHD_LABEL}.plist`);
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
- async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
4264
- assertMacOSLaunchdAvailable();
4265
- const action = String(actionRaw || 'status').trim().toLowerCase();
4266
- const plistPath = resolveBridgeLaunchAgentPlistPath();
4267
-
4268
- if (action === 'install') {
4269
- ensureDir(path.dirname(plistPath));
4270
- writeFile(plistPath, buildBridgeLaunchAgentPlist());
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
- if (action === 'uninstall') {
4283
- await stopBridgeLaunchdService();
4284
- if (fs.existsSync(plistPath)) {
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
- if (action === 'start') {
4292
- startBridgeLaunchdService();
4293
- incrementBridgeMetric('bridge_start_count');
4294
- console.log(`Bridge service started: ${launchctlServiceTarget()}`);
4295
- return;
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
- if (action === 'stop') {
4299
- const stopped = await stopBridgeLaunchdService();
4300
- if (Array.isArray(stopped.found) && stopped.found.length > 0) {
4301
- console.log(`Stopped bridge workers: ${stopped.stopped.join(', ') || 'none'}.`);
4302
- } else {
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
- if (action === 'restart') {
4310
- await stopBridgeLaunchdService();
4311
- startBridgeLaunchdService();
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
- console.log(`Bridge service restarted: ${launchctlServiceTarget()}`);
4314
- return;
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
- if (action === 'status') {
4318
- const status = readBridgeLaunchdStatus();
4319
- console.log('Bridge Service Status');
4320
- console.log('---------------------');
4321
- console.log(`Label: ${BRIDGE_LAUNCHD_LABEL}`);
4322
- console.log(`Target: ${status.target}`);
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
- throw new Error(
4337
- `Unknown bridge service action: ${action}. Use: oomi openclaw bridge service [install|start|stop|restart|status|uninstall]`
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 openclaw bridge [start|ensure|stop|restart|ps]`
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
- // Stop this connection only; never leave a worker using an old adapter after a change.
4516
- const supervised = !bridgeConnectionName && process.platform === 'darwin' && readBridgeLaunchdStatus().installed;
4517
- const stopped = supervised ? await stopBridgeLaunchdService() : await stopBridgeProcesses();
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 = supervised ? (startBridgeLaunchdService(), { pid: null }) : startBridgeDetachedProcess({ 'app-url': appUrl });
4522
- const deadline = Date.now() + 20000;
4523
- let connected = false;
4524
- while (Date.now() < deadline) {
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
- const connection = args.flags.connection || args.flags.name || (command === 'connect' && args.flags.runtime !== 'openclaw' ? args.flags.runtime : '');
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') return handleBridgeLifecycleCommand(args.flags, subcommand || 'start');
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;