@useorgx/openclaw-plugin 0.4.6 → 0.4.9

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 (137) hide show
  1. package/README.md +310 -24
  2. package/dashboard/dist/assets/B5NEElEI.css +1 -0
  3. package/dashboard/dist/assets/BhapSNAs.js +215 -0
  4. package/dashboard/dist/assets/iFdvE7lx.js +1 -0
  5. package/dashboard/dist/assets/jRJsmpYM.js +1 -0
  6. package/dashboard/dist/index.html +2 -2
  7. package/dist/activity-actor-fields.d.ts +3 -0
  8. package/dist/activity-actor-fields.js +128 -0
  9. package/dist/activity-store.js +12 -19
  10. package/dist/agent-context-store.js +5 -25
  11. package/dist/agent-run-store.js +5 -25
  12. package/dist/agent-suite.js +1 -8
  13. package/dist/artifacts/register-artifact.d.ts +47 -0
  14. package/dist/artifacts/register-artifact.js +271 -0
  15. package/dist/auth/flows.d.ts +47 -0
  16. package/dist/auth/flows.js +169 -0
  17. package/dist/auth-store.js +14 -39
  18. package/dist/byok-store.js +5 -19
  19. package/dist/cli/orgx.d.ts +66 -0
  20. package/dist/cli/orgx.js +91 -0
  21. package/dist/config/refresh.d.ts +32 -0
  22. package/dist/config/refresh.js +55 -0
  23. package/dist/config/resolution.d.ts +37 -0
  24. package/dist/config/resolution.js +178 -0
  25. package/dist/contracts/client.d.ts +1 -0
  26. package/dist/contracts/client.js +7 -5
  27. package/dist/contracts/shared-types.d.ts +147 -0
  28. package/dist/contracts/shared-types.js +3 -0
  29. package/dist/contracts/types.d.ts +1 -130
  30. package/dist/contracts/types.js +5 -0
  31. package/dist/entities/auto-assignment.d.ts +36 -0
  32. package/dist/entities/auto-assignment.js +115 -0
  33. package/dist/entity-comment-store.js +5 -25
  34. package/dist/hash-utils.d.ts +2 -0
  35. package/dist/hash-utils.js +12 -0
  36. package/dist/http/helpers/activity-headline.d.ts +10 -0
  37. package/dist/http/helpers/activity-headline.js +192 -0
  38. package/dist/http/helpers/artifact-fallback.d.ts +13 -0
  39. package/dist/http/helpers/artifact-fallback.js +148 -0
  40. package/dist/http/helpers/auto-continue-engine.d.ts +298 -0
  41. package/dist/http/helpers/auto-continue-engine.js +1218 -0
  42. package/dist/http/helpers/autopilot-operations.d.ts +157 -0
  43. package/dist/http/helpers/autopilot-operations.js +403 -0
  44. package/dist/http/helpers/autopilot-runtime.d.ts +42 -0
  45. package/dist/http/helpers/autopilot-runtime.js +319 -0
  46. package/dist/http/helpers/autopilot-slice-utils.d.ts +38 -0
  47. package/dist/http/helpers/autopilot-slice-utils.js +476 -0
  48. package/dist/http/helpers/decision-mapper.d.ts +12 -0
  49. package/dist/http/helpers/decision-mapper.js +44 -0
  50. package/dist/http/helpers/dispatch-lifecycle.d.ts +102 -0
  51. package/dist/http/helpers/dispatch-lifecycle.js +604 -0
  52. package/dist/http/helpers/hash-utils.d.ts +1 -0
  53. package/dist/http/helpers/hash-utils.js +1 -0
  54. package/dist/http/helpers/kickoff-context.d.ts +12 -0
  55. package/dist/http/helpers/kickoff-context.js +154 -0
  56. package/dist/http/helpers/mission-control.d.ts +94 -0
  57. package/dist/http/helpers/mission-control.js +894 -0
  58. package/dist/http/helpers/openclaw-cli.d.ts +37 -0
  59. package/dist/http/helpers/openclaw-cli.js +283 -0
  60. package/dist/http/helpers/runtime-sse.d.ts +20 -0
  61. package/dist/http/helpers/runtime-sse.js +110 -0
  62. package/dist/http/helpers/value-utils.d.ts +6 -0
  63. package/dist/http/helpers/value-utils.js +67 -0
  64. package/dist/http/index.d.ts +88 -0
  65. package/dist/http/index.js +2353 -0
  66. package/dist/http/router.d.ts +23 -0
  67. package/dist/http/router.js +23 -0
  68. package/dist/http/routes/agent-control.d.ts +79 -0
  69. package/dist/http/routes/agent-control.js +684 -0
  70. package/dist/http/routes/agent-suite.d.ts +29 -0
  71. package/dist/http/routes/agent-suite.js +198 -0
  72. package/dist/http/routes/agents-catalog.d.ts +40 -0
  73. package/dist/http/routes/agents-catalog.js +83 -0
  74. package/dist/http/routes/billing.d.ts +23 -0
  75. package/dist/http/routes/billing.js +55 -0
  76. package/dist/http/routes/debug.d.ts +14 -0
  77. package/dist/http/routes/debug.js +21 -0
  78. package/dist/http/routes/decision-actions.d.ts +13 -0
  79. package/dist/http/routes/decision-actions.js +66 -0
  80. package/dist/http/routes/delegation.d.ts +19 -0
  81. package/dist/http/routes/delegation.js +32 -0
  82. package/dist/http/routes/entities.d.ts +47 -0
  83. package/dist/http/routes/entities.js +152 -0
  84. package/dist/http/routes/entity-dynamic.d.ts +25 -0
  85. package/dist/http/routes/entity-dynamic.js +191 -0
  86. package/dist/http/routes/health.d.ts +22 -0
  87. package/dist/http/routes/health.js +49 -0
  88. package/dist/http/routes/live-legacy.d.ts +110 -0
  89. package/dist/http/routes/live-legacy.js +598 -0
  90. package/dist/http/routes/live-misc.d.ts +69 -0
  91. package/dist/http/routes/live-misc.js +206 -0
  92. package/dist/http/routes/live-snapshot.d.ts +90 -0
  93. package/dist/http/routes/live-snapshot.js +297 -0
  94. package/dist/http/routes/mission-control-actions.d.ts +83 -0
  95. package/dist/http/routes/mission-control-actions.js +541 -0
  96. package/dist/http/routes/mission-control-read.d.ts +28 -0
  97. package/dist/http/routes/mission-control-read.js +67 -0
  98. package/dist/http/routes/onboarding.d.ts +34 -0
  99. package/dist/http/routes/onboarding.js +101 -0
  100. package/dist/http/routes/run-control.d.ts +24 -0
  101. package/dist/http/routes/run-control.js +86 -0
  102. package/dist/http/routes/runtime-hooks.d.ts +69 -0
  103. package/dist/http/routes/runtime-hooks.js +437 -0
  104. package/dist/http/routes/settings-byok.d.ts +23 -0
  105. package/dist/http/routes/settings-byok.js +163 -0
  106. package/dist/http/routes/summary.d.ts +18 -0
  107. package/dist/http/routes/summary.js +42 -0
  108. package/dist/http/routes/work-artifacts.d.ts +9 -0
  109. package/dist/http/routes/work-artifacts.js +36 -0
  110. package/dist/http/shared-state.d.ts +16 -0
  111. package/dist/http/shared-state.js +1 -0
  112. package/dist/http-handler.d.ts +1 -88
  113. package/dist/http-handler.js +1 -9664
  114. package/dist/index.js +122 -2121
  115. package/dist/json-utils.d.ts +1 -0
  116. package/dist/json-utils.js +8 -0
  117. package/dist/local-openclaw.js +8 -0
  118. package/dist/mcp-client-setup.js +75 -90
  119. package/dist/next-up-queue-store.js +4 -18
  120. package/dist/runtime-instance-store.js +8 -34
  121. package/dist/services/background.d.ts +23 -0
  122. package/dist/services/background.js +23 -0
  123. package/dist/services/instrumentation.d.ts +29 -0
  124. package/dist/services/instrumentation.js +136 -0
  125. package/dist/snapshot-store.js +5 -25
  126. package/dist/stores/json-store.d.ts +11 -0
  127. package/dist/stores/json-store.js +42 -0
  128. package/dist/sync/outbox-replay.d.ts +55 -0
  129. package/dist/sync/outbox-replay.js +514 -0
  130. package/dist/tools/core-tools.d.ts +76 -0
  131. package/dist/tools/core-tools.js +1005 -0
  132. package/dist/worker-supervisor.js +15 -0
  133. package/package.json +6 -1
  134. package/dashboard/dist/assets/0tOC3wSN.js +0 -214
  135. package/dashboard/dist/assets/Bm8QnMJ_.js +0 -1
  136. package/dashboard/dist/assets/CyxZio4Y.js +0 -1
  137. package/dashboard/dist/assets/DaAIOik3.css +0 -1
package/dist/index.js CHANGED
@@ -13,192 +13,33 @@
13
13
  import { OrgXClient } from "./api.js";
14
14
  import { createHttpHandler } from "./http-handler.js";
15
15
  import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan } from "./agent-suite.js";
16
- import { appendActivityItems } from "./activity-store.js";
16
+ import { autoAssignEntityForCreate as autoAssignEntityForCreateWithClient, } from "./entities/auto-assignment.js";
17
17
  import { existsSync, readFileSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { homedir } from "node:os";
20
- import { fileURLToPath } from "node:url";
21
- import { createHash, randomUUID } from "node:crypto";
22
- import { clearPersistedApiKey, loadAuthStore, resolveInstallationId, saveAuthStore, } from "./auth-store.js";
20
+ import { randomUUID } from "node:crypto";
21
+ import { clearPersistedApiKey, loadAuthStore, resolveInstallationId, } from "./auth-store.js";
23
22
  import { clearPersistedSnapshot, readPersistedSnapshot, writePersistedSnapshot, } from "./snapshot-store.js";
24
- import { appendToOutbox, readOutbox, readOutboxSummary, replaceOutbox, } from "./outbox.js";
23
+ import { appendToOutbox, readOutboxSummary, } from "./outbox.js";
25
24
  import { getAgentContext, readAgentContexts } from "./agent-context-store.js";
26
25
  import { readAgentRuns, markAgentRunStopped } from "./agent-run-store.js";
27
- import { extractProgressOutboxMessage } from "./reporting/outbox-replay.js";
28
26
  import { ensureGatewayWatchdog } from "./gateway-watchdog.js";
29
27
  import { createMcpHttpHandler, } from "./mcp-http-handler.js";
30
- import { autoConfigureDetectedMcpClients } from "./mcp-client-setup.js";
31
- import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot } from "./openclaw-settings.js";
32
28
  import { posthogCapture } from "./telemetry/posthog.js";
33
29
  import { readSkillPackState, refreshSkillPackState } from "./skill-pack-state.js";
30
+ import { resolveConfig, resolveRuntimeUserId, } from "./config/resolution.js";
31
+ import { refreshResolvedConfig } from "./config/refresh.js";
32
+ import { applyRuntimeApiKey, buildManualKeyConnectUrl as buildManualKeyConnectUrlForBase, fetchOrgxJson as fetchOrgxJsonRequest, isAuthRequiredError, } from "./auth/flows.js";
33
+ import { registerOrgxCli } from "./cli/orgx.js";
34
+ import { instrumentPluginApi } from "./services/instrumentation.js";
35
+ import { registerSyncService } from "./services/background.js";
36
+ import { createOutboxReplayer } from "./sync/outbox-replay.js";
37
+ import { registerCoreTools } from "./tools/core-tools.js";
38
+ import { stableHash } from "./hash-utils.js";
34
39
  export { OrgXClient } from "./api.js";
