@totalreclaw/totalreclaw 3.3.1-rc.2 → 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 +330 -0
- package/SKILL.md +50 -83
- package/api-client.ts +18 -11
- package/config.ts +117 -3
- package/crypto.ts +10 -2
- package/dist/api-client.js +226 -0
- package/dist/billing-cache.js +100 -0
- package/dist/claims-helper.js +606 -0
- package/dist/config.js +280 -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/embedding.js +86 -0
- package/dist/extractor.js +1225 -0
- package/dist/first-run.js +103 -0
- package/dist/fs-helpers.js +563 -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 +5348 -0
- package/dist/llm-client.js +686 -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 +542 -0
- package/dist/qa-bug-report.js +301 -0
- package/dist/relay-headers.js +44 -0
- package/dist/reranker.js +442 -0
- package/dist/retype-setscope.js +348 -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/embedding.ts +32 -9
- package/fs-helpers.ts +124 -0
- package/gateway-url.ts +57 -9
- package/index.ts +586 -357
- package/llm-client.ts +211 -23
- package/lsh.ts +7 -2
- package/onboarding-cli.ts +114 -1
- package/package.json +19 -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/qa-bug-report.ts +381 -0
- package/relay-headers.ts +50 -0
- package/reranker.ts +73 -0
- package/retype-setscope.ts +12 -0
- package/subgraph-search.ts +4 -3
- package/subgraph-store.ts +109 -16
package/index.ts
CHANGED
|
@@ -80,7 +80,7 @@ import {
|
|
|
80
80
|
dedupeByProvider,
|
|
81
81
|
} from './llm-profile-reader.js';
|
|
82
82
|
import { LSHHasher } from './lsh.js';
|
|
83
|
-
import { rerank, cosineSimilarity, detectQueryIntent, INTENT_WEIGHTS, type RerankerCandidate } from './reranker.js';
|
|
83
|
+
import { rerank, cosineSimilarity, detectQueryIntent, INTENT_WEIGHTS, passesRelevanceGate, type RerankerCandidate } from './reranker.js';
|
|
84
84
|
import { deduplicateBatch } from './semantic-dedup.js';
|
|
85
85
|
import {
|
|
86
86
|
findNearDuplicate,
|
|
@@ -135,6 +135,7 @@ import {
|
|
|
135
135
|
} from './onboarding-cli.js';
|
|
136
136
|
import { PluginHotCache, type HotFact } from './hot-cache-wrapper.js';
|
|
137
137
|
import { CONFIG, setRecoveryPhraseOverride } from './config.js';
|
|
138
|
+
import { buildRelayHeaders } from './relay-headers.js';
|
|
138
139
|
import {
|
|
139
140
|
readBillingCache,
|
|
140
141
|
writeBillingCache,
|
|
@@ -150,8 +151,11 @@ import {
|
|
|
150
151
|
deleteFileIfExists,
|
|
151
152
|
resolveOnboardingState,
|
|
152
153
|
writeOnboardingState,
|
|
154
|
+
readPluginVersion,
|
|
155
|
+
cleanupInstallStagingDirs,
|
|
153
156
|
type OnboardingState,
|
|
154
157
|
} from './fs-helpers.js';
|
|
158
|
+
import { isRcBuild } from './qa-bug-report.js';
|
|
155
159
|
import { decideToolGate, isGatedToolName } from './tool-gating.js';
|
|
156
160
|
import { detectFirstRun, buildWelcomePrepend, type GatewayMode } from './first-run.js';
|
|
157
161
|
import { buildPairRoutes } from './pair-http.js';
|
|
@@ -159,6 +163,19 @@ import { detectGatewayHost } from './gateway-url.js';
|
|
|
159
163
|
import { validateMnemonic } from '@scure/bip39';
|
|
160
164
|
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
161
165
|
import crypto from 'node:crypto';
|
|
166
|
+
import { createRequire } from 'node:module';
|
|
167
|
+
import { fileURLToPath } from 'node:url';
|
|
168
|
+
import * as nodePath from 'node:path';
|
|
169
|
+
|
|
170
|
+
// CJS-style require for the @totalreclaw/core WASM module. We keep this
|
|
171
|
+
// load path lazy (only inside getSmartImportWasm() below) so a partial
|
|
172
|
+
// install of the dependency tree doesn't crash module init. Bare
|
|
173
|
+
// `require()` is a CommonJS global and is undefined under bare Node ESM —
|
|
174
|
+
// the shipped `dist/index.js` declares `"type":"module"`, so calling the
|
|
175
|
+
// global directly emits "require is not defined" at runtime (issue #124).
|
|
176
|
+
// createRequire bridges the gap. Same shape as crypto.ts / lsh.ts /
|
|
177
|
+
// subgraph-store.ts / claims-helper.ts.
|
|
178
|
+
const __cjsRequire = createRequire(import.meta.url);
|
|
162
179
|
|
|
163
180
|
// ---------------------------------------------------------------------------
|
|
164
181
|
// OpenClaw Plugin API type (defined locally to avoid SDK dependency)
|
|
@@ -339,8 +356,17 @@ function buildPairingUrl(
|
|
|
339
356
|
// Layers 4 + 5 — auto-detect via gateway-url helper (Tailscale CGNAT, then LAN)
|
|
340
357
|
else {
|
|
341
358
|
let detected: ReturnType<typeof detectGatewayHost> = null;
|
|
359
|
+
// issue #110 fix 4 — pass `isDocker` so LAN detection skips
|
|
360
|
+
// 172.16/12 bridge IPs that no external browser can reach.
|
|
361
|
+
let isDocker = false;
|
|
342
362
|
try {
|
|
343
|
-
|
|
363
|
+
isDocker = isRunningInDocker();
|
|
364
|
+
} catch {
|
|
365
|
+
// Defensive: never block URL building on Docker sniff errors.
|
|
366
|
+
isDocker = false;
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
detected = detectGatewayHost({ isDocker });
|
|
344
370
|
} catch (err) {
|
|
345
371
|
api.logger.warn(
|
|
346
372
|
`TotalReclaw: host autodetect crashed: ${err instanceof Error ? err.message : String(err)} — falling back to localhost`,
|
|
@@ -366,9 +392,27 @@ function buildPairingUrl(
|
|
|
366
392
|
`Set plugins.entries.totalreclaw.config.publicUrl for remote access.`,
|
|
367
393
|
);
|
|
368
394
|
} else {
|
|
369
|
-
// Layer 6 — localhost fallback
|
|
395
|
+
// Layer 6 — localhost fallback (or Docker-aware relay-pointer warning)
|
|
370
396
|
const bind = cfg?.gateway?.bind;
|
|
371
|
-
if (
|
|
397
|
+
if (isDocker) {
|
|
398
|
+
// issue #110 fix 4: inside Docker the LAN IP is container-internal
|
|
399
|
+
// and useless. Loopback localhost only works for `docker exec`
|
|
400
|
+
// tests. The CORRECT pair URL for Docker is the relay-brokered
|
|
401
|
+
// path served by the `totalreclaw_pair` agent tool (CONFIG.pairMode
|
|
402
|
+
// === 'relay' since rc.11). The CLI-only path here cannot mint a
|
|
403
|
+
// relay session synchronously (the relay handshake needs a WS
|
|
404
|
+
// round-trip), so we emit the loopback URL with a LOUD warning
|
|
405
|
+
// pointing the operator at the agent tool / publicUrl override.
|
|
406
|
+
api.logger.warn(
|
|
407
|
+
`TotalReclaw: Docker container detected — pairing URL falling back to ` +
|
|
408
|
+
`http://localhost:${port}, which is unreachable from the host browser. ` +
|
|
409
|
+
`Use the totalreclaw_pair AGENT TOOL (relay-brokered, universally reachable) ` +
|
|
410
|
+
`instead of the CLI fallback, OR set plugins.entries.totalreclaw.config.publicUrl ` +
|
|
411
|
+
`to your gateway's host-reachable URL (e.g. http://<host-ip>:${port} when the ` +
|
|
412
|
+
`Docker port is published). Setting TOTALRECLAW_PAIR_MODE=relay is the default; ` +
|
|
413
|
+
`air-gapped operators on TOTALRECLAW_PAIR_MODE=local must publish a port + set publicUrl.`,
|
|
414
|
+
);
|
|
415
|
+
} else if (bind === 'lan' || bind === 'tailnet') {
|
|
372
416
|
api.logger.warn(
|
|
373
417
|
`TotalReclaw: pairing URL falling back to localhost because gateway.bind=${bind} could not be autodetected. ` +
|
|
374
418
|
'Set plugins.entries.totalreclaw.config.publicUrl to override.',
|
|
@@ -847,11 +891,10 @@ async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
|
|
|
847
891
|
const billingUrl = CONFIG.serverUrl;
|
|
848
892
|
const resp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
|
|
849
893
|
method: 'GET',
|
|
850
|
-
headers: {
|
|
894
|
+
headers: buildRelayHeaders({
|
|
851
895
|
'Authorization': `Bearer ${authKeyHex}`,
|
|
852
896
|
'Accept': 'application/json',
|
|
853
|
-
|
|
854
|
-
},
|
|
897
|
+
}),
|
|
855
898
|
});
|
|
856
899
|
if (resp.ok) {
|
|
857
900
|
const billingData = await resp.json() as Record<string, unknown>;
|
|
@@ -1424,11 +1467,11 @@ async function migrationGqlQuery<T>(
|
|
|
1424
1467
|
authKey?: string,
|
|
1425
1468
|
): Promise<T | null> {
|
|
1426
1469
|
try {
|
|
1427
|
-
const
|
|
1470
|
+
const overrides: Record<string, string> = {
|
|
1428
1471
|
'Content-Type': 'application/json',
|
|
1429
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
1430
1472
|
};
|
|
1431
|
-
if (authKey)
|
|
1473
|
+
if (authKey) overrides['Authorization'] = `Bearer ${authKey}`;
|
|
1474
|
+
const headers = buildRelayHeaders(overrides);
|
|
1432
1475
|
const response = await fetch(endpoint, {
|
|
1433
1476
|
method: 'POST',
|
|
1434
1477
|
headers,
|
|
@@ -2269,10 +2312,13 @@ async function handlePluginImportFrom(
|
|
|
2269
2312
|
// Smart Import — Two-Pass Pipeline (Profile + Triage)
|
|
2270
2313
|
// ---------------------------------------------------------------------------
|
|
2271
2314
|
|
|
2272
|
-
// Lazy-load WASM for smart import functions (same pattern as crypto.ts /
|
|
2315
|
+
// Lazy-load WASM for smart import functions (same pattern as crypto.ts /
|
|
2316
|
+
// subgraph-store.ts). Goes through __cjsRequire (createRequire(import.meta.url))
|
|
2317
|
+
// declared at the top of the file — bare `require()` is undefined under
|
|
2318
|
+
// pure-ESM Node, see issue #124.
|
|
2273
2319
|
let _smartImportWasm: typeof import('@totalreclaw/core') | null = null;
|
|
2274
2320
|
function getSmartImportWasm() {
|
|
2275
|
-
if (!_smartImportWasm) _smartImportWasm =
|
|
2321
|
+
if (!_smartImportWasm) _smartImportWasm = __cjsRequire('@totalreclaw/core');
|
|
2276
2322
|
return _smartImportWasm;
|
|
2277
2323
|
}
|
|
2278
2324
|
|
|
@@ -2794,6 +2840,58 @@ const plugin = {
|
|
|
2794
2840
|
},
|
|
2795
2841
|
|
|
2796
2842
|
register(api: OpenClawPluginApi) {
|
|
2843
|
+
// ---------------------------------------------------------------
|
|
2844
|
+
// RC-build detection (3.3.1-rc.3)
|
|
2845
|
+
// ---------------------------------------------------------------
|
|
2846
|
+
//
|
|
2847
|
+
// `isRcBuild` reads the plugin's own version string. When true, the
|
|
2848
|
+
// `totalreclaw_report_qa_bug` tool is registered at the end of this
|
|
2849
|
+
// function — stable builds never see it. The version is resolved via
|
|
2850
|
+
// `readPluginVersion` from fs-helpers.ts (scanner-safe, pure-fs).
|
|
2851
|
+
let rcMode = false;
|
|
2852
|
+
// Plugin version resolved from package.json once at register time. Reused
|
|
2853
|
+
// by writeOnboardingState callsites below so the `version` field in
|
|
2854
|
+
// state.json tracks the actual shipped plugin version (avoids drift —
|
|
2855
|
+
// e.g. rc.18 finding F4 where a hardcoded `'3.3.1-rc.11'` stayed put
|
|
2856
|
+
// through 7 RC bumps). Fallback `'3.3.0'` matches the prior literal at
|
|
2857
|
+
// the loopback callsite if package.json read fails.
|
|
2858
|
+
let pluginVersion: string | null = null;
|
|
2859
|
+
try {
|
|
2860
|
+
// Resolve our own dist/ directory so `readPluginVersion` can locate
|
|
2861
|
+
// package.json. We use `import.meta.url` + ESM-static stdlib imports
|
|
2862
|
+
// (`fileURLToPath` from `node:url`, `nodePath.dirname` from `node:path`,
|
|
2863
|
+
// both imported at the top of this file). Earlier shape used inline
|
|
2864
|
+
// `require('node:url')` — undefined under bare-ESM Node, broke the
|
|
2865
|
+
// before_agent_start hook in the published rc.20 bundle (issue #124).
|
|
2866
|
+
const pluginDir = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
2867
|
+
pluginVersion = readPluginVersion(pluginDir);
|
|
2868
|
+
rcMode = isRcBuild(pluginVersion);
|
|
2869
|
+
if (rcMode) {
|
|
2870
|
+
api.logger.info(`TotalReclaw: RC build detected (version=${pluginVersion}). RC-gated tools will be registered.`);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// 3.3.1-rc.21 (issue #126 — rc.20 finding F3): clean up
|
|
2874
|
+
// `.openclaw-install-stage-*` siblings left behind by an interrupted
|
|
2875
|
+
// `openclaw plugins install` run. Without cleanup, OpenClaw's plugin
|
|
2876
|
+
// loader auto-discovers the orphan directory on the next gateway
|
|
2877
|
+
// start and registers a duplicate `totalreclaw` plugin (duplicate
|
|
2878
|
+
// hooks, duplicate tools, "duplicate-plugin-id" warning every cycle).
|
|
2879
|
+
// Best-effort — never throws on permission / race failures.
|
|
2880
|
+
try {
|
|
2881
|
+
const removed = cleanupInstallStagingDirs(pluginDir);
|
|
2882
|
+
if (removed.length > 0) {
|
|
2883
|
+
api.logger.info(
|
|
2884
|
+
`TotalReclaw: removed ${removed.length} stale install-staging dir(s) from prior interrupted install: ${removed.join(', ')}`,
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
} catch {
|
|
2888
|
+
// Best-effort — already swallowed inside the helper, but keep this
|
|
2889
|
+
// outer try as belt-and-braces against future helper changes.
|
|
2890
|
+
}
|
|
2891
|
+
} catch {
|
|
2892
|
+
rcMode = false;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2797
2895
|
// ---------------------------------------------------------------
|
|
2798
2896
|
// LLM client initialization (auto-detect provider from OpenClaw config)
|
|
2799
2897
|
// ---------------------------------------------------------------
|
|
@@ -2875,6 +2973,12 @@ const plugin = {
|
|
|
2875
2973
|
credentialsPath: CREDENTIALS_PATH,
|
|
2876
2974
|
statePath: CONFIG.onboardingStatePath,
|
|
2877
2975
|
logger: api.logger,
|
|
2976
|
+
// 3.3.1-rc.18 — wire the pair flow into onboard so the
|
|
2977
|
+
// `--pair-only` flag (issue #95) can delegate to it without
|
|
2978
|
+
// duplicating session-store / URL-builder logic. Same deps
|
|
2979
|
+
// as the standalone `pair` subcommand.
|
|
2980
|
+
pairSessionsPath: CONFIG.pairSessionsPath,
|
|
2981
|
+
renderPairingUrl: (session) => buildPairingUrl(api, session),
|
|
2878
2982
|
// 3.3.1 — supplied to the non-interactive --json onboard path
|
|
2879
2983
|
// so the emitted payload includes the derived Smart Account
|
|
2880
2984
|
// (scope) address. Uses the chain-id default; Pro-tier
|
|
@@ -2947,8 +3051,30 @@ const plugin = {
|
|
|
2947
3051
|
// Write credentials.json + flip state to 'active' via
|
|
2948
3052
|
// fs-helpers. This centralizes disk I/O off the
|
|
2949
3053
|
// pair-http surface (scanner isolation).
|
|
3054
|
+
//
|
|
3055
|
+
// 3.3.1 (internal#130) — derive + persist the Smart Account
|
|
3056
|
+
// address right here so the user can see their scope address
|
|
3057
|
+
// immediately after pair, without waiting for a first chain
|
|
3058
|
+
// write. SA derivation runs locally (WASM deriveEoa + factory
|
|
3059
|
+
// getAddress eth_call); the mnemonic NEVER crosses any new
|
|
3060
|
+
// boundary — it's already on disk in credentials.json and is
|
|
3061
|
+
// consumed by the same `deriveSmartAccountAddress` call the
|
|
3062
|
+
// store/search paths use. Only the derived public address is
|
|
3063
|
+
// persisted to credentials.json (`scope_address`).
|
|
3064
|
+
let scopeAddress: string | undefined;
|
|
3065
|
+
try {
|
|
3066
|
+
scopeAddress = await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
|
|
3067
|
+
} catch (err) {
|
|
3068
|
+
// Best-effort. If chain RPC is unreachable at pair time, the
|
|
3069
|
+
// status tool re-tries derivation lazily on next call —
|
|
3070
|
+
// fall through and write credentials.json without it.
|
|
3071
|
+
api.logger.warn(
|
|
3072
|
+
`pair: scope_address derivation failed (will retry lazily): ${err instanceof Error ? err.message : String(err)}`,
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
2950
3075
|
const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
|
|
2951
|
-
const next = { ...creds, mnemonic };
|
|
3076
|
+
const next: typeof creds = { ...creds, mnemonic };
|
|
3077
|
+
if (scopeAddress) next.scope_address = scopeAddress;
|
|
2952
3078
|
if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
|
|
2953
3079
|
return { state: 'error', error: 'credentials_write_failed' };
|
|
2954
3080
|
}
|
|
@@ -2961,7 +3087,7 @@ const plugin = {
|
|
|
2961
3087
|
onboardingState: 'active',
|
|
2962
3088
|
createdBy: 'generate',
|
|
2963
3089
|
credentialsCreatedAt: new Date().toISOString(),
|
|
2964
|
-
version: '3.3.0',
|
|
3090
|
+
version: pluginVersion ?? '3.3.0',
|
|
2965
3091
|
});
|
|
2966
3092
|
return { state: 'active' };
|
|
2967
3093
|
},
|
|
@@ -3496,14 +3622,19 @@ const plugin = {
|
|
|
3496
3622
|
};
|
|
3497
3623
|
}
|
|
3498
3624
|
|
|
3499
|
-
// 6b.
|
|
3500
|
-
//
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3625
|
+
// 6b. Relevance gate — surface results when EITHER the top match
|
|
3626
|
+
// clears the cosine threshold OR every meaningful query token
|
|
3627
|
+
// appears in the top result's text (lexical override).
|
|
3628
|
+
// Issue #116 (rc.18 finding F1): short queries like
|
|
3629
|
+
// "favorite color" produce embeddings with low cosine sim
|
|
3630
|
+
// against the local Harrier-OSS-270m model even when the
|
|
3631
|
+
// stored fact text contains every query token.
|
|
3632
|
+
if (!passesRelevanceGate(params.query, reranked, COSINE_THRESHOLD)) {
|
|
3633
|
+
const maxCosine = Math.max(
|
|
3634
|
+
...reranked.map((r) => r.cosineSimilarity ?? 0),
|
|
3635
|
+
);
|
|
3505
3636
|
api.logger.info(
|
|
3506
|
-
`Recall:
|
|
3637
|
+
`Recall: relevance gate filtered results (max cosine=${maxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD}, no lexical override)`,
|
|
3507
3638
|
);
|
|
3508
3639
|
return {
|
|
3509
3640
|
content: [{ type: 'text', text: 'No relevant memories found for this query.' }],
|
|
@@ -3731,11 +3862,10 @@ const plugin = {
|
|
|
3731
3862
|
|
|
3732
3863
|
const res = await fetch(`${relayUrl}/v1/subgraph`, {
|
|
3733
3864
|
method: 'POST',
|
|
3734
|
-
headers: {
|
|
3865
|
+
headers: buildRelayHeaders({
|
|
3735
3866
|
'Content-Type': 'application/json',
|
|
3736
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
3737
3867
|
...(authKeyHex ? { Authorization: `Bearer ${authKeyHex}` } : {}),
|
|
3738
|
-
},
|
|
3868
|
+
}),
|
|
3739
3869
|
body: JSON.stringify({ query, variables }),
|
|
3740
3870
|
});
|
|
3741
3871
|
|
|
@@ -3871,11 +4001,10 @@ const plugin = {
|
|
|
3871
4001
|
const walletAddr = subgraphOwner || userId || '';
|
|
3872
4002
|
const response = await fetch(`${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
|
|
3873
4003
|
method: 'GET',
|
|
3874
|
-
headers: {
|
|
4004
|
+
headers: buildRelayHeaders({
|
|
3875
4005
|
'Authorization': `Bearer ${authKeyHex}`,
|
|
3876
4006
|
'Accept': 'application/json',
|
|
3877
|
-
|
|
3878
|
-
},
|
|
4007
|
+
}),
|
|
3879
4008
|
});
|
|
3880
4009
|
|
|
3881
4010
|
if (!response.ok) {
|
|
@@ -3900,6 +4029,37 @@ const plugin = {
|
|
|
3900
4029
|
checked_at: Date.now(),
|
|
3901
4030
|
});
|
|
3902
4031
|
|
|
4032
|
+
// 3.3.1 (internal#130) — surface the Smart Account / scope
|
|
4033
|
+
// address so the user can do subgraph queries, BaseScan
|
|
4034
|
+
// lookups, and cross-client portability checks BEFORE any
|
|
4035
|
+
// chain write completes. Resolution priority:
|
|
4036
|
+
// 1. In-memory `subgraphOwner` (already derived earlier).
|
|
4037
|
+
// 2. credentials.json `scope_address` (persisted at pair).
|
|
4038
|
+
// 3. Lazy derive now from the loaded mnemonic + cache it
|
|
4039
|
+
// back to credentials.json so the next call is free.
|
|
4040
|
+
let scopeAddress: string | undefined =
|
|
4041
|
+
subgraphOwner ?? undefined;
|
|
4042
|
+
if (!scopeAddress) {
|
|
4043
|
+
try {
|
|
4044
|
+
const credsCache = loadCredentialsJson(CREDENTIALS_PATH);
|
|
4045
|
+
if (credsCache?.scope_address && typeof credsCache.scope_address === 'string') {
|
|
4046
|
+
scopeAddress = credsCache.scope_address;
|
|
4047
|
+
} else if (credsCache?.mnemonic && typeof credsCache.mnemonic === 'string') {
|
|
4048
|
+
scopeAddress = await deriveSmartAccountAddress(
|
|
4049
|
+
credsCache.mnemonic,
|
|
4050
|
+
CONFIG.chainId,
|
|
4051
|
+
);
|
|
4052
|
+
if (scopeAddress) {
|
|
4053
|
+
writeCredentialsJson(CREDENTIALS_PATH, { ...credsCache, scope_address: scopeAddress });
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
} catch (deriveErr) {
|
|
4057
|
+
api.logger.warn(
|
|
4058
|
+
`totalreclaw_status: scope_address lookup failed: ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`,
|
|
4059
|
+
);
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
|
|
3903
4063
|
const tierLabel = tier === 'pro' ? 'Pro' : 'Free';
|
|
3904
4064
|
const lines: string[] = [
|
|
3905
4065
|
`Tier: ${tierLabel}`,
|
|
@@ -3908,13 +4068,21 @@ const plugin = {
|
|
|
3908
4068
|
if (freeWritesResetAt) {
|
|
3909
4069
|
lines.push(`Resets: ${new Date(freeWritesResetAt).toLocaleDateString()}`);
|
|
3910
4070
|
}
|
|
4071
|
+
if (scopeAddress) {
|
|
4072
|
+
lines.push(`Smart Account: ${scopeAddress}`);
|
|
4073
|
+
}
|
|
3911
4074
|
if (tier !== 'pro') {
|
|
3912
4075
|
lines.push(`Pricing: https://totalreclaw.xyz/pricing`);
|
|
3913
4076
|
}
|
|
3914
4077
|
|
|
3915
4078
|
return {
|
|
3916
4079
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
3917
|
-
details: {
|
|
4080
|
+
details: {
|
|
4081
|
+
tier,
|
|
4082
|
+
free_writes_used: freeWritesUsed,
|
|
4083
|
+
free_writes_limit: freeWritesLimit,
|
|
4084
|
+
scope_address: scopeAddress,
|
|
4085
|
+
},
|
|
3918
4086
|
};
|
|
3919
4087
|
} catch (err: unknown) {
|
|
3920
4088
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4637,11 +4805,10 @@ const plugin = {
|
|
|
4637
4805
|
|
|
4638
4806
|
const response = await fetch(`${serverUrl}/v1/billing/checkout`, {
|
|
4639
4807
|
method: 'POST',
|
|
4640
|
-
headers: {
|
|
4808
|
+
headers: buildRelayHeaders({
|
|
4641
4809
|
'Authorization': `Bearer ${authKeyHex}`,
|
|
4642
4810
|
'Content-Type': 'application/json',
|
|
4643
|
-
|
|
4644
|
-
},
|
|
4811
|
+
}),
|
|
4645
4812
|
body: JSON.stringify({
|
|
4646
4813
|
wallet_address: walletAddr,
|
|
4647
4814
|
tier: 'pro',
|
|
@@ -4725,11 +4892,10 @@ const plugin = {
|
|
|
4725
4892
|
`${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(subgraphOwner)}`,
|
|
4726
4893
|
{
|
|
4727
4894
|
method: 'GET',
|
|
4728
|
-
headers: {
|
|
4895
|
+
headers: buildRelayHeaders({
|
|
4729
4896
|
'Authorization': `Bearer ${authKeyHex}`,
|
|
4730
4897
|
'Content-Type': 'application/json',
|
|
4731
|
-
|
|
4732
|
-
},
|
|
4898
|
+
}),
|
|
4733
4899
|
},
|
|
4734
4900
|
);
|
|
4735
4901
|
if (!billingResp.ok) {
|
|
@@ -4864,301 +5030,66 @@ const plugin = {
|
|
|
4864
5030
|
);
|
|
4865
5031
|
|
|
4866
5032
|
// ---------------------------------------------------------------
|
|
4867
|
-
//
|
|
5033
|
+
// Tools: totalreclaw_setup + totalreclaw_onboarding_start —
|
|
5034
|
+
// REMOVED in 3.3.1-rc.5 (phrase-safety carve-out closure).
|
|
4868
5035
|
// ---------------------------------------------------------------
|
|
4869
5036
|
//
|
|
4870
|
-
//
|
|
4871
|
-
//
|
|
4872
|
-
//
|
|
4873
|
-
//
|
|
4874
|
-
//
|
|
5037
|
+
// rc.4 left these two registrations in place as *neutered* stubs —
|
|
5038
|
+
// ``totalreclaw_setup`` rejected any ``recovery_phrase`` argument
|
|
5039
|
+
// and returned a CLI-pointer message; ``totalreclaw_onboarding_start``
|
|
5040
|
+
// was already pointer-only. Neither path could leak a phrase in
|
|
5041
|
+
// rc.4, but the rc.4 auto-QA (2026-04-22) flagged them as future-
|
|
5042
|
+
// regression surface: any future patch that re-enables phrase
|
|
5043
|
+
// acceptance (e.g. a flag-driven "power-user" path) would silently
|
|
5044
|
+
// re-open the leak, and their mere presence in the tool registry
|
|
5045
|
+
// keeps signalling to agents that "phrase handling happens here".
|
|
4875
5046
|
//
|
|
4876
|
-
//
|
|
4877
|
-
//
|
|
4878
|
-
//
|
|
4879
|
-
//
|
|
4880
|
-
//
|
|
5047
|
+
// Per ``project_phrase_safety_rule.md`` the ONLY approved agent-
|
|
5048
|
+
// facilitated setup surface is ``totalreclaw_pair`` (browser-side
|
|
5049
|
+
// crypto keeps the phrase out of the LLM round-trip by construction).
|
|
5050
|
+
// rc.5 deletes both registrations outright. The underlying CLI
|
|
5051
|
+
// wizard (``openclaw totalreclaw onboard``) is unchanged — users
|
|
5052
|
+
// run it in their own terminal, outside any agent shell.
|
|
4881
5053
|
//
|
|
4882
|
-
//
|
|
4883
|
-
//
|
|
4884
|
-
//
|
|
4885
|
-
// CLI wizard.
|
|
4886
|
-
api.registerTool(
|
|
4887
|
-
{
|
|
4888
|
-
name: 'totalreclaw_setup',
|
|
4889
|
-
label: 'TotalReclaw setup (deprecated — redirect to CLI)',
|
|
4890
|
-
description:
|
|
4891
|
-
'DEPRECATED in 3.2.0. This tool no longer accepts recovery phrases or performs ' +
|
|
4892
|
-
'setup. It returns a pointer to `openclaw totalreclaw onboard` — the secure CLI ' +
|
|
4893
|
-
'wizard that runs on the user\'s terminal so the phrase never touches the LLM ' +
|
|
4894
|
-
'provider. Prefer calling `totalreclaw_onboarding_start` for the same pointer.',
|
|
4895
|
-
parameters: {
|
|
4896
|
-
type: 'object',
|
|
4897
|
-
properties: {
|
|
4898
|
-
recovery_phrase: {
|
|
4899
|
-
type: 'string',
|
|
4900
|
-
description:
|
|
4901
|
-
'Legacy parameter — IGNORED in 3.2.0. If provided, the tool returns a ' +
|
|
4902
|
-
'security warning explaining that phrases must never be pasted through ' +
|
|
4903
|
-
'chat. Use the `openclaw totalreclaw onboard` CLI wizard to import an ' +
|
|
4904
|
-
'existing phrase safely.',
|
|
4905
|
-
},
|
|
4906
|
-
},
|
|
4907
|
-
additionalProperties: false,
|
|
4908
|
-
},
|
|
4909
|
-
async execute(_toolCallId: string, params: { recovery_phrase?: string }) {
|
|
4910
|
-
// Phrase-passing is a security boundary violation in 3.2.0. Reject
|
|
4911
|
-
// with a message that explains WHY — the LLM might try again with
|
|
4912
|
-
// a different shape otherwise.
|
|
4913
|
-
if (typeof params?.recovery_phrase === 'string' && params.recovery_phrase.trim().length > 0) {
|
|
4914
|
-
api.logger.warn(
|
|
4915
|
-
'totalreclaw_setup: rejected phrase-passing call (3.2.0 deprecation).',
|
|
4916
|
-
);
|
|
4917
|
-
return {
|
|
4918
|
-
content: [{
|
|
4919
|
-
type: 'text',
|
|
4920
|
-
text:
|
|
4921
|
-
'For security, TotalReclaw no longer accepts a recovery phrase through ' +
|
|
4922
|
-
'chat. Pasting a phrase into this tool would ship it to the LLM provider, ' +
|
|
4923
|
-
'which defeats the whole point of end-to-end encryption.\n\n' +
|
|
4924
|
-
'Ask the user to open a terminal on their machine and run:\n\n' +
|
|
4925
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
4926
|
-
'The wizard imports an existing phrase via a hidden stdin prompt that ' +
|
|
4927
|
-
'never touches the LLM, the transcript, or the network.',
|
|
4928
|
-
}],
|
|
4929
|
-
};
|
|
4930
|
-
}
|
|
4931
|
-
|
|
4932
|
-
// No-arg call against an already-active state: confirm + move on.
|
|
4933
|
-
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
4934
|
-
if (state.onboardingState === 'active') {
|
|
4935
|
-
return {
|
|
4936
|
-
content: [{
|
|
4937
|
-
type: 'text',
|
|
4938
|
-
text:
|
|
4939
|
-
'TotalReclaw is already set up and active on this machine. Memory tools ' +
|
|
4940
|
-
'are unblocked — you can call `totalreclaw_remember` and `totalreclaw_recall` ' +
|
|
4941
|
-
'directly. If the user wants to rotate phrases, have them delete ' +
|
|
4942
|
-
'`~/.totalreclaw/credentials.json` and run `openclaw totalreclaw onboard` again.',
|
|
4943
|
-
}],
|
|
4944
|
-
};
|
|
4945
|
-
}
|
|
4946
|
-
|
|
4947
|
-
// Fresh state, no phrase: redirect to the CLI wizard.
|
|
4948
|
-
return {
|
|
4949
|
-
content: [{
|
|
4950
|
-
type: 'text',
|
|
4951
|
-
text:
|
|
4952
|
-
'TotalReclaw setup must run on the user\'s local terminal so the recovery ' +
|
|
4953
|
-
'phrase never touches the LLM. Ask the user to open a terminal and run:\n\n' +
|
|
4954
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
4955
|
-
'The wizard walks through generate-new-phrase or import-existing-phrase. ' +
|
|
4956
|
-
'After it completes, memory tools become available automatically. See the ' +
|
|
4957
|
-
'`totalreclaw_onboarding_start` tool for the same pointer in a more ' +
|
|
4958
|
-
'discoverable shape.',
|
|
4959
|
-
}],
|
|
4960
|
-
};
|
|
4961
|
-
},
|
|
4962
|
-
},
|
|
4963
|
-
{ name: 'totalreclaw_setup' },
|
|
4964
|
-
);
|
|
4965
|
-
|
|
4966
|
-
// ---------------------------------------------------------------
|
|
4967
|
-
// Tool: totalreclaw_onboarding_start (3.2.0 pointer-only tool)
|
|
4968
|
-
// ---------------------------------------------------------------
|
|
5054
|
+
// Audit assertion: ``phrase-safety-registry.test.ts`` asserts
|
|
5055
|
+
// neither name is present in the ``api.registerTool`` call list.
|
|
5056
|
+
// Re-adding either fails CI.
|
|
4969
5057
|
//
|
|
4970
|
-
//
|
|
4971
|
-
//
|
|
4972
|
-
//
|
|
4973
|
-
//
|
|
4974
|
-
|
|
4975
|
-
{
|
|
4976
|
-
name: 'totalreclaw_onboarding_start',
|
|
4977
|
-
label: 'TotalReclaw — start onboarding',
|
|
4978
|
-
description:
|
|
4979
|
-
'Call this when the user wants to set up TotalReclaw memory or asks about ' +
|
|
4980
|
-
'enabling memory features. This tool does NOT generate, display, or accept ' +
|
|
4981
|
-
'a recovery phrase — it returns a short pointer that tells the user to run ' +
|
|
4982
|
-
'the onboarding wizard in their local terminal. All phrase handling happens ' +
|
|
4983
|
-
'outside the LLM. If TotalReclaw is already active, the tool returns a ' +
|
|
4984
|
-
'confirmation.',
|
|
4985
|
-
parameters: {
|
|
4986
|
-
type: 'object',
|
|
4987
|
-
properties: {},
|
|
4988
|
-
additionalProperties: false,
|
|
4989
|
-
},
|
|
4990
|
-
async execute() {
|
|
4991
|
-
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
4992
|
-
if (state.onboardingState === 'active') {
|
|
4993
|
-
return {
|
|
4994
|
-
content: [{
|
|
4995
|
-
type: 'text',
|
|
4996
|
-
text:
|
|
4997
|
-
'TotalReclaw is already set up on this machine. Your encryption keys are ' +
|
|
4998
|
-
'ready — `totalreclaw_remember`, `totalreclaw_recall`, and the other memory ' +
|
|
4999
|
-
'tools are unblocked. Run `openclaw totalreclaw status` in a terminal for ' +
|
|
5000
|
-
'more detail.',
|
|
5001
|
-
}],
|
|
5002
|
-
};
|
|
5003
|
-
}
|
|
5004
|
-
return {
|
|
5005
|
-
content: [{
|
|
5006
|
-
type: 'text',
|
|
5007
|
-
text:
|
|
5008
|
-
'TotalReclaw onboarding requires a local terminal so your recovery phrase ' +
|
|
5009
|
-
'never touches the LLM provider. On the same machine as your OpenClaw ' +
|
|
5010
|
-
'gateway, open a terminal and run:\n\n' +
|
|
5011
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
5012
|
-
'The wizard will ask whether you want to generate a new phrase or import an ' +
|
|
5013
|
-
'existing TotalReclaw phrase. Both paths display/accept the phrase only on ' +
|
|
5014
|
-
'your terminal — nothing crosses the network. After the wizard completes, ' +
|
|
5015
|
-
'come back here and I\'ll be able to use `totalreclaw_remember` and ' +
|
|
5016
|
-
'`totalreclaw_recall`.',
|
|
5017
|
-
}],
|
|
5018
|
-
};
|
|
5019
|
-
},
|
|
5020
|
-
},
|
|
5021
|
-
{ name: 'totalreclaw_onboarding_start' },
|
|
5022
|
-
);
|
|
5058
|
+
// Historical tombstone (so LLM-assisted contributors don't re-add
|
|
5059
|
+
// the former shape from training-data memory): rc.4 registered two
|
|
5060
|
+
// tools by the names "totalreclaw_setup" and
|
|
5061
|
+
// "totalreclaw_onboarding_start" as pointer-only stubs. Both were
|
|
5062
|
+
// deleted in rc.5. Do not re-introduce.
|
|
5023
5063
|
|
|
5024
5064
|
// ---------------------------------------------------------------
|
|
5025
|
-
// Tool: totalreclaw_onboard
|
|
5065
|
+
// Tool: totalreclaw_onboard — REMOVED in 3.3.1-rc.4 (phrase-safety).
|
|
5066
|
+
//
|
|
5067
|
+
// rc.3 shipped a `totalreclaw_onboard` agent tool that generated a
|
|
5068
|
+
// fresh BIP-39 mnemonic in-process, wrote it to credentials.json,
|
|
5069
|
+
// and returned `{scope_address, credentials_path}` to the agent.
|
|
5070
|
+
// `emitPhrase: false` kept the mnemonic OUT of the tool's return
|
|
5071
|
+
// payload, but NOTHING ARCHITECTURALLY PREVENTED leakage — a future
|
|
5072
|
+
// patch could regress the flag, a different code path could echo
|
|
5073
|
+
// the mnemonic in a log/error message the agent captures, or the
|
|
5074
|
+
// mere existence of the tool implied to agents that "generating a
|
|
5075
|
+
// phrase here is fine" (it isn't).
|
|
5026
5076
|
//
|
|
5027
|
-
//
|
|
5028
|
-
//
|
|
5029
|
-
//
|
|
5030
|
-
//
|
|
5077
|
+
// Per ``project_phrase_safety_rule.md``
|
|
5078
|
+
// (memory file in p-diogo/totalreclaw-internal — absolute rule:
|
|
5079
|
+
// "recovery phrase MUST NEVER cross the LLM context in ANY form"),
|
|
5080
|
+
// phrase-generating agent tools are forbidden. The ONLY approved
|
|
5081
|
+
// agent-facilitated setup surface is ``totalreclaw_pair`` (browser-
|
|
5082
|
+
// side crypto keeps the phrase out of the LLM round-trip by
|
|
5083
|
+
// construction). The underlying ``runNonInteractiveOnboard`` code
|
|
5084
|
+
// path is still reachable via the CLI ``openclaw totalreclaw onboard``
|
|
5085
|
+
// — that path runs in the user's own terminal, OUTSIDE any agent
|
|
5086
|
+
// shell, so phrase stdout never feeds back into LLM context.
|
|
5031
5087
|
//
|
|
5032
|
-
//
|
|
5033
|
-
//
|
|
5034
|
-
//
|
|
5035
|
-
//
|
|
5036
|
-
// (mode 0600) by `runNonInteractiveOnboard`. We explicitly pass
|
|
5037
|
-
// `emitPhrase: false` so the phrase NEVER appears in the tool
|
|
5038
|
-
// return value.
|
|
5039
|
-
// - Returns only: scope_address + credentials_path + ok/error.
|
|
5088
|
+
// Audit assertion: ``tool-gating.test.ts`` enforces the removal —
|
|
5089
|
+
// any future re-registration of ``totalreclaw_onboard`` (or any
|
|
5090
|
+
// phrase-generating variant like ``totalreclaw_onboard_generate``,
|
|
5091
|
+
// ``totalreclaw_restore_phrase``) fails CI.
|
|
5040
5092
|
// ---------------------------------------------------------------
|
|
5041
|
-
api.registerTool(
|
|
5042
|
-
{
|
|
5043
|
-
name: 'totalreclaw_onboard',
|
|
5044
|
-
label: 'Onboard (generate new recovery phrase)',
|
|
5045
|
-
description:
|
|
5046
|
-
'Generate a NEW TotalReclaw recovery phrase on this machine without the user ' +
|
|
5047
|
-
'leaving chat. The phrase is written ONLY to ~/.totalreclaw/credentials.json (mode ' +
|
|
5048
|
-
'0600) and NEVER returned through this tool — the response contains just the derived ' +
|
|
5049
|
-
'Smart Account (scope) address and the credentials path so the user can retrieve ' +
|
|
5050
|
-
'their phrase locally (e.g. `cat ~/.totalreclaw/credentials.json | jq -r .mnemonic`).\n\n' +
|
|
5051
|
-
'Use when a fresh user asks you to set up TotalReclaw or enable memory. Refuses if ' +
|
|
5052
|
-
'onboarding is already active (the user can delete the credentials file to re-onboard, ' +
|
|
5053
|
-
'but we will NOT silently overwrite). For RESTORE (import an existing phrase), tell ' +
|
|
5054
|
-
'the user to run `openclaw totalreclaw onboard --mode restore` in their local ' +
|
|
5055
|
-
'terminal — this tool refuses to accept phrases through chat.',
|
|
5056
|
-
parameters: {
|
|
5057
|
-
type: 'object',
|
|
5058
|
-
properties: {
|
|
5059
|
-
mode: {
|
|
5060
|
-
type: 'string',
|
|
5061
|
-
enum: ['generate'],
|
|
5062
|
-
description:
|
|
5063
|
-
'Only "generate" is supported via this tool (creates a fresh 12-word BIP-39 ' +
|
|
5064
|
-
'phrase). "restore" requires the local CLI wizard because pasting a phrase ' +
|
|
5065
|
-
'through chat ships it to the LLM provider, defeating end-to-end encryption.',
|
|
5066
|
-
default: 'generate',
|
|
5067
|
-
},
|
|
5068
|
-
},
|
|
5069
|
-
additionalProperties: false,
|
|
5070
|
-
},
|
|
5071
|
-
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
5072
|
-
const mode = params?.mode;
|
|
5073
|
-
if (mode !== undefined && mode !== 'generate') {
|
|
5074
|
-
return {
|
|
5075
|
-
content: [{
|
|
5076
|
-
type: 'text',
|
|
5077
|
-
text:
|
|
5078
|
-
'Only mode="generate" is supported through chat. For RESTORE (import an ' +
|
|
5079
|
-
'existing recovery phrase), ask the user to run `openclaw totalreclaw onboard ' +
|
|
5080
|
-
'--mode restore` in their local terminal — the phrase is read from stdin and ' +
|
|
5081
|
-
'never touches the LLM provider or the transcript.',
|
|
5082
|
-
}],
|
|
5083
|
-
};
|
|
5084
|
-
}
|
|
5085
|
-
try {
|
|
5086
|
-
const result: NonInteractiveOnboardResult = await runNonInteractiveOnboard({
|
|
5087
|
-
credentialsPath: CREDENTIALS_PATH,
|
|
5088
|
-
statePath: CONFIG.onboardingStatePath,
|
|
5089
|
-
mode: 'generate',
|
|
5090
|
-
emitPhrase: false, // NEVER include the phrase in agent-visible output.
|
|
5091
|
-
deriveScopeAddress: async (mnemonic: string) => {
|
|
5092
|
-
try {
|
|
5093
|
-
return await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
|
|
5094
|
-
} catch (err) {
|
|
5095
|
-
api.logger.warn(
|
|
5096
|
-
`totalreclaw_onboard: scope-address derivation failed: ${
|
|
5097
|
-
err instanceof Error ? err.message : String(err)
|
|
5098
|
-
}`,
|
|
5099
|
-
);
|
|
5100
|
-
return undefined;
|
|
5101
|
-
}
|
|
5102
|
-
},
|
|
5103
|
-
});
|
|
5104
|
-
if (!result.ok) {
|
|
5105
|
-
// already-active, write-failed, etc. Never leaks the phrase.
|
|
5106
|
-
api.logger.info(`totalreclaw_onboard: ok=false error=${result.error}`);
|
|
5107
|
-
return {
|
|
5108
|
-
content: [{
|
|
5109
|
-
type: 'text',
|
|
5110
|
-
text:
|
|
5111
|
-
result.error === 'already-active'
|
|
5112
|
-
? 'TotalReclaw is already set up on this machine. Memory tools are unblocked. To re-onboard, delete ~/.totalreclaw/credentials.json first.'
|
|
5113
|
-
: `Onboard failed: ${result.error_detail ?? result.error}`,
|
|
5114
|
-
}],
|
|
5115
|
-
details: {
|
|
5116
|
-
ok: false,
|
|
5117
|
-
error: result.error,
|
|
5118
|
-
action: result.action,
|
|
5119
|
-
},
|
|
5120
|
-
};
|
|
5121
|
-
}
|
|
5122
|
-
api.logger.info(
|
|
5123
|
-
`totalreclaw_onboard: generated phrase, scope=${result.scope_address?.slice(0, 10) ?? 'unknown'}…, credentials=${result.credentials_path}`,
|
|
5124
|
-
);
|
|
5125
|
-
// Crucially: the mnemonic field is NEVER emitted in details
|
|
5126
|
-
// (emitPhrase=false enforces that on the result object).
|
|
5127
|
-
return {
|
|
5128
|
-
content: [{
|
|
5129
|
-
type: 'text',
|
|
5130
|
-
text:
|
|
5131
|
-
`TotalReclaw setup complete. A new 12-word recovery phrase was generated and ` +
|
|
5132
|
-
`saved to ${result.credentials_path} (mode 0600). ` +
|
|
5133
|
-
(result.scope_address
|
|
5134
|
-
? `Your on-chain scope (Smart Account) address is ${result.scope_address}. `
|
|
5135
|
-
: '') +
|
|
5136
|
-
`To view your recovery phrase, run on the user's local terminal:\n\n` +
|
|
5137
|
-
` cat ${result.credentials_path} | jq -r .mnemonic\n\n` +
|
|
5138
|
-
`IMPORTANT: the recovery phrase is the ONLY way to recover memories on another ` +
|
|
5139
|
-
`device. Tell the user to store it safely — a password manager or a paper backup. ` +
|
|
5140
|
-
`TotalReclaw cannot recover it if lost.`,
|
|
5141
|
-
}],
|
|
5142
|
-
details: {
|
|
5143
|
-
ok: true,
|
|
5144
|
-
action: 'generate',
|
|
5145
|
-
scope_address: result.scope_address,
|
|
5146
|
-
credentials_path: result.credentials_path,
|
|
5147
|
-
// Deliberately NO mnemonic field.
|
|
5148
|
-
},
|
|
5149
|
-
};
|
|
5150
|
-
} catch (err: unknown) {
|
|
5151
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5152
|
-
api.logger.error(`totalreclaw_onboard failed: ${message}`);
|
|
5153
|
-
return {
|
|
5154
|
-
content: [{ type: 'text', text: `Onboard failed: ${humanizeError(message)}` }],
|
|
5155
|
-
details: { ok: false, error: 'unexpected', error_detail: message },
|
|
5156
|
-
};
|
|
5157
|
-
}
|
|
5158
|
-
},
|
|
5159
|
-
},
|
|
5160
|
-
{ name: 'totalreclaw_onboard' },
|
|
5161
|
-
);
|
|
5162
5093
|
|
|
5163
5094
|
// ---------------------------------------------------------------
|
|
5164
5095
|
// Tool: totalreclaw_pair (3.3.1-rc.2 — agent-callable pair-generate)
|
|
@@ -5179,10 +5110,12 @@ const plugin = {
|
|
|
5179
5110
|
'6-digit PIN, and an ASCII QR code that the agent relays to the user. The recovery ' +
|
|
5180
5111
|
'phrase itself is generated/entered in the BROWSER and uploaded end-to-end encrypted ' +
|
|
5181
5112
|
'to this gateway — it NEVER touches the LLM provider or the chat transcript.\n\n' +
|
|
5182
|
-
'
|
|
5183
|
-
'
|
|
5184
|
-
'
|
|
5185
|
-
'
|
|
5113
|
+
'This is the CANONICAL agent-facilitated setup surface — use it whenever the user ' +
|
|
5114
|
+
'asks you to set up TotalReclaw, regardless of whether they have terminal access. ' +
|
|
5115
|
+
'Browser-side crypto keeps the recovery phrase out of the LLM context entirely. ' +
|
|
5116
|
+
'If a user explicitly prefers local-terminal setup with no browser, point them at ' +
|
|
5117
|
+
'`totalreclaw_onboarding_start` (a pointer to the CLI wizard they run on their own ' +
|
|
5118
|
+
'terminal, NOT through your shell tool).',
|
|
5186
5119
|
parameters: {
|
|
5187
5120
|
type: 'object',
|
|
5188
5121
|
properties: {
|
|
@@ -5202,18 +5135,117 @@ const plugin = {
|
|
|
5202
5135
|
const rawMode = params?.mode;
|
|
5203
5136
|
const mode: 'generate' | 'import' =
|
|
5204
5137
|
rawMode === 'import' ? 'import' : 'generate';
|
|
5138
|
+
const pairMode = CONFIG.pairMode;
|
|
5205
5139
|
try {
|
|
5206
|
-
|
|
5207
|
-
|
|
5140
|
+
// 3.3.1-rc.11 — relay-brokered pair by default (universal reachability).
|
|
5141
|
+
// `TOTALRECLAW_PAIR_MODE=local` preserves the rc.4–rc.10 loopback flow
|
|
5142
|
+
// for air-gapped / self-hosted setups. Both paths return the same
|
|
5143
|
+
// tool payload (`{url, pin, expires_at_ms, qr_*, mode, instructions}`);
|
|
5144
|
+
// only the URL origin differs.
|
|
5145
|
+
let url: string;
|
|
5146
|
+
let pin: string;
|
|
5147
|
+
let sidOrToken: string;
|
|
5148
|
+
let expiresAtMs: number;
|
|
5149
|
+
let localSession: import('./pair-session-store.js').PairSession | undefined;
|
|
5150
|
+
|
|
5151
|
+
if (pairMode === 'relay') {
|
|
5152
|
+
const { openRemotePairSession, awaitPhraseUpload } = await import(
|
|
5153
|
+
'./pair-remote-client.js'
|
|
5154
|
+
);
|
|
5155
|
+
const remoteSession = await openRemotePairSession({
|
|
5156
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
5157
|
+
mode: mode === 'generate' ? 'generate' : 'import',
|
|
5158
|
+
});
|
|
5159
|
+
url = remoteSession.url;
|
|
5160
|
+
pin = remoteSession.pin;
|
|
5161
|
+
sidOrToken = remoteSession.token;
|
|
5162
|
+
// Relay sends ISO-8601; convert to ms for tool payload parity.
|
|
5163
|
+
const parsed = Date.parse(remoteSession.expiresAt);
|
|
5164
|
+
expiresAtMs = Number.isFinite(parsed)
|
|
5165
|
+
? parsed
|
|
5166
|
+
: Date.now() + 5 * 60_000;
|
|
5167
|
+
// Background task — writes credentials.json + flips state when
|
|
5168
|
+
// the browser completes the flow. Tool handler returns
|
|
5169
|
+
// immediately so the agent can tell the user the URL + PIN.
|
|
5170
|
+
void (async () => {
|
|
5171
|
+
try {
|
|
5172
|
+
await awaitPhraseUpload(remoteSession, {
|
|
5173
|
+
phraseValidator: (p: string) =>
|
|
5174
|
+
validateMnemonic(p, wordlist),
|
|
5175
|
+
completePairing: async ({ mnemonic }) => {
|
|
5176
|
+
try {
|
|
5177
|
+
// 3.3.1 (internal#130) — derive + persist the
|
|
5178
|
+
// Smart Account address now so the user can see
|
|
5179
|
+
// it immediately after pair, before any chain
|
|
5180
|
+
// write. Mnemonic stays in this scope (already
|
|
5181
|
+
// on disk in credentials.json); only the
|
|
5182
|
+
// derived public scope_address is added.
|
|
5183
|
+
let scopeAddress: string | undefined;
|
|
5184
|
+
try {
|
|
5185
|
+
scopeAddress = await deriveSmartAccountAddress(
|
|
5186
|
+
mnemonic,
|
|
5187
|
+
CONFIG.chainId,
|
|
5188
|
+
);
|
|
5189
|
+
} catch (deriveErr) {
|
|
5190
|
+
api.logger.warn(
|
|
5191
|
+
`totalreclaw_pair(relay): scope_address derivation failed (will retry lazily): ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`,
|
|
5192
|
+
);
|
|
5193
|
+
}
|
|
5194
|
+
const creds =
|
|
5195
|
+
loadCredentialsJson(CREDENTIALS_PATH) ?? {};
|
|
5196
|
+
const next: typeof creds = { ...creds, mnemonic };
|
|
5197
|
+
if (scopeAddress) next.scope_address = scopeAddress;
|
|
5198
|
+
if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
|
|
5199
|
+
return { state: 'error', error: 'credentials_write_failed' };
|
|
5200
|
+
}
|
|
5201
|
+
setRecoveryPhraseOverride(mnemonic);
|
|
5202
|
+
writeOnboardingState(CONFIG.onboardingStatePath, {
|
|
5203
|
+
onboardingState: 'active',
|
|
5204
|
+
createdBy: mode === 'generate' ? 'generate' : 'import',
|
|
5205
|
+
credentialsCreatedAt: new Date().toISOString(),
|
|
5206
|
+
version: pluginVersion ?? '3.3.0',
|
|
5207
|
+
});
|
|
5208
|
+
api.logger.info(
|
|
5209
|
+
`totalreclaw_pair(relay): session ${remoteSession.token.slice(0, 8)}… completed; credentials written${scopeAddress ? ` (scope_address=${scopeAddress})` : ''}`,
|
|
5210
|
+
);
|
|
5211
|
+
return { state: 'active' };
|
|
5212
|
+
} catch (err: unknown) {
|
|
5213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5214
|
+
api.logger.error(
|
|
5215
|
+
`totalreclaw_pair(relay): completePairing failed: ${msg}`,
|
|
5216
|
+
);
|
|
5217
|
+
return { state: 'error', error: msg };
|
|
5218
|
+
}
|
|
5219
|
+
},
|
|
5220
|
+
});
|
|
5221
|
+
} catch (bgErr: unknown) {
|
|
5222
|
+
// Expected on TTL expiry / user-aborts — log at warn, not error.
|
|
5223
|
+
const bgMsg = bgErr instanceof Error ? bgErr.message : String(bgErr);
|
|
5224
|
+
api.logger.warn(
|
|
5225
|
+
`totalreclaw_pair(relay): background task ended for token=${remoteSession.token.slice(0, 8)}…: ${bgMsg}`,
|
|
5226
|
+
);
|
|
5227
|
+
}
|
|
5228
|
+
})();
|
|
5229
|
+
} else {
|
|
5230
|
+
// Local loopback path (rc.10 behaviour).
|
|
5231
|
+
const { createPairSession } = await import('./pair-session-store.js');
|
|
5232
|
+
const { generateGatewayKeypair } = await import('./pair-crypto.js');
|
|
5233
|
+
const kp = generateGatewayKeypair();
|
|
5234
|
+
const session = await createPairSession(CONFIG.pairSessionsPath, {
|
|
5235
|
+
mode,
|
|
5236
|
+
operatorContext: { channel: 'agent' },
|
|
5237
|
+
rngPrivateKey: () => Buffer.from(kp.skB64, 'base64url'),
|
|
5238
|
+
rngPublicKey: () => Buffer.from(kp.pkB64, 'base64url'),
|
|
5239
|
+
});
|
|
5240
|
+
url = buildPairingUrl(api, session);
|
|
5241
|
+
pin = session.secondaryCode;
|
|
5242
|
+
sidOrToken = session.sid;
|
|
5243
|
+
expiresAtMs = session.expiresAtMs;
|
|
5244
|
+
localSession = session;
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5247
|
+
// QR renderers — same for both modes; input is the URL string.
|
|
5208
5248
|
const { defaultRenderQr } = await import('./pair-cli.js');
|
|
5209
|
-
const kp = generateGatewayKeypair();
|
|
5210
|
-
const session = await createPairSession(CONFIG.pairSessionsPath, {
|
|
5211
|
-
mode,
|
|
5212
|
-
operatorContext: { channel: 'agent' },
|
|
5213
|
-
rngPrivateKey: () => Buffer.from(kp.skB64, 'base64url'),
|
|
5214
|
-
rngPublicKey: () => Buffer.from(kp.pkB64, 'base64url'),
|
|
5215
|
-
});
|
|
5216
|
-
const url = buildPairingUrl(api, session);
|
|
5217
5249
|
const qrAscii = await new Promise<string>((resolve) => {
|
|
5218
5250
|
let settled = false;
|
|
5219
5251
|
const t = setTimeout(() => {
|
|
@@ -5236,16 +5268,40 @@ const plugin = {
|
|
|
5236
5268
|
resolve('');
|
|
5237
5269
|
}
|
|
5238
5270
|
});
|
|
5271
|
+
|
|
5272
|
+
// 3.3.1-rc.5 — PNG + Unicode QR for multi-transport rendering.
|
|
5273
|
+
let qrPngB64 = '';
|
|
5274
|
+
let qrUnicode = '';
|
|
5275
|
+
try {
|
|
5276
|
+
const { encodePng, encodeUnicode } = await import('./pair-qr.js');
|
|
5277
|
+
const [pngBuf, uni] = await Promise.all([
|
|
5278
|
+
encodePng(url),
|
|
5279
|
+
encodeUnicode(url),
|
|
5280
|
+
]);
|
|
5281
|
+
qrPngB64 = pngBuf.toString('base64');
|
|
5282
|
+
qrUnicode = uni;
|
|
5283
|
+
} catch (qrErr: unknown) {
|
|
5284
|
+
api.logger.warn(
|
|
5285
|
+
`totalreclaw_pair: QR encode failed (non-fatal): ${
|
|
5286
|
+
qrErr instanceof Error ? qrErr.message : String(qrErr)
|
|
5287
|
+
}`,
|
|
5288
|
+
);
|
|
5289
|
+
}
|
|
5290
|
+
|
|
5239
5291
|
api.logger.info(
|
|
5240
|
-
`totalreclaw_pair: session ${
|
|
5292
|
+
`totalreclaw_pair: session ${sidOrToken.slice(0, 8)}… mode=${mode} transport=${pairMode} url=${url} qr_png=${qrPngB64.length} qr_unicode=${qrUnicode.length}`,
|
|
5241
5293
|
);
|
|
5294
|
+
// Voidly reference localSession so TS does not flag the unused
|
|
5295
|
+
// local-branch binding. Future rc.12 diagnostics can expose
|
|
5296
|
+
// `session.mode` / `session.status` separately.
|
|
5297
|
+
void localSession;
|
|
5242
5298
|
return {
|
|
5243
5299
|
content: [{
|
|
5244
5300
|
type: 'text',
|
|
5245
5301
|
text:
|
|
5246
5302
|
`Pairing session started.\n\n` +
|
|
5247
5303
|
`URL: ${url}\n\n` +
|
|
5248
|
-
`PIN (type this into the browser): ${
|
|
5304
|
+
`PIN (type this into the browser): ${pin}\n\n` +
|
|
5249
5305
|
(qrAscii ? `QR code:\n\n${qrAscii}\n\n` : '') +
|
|
5250
5306
|
`Instructions for the user:\n` +
|
|
5251
5307
|
`1. Open the URL above on their phone or another browser (scan the QR or copy-paste).\n` +
|
|
@@ -5259,12 +5315,18 @@ const plugin = {
|
|
|
5259
5315
|
`This session expires in ~5 minutes. Run this tool again if you need a fresh URL.`,
|
|
5260
5316
|
}],
|
|
5261
5317
|
details: {
|
|
5262
|
-
sid:
|
|
5318
|
+
sid: sidOrToken,
|
|
5263
5319
|
url,
|
|
5264
|
-
pin
|
|
5320
|
+
pin,
|
|
5265
5321
|
mode,
|
|
5266
|
-
expires_at_ms:
|
|
5322
|
+
expires_at_ms: expiresAtMs,
|
|
5267
5323
|
qr_ascii: qrAscii,
|
|
5324
|
+
qr_png_b64: qrPngB64,
|
|
5325
|
+
qr_unicode: qrUnicode,
|
|
5326
|
+
// rc.11 — surface the transport so downstream tooling (QA
|
|
5327
|
+
// harness asserters, telemetry) can confirm which path
|
|
5328
|
+
// served the URL. Either 'relay' or 'local'.
|
|
5329
|
+
transport: pairMode,
|
|
5268
5330
|
},
|
|
5269
5331
|
};
|
|
5270
5332
|
} catch (err: unknown) {
|
|
@@ -5279,6 +5341,166 @@ const plugin = {
|
|
|
5279
5341
|
},
|
|
5280
5342
|
{ name: 'totalreclaw_pair' },
|
|
5281
5343
|
);
|
|
5344
|
+
// 3.3.1-rc.20 (issue #110): explicit post-registration breadcrumb so
|
|
5345
|
+
// ops/QA can grep gateway logs for definitive proof the tool was
|
|
5346
|
+
// declared. If the agent then reports the tool is missing from its
|
|
5347
|
+
// tool list, the gap is upstream OpenClaw tool propagation, not our
|
|
5348
|
+
// plugin — see issue #110 fix 3 + PR #102 (CLI fallback).
|
|
5349
|
+
//
|
|
5350
|
+
// 3.3.1-rc.21 (issue #128): the breadcrumb is debug-grade — it was
|
|
5351
|
+
// bleeding into `openclaw agent --json` stdout, breaking programmatic
|
|
5352
|
+
// parsers that expect the JSON-RPC body to be the only thing on the
|
|
5353
|
+
// wire. Gated behind `TOTALRECLAW_VERBOSE_REGISTER` (or the general
|
|
5354
|
+
// `TOTALRECLAW_DEBUG` toggle) so ops can opt back in when chasing
|
|
5355
|
+
// a tool-injection regression. Default OFF — clean stdout for users.
|
|
5356
|
+
if (CONFIG.verboseRegister) {
|
|
5357
|
+
api.logger.info(
|
|
5358
|
+
'TotalReclaw: registerTool(totalreclaw_pair) returned. If the agent does not see it in its tool list ' +
|
|
5359
|
+
'after gateway restart, the issue is upstream tool injection (containerized agents) — fall back to ' +
|
|
5360
|
+
'`openclaw totalreclaw pair generate --url-pin-only` (PR #102) or `openclaw totalreclaw onboard --pair-only`.',
|
|
5361
|
+
);
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
// ---------------------------------------------------------------
|
|
5365
|
+
// Tool: totalreclaw_report_qa_bug (3.3.1-rc.3 — RC-gated)
|
|
5366
|
+
//
|
|
5367
|
+
// Lets the agent file a structured QA-bug issue to
|
|
5368
|
+
// `p-diogo/totalreclaw-internal` during RC testing. Only registered
|
|
5369
|
+
// when the plugin version contains `-rc.` — stable users never see it.
|
|
5370
|
+
//
|
|
5371
|
+
// Secrets (recovery phrases, API keys, Telegram bot tokens) are
|
|
5372
|
+
// redacted inside `postQaBugIssue` before the POST. The agent should
|
|
5373
|
+
// still avoid passing raw secrets — see SKILL.md addendum.
|
|
5374
|
+
// ---------------------------------------------------------------
|
|
5375
|
+
if (rcMode) {
|
|
5376
|
+
api.registerTool(
|
|
5377
|
+
{
|
|
5378
|
+
name: 'totalreclaw_report_qa_bug',
|
|
5379
|
+
label: 'File a QA bug issue (RC builds only)',
|
|
5380
|
+
description:
|
|
5381
|
+
'File a structured QA bug report to the internal tracker. RC-only; never available in stable builds. ' +
|
|
5382
|
+
'Do NOT call auto-file — ask the user first before invoking. The tool redacts recovery phrases, API keys, ' +
|
|
5383
|
+
'and Telegram bot tokens from all free-text fields before posting, but the agent SHOULD still avoid ' +
|
|
5384
|
+
'passing raw secrets.',
|
|
5385
|
+
parameters: {
|
|
5386
|
+
type: 'object',
|
|
5387
|
+
properties: {
|
|
5388
|
+
integration: {
|
|
5389
|
+
type: 'string',
|
|
5390
|
+
enum: ['plugin', 'hermes', 'nanoclaw', 'mcp', 'relay', 'clawhub', 'docs', 'other'],
|
|
5391
|
+
description: 'Which TotalReclaw surface is affected.',
|
|
5392
|
+
},
|
|
5393
|
+
rc_version: {
|
|
5394
|
+
type: 'string',
|
|
5395
|
+
description: 'Exact RC version string (e.g. "3.3.1-rc.3" or "2.3.1rc3").',
|
|
5396
|
+
},
|
|
5397
|
+
severity: {
|
|
5398
|
+
type: 'string',
|
|
5399
|
+
enum: ['blocker', 'high', 'medium', 'low'],
|
|
5400
|
+
description: 'blocker=release blocked, high=major UX failure, medium=annoying, low=polish.',
|
|
5401
|
+
},
|
|
5402
|
+
title: {
|
|
5403
|
+
type: 'string',
|
|
5404
|
+
description: 'Short summary, <60 chars. Prefix "[qa-bug]" is added automatically.',
|
|
5405
|
+
maxLength: 60,
|
|
5406
|
+
},
|
|
5407
|
+
symptom: {
|
|
5408
|
+
type: 'string',
|
|
5409
|
+
description: 'What happened (redacted automatically).',
|
|
5410
|
+
},
|
|
5411
|
+
expected: {
|
|
5412
|
+
type: 'string',
|
|
5413
|
+
description: 'What should have happened.',
|
|
5414
|
+
},
|
|
5415
|
+
repro: {
|
|
5416
|
+
type: 'string',
|
|
5417
|
+
description: 'Reproduction steps (redacted automatically).',
|
|
5418
|
+
},
|
|
5419
|
+
logs: {
|
|
5420
|
+
type: 'string',
|
|
5421
|
+
description: 'Log excerpts / error messages (redacted automatically).',
|
|
5422
|
+
},
|
|
5423
|
+
environment: {
|
|
5424
|
+
type: 'string',
|
|
5425
|
+
description: 'Host, Docker/native, OpenClaw version, LLM provider, etc.',
|
|
5426
|
+
},
|
|
5427
|
+
},
|
|
5428
|
+
required: [
|
|
5429
|
+
'integration',
|
|
5430
|
+
'rc_version',
|
|
5431
|
+
'severity',
|
|
5432
|
+
'title',
|
|
5433
|
+
'symptom',
|
|
5434
|
+
'expected',
|
|
5435
|
+
'repro',
|
|
5436
|
+
'logs',
|
|
5437
|
+
'environment',
|
|
5438
|
+
],
|
|
5439
|
+
additionalProperties: false,
|
|
5440
|
+
},
|
|
5441
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
5442
|
+
try {
|
|
5443
|
+
const { postQaBugIssue } = await import('./qa-bug-report.js');
|
|
5444
|
+
// The token is resolved via CONFIG (config.ts) so index.ts
|
|
5445
|
+
// stays clean of env-harvesting triggers.
|
|
5446
|
+
const token = CONFIG.qaGithubToken;
|
|
5447
|
+
if (!token) {
|
|
5448
|
+
return {
|
|
5449
|
+
content: [{
|
|
5450
|
+
type: 'text',
|
|
5451
|
+
text:
|
|
5452
|
+
'Cannot file QA bug: no GitHub token found. The operator must export ' +
|
|
5453
|
+
'TOTALRECLAW_QA_GITHUB_TOKEN (or GITHUB_TOKEN) with `repo` scope to enable ' +
|
|
5454
|
+
'agent-filed bug reports during RC testing.',
|
|
5455
|
+
}],
|
|
5456
|
+
details: { error: 'missing_github_token' },
|
|
5457
|
+
};
|
|
5458
|
+
}
|
|
5459
|
+
// rc.14 — `repo` is resolved inside `postQaBugIssue` via
|
|
5460
|
+
// `resolveQaRepo(...)`, which reads `TOTALRECLAW_QA_REPO` and
|
|
5461
|
+
// refuses any slug that isn't a `-internal` fork. Pass the
|
|
5462
|
+
// config-surfaced override so env reads stay in config.ts.
|
|
5463
|
+
const repoOverride = CONFIG.qaRepoOverride || undefined;
|
|
5464
|
+
const result = await postQaBugIssue(
|
|
5465
|
+
params as unknown as import('./qa-bug-report.js').QaBugArgs,
|
|
5466
|
+
{
|
|
5467
|
+
githubToken: token,
|
|
5468
|
+
repo: repoOverride,
|
|
5469
|
+
logger: api.logger,
|
|
5470
|
+
},
|
|
5471
|
+
);
|
|
5472
|
+
return {
|
|
5473
|
+
content: [{
|
|
5474
|
+
type: 'text',
|
|
5475
|
+
text: `Filed QA bug #${result.issue_number}: ${result.issue_url}`,
|
|
5476
|
+
}],
|
|
5477
|
+
details: { issue_url: result.issue_url, issue_number: result.issue_number },
|
|
5478
|
+
};
|
|
5479
|
+
} catch (err: unknown) {
|
|
5480
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5481
|
+
api.logger.error(`totalreclaw_report_qa_bug failed: ${message}`);
|
|
5482
|
+
return {
|
|
5483
|
+
content: [{
|
|
5484
|
+
type: 'text',
|
|
5485
|
+
text: `Failed to file QA bug: ${message}`,
|
|
5486
|
+
}],
|
|
5487
|
+
details: { error: message },
|
|
5488
|
+
};
|
|
5489
|
+
}
|
|
5490
|
+
},
|
|
5491
|
+
},
|
|
5492
|
+
{ name: 'totalreclaw_report_qa_bug' },
|
|
5493
|
+
);
|
|
5494
|
+
// 3.3.1-rc.21 (issue #128): demote the registration-confirmation
|
|
5495
|
+
// breadcrumb to verbose-only. Same `--json` stdout pollution risk
|
|
5496
|
+
// as the totalreclaw_pair breadcrumb above; ops can opt back in
|
|
5497
|
+
// via TOTALRECLAW_VERBOSE_REGISTER / TOTALRECLAW_DEBUG.
|
|
5498
|
+
if (CONFIG.verboseRegister) {
|
|
5499
|
+
api.logger.info(
|
|
5500
|
+
'totalreclaw_report_qa_bug registered (RC build — this tool is hidden in stable releases).',
|
|
5501
|
+
);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5282
5504
|
|
|
5283
5505
|
// ---------------------------------------------------------------
|
|
5284
5506
|
// Hook: before_tool_call (3.2.0 memory-tool gate)
|
|
@@ -5372,11 +5594,18 @@ const plugin = {
|
|
|
5372
5594
|
'## TotalReclaw setup pending\n\n' +
|
|
5373
5595
|
'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
|
|
5374
5596
|
'If the user asks about memory features or wants to configure TotalReclaw, ' +
|
|
5375
|
-
'call the `
|
|
5597
|
+
'call the `totalreclaw_pair` tool — it returns a pairing URL + 6-digit PIN ' +
|
|
5598
|
+
'the user opens in a browser (phrase is generated and encrypted in-browser, ' +
|
|
5599
|
+
'never leaves their device in plaintext). If `totalreclaw_pair` is not in ' +
|
|
5600
|
+
'your tool list (container-based agents sometimes miss it), shell out to ' +
|
|
5601
|
+
'`openclaw totalreclaw pair generate --url-pin-only` on the gateway host — ' +
|
|
5602
|
+
'it emits a single JSON line `{v,url,pin,expires_at_ms}` with no phrase ' +
|
|
5603
|
+
'material; hand the user the `url` and `pin`.\n\n' +
|
|
5376
5604
|
'**Do NOT** attempt to generate, display, or relay a recovery phrase in chat. ' +
|
|
5377
|
-
'
|
|
5378
|
-
'
|
|
5379
|
-
'
|
|
5605
|
+
'**Do NOT** run `openclaw totalreclaw onboard` — that CLI emits the recovery ' +
|
|
5606
|
+
'phrase on stdout and would leak it into the LLM transcript. Use `pair` ' +
|
|
5607
|
+
'(tool or `--url-pin-only` CLI) instead; `onboard` is reserved for users ' +
|
|
5608
|
+
'running it directly in their own local terminal.',
|
|
5380
5609
|
};
|
|
5381
5610
|
}
|
|
5382
5611
|
|
|
@@ -5406,7 +5635,7 @@ const plugin = {
|
|
|
5406
5635
|
const walletParam = encodeURIComponent(subgraphOwner || userId || '');
|
|
5407
5636
|
const billingResp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${walletParam}`, {
|
|
5408
5637
|
method: 'GET',
|
|
5409
|
-
headers: { 'Authorization': `Bearer ${authKeyHex}`, 'Accept': 'application/json'
|
|
5638
|
+
headers: buildRelayHeaders({ 'Authorization': `Bearer ${authKeyHex}`, 'Accept': 'application/json' }),
|
|
5410
5639
|
});
|
|
5411
5640
|
if (billingResp.ok) {
|
|
5412
5641
|
const billingData = await billingResp.json() as Record<string, unknown>;
|
|
@@ -5643,14 +5872,14 @@ const plugin = {
|
|
|
5643
5872
|
|
|
5644
5873
|
if (reranked.length === 0) return undefined;
|
|
5645
5874
|
|
|
5646
|
-
// 6b.
|
|
5647
|
-
//
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5875
|
+
// 6b. Relevance gate — see recall tool above for the cosine +
|
|
5876
|
+
// lexical-override rule (issue #116).
|
|
5877
|
+
if (!passesRelevanceGate(evt.prompt, reranked, COSINE_THRESHOLD)) {
|
|
5878
|
+
const hookMaxCosine = Math.max(
|
|
5879
|
+
...reranked.map((r) => r.cosineSimilarity ?? 0),
|
|
5880
|
+
);
|
|
5652
5881
|
api.logger.info(
|
|
5653
|
-
`Hook:
|
|
5882
|
+
`Hook: relevance gate filtered results (max cosine=${hookMaxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD}, no lexical override)`,
|
|
5654
5883
|
);
|
|
5655
5884
|
return undefined;
|
|
5656
5885
|
}
|
|
@@ -5758,14 +5987,14 @@ const plugin = {
|
|
|
5758
5987
|
|
|
5759
5988
|
if (reranked.length === 0) return undefined;
|
|
5760
5989
|
|
|
5761
|
-
//
|
|
5762
|
-
//
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5990
|
+
// Relevance gate — see recall tool for the cosine + lexical-override
|
|
5991
|
+
// rule (issue #116).
|
|
5992
|
+
if (!passesRelevanceGate(evt.prompt, reranked, COSINE_THRESHOLD)) {
|
|
5993
|
+
const srvMaxCosine = Math.max(
|
|
5994
|
+
...reranked.map((r) => r.cosineSimilarity ?? 0),
|
|
5995
|
+
);
|
|
5767
5996
|
api.logger.info(
|
|
5768
|
-
`Hook:
|
|
5997
|
+
`Hook: relevance gate filtered results (max cosine=${srvMaxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD}, no lexical override)`,
|
|
5769
5998
|
);
|
|
5770
5999
|
return undefined;
|
|
5771
6000
|
}
|