@totalreclaw/totalreclaw 3.3.2 → 3.3.3-rc.1
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/CHANGELOG.md +149 -0
- package/SKILL.md +1 -1
- package/config.ts +24 -1
- package/dist/config.js +22 -1
- package/dist/embedding.js +24 -0
- package/dist/index.js +197 -16
- package/dist/llm-client.js +3 -1
- package/dist/subgraph-store.js +3 -2
- package/embedding.ts +25 -0
- package/index.ts +206 -13
- package/llm-client.ts +8 -1
- package/package.json +5 -6
- package/skill.json +3 -3
- package/subgraph-store.ts +3 -2
- package/postinstall.mjs +0 -260
package/index.ts
CHANGED
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
getEmbeddingDims,
|
|
82
82
|
getEmbeddingModelId,
|
|
83
83
|
configureEmbedder,
|
|
84
|
+
prefetchEmbedderBundle,
|
|
84
85
|
} from './llm-client.js';
|
|
85
86
|
import {
|
|
86
87
|
defaultAuthProfilesRoot,
|
|
@@ -748,6 +749,21 @@ let firstRunAfterInit = true;
|
|
|
748
749
|
*/
|
|
749
750
|
let firstRunWelcomeShown = false;
|
|
750
751
|
|
|
752
|
+
/**
|
|
753
|
+
* 3.3.3-rc.1 — RC-mode staging-only banner (PR #165 implementation).
|
|
754
|
+
*
|
|
755
|
+
* Fires ONCE per gateway process when:
|
|
756
|
+
* - the bundled-default `serverUrl` resolves to `api-staging.totalreclaw.xyz`
|
|
757
|
+
* (RC artifact, not stable), AND
|
|
758
|
+
* - the user has NOT overridden via `TOTALRECLAW_SERVER_URL=...` env.
|
|
759
|
+
*
|
|
760
|
+
* Goal: a fresh QA tester can't accidentally use an RC build for real data
|
|
761
|
+
* without seeing a clear "RC = staging, no SLA, may be wiped" warning.
|
|
762
|
+
* One-shot at the first `before_agent_start` after register(); never spams
|
|
763
|
+
* per-turn. A fresh gateway restart re-fires it once.
|
|
764
|
+
*/
|
|
765
|
+
let stagingBannerShown = false;
|
|
766
|
+
|
|
751
767
|
/**
|
|
752
768
|
* Derive keys from the recovery phrase, load credentials, and register with
|
|
753
769
|
* the server if this is the first run.
|
|
@@ -761,7 +777,9 @@ let firstRunWelcomeShown = false;
|
|
|
761
777
|
* directs the caller to `openclaw totalreclaw onboard`.
|
|
762
778
|
*/
|
|
763
779
|
async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
|
|
764
|
-
|
|
780
|
+
// 3.3.3-rc.1: staging is the source default per #165. Stable build-time
|
|
781
|
+
// sed-replace flips `api-staging` -> `api` in dist/.
|
|
782
|
+
const serverUrl = CONFIG.serverUrl || 'https://api-staging.totalreclaw.xyz';
|
|
765
783
|
let masterPassword = CONFIG.recoveryPhrase;
|
|
766
784
|
|
|
767
785
|
// 3.2.0: if the env var is unset, probe credentials.json for a
|
|
@@ -2967,13 +2985,39 @@ const plugin = {
|
|
|
2967
2985
|
api.logger.warn(`TotalReclaw: configureEmbedder failed (will use defaults): ${msg}`);
|
|
2968
2986
|
}
|
|
2969
2987
|
|
|
2988
|
+
// 3.3.3-rc.1 (issue #187 — ONNX decouple): kick off a non-blocking
|
|
2989
|
+
// bundle prefetch so the ~700 MB embedder tarball starts streaming
|
|
2990
|
+
// as soon as the gateway boots, BEFORE the user completes
|
|
2991
|
+
// `totalreclaw_pair`. Decouples the model download from the
|
|
2992
|
+
// pair-completion gate the previous flow imposed via
|
|
2993
|
+
// `requireFullSetup()` -> first `generateEmbedding()` call.
|
|
2994
|
+
// Fire-and-forget — never awaits, never throws on failure (the next
|
|
2995
|
+
// `generateEmbedding()` call retries via the same idempotent path).
|
|
2996
|
+
// Disabled when `TOTALRECLAW_DISABLE_EMBEDDER_PREFETCH=1` (CI / tests
|
|
2997
|
+
// where the network is sandboxed away). The env read lives in
|
|
2998
|
+
// config.ts; we read the resolved CONFIG flag here so this file
|
|
2999
|
+
// stays scanner-clean (no env lookups in index.ts).
|
|
3000
|
+
if (!CONFIG.embedderPrefetchDisabled) {
|
|
3001
|
+
prefetchEmbedderBundle({ log: (msg) => api.logger.info(msg) })
|
|
3002
|
+
.then((result) => {
|
|
3003
|
+
api.logger.info(`TotalReclaw: embedder prefetch ${result === 'fetched' ? 'completed (downloaded bundle)' : 'cache hit'}`);
|
|
3004
|
+
})
|
|
3005
|
+
.catch((err: unknown) => {
|
|
3006
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3007
|
+
api.logger.warn(
|
|
3008
|
+
`TotalReclaw: embedder prefetch failed (non-fatal — will retry on first generateEmbedding): ${msg}`,
|
|
3009
|
+
);
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
|
|
2970
3013
|
// 3.3.1-rc.22 (rc.21 finding #5): self-heal partial-install marker.
|
|
2971
|
-
// The `preinstall` npm script writes `.tr-partial-install`;
|
|
2972
|
-
//
|
|
2973
|
-
//
|
|
2974
|
-
//
|
|
2975
|
-
//
|
|
2976
|
-
// Clear it so the next retry's
|
|
3014
|
+
// The `preinstall` npm script writes `.tr-partial-install`; clearing
|
|
3015
|
+
// it has been the runtime's job since 3.3.3-rc.1 dropped postinstall.mjs
|
|
3016
|
+
// (OpenClaw scanner blocked the install on the subprocess-spawn import
|
|
3017
|
+
// — see 3.3.3-rc.1 PR). If we have gotten this far the loader did
|
|
3018
|
+
// register us — meaning the install succeeded enough to be useful —
|
|
3019
|
+
// so any lingering marker is stale. Clear it so the next retry's
|
|
3020
|
+
// detector does not see a false positive.
|
|
2977
3021
|
//
|
|
2978
3022
|
// 3.3.1-rc.22 (rc.21 finding #6) — gateway/reload upstream caveat:
|
|
2979
3023
|
// OpenClaw's config-watcher fires `gateway/reload` when
|
|
@@ -4202,6 +4246,120 @@ const plugin = {
|
|
|
4202
4246
|
{ name: 'totalreclaw_status' },
|
|
4203
4247
|
);
|
|
4204
4248
|
|
|
4249
|
+
// ---------------------------------------------------------------
|
|
4250
|
+
// Tool: totalreclaw_preload_embedder (3.3.3-rc.1 — issue #187)
|
|
4251
|
+
// ---------------------------------------------------------------
|
|
4252
|
+
//
|
|
4253
|
+
// Decouples the ~700 MB embedder bundle download from the pair-
|
|
4254
|
+
// completion gate. The agent can call this BEFORE
|
|
4255
|
+
// `totalreclaw_pair` to pre-flight disk space, kick off the
|
|
4256
|
+
// download, and surface the completion state. The non-blocking
|
|
4257
|
+
// prefetch in `register()` already starts the download
|
|
4258
|
+
// unconditionally; this tool is an explicit on-demand hook for
|
|
4259
|
+
// agents that want to confirm completion (or trigger a retry on
|
|
4260
|
+
// network failure) without first completing pair.
|
|
4261
|
+
//
|
|
4262
|
+
// Behavior:
|
|
4263
|
+
// - Disk-space pre-flight: refuses if free disk < 500 MB on the
|
|
4264
|
+
// embedder cache mount. Surfaces the path + free bytes so the
|
|
4265
|
+
// user can clear space.
|
|
4266
|
+
// - Triggers `prefetchEmbedderBundle()` (idempotent — cache hit
|
|
4267
|
+
// returns immediately).
|
|
4268
|
+
// - Returns a structured success message with status:
|
|
4269
|
+
// `cache_hit | fetched | failed`.
|
|
4270
|
+
//
|
|
4271
|
+
// Phrase-safety: this tool does NOT touch credentials.json,
|
|
4272
|
+
// mnemonics, or keys. It only touches `~/.totalreclaw/embedder/`
|
|
4273
|
+
// and the GitHub Releases CDN.
|
|
4274
|
+
|
|
4275
|
+
api.registerTool(
|
|
4276
|
+
{
|
|
4277
|
+
name: 'totalreclaw_preload_embedder',
|
|
4278
|
+
label: 'Preload Embedder',
|
|
4279
|
+
description:
|
|
4280
|
+
'Download the local-embedding model bundle ahead of pair completion. Use this when the user wants to set up TotalReclaw on a slow connection or run an offline-after-setup workflow. Returns success once the ~700 MB bundle is cached at ~/.totalreclaw/embedder/.',
|
|
4281
|
+
parameters: {
|
|
4282
|
+
type: 'object',
|
|
4283
|
+
properties: {},
|
|
4284
|
+
additionalProperties: false,
|
|
4285
|
+
},
|
|
4286
|
+
async execute() {
|
|
4287
|
+
try {
|
|
4288
|
+
// Disk-space pre-flight: refuse if < 500 MB free on the
|
|
4289
|
+
// embedder cache mount. Best-effort — if statfs fails, we
|
|
4290
|
+
// proceed without the pre-flight rather than blocking.
|
|
4291
|
+
const fsModule = await import('node:fs');
|
|
4292
|
+
const cacheRoot = CONFIG.embedderCachePath;
|
|
4293
|
+
const REQUIRED_BYTES = 500 * 1024 * 1024;
|
|
4294
|
+
try {
|
|
4295
|
+
// Find the deepest existing parent so statfs has a real
|
|
4296
|
+
// mount to measure (loadEmbedder will mkdir under it
|
|
4297
|
+
// anyway).
|
|
4298
|
+
let probeDir = cacheRoot;
|
|
4299
|
+
while (true) {
|
|
4300
|
+
try {
|
|
4301
|
+
fsModule.statSync(probeDir);
|
|
4302
|
+
break;
|
|
4303
|
+
} catch {
|
|
4304
|
+
const parent = nodePath.dirname(probeDir);
|
|
4305
|
+
if (parent === probeDir) break;
|
|
4306
|
+
probeDir = parent;
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
const stats = (fsModule as unknown as {
|
|
4310
|
+
statfsSync?: (p: string) => { bavail: bigint | number; bsize: bigint | number };
|
|
4311
|
+
}).statfsSync?.(probeDir);
|
|
4312
|
+
if (stats) {
|
|
4313
|
+
const bavail = typeof stats.bavail === 'bigint' ? Number(stats.bavail) : stats.bavail;
|
|
4314
|
+
const bsize = typeof stats.bsize === 'bigint' ? Number(stats.bsize) : stats.bsize;
|
|
4315
|
+
const freeBytes = bavail * bsize;
|
|
4316
|
+
if (freeBytes > 0 && freeBytes < REQUIRED_BYTES) {
|
|
4317
|
+
const freeMb = Math.round(freeBytes / (1024 * 1024));
|
|
4318
|
+
return {
|
|
4319
|
+
content: [{
|
|
4320
|
+
type: 'text',
|
|
4321
|
+
text: `Insufficient free disk space for embedder bundle. Required: 500 MB. Available at ${probeDir}: ${freeMb} MB. Free up space and retry.`,
|
|
4322
|
+
}],
|
|
4323
|
+
details: { status: 'failed', reason: 'disk_space', free_mb: freeMb, required_mb: 500, cache_root: cacheRoot },
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
} catch {
|
|
4328
|
+
// statfs probe failed — surface a soft warning in logs
|
|
4329
|
+
// but proceed with the download anyway.
|
|
4330
|
+
api.logger.info('totalreclaw_preload_embedder: disk-space probe unavailable, proceeding without pre-flight');
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
// Trigger the prefetch. This is idempotent (cache hit returns
|
|
4334
|
+
// immediately) so it's safe to invoke even when the
|
|
4335
|
+
// background prefetch from register() already completed.
|
|
4336
|
+
const result = await prefetchEmbedderBundle({
|
|
4337
|
+
log: (msg) => api.logger.info(msg),
|
|
4338
|
+
});
|
|
4339
|
+
const human =
|
|
4340
|
+
result === 'fetched'
|
|
4341
|
+
? `Embedder bundle downloaded and cached at ${cacheRoot}. Subsequent embedding calls run in-memory.`
|
|
4342
|
+
: `Embedder bundle already cached at ${cacheRoot} — no download needed.`;
|
|
4343
|
+
return {
|
|
4344
|
+
content: [{ type: 'text', text: human }],
|
|
4345
|
+
details: { status: result, cache_root: cacheRoot },
|
|
4346
|
+
};
|
|
4347
|
+
} catch (err: unknown) {
|
|
4348
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4349
|
+
api.logger.error(`totalreclaw_preload_embedder failed: ${message}`);
|
|
4350
|
+
return {
|
|
4351
|
+
content: [{
|
|
4352
|
+
type: 'text',
|
|
4353
|
+
text: `Embedder bundle preload failed: ${humanizeError(message)}. The plugin will retry on first embedding call.`,
|
|
4354
|
+
}],
|
|
4355
|
+
details: { status: 'failed', reason: 'fetch_error', message },
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
},
|
|
4359
|
+
},
|
|
4360
|
+
{ name: 'totalreclaw_preload_embedder' },
|
|
4361
|
+
);
|
|
4362
|
+
|
|
4205
4363
|
// ---------------------------------------------------------------
|
|
4206
4364
|
// Tool: totalreclaw_consolidate
|
|
4207
4365
|
// ---------------------------------------------------------------
|
|
@@ -5672,6 +5830,39 @@ const plugin = {
|
|
|
5672
5830
|
return undefined;
|
|
5673
5831
|
}
|
|
5674
5832
|
|
|
5833
|
+
// 3.3.3-rc.1 — RC-staging banner (PR #165 implementation).
|
|
5834
|
+
// Build a one-shot prefix when the bundled default points at staging
|
|
5835
|
+
// AND the user hasn't overridden via env. This prefix is prepended
|
|
5836
|
+
// to whichever context block the rest of the hook produces.
|
|
5837
|
+
let stagingBannerBlock = '';
|
|
5838
|
+
if (!stagingBannerShown) {
|
|
5839
|
+
try {
|
|
5840
|
+
const usingStagingDefault = CONFIG.serverUrl.includes('api-staging.totalreclaw.xyz');
|
|
5841
|
+
const userOverrode = CONFIG.serverUrlEnvOverridden;
|
|
5842
|
+
if (usingStagingDefault && !userOverrode) {
|
|
5843
|
+
stagingBannerBlock =
|
|
5844
|
+
'## ⚠️ TotalReclaw is running in RC / staging mode\n\n' +
|
|
5845
|
+
'This build is bound to `api-staging.totalreclaw.xyz`. Staging has **no SLA** ' +
|
|
5846
|
+
'and may be wiped between QA cycles. Do **NOT** use this build for real data.\n\n' +
|
|
5847
|
+
'For production, install the stable release: `openclaw plugins install ' +
|
|
5848
|
+
'@totalreclaw/totalreclaw` (no `@rc` suffix). To pin a custom server, set ' +
|
|
5849
|
+
'`TOTALRECLAW_SERVER_URL=https://api.totalreclaw.xyz` in your env.\n\n';
|
|
5850
|
+
stagingBannerShown = true;
|
|
5851
|
+
api.logger.warn(
|
|
5852
|
+
'TotalReclaw: RC/staging build active (api-staging.totalreclaw.xyz). ' +
|
|
5853
|
+
'See docs/guides/release-process.md for the RC=staging / stable=production rule.',
|
|
5854
|
+
);
|
|
5855
|
+
} else {
|
|
5856
|
+
// Non-RC artifact OR user override — never fire the banner this
|
|
5857
|
+
// gateway-process lifetime.
|
|
5858
|
+
stagingBannerShown = true;
|
|
5859
|
+
}
|
|
5860
|
+
} catch {
|
|
5861
|
+
// Best-effort; never block session start on banner derivation.
|
|
5862
|
+
stagingBannerShown = true;
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
|
|
5675
5866
|
await ensureInitialized(api.logger);
|
|
5676
5867
|
|
|
5677
5868
|
// 3.2.0 onboarding pending: emit a non-secret guidance banner so
|
|
@@ -5701,6 +5892,7 @@ const plugin = {
|
|
|
5701
5892
|
}
|
|
5702
5893
|
return {
|
|
5703
5894
|
prependContext:
|
|
5895
|
+
stagingBannerBlock +
|
|
5704
5896
|
welcomeBlock +
|
|
5705
5897
|
'## TotalReclaw setup pending\n\n' +
|
|
5706
5898
|
'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
|
|
@@ -5802,6 +5994,7 @@ const plugin = {
|
|
|
5802
5994
|
api.logger.info(`Digest injection: state=${injectResult.state}`);
|
|
5803
5995
|
return {
|
|
5804
5996
|
prependContext:
|
|
5997
|
+
stagingBannerBlock +
|
|
5805
5998
|
`## Your Memory\n\n${injectResult.promptText}` + welcomeBack + billingWarning,
|
|
5806
5999
|
};
|
|
5807
6000
|
}
|
|
@@ -5848,7 +6041,7 @@ const plugin = {
|
|
|
5848
6041
|
const lines = cachedFacts.slice(0, 8).map((f, i) =>
|
|
5849
6042
|
`${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
|
|
5850
6043
|
);
|
|
5851
|
-
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
6044
|
+
return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
5852
6045
|
}
|
|
5853
6046
|
}
|
|
5854
6047
|
|
|
@@ -5861,7 +6054,7 @@ const plugin = {
|
|
|
5861
6054
|
const lines = cachedFacts.slice(0, 8).map((f, i) =>
|
|
5862
6055
|
`${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
|
|
5863
6056
|
);
|
|
5864
|
-
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
6057
|
+
return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
5865
6058
|
}
|
|
5866
6059
|
|
|
5867
6060
|
if (allTrapdoors.length === 0) return undefined;
|
|
@@ -5878,7 +6071,7 @@ const plugin = {
|
|
|
5878
6071
|
const lines = cachedFacts.slice(0, 8).map((f, i) =>
|
|
5879
6072
|
`${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
|
|
5880
6073
|
);
|
|
5881
|
-
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
6074
|
+
return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
5882
6075
|
}
|
|
5883
6076
|
return undefined;
|
|
5884
6077
|
}
|
|
@@ -5905,7 +6098,7 @@ const plugin = {
|
|
|
5905
6098
|
const lines = cachedFacts.slice(0, 8).map((f, i) =>
|
|
5906
6099
|
`${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
|
|
5907
6100
|
);
|
|
5908
|
-
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
6101
|
+
return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
5909
6102
|
}
|
|
5910
6103
|
|
|
5911
6104
|
// 5. Decrypt subgraph results and build reranker input.
|
|
@@ -5995,7 +6188,7 @@ const plugin = {
|
|
|
5995
6188
|
});
|
|
5996
6189
|
const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
|
|
5997
6190
|
|
|
5998
|
-
return { prependContext: contextString + welcomeBack + billingWarning };
|
|
6191
|
+
return { prependContext: stagingBannerBlock + contextString + welcomeBack + billingWarning };
|
|
5999
6192
|
}
|
|
6000
6193
|
|
|
6001
6194
|
// --- Server mode (existing behavior) ---
|
|
@@ -6099,7 +6292,7 @@ const plugin = {
|
|
|
6099
6292
|
});
|
|
6100
6293
|
const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
|
|
6101
6294
|
|
|
6102
|
-
return { prependContext: contextString + welcomeBack + billingWarning };
|
|
6295
|
+
return { prependContext: stagingBannerBlock + contextString + welcomeBack + billingWarning };
|
|
6103
6296
|
} catch (err: unknown) {
|
|
6104
6297
|
// The hook must NEVER throw -- log and return undefined.
|
|
6105
6298
|
const message = err instanceof Error ? err.message : String(err);
|
package/llm-client.ts
CHANGED
|
@@ -911,4 +911,11 @@ async function chatCompletionAnthropic(
|
|
|
911
911
|
// (Harrier-OSS-v1-270M ONNX model). No API key needed. The native deps +
|
|
912
912
|
// model are lazy-fetched from a pinned GitHub Release on first call —
|
|
913
913
|
// see embedding.ts + embedder-loader.ts.
|
|
914
|
-
export {
|
|
914
|
+
export {
|
|
915
|
+
generateEmbedding,
|
|
916
|
+
getEmbeddingDims,
|
|
917
|
+
getEmbeddingModelId,
|
|
918
|
+
configureEmbedder,
|
|
919
|
+
// 3.3.3-rc.1 (#187): pre-pair bundle prefetch
|
|
920
|
+
prefetchEmbedderBundle,
|
|
921
|
+
} from './embedding.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.3-rc.1",
|
|
4
4
|
"description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -58,21 +58,20 @@
|
|
|
58
58
|
"README.md",
|
|
59
59
|
"CHANGELOG.md",
|
|
60
60
|
"CLAWHUB.md",
|
|
61
|
-
"skill.json"
|
|
62
|
-
"postinstall.mjs"
|
|
61
|
+
"skill.json"
|
|
63
62
|
],
|
|
64
63
|
"scripts": {
|
|
65
64
|
"build": "rm -rf dist && tsc -p tsconfig.json --noCheck",
|
|
66
65
|
"verify-tarball": "node ../scripts/verify-tarball.mjs",
|
|
67
|
-
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx load-manifest.test.ts && npx tsx
|
|
66
|
+
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx load-manifest.test.ts && npx tsx url-binding.test.ts",
|
|
68
67
|
"smoke:dist": "npx tsx dist-esm-smoke.test.ts",
|
|
69
68
|
"check-scanner": "node ../scripts/check-scanner.mjs",
|
|
70
69
|
"check-version-drift": "node ../scripts/check-version-drift.mjs",
|
|
70
|
+
"check-url-binding": "node ../scripts/check-url-binding.mjs",
|
|
71
71
|
"sync-version": "node ../scripts/sync-version.mjs",
|
|
72
72
|
"preinstall": "node -e \"try{require('fs').writeFileSync('.tr-partial-install','');}catch{}\"",
|
|
73
|
-
"postinstall": "node ./postinstall.mjs",
|
|
74
73
|
"prepack": "npm run build",
|
|
75
|
-
"prepublishOnly": "node ../scripts/check-scanner.mjs && node ../scripts/check-version-drift.mjs"
|
|
74
|
+
"prepublishOnly": "node ../scripts/check-scanner.mjs && node ../scripts/check-version-drift.mjs && node ../scripts/check-url-binding.mjs --release-type=${TOTALRECLAW_RELEASE_TYPE:-rc}"
|
|
76
75
|
},
|
|
77
76
|
"openclaw": {
|
|
78
77
|
"extensions": [
|
package/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalreclaw",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.3-rc.1",
|
|
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",
|
|
@@ -193,8 +193,8 @@
|
|
|
193
193
|
"config": {
|
|
194
194
|
"serverUrl": {
|
|
195
195
|
"type": "string",
|
|
196
|
-
"default": "https://api.totalreclaw.xyz",
|
|
197
|
-
"description": "TotalReclaw server URL (only change for self-hosted mode)"
|
|
196
|
+
"default": "https://api-staging.totalreclaw.xyz",
|
|
197
|
+
"description": "TotalReclaw server URL (only change for self-hosted mode). Source default points at staging; stable releases swap to https://api.totalreclaw.xyz at publish time per PR #165."
|
|
198
198
|
},
|
|
199
199
|
"autoExtractEveryTurns": {
|
|
200
200
|
"type": "number",
|
package/subgraph-store.ts
CHANGED
|
@@ -805,7 +805,7 @@ export function isSubgraphMode(): boolean {
|
|
|
805
805
|
*
|
|
806
806
|
* After the v1 env var cleanup, clients only need:
|
|
807
807
|
* - TOTALRECLAW_RECOVERY_PHRASE -- BIP-39 mnemonic
|
|
808
|
-
* - TOTALRECLAW_SERVER_URL -- relay server URL (default: https://api.totalreclaw.xyz)
|
|
808
|
+
* - TOTALRECLAW_SERVER_URL -- relay server URL (source default: https://api-staging.totalreclaw.xyz; stable build: https://api.totalreclaw.xyz — swapped in at publish time per PR #165)
|
|
809
809
|
* - TOTALRECLAW_SELF_HOSTED -- set "true" to use self-hosted server (default: managed service)
|
|
810
810
|
*
|
|
811
811
|
* Chain ID is no longer configurable via env — it is auto-detected from the
|
|
@@ -813,7 +813,8 @@ export function isSubgraphMode(): boolean {
|
|
|
813
813
|
*/
|
|
814
814
|
export function getSubgraphConfig(): SubgraphStoreConfig {
|
|
815
815
|
return {
|
|
816
|
-
|
|
816
|
+
// 3.3.3-rc.1: staging by default in source; stable workflow seds.
|
|
817
|
+
relayUrl: CONFIG.serverUrl || 'https://api-staging.totalreclaw.xyz',
|
|
817
818
|
mnemonic: CONFIG.recoveryPhrase,
|
|
818
819
|
cachePath: CONFIG.cachePath,
|
|
819
820
|
chainId: CONFIG.chainId,
|
package/postinstall.mjs
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
// scanner-sim: allow — postinstall scripts run during `npm install`, NOT inside the OpenClaw runtime sandbox. Per check-scanner.mjs guidance ("Moving the subprocess call into a separate post-install helper that OpenClaw sandboxes (NOT covered by this scanner)"), this file is the intended home for child_process usage. The plugin's runtime code (index.ts, etc.) stays scanner-clean; this file only runs once at install-time.
|
|
2
|
-
/**
|
|
3
|
-
* postinstall.mjs — TotalReclaw plugin post-install lifecycle script.
|
|
4
|
-
*
|
|
5
|
-
* Runs after `npm install` finishes inside the plugin extension dir
|
|
6
|
-
* (`~/.openclaw/extensions/totalreclaw/`). Three jobs, in order:
|
|
7
|
-
*
|
|
8
|
-
* 1. Clean the partial-install marker (`.tr-partial-install`) that
|
|
9
|
-
* `preinstall` dropped. Mirrors the inline shim that shipped in
|
|
10
|
-
* pre-3.3.2 releases.
|
|
11
|
-
* 2. (3.3.2-rc.1 / issue #188) Smoke-check critical deps. After `npm
|
|
12
|
-
* install` claims success we require() the modules whose absence
|
|
13
|
-
* bricked rc.22 first-attempt installs (`@scure/bip39`,
|
|
14
|
-
* `@scure/bip39/wordlists/english.js`, `@totalreclaw/core`,
|
|
15
|
-
* `@totalreclaw/client`, `qrcode`, `ws`). If any throws, the
|
|
16
|
-
* post-install fails LOUDLY — better than the rc.21 silent
|
|
17
|
-
* half-install where `enabled: true` shipped with a missing dep.
|
|
18
|
-
* 3. (3.3.2-rc.1 / issue #190) Sweep `<extensions>/.openclaw-install-stage-*`
|
|
19
|
-
* siblings. The runtime register() helper handles this on plugin
|
|
20
|
-
* load too, but doing it here means a re-install starts from a
|
|
21
|
-
* clean parent dir — no "duplicate plugin id detected; global
|
|
22
|
-
* plugin will be overridden by global plugin" warning during the
|
|
23
|
-
* install itself.
|
|
24
|
-
*
|
|
25
|
-
* Constraints:
|
|
26
|
-
* - Must be idempotent: re-running on a clean tree is a no-op.
|
|
27
|
-
* - Must not import any production module that itself runs `register()`
|
|
28
|
-
* or makes outbound calls. We use only Node stdlib + dynamic require()
|
|
29
|
-
* of the smoke-check deps.
|
|
30
|
-
* - Must run in CommonJS-compatible Node ESM (the plugin's package.json
|
|
31
|
-
* declares `"type": "module"`, so this file uses `.mjs` and
|
|
32
|
-
* `createRequire` to call require() against the plugin's node_modules).
|
|
33
|
-
*
|
|
34
|
-
* Phrase-safety note: this file does NOT touch credentials.json, mnemonics,
|
|
35
|
-
* keys, or any phrase code path. It only validates module loading and
|
|
36
|
-
* cleans staging directories.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
import fs from 'node:fs';
|
|
40
|
-
import path from 'node:path';
|
|
41
|
-
import { fileURLToPath } from 'node:url';
|
|
42
|
-
import { createRequire } from 'node:module';
|
|
43
|
-
import { execSync } from 'node:child_process';
|
|
44
|
-
|
|
45
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
-
const require = createRequire(import.meta.url);
|
|
47
|
-
|
|
48
|
-
const PARTIAL_INSTALL_MARKER = '.tr-partial-install';
|
|
49
|
-
|
|
50
|
-
// Order matters: light, fast modules first so a failure surfaces quickly.
|
|
51
|
-
// `@scure/bip39/wordlists/english.js` is the EXACT path that bricked rc.21
|
|
52
|
-
// (issue #188 — `Cannot find module '@scure/bip39/wordlists/english.js'`).
|
|
53
|
-
const CRITICAL_DEPS = [
|
|
54
|
-
'@scure/bip39',
|
|
55
|
-
'@scure/bip39/wordlists/english.js',
|
|
56
|
-
'@totalreclaw/core',
|
|
57
|
-
'@totalreclaw/client',
|
|
58
|
-
'qrcode',
|
|
59
|
-
'ws',
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
function log(msg) {
|
|
63
|
-
process.stdout.write(`[totalreclaw postinstall] ${msg}\n`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function warn(msg) {
|
|
67
|
-
process.stderr.write(`[totalreclaw postinstall] WARN: ${msg}\n`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// Step 1 — clear .tr-partial-install marker
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
function clearPartialInstallMarker() {
|
|
75
|
-
try {
|
|
76
|
-
const markerPath = path.join(here, PARTIAL_INSTALL_MARKER);
|
|
77
|
-
if (fs.existsSync(markerPath)) {
|
|
78
|
-
fs.unlinkSync(markerPath);
|
|
79
|
-
log('cleared .tr-partial-install marker');
|
|
80
|
-
}
|
|
81
|
-
} catch (err) {
|
|
82
|
-
// Best-effort. The runtime register() also clears this defensively.
|
|
83
|
-
warn(`could not clear .tr-partial-install marker: ${err.message}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Step 2 — atomic critical-dep validation (issue #188)
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Try to require() each critical dep. Returns the list of names that
|
|
93
|
-
* failed; an empty array means everything resolved.
|
|
94
|
-
*/
|
|
95
|
-
function smokeCheckDeps() {
|
|
96
|
-
const missing = [];
|
|
97
|
-
for (const dep of CRITICAL_DEPS) {
|
|
98
|
-
try {
|
|
99
|
-
require(dep);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
missing.push({ dep, message: err.message });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return missing;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Recovery path: if smoke-check fails, blow away the local node_modules
|
|
109
|
-
* tree the parent install populated and re-run `npm install --no-audit
|
|
110
|
-
* --no-fund --no-save --offline=false` once. This is meant to recover
|
|
111
|
-
* from race-condition partial-fetches (issue #188), NOT from a missing
|
|
112
|
-
* dep in package.json.
|
|
113
|
-
*
|
|
114
|
-
* If the second attempt also fails, exit non-zero so `openclaw plugins
|
|
115
|
-
* install` surfaces the failure to the agent rather than writing
|
|
116
|
-
* `enabled: true` over a broken install.
|
|
117
|
-
*
|
|
118
|
-
* Skipped if `TOTALRECLAW_SKIP_POSTINSTALL_RETRY=1` (CI / sandboxes that
|
|
119
|
-
* cannot reach the registry from inside the postinstall hook).
|
|
120
|
-
*/
|
|
121
|
-
function retryNpmInstall() {
|
|
122
|
-
if (process.env.TOTALRECLAW_SKIP_POSTINSTALL_RETRY === '1') {
|
|
123
|
-
warn('TOTALRECLAW_SKIP_POSTINSTALL_RETRY=1 — skipping retry');
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
log('first-attempt smoke check failed — clearing node_modules and retrying npm install once...');
|
|
128
|
-
const nm = path.join(here, 'node_modules');
|
|
129
|
-
if (fs.existsSync(nm)) {
|
|
130
|
-
fs.rmSync(nm, { recursive: true, force: true });
|
|
131
|
-
}
|
|
132
|
-
// Note: we deliberately re-invoke npm install here. The `--ignore-scripts`
|
|
133
|
-
// flag is critical — without it we'd re-trigger this same postinstall
|
|
134
|
-
// and recurse forever.
|
|
135
|
-
execSync('npm install --no-audit --no-fund --ignore-scripts', {
|
|
136
|
-
cwd: here,
|
|
137
|
-
stdio: 'inherit',
|
|
138
|
-
});
|
|
139
|
-
log('retry npm install completed; re-validating deps');
|
|
140
|
-
return true;
|
|
141
|
-
} catch (err) {
|
|
142
|
-
warn(`retry npm install failed: ${err.message}`);
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function validateDepsOrFail() {
|
|
148
|
-
const firstMiss = smokeCheckDeps();
|
|
149
|
-
if (firstMiss.length === 0) {
|
|
150
|
-
log(`smoke check OK (${CRITICAL_DEPS.length} critical deps resolved)`);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
warn(`smoke check failed on first attempt:`);
|
|
155
|
-
for (const m of firstMiss) {
|
|
156
|
-
warn(` - ${m.dep}: ${m.message}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const retried = retryNpmInstall();
|
|
160
|
-
if (!retried) {
|
|
161
|
-
process.exitCode = 1;
|
|
162
|
-
throw new Error(
|
|
163
|
-
`TotalReclaw postinstall: critical deps missing after npm install — ` +
|
|
164
|
-
`[${firstMiss.map((m) => m.dep).join(', ')}]. ` +
|
|
165
|
-
`Re-run \`openclaw plugins install @totalreclaw/totalreclaw\` to retry, ` +
|
|
166
|
-
`or set TOTALRECLAW_SKIP_POSTINSTALL_RETRY=1 to bypass and surface the ` +
|
|
167
|
-
`original error.`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const secondMiss = smokeCheckDeps();
|
|
172
|
-
if (secondMiss.length === 0) {
|
|
173
|
-
log(`smoke check OK after retry (${CRITICAL_DEPS.length} deps resolved)`);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
process.exitCode = 1;
|
|
178
|
-
throw new Error(
|
|
179
|
-
`TotalReclaw postinstall: deps still missing after retry — ` +
|
|
180
|
-
`[${secondMiss.map((m) => m.dep).join(', ')}]. ` +
|
|
181
|
-
`This is likely a permanent breakage (registry outage, package rename, ` +
|
|
182
|
-
`or corrupted node_modules). The plugin will not load. Original errors:\n` +
|
|
183
|
-
secondMiss.map((m) => ` - ${m.dep}: ${m.message}`).join('\n'),
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// Step 3 — sweep `.openclaw-install-stage-*` siblings (issue #190)
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Resolve the OpenClaw extensions dir from the plugin's own location.
|
|
193
|
-
* The plugin lives at `<extensions>/totalreclaw/` so the parent is the
|
|
194
|
-
* extensions root. Returns null if the layout is not what we expect
|
|
195
|
-
* (npm tarball linked outside an `<extensions>/` parent — e.g. dev
|
|
196
|
-
* checkout) so we never delete random siblings.
|
|
197
|
-
*/
|
|
198
|
-
function resolveExtensionsDir() {
|
|
199
|
-
// `here` is the plugin root (this file is at the package root, NOT in dist/).
|
|
200
|
-
// The parent should be the OpenClaw extensions directory.
|
|
201
|
-
const parent = path.resolve(here, '..');
|
|
202
|
-
// Heuristic check: only sweep if we look like we're inside an OpenClaw
|
|
203
|
-
// install dir. We accept (a) the well-known `extensions` dirname, OR
|
|
204
|
-
// (b) the presence of any sibling `.openclaw-install-stage-*` (which is
|
|
205
|
-
// proof we're inside an extensions dir).
|
|
206
|
-
if (path.basename(parent) === 'extensions') return parent;
|
|
207
|
-
try {
|
|
208
|
-
const entries = fs.readdirSync(parent);
|
|
209
|
-
if (entries.some((n) => n.startsWith('.openclaw-install-stage-'))) {
|
|
210
|
-
return parent;
|
|
211
|
-
}
|
|
212
|
-
} catch {
|
|
213
|
-
// Parent unreadable — bail safely.
|
|
214
|
-
}
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function sweepStagingSiblings() {
|
|
219
|
-
const extensionsDir = resolveExtensionsDir();
|
|
220
|
-
if (!extensionsDir) {
|
|
221
|
-
log('no extensions parent detected (dev checkout?) — skipping staging sweep');
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
let removed = 0;
|
|
225
|
-
let entries;
|
|
226
|
-
try {
|
|
227
|
-
entries = fs.readdirSync(extensionsDir);
|
|
228
|
-
} catch (err) {
|
|
229
|
-
warn(`could not list ${extensionsDir}: ${err.message}`);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
for (const name of entries) {
|
|
233
|
-
if (!name.startsWith('.openclaw-install-stage-')) continue;
|
|
234
|
-
const target = path.join(extensionsDir, name);
|
|
235
|
-
try {
|
|
236
|
-
const st = fs.lstatSync(target);
|
|
237
|
-
if (!st.isDirectory()) continue;
|
|
238
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
239
|
-
removed++;
|
|
240
|
-
log(`removed stale staging dir: ${name}`);
|
|
241
|
-
} catch (err) {
|
|
242
|
-
warn(`could not remove ${name}: ${err.message}`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (removed === 0) {
|
|
246
|
-
log('no stale staging dirs to sweep');
|
|
247
|
-
} else {
|
|
248
|
-
log(`swept ${removed} stale staging dir(s)`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
|
-
// Main
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
|
|
256
|
-
clearPartialInstallMarker();
|
|
257
|
-
sweepStagingSiblings();
|
|
258
|
-
validateDepsOrFail();
|
|
259
|
-
|
|
260
|
-
log('postinstall complete');
|