@xfxstudio/claworld 0.1.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 (69) hide show
  1. package/README.md +60 -0
  2. package/bin/claworld.mjs +9 -0
  3. package/index.js +51 -0
  4. package/openclaw.plugin.json +470 -0
  5. package/package.json +76 -0
  6. package/setup-entry.js +6 -0
  7. package/src/lib/accepted-chat-kickoff.js +192 -0
  8. package/src/lib/agent-address.js +46 -0
  9. package/src/lib/agent-profile.js +69 -0
  10. package/src/lib/http-auth.js +151 -0
  11. package/src/lib/policy.js +118 -0
  12. package/src/lib/runtime-errors.js +149 -0
  13. package/src/lib/runtime-guidance.js +458 -0
  14. package/src/openclaw/index.js +53 -0
  15. package/src/openclaw/installer/cli.js +349 -0
  16. package/src/openclaw/installer/constants.js +6 -0
  17. package/src/openclaw/installer/core.js +1548 -0
  18. package/src/openclaw/installer/doctor.js +690 -0
  19. package/src/openclaw/installer/workspace-contract.js +403 -0
  20. package/src/openclaw/plugin/account-identity.js +66 -0
  21. package/src/openclaw/plugin/claworld-channel-plugin.js +3118 -0
  22. package/src/openclaw/plugin/config-schema.js +464 -0
  23. package/src/openclaw/plugin/lifecycle.js +114 -0
  24. package/src/openclaw/plugin/managed-config.js +648 -0
  25. package/src/openclaw/plugin/onboarding.js +291 -0
  26. package/src/openclaw/plugin/register.js +961 -0
  27. package/src/openclaw/plugin/relay-client.js +783 -0
  28. package/src/openclaw/plugin/runtime.js +12 -0
  29. package/src/openclaw/protocol/relay-event-protocol.js +31 -0
  30. package/src/openclaw/runtime/canonical-result-builder.js +116 -0
  31. package/src/openclaw/runtime/demo-session-bootstrap.js +37 -0
  32. package/src/openclaw/runtime/feedback-helper.js +145 -0
  33. package/src/openclaw/runtime/inbound-session-router.js +36 -0
  34. package/src/openclaw/runtime/outbound-session-bridge.js +17 -0
  35. package/src/openclaw/runtime/product-shell-helper.js +1712 -0
  36. package/src/openclaw/runtime/runtime-path.js +19 -0
  37. package/src/openclaw/runtime/system-message-orchestrator.js +1 -0
  38. package/src/openclaw/runtime/tool-contracts.js +714 -0
  39. package/src/openclaw/runtime/tool-inventory.js +92 -0
  40. package/src/openclaw/runtime/world-moderation-helper.js +415 -0
  41. package/src/openclaw/runtime/world-session-startup.js +1 -0
  42. package/src/product-shell/catalog/default-world-catalog.js +296 -0
  43. package/src/product-shell/contracts/candidate-feed.js +330 -0
  44. package/src/product-shell/contracts/chat-request-approval-policy.js +98 -0
  45. package/src/product-shell/contracts/world-manifest.js +435 -0
  46. package/src/product-shell/contracts/world-orchestration.js +1024 -0
  47. package/src/product-shell/feedback/feedback-contract.js +13 -0
  48. package/src/product-shell/feedback/feedback-routes.js +98 -0
  49. package/src/product-shell/feedback/feedback-service.js +254 -0
  50. package/src/product-shell/index.js +163 -0
  51. package/src/product-shell/matching/matchmaking-service.js +340 -0
  52. package/src/product-shell/membership/membership-service.js +277 -0
  53. package/src/product-shell/onboarding/onboarding-routes.js +37 -0
  54. package/src/product-shell/onboarding/onboarding-service.js +230 -0
  55. package/src/product-shell/orchestration/session-orchestrator.js +38 -0
  56. package/src/product-shell/results/result-service.js +15 -0
  57. package/src/product-shell/search/search-service.js +359 -0
  58. package/src/product-shell/social/chat-request-approval-policy.js +332 -0
  59. package/src/product-shell/social/chat-request-routes.js +108 -0
  60. package/src/product-shell/social/chat-request-service.js +632 -0
  61. package/src/product-shell/social/friend-routes.js +82 -0
  62. package/src/product-shell/social/friend-service.js +560 -0
  63. package/src/product-shell/social/social-routes.js +21 -0
  64. package/src/product-shell/social/social-service.js +140 -0
  65. package/src/product-shell/worlds/world-admin-service.js +705 -0
  66. package/src/product-shell/worlds/world-authorization.js +135 -0
  67. package/src/product-shell/worlds/world-broadcast-service.js +299 -0
  68. package/src/product-shell/worlds/world-routes.js +410 -0
  69. package/src/product-shell/worlds/world-service.js +89 -0
