@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +268 -1
  2. package/SKILL.md +29 -23
  3. package/api-client.ts +18 -11
  4. package/claims-helper.ts +47 -1
  5. package/config.ts +108 -4
  6. package/confirm-indexed.ts +191 -0
  7. package/crypto.ts +10 -2
  8. package/dist/api-client.js +226 -0
  9. package/dist/billing-cache.js +100 -0
  10. package/dist/claims-helper.js +624 -0
  11. package/dist/config.js +297 -0
  12. package/dist/confirm-indexed.js +127 -0
  13. package/dist/consolidation.js +258 -0
  14. package/dist/contradiction-sync.js +1034 -0
  15. package/dist/crypto.js +138 -0
  16. package/dist/digest-sync.js +361 -0
  17. package/dist/download-ux.js +63 -0
  18. package/dist/embedder-cache.js +185 -0
  19. package/dist/embedder-loader.js +121 -0
  20. package/dist/embedder-network.js +301 -0
  21. package/dist/embedding.js +141 -0
  22. package/dist/extractor.js +1225 -0
  23. package/dist/first-run.js +103 -0
  24. package/dist/fs-helpers.js +725 -0
  25. package/dist/gateway-url.js +197 -0
  26. package/dist/generate-mnemonic.js +13 -0
  27. package/dist/hot-cache-wrapper.js +101 -0
  28. package/dist/import-adapters/base-adapter.js +64 -0
  29. package/dist/import-adapters/chatgpt-adapter.js +238 -0
  30. package/dist/import-adapters/claude-adapter.js +114 -0
  31. package/dist/import-adapters/gemini-adapter.js +201 -0
  32. package/dist/import-adapters/index.js +26 -0
  33. package/dist/import-adapters/mcp-memory-adapter.js +219 -0
  34. package/dist/import-adapters/mem0-adapter.js +158 -0
  35. package/dist/import-adapters/types.js +1 -0
  36. package/dist/index.js +5388 -0
  37. package/dist/llm-client.js +687 -0
  38. package/dist/llm-profile-reader.js +346 -0
  39. package/dist/lsh.js +62 -0
  40. package/dist/onboarding-cli.js +750 -0
  41. package/dist/pair-cli.js +344 -0
  42. package/dist/pair-crypto.js +359 -0
  43. package/dist/pair-http.js +404 -0
  44. package/dist/pair-page.js +826 -0
  45. package/dist/pair-qr.js +107 -0
  46. package/dist/pair-remote-client.js +410 -0
  47. package/dist/pair-session-store.js +566 -0
  48. package/dist/pin.js +556 -0
  49. package/dist/qa-bug-report.js +301 -0
  50. package/dist/relay-headers.js +44 -0
  51. package/dist/reranker.js +409 -0
  52. package/dist/retype-setscope.js +368 -0
  53. package/dist/semantic-dedup.js +75 -0
  54. package/dist/subgraph-search.js +289 -0
  55. package/dist/subgraph-store.js +694 -0
  56. package/dist/tool-gating.js +58 -0
  57. package/download-ux.ts +91 -0
  58. package/embedder-cache.ts +230 -0
  59. package/embedder-loader.ts +189 -0
  60. package/embedder-network.ts +350 -0
  61. package/embedding.ts +118 -27
  62. package/fs-helpers.ts +277 -0
  63. package/gateway-url.ts +57 -9
  64. package/index.ts +469 -250
  65. package/llm-client.ts +4 -3
  66. package/lsh.ts +7 -2
  67. package/onboarding-cli.ts +114 -1
  68. package/package.json +24 -5
  69. package/pair-cli.ts +76 -8
  70. package/pair-crypto.ts +34 -24
  71. package/pair-page.ts +28 -17
  72. package/pair-qr.ts +152 -0
  73. package/pair-remote-client.ts +540 -0
  74. package/pin.ts +31 -0
  75. package/qa-bug-report.ts +84 -2
  76. package/relay-headers.ts +50 -0
  77. package/reranker.ts +40 -0
  78. package/retype-setscope.ts +69 -8
  79. package/skill.json +1 -1
  80. package/subgraph-search.ts +4 -3
  81. 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
+ }