@totalreclaw/totalreclaw 3.3.2-rc.1 → 3.3.2-rc.3

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 CHANGED
@@ -4,32 +4,83 @@ All notable changes to `@totalreclaw/totalreclaw` (the OpenClaw plugin) are docu
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
8
-
9
- ### 3.3.2-rc.1 preinstall orphan-stage cleanup (umbrella #182 F6 / issue #190)
10
-
11
- The rc.21 fix (#126) added `cleanupInstallStagingDirs` called at plugin
12
- register time. That helper is too late for the re-install scenario: with
13
- an orphan `.openclaw-install-stage-*` sibling already present in
14
- `~/.openclaw/extensions/`, OpenClaw's config validator fires
15
- `duplicate plugin id detected; global plugin will be overridden by global
16
- plugin` BEFORE plugin register runs, so registration is blocked entirely
17
- and the helper never gets a chance to clean up. The user observes the
18
- warning and a plugin that silently fails to load.
19
-
20
- Fix: a new `preinstall.mjs` script runs at npm preinstall time (before
21
- the new install is renamed into place). It removes orphan
22
- `.openclaw-install-stage-*` siblings from `~/.openclaw/extensions/`
23
- (and `$OPENCLAW_STATE_DIR/extensions/` when set), skipping the script's
24
- own cwd basename so older OpenClaw versions that stage inside extensions/
25
- don't delete their own in-flight install. Also keeps the existing
26
- `.tr-partial-install` marker drop. Best-effort throughout never throws
27
- or exits non-zero.
28
-
29
- Regression: `test_issue_190_preinstall_orphan_cleanup.test.ts`
30
- (19 assertions) covers newer-OpenClaw flow (cwd in /tmp), older-OpenClaw
31
- flow (cwd inside extensions/), `OPENCLAW_STATE_DIR` env override,
32
- no-extensions-dir fallback, and unrelated-sibling preservation.
7
+ ## [3.3.2-rc.1] — 2026-04-27
8
+
9
+ Hotfix bundle for the inside-gateway agent-flow ship-stoppers caught by the
10
+ 2026-04-27 user QA against stable 3.3.1 (umbrella issue #182). The four fixes
11
+ combined unblock the agent-driven canonical install path.
12
+
13
+ ### Added filesystem load manifest (issue #186)
14
+
15
+ The plugin now writes `.loaded.json` and `.error.json` into its own
16
+ extension directory at register-time. The agent has no working CLI inside
17
+ the gateway (issue #182 finding F1 `openclaw plugins list` hangs in
18
+ some Docker setups), so the manifests are the canonical filesystem signal
19
+ that register() ran to completion AND which tools the SDK saw.
20
+
21
+ - `~/.openclaw/extensions/totalreclaw/.loaded.json`
22
+ `{loadedAt: <ms>, tools: [<name>...], version: <semver>}`. Written
23
+ synchronously at the end of `register(api)`. Captures every tool name
24
+ passed to `api.registerTool` during the call.
25
+ - `~/.openclaw/extensions/totalreclaw/.error.json`
26
+ `{loadedAt, error, stack?, version?}`. Written from the try/catch
27
+ surrounding the register() body when register() throws. Successful
28
+ boots clear any stale `.error.json`; failed boots preserve any prior
29
+ `.loaded.json` so the agent can compare timestamps.
30
+
31
+ Synchronous writes only (same constraint as `registerHttpRoute` the SDK
32
+ freezes plugin registries the moment register() returns; an async write
33
+ would race that freeze and the manifest could miss late tool
34
+ registrations).
35
+
36
+ Regression: `load-manifest.test.ts` (22 assertions).
37
+
38
+ ### Added — `totalreclaw_pair` declared in skill manifest (issue #185)
39
+
40
+ The `totalreclaw_pair` tool is now advertised in `skill.json` alongside
41
+ `totalreclaw_remember`/`recall`/`forget`/`export`/`status`/`consolidate`/
42
+ `upgrade`/`import_from`. Previously plugin-only — if the plugin runtime
43
+ load failed silently (e.g. dep race in #188), the tool never appeared
44
+ in the agent's toolset and the canonical setup flow was unreachable.
45
+
46
+ The skill-side declaration ensures the tool name is visible in the
47
+ skill registry advertisement even when plugin runtime issues prevent
48
+ binding. The implementation remains in the plugin (browser-side
49
+ e2e-encrypted recovery-phrase flow); only the declaration moves up.
50
+
51
+ ### Added — atomic dependency validation in postinstall (issue #188)
52
+
53
+ `postinstall.mjs` is now a real lifecycle script (replacing the inline
54
+ `node -e` shim). After `npm install`, it require()s every critical dep
55
+ (`@scure/bip39`, `@scure/bip39/wordlists/english.js`, `@totalreclaw/core`,
56
+ `@totalreclaw/client`, `qrcode`, `ws`). On first-attempt failure: clears
57
+ `node_modules`, re-runs `npm install --ignore-scripts` once, re-validates.
58
+ If retry also fails, exits non-zero so the install surfaces the failure
59
+ instead of writing `enabled: true` over a broken half-state.
60
+
61
+ The retry loop can be skipped with `TOTALRECLAW_SKIP_POSTINSTALL_RETRY=1`
62
+ for sandboxed CI / restricted-network environments.
63
+
64
+ Phrase-safety: the script does NOT touch credentials.json, mnemonics,
65
+ or any phrase code path. Only validates module loading and cleans
66
+ staging directories.
67
+
68
+ ### Added — install-stage cleanup at install-time (issue #190)
69
+
70
+ `postinstall.mjs` extends the rc.21 staging-cleanup behavior (which ran
71
+ at register-time) to ALSO sweep `<extensions>/.openclaw-install-stage-*`
72
+ siblings during the post-install step itself. Goal: a re-install starts
73
+ from a clean parent dir, eliminating the
74
+ "duplicate plugin id detected; global plugin will be overridden by global
75
+ plugin" warning during the install. Safety: skipped when the plugin's
76
+ parent dir is not an `extensions/` directory (dev checkouts) so no random
77
+ siblings are deleted.
78
+
79
+ Regression: `postinstall-validation.test.ts` (17 assertions) covers
80
+ happy path, marker clearing, staging sweep, idempotent re-runs,
81
+ unrelated-dotfile preservation, and dev-checkout safety.
82
+
83
+
33
84
 
34
85
  ### Install / runtime hygiene (issues #126, #128)
35
86
 
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: totalreclaw
3
3
  description: "Install + set up TotalReclaw encrypted memory for OpenClaw, then use totalreclaw_remember / totalreclaw_recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
4
- version: 3.3.2-rc.1
4
+ version: 3.3.2-rc.3
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -420,6 +420,107 @@ export function wipePartialInstall(pluginRootDir) {
420
420
  return false;
421
421
  }
422
422
  }
423
+ // ---------------------------------------------------------------------------
424
+ // Plugin load manifest (.loaded.json / .error.json) — 3.3.2-rc.1 #186
425
+ // ---------------------------------------------------------------------------
426
+ /**
427
+ * Filenames written into the plugin root dir at the end of register() and
428
+ * (on failure) from the surrounding try/catch. The presence of `.loaded.json`
429
+ * is the canonical filesystem signal that the plugin's register() body ran
430
+ * to completion AND the SDK tool/route/hook registries received calls.
431
+ *
432
+ * Acceptance criteria (issue #186):
433
+ * - `cat ~/.openclaw/extensions/totalreclaw/.loaded.json` shows
434
+ * `{loadedAt, tools, version}` after every successful gateway start.
435
+ * - `cat ~/.openclaw/extensions/totalreclaw/.error.json` shows
436
+ * `{loadedAt, error, stack}` if register() threw.
437
+ * - Both are overwritten on each register() call so the agent can rely
438
+ * on the timestamp matching the most recent gateway start.
439
+ *
440
+ * Why these MUST be synchronous writes (same constraint as
441
+ * `registerHttpRoute` per the comment in index.ts around the route
442
+ * registration site): the SDK loader treats register() returning as the
443
+ * signal to freeze the registries. An async `fs.promises.writeFile` would
444
+ * settle one microtask AFTER the loader has moved on, so the manifest
445
+ * could miss tools that registered late OR drop entirely if the gateway
446
+ * exits before the microtask runs. `writeFileSync` is the only safe choice.
447
+ */
448
+ export const PLUGIN_LOADED_MANIFEST = '.loaded.json';
449
+ export const PLUGIN_ERROR_MANIFEST = '.error.json';
450
+ /**
451
+ * Resolve the plugin root dir from the loaded module's directory. The plugin
452
+ * is shipped with `dist/index.js` as the entry, so `import.meta.url` resolves
453
+ * to `<root>/dist/`. We walk up one level to put the manifests next to
454
+ * `package.json`. Defensive: if the caller already passed the root (no
455
+ * trailing `dist`), we still return a sensible path.
456
+ */
457
+ function resolvePluginRootForManifest(pluginDir) {
458
+ const base = path.basename(pluginDir);
459
+ return base === 'dist' ? path.resolve(pluginDir, '..') : pluginDir;
460
+ }
461
+ /**
462
+ * Write the success manifest. SYNCHRONOUS — the SDK freezes the plugin
463
+ * registries the moment register() returns; `fs.promises.writeFile` would
464
+ * race that freeze and the manifest could miss late tool registrations or
465
+ * never land at all if the process exits before the microtask runs.
466
+ *
467
+ * Best-effort: returns `true` on success, `false` on any I/O error. Never
468
+ * throws — failing to write the manifest is a diagnostic loss, not a
469
+ * correctness loss, so we don't propagate.
470
+ *
471
+ * The manifest is written at mode 0644 (world-readable). It contains no
472
+ * secrets — only a timestamp, the (publicly known) tool names, and the
473
+ * plugin version. Cleared first so a stale `.error.json` from a previous
474
+ * failed boot doesn't survive a successful boot.
475
+ */
476
+ export function writePluginManifest(pluginDir, manifest) {
477
+ try {
478
+ const root = resolvePluginRootForManifest(pluginDir);
479
+ if (!fs.existsSync(root))
480
+ return false;
481
+ const loadedPath = path.join(root, PLUGIN_LOADED_MANIFEST);
482
+ const errorPath = path.join(root, PLUGIN_ERROR_MANIFEST);
483
+ // Best-effort error-file cleanup — a successful boot supersedes any
484
+ // prior failure marker. If the unlink fails (e.g. permission), the
485
+ // .loaded.json timestamp still tells the agent which is current.
486
+ try {
487
+ if (fs.existsSync(errorPath))
488
+ fs.unlinkSync(errorPath);
489
+ }
490
+ catch {
491
+ // Swallow — best-effort.
492
+ }
493
+ fs.writeFileSync(loadedPath, JSON.stringify(manifest, null, 2));
494
+ return true;
495
+ }
496
+ catch {
497
+ return false;
498
+ }
499
+ }
500
+ /**
501
+ * Write the error manifest. SYNCHRONOUS for the same reason as
502
+ * `writePluginManifest`. Called from the try/catch surrounding the
503
+ * register() body so the agent has a filesystem signal that the plugin
504
+ * registered AT LEAST attempted to load and failed in a specific way.
505
+ *
506
+ * Does NOT clear `.loaded.json` from a prior successful boot — keeping
507
+ * the older success marker around lets the agent see "last good boot was
508
+ * X, current boot failed at Y" without spelunking logs. The newer
509
+ * `.error.json` timestamp wins as "current state".
510
+ */
511
+ export function writePluginError(pluginDir, error) {
512
+ try {
513
+ const root = resolvePluginRootForManifest(pluginDir);
514
+ if (!fs.existsSync(root))
515
+ return false;
516
+ const errorPath = path.join(root, PLUGIN_ERROR_MANIFEST);
517
+ fs.writeFileSync(errorPath, JSON.stringify(error, null, 2));
518
+ return true;
519
+ }
520
+ catch {
521
+ return false;
522
+ }
523
+ }
423
524
  /**
424
525
  * Drop the `.tr-partial-install` marker into `pluginRootDir`. Idempotent
425
526
  * (overwrites any existing marker) and best-effort — returns `true` on