@useorgx/openclaw-plugin 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -2
- package/dashboard/dist/assets/index-BjqNjHpY.css +1 -0
- package/dashboard/dist/assets/index-DCLkU4AM.js +57 -0
- package/dashboard/dist/assets/orgx-logo-QSE5QWy4.png +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg +10 -0
- package/dashboard/dist/brand/control-tower.png +0 -0
- package/dashboard/dist/brand/design-codex.png +0 -0
- package/dashboard/dist/brand/engineering-autopilot.png +0 -0
- package/dashboard/dist/brand/launch-captain.png +0 -0
- package/dashboard/dist/brand/openai-mark.svg +10 -0
- package/dashboard/dist/brand/openclaw-mark.svg +11 -0
- package/dashboard/dist/brand/orgx-logo.png +0 -0
- package/dashboard/dist/brand/pipeline-intelligence.png +0 -0
- package/dashboard/dist/brand/product-orchestrator.png +0 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/adapters/outbox.d.ts +8 -0
- package/dist/adapters/outbox.d.ts.map +1 -0
- package/dist/adapters/outbox.js +6 -0
- package/dist/adapters/outbox.js.map +1 -0
- package/dist/agent-context-store.d.ts +24 -0
- package/dist/agent-context-store.d.ts.map +1 -0
- package/dist/agent-context-store.js +110 -0
- package/dist/agent-context-store.js.map +1 -0
- package/dist/agent-run-store.d.ts +31 -0
- package/dist/agent-run-store.d.ts.map +1 -0
- package/dist/agent-run-store.js +158 -0
- package/dist/agent-run-store.js.map +1 -0
- package/dist/api.d.ts +4 -131
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -285
- package/dist/api.js.map +1 -1
- package/dist/auth-store.d.ts +20 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +154 -0
- package/dist/auth-store.js.map +1 -0
- package/dist/byok-store.d.ts +11 -0
- package/dist/byok-store.d.ts.map +1 -0
- package/dist/byok-store.js +94 -0
- package/dist/byok-store.js.map +1 -0
- package/dist/contracts/client.d.ts +154 -0
- package/dist/contracts/client.d.ts.map +1 -0
- package/dist/contracts/client.js +422 -0
- package/dist/contracts/client.js.map +1 -0
- package/dist/contracts/types.d.ts +430 -0
- package/dist/contracts/types.d.ts.map +1 -0
- package/dist/contracts/types.js +8 -0
- package/dist/contracts/types.js.map +1 -0
- package/dist/dashboard-api.d.ts +2 -7
- package/dist/dashboard-api.d.ts.map +1 -1
- package/dist/dashboard-api.js +2 -4
- package/dist/dashboard-api.js.map +1 -1
- package/dist/http-handler.d.ts +37 -3
- package/dist/http-handler.d.ts.map +1 -1
- package/dist/http-handler.js +3880 -80
- package/dist/http-handler.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1615 -41
- package/dist/index.js.map +1 -1
- package/dist/local-openclaw.d.ts +87 -0
- package/dist/local-openclaw.d.ts.map +1 -0
- package/dist/local-openclaw.js +816 -0
- package/dist/local-openclaw.js.map +1 -0
- package/dist/openclaw.plugin.json +76 -0
- package/dist/outbox.d.ts +27 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +174 -0
- package/dist/outbox.js.map +1 -0
- package/dist/snapshot-store.d.ts +10 -0
- package/dist/snapshot-store.d.ts.map +1 -0
- package/dist/snapshot-store.js +64 -0
- package/dist/snapshot-store.js.map +1 -0
- package/dist/types.d.ts +5 -320
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -4
- package/dist/types.js.map +1 -1
- package/openclaw.plugin.json +4 -3
- package/package.json +14 -2
- package/skills/orgx/SKILL.md +180 -0
- package/dashboard/dist/assets/index-C_w24A8p.css +0 -1
- package/dashboard/dist/assets/index-DfkN5JSS.js +0 -48
package/dist/index.js
CHANGED
|
@@ -15,48 +15,176 @@ import { createHttpHandler } from "./http-handler.js";
|
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { clearPersistedApiKey, loadAuthStore, resolveInstallationId, saveAuthStore, } from "./auth-store.js";
|
|
21
|
+
import { clearPersistedSnapshot, readPersistedSnapshot, writePersistedSnapshot, } from "./snapshot-store.js";
|
|
22
|
+
import { appendToOutbox, readOutbox, readOutboxSummary, replaceOutbox, } from "./outbox.js";
|
|
18
23
|
export { OrgXClient } from "./api.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
catch {
|
|
34
|
-
// .env.local not found
|
|
24
|
+
const DEFAULT_BASE_URL = "https://www.useorgx.com";
|
|
25
|
+
const DEFAULT_DOCS_URL = "https://orgx.mintlify.site/guides/openclaw-plugin-setup";
|
|
26
|
+
function isUserScopedApiKey(apiKey) {
|
|
27
|
+
return apiKey.trim().toLowerCase().startsWith("oxk_");
|
|
28
|
+
}
|
|
29
|
+
function resolveRuntimeUserId(apiKey, candidates) {
|
|
30
|
+
if (isUserScopedApiKey(apiKey)) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
if (typeof candidate === "string") {
|
|
35
|
+
const trimmed = candidate.trim();
|
|
36
|
+
if (trimmed.length > 0)
|
|
37
|
+
return trimmed;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
function normalizeHost(value) {
|
|
43
|
+
return value.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
44
|
+
}
|
|
45
|
+
function isLoopbackHostname(hostname) {
|
|
46
|
+
const normalized = normalizeHost(hostname);
|
|
47
|
+
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
|
|
48
|
+
}
|
|
49
|
+
function normalizeBaseUrl(raw) {
|
|
50
|
+
const candidate = raw?.trim() ?? "";
|
|
51
|
+
if (!candidate) {
|
|
52
|
+
return DEFAULT_BASE_URL;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const parsed = new URL(candidate);
|
|
56
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
57
|
+
return DEFAULT_BASE_URL;
|
|
46
58
|
}
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
// Do not allow credential-bearing URLs.
|
|
60
|
+
if (parsed.username || parsed.password) {
|
|
61
|
+
return DEFAULT_BASE_URL;
|
|
62
|
+
}
|
|
63
|
+
// Plain HTTP is only allowed for local loopback development.
|
|
64
|
+
if (parsed.protocol === "http:" && !isLoopbackHostname(parsed.hostname)) {
|
|
65
|
+
return DEFAULT_BASE_URL;
|
|
66
|
+
}
|
|
67
|
+
parsed.search = "";
|
|
68
|
+
parsed.hash = "";
|
|
69
|
+
const normalizedPath = parsed.pathname.replace(/\/+$/, "");
|
|
70
|
+
parsed.pathname = normalizedPath;
|
|
71
|
+
const normalized = parsed.toString().replace(/\/+$/, "");
|
|
72
|
+
return normalized.length > 0 ? normalized : DEFAULT_BASE_URL;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return DEFAULT_BASE_URL;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function readLegacyEnvValue(keyPattern) {
|
|
79
|
+
try {
|
|
80
|
+
const envPath = join(homedir(), "Code", "orgx", "orgx", ".env.local");
|
|
81
|
+
const envContent = readFileSync(envPath, "utf-8");
|
|
82
|
+
const match = envContent.match(keyPattern);
|
|
83
|
+
return match?.[1]?.trim() ?? "";
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function readOpenClawOrgxConfig() {
|
|
90
|
+
try {
|
|
91
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
92
|
+
const raw = readFileSync(configPath, "utf8");
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
const plugins = parsed.plugins && typeof parsed.plugins === "object"
|
|
95
|
+
? parsed.plugins
|
|
96
|
+
: {};
|
|
97
|
+
const entries = plugins.entries && typeof plugins.entries === "object"
|
|
98
|
+
? plugins.entries
|
|
99
|
+
: {};
|
|
100
|
+
const orgx = entries.orgx && typeof entries.orgx === "object"
|
|
101
|
+
? entries.orgx
|
|
102
|
+
: {};
|
|
103
|
+
const config = orgx.config && typeof orgx.config === "object"
|
|
104
|
+
? orgx.config
|
|
105
|
+
: {};
|
|
106
|
+
const apiKey = typeof config.apiKey === "string" ? config.apiKey.trim() : "";
|
|
107
|
+
const userId = typeof config.userId === "string" ? config.userId.trim() : "";
|
|
108
|
+
const baseUrl = typeof config.baseUrl === "string" ? config.baseUrl.trim() : "";
|
|
109
|
+
return { apiKey, userId, baseUrl };
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { apiKey: "", userId: "", baseUrl: "" };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function resolveApiKey(pluginConf, persistedApiKey) {
|
|
116
|
+
if (pluginConf.apiKey && pluginConf.apiKey.trim().length > 0) {
|
|
117
|
+
return { value: pluginConf.apiKey.trim(), source: "config" };
|
|
118
|
+
}
|
|
119
|
+
if (process.env.ORGX_API_KEY && process.env.ORGX_API_KEY.trim().length > 0) {
|
|
120
|
+
return { value: process.env.ORGX_API_KEY.trim(), source: "environment" };
|
|
121
|
+
}
|
|
122
|
+
if (persistedApiKey && persistedApiKey.trim().length > 0) {
|
|
123
|
+
return { value: persistedApiKey.trim(), source: "persisted" };
|
|
124
|
+
}
|
|
125
|
+
const openclaw = readOpenClawOrgxConfig();
|
|
126
|
+
if (openclaw.apiKey) {
|
|
127
|
+
return { value: openclaw.apiKey, source: "openclaw-config-file" };
|
|
128
|
+
}
|
|
129
|
+
// For local dev convenience we read `ORGX_API_KEY` from `~/Code/orgx/orgx/.env.local`.
|
|
130
|
+
// Do not auto-consume `ORGX_SERVICE_KEY` because service keys often require `X-Orgx-User-Id`,
|
|
131
|
+
// and the dashboard/client flows are intended to run on user-scoped keys (`oxk_...`).
|
|
132
|
+
const legacy = readLegacyEnvValue(/^ORGX_API_KEY=["']?([^"'\n]+)["']?$/m);
|
|
133
|
+
if (legacy) {
|
|
134
|
+
return { value: legacy, source: "legacy-dev" };
|
|
135
|
+
}
|
|
136
|
+
return { value: "", source: "none" };
|
|
137
|
+
}
|
|
138
|
+
function resolvePluginVersion() {
|
|
139
|
+
try {
|
|
140
|
+
const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
141
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
142
|
+
return parsed.version && parsed.version.trim().length > 0
|
|
143
|
+
? parsed.version
|
|
144
|
+
: "dev";
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return "dev";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function resolveDocsUrl(baseUrl) {
|
|
151
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
152
|
+
try {
|
|
153
|
+
const parsed = new URL(normalized);
|
|
154
|
+
if (isLoopbackHostname(parsed.hostname)) {
|
|
155
|
+
return `${normalized}/docs/mintlify/guides/openclaw-plugin-setup`;
|
|
49
156
|
}
|
|
50
157
|
}
|
|
158
|
+
catch {
|
|
159
|
+
return DEFAULT_DOCS_URL;
|
|
160
|
+
}
|
|
161
|
+
return DEFAULT_DOCS_URL;
|
|
162
|
+
}
|
|
163
|
+
function resolveConfig(api, input) {
|
|
164
|
+
const pluginConf = api.config?.plugins?.entries?.orgx?.config ?? {};
|
|
165
|
+
const openclaw = readOpenClawOrgxConfig();
|
|
166
|
+
const apiKeyResolution = resolveApiKey(pluginConf, input.persistedApiKey);
|
|
167
|
+
const apiKey = apiKeyResolution.value;
|
|
168
|
+
// Resolve user ID for X-Orgx-User-Id header
|
|
169
|
+
const userId = resolveRuntimeUserId(apiKey, [
|
|
170
|
+
pluginConf.userId,
|
|
171
|
+
process.env.ORGX_USER_ID,
|
|
172
|
+
input.persistedUserId,
|
|
173
|
+
openclaw.userId,
|
|
174
|
+
readLegacyEnvValue(/^ORGX_USER_ID=["']?([^"'\n]+)["']?$/m),
|
|
175
|
+
]);
|
|
176
|
+
const baseUrl = normalizeBaseUrl(pluginConf.baseUrl || process.env.ORGX_BASE_URL || openclaw.baseUrl || DEFAULT_BASE_URL);
|
|
51
177
|
return {
|
|
52
178
|
apiKey,
|
|
53
179
|
userId,
|
|
54
|
-
baseUrl
|
|
55
|
-
process.env.ORGX_BASE_URL ||
|
|
56
|
-
"https://www.useorgx.com",
|
|
180
|
+
baseUrl,
|
|
57
181
|
syncIntervalMs: pluginConf.syncIntervalMs ?? 300_000,
|
|
58
182
|
enabled: pluginConf.enabled ?? true,
|
|
59
183
|
dashboardEnabled: pluginConf.dashboardEnabled ?? true,
|
|
184
|
+
installationId: input.installationId,
|
|
185
|
+
pluginVersion: resolvePluginVersion(),
|
|
186
|
+
docsUrl: resolveDocsUrl(baseUrl),
|
|
187
|
+
apiKeySource: apiKeyResolution.source,
|
|
60
188
|
};
|
|
61
189
|
}
|
|
62
190
|
function text(s) {
|
|
@@ -102,11 +230,78 @@ function formatSnapshot(snap) {
|
|
|
102
230
|
lines.push(`_Last synced: ${snap.syncedAt}_`);
|
|
103
231
|
return lines.join("\n");
|
|
104
232
|
}
|
|
233
|
+
function apiKeySourceLabel(source) {
|
|
234
|
+
switch (source) {
|
|
235
|
+
case "config":
|
|
236
|
+
return "Plugin Config";
|
|
237
|
+
case "environment":
|
|
238
|
+
return "Environment";
|
|
239
|
+
case "persisted":
|
|
240
|
+
return "Persisted Store";
|
|
241
|
+
case "openclaw-config-file":
|
|
242
|
+
return "OpenClaw Config";
|
|
243
|
+
case "legacy-dev":
|
|
244
|
+
return "Legacy Dev Env";
|
|
245
|
+
default:
|
|
246
|
+
return "Not configured";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function pickNonEmptyString(...values) {
|
|
250
|
+
for (const value of values) {
|
|
251
|
+
if (typeof value !== "string")
|
|
252
|
+
continue;
|
|
253
|
+
const trimmed = value.trim();
|
|
254
|
+
if (trimmed.length > 0) {
|
|
255
|
+
return trimmed;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
function isUuid(value) {
|
|
261
|
+
if (!value)
|
|
262
|
+
return false;
|
|
263
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
264
|
+
}
|
|
265
|
+
function toReportingPhase(phase, progressPct) {
|
|
266
|
+
if (progressPct === 100)
|
|
267
|
+
return "completed";
|
|
268
|
+
switch (phase) {
|
|
269
|
+
case "researching":
|
|
270
|
+
return "intent";
|
|
271
|
+
case "implementing":
|
|
272
|
+
case "testing":
|
|
273
|
+
return "execution";
|
|
274
|
+
case "reviewing":
|
|
275
|
+
return "review";
|
|
276
|
+
case "blocked":
|
|
277
|
+
return "blocked";
|
|
278
|
+
default:
|
|
279
|
+
return "execution";
|
|
280
|
+
}
|
|
281
|
+
}
|
|
105
282
|
// =============================================================================
|
|
106
283
|
// PLUGIN STATE
|
|
107
284
|
// =============================================================================
|
|
108
285
|
let cachedSnapshot = null;
|
|
109
286
|
let lastSnapshotAt = 0;
|
|
287
|
+
function updateCachedSnapshot(snapshot) {
|
|
288
|
+
cachedSnapshot = snapshot;
|
|
289
|
+
lastSnapshotAt = Date.now();
|
|
290
|
+
try {
|
|
291
|
+
writePersistedSnapshot(snapshot);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// best effort
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function hydrateCachedSnapshot() {
|
|
298
|
+
const persisted = readPersistedSnapshot();
|
|
299
|
+
if (!persisted?.snapshot)
|
|
300
|
+
return;
|
|
301
|
+
cachedSnapshot = persisted.snapshot;
|
|
302
|
+
const ts = Date.parse(persisted.updatedAt);
|
|
303
|
+
lastSnapshotAt = Number.isFinite(ts) ? ts : 0;
|
|
304
|
+
}
|
|
110
305
|
// =============================================================================
|
|
111
306
|
// PLUGIN ENTRY — DEFAULT EXPORT
|
|
112
307
|
// =============================================================================
|
|
@@ -117,7 +312,13 @@ let lastSnapshotAt = 0;
|
|
|
117
312
|
* @param api - The Clawdbot plugin API
|
|
118
313
|
*/
|
|
119
314
|
export default function register(api) {
|
|
120
|
-
const
|
|
315
|
+
const persistedAuth = loadAuthStore();
|
|
316
|
+
const installationId = resolveInstallationId();
|
|
317
|
+
const config = resolveConfig(api, {
|
|
318
|
+
installationId,
|
|
319
|
+
persistedApiKey: persistedAuth?.apiKey ?? null,
|
|
320
|
+
persistedUserId: persistedAuth?.userId ?? null,
|
|
321
|
+
});
|
|
121
322
|
if (!config.enabled) {
|
|
122
323
|
api.log?.info?.("[orgx] Plugin disabled");
|
|
123
324
|
return;
|
|
@@ -125,36 +326,884 @@ export default function register(api) {
|
|
|
125
326
|
if (!config.apiKey) {
|
|
126
327
|
api.log?.warn?.("[orgx] No API key. Set plugins.entries.orgx.config.apiKey, ORGX_API_KEY env, or ~/Code/orgx/orgx/.env.local");
|
|
127
328
|
}
|
|
329
|
+
hydrateCachedSnapshot();
|
|
128
330
|
const client = new OrgXClient(config.apiKey, config.baseUrl, config.userId);
|
|
331
|
+
let onboardingState = {
|
|
332
|
+
status: config.apiKey ? "connected" : "idle",
|
|
333
|
+
hasApiKey: Boolean(config.apiKey),
|
|
334
|
+
connectionVerified: Boolean(config.apiKey),
|
|
335
|
+
workspaceName: persistedAuth?.workspaceName ?? null,
|
|
336
|
+
lastError: null,
|
|
337
|
+
nextAction: config.apiKey ? "open_dashboard" : "connect",
|
|
338
|
+
docsUrl: config.docsUrl,
|
|
339
|
+
keySource: config.apiKeySource,
|
|
340
|
+
installationId: config.installationId,
|
|
341
|
+
connectUrl: null,
|
|
342
|
+
pairingId: null,
|
|
343
|
+
expiresAt: null,
|
|
344
|
+
pollIntervalMs: null,
|
|
345
|
+
};
|
|
346
|
+
let activePairing = null;
|
|
347
|
+
const baseApiUrl = config.baseUrl.replace(/\/+$/, "");
|
|
348
|
+
const defaultReportingCorrelationId = pickNonEmptyString(process.env.ORGX_CORRELATION_ID) ??
|
|
349
|
+
`openclaw-${config.installationId}`;
|
|
350
|
+
function resolveReportingContext(input) {
|
|
351
|
+
const initiativeId = pickNonEmptyString(input.initiative_id, process.env.ORGX_INITIATIVE_ID);
|
|
352
|
+
if (!initiativeId || !isUuid(initiativeId)) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
error: "initiative_id is required (set ORGX_INITIATIVE_ID or pass initiative_id).",
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const sourceCandidate = pickNonEmptyString(input.source_client, process.env.ORGX_SOURCE_CLIENT, "openclaw");
|
|
359
|
+
const sourceClient = sourceCandidate === "codex" ||
|
|
360
|
+
sourceCandidate === "claude-code" ||
|
|
361
|
+
sourceCandidate === "api" ||
|
|
362
|
+
sourceCandidate === "openclaw"
|
|
363
|
+
? sourceCandidate
|
|
364
|
+
: "openclaw";
|
|
365
|
+
const runIdCandidate = pickNonEmptyString(input.run_id, process.env.ORGX_RUN_ID);
|
|
366
|
+
const runId = isUuid(runIdCandidate) ? runIdCandidate : undefined;
|
|
367
|
+
const correlationId = runId
|
|
368
|
+
? undefined
|
|
369
|
+
: pickNonEmptyString(input.correlation_id, defaultReportingCorrelationId, `openclaw-${Date.now()}`);
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
value: {
|
|
373
|
+
initiativeId,
|
|
374
|
+
runId,
|
|
375
|
+
correlationId,
|
|
376
|
+
sourceClient,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function updateOnboardingState(updates) {
|
|
381
|
+
onboardingState = {
|
|
382
|
+
...onboardingState,
|
|
383
|
+
...updates,
|
|
384
|
+
};
|
|
385
|
+
return onboardingState;
|
|
386
|
+
}
|
|
387
|
+
function toErrorMessage(err) {
|
|
388
|
+
if (err instanceof Error)
|
|
389
|
+
return err.message;
|
|
390
|
+
return typeof err === "string" ? err : "Unexpected error";
|
|
391
|
+
}
|
|
392
|
+
function clearPairingState() {
|
|
393
|
+
activePairing = null;
|
|
394
|
+
updateOnboardingState({
|
|
395
|
+
connectUrl: null,
|
|
396
|
+
pairingId: null,
|
|
397
|
+
expiresAt: null,
|
|
398
|
+
pollIntervalMs: null,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function isAuthRequiredError(result) {
|
|
402
|
+
if (result.status !== 401) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
return /auth|unauthorized|token/i.test(result.error);
|
|
406
|
+
}
|
|
407
|
+
function buildManualKeyConnectUrl() {
|
|
408
|
+
try {
|
|
409
|
+
return new URL("/settings", baseApiUrl).toString();
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return "https://www.useorgx.com/settings";
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function fetchOrgxJson(method, path, body) {
|
|
416
|
+
try {
|
|
417
|
+
const response = await fetch(`${baseApiUrl}${path}`, {
|
|
418
|
+
method,
|
|
419
|
+
headers: {
|
|
420
|
+
"Content-Type": "application/json",
|
|
421
|
+
},
|
|
422
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
423
|
+
});
|
|
424
|
+
const payload = (await response.json().catch(() => null));
|
|
425
|
+
if (!response.ok) {
|
|
426
|
+
const rawError = payload?.error ?? payload?.message;
|
|
427
|
+
let errorMessage;
|
|
428
|
+
if (typeof rawError === "string") {
|
|
429
|
+
errorMessage = rawError;
|
|
430
|
+
}
|
|
431
|
+
else if (rawError &&
|
|
432
|
+
typeof rawError === "object" &&
|
|
433
|
+
"message" in rawError &&
|
|
434
|
+
typeof rawError.message === "string") {
|
|
435
|
+
errorMessage = rawError.message;
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
errorMessage = `OrgX request failed (${response.status})`;
|
|
439
|
+
}
|
|
440
|
+
return { ok: false, status: response.status, error: errorMessage };
|
|
441
|
+
}
|
|
442
|
+
if (payload?.data !== undefined) {
|
|
443
|
+
return { ok: true, data: payload.data };
|
|
444
|
+
}
|
|
445
|
+
return { ok: true, data: payload };
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
return { ok: false, status: 0, error: toErrorMessage(err) };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function setRuntimeApiKey(input) {
|
|
452
|
+
const nextApiKey = input.apiKey.trim();
|
|
453
|
+
config.apiKey = nextApiKey;
|
|
454
|
+
config.apiKeySource = "persisted";
|
|
455
|
+
config.userId = resolveRuntimeUserId(nextApiKey, [input.userId, config.userId]);
|
|
456
|
+
client.setCredentials({
|
|
457
|
+
apiKey: config.apiKey,
|
|
458
|
+
userId: config.userId,
|
|
459
|
+
baseUrl: config.baseUrl,
|
|
460
|
+
});
|
|
461
|
+
saveAuthStore({
|
|
462
|
+
installationId: config.installationId,
|
|
463
|
+
apiKey: nextApiKey,
|
|
464
|
+
userId: config.userId || null,
|
|
465
|
+
workspaceName: input.workspaceName ?? null,
|
|
466
|
+
keyPrefix: input.keyPrefix ?? null,
|
|
467
|
+
source: input.source,
|
|
468
|
+
});
|
|
469
|
+
updateOnboardingState({
|
|
470
|
+
hasApiKey: true,
|
|
471
|
+
keySource: "persisted",
|
|
472
|
+
installationId: config.installationId,
|
|
473
|
+
workspaceName: input.workspaceName ?? onboardingState.workspaceName,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
129
476
|
// ---------------------------------------------------------------------------
|
|
130
477
|
// 1. Background Sync Service
|
|
131
478
|
// ---------------------------------------------------------------------------
|
|
132
479
|
let syncTimer = null;
|
|
480
|
+
let syncInFlight = null;
|
|
481
|
+
let syncServiceRunning = false;
|
|
482
|
+
const outboxQueues = ["progress", "decisions", "artifacts"];
|
|
483
|
+
let outboxReplayState = {
|
|
484
|
+
status: "idle",
|
|
485
|
+
lastReplayAttemptAt: null,
|
|
486
|
+
lastReplaySuccessAt: null,
|
|
487
|
+
lastReplayFailureAt: null,
|
|
488
|
+
lastReplayError: null,
|
|
489
|
+
};
|
|
490
|
+
async function buildHealthReport(input = {}) {
|
|
491
|
+
const generatedAt = new Date().toISOString();
|
|
492
|
+
const probeRemote = input.probeRemote === true;
|
|
493
|
+
const outbox = await readOutboxSummary();
|
|
494
|
+
const checks = [];
|
|
495
|
+
const hasApiKey = Boolean(config.apiKey);
|
|
496
|
+
if (hasApiKey) {
|
|
497
|
+
checks.push({
|
|
498
|
+
id: "api_key",
|
|
499
|
+
status: "pass",
|
|
500
|
+
message: `API key detected (${apiKeySourceLabel(config.apiKeySource)}).`,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
checks.push({
|
|
505
|
+
id: "api_key",
|
|
506
|
+
status: "fail",
|
|
507
|
+
message: "API key missing. Connect OrgX in onboarding or set ORGX_API_KEY.",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (syncServiceRunning) {
|
|
511
|
+
checks.push({
|
|
512
|
+
id: "sync_service",
|
|
513
|
+
status: "pass",
|
|
514
|
+
message: "Background sync service is running.",
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
checks.push({
|
|
519
|
+
id: "sync_service",
|
|
520
|
+
status: "warn",
|
|
521
|
+
message: "Background sync service is not running.",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (outbox.pendingTotal > 0) {
|
|
525
|
+
checks.push({
|
|
526
|
+
id: "outbox",
|
|
527
|
+
status: "warn",
|
|
528
|
+
message: `Outbox has ${outbox.pendingTotal} queued event(s).`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
checks.push({
|
|
533
|
+
id: "outbox",
|
|
534
|
+
status: "pass",
|
|
535
|
+
message: "Outbox is empty.",
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
let remoteReachable = null;
|
|
539
|
+
let remoteLatencyMs = null;
|
|
540
|
+
let remoteError = null;
|
|
541
|
+
if (probeRemote) {
|
|
542
|
+
if (!hasApiKey) {
|
|
543
|
+
checks.push({
|
|
544
|
+
id: "remote_probe",
|
|
545
|
+
status: "warn",
|
|
546
|
+
message: "Skipped remote probe because API key is missing.",
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
const startedAt = Date.now();
|
|
551
|
+
try {
|
|
552
|
+
await client.getOrgSnapshot();
|
|
553
|
+
remoteReachable = true;
|
|
554
|
+
remoteLatencyMs = Date.now() - startedAt;
|
|
555
|
+
checks.push({
|
|
556
|
+
id: "remote_probe",
|
|
557
|
+
status: "pass",
|
|
558
|
+
message: `OrgX API reachable (${remoteLatencyMs}ms).`,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
remoteReachable = false;
|
|
563
|
+
remoteLatencyMs = Date.now() - startedAt;
|
|
564
|
+
remoteError = toErrorMessage(err);
|
|
565
|
+
checks.push({
|
|
566
|
+
id: "remote_probe",
|
|
567
|
+
status: "fail",
|
|
568
|
+
message: `OrgX API probe failed: ${remoteError}`,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (onboardingState.status === "error") {
|
|
574
|
+
checks.push({
|
|
575
|
+
id: "onboarding_state",
|
|
576
|
+
status: "warn",
|
|
577
|
+
message: onboardingState.lastError
|
|
578
|
+
? `Onboarding reports an error: ${onboardingState.lastError}`
|
|
579
|
+
: "Onboarding reports an error state.",
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const hasFail = checks.some((check) => check.status === "fail");
|
|
583
|
+
const hasWarn = checks.some((check) => check.status === "warn");
|
|
584
|
+
const status = hasFail
|
|
585
|
+
? "error"
|
|
586
|
+
: hasWarn
|
|
587
|
+
? "degraded"
|
|
588
|
+
: "ok";
|
|
589
|
+
return {
|
|
590
|
+
ok: status !== "error",
|
|
591
|
+
status,
|
|
592
|
+
generatedAt,
|
|
593
|
+
checks,
|
|
594
|
+
plugin: {
|
|
595
|
+
version: config.pluginVersion,
|
|
596
|
+
installationId: config.installationId,
|
|
597
|
+
enabled: config.enabled,
|
|
598
|
+
dashboardEnabled: config.dashboardEnabled,
|
|
599
|
+
baseUrl: config.baseUrl,
|
|
600
|
+
},
|
|
601
|
+
auth: {
|
|
602
|
+
hasApiKey,
|
|
603
|
+
keySource: config.apiKeySource,
|
|
604
|
+
userIdConfigured: Boolean(config.userId && config.userId.trim().length > 0),
|
|
605
|
+
onboardingStatus: onboardingState.status,
|
|
606
|
+
},
|
|
607
|
+
sync: {
|
|
608
|
+
serviceRunning: syncServiceRunning,
|
|
609
|
+
inFlight: syncInFlight !== null,
|
|
610
|
+
lastSnapshotAt: lastSnapshotAt > 0 ? new Date(lastSnapshotAt).toISOString() : null,
|
|
611
|
+
},
|
|
612
|
+
outbox: {
|
|
613
|
+
pendingTotal: outbox.pendingTotal,
|
|
614
|
+
pendingByQueue: outbox.pendingByQueue,
|
|
615
|
+
oldestEventAt: outbox.oldestEventAt,
|
|
616
|
+
newestEventAt: outbox.newestEventAt,
|
|
617
|
+
replayStatus: outboxReplayState.status,
|
|
618
|
+
lastReplayAttemptAt: outboxReplayState.lastReplayAttemptAt,
|
|
619
|
+
lastReplaySuccessAt: outboxReplayState.lastReplaySuccessAt,
|
|
620
|
+
lastReplayFailureAt: outboxReplayState.lastReplayFailureAt,
|
|
621
|
+
lastReplayError: outboxReplayState.lastReplayError,
|
|
622
|
+
},
|
|
623
|
+
remote: {
|
|
624
|
+
enabled: probeRemote,
|
|
625
|
+
reachable: remoteReachable,
|
|
626
|
+
latencyMs: remoteLatencyMs,
|
|
627
|
+
error: remoteError,
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function pickStringField(payload, key) {
|
|
632
|
+
const value = payload[key];
|
|
633
|
+
return typeof value === "string" && value.trim().length > 0
|
|
634
|
+
? value.trim()
|
|
635
|
+
: null;
|
|
636
|
+
}
|
|
637
|
+
function pickStringArrayField(payload, key) {
|
|
638
|
+
const value = payload[key];
|
|
639
|
+
if (!Array.isArray(value))
|
|
640
|
+
return undefined;
|
|
641
|
+
const strings = value
|
|
642
|
+
.filter((item) => typeof item === "string")
|
|
643
|
+
.map((item) => item.trim())
|
|
644
|
+
.filter(Boolean);
|
|
645
|
+
return strings.length > 0 ? strings : undefined;
|
|
646
|
+
}
|
|
647
|
+
async function replayOutboxEvent(event) {
|
|
648
|
+
const payload = event.payload ?? {};
|
|
649
|
+
if (event.type === "progress") {
|
|
650
|
+
const summary = pickStringField(payload, "summary");
|
|
651
|
+
if (!summary) {
|
|
652
|
+
api.log?.warn?.("[orgx] Dropping invalid progress outbox event", {
|
|
653
|
+
eventId: event.id,
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const context = resolveReportingContext(payload);
|
|
658
|
+
if (!context.ok) {
|
|
659
|
+
throw new Error(context.error);
|
|
660
|
+
}
|
|
661
|
+
const rawPhase = pickStringField(payload, "phase") ?? "implementing";
|
|
662
|
+
const progressPct = typeof payload.progress_pct === "number" ? payload.progress_pct : undefined;
|
|
663
|
+
const phase = rawPhase === "intent" ||
|
|
664
|
+
rawPhase === "execution" ||
|
|
665
|
+
rawPhase === "blocked" ||
|
|
666
|
+
rawPhase === "review" ||
|
|
667
|
+
rawPhase === "handoff" ||
|
|
668
|
+
rawPhase === "completed"
|
|
669
|
+
? rawPhase
|
|
670
|
+
: toReportingPhase(rawPhase, progressPct);
|
|
671
|
+
await client.emitActivity({
|
|
672
|
+
initiative_id: context.value.initiativeId,
|
|
673
|
+
run_id: context.value.runId,
|
|
674
|
+
correlation_id: context.value.correlationId,
|
|
675
|
+
source_client: context.value.sourceClient,
|
|
676
|
+
message: summary,
|
|
677
|
+
phase,
|
|
678
|
+
progress_pct: progressPct,
|
|
679
|
+
level: pickStringField(payload, "level"),
|
|
680
|
+
next_step: pickStringField(payload, "next_step") ?? undefined,
|
|
681
|
+
metadata: {
|
|
682
|
+
source: "orgx_openclaw_outbox_replay",
|
|
683
|
+
outbox_event_id: event.id,
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (event.type === "decision") {
|
|
689
|
+
const question = pickStringField(payload, "question");
|
|
690
|
+
if (!question) {
|
|
691
|
+
api.log?.warn?.("[orgx] Dropping invalid decision outbox event", {
|
|
692
|
+
eventId: event.id,
|
|
693
|
+
});
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const context = resolveReportingContext(payload);
|
|
697
|
+
if (!context.ok) {
|
|
698
|
+
throw new Error(context.error);
|
|
699
|
+
}
|
|
700
|
+
await client.applyChangeset({
|
|
701
|
+
initiative_id: context.value.initiativeId,
|
|
702
|
+
run_id: context.value.runId,
|
|
703
|
+
correlation_id: context.value.correlationId,
|
|
704
|
+
source_client: context.value.sourceClient,
|
|
705
|
+
idempotency_key: pickStringField(payload, "idempotency_key") ?? `decision:${event.id}`,
|
|
706
|
+
operations: [
|
|
707
|
+
{
|
|
708
|
+
op: "decision.create",
|
|
709
|
+
title: question,
|
|
710
|
+
summary: pickStringField(payload, "context") ?? undefined,
|
|
711
|
+
urgency: pickStringField(payload, "urgency") ?? "medium",
|
|
712
|
+
options: pickStringArrayField(payload, "options"),
|
|
713
|
+
blocking: typeof payload.blocking === "boolean" ? payload.blocking : true,
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (event.type === "changeset") {
|
|
720
|
+
const context = resolveReportingContext(payload);
|
|
721
|
+
if (!context.ok) {
|
|
722
|
+
throw new Error(context.error);
|
|
723
|
+
}
|
|
724
|
+
const operations = Array.isArray(payload.operations)
|
|
725
|
+
? payload.operations
|
|
726
|
+
: [];
|
|
727
|
+
if (operations.length === 0) {
|
|
728
|
+
api.log?.warn?.("[orgx] Dropping invalid changeset outbox event", {
|
|
729
|
+
eventId: event.id,
|
|
730
|
+
});
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
await client.applyChangeset({
|
|
734
|
+
initiative_id: context.value.initiativeId,
|
|
735
|
+
run_id: context.value.runId,
|
|
736
|
+
correlation_id: context.value.correlationId,
|
|
737
|
+
source_client: context.value.sourceClient,
|
|
738
|
+
idempotency_key: pickStringField(payload, "idempotency_key") ?? `changeset:${event.id}`,
|
|
739
|
+
operations,
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (event.type === "artifact") {
|
|
744
|
+
const name = pickStringField(payload, "name");
|
|
745
|
+
if (!name) {
|
|
746
|
+
api.log?.warn?.("[orgx] Dropping invalid artifact outbox event", {
|
|
747
|
+
eventId: event.id,
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
await client.createEntity("artifact", {
|
|
752
|
+
name,
|
|
753
|
+
artifact_type: pickStringField(payload, "artifact_type") ?? "other",
|
|
754
|
+
description: pickStringField(payload, "description"),
|
|
755
|
+
artifact_url: pickStringField(payload, "url"),
|
|
756
|
+
status: "active",
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function flushOutboxQueues() {
|
|
762
|
+
const attemptAt = new Date().toISOString();
|
|
763
|
+
outboxReplayState = {
|
|
764
|
+
...outboxReplayState,
|
|
765
|
+
status: "running",
|
|
766
|
+
lastReplayAttemptAt: attemptAt,
|
|
767
|
+
lastReplayError: null,
|
|
768
|
+
};
|
|
769
|
+
let hadReplayFailure = false;
|
|
770
|
+
let lastReplayError = null;
|
|
771
|
+
for (const queue of outboxQueues) {
|
|
772
|
+
const pending = await readOutbox(queue);
|
|
773
|
+
if (pending.length === 0) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const remaining = [];
|
|
777
|
+
for (const event of pending) {
|
|
778
|
+
try {
|
|
779
|
+
await replayOutboxEvent(event);
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
hadReplayFailure = true;
|
|
783
|
+
lastReplayError = toErrorMessage(err);
|
|
784
|
+
remaining.push(event);
|
|
785
|
+
api.log?.warn?.("[orgx] Outbox replay failed", {
|
|
786
|
+
queue,
|
|
787
|
+
eventId: event.id,
|
|
788
|
+
error: lastReplayError,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
await replaceOutbox(queue, remaining);
|
|
793
|
+
const replayedCount = pending.length - remaining.length;
|
|
794
|
+
if (replayedCount > 0) {
|
|
795
|
+
api.log?.info?.("[orgx] Replayed buffered outbox events", {
|
|
796
|
+
queue,
|
|
797
|
+
replayed: replayedCount,
|
|
798
|
+
remaining: remaining.length,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (hadReplayFailure) {
|
|
803
|
+
outboxReplayState = {
|
|
804
|
+
...outboxReplayState,
|
|
805
|
+
status: "error",
|
|
806
|
+
lastReplayFailureAt: new Date().toISOString(),
|
|
807
|
+
lastReplayError,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
outboxReplayState = {
|
|
812
|
+
...outboxReplayState,
|
|
813
|
+
status: "success",
|
|
814
|
+
lastReplaySuccessAt: new Date().toISOString(),
|
|
815
|
+
lastReplayError: null,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
}
|
|
133
819
|
async function doSync() {
|
|
820
|
+
if (syncInFlight) {
|
|
821
|
+
return syncInFlight;
|
|
822
|
+
}
|
|
823
|
+
syncInFlight = (async () => {
|
|
824
|
+
if (!config.apiKey) {
|
|
825
|
+
updateOnboardingState({
|
|
826
|
+
status: "idle",
|
|
827
|
+
hasApiKey: false,
|
|
828
|
+
connectionVerified: false,
|
|
829
|
+
nextAction: "connect",
|
|
830
|
+
});
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
updateCachedSnapshot(await client.getOrgSnapshot());
|
|
835
|
+
updateOnboardingState({
|
|
836
|
+
status: "connected",
|
|
837
|
+
hasApiKey: true,
|
|
838
|
+
connectionVerified: true,
|
|
839
|
+
lastError: null,
|
|
840
|
+
nextAction: "open_dashboard",
|
|
841
|
+
});
|
|
842
|
+
await flushOutboxQueues();
|
|
843
|
+
api.log?.debug?.("[orgx] Sync OK");
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
updateOnboardingState({
|
|
847
|
+
status: "error",
|
|
848
|
+
hasApiKey: true,
|
|
849
|
+
connectionVerified: false,
|
|
850
|
+
lastError: toErrorMessage(err),
|
|
851
|
+
nextAction: "reconnect",
|
|
852
|
+
});
|
|
853
|
+
api.log?.warn?.(`[orgx] Sync failed: ${err instanceof Error ? err.message : err}`);
|
|
854
|
+
}
|
|
855
|
+
})();
|
|
134
856
|
try {
|
|
135
|
-
|
|
136
|
-
lastSnapshotAt = Date.now();
|
|
137
|
-
api.log?.debug?.("[orgx] Sync OK");
|
|
857
|
+
await syncInFlight;
|
|
138
858
|
}
|
|
139
|
-
|
|
140
|
-
|
|
859
|
+
finally {
|
|
860
|
+
syncInFlight = null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function scheduleNextSync() {
|
|
864
|
+
if (!syncServiceRunning) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
syncTimer = setTimeout(async () => {
|
|
868
|
+
await doSync();
|
|
869
|
+
scheduleNextSync();
|
|
870
|
+
}, config.syncIntervalMs);
|
|
871
|
+
}
|
|
872
|
+
async function startPairing(input) {
|
|
873
|
+
updateOnboardingState({
|
|
874
|
+
status: "starting",
|
|
875
|
+
lastError: null,
|
|
876
|
+
nextAction: "connect",
|
|
877
|
+
});
|
|
878
|
+
const started = await fetchOrgxJson("POST", "/api/plugin/openclaw/pairings", {
|
|
879
|
+
installationId: config.installationId,
|
|
880
|
+
pluginVersion: config.pluginVersion,
|
|
881
|
+
openclawVersion: input.openclawVersion,
|
|
882
|
+
platform: input.platform || process.platform,
|
|
883
|
+
deviceName: input.deviceName,
|
|
884
|
+
});
|
|
885
|
+
if (!started.ok) {
|
|
886
|
+
if (isAuthRequiredError(started)) {
|
|
887
|
+
clearPairingState();
|
|
888
|
+
const manualConnectUrl = buildManualKeyConnectUrl();
|
|
889
|
+
const state = updateOnboardingState({
|
|
890
|
+
status: "manual_key",
|
|
891
|
+
hasApiKey: Boolean(config.apiKey),
|
|
892
|
+
connectionVerified: false,
|
|
893
|
+
lastError: null,
|
|
894
|
+
nextAction: "enter_manual_key",
|
|
895
|
+
connectUrl: manualConnectUrl,
|
|
896
|
+
pairingId: null,
|
|
897
|
+
expiresAt: null,
|
|
898
|
+
pollIntervalMs: null,
|
|
899
|
+
});
|
|
900
|
+
return {
|
|
901
|
+
pairingId: "manual_key",
|
|
902
|
+
connectUrl: manualConnectUrl,
|
|
903
|
+
expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
|
|
904
|
+
pollIntervalMs: 1_500,
|
|
905
|
+
state,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
const message = `Pairing start failed: ${started.error}`;
|
|
909
|
+
updateOnboardingState({
|
|
910
|
+
status: "error",
|
|
911
|
+
hasApiKey: Boolean(config.apiKey),
|
|
912
|
+
connectionVerified: false,
|
|
913
|
+
lastError: message,
|
|
914
|
+
nextAction: "enter_manual_key",
|
|
915
|
+
});
|
|
916
|
+
throw new Error(message);
|
|
917
|
+
}
|
|
918
|
+
activePairing = {
|
|
919
|
+
pairingId: started.data.pairingId,
|
|
920
|
+
pollToken: started.data.pollToken,
|
|
921
|
+
connectUrl: started.data.connectUrl,
|
|
922
|
+
expiresAt: started.data.expiresAt,
|
|
923
|
+
pollIntervalMs: started.data.pollIntervalMs,
|
|
924
|
+
};
|
|
925
|
+
const state = updateOnboardingState({
|
|
926
|
+
status: "awaiting_browser_auth",
|
|
927
|
+
hasApiKey: false,
|
|
928
|
+
connectionVerified: false,
|
|
929
|
+
lastError: null,
|
|
930
|
+
nextAction: "wait_for_browser",
|
|
931
|
+
connectUrl: started.data.connectUrl,
|
|
932
|
+
pairingId: started.data.pairingId,
|
|
933
|
+
expiresAt: started.data.expiresAt,
|
|
934
|
+
pollIntervalMs: started.data.pollIntervalMs,
|
|
935
|
+
});
|
|
936
|
+
return {
|
|
937
|
+
pairingId: started.data.pairingId,
|
|
938
|
+
connectUrl: started.data.connectUrl,
|
|
939
|
+
expiresAt: started.data.expiresAt,
|
|
940
|
+
pollIntervalMs: started.data.pollIntervalMs,
|
|
941
|
+
state,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
async function getPairingStatus() {
|
|
945
|
+
if (!activePairing) {
|
|
946
|
+
return { ...onboardingState };
|
|
947
|
+
}
|
|
948
|
+
const polled = await fetchOrgxJson("GET", `/api/plugin/openclaw/pairings/${encodeURIComponent(activePairing.pairingId)}?pollToken=${encodeURIComponent(activePairing.pollToken)}`);
|
|
949
|
+
if (!polled.ok) {
|
|
950
|
+
return updateOnboardingState({
|
|
951
|
+
status: "error",
|
|
952
|
+
hasApiKey: Boolean(config.apiKey),
|
|
953
|
+
connectionVerified: false,
|
|
954
|
+
lastError: polled.error,
|
|
955
|
+
nextAction: "enter_manual_key",
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
const status = polled.data.status;
|
|
959
|
+
if (status === "pending" || status === "authorized") {
|
|
960
|
+
return updateOnboardingState({
|
|
961
|
+
status: "pairing",
|
|
962
|
+
hasApiKey: false,
|
|
963
|
+
connectionVerified: false,
|
|
964
|
+
lastError: null,
|
|
965
|
+
nextAction: "wait_for_browser",
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
if (status === "ready") {
|
|
969
|
+
const key = typeof polled.data.key === "string" ? polled.data.key : "";
|
|
970
|
+
if (!key) {
|
|
971
|
+
clearPairingState();
|
|
972
|
+
return updateOnboardingState({
|
|
973
|
+
status: "error",
|
|
974
|
+
hasApiKey: false,
|
|
975
|
+
connectionVerified: false,
|
|
976
|
+
lastError: "Pairing completed without an API key payload.",
|
|
977
|
+
nextAction: "retry",
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
setRuntimeApiKey({
|
|
981
|
+
apiKey: key,
|
|
982
|
+
source: "browser_pairing",
|
|
983
|
+
workspaceName: polled.data.workspaceName ?? null,
|
|
984
|
+
keyPrefix: polled.data.keyPrefix ?? null,
|
|
985
|
+
});
|
|
986
|
+
await fetchOrgxJson("POST", `/api/plugin/openclaw/pairings/${encodeURIComponent(activePairing.pairingId)}/ack`, {
|
|
987
|
+
pollToken: activePairing.pollToken,
|
|
988
|
+
});
|
|
989
|
+
clearPairingState();
|
|
990
|
+
updateOnboardingState({
|
|
991
|
+
status: "connected",
|
|
992
|
+
hasApiKey: true,
|
|
993
|
+
connectionVerified: false,
|
|
994
|
+
workspaceName: polled.data.workspaceName ?? null,
|
|
995
|
+
nextAction: "open_dashboard",
|
|
996
|
+
lastError: null,
|
|
997
|
+
});
|
|
998
|
+
await doSync();
|
|
999
|
+
return { ...onboardingState };
|
|
1000
|
+
}
|
|
1001
|
+
if (status === "consumed") {
|
|
1002
|
+
clearPairingState();
|
|
1003
|
+
return updateOnboardingState({
|
|
1004
|
+
status: config.apiKey ? "connected" : "error",
|
|
1005
|
+
hasApiKey: Boolean(config.apiKey),
|
|
1006
|
+
connectionVerified: false,
|
|
1007
|
+
lastError: config.apiKey ? null : "Pairing consumed but key is unavailable.",
|
|
1008
|
+
nextAction: config.apiKey ? "open_dashboard" : "retry",
|
|
1009
|
+
});
|
|
141
1010
|
}
|
|
1011
|
+
clearPairingState();
|
|
1012
|
+
return updateOnboardingState({
|
|
1013
|
+
status: status === "cancelled" ? "manual_key" : "error",
|
|
1014
|
+
hasApiKey: Boolean(config.apiKey),
|
|
1015
|
+
connectionVerified: false,
|
|
1016
|
+
lastError: polled.data.errorMessage ?? "Pairing failed or expired.",
|
|
1017
|
+
nextAction: "retry",
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
async function submitManualKey(input) {
|
|
1021
|
+
const nextKey = input.apiKey.trim();
|
|
1022
|
+
if (!nextKey) {
|
|
1023
|
+
throw new Error("apiKey is required");
|
|
1024
|
+
}
|
|
1025
|
+
updateOnboardingState({
|
|
1026
|
+
status: "manual_key",
|
|
1027
|
+
hasApiKey: false,
|
|
1028
|
+
connectionVerified: false,
|
|
1029
|
+
lastError: null,
|
|
1030
|
+
nextAction: "enter_manual_key",
|
|
1031
|
+
});
|
|
1032
|
+
const probeClient = new OrgXClient(nextKey, config.baseUrl, resolveRuntimeUserId(nextKey, [input.userId, config.userId]));
|
|
1033
|
+
const snapshot = await probeClient.getOrgSnapshot();
|
|
1034
|
+
setRuntimeApiKey({
|
|
1035
|
+
apiKey: nextKey,
|
|
1036
|
+
source: "manual",
|
|
1037
|
+
userId: resolveRuntimeUserId(nextKey, [input.userId, config.userId]) || null,
|
|
1038
|
+
workspaceName: onboardingState.workspaceName,
|
|
1039
|
+
keyPrefix: null,
|
|
1040
|
+
});
|
|
1041
|
+
updateCachedSnapshot(snapshot);
|
|
1042
|
+
return updateOnboardingState({
|
|
1043
|
+
status: "connected",
|
|
1044
|
+
hasApiKey: true,
|
|
1045
|
+
connectionVerified: true,
|
|
1046
|
+
lastError: null,
|
|
1047
|
+
nextAction: "open_dashboard",
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
async function disconnectOnboarding() {
|
|
1051
|
+
if (activePairing) {
|
|
1052
|
+
await fetchOrgxJson("POST", `/api/plugin/openclaw/pairings/${encodeURIComponent(activePairing.pairingId)}/cancel`, {
|
|
1053
|
+
pollToken: activePairing.pollToken,
|
|
1054
|
+
reason: "disconnect",
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
clearPairingState();
|
|
1058
|
+
clearPersistedApiKey();
|
|
1059
|
+
clearPersistedSnapshot();
|
|
1060
|
+
config.apiKey = "";
|
|
1061
|
+
config.userId = "";
|
|
1062
|
+
client.setCredentials({ apiKey: "", userId: "" });
|
|
1063
|
+
cachedSnapshot = null;
|
|
1064
|
+
lastSnapshotAt = 0;
|
|
1065
|
+
return updateOnboardingState({
|
|
1066
|
+
status: "idle",
|
|
1067
|
+
hasApiKey: false,
|
|
1068
|
+
connectionVerified: false,
|
|
1069
|
+
workspaceName: null,
|
|
1070
|
+
lastError: null,
|
|
1071
|
+
nextAction: "connect",
|
|
1072
|
+
keySource: "none",
|
|
1073
|
+
});
|
|
142
1074
|
}
|
|
143
1075
|
api.registerService({
|
|
144
1076
|
id: "orgx-sync",
|
|
145
1077
|
start: async () => {
|
|
1078
|
+
syncServiceRunning = true;
|
|
146
1079
|
api.log?.info?.("[orgx] Starting sync service", {
|
|
147
1080
|
interval: config.syncIntervalMs,
|
|
148
1081
|
});
|
|
149
1082
|
await doSync();
|
|
150
|
-
|
|
1083
|
+
scheduleNextSync();
|
|
151
1084
|
},
|
|
152
1085
|
stop: async () => {
|
|
1086
|
+
syncServiceRunning = false;
|
|
153
1087
|
if (syncTimer)
|
|
154
|
-
|
|
1088
|
+
clearTimeout(syncTimer);
|
|
155
1089
|
syncTimer = null;
|
|
156
1090
|
},
|
|
157
1091
|
});
|
|
1092
|
+
async function autoAssignEntityForCreate(input) {
|
|
1093
|
+
const warnings = [];
|
|
1094
|
+
const byKey = new Map();
|
|
1095
|
+
const addAgent = (agent) => {
|
|
1096
|
+
const key = `${agent.id}:${agent.name}`.toLowerCase();
|
|
1097
|
+
if (!byKey.has(key))
|
|
1098
|
+
byKey.set(key, agent);
|
|
1099
|
+
};
|
|
1100
|
+
let liveAgents = [];
|
|
1101
|
+
try {
|
|
1102
|
+
const agentResp = await client.getLiveAgents({
|
|
1103
|
+
initiative: input.initiativeId,
|
|
1104
|
+
includeIdle: true,
|
|
1105
|
+
});
|
|
1106
|
+
liveAgents = (Array.isArray(agentResp.agents) ? agentResp.agents : [])
|
|
1107
|
+
.map((raw) => {
|
|
1108
|
+
if (!raw || typeof raw !== "object")
|
|
1109
|
+
return null;
|
|
1110
|
+
const record = raw;
|
|
1111
|
+
const id = (typeof record.id === "string" && record.id.trim()) ||
|
|
1112
|
+
(typeof record.agentId === "string" && record.agentId.trim()) ||
|
|
1113
|
+
"";
|
|
1114
|
+
const name = (typeof record.name === "string" && record.name.trim()) ||
|
|
1115
|
+
(typeof record.agentName === "string" && record.agentName.trim()) ||
|
|
1116
|
+
id;
|
|
1117
|
+
if (!name)
|
|
1118
|
+
return null;
|
|
1119
|
+
return {
|
|
1120
|
+
id: id || `name:${name}`,
|
|
1121
|
+
name,
|
|
1122
|
+
domain: (typeof record.domain === "string" && record.domain.trim()) ||
|
|
1123
|
+
(typeof record.role === "string" && record.role.trim()) ||
|
|
1124
|
+
null,
|
|
1125
|
+
status: (typeof record.status === "string" && record.status.trim()) || null,
|
|
1126
|
+
};
|
|
1127
|
+
})
|
|
1128
|
+
.filter((item) => item !== null);
|
|
1129
|
+
}
|
|
1130
|
+
catch (err) {
|
|
1131
|
+
warnings.push(`live agents unavailable (${toErrorMessage(err)})`);
|
|
1132
|
+
}
|
|
1133
|
+
const orchestrator = liveAgents.find((agent) => /holt|orchestrator/i.test(agent.name) ||
|
|
1134
|
+
/orchestrator/i.test(agent.domain ?? ""));
|
|
1135
|
+
if (orchestrator)
|
|
1136
|
+
addAgent(orchestrator);
|
|
1137
|
+
let assignmentSource = "fallback";
|
|
1138
|
+
try {
|
|
1139
|
+
const preflight = await client.delegationPreflight({
|
|
1140
|
+
intent: `${input.title}${input.summary ? `: ${input.summary}` : ""}`,
|
|
1141
|
+
});
|
|
1142
|
+
const recommendations = preflight.data?.recommended_split ?? [];
|
|
1143
|
+
const recommendedDomains = [
|
|
1144
|
+
...new Set(recommendations
|
|
1145
|
+
.map((entry) => String(entry.owner_domain ?? "").trim().toLowerCase())
|
|
1146
|
+
.filter(Boolean)),
|
|
1147
|
+
];
|
|
1148
|
+
for (const domain of recommendedDomains) {
|
|
1149
|
+
const match = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
|
|
1150
|
+
if (match)
|
|
1151
|
+
addAgent(match);
|
|
1152
|
+
}
|
|
1153
|
+
if (recommendedDomains.length > 0) {
|
|
1154
|
+
assignmentSource = "orchestrator";
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
catch (err) {
|
|
1158
|
+
warnings.push(`delegation preflight failed (${toErrorMessage(err)})`);
|
|
1159
|
+
}
|
|
1160
|
+
if (byKey.size === 0) {
|
|
1161
|
+
const haystack = `${input.title} ${input.summary ?? ""}`.toLowerCase();
|
|
1162
|
+
const domainHints = [];
|
|
1163
|
+
if (/market|campaign|thread|article|tweet|copy/.test(haystack)) {
|
|
1164
|
+
domainHints.push("marketing");
|
|
1165
|
+
}
|
|
1166
|
+
else if (/design|ux|ui|a11y/.test(haystack)) {
|
|
1167
|
+
domainHints.push("design");
|
|
1168
|
+
}
|
|
1169
|
+
else if (/ops|runbook|incident|reliability/.test(haystack)) {
|
|
1170
|
+
domainHints.push("operations");
|
|
1171
|
+
}
|
|
1172
|
+
else if (/sales|deal|pipeline/.test(haystack)) {
|
|
1173
|
+
domainHints.push("sales");
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
domainHints.push("engineering", "product");
|
|
1177
|
+
}
|
|
1178
|
+
for (const domain of domainHints) {
|
|
1179
|
+
const match = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
|
|
1180
|
+
if (match)
|
|
1181
|
+
addAgent(match);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (byKey.size === 0 && liveAgents.length > 0) {
|
|
1185
|
+
addAgent(liveAgents[0]);
|
|
1186
|
+
warnings.push("fallback selected first available live agent");
|
|
1187
|
+
}
|
|
1188
|
+
const assignedAgents = Array.from(byKey.values());
|
|
1189
|
+
let updatedEntity = null;
|
|
1190
|
+
try {
|
|
1191
|
+
updatedEntity = (await client.updateEntity(input.entityType, input.entityId, {
|
|
1192
|
+
assigned_agent_ids: assignedAgents.map((agent) => agent.id),
|
|
1193
|
+
assigned_agent_names: assignedAgents.map((agent) => agent.name),
|
|
1194
|
+
assignment_source: assignmentSource,
|
|
1195
|
+
}));
|
|
1196
|
+
}
|
|
1197
|
+
catch (err) {
|
|
1198
|
+
warnings.push(`assignment update failed (${toErrorMessage(err)})`);
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
assignmentSource,
|
|
1202
|
+
assignedAgents,
|
|
1203
|
+
warnings,
|
|
1204
|
+
updatedEntity,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
158
1207
|
// ---------------------------------------------------------------------------
|
|
159
1208
|
// 2. MCP Tools (Model Context Protocol compatible)
|
|
160
1209
|
// ---------------------------------------------------------------------------
|
|
@@ -475,8 +1524,44 @@ export default function register(api) {
|
|
|
475
1524
|
async execute(_callId, params = {}) {
|
|
476
1525
|
try {
|
|
477
1526
|
const { type, ...data } = params;
|
|
478
|
-
|
|
479
|
-
|
|
1527
|
+
let entity = await client.createEntity(type, data);
|
|
1528
|
+
let assignmentSummary = null;
|
|
1529
|
+
const entityType = String(type ?? "");
|
|
1530
|
+
if (entityType === "initiative" || entityType === "workstream") {
|
|
1531
|
+
const entityRecord = entity;
|
|
1532
|
+
const assignment = await autoAssignEntityForCreate({
|
|
1533
|
+
entityType,
|
|
1534
|
+
entityId: String(entityRecord.id ?? ""),
|
|
1535
|
+
initiativeId: entityType === "initiative"
|
|
1536
|
+
? String(entityRecord.id ?? "")
|
|
1537
|
+
: (typeof data.initiative_id === "string"
|
|
1538
|
+
? data.initiative_id
|
|
1539
|
+
: null),
|
|
1540
|
+
title: (typeof entityRecord.title === "string" && entityRecord.title) ||
|
|
1541
|
+
(typeof entityRecord.name === "string" && entityRecord.name) ||
|
|
1542
|
+
(typeof data.title === "string" && data.title) ||
|
|
1543
|
+
"Untitled",
|
|
1544
|
+
summary: (typeof entityRecord.summary === "string" && entityRecord.summary) ||
|
|
1545
|
+
(typeof data.summary === "string" && data.summary) ||
|
|
1546
|
+
null,
|
|
1547
|
+
});
|
|
1548
|
+
if (assignment.updatedEntity) {
|
|
1549
|
+
entity = assignment.updatedEntity;
|
|
1550
|
+
}
|
|
1551
|
+
assignmentSummary = {
|
|
1552
|
+
assignment_source: assignment.assignmentSource,
|
|
1553
|
+
assigned_agents: assignment.assignedAgents,
|
|
1554
|
+
warnings: assignment.warnings,
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
return json(`✅ Created ${type}: ${entity.title ?? entity.id}`, {
|
|
1558
|
+
entity,
|
|
1559
|
+
...(assignmentSummary
|
|
1560
|
+
? {
|
|
1561
|
+
auto_assignment: assignmentSummary,
|
|
1562
|
+
}
|
|
1563
|
+
: {}),
|
|
1564
|
+
});
|
|
480
1565
|
}
|
|
481
1566
|
catch (err) {
|
|
482
1567
|
return text(`❌ Creation failed: ${err instanceof Error ? err.message : err}`);
|
|
@@ -560,6 +1645,433 @@ export default function register(api) {
|
|
|
560
1645
|
}
|
|
561
1646
|
},
|
|
562
1647
|
}, { optional: true });
|
|
1648
|
+
async function emitActivityWithFallback(source, payload) {
|
|
1649
|
+
if (!payload.message || payload.message.trim().length === 0) {
|
|
1650
|
+
return text("❌ message is required");
|
|
1651
|
+
}
|
|
1652
|
+
const context = resolveReportingContext(payload);
|
|
1653
|
+
if (!context.ok) {
|
|
1654
|
+
return text(`❌ ${context.error}`);
|
|
1655
|
+
}
|
|
1656
|
+
const now = new Date().toISOString();
|
|
1657
|
+
const id = `progress:${randomUUID().slice(0, 8)}`;
|
|
1658
|
+
const normalizedPayload = {
|
|
1659
|
+
initiative_id: context.value.initiativeId,
|
|
1660
|
+
run_id: context.value.runId,
|
|
1661
|
+
correlation_id: context.value.correlationId,
|
|
1662
|
+
source_client: context.value.sourceClient,
|
|
1663
|
+
message: payload.message,
|
|
1664
|
+
phase: payload.phase ?? "execution",
|
|
1665
|
+
progress_pct: payload.progress_pct,
|
|
1666
|
+
level: payload.level ?? "info",
|
|
1667
|
+
next_step: payload.next_step,
|
|
1668
|
+
metadata: {
|
|
1669
|
+
...(payload.metadata ?? {}),
|
|
1670
|
+
source,
|
|
1671
|
+
},
|
|
1672
|
+
};
|
|
1673
|
+
const activityItem = {
|
|
1674
|
+
id,
|
|
1675
|
+
type: "delegation",
|
|
1676
|
+
title: payload.message,
|
|
1677
|
+
description: payload.next_step ?? null,
|
|
1678
|
+
agentId: null,
|
|
1679
|
+
agentName: null,
|
|
1680
|
+
runId: context.value.runId ?? null,
|
|
1681
|
+
initiativeId: context.value.initiativeId,
|
|
1682
|
+
timestamp: now,
|
|
1683
|
+
phase: normalizedPayload.phase,
|
|
1684
|
+
summary: payload.next_step ? `Next: ${payload.next_step}` : payload.message,
|
|
1685
|
+
metadata: normalizedPayload.metadata,
|
|
1686
|
+
};
|
|
1687
|
+
try {
|
|
1688
|
+
const result = await client.emitActivity(normalizedPayload);
|
|
1689
|
+
return text(`Activity emitted: ${payload.message} [${normalizedPayload.phase}${payload.progress_pct != null ? ` ${payload.progress_pct}%` : ""}] (run ${result.run_id.slice(0, 8)}...)`);
|
|
1690
|
+
}
|
|
1691
|
+
catch {
|
|
1692
|
+
await appendToOutbox("progress", {
|
|
1693
|
+
id,
|
|
1694
|
+
type: "progress",
|
|
1695
|
+
timestamp: now,
|
|
1696
|
+
payload: normalizedPayload,
|
|
1697
|
+
activityItem,
|
|
1698
|
+
});
|
|
1699
|
+
return text(`Activity saved locally: ${payload.message} [${normalizedPayload.phase}${payload.progress_pct != null ? ` ${payload.progress_pct}%` : ""}] (will sync when connected)`);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
async function applyChangesetWithFallback(source, payload) {
|
|
1703
|
+
const context = resolveReportingContext(payload);
|
|
1704
|
+
if (!context.ok) {
|
|
1705
|
+
return text(`❌ ${context.error}`);
|
|
1706
|
+
}
|
|
1707
|
+
if (!Array.isArray(payload.operations) || payload.operations.length === 0) {
|
|
1708
|
+
return text("❌ operations must contain at least one change");
|
|
1709
|
+
}
|
|
1710
|
+
const idempotencyKey = pickNonEmptyString(payload.idempotency_key) ??
|
|
1711
|
+
`${source}:${Date.now()}:${randomUUID().slice(0, 8)}`;
|
|
1712
|
+
const requestPayload = {
|
|
1713
|
+
initiative_id: context.value.initiativeId,
|
|
1714
|
+
run_id: context.value.runId,
|
|
1715
|
+
correlation_id: context.value.correlationId,
|
|
1716
|
+
source_client: context.value.sourceClient,
|
|
1717
|
+
idempotency_key: idempotencyKey,
|
|
1718
|
+
operations: payload.operations,
|
|
1719
|
+
};
|
|
1720
|
+
const now = new Date().toISOString();
|
|
1721
|
+
const id = `changeset:${randomUUID().slice(0, 8)}`;
|
|
1722
|
+
const activityItem = {
|
|
1723
|
+
id,
|
|
1724
|
+
type: "milestone_completed",
|
|
1725
|
+
title: "Changeset queued",
|
|
1726
|
+
description: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
|
|
1727
|
+
agentId: null,
|
|
1728
|
+
agentName: null,
|
|
1729
|
+
runId: context.value.runId ?? null,
|
|
1730
|
+
initiativeId: context.value.initiativeId,
|
|
1731
|
+
timestamp: now,
|
|
1732
|
+
phase: "review",
|
|
1733
|
+
summary: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
|
|
1734
|
+
metadata: {
|
|
1735
|
+
source,
|
|
1736
|
+
idempotency_key: idempotencyKey,
|
|
1737
|
+
},
|
|
1738
|
+
};
|
|
1739
|
+
try {
|
|
1740
|
+
const result = await client.applyChangeset(requestPayload);
|
|
1741
|
+
return text(`Changeset ${result.replayed ? "replayed" : "applied"}: ${result.applied_count} op${result.applied_count === 1 ? "" : "s"} (run ${result.run_id.slice(0, 8)}...)`);
|
|
1742
|
+
}
|
|
1743
|
+
catch {
|
|
1744
|
+
await appendToOutbox("decisions", {
|
|
1745
|
+
id,
|
|
1746
|
+
type: "changeset",
|
|
1747
|
+
timestamp: now,
|
|
1748
|
+
payload: requestPayload,
|
|
1749
|
+
activityItem,
|
|
1750
|
+
});
|
|
1751
|
+
return text(`Changeset saved locally (${payload.operations.length} op${payload.operations.length === 1 ? "" : "s"}) (will sync when connected)`);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
// --- orgx_emit_activity ---
|
|
1755
|
+
api.registerTool({
|
|
1756
|
+
name: "orgx_emit_activity",
|
|
1757
|
+
description: "Emit append-only OrgX activity telemetry (launch reporting contract primary write tool).",
|
|
1758
|
+
parameters: {
|
|
1759
|
+
type: "object",
|
|
1760
|
+
properties: {
|
|
1761
|
+
initiative_id: {
|
|
1762
|
+
type: "string",
|
|
1763
|
+
description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
|
|
1764
|
+
},
|
|
1765
|
+
message: {
|
|
1766
|
+
type: "string",
|
|
1767
|
+
description: "Human-readable activity update",
|
|
1768
|
+
},
|
|
1769
|
+
run_id: {
|
|
1770
|
+
type: "string",
|
|
1771
|
+
description: "Optional run UUID",
|
|
1772
|
+
},
|
|
1773
|
+
correlation_id: {
|
|
1774
|
+
type: "string",
|
|
1775
|
+
description: "Required when run_id is omitted",
|
|
1776
|
+
},
|
|
1777
|
+
source_client: {
|
|
1778
|
+
type: "string",
|
|
1779
|
+
enum: ["openclaw", "codex", "claude-code", "api"],
|
|
1780
|
+
description: "Required when run_id is omitted",
|
|
1781
|
+
},
|
|
1782
|
+
phase: {
|
|
1783
|
+
type: "string",
|
|
1784
|
+
enum: ["intent", "execution", "blocked", "review", "handoff", "completed"],
|
|
1785
|
+
description: "Reporting phase",
|
|
1786
|
+
},
|
|
1787
|
+
progress_pct: {
|
|
1788
|
+
type: "number",
|
|
1789
|
+
minimum: 0,
|
|
1790
|
+
maximum: 100,
|
|
1791
|
+
description: "Optional progress percentage",
|
|
1792
|
+
},
|
|
1793
|
+
level: {
|
|
1794
|
+
type: "string",
|
|
1795
|
+
enum: ["info", "warn", "error"],
|
|
1796
|
+
description: "Optional level (default info)",
|
|
1797
|
+
},
|
|
1798
|
+
next_step: {
|
|
1799
|
+
type: "string",
|
|
1800
|
+
description: "Optional next step",
|
|
1801
|
+
},
|
|
1802
|
+
metadata: {
|
|
1803
|
+
type: "object",
|
|
1804
|
+
description: "Optional structured metadata",
|
|
1805
|
+
},
|
|
1806
|
+
},
|
|
1807
|
+
required: ["message"],
|
|
1808
|
+
additionalProperties: false,
|
|
1809
|
+
},
|
|
1810
|
+
async execute(_callId, params = { message: "" }) {
|
|
1811
|
+
return emitActivityWithFallback("orgx_emit_activity", params);
|
|
1812
|
+
},
|
|
1813
|
+
}, { optional: true });
|
|
1814
|
+
// --- orgx_apply_changeset ---
|
|
1815
|
+
api.registerTool({
|
|
1816
|
+
name: "orgx_apply_changeset",
|
|
1817
|
+
description: "Apply an idempotent transactional OrgX changeset (launch reporting contract primary mutation tool).",
|
|
1818
|
+
parameters: {
|
|
1819
|
+
type: "object",
|
|
1820
|
+
properties: {
|
|
1821
|
+
initiative_id: {
|
|
1822
|
+
type: "string",
|
|
1823
|
+
description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
|
|
1824
|
+
},
|
|
1825
|
+
idempotency_key: {
|
|
1826
|
+
type: "string",
|
|
1827
|
+
description: "Idempotency key (<=120 chars). Auto-generated if omitted.",
|
|
1828
|
+
},
|
|
1829
|
+
operations: {
|
|
1830
|
+
type: "array",
|
|
1831
|
+
minItems: 1,
|
|
1832
|
+
maxItems: 25,
|
|
1833
|
+
description: "Changeset operations (task.create, task.update, milestone.update, decision.create)",
|
|
1834
|
+
items: { type: "object" },
|
|
1835
|
+
},
|
|
1836
|
+
run_id: {
|
|
1837
|
+
type: "string",
|
|
1838
|
+
description: "Optional run UUID",
|
|
1839
|
+
},
|
|
1840
|
+
correlation_id: {
|
|
1841
|
+
type: "string",
|
|
1842
|
+
description: "Required when run_id is omitted",
|
|
1843
|
+
},
|
|
1844
|
+
source_client: {
|
|
1845
|
+
type: "string",
|
|
1846
|
+
enum: ["openclaw", "codex", "claude-code", "api"],
|
|
1847
|
+
description: "Required when run_id is omitted",
|
|
1848
|
+
},
|
|
1849
|
+
},
|
|
1850
|
+
required: ["operations"],
|
|
1851
|
+
additionalProperties: false,
|
|
1852
|
+
},
|
|
1853
|
+
async execute(_callId, params = { operations: [] }) {
|
|
1854
|
+
return applyChangesetWithFallback("orgx_apply_changeset", params);
|
|
1855
|
+
},
|
|
1856
|
+
}, { optional: true });
|
|
1857
|
+
// --- orgx_report_progress (alias -> orgx_emit_activity) ---
|
|
1858
|
+
api.registerTool({
|
|
1859
|
+
name: "orgx_report_progress",
|
|
1860
|
+
description: "Alias for orgx_emit_activity. Report progress at key milestones so the team can track your work.",
|
|
1861
|
+
parameters: {
|
|
1862
|
+
type: "object",
|
|
1863
|
+
properties: {
|
|
1864
|
+
initiative_id: {
|
|
1865
|
+
type: "string",
|
|
1866
|
+
description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
|
|
1867
|
+
},
|
|
1868
|
+
run_id: {
|
|
1869
|
+
type: "string",
|
|
1870
|
+
description: "Optional run UUID",
|
|
1871
|
+
},
|
|
1872
|
+
correlation_id: {
|
|
1873
|
+
type: "string",
|
|
1874
|
+
description: "Required when run_id is omitted",
|
|
1875
|
+
},
|
|
1876
|
+
source_client: {
|
|
1877
|
+
type: "string",
|
|
1878
|
+
enum: ["openclaw", "codex", "claude-code", "api"],
|
|
1879
|
+
},
|
|
1880
|
+
summary: {
|
|
1881
|
+
type: "string",
|
|
1882
|
+
description: "What was accomplished (1-2 sentences, human-readable)",
|
|
1883
|
+
},
|
|
1884
|
+
phase: {
|
|
1885
|
+
type: "string",
|
|
1886
|
+
enum: ["researching", "implementing", "testing", "reviewing", "blocked"],
|
|
1887
|
+
description: "Current work phase",
|
|
1888
|
+
},
|
|
1889
|
+
progress_pct: {
|
|
1890
|
+
type: "number",
|
|
1891
|
+
description: "Progress percentage (0-100)",
|
|
1892
|
+
minimum: 0,
|
|
1893
|
+
maximum: 100,
|
|
1894
|
+
},
|
|
1895
|
+
next_step: {
|
|
1896
|
+
type: "string",
|
|
1897
|
+
description: "What you plan to do next",
|
|
1898
|
+
},
|
|
1899
|
+
},
|
|
1900
|
+
required: ["summary", "phase"],
|
|
1901
|
+
additionalProperties: false,
|
|
1902
|
+
},
|
|
1903
|
+
async execute(_callId, params = { summary: "", phase: "implementing" }) {
|
|
1904
|
+
return emitActivityWithFallback("orgx_report_progress", {
|
|
1905
|
+
initiative_id: params.initiative_id,
|
|
1906
|
+
run_id: params.run_id,
|
|
1907
|
+
correlation_id: params.correlation_id,
|
|
1908
|
+
source_client: params.source_client,
|
|
1909
|
+
message: params.summary,
|
|
1910
|
+
phase: toReportingPhase(params.phase, params.progress_pct),
|
|
1911
|
+
progress_pct: params.progress_pct,
|
|
1912
|
+
next_step: params.next_step,
|
|
1913
|
+
level: params.phase === "blocked" ? "warn" : "info",
|
|
1914
|
+
metadata: {
|
|
1915
|
+
legacy_phase: params.phase,
|
|
1916
|
+
},
|
|
1917
|
+
});
|
|
1918
|
+
},
|
|
1919
|
+
}, { optional: true });
|
|
1920
|
+
// --- orgx_request_decision (alias -> orgx_apply_changeset decision.create) ---
|
|
1921
|
+
api.registerTool({
|
|
1922
|
+
name: "orgx_request_decision",
|
|
1923
|
+
description: "Alias for orgx_apply_changeset with decision.create. Request a human decision before proceeding.",
|
|
1924
|
+
parameters: {
|
|
1925
|
+
type: "object",
|
|
1926
|
+
properties: {
|
|
1927
|
+
initiative_id: {
|
|
1928
|
+
type: "string",
|
|
1929
|
+
description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
|
|
1930
|
+
},
|
|
1931
|
+
run_id: {
|
|
1932
|
+
type: "string",
|
|
1933
|
+
description: "Optional run UUID",
|
|
1934
|
+
},
|
|
1935
|
+
correlation_id: {
|
|
1936
|
+
type: "string",
|
|
1937
|
+
description: "Required when run_id is omitted",
|
|
1938
|
+
},
|
|
1939
|
+
source_client: {
|
|
1940
|
+
type: "string",
|
|
1941
|
+
enum: ["openclaw", "codex", "claude-code", "api"],
|
|
1942
|
+
},
|
|
1943
|
+
question: {
|
|
1944
|
+
type: "string",
|
|
1945
|
+
description: "The decision question (e.g., 'Deploy to production?')",
|
|
1946
|
+
},
|
|
1947
|
+
context: {
|
|
1948
|
+
type: "string",
|
|
1949
|
+
description: "Background context to help the human decide",
|
|
1950
|
+
},
|
|
1951
|
+
options: {
|
|
1952
|
+
type: "array",
|
|
1953
|
+
items: { type: "string" },
|
|
1954
|
+
description: "Available choices (e.g., ['Yes, deploy now', 'Wait for more testing', 'Cancel'])",
|
|
1955
|
+
},
|
|
1956
|
+
urgency: {
|
|
1957
|
+
type: "string",
|
|
1958
|
+
enum: ["low", "medium", "high", "urgent"],
|
|
1959
|
+
description: "How urgent this decision is",
|
|
1960
|
+
},
|
|
1961
|
+
blocking: {
|
|
1962
|
+
type: "boolean",
|
|
1963
|
+
description: "Whether work should pause until this is decided (default: true)",
|
|
1964
|
+
},
|
|
1965
|
+
},
|
|
1966
|
+
required: ["question", "urgency"],
|
|
1967
|
+
additionalProperties: false,
|
|
1968
|
+
},
|
|
1969
|
+
async execute(_callId, params = { question: "", urgency: "medium" }) {
|
|
1970
|
+
const requestId = `decision:${randomUUID().slice(0, 8)}`;
|
|
1971
|
+
const changesetResult = await applyChangesetWithFallback("orgx_request_decision", {
|
|
1972
|
+
initiative_id: params.initiative_id,
|
|
1973
|
+
run_id: params.run_id,
|
|
1974
|
+
correlation_id: params.correlation_id,
|
|
1975
|
+
source_client: params.source_client,
|
|
1976
|
+
idempotency_key: `decision:${requestId}`,
|
|
1977
|
+
operations: [
|
|
1978
|
+
{
|
|
1979
|
+
op: "decision.create",
|
|
1980
|
+
title: params.question,
|
|
1981
|
+
summary: params.context,
|
|
1982
|
+
urgency: params.urgency,
|
|
1983
|
+
options: params.options,
|
|
1984
|
+
blocking: params.blocking ?? true,
|
|
1985
|
+
},
|
|
1986
|
+
],
|
|
1987
|
+
});
|
|
1988
|
+
await emitActivityWithFallback("orgx_request_decision", {
|
|
1989
|
+
initiative_id: params.initiative_id,
|
|
1990
|
+
run_id: params.run_id,
|
|
1991
|
+
correlation_id: params.correlation_id,
|
|
1992
|
+
source_client: params.source_client,
|
|
1993
|
+
message: `Decision requested: ${params.question}`,
|
|
1994
|
+
phase: "review",
|
|
1995
|
+
level: "info",
|
|
1996
|
+
metadata: {
|
|
1997
|
+
urgency: params.urgency,
|
|
1998
|
+
blocking: params.blocking ?? true,
|
|
1999
|
+
options: params.options ?? [],
|
|
2000
|
+
},
|
|
2001
|
+
});
|
|
2002
|
+
return changesetResult;
|
|
2003
|
+
},
|
|
2004
|
+
}, { optional: true });
|
|
2005
|
+
// --- orgx_register_artifact ---
|
|
2006
|
+
api.registerTool({
|
|
2007
|
+
name: "orgx_register_artifact",
|
|
2008
|
+
description: "Register a work output (PR, document, config change, report, etc.) with OrgX. Makes it visible in the dashboard.",
|
|
2009
|
+
parameters: {
|
|
2010
|
+
type: "object",
|
|
2011
|
+
properties: {
|
|
2012
|
+
name: {
|
|
2013
|
+
type: "string",
|
|
2014
|
+
description: "Human-readable artifact name (e.g., 'PR #107: Fix build size')",
|
|
2015
|
+
},
|
|
2016
|
+
artifact_type: {
|
|
2017
|
+
type: "string",
|
|
2018
|
+
enum: ["pr", "commit", "document", "config", "report", "design", "other"],
|
|
2019
|
+
description: "Type of artifact",
|
|
2020
|
+
},
|
|
2021
|
+
description: {
|
|
2022
|
+
type: "string",
|
|
2023
|
+
description: "What this artifact is and why it matters",
|
|
2024
|
+
},
|
|
2025
|
+
url: {
|
|
2026
|
+
type: "string",
|
|
2027
|
+
description: "Link to the artifact (PR URL, file path, etc.)",
|
|
2028
|
+
},
|
|
2029
|
+
},
|
|
2030
|
+
required: ["name", "artifact_type"],
|
|
2031
|
+
additionalProperties: false,
|
|
2032
|
+
},
|
|
2033
|
+
async execute(_callId, params = { name: "", artifact_type: "other" }) {
|
|
2034
|
+
const now = new Date().toISOString();
|
|
2035
|
+
const id = `artifact:${randomUUID().slice(0, 8)}`;
|
|
2036
|
+
const activityItem = {
|
|
2037
|
+
id,
|
|
2038
|
+
type: "artifact_created",
|
|
2039
|
+
title: params.name,
|
|
2040
|
+
description: params.description ?? null,
|
|
2041
|
+
agentId: null,
|
|
2042
|
+
agentName: null,
|
|
2043
|
+
runId: null,
|
|
2044
|
+
initiativeId: null,
|
|
2045
|
+
timestamp: now,
|
|
2046
|
+
summary: params.url ?? null,
|
|
2047
|
+
metadata: {
|
|
2048
|
+
source: "orgx_register_artifact",
|
|
2049
|
+
artifact_type: params.artifact_type,
|
|
2050
|
+
url: params.url,
|
|
2051
|
+
},
|
|
2052
|
+
};
|
|
2053
|
+
try {
|
|
2054
|
+
const entity = await client.createEntity("artifact", {
|
|
2055
|
+
name: params.name,
|
|
2056
|
+
artifact_type: params.artifact_type,
|
|
2057
|
+
description: params.description,
|
|
2058
|
+
artifact_url: params.url,
|
|
2059
|
+
status: "active",
|
|
2060
|
+
});
|
|
2061
|
+
return json(`Artifact registered: ${params.name} [${params.artifact_type}]`, entity);
|
|
2062
|
+
}
|
|
2063
|
+
catch {
|
|
2064
|
+
await appendToOutbox("artifacts", {
|
|
2065
|
+
id,
|
|
2066
|
+
type: "artifact",
|
|
2067
|
+
timestamp: now,
|
|
2068
|
+
payload: params,
|
|
2069
|
+
activityItem,
|
|
2070
|
+
});
|
|
2071
|
+
return text(`Artifact saved locally: ${params.name} [${params.artifact_type}] (will sync when connected)`);
|
|
2072
|
+
}
|
|
2073
|
+
},
|
|
2074
|
+
}, { optional: true });
|
|
563
2075
|
// ---------------------------------------------------------------------------
|
|
564
2076
|
// 3. CLI Command
|
|
565
2077
|
// ---------------------------------------------------------------------------
|
|
@@ -599,16 +2111,78 @@ export default function register(api) {
|
|
|
599
2111
|
process.exit(1);
|
|
600
2112
|
}
|
|
601
2113
|
});
|
|
2114
|
+
cmd
|
|
2115
|
+
.command("doctor")
|
|
2116
|
+
.description("Run plugin diagnostics and connectivity checks")
|
|
2117
|
+
.option("--json", "Print the report as JSON")
|
|
2118
|
+
.option("--no-remote", "Skip remote OrgX API reachability probe")
|
|
2119
|
+
.action(async (opts = {}) => {
|
|
2120
|
+
try {
|
|
2121
|
+
const report = await buildHealthReport({
|
|
2122
|
+
probeRemote: opts.remote !== false,
|
|
2123
|
+
});
|
|
2124
|
+
if (opts.json) {
|
|
2125
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2126
|
+
if (!report.ok)
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
console.log("OrgX Doctor");
|
|
2131
|
+
console.log(` Status: ${report.status.toUpperCase()}`);
|
|
2132
|
+
console.log(` Plugin: v${report.plugin.version}`);
|
|
2133
|
+
console.log(` Base URL: ${report.plugin.baseUrl}`);
|
|
2134
|
+
console.log(` API Key Source: ${apiKeySourceLabel(report.auth.keySource)}`);
|
|
2135
|
+
console.log(` Outbox Pending: ${report.outbox.pendingTotal}`);
|
|
2136
|
+
console.log("");
|
|
2137
|
+
console.log("Checks:");
|
|
2138
|
+
for (const check of report.checks) {
|
|
2139
|
+
const prefix = check.status === "pass"
|
|
2140
|
+
? "[PASS]"
|
|
2141
|
+
: check.status === "warn"
|
|
2142
|
+
? "[WARN]"
|
|
2143
|
+
: "[FAIL]";
|
|
2144
|
+
console.log(` ${prefix} ${check.message}`);
|
|
2145
|
+
}
|
|
2146
|
+
if (report.remote.enabled) {
|
|
2147
|
+
if (report.remote.reachable === true) {
|
|
2148
|
+
console.log(` Remote probe latency: ${report.remote.latencyMs ?? "?"}ms`);
|
|
2149
|
+
}
|
|
2150
|
+
else if (report.remote.reachable === false) {
|
|
2151
|
+
console.log(` Remote probe error: ${report.remote.error ?? "Unknown error"}`);
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
console.log(" Remote probe: skipped");
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (!report.ok) {
|
|
2158
|
+
process.exit(1);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
catch (err) {
|
|
2162
|
+
console.error(`Doctor failed: ${err instanceof Error ? err.message : err}`);
|
|
2163
|
+
process.exit(1);
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
602
2166
|
}, { commands: ["orgx"] });
|
|
603
2167
|
// ---------------------------------------------------------------------------
|
|
604
2168
|
// 4. HTTP Handler — Dashboard + API proxy
|
|
605
2169
|
// ---------------------------------------------------------------------------
|
|
606
|
-
const httpHandler = createHttpHandler(config, client, () => cachedSnapshot
|
|
2170
|
+
const httpHandler = createHttpHandler(config, client, () => cachedSnapshot, {
|
|
2171
|
+
getState: () => ({ ...onboardingState }),
|
|
2172
|
+
startPairing,
|
|
2173
|
+
getStatus: getPairingStatus,
|
|
2174
|
+
submitManualKey,
|
|
2175
|
+
disconnect: disconnectOnboarding,
|
|
2176
|
+
}, {
|
|
2177
|
+
getHealth: async (input = {}) => buildHealthReport({ probeRemote: input.probeRemote === true }),
|
|
2178
|
+
});
|
|
607
2179
|
api.registerHttpHandler(httpHandler);
|
|
608
2180
|
api.log?.info?.("[orgx] Plugin registered", {
|
|
609
2181
|
baseUrl: config.baseUrl,
|
|
610
2182
|
hasApiKey: !!config.apiKey,
|
|
611
2183
|
dashboardEnabled: config.dashboardEnabled,
|
|
2184
|
+
installationId: config.installationId,
|
|
2185
|
+
pluginVersion: config.pluginVersion,
|
|
612
2186
|
});
|
|
613
2187
|
}
|
|
614
2188
|
// =============================================================================
|