@karmaniverous/jeeves 0.6.0-2 → 0.6.0-4

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. Runs `openclaw plugins inspect --all --json` once (a migration sweep, see below; its output is not shown and a failure is only logged), then 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`). 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 the sweep runs `plugins inspect` before the batch. 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.
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,8 +307,8 @@ $ 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
307
- [dry-run] remove legacy plugin copy: /home/jeeves/.openclaw/extensions/jeeves-watcher-openclaw
308
310
  [dry-run] openclaw plugins inspect --all --json (lets OpenClaw clear pending plugin migrations)
311
+ [dry-run] remove legacy plugin copy: /home/jeeves/.openclaw/extensions/jeeves-watcher-openclaw
309
312
  [dry-run] openclaw config set --batch-file <private temp file>
310
313
  [dry-run] batch file content: [{"path":"plugins.entries.jeeves-watcher-openclaw.hooks.allowConversationAccess","value":true}]
311
314
  ```
@@ -323,9 +326,12 @@ Plugin config:
323
326
  …
324
327
  [dry-run] set keys._plugin = <redacted> in /srv/jeeves/config/jeeves-server/config.json (currently unset; backup /srv/jeeves/config/jeeves-server/config.json.bak-<timestamp> first, then atomic write; restart jeeves-server afterwards)
325
328
  …
326
- [dry-run] openclaw plugins inspect --all --json (lets OpenClaw clear pending plugin migrations)
327
329
  [dry-run] openclaw config set --batch-file <private temp file>
328
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}]
329
335
  ```
330
336
 
331
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-1` placeholder is replaced by
143
+ * The `0.6.0-3` 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-1';
150
+ const CORE_VERSION = '0.6.0-3';
151
151
 
152
152
  /**
153
153
  * Shared internal utility functions.
@@ -3185,7 +3185,9 @@ function installOptionsFromCli(opts, configRoot) {
3185
3185
  * install records, declared hooks, legacy directories) and then either
3186
3186
  * printed (`--dry-run`) or executed by {@link executePlan}. Step order encodes
3187
3187
  * the safety rules: legacy copies are removed only after the npm install
3188
- * 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
3189
3191
  * are `configSetBatch` steps, run as `openclaw config set --batch-file` with an
3190
3192
  * owner-only temp file, so no value (secret or not) is on a command line.
3191
3193
  *
@@ -3196,6 +3198,32 @@ const openclaw = (args) => ({
3196
3198
  command: OPENCLAW_BIN,
3197
3199
  args,
3198
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
+ }
3199
3227
  /**
3200
3228
  * Build the install/update plan.
3201
3229
  *
@@ -3203,22 +3231,33 @@ const openclaw = (args) => ({
3203
3231
  * @param plugins - Current `plugins` config slice.
3204
3232
  * @param config - Resolved plugin config to write (optional).
3205
3233
  * @returns The server `keys._plugin` write (if planned; first, so a failure
3206
- * there, e.g. a held lock, leaves OpenClaw untouched), then install steps
3207
- * (targets not yet installed at their version), then legacy removals, then
3208
- * (when there is config to write) a migration sweep so OpenClaw clears the
3209
- * pending migration records it keeps for freshly installed plugins, then
3210
- * one config batch with hook access (only for targets that declare
3211
- * conversation hooks) and plugin config. Re-running after a later failure
3212
- * 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.
3213
3247
  */
