@useorgx/openclaw-plugin 0.3.0 → 0.3.2

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