@totalreclaw/totalreclaw 3.3.1-rc.8 → 3.3.1
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 +268 -1
- package/SKILL.md +29 -23
- package/api-client.ts +18 -11
- package/claims-helper.ts +47 -1
- package/config.ts +108 -4
- package/confirm-indexed.ts +191 -0
- package/crypto.ts +10 -2
- package/dist/api-client.js +226 -0
- package/dist/billing-cache.js +100 -0
- package/dist/claims-helper.js +624 -0
- package/dist/config.js +297 -0
- package/dist/confirm-indexed.js +127 -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/embedder-cache.js +185 -0
- package/dist/embedder-loader.js +121 -0
- package/dist/embedder-network.js +301 -0
- package/dist/embedding.js +141 -0
- package/dist/extractor.js +1225 -0
- package/dist/first-run.js +103 -0
- package/dist/fs-helpers.js +725 -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 +5388 -0
- package/dist/llm-client.js +687 -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 +556 -0
- package/dist/qa-bug-report.js +301 -0
- package/dist/relay-headers.js +44 -0
- package/dist/reranker.js +409 -0
- package/dist/retype-setscope.js +368 -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/embedder-cache.ts +230 -0
- package/embedder-loader.ts +189 -0
- package/embedder-network.ts +350 -0
- package/embedding.ts +118 -27
- package/fs-helpers.ts +277 -0
- package/gateway-url.ts +57 -9
- package/index.ts +469 -250
- package/llm-client.ts +4 -3
- package/lsh.ts +7 -2
- package/onboarding-cli.ts +114 -1
- package/package.json +24 -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/pin.ts +31 -0
- package/qa-bug-report.ts +84 -2
- package/relay-headers.ts +50 -0
- package/reranker.ts +40 -0
- package/retype-setscope.ts +69 -8
- package/skill.json +1 -1
- package/subgraph-search.ts +4 -3
- package/subgraph-store.ts +15 -10
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subgraph search path — queries facts via GraphQL hash_in.
|
|
3
|
+
*
|
|
4
|
+
* Used when the managed service is active (TOTALRECLAW_SELF_HOSTED is not
|
|
5
|
+
* "true"). Replaces the HTTP POST to /v1/search with a GraphQL query to
|
|
6
|
+
* the subgraph via the relay server.
|
|
7
|
+
*
|
|
8
|
+
* The relay server proxies GraphQL queries to Graph Studio with its own
|
|
9
|
+
* API key at `${relayUrl}/v1/subgraph`. Clients never need a subgraph endpoint.
|
|
10
|
+
*
|
|
11
|
+
* Query cost optimization:
|
|
12
|
+
* Phase 1: Single query with ALL trapdoors (1 query).
|
|
13
|
+
* Phase 2: If saturated (1000 results), split into small parallel batches
|
|
14
|
+
* so rare trapdoor matches aren't drowned by common ones.
|
|
15
|
+
* Phase 3: Cursor-based pagination for any saturated batch.
|
|
16
|
+
*
|
|
17
|
+
* This minimizes Graph Network query costs (pay-per-query via GRT):
|
|
18
|
+
* - Small datasets (<1000 matches): 1 query total
|
|
19
|
+
* - Medium datasets: 1 + N batch queries
|
|
20
|
+
* - Large datasets: 1 + N batches + pagination queries
|
|
21
|
+
*/
|
|
22
|
+
import { getSubgraphConfig } from './subgraph-store.js';
|
|
23
|
+
import { CONFIG } from './config.js';
|
|
24
|
+
import { buildRelayHeaders } from './relay-headers.js';
|
|
25
|
+
/** Batch size for Phase 2 split queries. */
|
|
26
|
+
const TRAPDOOR_BATCH_SIZE = CONFIG.trapdoorBatchSize;
|
|
27
|
+
/** Graph Studio / Graph Network hard limit on `first` argument. */
|
|
28
|
+
const PAGE_SIZE = CONFIG.pageSize;
|
|
29
|
+
/**
|
|
30
|
+
* Execute a single GraphQL query against the subgraph endpoint.
|
|
31
|
+
* Returns null on any network or HTTP error (never throws).
|
|
32
|
+
*/
|
|
33
|
+
async function gqlQuery(endpoint, query, variables, authKeyHex) {
|
|
34
|
+
try {
|
|
35
|
+
const overrides = {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
};
|
|
38
|
+
if (authKeyHex) {
|
|
39
|
+
overrides['Authorization'] = `Bearer ${authKeyHex}`;
|
|
40
|
+
}
|
|
41
|
+
const headers = buildRelayHeaders(overrides);
|
|
42
|
+
const response = await fetch(endpoint, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers,
|
|
45
|
+
body: JSON.stringify({ query, variables }),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const body = await response.text().catch(() => '');
|
|
49
|
+
console.error(`[TotalReclaw] Subgraph query failed: HTTP ${response.status} — ${body.slice(0, 200)}`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const json = await response.json();
|
|
53
|
+
if (json.error || json.errors) {
|
|
54
|
+
console.error(`[TotalReclaw] Subgraph query error: ${json.error || json.errors?.map(e => e.message).join('; ')}`);
|
|
55
|
+
}
|
|
56
|
+
return json.data ?? null;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error(`[TotalReclaw] Subgraph query exception: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** GraphQL query for blind index lookup. */
|
|
64
|
+
const SEARCH_QUERY = `
|
|
65
|
+
query SearchByBlindIndex($trapdoors: [String!]!, $owner: Bytes!, $first: Int!) {
|
|
66
|
+
blindIndexes(
|
|
67
|
+
where: { hash_in: $trapdoors, owner: $owner, fact_: { isActive: true } }
|
|
68
|
+
first: $first
|
|
69
|
+
orderBy: id
|
|
70
|
+
orderDirection: desc
|
|
71
|
+
) {
|
|
72
|
+
id
|
|
73
|
+
fact {
|
|
74
|
+
id
|
|
75
|
+
encryptedBlob
|
|
76
|
+
encryptedEmbedding
|
|
77
|
+
decayScore
|
|
78
|
+
timestamp
|
|
79
|
+
createdAt
|
|
80
|
+
isActive
|
|
81
|
+
contentFp
|
|
82
|
+
sequenceId
|
|
83
|
+
version
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
/** Pagination query — cursor-based using id_gt, ascending for deterministic walk. */
|
|
89
|
+
const PAGINATE_QUERY = `
|
|
90
|
+
query PaginateBlindIndex($trapdoors: [String!]!, $owner: Bytes!, $first: Int!, $lastId: String!) {
|
|
91
|
+
blindIndexes(
|
|
92
|
+
where: { hash_in: $trapdoors, owner: $owner, id_gt: $lastId, fact_: { isActive: true } }
|
|
93
|
+
first: $first
|
|
94
|
+
orderBy: id
|
|
95
|
+
orderDirection: asc
|
|
96
|
+
) {
|
|
97
|
+
id
|
|
98
|
+
fact {
|
|
99
|
+
id
|
|
100
|
+
encryptedBlob
|
|
101
|
+
encryptedEmbedding
|
|
102
|
+
timestamp
|
|
103
|
+
createdAt
|
|
104
|
+
decayScore
|
|
105
|
+
isActive
|
|
106
|
+
contentFp
|
|
107
|
+
sequenceId
|
|
108
|
+
version
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
/** Collect facts from blind index entries, deduplicating by fact id. */
|
|
114
|
+
function collectFacts(entries, allResults) {
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (entry.fact && entry.fact.isActive !== false && !allResults.has(entry.fact.id)) {
|
|
117
|
+
allResults.set(entry.fact.id, entry.fact);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Paginate a single trapdoor chunk until exhausted or maxCandidates reached.
|
|
123
|
+
*/
|
|
124
|
+
async function paginateChunk(subgraphUrl, chunk, owner, allResults, maxCandidates, authKeyHex) {
|
|
125
|
+
let lastId = '';
|
|
126
|
+
while (allResults.size < maxCandidates) {
|
|
127
|
+
const data = await gqlQuery(subgraphUrl, PAGINATE_QUERY, { trapdoors: chunk, owner, first: PAGE_SIZE, lastId }, authKeyHex);
|
|
128
|
+
const entries = data?.blindIndexes ?? [];
|
|
129
|
+
if (entries.length === 0)
|
|
130
|
+
break;
|
|
131
|
+
collectFacts(entries, allResults);
|
|
132
|
+
if (entries.length < PAGE_SIZE)
|
|
133
|
+
break;
|
|
134
|
+
lastId = entries[entries.length - 1].id;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Search the subgraph for facts matching the given trapdoors.
|
|
139
|
+
*
|
|
140
|
+
* Adaptive strategy to minimize query costs:
|
|
141
|
+
*
|
|
142
|
+
* Phase 1: Single query with ALL trapdoors.
|
|
143
|
+
* - If not saturated (< PAGE_SIZE results): done. 1 query total.
|
|
144
|
+
* - If saturated: common trapdoors may be drowning rare ones. Go to Phase 2.
|
|
145
|
+
*
|
|
146
|
+
* Phase 2: Split trapdoors into small parallel batches (TRAPDOOR_BATCH_SIZE=5).
|
|
147
|
+
* - Each batch independently gets up to PAGE_SIZE results.
|
|
148
|
+
* - Rare trapdoor matches get their own budget.
|
|
149
|
+
*
|
|
150
|
+
* Phase 3: Cursor-based pagination for any saturated batch.
|
|
151
|
+
* - Only for power users with very large datasets.
|
|
152
|
+
*/
|
|
153
|
+
export async function searchSubgraph(owner, trapdoors, maxCandidates, authKeyHex) {
|
|
154
|
+
const config = getSubgraphConfig();
|
|
155
|
+
const subgraphUrl = `${config.relayUrl}/v1/subgraph`;
|
|
156
|
+
const allResults = new Map();
|
|
157
|
+
// -----------------------------------------------------------------------
|
|
158
|
+
// Phase 1: Single query with all trapdoors (1 query)
|
|
159
|
+
// -----------------------------------------------------------------------
|
|
160
|
+
const phase1 = await gqlQuery(subgraphUrl, SEARCH_QUERY, { trapdoors, owner, first: PAGE_SIZE }, authKeyHex);
|
|
161
|
+
const phase1Entries = phase1?.blindIndexes ?? [];
|
|
162
|
+
collectFacts(phase1Entries, allResults);
|
|
163
|
+
// Not saturated — we got everything in 1 query. Done.
|
|
164
|
+
if (phase1Entries.length < PAGE_SIZE) {
|
|
165
|
+
return Array.from(allResults.values());
|
|
166
|
+
}
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
// Phase 2: Saturated — split into small batches for better rare-word recall.
|
|
169
|
+
// Common trapdoors were drowning rare ones in the single-query result.
|
|
170
|
+
// -----------------------------------------------------------------------
|
|
171
|
+
const chunks = [];
|
|
172
|
+
for (let i = 0; i < trapdoors.length; i += TRAPDOOR_BATCH_SIZE) {
|
|
173
|
+
chunks.push(trapdoors.slice(i, i + TRAPDOOR_BATCH_SIZE));
|
|
174
|
+
}
|
|
175
|
+
const batchResults = await Promise.all(chunks.map(async (chunk) => {
|
|
176
|
+
const data = await gqlQuery(subgraphUrl, SEARCH_QUERY, { trapdoors: chunk, owner, first: PAGE_SIZE }, authKeyHex);
|
|
177
|
+
return { chunk, entries: data?.blindIndexes ?? [] };
|
|
178
|
+
}));
|
|
179
|
+
const saturatedChunks = [];
|
|
180
|
+
for (const { chunk, entries } of batchResults) {
|
|
181
|
+
collectFacts(entries, allResults);
|
|
182
|
+
if (entries.length >= PAGE_SIZE) {
|
|
183
|
+
saturatedChunks.push(chunk);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
// Phase 3: Cursor-based pagination for saturated batches (power users).
|
|
188
|
+
// -----------------------------------------------------------------------
|
|
189
|
+
for (const chunk of saturatedChunks) {
|
|
190
|
+
if (allResults.size >= maxCandidates)
|
|
191
|
+
break;
|
|
192
|
+
await paginateChunk(subgraphUrl, chunk, owner, allResults, maxCandidates, authKeyHex);
|
|
193
|
+
}
|
|
194
|
+
return Array.from(allResults.values());
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Broadened search: fetch recent active facts by owner without trapdoor filtering.
|
|
198
|
+
* Used as a fallback when trapdoor search returns 0 candidates (e.g., vague queries
|
|
199
|
+
* like "who am I?" where word trapdoors don't overlap with stored fact tokens).
|
|
200
|
+
*/
|
|
201
|
+
export async function searchSubgraphBroadened(owner, maxCandidates, authKeyHex) {
|
|
202
|
+
const config = getSubgraphConfig();
|
|
203
|
+
const subgraphUrl = `${config.relayUrl}/v1/subgraph`;
|
|
204
|
+
const query = `
|
|
205
|
+
query BroadenedSearch($owner: Bytes!, $first: Int!) {
|
|
206
|
+
facts(
|
|
207
|
+
where: { owner: $owner, isActive: true }
|
|
208
|
+
first: $first
|
|
209
|
+
orderBy: timestamp
|
|
210
|
+
orderDirection: desc
|
|
211
|
+
) {
|
|
212
|
+
id
|
|
213
|
+
encryptedBlob
|
|
214
|
+
encryptedEmbedding
|
|
215
|
+
decayScore
|
|
216
|
+
timestamp
|
|
217
|
+
createdAt
|
|
218
|
+
isActive
|
|
219
|
+
contentFp
|
|
220
|
+
sequenceId
|
|
221
|
+
version
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
`;
|
|
225
|
+
const data = await gqlQuery(subgraphUrl, query, { owner, first: Math.min(maxCandidates, 1000) }, authKeyHex);
|
|
226
|
+
return (data?.facts ?? []).filter(f => f.isActive !== false);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Fetch a single fact by its client-generated UUID.
|
|
230
|
+
*
|
|
231
|
+
* Used by the pin/unpin tools to retrieve a known fact's encrypted blob and
|
|
232
|
+
* metadata for re-encryption with an updated status. Returns null if the fact
|
|
233
|
+
* is not found, has been tombstoned (isActive=false), or the owner does not
|
|
234
|
+
* match the caller's Smart Account address (defense against stale IDs from
|
|
235
|
+
* another user's recall results).
|
|
236
|
+
*/
|
|
237
|
+
export async function fetchFactById(owner, factId, authKeyHex) {
|
|
238
|
+
const config = getSubgraphConfig();
|
|
239
|
+
const subgraphUrl = `${config.relayUrl}/v1/subgraph`;
|
|
240
|
+
const query = `
|
|
241
|
+
query GetFactById($id: ID!) {
|
|
242
|
+
fact(id: $id) {
|
|
243
|
+
id
|
|
244
|
+
owner
|
|
245
|
+
encryptedBlob
|
|
246
|
+
encryptedEmbedding
|
|
247
|
+
decayScore
|
|
248
|
+
timestamp
|
|
249
|
+
createdAt
|
|
250
|
+
isActive
|
|
251
|
+
contentFp
|
|
252
|
+
sequenceId
|
|
253
|
+
version
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
`;
|
|
257
|
+
const data = await gqlQuery(subgraphUrl, query, { id: factId }, authKeyHex);
|
|
258
|
+
const fact = data?.fact;
|
|
259
|
+
if (!fact)
|
|
260
|
+
return null;
|
|
261
|
+
if (fact.isActive === false)
|
|
262
|
+
return null;
|
|
263
|
+
if (typeof fact.owner === 'string' && fact.owner.toLowerCase() !== owner.toLowerCase()) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return fact;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get fact count from the subgraph for dynamic pool sizing.
|
|
270
|
+
* Uses the globalStates entity for a lightweight single-row lookup
|
|
271
|
+
* instead of fetching and counting individual fact IDs.
|
|
272
|
+
*/
|
|
273
|
+
export async function getSubgraphFactCount(owner, authKeyHex) {
|
|
274
|
+
const config = getSubgraphConfig();
|
|
275
|
+
const subgraphUrl = `${config.relayUrl}/v1/subgraph`;
|
|
276
|
+
const query = `
|
|
277
|
+
query FactCount {
|
|
278
|
+
globalStates(first: 1) {
|
|
279
|
+
totalFacts
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
const data = await gqlQuery(subgraphUrl, query, {}, authKeyHex);
|
|
284
|
+
if (data?.globalStates && data.globalStates.length > 0) {
|
|
285
|
+
const count = parseInt(data.globalStates[0].totalFacts, 10);
|
|
286
|
+
return isNaN(count) ? 0 : count;
|
|
287
|
+
}
|
|
288
|
+
return 0;
|
|
289
|
+
}
|