@totalreclaw/totalreclaw 3.3.12 → 3.3.13

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 (128) hide show
  1. package/SKILL.md +25 -8
  2. package/{billing-cache.ts → billing/billing-cache.ts} +1 -1
  3. package/{relay-headers.ts → billing/relay-headers.ts} +1 -1
  4. package/{tr-cli-export-helper.ts → cli/tr-cli-export-helper.ts} +5 -5
  5. package/{tr-cli.ts → cli/tr-cli.ts} +20 -20
  6. package/{contradiction-sync.ts → contradiction/contradiction-sync.ts} +13 -13
  7. package/{digest-sync.ts → digest/digest-sync.ts} +32 -2
  8. package/dist/{billing-cache.js → billing/billing-cache.js} +1 -1
  9. package/dist/{relay-headers.js → billing/relay-headers.js} +1 -1
  10. package/dist/{tr-cli-export-helper.js → cli/tr-cli-export-helper.js} +5 -5
  11. package/dist/{tr-cli.js → cli/tr-cli.js} +19 -19
  12. package/dist/{contradiction-sync.js → contradiction/contradiction-sync.js} +13 -13
  13. package/dist/{digest-sync.js → digest/digest-sync.js} +32 -2
  14. package/dist/{download-ux.js → embedding/download-ux.js} +1 -1
  15. package/dist/{reranker.js → embedding/reranker.js} +13 -4
  16. package/dist/{claims-helper.js → extraction/claims-helper.js} +54 -58
  17. package/dist/extraction/consolidation.js +170 -0
  18. package/dist/{extractor.js → extraction/extractor.js} +17 -17
  19. package/dist/extraction/importance-filter.js +38 -0
  20. package/dist/{semantic-dedup.js → extraction/semantic-dedup.js} +2 -2
  21. package/dist/{batch-gate.js → import/batch-gate.js} +1 -1
  22. package/dist/import/import-runtime.js +692 -0
  23. package/dist/import-adapters/chatgpt-adapter.js +7 -7
  24. package/dist/import-adapters/gemini-adapter.js +13 -13
  25. package/dist/import-adapters/mem0-adapter.js +10 -10
  26. package/dist/index.js +111 -1094
  27. package/dist/{llm-client.js → llm/llm-client.js} +2 -2
  28. package/dist/{hot-cache-wrapper.js → memory/hot-cache-wrapper.js} +5 -5
  29. package/dist/{memory-runtime.js → memory/memory-runtime.js} +1 -1
  30. package/dist/{pin.js → memory/pin.js} +18 -18
  31. package/dist/{retype-setscope.js → memory/retype-setscope.js} +19 -19
  32. package/dist/{credential-provider.js → pairing/credential-provider.js} +2 -2
  33. package/dist/{first-run.js → pairing/first-run.js} +1 -1
  34. package/dist/{onboarding-cli.js → pairing/onboarding-cli.js} +20 -20
  35. package/dist/{pair-cli-relay.js → pairing/pair-cli-relay.js} +10 -10
  36. package/dist/{pair-crypto.js → pairing/pair-crypto.js} +1 -1
  37. package/dist/{pair-http.js → pairing/pair-http.js} +3 -3
  38. package/dist/{pair-remote-client.js → pairing/pair-remote-client.js} +10 -10
  39. package/dist/{pair-session-store.js → pairing/pair-session-store.js} +4 -4
  40. package/dist/runtime/config-schema.js +59 -0
  41. package/dist/runtime/format-helpers.js +257 -0
  42. package/dist/runtime/types.js +11 -0
  43. package/dist/{confirm-indexed.js → subgraph/confirm-indexed.js} +1 -1
  44. package/dist/{subgraph-search.js → subgraph/subgraph-search.js} +14 -14
  45. package/dist/{subgraph-store.js → subgraph/subgraph-store.js} +6 -6
  46. package/{download-ux.ts → embedding/download-ux.ts} +1 -1
  47. package/{reranker.ts → embedding/reranker.ts} +13 -4
  48. package/{claims-helper.ts → extraction/claims-helper.ts} +52 -60
  49. package/{consolidation.ts → extraction/consolidation.ts} +63 -154
  50. package/{extractor.ts → extraction/extractor.ts} +18 -17
  51. package/extraction/importance-filter.ts +50 -0
  52. package/{semantic-dedup.ts → extraction/semantic-dedup.ts} +2 -2
  53. package/{batch-gate.ts → import/batch-gate.ts} +1 -1
  54. package/import/import-runtime.ts +853 -0
  55. package/import-adapters/chatgpt-adapter.ts +7 -7
  56. package/import-adapters/gemini-adapter.ts +13 -13
  57. package/import-adapters/mem0-adapter.ts +10 -10
  58. package/index.ts +129 -1377
  59. package/{llm-client.ts → llm/llm-client.ts} +2 -2
  60. package/{hot-cache-wrapper.ts → memory/hot-cache-wrapper.ts} +5 -5
  61. package/{memory-runtime.ts → memory/memory-runtime.ts} +1 -1
  62. package/{pin.ts → memory/pin.ts} +20 -20
  63. package/{retype-setscope.ts → memory/retype-setscope.ts} +21 -21
  64. package/{tool-gating.ts → memory/tool-gating.ts} +1 -1
  65. package/package.json +6 -6
  66. package/{credential-provider.ts → pairing/credential-provider.ts} +2 -2
  67. package/{first-run.ts → pairing/first-run.ts} +1 -1
  68. package/{onboarding-cli.ts → pairing/onboarding-cli.ts} +20 -20
  69. package/{pair-cli-relay.ts → pairing/pair-cli-relay.ts} +10 -10
  70. package/{pair-crypto.ts → pairing/pair-crypto.ts} +1 -1
  71. package/{pair-http.ts → pairing/pair-http.ts} +3 -3
  72. package/{pair-remote-client.ts → pairing/pair-remote-client.ts} +10 -10
  73. package/{pair-session-store.ts → pairing/pair-session-store.ts} +4 -4
  74. package/runtime/config-schema.ts +64 -0
  75. package/runtime/format-helpers.ts +293 -0
  76. package/runtime/types.ts +118 -0
  77. package/skill.json +44 -10
  78. package/{confirm-indexed.ts → subgraph/confirm-indexed.ts} +1 -1
  79. package/{subgraph-search.ts → subgraph/subgraph-search.ts} +14 -14
  80. package/{subgraph-store.ts → subgraph/subgraph-store.ts} +10 -10
  81. package/dist/consolidation.js +0 -249
  82. /package/{api-client.ts → billing/api-client.ts} +0 -0
  83. /package/{gateway-url.ts → billing/gateway-url.ts} +0 -0
  84. /package/{inbound-user-tracker.ts → billing/inbound-user-tracker.ts} +0 -0
  85. /package/{relay.ts → billing/relay.ts} +0 -0
  86. /package/{crypto.ts → crypto/crypto.ts} +0 -0
  87. /package/{vault-crypto.ts → crypto/vault-crypto.ts} +0 -0
  88. /package/dist/{api-client.js → billing/api-client.js} +0 -0
  89. /package/dist/{gateway-url.js → billing/gateway-url.js} +0 -0
  90. /package/dist/{inbound-user-tracker.js → billing/inbound-user-tracker.js} +0 -0
  91. /package/dist/{relay.js → billing/relay.js} +0 -0
  92. /package/dist/{crypto.js → crypto/crypto.js} +0 -0
  93. /package/dist/{vault-crypto.js → crypto/vault-crypto.js} +0 -0
  94. /package/dist/{embedder-cache.js → embedding/embedder-cache.js} +0 -0
  95. /package/dist/{embedder-loader.js → embedding/embedder-loader.js} +0 -0
  96. /package/dist/{embedder-network.js → embedding/embedder-network.js} +0 -0
  97. /package/dist/{embedding.js → embedding/embedding.js} +0 -0
  98. /package/dist/{lsh.js → embedding/lsh.js} +0 -0
  99. /package/dist/{import-state-manager.js → import/import-state-manager.js} +0 -0
  100. /package/dist/{llm-profile-reader.js → llm/llm-profile-reader.js} +0 -0
  101. /package/dist/{native-memory.js → memory/native-memory.js} +0 -0
  102. /package/dist/{tool-gating.js → memory/tool-gating.js} +0 -0
  103. /package/dist/{tools.js → memory/tools.js} +0 -0
  104. /package/dist/{generate-mnemonic.js → pairing/generate-mnemonic.js} +0 -0
  105. /package/dist/{pair-cli.js → pairing/pair-cli.js} +0 -0
  106. /package/dist/{pair-page.js → pairing/pair-page.js} +0 -0
  107. /package/dist/{pair-qr.js → pairing/pair-qr.js} +0 -0
  108. /package/dist/{restart-auth.js → pairing/restart-auth.js} +0 -0
  109. /package/dist/{qa-bug-report.js → setup/qa-bug-report.js} +0 -0
  110. /package/dist/{skill-register.js → setup/skill-register.js} +0 -0
  111. /package/dist/{trajectory-poller.js → subgraph/trajectory-poller.js} +0 -0
  112. /package/{embedder-cache.ts → embedding/embedder-cache.ts} +0 -0
  113. /package/{embedder-loader.ts → embedding/embedder-loader.ts} +0 -0
  114. /package/{embedder-network.ts → embedding/embedder-network.ts} +0 -0
  115. /package/{embedding.ts → embedding/embedding.ts} +0 -0
  116. /package/{lsh.ts → embedding/lsh.ts} +0 -0
  117. /package/{import-state-manager.ts → import/import-state-manager.ts} +0 -0
  118. /package/{llm-profile-reader.ts → llm/llm-profile-reader.ts} +0 -0
  119. /package/{native-memory.ts → memory/native-memory.ts} +0 -0
  120. /package/{tools.ts → memory/tools.ts} +0 -0
  121. /package/{generate-mnemonic.ts → pairing/generate-mnemonic.ts} +0 -0
  122. /package/{pair-cli.ts → pairing/pair-cli.ts} +0 -0
  123. /package/{pair-page.ts → pairing/pair-page.ts} +0 -0
  124. /package/{pair-qr.ts → pairing/pair-qr.ts} +0 -0
  125. /package/{restart-auth.ts → pairing/restart-auth.ts} +0 -0
  126. /package/{qa-bug-report.ts → setup/qa-bug-report.ts} +0 -0
  127. /package/{skill-register.ts → setup/skill-register.ts} +0 -0
  128. /package/{trajectory-poller.ts → subgraph/trajectory-poller.ts} +0 -0