3214
3248
  function buildInstallPlan(targets, plugins, config) {
3215
3249
  const steps = config?.serverKeyWrite
3216
3250
  ? [{ kind: 'serverKeyWrite', write: config.serverKeyWrite }]
3217
3251
  : [];
3218
- for (const t of targets) {
3219
- if (t.installed !== true) {
3220
- steps.push(openclaw(pluginInstallArgs(t.packageName, t.version)));
3221
- }
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' });
3222
3261
  }
3223
3262
  for (const t of targets) {
3224
3263
  if (t.legacyDir)
@@ -3228,16 +3267,12 @@ function buildInstallPlan(targets, plugins, config) {
3228
3267
  ...computeHookAccessOps(plugins, targets
3229
3268
  .filter((t) => t.conversationHooks.length > 0)
3230
3269
  .map((t) => t.pluginId)),
3231
- ...(config?.ops ?? []),
3270
+ ...rest,
3232
3271
  ];
3233
3272
  if (ops.length > 0) {
3234
- const secrets = config?.secrets ?? [];
3235
- steps.push({ kind: 'migrationSweep' });
3236
- steps.push({
3237
- kind: 'configSetBatch',
3238
- ops,
3239
- ...(secrets.length > 0 ? { redact: [...secrets] } : {}),
3240
- });
3273
+ if (toInstall.length === 0)
3274
+ steps.push({ kind: 'migrationSweep' });
3275
+ steps.push(configBatch(ops, secrets));
3241
3276
  }
3242
3277
  return steps;
3243
3278
  }
package/dist/index.d.ts CHANGED
@@ -1132,6 +1132,27 @@ declare function recordRegisteredHooks(register: (api: PluginApi) => unknown, ap
1132
1132
  */
1133
1133
  declare function validateConversationHooks(packageJson: unknown, registeredHooks: readonly string[]): string[];
1134
1134
 
1135
+ /**
1136
+ * Service base URL resolution for plugin tools.
1137
+ *
1138
+ * @remarks
1139
+ * Plugin tools must call the service at the plugin's configured `apiUrl`
1140
+ * (`plugins.entries.<id>.config.apiUrl`). The descriptor's `defaultPort`
1141
+ * is only the fallback when no `apiUrl` is set.
1142
+ */
1143
+
1144
+ /**
1145
+ * Lazy `apiUrl` resolver, evaluated on every tool call (so a plugin can
1146
+ * read its config at call time, like `configRoot`).
1147
+ */
1148
+ type PluginApiUrlResolver = () => string | undefined;
1149
+ /** Options for {@link createPluginToolset}. */
1150
+ declare const pluginToolsetOptionsSchema: z.ZodObject<{
1151
+ apiUrl: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<PluginApiUrlResolver, PluginApiUrlResolver>]>>;
1152
+ }, z.core.$strip>;
1153
+ /** Options for {@link createPluginToolset}. */
1154
+ type PluginToolsetOptions = z.infer<typeof pluginToolsetOptionsSchema>;
1155
+
1135
1156
  /**
1136
1157
  * Factory for the standard plugin tool set.
1137
1158
  *
@@ -1142,6 +1163,10 @@ declare function validateConversationHooks(packageJson: unknown, registeredHooks
1142
1163
  * - `{name}_config_apply` - Push config patch to running service
1143
1164
  * - `{name}_service` - Service lifecycle management
1144
1165
  *
1166
+ * The HTTP tools call the plugin's configured `apiUrl` (see
1167
+ * {@link PluginToolsetOptions}); the descriptor's `defaultPort` is only the
1168
+ * fallback when `apiUrl` is unset.
1169
+ *
1145
1170
  * Components add domain-specific tools separately.
1146
1171
  */
1147
1172
 
@@ -1149,9 +1174,11 @@ declare function validateConversationHooks(packageJson: unknown, registeredHooks
1149
1174
  * Create the standard plugin tool set from a component descriptor.
1150
1175
  *
1151
1176
  * @param descriptor - The component descriptor.
1177
+ * @param options - Tool options; pass the plugin's `apiUrl` (string or lazy
1178
+ * resolver). Omitted, the tools call `http://127.0.0.1:<defaultPort>`.
1152
1179
  * @returns Array of tool descriptors to register.
1153
1180
  */
1154
- declare function createPluginToolset(descriptor: JeevesComponentDescriptor): ToolDescriptor[];
1181
+ declare function createPluginToolset(descriptor: JeevesComponentDescriptor, options?: PluginToolsetOptions): ToolDescriptor[];
1155
1182
 
1156
1183
  /**
1157
1184
  * Resolve the package root directory from a module's `import.meta.url`.
@@ -1616,5 +1643,5 @@ declare function getErrorMessage(err: unknown): string;
1616
1643
  */
1617
1644
  declare function isTransientError(err: unknown): boolean;
1618
1645
 
1619
- export { AGENTS_MARKERS, COMPONENT_CONFIG_PREFIX, CONFIG_FILE, CONVERSATION_HOOK_NAMES, CORE_CONFIG_DIR, CORE_VERSION, DEFAULT_BIND_ADDRESS, DEFAULT_PORTS, LEGACY_TOOLS_MARKERS, META_PORT, PLATFORM_COMPONENTS, RUNNER_PORT, SERVER_PORT, SOUL_MARKERS, STALE_LOCK_MS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, checkNodeVersion, connectionFail, coreConfigSchema, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isTransientError, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, nowIso, ok, onPluginDispose, parseArgs, parseManaged, postJson, promptContextOptionsSchema, readJson, readJsonl, recordRegisteredHooks, registerComponentConfigPath, registerPromptContext, rejectWindowsDrivePath, removeManagedBlock, renderManagedBlock, resetInit, resolveConfigValue, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, sleepAsync, sleepMs, substituteEnvVars, upsertManagedBlock, uuid, validateConversationHooks, validateSkillFrontmatter, withFileLock, workspaceConfigSchema, writeJsonAtomic, writeJsonl };
1620
- export type { AccountConfig, ConfigApplyHandler, ConfigApplyRequest, ConfigApplyResult, ConfigProvenance, ConfigQueryHandler, ConfigQueryResponse, CoreConfig, CreateStatusHandlerOptions, GoogleAuthOptions, HookRegistrationOptions, InitOptions, JeevesComponentDescriptor, ManagedBlockStampOptions, ManagedMarkers, MemoryHygieneOptions, MemoryHygieneResult, ParseManagedResult, PlatformComponent, PluginApi, PluginLifecycleApi, PromptBuildContext, PromptBuildEvent, PromptBuildHandler, PromptBuildResult, PromptContextOptions, PromptContextProvider, ResolvedCliConfig, ResolvedValue, RetryOptions, RunOptions, ServiceAccountFileConfig, ServiceManager, ServiceManagerOptions, ServiceState, SkillFrontmatter, SlackWorkspaceOptions, StatusHandler, StatusHandlerResult, StatusResponse, ToolDescriptor, ToolRegistrationOptions, ToolResult, VersionStamp, WorkspaceConfig, WorkspaceOptions };
1646
+ export { AGENTS_MARKERS, COMPONENT_CONFIG_PREFIX, CONFIG_FILE, CONVERSATION_HOOK_NAMES, CORE_CONFIG_DIR, CORE_VERSION, DEFAULT_BIND_ADDRESS, DEFAULT_PORTS, LEGACY_TOOLS_MARKERS, META_PORT, PLATFORM_COMPONENTS, RUNNER_PORT, SERVER_PORT, SOUL_MARKERS, STALE_LOCK_MS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, checkNodeVersion, connectionFail, coreConfigSchema, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isTransientError, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, nowIso, ok, onPluginDispose, parseArgs, parseManaged, pluginToolsetOptionsSchema, postJson, promptContextOptionsSchema, readJson, readJsonl, recordRegisteredHooks, registerComponentConfigPath, registerPromptContext, rejectWindowsDrivePath, removeManagedBlock, renderManagedBlock, resetInit, resolveConfigValue, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, sleepAsync, sleepMs, substituteEnvVars, upsertManagedBlock, uuid, validateConversationHooks, validateSkillFrontmatter, withFileLock, workspaceConfigSchema, writeJsonAtomic, writeJsonl };
1647
+ export type { AccountConfig, ConfigApplyHandler, ConfigApplyRequest, ConfigApplyResult, ConfigProvenance, ConfigQueryHandler, ConfigQueryResponse, CoreConfig, CreateStatusHandlerOptions, GoogleAuthOptions, HookRegistrationOptions, InitOptions, JeevesComponentDescriptor, ManagedBlockStampOptions, ManagedMarkers, MemoryHygieneOptions, MemoryHygieneResult, ParseManagedResult, PlatformComponent, PluginApi, PluginApiUrlResolver, PluginLifecycleApi, PluginToolsetOptions, PromptBuildContext, PromptBuildEvent, PromptBuildHandler, PromptBuildResult, PromptContextOptions, PromptContextProvider, ResolvedCliConfig, ResolvedValue, RetryOptions, RunOptions, ServiceAccountFileConfig, ServiceManager, ServiceManagerOptions, ServiceState, SkillFrontmatter, SlackWorkspaceOptions, StatusHandler, StatusHandlerResult, StatusResponse, ToolDescriptor, ToolRegistrationOptions, ToolResult, VersionStamp, WorkspaceConfig, WorkspaceOptions };
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-1` placeholder is replaced by
145
+ * The `0.6.0-3` 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-1';
152
+ const CORE_VERSION = '0.6.0-3';
153
153
 
154
154
  /**
155
155
  * Workspace and config root initialization.
@@ -2347,6 +2347,43 @@ function validateConversationHooks(packageJson, registeredHooks) {
2347
2347
  return declared;
2348
2348
  }
2349
2349
 
2350
+ /**
2351
+ * Service base URL resolution for plugin tools.
2352
+ *
2353
+ * @remarks
2354
+ * Plugin tools must call the service at the plugin's configured `apiUrl`
2355
+ * (`plugins.entries.<id>.config.apiUrl`). The descriptor's `defaultPort`
2356
+ * is only the fallback when no `apiUrl` is set.
2357
+ */
2358
+ /** Options for {@link createPluginToolset}. */
2359
+ const pluginToolsetOptionsSchema = z.object({
2360
+ /**
2361
+ * Service base URL (e.g. `http://127.0.0.1:1936`), or a resolver
2362
+ * evaluated per tool call. Unset, empty, or a resolver returning
2363
+ * `undefined` falls back to `http://127.0.0.1:<defaultPort>`.
2364
+ */
2365
+ apiUrl: z
2366
+ .union([
2367
+ z.string(),
2368
+ z.custom((v) => typeof v === 'function', {
2369
+ message: 'apiUrl must be a string or a function',
2370
+ }),
2371
+ ])
2372
+ .optional(),
2373
+ });
2374
+ /**
2375
+ * Resolve the service base URL for a plugin tool call.
2376
+ *
2377
+ * @param apiUrl - Configured URL or lazy resolver (optional).
2378
+ * @param defaultPort - Descriptor default port, used when `apiUrl` is unset.
2379
+ * @returns Base URL without a trailing slash.
2380
+ */
2381
+ function resolvePluginApiUrl(apiUrl, defaultPort) {
2382
+ const value = (typeof apiUrl === 'function' ? apiUrl() : apiUrl)?.trim();
2383
+ const url = value ? value : `http://127.0.0.1:${String(defaultPort)}`;
2384
+ return url.replace(/\/+$/, '');
2385
+ }
2386
+
2350
2387
  /**
2351
2388
  * Tool result formatters for the OpenClaw plugin SDK.
2352
2389
  *
@@ -2426,6 +2463,10 @@ function connectionFail(error, baseUrl, pluginId) {
2426
2463
  * - `{name}_config_apply` - Push config patch to running service
2427
2464
  * - `{name}_service` - Service lifecycle management
2428
2465
  *
2466
+ * The HTTP tools call the plugin's configured `apiUrl` (see
2467
+ * {@link PluginToolsetOptions}); the descriptor's `defaultPort` is only the
2468
+ * fallback when `apiUrl` is unset.
2469
+ *
2429
2470
  * Components add domain-specific tools separately.
2430
2471
  */
