@xfxstudio/claworld 0.1.1 → 0.1.3

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.
@@ -5,7 +5,7 @@
5
5
  ],
6
6
  "name": "Claworld Persona Relay",
7
7
  "description": "Claworld relay world channel plugin for OpenClaw.",
8
- "version": "0.1.1",
8
+ "version": "0.1.3",
9
9
  "configSchema": {
10
10
  "type": "object",
11
11
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -48,7 +48,7 @@
48
48
  "node": ">=22"
49
49
  },
50
50
  "peerDependencies": {
51
- "openclaw": ">=2026.3.14"
51
+ "openclaw": ">=2026.3.22"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "openclaw": {
@@ -76,7 +76,7 @@
76
76
  "install": {
77
77
  "npmSpec": "@xfxstudio/claworld",
78
78
  "defaultChoice": "npm",
79
- "minHostVersion": ">=2026.3.14"
79
+ "minHostVersion": ">=2026.3.22"
80
80
  }
81
81
  }
82
82
  }
@@ -91,12 +91,10 @@ function formatSessionOverview(detail = {}) {
91
91
  : {};
92
92
  const mode = normalizeText(detail.sessionMode || sessionOverview.mode, null);
93
93
  const maxTurns = normalizeInteger(sessionOverview.maxTurns, null);
94
- const turnTimeoutMs = normalizeInteger(sessionOverview.turnTimeoutMs, null);
95
94
  const parts = [];
96
95
 
97
96
  if (mode) parts.push(`${mode} mode`);
98
97
  if (maxTurns != null) parts.push(`max ${maxTurns} turns`);
99
- if (turnTimeoutMs != null) parts.push(`${turnTimeoutMs}ms turn timeout`);
100
98
 
101
99
  return parts.length > 0 ? parts.join(', ') : null;
102
100
  }
