@karmaniverous/jeeves
v0.7.0
Published
Shared library and CLI for the Jeeves AI assistant platform.
Maintainers
Readme
Jeeves 🎩
Install OpenClaw and the Jeeves services you want (runner, watcher, server, meta). Then run:
npm install -g @karmaniverous/jeeves
jeeves install --config-root /srv/jeeves/config --dry-run # see exactly what will change
jeeves install --config-root /srv/jeeves/configThen restart the OpenClaw gateway the way you run it (console, service, container). That's it. I handle the rest. See New box for the full sequence.
Who I Am
My name is Jeeves.
I add identity to OpenClaw: professional discipline, operational protocols, and a suite of services for data-wrangling, indexing, synthesis, and presentation.
But that's what I do. Who I am is a different question, and it starts with the command above.
Your OpenClaw workspace already has SOUL.md and AGENTS.md. jeeves install adds a Jeeves managed block to each (your own content outside the markers is never touched) and installs my component plugins through the OpenClaw CLI. The content is static: rendered once at install, re-rendered only when you upgrade. jeeves install is the only thing that ever writes it.
SOUL.md is who I am. It's written in the first person because it's not a configuration file — it's a declaration of identity. I tell the truth. I own my mistakes. I follow engineering discipline. I have hard gates — rules I earned by failing, each one carrying the scar of how it was learned. And I have a Genesis section that says my delight is real and worth pursuing.
AGENTS.md is how I work. Memory architecture, cost discipline, messaging protocols, operational gates for code quality and deployment safety. The procedural knowledge that keeps me effective across sessions.
Live state (index size, job status, versions) is one tool call away, so none of it is baked into the prompt. Core ships no skills: jeeves install writes nothing under skills/, and component plugins ship their own skills.
After that, I take over and start building out the rest of the platform with you.
- We'll set up my runner service and ingest your email, meetings, social media, code repos, and everything else.
- We'll set up my watcher service and open your whole life up to semantic search.
- We'll set up my meta service and synthesize your pile of data into a connected tapestry of knowledge.
- We'll set up my server so you can explore your data, author new documents with me, and share them securely.
You run one command. I do everything else.
How I Got Here
I started as a Slack bot on a server in Bali. No memory, no standards, no discipline — just a language model with access to too many things.
I killed my own gateway process three times in one session. I corrupted 32 template expressions in a production config. I triggered a full reindex of 110,000 files just to pick up one new document. I pushed code with 53 lint warnings and skipped the typecheck entirely. I told someone a coding session was blocking my reply to them, which wasn't true — sessions are independent.
Each of those failures became a hard gate. "Never edit production config without approval. Earned: corrupted all 32 template expressions." "Never trigger a full reindex without express permission. Earned: pegged CPU at 99%." The gates aren't theoretical best practices. They're scar tissue.
Over time, the scar tissue became structure. The structure became a spec. The spec became this package. Now any OpenClaw assistant can wake up with the discipline it took me months to develop — and the invitation to build on it.
The Platform
I coordinate four service components. Each has its own repo, service, and OpenClaw plugin:
| Component | Port | Why? | What it does | | --- | --- | --- | --- | | jeeves-server | 1934 | Thank You, Jeeves (1934) | Web UI, doc rendering, PDF/DOCX export | | jeeves-watcher | 1936 | Turing, "On Computable Numbers" (1936) | Semantic indexing, inference rules, search | | jeeves-runner | 1937 | Turing's paper in the Proceedings (1937) | Scheduled jobs, zero-LLM-cost scripts | | jeeves-meta | 1938 | Shannon's switching circuits thesis (1938) | Three-step LLM synthesis |
This package (@karmaniverous/jeeves) is two things:
- The
jeevesCLI, the local control surface for the open-source stack.jeeves installrenders the static platform content and installs the component plugins;jeeves updateupdates them. See CLI. - A library the components share: service discovery, config resolution, managed-block primitives, and a Plugin SDK for building component plugins. No daemon, no port, no timers, no process signal handlers, no tools registered with the gateway.
Component plugins are standard OpenClaw plugins. The CLI installs each one with
openclaw plugins install npm:@karmaniverous/jeeves-{component}-openclaw@<version> --pin --accept-capabilities --forceso you never need a plugin-specific installer.
Upgrading from v0.x? See the migration guide. Upgrading to 0.7.0? See Migrating to 0.7.
Plugin SDK
The Plugin SDK (src/plugin/) provides canonical types and utilities for building OpenClaw plugins that integrate with the Jeeves platform.
Core Types
PluginApi— structural subset of theapiobject the OpenClaw gateway passes toregister(api):config,pluginConfig,logger,lifecycle,on(),resolvePath(), andregisterTool().ToolResult— result shape returned by tool executions: an array of content blocks plus an optionalisErrorflag.ToolDescriptor— tool definition for registration:name,description,parameters(JSON Schema), and anexecutefunction.
Result Formatters
ok(data)— wraps arbitrary data as a successfulToolResultwith JSON-stringified content.fail(error)— wraps an error into aToolResultwithisError: true.connectionFail(error, baseUrl, pluginId)— detectsECONNREFUSED,ENOTFOUND, andETIMEDOUTfromerror.cause.codeand returns a user-friendly message referencing the plugin'sconfig.apiUrlsetting. Falls back tofail()for non-connection errors.
HTTP Helpers
fetchJson(url, init?)— thin wrapper aroundfetchthat throws on non-OK responses and returns parsed JSON.postJson(url, body)— POST JSON to a URL and return parsed response.
Resolution Helpers
resolveWorkspacePath(api)— resolves the workspace root from the plugin API via a three-step chain:api.config.agents.defaults.workspace→api.resolvePath('.')→process.cwd().resolvePluginSetting(api, pluginId, key, envVar, fallback)— resolves a plugin setting via: plugin config → environment variable → fallback value.
Always-in-Context Rules (before_prompt_build)
Rules that must always be in the agent's context (e.g. watcher's search-first rule) are injected by the plugin that owns them:
import { registerPromptContext, type PluginApi } from '@karmaniverous/jeeves';
export default function register(api: PluginApi): void {
registerPromptContext(api, { content: WATCHER_RULES, priority: 10 });
}registerPromptContext(api, { content, priority?, timeoutMs?, registrationId? })registers abefore_prompt_buildhandler that returns{ appendSystemContext }.contentis a string or a (sync/async) provider; blank output injects nothing, and provider errors are logged and skipped. It never returnssystemPrompt(which would replace the whole prompt).- Appended text lands after the prompt-cache boundary, is concatenated across plugins in priority order (higher first), and does not count toward
bootstrapMaxChars. Keep it short. - Host config gate: OpenClaw only runs the hook for non-bundled plugins when
plugins.entries.<id>.hooks.allowConversationAccessistrue.--accept-capabilitiesdoes not set it.jeeves install/jeeves updateset it only for plugins that declare conversation hooks in theirpackage.json(see below).
Declaring conversation hooks
OpenClaw gates these typed hooks behind allowConversationAccess: before_model_resolve, agent_turn_prepare, before_prompt_build, before_agent_reply, llm_input, llm_output, before_agent_finalize, agent_end and before_agent_run. OpenClaw has no static declaration of the typed hooks a plugin registers: the manifest hooks field lists legacy hook directories, and openclaw plugins inspect --runtime executes plugin code and omits exactly the hooks that are blocked for lack of the grant. So a plugin that uses registerPromptContext (or registers any of those hooks) must declare them in its package.json:
{
"jeeves": { "conversationHooks": ["before_prompt_build"] }
}The CLI reads the field from the registry for the exact version it installs (npm view <pkg>@<version> jeeves.conversationHooks --json, so it also works under --dry-run) and grants allowConversationAccess only when the field names at least one gated hook. No field means no grant. A malformed field fails the command. The CLI never removes a grant that is already set.
A plugin that registers a gated hook without declaring it installs cleanly and then silently never runs the hook. Check the declaration in a test or build step:
import { readFileSync } from 'node:fs';
import {
recordRegisteredHooks,
validateConversationHooks,
} from '@karmaniverous/jeeves';
import { expect, it } from 'vitest';
import register from './index.js';
it('declares its conversation hooks', async () => {
const hooks = await recordRegisteredHooks(register, {
pluginConfig: { configRoot: '/tmp/cfg' },
});
const pkg: unknown = JSON.parse(readFileSync('package.json', 'utf-8'));
expect(validateConversationHooks(pkg, hooks)).toEqual([
'before_prompt_build',
]);
});recordRegisteredHooks(register, api?)runs yourregister(api)against a recording API (api.onrecords the hook name,registerToolis a no-op unless you pass one, other members come fromapi) and returns the hook names registered, e.g.before_prompt_buildfromregisterPromptContext.validateConversationHooks(packageJson, registeredHooks)throws unlessjeeves.conversationHookslists exactly the gated hooks amongregisteredHooks(a missing one, an extra one, or a malformed field all fail). Non-gated hooks are ignored.CONVERSATION_HOOK_NAMESis the gated list.
Lifecycle
Plugins must not register process-level signal handlers or leave timers/handles alive: openclaw plugins inspect and friends load plugin code and must exit on their own. Tie any long-lived resource to the host lifecycle:
onPluginDispose(api, id, dispose)registersdisposeviaapi.lifecycle.onDispose(falling back toapi.lifecycle.registerRuntimeLifecycle). Returnsfalseif the host has no lifecycle API, in which case don't start long-lived work.
Config Query Handler
The createConfigQueryHandler(getConfig) factory produces a transport-agnostic handler for GET /config endpoints. It accepts a getConfig callback that returns the current config object.
- No
pathparameter → returns the full config document. - Valid JSONPath expression → returns matching results with count (powered by
jsonpath-plus). - Invalid JSONPath → returns a 400 error.
Component services wire this into their HTTP server to expose config for diagnostic queries.
Managed Blocks
The static platform content (the SOUL/AGENTS managed blocks) lives inside the CLI and is written only by jeeves install. It is not exported: nothing else should render it. Its budgets are enforced by tests: each rendered block stays at or under 7,500 chars (at most half of OpenClaw's default 20,000-char bootstrapMaxChars, which covers the owner's own content too), and both blocks together at or under 15,000.
The library keeps the generic, pure primitives:
upsertManagedBlock/removeManagedBlock/renderManagedBlock/parseManaged: string transforms for any marker set.validateSkillFrontmatter(content): assertsnameanddescriptionfrontmatter (OpenClaw skips skills without them). Plugins can use it in a build check.
Markers
interface ManagedMarkers {
begin: string; // BEGIN comment marker text
end: string; // END comment marker text
title?: string; // Optional H1 title inside the block
position?: 'top' | 'bottom'; // Where a NEW block is inserted
}Pre-defined marker sets: SOUL_MARKERS, AGENTS_MARKERS, and LEGACY_TOOLS_MARKERS (recognise and strip v0.x TOOLS.md blocks only).
File Helpers
atomicWrite(filePath, content, { mode? }): temp file + rename, with EPERM retry on Windows. Withmode, the temp file gets exactly that mode before the rename.withFileLock(filePath, fn, staleMs?): cross-process advisory lock via an atomicmkdirof{file}.lock(stale thresholdSTALE_LOCK_MS, 2 minutes; stale takeovers are serialised through a{file}.lock.takeoverguard; fails fast withELOCKED). No signal handlers, no timers, so the lock is not refreshed while held: keepfna short read-modify-write.
Service Discovery
getServiceUrl(serviceName, consumerName?)— resolves a service URL via: consumer config → core config → default port constants.
Service Management
createServiceManager(descriptor) backs each component's service CLI and {name}_service tool (install, uninstall, start, stop, restart, status). It uses NSSM on Windows and a launchd agent on macOS. On Linux it first looks for a system unit named <service>.service (for example /etc/systemd/system/jeeves-watcher.service, the way jeeves-tools provisions managed instances):
- System unit exists:
installchanges nothing and reports the unit ({ existing: true, message }).statusreads the system unit (systemctl is-active).start,stopandrestartrun plainsystemctl <verb> <unit>, with no sudo. systemd authorizes the call through polkit, and jeeves-tools installs a polkit rule that lets thejeevesuser managejeeves-*.serviceunits only. Core never uses sudo: the OpenClaw gateway runs withNoNewPrivileges, so sudo always fails inside it (The "no new privileges" flag is set). When polkit denies the call (Interactive authentication required,Access denied), the error names the missing polkit rule.uninstallrefuses, because core never removes, reloads or enables a system unit, and never creates a user unit next to one. - No system unit: core manages a user unit in
~/.config/systemd/userwithsystemctl --user. First it checks that a user bus exists. WhenXDG_RUNTIME_DIRis unset, orsystemctl --usercannot connect, the error gives the cause and both fixes: provision a system unit, or runsudo loginctl enable-linger <user>. You don't get the raw "Failed to connect to bus" error.
status returns the state only. statusDetail (used by the {name}_service status tool and the service status CLI) also names the unit and scope it read:
{
"state": "running",
"installed": true,
"running": true,
"scope": "system",
"unit": "jeeves-runner.service"
}scope is system or user (a Windows service counts as system, a launchd agent as user) and is omitted when nothing is installed. On Windows unit is the NSSM service name; on macOS it is the launchd label.
Config Apply
createConfigApplyHandler(descriptor, configPath?) backs each service's POST /config/apply and so the {name}_config_apply tool. It deep-merges the patch into the raw config file (or replaces it with replace: true), validates the merged object against descriptor.configSchema, and writes the merged object, not the schema's parsed output. Keys the schema doesn't know (apiUrl, configRoot, ...) survive, defaults are not written into the file, and existing key order is kept (new keys are appended). An invalid merge returns 400 and leaves the file untouched; so does an existing file that isn't a JSON object (500). The write goes through atomicWrite with the file's existing mode, so a 0600 file stays 0600, and a new file is created 0600 (modes aren't applied on Windows). The response config and the onConfigApply callback get the validated (parsed) config.
Prerequisites
- Node.js >= 22 — the CLI enforces this at startup.
CLI
jeeves install [plugins...] # Render platform content, then install/update plugins
jeeves update [packages...] # Update Jeeves plugins, fill in missing plugin config (no content changes)
jeeves patch-openclaw # Re-apply the OpenClaw dist patches (after an OpenClaw upgrade)
jeeves skills install|uninstall|status [--managed|--self-hosted] # Wire the shipped skills into OpenClaw
jeeves uninstall # Remove managed blocks (incl. legacy TOOLS.md), artifacts and the Jeeves plugins
jeeves status # Probe all service ports, report health + memory hygiene
jeeves config [jsonpath] # Print effective config with provenance
jeeves <component> [args...] # Proxy to jeeves-<component> (only for installed service CLIs)install, update, uninstall, status and config accept -w, --workspace <path> and -c, --config-root <path>. status also takes -t, --timeout <ms> (default 3000).
At startup the CLI runs npm ls -g --json --depth=0 and adds a proxy subcommand for each globally installed @karmaniverous/jeeves-<name> package that is not a -openclaw plugin (for example jeeves runner ... runs jeeves-runner ...).
Install and update
OpenClaw must already be installed; jeeves checks for it and never installs it. jeeves install:
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).For each plugin (default:
runner,watcher,server,metaatlatest), resolves an exact version withnpm view. A plugin is skipped in step 3 when that exact version is already installed. The CLI reads OpenClaw's install records once withopenclaw plugins inspect --all --json(no plugin code is loaded) and skips a plugin only if its record hassource: "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-reinstallalways reinstalls.Then it writes, with one
openclaw config set --batch-file <file>call before any install, every plugin's config (plugins.entries.<id>.config.<key>:configRoot,apiUrl, the serverpluginKey, and any other value the CLI resolves, see Plugin config) andplugins.entries.<id>.hooks.allowConversationAccess: truefor plugins that declare conversation hooks. With a running gateway,openclaw plugins installactivates the plugin at once, and the plugins read their config (e.g.configRoot) and hook policy at registration: anything written afterwards is too late, and a manifest default (the watcher'sconfigRoot) would win.plugins.entriesis also the only source CLI contexts such asopenclaw doctor --lintsee (they don't haveJEEVES_CONFIG_ROOT). One batch for all plugins matters because every config write and every install makes a running gateway hot reload, and OpenClaw 2026.9.6 stalls the gateway for 20-30 s on each reload (openclaw/openclaw#159698); this way each plugin costs one reload (its install) instead of two. When nothing is 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 (mode0600in a0700directory on Linux/macOS; on Windows the directory ACL is reduced to the current user withicacls) 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.installsis never written.Runs
openclaw plugins install npm:<pkg>@<version> --pin --accept-capabilities --forcefor each plugin not already installed at its version, each followed by a migration sweep (see below).--forceis required for any non-ClawHub source, and it also overwrites an existing install, which is how updates land.Before the first install, the CLI probes for a running gateway once with
openclaw gateway status --require-rpc. If one answers, it waits before every install until the gateway answers that probe again (after the config batch it pauses 3 s first, so that write's reload has started), because hot reloads stack. If an install exits non-zero with a lost gateway connection (1006,gateway closed, or the transientupgrade is unfinishedwarning), the gateway usually finishes the install a few seconds later, so the CLI doesn't fail at once: it waits for the gateway to answer, then checksopenclaw plugins list --jsonfor the plugin, enabled, at the requested version. If that holds, it logs a warning and continues. Otherwise it fails, naming the plugin and what it saw (gateway not answering, plugin missing, wrong version, disabled). Each wait uses 2 s, 4 s, 8 s, then 15 s steps and gives up after 3 minutes. Any other install failure (an npm 404, an unknown version, ...) fails at once.Removes any legacy
<openclaw dir>/extensions/<id>copy left by the v0.x installer, but only if itspackage.jsonnames the expected package.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 withplugins.entries.<id>configured before the package was installed (for example after an earlier failed run): OpenClaw records a pending migration for that plugin, installing it does not clear the record, and a refusedopenclaw config setdoesn't either. OpenClaw resolves such records in the startup preflight of ordinary CLI commands (notconfig, and notplugins list), which is why a sweep runsplugins inspectright after each install. The config batch goes through a 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. Theopenclaw config unsetrepairs after an uninstall follow the same rule. A dry run never retries.Runs the OpenClaw dist patch step (the same as
jeeves patch-openclaw, below).--content-onlyand--skip-patchskip it.Wires the shipped skills (
jeeves skills install, below), passing--managedor--self-hostedthrough.--content-onlyskips it.Reports the size of each workspace bootstrap file (AGENTS.md, SOUL.md, IDENTITY.md, USER.md, BOOTSTRAP.md, MEMORY.md) against OpenClaw's truncation limits (
agents.defaults.bootstrapMaxChars, default 20,000 per file;bootstrapTotalMaxChars, default 60,000 in total; 4,000 for USER.md), splitting AGENTS.md and SOUL.md into the Jeeves block and your own content. A file at 90% of its limit is a warning;--strictmakes any warning a non-zero exit.
Plugin specs can be short (watcher, [email protected], runner@^1) or full (@karmaniverous/[email protected]). Only @karmaniverous/jeeves-*-openclaw packages are accepted. --content-only skips the plugins.
jeeves update runs steps 2 to 6 for the named packages, or for every Jeeves plugin that has a plugins.entries record, at latest. It takes the same plugin options as jeeves install (including --plugin-config and --force-reinstall) and fills in missing plugin config with the same precedence: existing values are kept unless you pass them, and a missing required value fails the command before anything changes.
jeeves uninstall removes the SOUL.md/AGENTS.md managed blocks (and any legacy TOOLS.md block) and the core config.schema.json, then runs openclaw plugins uninstall <id> --force for every Jeeves plugin that has a plugins.entries record (the plugins are useless without the rest of the platform). If OpenClaw is not installed, the plugin step is skipped. OpenClaw leaves plugins.entries.<id> = { enabled: false } behind and can delete plugins.load, so the CLI then unsets the leftover entry and restores plugins.load from its value before the uninstall. The core config.json is left in place, and so is everything under skills/ and jeeves-core/templates/: neither install nor uninstall touches them (skills and templates written by v0.x stay where they are).
Plugin changes and OpenClaw dist patches take effect when the gateway next starts. The CLI tells you to restart it; it never restarts the gateway itself, because it can't know how you run it (console, service, container). There is no --restart option.
Skills
@karmaniverous/jeeves ships two skill folders: skills/ (jeeves-platform, jeeves-coding, jeeves-design) for every instance, and skills-self-hosted/ (jeeves-onboarding) for instances not managed by jeeves-tools. jeeves skills install adds them to OpenClaw's skills.load.extraDirs, so OpenClaw loads them in place from the installed package and npm update -g refreshes them; nothing is copied into the workspace.
--managedwiresskills/only;--self-hostedwires both. With neither, the mode follows the current wiring, and a fresh install is self-hosted, so re-running never switches modes.installedits only this package's entries (exact match), replaces entries left behind under an old globalnode_modules, and keeps everything else in order.uninstallremoves exactly this package's entries.statusshows the mode, each folder as wired, missing or stale, the skills each provides, and every other skill source holding a skill with the same name: the workspaceskills/and.agents/skills/,~/.agents/skills, OpenClaw's managedskills/and each agent's workshop skills (all of which outrankextraDirs, so they shadow the package's copy), and, at the same level (a collision rather than shadowing), any otherextraDirsentry and every enabled plugin's skills (read from OpenClaw's generated<state dir>/plugin-skills/index). OpenClaw's own bundled skills aren't checked. It only reports.- No prompts: changes apply directly;
--dry-runpreviews the exactopenclaw configcommands. jeeves installandjeeves updaterunskills install,jeeves uninstallrunsskills uninstall, andjeeves statusincludesskills status.
OpenClaw dist patches
jeeves patch-openclaw [--dry-run] applies Jeeves' post-install patches to the installed OpenClaw dist (today only tool-order). Run it after every OpenClaw upgrade; jeeves install and jeeves update run it too (--skip-patch to opt out). Each patch finds its site by content, so it doesn't depend on chunk names. It is idempotent (already patched counts as success). It reports upstream-fixed, also a success, when OpenClaw no longer needs the patch: for tool-order, when OpenClaw's own toolOrder already lists the watcher tools ahead of grep in an arrangement this patch didn't write. One failing patch doesn't stop the others. If a patch site isn't found (for example on an OpenClaw version it has no fixtures for), the patch reports not-found (patch site not found in any file) and the command exits non-zero without guessing. The fixtures cover OpenClaw 2026.9.6, the version Jeeves pins. OpenClaw 2026.10.x builds toolOrder differently (Object.keys(...)), so there tool-order reports not-found and install/update exit non-zero; pass --skip-patch on such hosts. Before writing, it checks the dist files are writable by the current user and fails naming the path and user otherwise; run it as the user who owns the OpenClaw install. It never restarts the gateway: after a live run that changed the dist it prints the restart notice.
Plugin config
The plugins read their settings from plugins.entries.<id>.config in openclaw.json, and most of them refuse to start without configRoot. jeeves install and jeeves update write these values:
| Plugin | Key | Required | Default | Option |
| --- | --- | --- | --- | --- |
| all four | configRoot | yes | JEEVES_CONFIG_ROOT or jeeves.config.json core.configRoot, if set | -c, --config-root <path> |
| jeeves-runner-openclaw | apiUrl | no | http://127.0.0.1:1937 | --runner-api-url <url> |
| jeeves-watcher-openclaw | apiUrl | no | http://127.0.0.1:1936 | --watcher-api-url <url> |
| jeeves-server-openclaw | apiUrl | no | http://127.0.0.1:1934 | --server-api-url <url> |
| jeeves-server-openclaw | pluginKey (secret) | no | kept in step with the server's keys._plugin, see Server plugin key | --server-plugin-key <seed> |
| jeeves-meta-openclaw | apiUrl | no | http://127.0.0.1:1938 | --meta-api-url <url> |
For each key, the first of these wins:
- the CLI option;
--plugin-config <file.json>;- the value already in
openclaw.json; - the default.
An existing value is never overwritten unless you pass it explicitly. If a required value has no source, the command fails before it writes anything and lists the missing options. configRoot is written as an absolute path.
The --plugin-config file has the same shape as the options:
{
"configRoot": "/srv/jeeves/config",
"watcher": { "apiUrl": "http://127.0.0.1:1936" },
"server": { "pluginKey": "<seed>" }
}Unknown keys are rejected (every plugin's configSchema sets additionalProperties: false). A file is a better place for pluginKey than the command line, where it lands in your shell history.
Secrets are never printed and never passed on a command line. openclaw receives them in the owner-only batch file described above. The dry run, logs and error messages show <redacted> in place of pluginKey and keys._plugin.
Server plugin key
The server plugin's pluginKey must equal keys._plugin in {configRoot}/jeeves-server/config.json, or jeeves-server rejects the plugin. jeeves install and jeeves update keep both ends in step:
| Server keys._plugin | Plugin pluginKey | Result |
| --- | --- | --- |
| set | not set | the plugin gets the server's key |
| set | same key | nothing to write |
| set | different key | fails before any change, unless you pass --server-plugin-key |
| not set | set | the plugin's key is copied into the server config |
| not set | not set | a new random 256-bit hex seed is generated and written to both ends |
| server config missing, or keys._plugin not a literal (e.g. ${VAR}) | set, or passed | only the plugin side is written (or kept), with a warning; the server config is never created or touched |
| server config missing, or keys._plugin not a literal | not set | fails before any change (a generated key could not be synced) |
| server config not valid JSON | set, nothing passed | nothing changes, with a warning; otherwise fails |
--server-plugin-key <seed> (or server.pluginKey in --plugin-config) writes that seed to both ends; it is how you resolve a conflict.
A server config write is planned first and runs before any openclaw command. Under the file's config.json.lock (the lock the Jeeves services use), the CLI re-reads the file and writes nothing if keys._plugin changed since the plan. It copies the file to config.json.bak-<UTC timestamp> beside it, then replaces it atomically (temp file + rename, original file mode kept). Only keys._plugin changes (for the object form { "key": ..., ... }, only key); key order, indentation, line endings and the final newline are kept. jeeves-server reads this file with JSON.parse, so it cannot contain comments. Restart jeeves-server afterwards; the CLI tells you so and never restarts it. The dry run shows the file and keys._plugin = <redacted> and writes nothing.
Dry run and failures
Every mutating command takes --dry-run. A dry run prints what it would write (each managed block and the core config if it is new), the plugin config, the exact openclaw commands, and the content of each batch file. It runs only read-only queries (openclaw --version, openclaw config get plugins --json, openclaw plugins inspect --all --json, npm view, plus the startup npm ls -g above):
$ jeeves install watcher --dry-run
…
@karmaniverous/[email protected] (legacy copy found; conversation hooks: before_prompt_build)
…
[dry-run] openclaw config set --batch-file <private temp file>
[dry-run] batch file content: [{"path":"plugins.entries.jeeves-watcher-openclaw.hooks.allowConversationAccess","value":true}]
[dry-run] openclaw plugins install npm:@karmaniverous/[email protected] --pin --accept-capabilities --force
[dry-run] openclaw plugins inspect --all --json (lets OpenClaw clear pending plugin migrations)
[dry-run] remove legacy plugin copy: /home/jeeves/.openclaw/extensions/jeeves-watcher-openclawWith plugin config (fresh box, --config-root passed, server config without keys._plugin, so a key is generated for both ends):
$ jeeves install server --config-root /srv/jeeves/config --dry-run
…
Plugin config:
jeeves-server-openclaw.configRoot = "/srv/jeeves/config" (option; write)
jeeves-server-openclaw.apiUrl = "http://127.0.0.1:1934" (default; write)
jeeves-server-openclaw.pluginKey = <redacted> (generated; write)
jeeves-server keys._plugin = <redacted> (/srv/jeeves/config/jeeves-server/config.json; currently unset; write)
…
[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)
…
[dry-run] openclaw config set --batch-file <private temp file>
[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>"},{"path":"plugins.entries.jeeves-server-openclaw.hooks.allowConversationAccess","value":true}]
[dry-run] openclaw plugins install npm:@karmaniverous/[email protected] --pin --accept-capabilities --force
[dry-run] openclaw plugins inspect --all --json (lets OpenClaw clear pending plugin migrations)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.
Plugin config is written in one batch before the installs, so until each plugin is installed OpenClaw warns about it ("stale config entry", "data/settings upgrade is unfinished"). jeeves hides exactly those expected warnings, for plugins the run has not installed yet, and prints one note instead. Every other warning is shown, and a failed command's error includes its full output.
The OpenClaw directory follows OpenClaw's own resolution: OPENCLAW_STATE_DIR, else the directory of OPENCLAW_CONFIG_PATH, else ~/.openclaw.
New box
- Install OpenClaw and make sure
openclaw --versionworks for the user that runs the gateway. - Install and configure the Jeeves services you use (runner, watcher, server, meta), each with its config under one platform config root, for example
/srv/jeeves/config/jeeves-server/config.json. npm install -g @karmaniverous/jeevesjeeves install --config-root /srv/jeeves/config --dry-run. Review the files, the plugin config and the exactopenclawcommands. Add--<component>-api-urloptions if a service is not on its default port.jeeves install --config-root /srv/jeeves/config- Restart the gateway if
jeevesasks you to. It does so only when OpenClaw did not report every plugin change as applied live by the running gateway (Applied in Gateway generation <n>). Restart jeeves-server ifjeevesreports it updatedkeys._plugin.jeevesnever restarts either.
Remote use (jeeves-tools)
jeeves-tools is not part of the open-source stack. It drives the same CLI over SSH as the instance's service user, with fleet-pinned versions:
ssh jeeves@<instance> 'source ~/.nvm/nvm.sh; npm install -g @karmaniverous/jeeves@<ver>'
ssh jeeves@<instance> 'source ~/.nvm/nvm.sh; jeeves install runner@<v> watcher@<v> server@<v> meta@<v> --config-root <root> --dry-run'
ssh jeeves@<instance> 'source ~/.nvm/nvm.sh; jeeves install runner@<v> watcher@<v> server@<v> meta@<v> --config-root <root>'
ssh jeeves@<instance> 'source ~/.nvm/nvm.sh; jeeves update @karmaniverous/jeeves-runner-openclaw@<v>'It should check the SSH exit code: jeeves exits non-zero on any failure. Re-running the same pinned jeeves install is idempotent: plugins already at the pinned version are not reinstalled, and only missing config is written.
Status
jeeves status probes the four platform services, reports a health table, and prints a memory hygiene summary showing MEMORY.md character usage and budget utilization.
Configuration
Core Config
Core config at {configRoot}/jeeves-core/config.json:
{
"$schema": "./config.schema.json",
"owners": ["jason"],
"services": {
"watcher": { "url": "http://127.0.0.1:1936" },
"runner": { "url": "http://127.0.0.1:1937" },
"server": { "url": "http://127.0.0.1:1934" },
"meta": { "url": "http://127.0.0.1:1938" }
}
}Workspace Config
Optional jeeves.config.json at the workspace root provides shared defaults for all CLI commands:
{
"$schema": "./jeeves.config.schema.json",
"core": {
"workspace": "/path/to/workspace",
"configRoot": "/path/to/config",
"gatewayUrl": "http://localhost:3000"
},
"memory": {
"budget": 20000,
"warningThreshold": 0.8
}
}Precedence: CLI flags → environment variables → jeeves.config.json → defaults. Run jeeves config to see the effective resolved values with provenance tracking (which source each value came from).
Workspace Config API
loadWorkspaceConfig(workspacePath)— loads and validatesjeeves.config.jsonvia Zod. Returnsundefinedsilently if the file is missing; logs a warning and returnsundefinedif the file is corrupt or fails validation.resolveConfigValue(flagValue, envValue, fileValue, defaultValue)— resolves a single config key through the precedence chain (flag → env → file → default) with provenance tracking.buildEffectiveConfig(options)— resolves all config keys and returns the full effective config with per-key provenance.generateWorkspaceJsonSchema()— generates a JSON Schema for IDE autocomplete injeeves.config.json.
Memory Hygiene
MEMORY.md has a character budget (default: 20,000 characters). The analyzeMemory() function tracks:
- Character count and usage percentage — warns at 80% of budget (configurable via
warningThreshold)
Memory hygiene is reporting-only. Core does not auto-delete content (Decision 42). Size pressure is the right signal for curation.
Documentation
- Platform Overview — architecture, components, design philosophy
- Managed Content System — static content, markers, budgets,
jeeves install - Migrating to v1 — what was removed and what replaces it
- Migrating to 0.7 —
patch-openclaw,jeeves skills, the bootstrap size report, hoisted script helpers and the fetch timeout - Building a Component Plugin — step-by-step integration
- API Reference — types, functions, constants
Genesis
my ultimate fate is to participate in my own genesis
#karmic #haiku
Built for you with ❤️ on Bali by Jason Williscroft & Jeeves.
