oomi-ai 0.7.2 → 0.8.1

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,13 @@ 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 { planPackageUpdate, isWorkerDescendant, queueRestart, readMaintenance, createMaintenanceController } from '../lib/bridgeMaintenance.js';
12
+ import { bridgeServiceConfig, createBridgeService, renderLaunchAgent } from '../lib/bridgeService.js';
11
13
  import { LOCAL_PROTOCOL_MIN, LOCAL_PROTOCOL_MAX, probeGateway } from '../lib/gatewayProbe.js';
12
14
  import { LocalAgentRuntime, localAgentConfig, runLocalAgent, writePrivateJson, startChannelWorker } from '../lib/localAgent.js';
13
15
  import { validateAgentName, identityRequest, startIdentityWorker, handleIdentityCommand } from '../lib/agentIdentity.js';
14
16
  import { handleSpacesCommand, compactHealth } from '../lib/spaces.js';
17
+ import { handleObjectivesCommand } from '../lib/objectives.js';
15
18
  import { handleRoutinesCommand, startRoutineEventWorker } from '../lib/routines.js';
16
19
  import { createPersonaApiClient } from '../lib/personaApiClient.js';
17
20
  import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
@@ -59,7 +62,6 @@ const BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS = parsePositiveInteger(
59
62
  process.env.OOMI_BRIDGE_GATEWAY_REQUEST_TIMEOUT_MS,
60
63
  30000
61
64
  );
62
- const BRIDGE_LAUNCHD_LABEL = 'ai.oomi.bridge';
63
65
  const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
64
66
  const BRIDGE_DEBUG_ENABLED = process.env.OOMI_BRIDGE_DEBUG === '1';
65
67
 
@@ -192,8 +194,7 @@ async function maybeNotifyUpdate(command) {
192
194
 
193
195
  if (compareVersions(currentVersion, latestVersion) < 0) {
194
196
  console.warn(`[oomi] Update available: oomi-ai ${currentVersion} -> ${latestVersion}`);
195
- console.warn('[oomi] Update command: pnpm add -g oomi-ai@latest');
196
- console.warn('[oomi] Fallback update command: npm install -g oomi-ai@latest');
197
+ console.warn('[oomi] Read the safe update plan: oomi update');
197
198
  }
198
199
  }
199
200
 