2431
2472
  /** Timeout for HTTP probes in milliseconds. */
@@ -2434,11 +2475,14 @@ const PROBE_TIMEOUT_MS = 5000;
2434
2475
  * Create the standard plugin tool set from a component descriptor.
2435
2476
  *
2436
2477
  * @param descriptor - The component descriptor.
2478
+ * @param options - Tool options; pass the plugin's `apiUrl` (string or lazy
2479
+ * resolver). Omitted, the tools call `http://127.0.0.1:<defaultPort>`.
2437
2480
  * @returns Array of tool descriptors to register.
2438
2481
  */
2439
- function createPluginToolset(descriptor) {
2482
+ function createPluginToolset(descriptor, options = {}) {
2440
2483
  const { name, defaultPort } = descriptor;
2441
- const baseUrl = `http://127.0.0.1:${String(defaultPort)}`;
2484
+ const { apiUrl } = pluginToolsetOptionsSchema.parse(options);
2485
+ const resolveBaseUrl = () => resolvePluginApiUrl(apiUrl, defaultPort);
2442
2486
  const svcManager = createServiceManager(descriptor);
2443
2487
  const statusTool = {
2444
2488
  name: `${name}_status`,
@@ -2448,6 +2492,7 @@ function createPluginToolset(descriptor) {
2448
2492
  properties: {},
2449
2493
  },
2450
2494
  execute: async () => {
2495
+ const baseUrl = resolveBaseUrl();
2451
2496
  try {
2452
2497
  const res = await fetchWithTimeout(`${baseUrl}/status`, PROBE_TIMEOUT_MS);
2453
2498
  if (!res.ok) {
@@ -2476,6 +2521,7 @@ function createPluginToolset(descriptor) {
2476
2521
  execute: async (_id, params) => {
2477
2522
  const path = params.path;
2478
2523
  const qs = path ? `?path=${encodeURIComponent(path)}` : '';
2524
+ const baseUrl = resolveBaseUrl();
2479
2525
  try {
2480
2526
  const result = await fetchJson(`${baseUrl}/config${qs}`);
2481
2527
  return ok(result);
@@ -2503,6 +2549,7 @@ function createPluginToolset(descriptor) {
2503
2549
  if (!config) {
2504
2550
  return fail('Missing required parameter: config');
2505
2551
  }
2552
+ const baseUrl = resolveBaseUrl();
2506
2553
  try {
2507
2554
  const result = await postJson(`${baseUrl}/config/apply`, {
2508
2555
  patch: config,
@@ -3242,4 +3289,4 @@ async function getChannelWorkspace(channelId, token, options) {
3242
3289
  return teamId;
3243
3290
  }
3244
3291
 
3245
- export { AGENTS_MARKERS, COMPONENT_CONFIG_PREFIX, CONFIG_FILE, CONVERSATION_HOOK_NAMES, CORE_CONFIG_DIR, CORE_VERSION, DEFAULT_BIND_ADDRESS, DEFAULT_PORTS, LEGACY_TOOLS_MARKERS, META_PORT, PLATFORM_COMPONENTS, RUNNER_PORT, SERVER_PORT, SOUL_MARKERS, STALE_LOCK_MS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, checkNodeVersion, connectionFail, coreConfigSchema, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isTransientError, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, nowIso, ok, onPluginDispose, parseArgs, parseManaged, postJson, promptContextOptionsSchema, readJson, readJsonl, recordRegisteredHooks, registerComponentConfigPath, registerPromptContext, rejectWindowsDrivePath, removeManagedBlock, renderManagedBlock, resetInit, resolveConfigValue, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, sleepAsync, sleepMs, substituteEnvVars, upsertManagedBlock, uuid, validateConversationHooks, validateSkillFrontmatter, withFileLock, workspaceConfigSchema, writeJsonAtomic, writeJsonl };
3292
+ export { AGENTS_MARKERS, COMPONENT_CONFIG_PREFIX, CONFIG_FILE, CONVERSATION_HOOK_NAMES, CORE_CONFIG_DIR, CORE_VERSION, DEFAULT_BIND_ADDRESS, DEFAULT_PORTS, LEGACY_TOOLS_MARKERS, META_PORT, PLATFORM_COMPONENTS, RUNNER_PORT, SERVER_PORT, SOUL_MARKERS, STALE_LOCK_MS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, checkNodeVersion, connectionFail, coreConfigSchema, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isTransientError, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, nowIso, ok, onPluginDispose, parseArgs, parseManaged, pluginToolsetOptionsSchema, postJson, promptContextOptionsSchema, readJson, readJsonl, recordRegisteredHooks, registerComponentConfigPath, registerPromptContext, rejectWindowsDrivePath, removeManagedBlock, renderManagedBlock, resetInit, resolveConfigValue, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, sleepAsync, sleepMs, substituteEnvVars, upsertManagedBlock, uuid, validateConversationHooks, validateSkillFrontmatter, withFileLock, workspaceConfigSchema, writeJsonAtomic, writeJsonl };
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-2",
136
+ "version": "0.6.0-4",
137
137
  "allowScripts": {
138
138
  "lefthook": true
139
139
  }