@useorgx/openclaw-plugin 0.4.6 → 0.4.8

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/dist/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { OrgXClient } from "./api.js";
14
14
  import { createHttpHandler } from "./http-handler.js";
15
15
  import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan } from "./agent-suite.js";
16
- import { appendActivityItems } from "./activity-store.js";
16
+ import { registerArtifact } from "./artifacts/register-artifact.js";
17
17
  import { existsSync, readFileSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { homedir } from "node:os";
@@ -39,6 +39,16 @@ function isUserScopedApiKey(apiKey) {
39
39
  }
40
40
  function resolveRuntimeUserId(apiKey, candidates) {
41
41
  if (isUserScopedApiKey(apiKey)) {
42
+ // For oxk_ keys, the OrgX API ignores X-Orgx-User-Id, but we still keep a UUID
43
+ // around for created_by_id on certain entity writes (e.g., work_artifacts).
44
+ for (const candidate of candidates) {
45
+ if (typeof candidate !== "string")
46
+ continue;
47
+ const trimmed = candidate.trim();
48
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)) {
49
+ return trimmed;
50
+ }
51
+ }
42
52
  return "";
43
53
  }
44
54
  for (const candidate of candidates) {
@@ -1211,6 +1221,10 @@ export default function register(api) {
1211
1221
  description: null,
1212
1222
  agentId: stopped.agentId,
1213
1223
  agentName: null,
1224
+ requesterAgentId: stopped.agentId ?? null,
1225
+ requesterAgentName: null,
1226
+ executorAgentId: stopped.agentId ?? null,
1227
+ executorAgentName: null,
1214
1228
  runId: stopped.runId,
1215
1229
  initiativeId,
1216
1230
  timestamp,
@@ -1241,6 +1255,10 @@ export default function register(api) {
1241
1255
  description: null,
1242
1256
  agentId: stopped.agentId,
1243
1257
  agentName: null,
1258
+ requesterAgentId: stopped.agentId ?? null,
1259
+ requesterAgentName: null,
1260
+ executorAgentId: stopped.agentId ?? null,
1261
+ executorAgentName: null,
1244
1262
  runId: stopped.runId,
1245
1263
  initiativeId,
1246
1264
  timestamp,
@@ -1651,16 +1669,55 @@ export default function register(api) {
1651
1669
  return;
1652
1670
  }
1653
1671
  if (event.type === "artifact") {
1654
- // Artifacts are UI-level breadcrumbs and may not be supported by every
1655
- // OrgX deployment's `/api/entities` schema. Persist locally and drop from
1656
- // the outbox so progress reporting doesn't wedge on irreplayable items.
1657
- try {
1658
- if (event.activityItem) {
1659
- appendActivityItems([event.activityItem]);
1660
- }
1672
+ // Artifacts are first-class UX loop closure (activity stream + entity modals).
1673
+ // Try to persist upstream; if this fails, keep the event queued for retry.
1674
+ const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload)
1675
+ ? event.payload
1676
+ : {};
1677
+ const name = pickStringField(payload, "name") ?? pickStringField(payload, "title") ?? "";
1678
+ const artifactType = pickStringField(payload, "artifact_type") ?? "other";
1679
+ const entityType = pickStringField(payload, "entity_type") ?? "";
1680
+ const entityId = pickStringField(payload, "entity_id") ?? "";
1681
+ const artifactId = pickStringField(payload, "artifact_id") ?? null;
1682
+ const description = pickStringField(payload, "description") ?? undefined;
1683
+ const externalUrl = pickStringField(payload, "url") ?? pickStringField(payload, "artifact_url") ?? null;
1684
+ const content = pickStringField(payload, "content") ?? pickStringField(payload, "preview_markdown") ?? null;
1685
+ const allowedEntityType = entityType === "initiative" ||
1686
+ entityType === "milestone" ||
1687
+ entityType === "task" ||
1688
+ entityType === "decision" ||
1689
+ entityType === "project"
1690
+ ? entityType
1691
+ : null;
1692
+ if (!allowedEntityType || !entityId.trim() || !name.trim()) {
1693
+ api.log?.warn?.("[orgx] Dropping invalid artifact outbox event", {
1694
+ eventId: event.id,
1695
+ entityType,
1696
+ entityId,
1697
+ });
1698
+ return;
1661
1699
  }
1662
- catch {
1663
- // best effort
1700
+ const result = await registerArtifact(client, client.getBaseUrl(), {
1701
+ artifact_id: artifactId,
1702
+ entity_type: allowedEntityType,
1703
+ entity_id: entityId,
1704
+ name: name.trim(),
1705
+ artifact_type: artifactType.trim() || "other",
1706
+ description,
1707
+ external_url: externalUrl,
1708
+ preview_markdown: content,
1709
+ status: "draft",
1710
+ metadata: {
1711
+ source: "outbox_replay",
1712
+ outbox_event_id: event.id,
1713
+ ...(payload.metadata && typeof payload.metadata === "object" && !Array.isArray(payload.metadata)
1714
+ ? payload.metadata
1715
+ : {}),
1716
+ },
1717
+ validate_persistence: process.env.ORGX_VALIDATE_ARTIFACT_PERSISTENCE === "1",
1718
+ });
1719
+ if (!result.ok) {
1720
+ throw new Error(result.persistence.last_error ?? "artifact registration failed");
1664
1721
  }
1665
1722
  return;
1666
1723
  }
@@ -1996,9 +2053,15 @@ export default function register(api) {
1996
2053
  nextAction: "retry",
1997
2054
  });
1998
2055
  }
2056
+ const pairingUserIdRaw = typeof polled.data.supabaseUserId === "string"
2057
+ ? polled.data.supabaseUserId
2058
+ : typeof polled.data.userId === "string"
2059
+ ? polled.data.userId
2060
+ : null;
1999
2061
  setRuntimeApiKey({
2000
2062
  apiKey: key,
2001
2063
  source: "browser_pairing",
2064
+ userId: resolveRuntimeUserId(key, [pairingUserIdRaw, config.userId]) || null,
2002
2065
  workspaceName: polled.data.workspaceName ?? null,
2003
2066
  keyPrefix: polled.data.keyPrefix ?? null,
2004
2067
  });
@@ -2749,6 +2812,10 @@ export default function register(api) {
2749
2812
  description: payload.next_step ?? null,
2750
2813
  agentId: null,
2751
2814
  agentName: null,
2815
+ requesterAgentId: null,
2816
+ requesterAgentName: null,
2817
+ executorAgentId: null,
2818
+ executorAgentName: null,
2752
2819
  runId: context.value.runId ?? null,
2753
2820
  initiativeId: context.value.initiativeId,
2754
2821
  timestamp: now,
@@ -2798,6 +2865,10 @@ export default function register(api) {
2798
2865
  description: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
2799
2866
  agentId: null,
2800
2867
  agentName: null,
2868
+ requesterAgentId: null,
2869
+ requesterAgentName: null,
2870
+ executorAgentId: null,
2871
+ executorAgentName: null,
2801
2872
  runId: context.value.runId ?? null,
2802
2873
  initiativeId: context.value.initiativeId,
2803
2874
  timestamp: now,
@@ -3077,13 +3148,22 @@ export default function register(api) {
3077
3148
  // --- orgx_register_artifact ---
3078
3149
  registerMcpTool({
3079
3150
  name: "orgx_register_artifact",
3080
- description: "Register a work output (PR, document, config change, report, etc.) with OrgX. Makes it visible in the dashboard.",
3151
+ description: "Register a work output (PR, document, config change, report, etc.) as a work_artifact in OrgX. Makes it visible in the dashboard activity timeline and entity detail modals.",
3081
3152
  parameters: {
3082
3153
  type: "object",
3083
3154
  properties: {
3084
3155
  initiative_id: {
3085
3156
  type: "string",
3086
- description: "Optional initiative UUID to attach this artifact to",
3157
+ description: "Convenience: initiative UUID. Used as entity_type='initiative', entity_id=<this> when entity_type/entity_id are not provided.",
3158
+ },
3159
+ entity_type: {
3160
+ type: "string",
3161
+ enum: ["initiative", "milestone", "task", "decision", "project"],
3162
+ description: "The type of entity this artifact is attached to",
3163
+ },
3164
+ entity_id: {
3165
+ type: "string",
3166
+ description: "UUID of the entity this artifact is attached to",
3087
3167
  },
3088
3168
  name: {
3089
3169
  type: "string",
@@ -3091,8 +3171,7 @@ export default function register(api) {
3091
3171
  },
3092
3172
  artifact_type: {
3093
3173
  type: "string",
3094
- enum: ["pr", "commit", "document", "config", "report", "design", "other"],
3095
- description: "Type of artifact",
3174
+ description: "Artifact type code (e.g., 'eng.diff_pack', 'pr', 'document'). Falls back to 'shared.project_handbook' if the type is not recognized by OrgX.",
3096
3175
  },
3097
3176
  description: {
3098
3177
  type: "string",
@@ -3100,7 +3179,11 @@ export default function register(api) {
3100
3179
  },
3101
3180
  url: {
3102
3181
  type: "string",
3103
- description: "Link to the artifact (PR URL, file path, etc.)",
3182
+ description: "External link to the artifact (PR URL, file path, etc.)",
3183
+ },
3184
+ content: {
3185
+ type: "string",
3186
+ description: "Inline preview content (markdown/text). At least one of url or content is required.",
3104
3187
  },
3105
3188
  },
3106
3189
  required: ["name", "artifact_type"],
@@ -3109,9 +3192,32 @@ export default function register(api) {
3109
3192
  async execute(_callId, params = { name: "", artifact_type: "other" }) {
3110
3193
  const now = new Date().toISOString();
3111
3194
  const id = `artifact:${randomUUID().slice(0, 8)}`;
3112
- const initiativeId = isUuid(params.initiative_id)
3113
- ? params.initiative_id
3114
- : inferReportingInitiativeId(params) ?? null;
3195
+ // Resolve entity association: explicit entity_type+entity_id > initiative_id > inferred
3196
+ let resolvedEntityType = null;
3197
+ let resolvedEntityId = null;
3198
+ if (params.entity_type && isUuid(params.entity_id)) {
3199
+ resolvedEntityType = params.entity_type;
3200
+ resolvedEntityId = params.entity_id;
3201
+ }
3202
+ else if (isUuid(params.initiative_id)) {
3203
+ resolvedEntityType = "initiative";
3204
+ resolvedEntityId = params.initiative_id;
3205
+ }
3206
+ else {
3207
+ const inferred = inferReportingInitiativeId(params);
3208
+ if (inferred) {
3209
+ resolvedEntityType = "initiative";
3210
+ resolvedEntityId = inferred;
3211
+ }
3212
+ }
3213
+ if (!resolvedEntityType || !resolvedEntityId) {
3214
+ return text("❌ Cannot register artifact: provide entity_type + entity_id, or initiative_id, so the artifact can be attached to an entity.");
3215
+ }
3216
+ if (!params.url && !params.content) {
3217
+ return text("❌ Cannot register artifact: provide at least one of url or content.");
3218
+ }
3219
+ const baseUrl = client.getBaseUrl();
3220
+ const artifactId = randomUUID();
3115
3221
  const activityItem = {
3116
3222
  id,
3117
3223
  type: "artifact_created",
@@ -3119,35 +3225,65 @@ export default function register(api) {
3119
3225
  description: params.description ?? null,
3120
3226
  agentId: null,
3121
3227
  agentName: null,
3228
+ requesterAgentId: null,
3229
+ requesterAgentName: null,
3230
+ executorAgentId: null,
3231
+ executorAgentName: null,
3122
3232
  runId: null,
3123
- initiativeId,
3233
+ initiativeId: resolvedEntityType === "initiative" ? resolvedEntityId : null,
3124
3234
  timestamp: now,
3125
3235
  summary: params.url ?? null,
3126
3236
  metadata: withProvenanceMetadata({
3127
3237
  source: "orgx_register_artifact",
3128
3238
  artifact_type: params.artifact_type,
3129
3239
  url: params.url,
3240
+ entity_type: resolvedEntityType,
3241
+ entity_id: resolvedEntityId,
3130
3242
  }),
3131
3243
  };
3132
3244
  try {
3133
- const entity = await client.createEntity("artifact", {
3134
- title: params.name,
3245
+ const result = await registerArtifact(client, baseUrl, {
3246
+ artifact_id: artifactId,
3247
+ entity_type: resolvedEntityType,
3248
+ entity_id: resolvedEntityId,
3249
+ name: params.name,
3135
3250
  artifact_type: params.artifact_type,
3136
- summary: params.description,
3137
- initiative_id: initiativeId ?? undefined,
3138
- artifact_url: params.url,
3139
- status: "active",
3251
+ description: params.description ?? null,
3252
+ external_url: params.url ?? null,
3253
+ preview_markdown: params.content ?? null,
3254
+ status: "draft",
3255
+ metadata: {
3256
+ source: "orgx_register_artifact",
3257
+ artifact_id: artifactId,
3258
+ },
3259
+ validate_persistence: true,
3260
+ });
3261
+ if (!result.ok) {
3262
+ throw new Error(result.persistence.last_error ?? "Artifact registration failed");
3263
+ }
3264
+ activityItem.metadata = withProvenanceMetadata({
3265
+ ...activityItem.metadata,
3266
+ artifact_id: result.artifact_id,
3267
+ entity_type: resolvedEntityType,
3268
+ entity_id: resolvedEntityId,
3140
3269
  });
3141
- return json(`Artifact registered: ${params.name} [${params.artifact_type}]`, entity);
3270
+ return json(`Artifact registered: ${params.name} [${params.artifact_type}] → ${resolvedEntityType}/${resolvedEntityId} (id: ${result.artifact_id})`, result);
3142
3271
  }
3143
- catch {
3272
+ catch (firstError) {
3273
+ // Outbox fallback for offline/error scenarios
3144
3274
  await appendToOutbox("artifacts", {
3145
3275
  id,
3146
3276
  type: "artifact",
3147
3277
  timestamp: now,
3148
3278
  payload: {
3149
- ...params,
3150
- initiative_id: initiativeId,
3279
+ artifact_id: artifactId,
3280
+ name: params.name,
3281
+ artifact_type: params.artifact_type,
3282
+ description: params.description,
3283
+ url: params.url,
3284
+ content: params.content,
3285
+ entity_type: resolvedEntityType,
3286
+ entity_id: resolvedEntityId,
3151
3287
  },
3152
3288
  activityItem,
3153
3289
  });
@@ -580,6 +580,10 @@ function turnToActivity(turn, session, cachedSummary, index) {
580
580
  description: modelAlias,
581
581
  agentId: session.agentId,
582
582
  agentName: session.agentName,
583
+ requesterAgentId: session.agentId ?? null,
584
+ requesterAgentName: session.agentName ?? null,
585
+ executorAgentId: session.agentId ?? null,
586
+ executorAgentName: session.agentName ?? null,
583
587
  runId: session.sessionId ?? session.key,
584
588
  initiativeId: session.agentId ? `agent:${session.agentId}` : null,
585
589
  timestamp: turn.timestamp,
@@ -747,6 +751,10 @@ function makeSessionSummaryItem(session) {
747
751
  description: modelAlias ? `Local session (${modelAlias})` : "Local session",
748
752
  agentId: session.agentId,
749
753
  agentName: session.agentName,
754
+ requesterAgentId: session.agentId ?? null,
755
+ requesterAgentName: session.agentName ?? null,
756
+ executorAgentId: session.agentId ?? null,
757
+ executorAgentName: session.agentName ?? null,
750
758
  runId: session.sessionId ?? session.key,
751
759
  initiativeId: session.agentId ? `agent:${session.agentId}` : null,
752
760
  timestamp: session.updatedAt ?? new Date().toISOString(),
@@ -5,22 +5,6 @@ import { randomUUID } from "node:crypto";
5
5
  import { writeFileAtomicSync, writeJsonFileAtomicSync } from "./fs-utils.js";
6
6
  const ORGX_LOCAL_MCP_KEY = "orgx-openclaw";
7
7
  const ORGX_HOSTED_MCP_URL = "https://mcp.useorgx.com/mcp";
8
- const ORGX_LOCAL_MCP_SCOPES = [
9
- "engineering",
10
- "product",
11
- "design",
12
- "marketing",
13
- "sales",
14
- "operations",
15
- "orchestration",
16
- ];
17
- function scopedMcpServerKey(scope) {
18
- return `${ORGX_LOCAL_MCP_KEY}-${scope}`;
19
- }
20
- function scopedMcpUrl(localMcpUrl, scope) {
21
- const base = localMcpUrl.replace(/\/+$/, "");
22
- return `${base}/${scope}`;
23
- }
24
8
  function escapeRegExp(value) {
25
9
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
10
  }
@@ -59,6 +43,20 @@ function backupFileSync(path, mode) {
59
43
  return null;
60
44
  }
61
45
  }
46
+ function removeLegacyScopedMcpServers(servers) {
47
+ const next = { ...servers };
48
+ let updated = false;
49
+ const scopedPrefix = `${ORGX_LOCAL_MCP_KEY}-`;
50
+ for (const key of Object.keys(next)) {
51
+ if (key === ORGX_LOCAL_MCP_KEY)
52
+ continue;
53
+ if (!key.startsWith(scopedPrefix))
54
+ continue;
55
+ delete next[key];
56
+ updated = true;
57
+ }
58
+ return { updated, next };
59
+ }
62
60
  export function patchClaudeMcpConfig(input) {
63
61
  const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
64
62
  const existingOrgx = isRecord(currentServers.orgx) ? currentServers.orgx : {};
@@ -88,41 +86,20 @@ export function patchClaudeMcpConfig(input) {
88
86
  ? existing.description
89
87
  : "OrgX platform via local OpenClaw plugin (no OAuth)",
90
88
  };
91
- const updatedScopes = [];
92
- const scopedEntries = {};
93
- for (const scope of ORGX_LOCAL_MCP_SCOPES) {
94
- const key = scopedMcpServerKey(scope);
95
- const expectedUrl = scopedMcpUrl(input.localMcpUrl, scope);
96
- const existingScoped = isRecord(currentServers[key]) ? currentServers[key] : {};
97
- const priorUrl = typeof existingScoped.url === "string" ? existingScoped.url : "";
98
- const priorType = typeof existingScoped.type === "string" ? existingScoped.type : "";
99
- const nextScoped = {
100
- ...existingScoped,
101
- type: "http",
102
- url: expectedUrl,
103
- description: typeof existingScoped.description === "string" && existingScoped.description.trim().length > 0
104
- ? existingScoped.description
105
- : `OrgX platform via local OpenClaw plugin (${scope} scope)`,
106
- };
107
- scopedEntries[key] = nextScoped;
108
- if (priorUrl !== expectedUrl || priorType !== "http") {
109
- updatedScopes.push(scope);
110
- }
111
- }
112
- const nextServers = {
89
+ const mergedServers = {
113
90
  ...currentServers,
114
91
  ...(shouldSetHostedOrgx ? { orgx: nextOrgxEntry } : {}),
115
92
  [ORGX_LOCAL_MCP_KEY]: nextEntry,
116
- ...scopedEntries,
117
93
  };
94
+ const scopedCleanup = removeLegacyScopedMcpServers(mergedServers);
118
95
  const next = {
119
96
  ...input.current,
120
- mcpServers: nextServers,
97
+ mcpServers: scopedCleanup.next,
121
98
  };
122
99
  const updatedLocal = priorUrl !== input.localMcpUrl || priorType !== "http";
123
100
  const updatedHosted = shouldSetHostedOrgx &&
124
101
  (existingOrgxUrl !== ORGX_HOSTED_MCP_URL || existingOrgxType !== "http");
125
- const updated = updatedLocal || updatedHosted || updatedScopes.length > 0;
102
+ const updated = updatedLocal || updatedHosted || scopedCleanup.updated;
126
103
  return { updated, next };
127
104
  }
128
105
  export function patchCursorMcpConfig(input) {
@@ -133,30 +110,16 @@ export function patchCursorMcpConfig(input) {
133
110
  ...existing,
134
111
  url: input.localMcpUrl,
135
112
  };
136
- const scopedEntries = {};
137
- let updatedScopes = false;
138
- for (const scope of ORGX_LOCAL_MCP_SCOPES) {
139
- const key = scopedMcpServerKey(scope);
140
- const expectedUrl = scopedMcpUrl(input.localMcpUrl, scope);
141
- const existingScoped = isRecord(currentServers[key]) ? currentServers[key] : {};
142
- const priorScopedUrl = typeof existingScoped.url === "string" ? existingScoped.url : "";
143
- scopedEntries[key] = {
144
- ...existingScoped,
145
- url: expectedUrl,
146
- };
147
- if (priorScopedUrl !== expectedUrl)
148
- updatedScopes = true;
149
- }
150
- const nextServers = {
113
+ const mergedServers = {
151
114
  ...currentServers,
152
115
  [ORGX_LOCAL_MCP_KEY]: nextEntry,
153
- ...scopedEntries,
154
116
  };
117
+ const scopedCleanup = removeLegacyScopedMcpServers(mergedServers);
155
118
  const next = {
156
119
  ...input.current,
157
- mcpServers: nextServers,
120
+ mcpServers: scopedCleanup.next,
158
121
  };
159
- const updated = priorUrl !== input.localMcpUrl || updatedScopes;
122
+ const updated = priorUrl !== input.localMcpUrl || scopedCleanup.updated;
160
123
  return { updated, next };
161
124
  }
162
125
  function upsertCodexMcpServerSection(input) {
@@ -173,7 +136,9 @@ function upsertCodexMcpServerSection(input) {
173
136
  }
174
137
  const urlLine = `url = "${input.url}"`;
175
138
  if (headerIndex === -1) {
176
- const suffix = ["", `[mcp_servers.\"${input.key}\"]`, urlLine, ""].join("\n");
139
+ const needsQuote = /[^A-Za-z0-9_]/.test(input.key);
140
+ const keyLiteral = needsQuote ? `"${input.key}"` : input.key;
141
+ const suffix = ["", `[mcp_servers.${keyLiteral}]`, urlLine, ""].join("\n");
177
142
  const normalized = currentText.endsWith("\n") ? currentText : `${currentText}\n`;
178
143
  return { updated: true, next: `${normalized}${suffix}` };
179
144
  }
@@ -221,29 +186,52 @@ function upsertCodexMcpServerSection(input) {
221
186
  }
222
187
  return { updated, next: `${lines.join("\n")}\n` };
223
188
  }
224
- export function patchCodexConfigToml(input) {
225
- let current = input.current;
226
- let updatedHosted = false;
227
- // If the hosted OrgX entry is missing entirely, add a sensible default. This is
228
- // a no-op if the user already has `orgx` pointed at staging or another URL.
229
- const hostedHeaderRegex = /^\[mcp_servers\.(?:"orgx"|orgx)\]\s*$/;
230
- {
231
- const lines = current.split(/\r?\n/);
232
- const hasHosted = lines.some((line) => hostedHeaderRegex.test(line.trim()));
233
- if (!hasHosted) {
234
- const hostedUrlLine = `url = "${ORGX_HOSTED_MCP_URL}"`;
235
- const suffix = [
236
- ...(current.trim().length === 0 ? [] : [""]),
237
- "[mcp_servers.orgx]",
238
- hostedUrlLine,
239
- "",
240
- ].join("\n");
241
- const normalized = current.endsWith("\n") ? current : `${current}\n`;
242
- current = `${normalized}${suffix}`;
243
- updatedHosted = true;
189
+ function removeCodexLegacyScopedMcpSections(input) {
190
+ const lines = input.current.split(/\r?\n/);
191
+ const scopedPrefix = `${input.baseKey}-`;
192
+ let updated = false;
193
+ let index = 0;
194
+ while (index < lines.length) {
195
+ const trimmed = lines[index].trim();
196
+ const headerMatch = trimmed.match(/^\[mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_]+))\]\s*$/);
197
+ if (!headerMatch) {
198
+ index += 1;
199
+ continue;
200
+ }
201
+ const key = (headerMatch[1] ?? headerMatch[2] ?? "").trim();
202
+ const isLegacyScoped = key !== input.baseKey &&
203
+ key.startsWith(scopedPrefix);
204
+ if (!isLegacyScoped) {
205
+ index += 1;
206
+ continue;
244
207
  }
208
+ let sectionEnd = lines.length;
209
+ for (let i = index + 1; i < lines.length; i += 1) {
210
+ if (lines[i].trim().startsWith("[")) {
211
+ sectionEnd = i;
212
+ break;
213
+ }
214
+ }
215
+ const start = index > 0 && lines[index - 1].trim() === "" ? index - 1 : index;
216
+ lines.splice(start, sectionEnd - start);
217
+ updated = true;
218
+ index = Math.max(0, start);
245
219
  }
220
+ return { updated, next: `${lines.join("\n")}\n` };
221
+ }
222
+ export function patchCodexConfigToml(input) {
223
+ let current = input.current;
246
224
  let updated = false;
225
+ // Ensure the hosted OrgX entry uses a direct `url` (streamable HTTP) so that
226
+ // `codex mcp login orgx` can perform OAuth. Route through upsertCodexMcpServerSection
227
+ // so stale stdio-transport fields (command/args) are stripped automatically.
228
+ const hosted = upsertCodexMcpServerSection({
229
+ current,
230
+ key: "orgx",
231
+ url: ORGX_HOSTED_MCP_URL,
232
+ });
233
+ updated = updated || hosted.updated;
234
+ current = hosted.next;
247
235
  const base = upsertCodexMcpServerSection({
248
236
  current,
249
237
  key: ORGX_LOCAL_MCP_KEY,
@@ -251,16 +239,13 @@ export function patchCodexConfigToml(input) {
251
239
  });
252
240
  updated = updated || base.updated;
253
241
  current = base.next;
254
- for (const scope of ORGX_LOCAL_MCP_SCOPES) {
255
- const next = upsertCodexMcpServerSection({
256
- current,
257
- key: scopedMcpServerKey(scope),
258
- url: scopedMcpUrl(input.localMcpUrl, scope),
259
- });
260
- updated = updated || next.updated;
261
- current = next.next;
262
- }
263
- return { updated: updatedHosted || updated, next: current };
242
+ const removedScoped = removeCodexLegacyScopedMcpSections({
243
+ current,
244
+ baseKey: ORGX_LOCAL_MCP_KEY,
245
+ });
246
+ updated = updated || removedScoped.updated;
247
+ current = removedScoped.next;
248
+ return { updated, next: current };
264
249
  }
265
250
  export async function autoConfigureDetectedMcpClients(input) {
266
251
  const logger = input.logger ?? {};
@@ -85,7 +85,7 @@ function normalizeHookEvent(value) {
85
85
  }
86
86
  function toProviderLogo(sourceClient) {
87
87
  if (sourceClient === "codex")
88
- return "codex";
88
+ return "openai";
89
89
  if (sourceClient === "claude-code")
90
90
  return "anthropic";
91
91
  if (sourceClient === "openclaw")
@@ -97,9 +97,9 @@ function toProviderLogo(sourceClient) {
97
97
  function normalizeProviderLogo(value, sourceClient) {
98
98
  const normalized = normalizeNullableString(value)?.toLowerCase();
99
99
  if (normalized === "codex")
100
- return "codex";
100
+ return sourceClient === "codex" ? "openai" : "codex";
101
101
  if (normalized === "openai")
102
- return sourceClient === "codex" ? "codex" : "openai";
102
+ return "openai";
103
103
  if (normalized === "anthropic")
104
104
  return "anthropic";
105
105
  if (normalized === "openclaw")
@@ -4,6 +4,17 @@ function pickString(value) {
4
4
  const trimmed = value.trim();
5
5
  return trimmed.length > 0 ? trimmed : null;
6
6
  }
7
+ function parseIgnoredMcpServers(raw) {
8
+ if (typeof raw !== "string")
9
+ return new Set(["codex_apps"]);
10
+ const trimmed = raw.trim();
11
+ if (!trimmed || trimmed.toLowerCase() === "none")
12
+ return new Set();
13
+ return new Set(trimmed
14
+ .split(",")
15
+ .map((entry) => entry.trim().toLowerCase())
16
+ .filter(Boolean));
17
+ }
7
18
  export function detectMcpHandshakeFailure(logText) {
8
19
  const text = String(logText ?? "");
9
20
  const lower = text.toLowerCase();
@@ -28,6 +39,10 @@ export function detectMcpHandshakeFailure(logText) {
28
39
  signalLine?.match(/mcp client for\s+\[?([^\]]+)\]?\s+failed to start/i) ??
29
40
  null;
30
41
  const server = serverMatch ? pickString(serverMatch[1]) ?? null : null;
42
+ const ignoredServers = parseIgnoredMcpServers(process.env.ORGX_AUTOPILOT_MCP_HANDSHAKE_IGNORE_SERVERS);
43
+ if (server && ignoredServers.has(server.toLowerCase())) {
44
+ return null;
45
+ }
31
46
  return {
32
47
  kind: "mcp_handshake",
33
48
  server,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useorgx/openclaw-plugin",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "OrgX plugin for OpenClaw — agent orchestration, quality gates, model routing, and live dashboard",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,6 +40,11 @@
40
40
  "verify:clean-install": "node ./scripts/verify-clean-install.mjs",
41
41
  "verify:agent-suite": "npm run build:core && node ./scripts/verify-agent-suite-install.mjs",
42
42
  "verify:billing": "npm run build:core && node ./scripts/verify-billing-scenarios.mjs",
43
+ "verify:autopilot-e2e:local": "npm run build:core && node ./scripts/verify-autopilot-e2e-local.mjs",
44
+ "verify:autopilot-e2e:matrix": "npm run build:core && node ./scripts/verify-autopilot-e2e-matrix.mjs",
45
+ "verify:autopilot-e2e:entities": "npm run build:core && node ./scripts/verify-autopilot-e2e-entities.mjs",
46
+ "verify:autopilot-e2e:entities-matrix": "npm run build:core && node ./scripts/verify-autopilot-e2e-entities-matrix.mjs",
47
+ "verify:iwmt-cascade": "npm run build:core && node ./scripts/verify-iwmt-cascade-e2e.mjs",
43
48
  "e2e:auto-continue": "node ./scripts/e2e-auto-continue.mjs",
44
49
  "e2e:agent-suite": "npm run build:core && node ./scripts/e2e-agent-suite-kickoff-3x.mjs",
45
50
  "demo:record": "node ./scripts/record-demo.mjs",