@useorgx/openclaw-plugin 0.2.0 → 0.3.0

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 (50) hide show
  1. package/README.md +16 -1
  2. package/dashboard/dist/assets/index-BrAP-X_H.css +1 -0
  3. package/dashboard/dist/assets/index-cOk6qwh-.js +56 -0
  4. package/dashboard/dist/assets/orgx-logo-QSE5QWy4.png +0 -0
  5. package/dashboard/dist/brand/anthropic-mark.svg +10 -0
  6. package/dashboard/dist/brand/control-tower.png +0 -0
  7. package/dashboard/dist/brand/design-codex.png +0 -0
  8. package/dashboard/dist/brand/engineering-autopilot.png +0 -0
  9. package/dashboard/dist/brand/launch-captain.png +0 -0
  10. package/dashboard/dist/brand/openai-mark.svg +10 -0
  11. package/dashboard/dist/brand/openclaw-mark.svg +11 -0
  12. package/dashboard/dist/brand/orgx-logo.png +0 -0
  13. package/dashboard/dist/brand/pipeline-intelligence.png +0 -0
  14. package/dashboard/dist/brand/product-orchestrator.png +0 -0
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/api.d.ts +51 -1
  17. package/dist/api.d.ts.map +1 -1
  18. package/dist/api.js +105 -15
  19. package/dist/api.js.map +1 -1
  20. package/dist/auth-store.d.ts +20 -0
  21. package/dist/auth-store.d.ts.map +1 -0
  22. package/dist/auth-store.js +128 -0
  23. package/dist/auth-store.js.map +1 -0
  24. package/dist/dashboard-api.d.ts +2 -7
  25. package/dist/dashboard-api.d.ts.map +1 -1
  26. package/dist/dashboard-api.js +2 -4
  27. package/dist/dashboard-api.js.map +1 -1
  28. package/dist/http-handler.d.ts +32 -3
  29. package/dist/http-handler.d.ts.map +1 -1
  30. package/dist/http-handler.js +1849 -35
  31. package/dist/http-handler.js.map +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1453 -44
  34. package/dist/index.js.map +1 -1
  35. package/dist/local-openclaw.d.ts +87 -0
  36. package/dist/local-openclaw.d.ts.map +1 -0
  37. package/dist/local-openclaw.js +774 -0
  38. package/dist/local-openclaw.js.map +1 -0
  39. package/dist/openclaw.plugin.json +76 -0
  40. package/dist/outbox.d.ts +20 -0
  41. package/dist/outbox.d.ts.map +1 -0
  42. package/dist/outbox.js +86 -0
  43. package/dist/outbox.js.map +1 -0
  44. package/dist/types.d.ts +165 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/openclaw.plugin.json +1 -0
  47. package/package.json +4 -2
  48. package/skills/orgx/SKILL.md +180 -0
  49. package/dashboard/dist/assets/index-B_ag4FNd.css +0 -1
  50. package/dashboard/dist/assets/index-CNJpL8Wo.js +0 -40
package/dist/index.js CHANGED
@@ -15,48 +15,114 @@ 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 { appendToOutbox, readOutbox, replaceOutbox } from "./outbox.js";
18
22
  export { OrgXClient } from "./api.js";
