@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,301 @@
1
+ /**
2
+ * totalreclaw_report_qa_bug — RC-gated tool for agent-driven QA bug reports.
3
+ *
4
+ * Only registered when the plugin version contains `-rc.` (SemVer pre-release
5
+ * token); stable builds never expose this tool. Shipped in 3.3.1-rc.3 so
6
+ * agents running the `qa-totalreclaw` skill can file structured issues to
7
+ * `p-diogo/totalreclaw-internal` via direct GitHub REST API fetch (scanner-
8
+ * safe — no shelling out to CLIs) without the maintainer opening a fresh
9
+ * issue by hand for every RC finding.
10
+ *
11
+ * See `.github/ISSUE_TEMPLATE/qa-bug.yml` in the internal repo — the
12
+ * markdown body this module renders mirrors the form-template field
13
+ * names so future automation can parse either the form or the tool
14
+ * output identically.
15
+ *
16
+ * Security: all user-supplied strings (symptom / expected / repro / logs
17
+ * / environment) run through `redactSecrets()` fail-close before the
18
+ * POST. BIP-39 phrases, API keys, Telegram bot tokens, and bearer tokens
19
+ * in headers all become `<REDACTED>` in the posted issue. Refer to
20
+ * `redactSecrets()` for the exact rule set.
21
+ *
22
+ * Target repo safety: the default target is `p-diogo/totalreclaw-internal`.
23
+ * Operators can override via the `TOTALRECLAW_QA_REPO` env var, but only
24
+ * to another slug ending in `-internal`. Any other slug — including the
25
+ * public `p-diogo/totalreclaw` — is rejected with a loud error. rc.13 QA
26
+ * surfaced a repo-slug drift where QA findings leaked to the public
27
+ * tracker; rc.14 adds this fail-loud guard.
28
+ */
29
+ // ---------------------------------------------------------------------------
30
+ // RC-gate detection
31
+ // ---------------------------------------------------------------------------
32
+ /**
33
+ * True when the given version string indicates a pre-release build
34
+ * (SemVer `-rc.` or PEP-440 `rc`). Used to gate the QA bug-report tool so
35
+ * stable users never see it.
36
+ *
37
+ * Accepts:
38
+ * - `3.3.1-rc.3` → SemVer pre-release (plugin)
39
+ * - `2.3.1rc3` → PEP-440 release-candidate (Hermes-style)
40
+ * - `1.0.0-rc.1` → SemVer
41
+ *
42
+ * Rejects:
43
+ * - `3.3.1` → stable
44
+ * - `3.3.1-beta.1` → pre-release but not RC (future: might unblock beta QA)
45
+ * - `"" / null` → empty defensive
46
+ */
47
+ export function isRcBuild(version) {
48
+ if (!version || typeof version !== 'string')
49
+ return false;
50
+ const v = version.toLowerCase();
51
+ // SemVer: `-rc.<N>`
52
+ if (/-rc\.\d+/.test(v))
53
+ return true;
54
+ // PEP-440: `rc<N>` (no dash)
55
+ if (/\d+rc\d+/.test(v))
56
+ return true;
57
+ return false;
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Redaction — fail-close
61
+ // ---------------------------------------------------------------------------
62
+ const REDACTED = '<REDACTED>';
63
+ /**
64
+ * Redact likely secrets from free-text fields before posting to GitHub.
65
+ * Runs a sequence of patterns; order matters (longer/more-specific first).
66
+ *
67
+ * Covered:
68
+ * - BIP-39 recovery phrases (12 or 24 lowercase words, space-separated)
69
+ * - OpenAI-style `sk-` keys, Anthropic `sk-ant-` keys
70
+ * - Google-style `AIzaSy...` keys
71
+ * - Telegram bot tokens (`\d+:[A-Za-z0-9_-]{35,}`)
72
+ * - Bearer tokens in `Authorization:` headers
73
+ * - Hex auth keys (>=32 chars of hex alone on a line or after `key=`)
74
+ *
75
+ * Unknown shapes may still leak. Fail-close on the patterns we DO match,
76
+ * fail-open on patterns we don't — the agent is also instructed (via the
77
+ * SKILL.md addendum) to not pass raw secrets.
78
+ */
79
+ export function redactSecrets(text) {
80
+ if (!text || typeof text !== 'string')
81
+ return '';
82
+ let out = text;
83
+ // BIP-39 mnemonic — 12 or 24 lowercase alpha words separated by single
84
+ // spaces. Some test vectors use 15/18/21 words, accept those too.
85
+ //
86
+ // CAVEAT: the regex is a shape check, not a dictionary check. A line of
87
+ // 12 random English words that happen to all be lowercase will also be
88
+ // redacted — acceptable over-redaction for a bug report field.
89
+ out = out.replace(/\b(?:[a-z]{3,10}(?:\s+[a-z]{3,10}){11,23})\b/g, REDACTED);
90
+ // OpenAI / Anthropic-style `sk-...` keys. `sk-ant-api03-...` gets caught
91
+ // by the broader `sk-[A-Za-z0-9_-]{20,}` pattern below.
92
+ out = out.replace(/\bsk-[A-Za-z0-9_-]{20,}/g, REDACTED);
93
+ // Google API key: `AIzaSy` prefix + ~33 trailing chars (total 39).
94
+ // We accept 30–45 trailing chars so accidental suffixes / URL-encoded
95
+ // variants don't escape.
96
+ out = out.replace(/\bAIza[0-9A-Za-z\-_]{30,45}\b/g, REDACTED);
97
+ // Telegram bot token: `\d+:[A-Za-z0-9_-]{35,}`.
98
+ out = out.replace(/\b\d{6,}:[A-Za-z0-9_-]{35,}\b/g, REDACTED);
99
+ // Bearer token in Authorization header (case-insensitive). Preserves the
100
+ // header name so the log remains recognizable.
101
+ out = out.replace(/(authorization[:\s]*bearer\s+)[A-Za-z0-9._\-+/=]+/gi, `$1${REDACTED}`);
102
+ // X-Api-Key / x-api-key style header.
103
+ out = out.replace(/(x-api-key[:\s]*)[A-Za-z0-9._\-+/=]{20,}/gi, `$1${REDACTED}`);
104
+ // Hex blobs 64+ chars (typical auth-key / private-key shape). Must not
105
+ // eat commit SHAs or contract addresses; gate on length 40+. Bump to 64
106
+ // to avoid eating regular addresses.
107
+ out = out.replace(/\b[a-fA-F0-9]{64,}\b/g, REDACTED);
108
+ // Private-key-style 0x-prefixed 64-hex.
109
+ out = out.replace(/\b0x[a-fA-F0-9]{64}\b/g, REDACTED);
110
+ // UUIDs that appear alongside `token=` or `secret=` qualifiers. Naked
111
+ // UUIDs are left alone (fact IDs are legitimate UUIDs).
112
+ out = out.replace(/((?:token|secret|auth_key)\s*[=:]\s*)[A-Za-z0-9-]{20,}/gi, `$1${REDACTED}`);
113
+ return out;
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Target repo guard — fail-loud on any repo that isn't the internal tracker.
117
+ // ---------------------------------------------------------------------------
118
+ export const DEFAULT_QA_REPO = 'p-diogo/totalreclaw-internal';
119
+ /**
120
+ * Known-public repo slugs that must never receive QA bug reports. The
121
+ * structural rule (`endsWith('-internal')`) below should already block
122
+ * these, but the explicit denylist is a belt-and-braces safety against
123
+ * a future rename that accidentally drops the `-internal` suffix.
124
+ */
125
+ export const PUBLIC_REPOS_DENYLIST = new Set([
126
+ 'p-diogo/totalreclaw',
127
+ 'p-diogo/totalreclaw-website',
128
+ 'p-diogo/totalreclaw-relay',
129
+ 'p-diogo/totalreclaw-plugin',
130
+ 'p-diogo/totalreclaw-hermes',
131
+ ]);
132
+ /**
133
+ * Resolve the target repo for a QA bug filing.
134
+ *
135
+ * Precedence: explicit override → `TOTALRECLAW_QA_REPO` env → default.
136
+ * Throws if the slug is on the public denylist or does not end in
137
+ * `-internal`. rc.13 QA found agent-filed bug reports leaking to the
138
+ * public repo; this guard makes any such drift fail loudly rather than
139
+ * silently leak RC ship-stopper detail.
140
+ *
141
+ * `TOTALRECLAW_QA_REPO` is the documented override var. The env-var
142
+ * read lives in `config.ts` (CONFIG.qaRepoOverride) so this module
143
+ * never touches process environment directly — keeps the plugin
144
+ * scanner-sim clean because this file also performs a GitHub HTTPS
145
+ * request (env + network in the same file would trip OpenClaw's
146
+ * env-harvesting heuristic).
147
+ *
148
+ * Pass the env-resolved slug (or `null`/empty for default) as
149
+ * `override`. Tests can inject via the second arg.
150
+ */
151
+ export function resolveQaRepo(override, env) {
152
+ // `env` is only for test injection — production callers should
153
+ // pre-resolve the env value via CONFIG.qaRepoOverride and pass it as
154
+ // `override`. The env lookup is a last-resort fallback that works in
155
+ // Node but is NEVER the primary path in production.
156
+ const envOverride = env ? env.TOTALRECLAW_QA_REPO : undefined;
157
+ const raw = (override || envOverride || DEFAULT_QA_REPO).trim();
158
+ if (!raw || !raw.includes('/')) {
159
+ throw new Error(`invalid QA repo slug '${raw}': expected 'owner/name' format`);
160
+ }
161
+ if (PUBLIC_REPOS_DENYLIST.has(raw)) {
162
+ throw new Error(`refusing to file QA bug to PUBLIC repo '${raw}'. ` +
163
+ 'QA bug reports contain RC ship-stopper detail that must not ' +
164
+ "leak to public. Set TOTALRECLAW_QA_REPO to a repo ending in " +
165
+ "'-internal' (e.g. p-diogo/totalreclaw-internal).");
166
+ }
167
+ if (!raw.endsWith('-internal')) {
168
+ throw new Error(`refusing to file QA bug to repo '${raw}': slug must end in ` +
169
+ "'-internal' (structural safety rule). Override via " +
170
+ 'TOTALRECLAW_QA_REPO only to another internal fork.');
171
+ }
172
+ return raw;
173
+ }
174
+ const VALID_INTEGRATIONS = new Set([
175
+ 'plugin',
176
+ 'hermes',
177
+ 'nanoclaw',
178
+ 'mcp',
179
+ 'relay',
180
+ 'clawhub',
181
+ 'docs',
182
+ 'other',
183
+ ]);
184
+ // Internal → display-name mapping for the issue body. Matches the
185
+ // dropdown values in `.github/ISSUE_TEMPLATE/qa-bug.yml`.
186
+ const INTEGRATION_DISPLAY = {
187
+ plugin: 'OpenClaw plugin',
188
+ hermes: 'Hermes Python',
189
+ nanoclaw: 'NanoClaw skill',
190
+ mcp: 'MCP server',
191
+ relay: 'Relay (backend)',
192
+ clawhub: 'ClawHub publishing',
193
+ docs: 'Docs / setup guide',
194
+ other: 'Other',
195
+ };
196
+ const VALID_SEVERITIES = new Set(['blocker', 'high', 'medium', 'low']);
197
+ export function validateQaBugArgs(args) {
198
+ if (!args || typeof args !== 'object')
199
+ return { ok: false, error: 'args must be an object' };
200
+ const missing = ['integration', 'rc_version', 'severity', 'title', 'symptom', 'expected', 'repro', 'logs', 'environment']
201
+ .filter((f) => !args[f] || typeof args[f] !== 'string');
202
+ if (missing.length) {
203
+ return { ok: false, error: `missing or non-string fields: ${missing.join(', ')}` };
204
+ }
205
+ if (!VALID_INTEGRATIONS.has(args.integration)) {
206
+ return { ok: false, error: `invalid integration "${args.integration}"; expected one of ${[...VALID_INTEGRATIONS].join(', ')}` };
207
+ }
208
+ if (!VALID_SEVERITIES.has(args.severity)) {
209
+ return { ok: false, error: `invalid severity "${args.severity}"; expected one of ${[...VALID_SEVERITIES].join(', ')}` };
210
+ }
211
+ if (args.title.length > 60) {
212
+ return { ok: false, error: 'title must be <= 60 chars' };
213
+ }
214
+ return { ok: true };
215
+ }
216
+ /**
217
+ * Build the issue body mirroring the `.github/ISSUE_TEMPLATE/qa-bug.yml`
218
+ * layout. Runs every user-supplied string through `redactSecrets` before
219
+ * embedding. Exported for unit testing.
220
+ */
221
+ export function buildIssueBody(args) {
222
+ const integrationDisplay = INTEGRATION_DISPLAY[args.integration] ?? args.integration;
223
+ const header = [
224
+ '_Filed automatically by the TotalReclaw RC bug-report tool._',
225
+ '',
226
+ '### Integration',
227
+ integrationDisplay,
228
+ '',
229
+ '### RC version',
230
+ '`' + redactSecrets(args.rc_version) + '`',
231
+ '',
232
+ '### Severity',
233
+ args.severity,
234
+ '',
235
+ '### What happened',
236
+ redactSecrets(args.symptom),
237
+ '',
238
+ '### What was expected',
239
+ redactSecrets(args.expected),
240
+ '',
241
+ '### Reproduction steps',
242
+ redactSecrets(args.repro),
243
+ '',
244
+ '### Relevant logs / evidence',
245
+ '```',
246
+ redactSecrets(args.logs),
247
+ '```',
248
+ '',
249
+ '### Environment',
250
+ redactSecrets(args.environment),
251
+ '',
252
+ '---',
253
+ '> Reporter: LLM agent via `totalreclaw_report_qa_bug` (RC-gated tool)',
254
+ ].join('\n');
255
+ return header;
256
+ }
257
+ /**
258
+ * POST the bug to GitHub. Returns the issue URL on success; throws with a
259
+ * structured message on failure. The caller (tool handler) wraps the
260
+ * exception into a JSON tool response.
261
+ */
262
+ export async function postQaBugIssue(args, deps) {
263
+ const validation = validateQaBugArgs(args);
264
+ if ('error' in validation)
265
+ throw new Error(`invalid args: ${validation.error}`);
266
+ if (!deps.githubToken)
267
+ throw new Error('githubToken is required');
268
+ const repo = resolveQaRepo(deps.repo ?? null);
269
+ const url = `https://api.github.com/repos/${repo}/issues`;
270
+ const title = `[qa-bug] ${redactSecrets(args.title)}`;
271
+ const body = buildIssueBody(args);
272
+ const labels = [
273
+ 'qa-bug',
274
+ 'pending-triage',
275
+ `severity:${args.severity}`,
276
+ `component:${args.integration}`,
277
+ `rc:${args.rc_version.replace(/[^A-Za-z0-9.\-]/g, '_').slice(0, 40)}`,
278
+ ];
279
+ const fetchFn = deps.fetchImpl ?? fetch;
280
+ const res = await fetchFn(url, {
281
+ method: 'POST',
282
+ headers: {
283
+ Accept: 'application/vnd.github+json',
284
+ 'X-GitHub-Api-Version': '2022-11-28',
285
+ Authorization: `Bearer ${deps.githubToken}`,
286
+ 'Content-Type': 'application/json',
287
+ 'User-Agent': 'totalreclaw-plugin-qa-bug',
288
+ },
289
+ body: JSON.stringify({ title, body, labels }),
290
+ });
291
+ if (!res.ok) {
292
+ const text = await res.text().catch(() => '');
293
+ throw new Error(`GitHub API ${res.status}: ${text.slice(0, 200)}`);
294
+ }
295
+ const json = (await res.json());
296
+ if (!json.html_url || typeof json.number !== 'number') {
297
+ throw new Error('GitHub API returned no html_url / number');
298
+ }
299
+ deps.logger?.info(`Filed QA bug #${json.number}: ${json.html_url}`);
300
+ return { issue_url: json.html_url, issue_number: json.number };
301
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared outbound-header helper for relay calls.
3
+ *
4
+ * Centralizes the common `X-TotalReclaw-*` headers so every fetch site
5
+ * consistently tags requests with:
6
+ * - `X-TotalReclaw-Client` — caller identity (defaults to `openclaw-plugin`).
7
+ * - `X-TotalReclaw-Session` — optional QA / observability tag from
8
+ * `TOTALRECLAW_SESSION_ID`. Used by Axiom log filters and the
9
+ * `qa-totalreclaw` skill to scope log searches per QA run.
10
+ *
11
+ * Pure function — no I/O, no network. Reads `getSessionId()` (which reads the
12
+ * env var via getter so harnesses that flip the env between calls pick up
13
+ * the new value).
14
+ *
15
+ * The session-id env var was accidentally placed in the v1 REMOVED_ENV_VARS
16
+ * list and silently warned-and-dropped, breaking Axiom traceability for QA
17
+ * runs (see internal#127). This helper is the canonical re-entry point for
18
+ * the variable.
19
+ */
20
+ import { getSessionId } from './config.js';
21
+ /** Default `X-TotalReclaw-Client` value. */
22
+ export const DEFAULT_CLIENT_ID = 'openclaw-plugin';
23
+ /**
24
+ * Build the standard outbound header set.
25
+ *
26
+ * @param overrides - merge-in additional headers (`Authorization`,
27
+ * `Content-Type`, etc.); these win over the defaults.
28
+ * @param clientId - override the `X-TotalReclaw-Client` value.
29
+ *
30
+ * Always includes `X-TotalReclaw-Client`. Includes `X-TotalReclaw-Session`
31
+ * only when `TOTALRECLAW_SESSION_ID` is set + non-empty.
32
+ */
33
+ export function buildRelayHeaders(overrides = {}, clientId = DEFAULT_CLIENT_ID) {
34
+ const headers = {
35
+ 'X-TotalReclaw-Client': clientId,
36
+ };
37
+ const sessionId = getSessionId();
38
+ if (sessionId) {
39
+ headers['X-TotalReclaw-Session'] = sessionId;
40
+ }
41
+ // Caller-supplied headers (Authorization, Content-Type, Accept, etc.) take
42
+ // precedence over the defaults but should generally not stomp the X-* tags.
43
+ return { ...headers, ...overrides };
44
+ }