@@ -0,0 +1,293 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Pure formatting / resolution helpers
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Extracted from index.ts. Every function here is a pure computation over its
6
+ // arguments (plus imported constants / stateless helpers) — none reads the
7
+ // plugin's mutable session state, so they carve cleanly out of the composing
8
+ // entry point. No environment-variable reads live here (they stay in config.ts /
9
+ // entry.ts per the OpenClaw env-harvesting scanner rule).
10
+
11
+ import { encrypt, decrypt } from '../crypto/crypto.js';
12
+ import { detectGatewayHost } from '../billing/gateway-url.js';
13
+ import { isRunningInDocker } from '../fs-helpers.js';
14
+ import { readBillingCache } from '../billing/billing-cache.js';
15
+ import type { GatewayMode } from '../pairing/first-run.js';
16
+ import type { OpenClawPluginApi } from './types.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Human-friendly error messages
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Translate technical error messages from the on-chain submission pipeline
24
+ * into user-friendly messages. The original technical details are still
25
+ * logged via api.logger — this only affects what the agent sees.
26
+ */
27
+ export function humanizeError(rawMessage: string): string {
28
+ if (rawMessage.includes('AA23')) {
29
+ return 'Memory storage temporarily unavailable. Will retry next time.';
30
+ }
31
+ if (rawMessage.includes('AA10')) {
32
+ return 'Please wait a moment before storing more memories.';
33
+ }
34
+ if (rawMessage.includes('AA25')) {
35
+ return 'Memory storage busy. Will retry.';
36
+ }
37
+ if (rawMessage.includes('pm_sponsorUserOperation')) {
38
+ return 'Memory storage service temporarily unavailable.';
39
+ }
40
+ if (/Relay returned HTTP\s*404/.test(rawMessage)) {
41
+ return 'Memory service is temporarily offline.';
42
+ }
43
+ if (/Relay returned HTTP\s*5\d\d/.test(rawMessage)) {
44
+ return 'Memory service encountered a temporary error. Will retry next time.';
45
+ }
46
+ // Pass through non-technical messages as-is.
47
+ return rawMessage;
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // 3.3.0 — pairing URL resolution
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Build the full pairing URL (including `#pk=` fragment) for a fresh
56
+ * pairing session. Pulls gateway config from `api.config.gateway`.
57
+ *
58
+ * Resolution order (3.3.1 — six-layer cascade):
59
+ * 1. `plugins.entries.totalreclaw.config.publicUrl` — explicit override
60
+ * 2. `gateway.remote.url` — OpenClaw's own remote-gateway URL
61
+ * 3. `gateway.bind === 'custom'` + `gateway.customBindHost` + port
62
+ * 4. Tailscale auto-detect — `tailscale status --json` → `https://<MagicDNS>`
63
+ * (assumes `tailscale serve` proxies to the gateway port on 443)
64
+ * 5. LAN auto-detect — first non-loopback, non-virtual IPv4 interface.
65
+ * Emits a warning: "only works on the same network".
66
+ * 6. Fallback `http://localhost:<port>` — warns with a pointer to
67
+ * configure `plugins.entries.totalreclaw.config.publicUrl`.
68
+ *
69
+ * Always returns a working URL string; never throws. The caller (CLI or
70
+ * JSON output) prints whatever we give it.
71
+ */
72
+ export function buildPairingUrl(
73
+ api: Pick<OpenClawPluginApi, 'config' | 'pluginConfig' | 'logger'>,
74
+ session: { sid: string; pkGatewayB64: string },
75
+ ): string {
76
+ const cfg = api.config as {
77
+ gateway?: {
78
+ port?: number;
79
+ bind?: string;
80
+ customBindHost?: string;
81
+ tls?: { enabled?: boolean };
82
+ remote?: { url?: string };
83
+ };
84
+ } | undefined;
85
+ const pluginCfg = (api.pluginConfig ?? {}) as { publicUrl?: string };
86
+
87
+ const tlsEnabled = cfg?.gateway?.tls?.enabled === true;
88
+ const scheme = tlsEnabled ? 'https' : 'http';
89
+ const port = cfg?.gateway?.port ?? 18789;
90
+
91
+ let base: string;
92
+
93
+ // Layer 1 — explicit user override
94
+ if (typeof pluginCfg.publicUrl === 'string' && pluginCfg.publicUrl.trim()) {
95
+ base = pluginCfg.publicUrl.replace(/\/+$/, '');
96
+ base = base.replace(/^wss:\/\//i, 'https://').replace(/^ws:\/\//i, 'http://');
97
+ }
98
+ // Layer 2 — OpenClaw gateway remote URL
99
+ else if (typeof cfg?.gateway?.remote?.url === 'string' && cfg.gateway.remote.url.trim()) {
100
+ base = cfg.gateway.remote.url.trim().replace(/\/+$/, '');
101
+ base = base.replace(/^wss:\/\//i, 'https://').replace(/^ws:\/\//i, 'http://');
102
+ }
103
+ // Layer 3 — gateway.bind = custom + explicit customBindHost
104
+ else if (cfg?.gateway?.bind === 'custom' && cfg.gateway.customBindHost) {
105
+ base = `${scheme}://${cfg.gateway.customBindHost}:${port}`;
106
+ }
107
+ // Layers 4 + 5 — auto-detect via gateway-url helper (Tailscale CGNAT, then LAN)
108
+ else {
109
+ let detected: ReturnType<typeof detectGatewayHost> = null;
110
+ // issue #110 fix 4 — pass `isDocker` so LAN detection skips
111
+ // 172.16/12 bridge IPs that no external browser can reach.
112
+ let isDocker = false;
113
+ try {
114
+ isDocker = isRunningInDocker();
115
+ } catch {
116
+ // Defensive: never block URL building on Docker sniff errors.
117
+ isDocker = false;
118
+ }
119
+ try {
120
+ detected = detectGatewayHost({ isDocker });
121
+ } catch (err) {
122
+ api.logger.warn(
123
+ `TotalReclaw: host autodetect crashed: ${err instanceof Error ? err.message : String(err)} — falling back to localhost`,
124
+ );
125
+ }
126
+ if (detected?.kind === 'tailscale') {
127
+ // 3.3.1-rc.2: we surface the raw Tailscale CGNAT IP because passive
128
+ // NIC detection (no subprocess) cannot resolve the MagicDNS name.
129
+ // Caller can override via `publicUrl` for a proper https://<magicdns>.
130
+ // The IP + port URL still works inside the tailnet (peers can reach
131
+ // each other by CGNAT IP directly). TLS defaults to the gateway's
132
+ // own config because we no longer assume `tailscale serve`.
133
+ base = `${scheme}://${detected.host}:${port}`;
134
+ api.logger.warn(
135
+ `TotalReclaw: pairing URL using Tailscale CGNAT IP ${detected.host}:${port} — ` +
136
+ detected.note,
137
+ );
138
+ } else if (detected?.kind === 'lan') {
139
+ base = `${scheme}://${detected.host}:${port}`;
140
+ api.logger.warn(
141
+ `TotalReclaw: pairing URL using LAN host ${detected.host}:${port} — ` +
142
+ `this URL only works from the same network. ` +
143
+ `Set plugins.entries.totalreclaw.config.publicUrl for remote access.`,
144
+ );
145
+ } else {
146
+ // Layer 6 — localhost fallback (or Docker-aware relay-pointer warning)
147
+ const bind = cfg?.gateway?.bind;
148
+ if (isDocker) {
149
+ // issue #110 fix 4: inside Docker the LAN IP is container-internal
150
+ // and useless. Loopback localhost only works for `docker exec`
151
+ // tests. The CORRECT pair URL for Docker is the relay-brokered
152
+ // path served by `tr pair` / the `/plugin/totalreclaw/pair/*` HTTP
153
+ // routes (CONFIG.pairMode === 'relay' since rc.11). The CLI-only
154
+ // path here cannot mint a relay session synchronously (the relay
155
+ // handshake needs a WS round-trip), so we emit the loopback URL
156
+ // with a LOUD warning pointing the operator at the pair CLI /
157
+ // publicUrl override.
158
+ api.logger.warn(
159
+ `TotalReclaw: Docker container detected — pairing URL falling back to ` +
160
+ `http://localhost:${port}, which is unreachable from the host browser. ` +
161
+ `Run \`tr pair --url-pin\` (or \`openclaw totalreclaw pair generate --url-pin-only\`) ` +
162
+ `on the gateway host to mint a relay-brokered pair URL that reaches the host browser, ` +
163
+ `OR set plugins.entries.totalreclaw.config.publicUrl ` +
164
+ `to your gateway's host-reachable URL (e.g. http://<host-ip>:${port} when the ` +
165
+ `Docker port is published). Setting TOTALRECLAW_PAIR_MODE=relay is the default; ` +
166
+ `air-gapped operators on TOTALRECLAW_PAIR_MODE=local must publish a port + set publicUrl.`,
167
+ );
168
+ } else if (bind === 'lan' || bind === 'tailnet') {
169
+ api.logger.warn(
170
+ `TotalReclaw: pairing URL falling back to localhost because gateway.bind=${bind} could not be autodetected. ` +
171
+ 'Set plugins.entries.totalreclaw.config.publicUrl to override.',
172
+ );
173
+ } else {
174
+ api.logger.warn(
175
+ `TotalReclaw: pairing URL fell back to http://localhost:${port} — this URL only works on this machine. ` +
176
+ `Configure plugins.entries.totalreclaw.config.publicUrl for remote access.`,
177
+ );
178
+ }
179
+ base = `${scheme}://localhost:${port}`;
180
+ }
181
+ }
182
+
183
+ return `${base}/plugin/totalreclaw/pair/finish?sid=${encodeURIComponent(session.sid)}#pk=${encodeURIComponent(session.pkGatewayB64)}`;
184
+ }
185
+
186
+ /**
187
+ * Resolve whether this plugin is running on a `local` or `remote` gateway.
188
+ *
189
+ * Follows the same config surface `buildPairingUrl` uses:
190
+ * - `pluginConfig.publicUrl` set + non-localhost → remote
191
+ * - `gateway.remote.url` set + non-localhost → remote
192
+ * - `gateway.bind === 'lan' | 'tailnet' | 'custom'` → remote
193
+ * - anything else → local
194
+ *
195
+ * We treat a `publicUrl` or `remote.url` that points at `localhost` /
196
+ * `127.*` as local because that is what a dev-loopback override looks like;
197
+ * no one publishes a remote QR pairing for localhost.
198
+ */
199
+ export function resolveGatewayMode(
200
+ api: Pick<OpenClawPluginApi, 'config' | 'pluginConfig'>,
201
+ ): GatewayMode {
202
+ const cfg = api.config as
203
+ | { gateway?: { bind?: string; remote?: { url?: string } } }
204
+ | undefined;
205
+ const pluginCfg = (api.pluginConfig ?? {}) as { publicUrl?: string };
206
+ const looksLocal = (url: string | undefined): boolean => {
207
+ if (!url) return true;
208
+ const u = url.trim().toLowerCase();
209
+ if (u === '') return true;
210
+ return /^(?:wss?:\/\/|https?:\/\/)?(?:localhost|127\.|0\.0\.0\.0)/.test(u);
211
+ };
212
+ if (typeof pluginCfg.publicUrl === 'string' && !looksLocal(pluginCfg.publicUrl)) {
213
+ return 'remote';
214
+ }
215
+ const remoteUrl = cfg?.gateway?.remote?.url;
216
+ if (typeof remoteUrl === 'string' && !looksLocal(remoteUrl)) {
217
+ return 'remote';
218
+ }
219
+ const bind = cfg?.gateway?.bind;
220
+ if (bind === 'lan' || bind === 'tailnet' || bind === 'custom') {
221
+ return 'remote';
222
+ }
223
+ return 'local';
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Candidate-pool sizing
228
+ // ---------------------------------------------------------------------------
229
+
230
+ /**
231
+ * Compute the candidate pool size from a fact count.
232
+ *
233
+ * Server-side config takes priority (from billing cache), then local fallback.
234
+ * The server computes the optimal pool based on vault size and tier caps.
235
+ *
236
+ * Local fallback formula: pool = min(max(factCount * 3, 400), 5000)
237
+ * - At least 400 candidates (even for tiny vaults)
238
+ * - At most 5000 candidates (to bound decryption + reranking cost)
239
+ * - 3x fact count in between
240
+ */
241
+ export function computeCandidatePool(factCount: number): number {
242
+ const cache = readBillingCache();
243
+ if (cache?.features?.max_candidate_pool != null) return cache.features.max_candidate_pool;
244
+ // Fallback to local formula if no server config
245
+ return Math.min(Math.max(factCount * 3, 400), 5000);
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Encrypt/decrypt hex helpers (on-chain blob encoding)
250
+ // ---------------------------------------------------------------------------
251
+
252
+ export function encryptToHex(plaintext: string, key: Buffer): string {
253
+ const b64 = encrypt(plaintext, key);
254
+ return Buffer.from(b64, 'base64').toString('hex');
255
+ }
256
+
257
+ export function decryptFromHex(hexBlob: string, key: Buffer): string {
258
+ const hex = hexBlob.startsWith('0x') ? hexBlob.slice(2) : hexBlob;
259
+ const b64 = Buffer.from(hex, 'hex').toString('base64');
260
+ return decrypt(b64, key);
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Lexical scoring / relative time
265
+ // ---------------------------------------------------------------------------
266
+
267
+ export function textScore(query: string, docText: string): number {
268
+ const queryWords = new Set(
269
+ query.toLowerCase().split(/\s+/).filter((w) => w.length >= 2),
270
+ );
271
+ const docWords = docText.toLowerCase().split(/\s+/);
272
+ let score = 0;
273
+ for (const word of docWords) {
274
+ if (queryWords.has(word)) score++;
275
+ }
276
+ return score;
277
+ }
278
+
279
+ /**
280
+ * Format a relative time string (e.g. "2 hours ago").
281
+ */
282
+ export function relativeTime(isoOrMs: string | number): string {
283
+ const ms = typeof isoOrMs === 'number' ? isoOrMs : new Date(isoOrMs).getTime();
284
+ const diffMs = Date.now() - ms;
285
+ const seconds = Math.floor(diffMs / 1000);
286
+ if (seconds < 60) return 'just now';
287
+ const minutes = Math.floor(seconds / 60);
288
+ if (minutes < 60) return `${minutes}m ago`;
289
+ const hours = Math.floor(minutes / 60);
290
+ if (hours < 24) return `${hours}h ago`;
291
+ const days = Math.floor(hours / 24);
292
+ return `${days}d ago`;
293
+ }
@@ -0,0 +1,118 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared plugin runtime types
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Extracted from index.ts so the composing entry point AND the domain modules
6
+ // carved out of it (runtime/format-helpers, import/import-runtime, …) can share
7
+ // one definition of the OpenClaw plugin-API surface and the internal row shapes
8
+ // without a circular import back through index.ts.
9
+ //
10
+ // Nothing here reads the environment or performs I/O — pure type declarations.
11
+
12
+ /** OpenClaw Plugin API type (defined locally to avoid an SDK dependency). */
13
+ export interface OpenClawPluginApi {
14
+ logger: {
15
+ info(...args: unknown[]): void;
16
+ warn(...args: unknown[]): void;
17
+ error(...args: unknown[]): void;
18
+ };
19
+ config?: {
20
+ agents?: {
21
+ defaults?: {
22
+ model?: {
23
+ primary?: string;
24
+ };
25
+ };
26
+ };
27
+ models?: {
28
+ providers?: Record<string, {
29
+ baseUrl: string;
30
+ apiKey?: string;
31
+ api?: string;
32
+ models?: Array<{ id: string; [k: string]: unknown }>;
33
+ [k: string]: unknown;
34
+ }>;
35
+ [k: string]: unknown;
36
+ };
37
+ [key: string]: unknown;
38
+ };
39
+ pluginConfig?: Record<string, unknown>;
40
+ registerTool(tool: unknown, opts?: { name?: string; names?: string[] }): void;
41
+ registerService(service: { id: string; start(): void; stop?(): void }): void;
42
+ on(hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }): void;
43
+ /**
44
+ * 3.2.0 — register a top-level `openclaw <cmd>` subcommand. The handler
45
+ * receives a commander `Command` to attach subcommands to. Output goes
46
+ * straight to the user's TTY; nothing touches the LLM or the transcript.
47
+ * We deliberately type `program` as `unknown` at this boundary because
48
+ * we don't import the SDK's full types; the runtime shape is commander's
49
+ * `Command` which we cast at the call site.
50
+ */
51
+ registerCli?(
52
+ registrar: (ctx: { program: unknown; config?: unknown; workspaceDir?: string; logger?: unknown }) => void | Promise<void>,
53
+ opts?: { commands?: string[] },
54
+ ): void;
55
+ /**
56
+ * 3.2.0 — register a slash command (e.g. `/totalreclaw`). The handler
57
+ * runs before the agent; its reply is delivered via the channel adapter.
58
+ * Reply text IS appended to the session transcript (see gateway-cli
59
+ * L9300-9312), so we only emit non-secret pointers.
60
+ */
61
+ registerCommand?(command: {
62
+ name: string;
63
+ description: string;
64
+ acceptsArgs?: boolean;
65
+ requireAuth?: boolean;
66
+ handler: (ctx: {
67
+ senderId?: string;
68
+ channel?: string;
69
+ args?: string;
70
+ commandBody?: string;
71
+ isAuthorizedSender?: boolean;
72
+ config?: unknown;
73
+ }) => { text: string } | Promise<{ text: string }>;
74
+ }): void;
75
+ /**
76
+ * 3.3.0 — register an HTTP route on the gateway's HTTP server.
77
+ * Used by the QR-pairing flow to serve the pairing page + the
78
+ * encrypted-payload respond endpoint. Path is exact-match against
79
+ * `new URL(req.url, ...).pathname`; no params supported.
80
+ */
81
+ registerHttpRoute?(params: {
82
+ path: string;
83
+ handler: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void> | void;
84
+ /** OpenClaw 2026.4.2+ — required; loader silently drops the route if absent. */
85
+ auth: 'gateway' | 'plugin';
86
+ }): void;
87
+ }
88
+
89
+ /** Row shape returned by the migration/export GraphQL helpers. */
90
+ export interface MigrationFact {
91
+ id: string;
92
+ owner: string;
93
+ encryptedBlob: string;
94
+ encryptedEmbedding: string | null;
95
+ decayScore: string;
96
+ isActive: boolean;
97
+ contentFp: string;
98
+ source: string;
99
+ agentId: string;
100
+ version: number;
101
+ timestamp: string;
102
+ }
103
+
104
+ /** Smart import result containing profile, triage decisions, and enriched system prompt. */
105
+ export interface SmartImportContext {
106
+ /** JSON-serialized UserProfile (for WASM calls that require profile_json) */
107
+ profileJson: string;
108
+ /** Triage decisions indexed by chunk_index */
109
+ decisions: Array<{ chunk_index: number; decision: string; reason: string }>;
110
+ /** Enriched system prompt for extraction (profile context injected) */
111
+ enrichedSystemPrompt: string;
112
+ /** Number of chunks marked for extraction */
113
+ extractCount: number;
114
+ /** Number of chunks marked for skipping */
115
+ skipCount: number;
116
+ /** Duration of the profiling + triage pipeline in ms */
117
+ durationMs: number;
118
+ }
package/skill.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totalreclaw",
3
- "version": "3.3.12-rc.23",
3
+ "version": "3.3.13",
4
4
  "description": "End-to-end encrypted memory for AI agents — portable, yours forever. XChaCha20-Poly1305 E2EE: server never sees plaintext.",
5
5
  "author": "TotalReclaw Team",
6
6
  "license": "MIT",
@@ -24,7 +24,10 @@
24
24
  "mode": {
25
25
  "type": "string",
26
26
  "required": false,
27
- "enum": ["generate", "import"],
27
+ "enum": [
28
+ "generate",
29
+ "import"
30
+ ],
28
31
  "default": "generate",
29
32
  "description": "\"generate\" = browser will create a NEW recovery phrase. \"import\" = browser will accept an EXISTING phrase pasted by the user in their browser (never through chat)."
30
33
  }
@@ -43,7 +46,13 @@
43
46
  "type": {
44
47
  "type": "string",
45
48
  "required": false,
46
- "enum": ["fact", "preference", "decision", "episodic", "goal"],
49
+ "enum": [
50
+ "fact",
51
+ "preference",
52
+ "decision",
53
+ "episodic",
54
+ "goal"
55
+ ],
47
56
  "default": "fact",
48
57
  "description": "Type of memory"
49
58
  },
@@ -95,7 +104,10 @@
95
104
  "format": {
96
105
  "type": "string",
97
106
  "required": false,
98
- "enum": ["json", "markdown"],
107
+ "enum": [
108
+ "json",
109
+ "markdown"
110
+ ],
99
111
  "default": "json",
100
112
  "description": "Export format"
101
113
  }
@@ -121,7 +133,10 @@
121
133
  "source": {
122
134
  "type": "string",
123
135
  "required": true,
124
- "enum": ["mem0", "mcp-memory"],
136
+ "enum": [
137
+ "mem0",
138
+ "mcp-memory"
139
+ ],
125
140
  "description": "Source system to import from"
126
141
  }
127
142
  }
@@ -149,7 +164,11 @@
149
164
  "bins": []
150
165
  },
151
166
  "emoji": "🧠",
152
- "os": ["macos", "linux", "windows"],
167
+ "os": [
168
+ "macos",
169
+ "linux",
170
+ "windows"
171
+ ],
153
172
  "hooks": {
154
173
  "before_agent_start": {
155
174
  "priority": 10,
@@ -181,7 +200,13 @@
181
200
  "type": {
182
201
  "type": "string",
183
202
  "required": false,
184
- "enum": ["fact", "preference", "decision", "episodic", "goal"],
203
+ "enum": [
204
+ "fact",
205
+ "preference",
206
+ "decision",
207
+ "episodic",
208
+ "goal"
209
+ ],
185
210
  "default": "fact",
186
211
  "description": "Type of memory"
187
212
  },
@@ -230,7 +255,10 @@
230
255
  "format": {
231
256
  "type": "string",
232
257
  "required": false,
233
- "enum": ["json", "markdown"],
258
+ "enum": [
259
+ "json",
260
+ "markdown"
261
+ ],
234
262
  "default": "json",
235
263
  "description": "Export format"
236
264
  }
@@ -253,7 +281,10 @@
253
281
  "source": {
254
282
  "type": "string",
255
283
  "required": true,
256
- "enum": ["mem0", "mcp-memory"],
284
+ "enum": [
285
+ "mem0",
286
+ "mcp-memory"
287
+ ],
257
288
  "description": "Source system to import from"
258
289
  },
259
290
  "api_key": {
@@ -309,7 +340,10 @@
309
340
  "mode": {
310
341
  "type": "string",
311
342
  "required": false,
312
- "enum": ["generate", "import"],
343
+ "enum": [
344
+ "generate",
345
+ "import"
346
+ ],
313
347
  "default": "generate",
314
348
  "description": "\"generate\" = browser will create a NEW recovery phrase. \"import\" = browser will accept an EXISTING phrase pasted by the user in their browser (never through chat)."
315
349
  }
@@ -28,7 +28,7 @@
28
28
 
29
29
  import { createRequire } from 'node:module';
30
30
  import { getSubgraphConfig } from './subgraph-store.js';
31
- import { buildRelayHeaders } from './relay-headers.js';
31
+ import { buildRelayHeaders } from '../billing/relay-headers.js';
32
32
 
33
33
  /**
34
34
  * The four `wasmConfirmIndexed*` exports below ship in `@totalreclaw/core@2.3.x`
@@ -21,9 +21,9 @@
21
21
  */
22
22
 
23
23
  import { getSubgraphConfig } from './subgraph-store.js';
24
- import { CONFIG } from './config.js';
25
- import { buildRelayHeaders } from './relay-headers.js';
26
- import { relayFetch } from './relay.js';
24
+ import { CONFIG } from '../config.js';
25
+ import { buildRelayHeaders } from '../billing/relay-headers.js';
26
+ import { relayFetch } from '../billing/relay.js';
27
27
 
28
28
  export interface SubgraphSearchFact {
29
29
  id: string;
@@ -165,13 +165,13 @@ async function paginateChunk(
165
165
  ): Promise<void> {
166
166
  let lastId = '';
167
167
  while (allResults.size < maxCandidates) {
168
- const data = await gqlQuery<SearchResponse>(
168
+ const subgraphResponse = await gqlQuery<SearchResponse>(
169
169
  subgraphUrl,
170
170
  PAGINATE_QUERY,
171
171
  { trapdoors: chunk, owner, first: PAGE_SIZE, lastId },
172
172
  authKeyHex,
173
173
  );
174
- const entries = data?.blindIndexes ?? [];
174
+ const entries = subgraphResponse?.blindIndexes ?? [];
175
175
  if (entries.length === 0) break;
176
176
  collectFacts(entries, allResults);
177
177
  if (entries.length < PAGE_SIZE) break;
@@ -234,13 +234,13 @@ export async function searchSubgraph(
234
234
 
235
235
  const batchResults = await Promise.all(
236
236
  chunks.map(async (chunk) => {
237
- const data = await gqlQuery<SearchResponse>(
237
+ const subgraphResponse = await gqlQuery<SearchResponse>(
238
238
  subgraphUrl,
239
239
  SEARCH_QUERY,
240
240
  { trapdoors: chunk, owner, first: PAGE_SIZE },
241
241
  authKeyHex,
242
242
  );
243
- return { chunk, entries: data?.blindIndexes ?? [] };
243
+ return { chunk, entries: subgraphResponse?.blindIndexes ?? [] };
244
244
  }),
245
245
  );
246
246
 
@@ -298,14 +298,14 @@ export async function searchSubgraphBroadened(
298
298
  }
299
299
  `;
300
300
 
301
- const data = await gqlQuery<{ facts?: SubgraphSearchFact[] }>(
301
+ const subgraphResponse = await gqlQuery<{ facts?: SubgraphSearchFact[] }>(
302
302
  subgraphUrl,
303
303
  query,
304
304
  { owner, first: Math.min(maxCandidates, 1000) },
305
305
  authKeyHex,
306
306
  );
307
307
 
308
- return (data?.facts ?? []).filter(f => f.isActive !== false);
308
+ return (subgraphResponse?.facts ?? []).filter(f => f.isActive !== false);
309
309
  }
310
310
 
311
311
  /**
@@ -343,14 +343,14 @@ export async function fetchFactById(
343
343
  }
344
344
  `;
345
345
 
346
- const data = await gqlQuery<{ fact?: (SubgraphSearchFact & { owner: string }) | null }>(
346
+ const subgraphResponse = await gqlQuery<{ fact?: (SubgraphSearchFact & { owner: string }) | null }>(
347
347
  subgraphUrl,
348
348
  query,
349
349
  { id: factId },
350
350
  authKeyHex,
351
351
  );
352
352
 
353
- const fact = data?.fact;
353
+ const fact = subgraphResponse?.fact;
354
354
  if (!fact) return null;
355
355
  if (fact.isActive === false) return null;
356
356
  if (typeof fact.owner === 'string' && fact.owner.toLowerCase() !== owner.toLowerCase()) {
@@ -376,15 +376,15 @@ export async function getSubgraphFactCount(owner: string, authKeyHex?: string):
376
376
  }
377
377
  `;
378
378
 
379
- const data = await gqlQuery<{ globalStates?: Array<{ totalFacts: string }> }>(
379
+ const subgraphResponse = await gqlQuery<{ globalStates?: Array<{ totalFacts: string }> }>(
380
380
  subgraphUrl,
381
381
  query,
382
382
  {},
383
383
  authKeyHex,
384
384
  );
385
385
 
386
- if (data?.globalStates && data.globalStates.length > 0) {
387
- const count = parseInt(data.globalStates[0].totalFacts, 10);
386
+ if (subgraphResponse?.globalStates && subgraphResponse.globalStates.length > 0) {
387
+ const count = parseInt(subgraphResponse.globalStates[0].totalFacts, 10);
388
388
  return isNaN(count) ? 0 : count;
389
389
  }
390
390