19
- // =============================================================================
20
- // HELPERS
21
- // =============================================================================
22
- function resolveConfig(api) {
23
- const pluginConf = api.config?.plugins?.entries?.orgx?.config ?? {};
24
- let apiKey = pluginConf.apiKey || process.env.ORGX_API_KEY || "";
25
- if (!apiKey) {
26
- try {
27
- const envPath = join(homedir(), "Code", "orgx", "orgx", ".env.local");
28
- const envContent = readFileSync(envPath, "utf-8");
29
- const match = envContent.match(/^ORGX_(?:API_KEY|SERVICE_KEY)=["']?([^"'\n]+)["']?$/m);
30
- if (match)
31
- apiKey = match[1].trim();
32
- }
33
- catch {
34
- // .env.local not found
35
- }
23
+ const DEFAULT_DOCS_URL = "https://orgx.mintlify.site/guides/openclaw-plugin-setup";
24
+ function readLegacyEnvValue(keyPattern) {
25
+ try {
26
+ const envPath = join(homedir(), "Code", "orgx", "orgx", ".env.local");
27
+ const envContent = readFileSync(envPath, "utf-8");
28
+ const match = envContent.match(keyPattern);
29
+ return match?.[1]?.trim() ?? "";
36
30
  }
37
- // Resolve user ID for X-Orgx-User-Id header
38
- let userId = pluginConf.userId || process.env.ORGX_USER_ID || "";
39
- if (!userId) {
40
- try {
41
- const envPath = join(homedir(), "Code", "orgx", "orgx", ".env.local");
42
- const envContent = readFileSync(envPath, "utf-8");
43
- const match = envContent.match(/^ORGX_USER_ID=["']?([^"'\n]+)["']?$/m);
44
- if (match)
45
- userId = match[1].trim();
46
- }
47
- catch {
48
- // .env.local not found
49
- }
31
+ catch {
32
+ return "";
50
33
  }
34
+ }
35
+ function readOpenClawOrgxConfig() {
36
+ try {
37
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
38
+ const raw = readFileSync(configPath, "utf8");
39
+ const parsed = JSON.parse(raw);
40
+ const plugins = parsed.plugins && typeof parsed.plugins === "object"
41
+ ? parsed.plugins
42
+ : {};
43
+ const entries = plugins.entries && typeof plugins.entries === "object"
44
+ ? plugins.entries
45
+ : {};
46
+ const orgx = entries.orgx && typeof entries.orgx === "object"
47
+ ? entries.orgx
48
+ : {};
49
+ const config = orgx.config && typeof orgx.config === "object"
50
+ ? orgx.config
51
+ : {};
52
+ const apiKey = typeof config.apiKey === "string" ? config.apiKey.trim() : "";
53
+ const userId = typeof config.userId === "string" ? config.userId.trim() : "";
54
+ const baseUrl = typeof config.baseUrl === "string" ? config.baseUrl.trim() : "";
55
+ return { apiKey, userId, baseUrl };
56
+ }
57
+ catch {
58
+ return { apiKey: "", userId: "", baseUrl: "" };
59
+ }
60
+ }
61
+ function resolveApiKey(pluginConf, persistedApiKey) {
62
+ if (pluginConf.apiKey && pluginConf.apiKey.trim().length > 0) {
63
+ return { value: pluginConf.apiKey.trim(), source: "config" };
64
+ }
65
+ if (process.env.ORGX_API_KEY && process.env.ORGX_API_KEY.trim().length > 0) {
66
+ return { value: process.env.ORGX_API_KEY.trim(), source: "environment" };
67
+ }
68
+ if (persistedApiKey && persistedApiKey.trim().length > 0) {
69
+ return { value: persistedApiKey.trim(), source: "persisted" };
70
+ }
71
+ const openclaw = readOpenClawOrgxConfig();
72
+ if (openclaw.apiKey) {
73
+ return { value: openclaw.apiKey, source: "openclaw-config-file" };
74
+ }
75
+ const legacy = readLegacyEnvValue(/^ORGX_(?:API_KEY|SERVICE_KEY)=["']?([^"'\n]+)["']?$/m);
76
+ if (legacy) {
77
+ return { value: legacy, source: "legacy-dev" };
78
+ }
79
+ return { value: "", source: "none" };
80
+ }
81
+ function resolvePluginVersion() {
82
+ try {
83
+ const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
84
+ const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
85
+ return parsed.version && parsed.version.trim().length > 0
86
+ ? parsed.version
87
+ : "dev";
88
+ }
89
+ catch {
90
+ return "dev";
91
+ }
92
+ }
93
+ function resolveDocsUrl(baseUrl) {
94
+ const normalized = baseUrl.replace(/\/+$/, "");
95
+ if (normalized.includes("localhost") || normalized.includes("127.0.0.1")) {
96
+ return `${normalized}/docs/mintlify/guides/openclaw-plugin-setup`;
97
+ }
98
+ return DEFAULT_DOCS_URL;
99
+ }
100
+ function resolveConfig(api, input) {
101
+ const pluginConf = api.config?.plugins?.entries?.orgx?.config ?? {};
102
+ const openclaw = readOpenClawOrgxConfig();
103
+ const apiKeyResolution = resolveApiKey(pluginConf, input.persistedApiKey);
104
+ const apiKey = apiKeyResolution.value;
105
+ // Resolve user ID for X-Orgx-User-Id header
106
+ const userId = pluginConf.userId?.trim() ||
107
+ process.env.ORGX_USER_ID?.trim() ||
108
+ input.persistedUserId?.trim() ||
109
+ openclaw.userId ||
110
+ readLegacyEnvValue(/^ORGX_USER_ID=["']?([^"'\n]+)["']?$/m);
111
+ const baseUrl = pluginConf.baseUrl ||
112
+ process.env.ORGX_BASE_URL ||
113
+ openclaw.baseUrl ||
114
+ "https://www.useorgx.com";
51
115
  return {
52
116
  apiKey,
53
117
  userId,
54
- baseUrl: pluginConf.baseUrl ||
55
- process.env.ORGX_BASE_URL ||
56
- "https://www.useorgx.com",
118
+ baseUrl,
57
119
  syncIntervalMs: pluginConf.syncIntervalMs ?? 300_000,
58
120
  enabled: pluginConf.enabled ?? true,
59
121
  dashboardEnabled: pluginConf.dashboardEnabled ?? true,
122
+ installationId: input.installationId,
123
+ pluginVersion: resolvePluginVersion(),
124
+ docsUrl: resolveDocsUrl(baseUrl),
125
+ apiKeySource: apiKeyResolution.source,
60
126
  };
61
127
  }
62
128
  function text(s) {
@@ -102,6 +168,39 @@ function formatSnapshot(snap) {
102
168
  lines.push(`_Last synced: ${snap.syncedAt}_`);
103
169
  return lines.join("\n");
104
170
  }
171
+ function pickNonEmptyString(...values) {
172
+ for (const value of values) {
173
+ if (typeof value !== "string")
174
+ continue;
175
+ const trimmed = value.trim();
176
+ if (trimmed.length > 0) {
177
+ return trimmed;
178
+ }
179
+ }
180
+ return undefined;
181
+ }
182
+ function isUuid(value) {
183
+ if (!value)
184
+ return false;
185
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
186
+ }
187
+ function toReportingPhase(phase, progressPct) {
188
+ if (progressPct === 100)
189
+ return "completed";
190
+ switch (phase) {
191
+ case "researching":
192
+ return "intent";
193
+ case "implementing":
194
+ case "testing":
195
+ return "execution";
196
+ case "reviewing":
197
+ return "review";
198
+ case "blocked":
199
+ return "blocked";
200
+ default:
201
+ return "execution";
202
+ }
203
+ }
105
204
  // =============================================================================
106
205
  // PLUGIN STATE
107
206
  // =============================================================================
@@ -117,7 +216,13 @@ let lastSnapshotAt = 0;
117
216
  * @param api - The Clawdbot plugin API
118
217
  */
119
218
  export default function register(api) {
120
- const config = resolveConfig(api);
219
+ const persistedAuth = loadAuthStore();
220
+ const installationId = resolveInstallationId();
221
+ const config = resolveConfig(api, {
222
+ installationId,
223
+ persistedApiKey: persistedAuth?.apiKey ?? null,
224
+ persistedUserId: persistedAuth?.userId ?? null,
225
+ });
121
226
  if (!config.enabled) {
122
227
  api.log?.info?.("[orgx] Plugin disabled");
123
228
  return;
@@ -126,35 +231,709 @@ export default function register(api) {
126
231
  api.log?.warn?.("[orgx] No API key. Set plugins.entries.orgx.config.apiKey, ORGX_API_KEY env, or ~/Code/orgx/orgx/.env.local");
127
232
  }
128
233
  const client = new OrgXClient(config.apiKey, config.baseUrl, config.userId);
234
+ let onboardingState = {
235
+ status: config.apiKey ? "connected" : "idle",
236
+ hasApiKey: Boolean(config.apiKey),
237
+ connectionVerified: Boolean(config.apiKey),
238
+ workspaceName: persistedAuth?.workspaceName ?? null,
239
+ lastError: null,
240
+ nextAction: config.apiKey ? "open_dashboard" : "connect",
241
+ docsUrl: config.docsUrl,
242
+ keySource: config.apiKeySource,
243
+ installationId: config.installationId,
244
+ connectUrl: null,
245
+ pairingId: null,
246
+ expiresAt: null,
247
+ pollIntervalMs: null,
248
+ };
249
+ let activePairing = null;
250
+ const baseApiUrl = config.baseUrl.replace(/\/+$/, "");
251
+ const defaultReportingCorrelationId = pickNonEmptyString(process.env.ORGX_CORRELATION_ID) ??
252
+ `openclaw-${config.installationId}`;
253
+ function resolveReportingContext(input) {
254
+ const initiativeId = pickNonEmptyString(input.initiative_id, process.env.ORGX_INITIATIVE_ID);
255
+ if (!initiativeId || !isUuid(initiativeId)) {
256
+ return {
257
+ ok: false,
258
+ error: "initiative_id is required (set ORGX_INITIATIVE_ID or pass initiative_id).",
259
+ };
260
+ }
261
+ const sourceCandidate = pickNonEmptyString(input.source_client, process.env.ORGX_SOURCE_CLIENT, "openclaw");
262
+ const sourceClient = sourceCandidate === "codex" ||
263
+ sourceCandidate === "claude-code" ||
264
+ sourceCandidate === "api" ||
265
+ sourceCandidate === "openclaw"
266
+ ? sourceCandidate
267
+ : "openclaw";
268
+ const runIdCandidate = pickNonEmptyString(input.run_id, process.env.ORGX_RUN_ID);
269
+ const runId = isUuid(runIdCandidate) ? runIdCandidate : undefined;
270
+ const correlationId = runId
271
+ ? undefined
272
+ : pickNonEmptyString(input.correlation_id, defaultReportingCorrelationId, `openclaw-${Date.now()}`);
273
+ return {
274
+ ok: true,
275
+ value: {
276
+ initiativeId,
277
+ runId,
278
+ correlationId,
279
+ sourceClient,
280
+ },
281
+ };
282
+ }
283
+ function updateOnboardingState(updates) {
284
+ onboardingState = {
285
+ ...onboardingState,
286
+ ...updates,
287
+ };
288
+ return onboardingState;
289
+ }
290
+ function toErrorMessage(err) {
291
+ if (err instanceof Error)
292
+ return err.message;
293
+ return typeof err === "string" ? err : "Unexpected error";
294
+ }
295
+ function clearPairingState() {
296
+ activePairing = null;
297
+ updateOnboardingState({
298
+ connectUrl: null,
299
+ pairingId: null,
300
+ expiresAt: null,
301
+ pollIntervalMs: null,
302
+ });
303
+ }
304
+ function isAuthRequiredError(result) {
305
+ if (result.status !== 401) {
306
+ return false;
307
+ }
308
+ return /auth|unauthorized|token/i.test(result.error);
309
+ }
310
+ function buildManualKeyConnectUrl() {
311
+ try {
312
+ return new URL("/settings", baseApiUrl).toString();
313
+ }
314
+ catch {
315
+ return "https://www.useorgx.com/settings";
316
+ }
317
+ }
318
+ async function fetchOrgxJson(method, path, body) {
319
+ try {
320
+ const response = await fetch(`${baseApiUrl}${path}`, {
321
+ method,
322
+ headers: {
323
+ "Content-Type": "application/json",
324
+ },
325
+ body: body ? JSON.stringify(body) : undefined,
326
+ });
327
+ const payload = (await response.json().catch(() => null));
328
+ if (!response.ok) {
329
+ const rawError = payload?.error ?? payload?.message;
330
+ let errorMessage;
331
+ if (typeof rawError === "string") {
332
+ errorMessage = rawError;
333
+ }
334
+ else if (rawError &&
335
+ typeof rawError === "object" &&
336
+ "message" in rawError &&
337
+ typeof rawError.message === "string") {
338
+ errorMessage = rawError.message;
339
+ }
340
+ else {
341
+ errorMessage = `OrgX request failed (${response.status})`;
342
+ }
343
+ return { ok: false, status: response.status, error: errorMessage };
344
+ }
345
+ if (payload?.data !== undefined) {
346
+ return { ok: true, data: payload.data };
347
+ }
348
+ return { ok: true, data: payload };
349
+ }
350
+ catch (err) {
351
+ return { ok: false, status: 0, error: toErrorMessage(err) };
352
+ }
353
+ }
354
+ function setRuntimeApiKey(input) {
355
+ const nextApiKey = input.apiKey.trim();
356
+ config.apiKey = nextApiKey;
357
+ config.apiKeySource = "persisted";
358
+ if (typeof input.userId === "string" && input.userId.trim().length > 0) {
359
+ config.userId = input.userId.trim();
360
+ }
361
+ client.setCredentials({
362
+ apiKey: config.apiKey,
363
+ userId: config.userId,
364
+ baseUrl: config.baseUrl,
365
+ });
366
+ saveAuthStore({
367
+ installationId: config.installationId,
368
+ apiKey: nextApiKey,
369
+ userId: config.userId || null,
370
+ workspaceName: input.workspaceName ?? null,
371
+ keyPrefix: input.keyPrefix ?? null,
372
+ source: input.source,
373
+ });
374
+ updateOnboardingState({
375
+ hasApiKey: true,
376
+ keySource: "persisted",
377
+ installationId: config.installationId,
378
+ workspaceName: input.workspaceName ?? onboardingState.workspaceName,
379
+ });
380
+ }
129
381
  // ---------------------------------------------------------------------------
130
382
  // 1. Background Sync Service
131
383
  // ---------------------------------------------------------------------------
132
384
  let syncTimer = null;
385
+ let syncInFlight = null;
386
+ let syncServiceRunning = false;
387
+ const outboxQueues = ["progress", "decisions", "artifacts"];
388
+ function pickStringField(payload, key) {
389
+ const value = payload[key];
390
+ return typeof value === "string" && value.trim().length > 0
391
+ ? value.trim()
392
+ : null;
393
+ }
394
+ function pickStringArrayField(payload, key) {
395
+ const value = payload[key];
396
+ if (!Array.isArray(value))
397
+ return undefined;
398
+ const strings = value
399
+ .filter((item) => typeof item === "string")
400
+ .map((item) => item.trim())
401
+ .filter(Boolean);
402
+ return strings.length > 0 ? strings : undefined;
403
+ }
404
+ async function replayOutboxEvent(event) {
405
+ const payload = event.payload ?? {};
406
+ if (event.type === "progress") {
407
+ const summary = pickStringField(payload, "summary");
408
+ if (!summary) {
409
+ api.log?.warn?.("[orgx] Dropping invalid progress outbox event", {
410
+ eventId: event.id,
411
+ });
412
+ return;
413
+ }
414
+ const context = resolveReportingContext(payload);
415
+ if (!context.ok) {
416
+ throw new Error(context.error);
417
+ }
418
+ const rawPhase = pickStringField(payload, "phase") ?? "implementing";
419
+ const progressPct = typeof payload.progress_pct === "number" ? payload.progress_pct : undefined;
420
+ const phase = rawPhase === "intent" ||
421
+ rawPhase === "execution" ||
422
+ rawPhase === "blocked" ||
423
+ rawPhase === "review" ||
424
+ rawPhase === "handoff" ||
425
+ rawPhase === "completed"
426
+ ? rawPhase
427
+ : toReportingPhase(rawPhase, progressPct);
428
+ await client.emitActivity({
429
+ initiative_id: context.value.initiativeId,
430
+ run_id: context.value.runId,
431
+ correlation_id: context.value.correlationId,
432
+ source_client: context.value.sourceClient,
433
+ message: summary,
434
+ phase,
435
+ progress_pct: progressPct,
436
+ level: pickStringField(payload, "level"),
437
+ next_step: pickStringField(payload, "next_step") ?? undefined,
438
+ metadata: {
439
+ source: "orgx_openclaw_outbox_replay",
440
+ outbox_event_id: event.id,
441
+ },
442
+ });
443
+ return;
444
+ }
445
+ if (event.type === "decision") {
446
+ const question = pickStringField(payload, "question");
447
+ if (!question) {
448
+ api.log?.warn?.("[orgx] Dropping invalid decision outbox event", {
449
+ eventId: event.id,
450
+ });
451
+ return;
452
+ }
453
+ const context = resolveReportingContext(payload);
454
+ if (!context.ok) {
455
+ throw new Error(context.error);
456
+ }
457
+ await client.applyChangeset({
458
+ initiative_id: context.value.initiativeId,
459
+ run_id: context.value.runId,
460
+ correlation_id: context.value.correlationId,
461
+ source_client: context.value.sourceClient,
462
+ idempotency_key: pickStringField(payload, "idempotency_key") ?? `decision:${event.id}`,
463
+ operations: [
464
+ {
465
+ op: "decision.create",
466
+ title: question,
467
+ summary: pickStringField(payload, "context") ?? undefined,
468
+ urgency: pickStringField(payload, "urgency") ?? "medium",
469
+ options: pickStringArrayField(payload, "options"),
470
+ blocking: typeof payload.blocking === "boolean" ? payload.blocking : true,
471
+ },
472
+ ],
473
+ });
474
+ return;
475
+ }
476
+ if (event.type === "changeset") {
477
+ const context = resolveReportingContext(payload);
478
+ if (!context.ok) {
479
+ throw new Error(context.error);
480
+ }
481
+ const operations = Array.isArray(payload.operations)
482
+ ? payload.operations
483
+ : [];
484
+ if (operations.length === 0) {
485
+ api.log?.warn?.("[orgx] Dropping invalid changeset outbox event", {
486
+ eventId: event.id,
487
+ });
488
+ return;
489
+ }
490
+ await client.applyChangeset({
491
+ initiative_id: context.value.initiativeId,
492
+ run_id: context.value.runId,
493
+ correlation_id: context.value.correlationId,
494
+ source_client: context.value.sourceClient,
495
+ idempotency_key: pickStringField(payload, "idempotency_key") ?? `changeset:${event.id}`,
496
+ operations,
497
+ });
498
+ return;
499
+ }
500
+ if (event.type === "artifact") {
501
+ const name = pickStringField(payload, "name");
502
+ if (!name) {
503
+ api.log?.warn?.("[orgx] Dropping invalid artifact outbox event", {
504
+ eventId: event.id,
505
+ });
506
+ return;
507
+ }
508
+ await client.createEntity("artifact", {
509
+ name,
510
+ artifact_type: pickStringField(payload, "artifact_type") ?? "other",
511
+ description: pickStringField(payload, "description"),
512
+ artifact_url: pickStringField(payload, "url"),
513
+ status: "active",
514
+ });
515
+ return;
516
+ }
517
+ }
518
+ async function flushOutboxQueues() {
519
+ for (const queue of outboxQueues) {
520
+ const pending = await readOutbox(queue);
521
+ if (pending.length === 0) {
522
+ continue;
523
+ }
524
+ const remaining = [];
525
+ for (const event of pending) {
526
+ try {
527
+ await replayOutboxEvent(event);
528
+ }
529
+ catch (err) {
530
+ remaining.push(event);
531
+ api.log?.warn?.("[orgx] Outbox replay failed", {
532
+ queue,
533
+ eventId: event.id,
534
+ error: toErrorMessage(err),
535
+ });
536
+ }
537
+ }
538
+ await replaceOutbox(queue, remaining);
539
+ const replayedCount = pending.length - remaining.length;
540
+ if (replayedCount > 0) {
541
+ api.log?.info?.("[orgx] Replayed buffered outbox events", {
542
+ queue,
543
+ replayed: replayedCount,
544
+ remaining: remaining.length,
545
+ });
546
+ }
547
+ }
548
+ }
133
549
  async function doSync() {
550
+ if (syncInFlight) {
551
+ return syncInFlight;
552
+ }
553
+ syncInFlight = (async () => {
554
+ if (!config.apiKey) {
555
+ updateOnboardingState({
556
+ status: "idle",
557
+ hasApiKey: false,
558
+ connectionVerified: false,
559
+ nextAction: "connect",
560
+ });
561
+ return;
562
+ }
563
+ try {
564
+ cachedSnapshot = await client.getOrgSnapshot();
565
+ lastSnapshotAt = Date.now();
566
+ updateOnboardingState({
567
+ status: "connected",
568
+ hasApiKey: true,
569
+ connectionVerified: true,
570
+ lastError: null,
571
+ nextAction: "open_dashboard",
572
+ });
573
+ await flushOutboxQueues();
574
+ api.log?.debug?.("[orgx] Sync OK");
575
+ }
576
+ catch (err) {
577
+ updateOnboardingState({
578
+ status: "error",
579
+ hasApiKey: true,
580
+ connectionVerified: false,
581
+ lastError: toErrorMessage(err),
582
+ nextAction: "reconnect",
583
+ });
584
+ api.log?.warn?.(`[orgx] Sync failed: ${err instanceof Error ? err.message : err}`);
585
+ }
586
+ })();
134
587
  try {
135
- cachedSnapshot = await client.getOrgSnapshot();
136
- lastSnapshotAt = Date.now();
137
- api.log?.debug?.("[orgx] Sync OK");
588
+ await syncInFlight;
138
589
  }
139
- catch (err) {
140
- api.log?.warn?.(`[orgx] Sync failed: ${err instanceof Error ? err.message : err}`);
590
+ finally {
591
+ syncInFlight = null;
141
592
  }
142
593
  }
594
+ function scheduleNextSync() {
595
+ if (!syncServiceRunning) {
596
+ return;
597
+ }
598
+ syncTimer = setTimeout(async () => {
599
+ await doSync();
600
+ scheduleNextSync();
601
+ }, config.syncIntervalMs);
602
+ }
603
+ async function startPairing(input) {
604
+ updateOnboardingState({
605
+ status: "starting",
606
+ lastError: null,
607
+ nextAction: "connect",
608
+ });
609
+ const started = await fetchOrgxJson("POST", "/api/plugin/openclaw/pairings", {
610
+ installationId: config.installationId,
611
+ pluginVersion: config.pluginVersion,
612
+ openclawVersion: input.openclawVersion,
613
+ platform: input.platform || process.platform,
614
+ deviceName: input.deviceName,
615
+ });
616
+ if (!started.ok) {
617
+ if (isAuthRequiredError(started)) {
618
+ clearPairingState();
619
+ const manualConnectUrl = buildManualKeyConnectUrl();
620
+ const state = updateOnboardingState({
621
+ status: "manual_key",
622
+ hasApiKey: Boolean(config.apiKey),
623
+ connectionVerified: false,
624
+ lastError: null,
625
+ nextAction: "enter_manual_key",
626
+ connectUrl: manualConnectUrl,
627
+ pairingId: null,
628
+ expiresAt: null,
629
+ pollIntervalMs: null,
630
+ });
631
+ return {
632
+ pairingId: "manual_key",
633
+ connectUrl: manualConnectUrl,
634
+ expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
635
+ pollIntervalMs: 1_500,
636
+ state,
637
+ };
638
+ }
639
+ const message = `Pairing start failed: ${started.error}`;
640
+ updateOnboardingState({
641
+ status: "error",
642
+ hasApiKey: Boolean(config.apiKey),
643
+ connectionVerified: false,
644
+ lastError: message,
645
+ nextAction: "enter_manual_key",
646
+ });
647
+ throw new Error(message);
648
+ }
649
+ activePairing = {
650
+ pairingId: started.data.pairingId,
651
+ pollToken: started.data.pollToken,
652
+ connectUrl: started.data.connectUrl,
653
+ expiresAt: started.data.expiresAt,
654
+ pollIntervalMs: started.data.pollIntervalMs,
655
+ };
656
+ const state = updateOnboardingState({
657
+ status: "awaiting_browser_auth",
658
+ hasApiKey: false,
659
+ connectionVerified: false,
660
+ lastError: null,
661
+ nextAction: "wait_for_browser",
662
+ connectUrl: started.data.connectUrl,
663
+ pairingId: started.data.pairingId,
664
+ expiresAt: started.data.expiresAt,
665
+ pollIntervalMs: started.data.pollIntervalMs,
666
+ });
667
+ return {
668
+ pairingId: started.data.pairingId,
669
+ connectUrl: started.data.connectUrl,
670
+ expiresAt: started.data.expiresAt,
671
+ pollIntervalMs: started.data.pollIntervalMs,
672
+ state,
673
+ };
674
+ }
675
+ async function getPairingStatus() {
676
+ if (!activePairing) {
677
+ return { ...onboardingState };
678
+ }
679
+ const polled = await fetchOrgxJson("GET", `/api/plugin/openclaw/pairings/${encodeURIComponent(activePairing.pairingId)}?pollToken=${encodeURIComponent(activePairing.pollToken)}`);
680
+ if (!polled.ok) {
681
+ return updateOnboardingState({
682
+ status: "error",
683
+ hasApiKey: Boolean(config.apiKey),
684
+ connectionVerified: false,
685
+ lastError: polled.error,
686
+ nextAction: "enter_manual_key",
687
+ });
688
+ }
689
+ const status = polled.data.status;
690
+ if (status === "pending" || status === "authorized") {
691
+ return updateOnboardingState({
692
+ status: "pairing",
693
+ hasApiKey: false,
694
+ connectionVerified: false,
695
+ lastError: null,
696
+ nextAction: "wait_for_browser",
697
+ });
698
+ }
699
+ if (status === "ready") {
700
+ const key = typeof polled.data.key === "string" ? polled.data.key : "";
701
+ if (!key) {
702
+ clearPairingState();
703
+ return updateOnboardingState({
704
+ status: "error",
705
+ hasApiKey: false,
706
+ connectionVerified: false,
707
+ lastError: "Pairing completed without an API key payload.",
708
+ nextAction: "retry",
709
+ });
710
+ }
711
+ setRuntimeApiKey({
712
+ apiKey: key,
713
+ source: "browser_pairing",
714
+ workspaceName: polled.data.workspaceName ?? null,
715
+ keyPrefix: polled.data.keyPrefix ?? null,
716
+ });
717
+ await fetchOrgxJson("POST", `/api/plugin/openclaw/pairings/${encodeURIComponent(activePairing.pairingId)}/ack`, {
718
+ pollToken: activePairing.pollToken,
719
+ });
720
+ clearPairingState();
721
+ updateOnboardingState({
722
+ status: "connected",
723
+ hasApiKey: true,
724
+ connectionVerified: false,
725
+ workspaceName: polled.data.workspaceName ?? null,
726
+ nextAction: "open_dashboard",
727
+ lastError: null,
728
+ });
729
+ await doSync();
730
+ return { ...onboardingState };
731
+ }
732
+ if (status === "consumed") {
733
+ clearPairingState();
734
+ return updateOnboardingState({
735
+ status: config.apiKey ? "connected" : "error",
736
+ hasApiKey: Boolean(config.apiKey),
737
+ connectionVerified: false,
738
+ lastError: config.apiKey ? null : "Pairing consumed but key is unavailable.",
739
+ nextAction: config.apiKey ? "open_dashboard" : "retry",
740
+ });
741
+ }
742
+ clearPairingState();
743
+ return updateOnboardingState({
744
+ status: status === "cancelled" ? "manual_key" : "error",
745
+ hasApiKey: Boolean(config.apiKey),
746
+ connectionVerified: false,
747
+ lastError: polled.data.errorMessage ?? "Pairing failed or expired.",
748
+ nextAction: "retry",
749
+ });
750
+ }
751
+ async function submitManualKey(input) {
752
+ const nextKey = input.apiKey.trim();
753
+ if (!nextKey) {
754
+ throw new Error("apiKey is required");
755
+ }
756
+ updateOnboardingState({
757
+ status: "manual_key",
758
+ hasApiKey: false,
759
+ connectionVerified: false,
760
+ lastError: null,
761
+ nextAction: "enter_manual_key",
762
+ });
763
+ const probeClient = new OrgXClient(nextKey, config.baseUrl, input.userId?.trim() || config.userId);
764
+ const snapshot = await probeClient.getOrgSnapshot();
765
+ setRuntimeApiKey({
766
+ apiKey: nextKey,
767
+ source: "manual",
768
+ userId: input.userId?.trim() || null,
769
+ workspaceName: onboardingState.workspaceName,
770
+ keyPrefix: null,
771
+ });
772
+ cachedSnapshot = snapshot;
773
+ lastSnapshotAt = Date.now();
774
+ return updateOnboardingState({
775
+ status: "connected",
776
+ hasApiKey: true,
777
+ connectionVerified: true,
778
+ lastError: null,
779
+ nextAction: "open_dashboard",
780
+ });
781
+ }
782
+ async function disconnectOnboarding() {
783
+ if (activePairing) {
784
+ await fetchOrgxJson("POST", `/api/plugin/openclaw/pairings/${encodeURIComponent(activePairing.pairingId)}/cancel`, {
785
+ pollToken: activePairing.pollToken,
786
+ reason: "disconnect",
787
+ });
788
+ }
789
+ clearPairingState();
790
+ clearPersistedApiKey();
791
+ config.apiKey = "";
792
+ client.setCredentials({ apiKey: "" });
793
+ cachedSnapshot = null;
794
+ lastSnapshotAt = 0;
795
+ return updateOnboardingState({
796
+ status: "idle",
797
+ hasApiKey: false,
798
+ connectionVerified: false,
799
+ workspaceName: null,
800
+ lastError: null,
801
+ nextAction: "connect",
802
+ keySource: "none",
803
+ });
804
+ }
143
805
  api.registerService({
144
806
  id: "orgx-sync",
145
807
  start: async () => {
808
+ syncServiceRunning = true;
146
809
  api.log?.info?.("[orgx] Starting sync service", {
147
810
  interval: config.syncIntervalMs,
148
811
  });
149
812
  await doSync();
150
- syncTimer = setInterval(doSync, config.syncIntervalMs);
813
+ scheduleNextSync();
151
814
  },
152
815
  stop: async () => {
816
+ syncServiceRunning = false;
153
817
  if (syncTimer)
154
- clearInterval(syncTimer);
818
+ clearTimeout(syncTimer);
155
819
  syncTimer = null;
156
820
  },
157
821
  });
822
+ async function autoAssignEntityForCreate(input) {
823
+ const warnings = [];
824
+ const byKey = new Map();
825
+ const addAgent = (agent) => {
826
+ const key = `${agent.id}:${agent.name}`.toLowerCase();
827
+ if (!byKey.has(key))
828
+ byKey.set(key, agent);
829
+ };
830
+ let liveAgents = [];
831
+ try {
832
+ const agentResp = await client.getLiveAgents({
833
+ initiative: input.initiativeId,
834
+ includeIdle: true,
835
+ });
836
+ liveAgents = (Array.isArray(agentResp.agents) ? agentResp.agents : [])
837
+ .map((raw) => {
838
+ if (!raw || typeof raw !== "object")
839
+ return null;
840
+ const record = raw;
841
+ const id = (typeof record.id === "string" && record.id.trim()) ||
842
+ (typeof record.agentId === "string" && record.agentId.trim()) ||
843
+ "";
844
+ const name = (typeof record.name === "string" && record.name.trim()) ||
845
+ (typeof record.agentName === "string" && record.agentName.trim()) ||
846
+ id;
847
+ if (!name)
848
+ return null;
849
+ return {
850
+ id: id || `name:${name}`,
851
+ name,
852
+ domain: (typeof record.domain === "string" && record.domain.trim()) ||
853
+ (typeof record.role === "string" && record.role.trim()) ||
854
+ null,
855
+ status: (typeof record.status === "string" && record.status.trim()) || null,
856
+ };
857
+ })
858
+ .filter((item) => item !== null);
859
+ }
860
+ catch (err) {
861
+ warnings.push(`live agents unavailable (${toErrorMessage(err)})`);
862
+ }
863
+ const orchestrator = liveAgents.find((agent) => /holt|orchestrator/i.test(agent.name) ||
864
+ /orchestrator/i.test(agent.domain ?? ""));
865
+ if (orchestrator)
866
+ addAgent(orchestrator);
867
+ let assignmentSource = "fallback";
868
+ try {
869
+ const preflight = await client.delegationPreflight({
870
+ intent: `${input.title}${input.summary ? `: ${input.summary}` : ""}`,
871
+ });
872
+ const recommendations = preflight.data?.recommended_split ?? [];
873
+ const recommendedDomains = [
874
+ ...new Set(recommendations
875
+ .map((entry) => String(entry.owner_domain ?? "").trim().toLowerCase())
876
+ .filter(Boolean)),
877
+ ];
878
+ for (const domain of recommendedDomains) {
879
+ const match = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
880
+ if (match)
881
+ addAgent(match);
882
+ }
883
+ if (recommendedDomains.length > 0) {
884
+ assignmentSource = "orchestrator";
885
+ }
886
+ }
887
+ catch (err) {
888
+ warnings.push(`delegation preflight failed (${toErrorMessage(err)})`);
889
+ }
890
+ if (byKey.size === 0) {
891
+ const haystack = `${input.title} ${input.summary ?? ""}`.toLowerCase();
892
+ const domainHints = [];
893
+ if (/market|campaign|thread|article|tweet|copy/.test(haystack)) {
894
+ domainHints.push("marketing");
895
+ }
896
+ else if (/design|ux|ui|a11y/.test(haystack)) {
897
+ domainHints.push("design");
898
+ }
899
+ else if (/ops|runbook|incident|reliability/.test(haystack)) {
900
+ domainHints.push("operations");
901
+ }
902
+ else if (/sales|deal|pipeline/.test(haystack)) {
903
+ domainHints.push("sales");
904
+ }
905
+ else {
906
+ domainHints.push("engineering", "product");
907
+ }
908
+ for (const domain of domainHints) {
909
+ const match = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
910
+ if (match)
911
+ addAgent(match);
912
+ }
913
+ }
914
+ if (byKey.size === 0 && liveAgents.length > 0) {
915
+ addAgent(liveAgents[0]);
916
+ warnings.push("fallback selected first available live agent");
917
+ }
918
+ const assignedAgents = Array.from(byKey.values());
919
+ let updatedEntity = null;
920
+ try {
921
+ updatedEntity = (await client.updateEntity(input.entityType, input.entityId, {
922
+ assigned_agent_ids: assignedAgents.map((agent) => agent.id),
923
+ assigned_agent_names: assignedAgents.map((agent) => agent.name),
924
+ assignment_source: assignmentSource,
925
+ }));
926
+ }
927
+ catch (err) {
928
+ warnings.push(`assignment update failed (${toErrorMessage(err)})`);
929
+ }
930
+ return {
931
+ assignmentSource,
932
+ assignedAgents,
933
+ warnings,
934
+ updatedEntity,
935
+ };
936
+ }
158
937
  // ---------------------------------------------------------------------------
159
938
  // 2. MCP Tools (Model Context Protocol compatible)
160
939
  // ---------------------------------------------------------------------------
@@ -208,6 +987,165 @@ export default function register(api) {
208
987
  }
209
988
  },
210
989
  }, { optional: true });
990
+ // --- orgx_delegation_preflight ---
991
+ api.registerTool({
992
+ name: "orgx_delegation_preflight",
993
+ description: "Run delegation preflight to score scope quality, estimate ETA/cost, and suggest a split before autonomous execution.",
994
+ parameters: {
995
+ type: "object",
996
+ properties: {
997
+ intent: {
998
+ type: "string",
999
+ description: "Task intent in natural language",
1000
+ },
1001
+ acceptanceCriteria: {
1002
+ type: "array",
1003
+ items: { type: "string" },
1004
+ description: "Optional acceptance criteria to reduce ambiguity",
1005
+ },
1006
+ constraints: {
1007
+ type: "array",
1008
+ items: { type: "string" },
1009
+ description: "Optional constraints (deadline, stack, policy)",
1010
+ },
1011
+ domains: {
1012
+ type: "array",
1013
+ items: { type: "string" },
1014
+ description: "Optional preferred owner domains",
1015
+ },
1016
+ },
1017
+ required: ["intent"],
1018
+ additionalProperties: false,
1019
+ },
1020
+ async execute(_callId, params = { intent: "" }) {
1021
+ try {
1022
+ const result = await client.delegationPreflight({
1023
+ intent: params.intent,
1024
+ acceptanceCriteria: Array.isArray(params.acceptanceCriteria)
1025
+ ? params.acceptanceCriteria.filter((item) => typeof item === "string")
1026
+ : undefined,
1027
+ constraints: Array.isArray(params.constraints)
1028
+ ? params.constraints.filter((item) => typeof item === "string")
1029
+ : undefined,
1030
+ domains: Array.isArray(params.domains)
1031
+ ? params.domains.filter((item) => typeof item === "string")
1032
+ : undefined,
1033
+ });
1034
+ return json("Delegation preflight:", result.data ?? result);
1035
+ }
1036
+ catch (err) {
1037
+ return text(`❌ Delegation preflight failed: ${err instanceof Error ? err.message : err}`);
1038
+ }
1039
+ },
1040
+ }, { optional: true });
1041
+ // --- orgx_run_action ---
1042
+ api.registerTool({
1043
+ name: "orgx_run_action",
1044
+ description: "Apply a control action to a run: pause, resume, cancel, or rollback (rollback requires checkpointId).",
1045
+ parameters: {
1046
+ type: "object",
1047
+ properties: {
1048
+ runId: {
1049
+ type: "string",
1050
+ description: "Run UUID",
1051
+ },
1052
+ action: {
1053
+ type: "string",
1054
+ enum: ["pause", "resume", "cancel", "rollback"],
1055
+ description: "Control action",
1056
+ },
1057
+ checkpointId: {
1058
+ type: "string",
1059
+ description: "Checkpoint UUID (required for rollback)",
1060
+ },
1061
+ reason: {
1062
+ type: "string",
1063
+ description: "Optional reason for audit trail",
1064
+ },
1065
+ },
1066
+ required: ["runId", "action"],
1067
+ additionalProperties: false,
1068
+ },
1069
+ async execute(_callId, params = { runId: "", action: "pause" }) {
1070
+ try {
1071
+ if (params.action === "rollback" && !params.checkpointId) {
1072
+ return text("❌ rollback requires checkpointId");
1073
+ }
1074
+ const result = await client.runAction(params.runId, params.action, {
1075
+ checkpointId: params.checkpointId,
1076
+ reason: params.reason,
1077
+ });
1078
+ return json("Run action applied:", result.data ?? result);
1079
+ }
1080
+ catch (err) {
1081
+ return text(`❌ Run action failed: ${err instanceof Error ? err.message : err}`);
1082
+ }
1083
+ },
1084
+ }, { optional: true });
1085
+ // --- orgx_checkpoints_list ---
1086
+ api.registerTool({
1087
+ name: "orgx_checkpoints_list",
1088
+ description: "List checkpoints for a run.",
1089
+ parameters: {
1090
+ type: "object",
1091
+ properties: {
1092
+ runId: {
1093
+ type: "string",
1094
+ description: "Run UUID",
1095
+ },
1096
+ },
1097
+ required: ["runId"],
1098
+ additionalProperties: false,
1099
+ },
1100
+ async execute(_callId, params = { runId: "" }) {
1101
+ try {
1102
+ const result = await client.listRunCheckpoints(params.runId);
1103
+ return json("Run checkpoints:", result.data ?? result);
1104
+ }
1105
+ catch (err) {
1106
+ return text(`❌ Failed to list checkpoints: ${err instanceof Error ? err.message : err}`);
1107
+ }
1108
+ },
1109
+ }, { optional: true });
1110
+ // --- orgx_checkpoint_restore ---
1111
+ api.registerTool({
1112
+ name: "orgx_checkpoint_restore",
1113
+ description: "Restore a run to a specific checkpoint.",
1114
+ parameters: {
1115
+ type: "object",
1116
+ properties: {
1117
+ runId: {
1118
+ type: "string",
1119
+ description: "Run UUID",
1120
+ },
1121
+ checkpointId: {
1122
+ type: "string",
1123
+ description: "Checkpoint UUID",
1124
+ },
1125
+ reason: {
1126
+ type: "string",
1127
+ description: "Optional restoration reason",
1128
+ },
1129
+ },
1130
+ required: ["runId", "checkpointId"],
1131
+ additionalProperties: false,
1132
+ },
1133
+ async execute(_callId, params = {
1134
+ runId: "",
1135
+ checkpointId: "",
1136
+ }) {
1137
+ try {
1138
+ const result = await client.restoreRunCheckpoint(params.runId, {
1139
+ checkpointId: params.checkpointId,
1140
+ reason: params.reason,
1141
+ });
1142
+ return json("Checkpoint restored:", result.data ?? result);
1143
+ }
1144
+ catch (err) {
1145
+ return text(`❌ Checkpoint restore failed: ${err instanceof Error ? err.message : err}`);
1146
+ }
1147
+ },
1148
+ }, { optional: true });
211
1149
  // --- orgx_spawn_check ---
212
1150
  api.registerTool({
213
1151
  name: "orgx_spawn_check",
@@ -316,8 +1254,44 @@ export default function register(api) {
316
1254
  async execute(_callId, params = {}) {
317
1255
  try {
318
1256
  const { type, ...data } = params;
319
- const entity = await client.createEntity(type, data);
320
- return json(`✅ Created ${type}: ${entity.title ?? entity.id}`, entity);
1257
+ let entity = await client.createEntity(type, data);
1258
+ let assignmentSummary = null;
1259
+ const entityType = String(type ?? "");
1260
+ if (entityType === "initiative" || entityType === "workstream") {
1261
+ const entityRecord = entity;
1262
+ const assignment = await autoAssignEntityForCreate({
1263
+ entityType,
1264
+ entityId: String(entityRecord.id ?? ""),
1265
+ initiativeId: entityType === "initiative"
1266
+ ? String(entityRecord.id ?? "")
1267
+ : (typeof data.initiative_id === "string"
1268
+ ? data.initiative_id
1269
+ : null),
1270
+ title: (typeof entityRecord.title === "string" && entityRecord.title) ||
1271
+ (typeof entityRecord.name === "string" && entityRecord.name) ||
1272
+ (typeof data.title === "string" && data.title) ||
1273
+ "Untitled",
1274
+ summary: (typeof entityRecord.summary === "string" && entityRecord.summary) ||
1275
+ (typeof data.summary === "string" && data.summary) ||
1276
+ null,
1277
+ });
1278
+ if (assignment.updatedEntity) {
1279
+ entity = assignment.updatedEntity;
1280
+ }
1281
+ assignmentSummary = {
1282
+ assignment_source: assignment.assignmentSource,
1283
+ assigned_agents: assignment.assignedAgents,
1284
+ warnings: assignment.warnings,
1285
+ };
1286
+ }
1287
+ return json(`✅ Created ${type}: ${entity.title ?? entity.id}`, {
1288
+ entity,
1289
+ ...(assignmentSummary
1290
+ ? {
1291
+ auto_assignment: assignmentSummary,
1292
+ }
1293
+ : {}),
1294
+ });
321
1295
  }
322
1296
  catch (err) {
323
1297
  return text(`❌ Creation failed: ${err instanceof Error ? err.message : err}`);
@@ -401,6 +1375,433 @@ export default function register(api) {
401
1375
  }
402
1376
  },
403
1377
  }, { optional: true });
1378
+ async function emitActivityWithFallback(source, payload) {
1379
+ if (!payload.message || payload.message.trim().length === 0) {
1380
+ return text("❌ message is required");
1381
+ }
1382
+ const context = resolveReportingContext(payload);
1383
+ if (!context.ok) {
1384
+ return text(`❌ ${context.error}`);
1385
+ }
1386
+ const now = new Date().toISOString();
1387
+ const id = `progress:${randomUUID().slice(0, 8)}`;
1388
+ const normalizedPayload = {
1389
+ initiative_id: context.value.initiativeId,
1390
+ run_id: context.value.runId,
1391
+ correlation_id: context.value.correlationId,
1392
+ source_client: context.value.sourceClient,
1393
+ message: payload.message,
1394
+ phase: payload.phase ?? "execution",
1395
+ progress_pct: payload.progress_pct,
1396
+ level: payload.level ?? "info",
1397
+ next_step: payload.next_step,
1398
+ metadata: {
1399
+ ...(payload.metadata ?? {}),
1400
+ source,
1401
+ },
1402
+ };
1403
+ const activityItem = {
1404
+ id,
1405
+ type: "delegation",
1406
+ title: payload.message,
1407
+ description: payload.next_step ?? null,
1408
+ agentId: null,
1409
+ agentName: null,
1410
+ runId: context.value.runId ?? null,
1411
+ initiativeId: context.value.initiativeId,
1412
+ timestamp: now,
1413
+ phase: normalizedPayload.phase,
1414
+ summary: payload.next_step ? `Next: ${payload.next_step}` : payload.message,
1415
+ metadata: normalizedPayload.metadata,
1416
+ };
1417
+ try {
1418
+ const result = await client.emitActivity(normalizedPayload);
1419
+ return text(`Activity emitted: ${payload.message} [${normalizedPayload.phase}${payload.progress_pct != null ? ` ${payload.progress_pct}%` : ""}] (run ${result.run_id.slice(0, 8)}...)`);
1420
+ }
1421
+ catch {
1422
+ await appendToOutbox("progress", {
1423
+ id,
1424
+ type: "progress",
1425
+ timestamp: now,
1426
+ payload: normalizedPayload,
1427
+ activityItem,
1428
+ });
1429
+ return text(`Activity saved locally: ${payload.message} [${normalizedPayload.phase}${payload.progress_pct != null ? ` ${payload.progress_pct}%` : ""}] (will sync when connected)`);
1430
+ }
1431
+ }
1432
+ async function applyChangesetWithFallback(source, payload) {
1433
+ const context = resolveReportingContext(payload);
1434
+ if (!context.ok) {
1435
+ return text(`❌ ${context.error}`);
1436
+ }
1437
+ if (!Array.isArray(payload.operations) || payload.operations.length === 0) {
1438
+ return text("❌ operations must contain at least one change");
1439
+ }
1440
+ const idempotencyKey = pickNonEmptyString(payload.idempotency_key) ??
1441
+ `${source}:${Date.now()}:${randomUUID().slice(0, 8)}`;
1442
+ const requestPayload = {
1443
+ initiative_id: context.value.initiativeId,
1444
+ run_id: context.value.runId,
1445
+ correlation_id: context.value.correlationId,
1446
+ source_client: context.value.sourceClient,
1447
+ idempotency_key: idempotencyKey,
1448
+ operations: payload.operations,
1449
+ };
1450
+ const now = new Date().toISOString();
1451
+ const id = `changeset:${randomUUID().slice(0, 8)}`;
1452
+ const activityItem = {
1453
+ id,
1454
+ type: "milestone_completed",
1455
+ title: "Changeset queued",
1456
+ description: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
1457
+ agentId: null,
1458
+ agentName: null,
1459
+ runId: context.value.runId ?? null,
1460
+ initiativeId: context.value.initiativeId,
1461
+ timestamp: now,
1462
+ phase: "review",
1463
+ summary: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
1464
+ metadata: {
1465
+ source,
1466
+ idempotency_key: idempotencyKey,
1467
+ },
1468
+ };
1469
+ try {
1470
+ const result = await client.applyChangeset(requestPayload);
1471
+ return text(`Changeset ${result.replayed ? "replayed" : "applied"}: ${result.applied_count} op${result.applied_count === 1 ? "" : "s"} (run ${result.run_id.slice(0, 8)}...)`);
1472
+ }
1473
+ catch {
1474
+ await appendToOutbox("decisions", {
1475
+ id,
1476
+ type: "changeset",
1477
+ timestamp: now,
1478
+ payload: requestPayload,
1479
+ activityItem,
1480
+ });
1481
+ return text(`Changeset saved locally (${payload.operations.length} op${payload.operations.length === 1 ? "" : "s"}) (will sync when connected)`);
1482
+ }
1483
+ }
1484
+ // --- orgx_emit_activity ---
1485
+ api.registerTool({
1486
+ name: "orgx_emit_activity",
1487
+ description: "Emit append-only OrgX activity telemetry (launch reporting contract primary write tool).",
1488
+ parameters: {
1489
+ type: "object",
1490
+ properties: {
1491
+ initiative_id: {
1492
+ type: "string",
1493
+ description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
1494
+ },
1495
+ message: {
1496
+ type: "string",
1497
+ description: "Human-readable activity update",
1498
+ },
1499
+ run_id: {
1500
+ type: "string",
1501
+ description: "Optional run UUID",
1502
+ },
1503
+ correlation_id: {
1504
+ type: "string",
1505
+ description: "Required when run_id is omitted",
1506
+ },
1507
+ source_client: {
1508
+ type: "string",
1509
+ enum: ["openclaw", "codex", "claude-code", "api"],
1510
+ description: "Required when run_id is omitted",
1511
+ },
1512
+ phase: {
1513
+ type: "string",
1514
+ enum: ["intent", "execution", "blocked", "review", "handoff", "completed"],
1515
+ description: "Reporting phase",
1516
+ },
1517
+ progress_pct: {
1518
+ type: "number",
1519
+ minimum: 0,
1520
+ maximum: 100,
1521
+ description: "Optional progress percentage",
1522
+ },
1523
+ level: {
1524
+ type: "string",
1525
+ enum: ["info", "warn", "error"],
1526
+ description: "Optional level (default info)",
1527
+ },
1528
+ next_step: {
1529
+ type: "string",
1530
+ description: "Optional next step",
1531
+ },
1532
+ metadata: {
1533
+ type: "object",
1534
+ description: "Optional structured metadata",
1535
+ },
1536
+ },
1537
+ required: ["message"],
1538
+ additionalProperties: false,
1539
+ },
1540
+ async execute(_callId, params = { message: "" }) {
1541
+ return emitActivityWithFallback("orgx_emit_activity", params);
1542
+ },
1543
+ }, { optional: true });
1544
+ // --- orgx_apply_changeset ---
1545
+ api.registerTool({
1546
+ name: "orgx_apply_changeset",
1547
+ description: "Apply an idempotent transactional OrgX changeset (launch reporting contract primary mutation tool).",
1548
+ parameters: {
1549
+ type: "object",
1550
+ properties: {
1551
+ initiative_id: {
1552
+ type: "string",
1553
+ description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
1554
+ },
1555
+ idempotency_key: {
1556
+ type: "string",
1557
+ description: "Idempotency key (<=120 chars). Auto-generated if omitted.",
1558
+ },
1559
+ operations: {
1560
+ type: "array",
1561
+ minItems: 1,
1562
+ maxItems: 25,
1563
+ description: "Changeset operations (task.create, task.update, milestone.update, decision.create)",
1564
+ items: { type: "object" },
1565
+ },
1566
+ run_id: {
1567
+ type: "string",
1568
+ description: "Optional run UUID",
1569
+ },
1570
+ correlation_id: {
1571
+ type: "string",
1572
+ description: "Required when run_id is omitted",
1573
+ },
1574
+ source_client: {
1575
+ type: "string",
1576
+ enum: ["openclaw", "codex", "claude-code", "api"],
1577
+ description: "Required when run_id is omitted",
1578
+ },
1579
+ },
1580
+ required: ["operations"],
1581
+ additionalProperties: false,
1582
+ },
1583
+ async execute(_callId, params = { operations: [] }) {
1584
+ return applyChangesetWithFallback("orgx_apply_changeset", params);
1585
+ },
1586
+ }, { optional: true });
1587
+ // --- orgx_report_progress (alias -> orgx_emit_activity) ---
1588
+ api.registerTool({
1589
+ name: "orgx_report_progress",
1590
+ description: "Alias for orgx_emit_activity. Report progress at key milestones so the team can track your work.",
1591
+ parameters: {
1592
+ type: "object",
1593
+ properties: {
1594
+ initiative_id: {
1595
+ type: "string",
1596
+ description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
1597
+ },
1598
+ run_id: {
1599
+ type: "string",
1600
+ description: "Optional run UUID",
1601
+ },
1602
+ correlation_id: {
1603
+ type: "string",
1604
+ description: "Required when run_id is omitted",
1605
+ },
1606
+ source_client: {
1607
+ type: "string",
1608
+ enum: ["openclaw", "codex", "claude-code", "api"],
1609
+ },
1610
+ summary: {
1611
+ type: "string",
1612
+ description: "What was accomplished (1-2 sentences, human-readable)",
1613
+ },
1614
+ phase: {
1615
+ type: "string",
1616
+ enum: ["researching", "implementing", "testing", "reviewing", "blocked"],
1617
+ description: "Current work phase",
1618
+ },
1619
+ progress_pct: {
1620
+ type: "number",
1621
+ description: "Progress percentage (0-100)",
1622
+ minimum: 0,
1623
+ maximum: 100,
1624
+ },
1625
+ next_step: {
1626
+ type: "string",
1627
+ description: "What you plan to do next",
1628
+ },
1629
+ },
1630
+ required: ["summary", "phase"],
1631
+ additionalProperties: false,
1632
+ },
1633
+ async execute(_callId, params = { summary: "", phase: "implementing" }) {
1634
+ return emitActivityWithFallback("orgx_report_progress", {
1635
+ initiative_id: params.initiative_id,
1636
+ run_id: params.run_id,
1637
+ correlation_id: params.correlation_id,
1638
+ source_client: params.source_client,
1639
+ message: params.summary,
1640
+ phase: toReportingPhase(params.phase, params.progress_pct),
1641
+ progress_pct: params.progress_pct,
1642
+ next_step: params.next_step,
1643
+ level: params.phase === "blocked" ? "warn" : "info",
1644
+ metadata: {
1645
+ legacy_phase: params.phase,
1646
+ },
1647
+ });
1648
+ },
1649
+ }, { optional: true });
1650
+ // --- orgx_request_decision (alias -> orgx_apply_changeset decision.create) ---
1651
+ api.registerTool({
1652
+ name: "orgx_request_decision",
1653
+ description: "Alias for orgx_apply_changeset with decision.create. Request a human decision before proceeding.",
1654
+ parameters: {
1655
+ type: "object",
1656
+ properties: {
1657
+ initiative_id: {
1658
+ type: "string",
1659
+ description: "Initiative UUID (required unless ORGX_INITIATIVE_ID is set)",
1660
+ },
1661
+ run_id: {
1662
+ type: "string",
1663
+ description: "Optional run UUID",
1664
+ },
1665
+ correlation_id: {
1666
+ type: "string",
1667
+ description: "Required when run_id is omitted",
1668
+ },
1669
+ source_client: {
1670
+ type: "string",
1671
+ enum: ["openclaw", "codex", "claude-code", "api"],
1672
+ },
1673
+ question: {
1674
+ type: "string",
1675
+ description: "The decision question (e.g., 'Deploy to production?')",
1676
+ },
1677
+ context: {
1678
+ type: "string",
1679
+ description: "Background context to help the human decide",
1680
+ },
1681
+ options: {
1682
+ type: "array",
1683
+ items: { type: "string" },
1684
+ description: "Available choices (e.g., ['Yes, deploy now', 'Wait for more testing', 'Cancel'])",
1685
+ },
1686
+ urgency: {
1687
+ type: "string",
1688
+ enum: ["low", "medium", "high", "urgent"],
1689
+ description: "How urgent this decision is",
1690
+ },
1691
+ blocking: {
1692
+ type: "boolean",
1693
+ description: "Whether work should pause until this is decided (default: true)",
1694
+ },
1695
+ },
1696
+ required: ["question", "urgency"],
1697
+ additionalProperties: false,
1698
+ },
1699
+ async execute(_callId, params = { question: "", urgency: "medium" }) {
1700
+ const requestId = `decision:${randomUUID().slice(0, 8)}`;
1701
+ const changesetResult = await applyChangesetWithFallback("orgx_request_decision", {
1702
+ initiative_id: params.initiative_id,
1703
+ run_id: params.run_id,
1704
+ correlation_id: params.correlation_id,
1705
+ source_client: params.source_client,
1706
+ idempotency_key: `decision:${requestId}`,
1707
+ operations: [
1708
+ {
1709
+ op: "decision.create",
1710
+ title: params.question,
1711
+ summary: params.context,
1712
+ urgency: params.urgency,
1713
+ options: params.options,
1714
+ blocking: params.blocking ?? true,
1715
+ },
1716
+ ],
1717
+ });
1718
+ await emitActivityWithFallback("orgx_request_decision", {
1719
+ initiative_id: params.initiative_id,
1720
+ run_id: params.run_id,
1721
+ correlation_id: params.correlation_id,
1722
+ source_client: params.source_client,
1723
+ message: `Decision requested: ${params.question}`,
1724
+ phase: "review",
1725
+ level: "info",
1726
+ metadata: {
1727
+ urgency: params.urgency,
1728
+ blocking: params.blocking ?? true,
1729
+ options: params.options ?? [],
1730
+ },
1731
+ });
1732
+ return changesetResult;
1733
+ },
1734
+ }, { optional: true });
1735
+ // --- orgx_register_artifact ---
1736
+ api.registerTool({
1737
+ name: "orgx_register_artifact",
1738
+ description: "Register a work output (PR, document, config change, report, etc.) with OrgX. Makes it visible in the dashboard.",
1739
+ parameters: {
1740
+ type: "object",
1741
+ properties: {
1742
+ name: {
1743
+ type: "string",
1744
+ description: "Human-readable artifact name (e.g., 'PR #107: Fix build size')",
1745
+ },
1746
+ artifact_type: {
1747
+ type: "string",
1748
+ enum: ["pr", "commit", "document", "config", "report", "design", "other"],
1749
+ description: "Type of artifact",
1750
+ },
1751
+ description: {
1752
+ type: "string",
1753
+ description: "What this artifact is and why it matters",
1754
+ },
1755
+ url: {
1756
+ type: "string",
1757
+ description: "Link to the artifact (PR URL, file path, etc.)",
1758
+ },
1759
+ },
1760
+ required: ["name", "artifact_type"],
1761
+ additionalProperties: false,
1762
+ },
1763
+ async execute(_callId, params = { name: "", artifact_type: "other" }) {
1764
+ const now = new Date().toISOString();
1765
+ const id = `artifact:${randomUUID().slice(0, 8)}`;
1766
+ const activityItem = {
1767
+ id,
1768
+ type: "artifact_created",
1769
+ title: params.name,
1770
+ description: params.description ?? null,
1771
+ agentId: null,
1772
+ agentName: null,
1773
+ runId: null,
1774
+ initiativeId: null,
1775
+ timestamp: now,
1776
+ summary: params.url ?? null,
1777
+ metadata: {
1778
+ source: "orgx_register_artifact",
1779
+ artifact_type: params.artifact_type,
1780
+ url: params.url,
1781
+ },
1782
+ };
1783
+ try {
1784
+ const entity = await client.createEntity("artifact", {
1785
+ name: params.name,
1786
+ artifact_type: params.artifact_type,
1787
+ description: params.description,
1788
+ artifact_url: params.url,
1789
+ status: "active",
1790
+ });
1791
+ return json(`Artifact registered: ${params.name} [${params.artifact_type}]`, entity);
1792
+ }
1793
+ catch {
1794
+ await appendToOutbox("artifacts", {
1795
+ id,
1796
+ type: "artifact",
1797
+ timestamp: now,
1798
+ payload: params,
1799
+ activityItem,
1800
+ });
1801
+ return text(`Artifact saved locally: ${params.name} [${params.artifact_type}] (will sync when connected)`);
1802
+ }
1803
+ },
1804
+ }, { optional: true });
404
1805
  // ---------------------------------------------------------------------------
405
1806
  // 3. CLI Command
406
1807
  // ---------------------------------------------------------------------------
@@ -444,12 +1845,20 @@ export default function register(api) {
444
1845
  // ---------------------------------------------------------------------------
445
1846
  // 4. HTTP Handler — Dashboard + API proxy
446
1847
  // ---------------------------------------------------------------------------
447
- const httpHandler = createHttpHandler(config, client, () => cachedSnapshot);
1848
+ const httpHandler = createHttpHandler(config, client, () => cachedSnapshot, {
1849
+ getState: () => ({ ...onboardingState }),
1850
+ startPairing,
1851
+ getStatus: getPairingStatus,
1852
+ submitManualKey,
1853
+ disconnect: disconnectOnboarding,
1854
+ });
448
1855
  api.registerHttpHandler(httpHandler);
449
1856
  api.log?.info?.("[orgx] Plugin registered", {
450
1857
  baseUrl: config.baseUrl,
451
1858
  hasApiKey: !!config.apiKey,
452
1859
  dashboardEnabled: config.dashboardEnabled,
1860
+ installationId: config.installationId,
1861
+ pluginVersion: config.pluginVersion,
453
1862
  });
454
1863
  }
455
1864
  // =============================================================================