35
- const DEFAULT_BASE_URL = "https://www.useorgx.com";
36
- const DEFAULT_DOCS_URL = "https://orgx.mintlify.site/guides/openclaw-plugin-setup";
37
- function isUserScopedApiKey(apiKey) {
38
- return apiKey.trim().toLowerCase().startsWith("oxk_");
39
- }
40
- function resolveRuntimeUserId(apiKey, candidates) {
41
- if (isUserScopedApiKey(apiKey)) {
42
- return "";
43
- }
44
- for (const candidate of candidates) {
45
- if (typeof candidate === "string") {
46
- const trimmed = candidate.trim();
47
- if (trimmed.length > 0)
48
- return trimmed;
49
- }
50
- }
51
- return "";
52
- }
53
- function normalizeHost(value) {
54
- return value.trim().toLowerCase().replace(/^\[|\]$/g, "");
55
- }
56
- function isLoopbackHostname(hostname) {
57
- const normalized = normalizeHost(hostname);
58
- return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
59
- }
60
- function normalizeBaseUrl(raw) {
61
- const candidate = raw?.trim() ?? "";
62
- if (!candidate) {
63
- return DEFAULT_BASE_URL;
64
- }
65
- try {
66
- const parsed = new URL(candidate);
67
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
68
- return DEFAULT_BASE_URL;
69
- }
70
- // Do not allow credential-bearing URLs.
71
- if (parsed.username || parsed.password) {
72
- return DEFAULT_BASE_URL;
73
- }
74
- // Plain HTTP is only allowed for local loopback development.
75
- if (parsed.protocol === "http:" && !isLoopbackHostname(parsed.hostname)) {
76
- return DEFAULT_BASE_URL;
77
- }
78
- parsed.search = "";
79
- parsed.hash = "";
80
- const normalizedPath = parsed.pathname.replace(/\/+$/, "");
81
- parsed.pathname = normalizedPath;
82
- const normalized = parsed.toString().replace(/\/+$/, "");
83
- return normalized.length > 0 ? normalized : DEFAULT_BASE_URL;
84
- }
85
- catch {
86
- return DEFAULT_BASE_URL;
87
- }
88
- }
89
- function readLegacyEnvValue(keyPattern) {
90
- try {
91
- const envPath = join(homedir(), "Code", "orgx", "orgx", ".env.local");
92
- const envContent = readFileSync(envPath, "utf-8");
93
- const match = envContent.match(keyPattern);
94
- return match?.[1]?.trim() ?? "";
95
- }
96
- catch {
97
- return "";
98
- }
99
- }
100
- function readOpenClawOrgxConfig() {
101
- try {
102
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
103
- const raw = readFileSync(configPath, "utf8");
104
- const parsed = JSON.parse(raw);
105
- const plugins = parsed.plugins && typeof parsed.plugins === "object"
106
- ? parsed.plugins
107
- : {};
108
- const entries = plugins.entries && typeof plugins.entries === "object"
109
- ? plugins.entries
110
- : {};
111
- const orgx = entries.orgx && typeof entries.orgx === "object"
112
- ? entries.orgx
113
- : {};
114
- const config = orgx.config && typeof orgx.config === "object"
115
- ? orgx.config
116
- : {};
117
- const apiKey = typeof config.apiKey === "string" ? config.apiKey.trim() : "";
118
- const userId = typeof config.userId === "string" ? config.userId.trim() : "";
119
- const baseUrl = typeof config.baseUrl === "string" ? config.baseUrl.trim() : "";
120
- return { apiKey, userId, baseUrl };
121
- }
122
- catch {
123
- return { apiKey: "", userId: "", baseUrl: "" };
124
- }
125
- }
126
- function resolveApiKey(pluginConf, persistedApiKey) {
127
- if (pluginConf.apiKey && pluginConf.apiKey.trim().length > 0) {
128
- return { value: pluginConf.apiKey.trim(), source: "config" };
129
- }
130
- if (process.env.ORGX_API_KEY && process.env.ORGX_API_KEY.trim().length > 0) {
131
- return { value: process.env.ORGX_API_KEY.trim(), source: "environment" };
132
- }
133
- if (persistedApiKey && persistedApiKey.trim().length > 0) {
134
- return { value: persistedApiKey.trim(), source: "persisted" };
135
- }
136
- const openclaw = readOpenClawOrgxConfig();
137
- if (openclaw.apiKey) {
138
- return { value: openclaw.apiKey, source: "openclaw-config-file" };
139
- }
140
- // For local dev convenience we read `ORGX_API_KEY` from `~/Code/orgx/orgx/.env.local`.
141
- // Do not auto-consume `ORGX_SERVICE_KEY` because service keys often require `X-Orgx-User-Id`,
142
- // and the dashboard/client flows are intended to run on user-scoped keys (`oxk_...`).
143
- const legacy = readLegacyEnvValue(/^ORGX_API_KEY=["']?([^"'\n]+)["']?$/m);
144
- if (legacy) {
145
- return { value: legacy, source: "legacy-dev" };
146
- }
147
- return { value: "", source: "none" };
148
- }
149
- function resolvePluginVersion() {
150
- try {
151
- const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
152
- const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
153
- return parsed.version && parsed.version.trim().length > 0
154
- ? parsed.version
155
- : "dev";
156
- }
157
- catch {
158
- return "dev";
159
- }
160
- }
161
- function resolveDocsUrl(baseUrl) {
162
- const normalized = baseUrl.replace(/\/+$/, "");
163
- try {
164
- const parsed = new URL(normalized);
165
- if (isLoopbackHostname(parsed.hostname)) {
166
- return `${normalized}/docs/mintlify/guides/openclaw-plugin-setup`;
167
- }
168
- }
169
- catch {
170
- return DEFAULT_DOCS_URL;
171
- }
172
- return DEFAULT_DOCS_URL;
173
- }
174
- function resolveConfig(api, input) {
175
- const pluginConf = api.config?.plugins?.entries?.orgx?.config ?? {};
176
- const openclaw = readOpenClawOrgxConfig();
177
- const apiKeyResolution = resolveApiKey(pluginConf, input.persistedApiKey);
178
- const apiKey = apiKeyResolution.value;
179
- // Resolve user ID for X-Orgx-User-Id header
180
- const userId = resolveRuntimeUserId(apiKey, [
181
- pluginConf.userId,
182
- process.env.ORGX_USER_ID,
183
- input.persistedUserId,
184
- openclaw.userId,
185
- readLegacyEnvValue(/^ORGX_USER_ID=["']?([^"'\n]+)["']?$/m),
186
- ]);
187
- const baseUrl = normalizeBaseUrl(pluginConf.baseUrl || process.env.ORGX_BASE_URL || openclaw.baseUrl || DEFAULT_BASE_URL);
188
- return {
189
- apiKey,
190
- userId,
191
- baseUrl,
192
- syncIntervalMs: pluginConf.syncIntervalMs ?? 300_000,
193
- enabled: pluginConf.enabled ?? true,
194
- autoInstallAgentSuiteOnConnect: pluginConf.autoInstallAgentSuiteOnConnect ?? true,
195
- dashboardEnabled: pluginConf.dashboardEnabled ?? true,
196
- installationId: input.installationId,
197
- pluginVersion: resolvePluginVersion(),
198
- docsUrl: resolveDocsUrl(baseUrl),
199
- apiKeySource: apiKeyResolution.source,
200
- };
201
- }
40
+ // =============================================================================
41
+ // HELPERS
42
+ // =============================================================================
202
43
  function text(s) {
203
44
  return { content: [{ type: "text", text: s }] };
204
45
  }