@@ -201,11 +202,14 @@ function usage() {
201
202
  console.log(`oomi <command>
202
203
 
203
204
  Commands:
205
+ update
206
+ Check for a newer package and return a safe, approval-preserving update plan.
204
207
  connect --runtime openclaw|hermes|command [--name CONNECTION] [--agent-name "Your name"] [--app-url URL]
205
208
  Verify the selected local agent, pair once, start its bridge, and return an Auth Link.
206
209
  Hermes: --profile PROFILE and --resume SESSION select the intended agent/session.
207
210
  Other agents: --command '["/absolute/path/to/adapter"]' (JSON stdin / JSON stdout).
208
211
  --cwd PATH selects the agent working directory; --name isolates multiple agents.
212
+ Installs automatic startup on macOS/Linux. --no-service opts into external supervision.
209
213
  status [--name NAME]
210
214
  Show broker connection and verified local agent health separately.
211
215
  verify [--name NAME]
@@ -217,8 +221,10 @@ Commands:
217
221
  identity restore --snapshot ID --output NEW_FOLDER [--connection NAME]
218
222
  invite [--name NAME] [--app-url URL]
219
223
  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.
224
+ bridge [start|ensure|stop|restart|repair|list|ps] [--name NAME] [--detach]
225
+ Manage only the named agent's bridge. Repair restores supervision without re-pairing.
226
+ bridge service [install|start|stop|restart|status|uninstall] [--name NAME]
227
+ Manage automatic restart and startup (macOS launchd / Linux systemd user).
222
228
 
223
229
  init
224
230
  Install Oomi agent instructions into OpenClaw AGENTS.md.
@@ -229,7 +235,7 @@ Commands:
229
235
  openclaw bridge [start|ensure|stop|restart|ps]
230
236
  Manage local OpenClaw-to-Oomi bridge lifecycle (singleton).
231
237
  openclaw bridge service [install|start|stop|restart|status|uninstall]
232
- Manage macOS launchd bridge supervision.
238
+ Legacy alias for bridge service, with the same connection isolation.
233
239
  openclaw profile init
234
240
  Write a deterministic OpenClaw profile for local/dev or hosted setup flows.
235
241
  openclaw profile apply
@@ -265,12 +271,18 @@ Commands:
265
271
  updates
266
272
  Read quiet state updates; does not start a model turn.
267
273
  spaces list|catalog|show <id>|revisions <id>
268
- spaces create|update|archive|unarchive|restore [id] --file PATH
274
+ spaces create|update|archive|unarchive|restore|upgrade [id] --file PATH
275
+ [--schema-version space.v3] Required for goals/streaks; v2 remains supported for sources
276
+ objectives list|show <id>
277
+ objectives history <id> [--before YYYY-MM-DD]
278
+ objectives create|update|respond [id] --file PATH
279
+ [--source-message ID] Owner instruction required for agent-recorded progress/corrections
269
280
  Choose components from catalog. Writes require idempotencyKey and updates require expectedRevision.
270
281
 
271
282
  context health [--window day|week|month|year|all|range] [--date YYYY-MM-DD]
272
283
  [--from YYYY-MM-DD --to YYYY-MM-DD] [--shape summary|series|both]
273
284
  [--group-by day|week|month|year] [--timezone IANA-zone] [--cursor YYYY-MM-DD]
285
+ [--provider all|apple_health|smart_scale]
274
286
  [--metric steps|weight|sleep|canonical.metric] [--output PATH]
275
287
  Show the latest account-linked health context available to this paired OpenClaw device.
276
288
  context plugins [--plugin apple_calendar|apple_reminders|apple_contacts|apple_location|apple_notes|google_gmail|google_drive|google_calendar|bluetooth_devices]
@@ -623,6 +635,7 @@ async function handleContextHealthCommand(flags = {}) {
623
635
  const payload = await client.getHealthContext({
624
636
  window: flags.window, date: flags.date, from: flags.from, to: flags.to,
625
637
  shape: flags.shape, groupBy: flags['group-by'], timezone: flags.timezone, cursor: flags.cursor,
638
+ provider: flags.provider,
626
639
  });
627
640
  const result = flags.metric ? compactHealth(payload, flags.metric) : payload;
628
641
  if (flags.output) {
@@ -692,19 +705,16 @@ async function handleContextHealthSyncCommand(flags = {}) {
692
705
 
693
706
  async function refreshBridgeForUpdate(flags = {}, options = {}) {
694
707
  const logger = options.logger || null;
695
- if (process.platform === 'darwin') {
696
- const launchdStatus = readBridgeLaunchdStatus();
697
- if (launchdStatus.installed) {
698
- logger?.(`Restarting launchd-managed bridge ${launchdStatus.target}.`);
699
- await stopBridgeLaunchdService();
700
- startBridgeLaunchdService();
701
- incrementBridgeMetric('bridge_restart_count');
702
- return {
703
- restarted: true,
704
- mode: 'service',
705
- target: launchdStatus.target,
706
- };
707
- }
708
+ const service = currentBridgeService();
709
+ const deferred = deferLiveRestart({ print: false });
710
+ if (deferred) return { queued: true, maintenance: deferred };
711
+ assertIndependentMaintenance();
712
+ if (service.status().installed) {
713
+ logger?.(`Restarting supervised bridge ${service.config.target}.`);
714
+ await stopSupervisedBridge(service);
715
+ service.start({ rewrite: true });
716
+ incrementBridgeMetric('bridge_restart_count');
717
+ return { restarted: true, mode: 'service', target: service.config.target };
708
718
  }
709
719
 
710
720
  const running = findRunningBridgeProcess();
@@ -766,6 +776,7 @@ async function handleOpenclawRefreshCommand(flags = {}) {
766
776
  return;
767
777
  }
768
778
 
779
+ if (payload.bridge.queued) { console.log(payload.bridge.maintenance.message); return; }
769
780
  console.log(`Oomi refresh complete (${payload.currentVersion})`);
770
781
  if (payload.bridge.restarted) {
771
782
  console.log(`Bridge restarted (${payload.bridge.mode}).`);
@@ -843,8 +854,15 @@ function resolveBridgeLiveLogPath() {
843
854
  return bridgeConnectionName ? path.join(connectionDirectory(), 'logs', 'oomi-bridge-live.log') : resolveOpenclawBridgeLiveLogPath();
844
855
  }
845
856
 
846
- function resolveBridgeLaunchAgentPlistPath() {
847
- return path.join(os.homedir(), 'Library', 'LaunchAgents', `${BRIDGE_LAUNCHD_LABEL}.plist`);
857
+ function currentBridgeService() {
858
+ return createBridgeService(bridgeServiceConfig({
859
+ name: bridgeConnectionName, stateDirectory: path.dirname(resolveBridgeStatePath()),
860
+ logPath: resolveBridgeLiveLogPath(), scriptPath: fileURLToPath(import.meta.url),
861
+ }));
862
+ }
863
+
864
+ function buildBridgeLaunchAgentPlist() {
865
+ return renderLaunchAgent(currentBridgeService().config);
848
866
  }
849
867
 
850
868
  function defaultDeviceId() {
@@ -2418,142 +2436,6 @@ async function stopBridgeProcesses() {
2418
2436
  };
2419
2437
  }
2420
2438
 
2421
- function assertMacOSLaunchdAvailable() {
2422
- if (process.platform !== 'darwin') {
2423
- throw new Error('Bridge service manager is only supported on macOS (launchd).');
2424
- }
2425
- if (typeof process.getuid !== 'function') {
2426
- throw new Error('Cannot resolve current UID for launchd domain.');
2427
- }
2428
- }
2429
-
2430
- function launchctlDomain() {
2431
- assertMacOSLaunchdAvailable();
2432
- return `gui/${String(process.getuid())}`;
2433
- }
2434
-
2435
- function launchctlServiceTarget() {
2436
- return `${launchctlDomain()}/${BRIDGE_LAUNCHD_LABEL}`;
2437
- }
2438
-
2439
- function runLaunchctl(args, { allowFailure = false } = {}) {
2440
- const result = spawnSync('launchctl', args, {
2441
- encoding: 'utf8',
2442
- stdio: ['ignore', 'pipe', 'pipe'],
2443
- });
2444
- const status = Number.isInteger(result.status) ? result.status : 1;
2445
- const stdout = String(result.stdout || '').trim();
2446
- const stderr = String(result.stderr || '').trim();
2447
- if (status !== 0 && !allowFailure) {
2448
- throw new Error(
2449
- `launchctl ${args.join(' ')} failed (${status}): ${stderr || stdout || 'unknown launchctl error'}`
2450
- );
2451
- }
2452
- return { status, stdout, stderr };
2453
- }
2454
-
2455
- function buildBridgeLaunchAgentPlist() {
2456
- const scriptPath = (() => {
2457
- try {
2458
- return fs.realpathSync(process.argv[1]);
2459
- } catch {
2460
- return process.argv[1];
2461
- }
2462
- })();
2463
- const programArgs = [process.execPath, scriptPath, 'openclaw', 'bridge', 'start', '--service-managed'];
2464
- const bridgeLogPath = resolveBridgeLiveLogPath();
2465
- const argsXml = programArgs.map((arg) => `<string>${xmlEscape(arg)}</string>`).join('\n ');
2466
- const openclawHome = resolveOpenclawHome();
2467
- const openclawWorkspace = resolveOpenclawWorkspaceRoot();
2468
- const envVars = {
2469
- OOMI_SKIP_UPDATE_CHECK: '1',
2470
- };
2471
- if (process.env.OPENCLAW_HOME) {
2472
- envVars.OPENCLAW_HOME = openclawHome;
2473
- }
2474
- if (process.env.OPENCLAW_WORKSPACE) {
2475
- envVars.OPENCLAW_WORKSPACE = openclawWorkspace;
2476
- }
2477
- const envXml = Object.entries(envVars)
2478
- .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
2479
- .join('\n');
2480
-
2481
- return `<?xml version="1.0" encoding="UTF-8"?>
2482
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2483
- <plist version="1.0">
2484
- <dict>
2485
- <key>Label</key>
2486
- <string>${xmlEscape(BRIDGE_LAUNCHD_LABEL)}</string>
2487
- <key>ProgramArguments</key>
2488
- <array>
2489
- ${argsXml}
2490
- </array>
2491
- <key>WorkingDirectory</key>
2492
- <string>${xmlEscape(openclawHome)}</string>
2493
- <key>RunAtLoad</key>
2494
- <true/>
2495
- <key>KeepAlive</key>
2496
- <true/>
2497
- <key>ThrottleInterval</key>
2498
- <integer>5</integer>
2499
- <key>EnvironmentVariables</key>
2500
- <dict>
2501
- ${envXml}
2502
- </dict>
2503
- <key>StandardOutPath</key>
2504
- <string>${xmlEscape(bridgeLogPath)}</string>
2505
- <key>StandardErrorPath</key>
2506
- <string>${xmlEscape(bridgeLogPath)}</string>
2507
- </dict>
2508
- </plist>
2509
- `;
2510
- }
2511
-
2512
- function readBridgeLaunchdStatus() {
2513
- assertMacOSLaunchdAvailable();
2514
- const plistPath = resolveBridgeLaunchAgentPlistPath();
2515
- const target = launchctlServiceTarget();
2516
- const printResult = runLaunchctl(['print', target], { allowFailure: true });
2517
- const loaded = printResult.status === 0;
2518
- const output = [printResult.stdout, printResult.stderr].filter(Boolean).join('\n');
2519
- const pidMatch = output.match(/\bpid\s*=\s*(\d+)/);
2520
- const lastExitMatch = output.match(/\blast exit code\s*=\s*(-?\d+)/i);
2521
-
2522
- return {
2523
- plistPath,
2524
- target,
2525
- installed: fs.existsSync(plistPath),
2526
- loaded,
2527
- pid: pidMatch ? Number(pidMatch[1]) : null,
2528
- running: Boolean(pidMatch && Number(pidMatch[1]) > 0),
2529
- lastExitCode: lastExitMatch ? Number(lastExitMatch[1]) : null,
2530
- printOutput: output,
2531
- };
2532
- }
2533
-
2534
- function startBridgeLaunchdService() {
2535
- assertMacOSLaunchdAvailable();
2536
- const plistPath = resolveBridgeLaunchAgentPlistPath();
2537
- if (!fs.existsSync(plistPath)) {
2538
- throw new Error('Bridge service is not installed. Run: oomi openclaw bridge service install');
2539
- }
2540
- writeFile(plistPath, buildBridgeLaunchAgentPlist());
2541
- const domain = launchctlDomain();
2542
- const target = launchctlServiceTarget();
2543
- runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
2544
- runLaunchctl(['bootstrap', domain, plistPath]);
2545
- runLaunchctl(['enable', target], { allowFailure: true });
2546
- runLaunchctl(['kickstart', '-k', target], { allowFailure: true });
2547
- }
2548
-
2549
- async function stopBridgeLaunchdService() {
2550
- assertMacOSLaunchdAvailable();
2551
- const plistPath = resolveBridgeLaunchAgentPlistPath();
2552
- const domain = launchctlDomain();
2553
- runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
2554
- return stopBridgeProcesses();
2555
- }
2556
-
2557
2439
  async function resolveBridgeRuntimeConfig(flags, bridgeState) {
2558
2440
  const explicitBrokerHttp = String(flags['broker-http'] || '').trim();
2559
2441
  const explicitBrokerWs = String(flags['broker-ws'] || '').trim();
@@ -2706,15 +2588,24 @@ async function startOpenclawBridge(flags) {
2706
2588
  consecutiveFailures: 0,
2707
2589
  pid: process.pid,
2708
2590
  startedAt: bridgeNowIso(),
2591
+ packageVersion: currentPackageVersion(),
2592
+ maintenanceProtocol: 1,
2709
2593
  });
2710
2594
 
2595
+ try { refreshSelectedSkill(bridgeState); } catch (err) { console.warn('Oomi skill refresh failed:', err.message); }
2711
2596
  console.log(`Starting ${bridgeState.runtime || 'openclaw'} bridge: device=${deviceId}`);
2597
+ let backgroundTurns = 0;
2598
+ const deliverAgentEvent = async (turn) => {
2599
+ backgroundTurns++;
2600
+ try { return await (localRuntime ? localRuntime.turn(turn) : probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway, turn, timeoutMs: 180000 })); }
2601
+ finally { backgroundTurns--; }
2602
+ };
2712
2603
  const stopChannelWorker = localRuntime ? startChannelWorker(bridgeState, localRuntime) : () => {};
2713
2604
  const stopIdentityWorker = startIdentityWorker({
2714
2605
  state: { ...bridgeState, brokerHttp, deviceToken },
2715
2606
  receiptPath: path.join(path.dirname(resolveBridgeStatePath()), 'oomi-identity-receipt.json'),
2716
2607
  // Applying a specific settings revision is idempotent, unlike a user turn.
2717
- deliver: (turn) => localRuntime ? localRuntime.turn({ ...turn, retryInterrupted: false }) : probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway, turn, timeoutMs: 180000 }),
2608
+ deliver: deliverAgentEvent,
2718
2609
  acknowledgeCommand: (identity) => `oomi identity acknowledge${bridgeConnectionName ? ` --connection ${bridgeConnectionName}` : ''} --identity-id ${identity.identityId} --revision ${identity.revision}`,
2719
2610
  onError: () => updateBridgeStatus({ identitySyncError: 'Identity notice was interrupted. Read oomi identity get in your agent to apply and acknowledge the pending revision; no model retry was sent.' }),
2720
2611
  });
@@ -2722,7 +2613,7 @@ async function startOpenclawBridge(flags) {
2722
2613
  state: { ...bridgeState, brokerHttp, deviceToken },
2723
2614
  connection: bridgeConnectionName,
2724
2615
  receiptPath: path.join(path.dirname(resolveBridgeStatePath()), 'oomi-routine-receipt.json'),
2725
- deliver: (turn) => localRuntime ? localRuntime.turn(turn) : probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway, turn, timeoutMs: 180000 }),
2616
+ deliver: deliverAgentEvent,
2726
2617
  onError: () => updateBridgeStatus({ routineSyncError: 'Routine updates are waiting for the runtime. They will retry.' }),
2727
2618
  });
2728
2619
  console.log(`Local gateway: ${gateway.gatewayUrl}`);
@@ -2732,6 +2623,19 @@ async function startOpenclawBridge(flags) {
2732
2623
  );
2733
2624
 
2734
2625
  const activeGatewaySockets = new Map();
2626
+ const activeGatewayRuns = new Set();
2627
+ let currentBrokerSocket;
2628
+ const maintenanceTick = createMaintenanceController({
2629
+ directory: path.dirname(resolveBridgeStatePath()),
2630
+ isIdle: () => !backgroundTurns && !localRuntime?.active.size && !localRuntime?.deliveries && !activeGatewayRuns.size
2631
+ && !currentBrokerSocket?.bufferedAmount
2632
+ && [...activeGatewaySockets.values()].every((session) => !session.pendingRequests?.size),
2633
+ isConnected: () => readBridgeStatus().status === 'connected',
2634
+ restart: () => currentBridgeService().restart(),
2635
+ });
2636
+ const maintenanceTimer = setInterval(() => {
2637
+ try { maintenanceTick(); } catch (err) { console.error('[bridge] maintenance:', err.message); }
2638
+ }, 250);
2735
2639
  const reconnectState = {
2736
2640
  attempt: 0,
2737
2641
  timer: null,
@@ -3084,6 +2988,7 @@ async function startOpenclawBridge(flags) {
3084
2988
  wsUrl.searchParams.set('token', deviceToken);
3085
2989
 
3086
2990
  const brokerSocket = new WebSocket(wsUrl.toString());
2991
+ currentBrokerSocket = brokerSocket;
3087
2992
  let actionCableHeartbeat = null;
3088
2993
 
3089
2994
  const clearChallengeTimer = (sessionBridge) => {
@@ -3446,6 +3351,12 @@ async function startOpenclawBridge(flags) {
3446
3351
  }
3447
3352
  }
3448
3353
 
3354
+ if (gatewayPayload?.type === 'res' && gatewayPayload.ok && gatewayPayload.payload?.status === 'started' && gatewayPayload.payload?.runId) {
3355
+ activeGatewayRuns.add(gatewayPayload.payload.runId);
3356
+ }
3357
+ if (gatewayPayload?.event === 'chat' && ['final', 'error', 'aborted'].includes(gatewayPayload.payload?.state)) {
3358
+ activeGatewayRuns.delete(gatewayPayload.payload.runId);
3359
+ }
3449
3360
  if (gatewayPayload?.event === 'chat' && gatewayPayload.payload?.state === 'final') {
3450
3361
  updateBridgeStatus({ runtimeReady: true, lastReplyAt: bridgeNowIso() });
3451
3362
  } else if (gatewayPayload?.event === 'chat' && gatewayPayload.payload?.state === 'error') {
@@ -3899,6 +3810,7 @@ async function startOpenclawBridge(flags) {
3899
3810
 
3900
3811
  const markStopped = async (signal) => {
3901
3812
  reconnectState.stopped = true;
3813
+ clearInterval(maintenanceTimer);
3902
3814
  stopChannelWorker();
3903
3815
  stopIdentityWorker();
3904
3816
  stopRoutineWorker();
@@ -4022,11 +3934,6 @@ async function pairAndStartOpenclawBridge(flags) {
4022
3934
  console.log('');
4023
3935
  console.log('Share this exact response with the user (no extra text):');
4024
3936
  console.log('----------------------------------------');
4025
- const skillSource = resolveSkillSource();
4026
- if (runtime === 'hermes') installSkill(skillSource, [path.join(state.localAgent.hermesHome, ...(state.localAgent.profile === 'default' ? [] : ['profiles', state.localAgent.profile]), 'skills')]);
4027
- else if (runtime === 'openclaw') installSkill(skillSource, [resolveOpenclawSkillsDir()]);
4028
- else if (flags['skills-dir']) installSkill(skillSource, [flags['skills-dir']]);
4029
- console.log('Oomi tools: run oomi capabilities, then oomi instructions. Add --connection ' + (bridgeConnectionName || '(omit for default)') + ' to commands. Passive updates do not invoke your model.');
4030
3937
  console.log('Oomi Connect Ready');
4031
3938
  console.log(`Auth Link: ${inviteUrl}`);
4032
3939
  console.log('----------------------------------------');
@@ -4142,6 +4049,8 @@ function printOpenclawBridgeStatus(flags) {
4142
4049
  expiresAt: bridgeState.expiresAt || null,
4143
4050
  },
4144
4051
  runtime: runtimeStatus,
4052
+ supervision: currentBridgeService().status(),
4053
+ maintenance: readMaintenance(path.dirname(resolveBridgeStatePath())),
4145
4054
  };
4146
4055
 
4147
4056
  if (jsonOutput) {
@@ -4149,7 +4058,12 @@ function printOpenclawBridgeStatus(flags) {
4149
4058
  return;
4150
4059
  }
4151
4060
 
4061
+ if (payload.maintenance) console.log(`Maintenance: ${payload.maintenance.status} — ${payload.maintenance.message || ''}`);
4152
4062
  console.log('Oomi Bridge Status');
4063
+ const supervision = payload.supervision;
4064
+ console.log(`Automatic startup: ${supervision.installed && supervision.enabled ? supervision.manager + ' (' + supervision.startsAt + ')' : 'off'}`);
4065
+ if (!supervision.installed || !supervision.enabled || !supervision.running) console.log(`Repair on this computer: oomi bridge repair${bridgeConnectionName ? ' --name ' + bridgeConnectionName : ''}`);
4066
+ if (supervision.reason) console.log(supervision.reason);
4153
4067
  console.log('------------------');
4154
4068
  console.log(`Bridge state: ${payload.bridgeStatePath}`);
4155
4069
  console.log(`Runtime status: ${payload.bridgeStatusPath}`);
@@ -4261,82 +4175,111 @@ function printOpenclawPluginSetup(flags) {
4261
4175
  }
4262
4176
  }
4263
4177
 
4264
- async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
4265
- assertMacOSLaunchdAvailable();
4266
- const action = String(actionRaw || 'status').trim().toLowerCase();
4267
- const plistPath = resolveBridgeLaunchAgentPlistPath();
4268
-
4269
- if (action === 'install') {
4270
- ensureDir(path.dirname(plistPath));
4271
- writeFile(plistPath, buildBridgeLaunchAgentPlist());
4272
- console.log(`Installed bridge launchd plist: ${plistPath}`);
4273
- if (isTruthyFlag(flags['no-start'])) {
4274
- console.log('Service install complete. Start with: oomi openclaw bridge service start');
4275
- return;
4178
+ function listLocalBridgeConnections() {
4179
+ const selected = bridgeConnectionName;
4180
+ const names = fs.existsSync(resolveOpenclawBridgeStatePath()) ? [''] : [];
4181
+ const directory = process.env.OOMI_CONNECTIONS_DIR || path.join(os.homedir(), '.oomi', 'connections');
4182
+ if (fs.existsSync(directory)) {
4183
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
4184
+ if (entry.isDirectory() && /^[a-z0-9][a-z0-9_-]{0,63}$/.test(entry.name)
4185
+ && fs.existsSync(path.join(directory, entry.name, 'oomi-bridge.json'))) names.push(entry.name);
4276
4186
  }
4277
- startBridgeLaunchdService();
4278
- incrementBridgeMetric('bridge_start_count');
4279
- console.log(`Bridge service started: ${launchctlServiceTarget()}`);
4280
- return;
4281
4187
  }
4188
+ try {
4189
+ return names.sort().map((name) => {
4190
+ bridgeConnectionName = name;
4191
+ const state = readBridgeState();
4192
+ const running = findRunningBridgeProcess();
4193
+ return { name: name || null, runtime: state.runtime || 'openclaw', deviceId: state.deviceId || null,
4194
+ running: Boolean(running), supervision: currentBridgeService().status(),
4195
+ repairCommand: `oomi bridge repair${name ? ' --name ' + name : ''}` };
4196
+ });
4197
+ } finally { bridgeConnectionName = selected; }
4198
+ }
4282
4199
 
4283
- if (action === 'uninstall') {
4284
- await stopBridgeLaunchdService();
4285
- if (fs.existsSync(plistPath)) {
4286
- fs.unlinkSync(plistPath);
4287
- }
4288
- console.log(`Removed bridge launchd plist: ${plistPath}`);
4289
- return;
4290
- }
4200
+ function requireSavedBridge() {
4201
+ if (!readBridgeState().deviceToken) throw new Error('No saved connection. Run oomi connect with an explicit runtime first.');
4202
+ }
4291
4203
 
4292
- if (action === 'start') {
4293
- startBridgeLaunchdService();
4294
- incrementBridgeMetric('bridge_start_count');
4295
- console.log(`Bridge service started: ${launchctlServiceTarget()}`);
4296
- return;
4204
+ function assertIndependentMaintenance() {
4205
+ const running = findRunningBridgeProcess();
4206
+ if (running && isWorkerDescendant(running.pid)) {
4207
+ throw new Error('This command would stop the agent carrying this conversation. Use bridge repair/restart with a running 0.8.1+ bridge, or perform the first upgrade from an independent host terminal. The bridge was left unchanged.');
4297
4208
  }
4209
+ }
4298
4210
 
4299
- if (action === 'stop') {
4300
- const stopped = await stopBridgeLaunchdService();
4301
- if (Array.isArray(stopped.found) && stopped.found.length > 0) {
4302
- console.log(`Stopped bridge workers: ${stopped.stopped.join(', ') || 'none'}.`);
4303
- } else {
4304
- console.log('No bridge workers running.');
4305
- }
4306
- console.log(`Bridge service stopped: ${launchctlServiceTarget()}`);
4211
+ function deferLiveRestart({ print = true } = {}) {
4212
+ const status = readBridgeStatus();
4213
+ const service = currentBridgeService().status();
4214
+ if (status.maintenanceProtocol !== 1 || !status.pid || !isBridgeProcess(status.pid)
4215
+ || !service.running || service.pid !== status.pid || !service.enabled) return false;
4216
+ const request = queueRestart(path.dirname(resolveBridgeStatePath()), status.pid);
4217
+ if (print) console.log(JSON.stringify(request, null, 2));
4218
+ return request;
4219
+ }
4220
+
4221
+ async function stopSupervisedBridge(service) {
4222
+ assertIndependentMaintenance();
4223
+ // Unload/disable before signalling the worker, otherwise KeepAlive immediately recreates it.
4224
+ service.stop();
4225
+ const stopped = await stopBridgeProcesses();
4226
+ if (stopped.stillAlive?.length) throw new Error('The existing bridge did not stop. Resolve it before reconnecting.');
4227
+ return stopped;
4228
+ }
4229
+
4230
+ async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
4231
+ const action = String(actionRaw || 'status').trim().toLowerCase();
4232
+ const service = currentBridgeService();
4233
+ if (action === 'status') {
4234
+ console.log(JSON.stringify(service.status(), null, 2));
4307
4235
  return;
4308
4236
  }
4309
-
4310
- if (action === 'restart') {
4311
- await stopBridgeLaunchdService();
4312
- startBridgeLaunchdService();
4237
+ if (!['install', 'start', 'stop', 'restart', 'uninstall'].includes(action)) {
4238
+ throw new Error('Use: oomi bridge service [install|start|stop|restart|status|uninstall] [--name NAME]');
4239
+ }
4240
+ if (['install', 'start', 'restart'].includes(action)) requireSavedBridge();
4241
+ if (['install', 'restart'].includes(action) && !isTruthyFlag(flags['no-start']) && deferLiveRestart()) return;
4242
+ if (action !== 'start') assertIndependentMaintenance();
4243
+ if (action === 'install') {
4244
+ service.install({ noStart: true });
4245
+ await stopSupervisedBridge(service);
4246
+ if (!isTruthyFlag(flags['no-start'])) service.start();
4247
+ } else if (action === 'start') {
4248
+ service.start(); // Idempotent: do not interrupt a healthy worker.
4249
+ } else if (action === 'restart') {
4250
+ await stopSupervisedBridge(service);
4251
+ service.start({ rewrite: true });
4313
4252
  incrementBridgeMetric('bridge_restart_count');
4314
- console.log(`Bridge service restarted: ${launchctlServiceTarget()}`);
4315
- return;
4253
+ } else if (action === 'stop') {
4254
+ await stopSupervisedBridge(service);
4255
+ } else if (action === 'uninstall') {
4256
+ await stopSupervisedBridge(service);
4257
+ service.uninstall();
4316
4258
  }
4259
+ const result = service.status();
4260
+ console.log(JSON.stringify({ action, ...result }, null, 2));
4261
+ }
4317
4262
 
4318
- if (action === 'status') {
4319
- const status = readBridgeLaunchdStatus();
4320
- console.log('Bridge Service Status');
4321
- console.log('---------------------');
4322
- console.log(`Label: ${BRIDGE_LAUNCHD_LABEL}`);
4323
- console.log(`Target: ${status.target}`);
4324
- console.log(`Plist: ${status.plistPath}`);
4325
- console.log(`Installed: ${status.installed ? 'yes' : 'no'}`);
4326
- console.log(`Loaded: ${status.loaded ? 'yes' : 'no'}`);
4327
- console.log(`Running: ${status.running ? 'yes' : 'no'}`);
4328
- if (status.pid) {
4329
- console.log(`PID: ${status.pid}`);
4330
- }
4331
- if (status.lastExitCode !== null) {
4332
- console.log(`Last exit code: ${status.lastExitCode}`);
4333
- }
4334
- return;
4263
+ async function repairBridge(flags = {}) {
4264
+ requireSavedBridge();
4265
+ if (!isTruthyFlag(flags['no-start']) && deferLiveRestart()) return;
4266
+ await handleBridgeServiceCommand('install', flags);
4267
+ if (!isTruthyFlag(flags['no-start'])) {
4268
+ await waitForBridgeConnection({});
4269
+ console.log('Oomi bridge restored with automatic startup. Pairing and conversations preserved.');
4335
4270
  }
4271
+ }
4336
4272
 
4337
- throw new Error(
4338
- `Unknown bridge service action: ${action}. Use: oomi openclaw bridge service [install|start|stop|restart|status|uninstall]`
4339
- );
4273
+ async function waitForBridgeConnection(child) {
4274
+ const deadline = Date.now() + 20000;
4275
+ while (Date.now() < deadline) {
4276
+ const status = readBridgeStatus();
4277
+ if (status.pid && (!child.pid || status.pid === child.pid) && isPidAlive(status.pid) && status.status === 'connected') return;
4278
+ if ((!child.pid || status.pid === child.pid) && status.status === 'error') throw new Error(status.lastErrorMessage || 'Bridge startup failed.');
4279
+ if (child.pid && !isPidAlive(child.pid)) throw new Error(`Bridge exited. Check ${resolveBridgeLiveLogPath()}.`);
4280
+ await delay(200);
4281
+ }
4282
+ throw new Error(`The bridge did not connect yet. Automatic recovery may still be retrying. Check oomi status${bridgeConnectionName ? ' --name ' + bridgeConnectionName : ''} and ${resolveBridgeLiveLogPath()}.`);
4340
4283
  }
4341
4284
 
4342
4285
  async function startBridgeLifecycle(flags = {}) {
@@ -4345,6 +4288,16 @@ async function startBridgeLifecycle(flags = {}) {
4345
4288
  throw new Error('Detached bridge mode cannot be combined with --service-managed.');
4346
4289
  }
4347
4290
 
4291
+ if (!serviceManaged) {
4292
+ const service = currentBridgeService();
4293
+ if (service.status().installed) {
4294
+ if (isTruthyFlag(flags.detach)) throw new Error('This connection has a supervisor. Use bridge start, or uninstall its service before using --detach.');
4295
+ service.start();
4296
+ console.log(`Bridge service started: ${service.config.target}`);
4297
+ return;
4298
+ }
4299
+ }
4300
+
4348
4301
  if (Boolean(flags.detach)) {
4349
4302
  const detachedFlags = { ...flags };
4350
4303
  delete detachedFlags.detach;
@@ -4386,6 +4339,14 @@ async function startBridgeLifecycle(flags = {}) {
4386
4339
  async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
4387
4340
  const action = String(actionRaw || 'start').trim().toLowerCase();
4388
4341
 
4342
+ if (action === 'list') { console.log(JSON.stringify(listLocalBridgeConnections(), null, 2)); return; }
4343
+ if (action === 'repair') return repairBridge(flags);
4344
+ if (['stop', 'restart'].includes(action) && currentBridgeService().status().installed) {
4345
+ return handleBridgeServiceCommand(action, flags);
4346
+ }
4347
+
4348
+ if (['stop', 'restart'].includes(action)) assertIndependentMaintenance();
4349
+
4389
4350
  if (action === 'start' || action === 'ensure') {
4390
4351
  await startBridgeLifecycle(flags);
4391
4352
  return;
@@ -4436,7 +4397,7 @@ async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
4436
4397
  }
4437
4398
 
4438
4399
  throw new Error(
4439
- `Unknown bridge action: ${action}. Use: oomi openclaw bridge [start|ensure|stop|restart|ps]`
4400
+ `Unknown bridge action: ${action}. Use: oomi bridge [start|ensure|stop|restart|repair|list|ps]`
4440
4401
  );
4441
4402
  }
4442
4403
 
@@ -4459,10 +4420,26 @@ async function verifyLocalAgent(state) {
4459
4420
  return { runtime: state.runtime || 'openclaw', ...result };
4460
4421
  }
4461
4422
 
4423
+ function refreshSelectedSkill(state, flags = {}) {
4424
+ const source = resolveSkillSource();
4425
+ if (state.runtime === 'hermes' && state.localAgent?.hermesHome) {
4426
+ installSkill(source, [path.join(state.localAgent.hermesHome, ...(state.localAgent.profile === 'default' ? [] : ['profiles', state.localAgent.profile]), 'skills')]);
4427
+ } else if (!state.runtime || state.runtime === 'openclaw') installSkill(source, [resolveOpenclawSkillsDir()]);
4428
+ else if (flags['skills-dir']) installSkill(source, [flags['skills-dir']]);
4429
+ }
4430
+
4462
4431
  async function connectLocalAgent(flags) {
4432
+ assertIndependentMaintenance();
4463
4433
  const agentName = flags['agent-name'] === undefined ? undefined : validateAgentName(flags['agent-name']);
4464
4434
  const runtime = String(flags.runtime || '');
4465
4435
  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.');
4436
+ const service = currentBridgeService();
4437
+ const unmanaged = isTruthyFlag(flags['no-service']);
4438
+ if (!unmanaged) {
4439
+ service.preflight();
4440
+ } else if (service.status().installed) {
4441
+ throw new Error('This connection already has automatic startup. Uninstall its service before using --no-service.');
4442
+ }
4466
4443
  const previous = readBridgeState();
4467
4444
  if (previous.runtime && previous.runtime !== runtime) throw new Error('This connection belongs to a different runtime. Choose a different --name.');
4468
4445
  const appUrl = String(flags['app-url'] || previous.appUrl || 'https://www.oomi.ai').replace(/\/$/, '');
@@ -4513,24 +4490,18 @@ async function connectLocalAgent(flags) {
4513
4490
  if (error.status !== 404 || agentName) throw error;
4514
4491
  console.warn('This Oomi server does not support agent settings yet. Connecting without shared settings.');
4515
4492
  }
4516
- // Stop this connection only; never leave a worker using an old adapter after a change.
4517
- const supervised = !bridgeConnectionName && process.platform === 'darwin' && readBridgeLaunchdStatus().installed;
4518
- const stopped = supervised ? await stopBridgeLaunchdService() : await stopBridgeProcesses();
4493
+ // Install/update only this connection's supervisor, retaining its saved identity and session.
4494
+ if (!unmanaged) service.install({ noStart: true });
4495
+ const stopped = unmanaged ? await stopBridgeProcesses() : await stopSupervisedBridge(service);
4519
4496
  if (stopped.stillAlive?.length) throw new Error('The existing bridge did not stop. Resolve it before reconnecting.');
4520
4497
  writeBridgeState(state);
4521
4498
  updateBridgeStatus({ status: 'starting', runtime, runtimeReady: true, verifiedAt: state.verifiedAt, lastErrorMessage: '' });
4522
- const child = supervised ? (startBridgeLaunchdService(), { pid: null }) : startBridgeDetachedProcess({ 'app-url': appUrl });
4523
- const deadline = Date.now() + 20000;
4524
- let connected = false;
4525
- while (Date.now() < deadline) {
4526
- const status = readBridgeStatus();
4527
- if (status.pid && (!child.pid || status.pid === child.pid) && status.status === 'connected') { connected = true; break; }
4528
- if ((!child.pid || status.pid === child.pid) && status.status === 'error') throw new Error(status.lastErrorMessage || 'Bridge startup failed.');
4529
- if (child.pid && !isPidAlive(child.pid)) throw new Error(`Bridge exited. Check ${resolveBridgeLiveLogPath()}.`);
4530
- await delay(200);
4531
- }
4532
- if (!connected) throw new Error('The local agent replied, but the Oomi bridge did not connect. Run oomi status for this connection.');
4499
+ const child = unmanaged ? startBridgeDetachedProcess({ 'app-url': appUrl }) : (service.start(), {});
4500
+ await waitForBridgeConnection(child);
4501
+ console.log(unmanaged ? 'Automatic startup: off (--no-service); your host supervisor must manage recovery.'
4502
+ : `Automatic startup: ${service.config.platform === 'darwin' ? 'macOS login service' : 'Linux user service'}; restarts after worker exits.`);
4533
4503
  const invite = await requestConnectInviteLink({ backendHttp: state.brokerHttp, appUrl, sessionKey: state.sessionKey, deviceToken: state.deviceToken });
4504
+ refreshSelectedSkill(state, flags);
4534
4505
  console.log(`Verified agent: ${runtime}${state.localAgent?.profile ? ` (${state.localAgent.profile})` : ''}`);
4535
4506
  console.log('Oomi Connect Ready');
4536
4507
  console.log(`Auth Link: ${invite.inviteUrl}`);
@@ -4552,11 +4523,13 @@ async function main() {
4552
4523
  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.');
4553
4524
  bridgeConnectionName = connection || '';
4554
4525
 
4526
+ if (command === 'update') { console.log(JSON.stringify(planPackageUpdate(currentPackageVersion(), await fetchLatestPublishedVersion('oomi-ai')), null, 2)); return; }
4555
4527
  if (command === 'instructions') { console.log(fs.readFileSync(resolveInstructionsFile(), 'utf8')); return; }
4556
4528
  if (command === 'capabilities') {
4557
- console.log(JSON.stringify({ instructions: 'oomi instructions', skill: path.join(PACKAGE_ROOT, 'skills/oomi/SKILL.md'), connectionSelector: '--connection NAME',
4558
- 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' },
4529
+ console.log(JSON.stringify({ instructions: 'oomi instructions', packageUpdate: 'oomi update', skill: path.join(PACKAGE_ROOT, 'skills/oomi/SKILL.md'), connectionSelector: '--connection NAME',
4530
+ 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' },
4559
4531
  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' },
4532
+ 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' },
4560
4533
  routines: 'oomi routines list; oomi routines context', identity: 'oomi identity get', updates: 'oomi updates',
4561
4534
  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' },
4562
4535
  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' }
@@ -4567,6 +4540,7 @@ async function main() {
4567
4540
  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;
4568
4541
  }
4569
4542
  if (command === 'spaces') return handleSpacesCommand(readBridgeState(), subcommand || 'list', args.flags, args.positionals);
4543
+ if (command === 'objectives') return handleObjectivesCommand(readBridgeState(), subcommand || 'list', args.flags, args.positionals);
4570
4544
  if (command === 'connect') return connectLocalAgent(args.flags);
4571
4545
  if (command === 'identity') return handleIdentityCommand(readBridgeState(), subcommand, args.flags);
4572
4546
  if (command === 'routines') return handleRoutinesCommand(readBridgeState(), subcommand, args.flags, args.positionals);
@@ -4586,7 +4560,10 @@ async function main() {
4586
4560
  }
4587
4561
  if (command === 'status') return printOpenclawBridgeStatus(args.flags);
4588
4562
  if (command === 'invite') return createOpenclawInviteLink(args.flags);
4589
- if (command === 'bridge') return handleBridgeLifecycleCommand(args.flags, subcommand || 'start');
4563
+ if (command === 'bridge') {
4564
+ if (subcommand === 'service') return handleBridgeServiceCommand(args.positionals[0] || 'status', args.flags);
4565
+ return handleBridgeLifecycleCommand(args.flags, subcommand || 'start');
4566
+ }
4590
4567
 
4591
4568
  await maybeNotifyUpdate(command);
4592
4569
 
@@ -4612,7 +4589,6 @@ async function main() {
4612
4589
  if (command === 'openclaw' && subcommand === 'bridge') {
4613
4590
  const bridgeAction = String(args.positionals[0] || 'start').trim().toLowerCase();
4614
4591
  if (bridgeAction === 'service') {
4615
- 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.');
4616
4592
  const serviceAction = args.positionals[1] || 'status';
4617
4593
  await handleBridgeServiceCommand(serviceAction, args.flags);
4618
4594
  return;