@useorgx/openclaw-plugin 0.3.0 → 0.3.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.
Files changed (62) hide show
  1. package/README.md +48 -1
  2. package/dashboard/dist/assets/index-BjqNjHpY.css +1 -0
  3. package/dashboard/dist/assets/index-DCLkU4AM.js +57 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/dist/adapters/outbox.d.ts +8 -0
  6. package/dist/adapters/outbox.d.ts.map +1 -0
  7. package/dist/adapters/outbox.js +6 -0
  8. package/dist/adapters/outbox.js.map +1 -0
  9. package/dist/agent-context-store.d.ts +24 -0
  10. package/dist/agent-context-store.d.ts.map +1 -0
  11. package/dist/agent-context-store.js +110 -0
  12. package/dist/agent-context-store.js.map +1 -0
  13. package/dist/agent-run-store.d.ts +31 -0
  14. package/dist/agent-run-store.d.ts.map +1 -0
  15. package/dist/agent-run-store.js +158 -0
  16. package/dist/agent-run-store.js.map +1 -0
  17. package/dist/api.d.ts +4 -139
  18. package/dist/api.d.ts.map +1 -1
  19. package/dist/api.js +4 -347
  20. package/dist/api.js.map +1 -1
  21. package/dist/auth-store.d.ts.map +1 -1
  22. package/dist/auth-store.js +27 -1
  23. package/dist/auth-store.js.map +1 -1
  24. package/dist/byok-store.d.ts +11 -0
  25. package/dist/byok-store.d.ts.map +1 -0
  26. package/dist/byok-store.js +94 -0
  27. package/dist/byok-store.js.map +1 -0
  28. package/dist/contracts/client.d.ts +154 -0
  29. package/dist/contracts/client.d.ts.map +1 -0
  30. package/dist/contracts/client.js +422 -0
  31. package/dist/contracts/client.js.map +1 -0
  32. package/dist/contracts/types.d.ts +430 -0
  33. package/dist/contracts/types.d.ts.map +1 -0
  34. package/dist/contracts/types.js +8 -0
  35. package/dist/contracts/types.js.map +1 -0
  36. package/dist/http-handler.d.ts +10 -1
  37. package/dist/http-handler.d.ts.map +1 -1
  38. package/dist/http-handler.js +2256 -98
  39. package/dist/http-handler.js.map +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +348 -24
  42. package/dist/index.js.map +1 -1
  43. package/dist/local-openclaw.d.ts.map +1 -1
  44. package/dist/local-openclaw.js +57 -15
  45. package/dist/local-openclaw.js.map +1 -1
  46. package/dist/openclaw.plugin.json +3 -3
  47. package/dist/outbox.d.ts +7 -0
  48. package/dist/outbox.d.ts.map +1 -1
  49. package/dist/outbox.js +94 -6
  50. package/dist/outbox.js.map +1 -1
  51. package/dist/snapshot-store.d.ts +10 -0
  52. package/dist/snapshot-store.d.ts.map +1 -0
  53. package/dist/snapshot-store.js +64 -0
  54. package/dist/snapshot-store.js.map +1 -0
  55. package/dist/types.d.ts +5 -410
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js +5 -4
  58. package/dist/types.js.map +1 -1
  59. package/openclaw.plugin.json +3 -3
  60. package/package.json +13 -3
  61. package/dashboard/dist/assets/index-BrAP-X_H.css +0 -1
  62. package/dashboard/dist/assets/index-cOk6qwh-.js +0 -56
package/dist/index.js CHANGED
@@ -18,9 +18,63 @@ import { homedir } from "node:os";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { clearPersistedApiKey, loadAuthStore, resolveInstallationId, saveAuthStore, } from "./auth-store.js";
21
- import { appendToOutbox, readOutbox, replaceOutbox } from "./outbox.js";
21
+ import { clearPersistedSnapshot, readPersistedSnapshot, writePersistedSnapshot, } from "./snapshot-store.js";
22
+ import { appendToOutbox, readOutbox, readOutboxSummary, replaceOutbox, } from "./outbox.js";
22
23
  export { OrgXClient } from "./api.js";
24
+ const DEFAULT_BASE_URL = "https://www.useorgx.com";
23
25
  const DEFAULT_DOCS_URL = "https://orgx.mintlify.site/guides/openclaw-plugin-setup";