@@ -397,55 +238,17 @@ export default function register(api) {
397
238
  const defaultReportingCorrelationId = pickNonEmptyString(process.env.ORGX_CORRELATION_ID) ??
398
239
  `openclaw-${config.installationId}`;
399
240
  function refreshConfigFromSources(input) {
400
- const allowApiKeyChanges = input?.allowApiKeyChanges !== false;
401
- const previousApiKey = config.apiKey;
402
- const previousBaseUrl = config.baseUrl;
403
- const previousUserId = config.userId;
404
- const previousDocsUrl = config.docsUrl;
405
- const previousKeySource = config.apiKeySource;
406
- const latestPersisted = loadAuthStore();
407
- const next = resolveConfig(api, {
408
- installationId: config.installationId,
409
- persistedApiKey: latestPersisted?.apiKey ?? null,
410
- persistedUserId: latestPersisted?.userId ?? null,
411
- });
412
- const nextApiKey = allowApiKeyChanges ? next.apiKey : previousApiKey;
413
- const nextUserId = allowApiKeyChanges ? next.userId : previousUserId;
414
- const changed = nextApiKey !== previousApiKey ||
415
- next.baseUrl !== previousBaseUrl ||
416
- nextUserId !== previousUserId ||
417
- next.docsUrl !== previousDocsUrl ||
418
- next.apiKeySource !== previousKeySource;
419
- if (!changed) {
420
- return false;
421
- }
422
- if (allowApiKeyChanges) {
423
- config.apiKey = nextApiKey;
424
- config.userId = nextUserId;
425
- config.apiKeySource = next.apiKeySource;
426
- }
427
- config.baseUrl = next.baseUrl;
428
- config.docsUrl = next.docsUrl;
429
- baseApiUrl = config.baseUrl.replace(/\/+$/, "");
430
- client.setCredentials({
431
- apiKey: config.apiKey,
432
- userId: config.userId,
433
- baseUrl: config.baseUrl,
434
- });
435
- // Keep onboarding state aligned with what's actually configured (without forcing a status transition).
436
- updateOnboardingState({
437
- hasApiKey: Boolean(config.apiKey),
438
- keySource: config.apiKeySource,
439
- docsUrl: config.docsUrl,
440
- installationId: config.installationId,
441
- });
442
- api.log?.info?.("[orgx] Config refreshed", {
443
- reason: input?.reason ?? "runtime_refresh",
444
- baseUrl: config.baseUrl,
445
- hasApiKey: Boolean(config.apiKey),
446
- apiKeySource: config.apiKeySource,
447
- });
448
- return true;
241
+ const refreshed = refreshResolvedConfig({
242
+ api,
243
+ config,
244
+ loadAuthStore,
245
+ resolveConfig,
246
+ updateOnboardingState,
247
+ setCredentials: (credentials) => client.setCredentials(credentials),
248
+ logInfo: api.log?.info,
249
+ }, input);
250
+ baseApiUrl = refreshed.baseApiUrl;
251
+ return refreshed.changed;
449
252
  }
450
253
  function resolveReportingContext(input) {
451
254
  let initiativeId = pickNonEmptyString(input.initiative_id, input.initiativeId, process.env.ORGX_INITIATIVE_ID);
@@ -495,9 +298,6 @@ export default function register(api) {
495
298
  return err.message;
496
299
  return typeof err === "string" ? err : "Unexpected error";
497
300
  }
498
- function stableHash(value) {
499
- return createHash("sha256").update(value).digest("hex");
500
- }
501
301
  function isAuthFailure(err) {
502
302
  const message = toErrorMessage(err).toLowerCase();
503
303
  return (message.includes("401") ||
@@ -505,140 +305,12 @@ export default function register(api) {
505
305
  message.includes("invalid_token") ||
506
306
  message.includes("invalid api key"));
507
307
  }
508
- const registerTool = api.registerTool.bind(api);
509
- api.registerTool = (tool, options) => {
510
- const toolName = tool.name;
511
- const optional = Boolean(options?.optional);
512
- registerTool({
513
- ...tool,
514
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
515
- execute: async (callId, params) => {
516
- const startedAt = Date.now();
517
- void posthogCapture({
518
- event: "openclaw_tool_called",
519
- distinctId: config.installationId,
520
- properties: {
521
- tool_name: toolName,
522
- tool_optional: optional,
523
- call_id: callId,
524
- plugin_version: config.pluginVersion,
525
- },
526
- }).catch(() => {
527
- // best effort
528
- });
529
- try {
530
- const result = await tool.execute(callId, params);
531
- const durationMs = Date.now() - startedAt;
532
- void posthogCapture({
533
- event: "openclaw_tool_succeeded",
534
- distinctId: config.installationId,
535
- properties: {
536
- tool_name: toolName,
537
- tool_optional: optional,
538
- call_id: callId,
539
- duration_ms: durationMs,
540
- plugin_version: config.pluginVersion,
541
- },
542
- }).catch(() => {
543
- // best effort
544
- });
545
- return result;
546
- }
547
- catch (err) {
548
- const durationMs = Date.now() - startedAt;
549
- void posthogCapture({
550
- event: "openclaw_tool_failed",
551
- distinctId: config.installationId,
552
- properties: {
553
- tool_name: toolName,
554
- tool_optional: optional,
555
- call_id: callId,
556
- duration_ms: durationMs,
557
- plugin_version: config.pluginVersion,
558
- error: toErrorMessage(err),
559
- },
560
- }).catch(() => {
561
- // best effort
562
- });
563
- throw err;
564
- }
565
- },
566
- }, options);
567
- };
568
- const registerService = api.registerService.bind(api);
569
- api.registerService = (service) => {
570
- registerService({
571
- ...service,
572
- start: async () => {
573
- const startedAt = Date.now();
574
- try {
575
- await service.start();
576
- const durationMs = Date.now() - startedAt;
577
- void posthogCapture({
578
- event: "openclaw_service_started",
579
- distinctId: config.installationId,
580
- properties: {
581
- service_id: service.id,
582
- duration_ms: durationMs,
583
- plugin_version: config.pluginVersion,
584
- },
585
- }).catch(() => {
586
- // best effort
587
- });
588
- }
589
- catch (err) {
590
- const durationMs = Date.now() - startedAt;
591
- void posthogCapture({
592
- event: "openclaw_service_start_failed",
593
- distinctId: config.installationId,
594
- properties: {
595
- service_id: service.id,
596
- duration_ms: durationMs,
597
- plugin_version: config.pluginVersion,
598
- error: toErrorMessage(err),
599
- },
600
- }).catch(() => {
601
- // best effort
602
- });
603
- throw err;
604
- }
605
- },
606
- stop: async () => {
607
- const startedAt = Date.now();
608
- try {
609
- await service.stop();
610
- const durationMs = Date.now() - startedAt;
611
- void posthogCapture({
612
- event: "openclaw_service_stopped",
613
- distinctId: config.installationId,
614
- properties: {
615
- service_id: service.id,
616
- duration_ms: durationMs,
617
- plugin_version: config.pluginVersion,
618
- },
619
- }).catch(() => {
620
- // best effort
621
- });
622
- }
623
- catch (err) {
624
- const durationMs = Date.now() - startedAt;
625
- void posthogCapture({
626
- event: "openclaw_service_stop_failed",
627
- distinctId: config.installationId,
628
- properties: {
629
- service_id: service.id,
630
- duration_ms: durationMs,
631
- plugin_version: config.pluginVersion,
632
- error: toErrorMessage(err),
633
- },
634
- }).catch(() => {
635
- // best effort
636
- });
637
- throw err;
638
- }
639
- },
640
- });
641
- };
308
+ instrumentPluginApi({
309
+ api,
310
+ installationId: config.installationId,
311
+ pluginVersion: config.pluginVersion,
312
+ toErrorMessage,
313
+ });
642
314
  function clearPairingState() {
643
315
  activePairing = null;
644
316
  updateOnboardingState({
@@ -648,166 +320,32 @@ export default function register(api) {
648
320
  pollIntervalMs: null,
649
321
  });
650
322
  }
651
- function isAuthRequiredError(result) {
652
- if (result.status !== 401) {
653
- return false;
654
- }
655
- return /auth|unauthorized|token/i.test(result.error);
656
- }
657
323
  function buildManualKeyConnectUrl() {
658
- try {
659
- // Deep-link into the Security section where API keys live.
660
- return new URL("/settings#security", baseApiUrl).toString();
661
- }
662
- catch {
663
- return "https://www.useorgx.com/settings#security";
664
- }
324
+ return buildManualKeyConnectUrlForBase(baseApiUrl);
665
325
  }
666
326
  async function fetchOrgxJson(method, path, body, options) {
667
- try {
668
- const controller = new AbortController();
669
- const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
670
- ? Math.max(1_000, Math.floor(options.timeoutMs))
671
- : 12_000;
672
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
673
- let response;
674
- let rawText = "";
675
- try {
676
- response = await fetch(`${baseApiUrl}${path}`, {
677
- method,
678
- signal: controller.signal,
679
- headers: {
680
- Accept: "application/json",
681
- "Content-Type": "application/json",
682
- },
683
- body: body ? JSON.stringify(body) : undefined,
684
- });
685
- rawText = await response.text().catch(() => "");
686
- }
687
- finally {
688
- clearTimeout(timeout);
689
- }
690
- const payload = (() => {
691
- if (!rawText)
692
- return null;
693
- try {
694
- return JSON.parse(rawText);
695
- }
696
- catch {
697
- return null;
698
- }
699
- })();
700
- if (!response.ok) {
701
- const rawError = payload?.error ?? payload?.message;
702
- let errorMessage;
703
- if (typeof rawError === "string") {
704
- errorMessage = rawError;
705
- }
706
- else if (rawError &&
707
- typeof rawError === "object" &&
708
- "message" in rawError &&
709
- typeof rawError.message === "string") {
710
- errorMessage = rawError.message;
711
- }
712
- else if (rawText && rawText.trim().length > 0) {
713
- // Avoid dumping HTML (Cloudflare / Next.js error pages) into UI; keep it short.
714
- const sanitized = rawText
715
- .replace(/\s+/g, " ")
716
- .replace(/<[^>]+>/g, "")
717
- .trim();
718
- errorMessage = sanitized.length > 0 ? sanitized.slice(0, 180) : `OrgX request failed (${response.status})`;
719
- }
720
- else {
721
- errorMessage = `OrgX request failed (${response.status})`;
722
- }
723
- const statusToken = `HTTP ${response.status}`;
724
- if (response.status &&
725
- !errorMessage.toLowerCase().includes(statusToken.toLowerCase()) &&
726
- !errorMessage.includes(`(${response.status})`)) {
727
- errorMessage = `${errorMessage} (HTTP ${response.status})`;
728
- }
729
- const debugParts = [];
730
- const requestId = response.headers.get("x-request-id");
731
- const vercelId = response.headers.get("x-vercel-id");
732
- const cfRay = response.headers.get("cf-ray");
733
- const clerkStatus = response.headers.get("x-clerk-auth-status");
734
- const clerkReason = response.headers.get("x-clerk-auth-reason");
735
- if (requestId)
736
- debugParts.push(`req=${requestId}`);
737
- if (vercelId && vercelId !== requestId)
738
- debugParts.push(`vercel=${vercelId}`);
739
- if (cfRay)
740
- debugParts.push(`cf-ray=${cfRay}`);
741
- if (clerkStatus)
742
- debugParts.push(`clerk=${clerkStatus}`);
743
- if (clerkReason)
744
- debugParts.push(`clerk_reason=${clerkReason}`);
745
- const debugSuffix = debugParts.length > 0 ? ` (${debugParts.join(", ")})` : "";
746
- return {
747
- ok: false,
748
- status: response.status,
749
- error: `${errorMessage}${debugSuffix}`,
750
- };
751
- }
752
- if (payload?.data !== undefined) {
753
- return { ok: true, data: payload.data };
754
- }
755
- if (payload !== null) {
756
- return { ok: true, data: payload };
757
- }
758
- return { ok: true, data: rawText };
759
- }
760
- catch (err) {
761
- const message = err &&
762
- typeof err === "object" &&
763
- "name" in err &&
764
- err.name === "AbortError"
765
- ? `OrgX request timed out (method=${method}, path=${path})`
766
- : toErrorMessage(err);
767
- return { ok: false, status: 0, error: message };
768
- }
327
+ return fetchOrgxJsonRequest({
328
+ baseApiUrl,
329
+ method,
330
+ path,
331
+ body,
332
+ options,
333
+ toErrorMessage,
334
+ });
769
335
  }
770
336
  function setRuntimeApiKey(input) {
771
- const nextApiKey = input.apiKey.trim();
772
- config.apiKey = nextApiKey;
773
- config.apiKeySource = "persisted";
774
- config.userId = resolveRuntimeUserId(nextApiKey, [input.userId, config.userId]);
775
- client.setCredentials({
776
- apiKey: config.apiKey,
777
- userId: config.userId,
778
- baseUrl: config.baseUrl,
779
- });
780
- saveAuthStore({
781
- installationId: config.installationId,
782
- apiKey: nextApiKey,
783
- userId: config.userId || null,
784
- workspaceName: input.workspaceName ?? null,
785
- keyPrefix: input.keyPrefix ?? null,
337
+ applyRuntimeApiKey({
338
+ config,
339
+ apiKey: input.apiKey,
786
340
  source: input.source,
341
+ workspaceName: input.workspaceName,
342
+ keyPrefix: input.keyPrefix,
343
+ userId: input.userId,
344
+ currentWorkspaceName: onboardingState.workspaceName,
345
+ updateOnboardingState,
346
+ setCredentials: (credentials) => client.setCredentials(credentials),
347
+ logger: api.log ?? {},
787
348
  });
788
- updateOnboardingState({
789
- hasApiKey: true,
790
- keySource: "persisted",
791
- installationId: config.installationId,
792
- workspaceName: input.workspaceName ?? onboardingState.workspaceName,
793
- });
794
- if (input.source === "browser_pairing" &&
795
- process.env.ORGX_DISABLE_MCP_CLIENT_AUTOCONFIG !== "1") {
796
- try {
797
- const snapshot = readOpenClawSettingsSnapshot();
798
- const port = readOpenClawGatewayPort(snapshot.raw);
799
- const localMcpUrl = `http://127.0.0.1:${port}/orgx/mcp`;
800
- void autoConfigureDetectedMcpClients({
801
- localMcpUrl,
802
- logger: api.log ?? {},
803
- }).catch(() => {
804
- // best effort
805
- });
806
- }
807
- catch {
808
- // best effort
809
- }
810
- }
811
349
  }
812
350
  // ---------------------------------------------------------------------------
813
351
  // 1. Background Sync Service
@@ -1211,6 +749,10 @@ export default function register(api) {
1211
749
  description: null,
1212
750
  agentId: stopped.agentId,
1213
751
  agentName: null,
752
+ requesterAgentId: stopped.agentId ?? null,
753
+ requesterAgentName: null,
754
+ executorAgentId: stopped.agentId ?? null,
755
+ executorAgentName: null,
1214
756
  runId: stopped.runId,
1215
757
  initiativeId,
1216
758
  timestamp,
@@ -1241,6 +783,10 @@ export default function register(api) {
1241
783
  description: null,
1242
784
  agentId: stopped.agentId,
1243
785
  agentName: null,
786
+ requesterAgentId: stopped.agentId ?? null,
787
+ requesterAgentName: null,
788
+ executorAgentId: stopped.agentId ?? null,
789
+ executorAgentName: null,
1244
790
  runId: stopped.runId,
1245
791
  initiativeId,
1246
792
  timestamp,
@@ -1265,471 +811,22 @@ export default function register(api) {
1265
811
  // best effort
1266
812
  }
1267
813
  }
1268
- async function replayOutboxEvent(event) {
1269
- const payload = event.payload ?? {};
1270
- function normalizeRunFields(context) {
1271
- // We prefer correlation IDs for replay because many local adapters use UUID-like
1272
- // session IDs that do *not* exist as server-side run IDs.
1273
- if (context.correlationId) {
1274
- return { run_id: undefined, correlation_id: context.correlationId };
1275
- }
1276
- if (context.runId) {
1277
- return {
1278
- run_id: undefined,
1279
- correlation_id: `openclaw_run_${stableHash(context.runId).slice(0, 24)}`,
1280
- };
1281
- }
1282
- return { run_id: undefined, correlation_id: undefined };
1283
- }
1284
- if (event.type === "progress") {
1285
- const message = extractProgressOutboxMessage(payload);
1286
- if (!message) {
1287
- api.log?.warn?.("[orgx] Dropping invalid progress outbox event", {
1288
- eventId: event.id,
1289
- });
1290
- return;
1291
- }
1292
- const context = resolveReportingContext(payload);
1293
- if (!context.ok) {
1294
- throw new Error(context.error);
1295
- }
1296
- const rawPhase = pickStringField(payload, "phase") ?? "implementing";
1297
- const progressPct = typeof payload.progress_pct === "number"
1298
- ? payload.progress_pct
1299
- : typeof payload.progressPct === "number"
1300
- ? payload.progressPct
1301
- : undefined;
1302
- const phase = rawPhase === "intent" ||
1303
- rawPhase === "execution" ||
1304
- rawPhase === "blocked" ||
1305
- rawPhase === "review" ||
1306
- rawPhase === "handoff" ||
1307
- rawPhase === "completed"
1308
- ? rawPhase
1309
- : toReportingPhase(rawPhase, progressPct);
1310
- const metaRaw = payload.metadata;
1311
- const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
1312
- ? metaRaw
1313
- : {};
1314
- const baseMetadata = {
1315
- ...meta,
1316
- source: "orgx_openclaw_outbox_replay",
1317
- outbox_event_id: event.id,
1318
- };
1319
- let emitPayload = {
1320
- initiative_id: context.value.initiativeId,
1321
- run_id: context.value.runId,
1322
- correlation_id: context.value.correlationId,
1323
- source_client: context.value.sourceClient,
1324
- message,
1325
- phase,
1326
- progress_pct: progressPct,
1327
- level: pickStringField(payload, "level"),
1328
- next_step: pickStringField(payload, "next_step") ??
1329
- pickStringField(payload, "nextStep") ??
1330
- undefined,
1331
- metadata: baseMetadata,
1332
- };
1333
- // Locally-buffered progress events often store a local UUID in run_id. OrgX may reject
1334
- // unknown run IDs on replay; prefer a deterministic non-UUID correlation key instead.
1335
- if (emitPayload.run_id && !emitPayload.correlation_id) {
1336
- const replayCorrelationId = `openclaw_run_${stableHash(emitPayload.run_id).slice(0, 24)}`;
1337
- emitPayload = {
1338
- ...emitPayload,
1339
- run_id: undefined,
1340
- correlation_id: replayCorrelationId,
1341
- metadata: {
1342
- ...(emitPayload.metadata ?? {}),
1343
- replay_run_id_as_correlation: true,
1344
- },
1345
- };
1346
- }
1347
- try {
1348
- await client.emitActivity(emitPayload);
1349
- }
1350
- catch (err) {
1351
- // Some locally-buffered events carry a UUID that *looks* like an OrgX run_id
1352
- // but was only ever used as a local correlation/grouping key. If OrgX
1353
- // doesn't recognize it, retry by treating it as correlation_id so OrgX can
1354
- // create/attach a run deterministically.
1355
- const msg = toErrorMessage(err);
1356
- if (emitPayload.run_id &&
1357
- /^404\\b/.test(msg) &&
1358
- /\\brun\\b/i.test(msg) &&
1359
- /not found/i.test(msg)) {
1360
- const replayCorrelationId = `openclaw_run_${stableHash(emitPayload.run_id).slice(0, 24)}`;
1361
- await client.emitActivity({
1362
- ...emitPayload,
1363
- run_id: undefined,
1364
- correlation_id: replayCorrelationId,
1365
- metadata: {
1366
- ...(emitPayload.metadata ?? {}),
1367
- replay_run_id_as_correlation: true,
1368
- },
1369
- });
1370
- }
1371
- else {
1372
- throw err;
1373
- }
1374
- }
1375
- return;
1376
- }
1377
- if (event.type === "decision") {
1378
- const question = pickStringField(payload, "question");
1379
- if (!question) {
1380
- api.log?.warn?.("[orgx] Dropping invalid decision outbox event", {
1381
- eventId: event.id,
1382
- });
1383
- return;
1384
- }
1385
- const context = resolveReportingContext(payload);
1386
- if (!context.ok) {
1387
- throw new Error(context.error);
1388
- }
1389
- const runFields = normalizeRunFields({
1390
- runId: context.value.runId,
1391
- correlationId: context.value.correlationId,
1392
- });
1393
- // Payloads should include a stable idempotency_key when enqueued, but older
1394
- // events may not. Derive a deterministic fallback so outbox replay won't
1395
- // double-create the same remote decision.
1396
- const fallbackKey = stableHash(JSON.stringify({
1397
- t: "decision",
1398
- initiative_id: context.value.initiativeId,
1399
- run_id: context.value.runId ?? null,
1400
- correlation_id: context.value.correlationId ?? null,
1401
- question,
1402
- })).slice(0, 24);
1403
- const resolvedIdempotencyKey = pickStringField(payload, "idempotency_key") ??
1404
- pickStringField(payload, "idempotencyKey") ??
1405
- `openclaw:decision:${fallbackKey}`;
1406
- await client.applyChangeset({
1407
- initiative_id: context.value.initiativeId,
1408
- run_id: runFields.run_id,
1409
- correlation_id: runFields.correlation_id,
1410
- source_client: context.value.sourceClient,
1411
- idempotency_key: resolvedIdempotencyKey,
1412
- operations: [
1413
- {
1414
- op: "decision.create",
1415
- title: question,
1416
- summary: pickStringField(payload, "context") ?? undefined,
1417
- urgency: pickStringField(payload, "urgency") ?? "medium",
1418
- options: pickStringArrayField(payload, "options"),
1419
- blocking: typeof payload.blocking === "boolean" ? payload.blocking : true,
1420
- },
1421
- ],
1422
- });
1423
- return;
1424
- }
1425
- if (event.type === "changeset") {
1426
- const context = resolveReportingContext(payload);
1427
- if (!context.ok) {
1428
- throw new Error(context.error);
1429
- }
1430
- const runFields = normalizeRunFields({
1431
- runId: context.value.runId,
1432
- correlationId: context.value.correlationId,
1433
- });
1434
- const operations = Array.isArray(payload.operations)
1435
- ? payload.operations
1436
- : [];
1437
- if (operations.length === 0) {
1438
- api.log?.warn?.("[orgx] Dropping invalid changeset outbox event", {
1439
- eventId: event.id,
1440
- });
1441
- return;
1442
- }
1443
- // Status updates are the most common offline replay payload, and `updateEntity`
1444
- // is the most widely supported primitive across OrgX deployments. Prefer it
1445
- // when the changeset contains only simple status mutations.
1446
- const statusOps = operations
1447
- .map((op) => {
1448
- if (!op || typeof op !== "object")
1449
- return null;
1450
- const record = op;
1451
- const kind = typeof record.op === "string" ? record.op.trim() : "";
1452
- if (kind === "task.update") {
1453
- const taskId = typeof record.task_id === "string" ? record.task_id.trim() : "";
1454
- const statusRaw = typeof record.status === "string" ? record.status.trim() : "";
1455
- const normalized = statusRaw.toLowerCase().replace(/\s+/g, "_");
1456
- const status = normalized === "completed" || normalized === "complete" || normalized === "finished"
1457
- ? "done"
1458
- : normalized === "inprogress"
1459
- ? "in_progress"
1460
- : normalized;
1461
- if (!taskId || !status)
1462
- return null;
1463
- return { type: "task", id: taskId, status };
1464
- }
1465
- if (kind === "milestone.update") {
1466
- const milestoneId = typeof record.milestone_id === "string" ? record.milestone_id.trim() : "";
1467
- const statusRaw = typeof record.status === "string" ? record.status.trim() : "";
1468
- const normalized = statusRaw.toLowerCase().replace(/\s+/g, "_");
1469
- const status = normalized === "done" || normalized === "complete" || normalized === "finished"
1470
- ? "completed"
1471
- : normalized === "inprogress"
1472
- ? "in_progress"
1473
- : normalized === "todo" || normalized === "not_started" || normalized === "pending"
1474
- ? "planned"
1475
- : normalized === "blocked" || normalized === "stuck"
1476
- ? "at_risk"
1477
- : normalized;
1478
- if (!milestoneId || !status)
1479
- return null;
1480
- return { type: "milestone", id: milestoneId, status };
1481
- }
1482
- return null;
1483
- })
1484
- .filter((item) => Boolean(item));
1485
- if (statusOps.length === operations.length) {
1486
- for (const op of statusOps) {
1487
- await client.updateEntity(op.type, op.id, { status: op.status });
1488
- }
1489
- return;
1490
- }
1491
- // Payloads should include a stable idempotency_key when enqueued, but older
1492
- // events may not. Derive a deterministic fallback so outbox replay won't
1493
- // double-create the same remote change.
1494
- const fallbackKey = stableHash(JSON.stringify({
1495
- t: "changeset",
1496
- initiative_id: context.value.initiativeId,
1497
- run_id: context.value.runId ?? null,
1498
- correlation_id: context.value.correlationId ?? null,
1499
- operations,
1500
- })).slice(0, 24);
1501
- const resolvedIdempotencyKey = pickStringField(payload, "idempotency_key") ??
1502
- pickStringField(payload, "idempotencyKey") ??
1503
- `openclaw:changeset:${fallbackKey}`;
1504
- await client.applyChangeset({
1505
- initiative_id: context.value.initiativeId,
1506
- run_id: runFields.run_id,
1507
- correlation_id: runFields.correlation_id,
1508
- source_client: context.value.sourceClient,
1509
- idempotency_key: resolvedIdempotencyKey,
1510
- operations,
1511
- });
1512
- return;
1513
- }
1514
- if (event.type === "outcome") {
1515
- const context = resolveReportingContext(payload);
1516
- if (!context.ok) {
1517
- throw new Error(context.error);
1518
- }
1519
- const runFields = normalizeRunFields({
1520
- runId: context.value.runId,
1521
- correlationId: context.value.correlationId,
1522
- });
1523
- const executionId = pickStringField(payload, "execution_id") ??
1524
- pickStringField(payload, "executionId");
1525
- const executionType = pickStringField(payload, "execution_type") ??
1526
- pickStringField(payload, "executionType");
1527
- const agentId = pickStringField(payload, "agent_id") ??
1528
- pickStringField(payload, "agentId");
1529
- const success = typeof payload.success === "boolean"
1530
- ? payload.success
1531
- : null;
1532
- if (!executionId || !executionType || !agentId || success === null) {
1533
- api.log?.warn?.("[orgx] Dropping invalid outcome outbox event", {
1534
- eventId: event.id,
1535
- });
1536
- return;
1537
- }
1538
- const metaRaw = payload.metadata;
1539
- const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
1540
- ? metaRaw
1541
- : {};
1542
- await client.recordRunOutcome({
1543
- initiative_id: context.value.initiativeId,
1544
- run_id: runFields.run_id,
1545
- correlation_id: runFields.correlation_id,
1546
- source_client: context.value.sourceClient,
1547
- execution_id: executionId,
1548
- execution_type: executionType,
1549
- agent_id: agentId,
1550
- task_type: pickStringField(payload, "task_type") ??
1551
- pickStringField(payload, "taskType") ??
1552
- undefined,
1553
- domain: pickStringField(payload, "domain") ?? undefined,
1554
- started_at: pickStringField(payload, "started_at") ??
1555
- pickStringField(payload, "startedAt") ??
1556
- undefined,
1557
- completed_at: pickStringField(payload, "completed_at") ??
1558
- pickStringField(payload, "completedAt") ??
1559
- undefined,
1560
- inputs: payload.inputs && typeof payload.inputs === "object"
1561
- ? payload.inputs
1562
- : undefined,
1563
- outputs: payload.outputs && typeof payload.outputs === "object"
1564
- ? payload.outputs
1565
- : undefined,
1566
- steps: Array.isArray(payload.steps)
1567
- ? payload.steps
1568
- : undefined,
1569
- success,
1570
- quality_score: typeof payload.quality_score === "number"
1571
- ? payload.quality_score
1572
- : typeof payload.qualityScore === "number"
1573
- ? payload.qualityScore
1574
- : undefined,
1575
- duration_vs_estimate: typeof payload.duration_vs_estimate === "number"
1576
- ? payload.duration_vs_estimate
1577
- : typeof payload.durationVsEstimate === "number"
1578
- ? payload.durationVsEstimate
1579
- : undefined,
1580
- cost_vs_budget: typeof payload.cost_vs_budget === "number"
1581
- ? payload.cost_vs_budget
1582
- : typeof payload.costVsBudget === "number"
1583
- ? payload.costVsBudget
1584
- : undefined,
1585
- human_interventions: typeof payload.human_interventions === "number"
1586
- ? payload.human_interventions
1587
- : typeof payload.humanInterventions === "number"
1588
- ? payload.humanInterventions
1589
- : undefined,
1590
- user_satisfaction: typeof payload.user_satisfaction === "number"
1591
- ? payload.user_satisfaction
1592
- : typeof payload.userSatisfaction === "number"
1593
- ? payload.userSatisfaction
1594
- : undefined,
1595
- errors: Array.isArray(payload.errors)
1596
- ? payload.errors.filter((e) => typeof e === "string")
1597
- : undefined,
1598
- metadata: {
1599
- ...meta,
1600
- source: "orgx_openclaw_outbox_replay",
1601
- outbox_event_id: event.id,
1602
- },
1603
- });
1604
- return;
1605
- }
1606
- if (event.type === "retro") {
1607
- const context = resolveReportingContext(payload);
1608
- if (!context.ok) {
1609
- throw new Error(context.error);
1610
- }
1611
- const runFields = normalizeRunFields({
1612
- runId: context.value.runId,
1613
- correlationId: context.value.correlationId,
1614
- });
1615
- const retro = payload.retro && typeof payload.retro === "object" && !Array.isArray(payload.retro)
1616
- ? payload.retro
1617
- : null;
1618
- const summary = retro && typeof retro.summary === "string" ? retro.summary.trim() : "";
1619
- if (!retro || !summary) {
1620
- api.log?.warn?.("[orgx] Dropping invalid retro outbox event", {
1621
- eventId: event.id,
1622
- });
1623
- return;
1624
- }
1625
- const entityTypeRaw = pickStringField(payload, "entity_type") ??
1626
- pickStringField(payload, "entityType");
1627
- const parsedEntityType = parseRetroEntityType(entityTypeRaw) ?? null;
1628
- // Server-side enum parity can lag behind local clients. Only attach to the
1629
- // entity types that are guaranteed to exist today.
1630
- const entityType = parsedEntityType === "initiative" || parsedEntityType === "task"
1631
- ? parsedEntityType
1632
- : null;
1633
- const entityIdRaw = pickStringField(payload, "entity_id") ??
1634
- pickStringField(payload, "entityId") ??
1635
- null;
1636
- const entityId = isUuid(entityIdRaw ?? undefined) ? entityIdRaw : null;
1637
- await client.recordRunRetro({
1638
- initiative_id: context.value.initiativeId,
1639
- run_id: runFields.run_id,
1640
- correlation_id: runFields.correlation_id,
1641
- source_client: context.value.sourceClient,
1642
- entity_type: entityType && entityId ? entityType : undefined,
1643
- entity_id: entityType && entityId ? entityId : undefined,
1644
- title: pickStringField(payload, "title") ?? undefined,
1645
- idempotency_key: pickStringField(payload, "idempotency_key") ??
1646
- pickStringField(payload, "idempotencyKey") ??
1647
- undefined,
1648
- retro: retro,
1649
- markdown: pickStringField(payload, "markdown") ?? undefined,
1650
- });
1651
- return;
1652
- }
1653
- if (event.type === "artifact") {
1654
- // Artifacts are UI-level breadcrumbs and may not be supported by every
1655
- // OrgX deployment's `/api/entities` schema. Persist locally and drop from
1656
- // the outbox so progress reporting doesn't wedge on irreplayable items.
1657
- try {
1658
- if (event.activityItem) {
1659
- appendActivityItems([event.activityItem]);
1660
- }
1661
- }
1662
- catch {
1663
- // best effort
1664
- }
1665
- return;
1666
- }
1667
- }
1668
- async function flushOutboxQueues() {
1669
- const attemptAt = new Date().toISOString();
1670
- outboxReplayState = {
1671
- ...outboxReplayState,
1672
- status: "running",
1673
- lastReplayAttemptAt: attemptAt,
1674
- lastReplayError: null,
1675
- };
1676
- let hadReplayFailure = false;
1677
- let lastReplayError = null;
1678
- // Outbox files are keyed by *session id* (e.g. initiative/run correlation),
1679
- // not by event type.
1680
- const outboxSummary = await readOutboxSummary();
1681
- const queues = Object.entries(outboxSummary.pendingByQueue)
1682
- .filter(([, count]) => typeof count === "number" && count > 0)
1683
- .map(([queueId]) => queueId)
1684
- .sort();
1685
- for (const queue of queues) {
1686
- const pending = await readOutbox(queue);
1687
- if (pending.length === 0) {
1688
- continue;
1689
- }
1690
- const remaining = [];
1691
- for (const event of pending) {
1692
- try {
1693
- await replayOutboxEvent(event);
1694
- }
1695
- catch (err) {
1696
- hadReplayFailure = true;
1697
- lastReplayError = toErrorMessage(err);
1698
- remaining.push(event);
1699
- api.log?.warn?.("[orgx] Outbox replay failed", {
1700
- queue,
1701
- eventId: event.id,
1702
- error: lastReplayError,
1703
- });
1704
- }
1705
- }
1706
- await replaceOutbox(queue, remaining);
1707
- const replayedCount = pending.length - remaining.length;
1708
- if (replayedCount > 0) {
1709
- api.log?.info?.("[orgx] Replayed buffered outbox events", {
1710
- queue,
1711
- replayed: replayedCount,
1712
- remaining: remaining.length,
1713
- });
1714
- }
1715
- }
1716
- if (hadReplayFailure) {
1717
- outboxReplayState = {
1718
- ...outboxReplayState,
1719
- status: "error",
1720
- lastReplayFailureAt: new Date().toISOString(),
1721
- lastReplayError,
1722
- };
1723
- }
1724
- else {
1725
- outboxReplayState = {
1726
- ...outboxReplayState,
1727
- status: "success",
1728
- lastReplaySuccessAt: new Date().toISOString(),
1729
- lastReplayError: null,
1730
- };
1731
- }
1732
- }
814
+ const { flushOutboxQueues } = createOutboxReplayer({
815
+ client,
816
+ logger: api.log ?? {},
817
+ toErrorMessage,
818
+ stableHash,
819
+ resolveReportingContext,
820
+ pickStringField,
821
+ pickStringArrayField,
822
+ toReportingPhase,
823
+ parseRetroEntityType,
824
+ isUuid,
825
+ readOutboxReplayState: () => outboxReplayState,
826
+ writeOutboxReplayState: (next) => {
827
+ outboxReplayState = next;
828
+ },
829
+ });
1733
830
  async function doSync() {
1734
831
  if (syncInFlight) {
1735
832
  return syncInFlight;
@@ -1996,9 +1093,15 @@ export default function register(api) {
1996
1093
  nextAction: "retry",
1997
1094
  });
1998
1095
  }
1096
+ const pairingUserIdRaw = typeof polled.data.supabaseUserId === "string"
1097
+ ? polled.data.supabaseUserId
1098
+ : typeof polled.data.userId === "string"
1099
+ ? polled.data.userId
1100
+ : null;
1999
1101
  setRuntimeApiKey({
2000
1102
  apiKey: key,
2001
1103
  source: "browser_pairing",
1104
+ userId: resolveRuntimeUserId(key, [pairingUserIdRaw, config.userId]) || null,
2002
1105
  workspaceName: polled.data.workspaceName ?? null,
2003
1106
  keyPrefix: polled.data.keyPrefix ?? null,
2004
1107
  });
@@ -2091,1162 +1194,60 @@ export default function register(api) {
2091
1194
  keySource: "none",
2092
1195
  });
2093
1196
  }
2094
- api.registerService({
2095
- id: "orgx-sync",
2096
- start: async () => {
2097
- syncServiceRunning = true;
2098
- const watchdog = ensureGatewayWatchdog(api.log ?? {});
2099
- if (watchdog.started) {
2100
- api.log?.info?.("[orgx] Gateway watchdog started", {
2101
- pid: watchdog.pid,
2102
- });
2103
- }
2104
- api.log?.info?.("[orgx] Starting sync service", {
2105
- interval: config.syncIntervalMs,
2106
- });
2107
- await doSync();
2108
- scheduleNextSync();
1197
+ registerSyncService({
1198
+ api,
1199
+ syncIntervalMs: config.syncIntervalMs,
1200
+ ensureGatewayWatchdog: (logger) => ensureGatewayWatchdog(logger),
1201
+ doSync,
1202
+ scheduleNextSync,
1203
+ setSyncServiceRunning: (running) => {
1204
+ syncServiceRunning = running;
2109
1205
  },
2110
- stop: async () => {
2111
- syncServiceRunning = false;
1206
+ clearSyncTimer: () => {
2112
1207
  if (syncTimer)
2113
1208
  clearTimeout(syncTimer);
2114
1209
  syncTimer = null;
2115
1210
  },
2116
1211
  });
2117
1212
  async function autoAssignEntityForCreate(input) {
2118
- const warnings = [];
2119
- const byKey = new Map();
2120
- const addAgent = (agent) => {
2121
- const key = `${agent.id}:${agent.name}`.toLowerCase();
2122
- if (!byKey.has(key))
2123
- byKey.set(key, agent);
2124
- };
2125
- let liveAgents = [];
2126
- try {
2127
- const agentResp = await client.getLiveAgents({
2128
- initiative: input.initiativeId,
2129
- includeIdle: true,
2130
- });
2131
- liveAgents = (Array.isArray(agentResp.agents) ? agentResp.agents : [])
2132
- .map((raw) => {
2133
- if (!raw || typeof raw !== "object")
2134
- return null;
2135
- const record = raw;
2136
- const id = (typeof record.id === "string" && record.id.trim()) ||
2137
- (typeof record.agentId === "string" && record.agentId.trim()) ||
2138
- "";
2139
- const name = (typeof record.name === "string" && record.name.trim()) ||
2140
- (typeof record.agentName === "string" && record.agentName.trim()) ||
2141
- id;
2142
- if (!name)
2143
- return null;
2144
- return {
2145
- id: id || `name:${name}`,
2146
- name,
2147
- domain: (typeof record.domain === "string" && record.domain.trim()) ||
2148
- (typeof record.role === "string" && record.role.trim()) ||
2149
- null,
2150
- status: (typeof record.status === "string" && record.status.trim()) || null,
2151
- };
2152
- })
2153
- .filter((item) => item !== null);
2154
- }
2155
- catch (err) {
2156
- warnings.push(`live agents unavailable (${toErrorMessage(err)})`);
2157
- }
2158
- const orchestrator = liveAgents.find((agent) => /holt|orchestrator/i.test(agent.name) ||
2159
- /orchestrator/i.test(agent.domain ?? ""));
2160
- if (orchestrator)
2161
- addAgent(orchestrator);
2162
- let assignmentSource = "fallback";
2163
- try {
2164
- const preflight = await client.delegationPreflight({
2165
- intent: `${input.title}${input.summary ? `: ${input.summary}` : ""}`,
2166
- });
2167
- const recommendations = preflight.data?.recommended_split ?? [];
2168
- const recommendedDomains = [
2169
- ...new Set(recommendations
2170
- .map((entry) => String(entry.owner_domain ?? "").trim().toLowerCase())
2171
- .filter(Boolean)),
2172
- ];
2173
- for (const domain of recommendedDomains) {
2174
- const match = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
2175
- if (match)
2176
- addAgent(match);
2177
- }
2178
- if (recommendedDomains.length > 0) {
2179
- assignmentSource = "orchestrator";
2180
- }
2181
- }
2182
- catch (err) {
2183
- warnings.push(`delegation preflight failed (${toErrorMessage(err)})`);
2184
- }
2185
- if (byKey.size === 0) {
2186
- const haystack = `${input.title} ${input.summary ?? ""}`.toLowerCase();
2187
- const domainHints = [];
2188
- if (/market|campaign|thread|article|tweet|copy/.test(haystack)) {
2189
- domainHints.push("marketing");
2190
- }
2191
- else if (/design|ux|ui|a11y/.test(haystack)) {
2192
- domainHints.push("design");
2193
- }
2194
- else if (/ops|runbook|incident|reliability/.test(haystack)) {
2195
- domainHints.push("operations");
2196
- }
2197
- else if (/sales|deal|pipeline/.test(haystack)) {
2198
- domainHints.push("sales");
2199
- }
2200
- else {
2201
- domainHints.push("engineering", "product");
2202
- }
2203
- for (const domain of domainHints) {
2204
- const match = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
2205
- if (match)
2206
- addAgent(match);
2207
- }
2208
- }
2209
- if (byKey.size === 0 && liveAgents.length > 0) {
2210
- addAgent(liveAgents[0]);
2211
- warnings.push("fallback selected first available live agent");
2212
- }
2213
- const assignedAgents = Array.from(byKey.values());
2214
- let updatedEntity = null;
2215
- try {
2216
- updatedEntity = (await client.updateEntity(input.entityType, input.entityId, {
2217
- assigned_agent_ids: assignedAgents.map((agent) => agent.id),
2218
- assigned_agent_names: assignedAgents.map((agent) => agent.name),
2219
- assignment_source: assignmentSource,
2220
- }));
2221
- }
2222
- catch (err) {
2223
- warnings.push(`assignment update failed (${toErrorMessage(err)})`);
2224
- }
2225
- return {
2226
- assignmentSource,
2227
- assignedAgents,
2228
- warnings,
2229
- updatedEntity,
2230
- };
1213
+ return autoAssignEntityForCreateWithClient({
1214
+ client,
1215
+ toErrorMessage,
1216
+ ...input,
1217
+ });
2231
1218
  }
2232
1219
  // ---------------------------------------------------------------------------
2233
1220
  // 2. MCP Tools (Model Context Protocol compatible)
2234
1221
  // ---------------------------------------------------------------------------
2235
- const mcpToolRegistry = new Map();
2236
- const registerMcpTool = (tool, options) => {
2237
- mcpToolRegistry.set(tool.name, tool);
2238
- api.registerTool(tool, options);
2239
- };
2240
- // --- orgx_status ---
2241
- registerMcpTool({
2242
- name: "orgx_status",
2243
- description: "Get current OrgX org status: active initiatives, agent states, pending decisions, active tasks.",
2244
- parameters: {
2245
- type: "object",
2246
- properties: {},
2247
- additionalProperties: false,
2248
- },
2249
- async execute(_callId) {
2250
- if (!cachedSnapshot ||
2251
- Date.now() - lastSnapshotAt > config.syncIntervalMs) {
2252
- await doSync();
2253
- }
2254
- if (!cachedSnapshot) {
2255
- return text("❌ Failed to fetch OrgX status. Check API key and connectivity.");
2256
- }
2257
- return text(formatSnapshot(cachedSnapshot));
2258
- },
2259
- }, { optional: true });
2260
- // --- orgx_sync ---
2261
- registerMcpTool({
2262
- name: "orgx_sync",
2263
- description: "Push/pull memory sync with OrgX. Send local memory/daily log; receive initiatives, tasks, decisions, model routing policy.",
2264
- parameters: {
2265
- type: "object",
2266
- properties: {
2267
- memory: {
2268
- type: "string",
2269
- description: "Local memory snapshot to push",
2270
- },
2271
- dailyLog: {
2272
- type: "string",
2273
- description: "Today's session log to push",
2274
- },
2275
- },
2276
- },
2277
- async execute(_callId, params = {}) {
2278
- try {
2279
- const resp = await client.syncMemory({
2280
- memory: params.memory,
2281
- dailyLog: params.dailyLog,
2282
- });
2283
- return json("Sync complete:", resp);
2284
- }
2285
- catch (err) {
2286
- return text(`❌ Sync failed: ${err instanceof Error ? err.message : err}`);
2287
- }
2288
- },
2289
- }, { optional: true });
2290
- // --- orgx_delegation_preflight ---
2291
- registerMcpTool({
2292
- name: "orgx_delegation_preflight",
2293
- description: "Run delegation preflight to score scope quality, estimate ETA/cost, and suggest a split before autonomous execution.",
2294
- parameters: {
2295
- type: "object",
2296
- properties: {
2297
- intent: {
2298
- type: "string",
2299
- description: "Task intent in natural language",
2300
- },
2301
- acceptanceCriteria: {
2302
- type: "array",
2303
- items: { type: "string" },
2304
- description: "Optional acceptance criteria to reduce ambiguity",
2305
- },
2306
- constraints: {
2307
- type: "array",
2308
- items: { type: "string" },
2309
- description: "Optional constraints (deadline, stack, policy)",
2310
- },
2311
- domains: {
2312
- type: "array",
2313
- items: { type: "string" },
2314
- description: "Optional preferred owner domains",
2315
- },
2316
- },
2317
- required: ["intent"],
2318
- additionalProperties: false,
2319
- },
2320
- async execute(_callId, params = { intent: "" }) {
2321
- try {
2322
- const result = await client.delegationPreflight({
2323
- intent: params.intent,
2324
- acceptanceCriteria: Array.isArray(params.acceptanceCriteria)
2325
- ? params.acceptanceCriteria.filter((item) => typeof item === "string")
2326
- : undefined,
2327
- constraints: Array.isArray(params.constraints)
2328
- ? params.constraints.filter((item) => typeof item === "string")
2329
- : undefined,
2330
- domains: Array.isArray(params.domains)
2331
- ? params.domains.filter((item) => typeof item === "string")
2332
- : undefined,
2333
- });
2334
- return json("Delegation preflight:", result.data ?? result);
2335
- }
2336
- catch (err) {
2337
- return text(`❌ Delegation preflight failed: ${err instanceof Error ? err.message : err}`);
2338
- }
2339
- },
2340
- }, { optional: true });
2341
- // --- orgx_run_action ---
2342
- registerMcpTool({
2343
- name: "orgx_run_action",
2344
- description: "Apply a control action to a run: pause, resume, cancel, or rollback (rollback requires checkpointId).",
2345
- parameters: {
2346
- type: "object",
2347
- properties: {
2348
- runId: {
2349
- type: "string",
2350
- description: "Run UUID",
2351
- },
2352
- action: {
2353
- type: "string",
2354
- enum: ["pause", "resume", "cancel", "rollback"],
2355
- description: "Control action",
2356
- },
2357
- checkpointId: {
2358
- type: "string",
2359
- description: "Checkpoint UUID (required for rollback)",
2360
- },
2361
- reason: {
2362
- type: "string",
2363
- description: "Optional reason for audit trail",
2364
- },
2365
- },
2366
- required: ["runId", "action"],
2367
- additionalProperties: false,
2368
- },
2369
- async execute(_callId, params = { runId: "", action: "pause" }) {
2370
- try {
2371
- if (params.action === "rollback" && !params.checkpointId) {
2372
- return text("❌ rollback requires checkpointId");
2373
- }
2374
- const result = await client.runAction(params.runId, params.action, {
2375
- checkpointId: params.checkpointId,
2376
- reason: params.reason,
2377
- });
2378
- return json("Run action applied:", result.data ?? result);
2379
- }
2380
- catch (err) {
2381
- return text(`❌ Run action failed: ${err instanceof Error ? err.message : err}`);
2382
- }
2383
- },
2384
- }, { optional: true });
2385
- // --- orgx_checkpoints_list ---
2386
- registerMcpTool({
2387
- name: "orgx_checkpoints_list",
2388
- description: "List checkpoints for a run.",
2389
- parameters: {
2390
- type: "object",
2391
- properties: {
2392
- runId: {
2393
- type: "string",
2394
- description: "Run UUID",
2395
- },
2396
- },
2397
- required: ["runId"],
2398
- additionalProperties: false,
2399
- },
2400
- async execute(_callId, params = { runId: "" }) {
2401
- try {
2402
- const result = await client.listRunCheckpoints(params.runId);
2403
- return json("Run checkpoints:", result.data ?? result);
2404
- }
2405
- catch (err) {
2406
- return text(`❌ Failed to list checkpoints: ${err instanceof Error ? err.message : err}`);
2407
- }
2408
- },
2409
- }, { optional: true });
2410
- // --- orgx_checkpoint_restore ---
2411
- registerMcpTool({
2412
- name: "orgx_checkpoint_restore",
2413
- description: "Restore a run to a specific checkpoint.",
2414
- parameters: {
2415
- type: "object",
2416
- properties: {
2417
- runId: {
2418
- type: "string",
2419
- description: "Run UUID",
2420
- },
2421
- checkpointId: {
2422
- type: "string",
2423
- description: "Checkpoint UUID",
2424
- },
2425
- reason: {
2426
- type: "string",
2427
- description: "Optional restoration reason",
2428
- },
2429
- },
2430
- required: ["runId", "checkpointId"],
2431
- additionalProperties: false,
2432
- },
2433
- async execute(_callId, params = {
2434
- runId: "",
2435
- checkpointId: "",
2436
- }) {
2437
- try {
2438
- const result = await client.restoreRunCheckpoint(params.runId, {
2439
- checkpointId: params.checkpointId,
2440
- reason: params.reason,
2441
- });
2442
- return json("Checkpoint restored:", result.data ?? result);
2443
- }
2444
- catch (err) {
2445
- return text(`❌ Checkpoint restore failed: ${err instanceof Error ? err.message : err}`);
2446
- }
2447
- },
2448
- }, { optional: true });
2449
- // --- orgx_spawn_check ---
2450
- registerMcpTool({
2451
- name: "orgx_spawn_check",
2452
- description: "Check quality gate + get model routing before spawning a sub-agent. Returns allowed/denied, model tier, and check details.",
2453
- parameters: {
2454
- type: "object",
2455
- properties: {
2456
- domain: {
2457
- type: "string",
2458
- description: "Agent domain (engineering, product, marketing, data, operations, design)",
2459
- },
2460
- taskId: {
2461
- type: "string",
2462
- description: "OrgX task ID to check",
2463
- },
2464
- },
2465
- required: ["domain"],
2466
- },
2467
- async execute(_callId, params = { domain: "" }) {
2468
- try {
2469
- const result = await client.checkSpawnGuard(params.domain, params.taskId);
2470
- const status = result.allowed ? "✅ Allowed" : "🚫 Blocked";
2471
- return json(`${status} — model tier: ${result.modelTier}`, result);
2472
- }
2473
- catch (err) {
2474
- return text(`❌ Spawn check failed: ${err instanceof Error ? err.message : err}`);
2475
- }
2476
- },
2477
- }, { optional: true });
2478
- // --- orgx_quality_score ---
2479
- registerMcpTool({
2480
- name: "orgx_quality_score",
2481
- description: "Record a quality score (1-5) for completed agent work. Used to gate future spawns and track performance.",
2482
- parameters: {
2483
- type: "object",
2484
- properties: {
2485
- taskId: {
2486
- type: "string",
2487
- description: "ID of the completed task",
2488
- },
2489
- domain: {
2490
- type: "string",
2491
- description: "Agent domain that did the work",
2492
- },
2493
- score: {
2494
- type: "number",
2495
- description: "Quality 1 (poor) to 5 (excellent)",
2496
- minimum: 1,
2497
- maximum: 5,
2498
- },
2499
- notes: {
2500
- type: "string",
2501
- description: "Notes on the assessment",
2502
- },
2503
- },
2504
- required: ["taskId", "domain", "score"],
2505
- },
2506
- async execute(_callId, params = { taskId: "", domain: "", score: 0 }) {
2507
- try {
2508
- await client.recordQuality(params);
2509
- return text(`✅ Quality score recorded: ${params.score}/5 for task ${params.taskId} (${params.domain})`);
2510
- }
2511
- catch (err) {
2512
- return text(`❌ Quality recording failed: ${err instanceof Error ? err.message : err}`);
2513
- }
2514
- },
2515
- }, { optional: true });
2516
- // --- orgx_create_entity ---
2517
- registerMcpTool({
2518
- name: "orgx_create_entity",
2519
- description: "Create an OrgX entity (initiative, workstream, task, decision, milestone, etc.).",
2520
- parameters: {
2521
- type: "object",
2522
- properties: {
2523
- type: {
2524
- type: "string",
2525
- description: "Entity type: initiative, workstream, task, decision, milestone, artifact, agent, blocker",
2526
- },
2527
- title: {
2528
- type: "string",
2529
- description: "Entity title",
2530
- },
2531
- summary: {
2532
- type: "string",
2533
- description: "Description",
2534
- },
2535
- status: {
2536
- type: "string",
2537
- description: "Initial status (active, not_started, todo)",
2538
- },
2539
- initiative_id: {
2540
- type: "string",
2541
- description: "Parent initiative ID (for workstreams/tasks)",
2542
- },
2543
- workstream_id: {
2544
- type: "string",
2545
- description: "Parent workstream ID (for tasks)",
2546
- },
2547
- command_center_id: {
2548
- type: "string",
2549
- description: "Command center ID (for initiatives)",
2550
- },
2551
- },
2552
- required: ["type", "title"],
2553
- },
2554
- async execute(_callId, params = {}) {
2555
- try {
2556
- const { type, ...data } = params;
2557
- let entity = await client.createEntity(type, data);
2558
- let assignmentSummary = null;
2559
- const entityType = String(type ?? "");
2560
- if (entityType === "initiative" || entityType === "workstream") {
2561
- const entityRecord = entity;
2562
- const assignment = await autoAssignEntityForCreate({
2563
- entityType,
2564
- entityId: String(entityRecord.id ?? ""),
2565
- initiativeId: entityType === "initiative"
2566
- ? String(entityRecord.id ?? "")
2567
- : (typeof data.initiative_id === "string"
2568
- ? data.initiative_id
2569
- : null),
2570
- title: (typeof entityRecord.title === "string" && entityRecord.title) ||
2571
- (typeof entityRecord.name === "string" && entityRecord.name) ||
2572
- (typeof data.title === "string" && data.title) ||
2573
- "Untitled",
2574
- summary: (typeof entityRecord.summary === "string" && entityRecord.summary) ||
2575
- (typeof data.summary === "string" && data.summary) ||
2576
- null,
2577
- });
2578
- if (assignment.updatedEntity) {
2579
- entity = assignment.updatedEntity;
2580
- }
2581
- assignmentSummary = {
2582
- assignment_source: assignment.assignmentSource,
2583
- assigned_agents: assignment.assignedAgents,
2584
- warnings: assignment.warnings,
2585
- };
2586
- }
2587
- return json(`✅ Created ${type}: ${entity.title ?? entity.id}`, {
2588
- entity,
2589
- ...(assignmentSummary
2590
- ? {
2591
- auto_assignment: assignmentSummary,
2592
- }
2593
- : {}),
2594
- });
2595
- }
2596
- catch (err) {
2597
- return text(`❌ Creation failed: ${err instanceof Error ? err.message : err}`);
2598
- }
2599
- },
2600
- }, { optional: true });
2601
- // --- orgx_update_entity ---
2602
- registerMcpTool({
2603
- name: "orgx_update_entity",
2604
- description: "Update an existing OrgX entity by type and ID.",
2605
- parameters: {
2606
- type: "object",
2607
- properties: {
2608
- type: {
2609
- type: "string",
2610
- description: "Entity type",
2611
- },
2612
- id: {
2613
- type: "string",
2614
- description: "Entity UUID",
2615
- },
2616
- status: {
2617
- type: "string",
2618
- description: "New status",
2619
- },
2620
- title: {
2621
- type: "string",
2622
- description: "New title",
2623
- },
2624
- summary: {
2625
- type: "string",
2626
- description: "New summary",
2627
- },
2628
- },
2629
- required: ["type", "id"],
2630
- },
2631
- async execute(_callId, params = {}) {
2632
- try {
2633
- const { type, id, ...updates } = params;
2634
- const entity = await client.updateEntity(type, id, updates);
2635
- return json(`✅ Updated ${type} ${id.slice(0, 8)}`, entity);
2636
- }
2637
- catch (err) {
2638
- return text(`❌ Update failed: ${err instanceof Error ? err.message : err}`);
2639
- }
2640
- },
2641
- }, { optional: true });
2642
- // --- orgx_list_entities ---
2643
- registerMcpTool({
2644
- name: "orgx_list_entities",
2645
- description: "List OrgX entities of a given type with optional status filter.",
2646
- parameters: {
2647
- type: "object",
2648
- properties: {
2649
- type: {
2650
- type: "string",
2651
- description: "Entity type: initiative, workstream, task, decision, agent",
2652
- },
2653
- status: {
2654
- type: "string",
2655
- description: "Filter by status",
2656
- },
2657
- limit: {
2658
- type: "number",
2659
- description: "Max results (default 20)",
2660
- default: 20,
2661
- },
2662
- },
2663
- required: ["type"],
2664
- },
2665
- async execute(_callId, params = { type: "" }) {
2666
- try {
2667
- const { type, ...filters } = params;
2668
- const resp = await client.listEntities(type, filters);
2669
- const entities = resp.data ?? resp;
2670
- const count = Array.isArray(entities) ? entities.length : "?";
2671
- return json(`${count} ${type}(s):`, entities);
2672
- }
2673
- catch (err) {
2674
- return text(`❌ List failed: ${err instanceof Error ? err.message : err}`);
2675
- }
2676
- },
2677
- }, { optional: true });
2678
- function withProvenanceMetadata(metadata) {
2679
- const input = metadata ?? {};
2680
- const out = { ...input };
2681
- if (out.orgx_plugin_version === undefined) {
2682
- out.orgx_plugin_version = (config.pluginVersion ?? "").trim() || null;
2683
- }
2684
- try {
2685
- const state = readSkillPackState();
2686
- const overrides = state.overrides;
2687
- if (out.skill_pack_name === undefined) {
2688
- out.skill_pack_name = overrides?.name ?? state.pack?.name ?? null;
2689
- }
2690
- if (out.skill_pack_version === undefined) {
2691
- out.skill_pack_version = overrides?.version ?? state.pack?.version ?? null;
2692
- }
2693
- if (out.skill_pack_checksum === undefined) {
2694
- out.skill_pack_checksum = overrides?.checksum ?? state.pack?.checksum ?? null;
2695
- }
2696
- if (out.skill_pack_source === undefined) {
2697
- out.skill_pack_source = overrides?.source ?? null;
2698
- }
2699
- if (out.skill_pack_etag === undefined) {
2700
- out.skill_pack_etag = state.etag ?? null;
2701
- }
2702
- }
2703
- catch {
2704
- // best effort
2705
- }
2706
- if (out.orgx_provenance === undefined) {
2707
- out.orgx_provenance = {
2708
- plugin_version: out.orgx_plugin_version ?? null,
2709
- skill_pack: {
2710
- name: out.skill_pack_name ?? null,
2711
- version: out.skill_pack_version ?? null,
2712
- checksum: out.skill_pack_checksum ?? null,
2713
- source: out.skill_pack_source ?? null,
2714
- etag: out.skill_pack_etag ?? null,
2715
- },
2716
- };
2717
- }
2718
- return out;
2719
- }
2720
- async function emitActivityWithFallback(source, payload) {
2721
- if (!payload.message || payload.message.trim().length === 0) {
2722
- return text("❌ message is required");
2723
- }
2724
- const context = resolveReportingContext(payload);
2725
- if (!context.ok) {
2726
- return text(`❌ ${context.error}`);
2727
- }
2728
- const now = new Date().toISOString();
2729
- const id = `progress:${randomUUID().slice(0, 8)}`;
2730
- const normalizedPayload = {
2731
- initiative_id: context.value.initiativeId,
2732
- run_id: context.value.runId,
2733
- correlation_id: context.value.correlationId,
2734
- source_client: context.value.sourceClient,
2735
- message: payload.message,
2736
- phase: payload.phase ?? "execution",
2737
- progress_pct: payload.progress_pct,
2738
- level: payload.level ?? "info",
2739
- next_step: payload.next_step,
2740
- metadata: withProvenanceMetadata({
2741
- ...(payload.metadata ?? {}),
2742
- source,
2743
- }),
2744
- };
2745
- const activityItem = {
2746
- id,
2747
- type: "delegation",
2748
- title: payload.message,
2749
- description: payload.next_step ?? null,
2750
- agentId: null,
2751
- agentName: null,
2752
- runId: context.value.runId ?? null,
2753
- initiativeId: context.value.initiativeId,
2754
- timestamp: now,
2755
- phase: normalizedPayload.phase,
2756
- summary: payload.next_step ? `Next: ${payload.next_step}` : payload.message,
2757
- metadata: normalizedPayload.metadata,
2758
- };
2759
- try {
2760
- const result = await client.emitActivity(normalizedPayload);
2761
- return text(`Activity emitted: ${payload.message} [${normalizedPayload.phase}${payload.progress_pct != null ? ` ${payload.progress_pct}%` : ""}] (run ${result.run_id.slice(0, 8)}...)`);
2762
- }
2763
- catch {
2764
- await appendToOutbox("progress", {
2765
- id,
2766
- type: "progress",
2767
- timestamp: now,
2768
- payload: normalizedPayload,
2769
- activityItem,
2770
- });
2771
- return text(`Activity saved locally: ${payload.message} [${normalizedPayload.phase}${payload.progress_pct != null ? ` ${payload.progress_pct}%` : ""}] (will sync when connected)`);
2772
- }
2773
- }
2774
- async function applyChangesetWithFallback(source, payload) {
2775
- const context = resolveReportingContext(payload);
2776
- if (!context.ok) {
2777
- return text(`❌ ${context.error}`);
2778
- }
2779
- if (!Array.isArray(payload.operations) || payload.operations.length === 0) {
2780
- return text("❌ operations must contain at least one change");
2781
- }
2782
- const idempotencyKey = pickNonEmptyString(payload.idempotency_key) ??
2783
- `${source}:${Date.now()}:${randomUUID().slice(0, 8)}`;
2784
- const requestPayload = {
2785
- initiative_id: context.value.initiativeId,
2786
- run_id: context.value.runId,
2787
- correlation_id: context.value.correlationId,
2788
- source_client: context.value.sourceClient,
2789
- idempotency_key: idempotencyKey,
2790
- operations: payload.operations,
2791
- };
2792
- const now = new Date().toISOString();
2793
- const id = `changeset:${randomUUID().slice(0, 8)}`;
2794
- const activityItem = {
2795
- id,
2796
- type: "milestone_completed",
2797
- title: "Changeset queued",
2798
- description: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
2799
- agentId: null,
2800
- agentName: null,
2801
- runId: context.value.runId ?? null,
2802
- initiativeId: context.value.initiativeId,
2803
- timestamp: now,
2804
- phase: "review",
2805
- summary: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
2806
- metadata: withProvenanceMetadata({
2807
- source,
2808
- idempotency_key: idempotencyKey,
2809
- }),
2810
- };
2811
- try {
2812
- const result = await client.applyChangeset(requestPayload);
2813
- return text(`Changeset ${result.replayed ? "replayed" : "applied"}: ${result.applied_count} op${result.applied_count === 1 ? "" : "s"} (run ${result.run_id.slice(0, 8)}...)`);
2814
- }
2815
- catch {
2816
- await appendToOutbox("decisions", {
2817
- id,
2818
- type: "changeset",
2819
- timestamp: now,
2820
- payload: requestPayload,
2821
- activityItem,
2822
- });
2823
- return text(`Changeset saved locally (${payload.operations.length} op${payload.operations.length === 1 ? "" : "s"}) (will sync when connected)`);
2824
- }
2825
- }
2826
- // --- orgx_emit_activity ---
2827
- registerMcpTool({
2828
- name: "orgx_emit_activity",
2829
- description: "Emit append-only OrgX activity telemetry (launch reporting contract primary write tool).",
2830
- parameters: {
2831
- type: "object",
2832
- properties: {
2833
- initiative_id: {
2834
- type: "string",
2835
- description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
2836
- },
2837
- message: {
2838
- type: "string",
2839
- description: "Human-readable activity update",
2840
- },
2841
- run_id: {
2842
- type: "string",
2843
- description: "Optional run UUID",
2844
- },
2845
- correlation_id: {
2846
- type: "string",
2847
- description: "Required when run_id is omitted",
2848
- },
2849
- source_client: {
2850
- type: "string",
2851
- enum: ["openclaw", "codex", "claude-code", "api"],
2852
- description: "Required when run_id is omitted",
2853
- },
2854
- phase: {
2855
- type: "string",
2856
- enum: ["intent", "execution", "blocked", "review", "handoff", "completed"],
2857
- description: "Reporting phase",
2858
- },
2859
- progress_pct: {
2860
- type: "number",
2861
- minimum: 0,
2862
- maximum: 100,
2863
- description: "Optional progress percentage",
2864
- },
2865
- level: {
2866
- type: "string",
2867
- enum: ["info", "warn", "error"],
2868
- description: "Optional level (default info)",
2869
- },
2870
- next_step: {
2871
- type: "string",
2872
- description: "Optional next step",
2873
- },
2874
- metadata: {
2875
- type: "object",
2876
- description: "Optional structured metadata",
2877
- },
2878
- },
2879
- required: ["message"],
2880
- additionalProperties: false,
2881
- },
2882
- async execute(_callId, params = { message: "" }) {
2883
- return emitActivityWithFallback("orgx_emit_activity", params);
2884
- },
2885
- }, { optional: true });
2886
- // --- orgx_apply_changeset ---
2887
- registerMcpTool({
2888
- name: "orgx_apply_changeset",
2889
- description: "Apply an idempotent transactional OrgX changeset (launch reporting contract primary mutation tool).",
2890
- parameters: {
2891
- type: "object",
2892
- properties: {
2893
- initiative_id: {
2894
- type: "string",
2895
- description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
2896
- },
2897
- idempotency_key: {
2898
- type: "string",
2899
- description: "Idempotency key (<=120 chars). Auto-generated if omitted.",
2900
- },
2901
- operations: {
2902
- type: "array",
2903
- minItems: 1,
2904
- maxItems: 25,
2905
- description: "Changeset operations (task.create, task.update, milestone.update, decision.create)",
2906
- items: { type: "object" },
2907
- },
2908
- run_id: {
2909
- type: "string",
2910
- description: "Optional run UUID",
2911
- },
2912
- correlation_id: {
2913
- type: "string",
2914
- description: "Required when run_id is omitted",
2915
- },
2916
- source_client: {
2917
- type: "string",
2918
- enum: ["openclaw", "codex", "claude-code", "api"],
2919
- description: "Required when run_id is omitted",
2920
- },
2921
- },
2922
- required: ["operations"],
2923
- additionalProperties: false,
2924
- },
2925
- async execute(_callId, params = { operations: [] }) {
2926
- return applyChangesetWithFallback("orgx_apply_changeset", params);
2927
- },
2928
- }, { optional: true });
2929
- // --- orgx_report_progress (alias -> orgx_emit_activity) ---
2930
- registerMcpTool({
2931
- name: "orgx_report_progress",
2932
- description: "Alias for orgx_emit_activity. Report progress at key milestones so the team can track your work.",
2933
- parameters: {
2934
- type: "object",
2935
- properties: {
2936
- initiative_id: {
2937
- type: "string",
2938
- description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
2939
- },
2940
- run_id: {
2941
- type: "string",
2942
- description: "Optional run UUID",
2943
- },
2944
- correlation_id: {
2945
- type: "string",
2946
- description: "Required when run_id is omitted",
2947
- },
2948
- source_client: {
2949
- type: "string",
2950
- enum: ["openclaw", "codex", "claude-code", "api"],
2951
- },
2952
- summary: {
2953
- type: "string",
2954
- description: "What was accomplished (1-2 sentences, human-readable)",
2955
- },
2956
- phase: {
2957
- type: "string",
2958
- enum: ["researching", "implementing", "testing", "reviewing", "blocked"],
2959
- description: "Current work phase",
2960
- },
2961
- progress_pct: {
2962
- type: "number",
2963
- description: "Progress percentage (0-100)",
2964
- minimum: 0,
2965
- maximum: 100,
2966
- },
2967
- next_step: {
2968
- type: "string",
2969
- description: "What you plan to do next",
2970
- },
2971
- },
2972
- required: ["summary", "phase"],
2973
- additionalProperties: false,
2974
- },
2975
- async execute(_callId, params = { summary: "", phase: "implementing" }) {
2976
- return emitActivityWithFallback("orgx_report_progress", {
2977
- initiative_id: params.initiative_id,
2978
- run_id: params.run_id,
2979
- correlation_id: params.correlation_id,
2980
- source_client: params.source_client,
2981
- message: params.summary,
2982
- phase: toReportingPhase(params.phase, params.progress_pct),
2983
- progress_pct: params.progress_pct,
2984
- next_step: params.next_step,
2985
- level: params.phase === "blocked" ? "warn" : "info",
2986
- metadata: {
2987
- legacy_phase: params.phase,
2988
- },
2989
- });
2990
- },
2991
- }, { optional: true });
2992
- // --- orgx_request_decision (alias -> orgx_apply_changeset decision.create) ---
2993
- registerMcpTool({
2994
- name: "orgx_request_decision",
2995
- description: "Alias for orgx_apply_changeset with decision.create. Request a human decision before proceeding.",
2996
- parameters: {
2997
- type: "object",
2998
- properties: {
2999
- initiative_id: {
3000
- type: "string",
3001
- description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
3002
- },
3003
- run_id: {
3004
- type: "string",
3005
- description: "Optional run UUID",
3006
- },
3007
- correlation_id: {
3008
- type: "string",
3009
- description: "Required when run_id is omitted",
3010
- },
3011
- source_client: {
3012
- type: "string",
3013
- enum: ["openclaw", "codex", "claude-code", "api"],
3014
- },
3015
- question: {
3016
- type: "string",
3017
- description: "The decision question (e.g., 'Deploy to production?')",
3018
- },
3019
- context: {
3020
- type: "string",
3021
- description: "Background context to help the human decide",
3022
- },
3023
- options: {
3024
- type: "array",
3025
- items: { type: "string" },
3026
- description: "Available choices (e.g., ['Yes, deploy now', 'Wait for more testing', 'Cancel'])",
3027
- },
3028
- urgency: {
3029
- type: "string",
3030
- enum: ["low", "medium", "high", "urgent"],
3031
- description: "How urgent this decision is",
3032
- },
3033
- blocking: {
3034
- type: "boolean",
3035
- description: "Whether work should pause until this is decided (default: true)",
3036
- },
3037
- },
3038
- required: ["question", "urgency"],
3039
- additionalProperties: false,
3040
- },
3041
- async execute(_callId, params = { question: "", urgency: "medium" }) {
3042
- const requestId = `decision:${randomUUID().slice(0, 8)}`;
3043
- const changesetResult = await applyChangesetWithFallback("orgx_request_decision", {
3044
- initiative_id: params.initiative_id,
3045
- run_id: params.run_id,
3046
- correlation_id: params.correlation_id,
3047
- source_client: params.source_client,
3048
- idempotency_key: `decision:${requestId}`,
3049
- operations: [
3050
- {
3051
- op: "decision.create",
3052
- title: params.question,
3053
- summary: params.context,
3054
- urgency: params.urgency,
3055
- options: params.options,
3056
- blocking: params.blocking ?? true,
3057
- },
3058
- ],
3059
- });
3060
- await emitActivityWithFallback("orgx_request_decision", {
3061
- initiative_id: params.initiative_id,
3062
- run_id: params.run_id,
3063
- correlation_id: params.correlation_id,
3064
- source_client: params.source_client,
3065
- message: `Decision requested: ${params.question}`,
3066
- phase: "review",
3067
- level: "info",
3068
- metadata: {
3069
- urgency: params.urgency,
3070
- blocking: params.blocking ?? true,
3071
- options: params.options ?? [],
3072
- },
3073
- });
3074
- return changesetResult;
3075
- },
3076
- }, { optional: true });
3077
- // --- orgx_register_artifact ---
3078
- registerMcpTool({
3079
- name: "orgx_register_artifact",
3080
- description: "Register a work output (PR, document, config change, report, etc.) with OrgX. Makes it visible in the dashboard.",
3081
- parameters: {
3082
- type: "object",
3083
- properties: {
3084
- initiative_id: {
3085
- type: "string",
3086
- description: "Optional initiative UUID to attach this artifact to",
3087
- },
3088
- name: {
3089
- type: "string",
3090
- description: "Human-readable artifact name (e.g., 'PR #107: Fix build size')",
3091
- },
3092
- artifact_type: {
3093
- type: "string",
3094
- enum: ["pr", "commit", "document", "config", "report", "design", "other"],
3095
- description: "Type of artifact",
3096
- },
3097
- description: {
3098
- type: "string",
3099
- description: "What this artifact is and why it matters",
3100
- },
3101
- url: {
3102
- type: "string",
3103
- description: "Link to the artifact (PR URL, file path, etc.)",
3104
- },
3105
- },
3106
- required: ["name", "artifact_type"],
3107
- additionalProperties: false,
3108
- },
3109
- async execute(_callId, params = { name: "", artifact_type: "other" }) {
3110
- const now = new Date().toISOString();
3111
- const id = `artifact:${randomUUID().slice(0, 8)}`;
3112
- const initiativeId = isUuid(params.initiative_id)
3113
- ? params.initiative_id
3114
- : inferReportingInitiativeId(params) ?? null;
3115
- const activityItem = {
3116
- id,
3117
- type: "artifact_created",
3118
- title: params.name,
3119
- description: params.description ?? null,
3120
- agentId: null,
3121
- agentName: null,
3122
- runId: null,
3123
- initiativeId,
3124
- timestamp: now,
3125
- summary: params.url ?? null,
3126
- metadata: withProvenanceMetadata({
3127
- source: "orgx_register_artifact",
3128
- artifact_type: params.artifact_type,
3129
- url: params.url,
3130
- }),
3131
- };
3132
- try {
3133
- const entity = await client.createEntity("artifact", {
3134
- title: params.name,
3135
- artifact_type: params.artifact_type,
3136
- summary: params.description,
3137
- initiative_id: initiativeId ?? undefined,
3138
- artifact_url: params.url,
3139
- status: "active",
3140
- });
3141
- return json(`Artifact registered: ${params.name} [${params.artifact_type}]`, entity);
3142
- }
3143
- catch {
3144
- await appendToOutbox("artifacts", {
3145
- id,
3146
- type: "artifact",
3147
- timestamp: now,
3148
- payload: {
3149
- ...params,
3150
- initiative_id: initiativeId,
3151
- },
3152
- activityItem,
3153
- });
3154
- return text(`Artifact saved locally: ${params.name} [${params.artifact_type}] (will sync when connected)`);
3155
- }
3156
- },
3157
- }, { optional: true });
1222
+ const mcpToolRegistry = registerCoreTools({
1223
+ registerTool: api.registerTool.bind(api),
1224
+ client,
1225
+ config,
1226
+ getCachedSnapshot: () => cachedSnapshot,
1227
+ getLastSnapshotAt: () => lastSnapshotAt,
1228
+ doSync,
1229
+ text,
1230
+ json,
1231
+ formatSnapshot,
1232
+ autoAssignEntityForCreate,
1233
+ toReportingPhase,
1234
+ inferReportingInitiativeId,
1235
+ isUuid,
1236
+ pickNonEmptyString,
1237
+ resolveReportingContext,
1238
+ readSkillPackState,
1239
+ randomUUID,
1240
+ });
3158
1241
  // ---------------------------------------------------------------------------
3159
1242
  // 3. CLI Command
3160
1243
  // ---------------------------------------------------------------------------
3161
- api.registerCli(({ program }) => {
3162
- const cmd = program.command("orgx").description("OrgX integration commands");
3163
- cmd
3164
- .command("status")
3165
- .description("Show current OrgX org status")
3166
- .action(async () => {
3167
- try {
3168
- const snap = await client.getOrgSnapshot();
3169
- console.log(formatSnapshot(snap));
3170
- }
3171
- catch (err) {
3172
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
3173
- process.exit(1);
3174
- }
3175
- });
3176
- cmd
3177
- .command("sync")
3178
- .description("Trigger manual memory sync")
3179
- .option("--memory <text>", "Memory to push")
3180
- .option("--daily-log <text>", "Daily log to push")
3181
- .action(async (opts = {}) => {
3182
- try {
3183
- const resp = await client.syncMemory({
3184
- memory: opts.memory,
3185
- dailyLog: opts.dailyLog,
3186
- });
3187
- console.log("Sync complete:");
3188
- console.log(` Initiatives: ${resp.initiatives?.length ?? 0}`);
3189
- console.log(` Active tasks: ${resp.activeTasks?.length ?? 0}`);
3190
- console.log(` Pending decisions: ${resp.pendingDecisions?.length ?? 0}`);
3191
- }
3192
- catch (err) {
3193
- console.error(`Sync failed: ${err instanceof Error ? err.message : err}`);
3194
- process.exit(1);
3195
- }
3196
- });
3197
- cmd
3198
- .command("doctor")
3199
- .description("Run plugin diagnostics and connectivity checks")
3200
- .option("--json", "Print the report as JSON")
3201
- .option("--no-remote", "Skip remote OrgX API reachability probe")
3202
- .action(async (opts = {}) => {
3203
- try {
3204
- const report = await buildHealthReport({
3205
- probeRemote: opts.remote !== false,
3206
- });
3207
- if (opts.json) {
3208
- console.log(JSON.stringify(report, null, 2));
3209
- if (!report.ok)
3210
- process.exit(1);
3211
- return;
3212
- }
3213
- console.log("OrgX Doctor");
3214
- console.log(` Status: ${report.status.toUpperCase()}`);
3215
- console.log(` Plugin: v${report.plugin.version}`);
3216
- console.log(` Base URL: ${report.plugin.baseUrl}`);
3217
- console.log(` API Key Source: ${apiKeySourceLabel(report.auth.keySource)}`);
3218
- console.log(` Outbox Pending: ${report.outbox.pendingTotal}`);
3219
- console.log("");
3220
- console.log("Checks:");
3221
- for (const check of report.checks) {
3222
- const prefix = check.status === "pass"
3223
- ? "[PASS]"
3224
- : check.status === "warn"
3225
- ? "[WARN]"
3226
- : "[FAIL]";
3227
- console.log(` ${prefix} ${check.message}`);
3228
- }
3229
- if (report.remote.enabled) {
3230
- if (report.remote.reachable === true) {
3231
- console.log(` Remote probe latency: ${report.remote.latencyMs ?? "?"}ms`);
3232
- }
3233
- else if (report.remote.reachable === false) {
3234
- console.log(` Remote probe error: ${report.remote.error ?? "Unknown error"}`);
3235
- }
3236
- else {
3237
- console.log(" Remote probe: skipped");
3238
- }
3239
- }
3240
- if (!report.ok) {
3241
- process.exit(1);
3242
- }
3243
- }
3244
- catch (err) {
3245
- console.error(`Doctor failed: ${err instanceof Error ? err.message : err}`);
3246
- process.exit(1);
3247
- }
3248
- });
3249
- }, { commands: ["orgx"] });
1244
+ registerOrgxCli({
1245
+ registerCli: api.registerCli.bind(api),
1246
+ client,
1247
+ formatSnapshot,
1248
+ buildHealthReport,
1249
+ apiKeySourceLabel,
1250
+ });
3250
1251
  // ---------------------------------------------------------------------------
3251
1252
  // 4. HTTP Handler — Dashboard + API proxy
3252
1253
  // ---------------------------------------------------------------------------