@totalreclaw/totalreclaw 3.3.1-rc.15 → 3.3.1-rc.17
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 +0 -41
- package/SKILL.md +2 -2
- package/embedding.ts +4 -40
- package/index.ts +11 -4
- package/package.json +4 -14
- package/pair-cli.ts +43 -8
package/CHANGELOG.md
CHANGED
|
@@ -4,47 +4,6 @@ All notable changes to `@totalreclaw/totalreclaw` (the OpenClaw plugin) are docu
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [3.3.1-rc.15] — 2026-04-24
|
|
8
|
-
|
|
9
|
-
Install-time unblock for bandwidth-constrained hosts. Lazy-loads the ONNX
|
|
10
|
-
runtime instead of forcing a ~216MB download during `openclaw plugins
|
|
11
|
-
install`.
|
|
12
|
-
|
|
13
|
-
### Lazy-loaded ONNX / `@huggingface/transformers`
|
|
14
|
-
|
|
15
|
-
`openclaw plugins install @totalreclaw/totalreclaw` on slow hosts (VPNs,
|
|
16
|
-
CI containers with limited bandwidth, metered connections) was exceeding
|
|
17
|
-
the plugin-install timeout mid-download and getting SIGTERM'd, leaving
|
|
18
|
-
the plugin partially installed. Root cause: `@huggingface/transformers`
|
|
19
|
-
was a direct `dependency`, which transitively pulled
|
|
20
|
-
`onnxruntime-node`'s postinstall binary fetch (~216MB from GitHub
|
|
21
|
-
Releases).
|
|
22
|
-
|
|
23
|
-
- `@huggingface/transformers` and `onnxruntime-node` moved from
|
|
24
|
-
`dependencies` to optional `peerDependencies`
|
|
25
|
-
(`peerDependenciesMeta.<name>.optional: true`). npm v7+ and the
|
|
26
|
-
OpenClaw install path (`--legacy-peer-deps`) both skip these by
|
|
27
|
-
default — plugin install is now lean.
|
|
28
|
-
- `embedding.ts` converts the static
|
|
29
|
-
`import { AutoTokenizer, AutoModel, pipeline } from
|
|
30
|
-
'@huggingface/transformers'` into a dynamic `await import(...)` on
|
|
31
|
-
the first `generateEmbedding` call. If the optional peer is missing,
|
|
32
|
-
the error surfaces a clear install hint:
|
|
33
|
-
`npm install @huggingface/transformers`.
|
|
34
|
-
- New regression test `lazy-load-embedding.test.ts` asserts the
|
|
35
|
-
invariants (no top-level static runtime import; heavy packages not in
|
|
36
|
-
`dependencies`; peer-dep `optional` flag set) so a future refactor
|
|
37
|
-
can't silently reintroduce the install-time block.
|
|
38
|
-
|
|
39
|
-
**User impact:** users on constrained hosts can now install the plugin
|
|
40
|
-
without the 216MB download. Users who want semantic memory (recall /
|
|
41
|
-
search over encrypted facts) install `@huggingface/transformers`
|
|
42
|
-
separately — one-time, resumable if it times out.
|
|
43
|
-
|
|
44
|
-
Fixes [issue #92][i92] (QA bug 6 of 10, split from #84).
|
|
45
|
-
|
|
46
|
-
[i92]: https://github.com/p-diogo/totalreclaw-internal/issues/92
|
|
47
|
-
|
|
48
7
|
## [3.3.1-rc.14] — 2026-04-24
|
|
49
8
|
|
|
50
9
|
Coordinated version bump with Python `2.3.1rc14`. Two narrow bug fixes
|
package/SKILL.md
CHANGED
|
@@ -66,7 +66,7 @@ The QR encodes ONLY the pair URL. The 6-digit PIN is a separate out-of-band conf
|
|
|
66
66
|
|
|
67
67
|
## Phrase safety (HARD — never break)
|
|
68
68
|
|
|
69
|
-
NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard`, `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated setup
|
|
69
|
+
NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard`, `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated setup paths are (in order of preference): (1) `totalreclaw_pair` tool; (2) if the tool is missing from your tool list — a known gap for some container-based agents, issue #87 — shell out to `openclaw totalreclaw pair generate --url-pin-only`, which emits a single-line JSON `{v,url,pin,expires_at_ms}` with zero phrase material on stdout (pair-crypto is x25519-only, not BIP-39). If the user pastes a phrase anyway: tell them it is compromised, call `totalreclaw_pair` with `mode=generate` for a fresh wallet.
|
|
70
70
|
|
|
71
71
|
## Usage (post-setup)
|
|
72
72
|
|
|
@@ -78,7 +78,7 @@ NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER i
|
|
|
78
78
|
|
|
79
79
|
## Diagnostics
|
|
80
80
|
|
|
81
|
-
- `totalreclaw_*` tools not visible → plugin not installed or gateway not restarted. Verify via `openclaw plugins list | grep totalreclaw`. If listed,
|
|
81
|
+
- `totalreclaw_*` tools not visible → plugin not installed or gateway not restarted. Verify via `openclaw plugins list | grep totalreclaw`. If listed but tools still absent (container-agent gap, issue #87), shell out to `openclaw totalreclaw pair generate --url-pin-only` on the gateway host and hand the user the `url` + `pin` from the JSON payload — do NOT suggest `openclaw totalreclaw onboard` (leaks phrase on stdout).
|
|
82
82
|
- User says done but `credentials.json` missing → PIN expired or entered wrong phrase; call `totalreclaw_pair` again.
|
|
83
83
|
- `onboarding required` → credentials missing; redo from the pair step.
|
|
84
84
|
- `quota exceeded` → `totalreclaw_status`, then offer `totalreclaw_upgrade`.
|
package/embedding.ts
CHANGED
|
@@ -8,44 +8,11 @@
|
|
|
8
8
|
* embedding model breaks search across an existing vault, so the
|
|
9
9
|
* `TOTALRECLAW_EMBEDDING_MODEL` user-facing env var was removed in v1.
|
|
10
10
|
*
|
|
11
|
-
* Dependencies: @huggingface/transformers
|
|
12
|
-
* dependency. It is lazy-loaded on the first `generateEmbedding` call so
|
|
13
|
-
* `openclaw plugins install @totalreclaw/totalreclaw` does not block on the
|
|
14
|
-
* ~216MB onnxruntime-node native-binary download. Install it separately to
|
|
15
|
-
* enable semantic search: `npm install @huggingface/transformers`.
|
|
11
|
+
* Dependencies: @huggingface/transformers
|
|
16
12
|
*/
|
|
17
13
|
|
|
18
|
-
// Type-only import — erased at compile time, no runtime dep on the package.
|
|
19
14
|
// @ts-ignore - @huggingface/transformers types may not be perfect
|
|
20
|
-
import type
|
|
21
|
-
|
|
22
|
-
type HFTransformers = typeof import('@huggingface/transformers');
|
|
23
|
-
|
|
24
|
-
/** Cached module handle after first successful dynamic import. */
|
|
25
|
-
let transformersModule: HFTransformers | null = null;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Lazily import @huggingface/transformers. The package is declared as an
|
|
29
|
-
* optional peer dependency so the plugin installs on bandwidth-constrained
|
|
30
|
-
* hosts without pulling the onnxruntime-node native binary (~216MB). On first
|
|
31
|
-
* use, try to load it; if the user never installed it, surface a clear
|
|
32
|
-
* actionable error with the install command.
|
|
33
|
-
*/
|
|
34
|
-
async function loadTransformers(): Promise<HFTransformers> {
|
|
35
|
-
if (transformersModule) return transformersModule;
|
|
36
|
-
try {
|
|
37
|
-
// @ts-ignore - dynamic import target is the optional peer dep
|
|
38
|
-
transformersModule = (await import('@huggingface/transformers')) as HFTransformers;
|
|
39
|
-
return transformersModule;
|
|
40
|
-
} catch (err) {
|
|
41
|
-
const hint =
|
|
42
|
-
'[TotalReclaw] @huggingface/transformers is not installed. ' +
|
|
43
|
-
'Semantic memory requires it (one-time ~216MB download of ONNX runtime + model). ' +
|
|
44
|
-
'Install with: npm install @huggingface/transformers';
|
|
45
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
46
|
-
throw new Error(`${hint}\nUnderlying load error: ${detail}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
15
|
+
import { AutoTokenizer, AutoModel, pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
|
|
49
16
|
|
|
50
17
|
interface ModelConfig {
|
|
51
18
|
id: string;
|
|
@@ -78,17 +45,14 @@ let activeModel: ModelConfig | null = null;
|
|
|
78
45
|
/**
|
|
79
46
|
* Generate an embedding vector for the given text.
|
|
80
47
|
*
|
|
81
|
-
* On first call,
|
|
82
|
-
*
|
|
83
|
-
* (cached after download). Subsequent calls reuse the loaded module + model
|
|
84
|
-
* and run in ~100ms.
|
|
48
|
+
* On first call, downloads and loads the ONNX model (cached after download).
|
|
49
|
+
* Subsequent calls reuse the loaded model and run in ~100ms.
|
|
85
50
|
*/
|
|
86
51
|
export async function generateEmbedding(
|
|
87
52
|
text: string,
|
|
88
53
|
options?: { isQuery?: boolean },
|
|
89
54
|
): Promise<number[]> {
|
|
90
55
|
if (!activeModel) {
|
|
91
|
-
const { AutoTokenizer, AutoModel, pipeline } = await loadTransformers();
|
|
92
56
|
activeModel = getModelConfig();
|
|
93
57
|
console.error(`[TotalReclaw] Downloading embedding model (${activeModel.size}, one-time setup)...`);
|
|
94
58
|
console.error('[TotalReclaw] This enables semantic search across your encrypted memories.');
|
package/index.ts
CHANGED
|
@@ -5412,11 +5412,18 @@ const plugin = {
|
|
|
5412
5412
|
'## TotalReclaw setup pending\n\n' +
|
|
5413
5413
|
'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
|
|
5414
5414
|
'If the user asks about memory features or wants to configure TotalReclaw, ' +
|
|
5415
|
-
'call the `
|
|
5415
|
+
'call the `totalreclaw_pair` tool — it returns a pairing URL + 6-digit PIN ' +
|
|
5416
|
+
'the user opens in a browser (phrase is generated and encrypted in-browser, ' +
|
|
5417
|
+
'never leaves their device in plaintext). If `totalreclaw_pair` is not in ' +
|
|
5418
|
+
'your tool list (container-based agents sometimes miss it), shell out to ' +
|
|
5419
|
+
'`openclaw totalreclaw pair generate --url-pin-only` on the gateway host — ' +
|
|
5420
|
+
'it emits a single JSON line `{v,url,pin,expires_at_ms}` with no phrase ' +
|
|
5421
|
+
'material; hand the user the `url` and `pin`.\n\n' +
|
|
5416
5422
|
'**Do NOT** attempt to generate, display, or relay a recovery phrase in chat. ' +
|
|
5417
|
-
'
|
|
5418
|
-
'
|
|
5419
|
-
'
|
|
5423
|
+
'**Do NOT** run `openclaw totalreclaw onboard` — that CLI emits the recovery ' +
|
|
5424
|
+
'phrase on stdout and would leak it into the LLM transcript. Use `pair` ' +
|
|
5425
|
+
'(tool or `--url-pin-only` CLI) instead; `onboard` is reserved for users ' +
|
|
5426
|
+
'running it directly in their own local terminal.',
|
|
5420
5427
|
};
|
|
5421
5428
|
}
|
|
5422
5429
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.1-rc.
|
|
3
|
+
"version": "3.3.1-rc.17",
|
|
4
4
|
"description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -31,26 +31,16 @@
|
|
|
31
31
|
"author": "TotalReclaw Team",
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@huggingface/transformers": "^4.0.1",
|
|
34
35
|
"@totalreclaw/client": "^1.2.0",
|
|
35
36
|
"@totalreclaw/core": "^2.1.1",
|
|
36
37
|
"@types/qrcode": "^1.5.6",
|
|
37
38
|
"@types/ws": "^8.5.12",
|
|
39
|
+
"onnxruntime-node": "^1.24.0",
|
|
38
40
|
"qrcode": "^1.5.4",
|
|
39
41
|
"qrcode-terminal": "^0.12.0",
|
|
40
42
|
"ws": "^8.18.3"
|
|
41
43
|
},
|
|
42
|
-
"peerDependencies": {
|
|
43
|
-
"@huggingface/transformers": "^4.0.1",
|
|
44
|
-
"onnxruntime-node": "^1.24.0"
|
|
45
|
-
},
|
|
46
|
-
"peerDependenciesMeta": {
|
|
47
|
-
"@huggingface/transformers": {
|
|
48
|
-
"optional": true
|
|
49
|
-
},
|
|
50
|
-
"onnxruntime-node": {
|
|
51
|
-
"optional": true
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
44
|
"files": [
|
|
55
45
|
"*.ts",
|
|
56
46
|
"import-adapters/",
|
|
@@ -64,7 +54,7 @@
|
|
|
64
54
|
"skill.json"
|
|
65
55
|
],
|
|
66
56
|
"scripts": {
|
|
67
|
-
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts
|
|
57
|
+
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts",
|
|
68
58
|
"check-scanner": "node ../scripts/check-scanner.mjs",
|
|
69
59
|
"prepublishOnly": "node ../scripts/check-scanner.mjs"
|
|
70
60
|
},
|
package/pair-cli.ts
CHANGED
|
@@ -85,8 +85,15 @@ export interface PairCliOutcome {
|
|
|
85
85
|
* as the session reaches a terminal state — same status-code
|
|
86
86
|
* semantics as 'human' (0 on completed, 1 on expired/rejected/error,
|
|
87
87
|
* 130 on canceled).
|
|
88
|
+
* - 'url-pin': (3.3.1-rc.15, issue #87) headless container-agent fallback.
|
|
89
|
+
* Emits ONLY `{ v, url, pin, expires_at_ms }` — no QR ASCII, no SID,
|
|
90
|
+
* no mode echo. Use when a container-based agent cannot see the
|
|
91
|
+
* `totalreclaw_pair` tool (OpenClaw gateway-to-container tool-injection
|
|
92
|
+
* gap) and must shell out to the CLI. Guarantees zero phrase material
|
|
93
|
+
* on stdout by construction — pair-crypto is x25519-only and the slim
|
|
94
|
+
* payload carries nothing BIP-39-adjacent.
|
|
88
95
|
*/
|
|
89
|
-
export type PairCliOutputMode = 'human' | 'json';
|
|
96
|
+
export type PairCliOutputMode = 'human' | 'json' | 'url-pin';
|
|
90
97
|
|
|
91
98
|
/**
|
|
92
99
|
* JSON payload emitted by runPairCli when outputMode === 'json'. Printed
|
|
@@ -103,6 +110,17 @@ export interface PairCliJsonPayload {
|
|
|
103
110
|
qr_ascii: string;
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Slim payload for outputMode === 'url-pin'. Intentionally a subset of
|
|
115
|
+
* `PairCliJsonPayload` with no QR ASCII, SID, or mode echo. Issue #87.
|
|
116
|
+
*/
|
|
117
|
+
export interface PairCliUrlPinPayload {
|
|
118
|
+
v: 1;
|
|
119
|
+
url: string;
|
|
120
|
+
pin: string;
|
|
121
|
+
expires_at_ms: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
// ---------------------------------------------------------------------------
|
|
107
125
|
// Default stdout IO
|
|
108
126
|
// ---------------------------------------------------------------------------
|
|
@@ -213,9 +231,11 @@ export async function runPairCli(
|
|
|
213
231
|
return { status: 'error', error: msg };
|
|
214
232
|
}
|
|
215
233
|
|
|
216
|
-
// 2.
|
|
234
|
+
// 2. Build the URL unconditionally, but only render the QR for modes
|
|
235
|
+
// that actually emit it. url-pin mode skips the renderer entirely —
|
|
236
|
+
// no CPU cost, no qrcode-terminal import, no ASCII on stdout.
|
|
217
237
|
const url = deps.renderPairingUrl(session);
|
|
218
|
-
const qrAscii = await new Promise<string>((resolve) => {
|
|
238
|
+
const qrAscii = outputMode === 'url-pin' ? '' : await new Promise<string>((resolve) => {
|
|
219
239
|
// Guard against QR renderers that never fire their callback (shouldn't
|
|
220
240
|
// happen with qrcode-terminal, but defensive): a 10-second timeout
|
|
221
241
|
// returns an empty string so we never hang the pairing flow.
|
|
@@ -241,8 +261,16 @@ export async function runPairCli(
|
|
|
241
261
|
}
|
|
242
262
|
});
|
|
243
263
|
|
|
244
|
-
// 3. Emit the visible surface (JSON first — single line — or human copy).
|
|
245
|
-
if (outputMode === '
|
|
264
|
+
// 3. Emit the visible surface (JSON/url-pin first — single line — or human copy).
|
|
265
|
+
if (outputMode === 'url-pin') {
|
|
266
|
+
const payload: PairCliUrlPinPayload = {
|
|
267
|
+
v: 1,
|
|
268
|
+
url,
|
|
269
|
+
pin: session.secondaryCode,
|
|
270
|
+
expires_at_ms: session.expiresAtMs,
|
|
271
|
+
};
|
|
272
|
+
stdout.write(JSON.stringify(payload) + '\n');
|
|
273
|
+
} else if (outputMode === 'json') {
|
|
246
274
|
const payload: PairCliJsonPayload = {
|
|
247
275
|
v: 1,
|
|
248
276
|
sid: session.sid,
|
|
@@ -276,7 +304,9 @@ export async function runPairCli(
|
|
|
276
304
|
canceled = true;
|
|
277
305
|
});
|
|
278
306
|
|
|
279
|
-
// 5. Poll
|
|
307
|
+
// 5. Poll — status transitions only surface in human mode; json/url-pin
|
|
308
|
+
// modes stay silent after the single payload line so agents parsing
|
|
309
|
+
// stdout get one JSON line and an exit code, nothing else.
|
|
280
310
|
const emitStatus = (text: string): void => {
|
|
281
311
|
if (outputMode === 'human') stdout.write(text);
|
|
282
312
|
};
|
|
@@ -399,14 +429,19 @@ export function registerPairCli(
|
|
|
399
429
|
'Pair a remote browser device to this gateway (mode = generate | import; default generate)',
|
|
400
430
|
)
|
|
401
431
|
.option('--json', 'Emit a single JSON payload (url/pin/sid/qr_ascii) instead of the human-readable banner. Enables agent-driven pairing.')
|
|
432
|
+
.option('--url-pin-only', 'Emit ONLY {v,url,pin,expires_at_ms} — no QR ASCII, no SID, no mode echo. Headless fallback for container-based agents where the totalreclaw_pair tool is not injected (issue #87). Zero phrase exposure on stdout.')
|
|
402
433
|
.option('--timeout <sec>', 'Session TTL in seconds (default: 900 = 15 min, matches pair-session-store default)')
|
|
403
434
|
.action(async (...args: unknown[]) => {
|
|
404
435
|
// commander passes: [modeArg, options, cmd]
|
|
405
436
|
const modeRaw = typeof args[0] === 'string' ? args[0] : undefined;
|
|
406
|
-
const opts = (args[1] ?? {}) as { json?: boolean; timeout?: string | number };
|
|
437
|
+
const opts = (args[1] ?? {}) as { json?: boolean; urlPinOnly?: boolean; timeout?: string | number };
|
|
407
438
|
const mode: PairCliMode =
|
|
408
439
|
modeRaw === 'import' || modeRaw === 'imp' ? 'import' : 'generate';
|
|
409
|
-
|
|
440
|
+
// --url-pin-only wins over --json when both are passed, since it is
|
|
441
|
+
// strictly the tighter surface (no QR, no SID). The flag is a subset.
|
|
442
|
+
const outputMode: PairCliOutputMode = opts.urlPinOnly
|
|
443
|
+
? 'url-pin'
|
|
444
|
+
: opts.json ? 'json' : 'human';
|
|
410
445
|
let ttlSeconds: number | undefined;
|
|
411
446
|
if (typeof opts.timeout === 'number' && Number.isFinite(opts.timeout)) {
|
|
412
447
|
ttlSeconds = opts.timeout;
|