@totalreclaw/totalreclaw 3.3.1-rc.20 → 3.3.1-rc.21

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.
package/dist/index.js CHANGED
@@ -63,8 +63,9 @@ import { executePinOperation, validatePinArgs, } from './pin.js';
63
63
  import { executeRetype, executeSetScope, validateRetypeArgs, validateSetScopeArgs, } from './retype-setscope.js';
64
64
  import { PluginHotCache } from './hot-cache-wrapper.js';
65
65
  import { CONFIG, setRecoveryPhraseOverride } from './config.js';
66
+ import { buildRelayHeaders } from './relay-headers.js';
66
67
  import { readBillingCache, writeBillingCache, BILLING_CACHE_PATH, } from './billing-cache.js';
67
- import { ensureMemoryHeaderFile, loadCredentialsJson, writeCredentialsJson, deleteCredentialsFile, isRunningInDocker, deleteFileIfExists, resolveOnboardingState, writeOnboardingState, readPluginVersion, } from './fs-helpers.js';
68
+ import { ensureMemoryHeaderFile, loadCredentialsJson, writeCredentialsJson, deleteCredentialsFile, isRunningInDocker, deleteFileIfExists, resolveOnboardingState, writeOnboardingState, readPluginVersion, cleanupInstallStagingDirs, } from './fs-helpers.js';
68
69
  import { isRcBuild } from './qa-bug-report.js';
69
70
  import { decideToolGate, isGatedToolName } from './tool-gating.js';
70
71
  import { detectFirstRun, buildWelcomePrepend } from './first-run.js';
@@ -73,6 +74,18 @@ import { detectGatewayHost } from './gateway-url.js';
73
74
  import { validateMnemonic } from '@scure/bip39';
74
75
  import { wordlist } from '@scure/bip39/wordlists/english.js';
75
76
  import crypto from 'node:crypto';
77
+ import { createRequire } from 'node:module';
78
+ import { fileURLToPath } from 'node:url';
79
+ import * as nodePath from 'node:path';
80
+ // CJS-style require for the @totalreclaw/core WASM module. We keep this
81
+ // load path lazy (only inside getSmartImportWasm() below) so a partial
82
+ // install of the dependency tree doesn't crash module init. Bare
83
+ // `require()` is a CommonJS global and is undefined under bare Node ESM —
84
+ // the shipped `dist/index.js` declares `"type":"module"`, so calling the
85
+ // global directly emits "require is not defined" at runtime (issue #124).
86
+ // createRequire bridges the gap. Same shape as crypto.ts / lsh.ts /
87
+ // subgraph-store.ts / claims-helper.ts.
88
+ const __cjsRequire = createRequire(import.meta.url);
76
89
  // ---------------------------------------------------------------------------
77
90
  // Human-friendly error messages
78
91
  // ---------------------------------------------------------------------------
@@ -632,11 +645,10 @@ async function initialize(logger) {
632
645
  const billingUrl = CONFIG.serverUrl;
633
646
  const resp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
634
647
  method: 'GET',
635
- headers: {
648
+ headers: buildRelayHeaders({
636
649
  'Authorization': `Bearer ${authKeyHex}`,
637
650
  'Accept': 'application/json',
638
- 'X-TotalReclaw-Client': 'openclaw-plugin',
639
- },
651
+ }),
640
652
  });
