@totalreclaw/totalreclaw 1.1.0 → 1.2.0

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/README.md CHANGED
@@ -1,32 +1,52 @@
1
- # @totalreclaw/totalreclaw
1
+ <p align="center">
2
+ <img src="../../docs/assets/logo.png" alt="TotalReclaw" width="80" />
3
+ </p>
2
4
 
3
- Encrypted memory for your AI agent — zero-knowledge E2EE vault with automatic extraction, semantic search, and portable storage.
5
+ <h1 align="center">@totalreclaw/totalreclaw</h1>
4
6
 
5
- Built for [OpenClaw](https://openclaw.ai). Your memories are encrypted on your device before leaving — no one can read them, not even us.
7
+ <p align="center">
8
+ <strong>End-to-end encrypted memory for OpenClaw -- fully automatic, yours forever</strong>
9
+ </p>
6
10
 
7
- **[totalreclaw.xyz](https://totalreclaw.xyz)**
11
+ <p align="center">
12
+ <a href="https://totalreclaw.xyz">Website</a> &middot;
13
+ <a href="https://www.npmjs.com/package/@totalreclaw/totalreclaw">npm</a> &middot;
14
+ <a href="../../docs/guides/beta-tester-guide.md">Getting Started</a>
15
+ </p>
16
+
17
+ <p align="center">
18
+ <a href="https://www.npmjs.com/package/@totalreclaw/totalreclaw"><img src="https://img.shields.io/npm/v/@totalreclaw/totalreclaw?color=7B5CFF" alt="npm version"></a>
19
+ <a href="https://www.npmjs.com/package/@totalreclaw/totalreclaw"><img src="https://img.shields.io/npm/dm/@totalreclaw/totalreclaw" alt="npm downloads"></a>
20
+ <a href="../../LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
21
+ </p>
22
+
23
+ ---
24
+
25
+ Your AI agent remembers everything -- preferences, decisions, facts -- encrypted so only you can read it. Built for [OpenClaw](https://openclaw.ai) with fully automatic memory extraction and recall.
8
26
 
9
27
  ## Install
10
28
 
29
+ Ask your OpenClaw agent:
30
+
31
+ > "Install the @totalreclaw/totalreclaw plugin"
32
+
33
+ Or from the terminal:
34
+
11
35
  ```bash
12
36
  openclaw plugins install @totalreclaw/totalreclaw
13
37
  ```
14
38
 
15
- Or just ask your agent:
16
-
17
- > "Install the totalreclaw plugin"
18
-
19
- The agent handles setup: generates your encryption keys and registers you. You'll be asked to write down a 12-word recovery phrase — that's the only thing you need to keep safe.
39
+ The agent handles setup: generates your encryption keys, asks you to save a 12-word recovery phrase, and registers you. After that, memory is fully automatic.
20
40
 
21
41
  ## How It Works
22
42
 
23
- After setup, memory is **fully automatic**:
43
+ After setup, everything happens in the background:
24
44
 
25
- - **Start of conversation** loads relevant memories from your vault
26
- - **End of conversation** extracts and encrypts new facts before storing them
27
- - **Before context compaction** saves everything important before the context window is trimmed
45
+ - **Start of conversation** -- loads relevant memories from your encrypted vault
46
+ - **During conversation** -- extracts facts, preferences, and decisions automatically
47
+ - **Before context compaction** -- saves important context before the window is trimmed
28
48
 
29
- All encryption happens client-side using AES-256-GCM. Search uses blind indices (SHA-256 hashes) — the server never sees your queries or data. Your 12-word recovery phrase derives all keys via Argon2id + HKDF.
49
+ All encryption happens client-side using AES-256-GCM. The server never sees your plaintext data.
30
50
 
31
51
  ## Tools
32
52
 
@@ -39,48 +59,45 @@ Your agent gets these tools automatically:
39
59
  | `totalreclaw_forget` | Delete a specific memory |
40
60
  | `totalreclaw_export` | Export all memories as plaintext |
41
61
  | `totalreclaw_status` | Check billing status and quota |
62
+ | `totalreclaw_consolidate` | Merge duplicate memories |
63
+ | `totalreclaw_import_from` | Import from Mem0 or MCP Memory Server |
42
64
 
43
- Most of the time you won't use these directly the automatic hooks handle memory for you.
65
+ Most of the time you won't use these directly -- the automatic hooks handle memory for you.
44
66
 
45
67
  ## Features
46
68
 
47
- - **Zero-knowledge E2EE** AES-256-GCM encryption, blind index search, HKDF auth
48
- - **Semantic search** Local embeddings (bge-small-en-v1.5) + BM25 + cosine reranking with RRF
49
- - **Automatic extraction** LLM extracts facts from conversations, no manual input needed
50
- - **Dedup** Cosine similarity catches paraphrases; LLM-guided dedup catches contradictions (Pro)
51
- - **On-chain storage** Encrypted data stored on Gnosis Chain, indexed by The Graph
52
- - **Portable** One 12-word phrase. Any device, same memories, no lock-in
53
- - **Import** Migrate from Mem0 or MCP Memory Server
69
+ - **End-to-end encrypted** -- AES-256-GCM encryption, blind index search, HKDF auth
70
+ - **Automatic extraction** -- LLM extracts facts from conversations, no manual input needed
71
+ - **Semantic search** -- Local embeddings + BM25 + cosine reranking with RRF fusion
72
+ - **Smart dedup** -- Cosine similarity catches paraphrases; LLM-guided dedup catches contradictions (Pro)
73
+ - **On-chain storage** -- Encrypted data stored on Gnosis Chain, indexed by The Graph
74
+ - **Portable** -- One 12-word phrase. Any device, same memories, no lock-in
75
+ - **Import** -- Migrate from Mem0 or MCP Memory Server
54
76
 
55
77
  ## Free Tier & Pricing
56
78
 
57
- | Tier | Writes | Reads | Price |
58
- |------|--------|-------|-------|
59
- | **Free** | 250/month | Unlimited | $0 |
60
- | **Pro** | 10,000/month | Unlimited | $2-5/month |
61
-
62
- Pay with card (Stripe) or crypto (Coinbase Commerce). Counter resets monthly.
63
-
64
- ## Configuration
79
+ | Tier | Memories | Reads | Storage | Price |
80
+ |------|----------|-------|---------|-------|
81
+ | **Free** | 500/month | Unlimited | Testnet (trial) | $0 |
82
+ | **Pro** | Unlimited | Unlimited | Permanent on-chain (Gnosis) | $5/month |
65
83
 
66
- Set these environment variables before the agent starts:
67
-
68
- | Variable | Description | Default |
69
- |----------|-------------|---------|
70
- | `TOTALRECLAW_SERVER_URL` | Server URL | `https://api.totalreclaw.xyz` |
71
- | `TOTALRECLAW_CREDENTIALS_PATH` | Path to credentials file | `~/.totalreclaw/credentials.json` |
72
- | `TOTALRECLAW_SELF_HOSTED` | Set to `true` to use your own self-hosted server instead of the managed service | `false` (managed service) |
73
- | `TOTALRECLAW_EXTRACT_EVERY_TURNS` | Auto-extract interval (turns) | `5` (Free) / `2` (Pro min) |
84
+ Pay with card via Stripe. Counter resets monthly.
74
85
 
75
86
  ## Using with Other Agents
76
87
 
77
88
  TotalReclaw also works outside OpenClaw:
78
89
 
79
- - **Claude Desktop / Cursor / Windsurf** Use [@totalreclaw/mcp-server](https://www.npmjs.com/package/@totalreclaw/mcp-server)
80
- - **NanoClaw** Lightweight skill with MCP bridge
90
+ - **Claude Desktop / Cursor / Windsurf** -- Use [@totalreclaw/mcp-server](https://www.npmjs.com/package/@totalreclaw/mcp-server)
91
+ - **NanoClaw** -- Built-in support via MCP bridge
81
92
 
82
93
  Same encryption, same recovery phrase, same memories across all agents.
83
94
 
95
+ ## Learn More
96
+
97
+ - [Getting Started Guide](../../docs/guides/beta-tester-guide.md)
98
+ - [totalreclaw.xyz](https://totalreclaw.xyz)
99
+ - [Main Repository](https://github.com/p-diogo/totalreclaw)
100
+
84
101
  ## License
85
102
 
86
103
  MIT
package/embedding.ts CHANGED
@@ -1,73 +1,64 @@
1
1
  /**
2
2
  * TotalReclaw Plugin - Local Embedding via @huggingface/transformers
3
3
  *
4
- * Uses the Xenova/bge-small-en-v1.5 ONNX model to generate 384-dimensional
4
+ * Uses the Qwen3-Embedding-0.6B ONNX model to generate 1024-dimensional
5
5
  * text embeddings locally. No API key needed, no data leaves the machine.
6
+ * Supports 100+ languages (EN, PT, ES, ZH, etc.).
6
7
  *
7
- * This preserves the zero-knowledge guarantee: embeddings are generated
8
+ * This preserves the E2EE guarantee: embeddings are generated
8
9
  * CLIENT-SIDE before encryption, so no plaintext ever reaches an external API.
9
10
  *
10
11
  * Model details:
11
- * - Quantized (int8) ONNX model: ~33.8MB download on first use
12
+ * - Quantized (int8) ONNX model: ~600MB download on first use
12
13
  * - Cached in ~/.cache/huggingface/ after first download
13
- * - Lazy initialization: first call ~2-3s (model load), subsequent ~15ms
14
- * - Output: 384-dimensional normalized embedding vector
15
- * - For retrieval, queries should be prefixed with an instruction string
16
- * (documents/passages should NOT be prefixed)
14
+ * - Lazy initialization: first call ~3-5s (model load), subsequent ~100ms
15
+ * - Output: 1024-dimensional normalized embedding vector
16
+ * - No instruction prefix needed (bare queries perform better)
17
17
  *
18
- * Dependencies: @huggingface/transformers (handles model download, WordPiece
19
- * tokenization, ONNX inference, mean pooling, and normalization).
18
+ * Dependencies: @huggingface/transformers (handles model download,
19
+ * tokenization, ONNX inference, last-token pooling, and normalization).
20
20
  */
21
21
 
22
22
  // @ts-ignore - @huggingface/transformers types may not be perfect
23
23
  import { pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
24
24
 
25
- /** ONNX-optimized bge-small-en-v1.5 from HuggingFace Hub. */
26
- const MODEL_ID = 'Xenova/bge-small-en-v1.5';
25
+ /** ONNX-optimized Qwen3-Embedding-0.6B from HuggingFace Hub. */
26
+ const MODEL_ID = 'onnx-community/Qwen3-Embedding-0.6B-ONNX';
27
27
 
28
- /** Fixed output dimensionality for bge-small-en-v1.5. */
29
- const EMBEDDING_DIM = 384;
30
-
31
- /**
32
- * Query instruction prefix for bge-small-en-v1.5 retrieval tasks.
33
- *
34
- * Per the BAAI model card: prepend this to short queries when searching
35
- * for relevant passages. Do NOT prepend for documents/passages being stored.
36
- */
37
- const QUERY_PREFIX = 'Represent this sentence for searching relevant passages: ';
28
+ /** Fixed output dimensionality for Qwen3-Embedding-0.6B. */
29
+ const EMBEDDING_DIM = 1024;
38
30
 
39
31
  /** Lazily initialized feature extraction pipeline. */
40
32
  let extractor: FeatureExtractionPipeline | null = null;
41
33
 
42
34
  /**
43
- * Generate a 384-dimensional embedding vector for the given text.
35
+ * Generate a 1024-dimensional embedding vector for the given text.
44
36
  *
45
- * On first call, downloads and loads the ONNX model (~33.8MB, cached).
46
- * Subsequent calls reuse the loaded model and run in ~15ms.
37
+ * On first call, downloads and loads the ONNX model (~600MB, cached).
38
+ * Subsequent calls reuse the loaded model and run in ~100ms.
47
39
  *
48
- * For bge-small-en-v1.5, queries should set `isQuery: true` to prepend the
49
- * retrieval instruction prefix. Documents being stored should use the default
50
- * (`isQuery: false`) so no prefix is added.
40
+ * The isQuery option is accepted for forward compatibility but does not
41
+ * change behavior -- Qwen3 performs better without instruction prefixes.
51
42
  *
52
43
  * @param text - The text to embed.
53
44
  * @param options - Optional settings.
54
- * @param options.isQuery - If true, prepend the BGE query instruction prefix
55
- * for improved retrieval accuracy (default: false).
56
- * @returns 384-dimensional normalized embedding as a number array.
45
+ * @param options.isQuery - Accepted for forward compatibility (no-op).
46
+ * @returns 1024-dimensional normalized embedding as a number array.
57
47
  */
58
48
  export async function generateEmbedding(
59
49
  text: string,
60
50
  options?: { isQuery?: boolean },
61
51
  ): Promise<number[]> {
62
52
  if (!extractor) {
53
+ console.log('Downloading embedding model (one-time setup, ~600MB)...');
63
54
  extractor = await pipeline('feature-extraction', MODEL_ID, {
64
- // Use quantized (int8) model for smaller download (~33.8MB vs ~67MB)
65
55
  quantized: true,
66
56
  });
57
+ console.log('Embedding model ready.');
67
58
  }
68
59
 
69
- const input = options?.isQuery ? QUERY_PREFIX + text : text;
70
- const output = await extractor(input, { pooling: 'mean', normalize: true });
60
+ const input = text;
61
+ const output = await extractor(input, { pooling: 'last_token', normalize: true });
71
62
  // output.data is a Float32Array; convert to plain number[]
72
63
  return Array.from(output.data as Float32Array);
73
64
  }
@@ -75,7 +66,7 @@ export async function generateEmbedding(
75
66
  /**
76
67
  * Get the embedding vector dimensionality.
77
68
  *
78
- * Always returns 384 (fixed for bge-small-en-v1.5).
69
+ * Always returns 1024 (fixed for Qwen3-Embedding-0.6B).
79
70
  * This is needed by downstream code (e.g. LSH hasher) to know the vector
80
71
  * size without calling the embedding model.
81
72
  */
package/index.ts CHANGED
@@ -126,7 +126,10 @@ const SEMANTIC_SKIP_THRESHOLD = parseFloat(process.env.TOTALRECLAW_SEMANTIC_SKIP
126
126
 
127
127
  // Auto-extract throttle (C3): only extract every N turns in agent_end hook
128
128
  let turnsSinceLastExtraction = 0;
129
- const AUTO_EXTRACT_EVERY_TURNS_ENV = parseInt(process.env.TOTALRECLAW_EXTRACT_EVERY_TURNS ?? '5', 10);
129
+ const AUTO_EXTRACT_EVERY_TURNS_ENV = parseInt(process.env.TOTALRECLAW_EXTRACT_EVERY_TURNS ?? '3', 10);
130
+
131
+ // Hard cap on facts per extraction to prevent LLM over-extraction from dense conversations
132
+ const MAX_FACTS_PER_EXTRACTION = 15;
130
133
 
131
134
  // Store-time near-duplicate detection (consolidation module)
132
135
  const STORE_DEDUP_ENABLED = process.env.TOTALRECLAW_STORE_DEDUP !== 'false';
@@ -188,13 +191,11 @@ function isLlmDedupEnabled(): boolean {
188
191
  }
189
192
 
190
193
  /**
191
- * Get the effective extraction interval based on tier.
192
- * Pro users can set interval as low as 2 via env; Free users are clamped to minimum 5.
194
+ * Get the effective extraction interval.
195
+ * Unified to 3 turns for all tiers (quota is per-transaction, not per-memory).
193
196
  */
194
197
  function getExtractInterval(): number {
195
- const cache = readBillingCache();
196
- const minInterval = cache?.features?.min_extract_interval ?? 5;
197
- return Math.max(AUTO_EXTRACT_EVERY_TURNS_ENV, minInterval);
198
+ return AUTO_EXTRACT_EVERY_TURNS_ENV;
198
199
  }
199
200
 
200
201
  /**
@@ -517,7 +518,7 @@ async function generateEmbeddingAndLSH(
517
518
  const hasher = getLSHHasher(logger);
518
519
  const lshBuckets = hasher ? hasher.hash(embedding) : [];
519
520
 
520
- // Encrypt the embedding (JSON array of numbers) for zero-knowledge storage
521
+ // Encrypt the embedding (JSON array of numbers) for server-blind storage
521
522
  const encryptedEmbedding = encryptToHex(JSON.stringify(embedding), encryptionKey!);
522
523
 
523
524
  return { embedding, lshBuckets, encryptedEmbedding };
@@ -1177,7 +1178,7 @@ async function handlePluginImportFrom(
1177
1178
  const plugin = {
1178
1179
  id: 'totalreclaw',
1179
1180
  name: 'TotalReclaw',
1180
- description: 'Zero-knowledge encrypted memory vault for AI agents',
1181
+ description: 'End-to-end encrypted memory vault for AI agents',
1181
1182
  kind: 'memory' as const,
1182
1183
  configSchema: {
1183
1184
  type: 'object',
@@ -2548,7 +2549,13 @@ const plugin = {
2548
2549
  ? await fetchExistingMemoriesForExtraction(api.logger, 20, evt.messages)
2549
2550
  : [];
2550
2551
  const rawFacts = await extractFacts(evt.messages, 'turn', existingMemories);
2551
- const { kept: facts } = filterByImportance(rawFacts, api.logger);
2552
+ const { kept: importanceFiltered } = filterByImportance(rawFacts, api.logger);
2553
+ if (importanceFiltered.length > MAX_FACTS_PER_EXTRACTION) {
2554
+ api.logger.info(
2555
+ `Capped extraction from ${importanceFiltered.length} to ${MAX_FACTS_PER_EXTRACTION} facts`,
2556
+ );
2557
+ }
2558
+ const facts = importanceFiltered.slice(0, MAX_FACTS_PER_EXTRACTION);
2552
2559
  if (facts.length > 0) {
2553
2560
  await storeExtractedFacts(facts, api.logger);
2554
2561
  }
@@ -2584,7 +2591,13 @@ const plugin = {
2584
2591
  ? await fetchExistingMemoriesForExtraction(api.logger, 50, evt.messages)
2585
2592
  : [];
2586
2593
  const rawCompactFacts = await extractFacts(evt.messages, 'full', existingMemories);
2587
- const { kept: facts } = filterByImportance(rawCompactFacts, api.logger);
2594
+ const { kept: compactImportanceFiltered } = filterByImportance(rawCompactFacts, api.logger);
2595
+ if (compactImportanceFiltered.length > MAX_FACTS_PER_EXTRACTION) {
2596
+ api.logger.info(
2597
+ `Capped compaction extraction from ${compactImportanceFiltered.length} to ${MAX_FACTS_PER_EXTRACTION} facts`,
2598
+ );
2599
+ }
2600
+ const facts = compactImportanceFiltered.slice(0, MAX_FACTS_PER_EXTRACTION);
2588
2601
  if (facts.length > 0) {
2589
2602
  await storeExtractedFacts(facts, api.logger);
2590
2603
  }
@@ -2619,7 +2632,13 @@ const plugin = {
2619
2632
  ? await fetchExistingMemoriesForExtraction(api.logger, 50, evt.messages)
2620
2633
  : [];
2621
2634
  const rawResetFacts = await extractFacts(evt.messages, 'full', existingMemories);
2622
- const { kept: facts } = filterByImportance(rawResetFacts, api.logger);
2635
+ const { kept: resetImportanceFiltered } = filterByImportance(rawResetFacts, api.logger);
2636
+ if (resetImportanceFiltered.length > MAX_FACTS_PER_EXTRACTION) {
2637
+ api.logger.info(
2638
+ `Capped reset extraction from ${resetImportanceFiltered.length} to ${MAX_FACTS_PER_EXTRACTION} facts`,
2639
+ );
2640
+ }
2641
+ const facts = resetImportanceFiltered.slice(0, MAX_FACTS_PER_EXTRACTION);
2623
2642
  if (facts.length > 0) {
2624
2643
  await storeExtractedFacts(facts, api.logger);
2625
2644
  }
package/lsh.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * TotalReclaw Plugin - LSH Hasher (Locality-Sensitive Hashing)
3
3
  *
4
- * Pure TypeScript implementation of Random Hyperplane LSH for zero-knowledge
4
+ * Pure TypeScript implementation of Random Hyperplane LSH for server-blind
5
5
  * semantic search. Generates deterministic hyperplane matrices from a seed
6
6
  * derived from the user's master key, so the same embedding always hashes to
7
7
  * the same buckets across sessions.
@@ -2,7 +2,7 @@
2
2
  "id": "totalreclaw",
3
3
  "name": "TotalReclaw",
4
4
  "kind": "memory",
5
- "description": "Zero-knowledge encrypted memory vault for AI agents",
5
+ "description": "End-to-end encrypted memory vault for AI agents",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "1.1.0",
4
- "description": "Encrypted memory for your AI agentzero-knowledge E2EE vault with automatic extraction, semantic search, and on-chain storage",
3
+ "version": "1.2.0",
4
+ "description": "End-to-end encrypted memory for AI agentsportable, yours forever. Automatic extraction, semantic search, and on-chain storage",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "totalreclaw",
8
8
  "openclaw",
9
9
  "ai-memory",
10
10
  "ai-agent",
11
- "zero-knowledge",
11
+ "e2e-encryption",
12
12
  "encryption",
13
13
  "e2ee",
14
14
  "lsh",
package/subgraph-store.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  import { createPublicClient, http, type Hex, type Address, type Chain } from 'viem';
14
14
  import { entryPoint07Address } from 'viem/account-abstraction';
15
15
  import { mnemonicToAccount } from 'viem/accounts';
16
- import { gnosis, gnosisChiado } from 'viem/chains';
16
+ import { gnosis, gnosisChiado, baseSepolia } from 'viem/chains';
17
17
  import { createSmartAccountClient } from 'permissionless';
18
18
  import { toSimpleSmartAccount } from 'permissionless/accounts';
19
19
  import { createPimlicoClient } from 'permissionless/clients/pimlico';
@@ -32,7 +32,7 @@ export interface SubgraphStoreConfig {
32
32
  relayUrl: string; // TotalReclaw relay server URL (proxies bundler + subgraph)
33
33
  mnemonic: string; // BIP-39 mnemonic for key derivation
34
34
  cachePath: string; // Hot cache file path
35
- chainId: number; // 100 for Gnosis mainnet, 10200 for Chiado testnet
35
+ chainId: number; // 100 for Gnosis mainnet, 10200 for Chiado testnet, 84532 for Base Sepolia
36
36
  dataEdgeAddress: string; // EventfulDataEdge contract address
37
37
  entryPointAddress: string; // ERC-4337 EntryPoint v0.7
38
38
  authKeyHex?: string; // HKDF auth key for relay server Authorization header
@@ -151,8 +151,10 @@ function getChainFromId(chainId: number): Chain {
151
151
  return gnosis;
152
152
  case 10200:
153
153
  return gnosisChiado;
154
+ case 84532:
155
+ return baseSepolia;
154
156
  default:
155
- return gnosisChiado;
157
+ return gnosis;
156
158
  }
157
159
  }
158
160
 
@@ -311,7 +313,7 @@ export function isSubgraphMode(): boolean {
311
313
  * This is the on-chain owner identity used in the subgraph.
312
314
  */
313
315
  export async function deriveSmartAccountAddress(mnemonic: string, chainId?: number): Promise<string> {
314
- const chain: Chain = (chainId ?? 100) === 100 ? gnosis : gnosisChiado;
316
+ const chain: Chain = getChainFromId(chainId ?? 100);
315
317
  const ownerAccount = mnemonicToAccount(mnemonic);
316
318
  const entryPointAddr = (process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || DEFAULT_ENTRYPOINT_ADDRESS) as Address;
317
319
  const rpcUrl = process.env.TOTALRECLAW_RPC_URL;