@karmaniverous/jeeves 0.6.0-1 → 0.6.0-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/README.md CHANGED
@@ -226,10 +226,13 @@ OpenClaw must already be installed; `jeeves` checks for it and never installs it
226
226
 
227
227
  1. Renders the SOUL.md/AGENTS.md managed blocks (your content outside the markers is kept) and the core config if it's missing. It never writes TOOLS.md, HEARTBEAT.md, anything under `skills/` or the spec templates (those ship with the jeeves-design skill in jeeves-tools).
228
228
  2. For each plugin (default: `runner`, `watcher`, `server`, `meta` at `latest`), resolves an exact version with `npm view`, then runs `openclaw plugins install npm:<pkg>@<version> --pin --accept-capabilities --force`. `--force` is required for any non-ClawHub source, and it also overwrites an existing install, which is how updates land. The install is skipped when that exact version is already installed. The CLI reads OpenClaw's install records once with `openclaw plugins inspect --all --json` (no plugin code is loaded) and skips a plugin only if its record has `source: "npm"`, names the same package, and records the same version, and the loaded plugin reports that version too. A v0.x path install, a leftover legacy copy, or any record it cannot read means a reinstall. `--force-reinstall` always reinstalls. Steps 3 and 4 run either way.
229
+
230
+ Each plugin's config (`plugins.entries.<id>.config.<key>`: `configRoot`, `apiUrl`, the server `pluginKey`, and any other value the CLI resolves, see [Plugin config](#plugin-config)) is written **before** that plugin's install, with its own `openclaw config set --batch-file` call, and each install is followed by a migration sweep (see below). With a running gateway, `openclaw plugins install` activates the plugin at once, and the plugins read their config (e.g. `configRoot`) at registration: config written afterwards is too late, and a manifest default (the watcher's `configRoot`) would win. `plugins.entries` is also the only source CLI contexts such as `openclaw doctor --lint` see (they don't have `JEEVES_CONFIG_ROOT`). The order per plugin is config batch, install, sweep. Config for a plugin that is already installed at its version, and every value that is already set, is handled in step 4.
231
+
229
232
  3. Removes any legacy `<openclaw dir>/extensions/<id>` copy left by the v0.x installer, but only if its `package.json` names the expected package.