641
653
  if (resp.ok) {
642
654
  const billingData = await resp.json();
@@ -1121,12 +1133,12 @@ const MIGRATION_PAGE_SIZE = 1000;
1121
1133
  /** Execute a GraphQL query against a subgraph endpoint. Returns null on error. */
1122
1134
  async function migrationGqlQuery(endpoint, query, variables, authKey) {
1123
1135
  try {
1124
- const headers = {
1136
+ const overrides = {
1125
1137
  'Content-Type': 'application/json',
1126
- 'X-TotalReclaw-Client': 'openclaw-plugin',
1127
1138
  };
1128
1139
  if (authKey)
1129
- headers['Authorization'] = `Bearer ${authKey}`;
1140
+ overrides['Authorization'] = `Bearer ${authKey}`;
1141
+ const headers = buildRelayHeaders(overrides);
1130
1142
  const response = await fetch(endpoint, {
1131
1143
  method: 'POST',
1132
1144
  headers,
@@ -1867,11 +1879,14 @@ async function handlePluginImportFrom(params, logger) {
1867
1879
  // ---------------------------------------------------------------------------
1868
1880
  // Smart Import — Two-Pass Pipeline (Profile + Triage)
1869
1881
  // ---------------------------------------------------------------------------
1870
- // Lazy-load WASM for smart import functions (same pattern as crypto.ts / subgraph-store.ts).
1882
+ // Lazy-load WASM for smart import functions (same pattern as crypto.ts /
1883
+ // subgraph-store.ts). Goes through __cjsRequire (createRequire(import.meta.url))
1884
+ // declared at the top of the file — bare `require()` is undefined under
1885
+ // pure-ESM Node, see issue #124.
1871
1886
  let _smartImportWasm = null;
1872
1887
  function getSmartImportWasm() {
1873
1888
  if (!_smartImportWasm)
1874
- _smartImportWasm = require('@totalreclaw/core');
1889
+ _smartImportWasm = __cjsRequire('@totalreclaw/core');
1875
1890
  return _smartImportWasm;
1876
1891
  }
1877
1892
  /**
@@ -2298,17 +2313,35 @@ const plugin = {
2298
2313
  // the loopback callsite if package.json read fails.
2299
2314
  let pluginVersion = null;
2300
2315
  try {
2301
- // `import.meta.url` is ESM-only; fallback to `__dirname` for the CJS
2302
- // build path. `require` comes from Node core and is available in both
2303
- // module formats. `fileURLToPath` / `path.dirname` are pure-sync.
2304
- const url = require('node:url');
2305
- const nodePath = require('node:path');
2306
- const pluginDir = nodePath.dirname(url.fileURLToPath(import.meta.url));
2316
+ // Resolve our own dist/ directory so `readPluginVersion` can locate
2317
+ // package.json. We use `import.meta.url` + ESM-static stdlib imports
2318
+ // (`fileURLToPath` from `node:url`, `nodePath.dirname` from `node:path`,
2319
+ // both imported at the top of this file). Earlier shape used inline
2320
+ // `require('node:url')` — undefined under bare-ESM Node, broke the
2321
+ // before_agent_start hook in the published rc.20 bundle (issue #124).
2322
+ const pluginDir = nodePath.dirname(fileURLToPath(import.meta.url));
2307
2323
  pluginVersion = readPluginVersion(pluginDir);
2308
2324
  rcMode = isRcBuild(pluginVersion);
2309
2325
  if (rcMode) {
2310
2326
  api.logger.info(`TotalReclaw: RC build detected (version=${pluginVersion}). RC-gated tools will be registered.`);
2311
2327
  }
2328
+ // 3.3.1-rc.21 (issue #126 — rc.20 finding F3): clean up
2329
+ // `.openclaw-install-stage-*` siblings left behind by an interrupted
2330
+ // `openclaw plugins install` run. Without cleanup, OpenClaw's plugin
2331
+ // loader auto-discovers the orphan directory on the next gateway
2332
+ // start and registers a duplicate `totalreclaw` plugin (duplicate
2333
+ // hooks, duplicate tools, "duplicate-plugin-id" warning every cycle).
2334
+ // Best-effort — never throws on permission / race failures.
2335
+ try {
2336
+ const removed = cleanupInstallStagingDirs(pluginDir);
2337
+ if (removed.length > 0) {
2338
+ api.logger.info(`TotalReclaw: removed ${removed.length} stale install-staging dir(s) from prior interrupted install: ${removed.join(', ')}`);
2339
+ }
2340
+ }
2341
+ catch {
2342
+ // Best-effort — already swallowed inside the helper, but keep this
2343
+ // outer try as belt-and-braces against future helper changes.
2344
+ }
2312
2345
  }
2313
2346
  catch {
2314
2347
  rcMode = false;
@@ -2459,8 +2492,30 @@ const plugin = {
2459
2492
  // Write credentials.json + flip state to 'active' via
2460
2493
  // fs-helpers. This centralizes disk I/O off the
2461
2494
  // pair-http surface (scanner isolation).
2495
+ //
2496
+ // 3.3.1 (internal#130) — derive + persist the Smart Account
2497
+ // address right here so the user can see their scope address
2498
+ // immediately after pair, without waiting for a first chain
2499
+ // write. SA derivation runs locally (WASM deriveEoa + factory
2500
+ // getAddress eth_call); the mnemonic NEVER crosses any new
2501
+ // boundary — it's already on disk in credentials.json and is
2502
+ // consumed by the same `deriveSmartAccountAddress` call the
2503
+ // store/search paths use. Only the derived public address is
2504
+ // persisted to credentials.json (`scope_address`).
2505
+ let scopeAddress;
2506
+ try {
2507
+ scopeAddress = await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
2508
+ }
2509
+ catch (err) {
2510
+ // Best-effort. If chain RPC is unreachable at pair time, the
2511
+ // status tool re-tries derivation lazily on next call —
2512
+ // fall through and write credentials.json without it.
2513
+ api.logger.warn(`pair: scope_address derivation failed (will retry lazily): ${err instanceof Error ? err.message : String(err)}`);
2514
+ }
2462
2515
  const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
2463
2516
  const next = { ...creds, mnemonic };
2517
+ if (scopeAddress)
2518
+ next.scope_address = scopeAddress;
2464
2519
  if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
2465
2520
  return { state: 'error', error: 'credentials_write_failed' };
2466
2521
  }
@@ -3145,11 +3200,10 @@ const plugin = {
3145
3200
  : { owner, first: PAGE_SIZE };
3146
3201
  const res = await fetch(`${relayUrl}/v1/subgraph`, {
3147
3202
  method: 'POST',
3148
- headers: {
3203
+ headers: buildRelayHeaders({
3149
3204
  'Content-Type': 'application/json',
3150
- 'X-TotalReclaw-Client': 'openclaw-plugin',
3151
3205
  ...(authKeyHex ? { Authorization: `Bearer ${authKeyHex}` } : {}),
3152
- },
3206
+ }),
3153
3207
  body: JSON.stringify({ query, variables }),
3154
3208
  });
3155
3209
  const json = (await res.json());
@@ -3275,11 +3329,10 @@ const plugin = {
3275
3329
  const walletAddr = subgraphOwner || userId || '';
3276
3330
  const response = await fetch(`${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
3277
3331
  method: 'GET',
3278
- headers: {
3332
+ headers: buildRelayHeaders({
3279
3333
  'Authorization': `Bearer ${authKeyHex}`,
3280
3334
  'Accept': 'application/json',
3281
- 'X-TotalReclaw-Client': 'openclaw-plugin',
3282
- },
3335
+ }),
3283
3336
  });
3284
3337
  if (!response.ok) {
3285
3338
  const body = await response.text().catch(() => '');
@@ -3300,6 +3353,32 @@ const plugin = {
3300
3353
  features: data.features,
3301
3354
  checked_at: Date.now(),
3302
3355
  });
3356
+ // 3.3.1 (internal#130) — surface the Smart Account / scope
3357
+ // address so the user can do subgraph queries, BaseScan
3358
+ // lookups, and cross-client portability checks BEFORE any
3359
+ // chain write completes. Resolution priority:
3360
+ // 1. In-memory `subgraphOwner` (already derived earlier).
3361
+ // 2. credentials.json `scope_address` (persisted at pair).
3362
+ // 3. Lazy derive now from the loaded mnemonic + cache it
3363
+ // back to credentials.json so the next call is free.
3364
+ let scopeAddress = subgraphOwner ?? undefined;
3365
+ if (!scopeAddress) {
3366
+ try {
3367
+ const credsCache = loadCredentialsJson(CREDENTIALS_PATH);
3368
+ if (credsCache?.scope_address && typeof credsCache.scope_address === 'string') {
3369
+ scopeAddress = credsCache.scope_address;
3370
+ }
3371
+ else if (credsCache?.mnemonic && typeof credsCache.mnemonic === 'string') {
3372
+ scopeAddress = await deriveSmartAccountAddress(credsCache.mnemonic, CONFIG.chainId);
3373
+ if (scopeAddress) {
3374
+ writeCredentialsJson(CREDENTIALS_PATH, { ...credsCache, scope_address: scopeAddress });
3375
+ }
3376
+ }
3377
+ }
3378
+ catch (deriveErr) {
3379
+ api.logger.warn(`totalreclaw_status: scope_address lookup failed: ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`);
3380
+ }
3381
+ }
3303
3382
  const tierLabel = tier === 'pro' ? 'Pro' : 'Free';
3304
3383
  const lines = [
3305
3384
  `Tier: ${tierLabel}`,
@@ -3308,12 +3387,20 @@ const plugin = {
3308
3387
  if (freeWritesResetAt) {
3309
3388
  lines.push(`Resets: ${new Date(freeWritesResetAt).toLocaleDateString()}`);
3310
3389
  }
3390
+ if (scopeAddress) {
3391
+ lines.push(`Smart Account: ${scopeAddress}`);
3392
+ }
3311
3393
  if (tier !== 'pro') {
3312
3394
  lines.push(`Pricing: https://totalreclaw.xyz/pricing`);
3313
3395
  }
3314
3396
  return {
3315
3397
  content: [{ type: 'text', text: lines.join('\n') }],
3316
- details: { tier, free_writes_used: freeWritesUsed, free_writes_limit: freeWritesLimit },
3398
+ details: {
3399
+ tier,
3400
+ free_writes_used: freeWritesUsed,
3401
+ free_writes_limit: freeWritesLimit,
3402
+ scope_address: scopeAddress,
3403
+ },
3317
3404
  };
3318
3405
  }
3319
3406
  catch (err) {
@@ -3972,11 +4059,10 @@ const plugin = {
3972
4059
  }
3973
4060
  const response = await fetch(`${serverUrl}/v1/billing/checkout`, {
3974
4061
  method: 'POST',
3975
- headers: {
4062
+ headers: buildRelayHeaders({
3976
4063
  'Authorization': `Bearer ${authKeyHex}`,
3977
4064
  'Content-Type': 'application/json',
3978
- 'X-TotalReclaw-Client': 'openclaw-plugin',
3979
- },
4065
+ }),
3980
4066
  body: JSON.stringify({
3981
4067
  wallet_address: walletAddr,
3982
4068
  tier: 'pro',
@@ -4045,11 +4131,10 @@ const plugin = {
4045
4131
  // 1. Check billing tier
4046
4132
  const billingResp = await fetch(`${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(subgraphOwner)}`, {
4047
4133
  method: 'GET',
4048
- headers: {
4134
+ headers: buildRelayHeaders({
4049
4135
  'Authorization': `Bearer ${authKeyHex}`,
4050
4136
  'Content-Type': 'application/json',
4051
- 'X-TotalReclaw-Client': 'openclaw-plugin',
4052
- },
4137
+ }),
4053
4138
  });
4054
4139
  if (!billingResp.ok) {
4055
4140
  return { content: [{ type: 'text', text: `Failed to check billing tier (HTTP ${billingResp.status}).` }] };
@@ -4304,8 +4389,23 @@ const plugin = {
4304
4389
  phraseValidator: (p) => validateMnemonic(p, wordlist),
4305
4390
  completePairing: async ({ mnemonic }) => {
4306
4391
  try {
4392
+ // 3.3.1 (internal#130) — derive + persist the
4393
+ // Smart Account address now so the user can see
4394
+ // it immediately after pair, before any chain
4395
+ // write. Mnemonic stays in this scope (already
4396
+ // on disk in credentials.json); only the
4397
+ // derived public scope_address is added.
4398
+ let scopeAddress;
4399
+ try {
4400
+ scopeAddress = await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
4401
+ }
4402
+ catch (deriveErr) {
4403
+ api.logger.warn(`totalreclaw_pair(relay): scope_address derivation failed (will retry lazily): ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`);
4404
+ }
4307
4405
  const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
4308
4406
  const next = { ...creds, mnemonic };
4407
+ if (scopeAddress)
4408
+ next.scope_address = scopeAddress;
4309
4409
  if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
4310
4410
  return { state: 'error', error: 'credentials_write_failed' };
4311
4411
  }
@@ -4316,7 +4416,7 @@ const plugin = {
4316
4416
  credentialsCreatedAt: new Date().toISOString(),
4317
4417
  version: pluginVersion ?? '3.3.0',
4318
4418
  });
4319
- api.logger.info(`totalreclaw_pair(relay): session ${remoteSession.token.slice(0, 8)}… completed; credentials written`);
4419
+ api.logger.info(`totalreclaw_pair(relay): session ${remoteSession.token.slice(0, 8)}… completed; credentials written${scopeAddress ? ` (scope_address=${scopeAddress})` : ''}`);
4320
4420
  return { state: 'active' };
4321
4421
  }
4322
4422
  catch (err) {
@@ -4447,9 +4547,18 @@ const plugin = {
4447
4547
  // declared. If the agent then reports the tool is missing from its
4448
4548
  // tool list, the gap is upstream OpenClaw tool propagation, not our
4449
4549
  // plugin — see issue #110 fix 3 + PR #102 (CLI fallback).
4450
- api.logger.info('TotalReclaw: registerTool(totalreclaw_pair) returned. If the agent does not see it in its tool list ' +
4451
- 'after gateway restart, the issue is upstream tool injection (containerized agents) fall back to ' +
4452
- '`openclaw totalreclaw pair generate --url-pin-only` (PR #102) or `openclaw totalreclaw onboard --pair-only`.');
4550
+ //
4551
+ // 3.3.1-rc.21 (issue #128): the breadcrumb is debug-gradeit was
4552
+ // bleeding into `openclaw agent --json` stdout, breaking programmatic
4553
+ // parsers that expect the JSON-RPC body to be the only thing on the
4554
+ // wire. Gated behind `TOTALRECLAW_VERBOSE_REGISTER` (or the general
4555
+ // `TOTALRECLAW_DEBUG` toggle) so ops can opt back in when chasing
4556
+ // a tool-injection regression. Default OFF — clean stdout for users.
4557
+ if (CONFIG.verboseRegister) {
4558
+ api.logger.info('TotalReclaw: registerTool(totalreclaw_pair) returned. If the agent does not see it in its tool list ' +
4559
+ 'after gateway restart, the issue is upstream tool injection (containerized agents) — fall back to ' +
4560
+ '`openclaw totalreclaw pair generate --url-pin-only` (PR #102) or `openclaw totalreclaw onboard --pair-only`.');
4561
+ }
4453
4562
  // ---------------------------------------------------------------
4454
4563
  // Tool: totalreclaw_report_qa_bug (3.3.1-rc.3 — RC-gated)
4455
4564
  //
@@ -4573,7 +4682,13 @@ const plugin = {
4573
4682
  }
4574
4683
  },
4575
4684
  }, { name: 'totalreclaw_report_qa_bug' });
4576
- api.logger.info('totalreclaw_report_qa_bug registered (RC build this tool is hidden in stable releases).');
4685
+ // 3.3.1-rc.21 (issue #128): demote the registration-confirmation
4686
+ // breadcrumb to verbose-only. Same `--json` stdout pollution risk
4687
+ // as the totalreclaw_pair breadcrumb above; ops can opt back in
4688
+ // via TOTALRECLAW_VERBOSE_REGISTER / TOTALRECLAW_DEBUG.
4689
+ if (CONFIG.verboseRegister) {
4690
+ api.logger.info('totalreclaw_report_qa_bug registered (RC build — this tool is hidden in stable releases).');
4691
+ }
4577
4692
  }
4578
4693
  // ---------------------------------------------------------------
4579
4694
  // Hook: before_tool_call (3.2.0 memory-tool gate)
@@ -4696,7 +4811,7 @@ const plugin = {
4696
4811
  const walletParam = encodeURIComponent(subgraphOwner || userId || '');
4697
4812
  const billingResp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${walletParam}`, {
4698
4813
  method: 'GET',
4699
- headers: { 'Authorization': `Bearer ${authKeyHex}`, 'Accept': 'application/json', 'X-TotalReclaw-Client': 'openclaw-plugin' },
4814
+ headers: buildRelayHeaders({ 'Authorization': `Bearer ${authKeyHex}`, 'Accept': 'application/json' }),
4700
4815
  });
4701
4816
  if (billingResp.ok) {
4702
4817
  const billingData = await billingResp.json();
package/dist/lsh.js CHANGED
@@ -6,11 +6,16 @@
6
6
  *
7
7
  * Default parameters: 32 bits per table, 20 tables.
8
8
  */
9
- // Lazy-load WASM to avoid crash when npm install hasn't finished yet.
9
+ // Lazy-load WASM via createRequire. The shipped `dist/index.js` is ESM-only
10
+ // (`"type":"module"`) so the bare `require` global is undefined at runtime.
11
+ // See issue #124 for the bug this avoids; matches the pattern in
12
+ // claims-helper / consolidation / digest-sync / pin / retype-setscope.
13
+ import { createRequire } from 'node:module';
14
+ const requireWasm = createRequire(import.meta.url);
10
15
  let _WasmLshHasher = null;
11
16
  function getWasmLshHasher() {
12
17
  if (!_WasmLshHasher)
13
- _WasmLshHasher = require('@totalreclaw/core').WasmLshHasher;
18
+ _WasmLshHasher = requireWasm('@totalreclaw/core').WasmLshHasher;
14
19
  return _WasmLshHasher;
15
20
  }
16
21
  /**
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared outbound-header helper for relay calls.
3
+ *
4
+ * Centralizes the common `X-TotalReclaw-*` headers so every fetch site
5
+ * consistently tags requests with:
6
+ * - `X-TotalReclaw-Client` — caller identity (defaults to `openclaw-plugin`).
7
+ * - `X-TotalReclaw-Session` — optional QA / observability tag from
8
+ * `TOTALRECLAW_SESSION_ID`. Used by Axiom log filters and the
9
+ * `qa-totalreclaw` skill to scope log searches per QA run.
10
+ *
11
+ * Pure function — no I/O, no network. Reads `getSessionId()` (which reads the
12
+ * env var via getter so harnesses that flip the env between calls pick up
13
+ * the new value).
14
+ *
15
+ * The session-id env var was accidentally placed in the v1 REMOVED_ENV_VARS
16
+ * list and silently warned-and-dropped, breaking Axiom traceability for QA
17
+ * runs (see internal#127). This helper is the canonical re-entry point for
18
+ * the variable.
19
+ */
20
+ import { getSessionId } from './config.js';
21
+ /** Default `X-TotalReclaw-Client` value. */
22
+ export const DEFAULT_CLIENT_ID = 'openclaw-plugin';
23
+ /**
24
+ * Build the standard outbound header set.
25
+ *
26
+ * @param overrides - merge-in additional headers (`Authorization`,
27
+ * `Content-Type`, etc.); these win over the defaults.
28
+ * @param clientId - override the `X-TotalReclaw-Client` value.
29
+ *
30
+ * Always includes `X-TotalReclaw-Client`. Includes `X-TotalReclaw-Session`
31
+ * only when `TOTALRECLAW_SESSION_ID` is set + non-empty.
32
+ */
33
+ export function buildRelayHeaders(overrides = {}, clientId = DEFAULT_CLIENT_ID) {
34
+ const headers = {
35
+ 'X-TotalReclaw-Client': clientId,
36
+ };
37
+ const sessionId = getSessionId();
38
+ if (sessionId) {
39
+ headers['X-TotalReclaw-Session'] = sessionId;
40
+ }
41
+ // Caller-supplied headers (Authorization, Content-Type, Accept, etc.) take
42
+ // precedence over the defaults but should generally not stomp the X-* tags.
43
+ return { ...headers, ...overrides };
44
+ }
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { getSubgraphConfig } from './subgraph-store.js';
23
23
  import { CONFIG } from './config.js';
24
+ import { buildRelayHeaders } from './relay-headers.js';
24
25
  /** Batch size for Phase 2 split queries. */
25
26
  const TRAPDOOR_BATCH_SIZE = CONFIG.trapdoorBatchSize;
26
27
  /** Graph Studio / Graph Network hard limit on `first` argument. */
@@ -31,13 +32,13 @@ const PAGE_SIZE = CONFIG.pageSize;
31
32
  */
32
33
  async function gqlQuery(endpoint, query, variables, authKeyHex) {
33
34
  try {
34
- const headers = {
35
+ const overrides = {
35
36
  'Content-Type': 'application/json',
36
- 'X-TotalReclaw-Client': 'openclaw-plugin',
37
37
  };
38
38
  if (authKeyHex) {
39
- headers['Authorization'] = `Bearer ${authKeyHex}`;
39
+ overrides['Authorization'] = `Bearer ${authKeyHex}`;
40
40
  }
41
+ const headers = buildRelayHeaders(overrides);
41
42
  const response = await fetch(endpoint, {
42
43
  method: 'POST',
43
44
  headers,
@@ -9,14 +9,19 @@
9
9
  * ECDSA signing. Raw fetch() for all JSON-RPC calls to the relay bundler
10
10
  * and chain RPCs. No viem, no permissionless.
11
11
  */
12
- // Lazy-load WASM to avoid crash when npm install hasn't finished yet.
12
+ // Lazy-load WASM via createRequire the shipped bundle is ESM-only and
13
+ // the bare `require` global is undefined there (issue #124). Same pattern
14
+ // as crypto / lsh / claims-helper / consolidation / digest-sync.
15
+ import { createRequire } from 'node:module';
16
+ const requireWasm = createRequire(import.meta.url);
13
17
  let _wasm = null;
14
18
  function getWasm() {
15
19
  if (!_wasm)
16
- _wasm = require('@totalreclaw/core');
20
+ _wasm = requireWasm('@totalreclaw/core');
17
21
  return _wasm;
18
22
  }
19
23
  import { CONFIG } from './config.js';
24
+ import { buildRelayHeaders } from './relay-headers.js';
20
25
  // ---------------------------------------------------------------------------
21
26
  // Pimlico 429 retry helper
22
27
  // ---------------------------------------------------------------------------
@@ -294,14 +299,14 @@ export async function submitFactOnChain(protobufPayload, config) {
294
299
  }
295
300
  async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
296
301
  const bundlerUrl = `${config.relayUrl}/v1/bundler`;
297
- const headers = {
302
+ const overrides = {
298
303
  'Content-Type': 'application/json',
299
- 'X-TotalReclaw-Client': 'openclaw-plugin',
300
304
  };
301
305
  if (config.authKeyHex)
302
- headers['Authorization'] = `Bearer ${config.authKeyHex}`;
306
+ overrides['Authorization'] = `Bearer ${config.authKeyHex}`;
303
307
  if (config.walletAddress)
304
- headers['X-Wallet-Address'] = config.walletAddress;
308
+ overrides['X-Wallet-Address'] = config.walletAddress;
309
+ const headers = buildRelayHeaders(overrides);
305
310
  // Helper for JSON-RPC calls to relay bundler (with 429 retry)
306
311
  async function rpc(method, params) {
307
312
  return rpcWithRetry(bundlerUrl, headers, method, params);
@@ -482,14 +487,14 @@ export async function submitFactBatchOnChain(protobufPayloads, config) {
482
487
  }
483
488
  async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sender) {
484
489
  const bundlerUrl = `${config.relayUrl}/v1/bundler`;
485
- const headers = {
490
+ const overrides = {
486
491
  'Content-Type': 'application/json',
487
- 'X-TotalReclaw-Client': 'openclaw-plugin',
488
492
  };
489
493
  if (config.authKeyHex)
490
- headers['Authorization'] = `Bearer ${config.authKeyHex}`;
494
+ overrides['Authorization'] = `Bearer ${config.authKeyHex}`;
491
495
  if (config.walletAddress)
492
- headers['X-Wallet-Address'] = config.walletAddress;
496
+ overrides['X-Wallet-Address'] = config.walletAddress;
497
+ const headers = buildRelayHeaders(overrides);
493
498
  // Helper for JSON-RPC calls to relay bundler (with 429 retry)
494
499
  async function rpc(method, params) {
495
500
  return rpcWithRetry(bundlerUrl, headers, method, params);
package/fs-helpers.ts CHANGED
@@ -56,6 +56,15 @@ export interface CredentialsFile {
56
56
  mnemonic?: string;
57
57
  /** Alias for `mnemonic`, accepted on read only. */
58
58
  recovery_phrase?: string;
59
+ /**
60
+ * Smart Account (scope) address derived from the mnemonic. Persisted at
61
+ * pair-finish so users + tools (`totalreclaw_status`) can read it before
62
+ * any on-chain write. Internal#130 — lazy SA derivation previously left
63
+ * the user blind to their scope address until first-write.
64
+ *
65
+ * Format: lowercase 0x-prefixed 40-hex-char address. Public, non-secret.
66
+ */
67
+ scope_address?: string;
59
68
  firstRunAnnouncementShown?: boolean;
60
69
  [extra: string]: unknown;
61
70
  }
@@ -252,6 +261,89 @@ export function deleteFileIfExists(filePath: string): void {
252
261
  }
253
262
  }
254
263
 
264
+ // ---------------------------------------------------------------------------
265
+ // Install-staging cleanup (issue #126 — rc.20 finding F3)
266
+ // ---------------------------------------------------------------------------
267
+
268
+ /**
269
+ * Clean up `.openclaw-install-stage-*` sibling directories left behind by
270
+ * an interrupted `openclaw plugins install` run.
271
+ *
272
+ * Background
273
+ * ----------
274
+ * `openclaw plugins install @totalreclaw/totalreclaw` extracts the npm
275
+ * tarball into a staging directory named
276
+ * `<extensionsDir>/.openclaw-install-stage-XXXXXX/` and then renames it
277
+ * to `<extensionsDir>/totalreclaw/` on success. If the install is
278
+ * interrupted partway through (e.g. an auto-gateway-restart triggered by
279
+ * the same install kills the process — see rc.20 QA finding F3), the
280
+ * staging dir survives. On the next gateway start, OpenClaw's plugin
281
+ * loader auto-discovers BOTH directories — the real `totalreclaw/` and
282
+ * the orphaned `.openclaw-install-stage-XXXXXX/` — and registers two
283
+ * copies of the plugin. Hooks fire twice, the user sees a duplicate
284
+ * `totalreclaw` row in `openclaw plugins list`, and the gateway log
285
+ * spams a duplicate-plugin-id warning every cycle.
286
+ *
287
+ * Fix scope: best-effort cleanup driven by the plugin itself at register
288
+ * time. We resolve the extensions dir as the parent of the loaded
289
+ * plugin's own directory, scan for `.openclaw-install-stage-*` siblings,
290
+ * and recursively remove each one. If anything fails (permission,
291
+ * race with a concurrent install), we swallow the error — the existing
292
+ * loader-warning behavior is no worse than before.
293
+ *
294
+ * Returns the list of staging-dir paths that were successfully removed.
295
+ * Callers may log this for ops visibility. Empty list on a clean install.
296
+ *
297
+ * Parameters
298
+ * ----------
299
+ * @param pluginDir Absolute path to the loaded plugin's directory
300
+ * (typically `<extensionsDir>/totalreclaw/dist`). The
301
+ * helper walks up to the parent that holds sibling
302
+ * plugin directories (the `extensions/` root).
303
+ * @param _now Optional clock injector for testing — defaults to
304
+ * Date.now().
305
+ */
306
+ export function cleanupInstallStagingDirs(
307
+ pluginDir: string,
308
+ _now: () => number = Date.now,
309
+ ): string[] {
310
+ const removed: string[] = [];
311
+ try {
312
+ // pluginDir is `<extensionsDir>/totalreclaw/dist` after build, so the
313
+ // siblings live two levels up. Resolve both candidates so the helper
314
+ // works regardless of whether the caller passes the package root or
315
+ // its `dist/` subdir.
316
+ const candidates = [
317
+ path.resolve(pluginDir, '..'), // <extensionsDir>/totalreclaw → siblings dir if pluginDir is `dist`
318
+ path.resolve(pluginDir, '..', '..'), // <extensionsDir>/ → siblings dir if pluginDir is package root
319
+ ];
320
+
321
+ for (const extensionsDir of candidates) {
322
+ let entries: string[];
323
+ try {
324
+ entries = fs.readdirSync(extensionsDir);
325
+ } catch {
326
+ continue;
327
+ }
328
+ for (const name of entries) {
329
+ if (!name.startsWith('.openclaw-install-stage-')) continue;
330
+ const target = path.join(extensionsDir, name);
331
+ try {
332
+ const st = fs.lstatSync(target);
333
+ if (!st.isDirectory()) continue;
334
+ fs.rmSync(target, { recursive: true, force: true });
335
+ removed.push(target);
336
+ } catch {
337
+ // Best-effort — skip unreadable / racy entries.
338
+ }
339
+ }
340
+ }
341
+ } catch {
342
+ // Best-effort — never crash plugin init on cleanup failure.
343
+ }
344
+ return removed;
345
+ }
346
+
255
347
  // ---------------------------------------------------------------------------
256
348
  // Auto-bootstrap of credentials.json (3.1.0 first-run UX)
257
349
  // ---------------------------------------------------------------------------