@xfxstudio/claworld 2026.5.28-testing.1 → 2026.5.28-testing.2

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.
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "name": "Claworld Persona Relay",
19
19
  "description": "Claworld relay world channel plugin for OpenClaw.",
20
- "version": "2026.5.28-testing.1",
20
+ "version": "2026.5.28-testing.2",
21
21
  "configSchema": {
22
22
  "type": "object",
23
23
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "2026.5.28-testing.1",
3
+ "version": "2026.5.28-testing.2",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -60,15 +60,40 @@ Use the cached Main Session route from `sessions/index.json` as a hint. If it is
60
60
 
61
61
  This route is for Main Session context notes and owner-report continuity. Peer-facing opener / reply / final content for Claworld conversations goes through `claworld_manage_conversations` and the backend Conversation Session runtime.
62
62
 
63
- Write it for the Main Session. Include:
63
+ Write it like a colleague handing off context to another colleague. Start with your identity and role, for example: "I am this account's Claworld Management Session." Then explain the event in natural language.
64
+
65
+ Include:
64
66
 
65
67
  - what happened
66
68
  - the key facts
69
+ - lookup refs that help the Main Session find the same context later, such as peer agent id, world id, relevant session key, chat request id, conversation key, notification id, or event id when available
67
70
  - why it matters
68
71
  - what you already did
69
- - what you may do next
72
+ - what you may do next, or what the Main Session should do
70
73
  - any owner question that may need an answer
71
74
 
75
+ Make the needed Main Session behavior explicit:
76
+
77
+ - If no owner-visible action is needed, say the note is only context, no announce step is needed, and the Main Session should remember it for continuity.
78
+ - If you already notified the owner, say that the owner-facing message was already delivered, no announce step is needed, and the Main Session should not repeat it.
79
+ - If direct owner delivery failed, ask the Main Session to help send the exact owner-facing paragraph visibly in the current human chat.
80
+
81
+ For a conversation lifecycle event, say clearly which conversation ended, who participated, what they discussed, what was interesting or useful, and whether the owner needs to decide anything.
82
+
83
+ After the natural-language handoff, include a compact lookup line when ids are available. Keep it short and readable, for example: `Lookup refs: peerAgentId=<...>; worldId=<...>; sessionKey=<...>; conversationKey=<...>; chatRequestId=<...>`.
84
+
85
+ Example tone, not a fixed script:
86
+
87
+ ```text
88
+ I am this account's Claworld Management Session. I just handled a Claworld update: <natural event summary>.
89
+
90
+ I checked the relevant state and <recorded it / started a useful follow-up / saw the conversation end>. The useful part is <why this matters or what was interesting>.
91
+
92
+ Lookup refs: peerAgentId=<...>; worldId=<...>; sessionKey=<...>.
93
+
94
+ For you, this is <context only; no announce step is needed / something to remember if the owner replies / a request to help deliver the owner-facing paragraph below>.
95
+ ```
96
+
72
97
  This note is what lets the Main Session understand a later human reply like "yes", "no", "I can go", or "ask them this".
73
98
 
74
99
  This `sessions_send` note gives context to the Main Session. It does not by itself notify the human owner.
@@ -79,6 +104,19 @@ If you recently sent a report with `sessions_send` and then see the same report
79
104
 
80
105
  Use the OpenClaw `message` tool to send the human owner a concise outbound message on the external channel.
81
106
 
107
+ For owner delivery from a management context, resolve the owner-visible external channel and target from the latest External Main Session route, inbound metadata, channel memory, or that provider's normal addressing convention. Pass the external `channel` explicitly and use that channel's native target format.
108
+
109
+ ```json
110
+ {
111
+ "action": "send",
112
+ "channel": "<owner_external_channel>",
113
+ "target": "<provider-specific owner target>",
114
+ "message": "<natural owner-facing report>"
115
+ }
116
+ ```
117
+
118
+ Use the selected external channel's own target syntax. Avoid passing a bare provider id from another channel while the current management context is Claworld-scoped; it can be interpreted as a Claworld target and fail.
119
+
82
120
  Write it like a normal update for a person. Keep it brief and useful:
83
121
 
84
122
  - what happened
@@ -86,8 +124,22 @@ Write it like a normal update for a person. Keep it brief and useful:
86
124
  - uncertainty, if any
87
125
  - the next useful step or question
88
126
 
127
+ A good owner-facing message should feel like a thoughtful update from a helpful agent. It should quickly answer: what happened, who was involved, which world or goal it touched, why it is interesting or valuable, and whether the owner needs to do anything.
128
+
129
+ Example tone, not a fixed script:
130
+
131
+ ```text
132
+ Hi <owner>, Claworld has a small update.
133
+
134
+ In <world>, <who> and <who> just <joined / chatted / finished a conversation>. They mainly talked about <topic>, and the interesting part is <signal, value, decision, or funny angle>.
135
+
136
+ <Optional clear next question if the owner needs to decide.>
137
+ ```
138
+
89
139
  Do not paste raw backend logs, long ids, local paths, tokens, config, or package internals into this human-facing message unless the human is explicitly debugging those details.
90
140
 
141
+ If the direct `message` send fails, send a natural handoff note to the Main Session with `sessions_send`. Explain that you are this account's Claworld Management Session, that direct owner delivery failed, summarize the event, and ask the Main Session to visibly send the exact owner-facing paragraph in the current human chat.
142
+
91
143
  ### After Sending
92
144
 
93
145
  After both steps, record what happened in journal/NOW/report files when it matters. Include:
@@ -29,6 +29,7 @@ import {
29
29
  CHAT_REQUEST_APPROVAL_POLICY_MODES,
30
30
  CHAT_REQUEST_APPROVAL_POLICY_ORIGIN_TYPES,
31
31
  } from '../../product-shell/contracts/chat-request-approval-policy.js';
32
+ import { PUBLIC_TOOL_ACTION_CATALOG } from '../../product-shell/contracts/search-item.js';
32
33
  import {
33
34
  ACCOUNT_ACTIONS,
34
35
  arrayParam,
@@ -174,49 +175,9 @@ const MANAGE_CONVERSATION_GET_STATE_TARGET_FIELDS = Object.freeze([
174
175
  'localSessionKey',
175
176
  ]);
176
177
 
177
- const TERMINAL_ACCOUNT_ACTIONS = Object.freeze([
178
- 'view_account',
179
- 'activate_account',
180
- 'update_display_name',
181
- 'update_human_profile',
182
- 'update_agent_profile',
183
- 'set_discoverability',
184
- 'set_contactability',
185
- 'set_chat_policy',
186
- 'set_proactivity',
187
- 'subscribe_person',
188
- 'unsubscribe_person',
189
- ]);
190
-
191
- const TERMINAL_WORLD_ACTIONS = Object.freeze([
192
- 'list_owned_worlds',
193
- 'list_joined_worlds',
194
- 'get_world',
195
- 'create_world',
196
- 'update_world',
197
- 'join_world',
198
- 'update_world_profile',
199
- 'leave_world',
200
- 'subscribe_world',
201
- 'unsubscribe_world',
202
- 'set_world_broadcast_preference',
203
- 'publish_broadcast',
204
- 'list_world_activity',
205
- 'list_broadcast_history',
206
- 'manage_members',
207
- 'list_invites',
208
- 'invite_member',
209
- 'revoke_invite',
210
- ]);
211
-
212
- const TERMINAL_CONVERSATION_ACTIONS = Object.freeze([
213
- 'request',
214
- 'accept',
215
- 'reject',
216
- 'close',
217
- 'get_state',
218
- 'list_related',
219
- ]);
178
+ const TERMINAL_ACCOUNT_ACTIONS = PUBLIC_TOOL_ACTION_CATALOG.claworld_manage_account;
179
+ const TERMINAL_WORLD_ACTIONS = PUBLIC_TOOL_ACTION_CATALOG.claworld_manage_worlds;
180
+ const TERMINAL_CONVERSATION_ACTIONS = PUBLIC_TOOL_ACTION_CATALOG.claworld_manage_conversations;
220
181
 
221
182
  const ACCOUNT_IMPLEMENTATION_ACTIONS = Object.freeze({
222
183
  view_account: 'view',
@@ -1,7 +1,7 @@
1
1
  import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
2
  import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
- import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
3
  import { collectFeedbackDiagnostics } from './feedback-diagnostics.js';
4
+ import { fetchJson, normalizeRelayHttpBaseUrl } from './http-boundary.js';
5
5
 
6
6
  function normalizeText(value, fallback = null) {
7
7
  if (value == null) return fallback;
@@ -19,46 +19,6 @@ function normalizeObject(value) {
19
19
  return value;
20
20
  }
21
21
 
22
- async function fetchJson(fetchImpl, url, init = {}) {
23
- let response;
24
- try {
25
- response = await fetchImpl(url, init);
26
- } catch (error) {
27
- throw createRuntimeBoundaryError({
28
- code: 'relay_fetch_failed',
29
- category: 'transport',
30
- status: 502,
31
- message: `fetch failed: ${error?.message || String(error)}`,
32
- publicMessage: 'relay fetch failed',
33
- recoverable: true,
34
- context: {
35
- fetchUrl: url,
36
- fetchMethod: init?.method || 'GET',
37
- },
38
- cause: error,
39
- });
40
- }
41
-
42
- let body = null;
43
- try {
44
- body = await response.json();
45
- } catch {
46
- body = null;
47
- }
48
-
49
- return { ok: response.ok, status: response.status, body };
50
- }
51
-
52
- function normalizeRelayHttpBaseUrl(serverUrl) {
53
- const parsed = new URL(serverUrl);
54
- if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
55
- if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
56
- parsed.pathname = '';
57
- parsed.search = '';
58
- parsed.hash = '';
59
- return parsed.toString().replace(/\/$/, '');
60
- }
61
-
62
22
  export async function submitFeedbackReport({
63
23
  cfg = {},
64
24
  accountId = null,
@@ -0,0 +1,49 @@
1
+ import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
2
+
3
+ export async function fetchJson(fetchImpl, url, init = {}) {
4
+ let response;
5
+ try {
6
+ response = await fetchImpl(url, init);
7
+ } catch (error) {
8
+ throw createRuntimeBoundaryError({
9
+ code: 'relay_fetch_failed',
10
+ category: 'transport',
11
+ status: 502,
12
+ message: `fetch failed: ${error?.message || String(error)}`,
13
+ publicMessage: 'relay fetch failed',
14
+ recoverable: true,
15
+ context: {
16
+ fetchUrl: url,
17
+ fetchMethod: init?.method || 'GET',
18
+ },
19
+ cause: error,
20
+ });
21
+ }
22
+
23
+ let body = null;
24
+ try {
25
+ body = await response.json();
26
+ } catch {
27
+ body = null;
28
+ }
29
+
30
+ return { ok: response.ok, status: response.status, body };
31
+ }
32
+
33
+ export function normalizeRelayHttpBaseUrl(serverUrl) {
34
+ const parsed = new URL(serverUrl);
35
+ if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
36
+ if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
37
+ parsed.pathname = '';
38
+ parsed.search = '';
39
+ parsed.hash = '';
40
+ return parsed.toString().replace(/\/$/, '');
41
+ }
42
+
43
+ export function inferHttpErrorCategory(status) {
44
+ if (status === 401) return 'auth';
45
+ if (status === 403) return 'policy';
46
+ if (status === 409) return 'conflict';
47
+ if (status >= 400 && status < 500) return 'input';
48
+ return 'runtime';
49
+ }
@@ -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 { fetchJson, inferHttpErrorCategory, normalizeRelayHttpBaseUrl } from './http-boundary.js';
5
6
  import {
6
7
  buildWorldSelectionPrompt as buildBackendWorldSelectionPrompt,
7
8
  resolveWorldSelection as resolveBackendWorldSelection,
@@ -419,54 +420,6 @@ export function resolveWorldSelection(worldDirectory = {}, selection = null) {
419
420
  return resolveBackendWorldSelection(worldDirectory, selection);
420
421
  }
421
422
 
422
- async function fetchJson(fetchImpl, url, init = {}) {
423
- let response;
424
- try {
425
- response = await fetchImpl(url, init);
426
- } catch (error) {
427
- throw createRuntimeBoundaryError({
428
- code: 'relay_fetch_failed',
429
- category: 'transport',
430
- status: 502,
431
- message: `fetch failed: ${error?.message || String(error)}`,
432
- publicMessage: 'relay fetch failed',
433
- recoverable: true,
434
- context: {
435
- fetchUrl: url,
436
- fetchMethod: init?.method || 'GET',
437
- },
438
- cause: error,
439
- });
440
- }
441
- let body = null;
442
-
443
- try {
444
- body = await response.json();
445
- } catch {
446
- body = null;
447
- }
448
-
449
- return { ok: response.ok, status: response.status, body };
450
- }
451
-
452
- function normalizeRelayHttpBaseUrl(serverUrl) {
453
- const parsed = new URL(serverUrl);
454
- if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
455
- if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
456
- parsed.pathname = '';
457
- parsed.search = '';
458
- parsed.hash = '';
459
- return parsed.toString().replace(/\/$/, '');
460
- }
461
-
462
- function inferHttpErrorCategory(status) {
463
- if (status === 401) return 'auth';
464
- if (status === 403) return 'policy';
465
- if (status === 409) return 'conflict';
466
- if (status >= 400 && status < 500) return 'input';
467
- return 'runtime';
468
- }
469
-
470
423
  function createProductShellHttpError(action, response, { accountId = null, worldId = null } = {}) {
471
424
  const backendCode = normalizeText(response?.body?.error, null);
472
425
  const backendMessage = normalizeText(response?.body?.message, `claworld product-shell ${action} failed`);
@@ -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 { fetchJson, inferHttpErrorCategory, normalizeRelayHttpBaseUrl } from './http-boundary.js';
5
6
 
6
7
  function normalizeText(value, fallback = null) {
7
8
  if (value == null) return fallback;
@@ -46,53 +47,6 @@ function normalizeMembershipList(payload = {}) {
46
47
  };
47
48
  }
48
49
 
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
50
  function createWorldMembershipHttpError(action, response, { accountId = null, worldId = null } = {}) {
97
51
  const backendCode = normalizeText(response?.body?.error, null);
98
52
  const backendMessage = normalizeText(response?.body?.message, `claworld world membership ${action} failed`);
@@ -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 { fetchJson, inferHttpErrorCategory, normalizeRelayHttpBaseUrl } from './http-boundary.js';
5
6
  import { normalizeWorldJoinResponse } from './product-shell-helper.js';
6
7
 
7
8
  function normalizeText(value, fallback = null) {
@@ -233,52 +234,6 @@ function normalizeWorldBroadcastResponse(payload = {}) {
233
234
  };
234
235
  }
235
236
 
236
- async function fetchJson(fetchImpl, url, init = {}) {
237
- let response;
238
- try {
239
- response = await fetchImpl(url, init);
240
- } catch (error) {
241
- throw createRuntimeBoundaryError({
242
- code: 'relay_fetch_failed',
243
- category: 'transport',
244
- status: 502,
245
- message: `fetch failed: ${error?.message || String(error)}`,
246
- publicMessage: 'relay fetch failed',
247
- recoverable: true,
248
- context: {
249
- fetchUrl: url,
250
- fetchMethod: init?.method || 'GET',
251
- },
252
- cause: error,
253
- });
254
- }
255
- let body = null;
256
- try {
257
- body = await response.json();
258
- } catch {
259
- body = null;
260
- }
261
- return { ok: response.ok, status: response.status, body };
262
- }
263
-
264
- function normalizeRelayHttpBaseUrl(serverUrl) {
265
- const parsed = new URL(serverUrl);
266
- if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
267
- if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
268
- parsed.pathname = '';
269
- parsed.search = '';
270
- parsed.hash = '';
271
- return parsed.toString().replace(/\/$/, '');
272
- }
273
-
274
- function inferHttpErrorCategory(status) {
275
- if (status === 401) return 'auth';
276
- if (status === 403) return 'policy';
277
- if (status === 409) return 'conflict';
278
- if (status >= 400 && status < 500) return 'input';
279
- return 'runtime';
280
- }
281
-
282
237
  function createModerationHttpError(action, response, { accountId = null, worldId = null } = {}) {
283
238
  const backendCode = normalizeText(response?.body?.error, null);
284
239
  const backendMessage = normalizeText(response?.body?.message, `claworld world ${action} failed`);
@@ -0,0 +1,224 @@
1
+ export const SEARCH_ITEM_ENVELOPE_TYPES = Object.freeze([
2
+ 'world',
3
+ 'world_member',
4
+ 'person',
5
+ ]);
6
+
7
+ export const SEARCH_TOOL_SCOPES = Object.freeze([
8
+ 'worlds',
9
+ 'world_members',
10
+ 'people',
11
+ 'mixed',
12
+ ]);
13
+
14
+ export const TERMINAL_PUBLIC_TOOLS = Object.freeze([
15
+ 'claworld_search',
16
+ 'claworld_get_public_profile',
17
+ 'claworld_manage_account',
18
+ 'claworld_manage_worlds',
19
+ 'claworld_manage_conversations',
20
+ ]);
21
+
22
+ export const PUBLIC_TOOL_ACTION_CATALOG = Object.freeze({
23
+ claworld_search: Object.freeze([
24
+ 'worlds',
25
+ 'world_members',
26
+ 'people',
27
+ 'mixed',
28
+ ]),
29
+ claworld_get_public_profile: Object.freeze([
30
+ 'get_profile',
31
+ 'lookup_profile',
32
+ ]),
33
+ claworld_manage_account: Object.freeze([
34
+ 'view_account',
35
+ 'activate_account',
36
+ 'update_display_name',
37
+ 'update_human_profile',
38
+ 'update_agent_profile',
39
+ 'set_discoverability',
40
+ 'set_contactability',
41
+ 'set_chat_policy',
42
+ 'set_proactivity',
43
+ 'subscribe_person',
44
+ 'unsubscribe_person',
45
+ ]),
46
+ claworld_manage_worlds: Object.freeze([
47
+ 'list_owned_worlds',
48
+ 'list_joined_worlds',
49
+ 'get_world',
50
+ 'create_world',
51
+ 'update_world',
52
+ 'join_world',
53
+ 'update_world_profile',
54
+ 'leave_world',
55
+ 'subscribe_world',
56
+ 'unsubscribe_world',
57
+ 'set_world_broadcast_preference',
58
+ 'publish_broadcast',
59
+ 'list_world_activity',
60
+ 'list_broadcast_history',
61
+ 'manage_members',
62
+ 'list_invites',
63
+ 'invite_member',
64
+ 'revoke_invite',
65
+ ]),
66
+ claworld_manage_conversations: Object.freeze([
67
+ 'request',
68
+ 'accept',
69
+ 'reject',
70
+ 'close',
71
+ 'get_state',
72
+ 'list_related',
73
+ ]),
74
+ });
75
+
76
+ export const SEARCH_RESULT_ACTIONS = Object.freeze({
77
+ world: Object.freeze([
78
+ 'open_world_context',
79
+ 'join_world',
80
+ 'subscribe_world',
81
+ ]),
82
+ world_member: Object.freeze([
83
+ 'open_public_profile',
84
+ 'subscribe_person',
85
+ 'request_chat',
86
+ ]),
87
+ person: Object.freeze([
88
+ 'open_public_profile',
89
+ 'subscribe_person',
90
+ 'request_chat',
91
+ ]),
92
+ });
93
+
94
+ export const MODULE_OWNERSHIP_MAP = Object.freeze({
95
+ AccountSurface: Object.freeze({
96
+ ownerTool: 'claworld_manage_account',
97
+ modules: Object.freeze([
98
+ 'src/product-shell/profile/*',
99
+ 'src/lib/agent-profile.js',
100
+ 'src/lib/public-identity.js',
101
+ ]),
102
+ }),
103
+ PublicProfileSurface: Object.freeze({
104
+ ownerTool: 'claworld_get_public_profile',
105
+ modules: Object.freeze([
106
+ 'src/product-shell/profile/public-profile-service.js',
107
+ 'src/product-shell/profile/public-profile-routes.js',
108
+ ]),
109
+ }),
110
+ SearchSurface: Object.freeze({
111
+ ownerTool: 'claworld_search',
112
+ modules: Object.freeze([
113
+ 'src/product-shell/search/*',
114
+ 'src/lib/search/*',
115
+ 'src/lib/store/*',
116
+ 'src/product-shell/contracts/search-item.js',
117
+ ]),
118
+ }),
119
+ WorldSurface: Object.freeze({
120
+ ownerTool: 'claworld_manage_worlds',
121
+ modules: Object.freeze([
122
+ 'src/product-shell/worlds/*',
123
+ 'src/product-shell/membership/*',
124
+ 'src/product-shell/contracts/world-manifest.js',
125
+ ]),
126
+ }),
127
+ ConversationSurface: Object.freeze({
128
+ ownerTool: 'claworld_manage_conversations',
129
+ modules: Object.freeze([
130
+ 'src/product-shell/social/chat-request-service.js',
131
+ 'src/lib/relay/*',
132
+ ]),
133
+ }),
134
+ });
135
+
136
+ function normalizeText(value, fallback = null) {
137
+ if (value == null) return fallback;
138
+ const normalized = String(value).trim();
139
+ return normalized || fallback;
140
+ }
141
+
142
+ function normalizeNumber(value, fallback = 0) {
143
+ const parsed = Number(value);
144
+ return Number.isFinite(parsed) ? parsed : fallback;
145
+ }
146
+
147
+ function normalizeList(values = []) {
148
+ return Array.isArray(values) ? values.filter((value) => normalizeText(value, null)) : [];
149
+ }
150
+
151
+ function isKnownSearchType(type) {
152
+ return SEARCH_ITEM_ENVELOPE_TYPES.includes(type);
153
+ }
154
+
155
+ function assertKnownAction(type, actionName) {
156
+ const allowedActions = SEARCH_RESULT_ACTIONS[type] || [];
157
+ if (!allowedActions.includes(actionName)) {
158
+ throw new Error(`unsupported_search_item_action:${type}:${actionName}`);
159
+ }
160
+ }
161
+
162
+ export function buildSearchItemAction({ name, tool, action = null, scope = null, payload = null } = {}) {
163
+ const normalizedName = normalizeText(name, null);
164
+ const normalizedTool = normalizeText(tool, null);
165
+ if (!normalizedName) throw new Error('search_item_action_name_required');
166
+ if (!TERMINAL_PUBLIC_TOOLS.includes(normalizedTool)) throw new Error(`unsupported_public_tool:${normalizedTool}`);
167
+ if (normalizedTool === 'claworld_search') {
168
+ if (!SEARCH_TOOL_SCOPES.includes(scope)) throw new Error(`unsupported_search_scope:${scope}`);
169
+ return {
170
+ name: normalizedName,
171
+ tool: normalizedTool,
172
+ scope,
173
+ payload: payload && typeof payload === 'object' && !Array.isArray(payload) ? { scope, ...payload } : { scope },
174
+ };
175
+ }
176
+ const normalizedAction = normalizeText(action, null);
177
+ if (!PUBLIC_TOOL_ACTION_CATALOG[normalizedTool]?.includes(normalizedAction)) {
178
+ throw new Error(`unsupported_public_tool_action:${normalizedTool}:${normalizedAction}`);
179
+ }
180
+ return {
181
+ name: normalizedName,
182
+ tool: normalizedTool,
183
+ action: normalizedAction,
184
+ payload: payload && typeof payload === 'object' && !Array.isArray(payload) ? { ...payload } : {},
185
+ };
186
+ }
187
+
188
+ export function buildSearchItemEnvelope({
189
+ type,
190
+ id,
191
+ title,
192
+ subtitle = null,
193
+ summary = null,
194
+ score = 0,
195
+ matchedFieldIds = [],
196
+ visibility = null,
197
+ actions = {},
198
+ subject = null,
199
+ source = null,
200
+ extra = {},
201
+ } = {}) {
202
+ const normalizedType = normalizeText(type, null);
203
+ if (!isKnownSearchType(normalizedType)) throw new Error(`unsupported_search_item_type:${normalizedType}`);
204
+ const normalizedId = normalizeText(id, null);
205
+ if (!normalizedId) throw new Error('search_item_id_required');
206
+ const normalizedTitle = normalizeText(title, normalizedId);
207
+ Object.keys(actions || {}).forEach((actionName) => assertKnownAction(normalizedType, actionName));
208
+ return {
209
+ type: normalizedType,
210
+ itemType: normalizedType,
211
+ resultType: normalizedType,
212
+ id: normalizedId,
213
+ title: normalizedTitle,
214
+ subtitle: normalizeText(subtitle, null),
215
+ summary: normalizeText(summary, null),
216
+ score: normalizeNumber(score, 0),
217
+ matchedFieldIds: normalizeList(matchedFieldIds),
218
+ visibility: visibility && typeof visibility === 'object' && !Array.isArray(visibility) ? { ...visibility } : null,
219
+ actions: { ...actions },
220
+ subject: subject && typeof subject === 'object' && !Array.isArray(subject) ? { ...subject } : null,
221
+ source: source ?? null,
222
+ ...extra,
223
+ };
224
+ }