@totalreclaw/totalreclaw 1.0.5 → 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 +67 -87
- package/api-client.ts +328 -0
- package/consolidation.test.ts +356 -0
- package/consolidation.ts +227 -0
- package/crypto.ts +351 -0
- package/embedding.ts +75 -0
- package/extractor-dedup.test.ts +168 -0
- package/extractor.ts +237 -0
- package/generate-mnemonic.ts +14 -0
- package/hot-cache-wrapper.ts +126 -0
- package/import-adapters/base-adapter.ts +93 -0
- package/import-adapters/import-adapters.test.ts +595 -0
- package/import-adapters/index.ts +22 -0
- package/import-adapters/mcp-memory-adapter.ts +274 -0
- package/import-adapters/mem0-adapter.ts +233 -0
- package/import-adapters/types.ts +89 -0
- package/index.ts +2680 -0
- package/llm-client.ts +418 -0
- package/lsh.test.ts +463 -0
- package/lsh.ts +257 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +19 -34
- package/pocv2-e2e-test.ts +917 -0
- package/reranker.test.ts +594 -0
- package/reranker.ts +537 -0
- package/semantic-dedup.test.ts +392 -0
- package/semantic-dedup.ts +100 -0
- package/setup.sh +19 -0
- package/store-dedup-wiring.test.ts +186 -0
- package/subgraph-search.ts +282 -0
- package/subgraph-store.ts +348 -0
- package/SKILL.md +0 -709
- package/dist/index.js +0 -32154
package/README.md
CHANGED
|
@@ -1,122 +1,102 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../../docs/assets/logo.png" alt="TotalReclaw" width="80" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">@totalreclaw/totalreclaw</h1>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>End-to-end encrypted memory for OpenClaw -- fully automatic, yours forever</strong>
|
|
9
|
+
</p>
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://totalreclaw.xyz">Website</a> ·
|
|
13
|
+
<a href="https://www.npmjs.com/package/@totalreclaw/totalreclaw">npm</a> ·
|
|
14
|
+
<a href="../../docs/guides/beta-tester-guide.md">Getting Started</a>
|
|
15
|
+
</p>
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
15
26
|
|
|
16
27
|
## Install
|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
Ask your OpenClaw agent:
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
> "Install the @totalreclaw/totalreclaw plugin"
|
|
32
|
+
|
|
33
|
+
Or from the terminal:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
21
36
|
openclaw plugins install @totalreclaw/totalreclaw
|
|
22
37
|
```
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
> "Install the totalreclaw plugin"
|
|
26
|
-
|
|
27
|
-
The plugin sets itself up on first run -- it will ask if you have an existing recovery phrase or need a new one.
|
|
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.
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
## How It Works
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
After setup, everything happens in the background:
|
|
32
44
|
|
|
33
|
-
|
|
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
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
2. **Talk normally** -- TotalReclaw automatically extracts facts, preferences, and decisions from your conversations, encrypts them client-side, and stores them on-chain.
|
|
37
|
-
3. **Recall** -- At the start of each new conversation, relevant memories are decrypted and injected into context. You can also search explicitly with the `totalreclaw_recall` tool.
|
|
38
|
-
4. **Export anytime** -- `totalreclaw_export` dumps all your memories as plaintext JSON or Markdown. Your data, your format.
|
|
49
|
+
All encryption happens client-side using AES-256-GCM. The server never sees your plaintext data.
|
|
39
50
|
|
|
40
51
|
## Tools
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
Your agent gets these tools automatically:
|
|
54
|
+
|
|
55
|
+
| Tool | Description |
|
|
43
56
|
|------|-------------|
|
|
44
|
-
| `totalreclaw_remember` |
|
|
45
|
-
| `totalreclaw_recall` | Search
|
|
46
|
-
| `totalreclaw_forget` | Delete a specific memory
|
|
57
|
+
| `totalreclaw_remember` | Manually store a fact |
|
|
58
|
+
| `totalreclaw_recall` | Search memories by natural language |
|
|
59
|
+
| `totalreclaw_forget` | Delete a specific memory |
|
|
47
60
|
| `totalreclaw_export` | Export all memories as plaintext |
|
|
48
|
-
| `totalreclaw_status` | Check
|
|
49
|
-
| `
|
|
50
|
-
|
|
51
|
-
## Lifecycle hooks
|
|
52
|
-
|
|
53
|
-
| Hook | When | What |
|
|
54
|
-
|------|------|------|
|
|
55
|
-
| `before_agent_start` | Every conversation | Recalls relevant memories and injects them into context |
|
|
56
|
-
| `agent_end` | After each turn | Extracts new facts from the conversation |
|
|
57
|
-
| `pre_compaction` | Before context compaction | Full memory extraction to prevent data loss |
|
|
58
|
-
|
|
59
|
-
## Recovery phrase
|
|
60
|
-
|
|
61
|
-
Your recovery phrase is a 12-word BIP-39 mnemonic -- like a crypto wallet seed. It derives all encryption keys locally. The server never sees it.
|
|
62
|
-
|
|
63
|
-
- **New user**: The plugin generates a random phrase and displays it once. Save it somewhere safe.
|
|
64
|
-
- **Returning user**: Enter your existing phrase to restore all your memories on a new device.
|
|
65
|
-
- **Lost phrase**: Memories cannot be recovered. This is the zero-knowledge guarantee.
|
|
66
|
-
|
|
67
|
-
## Privacy and security
|
|
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 |
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
- The server stores only encrypted blobs and blind indices
|
|
71
|
-
- On-chain storage via Gnosis Chain (The Graph subgraph) -- fully auditable
|
|
72
|
-
- Master password never leaves your device
|
|
73
|
-
- One-click plaintext export -- no vendor lock-in
|
|
65
|
+
Most of the time you won't use these directly -- the automatic hooks handle memory for you.
|
|
74
66
|
|
|
75
|
-
##
|
|
67
|
+
## Features
|
|
76
68
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
83
76
|
|
|
84
|
-
##
|
|
77
|
+
## Free Tier & Pricing
|
|
85
78
|
|
|
86
|
-
|
|
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 |
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
|---|---|---|---|---|---|---|
|
|
90
|
-
| **Server sees plaintext** | Never | Yes | Yes | Yes | N/A (local) | Yes |
|
|
91
|
-
| **Client-side E2EE** | AES-256-GCM | No | No | No | No | No |
|
|
92
|
-
| **Cross-device sync** | Seed phrase | Account | Account | Account | No | Account |
|
|
93
|
-
| **Data export** | One-click plaintext | JSON (7-day link) | No | Via API | Copy file | No |
|
|
94
|
-
| **On-chain storage** | Gnosis Chain | No | No | No | No | No |
|
|
95
|
-
| **Self-hostable** | Yes | Yes | No | Yes | Yes (local) | Yes |
|
|
96
|
-
| **OpenClaw plugin** | Yes | Yes | No | No | No | Yes |
|
|
97
|
-
| **MCP server** | Yes | Yes | No | No | Yes | No |
|
|
98
|
-
| **Knowledge graph** | No | Yes ($249/mo) | Yes | Yes | Simple | No |
|
|
99
|
-
| **Free tier** | 100 writes/mo | 10K memories | 1K credits | 3 agents | Unlimited | Varies |
|
|
84
|
+
Pay with card via Stripe. Counter resets monthly.
|
|
100
85
|
|
|
101
|
-
|
|
86
|
+
## Using with Other Agents
|
|
102
87
|
|
|
103
|
-
|
|
104
|
-
- **Seed-phrase portability** -- One 12-word phrase, any device, any agent. No accounts, no passwords, no vendor. Works like a crypto wallet.
|
|
105
|
-
- **On-chain anchoring** -- Memories are stored on Gnosis Chain and indexed by The Graph. No single server controls your data.
|
|
106
|
-
- **True data ownership** -- One-click plaintext export. No 7-day expiry links, no API-only access. Your data, your format.
|
|
88
|
+
TotalReclaw also works outside OpenClaw:
|
|
107
89
|
|
|
108
|
-
|
|
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
|
|
109
92
|
|
|
110
|
-
|
|
111
|
-
- **Ecosystem maturity** -- Mem0 has 49K GitHub stars, $24M in funding, and integrations with every major framework. TotalReclaw is a beta product.
|
|
112
|
-
- **Offline simplicity** -- The official MCP Memory Server and Engram need zero network, zero accounts, zero setup. Good enough for single-device use.
|
|
113
|
-
- **Enterprise compliance** -- Mem0 and Zep offer SOC 2, HIPAA, RBAC, SSO. TotalReclaw doesn't need most of these (zero-knowledge means there's nothing to comply about), but enterprises want the paperwork.
|
|
93
|
+
Same encryption, same recovery phrase, same memories across all agents.
|
|
114
94
|
|
|
115
|
-
##
|
|
95
|
+
## Learn More
|
|
116
96
|
|
|
117
|
-
- [
|
|
118
|
-
- [
|
|
119
|
-
- [
|
|
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)
|
|
120
100
|
|
|
121
101
|
## License
|
|
122
102
|
|
package/api-client.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TotalReclaw Plugin - HTTP API Client
|
|
3
|
+
*
|
|
4
|
+
* Communicates with the TotalReclaw server over JSON/HTTP. Uses Node.js
|
|
5
|
+
* built-in `fetch` (available since Node 18).
|
|
6
|
+
*
|
|
7
|
+
* All authenticated endpoints expect:
|
|
8
|
+
* Authorization: Bearer <hex-encoded-auth-key>
|
|
9
|
+
*
|
|
10
|
+
* The server hashes the auth key with SHA-256 to look up the user.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Request / Response Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A single fact payload for the `/v1/store` endpoint.
|
|
19
|
+
*
|
|
20
|
+
* Field naming matches the server's `FactJSON` Pydantic model in
|
|
21
|
+
* `server/src/handlers/store.py`.
|
|
22
|
+
*/
|
|
23
|
+
export interface StoreFactPayload {
|
|
24
|
+
/** UUIDv7 fact identifier */
|
|
25
|
+
id: string;
|
|
26
|
+
/** ISO 8601 timestamp */
|
|
27
|
+
timestamp: string;
|
|
28
|
+
/** Hex-encoded AES-256-GCM ciphertext (iv || tag || ciphertext) */
|
|
29
|
+
encrypted_blob: string;
|
|
30
|
+
/** SHA-256 hashes of tokens for blind search */
|
|
31
|
+
blind_indices: string[];
|
|
32
|
+
/** Importance / decay score (0-10) */
|
|
33
|
+
decay_score: number;
|
|
34
|
+
/** Origin label */
|
|
35
|
+
source: string;
|
|
36
|
+
/** HMAC-SHA256 content fingerprint for dedup (hex) */
|
|
37
|
+
content_fp?: string;
|
|
38
|
+
/** Identifier of the creating agent */
|
|
39
|
+
agent_id?: string;
|
|
40
|
+
/** Hex-encoded AES-256-GCM encrypted embedding vector (PoC v2) */
|
|
41
|
+
encrypted_embedding?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A search result candidate returned by `/v1/search`.
|
|
46
|
+
*
|
|
47
|
+
* Field naming matches the server's `SearchResultJSON` model.
|
|
48
|
+
*/
|
|
49
|
+
export interface SearchCandidate {
|
|
50
|
+
fact_id: string;
|
|
51
|
+
/** Hex-encoded AES-256-GCM ciphertext */
|
|
52
|
+
encrypted_blob: string;
|
|
53
|
+
decay_score: number;
|
|
54
|
+
/** Unix milliseconds */
|
|
55
|
+
timestamp: number;
|
|
56
|
+
version: number;
|
|
57
|
+
/** Hex-encoded AES-256-GCM encrypted embedding vector (PoC v2, optional) */
|
|
58
|
+
encrypted_embedding?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A fact object returned by `/v1/export`.
|
|
63
|
+
*/
|
|
64
|
+
export interface ExportedFact {
|
|
65
|
+
id: string;
|
|
66
|
+
encrypted_blob: string;
|
|
67
|
+
blind_indices: string[];
|
|
68
|
+
decay_score: number;
|
|
69
|
+
version: number;
|
|
70
|
+
source: string;
|
|
71
|
+
created_at: string;
|
|
72
|
+
updated_at: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// API Client Factory
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create an API client bound to a specific TotalReclaw server URL.
|
|
81
|
+
*
|
|
82
|
+
* All methods are async and throw descriptive errors on non-2xx responses.
|
|
83
|
+
*/
|
|
84
|
+
export function createApiClient(serverUrl: string) {
|
|
85
|
+
// Normalise URL -- strip trailing slash.
|
|
86
|
+
const baseUrl = serverUrl.replace(/\/+$/, '');
|
|
87
|
+
|
|
88
|
+
// ------------------------------------------------------------------
|
|
89
|
+
// Shared helpers
|
|
90
|
+
// ------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Throw a descriptive error when the server returns a non-2xx status.
|
|
94
|
+
*/
|
|
95
|
+
async function assertOk(res: Response, context: string): Promise<void> {
|
|
96
|
+
if (res.ok) return;
|
|
97
|
+
let body: string;
|
|
98
|
+
try {
|
|
99
|
+
body = await res.text();
|
|
100
|
+
} catch {
|
|
101
|
+
body = '(could not read response body)';
|
|
102
|
+
}
|
|
103
|
+
const hint = res.status === 401
|
|
104
|
+
? ' Authentication failed. If using a recovery phrase, check that all 12 words are in the correct order and spelled correctly.'
|
|
105
|
+
: '';
|
|
106
|
+
throw new Error(`${context}: HTTP ${res.status} - ${body}${hint}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ------------------------------------------------------------------
|
|
110
|
+
// Public methods
|
|
111
|
+
// ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
// ---- Registration (unauthenticated) ----
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Register a new user.
|
|
118
|
+
*
|
|
119
|
+
* @param authKeyHash Hex-encoded SHA-256 of the auth key (64 chars).
|
|
120
|
+
* @param saltHex Hex-encoded 32-byte salt (64 chars).
|
|
121
|
+
* @returns `{ user_id }` on success.
|
|
122
|
+
*/
|
|
123
|
+
async register(
|
|
124
|
+
authKeyHash: string,
|
|
125
|
+
saltHex: string,
|
|
126
|
+
): Promise<{ user_id: string }> {
|
|
127
|
+
const res = await fetch(`${baseUrl}/v1/register`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ auth_key_hash: authKeyHash, salt: saltHex }),
|
|
131
|
+
});
|
|
132
|
+
await assertOk(res, 'register');
|
|
133
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
134
|
+
if (!json.success && json.error_code !== 'USER_EXISTS') {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`register: server returned success=false - ${json.error_code}: ${json.error_message}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (!json.user_id) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`register: server did not return user_id (error_code=${json.error_code})`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return { user_id: json.user_id as string };
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// ---- Store (authenticated) ----
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Store one or more encrypted facts.
|
|
151
|
+
*
|
|
152
|
+
* @param userId The authenticated user's ID.
|
|
153
|
+
* @param facts Array of `StoreFactPayload` objects.
|
|
154
|
+
* @param authKeyHex Hex-encoded raw auth key (64 chars) for Bearer header.
|
|
155
|
+
*/
|
|
156
|
+
async store(
|
|
157
|
+
userId: string,
|
|
158
|
+
facts: StoreFactPayload[],
|
|
159
|
+
authKeyHex: string,
|
|
160
|
+
): Promise<{ ids: string[]; duplicate_ids?: string[] }> {
|
|
161
|
+
const res = await fetch(`${baseUrl}/v1/store`, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
Authorization: `Bearer ${authKeyHex}`,
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({ user_id: userId, facts }),
|
|
168
|
+
});
|
|
169
|
+
await assertOk(res, 'store');
|
|
170
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
171
|
+
if (!json.success) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`store: server returned success=false - ${json.error_code}: ${json.error_message}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
ids: (json.ids as string[]) ?? [],
|
|
178
|
+
duplicate_ids: json.duplicate_ids as string[] | undefined,
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// ---- Search (authenticated) ----
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Search for facts using blind trapdoors.
|
|
186
|
+
*
|
|
187
|
+
* @param userId The authenticated user's ID.
|
|
188
|
+
* @param trapdoors SHA-256 hex hashes of query tokens.
|
|
189
|
+
* @param maxCandidates Maximum candidates to retrieve.
|
|
190
|
+
* @param authKeyHex Hex-encoded raw auth key for Bearer header.
|
|
191
|
+
* @returns Array of encrypted search candidates.
|
|
192
|
+
*/
|
|
193
|
+
async search(
|
|
194
|
+
userId: string,
|
|
195
|
+
trapdoors: string[],
|
|
196
|
+
maxCandidates: number,
|
|
197
|
+
authKeyHex: string,
|
|
198
|
+
): Promise<SearchCandidate[]> {
|
|
199
|
+
const res = await fetch(`${baseUrl}/v1/search`, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: {
|
|
202
|
+
'Content-Type': 'application/json',
|
|
203
|
+
Authorization: `Bearer ${authKeyHex}`,
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
user_id: userId,
|
|
207
|
+
trapdoors,
|
|
208
|
+
max_candidates: maxCandidates,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
await assertOk(res, 'search');
|
|
212
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
213
|
+
if (!json.success) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`search: server returned success=false - ${json.error_code}: ${json.error_message}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return (json.results as SearchCandidate[]) ?? [];
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// ---- Delete (authenticated) ----
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Soft-delete a fact by ID.
|
|
225
|
+
*
|
|
226
|
+
* @param factId The fact UUID to delete.
|
|
227
|
+
* @param authKeyHex Hex-encoded raw auth key for Bearer header.
|
|
228
|
+
*/
|
|
229
|
+
async deleteFact(factId: string, authKeyHex: string): Promise<void> {
|
|
230
|
+
const res = await fetch(`${baseUrl}/v1/facts/${encodeURIComponent(factId)}`, {
|
|
231
|
+
method: 'DELETE',
|
|
232
|
+
headers: {
|
|
233
|
+
Authorization: `Bearer ${authKeyHex}`,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
await assertOk(res, 'deleteFact');
|
|
237
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
238
|
+
if (!json.success) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`deleteFact: server returned success=false - ${json.error_code}: ${json.error_message}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// ---- Batch Delete (authenticated) ----
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Batch soft-delete facts by ID list.
|
|
249
|
+
*
|
|
250
|
+
* @param factIds Array of fact UUIDs to delete (max 500).
|
|
251
|
+
* @param authKeyHex Hex-encoded raw auth key for Bearer header.
|
|
252
|
+
* @returns The number of facts that were actually deleted.
|
|
253
|
+
*/
|
|
254
|
+
async batchDelete(factIds: string[], authKeyHex: string): Promise<number> {
|
|
255
|
+
const res = await fetch(`${baseUrl}/v1/facts/batch-delete`, {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: {
|
|
258
|
+
'Content-Type': 'application/json',
|
|
259
|
+
Authorization: `Bearer ${authKeyHex}`,
|
|
260
|
+
},
|
|
261
|
+
body: JSON.stringify({ fact_ids: factIds }),
|
|
262
|
+
});
|
|
263
|
+
await assertOk(res, 'batchDelete');
|
|
264
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
265
|
+
if (!json.success) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`batchDelete: server returned success=false - ${json.error_code}: ${json.error_message}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return (json.deleted_count as number) ?? 0;
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// ---- Export (authenticated) ----
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Export all active facts (paginated).
|
|
277
|
+
*
|
|
278
|
+
* @param authKeyHex Hex-encoded raw auth key for Bearer header.
|
|
279
|
+
* @param limit Page size (default 1000, max 5000).
|
|
280
|
+
* @param cursor Cursor from previous page (omit for first page).
|
|
281
|
+
* @returns Page of facts with pagination metadata.
|
|
282
|
+
*/
|
|
283
|
+
async exportFacts(
|
|
284
|
+
authKeyHex: string,
|
|
285
|
+
limit: number = 1000,
|
|
286
|
+
cursor?: string,
|
|
287
|
+
): Promise<{ facts: ExportedFact[]; cursor?: string; has_more: boolean; total_count?: number }> {
|
|
288
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
289
|
+
if (cursor) params.set('cursor', cursor);
|
|
290
|
+
|
|
291
|
+
const res = await fetch(`${baseUrl}/v1/export?${params.toString()}`, {
|
|
292
|
+
method: 'GET',
|
|
293
|
+
headers: {
|
|
294
|
+
Authorization: `Bearer ${authKeyHex}`,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
await assertOk(res, 'exportFacts');
|
|
298
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
299
|
+
if (!json.success) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`exportFacts: server returned success=false - ${json.error_code}: ${json.error_message}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
facts: (json.facts as ExportedFact[]) ?? [],
|
|
306
|
+
cursor: json.cursor as string | undefined,
|
|
307
|
+
has_more: (json.has_more as boolean) ?? false,
|
|
308
|
+
total_count: json.total_count as number | undefined,
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
// ---- Health (unauthenticated) ----
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check server health.
|
|
316
|
+
*
|
|
317
|
+
* @returns `true` if the server responds with HTTP 200.
|
|
318
|
+
*/
|
|
319
|
+
async health(): Promise<boolean> {
|
|
320
|
+
try {
|
|
321
|
+
const res = await fetch(`${baseUrl}/health`, { method: 'GET' });
|
|
322
|
+
return res.status === 200;
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|