@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/CHANGELOG.md +47 -0
- package/api-client.ts +18 -11
- package/config.ts +60 -3
- package/crypto.ts +10 -2
- package/dist/api-client.js +17 -11
- package/dist/config.js +60 -3
- package/dist/crypto.js +10 -2
- package/dist/fs-helpers.js +82 -0
- package/dist/index.js +149 -34
- package/dist/lsh.js +7 -2
- package/dist/relay-headers.js +44 -0
- package/dist/subgraph-search.js +4 -3
- package/dist/subgraph-store.js +15 -10
- package/fs-helpers.ts +92 -0
- package/index.ts +166 -39
- package/lsh.ts +7 -2
- package/package.json +3 -2
- package/relay-headers.ts +50 -0
- package/subgraph-search.ts +4 -3
- package/subgraph-store.ts +15 -10
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
|
-
|
|
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
|
|
1136
|
+
const overrides = {
|
|
1125
1137
|
'Content-Type': 'application/json',
|
|
1126
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
1127
1138
|
};
|
|
1128
1139
|
if (authKey)
|
|
1129
|
-
|
|
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 /
|
|
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 =
|
|
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
|
-
//
|
|
2302
|
-
//
|
|
2303
|
-
//
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4550
|
+
//
|
|
4551
|
+
// 3.3.1-rc.21 (issue #128): the breadcrumb is debug-grade — it 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
|
-
|
|
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'
|
|
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
|
|
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 =
|
|
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
|
+
}
|
package/dist/subgraph-search.js
CHANGED
|
@@ -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
|
|
35
|
+
const overrides = {
|
|
35
36
|
'Content-Type': 'application/json',
|
|
36
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
37
37
|
};
|
|
38
38
|
if (authKeyHex) {
|
|
39
|
-
|
|
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,
|
package/dist/subgraph-store.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
302
|
+
const overrides = {
|
|
298
303
|
'Content-Type': 'application/json',
|
|
299
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
300
304
|
};
|
|
301
305
|
if (config.authKeyHex)
|
|
302
|
-
|
|
306
|
+
overrides['Authorization'] = `Bearer ${config.authKeyHex}`;
|
|
303
307
|
if (config.walletAddress)
|
|
304
|
-
|
|
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
|
|
490
|
+
const overrides = {
|
|
486
491
|
'Content-Type': 'application/json',
|
|
487
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
488
492
|
};
|
|
489
493
|
if (config.authKeyHex)
|
|
490
|
-
|
|
494
|
+
overrides['Authorization'] = `Bearer ${config.authKeyHex}`;
|
|
491
495
|
if (config.walletAddress)
|
|
492
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|