@totalreclaw/totalreclaw 3.3.1-rc.2 → 3.3.1-rc.21

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 (70) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/SKILL.md +50 -83
  3. package/api-client.ts +18 -11
  4. package/config.ts +117 -3
  5. package/crypto.ts +10 -2
  6. package/dist/api-client.js +226 -0
  7. package/dist/billing-cache.js +100 -0
  8. package/dist/claims-helper.js +606 -0
  9. package/dist/config.js +280 -0
  10. package/dist/consolidation.js +258 -0
  11. package/dist/contradiction-sync.js +1034 -0
  12. package/dist/crypto.js +138 -0
  13. package/dist/digest-sync.js +361 -0
  14. package/dist/download-ux.js +63 -0
  15. package/dist/embedding.js +86 -0
  16. package/dist/extractor.js +1225 -0
  17. package/dist/first-run.js +103 -0
  18. package/dist/fs-helpers.js +563 -0
  19. package/dist/gateway-url.js +197 -0
  20. package/dist/generate-mnemonic.js +13 -0
  21. package/dist/hot-cache-wrapper.js +101 -0
  22. package/dist/import-adapters/base-adapter.js +64 -0
  23. package/dist/import-adapters/chatgpt-adapter.js +238 -0
  24. package/dist/import-adapters/claude-adapter.js +114 -0
  25. package/dist/import-adapters/gemini-adapter.js +201 -0
  26. package/dist/import-adapters/index.js +26 -0
  27. package/dist/import-adapters/mcp-memory-adapter.js +219 -0
  28. package/dist/import-adapters/mem0-adapter.js +158 -0
  29. package/dist/import-adapters/types.js +1 -0
  30. package/dist/index.js +5348 -0
  31. package/dist/llm-client.js +686 -0
  32. package/dist/llm-profile-reader.js +346 -0
  33. package/dist/lsh.js +62 -0
  34. package/dist/onboarding-cli.js +750 -0
  35. package/dist/pair-cli.js +344 -0
  36. package/dist/pair-crypto.js +359 -0
  37. package/dist/pair-http.js +404 -0
  38. package/dist/pair-page.js +826 -0
  39. package/dist/pair-qr.js +107 -0
  40. package/dist/pair-remote-client.js +410 -0
  41. package/dist/pair-session-store.js +566 -0
  42. package/dist/pin.js +542 -0
  43. package/dist/qa-bug-report.js +301 -0
  44. package/dist/relay-headers.js +44 -0
  45. package/dist/reranker.js +442 -0
  46. package/dist/retype-setscope.js +348 -0
  47. package/dist/semantic-dedup.js +75 -0
  48. package/dist/subgraph-search.js +289 -0
  49. package/dist/subgraph-store.js +694 -0
  50. package/dist/tool-gating.js +58 -0
  51. package/download-ux.ts +91 -0
  52. package/embedding.ts +32 -9
  53. package/fs-helpers.ts +124 -0
  54. package/gateway-url.ts +57 -9
  55. package/index.ts +586 -357
  56. package/llm-client.ts +211 -23
  57. package/lsh.ts +7 -2
  58. package/onboarding-cli.ts +114 -1
  59. package/package.json +19 -5
  60. package/pair-cli.ts +76 -8
  61. package/pair-crypto.ts +34 -24
  62. package/pair-page.ts +28 -17
  63. package/pair-qr.ts +152 -0
  64. package/pair-remote-client.ts +540 -0
  65. package/qa-bug-report.ts +381 -0
  66. package/relay-headers.ts +50 -0
  67. package/reranker.ts +73 -0
  68. package/retype-setscope.ts +12 -0
  69. package/subgraph-search.ts +4 -3
  70. package/subgraph-store.ts +109 -16
