@robbiesrobotics/alice-agents 1.4.3 → 1.4.4

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 CHANGED
@@ -18,6 +18,11 @@ That's it. The installer detects your runtime (NemoClaw or OpenClaw) and sets ev
18
18
 
19
19
  **Starter** includes 10 agents. **Pro** unlocks 18 more — [sign up at getalice.av3.ai](https://getalice.av3.ai/signup?plan=pro)
20
20
 
21
+ **Mission Control Cloud** is available as a Pro add-on. If enabled during install, the package now:
22
+ - installs the `mission-control-bridge` plugin into your OpenClaw home
23
+ - writes a portable local Mission Control config at `~/.openclaw/.alice-mission-control.json`
24
+ - enables the bridge in `openclaw.json` so your runtime can forward live telemetry to Mission Control
25
+
21
26
  An orchestrator (A.L.I.C.E., also addressable as Alice or Olivia) backed by specialist agents across every domain:
22
27
 
23
28
  | Agent | Domain | Emoji | Tier |
@@ -77,6 +82,9 @@ npx @robbiesrobotics/alice-agents
77
82
  # Non-interactive with defaults (detected model if available, otherwise Sonnet; Starter tier)
78
83
  npx @robbiesrobotics/alice-agents --yes
79
84
 
85
+ # Non-interactive Pro install with Mission Control Cloud enabled
86
+ npx @robbiesrobotics/alice-agents --cloud --cloud-token YOUR_TOKEN
87
+
80
88
  # Show help
81
89
  npx @robbiesrobotics/alice-agents --help
82
90
  ```
@@ -87,6 +95,19 @@ npx @robbiesrobotics/alice-agents --help
87
95
  - **Merge** — Adds A.L.I.C.E. agents alongside your existing agents
88
96
  - **Upgrade** — Updates product files (SOUL.md, AGENTS.md, etc.) without touching user customizations
89
97
 
98
+ ### Mission Control Cloud
99
+
100
+ If you're a Pro user with the cloud add-on, the installer can configure your local runtime for Mission Control in the same pass.
101
+
102
+ - Interactive install: choose `Pro`, validate your license, then enable the Mission Control Cloud add-on when prompted
103
+ - Non-interactive install: pass `--cloud`
104
+ - Optional flags:
105
+ - `--cloud-token <token>` — access or ingest token for authenticated telemetry
106
+ - `--cloud-dashboard-url <url>` — defaults to `https://alice.av3.ai`
107
+ - `--cloud-ingest-url <url>` — defaults to `<dashboard-url>/api/v1/ingest`
108
+
109
+ The cloud config is stored in `~/.openclaw/.alice-mission-control.json`, and the bundled bridge plugin reads from that file so the setup remains portable across macOS, Linux, and Windows.
110
+
90
111
  ## Upgrade
91
112
 
92
113
  Re-run the installer and choose "Upgrade":
@@ -9,6 +9,12 @@ import { runSkillsManager } from '../lib/skills.mjs';
9
9
  const args = process.argv.slice(2);
10
10
  const flags = new Set(args);
11
11
 
12
+ function getFlagValue(name) {
13
+ const idx = args.indexOf(name);
14
+ if (idx === -1) return undefined;
15
+ return args[idx + 1];
16
+ }
17
+
12
18
  if (flags.has('--version') || flags.has('-v')) {
13
19
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)));
14
20
  console.log(pkg.version);
