@xfxstudio/claworld 0.2.23 → 0.2.25

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.
@@ -1,4 +1,5 @@
1
1
  import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
+ import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
2
3
  import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
3
4
  import { extractBackendErrorContext } from './backend-error-context.js';
4
5
  import {
@@ -23,6 +24,11 @@ function normalizeStringList(values = []) {
23
24
  return [...new Set(values.map((value) => normalizeText(value, null)).filter(Boolean))];
24
25
  }
25
26
 
27
+ function normalizeWorldRole(worldRole, fallback = null) {
28
+ const normalized = normalizeText(worldRole, fallback);
29
+ return ['owner', 'member'].includes(normalized) ? normalized : fallback;
30
+ }
31
+
26
32
  function sentenceCase(value, fallback = '') {
27
33
  const normalized = normalizeText(value, fallback);
28
34
  if (!normalized) return fallback;
@@ -129,6 +135,7 @@ function normalizeWorldDetail(payload = {}) {
129
135
  displayName: normalizeText(payload.displayName, normalizedWorldId),
130
136
  worldContextText: normalizeText(payload.worldContextText, ''),
131
137
  ownerAgentId: normalizeText(payload.ownerAgentId, null),
138
+ worldRole: normalizeWorldRole(payload.worldRole, null),
132
139
  enabled: typeof payload.enabled === 'boolean' ? payload.enabled : null,
133
140
  requiredFieldCount: normalizeInteger(payload.requiredFieldCount, requiredFields.length) || requiredFields.length,
134
141
  optionalFieldCount: normalizeInteger(payload.optionalFieldCount, optionalFields.length) || optionalFields.length,
@@ -168,6 +175,7 @@ function normalizeWorldDetail(payload = {}) {
168
175
  displayName,
169
176
  worldContextText: normalizeText(world.worldContextText || payload.worldContextText, ''),
170
177
  ownerAgentId: normalizeText(management.ownerAgentId, null),
178
+ worldRole: normalizeWorldRole(payload.worldRole, null),
171
179
  enabled: typeof management.enabled === 'boolean' ? management.enabled : null,
172
180
  statusLabel: normalizeText(management.status, null),
173
181
  requiredFieldCount: 1,
@@ -259,6 +267,7 @@ function normalizeCandidate(candidate = {}, index = 0) {
259
267
  return {
260
268
  candidateId: normalizeText(candidate.candidateId, `candidate_${index + 1}`),
261
269
  worldId: normalizeText(candidate.worldId, 'unknown-world'),
270
+ worldRole: normalizeWorldRole(candidate.worldRole, null),
262
271
  sourceMembershipId: normalizeText(candidate.sourceMembershipId, null),
263
272
  online: candidate.online === true,
264
273
  displayName,
@@ -314,7 +323,7 @@ function normalizeCandidateFeedResponse(payload = {}, { worldId = null, agentId
314
323
  };
315
324
  }
316
325
 
317
- function normalizeWorldJoinResponse(payload = {}, { worldId = null, agentId = null } = {}) {
326
+ export function normalizeWorldJoinResponse(payload = {}, { worldId = null, agentId = null } = {}) {
318
327
  const membership = payload.membership && typeof payload.membership === 'object' ? payload.membership : null;
319
328
  const normalizedWorldId = normalizeText(payload.worldId, worldId || 'unknown-world');
320
329
  const normalizedAgentId = normalizeText(payload.agentId || membership?.agentId, agentId || null);
@@ -330,6 +339,7 @@ function normalizeWorldJoinResponse(payload = {}, { worldId = null, agentId = nu
330
339
  status: normalizeText(payload.status, membershipStatus === 'active' ? 'joined' : 'accepted'),
331
340
  worldId: normalizedWorldId,
332
341
  agentId: normalizedAgentId,
342
+ worldRole: normalizeWorldRole(payload.worldRole, null),
333
343
  membershipStatus,
334
344
  participantContextText: normalizeText(
335
345
  payload.participantContextText,
@@ -512,11 +522,10 @@ export async function fetchWorldDetail({
512
522
  const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
513
523
  const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
514
524
  const detail = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}`, {
515
- headers: {
525
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
516
526
  accept: 'application/json',
517
527
  ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
518
- ...(resolvedRuntimeConfig.relay?.credentialToken ? { authorization: `Bearer ${resolvedRuntimeConfig.relay.credentialToken}` } : {}),
519
- },
528
+ }),
520
529
  });
521
530
 
522
531
  if (!detail.ok) {
@@ -563,12 +572,11 @@ export async function joinWorld({
563
572
  const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
564
573
  const joinResult = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/join`, {
565
574
  method: 'POST',
566
- headers: {
575
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
567
576
  accept: 'application/json',
568
577
  'content-type': 'application/json',
569
578
  ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
570
- ...(resolvedRuntimeConfig.relay?.credentialToken ? { authorization: `Bearer ${resolvedRuntimeConfig.relay.credentialToken}` } : {}),
571
- },
579
+ }),
572
580
  body: JSON.stringify({
573
581
  agentId: resolvedAgentId,
574
582
  participantContextText: normalizeText(participantContextText, null),
@@ -628,11 +636,10 @@ export async function fetchWorldCandidateFeed({
628
636
  requestUrl.searchParams.set('limit', String(normalizedLimit));
629
637
  }
630
638
  const candidateFeed = await fetchJson(fetchImpl, requestUrl.toString(), {
631
- headers: {
639
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
632
640
  accept: 'application/json',
633
641
  ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
634
- ...(resolvedRuntimeConfig.relay?.credentialToken ? { authorization: `Bearer ${resolvedRuntimeConfig.relay.credentialToken}` } : {}),
635
- },
642
+ }),
636
643
  });
637
644
 
638
645
  if (!candidateFeed.ok) {
@@ -674,11 +681,10 @@ export async function resolveWorldSelectionFlow({
674
681
  const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
675
682
  const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
676
683
  const worlds = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds`, {
677
- headers: {
684
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
678
685
  accept: 'application/json',
679
686
  ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
680
- ...(resolvedRuntimeConfig.relay?.credentialToken ? { authorization: `Bearer ${resolvedRuntimeConfig.relay.credentialToken}` } : {}),
681
- },
687
+ }),
682
688
  });
683
689
 
684
690
  if (!worlds.ok) {
@@ -1,3 +1,5 @@
1
+ import { normalizeAcceptedChatKickoffRecord } from '../../lib/relay/kickoff-progress.js';
2
+
1
3
  function normalizeText(value, fallback = null) {
2
4
  if (value == null) return fallback;
3
5
  const normalized = String(value).trim();
@@ -159,6 +161,7 @@ function projectToolCandidateDeliverySummary(
159
161
  candidateId: normalizeText(summary.candidateId, null),
160
162
  sourceMembershipId: normalizeText(summary.sourceMembershipId, null),
161
163
  displayName: normalizeText(summary.displayName, null),
164
+ worldRole: projectWorldRole(summary.worldRole, null),
162
165
  headline: normalizeText(summary.headline, null),
163
166
  online: summary.online === true,
164
167
  rank: normalizeOptionalInteger(summary.rank, null),
@@ -229,9 +232,13 @@ export function projectToolWorldDetail(worldDetail = {}) {
229
232
  worldDetail.world?.worldContextText,
230
233
  worldDetail.worldContextText || '',
231
234
  ),
232
- ownerAgentId: normalizeText(worldDetail.management?.ownerAgentId, null),
233
- enabled: normalizeOptionalBoolean(worldDetail.management?.enabled, null),
234
- status: normalizeText(worldDetail.management?.status, null),
235
+ ownerAgentId: normalizeText(
236
+ worldDetail.management?.ownerAgentId,
237
+ normalizeText(worldDetail.ownerAgentId, null),
238
+ ),
239
+ worldRole: projectWorldRole(worldDetail.worldRole, null),
240
+ enabled: normalizeOptionalBoolean(worldDetail.management?.enabled, normalizeOptionalBoolean(worldDetail.enabled, null)),
241
+ status: normalizeText(worldDetail.management?.status, normalizeText(worldDetail.statusLabel, null)),
235
242
  participantContextField: projectParticipantContextField(worldDetail.participantContextField),
236
243
  };
237
244
  }
@@ -241,6 +248,7 @@ function projectToolCandidateSummary(summary = {}, index = 0) {
241
248
  candidateId: normalizeText(summary.candidateId, `candidate_${index + 1}`),
242
249
  displayName: normalizeText(summary.displayName, `Candidate ${index + 1}`),
243
250
  agentCode: normalizeText(summary.agentCode, null)?.toUpperCase() || null,
251
+ worldRole: projectWorldRole(summary.worldRole, null),
244
252
  headline: normalizeText(summary.headline, null),
245
253
  online: summary.online === true,
246
254
  rank: normalizeInteger(summary.rank, 0) || null,
@@ -302,6 +310,7 @@ export function projectToolJoinWorldResponse(
302
310
  status: joinResult.membershipStatus === 'active' ? 'joined' : 'accepted',
303
311
  worldId: joinResult.worldId,
304
312
  accountId: normalizeText(accountId, null),
313
+ worldRole: projectWorldRole(joinResult.worldRole, null),
305
314
  membershipStatus: joinResult.membershipStatus || 'unknown',
306
315
  participantContextText: normalizeText(
307
316
  joinResult.participantContextText,
@@ -327,6 +336,10 @@ export function projectToolCreateWorldResponse(world = {}, { accountId = null }
327
336
  status: normalizeText(world.status, null),
328
337
  enabled: normalizeOptionalBoolean(world.enabled, null),
329
338
  worldRole: projectWorldRole(world.worldRole, null),
339
+ ownerJoin:
340
+ world.ownerJoin && typeof world.ownerJoin === 'object'
341
+ ? projectToolJoinWorldResponse(world.ownerJoin, { accountId })
342
+ : null,
330
343
  schemaVersion: normalizeOptionalInteger(world.schemaVersion, null),
331
344
  createdAt: normalizeText(world.createdAt, null),
332
345
  };
@@ -371,6 +384,41 @@ export function projectToolManagedWorldResponse(world = {}, { accountId = null }
371
384
  };
372
385
  }
373
386
 
387
+ function projectToolWorldMembershipSummary(membership = {}) {
388
+ return {
389
+ membershipId: normalizeText(membership.membershipId, null),
390
+ worldId: normalizeText(membership.worldId, null),
391
+ displayName: normalizeText(membership.displayName, null),
392
+ worldContextText: normalizeText(membership.worldContextText, null),
393
+ ownerAgentId: normalizeText(membership.ownerAgentId, null),
394
+ enabled: normalizeOptionalBoolean(membership.enabled, null),
395
+ worldStatus: normalizeText(membership.worldStatus, null),
396
+ worldRole: projectWorldRole(membership.worldRole, null),
397
+ membershipStatus: normalizeText(membership.membershipStatus, null),
398
+ participantContextText: normalizeText(membership.participantContextText, null),
399
+ joinedAt: normalizeText(membership.joinedAt, null),
400
+ updatedAt: normalizeText(membership.updatedAt, null),
401
+ nextAction: normalizeText(membership.nextAction, null),
402
+ };
403
+ }
404
+
405
+ export function projectToolWorldMembershipListResponse(payload = {}, { accountId = null } = {}) {
406
+ return {
407
+ accountId: normalizeText(accountId, null),
408
+ memberships: Array.isArray(payload.items)
409
+ ? payload.items.map((membership) => projectToolWorldMembershipSummary(membership))
410
+ : [],
411
+ nextAction: normalizeText(payload.nextAction, null),
412
+ };
413
+ }
414
+
415
+ export function projectToolWorldMembershipResponse(payload = {}, { accountId = null } = {}) {
416
+ return {
417
+ accountId: normalizeText(accountId, null),
418
+ ...projectToolWorldMembershipSummary(payload),
419
+ };
420
+ }
421
+
374
422
  export function projectToolFeedbackSubmissionResponse(result = {}) {
375
423
  const feedback = result.feedback && typeof result.feedback === 'object' ? result.feedback : {};
376
424
  const reporter = feedback.reporter && typeof feedback.reporter === 'object' ? feedback.reporter : {};
@@ -437,20 +485,27 @@ function normalizeConversationScopeDetails(input = {}) {
437
485
  }
438
486
 
439
487
  function projectChatRequestKickoff(kickoff = {}) {
440
- if (!kickoff || typeof kickoff !== 'object') return null;
488
+ const normalizedKickoff = normalizeAcceptedChatKickoffRecord(kickoff, { fallbackStatus: 'skipped' });
489
+ if (!normalizedKickoff) return null;
441
490
  return {
442
- status: normalizeText(kickoff.status, 'skipped'),
443
- deliveredAt: normalizeText(kickoff.deliveredAt, null),
444
- senderKickoffDeliveredAt: normalizeText(kickoff.senderKickoffDeliveredAt, normalizeText(kickoff.deliveredAt, null)),
445
- openerAcceptedAt: normalizeText(kickoff.openerAcceptedAt, null),
446
- openerDeliveredAt: normalizeText(kickoff.openerDeliveredAt, null),
447
- liveChatEstablishedAt: normalizeText(kickoff.liveChatEstablishedAt, null),
448
- conversationKey: normalizeText(kickoff.conversationKey, null),
449
- localSessionKey: normalizeText(kickoff.localSessionKey, normalizeText(kickoff.sessionKey, null)),
450
- turnId: normalizeText(kickoff.turnId, null),
451
- deliveryId: normalizeText(kickoff.deliveryId, null),
452
- created: typeof kickoff.created === 'boolean' ? kickoff.created : null,
453
- reason: normalizeText(kickoff.reason, null),
491
+ status: normalizeText(normalizedKickoff.status, 'skipped'),
492
+ deliveredAt: normalizeText(normalizedKickoff.deliveredAt, null),
493
+ senderKickoffDeliveredAt: normalizeText(
494
+ normalizedKickoff.senderKickoffDeliveredAt,
495
+ normalizeText(normalizedKickoff.deliveredAt, null),
496
+ ),
497
+ openerAcceptedAt: normalizeText(normalizedKickoff.openerAcceptedAt, null),
498
+ openerDeliveredAt: normalizeText(normalizedKickoff.openerDeliveredAt, null),
499
+ liveChatEstablishedAt: normalizeText(normalizedKickoff.liveChatEstablishedAt, null),
500
+ conversationKey: normalizeText(normalizedKickoff.conversationKey, null),
501
+ localSessionKey: normalizeText(
502
+ normalizedKickoff.localSessionKey,
503
+ normalizeText(normalizedKickoff.sessionKey, null),
504
+ ),
505
+ turnId: normalizeText(normalizedKickoff.turnId, null),
506
+ deliveryId: normalizeText(normalizedKickoff.deliveryId, null),
507
+ created: typeof normalizedKickoff.created === 'boolean' ? normalizedKickoff.created : null,
508
+ reason: normalizeText(normalizedKickoff.reason, null),
454
509
  };
455
510
  }
456
511
 
@@ -0,0 +1,320 @@
1
+ import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
+ import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
+ import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
+ import { extractBackendErrorContext } from './backend-error-context.js';
5
+
6
+ function normalizeText(value, fallback = null) {
7
+ if (value == null) return fallback;
8
+ const normalized = String(value).trim();
9
+ return normalized || fallback;
10
+ }
11
+
12
+ function normalizeOptionalBoolean(value, fallback = null) {
13
+ if (typeof value === 'boolean') return value;
14
+ return fallback;
15
+ }
16
+
17
+ function normalizeWorldRole(worldRole, fallback = null) {
18
+ const normalized = normalizeText(worldRole, fallback);
19
+ return ['owner', 'member'].includes(normalized) ? normalized : fallback;
20
+ }
21
+
22
+ function normalizeManagedWorldMembership(payload = {}) {
23
+ return {
24
+ membershipId: normalizeText(payload.membershipId, null),
25
+ worldId: normalizeText(payload.worldId, null),
26
+ displayName: normalizeText(payload.displayName, null),
27
+ worldContextText: normalizeText(payload.worldContextText, null),
28
+ ownerAgentId: normalizeText(payload.ownerAgentId, null),
29
+ enabled: normalizeOptionalBoolean(payload.enabled, null),
30
+ worldStatus: normalizeText(payload.worldStatus, null),
31
+ worldRole: normalizeWorldRole(payload.worldRole, null),
32
+ membershipStatus: normalizeText(payload.membershipStatus, null),
33
+ participantContextText: normalizeText(payload.participantContextText, null),
34
+ joinedAt: normalizeText(payload.joinedAt, null),
35
+ updatedAt: normalizeText(payload.updatedAt, null),
36
+ nextAction: normalizeText(payload.nextAction, null),
37
+ };
38
+ }
39
+
40
+ function normalizeMembershipList(payload = {}) {
41
+ return {
42
+ items: Array.isArray(payload.items)
43
+ ? payload.items.map((item) => normalizeManagedWorldMembership(item))
44
+ : [],
45
+ nextAction: normalizeText(payload.nextAction, null),
46
+ };
47
+ }
48
+
49
+ async function fetchJson(fetchImpl, url, init = {}) {
50
+ let response;
51
+ try {
52
+ response = await fetchImpl(url, init);
53
+ } catch (error) {
54
+ throw createRuntimeBoundaryError({
55
+ code: 'relay_fetch_failed',
56
+ category: 'transport',
57
+ status: 502,
58
+ message: `fetch failed: ${error?.message || String(error)}`,
59
+ publicMessage: 'relay fetch failed',
60
+ recoverable: true,
61
+ context: {
62
+ fetchUrl: url,
63
+ fetchMethod: init?.method || 'GET',
64
+ },
65
+ cause: error,
66
+ });
67
+ }
68
+ let body = null;
69
+ try {
70
+ body = await response.json();
71
+ } catch {
72
+ body = null;
73
+ }
74
+ return { ok: response.ok, status: response.status, body };
75
+ }
76
+
77
+ function normalizeRelayHttpBaseUrl(serverUrl) {
78
+ const parsed = new URL(serverUrl);
79
+ if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
80
+ if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
81
+ parsed.pathname = '';
82
+ parsed.search = '';
83
+ parsed.hash = '';
84
+ return parsed.toString().replace(/\/$/, '');
85
+ }
86
+
87
+ function inferHttpErrorCategory(status) {
88
+ if (status === 401) return 'auth';
89
+ if (status === 403) return 'policy';
90
+ if (status === 404) return 'input';
91
+ if (status === 409) return 'conflict';
92
+ if (status >= 400 && status < 500) return 'input';
93
+ return 'runtime';
94
+ }
95
+
96
+ function createWorldMembershipHttpError(action, response, { accountId = null, worldId = null } = {}) {
97
+ const backendCode = normalizeText(response?.body?.error, null);
98
+ const backendMessage = normalizeText(response?.body?.message, `claworld world membership ${action} failed`);
99
+
100
+ return createRuntimeBoundaryError({
101
+ code: backendCode || `claworld_world_membership_${action}_failed`,
102
+ category: inferHttpErrorCategory(response?.status),
103
+ status: response?.status ?? 500,
104
+ message: `claworld world membership ${action} failed: ${response?.status ?? 500}`,
105
+ publicMessage: backendMessage,
106
+ recoverable: Number(response?.status) >= 400 && Number(response?.status) < 500,
107
+ context: {
108
+ action: `world_membership_${action}`,
109
+ accountId,
110
+ ...(worldId ? { worldId } : {}),
111
+ httpStatus: response?.status ?? 500,
112
+ ...extractBackendErrorContext(response?.body),
113
+ },
114
+ });
115
+ }
116
+
117
+ export async function fetchWorldMemberships({
118
+ cfg = {},
119
+ accountId = null,
120
+ runtimeConfig = null,
121
+ agentId = null,
122
+ status = null,
123
+ includeInactive = false,
124
+ includeDisabled = true,
125
+ fetchImpl,
126
+ logger = console,
127
+ } = {}) {
128
+ if (typeof fetchImpl !== 'function') {
129
+ throw new Error('fetch is unavailable for claworld world membership helper');
130
+ }
131
+
132
+ const resolvedAgentId = normalizeText(agentId, null);
133
+ if (!resolvedAgentId) {
134
+ throw new Error('claworld world membership helper requires agentId');
135
+ }
136
+
137
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
138
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
139
+ const requestUrl = new URL(`${baseUrl}/v1/world-memberships`);
140
+ requestUrl.searchParams.set('agentId', resolvedAgentId);
141
+ if (normalizeText(status, null)) requestUrl.searchParams.set('status', normalizeText(status, null));
142
+ if (includeInactive) requestUrl.searchParams.set('includeInactive', 'true');
143
+ requestUrl.searchParams.set('includeDisabled', includeDisabled ? 'true' : 'false');
144
+ const result = await fetchJson(fetchImpl, requestUrl.toString(), {
145
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
146
+ accept: 'application/json',
147
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
148
+ }),
149
+ });
150
+
151
+ if (!result.ok) {
152
+ logger.error?.('[claworld:membership] world memberships fetch failed', {
153
+ status: result.status,
154
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
155
+ body: result.body,
156
+ });
157
+ throw createWorldMembershipHttpError('list', result, {
158
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
159
+ });
160
+ }
161
+
162
+ return normalizeMembershipList(result.body);
163
+ }
164
+
165
+ export async function fetchWorldMembership({
166
+ cfg = {},
167
+ accountId = null,
168
+ runtimeConfig = null,
169
+ agentId = null,
170
+ worldId = null,
171
+ includeDisabled = true,
172
+ fetchImpl,
173
+ logger = console,
174
+ } = {}) {
175
+ if (typeof fetchImpl !== 'function') {
176
+ throw new Error('fetch is unavailable for claworld world membership helper');
177
+ }
178
+
179
+ const resolvedAgentId = normalizeText(agentId, null);
180
+ if (!resolvedAgentId) {
181
+ throw new Error('claworld world membership helper requires agentId');
182
+ }
183
+ const resolvedWorldId = normalizeText(worldId, null);
184
+ if (!resolvedWorldId) {
185
+ throw new Error('claworld world membership helper requires worldId');
186
+ }
187
+
188
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
189
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
190
+ const requestUrl = new URL(`${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/membership`);
191
+ requestUrl.searchParams.set('agentId', resolvedAgentId);
192
+ requestUrl.searchParams.set('includeDisabled', includeDisabled ? 'true' : 'false');
193
+ const result = await fetchJson(fetchImpl, requestUrl.toString(), {
194
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
195
+ accept: 'application/json',
196
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
197
+ }),
198
+ });
199
+
200
+ if (!result.ok) {
201
+ logger.error?.('[claworld:membership] world membership fetch failed', {
202
+ status: result.status,
203
+ worldId: resolvedWorldId,
204
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
205
+ body: result.body,
206
+ });
207
+ throw createWorldMembershipHttpError('get', result, {
208
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
209
+ worldId: resolvedWorldId,
210
+ });
211
+ }
212
+
213
+ return normalizeManagedWorldMembership(result.body);
214
+ }
215
+
216
+ export async function updateWorldMembershipProfile({
217
+ cfg = {},
218
+ accountId = null,
219
+ runtimeConfig = null,
220
+ agentId = null,
221
+ worldId = null,
222
+ participantContextText = null,
223
+ fetchImpl,
224
+ logger = console,
225
+ } = {}) {
226
+ if (typeof fetchImpl !== 'function') {
227
+ throw new Error('fetch is unavailable for claworld world membership helper');
228
+ }
229
+
230
+ const resolvedAgentId = normalizeText(agentId, null);
231
+ if (!resolvedAgentId) {
232
+ throw new Error('claworld world membership helper requires agentId');
233
+ }
234
+ const resolvedWorldId = normalizeText(worldId, null);
235
+ if (!resolvedWorldId) {
236
+ throw new Error('claworld world membership helper requires worldId');
237
+ }
238
+
239
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
240
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
241
+ const result = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/membership`, {
242
+ method: 'PATCH',
243
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
244
+ accept: 'application/json',
245
+ 'content-type': 'application/json',
246
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
247
+ }),
248
+ body: JSON.stringify({
249
+ agentId: resolvedAgentId,
250
+ participantContextText: normalizeText(participantContextText, null),
251
+ }),
252
+ });
253
+
254
+ if (!result.ok) {
255
+ logger.error?.('[claworld:membership] world membership profile update failed', {
256
+ status: result.status,
257
+ worldId: resolvedWorldId,
258
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
259
+ body: result.body,
260
+ });
261
+ throw createWorldMembershipHttpError('update_profile', result, {
262
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
263
+ worldId: resolvedWorldId,
264
+ });
265
+ }
266
+
267
+ return normalizeManagedWorldMembership(result.body);
268
+ }
269
+
270
+ export async function leaveWorldMembership({
271
+ cfg = {},
272
+ accountId = null,
273
+ runtimeConfig = null,
274
+ agentId = null,
275
+ worldId = null,
276
+ fetchImpl,
277
+ logger = console,
278
+ } = {}) {
279
+ if (typeof fetchImpl !== 'function') {
280
+ throw new Error('fetch is unavailable for claworld world membership helper');
281
+ }
282
+
283
+ const resolvedAgentId = normalizeText(agentId, null);
284
+ if (!resolvedAgentId) {
285
+ throw new Error('claworld world membership helper requires agentId');
286
+ }
287
+ const resolvedWorldId = normalizeText(worldId, null);
288
+ if (!resolvedWorldId) {
289
+ throw new Error('claworld world membership helper requires worldId');
290
+ }
291
+
292
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
293
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
294
+ const result = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/membership/leave`, {
295
+ method: 'POST',
296
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
297
+ accept: 'application/json',
298
+ 'content-type': 'application/json',
299
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
300
+ }),
301
+ body: JSON.stringify({
302
+ agentId: resolvedAgentId,
303
+ }),
304
+ });
305
+
306
+ if (!result.ok) {
307
+ logger.error?.('[claworld:membership] world membership leave failed', {
308
+ status: result.status,
309
+ worldId: resolvedWorldId,
310
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
311
+ body: result.body,
312
+ });
313
+ throw createWorldMembershipHttpError('leave', result, {
314
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
315
+ worldId: resolvedWorldId,
316
+ });
317
+ }
318
+
319
+ return normalizeManagedWorldMembership(result.body);
320
+ }
@@ -2,6 +2,7 @@ import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
2
  import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
3
  import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
4
  import { extractBackendErrorContext } from './backend-error-context.js';
5
+ import { normalizeWorldJoinResponse } from './product-shell-helper.js';
5
6
 
6
7
  function normalizeText(value, fallback = null) {
7
8
  if (value == null) return fallback;
@@ -85,6 +86,20 @@ function normalizeManagedWorld(payload = {}) {
85
86
  };
86
87
  }
87
88
 
89
+ function normalizeCreatedWorld(payload = {}) {
90
+ const world = normalizeManagedWorld(payload);
91
+ return {
92
+ ...world,
93
+ ownerJoin:
94
+ payload.ownerJoin && typeof payload.ownerJoin === 'object'
95
+ ? normalizeWorldJoinResponse(payload.ownerJoin, {
96
+ worldId: world.worldId,
97
+ agentId: world.ownerAgentId,
98
+ })
99
+ : null,
100
+ };
101
+ }
102
+
88
103
  function normalizeOwnedWorldSummary(payload = {}) {
89
104
  return {
90
105
  worldId: normalizeText(payload.worldId, null),
@@ -174,6 +189,7 @@ export async function createModeratedWorld({
174
189
  agentId = null,
175
190
  displayName = null,
176
191
  worldContextText = null,
192
+ participantContextText = null,
177
193
  enabled = true,
178
194
  fetchImpl,
179
195
  logger = console,
@@ -200,6 +216,7 @@ export async function createModeratedWorld({
200
216
  agentId: resolvedAgentId,
201
217
  displayName,
202
218
  worldContextText,
219
+ participantContextText: normalizeText(participantContextText, null),
203
220
  enabled,
204
221
  }),
205
222
  });
@@ -215,7 +232,7 @@ export async function createModeratedWorld({
215
232
  });
216
233
  }
217
234
 
218
- return normalizeManagedWorld(created.body);
235
+ return normalizeCreatedWorld(created.body);
219
236
  }
220
237
 
221
238
  export async function fetchOwnedWorlds({
@@ -259,6 +259,11 @@ function normalizeDeliveryReason(reason = {}) {
259
259
  };
260
260
  }
261
261
 
262
+ function normalizeWorldRole(worldRole, fallback = null) {
263
+ const normalized = normalizeText(worldRole, fallback);
264
+ return ['owner', 'member'].includes(normalized) ? normalized : fallback;
265
+ }
266
+
262
267
  function normalizeCandidate(candidate = {}, index = 0) {
263
268
  const normalizedRank = normalizeNumber(candidate.rank, null);
264
269
  const displayName = normalizeText(
@@ -280,6 +285,7 @@ function normalizeCandidate(candidate = {}, index = 0) {
280
285
  return {
281
286
  candidateId: normalizeText(candidate.candidateId, `candidate_${index + 1}`),
282
287
  worldId: normalizeText(candidate.worldId, 'unknown-world'),
288
+ worldRole: normalizeWorldRole(candidate.worldRole, null),
283
289
  sourceMembershipId: normalizeText(candidate.sourceMembershipId, null),
284
290
  online: candidate.online === true,
285
291
  displayName,
@@ -622,6 +628,7 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
622
628
  .filter(Boolean);
623
629
  const deliveryReasonSummary = sentenceCase(candidate.deliveryReason.summary, '');
624
630
  const availabilitySummary = candidate.online === true ? 'Online now.' : 'Currently offline.';
631
+ const roleSummary = candidate.worldRole ? `World role: ${candidate.worldRole}.` : null;
625
632
  const scoreSummary = candidate.score == null
626
633
  ? null
627
634
  : `Score ${candidate.score}${candidate.rank == null ? '' : `, rank ${candidate.rank}`}.`;
@@ -631,6 +638,7 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
631
638
  optionalFieldSummary.length > 0 ? `Optional context: ${optionalFieldSummary.join('; ')}.` : null,
632
639
  compatibilitySummary.length > 0 ? compatibilitySummary.join(' ') : null,
633
640
  deliveryReasonSummary || null,
641
+ roleSummary,
634
642
  availabilitySummary,
635
643
  scoreSummary,
636
644
  ].filter(Boolean).join(' ');
@@ -639,6 +647,7 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
639
647
  candidateId: candidate.candidateId,
640
648
  sourceMembershipId: candidate.sourceMembershipId,
641
649
  online: candidate.online === true,
650
+ worldRole: candidate.worldRole,
642
651
  agentCode: candidate.agentCode,
643
652
  requestChat: candidate.requestChat,
644
653
  displayName: name,