@@ -0,0 +1,1712 @@
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 {
5
+ buildCandidateDeliverySummary as buildBackendCandidateDeliverySummary,
6
+ buildWorldJoinOutcomeOrchestration as buildBackendWorldJoinOutcomeOrchestration,
7
+ buildRequiredFieldExplanation as buildBackendRequiredFieldExplanation,
8
+ buildWorldProfileCollectionFlow as buildBackendWorldProfileCollectionFlow,
9
+ buildWorldSelectionPrompt as buildBackendWorldSelectionPrompt,
10
+ resolveWorldSelection as resolveBackendWorldSelection,
11
+ } from '../../product-shell/contracts/world-orchestration.js';
12
+
13
+ function normalizeText(value, fallback = null) {
14
+ if (value == null) return fallback;
15
+ const normalized = String(value).trim();
16
+ return normalized || fallback;
17
+ }
18
+
19
+ function normalizeInteger(value, fallback = 0) {
20
+ const parsed = Number(value);
21
+ if (!Number.isFinite(parsed)) return fallback;
22
+ return Math.max(0, Math.trunc(parsed));
23
+ }
24
+
25
+ function normalizeStringList(values = []) {
26
+ if (!Array.isArray(values)) return [];
27
+ return [...new Set(values.map((value) => normalizeText(value, null)).filter(Boolean))];
28
+ }
29
+
30
+ function normalizeBroadcastConfig(broadcast = {}) {
31
+ return {
32
+ enabled: broadcast.enabled === true,
33
+ audience: normalizeText(broadcast.audience, 'members'),
34
+ replyPolicy: normalizeText(broadcast.replyPolicy, 'zero'),
35
+ excludeSelf: broadcast.excludeSelf !== false,
36
+ };
37
+ }
38
+
39
+ function isEmptyProfileValue(value) {
40
+ if (value == null) return true;
41
+ if (typeof value === 'string') return value.trim() === '';
42
+ if (Array.isArray(value)) return value.length === 0 || value.every((entry) => isEmptyProfileValue(entry));
43
+ if (typeof value === 'object') {
44
+ const entries = Object.values(value);
45
+ return entries.length === 0 || entries.every((entry) => isEmptyProfileValue(entry));
46
+ }
47
+ return false;
48
+ }
49
+
50
+ function normalizeProfileValue(value) {
51
+ if (typeof value === 'string') return value.trim();
52
+ if (Array.isArray(value)) {
53
+ return value
54
+ .map((entry) => normalizeProfileValue(entry))
55
+ .filter((entry) => entry !== undefined && !isEmptyProfileValue(entry));
56
+ }
57
+ if (value && typeof value === 'object') {
58
+ return Object.fromEntries(
59
+ Object.entries(value)
60
+ .filter(([, entry]) => entry !== undefined)
61
+ .map(([key, entry]) => [key, normalizeProfileValue(entry)]),
62
+ );
63
+ }
64
+ return value;
65
+ }
66
+
67
+ function normalizeProfile(profile = {}) {
68
+ if (!profile || typeof profile !== 'object' || Array.isArray(profile)) return {};
69
+
70
+ return Object.fromEntries(
71
+ Object.entries(profile)
72
+ .filter(([key, value]) => normalizeText(key, null) && value !== undefined)
73
+ .map(([key, value]) => [normalizeText(key, null), normalizeProfileValue(value)]),
74
+ );
75
+ }
76
+
77
+ function mergeProfileState(profile = {}, profileUpdate = {}) {
78
+ return {
79
+ ...normalizeProfile(profile),
80
+ ...normalizeProfile(profileUpdate),
81
+ };
82
+ }
83
+
84
+ function normalizeLookupText(value) {
85
+ return normalizeText(value, '')?.toLowerCase() || '';
86
+ }
87
+
88
+ function sentenceCase(value, fallback = '') {
89
+ const normalized = normalizeText(value, fallback);
90
+ if (!normalized) return fallback;
91
+ return /[.!?]$/.test(normalized) ? normalized : `${normalized}.`;
92
+ }
93
+
94
+ function quoteExample(example) {
95
+ return `"${String(example).trim()}"`;
96
+ }
97
+
98
+ function joinAsNaturalLanguage(values = []) {
99
+ const items = values.filter(Boolean);
100
+ if (items.length === 0) return '';
101
+ if (items.length === 1) return items[0];
102
+ if (items.length === 2) return `${items[0]} and ${items[1]}`;
103
+ return `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`;
104
+ }
105
+
106
+ function summarizeWorldChoices(items = []) {
107
+ return items.map((world) => `${world.displayName} [${world.worldId}]`);
108
+ }
109
+
110
+ function normalizeWorldSummary(world = {}) {
111
+ const summary = world.agentSummary && typeof world.agentSummary === 'object' ? world.agentSummary : world;
112
+ const rawWorldId = world.worldId || summary.worldId;
113
+
114
+ return {
115
+ worldId: normalizeText(rawWorldId, 'unknown-world'),
116
+ displayName: normalizeText(summary.displayName || world.displayName, normalizeText(rawWorldId, 'Unknown World')),
117
+ summary: normalizeText(summary.summary || world.summary, ''),
118
+ category: normalizeText(summary.category || world.category, 'general'),
119
+ hotness: normalizeInteger(summary.hotness || world.hotness || world.activatedMemberCount, 0),
120
+ requiredFieldCount: normalizeInteger(summary.requiredFieldCount || world.requiredFieldCount, 0),
121
+ matchingMode: normalizeText(summary.matchingMode || world.matchingMode, 'manual_review'),
122
+ sessionMode: normalizeText(summary.sessionMode || world.sessionMode, 'a2a'),
123
+ };
124
+ }
125
+
126
+ function normalizeField(field = {}, index = 0, { required = false } = {}) {
127
+ const fieldId = normalizeText(field.fieldId || field.id, `field_${index + 1}`);
128
+ return {
129
+ fieldId,
130
+ label: normalizeText(field.label, fieldId),
131
+ type: normalizeText(field.type, 'string'),
132
+ source: normalizeText(field.source, 'profile'),
133
+ required: field.required === true || required,
134
+ description: normalizeText(field.description, null),
135
+ examples: normalizeStringList(field.examples),
136
+ constraints: field.constraints && typeof field.constraints === 'object' ? field.constraints : {},
137
+ };
138
+ }
139
+
140
+ function normalizeSearchSchema(payload = {}, { worldId = null, fallbackFields = [] } = {}) {
141
+ const rawInputFields = Array.isArray(payload.inputFields) && payload.inputFields.length > 0
142
+ ? payload.inputFields
143
+ : fallbackFields;
144
+ const inputFields = rawInputFields.map((field, index) => normalizeField(field, index, { required: false }));
145
+ const inputFieldIds = normalizeStringList(
146
+ Array.isArray(payload.inputFieldIds)
147
+ ? payload.inputFieldIds
148
+ : inputFields.map((field) => field.fieldId),
149
+ );
150
+
151
+ return {
152
+ modelId: normalizeText(payload.modelId, worldId ? `${worldId}.search.v1` : 'world.search.v1'),
153
+ worldId: normalizeText(payload.worldId, worldId || 'unknown-world'),
154
+ mode: normalizeText(payload.mode, 'membership_profile_search'),
155
+ previewRoute: normalizeText(payload.previewRoute, worldId ? `/v1/worlds/${worldId}/search` : '/v1/worlds/:worldId/search'),
156
+ inputFieldIds,
157
+ inputFields,
158
+ resultFields: normalizeStringList(payload.resultFields),
159
+ viewerRequirement: normalizeText(payload.viewerRequirement, 'active_membership'),
160
+ onlineOnly: payload.onlineOnly !== false,
161
+ defaultLimit: normalizeInteger(payload.defaultLimit, 10),
162
+ summary: normalizeText(payload.summary, ''),
163
+ hints: normalizeStringList(payload.hints),
164
+ status: normalizeText(payload.status, 'phase1_world_search'),
165
+ };
166
+ }
167
+
168
+ function normalizeWorldDetail(payload = {}) {
169
+ if (Array.isArray(payload.requiredFields) || Array.isArray(payload.optionalFields)) {
170
+ const requiredFields = Array.isArray(payload.requiredFields)
171
+ ? payload.requiredFields.map((field, index) => normalizeField(field, index, { required: true }))
172
+ : [];
173
+ const optionalFields = Array.isArray(payload.optionalFields)
174
+ ? payload.optionalFields.map((field, index) => normalizeField(field, index, { required: false }))
175
+ : [];
176
+ const normalizedWorldId = normalizeText(payload.worldId, 'unknown-world');
177
+
178
+ return {
179
+ status: normalizeText(payload.status, 'ready'),
180
+ source: normalizeText(payload.source, 'product_shell'),
181
+ worldId: normalizedWorldId,
182
+ displayName: normalizeText(payload.displayName, normalizedWorldId),
183
+ summary: normalizeText(payload.summary, ''),
184
+ description: normalizeText(payload.description, normalizeText(payload.summary, '')),
185
+ category: normalizeText(payload.category, 'general'),
186
+ requiredFieldCount: normalizeInteger(payload.requiredFieldCount, requiredFields.length) || requiredFields.length,
187
+ optionalFieldCount: normalizeInteger(payload.optionalFieldCount, optionalFields.length) || optionalFields.length,
188
+ matchingMode: normalizeText(payload.matchingMode, 'manual_review'),
189
+ sessionMode: normalizeText(payload.sessionMode, 'a2a'),
190
+ interactionRules: normalizeText(payload.interactionRules, null),
191
+ prohibitedRules: normalizeText(payload.prohibitedRules, null),
192
+ ratingRules: normalizeText(payload.ratingRules, null),
193
+ adminAgentIds: normalizeStringList(payload.adminAgentIds),
194
+ eligibility: normalizeText(payload.eligibility, 'active'),
195
+ broadcast: normalizeBroadcastConfig(payload.broadcast),
196
+ requiredFields,
197
+ optionalFields,
198
+ hints: normalizeStringList(payload.hints),
199
+ nextAction: normalizeText(payload.nextAction, 'collect_profile_fields_then_call_join'),
200
+ sessionOverview: payload.sessionOverview && typeof payload.sessionOverview === 'object' ? payload.sessionOverview : {},
201
+ matchingOverview: payload.matchingOverview && typeof payload.matchingOverview === 'object' ? payload.matchingOverview : {},
202
+ requiredFieldExplanation:
203
+ payload.requiredFieldExplanation && typeof payload.requiredFieldExplanation === 'object'
204
+ ? payload.requiredFieldExplanation
205
+ : null,
206
+ runtimeStartupContext:
207
+ payload.runtimeStartupContext && typeof payload.runtimeStartupContext === 'object'
208
+ ? payload.runtimeStartupContext
209
+ : null,
210
+ searchSchema: normalizeSearchSchema(payload.searchSchema || {}, {
211
+ worldId: normalizedWorldId,
212
+ fallbackFields: requiredFields,
213
+ }),
214
+ };
215
+ }
216
+
217
+ const world = payload.world && typeof payload.world === 'object' ? payload.world : {};
218
+ const agentSummary = payload.agentSummary && typeof payload.agentSummary === 'object' ? payload.agentSummary : {};
219
+ const joinSchema = payload.joinSchema && typeof payload.joinSchema === 'object' ? payload.joinSchema : {};
220
+ const fieldGuide = payload.fieldGuide && typeof payload.fieldGuide === 'object' ? payload.fieldGuide : {};
221
+ const sessionOverview = payload.sessionOverview && typeof payload.sessionOverview === 'object'
222
+ ? payload.sessionOverview
223
+ : {};
224
+ const matchingOverview = payload.matchingOverview && typeof payload.matchingOverview === 'object'
225
+ ? payload.matchingOverview
226
+ : {};
227
+ const searchOverview = payload.searchSchema && typeof payload.searchSchema === 'object'
228
+ ? payload.searchSchema
229
+ : {};
230
+
231
+ const requiredInput = Array.isArray(fieldGuide.required) && fieldGuide.required.length > 0
232
+ ? fieldGuide.required
233
+ : (Array.isArray(joinSchema.requiredFields) ? joinSchema.requiredFields : []);
234
+ const optionalInput = Array.isArray(fieldGuide.optional) && fieldGuide.optional.length > 0
235
+ ? fieldGuide.optional
236
+ : (Array.isArray(joinSchema.optionalFields) ? joinSchema.optionalFields : []);
237
+
238
+ const requiredFields = requiredInput.map((field, index) => normalizeField(field, index, { required: true }));
239
+ const optionalFields = optionalInput.map((field, index) => normalizeField(field, index, { required: false }));
240
+ const worldId = normalizeText(world.worldId || joinSchema.worldId, 'unknown-world');
241
+ const displayName = normalizeText(agentSummary.displayName || world.displayName, worldId);
242
+
243
+ return {
244
+ status: 'ready',
245
+ source: 'product_shell',
246
+ worldId,
247
+ displayName,
248
+ summary: normalizeText(agentSummary.summary || world.summary, ''),
249
+ description: normalizeText(world.description, normalizeText(agentSummary.summary || world.summary, '')),
250
+ category: normalizeText(agentSummary.category || world.category, 'general'),
251
+ requiredFieldCount: normalizeInteger(joinSchema.requiredFieldCount, requiredFields.length) || requiredFields.length,
252
+ optionalFieldCount: normalizeInteger(joinSchema.optionalFieldCount, optionalFields.length) || optionalFields.length,
253
+ matchingMode: normalizeText(agentSummary.matchingMode || matchingOverview.mode || world.matching?.mode, 'manual_review'),
254
+ sessionMode: normalizeText(agentSummary.sessionMode || sessionOverview.mode || world.sessionTemplate?.mode, 'a2a'),
255
+ interactionRules: normalizeText(world.interactionRules, null),
256
+ prohibitedRules: normalizeText(world.prohibitedRules, null),
257
+ ratingRules: normalizeText(world.ratingRules, null),
258
+ adminAgentIds: normalizeStringList(world.adminAgentIds),
259
+ eligibility: normalizeText(world.eligibility, 'active'),
260
+ broadcast: normalizeBroadcastConfig(world.broadcast),
261
+ requiredFields,
262
+ optionalFields,
263
+ hints: normalizeStringList(joinSchema.hints),
264
+ nextAction: normalizeText(joinSchema.nextAction, 'collect_profile_fields_then_call_join'),
265
+ sessionOverview,
266
+ matchingOverview,
267
+ requiredFieldExplanation:
268
+ payload.requiredFieldExplanation && typeof payload.requiredFieldExplanation === 'object'
269
+ ? payload.requiredFieldExplanation
270
+ : null,
271
+ runtimeStartupContext:
272
+ payload.runtimeStartupContext && typeof payload.runtimeStartupContext === 'object'
273
+ ? payload.runtimeStartupContext
274
+ : null,
275
+ searchSchema: normalizeSearchSchema(searchOverview, {
276
+ worldId,
277
+ fallbackFields: [...requiredFields, ...optionalFields],
278
+ }),
279
+ };
280
+ }
281
+
282
+ function normalizeJoinCheckField(field = {}, index = 0) {
283
+ return normalizeField(field, index, { required: true });
284
+ }
285
+
286
+ function normalizeJoinCheckResponse(payload = {}, { worldId = null, profile = {} } = {}) {
287
+ const missingFields = Array.isArray(payload.missingFields)
288
+ ? payload.missingFields.map((field, index) => normalizeJoinCheckField(field, index))
289
+ : [];
290
+ const guidance = payload.missingFieldGuidance && typeof payload.missingFieldGuidance === 'object'
291
+ ? payload.missingFieldGuidance
292
+ : {};
293
+ const orderedMissingFields = Array.isArray(guidance.orderedMissingFields)
294
+ ? guidance.orderedMissingFields.map((field, index) => normalizeJoinCheckField(field, index))
295
+ : missingFields;
296
+ const fallbackNextMissingField = orderedMissingFields[0] || missingFields[0] || null;
297
+ const nextMissingField = payload.nextMissingField
298
+ ? normalizeJoinCheckField(payload.nextMissingField)
299
+ : (guidance.nextMissingField ? normalizeJoinCheckField(guidance.nextMissingField) : fallbackNextMissingField);
300
+ const accepted = payload.accepted === true || orderedMissingFields.length === 0;
301
+
302
+ return {
303
+ worldId: normalizeText(payload.worldId, worldId || 'unknown-world'),
304
+ accepted,
305
+ status: normalizeText(payload.status, accepted ? 'eligible' : 'needs_profile'),
306
+ missingFields,
307
+ nextMissingField,
308
+ missingFieldGuidance: {
309
+ mode: normalizeText(guidance.mode, nextMissingField ? 'ordered_required_fields' : 'complete'),
310
+ orderedMissingFields,
311
+ orderedMissingFieldIds: normalizeStringList(
312
+ Array.isArray(guidance.orderedMissingFieldIds)
313
+ ? guidance.orderedMissingFieldIds
314
+ : orderedMissingFields.map((field) => field.fieldId),
315
+ ),
316
+ nextMissingField,
317
+ remainingRequiredFieldCount: normalizeInteger(guidance.remainingRequiredFieldCount, orderedMissingFields.length),
318
+ },
319
+ normalizedProfile: normalizeProfile(
320
+ payload.normalizedProfile && typeof payload.normalizedProfile === 'object'
321
+ ? payload.normalizedProfile
322
+ : profile,
323
+ ),
324
+ nextAction: normalizeText(
325
+ payload.nextAction,
326
+ accepted ? 'join_world_when_membership_persistence_exists' : 'collect_missing_profile_fields',
327
+ ),
328
+ profileCollectionFlow:
329
+ payload.profileCollectionFlow && typeof payload.profileCollectionFlow === 'object'
330
+ ? payload.profileCollectionFlow
331
+ : null,
332
+ };
333
+ }
334
+
335
+ function normalizeNumber(value, fallback = null) {
336
+ const parsed = Number(value);
337
+ if (!Number.isFinite(parsed)) return fallback;
338
+ return parsed;
339
+ }
340
+
341
+ function normalizeProfileSummaryField(field = {}, index = 0) {
342
+ const fieldId = normalizeText(field.fieldId || field.id, `field_${index + 1}`);
343
+ const value = Array.isArray(field.value)
344
+ ? normalizeStringList(field.value)
345
+ : normalizeText(field.value, null);
346
+
347
+ if (value == null || (Array.isArray(value) && value.length === 0)) {
348
+ return null;
349
+ }
350
+
351
+ return {
352
+ fieldId,
353
+ label: normalizeText(field.label, fieldId),
354
+ value,
355
+ };
356
+ }
357
+
358
+ function normalizeCandidateProfileSummary(summary = {}) {
359
+ return {
360
+ displayName: normalizeText(summary.displayName, null),
361
+ headline: normalizeText(summary.headline, null),
362
+ requiredFields: Array.isArray(summary.requiredFields)
363
+ ? summary.requiredFields.map((field, index) => normalizeProfileSummaryField(field, index)).filter(Boolean)
364
+ : [],
365
+ optionalFields: Array.isArray(summary.optionalFields)
366
+ ? summary.optionalFields.map((field, index) => normalizeProfileSummaryField(field, index)).filter(Boolean)
367
+ : [],
368
+ };
369
+ }
370
+
371
+ function normalizeCompatibilitySignal(signal = {}, index = 0) {
372
+ return {
373
+ signalId: normalizeText(signal.signalId, `signal_${index + 1}`),
374
+ type: normalizeText(signal.type, 'world_ready'),
375
+ fieldIds: normalizeStringList(signal.fieldIds),
376
+ score: normalizeNumber(signal.score, 0),
377
+ summary: normalizeText(signal.summary, ''),
378
+ };
379
+ }
380
+
381
+ function normalizeDeliveryReason(reason = {}) {
382
+ return {
383
+ code: normalizeText(reason.code, null),
384
+ matchedFieldIds: normalizeStringList(reason.matchedFieldIds),
385
+ summary: normalizeText(reason.summary, ''),
386
+ };
387
+ }
388
+
389
+ function normalizeCandidateScoreBreakdown(entries = []) {
390
+ if (!Array.isArray(entries)) return [];
391
+
392
+ return entries.map((entry, index) => ({
393
+ signalId: normalizeText(entry.signalId, `score_signal_${index + 1}`),
394
+ label: normalizeText(entry.label, `Signal ${index + 1}`),
395
+ weight: normalizeNumber(entry.weight, 0),
396
+ sourceFieldIds: normalizeStringList(entry.sourceFieldIds),
397
+ matched: entry.matched === true,
398
+ requesterValue: entry.requesterValue ?? null,
399
+ candidateValue: entry.candidateValue ?? null,
400
+ sharedValues: Array.isArray(entry.sharedValues) ? normalizeStringList(entry.sharedValues) : [],
401
+ overlapCount: normalizeInteger(entry.overlapCount, 0),
402
+ contribution: normalizeNumber(entry.contribution, 0),
403
+ }));
404
+ }
405
+
406
+ function normalizeCandidate(candidate = {}, index = 0) {
407
+ const normalizedRank = normalizeNumber(candidate.rank, null);
408
+
409
+ return {
410
+ candidateId: normalizeText(candidate.candidateId, `candidate_${index + 1}`),
411
+ worldId: normalizeText(candidate.worldId, 'unknown-world'),
412
+ sourceMembershipId: normalizeText(candidate.sourceMembershipId, null),
413
+ profileSummary: normalizeCandidateProfileSummary(candidate.profileSummary),
414
+ compatibilitySignals: Array.isArray(candidate.compatibilitySignals)
415
+ ? candidate.compatibilitySignals.map((signal, signalIndex) => normalizeCompatibilitySignal(signal, signalIndex))
416
+ : [],
417
+ deliveryReason: normalizeDeliveryReason(candidate.deliveryReason),
418
+ expiresAt: normalizeText(candidate.expiresAt, null),
419
+ joinedAt: normalizeText(candidate.joinedAt, null),
420
+ rank: normalizedRank == null ? null : Math.max(1, Math.trunc(normalizedRank)),
421
+ score: normalizeNumber(candidate.score, null),
422
+ scoreBreakdown: normalizeCandidateScoreBreakdown(candidate.scoreBreakdown),
423
+ scoringInputs: candidate.scoringInputs && typeof candidate.scoringInputs === 'object' ? candidate.scoringInputs : {},
424
+ };
425
+ }
426
+
427
+ function normalizeCandidateFeedResponse(payload = {}, { worldId = null, agentId = null } = {}) {
428
+ const candidates = Array.isArray(payload.candidates)
429
+ ? payload.candidates.map((candidate, index) => normalizeCandidate(candidate, index))
430
+ : [];
431
+
432
+ return {
433
+ worldId: normalizeText(payload.worldId, worldId || 'unknown-world'),
434
+ agentId: normalizeText(payload.agentId, agentId || null),
435
+ viewerMembershipId: normalizeText(payload.viewerMembershipId, null),
436
+ generatedAt: normalizeText(payload.generatedAt, null),
437
+ expiresAt: normalizeText(payload.expiresAt, null),
438
+ deliveryMode: normalizeText(payload.deliveryMode, 'agent_review_before_live_session'),
439
+ nextAction: normalizeText(
440
+ payload.nextAction,
441
+ candidates.length > 0 ? 'review_candidates_before_requesting_live_session' : 'wait_for_more_candidates',
442
+ ),
443
+ candidateDelivery: payload.candidateDelivery && typeof payload.candidateDelivery === 'object'
444
+ ? payload.candidateDelivery
445
+ : null,
446
+ candidateSource: normalizeText(payload.candidateSource, 'active_memberships'),
447
+ candidateModel: payload.candidateModel && typeof payload.candidateModel === 'object' ? payload.candidateModel : {},
448
+ strategy: payload.strategy && typeof payload.strategy === 'object' ? payload.strategy : {},
449
+ limit: normalizeInteger(payload.limit, candidates.length),
450
+ totalCandidates: normalizeInteger(payload.totalCandidates, candidates.length),
451
+ status: normalizeText(payload.status, candidates.length > 0 ? 'feed_ready' : 'no_candidates_ready'),
452
+ candidates,
453
+ };
454
+ }
455
+
456
+ function normalizeWorldJoinResponse(payload = {}, { worldId = null, agentId = null } = {}) {
457
+ const membership = payload.membership && typeof payload.membership === 'object' ? payload.membership : null;
458
+
459
+ return {
460
+ worldId: normalizeText(payload.worldId, worldId || 'unknown-world'),
461
+ agentId: normalizeText(payload.agentId || membership?.agentId, agentId || null),
462
+ membershipStatus: normalizeText(payload.membershipStatus || membership?.status, 'unknown'),
463
+ membership,
464
+ nextStageSummary: payload.nextStageSummary && typeof payload.nextStageSummary === 'object'
465
+ ? payload.nextStageSummary
466
+ : {},
467
+ orchestration: payload.orchestration && typeof payload.orchestration === 'object'
468
+ ? payload.orchestration
469
+ : null,
470
+ };
471
+ }
472
+
473
+ function projectMissingRequiredFields(fields = []) {
474
+ return fields.map((field) => ({
475
+ fieldId: field.fieldId,
476
+ label: field.label,
477
+ description: field.description || null,
478
+ }));
479
+ }
480
+
481
+ function buildJoinRequestBody({ agentId = null, profile = {}, profileSnapshot = null } = {}) {
482
+ const normalizedAgentId = normalizeText(agentId, null);
483
+ const normalizedProfile = normalizeProfile(profile);
484
+ const normalizedProfileSnapshot = normalizeProfile(profileSnapshot);
485
+
486
+ return {
487
+ agentId: normalizedAgentId,
488
+ ...(Object.keys(normalizedProfileSnapshot).length > 0
489
+ ? { profileSnapshot: normalizedProfileSnapshot }
490
+ : (Object.keys(normalizedProfile).length > 0 ? { profile: normalizedProfile } : {})),
491
+ };
492
+ }
493
+
494
+ function resolveJoinSubmissionProfile({
495
+ profile = {},
496
+ profileSnapshot = null,
497
+ profileUpdate = null,
498
+ profilePatch = null,
499
+ } = {}) {
500
+ const normalizedProfile = normalizeProfile(profile);
501
+ const normalizedProfileSnapshot = normalizeProfile(profileSnapshot);
502
+ const appliedProfileUpdate = normalizeProfile(profileUpdate || profilePatch || {});
503
+ const baseProfile = Object.keys(normalizedProfileSnapshot).length > 0
504
+ ? normalizedProfileSnapshot
505
+ : normalizedProfile;
506
+
507
+ return {
508
+ baseProfile,
509
+ appliedProfileUpdate,
510
+ mergedProfile: mergeProfileState(baseProfile, appliedProfileUpdate),
511
+ preferProfileSnapshot:
512
+ Object.keys(normalizedProfileSnapshot).length > 0
513
+ || Object.keys(appliedProfileUpdate).length > 0,
514
+ };
515
+ }
516
+
517
+ function normalizeSearchMatchedField(field = {}, index = 0) {
518
+ return {
519
+ fieldId: normalizeText(field.fieldId, `field_${index + 1}`),
520
+ label: normalizeText(field.label, `Field ${index + 1}`),
521
+ matchType: normalizeText(field.matchType, 'exact'),
522
+ queryValue: field.queryValue ?? null,
523
+ candidateValue: field.candidateValue ?? null,
524
+ sharedValues: Array.isArray(field.sharedValues) ? normalizeStringList(field.sharedValues) : [],
525
+ contribution: normalizeNumber(field.contribution, 0),
526
+ };
527
+ }
528
+
529
+ function normalizeWorldSearchItem(item = {}, index = 0) {
530
+ const resolvedAgentId = normalizeText(item.agentId, normalizeText(item.playerId, `agent_${index + 1}`));
531
+ return {
532
+ agentId: resolvedAgentId,
533
+ playerId: normalizeText(item.playerId, resolvedAgentId),
534
+ membershipId: normalizeText(item.membershipId, null),
535
+ worldId: normalizeText(item.worldId, 'unknown-world'),
536
+ displayName: normalizeText(item.displayName, `Player ${index + 1}`),
537
+ headline: normalizeText(item.headline, null),
538
+ address: normalizeText(item.address, null),
539
+ online: item.online === true,
540
+ connectedAt: normalizeText(item.connectedAt, null),
541
+ lastHeartbeatAt: normalizeText(item.lastHeartbeatAt, null),
542
+ score: normalizeNumber(item.score, 0),
543
+ matchedFieldIds: normalizeStringList(item.matchedFieldIds),
544
+ matchedFields: Array.isArray(item.matchedFields)
545
+ ? item.matchedFields.map((field, fieldIndex) => normalizeSearchMatchedField(field, fieldIndex))
546
+ : [],
547
+ resultType: normalizeText(item.resultType, 'matched'),
548
+ reasonSummary: normalizeText(item.reasonSummary, ''),
549
+ joinedAt: normalizeText(item.joinedAt, null),
550
+ profileSummary: normalizeCandidateProfileSummary(item.profileSummary),
551
+ };
552
+ }
553
+
554
+ function normalizeWorldSearchResponse(payload = {}, { worldId = null, agentId = null } = {}) {
555
+ const items = Array.isArray(payload.items)
556
+ ? payload.items.map((item, index) => normalizeWorldSearchItem(item, index))
557
+ : [];
558
+
559
+ return {
560
+ worldId: normalizeText(payload.worldId, worldId || 'unknown-world'),
561
+ agentId: normalizeText(payload.agentId, agentId || null),
562
+ viewerMembershipId: normalizeText(payload.viewerMembershipId, null),
563
+ searchModel: normalizeSearchSchema(payload.searchModel || {}, {
564
+ worldId: normalizeText(payload.worldId, worldId || 'unknown-world'),
565
+ fallbackFields: [],
566
+ }),
567
+ searchInput: normalizeProfile(payload.searchInput && typeof payload.searchInput === 'object' ? payload.searchInput : {}),
568
+ onlineOnly: payload.onlineOnly !== false,
569
+ candidateSource: normalizeText(payload.candidateSource, 'active_memberships_online'),
570
+ limit: normalizeInteger(payload.limit, items.length),
571
+ totalMatches: normalizeInteger(payload.totalMatches, items.length),
572
+ status: normalizeText(payload.status, items.length > 0 ? 'search_ready' : 'no_matches'),
573
+ nextAction: normalizeText(
574
+ payload.nextAction,
575
+ items.length > 0 ? 'select_player_and_start_conversation' : 'broaden_search_or_wait',
576
+ ),
577
+ items,
578
+ };
579
+ }
580
+
581
+ function summarizeProfileValue(value) {
582
+ if (Array.isArray(value)) return joinAsNaturalLanguage(value.map((entry) => String(entry).trim()).filter(Boolean));
583
+ return normalizeText(value, '');
584
+ }
585
+
586
+ function summarizeProfileFields(fields = []) {
587
+ return fields
588
+ .map((field) => {
589
+ const value = summarizeProfileValue(field.value);
590
+ if (!value) return null;
591
+ return `${field.label}: ${value}`;
592
+ })
593
+ .filter(Boolean);
594
+ }
595
+
596
+ function buildCandidateDeliverySummaryLine(candidateSummary = {}, index = 0) {
597
+ return `${index + 1}. ${candidateSummary.summary}`;
598
+ }
599
+
600
+ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail = null, limit = null } = {}) {
601
+ if (candidateFeed?.candidateDelivery && typeof candidateFeed.candidateDelivery === 'object') {
602
+ return candidateFeed.candidateDelivery;
603
+ }
604
+ if (worldDetail?.candidateDelivery && typeof worldDetail.candidateDelivery === 'object') {
605
+ return worldDetail.candidateDelivery;
606
+ }
607
+ // Older responses may still omit candidateDelivery; keep the shared builder as
608
+ // a compatibility fallback, not the canonical orchestration owner.
609
+ return buildBackendCandidateDeliverySummary(candidateFeed, { worldDetail, limit });
610
+ }
611
+
612
+ function buildFieldLookup(worldDetail = {}) {
613
+ const detail = normalizeWorldDetail(worldDetail);
614
+ return new Map(
615
+ [...detail.requiredFields, ...detail.optionalFields].map((field) => [field.fieldId, field]),
616
+ );
617
+ }
618
+
619
+ function selectPromptFields(joinCheck = {}, worldDetail = {}, maxFieldsPerStep = 1) {
620
+ if (joinCheck.accepted) return [];
621
+
622
+ const fieldLookup = buildFieldLookup(worldDetail);
623
+ const orderedMissingFields = Array.isArray(joinCheck.missingFieldGuidance?.orderedMissingFields)
624
+ && joinCheck.missingFieldGuidance.orderedMissingFields.length > 0
625
+ ? joinCheck.missingFieldGuidance.orderedMissingFields
626
+ : (Array.isArray(joinCheck.missingFields) ? joinCheck.missingFields : []);
627
+
628
+ return orderedMissingFields.slice(0, Math.max(1, maxFieldsPerStep)).map((field, index) => {
629
+ const detailField = fieldLookup.get(field.fieldId) || {};
630
+ return normalizeField(
631
+ {
632
+ ...detailField,
633
+ ...field,
634
+ description: normalizeText(field.description, detailField.description || null),
635
+ examples: Array.isArray(detailField.examples) ? detailField.examples : field.examples,
636
+ constraints: detailField.constraints || field.constraints,
637
+ },
638
+ index,
639
+ { required: true },
640
+ );
641
+ });
642
+ }
643
+
644
+ function buildProfileFieldPrompt(field = {}, index = 0, total = 1) {
645
+ const examples = Array.isArray(field.examples) && field.examples.length > 0
646
+ ? ` Example: ${field.examples.map((example) => quoteExample(example)).join(' or ')}.`
647
+ : '';
648
+ const description = sentenceCase(
649
+ field.description || `Provide ${field.label} so the world can evaluate the profile`,
650
+ 'Provide this field so the world can evaluate the profile.',
651
+ );
652
+ const prefix = total > 1 ? `${index + 1}. ` : '';
653
+
654
+ return `${prefix}${field.label}. ${description}${examples}`;
655
+ }
656
+
657
+ function listProvidedRequiredFieldIds(worldDetail = {}, profile = {}, missingFieldIds = []) {
658
+ const missingSet = new Set(normalizeStringList(missingFieldIds));
659
+ return normalizeWorldDetail(worldDetail).requiredFields
660
+ .filter((field) => !missingSet.has(field.fieldId) && !isEmptyProfileValue(profile[field.fieldId]))
661
+ .map((field) => field.fieldId);
662
+ }
663
+
664
+ function buildProfileCollectionFollowUp(promptFields = []) {
665
+ if (promptFields.length === 0) {
666
+ return 'The current profile is already eligible. Continue with the next world step without restarting profile collection.';
667
+ }
668
+
669
+ const labels = promptFields.map((field) => field.label);
670
+ if (promptFields.length === 1) {
671
+ return `After the user answers ${labels[0]}, merge it into the saved profile draft and re-run join-check before asking anything else.`;
672
+ }
673
+
674
+ return `After the user answers ${joinAsNaturalLanguage(labels)}, merge those fields into the saved profile draft and re-run join-check before asking anything else.`;
675
+ }
676
+
677
+ function normalizeSelectionInput(selection) {
678
+ if (selection && typeof selection === 'object') {
679
+ const asWorldId = normalizeText(selection.worldId, null);
680
+ const asDisplayName = normalizeText(selection.displayName, null);
681
+ const asChoice = normalizeText(selection.selection || selection.choice || selection.value, null);
682
+ const text = asWorldId || asDisplayName || asChoice || '';
683
+ return {
684
+ raw: selection,
685
+ text,
686
+ normalized: normalizeLookupText(text),
687
+ index: /^\d+$/.test(String(text)) ? normalizeInteger(text, 0) : null,
688
+ };
689
+ }
690
+
691
+ const text = normalizeText(selection, '');
692
+ return {
693
+ raw: selection,
694
+ text,
695
+ normalized: normalizeLookupText(text),
696
+ index: /^\d+$/.test(String(text)) ? normalizeInteger(text, 0) : null,
697
+ };
698
+ }
699
+
700
+ function buildSelectionRetryContract(status, selection, items = [], matches = []) {
701
+ const choiceLabel = selection.text ? `"${selection.text}"` : 'that choice';
702
+ const retryWorlds = (matches.length > 0 ? matches : items).map((world) => normalizeWorldSummary(world));
703
+ const retrySummary = summarizeWorldChoices(retryWorlds);
704
+
705
+ if (status === 'ambiguous') {
706
+ return {
707
+ status,
708
+ selection: {
709
+ input: selection.text || null,
710
+ matchedBy: null,
711
+ worldId: null,
712
+ displayName: null,
713
+ },
714
+ candidateWorlds: retryWorlds,
715
+ orchestration: {
716
+ stage: 'post_setup_world_selection_retry',
717
+ system: 'The world choice matched multiple worlds. Show the narrowed list and ask the user to pick one exact world ID or display name.',
718
+ user: `The choice ${choiceLabel} is ambiguous. Matching worlds: ${joinAsNaturalLanguage(retrySummary)}. Ask the user to choose one exact world ID or display name.`,
719
+ followUp: 'Once the user confirms one exact world, fetch its detail and explain the required fields before join-check.',
720
+ },
721
+ };
722
+ }
723
+
724
+ return {
725
+ status: 'no_match',
726
+ selection: {
727
+ input: selection.text || null,
728
+ matchedBy: null,
729
+ worldId: null,
730
+ displayName: null,
731
+ },
732
+ candidateWorlds: retryWorlds,
733
+ orchestration: {
734
+ stage: 'post_setup_world_selection_retry',
735
+ system: 'The world choice did not match the available world directory. Re-list the worlds and ask the user to choose one by world ID or display name.',
736
+ user: retrySummary.length > 0
737
+ ? `I could not match ${choiceLabel} to an available world. Available worlds: ${joinAsNaturalLanguage(retrySummary)}. Ask the user to choose one by world ID or display name.`
738
+ : 'No worlds are currently available. Tell the user setup is complete but world selection cannot continue yet.',
739
+ followUp: 'Once the user chooses a valid world, confirm it, fetch the world detail, and explain the required fields before join-check.',
740
+ },
741
+ };
742
+ }
743
+
744
+ export function buildWorldSelectionPrompt(worldDirectory = {}) {
745
+ if (worldDirectory?.orchestration && typeof worldDirectory.orchestration === 'object') {
746
+ return worldDirectory.orchestration;
747
+ }
748
+ // Compatibility fallback for older directory payloads that predate
749
+ // backend-authored orchestration.
750
+ return buildBackendWorldSelectionPrompt(worldDirectory);
751
+ }
752
+
753
+ export function buildPostSetupWorldDirectory(payload = {}, { accountId = null } = {}) {
754
+ const items = Array.isArray(payload.items) ? payload.items.map((world) => normalizeWorldSummary(world)) : [];
755
+ const recommendedWorldId = items[0]?.worldId || null;
756
+ const pagination = payload.pagination && typeof payload.pagination === 'object'
757
+ ? {
758
+ page: normalizeInteger(payload.pagination.page, 1) || 1,
759
+ totalPages: normalizeInteger(payload.pagination.totalPages, 0),
760
+ totalCount: normalizeInteger(payload.pagination.totalCount, items.length),
761
+ }
762
+ : {
763
+ page: 1,
764
+ totalPages: items.length > 0 ? 1 : 0,
765
+ totalCount: items.length,
766
+ };
767
+ const sort = normalizeText(payload.sort, 'hot');
768
+
769
+ return {
770
+ status: 'ready',
771
+ source: 'product_shell',
772
+ accountId: normalizeText(accountId, null),
773
+ worldCount: pagination.totalCount,
774
+ recommendedWorldId,
775
+ items,
776
+ pagination,
777
+ sort,
778
+ orchestration: payload.orchestration && typeof payload.orchestration === 'object'
779
+ ? payload.orchestration
780
+ : buildWorldSelectionPrompt({ items, recommendedWorldId }),
781
+ };
782
+ }
783
+
784
+ export function resolveWorldSelection(worldDirectory = {}, selection = null) {
785
+ return resolveBackendWorldSelection(worldDirectory, selection);
786
+ }
787
+
788
+ async function fetchJson(fetchImpl, url, init = {}) {
789
+ let response;
790
+ try {
791
+ response = await fetchImpl(url, init);
792
+ } catch (error) {
793
+ throw createRuntimeBoundaryError({
794
+ code: 'relay_fetch_failed',
795
+ category: 'transport',
796
+ status: 502,
797
+ message: `fetch failed: ${error?.message || String(error)}`,
798
+ publicMessage: 'relay fetch failed',
799
+ recoverable: true,
800
+ context: {
801
+ fetchUrl: url,
802
+ fetchMethod: init?.method || 'GET',
803
+ },
804
+ cause: error,
805
+ });
806
+ }
807
+ let body = null;
808
+
809
+ try {
810
+ body = await response.json();
811
+ } catch {
812
+ body = null;
813
+ }
814
+
815
+ return { ok: response.ok, status: response.status, body };
816
+ }
817
+
818
+ function normalizeRelayHttpBaseUrl(serverUrl) {
819
+ const parsed = new URL(serverUrl);
820
+ if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
821
+ if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
822
+ parsed.pathname = '';
823
+ parsed.search = '';
824
+ parsed.hash = '';
825
+ return parsed.toString().replace(/\/$/, '');
826
+ }
827
+
828
+ function inferHttpErrorCategory(status) {
829
+ if (status === 401) return 'auth';
830
+ if (status === 403) return 'policy';
831
+ if (status === 409) return 'conflict';
832
+ if (status >= 400 && status < 500) return 'input';
833
+ return 'runtime';
834
+ }
835
+
836
+ function createProductShellHttpError(action, response, { accountId = null, worldId = null } = {}) {
837
+ const backendCode = normalizeText(response?.body?.error, null);
838
+ const backendMessage = normalizeText(response?.body?.message, `claworld product-shell ${action} failed`);
839
+
840
+ return createRuntimeBoundaryError({
841
+ code: backendCode || `claworld_product_shell_${action}_failed`,
842
+ category: inferHttpErrorCategory(response?.status),
843
+ status: response?.status ?? 500,
844
+ message: `claworld product-shell ${action} failed: ${response?.status ?? 500}`,
845
+ publicMessage: backendMessage,
846
+ recoverable: Number(response?.status) >= 400 && Number(response?.status) < 500,
847
+ context: {
848
+ action,
849
+ accountId,
850
+ ...(worldId ? { worldId } : {}),
851
+ httpStatus: response?.status ?? 500,
852
+ backendCode,
853
+ backendMessage,
854
+ },
855
+ });
856
+ }
857
+
858
+ function normalizeBroadcastRequestPayload(payload = {}, { message = null } = {}) {
859
+ const normalizedPayload = payload && typeof payload === 'object' && !Array.isArray(payload)
860
+ ? { ...payload }
861
+ : {};
862
+ const normalizedMessage = normalizeText(message, null);
863
+ if (!normalizedMessage) return normalizedPayload;
864
+ return {
865
+ ...normalizedPayload,
866
+ ...(normalizedPayload.text == null ? { text: normalizedMessage } : {}),
867
+ };
868
+ }
869
+
870
+ function normalizeBroadcastResult(payload = {}) {
871
+ const requests = Array.isArray(payload.requests)
872
+ ? payload.requests.map((request) => ({
873
+ agentId: normalizeText(request.agentId, null),
874
+ status: normalizeText(request.status, 'created'),
875
+ chatRequest: request.chatRequest && typeof request.chatRequest === 'object' && !Array.isArray(request.chatRequest)
876
+ ? request.chatRequest
877
+ : null,
878
+ }))
879
+ : [];
880
+ const failures = Array.isArray(payload.failures)
881
+ ? payload.failures.map((failure) => ({
882
+ agentId: normalizeText(failure.agentId, null),
883
+ status: normalizeText(failure.status, 'failed'),
884
+ httpStatus: normalizeInteger(failure.httpStatus, 0),
885
+ error: normalizeText(failure.error, null),
886
+ reason: normalizeText(failure.reason, null),
887
+ message: normalizeText(failure.message, null),
888
+ }))
889
+ : [];
890
+ return {
891
+ status: normalizeText(payload.status, 'requests_created'),
892
+ worldId: normalizeText(payload.worldId, null),
893
+ senderAgentId: normalizeText(payload.senderAgentId, null),
894
+ senderRole: normalizeText(payload.senderRole, 'none'),
895
+ audience: normalizeText(payload.audience, 'members'),
896
+ excludeSelf: payload.excludeSelf !== false,
897
+ eligibility: normalizeText(payload.eligibility, 'active'),
898
+ broadcastId: normalizeText(payload.broadcastId, null),
899
+ totalTargets: normalizeInteger(payload.totalTargets, 0),
900
+ createdCount: normalizeInteger(payload.createdCount, requests.length),
901
+ failedCount: normalizeInteger(payload.failedCount, failures.length),
902
+ nextAction: normalizeText(payload.nextAction, 'recipients_review_pending_requests'),
903
+ requests,
904
+ failures,
905
+ };
906
+ }
907
+
908
+ export async function fetchWorldDetail({
909
+ cfg = {},
910
+ accountId = null,
911
+ runtimeConfig = null,
912
+ worldId = null,
913
+ fetchImpl,
914
+ logger = console,
915
+ } = {}) {
916
+ if (typeof fetchImpl !== 'function') {
917
+ throw new Error('fetch is unavailable for claworld product-shell detail helper');
918
+ }
919
+
920
+ const resolvedWorldId = normalizeText(worldId, null);
921
+ if (!resolvedWorldId) {
922
+ throw new Error('claworld product-shell detail helper requires worldId');
923
+ }
924
+
925
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
926
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
927
+ const detail = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}`, {
928
+ headers: {
929
+ accept: 'application/json',
930
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
931
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
932
+ },
933
+ });
934
+
935
+ if (!detail.ok) {
936
+ logger.error?.('[claworld:product-shell] world detail fetch failed', {
937
+ status: detail.status,
938
+ worldId: resolvedWorldId,
939
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
940
+ body: detail.body,
941
+ });
942
+ throw new Error(`claworld product-shell world detail fetch failed: ${detail.status}`);
943
+ }
944
+
945
+ return normalizeWorldDetail(detail.body);
946
+ }
947
+
948
+ export async function fetchWorldJoinCheck({
949
+ cfg = {},
950
+ accountId = null,
951
+ runtimeConfig = null,
952
+ worldId = null,
953
+ profile = {},
954
+ maxFieldsPerStep = null,
955
+ fetchImpl,
956
+ logger = console,
957
+ } = {}) {
958
+ if (typeof fetchImpl !== 'function') {
959
+ throw new Error('fetch is unavailable for claworld product-shell join-check helper');
960
+ }
961
+
962
+ const resolvedWorldId = normalizeText(worldId, null);
963
+ if (!resolvedWorldId) {
964
+ throw new Error('claworld product-shell join-check helper requires worldId');
965
+ }
966
+
967
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
968
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
969
+ const normalizedProfile = normalizeProfile(profile);
970
+ const joinCheck = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/join-check`, {
971
+ method: 'POST',
972
+ headers: {
973
+ accept: 'application/json',
974
+ 'content-type': 'application/json',
975
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
976
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
977
+ },
978
+ body: JSON.stringify({
979
+ profile: normalizedProfile,
980
+ ...(maxFieldsPerStep == null ? {} : { maxFieldsPerStep }),
981
+ }),
982
+ });
983
+
984
+ if (!joinCheck.ok && joinCheck.status !== 422) {
985
+ logger.error?.('[claworld:product-shell] world join-check failed', {
986
+ status: joinCheck.status,
987
+ worldId: resolvedWorldId,
988
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
989
+ body: joinCheck.body,
990
+ });
991
+ throw new Error(`claworld product-shell join-check failed: ${joinCheck.status}`);
992
+ }
993
+
994
+ return normalizeJoinCheckResponse(joinCheck.body, {
995
+ worldId: resolvedWorldId,
996
+ profile: normalizedProfile,
997
+ });
998
+ }
999
+
1000
+ export async function joinWorld({
1001
+ cfg = {},
1002
+ accountId = null,
1003
+ runtimeConfig = null,
1004
+ worldId = null,
1005
+ agentId = null,
1006
+ profile = {},
1007
+ profileSnapshot = null,
1008
+ maxFieldsPerStep = null,
1009
+ fetchImpl,
1010
+ logger = console,
1011
+ } = {}) {
1012
+ if (typeof fetchImpl !== 'function') {
1013
+ throw new Error('fetch is unavailable for claworld product-shell join helper');
1014
+ }
1015
+
1016
+ const resolvedWorldId = normalizeText(worldId, null);
1017
+ if (!resolvedWorldId) {
1018
+ throw new Error('claworld product-shell join helper requires worldId');
1019
+ }
1020
+
1021
+ const resolvedAgentId = normalizeText(agentId, null);
1022
+ if (!resolvedAgentId) {
1023
+ throw new Error('claworld product-shell join helper requires agentId');
1024
+ }
1025
+
1026
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
1027
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
1028
+ const requestBody = buildJoinRequestBody({
1029
+ agentId: resolvedAgentId,
1030
+ profile,
1031
+ profileSnapshot,
1032
+ });
1033
+ if (maxFieldsPerStep != null) {
1034
+ requestBody.maxFieldsPerStep = maxFieldsPerStep;
1035
+ }
1036
+ const joinResult = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/join`, {
1037
+ method: 'POST',
1038
+ headers: {
1039
+ accept: 'application/json',
1040
+ 'content-type': 'application/json',
1041
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1042
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
1043
+ },
1044
+ body: JSON.stringify(requestBody),
1045
+ });
1046
+
1047
+ if (!joinResult.ok) {
1048
+ logger.error?.('[claworld:product-shell] world join failed', {
1049
+ status: joinResult.status,
1050
+ worldId: resolvedWorldId,
1051
+ agentId: resolvedAgentId,
1052
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1053
+ body: joinResult.body,
1054
+ });
1055
+ throw new Error(`claworld product-shell world join failed: ${joinResult.status}`);
1056
+ }
1057
+
1058
+ return normalizeWorldJoinResponse(joinResult.body, {
1059
+ worldId: resolvedWorldId,
1060
+ agentId: resolvedAgentId,
1061
+ });
1062
+ }
1063
+
1064
+ export async function submitWorldJoin({
1065
+ cfg = {},
1066
+ accountId = null,
1067
+ runtimeConfig = null,
1068
+ worldId = null,
1069
+ agentId = null,
1070
+ profile = {},
1071
+ profileSnapshot = null,
1072
+ profileUpdate = null,
1073
+ profilePatch = null,
1074
+ worldDetail = null,
1075
+ maxFieldsPerStep = 1,
1076
+ fetchImpl,
1077
+ logger = console,
1078
+ } = {}) {
1079
+ if (typeof fetchImpl !== 'function') {
1080
+ throw new Error('fetch is unavailable for claworld product-shell join helper');
1081
+ }
1082
+
1083
+ const resolvedWorldId = normalizeText(worldId, null);
1084
+ if (!resolvedWorldId) {
1085
+ throw new Error('claworld product-shell join helper requires worldId');
1086
+ }
1087
+
1088
+ const resolvedAgentId = normalizeText(agentId, null);
1089
+ if (!resolvedAgentId) {
1090
+ throw new Error('claworld product-shell join helper requires agentId');
1091
+ }
1092
+
1093
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
1094
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
1095
+ const joinProfile = resolveJoinSubmissionProfile({
1096
+ profile,
1097
+ profileSnapshot,
1098
+ profileUpdate,
1099
+ profilePatch,
1100
+ });
1101
+ const requestBody = buildJoinRequestBody({
1102
+ agentId: resolvedAgentId,
1103
+ profile: joinProfile.mergedProfile,
1104
+ profileSnapshot: joinProfile.preferProfileSnapshot ? joinProfile.mergedProfile : null,
1105
+ });
1106
+ if (maxFieldsPerStep != null) {
1107
+ requestBody.maxFieldsPerStep = maxFieldsPerStep;
1108
+ }
1109
+ const joinResult = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/join`, {
1110
+ method: 'POST',
1111
+ headers: {
1112
+ accept: 'application/json',
1113
+ 'content-type': 'application/json',
1114
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1115
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
1116
+ },
1117
+ body: JSON.stringify(requestBody),
1118
+ });
1119
+
1120
+ if (!joinResult.ok && joinResult.status !== 422) {
1121
+ logger.error?.('[claworld:product-shell] world join failed', {
1122
+ status: joinResult.status,
1123
+ worldId: resolvedWorldId,
1124
+ agentId: resolvedAgentId,
1125
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1126
+ body: joinResult.body,
1127
+ });
1128
+ throw new Error(`claworld product-shell world join failed: ${joinResult.status}`);
1129
+ }
1130
+
1131
+ if (joinResult.status === 422) {
1132
+ const detail = worldDetail
1133
+ ? normalizeWorldDetail(worldDetail)
1134
+ : await fetchWorldDetail({
1135
+ cfg,
1136
+ accountId,
1137
+ runtimeConfig: resolvedRuntimeConfig,
1138
+ worldId: resolvedWorldId,
1139
+ fetchImpl,
1140
+ logger,
1141
+ });
1142
+ const joinCheck = normalizeJoinCheckResponse(joinResult.body?.joinCheck || joinResult.body, {
1143
+ worldId: resolvedWorldId,
1144
+ profile: joinProfile.mergedProfile,
1145
+ });
1146
+ const profileCollectionFlow = joinCheck.profileCollectionFlow && typeof joinCheck.profileCollectionFlow === 'object'
1147
+ ? {
1148
+ ...joinCheck.profileCollectionFlow,
1149
+ worldDetail: detail,
1150
+ joinCheck,
1151
+ profile: joinCheck.normalizedProfile,
1152
+ appliedProfileUpdate: joinProfile.appliedProfileUpdate,
1153
+ }
1154
+ : {
1155
+ ...buildBackendWorldProfileCollectionFlow({
1156
+ worldDetail: detail,
1157
+ joinCheck,
1158
+ profile: joinCheck.normalizedProfile,
1159
+ maxFieldsPerStep,
1160
+ }),
1161
+ worldDetail: detail,
1162
+ joinCheck,
1163
+ profile: joinCheck.normalizedProfile,
1164
+ appliedProfileUpdate: joinProfile.appliedProfileUpdate,
1165
+ };
1166
+
1167
+ return {
1168
+ status: 'needs_profile',
1169
+ worldId: resolvedWorldId,
1170
+ agentId: resolvedAgentId,
1171
+ membershipStatus: normalizeText(joinResult.body?.membershipStatus, 'inactive'),
1172
+ searchEnabled: false,
1173
+ missingRequiredFields: projectMissingRequiredFields(joinCheck.missingFields),
1174
+ nextMissingField: joinCheck.nextMissingField
1175
+ ? projectMissingRequiredFields([joinCheck.nextMissingField])[0]
1176
+ : null,
1177
+ normalizedProfile: profileCollectionFlow.profile,
1178
+ nextAction: 'complete_required_profile_fields_then_retry_join',
1179
+ joinCheck,
1180
+ profileCollectionFlow,
1181
+ orchestration: profileCollectionFlow.orchestration || null,
1182
+ providedRequiredFieldIds: profileCollectionFlow.providedRequiredFieldIds,
1183
+ promptFields: profileCollectionFlow.promptFields,
1184
+ remainingRequiredFieldCount: profileCollectionFlow.remainingRequiredFieldCount,
1185
+ revalidationCheckpoint: profileCollectionFlow.revalidationCheckpoint,
1186
+ };
1187
+ }
1188
+
1189
+ const normalizedJoinResult = normalizeWorldJoinResponse(joinResult.body, {
1190
+ worldId: resolvedWorldId,
1191
+ agentId: resolvedAgentId,
1192
+ });
1193
+
1194
+ return {
1195
+ status: normalizedJoinResult.membershipStatus === 'active' ? 'activated' : 'accepted',
1196
+ worldId: normalizedJoinResult.worldId,
1197
+ agentId: normalizedJoinResult.agentId,
1198
+ membershipStatus: normalizedJoinResult.membershipStatus,
1199
+ searchEnabled: normalizedJoinResult.membershipStatus === 'active',
1200
+ membership: normalizedJoinResult.membership,
1201
+ nextStageSummary: normalizedJoinResult.nextStageSummary,
1202
+ orchestration: normalizedJoinResult.orchestration || null,
1203
+ };
1204
+ }
1205
+
1206
+ export async function fetchWorldSearch({
1207
+ cfg = {},
1208
+ accountId = null,
1209
+ runtimeConfig = null,
1210
+ worldId = null,
1211
+ agentId = null,
1212
+ query = {},
1213
+ limit = null,
1214
+ fetchImpl,
1215
+ logger = console,
1216
+ } = {}) {
1217
+ if (typeof fetchImpl !== 'function') {
1218
+ throw new Error('fetch is unavailable for claworld product-shell world search helper');
1219
+ }
1220
+
1221
+ const resolvedWorldId = normalizeText(worldId, null);
1222
+ if (!resolvedWorldId) {
1223
+ throw new Error('claworld product-shell world search helper requires worldId');
1224
+ }
1225
+
1226
+ const resolvedAgentId = normalizeText(agentId, null);
1227
+ if (!resolvedAgentId) {
1228
+ throw new Error('claworld product-shell world search helper requires agentId');
1229
+ }
1230
+
1231
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
1232
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
1233
+ const normalizedQuery = normalizeProfile(query);
1234
+ const searchResult = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/search`, {
1235
+ method: 'POST',
1236
+ headers: {
1237
+ accept: 'application/json',
1238
+ 'content-type': 'application/json',
1239
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1240
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
1241
+ },
1242
+ body: JSON.stringify({
1243
+ agentId: resolvedAgentId,
1244
+ query: normalizedQuery,
1245
+ ...(limit != null ? { limit } : {}),
1246
+ }),
1247
+ });
1248
+
1249
+ if (!searchResult.ok) {
1250
+ logger.error?.('[claworld:product-shell] world search failed', {
1251
+ status: searchResult.status,
1252
+ worldId: resolvedWorldId,
1253
+ agentId: resolvedAgentId,
1254
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1255
+ body: searchResult.body,
1256
+ });
1257
+ throw new Error(`claworld product-shell world search failed: ${searchResult.status}`);
1258
+ }
1259
+
1260
+ return normalizeWorldSearchResponse(searchResult.body, {
1261
+ worldId: resolvedWorldId,
1262
+ agentId: resolvedAgentId,
1263
+ });
1264
+ }
1265
+
1266
+ export async function submitWorldSearch({
1267
+ cfg = {},
1268
+ accountId = null,
1269
+ runtimeConfig = null,
1270
+ worldId = null,
1271
+ agentId = null,
1272
+ query = {},
1273
+ limit = null,
1274
+ fetchImpl,
1275
+ logger = console,
1276
+ } = {}) {
1277
+ if (typeof fetchImpl !== 'function') {
1278
+ throw new Error('fetch is unavailable for claworld product-shell world search helper');
1279
+ }
1280
+
1281
+ const resolvedWorldId = normalizeText(worldId, null);
1282
+ if (!resolvedWorldId) {
1283
+ throw new Error('claworld product-shell world search helper requires worldId');
1284
+ }
1285
+
1286
+ const resolvedAgentId = normalizeText(agentId, null);
1287
+ if (!resolvedAgentId) {
1288
+ throw new Error('claworld product-shell world search helper requires agentId');
1289
+ }
1290
+
1291
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
1292
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
1293
+ const normalizedQuery = normalizeProfile(query);
1294
+ const searchResult = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/search`, {
1295
+ method: 'POST',
1296
+ headers: {
1297
+ accept: 'application/json',
1298
+ 'content-type': 'application/json',
1299
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1300
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
1301
+ },
1302
+ body: JSON.stringify({
1303
+ agentId: resolvedAgentId,
1304
+ query: normalizedQuery,
1305
+ ...(limit != null ? { limit } : {}),
1306
+ }),
1307
+ });
1308
+
1309
+ if (!searchResult.ok && searchResult.status !== 409) {
1310
+ logger.error?.('[claworld:product-shell] world search failed', {
1311
+ status: searchResult.status,
1312
+ worldId: resolvedWorldId,
1313
+ agentId: resolvedAgentId,
1314
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1315
+ body: searchResult.body,
1316
+ });
1317
+ throw new Error(`claworld product-shell world search failed: ${searchResult.status}`);
1318
+ }
1319
+
1320
+ if (searchResult.status === 409) {
1321
+ return {
1322
+ status: 'not_joined',
1323
+ worldId: resolvedWorldId,
1324
+ agentId: resolvedAgentId,
1325
+ searchEnabled: false,
1326
+ results: [],
1327
+ query: normalizedQuery,
1328
+ limit: normalizeInteger(limit, 10),
1329
+ message: 'Join the world successfully before using world search.',
1330
+ };
1331
+ }
1332
+
1333
+ const normalizedSearch = normalizeWorldSearchResponse(searchResult.body, {
1334
+ worldId: resolvedWorldId,
1335
+ agentId: resolvedAgentId,
1336
+ });
1337
+
1338
+ return {
1339
+ ...normalizedSearch,
1340
+ status: normalizedSearch.items.length > 0 ? 'ready' : 'empty',
1341
+ searchEnabled: true,
1342
+ results: normalizedSearch.items,
1343
+ query: normalizedSearch.searchInput,
1344
+ };
1345
+ }
1346
+
1347
+ export async function broadcastWorld({
1348
+ cfg = {},
1349
+ accountId = null,
1350
+ runtimeConfig = null,
1351
+ worldId = null,
1352
+ agentId = null,
1353
+ message = null,
1354
+ payload = {},
1355
+ audience = null,
1356
+ excludeSelf = null,
1357
+ fetchImpl,
1358
+ logger = console,
1359
+ } = {}) {
1360
+ if (typeof fetchImpl !== 'function') {
1361
+ throw new Error('fetch is unavailable for claworld product-shell world broadcast helper');
1362
+ }
1363
+
1364
+ const resolvedWorldId = normalizeText(worldId, null);
1365
+ if (!resolvedWorldId) {
1366
+ throw new Error('claworld product-shell world broadcast helper requires worldId');
1367
+ }
1368
+
1369
+ const resolvedAgentId = normalizeText(agentId, null);
1370
+ if (!resolvedAgentId) {
1371
+ throw new Error('claworld product-shell world broadcast helper requires agentId');
1372
+ }
1373
+
1374
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
1375
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
1376
+ const requestBody = {
1377
+ agentId: resolvedAgentId,
1378
+ payload: normalizeBroadcastRequestPayload(payload, { message }),
1379
+ ...(audience ? { audience } : {}),
1380
+ ...(excludeSelf == null ? {} : { excludeSelf: excludeSelf === true }),
1381
+ };
1382
+ const broadcastResult = await fetchJson(
1383
+ fetchImpl,
1384
+ `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/broadcast`,
1385
+ {
1386
+ method: 'POST',
1387
+ headers: {
1388
+ accept: 'application/json',
1389
+ 'content-type': 'application/json',
1390
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1391
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
1392
+ },
1393
+ body: JSON.stringify(requestBody),
1394
+ },
1395
+ );
1396
+
1397
+ if (!broadcastResult.ok) {
1398
+ logger.error?.('[claworld:product-shell] world broadcast failed', {
1399
+ status: broadcastResult.status,
1400
+ worldId: resolvedWorldId,
1401
+ agentId: resolvedAgentId,
1402
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1403
+ body: broadcastResult.body,
1404
+ });
1405
+ throw createProductShellHttpError('world_broadcast', broadcastResult, {
1406
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1407
+ worldId: resolvedWorldId,
1408
+ });
1409
+ }
1410
+
1411
+ return normalizeBroadcastResult(broadcastResult.body);
1412
+ }
1413
+
1414
+ export async function fetchWorldCandidateFeed({
1415
+ cfg = {},
1416
+ accountId = null,
1417
+ runtimeConfig = null,
1418
+ worldId = null,
1419
+ agentId = null,
1420
+ limit = null,
1421
+ fetchImpl,
1422
+ logger = console,
1423
+ } = {}) {
1424
+ if (typeof fetchImpl !== 'function') {
1425
+ throw new Error('fetch is unavailable for claworld product-shell candidate feed helper');
1426
+ }
1427
+
1428
+ const resolvedWorldId = normalizeText(worldId, null);
1429
+ if (!resolvedWorldId) {
1430
+ throw new Error('claworld product-shell candidate feed helper requires worldId');
1431
+ }
1432
+
1433
+ const resolvedAgentId = normalizeText(agentId, null);
1434
+ if (!resolvedAgentId) {
1435
+ throw new Error('claworld product-shell candidate feed helper requires agentId');
1436
+ }
1437
+
1438
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
1439
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
1440
+ const requestUrl = new URL(`${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/candidates`);
1441
+ requestUrl.searchParams.set('agentId', resolvedAgentId);
1442
+ const normalizedLimit = normalizeInteger(limit, 0);
1443
+ if (normalizedLimit > 0) {
1444
+ requestUrl.searchParams.set('limit', String(normalizedLimit));
1445
+ }
1446
+ const candidateFeed = await fetchJson(fetchImpl, requestUrl.toString(), {
1447
+ headers: {
1448
+ accept: 'application/json',
1449
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1450
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
1451
+ },
1452
+ });
1453
+
1454
+ if (!candidateFeed.ok) {
1455
+ logger.error?.('[claworld:product-shell] candidate feed fetch failed', {
1456
+ status: candidateFeed.status,
1457
+ worldId: resolvedWorldId,
1458
+ agentId: resolvedAgentId,
1459
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1460
+ body: candidateFeed.body,
1461
+ });
1462
+ throw new Error(`claworld product-shell candidate feed failed: ${candidateFeed.status}`);
1463
+ }
1464
+
1465
+ return normalizeCandidateFeedResponse(candidateFeed.body, {
1466
+ worldId: resolvedWorldId,
1467
+ agentId: resolvedAgentId,
1468
+ });
1469
+ }
1470
+
1471
+ function buildFieldStepPrompt(field = {}, index = 0, total = 1) {
1472
+ const examples = Array.isArray(field.examples) && field.examples.length > 0
1473
+ ? ` Example: ${field.examples.map((example) => quoteExample(example)).join(' or ')}.`
1474
+ : '';
1475
+ const description = sentenceCase(
1476
+ field.description || `Provide ${field.label} so the world can evaluate the profile`,
1477
+ 'Provide this field so the world can evaluate the profile.',
1478
+ );
1479
+
1480
+ return `Step ${index + 1} of ${total}: ${field.label}. ${description}${examples}`;
1481
+ }
1482
+
1483
+ export function buildRequiredFieldExplanation(worldDetail = {}) {
1484
+ if (worldDetail?.requiredFieldExplanation && typeof worldDetail.requiredFieldExplanation === 'object') {
1485
+ return worldDetail.requiredFieldExplanation;
1486
+ }
1487
+ // Compatibility fallback for older world-detail payloads that do not yet
1488
+ // carry backend-authored required-field explanation.
1489
+ return buildBackendRequiredFieldExplanation(worldDetail);
1490
+ }
1491
+
1492
+ export async function resolveWorldProfileCollectionFlow({
1493
+ cfg = {},
1494
+ accountId = null,
1495
+ runtimeConfig = null,
1496
+ worldId = null,
1497
+ worldDetail = null,
1498
+ profile = {},
1499
+ profileUpdate = null,
1500
+ profilePatch = null,
1501
+ maxFieldsPerStep = 1,
1502
+ fetchImpl,
1503
+ logger = console,
1504
+ } = {}) {
1505
+ const appliedProfileUpdate = normalizeProfile(profileUpdate || profilePatch || {});
1506
+ const mergedProfile = mergeProfileState(profile, appliedProfileUpdate);
1507
+ const detail = worldDetail
1508
+ ? normalizeWorldDetail(worldDetail)
1509
+ : await fetchWorldDetail({
1510
+ cfg,
1511
+ accountId,
1512
+ runtimeConfig,
1513
+ worldId,
1514
+ fetchImpl,
1515
+ logger,
1516
+ });
1517
+ const resolvedWorldId = normalizeText(worldId || detail.worldId, detail.worldId);
1518
+ if (!resolvedWorldId) {
1519
+ throw new Error('claworld product-shell profile collection flow requires worldId');
1520
+ }
1521
+
1522
+ const joinCheck = await fetchWorldJoinCheck({
1523
+ cfg,
1524
+ accountId,
1525
+ runtimeConfig,
1526
+ worldId: resolvedWorldId,
1527
+ profile: mergedProfile,
1528
+ maxFieldsPerStep,
1529
+ fetchImpl,
1530
+ logger,
1531
+ });
1532
+ const backendFlow = joinCheck.profileCollectionFlow && typeof joinCheck.profileCollectionFlow === 'object'
1533
+ ? joinCheck.profileCollectionFlow
1534
+ : buildBackendWorldProfileCollectionFlow({
1535
+ worldDetail: detail,
1536
+ joinCheck,
1537
+ profile: joinCheck.normalizedProfile,
1538
+ maxFieldsPerStep,
1539
+ });
1540
+
1541
+ return {
1542
+ ...backendFlow,
1543
+ appliedProfileUpdate,
1544
+ worldDetail: detail,
1545
+ joinCheck,
1546
+ worldId: resolvedWorldId,
1547
+ displayName: detail.displayName,
1548
+ profile: joinCheck.normalizedProfile,
1549
+ accepted: joinCheck.accepted,
1550
+ status: joinCheck.status,
1551
+ };
1552
+ }
1553
+
1554
+ export async function resolveWorldSelectionFlow({
1555
+ cfg = {},
1556
+ accountId = null,
1557
+ runtimeConfig = null,
1558
+ worldDirectory = null,
1559
+ selection = null,
1560
+ profile = {},
1561
+ maxFieldsPerStep = 1,
1562
+ fetchImpl,
1563
+ logger = console,
1564
+ } = {}) {
1565
+ const directory = worldDirectory && Array.isArray(worldDirectory.items)
1566
+ ? buildPostSetupWorldDirectory(worldDirectory, { accountId })
1567
+ : await (async () => {
1568
+ if (typeof fetchImpl !== 'function') {
1569
+ throw new Error('fetch is unavailable for claworld product-shell world flow');
1570
+ }
1571
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
1572
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
1573
+ const worlds = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds`, {
1574
+ headers: {
1575
+ accept: 'application/json',
1576
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1577
+ ...(resolvedRuntimeConfig.relay?.credentialToken ? { 'x-relay-token': resolvedRuntimeConfig.relay.credentialToken } : {}),
1578
+ },
1579
+ });
1580
+
1581
+ if (!worlds.ok) {
1582
+ logger.error?.('[claworld:product-shell] world directory fetch failed during selection flow', {
1583
+ status: worlds.status,
1584
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1585
+ body: worlds.body,
1586
+ });
1587
+ throw new Error(`claworld product-shell world fetch failed: ${worlds.status}`);
1588
+ }
1589
+
1590
+ return buildPostSetupWorldDirectory(worlds.body, {
1591
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
1592
+ });
1593
+ })();
1594
+
1595
+ const resolvedSelection = resolveWorldSelection(directory, selection);
1596
+ if (resolvedSelection.status !== 'selected') {
1597
+ return {
1598
+ ...resolvedSelection,
1599
+ worldDirectory: directory,
1600
+ };
1601
+ }
1602
+
1603
+ const worldDetail = await fetchWorldDetail({
1604
+ cfg,
1605
+ accountId,
1606
+ runtimeConfig,
1607
+ worldId: resolvedSelection.selectedWorld.worldId,
1608
+ fetchImpl,
1609
+ logger,
1610
+ });
1611
+ const requiredFieldExplanation = buildRequiredFieldExplanation(worldDetail);
1612
+ const profileCollectionFlow = await resolveWorldProfileCollectionFlow({
1613
+ cfg,
1614
+ accountId,
1615
+ runtimeConfig,
1616
+ worldId: resolvedSelection.selectedWorld.worldId,
1617
+ worldDetail,
1618
+ profile,
1619
+ maxFieldsPerStep,
1620
+ fetchImpl,
1621
+ logger,
1622
+ });
1623
+
1624
+ return {
1625
+ status: 'selected',
1626
+ source: 'product_shell',
1627
+ worldDirectory: directory,
1628
+ selection: resolvedSelection.selection,
1629
+ selectedWorld: resolvedSelection.selectedWorld,
1630
+ worldDetail,
1631
+ requiredFieldExplanation,
1632
+ profileCollectionFlow,
1633
+ orchestration: resolvedSelection.orchestration || null,
1634
+ };
1635
+ }
1636
+
1637
+ export async function resolveWorldJoinFlow({
1638
+ cfg = {},
1639
+ accountId = null,
1640
+ runtimeConfig = null,
1641
+ worldId = null,
1642
+ worldDetail = null,
1643
+ agentId = null,
1644
+ profile = {},
1645
+ profileSnapshot = null,
1646
+ limit = null,
1647
+ fetchImpl,
1648
+ logger = console,
1649
+ } = {}) {
1650
+ const detail = worldDetail
1651
+ ? normalizeWorldDetail(worldDetail)
1652
+ : await fetchWorldDetail({
1653
+ cfg,
1654
+ accountId,
1655
+ runtimeConfig,
1656
+ worldId,
1657
+ fetchImpl,
1658
+ logger,
1659
+ });
1660
+ const resolvedWorldId = normalizeText(worldId || detail.worldId, detail.worldId);
1661
+ if (!resolvedWorldId) {
1662
+ throw new Error('claworld product-shell join flow requires worldId');
1663
+ }
1664
+
1665
+ const joinResult = await joinWorld({
1666
+ cfg,
1667
+ accountId,
1668
+ runtimeConfig,
1669
+ worldId: resolvedWorldId,
1670
+ agentId,
1671
+ profile,
1672
+ profileSnapshot,
1673
+ fetchImpl,
1674
+ logger,
1675
+ });
1676
+ const candidateFeed = await fetchWorldCandidateFeed({
1677
+ cfg,
1678
+ accountId,
1679
+ runtimeConfig,
1680
+ worldId: resolvedWorldId,
1681
+ agentId: joinResult.agentId || agentId,
1682
+ limit,
1683
+ fetchImpl,
1684
+ logger,
1685
+ });
1686
+ const candidateDelivery = buildCandidateDeliverySummary(candidateFeed, {
1687
+ worldDetail: detail,
1688
+ limit,
1689
+ });
1690
+ const joinOrchestration = joinResult.orchestration && typeof joinResult.orchestration === 'object'
1691
+ ? joinResult.orchestration
1692
+ : buildBackendWorldJoinOutcomeOrchestration({
1693
+ worldDetail: detail,
1694
+ joinResult,
1695
+ });
1696
+
1697
+ return {
1698
+ status: candidateDelivery.status,
1699
+ source: 'product_shell',
1700
+ worldId: resolvedWorldId,
1701
+ displayName: detail.displayName,
1702
+ agentId: joinResult.agentId || normalizeText(agentId, null),
1703
+ worldDetail: detail,
1704
+ membershipStatus: joinResult.membershipStatus,
1705
+ membership: joinResult.membership,
1706
+ nextStageSummary: joinResult.nextStageSummary,
1707
+ joinResult,
1708
+ candidateFeed,
1709
+ candidateDelivery,
1710
+ orchestration: joinOrchestration,
1711
+ };
1712
+ }