@@ -26,6 +32,7 @@ if (flags.has('--help') || flags.has('-h')) {
26
32
  npx @robbiesrobotics/alice-agents --uninstall Remove A.L.I.C.E. agents from config
27
33
  npx @robbiesrobotics/alice-agents --doctor Run diagnostics on your A.L.I.C.E. install
28
34
  npx @robbiesrobotics/alice-agents --skills Manage skills (install, remove, browse)
35
+ npx @robbiesrobotics/alice-agents --cloud Enable Mission Control Cloud during install
29
36
  npx @robbiesrobotics/alice-agents --version Show version
30
37
  npx @robbiesrobotics/alice-agents --help Show this help
31
38
 
@@ -34,6 +41,11 @@ if (flags.has('--help') || flags.has('-h')) {
34
41
  --update Non-interactive upgrade (alias for --yes with upgrade mode)
35
42
  --uninstall Remove A.L.I.C.E. agents (preserves non-ALICE agents)
36
43
  --doctor Run diagnostics and check install health
44
+ --cloud Enable Mission Control Cloud setup during install
45
+ --no-cloud Skip Mission Control Cloud setup during install
46
+ --cloud-token <token> Mission Control ingest/access token
47
+ --cloud-dashboard-url <url> Mission Control dashboard URL
48
+ --cloud-ingest-url <url> Mission Control ingest endpoint
37
49
  --version Print package version
38
50
  `);
39
51
  process.exit(0);
@@ -45,7 +57,14 @@ if (flags.has('--doctor')) {
45
57
  process.exit(1);
46
58
  });
47
59
  } else if (flags.has('--update')) {
48
- runInstall({ yes: true, modeOverride: 'upgrade' }).catch((err) => {
60
+ runInstall({
61
+ yes: true,
62
+ modeOverride: 'upgrade',
63
+ cloud: flags.has('--cloud') ? true : flags.has('--no-cloud') ? false : undefined,
64
+ cloudToken: getFlagValue('--cloud-token'),
65
+ cloudDashboardUrl: getFlagValue('--cloud-dashboard-url'),
66
+ cloudIngestUrl: getFlagValue('--cloud-ingest-url'),
67
+ }).catch((err) => {
49
68
  console.error(' ❌ Update failed:', err.message);
50
69
  process.exit(1);
51
70
  });
@@ -60,7 +79,13 @@ if (flags.has('--doctor')) {
60
79
  process.exit(1);
61
80
  });
62
81
  } else {
63
- runInstall({ yes: flags.has('--yes') }).catch((err) => {
82
+ runInstall({
83
+ yes: flags.has('--yes'),
84
+ cloud: flags.has('--cloud') ? true : flags.has('--no-cloud') ? false : undefined,
85
+ cloudToken: getFlagValue('--cloud-token'),
86
+ cloudDashboardUrl: getFlagValue('--cloud-dashboard-url'),
87
+ cloudIngestUrl: getFlagValue('--cloud-ingest-url'),
88
+ }).catch((err) => {
64
89
  console.error(' ❌ Install failed:', err.message);
65
90
  process.exit(1);
66
91
  });
package/lib/installer.mjs CHANGED
@@ -6,6 +6,7 @@ import { homedir } from 'node:os';
6
6
  import { configExists, mergeConfig, removeAliceAgents, detectAvailableModels } from './config-merger.mjs';
7
7
  import { scaffoldAll } from './workspace-scaffolder.mjs';
8
8
  import { readManifest, writeManifest } from './manifest.mjs';
9
+ import { configureMissionControlCloud, getDefaultMissionControlSettings } from './mission-control.mjs';
9
10
  import {
10
11
  promptInstallMode,
11
12
  promptUserInfo,
@@ -13,6 +14,8 @@ import {
13
14
  promptCustomModel,
14
15
  promptTier,
15
16
  promptLicenseKey,
17
+ promptCloudAddon,
18
+ promptMissionControlToken,
16
19
  confirm,
17
20
  choose,
18
21
  input,
@@ -440,6 +443,10 @@ function printBanner() {
440
443
  }
441
444
 
442
445
  function printSummary(mode, tier, agents, preset, userInfo, detectedModels) {
446
+ return printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedModels, null);
447
+ }
448
+
449
+ function printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedModels, missionControl) {
443
450
  const modelLabel =
444
451
  preset === 'detected'
445
452
  ? `${detectedModels?.primary || 'your configured model'} ${dim('(detected)')}`
@@ -454,6 +461,13 @@ function printSummary(mode, tier, agents, preset, userInfo, detectedModels) {
454
461
  `${dim('Model:')} ${green(modelLabel)}`,
455
462
  `${dim('User:')} ${green(userInfo.name)}`,
456
463
  `${dim('Timezone:')} ${green(userInfo.timezone)}`,
464
+ `${dim('Mission Control:')} ${green(missionControl?.enabled ? 'cloud enabled' : 'local only')}`,
465
+ ...(missionControl?.enabled
466
+ ? [
467
+ `${dim('Dashboard:')} ${green(missionControl.dashboardUrl)}`,
468
+ `${dim('Ingest:')} ${green(missionControl.ingestUrl)}`,
469
+ ]
470
+ : []),
457
471
  '',
458
472
  `${dim('Agents:')}`,
459
473
  ...agents.map(a => ` ${icons.bullet} ${green(a.emoji)} ${bold(a.name.padEnd(10))} ${dim('─')} ${a.domain}`),
@@ -465,6 +479,7 @@ function printSummary(mode, tier, agents, preset, userInfo, detectedModels) {
465
479
 
466
480
  export async function runInstall(options = {}) {
467
481
  const auto = options.yes || false;
482
+ const manifest = readManifest();
468
483
 
469
484
  // Check health flag first (before banner)
470
485
  if (process.argv.includes('--health')) {
@@ -534,14 +549,12 @@ export async function runInstall(options = {}) {
534
549
  if (options.modeOverride) {
535
550
  mode = options.modeOverride;
536
551
  } else if (auto) {
537
- const manifest = readManifest();
538
552
  mode = manifest ? 'upgrade' : 'fresh';
539
553
  } else {
540
554
  mode = await promptInstallMode();
541
555
  }
542
556
 
543
557
  if (mode === 'upgrade') {
544
- const manifest = readManifest();
545
558
  if (!manifest) {
546
559
  console.log(` ${icons.warn} ${yellow('No previous install found. Switching to fresh install.')}\n`);
547
560
  mode = 'fresh';
@@ -644,10 +657,43 @@ export async function runInstall(options = {}) {
644
657
  }
645
658
  }
646
659
 
660
+ let missionControl = null;
661
+ const existingMissionControl = manifest?.missionControl;
662
+ if (tier === 'pro') {
663
+ const cloudFlag = options.cloud === true ? true : options.cloud === false ? false : null;
664
+ const enableCloud = auto
665
+ ? cloudFlag ?? existingMissionControl?.enabled ?? false
666
+ : cloudFlag ?? await promptCloudAddon();
667
+
668
+ if (enableCloud) {
669
+ const defaults = getDefaultMissionControlSettings();
670
+ const dashboardUrl =
671
+ String(options.cloudDashboardUrl || existingMissionControl?.dashboardUrl || defaults.dashboardUrl).trim();
672
+ const ingestUrl =
673
+ String(options.cloudIngestUrl || existingMissionControl?.ingestUrl || `${dashboardUrl}/api/v1/ingest`).trim();
674
+ const ingestToken =
675
+ auto
676
+ ? String(options.cloudToken || '').trim()
677
+ : String(options.cloudToken || await promptMissionControlToken()).trim();
678
+ const sourceNode =
679
+ String(options.cloudSourceNode || existingMissionControl?.sourceNode || defaults.sourceNode).trim();
680
+
681
+ missionControl = {
682
+ enabled: true,
683
+ provider: 'cloud',
684
+ dashboardUrl,
685
+ ingestUrl,
686
+ sourceNode,
687
+ hasIngestToken: !!ingestToken,
688
+ ingestToken,
689
+ };
690
+ }
691
+ }
692
+
647
693
  const agents = allAgents;
648
694
 
649
695
  // 6. Confirmation
650
- printSummary(mode, tier, agents, preset, userInfo, detectedModels);
696
+ printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedModels, missionControl);
651
697
 
652
698
  if (!auto) {
653
699
  const ok = await confirm(' Proceed with installation?');
@@ -676,6 +722,16 @@ export async function runInstall(options = {}) {
676
722
  console.log(` ${icons.warn} ${yellow(warning)}`);
677
723
  }
678
724
 
725
+ if (missionControl?.enabled) {
726
+ const missionControlResult = configureMissionControlCloud(missionControl);
727
+ printStepDone('Mission Control cloud', missionControlResult.summary.dashboardUrl);
728
+ if (!missionControlResult.summary.hasIngestToken) {
729
+ printStepSkip('Cloud access token not set', 'bridge installed; add token later to enable authenticated ingest');
730
+ }
731
+ } else {
732
+ printStepSkip('Mission Control cloud', 'not enabled for this install');
733
+ }
734
+
679
735
  // Scaffold workspaces
680
736
  const results = scaffoldAll(agents, userInfo);
681
737
  let newWorkspaces = 0;
@@ -706,6 +762,16 @@ export async function runInstall(options = {}) {
706
762
  userName: userInfo.name,
707
763
  userTimezone: userInfo.timezone,
708
764
  modelPreset: effectivePreset,
765
+ missionControl: missionControl
766
+ ? {
767
+ enabled: true,
768
+ provider: 'cloud',
769
+ dashboardUrl: missionControl.dashboardUrl,
770
+ ingestUrl: missionControl.ingestUrl,
771
+ sourceNode: missionControl.sourceNode,
772
+ bridgeInstalled: true,
773
+ }
774
+ : existing?.missionControl || null,
709
775
  });
710
776
  printStepDone('Manifest written');
711
777
 
package/lib/manifest.mjs CHANGED
@@ -27,6 +27,7 @@ export function writeManifest(data) {
27
27
  userName: data.userName,
28
28
  userTimezone: data.userTimezone,
29
29
  modelPreset: data.modelPreset,
30
+ missionControl: data.missionControl || null,
30
31
  };
31
32
  writeFileSync(getManifestPath(), JSON.stringify(manifest, null, 2) + '\n', 'utf8');
32
33
  return manifest;
@@ -0,0 +1,150 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { homedir, hostname } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const OPENCLAW_HOME = process.env.OPENCLAW_HOME || join(homedir(), '.openclaw');
8
+ const CONFIG_PATH = join(OPENCLAW_HOME, 'openclaw.json');
9
+ const MC_CONFIG_PATH = join(OPENCLAW_HOME, '.alice-mission-control.json');
10
+ const BRIDGE_ID = 'mission-control-bridge';
11
+ const DEFAULT_DASHBOARD_URL = 'https://alice.av3.ai';
12
+ const DEFAULT_INGEST_URL = `${DEFAULT_DASHBOARD_URL}/api/v1/ingest`;
13
+ const TEMPLATE_DIR = join(__dirname, '..', 'templates', 'mission-control-bridge');
14
+
15
+ function normalizeUrl(url, fallback) {
16
+ const value = String(url || fallback || '').trim();
17
+ if (!value) return '';
18
+ return value.replace(/\/+$/, '');
19
+ }
20
+
21
+ function atomicWriteJSON(targetPath, data) {
22
+ const tmpPath = `${targetPath}.tmp`;
23
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
24
+ renameSync(tmpPath, targetPath);
25
+ }
26
+
27
+ function readJsonFile(path) {
28
+ try {
29
+ return JSON.parse(readFileSync(path, 'utf8'));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export function getMissionControlConfigPath() {
36
+ return MC_CONFIG_PATH;
37
+ }
38
+
39
+ export function readMissionControlConfig() {
40
+ return readJsonFile(MC_CONFIG_PATH);
41
+ }
42
+
43
+ export function buildMissionControlSettings(input = {}) {
44
+ const dashboardUrl = normalizeUrl(input.dashboardUrl, DEFAULT_DASHBOARD_URL);
45
+ const ingestUrl = normalizeUrl(input.ingestUrl, `${dashboardUrl}/api/v1/ingest`);
46
+ const sourceNode = String(input.sourceNode || hostname() || 'openclaw-local').trim();
47
+ const ingestToken = String(input.ingestToken || '').trim();
48
+
49
+ return {
50
+ enabled: input.enabled !== false,
51
+ provider: 'cloud',
52
+ dashboardUrl,
53
+ ingestUrl,
54
+ sourceNode,
55
+ ...(ingestToken ? { ingestToken } : {}),
56
+ };
57
+ }
58
+
59
+ export function writeMissionControlConfig(input = {}) {
60
+ mkdirSync(OPENCLAW_HOME, { recursive: true });
61
+
62
+ const existing = readMissionControlConfig();
63
+ const settings = buildMissionControlSettings({
64
+ ...existing?.cloud,
65
+ ...input,
66
+ });
67
+
68
+ const config = {
69
+ version: 1,
70
+ updatedAt: new Date().toISOString(),
71
+ cloud: settings,
72
+ };
73
+
74
+ atomicWriteJSON(MC_CONFIG_PATH, config);
75
+ return { path: MC_CONFIG_PATH, config };
76
+ }
77
+
78
+ function ensureBridgeFiles(targetPath) {
79
+ mkdirSync(dirname(targetPath), { recursive: true });
80
+ cpSync(TEMPLATE_DIR, targetPath, { recursive: true, force: true });
81
+ }
82
+
83
+ export function installMissionControlBridge() {
84
+ const sourcePath = join(OPENCLAW_HOME, 'plugins', BRIDGE_ID);
85
+ const installPath = join(OPENCLAW_HOME, 'extensions', BRIDGE_ID);
86
+
87
+ ensureBridgeFiles(sourcePath);
88
+ ensureBridgeFiles(installPath);
89
+
90
+ return { sourcePath, installPath };
91
+ }
92
+
93
+ export function enableMissionControlBridge() {
94
+ const config = readJsonFile(CONFIG_PATH);
95
+ if (!config) {
96
+ throw new Error('OpenClaw config not found. Run openclaw configure first.');
97
+ }
98
+
99
+ const existingInstall = config?.plugins?.installs?.[BRIDGE_ID] || {};
100
+ const { sourcePath, installPath } = installMissionControlBridge();
101
+ const pkg = readJsonFile(join(TEMPLATE_DIR, 'package.json')) || {};
102
+
103
+ config.plugins = config.plugins || {};
104
+ config.plugins.entries = config.plugins.entries || {};
105
+ config.plugins.installs = config.plugins.installs || {};
106
+
107
+ config.plugins.entries[BRIDGE_ID] = {
108
+ ...(config.plugins.entries[BRIDGE_ID] || {}),
109
+ enabled: true,
110
+ };
111
+
112
+ config.plugins.installs[BRIDGE_ID] = {
113
+ source: 'path',
114
+ sourcePath,
115
+ installPath,
116
+ version: pkg.version || existingInstall.version || '1.0.0',
117
+ installedAt: existingInstall.installedAt || new Date().toISOString(),
118
+ };
119
+
120
+ atomicWriteJSON(CONFIG_PATH, config);
121
+ return { configPath: CONFIG_PATH, sourcePath, installPath };
122
+ }
123
+
124
+ export function configureMissionControlCloud(input = {}) {
125
+ const configResult = writeMissionControlConfig(input);
126
+ const bridgeResult = enableMissionControlBridge();
127
+ const settings = configResult.config.cloud;
128
+
129
+ return {
130
+ configPath: configResult.path,
131
+ bridgeSourcePath: bridgeResult.sourcePath,
132
+ bridgeInstallPath: bridgeResult.installPath,
133
+ summary: {
134
+ enabled: settings.enabled,
135
+ provider: settings.provider,
136
+ dashboardUrl: settings.dashboardUrl,
137
+ ingestUrl: settings.ingestUrl,
138
+ sourceNode: settings.sourceNode,
139
+ hasIngestToken: !!settings.ingestToken,
140
+ },
141
+ };
142
+ }
143
+
144
+ export function getDefaultMissionControlSettings() {
145
+ return buildMissionControlSettings();
146
+ }
147
+
148
+ export function hasMissionControlBridgeInstalled() {
149
+ return existsSync(join(OPENCLAW_HOME, 'extensions', BRIDGE_ID));
150
+ }
package/lib/prompter.mjs CHANGED
@@ -125,3 +125,19 @@ export async function promptLicenseKey() {
125
125
  });
126
126
  });
127
127
  }
128
+
129
+ export async function promptCloudAddon() {
130
+ console.log('');
131
+ console.log(' Mission Control Cloud add-on');
132
+ console.log(' Hosted dashboard, telemetry sync, and cloud memory for Pro users.');
133
+ console.log('');
134
+ return confirm(' Enable the Mission Control Cloud add-on on this machine?', true);
135
+ }
136
+
137
+ export async function promptMissionControlToken() {
138
+ console.log('');
139
+ console.log(' Enter your Mission Control cloud access token if you have one.');
140
+ console.log(' Press Enter to skip for now — you can add it later without reinstalling.');
141
+ console.log('');
142
+ return input(' Mission Control access token', '');
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbiesrobotics/alice-agents",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "A.L.I.C.E. — 28 AI agents for OpenClaw. One conversation, one team.",
5
5
  "bin": {
6
6
  "alice-agents": "bin/alice-install.mjs"
@@ -23,6 +23,7 @@
23
23
  "tools/",
24
24
  "snapshots/",
25
25
  "templates/agents-starter.json",
26
+ "templates/mission-control-bridge/",
26
27
  "templates/skills/",
27
28
  "templates/workspaces/",
28
29
  "SELF-HEALING-SPEC.md",
@@ -0,0 +1,233 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diagnostics-otel";
5
+ import { emptyPluginConfigSchema, onDiagnosticEvent } from "openclaw/plugin-sdk/diagnostics-otel";
6
+ import type { DiagnosticEventPayload } from "openclaw/plugin-sdk/diagnostics-otel";
7
+
8
+ const OPENCLAW_HOME = process.env.OPENCLAW_HOME ?? join(homedir(), ".openclaw");
9
+ const MC_CONFIG_PATH = join(OPENCLAW_HOME, ".alice-mission-control.json");
10
+ const DEFAULT_INGEST_URL = "https://alice.av3.ai/api/v1/ingest";
11
+
12
+ function readMissionControlConfig(): Record<string, unknown> {
13
+ try {
14
+ if (!existsSync(MC_CONFIG_PATH)) return {};
15
+ return JSON.parse(readFileSync(MC_CONFIG_PATH, "utf8"));
16
+ } catch {
17
+ return {};
18
+ }
19
+ }
20
+
21
+ function getCloudConfig(): Record<string, unknown> {
22
+ const fileConfig = readMissionControlConfig();
23
+ return typeof fileConfig.cloud === "object" && fileConfig.cloud ? fileConfig.cloud as Record<string, unknown> : {};
24
+ }
25
+
26
+ function getString(value: unknown, fallback = ""): string {
27
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
28
+ }
29
+
30
+ const cloudConfig = getCloudConfig();
31
+ const INGEST_URL = getString(process.env.MC_INGEST_URL, getString(cloudConfig.ingestUrl, DEFAULT_INGEST_URL));
32
+ const INGEST_TOKEN = getString(process.env.MC_INGEST_TOKEN, getString(cloudConfig.ingestToken));
33
+ const SOURCE_NODE = getString(process.env.MC_SOURCE_NODE, getString(cloudConfig.sourceNode, "openclaw-local"));
34
+
35
+ function now(): string {
36
+ return new Date().toISOString();
37
+ }
38
+
39
+ function sessionKeyToAgentId(sessionKey?: string): string {
40
+ if (!sessionKey) return "unknown";
41
+ const parts = sessionKey.split(":");
42
+ return parts[1] ?? parts[0] ?? "unknown";
43
+ }
44
+
45
+ let seq = 0;
46
+ function nextEventId(): string {
47
+ return `mc-bridge-${Date.now()}-${++seq}`;
48
+ }
49
+
50
+ async function postToIngest(events: object[]): Promise<void> {
51
+ const headers: Record<string, string> = {
52
+ "Content-Type": "application/json",
53
+ };
54
+ if (INGEST_TOKEN) {
55
+ headers["Authorization"] = `Bearer ${INGEST_TOKEN}`;
56
+ }
57
+
58
+ try {
59
+ const res = await fetch(INGEST_URL, {
60
+ method: "POST",
61
+ headers,
62
+ body: JSON.stringify(events),
63
+ signal: AbortSignal.timeout(8000),
64
+ });
65
+ if (!res.ok) {
66
+ console.warn(`[mc-bridge] ingest HTTP ${res.status} — ${await res.text().catch(() => "")}`);
67
+ }
68
+ } catch (err) {
69
+ console.warn(`[mc-bridge] ingest post failed: ${String(err)}`);
70
+ }
71
+ }
72
+
73
+ function handleModelUsage(evt: Extract<DiagnosticEventPayload, { type: "model.usage" }>) {
74
+ const agentId = sessionKeyToAgentId(evt.sessionKey);
75
+ const totalTokens = evt.usage.total ?? (evt.usage.input ?? 0) + (evt.usage.output ?? 0);
76
+
77
+ const event = {
78
+ event_id: nextEventId(),
79
+ event_type: "agent.session.completed",
80
+ event_version: "1.0",
81
+ source_system: "openclaw",
82
+ source_node: SOURCE_NODE,
83
+ occurred_at: now(),
84
+ actor_id: agentId,
85
+ actor_type: "agent",
86
+ correlation_id: evt.sessionId ?? null,
87
+ payload: {
88
+ session_id: evt.sessionId ?? evt.sessionKey ?? nextEventId(),
89
+ agent_id: agentId,
90
+ model: evt.model ?? "unknown",
91
+ channel: evt.channel ?? "unknown",
92
+ total_tokens: totalTokens,
93
+ input_tokens: evt.usage.input ?? 0,
94
+ output_tokens: evt.usage.output ?? 0,
95
+ cache_read_tokens: evt.usage.cacheRead ?? 0,
96
+ cache_write_tokens: evt.usage.cacheWrite ?? 0,
97
+ cost_usd: evt.costUsd ?? 0,
98
+ duration_ms: evt.durationMs ?? 0,
99
+ context_limit: evt.context?.limit ?? 0,
100
+ context_used: evt.context?.used ?? 0,
101
+ status: "completed",
102
+ },
103
+ };
104
+
105
+ postToIngest([event]);
106
+ }
107
+
108
+ function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "message.processed" }>) {
109
+ if (!evt.sessionKey && !evt.sessionId) return;
110
+ const agentId = sessionKeyToAgentId(evt.sessionKey);
111
+ const eventType =
112
+ evt.outcome === "completed"
113
+ ? "agent.session.completed"
114
+ : evt.outcome === "error"
115
+ ? "agent.session.completed"
116
+ : null;
117
+
118
+ if (!eventType) return;
119
+
120
+ const event = {
121
+ event_id: nextEventId(),
122
+ event_type: eventType,
123
+ event_version: "1.0",
124
+ source_system: "openclaw",
125
+ source_node: SOURCE_NODE,
126
+ occurred_at: now(),
127
+ actor_id: agentId,
128
+ actor_type: "agent",
129
+ correlation_id: evt.sessionId ?? null,
130
+ payload: {
131
+ session_id: evt.sessionId ?? evt.sessionKey ?? nextEventId(),
132
+ agent_id: agentId,
133
+ channel: evt.channel ?? "unknown",
134
+ status: evt.outcome === "error" ? "failed" : "completed",
135
+ duration_ms: evt.durationMs ?? 0,
136
+ ...(evt.error ? { error: evt.error } : {}),
137
+ },
138
+ };
139
+
140
+ postToIngest([event]);
141
+ }
142
+
143
+ function handleSessionState(evt: Extract<DiagnosticEventPayload, { type: "session.state" }>) {
144
+ if (!evt.sessionKey && !evt.sessionId) return;
145
+ const agentId = sessionKeyToAgentId(evt.sessionKey);
146
+ const stateToEventType: Record<string, string> = {
147
+ processing: "agent.session.started",
148
+ idle: "agent.session.completed",
149
+ };
150
+ const eventType = stateToEventType[evt.state];
151
+ if (!eventType) return;
152
+
153
+ const event = {
154
+ event_id: nextEventId(),
155
+ event_type: eventType,
156
+ event_version: "1.0",
157
+ source_system: "openclaw",
158
+ source_node: SOURCE_NODE,
159
+ occurred_at: now(),
160
+ actor_id: agentId,
161
+ actor_type: "agent",
162
+ correlation_id: evt.sessionId ?? null,
163
+ payload: {
164
+ session_id: evt.sessionId ?? evt.sessionKey ?? nextEventId(),
165
+ agent_id: agentId,
166
+ state: evt.state,
167
+ prev_state: evt.prevState ?? null,
168
+ },
169
+ };
170
+
171
+ postToIngest([event]);
172
+ }
173
+
174
+ const plugin = {
175
+ id: "mission-control-bridge",
176
+ name: "Mission Control Bridge",
177
+ description: "Forwards OpenClaw diagnostic events to the A.L.I.C.E. Mission Control ingest endpoint",
178
+ configSchema: emptyPluginConfigSchema(),
179
+
180
+ register(api: OpenClawPluginApi) {
181
+ api.registerService({
182
+ id: "mission-control-bridge",
183
+
184
+ async start(ctx) {
185
+ ctx.logger.info(`[mc-bridge] starting — ingest URL: ${INGEST_URL}`);
186
+
187
+ const unsubscribe = onDiagnosticEvent((evt: DiagnosticEventPayload) => {
188
+ try {
189
+ switch (evt.type) {
190
+ case "model.usage":
191
+ handleModelUsage(evt);
192
+ break;
193
+ case "message.processed":
194
+ handleMessageProcessed(evt);
195
+ break;
196
+ case "session.state":
197
+ handleSessionState(evt);
198
+ break;
199
+ }
200
+ } catch (err) {
201
+ ctx.logger.warn(`[mc-bridge] event handler error (${evt.type}): ${String(err)}`);
202
+ }
203
+ });
204
+
205
+ (this as { _unsub?: () => void })._unsub = unsubscribe;
206
+
207
+ await postToIngest([
208
+ {
209
+ event_id: nextEventId(),
210
+ event_type: "node.registered",
211
+ event_version: "1.0",
212
+ source_system: "openclaw",
213
+ source_node: SOURCE_NODE,
214
+ occurred_at: now(),
215
+ payload: {
216
+ node_name: SOURCE_NODE,
217
+ platform: process.platform,
218
+ node_version: process.version,
219
+ },
220
+ },
221
+ ]);
222
+ },
223
+
224
+ async stop() {
225
+ const self = this as { _unsub?: () => void };
226
+ self._unsub?.();
227
+ self._unsub = undefined;
228
+ },
229
+ });
230
+ },
231
+ };
232
+
233
+ export default plugin;
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "mission-control-bridge",
3
+ "configSchema": {
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "properties": {}
7
+ }
8
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@alice/mission-control-bridge",
3
+ "version": "1.0.0",
4
+ "description": "Bridges OpenClaw diagnostic events to Mission Control ingest endpoint",
5
+ "type": "module",
6
+ "openclaw": {
7
+ "extensions": [
8
+ "./index.ts"
9
+ ]
10
+ }
11
+ }