@totalreclaw/totalreclaw 1.0.4 → 1.1.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 +48 -67
- 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 +84 -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 +2661 -0
- package/llm-client.ts +418 -0
- package/lsh.test.ts +463 -0
- package/lsh.ts +257 -0
- package/package.json +18 -33
- 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 +346 -0
- package/SKILL.md +0 -709
- package/dist/index.js +0 -32154
package/README.md
CHANGED
|
@@ -1,104 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @totalreclaw/totalreclaw
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Encrypted memory for your AI agent — zero-knowledge E2EE vault with automatic extraction, semantic search, and portable storage.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Built for [OpenClaw](https://openclaw.ai). Your memories are encrypted on your device before leaving — no one can read them, not even us.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **True zero-knowledge E2EE** -- Client-side AES-256-GCM encryption. The server only sees encrypted blobs.
|
|
10
|
-
- **Portable** -- One-click plaintext export. No vendor lock-in.
|
|
11
|
-
- **Automatic** -- Memory extraction and recall happen via lifecycle hooks. No manual commands needed.
|
|
12
|
-
- **Cross-device** -- Same 12-word recovery phrase restores all your memories on any device.
|
|
13
|
-
- **98.1% recall** -- Blind-index search with BM25 + cosine + RRF fusion reranking.
|
|
14
|
-
- **Free tier** -- 100 writes/month, unlimited reads. Upgrade at [totalreclaw.xyz/pricing](https://totalreclaw.xyz/pricing).
|
|
7
|
+
**[totalreclaw.xyz](https://totalreclaw.xyz)**
|
|
15
8
|
|
|
16
9
|
## Install
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
```
|
|
11
|
+
```bash
|
|
21
12
|
openclaw plugins install @totalreclaw/totalreclaw
|
|
22
13
|
```
|
|
23
14
|
|
|
24
|
-
Or just ask your
|
|
15
|
+
Or just ask your agent:
|
|
16
|
+
|
|
25
17
|
> "Install the totalreclaw plugin"
|
|
26
18
|
|
|
27
|
-
The
|
|
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.
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
## How It Works
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
After setup, memory is **fully automatic**:
|
|
32
24
|
|
|
33
|
-
|
|
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
|
|
34
28
|
|
|
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.
|
|
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.
|
|
39
30
|
|
|
40
31
|
## Tools
|
|
41
32
|
|
|
42
|
-
|
|
33
|
+
Your agent gets these tools automatically:
|
|
34
|
+
|
|
35
|
+
| Tool | Description |
|
|
43
36
|
|------|-------------|
|
|
44
|
-
| `totalreclaw_remember` |
|
|
45
|
-
| `totalreclaw_recall` | Search
|
|
46
|
-
| `totalreclaw_forget` | Delete a specific memory
|
|
37
|
+
| `totalreclaw_remember` | Manually store a fact |
|
|
38
|
+
| `totalreclaw_recall` | Search memories by natural language |
|
|
39
|
+
| `totalreclaw_forget` | Delete a specific memory |
|
|
47
40
|
| `totalreclaw_export` | Export all memories as plaintext |
|
|
48
|
-
| `totalreclaw_status` | Check
|
|
49
|
-
| `totalreclaw_generate_recovery_phrase` | Generate a secure 12-word BIP-39 mnemonic (onboarding) |
|
|
50
|
-
|
|
51
|
-
## Lifecycle hooks
|
|
41
|
+
| `totalreclaw_status` | Check billing status and quota |
|
|
52
42
|
|
|
53
|
-
|
|
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 |
|
|
43
|
+
Most of the time you won't use these directly — the automatic hooks handle memory for you.
|
|
58
44
|
|
|
59
|
-
##
|
|
45
|
+
## Features
|
|
60
46
|
|
|
61
|
-
|
|
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
|
|
62
54
|
|
|
63
|
-
|
|
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.
|
|
55
|
+
## Free Tier & Pricing
|
|
66
56
|
|
|
67
|
-
|
|
57
|
+
| Tier | Writes | Reads | Price |
|
|
58
|
+
|------|--------|-------|-------|
|
|
59
|
+
| **Free** | 250/month | Unlimited | $0 |
|
|
60
|
+
| **Pro** | 10,000/month | Unlimited | $2-5/month |
|
|
68
61
|
|
|
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
|
|
62
|
+
Pay with card (Stripe) or crypto (Coinbase Commerce). Counter resets monthly.
|
|
74
63
|
|
|
75
64
|
## Configuration
|
|
76
65
|
|
|
77
|
-
|
|
78
|
-
|---------------------|---------|-------------|
|
|
79
|
-
| `TOTALRECLAW_MASTER_PASSWORD` | *(required)* | 12-word BIP-39 recovery phrase |
|
|
80
|
-
| `TOTALRECLAW_SERVER_URL` | `https://api.totalreclaw.xyz` | Relay server URL |
|
|
81
|
-
| `TOTALRECLAW_SUBGRAPH_MODE` | `true` | Enable on-chain storage |
|
|
82
|
-
| `TOTALRECLAW_EXTRACT_EVERY_TURNS` | `5` | Turns between automatic extractions |
|
|
66
|
+
Set these environment variables before the agent starts:
|
|
83
67
|
|
|
84
|
-
|
|
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) |
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
|---------|-------------|------|
|
|
88
|
-
| Encryption | Client-side (zero-knowledge) | Server-side (server can read) |
|
|
89
|
-
| Data portability | One-click plaintext export | No export |
|
|
90
|
-
| Key management | BIP-39 seed phrase (user-controlled) | Server-managed keys |
|
|
91
|
-
| Search method | Blind-index + encrypted reranking | Plaintext vector search |
|
|
92
|
-
| On-chain storage | Yes (Gnosis Chain subgraph) | No |
|
|
93
|
-
| Cross-device | Same seed = same memories | Tied to account |
|
|
75
|
+
## Using with Other Agents
|
|
94
76
|
|
|
95
|
-
|
|
77
|
+
TotalReclaw also works outside OpenClaw:
|
|
96
78
|
|
|
97
|
-
|
|
79
|
+
- **Claude Desktop / Cursor / Windsurf** — Use [@totalreclaw/mcp-server](https://www.npmjs.com/package/@totalreclaw/mcp-server)
|
|
80
|
+
- **NanoClaw** — Lightweight skill with MCP bridge
|
|
98
81
|
|
|
99
|
-
|
|
100
|
-
- [Documentation](https://github.com/p-diogo/totalreclaw)
|
|
101
|
-
- [Pricing](https://totalreclaw.xyz/pricing)
|
|
82
|
+
Same encryption, same recovery phrase, same memories across all agents.
|
|
102
83
|
|
|
103
84
|
## License
|
|
104
85
|
|
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
|
+
}
|