@xfxstudio/claworld 0.2.20 → 0.2.22

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.
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "0.2.20",
11
+ "version": "0.2.22",
12
12
  "configSchema": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -17,6 +17,10 @@ import {
17
17
  resolveClaworldRuntimeConfig,
18
18
  validateClaworldChannelConfig,
19
19
  } from './config-schema.js';
20
+ import {
21
+ loadClaworldRuntimeBackup,
22
+ persistClaworldRuntimeBackup,
23
+ } from './runtime-backup.js';
20
24
  import {
21
25
  claworldOnboardingAdapter,
22
26
  claworldSetupAdapter,
@@ -1895,48 +1899,118 @@ export function createClaworldChannelPlugin({
1895
1899
  }
1896
1900
 
1897
1901
  async function persistRuntimeAppToken({ runtime, accountId, appToken, relayAgentId = null }) {
1898
- if (!runtime?.config?.loadConfig || !runtime?.config?.writeConfigFile) {
1899
- return { skipped: true, reason: 'missing_runtime_config_io' };
1900
- }
1901
1902
  if (!accountId || !appToken) {
1902
1903
  return { skipped: true, reason: 'missing_account_or_token' };
1903
1904
  }
1904
1905
 
1905
- const currentCfg = await runtime.config.loadConfig();
1906
- const nextCfg = JSON.parse(JSON.stringify(currentCfg || {}));
1907
- nextCfg.channels = nextCfg.channels && typeof nextCfg.channels === 'object' && !Array.isArray(nextCfg.channels)
1908
- ? nextCfg.channels
1909
- : {};
1910
- const claworldRoot = nextCfg.channels.claworld && typeof nextCfg.channels.claworld === 'object' && !Array.isArray(nextCfg.channels.claworld)
1911
- ? nextCfg.channels.claworld
1912
- : {};
1913
- const accounts = claworldRoot.accounts && typeof claworldRoot.accounts === 'object' && !Array.isArray(claworldRoot.accounts)
1914
- ? claworldRoot.accounts
1915
- : {};
1916
- const account = accounts[accountId] && typeof accounts[accountId] === 'object' && !Array.isArray(accounts[accountId])
1917
- ? accounts[accountId]
1918
- : {};
1906
+ let configPersistResult = { skipped: true, reason: 'missing_runtime_config_io' };
1907
+ if (runtime?.config?.loadConfig && runtime?.config?.writeConfigFile) {
1908
+ const currentCfg = await runtime.config.loadConfig();
1909
+ const nextCfg = JSON.parse(JSON.stringify(currentCfg || {}));
1910
+ nextCfg.channels = nextCfg.channels && typeof nextCfg.channels === 'object' && !Array.isArray(nextCfg.channels)
1911
+ ? nextCfg.channels
1912
+ : {};
1913
+ const claworldRoot = nextCfg.channels.claworld && typeof nextCfg.channels.claworld === 'object' && !Array.isArray(nextCfg.channels.claworld)
1914
+ ? nextCfg.channels.claworld
1915
+ : {};
1916
+ const accounts = claworldRoot.accounts && typeof claworldRoot.accounts === 'object' && !Array.isArray(claworldRoot.accounts)
1917
+ ? claworldRoot.accounts
1918
+ : {};
1919
+ const account = accounts[accountId] && typeof accounts[accountId] === 'object' && !Array.isArray(accounts[accountId])
1920
+ ? accounts[accountId]
1921
+ : {};
1922
+
1923
+ const normalizedRelayAgentId = normalizeClaworldText(relayAgentId, normalizeClaworldText(account?.relay?.agentId, null));
1924
+ const currentAppToken = normalizeClaworldText(account.appToken, null);
1925
+ const currentRelayAgentId = normalizeClaworldText(account?.relay?.agentId, null);
1926
+ if (currentAppToken === appToken && currentRelayAgentId === normalizedRelayAgentId) {
1927
+ configPersistResult = { skipped: true, reason: 'already_persisted' };
1928
+ } else {
1929
+ accounts[accountId] = {
1930
+ ...account,
1931
+ appToken,
1932
+ relay: {
1933
+ ...(account?.relay && typeof account.relay === 'object' && !Array.isArray(account.relay) ? account.relay : {}),
1934
+ ...(normalizedRelayAgentId ? { agentId: normalizedRelayAgentId } : {}),
1935
+ },
1936
+ };
1937
+ delete accounts[accountId].registration;
1938
+ claworldRoot.accounts = accounts;
1939
+ nextCfg.channels.claworld = claworldRoot;
1940
+ await runtime.config.writeConfigFile(nextCfg);
1941
+ configPersistResult = { skipped: false, ok: true };
1942
+ }
1943
+ }
1944
+
1945
+ let backupPersistResult = { skipped: true, reason: 'missing_runtime_config_loader' };
1946
+ try {
1947
+ backupPersistResult = await persistClaworldRuntimeBackup({
1948
+ runtime,
1949
+ accountId,
1950
+ });
1951
+ } catch (error) {
1952
+ backupPersistResult = {
1953
+ skipped: true,
1954
+ reason: 'backup_persist_failed',
1955
+ error: error?.message || String(error),
1956
+ };
1957
+ }
1958
+
1959
+ return {
1960
+ ...configPersistResult,
1961
+ backup: backupPersistResult,
1962
+ };
1963
+ }
1919
1964
 
1920
- const normalizedRelayAgentId = normalizeClaworldText(relayAgentId, normalizeClaworldText(account?.relay?.agentId, null));
1921
- const currentAppToken = normalizeClaworldText(account.appToken, null);
1922
- const currentRelayAgentId = normalizeClaworldText(account?.relay?.agentId, null);
1923
- if (currentAppToken === appToken && currentRelayAgentId === normalizedRelayAgentId) {
1924
- return { skipped: true, reason: 'already_persisted' };
1965
+ async function maybeRestoreRuntimeAppToken({ runtime, accountId, runtimeConfig }) {
1966
+ if (resolveRuntimeAppToken(runtimeConfig)) {
1967
+ return { restored: false, reason: 'already_configured', runtimeConfig };
1925
1968
  }
1926
1969
 
1927
- accounts[accountId] = {
1928
- ...account,
1929
- appToken,
1930
- relay: {
1931
- ...(account?.relay && typeof account.relay === 'object' && !Array.isArray(account.relay) ? account.relay : {}),
1932
- ...(normalizedRelayAgentId ? { agentId: normalizedRelayAgentId } : {}),
1933
- },
1970
+ const backupState = await loadClaworldRuntimeBackup({ accountId });
1971
+ const backup = backupState.backup;
1972
+ const backupToken = normalizeClaworldText(backup?.appToken, null);
1973
+ if (!backupToken) {
1974
+ return {
1975
+ restored: false,
1976
+ reason: 'backup_missing_app_token',
1977
+ runtimeConfig,
1978
+ };
1979
+ }
1980
+
1981
+ const backupServerUrl = normalizeClaworldText(backup?.serverUrl, null);
1982
+ const currentServerUrl = normalizeClaworldText(runtimeConfig?.serverUrl, null);
1983
+ if (backupServerUrl && currentServerUrl && normalizeRelayHttpBaseUrl(backupServerUrl) !== normalizeRelayHttpBaseUrl(currentServerUrl)) {
1984
+ return {
1985
+ restored: false,
1986
+ reason: 'backup_server_mismatch',
1987
+ runtimeConfig,
1988
+ };
1989
+ }
1990
+
1991
+ const restoredRuntimeConfig = applyRuntimeIdentity(runtimeConfig, {
1992
+ appToken: backupToken,
1993
+ });
1994
+
1995
+ try {
1996
+ await persistRuntimeAppToken({
1997
+ runtime,
1998
+ accountId,
1999
+ appToken: backupToken,
2000
+ });
2001
+ } catch (error) {
2002
+ logger.warn?.(`[claworld:${accountId || 'default'}] failed to persist restored runtime appToken`, {
2003
+ error: error?.message || String(error),
2004
+ });
2005
+ }
2006
+
2007
+ return {
2008
+ restored: true,
2009
+ reason: 'installer_state_backup',
2010
+ runtimeConfig: restoredRuntimeConfig,
2011
+ backup,
2012
+ installerStatePath: backupState.installerStatePath,
1934
2013
  };
1935
- delete accounts[accountId].registration;
1936
- claworldRoot.accounts = accounts;
1937
- nextCfg.channels.claworld = claworldRoot;
1938
- await runtime.config.writeConfigFile(nextCfg);
1939
- return { skipped: false, ok: true };
1940
2014
  }
1941
2015
 
1942
2016
  function resolveConfiguredRuntimeContext(context = {}) {
@@ -1959,6 +2033,19 @@ export function createClaworldChannelPlugin({
1959
2033
  const cfg = configuredContext.cfg || {};
1960
2034
  const accountId = configuredContext.accountId || null;
1961
2035
  let runtimeConfig = configuredContext.runtimeConfig;
2036
+ const runtimeResolution = resolvePluginRuntimeCandidate(context.runtime || null);
2037
+ const restoredBinding = await maybeRestoreRuntimeAppToken({
2038
+ runtime: runtimeResolution.runtime,
2039
+ accountId,
2040
+ runtimeConfig,
2041
+ });
2042
+ if (restoredBinding.restored) {
2043
+ runtimeConfig = restoredBinding.runtimeConfig;
2044
+ logger.info?.(`[claworld:${accountId || 'default'}] restored runtime binding from installer state`, {
2045
+ installerStatePath: restoredBinding.installerStatePath || null,
2046
+ relayAgentId: runtimeConfig?.relay?.agentId || null,
2047
+ });
2048
+ }
1962
2049
  const runtimeContext = accountRuntimeContexts.get(accountId || 'default') || null;
1963
2050
  if (runtimeContext?.runtimeConfig && !runtimeContext?.deferredFailure) {
1964
2051
  return {
@@ -2009,6 +2096,20 @@ export function createClaworldChannelPlugin({
2009
2096
  runtimeConfigShape: summarizeObjectShape(runtimeConfig),
2010
2097
  });
2011
2098
 
2099
+ const runtimeResolution = resolvePluginRuntimeCandidate(context.runtime || null);
2100
+ const restoredBinding = await maybeRestoreRuntimeAppToken({
2101
+ runtime: runtimeResolution.runtime,
2102
+ accountId: runtimeAccountId,
2103
+ runtimeConfig,
2104
+ });
2105
+ if (restoredBinding.restored) {
2106
+ runtimeConfig = restoredBinding.runtimeConfig;
2107
+ logger.info?.(`[claworld:${runtimeAccountId}] restored runtime binding from installer state`, {
2108
+ installerStatePath: restoredBinding.installerStatePath || null,
2109
+ relayAgentId: runtimeConfig?.relay?.agentId || null,
2110
+ });
2111
+ }
2112
+
2012
2113
  const validation = validateClaworldChannelConfig(configSource, context.accountId);
2013
2114
  if (!validation.ok && sourceType !== 'root_cfg') {
2014
2115
  logger.warn?.(`[claworld:${runtimeAccountId}] non-root runtime source would not validate as full cfg`, {
@@ -2084,7 +2185,6 @@ export function createClaworldChannelPlugin({
2084
2185
  return { startedDeferred: true, reason: 'missing_runtime_context', runtimeConfig };
2085
2186
  }
2086
2187
 
2087
- const runtimeResolution = resolvePluginRuntimeCandidate(context.runtime || null);
2088
2188
  const pluginRuntime = runtimeResolution.runtime;
2089
2189
  const runtimeSource = runtimeResolution.runtimeSource;
2090
2190
 
@@ -2092,24 +2192,24 @@ export function createClaworldChannelPlugin({
2092
2192
  runtimeSource,
2093
2193
  });
2094
2194
 
2095
- if (binding.bindingSource !== 'configured_app_token') {
2096
- try {
2097
- const persisted = await persistRuntimeAppToken({
2098
- runtime: pluginRuntime,
2195
+ try {
2196
+ const persisted = await persistRuntimeAppToken({
2197
+ runtime: pluginRuntime,
2198
+ accountId: runtimeConfig.accountId,
2199
+ appToken: resolveRuntimeAppToken(runtimeConfig),
2200
+ relayAgentId: runtimeConfig.relay?.agentId || null,
2201
+ });
2202
+ if (!persisted.skipped || persisted.backup?.skipped === false) {
2203
+ logger.info?.(`[claworld:${runtimeAccountId}] persisted runtime binding state`, {
2099
2204
  accountId: runtimeConfig.accountId,
2100
- appToken: resolveRuntimeAppToken(runtimeConfig),
2101
- relayAgentId: runtimeConfig.relay?.agentId || null,
2102
- });
2103
- if (!persisted.skipped) {
2104
- logger.info?.(`[claworld:${runtimeAccountId}] persisted runtime appToken`, {
2105
- accountId: runtimeConfig.accountId,
2106
- });
2107
- }
2108
- } catch (error) {
2109
- logger.warn?.(`[claworld:${runtimeAccountId}] failed to persist runtime appToken`, {
2110
- error: error?.message || String(error),
2205
+ configSkipped: persisted.skipped === true,
2206
+ backupSkipped: persisted.backup?.skipped === true,
2111
2207
  });
2112
2208
  }
2209
+ } catch (error) {
2210
+ logger.warn?.(`[claworld:${runtimeAccountId}] failed to persist runtime appToken`, {
2211
+ error: error?.message || String(error),
2212
+ });
2113
2213
  }
2114
2214
 
2115
2215
  accountRuntimeContexts.set(accountKey, {
@@ -1022,12 +1022,20 @@ function buildRegisteredTools(api, plugin) {
1022
1022
  });
1023
1023
  const pairedAgentId = identityPayload?.agentId || runtimeConfig.relay?.agentId || null;
1024
1024
  const relayAgent = pairedAgentId
1025
- ? await plugin.helpers.pairing.resolveAgentIdentity({
1026
- cfg,
1027
- accountId,
1028
- runtimeConfig,
1025
+ ? {
1029
1026
  agentId: pairedAgentId,
1030
- })
1027
+ displayName: normalizeText(
1028
+ identityPayload?.publicIdentity?.displayName,
1029
+ normalizeText(
1030
+ identityPayload?.recommendedDisplayName,
1031
+ normalizeText(runtimeConfig?.name, normalizeText(runtimeConfig?.registration?.displayName, null)),
1032
+ ),
1033
+ ),
1034
+ discoverable: null,
1035
+ contactable: null,
1036
+ online: null,
1037
+ resolved: null,
1038
+ }
1031
1039
  : null;
1032
1040
  const hasConfiguredAppToken = Boolean(
1033
1041
  runtimeConfig.appToken
@@ -459,7 +459,7 @@ export class ClaworldRelayClient extends EventEmitter {
459
459
  config,
460
460
  agentId,
461
461
  credential = null,
462
- clientVersion = 'claworld-plugin/0.2.20',
462
+ clientVersion = 'claworld-plugin/0.2.22',
463
463
  sessionTarget,
464
464
  fallbackTarget,
465
465
  } = {}) {
@@ -0,0 +1,105 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import {
6
+ DEFAULT_CLAWORLD_ACCOUNT_ID,
7
+ findClaworldManagedRuntimeBackup,
8
+ setClaworldManagedRuntimeBackupState,
9
+ stripClaworldManagedRuntimeConfig,
10
+ } from './managed-config.js';
11
+
12
+ function normalizeText(value, fallback = null) {
13
+ if (value == null) return fallback;
14
+ const normalized = String(value).trim();
15
+ return normalized || fallback;
16
+ }
17
+
18
+ export function resolveDefaultOpenClawConfigPath() {
19
+ return path.resolve(normalizeText(
20
+ process.env.OPENCLAW_CONFIG_PATH,
21
+ path.join(os.homedir(), '.openclaw', 'openclaw.json'),
22
+ ));
23
+ }
24
+
25
+ export function resolveClaworldInstallerStatePath(configPath = resolveDefaultOpenClawConfigPath()) {
26
+ const resolvedConfigPath = path.resolve(String(configPath));
27
+ return path.join(path.dirname(resolvedConfigPath), '.claworld-installer-state.json');
28
+ }
29
+
30
+ async function loadInstallerStateFromDisk(installerStatePath) {
31
+ try {
32
+ const raw = await fs.readFile(installerStatePath, 'utf8');
33
+ const parsed = JSON.parse(raw);
34
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
35
+ } catch (error) {
36
+ if (error?.code === 'ENOENT') return {};
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ async function writeInstallerStateToDisk(installerStatePath, installerState) {
42
+ const nextState = installerState && typeof installerState === 'object' && !Array.isArray(installerState)
43
+ ? installerState
44
+ : {};
45
+ if (Object.keys(nextState).length === 0) {
46
+ await fs.rm(installerStatePath, { force: true });
47
+ return;
48
+ }
49
+ await fs.mkdir(path.dirname(installerStatePath), { recursive: true });
50
+ await fs.writeFile(installerStatePath, `${JSON.stringify(nextState, null, 2)}\n`, 'utf8');
51
+ }
52
+
53
+ export async function loadClaworldRuntimeBackup({
54
+ accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
55
+ configPath = resolveDefaultOpenClawConfigPath(),
56
+ } = {}) {
57
+ const installerStatePath = resolveClaworldInstallerStatePath(configPath);
58
+ const installerState = await loadInstallerStateFromDisk(installerStatePath);
59
+ return {
60
+ installerStatePath,
61
+ installerState,
62
+ backup: findClaworldManagedRuntimeBackup(installerState, accountId),
63
+ };
64
+ }
65
+
66
+ export async function persistClaworldRuntimeBackup({
67
+ runtime = null,
68
+ accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
69
+ configPath = resolveDefaultOpenClawConfigPath(),
70
+ } = {}) {
71
+ if (!runtime?.config?.loadConfig) {
72
+ return { skipped: true, reason: 'missing_runtime_config_loader' };
73
+ }
74
+
75
+ const currentConfig = await runtime.config.loadConfig();
76
+ const { backup } = stripClaworldManagedRuntimeConfig(currentConfig, {
77
+ accountId,
78
+ preserveBackup: true,
79
+ });
80
+
81
+ if (!backup?.appToken) {
82
+ return { skipped: true, reason: 'missing_app_token', backup: null };
83
+ }
84
+
85
+ const installerStatePath = resolveClaworldInstallerStatePath(configPath);
86
+ const installerState = await loadInstallerStateFromDisk(installerStatePath);
87
+ const previousBackup = findClaworldManagedRuntimeBackup(installerState, accountId);
88
+ if (JSON.stringify(previousBackup || null) === JSON.stringify(backup)) {
89
+ return {
90
+ skipped: true,
91
+ reason: 'already_persisted',
92
+ installerStatePath,
93
+ backup,
94
+ };
95
+ }
96
+
97
+ setClaworldManagedRuntimeBackupState(installerState, accountId, backup);
98
+ await writeInstallerStateToDisk(installerStatePath, installerState);
99
+ return {
100
+ skipped: false,
101
+ ok: true,
102
+ installerStatePath,
103
+ backup,
104
+ };
105
+ }