@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.
- package/SKILL.md +25 -8
- package/{billing-cache.ts → billing/billing-cache.ts} +1 -1
- package/{relay-headers.ts → billing/relay-headers.ts} +1 -1
- package/{tr-cli-export-helper.ts → cli/tr-cli-export-helper.ts} +5 -5
- package/{tr-cli.ts → cli/tr-cli.ts} +20 -20
- package/{contradiction-sync.ts → contradiction/contradiction-sync.ts} +13 -13
- package/{digest-sync.ts → digest/digest-sync.ts} +32 -2
- package/dist/{billing-cache.js → billing/billing-cache.js} +1 -1
- package/dist/{relay-headers.js → billing/relay-headers.js} +1 -1
- package/dist/{tr-cli-export-helper.js → cli/tr-cli-export-helper.js} +5 -5
- package/dist/{tr-cli.js → cli/tr-cli.js} +19 -19
- package/dist/{contradiction-sync.js → contradiction/contradiction-sync.js} +13 -13
- package/dist/{digest-sync.js → digest/digest-sync.js} +32 -2
- package/dist/{download-ux.js → embedding/download-ux.js} +1 -1
- package/dist/{reranker.js → embedding/reranker.js} +13 -4
- package/dist/{claims-helper.js → extraction/claims-helper.js} +54 -58
- package/dist/extraction/consolidation.js +170 -0
- package/dist/{extractor.js → extraction/extractor.js} +17 -17
- package/dist/extraction/importance-filter.js +38 -0
- package/dist/{semantic-dedup.js → extraction/semantic-dedup.js} +2 -2
- package/dist/{batch-gate.js → import/batch-gate.js} +1 -1
- package/dist/import/import-runtime.js +692 -0
- package/dist/import-adapters/chatgpt-adapter.js +7 -7
- package/dist/import-adapters/gemini-adapter.js +13 -13
- package/dist/import-adapters/mem0-adapter.js +10 -10
- package/dist/index.js +111 -1094
- package/dist/{llm-client.js → llm/llm-client.js} +2 -2
- package/dist/{hot-cache-wrapper.js → memory/hot-cache-wrapper.js} +5 -5
- package/dist/{memory-runtime.js → memory/memory-runtime.js} +1 -1
- package/dist/{pin.js → memory/pin.js} +18 -18
- package/dist/{retype-setscope.js → memory/retype-setscope.js} +19 -19
- package/dist/{credential-provider.js → pairing/credential-provider.js} +2 -2
- package/dist/{first-run.js → pairing/first-run.js} +1 -1
- package/dist/{onboarding-cli.js → pairing/onboarding-cli.js} +20 -20
- package/dist/{pair-cli-relay.js → pairing/pair-cli-relay.js} +10 -10
- package/dist/{pair-crypto.js → pairing/pair-crypto.js} +1 -1
- package/dist/{pair-http.js → pairing/pair-http.js} +3 -3
- package/dist/{pair-remote-client.js → pairing/pair-remote-client.js} +10 -10
- package/dist/{pair-session-store.js → pairing/pair-session-store.js} +4 -4
- package/dist/runtime/config-schema.js +59 -0
- package/dist/runtime/format-helpers.js +257 -0
- package/dist/runtime/types.js +11 -0
- package/dist/{confirm-indexed.js → subgraph/confirm-indexed.js} +1 -1
- package/dist/{subgraph-search.js → subgraph/subgraph-search.js} +14 -14
- package/dist/{subgraph-store.js → subgraph/subgraph-store.js} +6 -6
- package/{download-ux.ts → embedding/download-ux.ts} +1 -1
- package/{reranker.ts → embedding/reranker.ts} +13 -4
- package/{claims-helper.ts → extraction/claims-helper.ts} +52 -60
- package/{consolidation.ts → extraction/consolidation.ts} +63 -154
- package/{extractor.ts → extraction/extractor.ts} +18 -17
- package/extraction/importance-filter.ts +50 -0
- package/{semantic-dedup.ts → extraction/semantic-dedup.ts} +2 -2
- package/{batch-gate.ts → import/batch-gate.ts} +1 -1
- package/import/import-runtime.ts +853 -0
- package/import-adapters/chatgpt-adapter.ts +7 -7
- package/import-adapters/gemini-adapter.ts +13 -13
- package/import-adapters/mem0-adapter.ts +10 -10
- package/index.ts +129 -1377
- package/{llm-client.ts → llm/llm-client.ts} +2 -2
- package/{hot-cache-wrapper.ts → memory/hot-cache-wrapper.ts} +5 -5
- package/{memory-runtime.ts → memory/memory-runtime.ts} +1 -1
- package/{pin.ts → memory/pin.ts} +20 -20
- package/{retype-setscope.ts → memory/retype-setscope.ts} +21 -21
- package/{tool-gating.ts → memory/tool-gating.ts} +1 -1
- package/package.json +6 -6
- package/{credential-provider.ts → pairing/credential-provider.ts} +2 -2
- package/{first-run.ts → pairing/first-run.ts} +1 -1
- package/{onboarding-cli.ts → pairing/onboarding-cli.ts} +20 -20
- package/{pair-cli-relay.ts → pairing/pair-cli-relay.ts} +10 -10
- package/{pair-crypto.ts → pairing/pair-crypto.ts} +1 -1
- package/{pair-http.ts → pairing/pair-http.ts} +3 -3
- package/{pair-remote-client.ts → pairing/pair-remote-client.ts} +10 -10
- package/{pair-session-store.ts → pairing/pair-session-store.ts} +4 -4
- package/runtime/config-schema.ts +64 -0
- package/runtime/format-helpers.ts +293 -0
- package/runtime/types.ts +118 -0
- package/skill.json +44 -10
- package/{confirm-indexed.ts → subgraph/confirm-indexed.ts} +1 -1
- package/{subgraph-search.ts → subgraph/subgraph-search.ts} +14 -14
- package/{subgraph-store.ts → subgraph/subgraph-store.ts} +10 -10
- package/dist/consolidation.js +0 -249
- /package/{api-client.ts → billing/api-client.ts} +0 -0
- /package/{gateway-url.ts → billing/gateway-url.ts} +0 -0
- /package/{inbound-user-tracker.ts → billing/inbound-user-tracker.ts} +0 -0
- /package/{relay.ts → billing/relay.ts} +0 -0
- /package/{crypto.ts → crypto/crypto.ts} +0 -0
- /package/{vault-crypto.ts → crypto/vault-crypto.ts} +0 -0
- /package/dist/{api-client.js → billing/api-client.js} +0 -0
- /package/dist/{gateway-url.js → billing/gateway-url.js} +0 -0
- /package/dist/{inbound-user-tracker.js → billing/inbound-user-tracker.js} +0 -0
- /package/dist/{relay.js → billing/relay.js} +0 -0
- /package/dist/{crypto.js → crypto/crypto.js} +0 -0
- /package/dist/{vault-crypto.js → crypto/vault-crypto.js} +0 -0
- /package/dist/{embedder-cache.js → embedding/embedder-cache.js} +0 -0
- /package/dist/{embedder-loader.js → embedding/embedder-loader.js} +0 -0
- /package/dist/{embedder-network.js → embedding/embedder-network.js} +0 -0
- /package/dist/{embedding.js → embedding/embedding.js} +0 -0
- /package/dist/{lsh.js → embedding/lsh.js} +0 -0
- /package/dist/{import-state-manager.js → import/import-state-manager.js} +0 -0
- /package/dist/{llm-profile-reader.js → llm/llm-profile-reader.js} +0 -0
- /package/dist/{native-memory.js → memory/native-memory.js} +0 -0
- /package/dist/{tool-gating.js → memory/tool-gating.js} +0 -0
- /package/dist/{tools.js → memory/tools.js} +0 -0
- /package/dist/{generate-mnemonic.js → pairing/generate-mnemonic.js} +0 -0
- /package/dist/{pair-cli.js → pairing/pair-cli.js} +0 -0
- /package/dist/{pair-page.js → pairing/pair-page.js} +0 -0
- /package/dist/{pair-qr.js → pairing/pair-qr.js} +0 -0
- /package/dist/{restart-auth.js → pairing/restart-auth.js} +0 -0
- /package/dist/{qa-bug-report.js → setup/qa-bug-report.js} +0 -0
- /package/dist/{skill-register.js → setup/skill-register.js} +0 -0
- /package/dist/{trajectory-poller.js → subgraph/trajectory-poller.js} +0 -0
- /package/{embedder-cache.ts → embedding/embedder-cache.ts} +0 -0
- /package/{embedder-loader.ts → embedding/embedder-loader.ts} +0 -0
- /package/{embedder-network.ts → embedding/embedder-network.ts} +0 -0
- /package/{embedding.ts → embedding/embedding.ts} +0 -0
- /package/{lsh.ts → embedding/lsh.ts} +0 -0
- /package/{import-state-manager.ts → import/import-state-manager.ts} +0 -0
- /package/{llm-profile-reader.ts → llm/llm-profile-reader.ts} +0 -0
- /package/{native-memory.ts → memory/native-memory.ts} +0 -0
- /package/{tools.ts → memory/tools.ts} +0 -0
- /package/{generate-mnemonic.ts → pairing/generate-mnemonic.ts} +0 -0
- /package/{pair-cli.ts → pairing/pair-cli.ts} +0 -0
- /package/{pair-page.ts → pairing/pair-page.ts} +0 -0
- /package/{pair-qr.ts → pairing/pair-qr.ts} +0 -0
- /package/{restart-auth.ts → pairing/restart-auth.ts} +0 -0
- /package/{qa-bug-report.ts → setup/qa-bug-report.ts} +0 -0
- /package/{skill-register.ts → setup/skill-register.ts} +0 -0
- /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
|
+
}
|
package/runtime/types.ts
ADDED
|
@@ -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.
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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 '
|
|
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 '
|
|
25
|
-
import { buildRelayHeaders } from '
|
|
26
|
-
import { relayFetch } from '
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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 (
|
|
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
|
|
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 =
|
|
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
|
|
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 (
|
|
387
|
-
const count = parseInt(
|
|
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
|
|