@totalreclaw/totalreclaw 3.3.1-rc.8 → 3.3.1
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 +268 -1
- package/SKILL.md +29 -23
- package/api-client.ts +18 -11
- package/claims-helper.ts +47 -1
- package/config.ts +108 -4
- package/confirm-indexed.ts +191 -0
- package/crypto.ts +10 -2
- package/dist/api-client.js +226 -0
- package/dist/billing-cache.js +100 -0
- package/dist/claims-helper.js +624 -0
- package/dist/config.js +297 -0
- package/dist/confirm-indexed.js +127 -0
- package/dist/consolidation.js +258 -0
- package/dist/contradiction-sync.js +1034 -0
- package/dist/crypto.js +138 -0
- package/dist/digest-sync.js +361 -0
- package/dist/download-ux.js +63 -0
- package/dist/embedder-cache.js +185 -0
- package/dist/embedder-loader.js +121 -0
- package/dist/embedder-network.js +301 -0
- package/dist/embedding.js +141 -0
- package/dist/extractor.js +1225 -0
- package/dist/first-run.js +103 -0
- package/dist/fs-helpers.js +725 -0
- package/dist/gateway-url.js +197 -0
- package/dist/generate-mnemonic.js +13 -0
- package/dist/hot-cache-wrapper.js +101 -0
- package/dist/import-adapters/base-adapter.js +64 -0
- package/dist/import-adapters/chatgpt-adapter.js +238 -0
- package/dist/import-adapters/claude-adapter.js +114 -0
- package/dist/import-adapters/gemini-adapter.js +201 -0
- package/dist/import-adapters/index.js +26 -0
- package/dist/import-adapters/mcp-memory-adapter.js +219 -0
- package/dist/import-adapters/mem0-adapter.js +158 -0
- package/dist/import-adapters/types.js +1 -0
- package/dist/index.js +5388 -0
- package/dist/llm-client.js +687 -0
- package/dist/llm-profile-reader.js +346 -0
- package/dist/lsh.js +62 -0
- package/dist/onboarding-cli.js +750 -0
- package/dist/pair-cli.js +344 -0
- package/dist/pair-crypto.js +359 -0
- package/dist/pair-http.js +404 -0
- package/dist/pair-page.js +826 -0
- package/dist/pair-qr.js +107 -0
- package/dist/pair-remote-client.js +410 -0
- package/dist/pair-session-store.js +566 -0
- package/dist/pin.js +556 -0
- package/dist/qa-bug-report.js +301 -0
- package/dist/relay-headers.js +44 -0
- package/dist/reranker.js +409 -0
- package/dist/retype-setscope.js +368 -0
- package/dist/semantic-dedup.js +75 -0
- package/dist/subgraph-search.js +289 -0
- package/dist/subgraph-store.js +694 -0
- package/dist/tool-gating.js +58 -0
- package/download-ux.ts +91 -0
- package/embedder-cache.ts +230 -0
- package/embedder-loader.ts +189 -0
- package/embedder-network.ts +350 -0
- package/embedding.ts +118 -27
- package/fs-helpers.ts +277 -0
- package/gateway-url.ts +57 -9
- package/index.ts +469 -250
- package/llm-client.ts +4 -3
- package/lsh.ts +7 -2
- package/onboarding-cli.ts +114 -1
- package/package.json +24 -5
- package/pair-cli.ts +76 -8
- package/pair-crypto.ts +34 -24
- package/pair-page.ts +28 -17
- package/pair-qr.ts +152 -0
- package/pair-remote-client.ts +540 -0
- package/pin.ts +31 -0
- package/qa-bug-report.ts +84 -2
- package/relay-headers.ts +50 -0
- package/reranker.ts +40 -0
- package/retype-setscope.ts +69 -8
- package/skill.json +1 -1
- package/subgraph-search.ts +4 -3
- package/subgraph-store.ts +15 -10
package/index.ts
CHANGED
|
@@ -73,7 +73,15 @@ import {
|
|
|
73
73
|
type MemorySource,
|
|
74
74
|
type MemoryScope,
|
|
75
75
|
} from './extractor.js';
|
|
76
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
initLLMClient,
|
|
78
|
+
resolveLLMConfig,
|
|
79
|
+
chatCompletion,
|
|
80
|
+
generateEmbedding,
|
|
81
|
+
getEmbeddingDims,
|
|
82
|
+
getEmbeddingModelId,
|
|
83
|
+
configureEmbedder,
|
|
84
|
+
} from './llm-client.js';
|
|
77
85
|
import {
|
|
78
86
|
defaultAuthProfilesRoot,
|
|
79
87
|
readAllProfileKeys,
|
|
@@ -92,6 +100,7 @@ import {
|
|
|
92
100
|
type DecryptedCandidate,
|
|
93
101
|
} from './consolidation.js';
|
|
94
102
|
import { isSubgraphMode, getSubgraphConfig, encodeFactProtobuf, submitFactOnChain, submitFactBatchOnChain, deriveSmartAccountAddress, PROTOBUF_VERSION_V4, type FactPayload } from './subgraph-store.js';
|
|
103
|
+
import { confirmIndexed } from './confirm-indexed.js';
|
|
95
104
|
import {
|
|
96
105
|
DIGEST_TRAPDOOR,
|
|
97
106
|
buildCanonicalClaim,
|
|
@@ -135,6 +144,7 @@ import {
|
|
|
135
144
|
} from './onboarding-cli.js';
|
|
136
145
|
import { PluginHotCache, type HotFact } from './hot-cache-wrapper.js';
|
|
137
146
|
import { CONFIG, setRecoveryPhraseOverride } from './config.js';
|
|
147
|
+
import { buildRelayHeaders } from './relay-headers.js';
|
|
138
148
|
import {
|
|
139
149
|
readBillingCache,
|
|
140
150
|
writeBillingCache,
|
|
@@ -151,6 +161,9 @@ import {
|
|
|
151
161
|
resolveOnboardingState,
|
|
152
162
|
writeOnboardingState,
|
|
153
163
|
readPluginVersion,
|
|
164
|
+
cleanupInstallStagingDirs,
|
|
165
|
+
detectPartialInstall,
|
|
166
|
+
clearPartialInstallMarker,
|
|
154
167
|
type OnboardingState,
|
|
155
168
|
} from './fs-helpers.js';
|
|
156
169
|
import { isRcBuild } from './qa-bug-report.js';
|
|
@@ -161,6 +174,19 @@ import { detectGatewayHost } from './gateway-url.js';
|
|
|
161
174
|
import { validateMnemonic } from '@scure/bip39';
|
|
162
175
|
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
163
176
|
import crypto from 'node:crypto';
|
|
177
|
+
import { createRequire } from 'node:module';
|
|
178
|
+
import { fileURLToPath } from 'node:url';
|
|
179
|
+
import * as nodePath from 'node:path';
|
|
180
|
+
|
|
181
|
+
// CJS-style require for the @totalreclaw/core WASM module. We keep this
|
|
182
|
+
// load path lazy (only inside getSmartImportWasm() below) so a partial
|
|
183
|
+
// install of the dependency tree doesn't crash module init. Bare
|
|
184
|
+
// `require()` is a CommonJS global and is undefined under bare Node ESM —
|
|
185
|
+
// the shipped `dist/index.js` declares `"type":"module"`, so calling the
|
|
186
|
+
// global directly emits "require is not defined" at runtime (issue #124).
|
|
187
|
+
// createRequire bridges the gap. Same shape as crypto.ts / lsh.ts /
|
|
188
|
+
// subgraph-store.ts / claims-helper.ts.
|
|
189
|
+
const __cjsRequire = createRequire(import.meta.url);
|
|
164
190
|
|
|
165
191
|
// ---------------------------------------------------------------------------
|
|
166
192
|
// OpenClaw Plugin API type (defined locally to avoid SDK dependency)
|
|
@@ -341,8 +367,17 @@ function buildPairingUrl(
|
|
|
341
367
|
// Layers 4 + 5 — auto-detect via gateway-url helper (Tailscale CGNAT, then LAN)
|
|
342
368
|
else {
|
|
343
369
|
let detected: ReturnType<typeof detectGatewayHost> = null;
|
|
370
|
+
// issue #110 fix 4 — pass `isDocker` so LAN detection skips
|
|
371
|
+
// 172.16/12 bridge IPs that no external browser can reach.
|
|
372
|
+
let isDocker = false;
|
|
344
373
|
try {
|
|
345
|
-
|
|
374
|
+
isDocker = isRunningInDocker();
|
|
375
|
+
} catch {
|
|
376
|
+
// Defensive: never block URL building on Docker sniff errors.
|
|
377
|
+
isDocker = false;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
detected = detectGatewayHost({ isDocker });
|
|
346
381
|
} catch (err) {
|
|
347
382
|
api.logger.warn(
|
|
348
383
|
`TotalReclaw: host autodetect crashed: ${err instanceof Error ? err.message : String(err)} — falling back to localhost`,
|
|
@@ -368,9 +403,27 @@ function buildPairingUrl(
|
|
|
368
403
|
`Set plugins.entries.totalreclaw.config.publicUrl for remote access.`,
|
|
369
404
|
);
|
|
370
405
|
} else {
|
|
371
|
-
// Layer 6 — localhost fallback
|
|
406
|
+
// Layer 6 — localhost fallback (or Docker-aware relay-pointer warning)
|
|
372
407
|
const bind = cfg?.gateway?.bind;
|
|
373
|
-
if (
|
|
408
|
+
if (isDocker) {
|
|
409
|
+
// issue #110 fix 4: inside Docker the LAN IP is container-internal
|
|
410
|
+
// and useless. Loopback localhost only works for `docker exec`
|
|
411
|
+
// tests. The CORRECT pair URL for Docker is the relay-brokered
|
|
412
|
+
// path served by the `totalreclaw_pair` agent tool (CONFIG.pairMode
|
|
413
|
+
// === 'relay' since rc.11). The CLI-only path here cannot mint a
|
|
414
|
+
// relay session synchronously (the relay handshake needs a WS
|
|
415
|
+
// round-trip), so we emit the loopback URL with a LOUD warning
|
|
416
|
+
// pointing the operator at the agent tool / publicUrl override.
|
|
417
|
+
api.logger.warn(
|
|
418
|
+
`TotalReclaw: Docker container detected — pairing URL falling back to ` +
|
|
419
|
+
`http://localhost:${port}, which is unreachable from the host browser. ` +
|
|
420
|
+
`Use the totalreclaw_pair AGENT TOOL (relay-brokered, universally reachable) ` +
|
|
421
|
+
`instead of the CLI fallback, OR set plugins.entries.totalreclaw.config.publicUrl ` +
|
|
422
|
+
`to your gateway's host-reachable URL (e.g. http://<host-ip>:${port} when the ` +
|
|
423
|
+
`Docker port is published). Setting TOTALRECLAW_PAIR_MODE=relay is the default; ` +
|
|
424
|
+
`air-gapped operators on TOTALRECLAW_PAIR_MODE=local must publish a port + set publicUrl.`,
|
|
425
|
+
);
|
|
426
|
+
} else if (bind === 'lan' || bind === 'tailnet') {
|
|
374
427
|
api.logger.warn(
|
|
375
428
|
`TotalReclaw: pairing URL falling back to localhost because gateway.bind=${bind} could not be autodetected. ` +
|
|
376
429
|
'Set plugins.entries.totalreclaw.config.publicUrl to override.',
|
|
@@ -849,11 +902,10 @@ async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
|
|
|
849
902
|
const billingUrl = CONFIG.serverUrl;
|
|
850
903
|
const resp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
|
|
851
904
|
method: 'GET',
|
|
852
|
-
headers: {
|
|
905
|
+
headers: buildRelayHeaders({
|
|
853
906
|
'Authorization': `Bearer ${authKeyHex}`,
|
|
854
907
|
'Accept': 'application/json',
|
|
855
|
-
|
|
856
|
-
},
|
|
908
|
+
}),
|
|
857
909
|
});
|
|
858
910
|
if (resp.ok) {
|
|
859
911
|
const billingData = await resp.json() as Record<string, unknown>;
|
|
@@ -1426,11 +1478,11 @@ async function migrationGqlQuery<T>(
|
|
|
1426
1478
|
authKey?: string,
|
|
1427
1479
|
): Promise<T | null> {
|
|
1428
1480
|
try {
|
|
1429
|
-
const
|
|
1481
|
+
const overrides: Record<string, string> = {
|
|
1430
1482
|
'Content-Type': 'application/json',
|
|
1431
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
1432
1483
|
};
|
|
1433
|
-
if (authKey)
|
|
1484
|
+
if (authKey) overrides['Authorization'] = `Bearer ${authKey}`;
|
|
1485
|
+
const headers = buildRelayHeaders(overrides);
|
|
1434
1486
|
const response = await fetch(endpoint, {
|
|
1435
1487
|
method: 'POST',
|
|
1436
1488
|
headers,
|
|
@@ -1905,6 +1957,11 @@ async function storeExtractedFacts(
|
|
|
1905
1957
|
fact: factForBlob,
|
|
1906
1958
|
importance: effectiveImportance,
|
|
1907
1959
|
sourceAgent: factSource,
|
|
1960
|
+
// 3.3.1-rc.22 — tag every new claim with the active embedder id
|
|
1961
|
+
// so future distillation can rescore selectively. Plugin-only
|
|
1962
|
+
// field; survives the core validator strip via re-attach in
|
|
1963
|
+
// `buildCanonicalClaimV1`.
|
|
1964
|
+
embeddingModelId: getEmbeddingModelId(),
|
|
1908
1965
|
});
|
|
1909
1966
|
|
|
1910
1967
|
const factId = crypto.randomUUID();
|
|
@@ -2271,10 +2328,13 @@ async function handlePluginImportFrom(
|
|
|
2271
2328
|
// Smart Import — Two-Pass Pipeline (Profile + Triage)
|
|
2272
2329
|
// ---------------------------------------------------------------------------
|
|
2273
2330
|
|
|
2274
|
-
// Lazy-load WASM for smart import functions (same pattern as crypto.ts /
|
|
2331
|
+
// Lazy-load WASM for smart import functions (same pattern as crypto.ts /
|
|
2332
|
+
// subgraph-store.ts). Goes through __cjsRequire (createRequire(import.meta.url))
|
|
2333
|
+
// declared at the top of the file — bare `require()` is undefined under
|
|
2334
|
+
// pure-ESM Node, see issue #124.
|
|
2275
2335
|
let _smartImportWasm: typeof import('@totalreclaw/core') | null = null;
|
|
2276
2336
|
function getSmartImportWasm() {
|
|
2277
|
-
if (!_smartImportWasm) _smartImportWasm =
|
|
2337
|
+
if (!_smartImportWasm) _smartImportWasm = __cjsRequire('@totalreclaw/core');
|
|
2278
2338
|
return _smartImportWasm;
|
|
2279
2339
|
}
|
|
2280
2340
|
|
|
@@ -2805,17 +2865,87 @@ const plugin = {
|
|
|
2805
2865
|
// function — stable builds never see it. The version is resolved via
|
|
2806
2866
|
// `readPluginVersion` from fs-helpers.ts (scanner-safe, pure-fs).
|
|
2807
2867
|
let rcMode = false;
|
|
2868
|
+
// Plugin version resolved from package.json once at register time. Reused
|
|
2869
|
+
// by writeOnboardingState callsites below so the `version` field in
|
|
2870
|
+
// state.json tracks the actual shipped plugin version (avoids drift —
|
|
2871
|
+
// e.g. rc.18 finding F4 where a hardcoded `'3.3.1-rc.11'` stayed put
|
|
2872
|
+
// through 7 RC bumps). Fallback `'3.3.0'` matches the prior literal at
|
|
2873
|
+
// the loopback callsite if package.json read fails.
|
|
2874
|
+
let pluginVersion: string | null = null;
|
|
2808
2875
|
try {
|
|
2809
|
-
//
|
|
2810
|
-
//
|
|
2811
|
-
//
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
const
|
|
2816
|
-
|
|
2876
|
+
// Resolve our own dist/ directory so `readPluginVersion` can locate
|
|
2877
|
+
// package.json. We use `import.meta.url` + ESM-static stdlib imports
|
|
2878
|
+
// (`fileURLToPath` from `node:url`, `nodePath.dirname` from `node:path`,
|
|
2879
|
+
// both imported at the top of this file). Earlier shape used inline
|
|
2880
|
+
// `require('node:url')` — undefined under bare-ESM Node, broke the
|
|
2881
|
+
// before_agent_start hook in the published rc.20 bundle (issue #124).
|
|
2882
|
+
const pluginDir = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
2883
|
+
pluginVersion = readPluginVersion(pluginDir);
|
|
2884
|
+
rcMode = isRcBuild(pluginVersion);
|
|
2817
2885
|
if (rcMode) {
|
|
2818
|
-
api.logger.info(`TotalReclaw: RC build detected (version=${
|
|
2886
|
+
api.logger.info(`TotalReclaw: RC build detected (version=${pluginVersion}). RC-gated tools will be registered.`);
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// 3.3.1-rc.21 (issue #126 — rc.20 finding F3): clean up
|
|
2890
|
+
// `.openclaw-install-stage-*` siblings left behind by an interrupted
|
|
2891
|
+
// `openclaw plugins install` run. Without cleanup, OpenClaw's plugin
|
|
2892
|
+
// loader auto-discovers the orphan directory on the next gateway
|
|
2893
|
+
// start and registers a duplicate `totalreclaw` plugin (duplicate
|
|
2894
|
+
// hooks, duplicate tools, "duplicate-plugin-id" warning every cycle).
|
|
2895
|
+
// Best-effort — never throws on permission / race failures.
|
|
2896
|
+
try {
|
|
2897
|
+
const removed = cleanupInstallStagingDirs(pluginDir);
|
|
2898
|
+
if (removed.length > 0) {
|
|
2899
|
+
api.logger.info(
|
|
2900
|
+
`TotalReclaw: removed ${removed.length} stale install-staging dir(s) from prior interrupted install: ${removed.join(', ')}`,
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
} catch {
|
|
2904
|
+
// Best-effort — already swallowed inside the helper, but keep this
|
|
2905
|
+
// outer try as belt-and-braces against future helper changes.
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// 3.3.1-rc.22 — wire the lazy-embedder runtime config so the first
|
|
2909
|
+
// `generateEmbedding()` call knows where to cache the bundle and
|
|
2910
|
+
// which RC's GitHub Release to fetch from. `pluginVersion` may be
|
|
2911
|
+
// `null` if package.json is unreadable; the embedder defaults to
|
|
2912
|
+
// a "0.0.0-dev" tag in that case.
|
|
2913
|
+
try {
|
|
2914
|
+
configureEmbedder({
|
|
2915
|
+
cacheRoot: CONFIG.embedderCachePath,
|
|
2916
|
+
rcTag: pluginVersion ?? '0.0.0-dev',
|
|
2917
|
+
});
|
|
2918
|
+
} catch (err) {
|
|
2919
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2920
|
+
api.logger.warn(`TotalReclaw: configureEmbedder failed (will use defaults): ${msg}`);
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// 3.3.1-rc.22 (rc.21 finding #5): self-heal partial-install marker.
|
|
2924
|
+
// The `preinstall` npm script writes `.tr-partial-install`; the
|
|
2925
|
+
// `postinstall` script removes it on a successful install. If we
|
|
2926
|
+
// have gotten this far the loader did register us — meaning the
|
|
2927
|
+
// install succeeded enough to be useful — so any lingering marker
|
|
2928
|
+
// (e.g. npm ran preinstall but postinstall misfired) is stale.
|
|
2929
|
+
// Clear it so the next retry's detector does not see a false positive.
|
|
2930
|
+
//
|
|
2931
|
+
// 3.3.1-rc.22 (rc.21 finding #6) — gateway/reload upstream caveat:
|
|
2932
|
+
// OpenClaw's config-watcher fires `gateway/reload` when
|
|
2933
|
+
// `plugins.entries.totalreclaw` mutates (e.g. mid-install). In-flight
|
|
2934
|
+
// CLI clients see `1006 abnormal closure` and start a 600-second wait.
|
|
2935
|
+
// Proper fix is upstream OpenClaw FR. Plugin-side mitigation = these
|
|
2936
|
+
// helper calls MUST be idempotent under repeated register() calls
|
|
2937
|
+
// triggered by reload chatter. Asserted by
|
|
2938
|
+
// `install-reload-idempotency.test.ts`.
|
|
2939
|
+
try {
|
|
2940
|
+
const pluginRoot = nodePath.resolve(pluginDir, '..');
|
|
2941
|
+
const cleared = clearPartialInstallMarker(pluginRoot);
|
|
2942
|
+
if (cleared) {
|
|
2943
|
+
api.logger.info(
|
|
2944
|
+
`TotalReclaw: cleared stale .tr-partial-install marker (rc.22 finding #5)`,
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
} catch {
|
|
2948
|
+
// Best-effort. Helper logs internally and never throws.
|
|
2819
2949
|
}
|
|
2820
2950
|
} catch {
|
|
2821
2951
|
rcMode = false;
|
|
@@ -2902,6 +3032,12 @@ const plugin = {
|
|
|
2902
3032
|
credentialsPath: CREDENTIALS_PATH,
|
|
2903
3033
|
statePath: CONFIG.onboardingStatePath,
|
|
2904
3034
|
logger: api.logger,
|
|
3035
|
+
// 3.3.1-rc.18 — wire the pair flow into onboard so the
|
|
3036
|
+
// `--pair-only` flag (issue #95) can delegate to it without
|
|
3037
|
+
// duplicating session-store / URL-builder logic. Same deps
|
|
3038
|
+
// as the standalone `pair` subcommand.
|
|
3039
|
+
pairSessionsPath: CONFIG.pairSessionsPath,
|
|
3040
|
+
renderPairingUrl: (session) => buildPairingUrl(api, session),
|
|
2905
3041
|
// 3.3.1 — supplied to the non-interactive --json onboard path
|
|
2906
3042
|
// so the emitted payload includes the derived Smart Account
|
|
2907
3043
|
// (scope) address. Uses the chain-id default; Pro-tier
|
|
@@ -2974,8 +3110,30 @@ const plugin = {
|
|
|
2974
3110
|
// Write credentials.json + flip state to 'active' via
|
|
2975
3111
|
// fs-helpers. This centralizes disk I/O off the
|
|
2976
3112
|
// pair-http surface (scanner isolation).
|
|
3113
|
+
//
|
|
3114
|
+
// 3.3.1 (internal#130) — derive + persist the Smart Account
|
|
3115
|
+
// address right here so the user can see their scope address
|
|
3116
|
+
// immediately after pair, without waiting for a first chain
|
|
3117
|
+
// write. SA derivation runs locally (WASM deriveEoa + factory
|
|
3118
|
+
// getAddress eth_call); the mnemonic NEVER crosses any new
|
|
3119
|
+
// boundary — it's already on disk in credentials.json and is
|
|
3120
|
+
// consumed by the same `deriveSmartAccountAddress` call the
|
|
3121
|
+
// store/search paths use. Only the derived public address is
|
|
3122
|
+
// persisted to credentials.json (`scope_address`).
|
|
3123
|
+
let scopeAddress: string | undefined;
|
|
3124
|
+
try {
|
|
3125
|
+
scopeAddress = await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
|
|
3126
|
+
} catch (err) {
|
|
3127
|
+
// Best-effort. If chain RPC is unreachable at pair time, the
|
|
3128
|
+
// status tool re-tries derivation lazily on next call —
|
|
3129
|
+
// fall through and write credentials.json without it.
|
|
3130
|
+
api.logger.warn(
|
|
3131
|
+
`pair: scope_address derivation failed (will retry lazily): ${err instanceof Error ? err.message : String(err)}`,
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
2977
3134
|
const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
|
|
2978
|
-
const next = { ...creds, mnemonic };
|
|
3135
|
+
const next: typeof creds = { ...creds, mnemonic };
|
|
3136
|
+
if (scopeAddress) next.scope_address = scopeAddress;
|
|
2979
3137
|
if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
|
|
2980
3138
|
return { state: 'error', error: 'credentials_write_failed' };
|
|
2981
3139
|
}
|
|
@@ -2988,7 +3146,7 @@ const plugin = {
|
|
|
2988
3146
|
onboardingState: 'active',
|
|
2989
3147
|
createdBy: 'generate',
|
|
2990
3148
|
credentialsCreatedAt: new Date().toISOString(),
|
|
2991
|
-
version: '3.3.0',
|
|
3149
|
+
version: pluginVersion ?? '3.3.0',
|
|
2992
3150
|
});
|
|
2993
3151
|
return { state: 'active' };
|
|
2994
3152
|
},
|
|
@@ -3523,20 +3681,10 @@ const plugin = {
|
|
|
3523
3681
|
};
|
|
3524
3682
|
}
|
|
3525
3683
|
|
|
3526
|
-
// 6b.
|
|
3527
|
-
//
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
);
|
|
3531
|
-
if (maxCosine < COSINE_THRESHOLD) {
|
|
3532
|
-
api.logger.info(
|
|
3533
|
-
`Recall: cosine threshold gate filtered results (max=${maxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD})`,
|
|
3534
|
-
);
|
|
3535
|
-
return {
|
|
3536
|
-
content: [{ type: 'text', text: 'No relevant memories found for this query.' }],
|
|
3537
|
-
details: { count: 0, memories: [] },
|
|
3538
|
-
};
|
|
3539
|
-
}
|
|
3684
|
+
// 6b. Relevance gate removed in rc.22 -- core's intent-weighted
|
|
3685
|
+
// RRF + Tier 1 source weighting handles short queries via the
|
|
3686
|
+
// BM25 component, making the rc.18 cosine + lexical-override
|
|
3687
|
+
// band-aid (issue #116) redundant.
|
|
3540
3688
|
|
|
3541
3689
|
// 7. Format results.
|
|
3542
3690
|
const lines = reranked.map((m, i) => {
|
|
@@ -3675,14 +3823,29 @@ const plugin = {
|
|
|
3675
3823
|
throw new Error(`On-chain tombstone failed (tx=${result.txHash?.slice(0, 10) || 'none'}…)`);
|
|
3676
3824
|
}
|
|
3677
3825
|
api.logger.info(`Tombstone written for ${factId}: tx=${result.txHash}`);
|
|
3826
|
+
// Read-after-write: poll the subgraph until the original fact id
|
|
3827
|
+
// is no longer active (forget flips isActive=false). On timeout
|
|
3828
|
+
// surface `partial: true` so the agent can explain the chain
|
|
3829
|
+
// write succeeded but the subgraph is still propagating.
|
|
3830
|
+
const confirm = await confirmIndexed(factId, {
|
|
3831
|
+
expect: 'inactive',
|
|
3832
|
+
authKeyHex: authKeyHex!,
|
|
3833
|
+
});
|
|
3678
3834
|
return {
|
|
3679
3835
|
content: [{
|
|
3680
3836
|
type: 'text',
|
|
3681
|
-
text:
|
|
3682
|
-
`Memory ${factId} deleted on-chain (tx: ${result.txHash})
|
|
3683
|
-
|
|
3837
|
+
text: confirm.indexed
|
|
3838
|
+
? `Memory ${factId} deleted on-chain and confirmed by the subgraph (tx: ${result.txHash}).`
|
|
3839
|
+
: `Memory ${factId} deleted on-chain (tx: ${result.txHash}). ` +
|
|
3840
|
+
'The subgraph indexer is still propagating the change — ' +
|
|
3841
|
+
'recall/export may briefly show the memory as still active.',
|
|
3684
3842
|
}],
|
|
3685
|
-
details: {
|
|
3843
|
+
details: {
|
|
3844
|
+
deleted: true,
|
|
3845
|
+
txHash: result.txHash,
|
|
3846
|
+
factId,
|
|
3847
|
+
...(confirm.indexed ? {} : { partial: true }),
|
|
3848
|
+
},
|
|
3686
3849
|
};
|
|
3687
3850
|
} else {
|
|
3688
3851
|
await apiClient!.deleteFact(factId, authKeyHex!);
|
|
@@ -3758,11 +3921,10 @@ const plugin = {
|
|
|
3758
3921
|
|
|
3759
3922
|
const res = await fetch(`${relayUrl}/v1/subgraph`, {
|
|
3760
3923
|
method: 'POST',
|
|
3761
|
-
headers: {
|
|
3924
|
+
headers: buildRelayHeaders({
|
|
3762
3925
|
'Content-Type': 'application/json',
|
|
3763
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
3764
3926
|
...(authKeyHex ? { Authorization: `Bearer ${authKeyHex}` } : {}),
|
|
3765
|
-
},
|
|
3927
|
+
}),
|
|
3766
3928
|
body: JSON.stringify({ query, variables }),
|
|
3767
3929
|
});
|
|
3768
3930
|
|
|
@@ -3898,11 +4060,10 @@ const plugin = {
|
|
|
3898
4060
|
const walletAddr = subgraphOwner || userId || '';
|
|
3899
4061
|
const response = await fetch(`${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
|
|
3900
4062
|
method: 'GET',
|
|
3901
|
-
headers: {
|
|
4063
|
+
headers: buildRelayHeaders({
|
|
3902
4064
|
'Authorization': `Bearer ${authKeyHex}`,
|
|
3903
4065
|
'Accept': 'application/json',
|
|
3904
|
-
|
|
3905
|
-
},
|
|
4066
|
+
}),
|
|
3906
4067
|
});
|
|
3907
4068
|
|
|
3908
4069
|
if (!response.ok) {
|
|
@@ -3927,6 +4088,37 @@ const plugin = {
|
|
|
3927
4088
|
checked_at: Date.now(),
|
|
3928
4089
|
});
|
|
3929
4090
|
|
|
4091
|
+
// 3.3.1 (internal#130) — surface the Smart Account / scope
|
|
4092
|
+
// address so the user can do subgraph queries, BaseScan
|
|
4093
|
+
// lookups, and cross-client portability checks BEFORE any
|
|
4094
|
+
// chain write completes. Resolution priority:
|
|
4095
|
+
// 1. In-memory `subgraphOwner` (already derived earlier).
|
|
4096
|
+
// 2. credentials.json `scope_address` (persisted at pair).
|
|
4097
|
+
// 3. Lazy derive now from the loaded mnemonic + cache it
|
|
4098
|
+
// back to credentials.json so the next call is free.
|
|
4099
|
+
let scopeAddress: string | undefined =
|
|
4100
|
+
subgraphOwner ?? undefined;
|
|
4101
|
+
if (!scopeAddress) {
|
|
4102
|
+
try {
|
|
4103
|
+
const credsCache = loadCredentialsJson(CREDENTIALS_PATH);
|
|
4104
|
+
if (credsCache?.scope_address && typeof credsCache.scope_address === 'string') {
|
|
4105
|
+
scopeAddress = credsCache.scope_address;
|
|
4106
|
+
} else if (credsCache?.mnemonic && typeof credsCache.mnemonic === 'string') {
|
|
4107
|
+
scopeAddress = await deriveSmartAccountAddress(
|
|
4108
|
+
credsCache.mnemonic,
|
|
4109
|
+
CONFIG.chainId,
|
|
4110
|
+
);
|
|
4111
|
+
if (scopeAddress) {
|
|
4112
|
+
writeCredentialsJson(CREDENTIALS_PATH, { ...credsCache, scope_address: scopeAddress });
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
} catch (deriveErr) {
|
|
4116
|
+
api.logger.warn(
|
|
4117
|
+
`totalreclaw_status: scope_address lookup failed: ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`,
|
|
4118
|
+
);
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
|
|
3930
4122
|
const tierLabel = tier === 'pro' ? 'Pro' : 'Free';
|
|
3931
4123
|
const lines: string[] = [
|
|
3932
4124
|
`Tier: ${tierLabel}`,
|
|
@@ -3935,13 +4127,21 @@ const plugin = {
|
|
|
3935
4127
|
if (freeWritesResetAt) {
|
|
3936
4128
|
lines.push(`Resets: ${new Date(freeWritesResetAt).toLocaleDateString()}`);
|
|
3937
4129
|
}
|
|
4130
|
+
if (scopeAddress) {
|
|
4131
|
+
lines.push(`Smart Account: ${scopeAddress}`);
|
|
4132
|
+
}
|
|
3938
4133
|
if (tier !== 'pro') {
|
|
3939
4134
|
lines.push(`Pricing: https://totalreclaw.xyz/pricing`);
|
|
3940
4135
|
}
|
|
3941
4136
|
|
|
3942
4137
|
return {
|
|
3943
4138
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
3944
|
-
details: {
|
|
4139
|
+
details: {
|
|
4140
|
+
tier,
|
|
4141
|
+
free_writes_used: freeWritesUsed,
|
|
4142
|
+
free_writes_limit: freeWritesLimit,
|
|
4143
|
+
scope_address: scopeAddress,
|
|
4144
|
+
},
|
|
3945
4145
|
};
|
|
3946
4146
|
} catch (err: unknown) {
|
|
3947
4147
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4664,11 +4864,10 @@ const plugin = {
|
|
|
4664
4864
|
|
|
4665
4865
|
const response = await fetch(`${serverUrl}/v1/billing/checkout`, {
|
|
4666
4866
|
method: 'POST',
|
|
4667
|
-
headers: {
|
|
4867
|
+
headers: buildRelayHeaders({
|
|
4668
4868
|
'Authorization': `Bearer ${authKeyHex}`,
|
|
4669
4869
|
'Content-Type': 'application/json',
|
|
4670
|
-
|
|
4671
|
-
},
|
|
4870
|
+
}),
|
|
4672
4871
|
body: JSON.stringify({
|
|
4673
4872
|
wallet_address: walletAddr,
|
|
4674
4873
|
tier: 'pro',
|
|
@@ -4752,11 +4951,10 @@ const plugin = {
|
|
|
4752
4951
|
`${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(subgraphOwner)}`,
|
|
4753
4952
|
{
|
|
4754
4953
|
method: 'GET',
|
|
4755
|
-
headers: {
|
|
4954
|
+
headers: buildRelayHeaders({
|
|
4756
4955
|
'Authorization': `Bearer ${authKeyHex}`,
|
|
4757
4956
|
'Content-Type': 'application/json',
|
|
4758
|
-
|
|
4759
|
-
},
|
|
4957
|
+
}),
|
|
4760
4958
|
},
|
|
4761
4959
|
);
|
|
4762
4960
|
if (!billingResp.ok) {
|
|
@@ -4891,162 +5089,36 @@ const plugin = {
|
|
|
4891
5089
|
);
|
|
4892
5090
|
|
|
4893
5091
|
// ---------------------------------------------------------------
|
|
4894
|
-
//
|
|
5092
|
+
// Tools: totalreclaw_setup + totalreclaw_onboarding_start —
|
|
5093
|
+
// REMOVED in 3.3.1-rc.5 (phrase-safety carve-out closure).
|
|
4895
5094
|
// ---------------------------------------------------------------
|
|
4896
5095
|
//
|
|
4897
|
-
//
|
|
4898
|
-
//
|
|
4899
|
-
//
|
|
4900
|
-
//
|
|
4901
|
-
//
|
|
5096
|
+
// rc.4 left these two registrations in place as *neutered* stubs —
|
|
5097
|
+
// ``totalreclaw_setup`` rejected any ``recovery_phrase`` argument
|
|
5098
|
+
// and returned a CLI-pointer message; ``totalreclaw_onboarding_start``
|
|
5099
|
+
// was already pointer-only. Neither path could leak a phrase in
|
|
5100
|
+
// rc.4, but the rc.4 auto-QA (2026-04-22) flagged them as future-
|
|
5101
|
+
// regression surface: any future patch that re-enables phrase
|
|
5102
|
+
// acceptance (e.g. a flag-driven "power-user" path) would silently
|
|
5103
|
+
// re-open the leak, and their mere presence in the tool registry
|
|
5104
|
+
// keeps signalling to agents that "phrase handling happens here".
|
|
4902
5105
|
//
|
|
4903
|
-
//
|
|
4904
|
-
//
|
|
4905
|
-
//
|
|
4906
|
-
//
|
|
4907
|
-
//
|
|
5106
|
+
// Per ``project_phrase_safety_rule.md`` the ONLY approved agent-
|
|
5107
|
+
// facilitated setup surface is ``totalreclaw_pair`` (browser-side
|
|
5108
|
+
// crypto keeps the phrase out of the LLM round-trip by construction).
|
|
5109
|
+
// rc.5 deletes both registrations outright. The underlying CLI
|
|
5110
|
+
// wizard (``openclaw totalreclaw onboard``) is unchanged — users
|
|
5111
|
+
// run it in their own terminal, outside any agent shell.
|
|
4908
5112
|
//
|
|
4909
|
-
//
|
|
4910
|
-
//
|
|
4911
|
-
//
|
|
4912
|
-
// CLI wizard.
|
|
4913
|
-
api.registerTool(
|
|
4914
|
-
{
|
|
4915
|
-
name: 'totalreclaw_setup',
|
|
4916
|
-
label: 'TotalReclaw setup (deprecated — redirect to CLI)',
|
|
4917
|
-
description:
|
|
4918
|
-
'DEPRECATED in 3.2.0. This tool no longer accepts recovery phrases or performs ' +
|
|
4919
|
-
'setup. It returns a pointer to `openclaw totalreclaw onboard` — the secure CLI ' +
|
|
4920
|
-
'wizard that runs on the user\'s terminal so the phrase never touches the LLM ' +
|
|
4921
|
-
'provider. Prefer calling `totalreclaw_onboarding_start` for the same pointer.',
|
|
4922
|
-
parameters: {
|
|
4923
|
-
type: 'object',
|
|
4924
|
-
properties: {
|
|
4925
|
-
recovery_phrase: {
|
|
4926
|
-
type: 'string',
|
|
4927
|
-
description:
|
|
4928
|
-
'Legacy parameter — IGNORED in 3.2.0. If provided, the tool returns a ' +
|
|
4929
|
-
'security warning explaining that phrases must never be pasted through ' +
|
|
4930
|
-
'chat. Use the `openclaw totalreclaw onboard` CLI wizard to import an ' +
|
|
4931
|
-
'existing phrase safely.',
|
|
4932
|
-
},
|
|
4933
|
-
},
|
|
4934
|
-
additionalProperties: false,
|
|
4935
|
-
},
|
|
4936
|
-
async execute(_toolCallId: string, params: { recovery_phrase?: string }) {
|
|
4937
|
-
// Phrase-passing is a security boundary violation in 3.2.0. Reject
|
|
4938
|
-
// with a message that explains WHY — the LLM might try again with
|
|
4939
|
-
// a different shape otherwise.
|
|
4940
|
-
if (typeof params?.recovery_phrase === 'string' && params.recovery_phrase.trim().length > 0) {
|
|
4941
|
-
api.logger.warn(
|
|
4942
|
-
'totalreclaw_setup: rejected phrase-passing call (3.2.0 deprecation).',
|
|
4943
|
-
);
|
|
4944
|
-
return {
|
|
4945
|
-
content: [{
|
|
4946
|
-
type: 'text',
|
|
4947
|
-
text:
|
|
4948
|
-
'For security, TotalReclaw no longer accepts a recovery phrase through ' +
|
|
4949
|
-
'chat. Pasting a phrase into this tool would ship it to the LLM provider, ' +
|
|
4950
|
-
'which defeats the whole point of end-to-end encryption.\n\n' +
|
|
4951
|
-
'Ask the user to open a terminal on their machine and run:\n\n' +
|
|
4952
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
4953
|
-
'The wizard imports an existing phrase via a hidden stdin prompt that ' +
|
|
4954
|
-
'never touches the LLM, the transcript, or the network.',
|
|
4955
|
-
}],
|
|
4956
|
-
};
|
|
4957
|
-
}
|
|
4958
|
-
|
|
4959
|
-
// No-arg call against an already-active state: confirm + move on.
|
|
4960
|
-
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
4961
|
-
if (state.onboardingState === 'active') {
|
|
4962
|
-
return {
|
|
4963
|
-
content: [{
|
|
4964
|
-
type: 'text',
|
|
4965
|
-
text:
|
|
4966
|
-
'TotalReclaw is already set up and active on this machine. Memory tools ' +
|
|
4967
|
-
'are unblocked — you can call `totalreclaw_remember` and `totalreclaw_recall` ' +
|
|
4968
|
-
'directly. If the user wants to rotate phrases, have them delete ' +
|
|
4969
|
-
'`~/.totalreclaw/credentials.json` and run `openclaw totalreclaw onboard` again.',
|
|
4970
|
-
}],
|
|
4971
|
-
};
|
|
4972
|
-
}
|
|
4973
|
-
|
|
4974
|
-
// Fresh state, no phrase: redirect to the CLI wizard.
|
|
4975
|
-
return {
|
|
4976
|
-
content: [{
|
|
4977
|
-
type: 'text',
|
|
4978
|
-
text:
|
|
4979
|
-
'TotalReclaw setup must run on the user\'s local terminal so the recovery ' +
|
|
4980
|
-
'phrase never touches the LLM. Ask the user to open a terminal and run:\n\n' +
|
|
4981
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
4982
|
-
'The wizard walks through generate-new-phrase or import-existing-phrase. ' +
|
|
4983
|
-
'After it completes, memory tools become available automatically. See the ' +
|
|
4984
|
-
'`totalreclaw_onboarding_start` tool for the same pointer in a more ' +
|
|
4985
|
-
'discoverable shape.',
|
|
4986
|
-
}],
|
|
4987
|
-
};
|
|
4988
|
-
},
|
|
4989
|
-
},
|
|
4990
|
-
{ name: 'totalreclaw_setup' },
|
|
4991
|
-
);
|
|
4992
|
-
|
|
4993
|
-
// ---------------------------------------------------------------
|
|
4994
|
-
// Tool: totalreclaw_onboarding_start (3.2.0 pointer-only tool)
|
|
4995
|
-
// ---------------------------------------------------------------
|
|
5113
|
+
// Audit assertion: ``phrase-safety-registry.test.ts`` asserts
|
|
5114
|
+
// neither name is present in the ``api.registerTool`` call list.
|
|
5115
|
+
// Re-adding either fails CI.
|
|
4996
5116
|
//
|
|
4997
|
-
//
|
|
4998
|
-
//
|
|
4999
|
-
//
|
|
5000
|
-
//
|
|
5001
|
-
|
|
5002
|
-
{
|
|
5003
|
-
name: 'totalreclaw_onboarding_start',
|
|
5004
|
-
label: 'TotalReclaw — start onboarding',
|
|
5005
|
-
description:
|
|
5006
|
-
'Call this when the user wants to set up TotalReclaw memory or asks about ' +
|
|
5007
|
-
'enabling memory features. This tool does NOT generate, display, or accept ' +
|
|
5008
|
-
'a recovery phrase — it returns a short pointer that tells the user to run ' +
|
|
5009
|
-
'the onboarding wizard in their local terminal. All phrase handling happens ' +
|
|
5010
|
-
'outside the LLM. If TotalReclaw is already active, the tool returns a ' +
|
|
5011
|
-
'confirmation.',
|
|
5012
|
-
parameters: {
|
|
5013
|
-
type: 'object',
|
|
5014
|
-
properties: {},
|
|
5015
|
-
additionalProperties: false,
|
|
5016
|
-
},
|
|
5017
|
-
async execute() {
|
|
5018
|
-
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
5019
|
-
if (state.onboardingState === 'active') {
|
|
5020
|
-
return {
|
|
5021
|
-
content: [{
|
|
5022
|
-
type: 'text',
|
|
5023
|
-
text:
|
|
5024
|
-
'TotalReclaw is already set up on this machine. Your encryption keys are ' +
|
|
5025
|
-
'ready — `totalreclaw_remember`, `totalreclaw_recall`, and the other memory ' +
|
|
5026
|
-
'tools are unblocked. Run `openclaw totalreclaw status` in a terminal for ' +
|
|
5027
|
-
'more detail.',
|
|
5028
|
-
}],
|
|
5029
|
-
};
|
|
5030
|
-
}
|
|
5031
|
-
return {
|
|
5032
|
-
content: [{
|
|
5033
|
-
type: 'text',
|
|
5034
|
-
text:
|
|
5035
|
-
'TotalReclaw onboarding requires a local terminal so your recovery phrase ' +
|
|
5036
|
-
'never touches the LLM provider. On the same machine as your OpenClaw ' +
|
|
5037
|
-
'gateway, open a terminal and run:\n\n' +
|
|
5038
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
5039
|
-
'The wizard will ask whether you want to generate a new phrase or import an ' +
|
|
5040
|
-
'existing TotalReclaw phrase. Both paths display/accept the phrase only on ' +
|
|
5041
|
-
'your terminal — nothing crosses the network. After the wizard completes, ' +
|
|
5042
|
-
'come back here and I\'ll be able to use `totalreclaw_remember` and ' +
|
|
5043
|
-
'`totalreclaw_recall`.',
|
|
5044
|
-
}],
|
|
5045
|
-
};
|
|
5046
|
-
},
|
|
5047
|
-
},
|
|
5048
|
-
{ name: 'totalreclaw_onboarding_start' },
|
|
5049
|
-
);
|
|
5117
|
+
// Historical tombstone (so LLM-assisted contributors don't re-add
|
|
5118
|
+
// the former shape from training-data memory): rc.4 registered two
|
|
5119
|
+
// tools by the names "totalreclaw_setup" and
|
|
5120
|
+
// "totalreclaw_onboarding_start" as pointer-only stubs. Both were
|
|
5121
|
+
// deleted in rc.5. Do not re-introduce.
|
|
5050
5122
|
|
|
5051
5123
|
// ---------------------------------------------------------------
|
|
5052
5124
|
// Tool: totalreclaw_onboard — REMOVED in 3.3.1-rc.4 (phrase-safety).
|
|
@@ -5122,18 +5194,117 @@ const plugin = {
|
|
|
5122
5194
|
const rawMode = params?.mode;
|
|
5123
5195
|
const mode: 'generate' | 'import' =
|
|
5124
5196
|
rawMode === 'import' ? 'import' : 'generate';
|
|
5197
|
+
const pairMode = CONFIG.pairMode;
|
|
5125
5198
|
try {
|
|
5126
|
-
|
|
5127
|
-
|
|
5199
|
+
// 3.3.1-rc.11 — relay-brokered pair by default (universal reachability).
|
|
5200
|
+
// `TOTALRECLAW_PAIR_MODE=local` preserves the rc.4–rc.10 loopback flow
|
|
5201
|
+
// for air-gapped / self-hosted setups. Both paths return the same
|
|
5202
|
+
// tool payload (`{url, pin, expires_at_ms, qr_*, mode, instructions}`);
|
|
5203
|
+
// only the URL origin differs.
|
|
5204
|
+
let url: string;
|
|
5205
|
+
let pin: string;
|
|
5206
|
+
let sidOrToken: string;
|
|
5207
|
+
let expiresAtMs: number;
|
|
5208
|
+
let localSession: import('./pair-session-store.js').PairSession | undefined;
|
|
5209
|
+
|
|
5210
|
+
if (pairMode === 'relay') {
|
|
5211
|
+
const { openRemotePairSession, awaitPhraseUpload } = await import(
|
|
5212
|
+
'./pair-remote-client.js'
|
|
5213
|
+
);
|
|
5214
|
+
const remoteSession = await openRemotePairSession({
|
|
5215
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
5216
|
+
mode: mode === 'generate' ? 'generate' : 'import',
|
|
5217
|
+
});
|
|
5218
|
+
url = remoteSession.url;
|
|
5219
|
+
pin = remoteSession.pin;
|
|
5220
|
+
sidOrToken = remoteSession.token;
|
|
5221
|
+
// Relay sends ISO-8601; convert to ms for tool payload parity.
|
|
5222
|
+
const parsed = Date.parse(remoteSession.expiresAt);
|
|
5223
|
+
expiresAtMs = Number.isFinite(parsed)
|
|
5224
|
+
? parsed
|
|
5225
|
+
: Date.now() + 5 * 60_000;
|
|
5226
|
+
// Background task — writes credentials.json + flips state when
|
|
5227
|
+
// the browser completes the flow. Tool handler returns
|
|
5228
|
+
// immediately so the agent can tell the user the URL + PIN.
|
|
5229
|
+
void (async () => {
|
|
5230
|
+
try {
|
|
5231
|
+
await awaitPhraseUpload(remoteSession, {
|
|
5232
|
+
phraseValidator: (p: string) =>
|
|
5233
|
+
validateMnemonic(p, wordlist),
|
|
5234
|
+
completePairing: async ({ mnemonic }) => {
|
|
5235
|
+
try {
|
|
5236
|
+
// 3.3.1 (internal#130) — derive + persist the
|
|
5237
|
+
// Smart Account address now so the user can see
|
|
5238
|
+
// it immediately after pair, before any chain
|
|
5239
|
+
// write. Mnemonic stays in this scope (already
|
|
5240
|
+
// on disk in credentials.json); only the
|
|
5241
|
+
// derived public scope_address is added.
|
|
5242
|
+
let scopeAddress: string | undefined;
|
|
5243
|
+
try {
|
|
5244
|
+
scopeAddress = await deriveSmartAccountAddress(
|
|
5245
|
+
mnemonic,
|
|
5246
|
+
CONFIG.chainId,
|
|
5247
|
+
);
|
|
5248
|
+
} catch (deriveErr) {
|
|
5249
|
+
api.logger.warn(
|
|
5250
|
+
`totalreclaw_pair(relay): scope_address derivation failed (will retry lazily): ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`,
|
|
5251
|
+
);
|
|
5252
|
+
}
|
|
5253
|
+
const creds =
|
|
5254
|
+
loadCredentialsJson(CREDENTIALS_PATH) ?? {};
|
|
5255
|
+
const next: typeof creds = { ...creds, mnemonic };
|
|
5256
|
+
if (scopeAddress) next.scope_address = scopeAddress;
|
|
5257
|
+
if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
|
|
5258
|
+
return { state: 'error', error: 'credentials_write_failed' };
|
|
5259
|
+
}
|
|
5260
|
+
setRecoveryPhraseOverride(mnemonic);
|
|
5261
|
+
writeOnboardingState(CONFIG.onboardingStatePath, {
|
|
5262
|
+
onboardingState: 'active',
|
|
5263
|
+
createdBy: mode === 'generate' ? 'generate' : 'import',
|
|
5264
|
+
credentialsCreatedAt: new Date().toISOString(),
|
|
5265
|
+
version: pluginVersion ?? '3.3.0',
|
|
5266
|
+
});
|
|
5267
|
+
api.logger.info(
|
|
5268
|
+
`totalreclaw_pair(relay): session ${remoteSession.token.slice(0, 8)}… completed; credentials written${scopeAddress ? ` (scope_address=${scopeAddress})` : ''}`,
|
|
5269
|
+
);
|
|
5270
|
+
return { state: 'active' };
|
|
5271
|
+
} catch (err: unknown) {
|
|
5272
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5273
|
+
api.logger.error(
|
|
5274
|
+
`totalreclaw_pair(relay): completePairing failed: ${msg}`,
|
|
5275
|
+
);
|
|
5276
|
+
return { state: 'error', error: msg };
|
|
5277
|
+
}
|
|
5278
|
+
},
|
|
5279
|
+
});
|
|
5280
|
+
} catch (bgErr: unknown) {
|
|
5281
|
+
// Expected on TTL expiry / user-aborts — log at warn, not error.
|
|
5282
|
+
const bgMsg = bgErr instanceof Error ? bgErr.message : String(bgErr);
|
|
5283
|
+
api.logger.warn(
|
|
5284
|
+
`totalreclaw_pair(relay): background task ended for token=${remoteSession.token.slice(0, 8)}…: ${bgMsg}`,
|
|
5285
|
+
);
|
|
5286
|
+
}
|
|
5287
|
+
})();
|
|
5288
|
+
} else {
|
|
5289
|
+
// Local loopback path (rc.10 behaviour).
|
|
5290
|
+
const { createPairSession } = await import('./pair-session-store.js');
|
|
5291
|
+
const { generateGatewayKeypair } = await import('./pair-crypto.js');
|
|
5292
|
+
const kp = generateGatewayKeypair();
|
|
5293
|
+
const session = await createPairSession(CONFIG.pairSessionsPath, {
|
|
5294
|
+
mode,
|
|
5295
|
+
operatorContext: { channel: 'agent' },
|
|
5296
|
+
rngPrivateKey: () => Buffer.from(kp.skB64, 'base64url'),
|
|
5297
|
+
rngPublicKey: () => Buffer.from(kp.pkB64, 'base64url'),
|
|
5298
|
+
});
|
|
5299
|
+
url = buildPairingUrl(api, session);
|
|
5300
|
+
pin = session.secondaryCode;
|
|
5301
|
+
sidOrToken = session.sid;
|
|
5302
|
+
expiresAtMs = session.expiresAtMs;
|
|
5303
|
+
localSession = session;
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
// QR renderers — same for both modes; input is the URL string.
|
|
5128
5307
|
const { defaultRenderQr } = await import('./pair-cli.js');
|
|
5129
|
-
const kp = generateGatewayKeypair();
|
|
5130
|
-
const session = await createPairSession(CONFIG.pairSessionsPath, {
|
|
5131
|
-
mode,
|
|
5132
|
-
operatorContext: { channel: 'agent' },
|
|
5133
|
-
rngPrivateKey: () => Buffer.from(kp.skB64, 'base64url'),
|
|
5134
|
-
rngPublicKey: () => Buffer.from(kp.pkB64, 'base64url'),
|
|
5135
|
-
});
|
|
5136
|
-
const url = buildPairingUrl(api, session);
|
|
5137
5308
|
const qrAscii = await new Promise<string>((resolve) => {
|
|
5138
5309
|
let settled = false;
|
|
5139
5310
|
const t = setTimeout(() => {
|
|
@@ -5156,16 +5327,40 @@ const plugin = {
|
|
|
5156
5327
|
resolve('');
|
|
5157
5328
|
}
|
|
5158
5329
|
});
|
|
5330
|
+
|
|
5331
|
+
// 3.3.1-rc.5 — PNG + Unicode QR for multi-transport rendering.
|
|
5332
|
+
let qrPngB64 = '';
|
|
5333
|
+
let qrUnicode = '';
|
|
5334
|
+
try {
|
|
5335
|
+
const { encodePng, encodeUnicode } = await import('./pair-qr.js');
|
|
5336
|
+
const [pngBuf, uni] = await Promise.all([
|
|
5337
|
+
encodePng(url),
|
|
5338
|
+
encodeUnicode(url),
|
|
5339
|
+
]);
|
|
5340
|
+
qrPngB64 = pngBuf.toString('base64');
|
|
5341
|
+
qrUnicode = uni;
|
|
5342
|
+
} catch (qrErr: unknown) {
|
|
5343
|
+
api.logger.warn(
|
|
5344
|
+
`totalreclaw_pair: QR encode failed (non-fatal): ${
|
|
5345
|
+
qrErr instanceof Error ? qrErr.message : String(qrErr)
|
|
5346
|
+
}`,
|
|
5347
|
+
);
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5159
5350
|
api.logger.info(
|
|
5160
|
-
`totalreclaw_pair: session ${
|
|
5351
|
+
`totalreclaw_pair: session ${sidOrToken.slice(0, 8)}… mode=${mode} transport=${pairMode} url=${url} qr_png=${qrPngB64.length} qr_unicode=${qrUnicode.length}`,
|
|
5161
5352
|
);
|
|
5353
|
+
// Voidly reference localSession so TS does not flag the unused
|
|
5354
|
+
// local-branch binding. Future rc.12 diagnostics can expose
|
|
5355
|
+
// `session.mode` / `session.status` separately.
|
|
5356
|
+
void localSession;
|
|
5162
5357
|
return {
|
|
5163
5358
|
content: [{
|
|
5164
5359
|
type: 'text',
|
|
5165
5360
|
text:
|
|
5166
5361
|
`Pairing session started.\n\n` +
|
|
5167
5362
|
`URL: ${url}\n\n` +
|
|
5168
|
-
`PIN (type this into the browser): ${
|
|
5363
|
+
`PIN (type this into the browser): ${pin}\n\n` +
|
|
5169
5364
|
(qrAscii ? `QR code:\n\n${qrAscii}\n\n` : '') +
|
|
5170
5365
|
`Instructions for the user:\n` +
|
|
5171
5366
|
`1. Open the URL above on their phone or another browser (scan the QR or copy-paste).\n` +
|
|
@@ -5179,12 +5374,18 @@ const plugin = {
|
|
|
5179
5374
|
`This session expires in ~5 minutes. Run this tool again if you need a fresh URL.`,
|
|
5180
5375
|
}],
|
|
5181
5376
|
details: {
|
|
5182
|
-
sid:
|
|
5377
|
+
sid: sidOrToken,
|
|
5183
5378
|
url,
|
|
5184
|
-
pin
|
|
5379
|
+
pin,
|
|
5185
5380
|
mode,
|
|
5186
|
-
expires_at_ms:
|
|
5381
|
+
expires_at_ms: expiresAtMs,
|
|
5187
5382
|
qr_ascii: qrAscii,
|
|
5383
|
+
qr_png_b64: qrPngB64,
|
|
5384
|
+
qr_unicode: qrUnicode,
|
|
5385
|
+
// rc.11 — surface the transport so downstream tooling (QA
|
|
5386
|
+
// harness asserters, telemetry) can confirm which path
|
|
5387
|
+
// served the URL. Either 'relay' or 'local'.
|
|
5388
|
+
transport: pairMode,
|
|
5188
5389
|
},
|
|
5189
5390
|
};
|
|
5190
5391
|
} catch (err: unknown) {
|
|
@@ -5199,6 +5400,25 @@ const plugin = {
|
|
|
5199
5400
|
},
|
|
5200
5401
|
{ name: 'totalreclaw_pair' },
|
|
5201
5402
|
);
|
|
5403
|
+
// 3.3.1-rc.20 (issue #110): explicit post-registration breadcrumb so
|
|
5404
|
+
// ops/QA can grep gateway logs for definitive proof the tool was
|
|
5405
|
+
// declared. If the agent then reports the tool is missing from its
|
|
5406
|
+
// tool list, the gap is upstream OpenClaw tool propagation, not our
|
|
5407
|
+
// plugin — see issue #110 fix 3 + PR #102 (CLI fallback).
|
|
5408
|
+
//
|
|
5409
|
+
// 3.3.1-rc.21 (issue #128): the breadcrumb is debug-grade — it was
|
|
5410
|
+
// bleeding into `openclaw agent --json` stdout, breaking programmatic
|
|
5411
|
+
// parsers that expect the JSON-RPC body to be the only thing on the
|
|
5412
|
+
// wire. Gated behind `TOTALRECLAW_VERBOSE_REGISTER` (or the general
|
|
5413
|
+
// `TOTALRECLAW_DEBUG` toggle) so ops can opt back in when chasing
|
|
5414
|
+
// a tool-injection regression. Default OFF — clean stdout for users.
|
|
5415
|
+
if (CONFIG.verboseRegister) {
|
|
5416
|
+
api.logger.info(
|
|
5417
|
+
'TotalReclaw: registerTool(totalreclaw_pair) returned. If the agent does not see it in its tool list ' +
|
|
5418
|
+
'after gateway restart, the issue is upstream tool injection (containerized agents) — fall back to ' +
|
|
5419
|
+
'`openclaw totalreclaw pair generate --url-pin-only` (PR #102) or `openclaw totalreclaw onboard --pair-only`.',
|
|
5420
|
+
);
|
|
5421
|
+
}
|
|
5202
5422
|
|
|
5203
5423
|
// ---------------------------------------------------------------
|
|
5204
5424
|
// Tool: totalreclaw_report_qa_bug (3.3.1-rc.3 — RC-gated)
|
|
@@ -5295,10 +5515,16 @@ const plugin = {
|
|
|
5295
5515
|
details: { error: 'missing_github_token' },
|
|
5296
5516
|
};
|
|
5297
5517
|
}
|
|
5518
|
+
// rc.14 — `repo` is resolved inside `postQaBugIssue` via
|
|
5519
|
+
// `resolveQaRepo(...)`, which reads `TOTALRECLAW_QA_REPO` and
|
|
5520
|
+
// refuses any slug that isn't a `-internal` fork. Pass the
|
|
5521
|
+
// config-surfaced override so env reads stay in config.ts.
|
|
5522
|
+
const repoOverride = CONFIG.qaRepoOverride || undefined;
|
|
5298
5523
|
const result = await postQaBugIssue(
|
|
5299
5524
|
params as unknown as import('./qa-bug-report.js').QaBugArgs,
|
|
5300
5525
|
{
|
|
5301
5526
|
githubToken: token,
|
|
5527
|
+
repo: repoOverride,
|
|
5302
5528
|
logger: api.logger,
|
|
5303
5529
|
},
|
|
5304
5530
|
);
|
|
@@ -5324,9 +5550,15 @@ const plugin = {
|
|
|
5324
5550
|
},
|
|
5325
5551
|
{ name: 'totalreclaw_report_qa_bug' },
|
|
5326
5552
|
);
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5553
|
+
// 3.3.1-rc.21 (issue #128): demote the registration-confirmation
|
|
5554
|
+
// breadcrumb to verbose-only. Same `--json` stdout pollution risk
|
|
5555
|
+
// as the totalreclaw_pair breadcrumb above; ops can opt back in
|
|
5556
|
+
// via TOTALRECLAW_VERBOSE_REGISTER / TOTALRECLAW_DEBUG.
|
|
5557
|
+
if (CONFIG.verboseRegister) {
|
|
5558
|
+
api.logger.info(
|
|
5559
|
+
'totalreclaw_report_qa_bug registered (RC build — this tool is hidden in stable releases).',
|
|
5560
|
+
);
|
|
5561
|
+
}
|
|
5330
5562
|
}
|
|
5331
5563
|
|
|
5332
5564
|
// ---------------------------------------------------------------
|
|
@@ -5421,11 +5653,18 @@ const plugin = {
|
|
|
5421
5653
|
'## TotalReclaw setup pending\n\n' +
|
|
5422
5654
|
'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
|
|
5423
5655
|
'If the user asks about memory features or wants to configure TotalReclaw, ' +
|
|
5424
|
-
'call the `
|
|
5656
|
+
'call the `totalreclaw_pair` tool — it returns a pairing URL + 6-digit PIN ' +
|
|
5657
|
+
'the user opens in a browser (phrase is generated and encrypted in-browser, ' +
|
|
5658
|
+
'never leaves their device in plaintext). If `totalreclaw_pair` is not in ' +
|
|
5659
|
+
'your tool list (container-based agents sometimes miss it), shell out to ' +
|
|
5660
|
+
'`openclaw totalreclaw pair generate --url-pin-only` on the gateway host — ' +
|
|
5661
|
+
'it emits a single JSON line `{v,url,pin,expires_at_ms}` with no phrase ' +
|
|
5662
|
+
'material; hand the user the `url` and `pin`.\n\n' +
|
|
5425
5663
|
'**Do NOT** attempt to generate, display, or relay a recovery phrase in chat. ' +
|
|
5426
|
-
'
|
|
5427
|
-
'
|
|
5428
|
-
'
|
|
5664
|
+
'**Do NOT** run `openclaw totalreclaw onboard` — that CLI emits the recovery ' +
|
|
5665
|
+
'phrase on stdout and would leak it into the LLM transcript. Use `pair` ' +
|
|
5666
|
+
'(tool or `--url-pin-only` CLI) instead; `onboard` is reserved for users ' +
|
|
5667
|
+
'running it directly in their own local terminal.',
|
|
5429
5668
|
};
|
|
5430
5669
|
}
|
|
5431
5670
|
|
|
@@ -5455,7 +5694,7 @@ const plugin = {
|
|
|
5455
5694
|
const walletParam = encodeURIComponent(subgraphOwner || userId || '');
|
|
5456
5695
|
const billingResp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${walletParam}`, {
|
|
5457
5696
|
method: 'GET',
|
|
5458
|
-
headers: { 'Authorization': `Bearer ${authKeyHex}`, 'Accept': 'application/json'
|
|
5697
|
+
headers: buildRelayHeaders({ 'Authorization': `Bearer ${authKeyHex}`, 'Accept': 'application/json' }),
|
|
5459
5698
|
});
|
|
5460
5699
|
if (billingResp.ok) {
|
|
5461
5700
|
const billingData = await billingResp.json() as Record<string, unknown>;
|
|
@@ -5692,17 +5931,7 @@ const plugin = {
|
|
|
5692
5931
|
|
|
5693
5932
|
if (reranked.length === 0) return undefined;
|
|
5694
5933
|
|
|
5695
|
-
//
|
|
5696
|
-
// best match is below the minimum relevance threshold.
|
|
5697
|
-
const hookMaxCosine = Math.max(
|
|
5698
|
-
...reranked.map((r) => r.cosineSimilarity ?? 0),
|
|
5699
|
-
);
|
|
5700
|
-
if (hookMaxCosine < COSINE_THRESHOLD) {
|
|
5701
|
-
api.logger.info(
|
|
5702
|
-
`Hook: cosine threshold gate filtered results (max=${hookMaxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD})`,
|
|
5703
|
-
);
|
|
5704
|
-
return undefined;
|
|
5705
|
-
}
|
|
5934
|
+
// Relevance gate removed in rc.22 (see recall tool comment).
|
|
5706
5935
|
|
|
5707
5936
|
// 7. Build context string.
|
|
5708
5937
|
const lines = reranked.map((m, i) => {
|
|
@@ -5807,17 +6036,7 @@ const plugin = {
|
|
|
5807
6036
|
|
|
5808
6037
|
if (reranked.length === 0) return undefined;
|
|
5809
6038
|
|
|
5810
|
-
//
|
|
5811
|
-
// best match is below the minimum relevance threshold.
|
|
5812
|
-
const srvMaxCosine = Math.max(
|
|
5813
|
-
...reranked.map((r) => r.cosineSimilarity ?? 0),
|
|
5814
|
-
);
|
|
5815
|
-
if (srvMaxCosine < COSINE_THRESHOLD) {
|
|
5816
|
-
api.logger.info(
|
|
5817
|
-
`Hook: cosine threshold gate filtered results (max=${srvMaxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD})`,
|
|
5818
|
-
);
|
|
5819
|
-
return undefined;
|
|
5820
|
-
}
|
|
6039
|
+
// Relevance gate removed in rc.22 (see recall tool comment).
|
|
5821
6040
|
|
|
5822
6041
|
// 7. Build context string.
|
|
5823
6042
|
const lines = reranked.map((m, i) => {
|