@@ -0,0 +1,346 @@
1
+ /**
2
+ * llm-profile-reader — read OpenClaw's `auth-profiles.json` to harvest
3
+ * provider API keys when the plugin has no other source.
4
+ *
5
+ * Background
6
+ * ----------
7
+ * In 3.3.0-rc.6 and earlier, the plugin's `initLLMClient` only looked in:
8
+ * 1. `api.config.providers` / `openclawProviders` passed by the SDK
9
+ * 2. Env vars (`ZAI_API_KEY`, `OPENAI_API_KEY`, ...)
10
+ * 3. Plugin-config override `extraction.llm`
11
+ *
12
+ * Real-world OpenClaw installs store user API keys in
13
+ * `~/.openclaw/agents/<agent>/agent/auth-profiles.json`. None of the three
14
+ * paths above reach that file, so the plugin silently logged
15
+ * `No LLM available for auto-extraction` on every turn — auto-extraction
16
+ * was a no-op for virtually every real user. See user-findings 3.3.0-rc.6.
17
+ *
18
+ * 3.3.1 adds this file as the fourth resolution tier, sitting between
19
+ * "openclawProviders (SDK-passed)" and "env vars".
20
+ *
21
+ * Scope and scanner surface
22
+ * -------------------------
23
+ * This file does disk I/O. It MUST NOT contain the trigger substrings
24
+ * used by OpenClaw's scanner (see `skill/scripts/check-scanner.mjs`) —
25
+ * namely the outbound-request markers. All network work stays in
26
+ * `llm-client.ts` and friends; this file only reads local files.
27
+ *
28
+ * File format (auth-profiles.json, OpenClaw canonical shape)
29
+ * ----------------------------------------------------------
30
+ * {
31
+ * "profiles": {
32
+ * "openai:default": { "key": "sk-..." },
33
+ * "anthropic:default": { "key": "sk-ant-..." },
34
+ * "zai:default": { "key": "..." },
35
+ * ...
36
+ * }
37
+ * }
38
+ *
39
+ * We map the `<provider>:default` profile id to the canonical provider
40
+ * name the plugin uses elsewhere (openai, anthropic, zai, gemini, etc.).
41
+ * Non-default profile ids are ignored — a deliberate choice so users who
42
+ * have multiple profiles (`openai:work`, `openai:personal`) see the one
43
+ * they've explicitly flagged as `default`.
44
+ *
45
+ * File format (models.json, legacy OpenClaw shape) — 3.3.1-rc.2
46
+ * ------------------------------------------------------------
47
+ * {
48
+ * "providers": {
49
+ * "zai": { "apiKey": "..." },
50
+ * "openai": { "apiKey": "sk-..." },
51
+ * "anthropic": { "apiKey": "sk-ant-..." },
52
+ * ...
53
+ * }
54
+ * }
55
+ *
56
+ * 3.3.1-rc.1 QA found that some real OpenClaw installs (the VPS used for
57
+ * QA in particular) still have the pre-auth-profiles format — a single
58
+ * `models.json` at the same path with a `providers` map. Reading only
59
+ * auth-profiles.json silently no-op'd on those hosts. 3.3.1-rc.2 adds a
60
+ * 5th tier to the cascade: if auth-profiles.json is absent, fall back to
61
+ * the adjacent `models.json`.
62
+ */
63
+ import fs from 'node:fs';
64
+ import path from 'node:path';
65
+ // ---------------------------------------------------------------------------
66
+ // Provider-name normalization
67
+ // ---------------------------------------------------------------------------
68
+ /**
69
+ * Map an auth-profile namespace (the part before `:` in a profile id like
70
+ * `openai:default`) to the plugin's canonical provider name.
71
+ */
72
+ const PROFILE_NS_TO_PROVIDER = {
73
+ openai: 'openai',
74
+ anthropic: 'anthropic',
75
+ zai: 'zai',
76
+ 'z.ai': 'zai',
77
+ google: 'gemini',
78
+ gemini: 'gemini',
79
+ mistral: 'mistral',
80
+ groq: 'groq',
81
+ deepseek: 'deepseek',
82
+ openrouter: 'openrouter',
83
+ xai: 'xai',
84
+ 'x.ai': 'xai',
85
+ together: 'together',
86
+ cerebras: 'cerebras',
87
+ };
88
+ // ---------------------------------------------------------------------------
89
+ // File discovery
90
+ // ---------------------------------------------------------------------------
91
+ /**
92
+ * Default search root — `$HOME/.openclaw/agents`. Returns an empty
93
+ * string when HOME is unset (avoids path.join crash on bare envs).
94
+ */
95
+ export function defaultAuthProfilesRoot(homeDir) {
96
+ if (!homeDir)
97
+ return '';
98
+ return path.join(homeDir, '.openclaw', 'agents');
99
+ }
100
+ /**
101
+ * Walk `$HOME/.openclaw/agents/*` (one level deep), each subdirectory
102
+ * being an agent with potentially an `agent/auth-profiles.json`. Returns
103
+ * every auth-profiles.json path that exists on disk, in alphabetical
104
+ * order of the agent dir name (stable, so tests don't flake).
105
+ *
106
+ * Silently tolerates a missing root — returns [] instead of throwing.
107
+ */
108
+ export function findAuthProfilesFiles(root) {
109
+ if (!root)
110
+ return [];
111
+ let entries;
112
+ try {
113
+ entries = fs.readdirSync(root, { withFileTypes: true });
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ const out = [];
119
+ for (const e of entries) {
120
+ if (!e.isDirectory())
121
+ continue;
122
+ if (e.name.startsWith('.'))
123
+ continue;
124
+ const candidate = path.join(root, e.name, 'agent', 'auth-profiles.json');
125
+ try {
126
+ if (fs.statSync(candidate).isFile())
127
+ out.push(candidate);
128
+ }
129
+ catch {
130
+ // missing or inaccessible — skip.
131
+ }
132
+ }
133
+ out.sort();
134
+ return out;
135
+ }
136
+ /**
137
+ * Parse one auth-profiles.json file into a list of (provider, apiKey)
138
+ * entries, keeping only `<ns>:default` profiles and only those whose `key`
139
+ * is a non-empty string. Unknown namespaces are skipped silently.
140
+ */
141
+ export function parseAuthProfilesFile(filePath) {
142
+ let raw;
143
+ try {
144
+ raw = fs.readFileSync(filePath, 'utf-8');
145
+ }
146
+ catch {
147
+ return [];
148
+ }
149
+ let json;
150
+ try {
151
+ json = JSON.parse(raw);
152
+ }
153
+ catch {
154
+ return [];
155
+ }
156
+ if (typeof json !== 'object' || json === null)
157
+ return [];
158
+ const profilesField = json.profiles;
159
+ if (typeof profilesField !== 'object' || profilesField === null)
160
+ return [];
161
+ const profiles = profilesField;
162
+ const out = [];
163
+ for (const [profileId, entryRaw] of Object.entries(profiles)) {
164
+ const parts = profileId.split(':');
165
+ if (parts.length !== 2)
166
+ continue;
167
+ const [ns, suffix] = parts;
168
+ if (suffix !== 'default')
169
+ continue;
170
+ const nsKey = ns.toLowerCase();
171
+ const provider = PROFILE_NS_TO_PROVIDER[nsKey];
172
+ if (!provider)
173
+ continue;
174
+ if (typeof entryRaw !== 'object' || entryRaw === null)
175
+ continue;
176
+ const keyField = entryRaw.key;
177
+ if (typeof keyField !== 'string')
178
+ continue;
179
+ const trimmed = keyField.trim();
180
+ if (!trimmed)
181
+ continue;
182
+ out.push({
183
+ provider,
184
+ apiKey: trimmed,
185
+ sourcePath: filePath,
186
+ profileId,
187
+ });
188
+ }
189
+ return out;
190
+ }
191
+ // ---------------------------------------------------------------------------
192
+ // Public aggregate
193
+ // ---------------------------------------------------------------------------
194
+ /**
195
+ * Harvest every non-empty provider key from every
196
+ * `~/.openclaw/agents/<agent>/agent/auth-profiles.json` on disk. Later files
197
+ * (alphabetical) win for duplicate provider names — intentional so a
198
+ * newly-added agent's keys shadow older ones. Callers that want the
199
+ * single "first match per provider" list should run through
200
+ * `dedupeByProvider` below.
201
+ */
202
+ export function readAllAuthProfileKeys(options) {
203
+ const files = findAuthProfilesFiles(options.root);
204
+ const out = [];
205
+ for (const file of files) {
206
+ const keys = parseAuthProfilesFile(file);
207
+ out.push(...keys);
208
+ }
209
+ return out;
210
+ }
211
+ /**
212
+ * Reduce a list of AuthProfileKey entries to one-per-provider, picking
213
+ * the LAST one in list order (so later agent files override earlier ones
214
+ * for the same provider).
215
+ */
216
+ export function dedupeByProvider(entries) {
217
+ const map = {};
218
+ for (const e of entries) {
219
+ map[e.provider] = e;
220
+ }
221
+ return map;
222
+ }
223
+ // ---------------------------------------------------------------------------
224
+ // 3.3.1-rc.2 — legacy models.json reader
225
+ // ---------------------------------------------------------------------------
226
+ /**
227
+ * Walk `$HOME/.openclaw/agents/*` and return every
228
+ * `agent/models.json` path that exists on disk. Mirrors findAuthProfilesFiles
229
+ * but targets the pre-auth-profiles filename.
230
+ */
231
+ export function findModelsJsonFiles(root) {
232
+ if (!root)
233
+ return [];
234
+ let entries;
235
+ try {
236
+ entries = fs.readdirSync(root, { withFileTypes: true });
237
+ }
238
+ catch {
239
+ return [];
240
+ }
241
+ const out = [];
242
+ for (const e of entries) {
243
+ if (!e.isDirectory())
244
+ continue;
245
+ if (e.name.startsWith('.'))
246
+ continue;
247
+ const candidate = path.join(root, e.name, 'agent', 'models.json');
248
+ try {
249
+ if (fs.statSync(candidate).isFile())
250
+ out.push(candidate);
251
+ }
252
+ catch {
253
+ // missing — skip.
254
+ }
255
+ }
256
+ out.sort();
257
+ return out;
258
+ }
259
+ /**
260
+ * Parse a legacy `models.json` file into AuthProfileKey entries. Unknown
261
+ * provider namespaces (anything not in PROFILE_NS_TO_PROVIDER) are
262
+ * skipped silently. Accepts `apiKey`, `api_key`, or `key` as the key
263
+ * field — different OpenClaw versions used different names.
264
+ */
265
+ export function parseModelsJsonFile(filePath) {
266
+ let raw;
267
+ try {
268
+ raw = fs.readFileSync(filePath, 'utf-8');
269
+ }
270
+ catch {
271
+ return [];
272
+ }
273
+ let json;
274
+ try {
275
+ json = JSON.parse(raw);
276
+ }
277
+ catch {
278
+ return [];
279
+ }
280
+ if (typeof json !== 'object' || json === null)
281
+ return [];
282
+ const providersField = json.providers;
283
+ if (typeof providersField !== 'object' || providersField === null)
284
+ return [];
285
+ const providers = providersField;
286
+ const out = [];
287
+ for (const [providerName, entryRaw] of Object.entries(providers)) {
288
+ const nsKey = providerName.toLowerCase();
289
+ const provider = PROFILE_NS_TO_PROVIDER[nsKey];
290
+ if (!provider)
291
+ continue;
292
+ if (typeof entryRaw !== 'object' || entryRaw === null)
293
+ continue;
294
+ const rec = entryRaw;
295
+ const rawKey = rec.apiKey ?? rec.api_key ?? rec.key;
296
+ if (typeof rawKey !== 'string')
297
+ continue;
298
+ const trimmed = rawKey.trim();
299
+ if (!trimmed)
300
+ continue;
301
+ out.push({
302
+ provider,
303
+ apiKey: trimmed,
304
+ sourcePath: filePath,
305
+ profileId: `${providerName}:models-json-legacy`,
306
+ });
307
+ }
308
+ return out;
309
+ }
310
+ /**
311
+ * Read every models.json file under the agents root. Counterpart to
312
+ * `readAllAuthProfileKeys`.
313
+ */
314
+ export function readAllModelsJsonKeys(options) {
315
+ const files = findModelsJsonFiles(options.root);
316
+ const out = [];
317
+ for (const file of files) {
318
+ const keys = parseModelsJsonFile(file);
319
+ out.push(...keys);
320
+ }
321
+ return out;
322
+ }
323
+ /**
324
+ * 3.3.1-rc.2 — combined reader. Reads auth-profiles.json first (if
325
+ * present), then merges in models.json entries for any provider NOT
326
+ * already covered by auth-profiles. The newer format wins on overlap.
327
+ */
328
+ export function readAllProfileKeys(options) {
329
+ const primary = readAllAuthProfileKeys(options);
330
+ const primaryProviders = new Set(primary.map((e) => e.provider));
331
+ const legacy = readAllModelsJsonKeys(options);
332
+ const merged = [...primary];
333
+ for (const entry of legacy) {
334
+ if (!primaryProviders.has(entry.provider)) {
335
+ merged.push(entry);
336
+ }
337
+ }
338
+ return merged;
339
+ }
340
+ // ---------------------------------------------------------------------------
341
+ // Test hook
342
+ // ---------------------------------------------------------------------------
343
+ /** Internal — exposed for tests. */
344
+ export const __internal = {
345
+ PROFILE_NS_TO_PROVIDER,
346
+ };
package/dist/lsh.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * TotalReclaw Plugin - LSH Hasher
3
+ *
4
+ * Re-exports `WasmLshHasher` from `@totalreclaw/core` as `LSHHasher`
5
+ * for backward compatibility with existing plugin code.
6
+ *
7
+ * Default parameters: 32 bits per table, 20 tables.
8
+ */
9
+ // Lazy-load WASM via createRequire. The shipped `dist/index.js` is ESM-only
10
+ // (`"type":"module"`) so the bare `require` global is undefined at runtime.
11
+ // See issue #124 for the bug this avoids; matches the pattern in
12
+ // claims-helper / consolidation / digest-sync / pin / retype-setscope.
13
+ import { createRequire } from 'node:module';
14
+ const requireWasm = createRequire(import.meta.url);
15
+ let _WasmLshHasher = null;
16
+ function getWasmLshHasher() {
17
+ if (!_WasmLshHasher)
18
+ _WasmLshHasher = requireWasm('@totalreclaw/core').WasmLshHasher;
19
+ return _WasmLshHasher;
20
+ }
21
+ /**
22
+ * Random Hyperplane LSH hasher.
23
+ *
24
+ * All state is deterministic from the seed -- no randomness at hash time.
25
+ * Construct once per session; call `hash()` for every store/search operation.
26
+ */
27
+ export class LSHHasher {
28
+ inner;
29
+ /**
30
+ * Create a new LSH hasher.
31
+ *
32
+ * @param seed - 32-byte seed from `deriveLshSeed()` in crypto.ts.
33
+ * @param dims - Embedding dimensionality (e.g. 640 for Harrier).
34
+ * @param nTables - Number of independent hash tables (default 20).
35
+ * @param nBits - Number of bits per table (default 32).
36
+ */
37
+ constructor(seed, dims, nTables = 20, nBits = 32) {
38
+ const seedHex = Buffer.from(seed).toString('hex');
39
+ this.inner = getWasmLshHasher().withParams(seedHex, dims, nTables, nBits);
40
+ }
41
+ /**
42
+ * Hash an embedding vector to an array of blind-hashed bucket IDs.
43
+ *
44
+ * @param embedding - The embedding vector (must have `dims` elements).
45
+ * @returns Array of `nTables` hex strings (one blind hash per table).
46
+ */
47
+ hash(embedding) {
48
+ return this.inner.hash(new Float64Array(embedding));
49
+ }
50
+ /** Number of hash tables. */
51
+ get tables() {
52
+ return this.inner.tables;
53
+ }
54
+ /** Number of bits per table. */
55
+ get bits() {
56
+ return this.inner.bits;
57
+ }
58
+ /** Embedding dimensionality. */
59
+ get dimensions() {
60
+ return this.inner.dimensions;
61
+ }
62
+ }