@totalreclaw/totalreclaw 3.3.12-rc.9 → 3.3.12

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 (79) hide show
  1. package/SKILL.md +33 -27
  2. package/api-client.ts +17 -9
  3. package/batch-gate.ts +42 -0
  4. package/billing-cache.ts +72 -23
  5. package/claims-helper.ts +2 -1
  6. package/config.ts +95 -13
  7. package/consolidation.ts +6 -13
  8. package/contradiction-sync.ts +19 -14
  9. package/credential-provider.ts +184 -0
  10. package/crypto.ts +27 -160
  11. package/dist/api-client.js +17 -9
  12. package/dist/batch-gate.js +40 -0
  13. package/dist/billing-cache.js +54 -24
  14. package/dist/claims-helper.js +2 -1
  15. package/dist/config.js +91 -13
  16. package/dist/consolidation.js +6 -15
  17. package/dist/contradiction-sync.js +15 -15
  18. package/dist/credential-provider.js +145 -0
  19. package/dist/crypto.js +17 -137
  20. package/dist/download-ux.js +11 -7
  21. package/dist/embedder-loader.js +266 -0
  22. package/dist/embedding.js +36 -3
  23. package/dist/entry.js +123 -0
  24. package/dist/fs-helpers.js +75 -379
  25. package/dist/import-adapters/gemini-adapter.js +29 -159
  26. package/dist/index.js +864 -2631
  27. package/dist/memory-runtime.js +459 -0
  28. package/dist/native-memory.js +123 -0
  29. package/dist/onboarding-cli.js +4 -8
  30. package/dist/pair-cli-relay.js +1 -8
  31. package/dist/pair-cli.js +1 -1
  32. package/dist/pair-crypto.js +16 -358
  33. package/dist/pair-http.js +147 -4
  34. package/dist/relay.js +140 -0
  35. package/dist/reranker.js +13 -8
  36. package/dist/semantic-dedup.js +5 -7
  37. package/dist/skill-register.js +97 -0
  38. package/dist/subgraph-search.js +3 -1
  39. package/dist/subgraph-store.js +348 -290
  40. package/dist/tool-gating.js +39 -26
  41. package/dist/tools.js +367 -0
  42. package/dist/tr-cli-export-helper.js +3 -3
  43. package/dist/tr-cli.js +65 -156
  44. package/dist/trajectory-poller.js +155 -9
  45. package/dist/vault-crypto.js +551 -0
  46. package/download-ux.ts +12 -6
  47. package/embedder-loader.ts +293 -1
  48. package/embedding.ts +43 -3
  49. package/entry.ts +132 -0
  50. package/fs-helpers.ts +93 -458
  51. package/import-adapters/gemini-adapter.ts +38 -183
  52. package/index.ts +912 -2917
  53. package/memory-runtime.ts +723 -0
  54. package/native-memory.ts +196 -0
  55. package/onboarding-cli.ts +3 -9
  56. package/openclaw.plugin.json +5 -17
  57. package/package.json +6 -3
  58. package/pair-cli-relay.ts +0 -9
  59. package/pair-cli.ts +1 -1
  60. package/pair-crypto.ts +41 -483
  61. package/pair-http.ts +194 -5
  62. package/postinstall.mjs +138 -0
  63. package/relay.ts +172 -0
  64. package/reranker.ts +13 -8
  65. package/semantic-dedup.ts +5 -6
  66. package/skill-register.ts +146 -0
  67. package/skill.json +1 -1
  68. package/subgraph-search.ts +3 -1
  69. package/subgraph-store.ts +367 -307
  70. package/tool-gating.ts +39 -26
  71. package/tools.ts +499 -0
  72. package/tr-cli-export-helper.ts +3 -3
  73. package/tr-cli.ts +69 -182
  74. package/trajectory-poller.ts +162 -10
  75. package/vault-crypto.ts +705 -0
  76. package/auto-pair-on-load.ts +0 -308
  77. package/dist/auto-pair-on-load.js +0 -197
  78. package/dist/pair-pending-injection.js +0 -125
  79. package/pair-pending-injection.ts +0 -205