26
+ function isUserScopedApiKey(apiKey) {
27
+ return apiKey.trim().toLowerCase().startsWith("oxk_");
28
+ }
29
+ function resolveRuntimeUserId(apiKey, candidates) {
30
+ if (isUserScopedApiKey(apiKey)) {
31
+ return "";
32
+ }
33
+ for (const candidate of candidates) {
34
+ if (typeof candidate === "string") {
35
+ const trimmed = candidate.trim();
36
+ if (trimmed.length > 0)
37
+ return trimmed;
38
+ }
39
+ }
40
+ return "";
41
+ }
42
+ function normalizeHost(value) {
43
+ return value.trim().toLowerCase().replace(/^\[|\]$/g, "");
44
+ }
45
+ function isLoopbackHostname(hostname) {
46
+ const normalized = normalizeHost(hostname);
47
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
48
+ }
49
+ function normalizeBaseUrl(raw) {
50
+ const candidate = raw?.trim() ?? "";
51
+ if (!candidate) {
52
+ return DEFAULT_BASE_URL;
53
+ }
54
+ try {
55
+ const parsed = new URL(candidate);
56
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
57
+ return DEFAULT_BASE_URL;
58
+ }
59
+ // Do not allow credential-bearing URLs.
60
+ if (parsed.username || parsed.password) {
61
+ return DEFAULT_BASE_URL;
62
+ }
63
+ // Plain HTTP is only allowed for local loopback development.
64
+ if (parsed.protocol === "http:" && !isLoopbackHostname(parsed.hostname)) {
65
+ return DEFAULT_BASE_URL;
66
+ }
67
+ parsed.search = "";
68
+ parsed.hash = "";
69
+ const normalizedPath = parsed.pathname.replace(/\/+$/, "");
70
+ parsed.pathname = normalizedPath;
71
+ const normalized = parsed.toString().replace(/\/+$/, "");
72
+ return normalized.length > 0 ? normalized : DEFAULT_BASE_URL;
73
+ }
74
+ catch {
75
+ return DEFAULT_BASE_URL;
76
+ }
77
+ }
24
78
  function readLegacyEnvValue(keyPattern) {
25
79
  try {
26
80
  const envPath = join(homedir(), "Code", "orgx", "orgx", ".env.local");
@@ -72,7 +126,10 @@ function resolveApiKey(pluginConf, persistedApiKey) {
72
126
  if (openclaw.apiKey) {
73
127
  return { value: openclaw.apiKey, source: "openclaw-config-file" };
74
128
  }
75
- const legacy = readLegacyEnvValue(/^ORGX_(?:API_KEY|SERVICE_KEY)=["']?([^"'\n]+)["']?$/m);
129
+ // For local dev convenience we read `ORGX_API_KEY` from `~/Code/orgx/orgx/.env.local`.
130
+ // Do not auto-consume `ORGX_SERVICE_KEY` because service keys often require `X-Orgx-User-Id`,
131
+ // and the dashboard/client flows are intended to run on user-scoped keys (`oxk_...`).
132
+ const legacy = readLegacyEnvValue(/^ORGX_API_KEY=["']?([^"'\n]+)["']?$/m);
76
133
  if (legacy) {
77
134
  return { value: legacy, source: "legacy-dev" };
78
135
  }
@@ -92,8 +149,14 @@ function resolvePluginVersion() {
92
149
  }
93
150
  function resolveDocsUrl(baseUrl) {
94
151
  const normalized = baseUrl.replace(/\/+$/, "");
95
- if (normalized.includes("localhost") || normalized.includes("127.0.0.1")) {
96
- return `${normalized}/docs/mintlify/guides/openclaw-plugin-setup`;
152
+ try {
153
+ const parsed = new URL(normalized);
154
+ if (isLoopbackHostname(parsed.hostname)) {
155
+ return `${normalized}/docs/mintlify/guides/openclaw-plugin-setup`;
156
+ }
157
+ }
158
+ catch {
159
+ return DEFAULT_DOCS_URL;
97
160
  }
98
161
  return DEFAULT_DOCS_URL;
99
162
  }
@@ -103,15 +166,14 @@ function resolveConfig(api, input) {
103
166
  const apiKeyResolution = resolveApiKey(pluginConf, input.persistedApiKey);
104
167
  const apiKey = apiKeyResolution.value;
105
168
  // Resolve user ID for X-Orgx-User-Id header
106
- const userId = pluginConf.userId?.trim() ||
107
- process.env.ORGX_USER_ID?.trim() ||
108
- input.persistedUserId?.trim() ||
109
- openclaw.userId ||
110
- readLegacyEnvValue(/^ORGX_USER_ID=["']?([^"'\n]+)["']?$/m);
111
- const baseUrl = pluginConf.baseUrl ||
112
- process.env.ORGX_BASE_URL ||
113
- openclaw.baseUrl ||
114
- "https://www.useorgx.com";
169
+ const userId = resolveRuntimeUserId(apiKey, [
170
+ pluginConf.userId,
171
+ process.env.ORGX_USER_ID,
172
+ input.persistedUserId,
173
+ openclaw.userId,
174
+ readLegacyEnvValue(/^ORGX_USER_ID=["']?([^"'\n]+)["']?$/m),
175
+ ]);
176
+ const baseUrl = normalizeBaseUrl(pluginConf.baseUrl || process.env.ORGX_BASE_URL || openclaw.baseUrl || DEFAULT_BASE_URL);
115
177
  return {
116
178
  apiKey,
117
179
  userId,
@@ -168,6 +230,22 @@ function formatSnapshot(snap) {
168
230
  lines.push(`_Last synced: ${snap.syncedAt}_`);
169
231
  return lines.join("\n");
170
232
  }
233
+ function apiKeySourceLabel(source) {
234
+ switch (source) {
235
+ case "config":
236
+ return "Plugin Config";
237
+ case "environment":
238
+ return "Environment";
239
+ case "persisted":
240
+ return "Persisted Store";
241
+ case "openclaw-config-file":
242
+ return "OpenClaw Config";
243
+ case "legacy-dev":
244
+ return "Legacy Dev Env";
245
+ default:
246
+ return "Not configured";
247
+ }
248
+ }
171
249
  function pickNonEmptyString(...values) {
172
250
  for (const value of values) {
173
251
  if (typeof value !== "string")
@@ -206,6 +284,24 @@ function toReportingPhase(phase, progressPct) {
206
284
  // =============================================================================
207
285
  let cachedSnapshot = null;
208
286
  let lastSnapshotAt = 0;
287
+ function updateCachedSnapshot(snapshot) {
288
+ cachedSnapshot = snapshot;
289
+ lastSnapshotAt = Date.now();
290
+ try {
291
+ writePersistedSnapshot(snapshot);
292
+ }
293
+ catch {
294
+ // best effort
295
+ }
296
+ }
297
+ function hydrateCachedSnapshot() {
298
+ const persisted = readPersistedSnapshot();
299
+ if (!persisted?.snapshot)
300
+ return;
301
+ cachedSnapshot = persisted.snapshot;
302
+ const ts = Date.parse(persisted.updatedAt);
303
+ lastSnapshotAt = Number.isFinite(ts) ? ts : 0;
304
+ }
209
305
  // =============================================================================
210
306
  // PLUGIN ENTRY — DEFAULT EXPORT
211
307
  // =============================================================================
@@ -230,6 +326,7 @@ export default function register(api) {
230
326
  if (!config.apiKey) {
231
327
  api.log?.warn?.("[orgx] No API key. Set plugins.entries.orgx.config.apiKey, ORGX_API_KEY env, or ~/Code/orgx/orgx/.env.local");
232
328
  }
329
+ hydrateCachedSnapshot();
233
330
  const client = new OrgXClient(config.apiKey, config.baseUrl, config.userId);
234
331
  let onboardingState = {
235
332
  status: config.apiKey ? "connected" : "idle",
@@ -355,9 +452,7 @@ export default function register(api) {
355
452
  const nextApiKey = input.apiKey.trim();
356
453
  config.apiKey = nextApiKey;
357
454
  config.apiKeySource = "persisted";
358
- if (typeof input.userId === "string" && input.userId.trim().length > 0) {
359
- config.userId = input.userId.trim();
360
- }
455
+ config.userId = resolveRuntimeUserId(nextApiKey, [input.userId, config.userId]);
361
456
  client.setCredentials({
362
457
  apiKey: config.apiKey,
363
458
  userId: config.userId,
@@ -385,6 +480,154 @@ export default function register(api) {
385
480
  let syncInFlight = null;
386
481
  let syncServiceRunning = false;
387
482
  const outboxQueues = ["progress", "decisions", "artifacts"];
483
+ let outboxReplayState = {
484
+ status: "idle",
485
+ lastReplayAttemptAt: null,
486
+ lastReplaySuccessAt: null,
487
+ lastReplayFailureAt: null,
488
+ lastReplayError: null,
489
+ };
490
+ async function buildHealthReport(input = {}) {
491
+ const generatedAt = new Date().toISOString();
492
+ const probeRemote = input.probeRemote === true;
493
+ const outbox = await readOutboxSummary();
494
+ const checks = [];
495
+ const hasApiKey = Boolean(config.apiKey);
496
+ if (hasApiKey) {
497
+ checks.push({
498
+ id: "api_key",
499
+ status: "pass",
500
+ message: `API key detected (${apiKeySourceLabel(config.apiKeySource)}).`,
501
+ });
502
+ }
503
+ else {
504
+ checks.push({
505
+ id: "api_key",
506
+ status: "fail",
507
+ message: "API key missing. Connect OrgX in onboarding or set ORGX_API_KEY.",
508
+ });
509
+ }
510
+ if (syncServiceRunning) {
511
+ checks.push({
512
+ id: "sync_service",
513
+ status: "pass",
514
+ message: "Background sync service is running.",
515
+ });
516
+ }
517
+ else {
518
+ checks.push({
519
+ id: "sync_service",
520
+ status: "warn",
521
+ message: "Background sync service is not running.",
522
+ });
523
+ }
524
+ if (outbox.pendingTotal > 0) {
525
+ checks.push({
526
+ id: "outbox",
527
+ status: "warn",
528
+ message: `Outbox has ${outbox.pendingTotal} queued event(s).`,
529
+ });
530
+ }
531
+ else {
532
+ checks.push({
533
+ id: "outbox",
534
+ status: "pass",
535
+ message: "Outbox is empty.",
536
+ });
537
+ }
538
+ let remoteReachable = null;
539
+ let remoteLatencyMs = null;
540
+ let remoteError = null;
541
+ if (probeRemote) {
542
+ if (!hasApiKey) {
543
+ checks.push({
544
+ id: "remote_probe",
545
+ status: "warn",
546
+ message: "Skipped remote probe because API key is missing.",
547
+ });
548
+ }
549
+ else {
550
+ const startedAt = Date.now();
551
+ try {
552
+ await client.getOrgSnapshot();
553
+ remoteReachable = true;
554
+ remoteLatencyMs = Date.now() - startedAt;
555
+ checks.push({
556
+ id: "remote_probe",
557
+ status: "pass",
558
+ message: `OrgX API reachable (${remoteLatencyMs}ms).`,
559
+ });
560
+ }
561
+ catch (err) {
562
+ remoteReachable = false;
563
+ remoteLatencyMs = Date.now() - startedAt;
564
+ remoteError = toErrorMessage(err);
565
+ checks.push({
566
+ id: "remote_probe",
567
+ status: "fail",
568
+ message: `OrgX API probe failed: ${remoteError}`,
569
+ });
570
+ }
571
+ }
572
+ }
573
+ if (onboardingState.status === "error") {
574
+ checks.push({
575
+ id: "onboarding_state",
576
+ status: "warn",
577
+ message: onboardingState.lastError
578
+ ? `Onboarding reports an error: ${onboardingState.lastError}`
579
+ : "Onboarding reports an error state.",
580
+ });
581
+ }
582
+ const hasFail = checks.some((check) => check.status === "fail");
583
+ const hasWarn = checks.some((check) => check.status === "warn");
584
+ const status = hasFail
585
+ ? "error"
586
+ : hasWarn
587
+ ? "degraded"
588
+ : "ok";
589
+ return {
590
+ ok: status !== "error",
591
+ status,
592
+ generatedAt,
593
+ checks,
594
+ plugin: {
595
+ version: config.pluginVersion,
596
+ installationId: config.installationId,
597
+ enabled: config.enabled,
598
+ dashboardEnabled: config.dashboardEnabled,
599
+ baseUrl: config.baseUrl,
600
+ },
601
+ auth: {
602
+ hasApiKey,
603
+ keySource: config.apiKeySource,
604
+ userIdConfigured: Boolean(config.userId && config.userId.trim().length > 0),
605
+ onboardingStatus: onboardingState.status,
606
+ },
607
+ sync: {
608
+ serviceRunning: syncServiceRunning,
609
+ inFlight: syncInFlight !== null,
610
+ lastSnapshotAt: lastSnapshotAt > 0 ? new Date(lastSnapshotAt).toISOString() : null,
611
+ },
612
+ outbox: {
613
+ pendingTotal: outbox.pendingTotal,
614
+ pendingByQueue: outbox.pendingByQueue,
615
+ oldestEventAt: outbox.oldestEventAt,
616
+ newestEventAt: outbox.newestEventAt,
617
+ replayStatus: outboxReplayState.status,
618
+ lastReplayAttemptAt: outboxReplayState.lastReplayAttemptAt,
619
+ lastReplaySuccessAt: outboxReplayState.lastReplaySuccessAt,
620
+ lastReplayFailureAt: outboxReplayState.lastReplayFailureAt,
621
+ lastReplayError: outboxReplayState.lastReplayError,
622
+ },
623
+ remote: {
624
+ enabled: probeRemote,
625
+ reachable: remoteReachable,
626
+ latencyMs: remoteLatencyMs,
627
+ error: remoteError,
628
+ },
629
+ };
630
+ }
388
631
  function pickStringField(payload, key) {
389
632
  const value = payload[key];
390
633
  return typeof value === "string" && value.trim().length > 0
@@ -516,6 +759,15 @@ export default function register(api) {
516
759
  }
517
760
  }
518
761
  async function flushOutboxQueues() {
762
+ const attemptAt = new Date().toISOString();
763
+ outboxReplayState = {
764
+ ...outboxReplayState,
765
+ status: "running",
766
+ lastReplayAttemptAt: attemptAt,
767
+ lastReplayError: null,
768
+ };
769
+ let hadReplayFailure = false;
770
+ let lastReplayError = null;
519
771
  for (const queue of outboxQueues) {
520
772
  const pending = await readOutbox(queue);
521
773
  if (pending.length === 0) {
@@ -527,11 +779,13 @@ export default function register(api) {
527
779
  await replayOutboxEvent(event);
528
780
  }
529
781
  catch (err) {
782
+ hadReplayFailure = true;
783
+ lastReplayError = toErrorMessage(err);
530
784
  remaining.push(event);
531
785
  api.log?.warn?.("[orgx] Outbox replay failed", {
532
786
  queue,
533
787
  eventId: event.id,
534
- error: toErrorMessage(err),
788
+ error: lastReplayError,
535
789
  });
536
790
  }
537
791
  }
@@ -545,6 +799,22 @@ export default function register(api) {
545
799
  });
546
800
  }
547
801
  }
802
+ if (hadReplayFailure) {
803
+ outboxReplayState = {
804
+ ...outboxReplayState,
805
+ status: "error",
806
+ lastReplayFailureAt: new Date().toISOString(),
807
+ lastReplayError,
808
+ };
809
+ }
810
+ else {
811
+ outboxReplayState = {
812
+ ...outboxReplayState,
813
+ status: "success",
814
+ lastReplaySuccessAt: new Date().toISOString(),
815
+ lastReplayError: null,
816
+ };
817
+ }
548
818
  }
549
819
  async function doSync() {
550
820
  if (syncInFlight) {
@@ -561,8 +831,7 @@ export default function register(api) {
561
831
  return;
562
832
  }
563
833
  try {
564
- cachedSnapshot = await client.getOrgSnapshot();
565
- lastSnapshotAt = Date.now();
834
+ updateCachedSnapshot(await client.getOrgSnapshot());
566
835
  updateOnboardingState({
567
836
  status: "connected",
568
837
  hasApiKey: true,
@@ -760,17 +1029,16 @@ export default function register(api) {
760
1029
  lastError: null,
761
1030
  nextAction: "enter_manual_key",
762
1031
  });
763
- const probeClient = new OrgXClient(nextKey, config.baseUrl, input.userId?.trim() || config.userId);
1032
+ const probeClient = new OrgXClient(nextKey, config.baseUrl, resolveRuntimeUserId(nextKey, [input.userId, config.userId]));
764
1033
  const snapshot = await probeClient.getOrgSnapshot();
765
1034
  setRuntimeApiKey({
766
1035
  apiKey: nextKey,
767
1036
  source: "manual",
768
- userId: input.userId?.trim() || null,
1037
+ userId: resolveRuntimeUserId(nextKey, [input.userId, config.userId]) || null,
769
1038
  workspaceName: onboardingState.workspaceName,
770
1039
  keyPrefix: null,
771
1040
  });
772
- cachedSnapshot = snapshot;
773
- lastSnapshotAt = Date.now();
1041
+ updateCachedSnapshot(snapshot);
774
1042
  return updateOnboardingState({
775
1043
  status: "connected",
776
1044
  hasApiKey: true,
@@ -788,8 +1056,10 @@ export default function register(api) {
788
1056
  }
789
1057
  clearPairingState();
790
1058
  clearPersistedApiKey();
1059
+ clearPersistedSnapshot();
791
1060
  config.apiKey = "";
792
- client.setCredentials({ apiKey: "" });
1061
+ config.userId = "";
1062
+ client.setCredentials({ apiKey: "", userId: "" });
793
1063
  cachedSnapshot = null;
794
1064
  lastSnapshotAt = 0;
795
1065
  return updateOnboardingState({
@@ -1841,6 +2111,58 @@ export default function register(api) {
1841
2111
  process.exit(1);
1842
2112
  }
1843
2113
  });
2114
+ cmd
2115
+ .command("doctor")
2116
+ .description("Run plugin diagnostics and connectivity checks")
2117
+ .option("--json", "Print the report as JSON")
2118
+ .option("--no-remote", "Skip remote OrgX API reachability probe")
2119
+ .action(async (opts = {}) => {
2120
+ try {
2121
+ const report = await buildHealthReport({
2122
+ probeRemote: opts.remote !== false,
2123
+ });
2124
+ if (opts.json) {
2125
+ console.log(JSON.stringify(report, null, 2));
2126
+ if (!report.ok)
2127
+ process.exit(1);
2128
+ return;
2129
+ }
2130
+ console.log("OrgX Doctor");
2131
+ console.log(` Status: ${report.status.toUpperCase()}`);
2132
+ console.log(` Plugin: v${report.plugin.version}`);
2133
+ console.log(` Base URL: ${report.plugin.baseUrl}`);
2134
+ console.log(` API Key Source: ${apiKeySourceLabel(report.auth.keySource)}`);
2135
+ console.log(` Outbox Pending: ${report.outbox.pendingTotal}`);
2136
+ console.log("");
2137
+ console.log("Checks:");
2138
+ for (const check of report.checks) {
2139
+ const prefix = check.status === "pass"
2140
+ ? "[PASS]"
2141
+ : check.status === "warn"
2142
+ ? "[WARN]"
2143
+ : "[FAIL]";
2144
+ console.log(` ${prefix} ${check.message}`);
2145
+ }
2146
+ if (report.remote.enabled) {
2147
+ if (report.remote.reachable === true) {
2148
+ console.log(` Remote probe latency: ${report.remote.latencyMs ?? "?"}ms`);
2149
+ }
2150
+ else if (report.remote.reachable === false) {
2151
+ console.log(` Remote probe error: ${report.remote.error ?? "Unknown error"}`);
2152
+ }
2153
+ else {
2154
+ console.log(" Remote probe: skipped");
2155
+ }
2156
+ }
2157
+ if (!report.ok) {
2158
+ process.exit(1);
2159
+ }
2160
+ }
2161
+ catch (err) {
2162
+ console.error(`Doctor failed: ${err instanceof Error ? err.message : err}`);
2163
+ process.exit(1);
2164
+ }
2165
+ });
1844
2166
  }, { commands: ["orgx"] });
1845
2167
  // ---------------------------------------------------------------------------
1846
2168
  // 4. HTTP Handler — Dashboard + API proxy
@@ -1851,6 +2173,8 @@ export default function register(api) {
1851
2173
  getStatus: getPairingStatus,
1852
2174
  submitManualKey,
1853
2175
  disconnect: disconnectOnboarding,
2176
+ }, {
2177
+ getHealth: async (input = {}) => buildHealthReport({ probeRemote: input.probeRemote === true }),
1854
2178
  });
1855
2179
  api.registerHttpHandler(httpHandler);
1856
2180
  api.log?.info?.("[orgx] Plugin registered", {