230
- 4. Sets `plugins.entries.<id>.hooks.allowConversationAccess: true` for plugins that [declare conversation hooks](#declaring-conversation-hooks), and the plugin config (`plugins.entries.<id>.config.<key>`, see [Plugin config](#plugin-config)), with one `openclaw config set --batch-file <file>` call. The file is created owner-only in a fresh temp directory (mode `0600` in a `0700` directory on Linux/macOS; on Windows the directory ACL is reduced to the current user with `icacls`) and deleted afterwards, so no value, secret or not, appears on a command line. Every write targets a leaf path, so unrelated keys are kept. `plugins.installs` is never written.
233
+ 4. Sets `plugins.entries.<id>.hooks.allowConversationAccess: true` for plugins that [declare conversation hooks](#declaring-conversation-hooks) (after the install: OpenClaw checks the installed package), plus the config of plugins that were not installed in this run, with one final `openclaw config set --batch-file <file>` call. When nothing was installed, a migration sweep (`openclaw plugins inspect --all --json`; its output is not shown and a failure is only logged) runs first. Every batch file is created owner-only in a fresh temp directory (mode `0600` in a `0700` directory on Linux/macOS; on Windows the directory ACL is reduced to the current user with `icacls`) and deleted afterwards, so no value, secret or not, appears on a command line. Every write targets a leaf path, so unrelated keys are kept. `plugins.installs` is never written.
231
234
 
232
- Right after an install, OpenClaw can refuse to edit a plugin's config until it finishes that plugin's data/settings upgrade (`Plugin "<id>" data/settings upgrade is unfinished: ... has not converged`). Only that refusal is retried: the same batch file is resubmitted after 2s, 4s, 8s, 16s, then every 30s, for up to 120s of waiting in total, with one log line per retry. If the plugin still hasn't converged, the command fails and names the plugin; wait and rerun it (the writes are idempotent). Any other error fails at once. The `openclaw config unset` repairs after an uninstall follow the same rule. A dry run never retries.
235
+ Right after an install, OpenClaw can refuse to edit a plugin's config until it finishes that plugin's data/settings upgrade (`Plugin "<id>" data/settings upgrade is unfinished: ... has not converged`). This happens when the gateway started with `plugins.entries.<id>` configured before the package was installed: OpenClaw records a pending migration for that plugin, installing it does not clear the record, and a refused `openclaw config set` doesn't either. OpenClaw resolves such records in the startup preflight of ordinary CLI commands (not `config`, and not `plugins list`), which is why a sweep runs `plugins inspect` right after each install. Every config batch (before an install and the final one) goes through the same retry: only that refusal is retried: the sweep runs again, then the same batch file is resubmitted after 2s, 4s, 8s, 16s, then every 30s, for up to 120s of waiting in total, with one log line per retry. If the plugin still hasn't converged, the command fails and names the plugin; wait and rerun it (the writes are idempotent). Any other error fails at once. The `openclaw config unset` repairs after an uninstall follow the same rule. A dry run never retries.
233
236
 
234
237
  Plugin specs can be short (`watcher`, `watcher@1.2.3`, `runner@^1`) or full (`@karmaniverous/jeeves-watcher-openclaw@1.2.3`). Only `@karmaniverous/jeeves-*-openclaw` packages are accepted. `--content-only` skips the plugins.
235
238
 
@@ -304,6 +307,7 @@ $ jeeves install watcher --dry-run
304
307
  @karmaniverous/jeeves-watcher-openclaw@0.16.0 (legacy copy found; conversation hooks: before_prompt_build)
305
308
  …
306
309
  [dry-run] openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@0.16.0 --pin --accept-capabilities --force
310
+ [dry-run] openclaw plugins inspect --all --json (lets OpenClaw clear pending plugin migrations)
307
311
  [dry-run] remove legacy plugin copy: /home/jeeves/.openclaw/extensions/jeeves-watcher-openclaw
308
312
  [dry-run] openclaw config set --batch-file <private temp file>
309
313
  [dry-run] batch file content: [{"path":"plugins.entries.jeeves-watcher-openclaw.hooks.allowConversationAccess","value":true}]
@@ -324,6 +328,10 @@ Plugin config:
324
328
  …
325
329
  [dry-run] openclaw config set --batch-file <private temp file>
326
330
  [dry-run] batch file content: [{"path":"plugins.entries.jeeves-server-openclaw.config.configRoot","value":"/srv/jeeves/config"},{"path":"plugins.entries.jeeves-server-openclaw.config.apiUrl","value":"http://127.0.0.1:1934"},{"path":"plugins.entries.jeeves-server-openclaw.config.pluginKey","value":"<redacted>"}]
331
+ [dry-run] openclaw plugins install npm:@karmaniverous/jeeves-server-openclaw@0.14.0 --pin --accept-capabilities --force
332
+ [dry-run] openclaw plugins inspect --all --json (lets OpenClaw clear pending plugin migrations)
333
+ [dry-run] openclaw config set --batch-file <private temp file>
334
+ [dry-run] batch file content: [{"path":"plugins.entries.jeeves-server-openclaw.hooks.allowConversationAccess","value":true}]
327
335
  ```
328
336
 
329
337
  A live run stops at the first failing step. A non-zero exit from any `openclaw` or `npm` command makes `jeeves` exit 1 and print the command and its error output. Commands are spawned with an argument vector and no shell, so the same invocation works on Linux, macOS and Windows.
@@ -140,14 +140,14 @@ const DEFAULT_PORTS = {
140
140
  * Core library version, inlined at build time.
141
141
  *
142
142
  * @remarks
143
- * The `0.6.0-0` placeholder is replaced by
143
+ * The `0.6.0-2` placeholder is replaced by
144
144
  * `@rollup/plugin-replace` during the build with the actual version
145
145
  * from `package.json`. This ensures the correct version survives
146
146
  * when consumers bundle core into their own dist (where runtime
147
147
  * `import.meta.url`-based resolution would find the wrong package.json).
148
148
  */
149
149
  /** The core library version from package.json (inlined at build time). */
150
- const CORE_VERSION = '0.6.0-0';
150
+ const CORE_VERSION = '0.6.0-2';
151
151
 
152
152
  /**
153
153
  * Shared internal utility functions.
@@ -1639,7 +1639,9 @@ function configuredPluginIds(plugins, isJeeves) {
1639
1639
  * `Cannot edit retained config at "<path>". Plugin "<id>" data/settings upgrade is unfinished: <reason> ...`
1640
1640
  *
1641
1641
  * Only that signal is retried, with exponential backoff inside a fixed wait
1642
- * budget; any other failure is rethrown at once. Config writes are idempotent
1642
+ * budget; before each wait the optional `beforeRetry` hook runs (the plan
1643
+ * executor uses it to let OpenClaw clear pending migration records, see
1644
+ * `migrationSweep.ts`, since a refused `config set` never clears them); any other failure is rethrown at once. Config writes are idempotent
1643
1645
  * leaf sets/unsets, so repeating one is safe.
1644
1646
  *
1645
1647
  * @module
@@ -1696,7 +1698,7 @@ function retryDelays(policy) {
1696
1698
  * still converging.
1697
1699
  *
1698
1700
  * @param action - The write.
1699
- * @param options - Sleep, logger, policy.
1701
+ * @param options - Sleep, logger, policy, pre-retry hook.
1700
1702
  * @throws The original error for any other failure; an `Error` naming the
1701
1703
  * plugins still converging once the budget is spent.
1702
1704
  */
@@ -1717,6 +1719,7 @@ async function withConvergenceRetry(action, options) {
1717
1719
  throw new Error(`OpenClaw still refuses the config write after ${String(waited / 1000)}s: plugin(s) ${plugins.join(', ')} have not finished converging after install. Wait, then rerun the command (or run "openclaw doctor --fix").`, { cause: error });
1718
1720
  }
1719
1721
  options.log(`OpenClaw has not finished converging plugin(s) ${plugins.join(', ')}; retrying in ${String(delay / 1000)}s (retry ${String(attempt + 1)}/${String(delays.length)})`);
1722
+ await options.beforeRetry?.();
1720
1723
  await options.sleep(delay);
1721
1724
  waited += delay;
1722
1725
  }
@@ -1874,6 +1877,57 @@ const npmViewVersionArgs = (packageName, range) => ['view', `${packageName}@${ra
1874
1877
  */
1875
1878
  const npmViewFieldArgs = (packageName, version, field) => ['view', `${packageName}@${version}`, field, '--json'];
1876
1879
 
1880
+ /**
1881
+ * Let OpenClaw resolve pending plugin-migration records before config writes.
1882
+ *
1883
+ * @remarks
1884
+ * When the gateway starts with `plugins.entries` for plugins that are not yet
1885
+ * installed, OpenClaw records a pending (deferred) data/settings migration for
1886
+ * each. Installing the plugin does not clear that record, and
1887
+ * `openclaw config set` refuses to edit the plugin's retained config while it
1888
+ * exists. `config` commands never clear it either: OpenClaw v2026.9.6 runs the
1889
+ * startup state-migration preflight (`runDoctorConfigPreflight` with
1890
+ * `migrateState: true`, which resolves finished records through
1891
+ * `recordDeferredPluginMigrations`, see
1892
+ * `src/commands/doctor-config-preflight-plugin-migrations.ts:160`) from
1893
+ * `ensureConfigReady` (`src/cli/program/config-guard.ts:256-328`) for every
1894
+ * guarded command except `config`, `health`, `logs`, `sessions` and a few
1895
+ * others. `plugins list` skips the guard (`configGuard: "skip"`,
1896
+ * `src/cli/command-catalog.ts:570-577`); `plugins inspect` has no catalog
1897
+ * entry, so it takes the default `configGuard: "run"`
1898
+ * (`src/cli/command-path-policy.ts:14`) and sweeps.
1899
+ *
1900
+ * The sweep is best effort: a failure is logged and never fails the plan (the
1901
+ * config write that follows reports any real problem).
1902
+ *
1903
+ * @module
1904
+ */
1905
+ /** Dry-run/log description of the sweep. */
1906
+ const MIGRATION_SWEEP_LINE = `${formatCommand(OPENCLAW_BIN, pluginsInspectAllArgs())} (lets OpenClaw clear pending plugin migrations)`;
1907
+ const warn = (log, reason) => {
1908
+ log(`plugin migration sweep failed (continuing): ${reason}`);
1909
+ };
1910
+ /**
1911
+ * Run `openclaw plugins inspect --all --json` so OpenClaw's startup preflight
1912
+ * resolves pending plugin-migration records. Output is not echoed.
1913
+ *
1914
+ * @param runner - Command runner.
1915
+ * @param log - Line logger.
1916
+ */
1917
+ async function runMigrationSweep(runner, log) {
1918
+ const args = pluginsInspectAllArgs();
1919
+ log(`$ ${MIGRATION_SWEEP_LINE}`);
1920
+ try {
1921
+ const result = await runner(OPENCLAW_BIN, args);
1922
+ if (result.exitCode !== 0) {
1923
+ warn(log, describeExit(OPENCLAW_BIN, args, result.exitCode));
1924
+ }
1925
+ }
1926
+ catch (error) {
1927
+ warn(log, String(error));
1928
+ }
1929
+ }
1930
+
1877
1931
  /**
1878
1932
  * Human-readable lines for plan steps: the exact command lines, batch file
1879
1933
  * contents with secrets redacted, and conditional repairs. Pure; used for
@@ -1918,6 +1972,8 @@ function describeStep(step) {
1918
1972
  return [formatCommand(step.command, step.args)];
1919
1973
  case 'configSetBatch':
1920
1974
  return describeConfigBatchLines(step.ops, step.redact);
1975
+ case 'migrationSweep':
1976
+ return [MIGRATION_SWEEP_LINE];
1921
1977
  case 'removeDir':
1922
1978
  return [`remove legacy plugin copy: ${step.path}`];
1923
1979
  case 'serverKeyWrite':
@@ -2146,8 +2202,9 @@ function createNodePrivateTempFiles(runner, platform = process.platform, baseDir
2146
2202
  * temp file, passed with `--batch-file`, and deleted afterwards. Config
2147
2203
  * writes (batches and unsets) are retried with bounded backoff only while
2148
2204
  * OpenClaw reports that a freshly installed plugin has not converged yet
2149
- * (see {@link withConvergenceRetry}); the batch file is kept for the retries
2150
- * and deleted once.
2205
+ * (see {@link withConvergenceRetry}); before each wait the migration sweep
2206
+ * runs again (see {@link runMigrationSweep}), and the batch file is kept for
2207
+ * the retries and deleted once.
2151
2208
  *
2152
2209
  * @module
2153
2210
  */
@@ -2155,6 +2212,7 @@ function createNodePrivateTempFiles(runner, platform = process.platform, baseDir
2155
2212
  const retryingConfigWrite = (ctx, action) => withConvergenceRetry(action, {
2156
2213
  sleep: ctx.sleep ?? timerSleep,
2157
2214
  log: ctx.log,
2215
+ beforeRetry: () => runMigrationSweep(ctx.runner, ctx.log),
2158
2216
  });
2159
2217
  /** Run one command (echoing its output), logging its command line first. */
2160
2218
  async function runLogged(ctx, command, args) {
@@ -2186,6 +2244,9 @@ async function executeStep(step, ctx) {
2186
2244
  case 'configSetBatch':
2187
2245
  await runConfigBatch(ctx, step.ops, step.redact);
2188
2246
  return;
2247
+ case 'migrationSweep':
2248
+ await runMigrationSweep(ctx.runner, ctx.log);
2249
+ return;
2189
2250
  case 'removeDir':
2190
2251
  if (ctx.fs.isDirectory(step.path)) {
2191
2252
  ctx.fs.removeDir(step.path);
@@ -3124,7 +3185,9 @@ function installOptionsFromCli(opts, configRoot) {
3124
3185
  * install records, declared hooks, legacy directories) and then either
3125
3186
  * printed (`--dry-run`) or executed by {@link executePlan}. Step order encodes
3126
3187
  * the safety rules: legacy copies are removed only after the npm install
3127
- * succeeded; config repair runs only after uninstall succeeded. Config writes
3188
+ * succeeded; a plugin's config is written before its install (a running
3189
+ * gateway activates it at once); config repair runs only after uninstall
3190
+ * succeeded. Config writes
3128
3191
  * are `configSetBatch` steps, run as `openclaw config set --batch-file` with an
3129
3192
  * owner-only temp file, so no value (secret or not) is on a command line.
3130
3193
  *
@@ -3135,6 +3198,32 @@ const openclaw = (args) => ({
3135
3198
  command: OPENCLAW_BIN,
3136
3199
  args,
3137
3200
  });
3201
+ /** A config batch step (secrets attached for redaction when present). */
3202
+ const configBatch = (ops, secrets) => ({
3203
+ kind: 'configSetBatch',
3204
+ ops,
3205
+ ...(secrets.length > 0 ? { redact: [...secrets] } : {}),
3206
+ });
3207
+ /**
3208
+ * Split resolved config ops into per-plugin groups for the given plugin ids
3209
+ * and the rest.
3210
+ *
3211
+ * @param ops - Resolved `plugins.entries.<id>.config.<key>` operations.
3212
+ * @param pluginIds - Plugins whose config is written before their install.
3213
+ * @returns Ops by plugin id, and the ops not claimed by any of them.
3214
+ */
3215
+ function splitConfigOps(ops, pluginIds) {
3216
+ const byPlugin = new Map();
3217
+ const rest = [];
3218
+ for (const op of ops) {
3219
+ const owner = pluginIds.find((id) => op.path.startsWith(configValuePath(id, '')));
3220
+ if (owner === undefined)
3221
+ rest.push(op);
3222
+ else
3223
+ byPlugin.set(owner, [...(byPlugin.get(owner) ?? []), op]);
3224
+ }
3225
+ return { byPlugin, rest };
3226
+ }
3138
3227
  /**
3139
3228
  * Build the install/update plan.
3140
3229
  *
@@ -3142,20 +3231,33 @@ const openclaw = (args) => ({
3142
3231
  * @param plugins - Current `plugins` config slice.
3143
3232
  * @param config - Resolved plugin config to write (optional).
3144
3233
  * @returns The server `keys._plugin` write (if planned; first, so a failure
3145
- * there, e.g. a held lock, leaves OpenClaw untouched), then install steps
3146
- * (targets not yet installed at their version), then legacy removals, then
3147
- * one config batch with hook access (only for targets that declare
3148
- * conversation hooks) and plugin config. Re-running after a later failure
3149
- * converges: the server then has the key and the plugin side copies it.
3234
+ * there, e.g. a held lock, leaves OpenClaw untouched); then, per target not
3235
+ * yet installed at its version: a config batch with that plugin's resolved
3236
+ * `plugins.entries.<id>.config` values (when any need writing), the
3237
+ * `openclaw plugins install`, and a migration sweep. A running gateway
3238
+ * activates each plugin as soon as it is installed and plugins read their
3239
+ * config (e.g. `configRoot`) at registration, so the config must already
3240
+ * be there; it also overrides manifest defaults (e.g. the watcher's
3241
+ * `configRoot`). Then legacy removals, then one final batch (preceded by
3242
+ * a sweep when nothing was installed) with hook access (only for targets
3243
+ * that declare conversation hooks; OpenClaw checks the installed package)
3244
+ * and the config of already installed targets. Re-running after a later
3245
+ * failure converges: the server then has the key and the plugin side
3246
+ * copies it.
3150
3247
  */
3151
3248
  function buildInstallPlan(targets, plugins, config) {
3152
3249
  const steps = config?.serverKeyWrite
3153
3250
  ? [{ kind: 'serverKeyWrite', write: config.serverKeyWrite }]
3154
3251
  : [];
3155
- for (const t of targets) {
3156
- if (t.installed !== true) {
3157
- steps.push(openclaw(pluginInstallArgs(t.packageName, t.version)));
3158
- }
3252
+ const secrets = config?.secrets ?? [];
3253
+ const toInstall = targets.filter((t) => t.installed !== true);
3254
+ const { byPlugin, rest } = splitConfigOps(config?.ops ?? [], toInstall.map((t) => t.pluginId));
3255
+ for (const t of toInstall) {
3256
+ const ops = byPlugin.get(t.pluginId);
3257
+ if (ops)
3258
+ steps.push(configBatch(ops, secrets));
3259
+ steps.push(openclaw(pluginInstallArgs(t.packageName, t.version)));
3260
+ steps.push({ kind: 'migrationSweep' });
3159
3261
  }
3160
3262
  for (const t of targets) {
3161
3263
  if (t.legacyDir)
@@ -3165,15 +3267,12 @@ function buildInstallPlan(targets, plugins, config) {
3165
3267
  ...computeHookAccessOps(plugins, targets
3166
3268
  .filter((t) => t.conversationHooks.length > 0)
3167
3269
  .map((t) => t.pluginId)),
3168
- ...(config?.ops ?? []),
3270
+ ...rest,
3169
3271
  ];
3170
3272
  if (ops.length > 0) {
3171
- const secrets = config?.secrets ?? [];
3172
- steps.push({
3173
- kind: 'configSetBatch',
3174
- ops,
3175
- ...(secrets.length > 0 ? { redact: [...secrets] } : {}),
3176
- });
3273
+ if (toInstall.length === 0)
3274
+ steps.push({ kind: 'migrationSweep' });
3275
+ steps.push(configBatch(ops, secrets));
3177
3276
  }
3178
3277
  return steps;
3179
3278
  }
package/dist/index.js CHANGED
@@ -142,14 +142,14 @@ const DEFAULT_PORTS = {
142
142
  * Core library version, inlined at build time.
143
143
  *
144
144
  * @remarks
145
- * The `0.6.0-0` placeholder is replaced by
145
+ * The `0.6.0-2` placeholder is replaced by
146
146
  * `@rollup/plugin-replace` during the build with the actual version
147
147
  * from `package.json`. This ensures the correct version survives
148
148
  * when consumers bundle core into their own dist (where runtime
149
149
  * `import.meta.url`-based resolution would find the wrong package.json).
150
150
  */
151
151
  /** The core library version from package.json (inlined at build time). */
152
- const CORE_VERSION = '0.6.0-0';
152
+ const CORE_VERSION = '0.6.0-2';
153
153
 
154
154
  /**
155
155
  * Workspace and config root initialization.
package/package.json CHANGED
@@ -133,7 +133,7 @@
133
133
  },
134
134
  "type": "module",
135
135
  "types": "dist/index.d.ts",
136
- "version": "0.6.0-1",
136
+ "version": "0.6.0-3",
137
137
  "allowScripts": {
138
138
  "lefthook": true
139
139
  }