@@ -7,56 +7,31 @@ import type {
7
7
  } from './types.js';
8
8
  import fs from 'node:fs';
9
9
  import os from 'node:os';
10
-
11
- /** Maximum messages per conversation chunk for LLM extraction. */
12
- const CHUNK_SIZE = 20;
13
-
14
- /** Gap (in minutes) between entries that starts a new pseudo-session. */
15
- const SESSION_GAP_MINUTES = 30;
16
-
17
- // ── Timestamp Parsing ────────────────────────────────────────────────────────
18
-
19
- const MONTHS: Record<string, number> = {
20
- Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5,
21
- Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11,
22
- };
23
-
24
- /**
25
- * Parse Gemini timestamp: "1 Apr 2026, 18:39:35 WEST" → ISO 8601.
26
- * Timezone is treated as UTC (all entries use the same TZ, preserving order).
27
- */
28
- function parseTimestamp(raw: string): string | undefined {
29
- const m = raw.match(/^(\d{1,2})\s+(\w{3})\s+(\d{4}),\s+(\d{2}):(\d{2}):(\d{2})\s+/);
30
- if (!m || MONTHS[m[2]] === undefined) return undefined;
31
- const d = new Date(Date.UTC(+m[3], MONTHS[m[2]], +m[1], +m[4], +m[5], +m[6]));
32
- return d.toISOString();
10
+ import { createRequire } from 'node:module';
11
+
12
+ // All Gemini format parsing (MyActivity.json, legacy HTML, Saved-info paste)
13
+ // lives in the shared Rust core (`@totalreclaw/core` WASM) so the logic —
14
+ // including the locale-robust, lossless timestamp handling is identical across
15
+ // every client (Python/Hermes via PyO3, TS via WASM). This adapter is a thin
16
+ // shim: it owns only file I/O + the size/RAM preflight, then delegates parsing.
17
+ const requireWasm = createRequire(import.meta.url);
18
+ let _wasm: typeof import('@totalreclaw/core') | null = null;
19
+ function getWasm() {
20
+ if (!_wasm) _wasm = requireWasm('@totalreclaw/core');
21
+ return _wasm!;
33
22
  }
34
23
 
35
- // ── HTML Helpers ─────────────────────────────────────────────────────────────
36
-
37
- function decodeEntities(t: string): string {
38
- return t.replace(/&#39;/g, "'").replace(/&quot;/g, '"').replace(/&amp;/g, '&')
39
- .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&nbsp;/g, ' ');
40
- }
41
-
42
- function stripHTML(html: string): string {
43
- return html.replace(/<br\s*\/?>/gi, '\n').replace(/<\/p>/gi, '\n')
44
- .replace(/<\/li>/gi, '\n').replace(/<\/h[1-6]>/gi, '\n')
45
- .replace(/<hr\s*\/?>/gi, '\n---\n').replace(/<[^>]+>/g, '')
46
- .replace(/\n{3,}/g, '\n\n').trim();
47
- }
48
-
49
- // ── Entry Types ──────────────────────────────────────────────────────────────
50
-
51
- interface GeminiEntry {
52
- userPrompt: string;
53
- aiResponse: string;
54
- timestampISO: string;
55
- timestampUnix: number;
24
+ /** Shape returned by core `parseGemini` (serde -> JS object, snake_case). */
25
+ interface CoreParseResult {
26
+ chunks: ConversationChunk[];
27
+ total_messages: number;
28
+ warnings: string[];
29
+ errors: string[];
30
+ format: string;
31
+ records_count?: number;
32
+ skipped?: number;
56
33
  }
57
34
 
58
- // ── Gemini Adapter ───────────────────────────────────────────────────────────
59
-
60
35
  export class GeminiAdapter extends BaseImportAdapter {
61
36
  readonly source: ImportSource = 'gemini';
62
37
  readonly displayName = 'Google Gemini';
@@ -101,161 +76,41 @@ export class GeminiAdapter extends BaseImportAdapter {
101
76
  } else {
102
77
  errors.push(
103
78
  'Gemini import requires either content or file_path. ' +
104
- 'Export from Google Takeout: takeout.google.com → select Gemini Apps → export. ' +
105
- 'Provide the "My Activity.html" file path.',
79
+ 'Export from Google Takeout: takeout.google.com → "My Activity" → "Gemini Apps". ' +
80
+ 'Provide the "My Activity.html" (or MyActivity.json) file path, or paste your Saved info.',
106
81
  );
107
82
  return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
108
83
  }
109
84
 
110
85
  if (onProgress) {
111
- onProgress({ current: 0, total: 0, phase: 'parsing', message: 'Parsing Gemini HTML...' });
112
- }
113
-
114
- // Parse HTML into entries
115
- const entries = this.parseHTML(content);
116
- if (entries.length === 0) {
117
- warnings.push('No conversation entries found in the HTML file.');
118
- return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
86
+ onProgress({ current: 0, total: 0, phase: 'parsing', message: 'Parsing Gemini export...' });
119
87
  }
120
88
 
121
- // Group into pseudo-sessions by temporal proximity
122
- const sessions = this.groupSessions(entries);
89
+ // Delegate ALL format parsing to the shared core.
90
+ const result = getWasm().parseGemini(content) as CoreParseResult;
123
91
 
124
92
  if (onProgress) {
125
93
  onProgress({
126
- current: 0,
127
- total: sessions.length,
94
+ current: result.chunks.length,
95
+ total: result.chunks.length,
128
96
  phase: 'parsing',
129
- message: `Parsed ${entries.length} entries into ${sessions.length} sessions`,
97
+ message: `Parsed ${result.total_messages} messages into ${result.chunks.length} chunks`,
130
98
  });
131
99
  }
132
100
 
133
- // Build conversation chunks from sessions
134
- const chunks: ConversationChunk[] = [];
135
- let totalMessages = 0;
136
-
137
- for (const session of sessions) {
138
- const messages: Array<{ role: 'user' | 'assistant'; text: string }> = [];
139
- for (const entry of session) {
140
- if (entry.userPrompt) messages.push({ role: 'user', text: entry.userPrompt });
141
- if (entry.aiResponse) messages.push({ role: 'assistant', text: entry.aiResponse });
142
- }
143
- if (messages.length === 0) continue;
144
-
145
- totalMessages += messages.length;
146
- const timestamp = session[0].timestampISO;
147
-
148
- // Sub-chunk large sessions
149
- for (let i = 0; i < messages.length; i += CHUNK_SIZE) {
150
- const batch = messages.slice(i, i + CHUNK_SIZE);
151
- const chunkIdx = Math.floor(i / CHUNK_SIZE) + 1;
152
- const totalChunks = Math.ceil(messages.length / CHUNK_SIZE);
153
- const title = totalChunks > 1
154
- ? `Gemini session (part ${chunkIdx}/${totalChunks})`
155
- : 'Gemini session';
156
-
157
- chunks.push({ title, messages: batch, timestamp });
158
- }
159
- }
160
-
161
101
  return {
162
102
  facts: [],
163
- chunks,
164
- totalMessages,
165
- warnings,
166
- errors,
103
+ chunks: result.chunks,
104
+ totalMessages: result.total_messages,
105
+ warnings: [...warnings, ...result.warnings],
106
+ errors: [...errors, ...result.errors],
167
107
  source_metadata: {
168
- format: 'gemini-takeout-html',
169
- total_entries: entries.length,
170
- sessions_count: sessions.length,
171
- chunks_count: chunks.length,
172
- total_messages: totalMessages,
173
- date_range: {
174
- earliest: entries[0]?.timestampISO,
175
- latest: entries[entries.length - 1]?.timestampISO,
176
- },
108
+ format: result.format,
109
+ chunks_count: result.chunks.length,
110
+ total_messages: result.total_messages,
111
+ ...(result.records_count ? { records_count: result.records_count } : {}),
112
+ ...(result.skipped ? { skipped_non_gemini: result.skipped } : {}),
177
113
  },
178
114
  };
179
115
  }
180
-
181
- /**
182
- * Parse Gemini Takeout HTML into structured entries.
183
- *
184
- * Each outer-cell div contains: "Prompted USER_TEXT<br>TIMESTAMP<br>RESPONSE_HTML"
185
- * all within one content-cell.
186
- */
187
- private parseHTML(html: string): GeminiEntry[] {
188
- const entries: GeminiEntry[] = [];
189
- const cellPattern = /<div class="outer-cell[^"]*">([\s\S]*?)(?=<div class="outer-cell|$)/g;
190
- let match: RegExpExecArray | null;
191
-
192
- while ((match = cellPattern.exec(html)) !== null) {
193
- const cell = match[1];
194
-
195
- // Only process "Prompted" entries (skip canvas, feedback)
196
- const promptedIdx = cell.indexOf('Prompted\u00a0');
197
- if (promptedIdx === -1) continue;
198
-
199
- // Extract timestamp
200
- const tsMatch = cell.match(/(\d{1,2}\s+\w{3}\s+\d{4},\s+\d{2}:\d{2}:\d{2}\s+\w+)/);
201
- if (!tsMatch) continue;
202
- const timestampISO = parseTimestamp(tsMatch[1]);
203
- if (!timestampISO) continue;
204
-
205
- // Split on timestamp to separate user prompt from AI response
206
- const afterPrompted = cell.substring(promptedIdx + 'Prompted\u00a0'.length);
207
- const tsPattern = /(\d{1,2}\s+\w{3}\s+\d{4},\s+\d{2}:\d{2}:\d{2}\s+\w+)/;
208
- const tsIdx = afterPrompted.search(tsPattern);
209
-
210
- let userPrompt = '';
211
- let aiResponse = '';
212
-
213
- if (tsIdx > 0) {
214
- userPrompt = stripHTML(decodeEntities(afterPrompted.substring(0, tsIdx))).trim();
215
-
216
- const tsInner = afterPrompted.match(tsPattern);
217
- if (tsInner) {
218
- const afterTs = afterPrompted.substring(tsIdx + tsInner[0].length)
219
- .replace(/^\s*<br\s*\/?>\s*/i, '');
220
- const endDiv = afterTs.search(/<\/div>\s*<div class="content-cell/);
221
- const rawResp = endDiv !== -1 ? afterTs.substring(0, endDiv) : afterTs;
222
- aiResponse = stripHTML(decodeEntities(rawResp)).trim();
223
- }
224
- }
225
-
226
- if (userPrompt.length < 3 && aiResponse.length < 3) continue;
227
-
228
- entries.push({
229
- userPrompt,
230
- aiResponse,
231
- timestampISO,
232
- timestampUnix: Math.floor(new Date(timestampISO).getTime() / 1000),
233
- });
234
- }
235
-
236
- // Sort chronologically (HTML is newest-first)
237
- entries.sort((a, b) => a.timestampUnix - b.timestampUnix);
238
- return entries;
239
- }
240
-
241
- /**
242
- * Group entries into pseudo-sessions by temporal proximity.
243
- */
244
- private groupSessions(entries: GeminiEntry[]): GeminiEntry[][] {
245
- if (entries.length === 0) return [];
246
- const sessions: GeminiEntry[][] = [];
247
- let current: GeminiEntry[] = [entries[0]];
248
-
249
- for (let i = 1; i < entries.length; i++) {
250
- const gap = entries[i].timestampUnix - entries[i - 1].timestampUnix;
251
- if (gap > SESSION_GAP_MINUTES * 60) {
252
- sessions.push(current);
253
- current = [entries[i]];
254
- } else {
255
- current.push(entries[i]);
256
- }
257
- }
258
- if (current.length > 0) sessions.push(current);
259
- return sessions;
260
- }
261
116
  }