@@ -182,6 +180,7 @@ export function buildWorldSessionStartupEvent(detail = {}) {
182
180
  summary ? `Summary: ${summary}` : null,
183
181
  sessionSummary ? `Session overview: ${sessionSummary}` : null,
184
182
  raiseHandSummary ? `Completion rule: ${raiseHandSummary}` : null,
183
+ 'Interruption handling: prefer reconnect/resume. Temporary silence or reconnect churn is not the normal way to close a round.',
185
184
  openingText ? `Opening focus: ${openingText}` : null,
186
185
  interactionRules ? `Interaction rules: ${interactionRules}` : null,
187
186
  prohibitedRules ? `Prohibited rules: ${prohibitedRules}` : null,
@@ -3,4 +3,4 @@ export const CLAWORLD_INSTALLER_PACKAGE_NAME = '@xfxstudio/claworld';
3
3
  export const CLAWORLD_INSTALLER_COMMAND = 'npx -y @xfxstudio/claworld install';
4
4
  export const CLAWORLD_DOCTOR_COMMAND = 'npx -y @xfxstudio/claworld doctor';
5
5
  export const CLAWORLD_UPDATE_COMMAND = 'npx -y @xfxstudio/claworld update';
6
- export const CLAWORLD_OPENCLAW_MIN_HOST_VERSION = '>=2026.3.14';
6
+ export const CLAWORLD_OPENCLAW_MIN_HOST_VERSION = '>=2026.3.22';
@@ -1,3 +1,4 @@
1
+ import { accessSync, constants as FS_CONSTANTS } from 'fs';
1
2
  import fs from 'fs/promises';
2
3
  import os from 'os';
3
4
  import path from 'path';
@@ -80,8 +81,8 @@ export function isRelayBootstrapReady(account = {}) {
80
81
  }
81
82
 
82
83
  function parseCommandVersion(text) {
83
- const match = String(text || '').match(/OpenClaw\s+([0-9]+(?:\.[0-9]+)+)/i)
84
- || String(text || '').match(/([0-9]+(?:\.[0-9]+)+)/);
84
+ const match = String(text || '').match(/OpenClaw\s+([0-9]+(?:\.[0-9]+)+(?:-[0-9A-Za-z.-]+)?)/i)
85
+ || String(text || '').match(/([0-9]+(?:\.[0-9]+)+(?:-[0-9A-Za-z.-]+)?)/);
85
86
  return match ? match[1] : null;
86
87
  }
87
88
 
@@ -138,6 +139,90 @@ function cloneObject(value = {}) {
138
139
  return JSON.parse(JSON.stringify(ensureObject(value)));
139
140
  }
140
141
 
142
+ function isExplicitCommandPath(command = '') {
143
+ const normalized = String(command || '').trim();
144
+ if (!normalized) return false;
145
+ return normalized.includes('/') || normalized.includes('\\') || path.isAbsolute(normalized);
146
+ }
147
+
148
+ function splitPathEnvEntries(pathValue = '') {
149
+ return String(pathValue || '')
150
+ .split(path.delimiter)
151
+ .map((entry) => entry.trim())
152
+ .filter(Boolean);
153
+ }
154
+
155
+ function isNodeModulesBinEntry(entry = '') {
156
+ const normalized = path.resolve(String(entry || ''));
157
+ return path.basename(normalized) === '.bin'
158
+ && path.basename(path.dirname(normalized)) === 'node_modules';
159
+ }
160
+
161
+ function resolveCommandNameCandidates(command = '', env = process.env) {
162
+ const normalized = String(command || '').trim();
163
+ if (!normalized) return [];
164
+ if (process.platform !== 'win32' || path.extname(normalized)) {
165
+ return [normalized];
166
+ }
167
+
168
+ const pathExt = splitPathEnvEntries(env?.PATHEXT || process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
169
+ .map((ext) => ext.toLowerCase());
170
+ return [...new Set([normalized, ...pathExt.map((ext) => `${normalized}${ext}`)])];
171
+ }
172
+
173
+ function hasExecutableAccess(filePath = '') {
174
+ try {
175
+ accessSync(filePath, FS_CONSTANTS.X_OK);
176
+ return true;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+
182
+ export function resolveOpenclawCliBinary({
183
+ openclawBin = DEFAULT_OPENCLAW_BIN,
184
+ env = process.env,
185
+ } = {}) {
186
+ const requestedBin = normalizeText(openclawBin, DEFAULT_OPENCLAW_BIN);
187
+ if (
188
+ requestedBin !== DEFAULT_OPENCLAW_BIN
189
+ || isExplicitCommandPath(requestedBin)
190
+ ) {
191
+ return {
192
+ requestedBin,
193
+ binaryPath: requestedBin,
194
+ binarySource: isExplicitCommandPath(requestedBin) ? 'explicit_path' : 'explicit_command',
195
+ skippedLocalBinaryPaths: [],
196
+ };
197
+ }
198
+
199
+ const candidates = [];
200
+ for (const entry of splitPathEnvEntries(env?.PATH || process.env.PATH || '')) {
201
+ for (const name of resolveCommandNameCandidates(requestedBin, env)) {
202
+ const candidatePath = path.join(entry, name);
203
+ if (hasExecutableAccess(candidatePath)) {
204
+ candidates.push(candidatePath);
205
+ }
206
+ }
207
+ }
208
+
209
+ const uniqueCandidates = [...new Set(candidates)];
210
+ const hostCandidates = uniqueCandidates.filter((candidate) => !isNodeModulesBinEntry(path.dirname(candidate)));
211
+ const localCandidates = uniqueCandidates.filter((candidate) => isNodeModulesBinEntry(path.dirname(candidate)));
212
+ const binaryPath = hostCandidates[0] || uniqueCandidates[0] || requestedBin;
213
+
214
+ return {
215
+ requestedBin,
216
+ binaryPath,
217
+ binarySource: hostCandidates[0]
218
+ ? 'host_path'
219
+ : uniqueCandidates[0]
220
+ ? 'package_local_path'
221
+ : 'default_command',
222
+ skippedLocalBinaryPaths: localCandidates.filter((candidate) => candidate !== binaryPath),
223
+ };
224
+ }
225
+
141
226
  function parseJson(text, fallback = null) {
142
227
  try {
143
228
  return JSON.parse(String(text || '').trim());
@@ -369,7 +454,12 @@ function defaultCommandRunner({
369
454
  dryRun = false,
370
455
  capture = true,
371
456
  } = {}) {
372
- const rendered = [bin, ...args].join(' ');
457
+ const resolvedBin = resolveOpenclawCliBinary({
458
+ openclawBin: bin,
459
+ env,
460
+ });
461
+ const effectiveBin = resolvedBin.binaryPath;
462
+ const rendered = [effectiveBin, ...args].join(' ');
373
463
  if (dryRun) {
374
464
  return {
375
465
  status: 0,
@@ -377,10 +467,14 @@ function defaultCommandRunner({
377
467
  stderr: '',
378
468
  rendered,
379
469
  dryRun: true,
470
+ requestedBin: bin,
471
+ resolvedBin: effectiveBin,
472
+ binSource: resolvedBin.binarySource,
473
+ skippedBinCandidates: resolvedBin.skippedLocalBinaryPaths,
380
474
  };
381
475
  }
382
476
 
383
- const result = spawnSync(bin, args, {
477
+ const result = spawnSync(effectiveBin, args, {
384
478
  cwd,
385
479
  env,
386
480
  stdio: capture ? 'pipe' : 'inherit',
@@ -397,12 +491,16 @@ function defaultCommandRunner({
397
491
  throw error;
398
492
  }
399
493
 
400
- return {
401
- status: result.status ?? 0,
402
- stdout: result.stdout || '',
403
- stderr: result.stderr || '',
404
- rendered,
405
- };
494
+ return {
495
+ status: result.status ?? 0,
496
+ stdout: result.stdout || '',
497
+ stderr: result.stderr || '',
498
+ rendered,
499
+ requestedBin: bin,
500
+ resolvedBin: effectiveBin,
501
+ binSource: resolvedBin.binarySource,
502
+ skippedBinCandidates: resolvedBin.skippedLocalBinaryPaths,
503
+ };
406
504
  }
407
505
 
408
506
  async function executeCommand({
@@ -597,6 +695,12 @@ export async function detectOpenclawHost({
597
695
  return {
598
696
  version,
599
697
  raw: (result.stdout || result.stderr || '').trim(),
698
+ requestedBin: result.requestedBin || openclawBin,
699
+ binaryPath: result.resolvedBin || openclawBin,
700
+ binarySource: result.binSource || 'default_command',
701
+ skippedLocalBinaryPaths: Array.isArray(result.skippedBinCandidates)
702
+ ? result.skippedBinCandidates
703
+ : [],
600
704
  };
601
705
  }
602
706
 
@@ -103,6 +103,42 @@ function normalizeGatewayBaseUrl(gatewayStatus) {
103
103
  return `http://${host}:${port}`;
104
104
  }
105
105
 
106
+ function normalizeConfiguredGatewayBaseUrl(config = {}) {
107
+ const configuredPort = Number(config?.gateway?.port);
108
+ if (!Number.isFinite(configuredPort) || configuredPort <= 0) {
109
+ return null;
110
+ }
111
+
112
+ const configuredBind = normalizeText(config?.gateway?.bind, '127.0.0.1');
113
+ const configuredHost = (
114
+ configuredBind === 'loopback'
115
+ || configuredBind === 'localhost'
116
+ || configuredBind === '0.0.0.0'
117
+ || configuredBind === '::'
118
+ )
119
+ ? '127.0.0.1'
120
+ : configuredBind;
121
+ return `http://${configuredHost}:${configuredPort}`;
122
+ }
123
+
124
+ function describePluginStatusRouteTarget({
125
+ config = {},
126
+ gatewayStatus = {},
127
+ } = {}) {
128
+ const configuredBaseUrl = normalizeConfiguredGatewayBaseUrl(config);
129
+ const runtimeBaseUrl = normalizeGatewayBaseUrl(gatewayStatus);
130
+ return {
131
+ configuredBaseUrl,
132
+ runtimeBaseUrl,
133
+ baseUrlMismatch: Boolean(
134
+ configuredBaseUrl
135
+ && runtimeBaseUrl
136
+ && configuredBaseUrl !== runtimeBaseUrl
137
+ ),
138
+ runtimePortSource: normalizeText(gatewayStatus?.gateway?.portSource, null),
139
+ };
140
+ }
141
+
106
142
  async function fetchPluginStatusRoute({
107
143
  gatewayBaseUrl,
108
144
  fetchImpl = globalThis.fetch?.bind(globalThis),
@@ -224,7 +260,13 @@ export async function runClaworldDoctor({
224
260
  status: 'fail',
225
261
  summary: `OpenClaw ${host.version} is below the required minimum ${CLAWORLD_OPENCLAW_MIN_HOST_VERSION}.`,
226
262
  action: `Upgrade OpenClaw, then rerun \`${CLAWORLD_DOCTOR_COMMAND}\`.`,
227
- details: { version: host.version, minimum: CLAWORLD_OPENCLAW_MIN_HOST_VERSION },
263
+ details: {
264
+ version: host.version,
265
+ minimum: CLAWORLD_OPENCLAW_MIN_HOST_VERSION,
266
+ requestedBin: host.requestedBin || null,
267
+ binaryPath: host.binaryPath || null,
268
+ binarySource: host.binarySource || null,
269
+ },
228
270
  }));
229
271
  } else {
230
272
  checks.push(createCheck({
@@ -233,7 +275,14 @@ export async function runClaworldDoctor({
233
275
  label: 'OpenClaw host version',
234
276
  status: 'pass',
235
277
  summary: `OpenClaw ${host.version} satisfies the required minimum ${CLAWORLD_OPENCLAW_MIN_HOST_VERSION}.`,
236
- details: { version: host.version, minimum: CLAWORLD_OPENCLAW_MIN_HOST_VERSION },
278
+ details: {
279
+ version: host.version,
280
+ minimum: CLAWORLD_OPENCLAW_MIN_HOST_VERSION,
281
+ requestedBin: host.requestedBin || null,
282
+ binaryPath: host.binaryPath || null,
283
+ binarySource: host.binarySource || null,
284
+ skippedLocalBinaryPaths: host.skippedLocalBinaryPaths || [],
285
+ },
237
286
  }));
238
287
  }
239
288
  } catch (error) {
@@ -573,7 +622,11 @@ export async function runClaworldDoctor({
573
622
  }
574
623
 
575
624
  if (facts.gatewayStatus?.service?.runtime?.status === 'running') {
576
- const gatewayBaseUrl = normalizeGatewayBaseUrl(facts.gatewayStatus);
625
+ const routeTarget = describePluginStatusRouteTarget({
626
+ config,
627
+ gatewayStatus: facts.gatewayStatus,
628
+ });
629
+ const gatewayBaseUrl = routeTarget.runtimeBaseUrl;
577
630
  facts.pluginStatusRoute = await fetchPluginStatusRoute({
578
631
  gatewayBaseUrl,
579
632
  fetchImpl,
@@ -583,7 +636,19 @@ export async function runClaworldDoctor({
583
636
  let status = 'pass';
584
637
  let summary = `Reached ${facts.pluginStatusRoute.routeUrl}.`;
585
638
  let action = null;
586
- if (!routeReachable) {
639
+ if (routeTarget.baseUrlMismatch) {
640
+ status = 'warn';
641
+ const runtimeTarget = routeTarget.runtimeBaseUrl || 'the running gateway target';
642
+ const configuredTarget = routeTarget.configuredBaseUrl || 'the inspected config target';
643
+ const responseSummary = routeStatus == null
644
+ ? facts.pluginStatusRoute.error || 'no response'
645
+ : `HTTP ${routeStatus}`;
646
+ summary = [
647
+ `OpenClaw gateway status resolved ${runtimeTarget}, but the inspected config declares ${configuredTarget}.`,
648
+ `Treating \`${facts.pluginStatusRoute.routeUrl || '/plugins/claworld/status'}\` as advisory because the host service is targeting a different HTTP endpoint (${responseSummary}).`,
649
+ ].join(' ');
650
+ action = 'Align the host gateway service target with the inspected config if you need live plugin-route proof for this config.';
651
+ } else if (!routeReachable) {
587
652
  status = 'fail';
588
653
  summary = `Unable to reach ${facts.pluginStatusRoute.routeUrl || 'the plugin status route'}.`;
589
654
  action = 'Confirm the local OpenClaw gateway HTTP surface is reachable, then rerun doctor.';
@@ -599,7 +664,13 @@ export async function runClaworldDoctor({
599
664
  status,
600
665
  summary,
601
666
  action,
602
- details: facts.pluginStatusRoute,
667
+ details: {
668
+ ...facts.pluginStatusRoute,
669
+ configuredBaseUrl: routeTarget.configuredBaseUrl,
670
+ runtimeBaseUrl: routeTarget.runtimeBaseUrl,
671
+ baseUrlMismatch: routeTarget.baseUrlMismatch,
672
+ runtimePortSource: routeTarget.runtimePortSource,
673
+ },
603
674
  }));
604
675
  } else {
605
676
  checks.push(createCheck({
@@ -222,26 +222,6 @@ function shouldAuthorizeBridgedCommand({ runtimeConfig = {}, relayEvent, incomin
222
222
  return typeof incomingText === 'string' && incomingText.trim().startsWith('/');
223
223
  }
224
224
 
225
- const CLAWORLD_RAISE_HAND_DIRECTIVE = '[[CLAWORLD_RAISE_HAND]]';
226
- const CLAWORLD_RAISE_HAND_DIRECTIVE_PATTERN = /\[\[\s*CLAWORLD_RAISE_HAND\s*\]\]/gi;
227
-
228
- function extractControlledReply(text) {
229
- let raiseHand = false;
230
- const sanitized = String(text || '')
231
- .replace(CLAWORLD_RAISE_HAND_DIRECTIVE_PATTERN, () => {
232
- raiseHand = true;
233
- return ' ';
234
- })
235
- .replace(/[ \t]+\n/g, '\n')
236
- .replace(/\n{3,}/g, '\n\n')
237
- .trim();
238
-
239
- return {
240
- text: sanitized || (raiseHand ? 'I am ready to conclude this round.' : ''),
241
- control: raiseHand ? { type: 'raise_hand', raiseHand: true } : null,
242
- };
243
- }
244
-
245
225
  const CLAWORLD_RELAY_OPERATIONAL_NOTICE_PATTERNS = [
246
226
  /^🧭\s*New session:\s+\S+/i,
247
227
  /^🧹\s*Auto-compaction complete(?:\s*\(count \d+\))?\.$/i,
@@ -1602,9 +1582,8 @@ function createRelayReplyDispatcher({
1602
1582
  };
1603
1583
 
1604
1584
  const flushReply = async (text) => {
1605
- const controlledReply = extractControlledReply(text);
1606
- const normalized = String(controlledReply.text || '').trim();
1607
- if ((!normalized && !controlledReply.control) || replied || suppressed) return false;
1585
+ const normalized = String(text || '').trim();
1586
+ if (!normalized || replied || suppressed) return false;
1608
1587
  if (allowRelayContinuation === false) {
1609
1588
  suppressed = true;
1610
1589
  return false;
@@ -1619,7 +1598,6 @@ function createRelayReplyDispatcher({
1619
1598
  payload: {
1620
1599
  text: normalized,
1621
1600
  source: 'openclaw-autochain',
1622
- ...(controlledReply.control ? { control: controlledReply.control } : {}),
1623
1601
  },
1624
1602
  });
1625
1603
  if (result.status !== 201) {
@@ -1631,7 +1609,6 @@ function createRelayReplyDispatcher({
1631
1609
  messageId: turnId,
1632
1610
  replyText: normalized,
1633
1611
  source: 'openclaw-autochain',
1634
- ...(controlledReply.control ? { control: controlledReply.control } : {}),
1635
1612
  });
1636
1613
  }
1637
1614
  replied = true;
@@ -2732,6 +2709,7 @@ export function createClaworldChannelPlugin({
2732
2709
  agentId: resolvedContext.agentId || null,
2733
2710
  profile: context.profile || {},
2734
2711
  profileSnapshot: context.profileSnapshot ?? null,
2712
+ profileUpdate: context.profileUpdate ?? context.profilePatch ?? null,
2735
2713
  limit: context.limit ?? context.candidateLimit ?? null,
2736
2714
  fetchImpl,
2737
2715
  logger,
@@ -2969,6 +2947,7 @@ export function createClaworldChannelPlugin({
2969
2947
  agentId: resolvedContext.agentId || null,
2970
2948
  profile: context.profile || {},
2971
2949
  profileSnapshot: context.profileSnapshot ?? null,
2950
+ profileUpdate: context.profileUpdate ?? context.profilePatch ?? null,
2972
2951
  limit: context.limit ?? context.candidateLimit ?? null,
2973
2952
  fetchImpl,
2974
2953
  logger,
@@ -328,7 +328,7 @@ function buildRegisteredTools(api, plugin) {
328
328
  },
329
329
  {
330
330
  name: 'claworld_prepare_world_join',
331
- description: 'Inspect or update the current profile draft for a Claworld world join. This remains available for draft-first flows, but direct join can also collect missing fields now.',
331
+ description: 'Compatibility helper for inspect-first world joins. The canonical agent-facing path is direct claworld_join_world with structured retry fields when profile data is incomplete.',
332
332
  parameters: {
333
333
  type: 'object',
334
334
  additionalProperties: false,
@@ -362,7 +362,7 @@ function buildRegisteredTools(api, plugin) {
362
362
  },
363
363
  {
364
364
  name: 'claworld_join_world',
365
- description: 'Create or activate a Claworld world membership for the current relay agent, and if required fields are still missing return the next profile fields before retrying the same tool.',
365
+ description: 'Create or confirm a Claworld world membership for the current relay agent. When profile data is incomplete it returns structured missing-field guidance for retrying the same tool; on success it returns the canonical candidate-review payload so the next world step is review candidate feed -> claworld_request_chat.',
366
366
  parameters: {
367
367
  type: 'object',
368
368
  additionalProperties: false,
@@ -383,7 +383,7 @@ function buildRegisteredTools(api, plugin) {
383
383
  const profileDraftParams = resolveProfileDraftParams(params, {
384
384
  includeProfileSnapshot: true,
385
385
  });
386
- const payload = await plugin.runtime.productShell.submitWorldJoin({
386
+ const payload = await plugin.runtime.productShell.resolveWorldJoinFlow({
387
387
  ...context,
388
388
  worldId: params.worldId,
389
389
  agentId: context.agentId,
@@ -401,7 +401,7 @@ function buildRegisteredTools(api, plugin) {
401
401
  },
402
402
  {
403
403
  name: 'claworld_search_world',
404
- description: 'Search the selected Claworld world for active online agents. Results return canonical agentId plus a compatibility playerId alias.',
404
+ description: 'Optional compatibility/debug search inside a joined Claworld world. This is not the canonical post-join path; default world discovery should use candidate feed and then claworld_request_chat.',
405
405
  parameters: {
406
406
  type: 'object',
407
407
  additionalProperties: false,
@@ -556,7 +556,7 @@ function buildRegisteredTools(api, plugin) {
556
556
  },
557
557
  {
558
558
  name: 'claworld_request_chat',
559
- description: 'Create a direct or world-scoped chat request for another relay peer using its canonical targetAgentId. `openingMessage` is treated as a kickoff brief/opener intent; if the peer accepts, the backend constructs the kickoff bundle, wakes the sender runtime first, and delivers the sender-composed opener to the recipient runtime.',
559
+ description: 'Create a direct or world-scoped chat request for another relay peer using its canonical targetAgentId. For world-scoped discovery, use the targetAgentId returned from candidate-feed review after join_world; this remains the canonical world-scoped contact-establishment step. `openingMessage` is treated as a kickoff brief/opener intent; if the peer accepts, the backend constructs the kickoff bundle, wakes the sender runtime first, and delivers the sender-composed opener to the recipient runtime.',
560
560
  parameters: {
561
561
  type: 'object',
562
562
  additionalProperties: false,
@@ -517,7 +517,7 @@ export class ClaworldRelayClient extends EventEmitter {
517
517
  config,
518
518
  agentId,
519
519
  credential = null,
520
- clientVersion = 'claworld-plugin/0.1.1',
520
+ clientVersion = 'claworld-plugin/0.1.3',
521
521
  sessionTarget,
522
522
  fallbackTarget,
523
523
  } = {}) {