@atbash/cli 0.6.0-dev.1 → 0.6.1-dev.2
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 +57 -8
- package/dist/bin/atbash.js +6 -3
- package/dist/bin/atbash.js.map +1 -1
- package/dist/commands/config-cmd.js +15 -15
- package/dist/commands/config-cmd.js.map +1 -1
- package/dist/commands/connect.d.ts +84 -24
- package/dist/commands/connect.js +289 -73
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/held.js +2 -2
- package/dist/commands/held.js.map +1 -1
- package/dist/commands/history.js +1 -1
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/judge.js +2 -2
- package/dist/commands/judge.js.map +1 -1
- package/dist/commands/mcp-cmd.d.ts +13 -0
- package/dist/commands/mcp-cmd.js +216 -0
- package/dist/commands/mcp-cmd.js.map +1 -0
- package/dist/commands/policy.js +1 -1
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/setup.d.ts +390 -8
- package/dist/commands/setup.js +1873 -164
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/stats.js +1 -1
- package/dist/commands/stats.js.map +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/tier.js +1 -1
- package/dist/commands/tier.js.map +1 -1
- package/dist/commands/tools.js +3 -3
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/shared/atbash-targets.d.ts +49 -0
- package/dist/shared/atbash-targets.js +63 -0
- package/dist/shared/atbash-targets.js.map +1 -0
- package/dist/shared/openclaw-runtime.d.ts +221 -0
- package/dist/shared/openclaw-runtime.js +476 -0
- package/dist/shared/openclaw-runtime.js.map +1 -0
- package/dist/shared/win-exec.d.ts +63 -0
- package/dist/shared/win-exec.js +147 -0
- package/dist/shared/win-exec.js.map +1 -0
- package/package.json +3 -2
package/dist/commands/setup.d.ts
CHANGED
|
@@ -20,7 +20,27 @@ interface KeyMaterial {
|
|
|
20
20
|
export declare function parseKeyMaterial(raw: string): KeyMaterial | null;
|
|
21
21
|
/** Normalize to the lowercase 64-hex the SDK validates, or "" if it is not one. */
|
|
22
22
|
export declare function normalizePrivkey(raw: string): string;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Files in a directory that plausibly hold an Atbash agent key, newest first.
|
|
25
|
+
*
|
|
26
|
+
* TWO PASSES, and the second one is the point.
|
|
27
|
+
*
|
|
28
|
+
* By name first — `guard-client-key`, `agent-keys-*.txt` and friends — because
|
|
29
|
+
* matching the name is cheap and unambiguous. But a name-only match is a cliff:
|
|
30
|
+
* rename the download, or export from a wallet UI that picks its own filename,
|
|
31
|
+
* and the operator gets "no key file found" while the key sits right there in the
|
|
32
|
+
* directory they explicitly pointed at.
|
|
33
|
+
*
|
|
34
|
+
* So if no name matches, read the small files and keep the ones that actually
|
|
35
|
+
* PARSE as key material. That is a narrow test — `privkey=`, the documented JSON
|
|
36
|
+
* shape, or a file that is nothing but a 64-hex key — not "contains something
|
|
37
|
+
* hex-looking", so an unrelated file does not get mistaken for an identity.
|
|
38
|
+
*
|
|
39
|
+
* Reading files the operator did not name individually is justified by the flag
|
|
40
|
+
* itself: `--keys-dir` is an explicit instruction to look in that directory. It
|
|
41
|
+
* is bounded to small regular files and a file count, nothing is transmitted, and
|
|
42
|
+
* the caller prints WHICH file it used before doing anything with it.
|
|
43
|
+
*/
|
|
24
44
|
export declare function keyCandidatesInDir(dir: string): string[];
|
|
25
45
|
interface KeySource {
|
|
26
46
|
material: KeyMaterial;
|
|
@@ -58,12 +78,72 @@ export type Step = {
|
|
|
58
78
|
before: string | null;
|
|
59
79
|
after: string;
|
|
60
80
|
secret?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Id of an `exec` step this write DEPENDS ON. The write is skipped entirely
|
|
83
|
+
* when that step did not succeed.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ This exists because the order used to be the other way round, on a
|
|
86
|
+
* rationale that reads convincingly and is wrong: "writes first, then
|
|
87
|
+
* commands, so a failed install still leaves a correct config behind for a
|
|
88
|
+
* manual retry". A plugin config with no plugin is not a head start — it is
|
|
89
|
+
* a DANGLING REFERENCE, and OpenClaw treats it as one:
|
|
90
|
+
*
|
|
91
|
+
* plugins.entries.atbash-openclaw: plugin not found: atbash-openclaw
|
|
92
|
+
* (stale config entry ignored; remove it from plugins config)
|
|
93
|
+
* → Run "openclaw doctor --fix" to remove stale plugin ids
|
|
94
|
+
*
|
|
95
|
+
* So the leftover config is not merely useless: OpenClaw's own repair
|
|
96
|
+
* command offers to DELETE it, and an operator who accepts silently
|
|
97
|
+
* un-governs the agent. On 2026.2.x it was worse still — an entry naming an
|
|
98
|
+
* absent plugin contributed to the config being refused, which took down
|
|
99
|
+
* the very `plugins install` that would have created it.
|
|
100
|
+
*
|
|
101
|
+
* Not writing is strictly better than writing-then-rolling-back: there is
|
|
102
|
+
* no window in which a broken state exists on disk at all.
|
|
103
|
+
*/
|
|
104
|
+
requires?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Apply this write by handing the change to the runtime instead of writing
|
|
107
|
+
* the file ourselves.
|
|
108
|
+
*
|
|
109
|
+
* `payload` is piped to `command args` on stdin. Used for
|
|
110
|
+
* `openclaw config patch --stdin`, which merges recursively and validates
|
|
111
|
+
* in ONE write — so OpenClaw owns its own file format, key names, schema,
|
|
112
|
+
* comments, permissions and migrations. Every bug in this thread came from
|
|
113
|
+
* us asserting that shape from outside.
|
|
114
|
+
*
|
|
115
|
+
* It also closes a hazard we cannot otherwise handle: OpenClaw rewrites
|
|
116
|
+
* `openclaw.json` on its own (a machine in the field carried three
|
|
117
|
+
* `openclaw.json.clobbered.<timestamp>` files plus an
|
|
118
|
+
* `openclaw.json.last-good`), so anything we hand-merge can be discarded
|
|
119
|
+
* without warning. A patch describes INTENT and survives that.
|
|
120
|
+
*
|
|
121
|
+
* `before`/`after` are still computed so `--dry-run` shows a real diff —
|
|
122
|
+
* the preview stays ours, only the application moves.
|
|
123
|
+
*/
|
|
124
|
+
applyVia?: {
|
|
125
|
+
command: string;
|
|
126
|
+
args: string[];
|
|
127
|
+
payload: string;
|
|
128
|
+
/**
|
|
129
|
+
* Extra environment for the child.
|
|
130
|
+
*
|
|
131
|
+
* ⚠️ REQUIRED IN PRACTICE, not a nicety. `openclaw config patch` edits
|
|
132
|
+
* whatever config OpenClaw considers active, which is NOT necessarily
|
|
133
|
+
* `step.file` — so without pinning `OPENCLAW_CONFIG_PATH` to the file this
|
|
134
|
+
* step names, a `--home <fixture>` run would patch the operator's REAL
|
|
135
|
+
* `~/.openclaw/openclaw.json`. A step must only ever change the file it
|
|
136
|
+
* declares; that is what makes the plan an honest preview.
|
|
137
|
+
*/
|
|
138
|
+
env?: Record<string, string>;
|
|
139
|
+
};
|
|
61
140
|
} | {
|
|
62
141
|
kind: "exec";
|
|
63
142
|
label: string;
|
|
64
143
|
command: string;
|
|
65
144
|
args: string[];
|
|
66
145
|
optionalWhy?: string;
|
|
146
|
+
id?: string;
|
|
67
147
|
} | {
|
|
68
148
|
kind: "manual";
|
|
69
149
|
label: string;
|
|
@@ -92,15 +172,269 @@ export declare function isJsonc(text: string): boolean;
|
|
|
92
172
|
*
|
|
93
173
|
* A MERGE, not a replacement — that distinction is the whole reason this command
|
|
94
174
|
* exists. Other plugins already in `allow`, `load.paths` and `entries` are
|
|
95
|
-
* preserved
|
|
96
|
-
*
|
|
97
|
-
*
|
|
175
|
+
* preserved.
|
|
176
|
+
*
|
|
177
|
+
* WHICH entry is governed depends on what is being installed. When this run
|
|
178
|
+
* installs `@atbash/atbash-openclaw` — every run that is not `--no-install` —
|
|
179
|
+
* the plugin that ends up on disk registers as `atbash-openclaw`, so that is the
|
|
180
|
+
* entry that gets the config, and a legacy `atbash-plugin` entry is STOOD DOWN
|
|
181
|
+
* rather than configured. Only a `--no-install` run on a box whose sole entry is
|
|
182
|
+
* the legacy one keeps writing to the legacy key: there, nothing is arriving to
|
|
183
|
+
* replace it, and switching it off would leave the machine unguarded.
|
|
98
184
|
*
|
|
99
185
|
* `load.paths` gets the real absolute extension path. The published docs show a
|
|
100
186
|
* `<your-username>` placeholder that people paste verbatim, producing a path that
|
|
101
187
|
* does not exist and a plugin that never loads.
|
|
102
188
|
*/
|
|
103
|
-
|
|
189
|
+
/**
|
|
190
|
+
* WHICH entry this run governs, and whether a legacy one is being stood down.
|
|
191
|
+
*
|
|
192
|
+
* Extracted so the hand-merge and the `openclaw config patch` payload cannot
|
|
193
|
+
* disagree. Two implementations of this decision is exactly how a build ended up
|
|
194
|
+
* writing the modern plugin's `orgName` onto the LEGACY entry — whose schema is
|
|
195
|
+
* closed and has no such field — so OpenClaw rejected the whole file and the run
|
|
196
|
+
* broke its own install step.
|
|
197
|
+
*
|
|
198
|
+
* An entry is not the only way the legacy plugin is present: `plugins.installs`
|
|
199
|
+
* records it independently, and an installed, allowed plugin loads with its
|
|
200
|
+
* defaults whether or not anyone wrote an entry for it.
|
|
201
|
+
*/
|
|
202
|
+
export declare function openclawEntryDecision(config: Record<string, unknown>, opts?: {
|
|
203
|
+
installsModern?: boolean;
|
|
204
|
+
}): {
|
|
205
|
+
entryKey: string;
|
|
206
|
+
useLegacy: boolean;
|
|
207
|
+
legacy: boolean;
|
|
208
|
+
standDownLegacy: boolean;
|
|
209
|
+
};
|
|
210
|
+
export declare function mergeOpenclawConfig(config: Record<string, unknown>, home: string, orgName?: string, opts?: {
|
|
211
|
+
installsModern?: boolean;
|
|
212
|
+
hooksSupported?: boolean;
|
|
213
|
+
}): Record<string, unknown>;
|
|
214
|
+
export declare function openclawInstallState(home: string, spec: string): {
|
|
215
|
+
installed: boolean;
|
|
216
|
+
sameSpec: boolean;
|
|
217
|
+
version?: string;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Detect the indentation a JSON file already uses, so a merge does not reformat
|
|
221
|
+
* the parts it did not touch.
|
|
222
|
+
*
|
|
223
|
+
* Without this, `JSON.stringify(obj, null, 2)` re-indents a tab-indented or
|
|
224
|
+
* 4-space config from top to bottom. The RESULT is still correct, but the diff
|
|
225
|
+
* shown for approval becomes every line in the file, which buries the two lines
|
|
226
|
+
* that actually changed — and the operator's own formatting choice is collateral
|
|
227
|
+
* damage in a file we were asked to make one addition to.
|
|
228
|
+
*
|
|
229
|
+
* Falls back to two spaces, which is what the published docs show.
|
|
230
|
+
*/
|
|
231
|
+
export declare function detectIndent(text: string | null): string | number;
|
|
232
|
+
/**
|
|
233
|
+
* Serialize a merged config the way the file was already written: same
|
|
234
|
+
* indentation, and a trailing newline only if the original had one.
|
|
235
|
+
*/
|
|
236
|
+
export declare function serializeLike(original: string | null, value: unknown): string;
|
|
237
|
+
interface McpClient {
|
|
238
|
+
label: string;
|
|
239
|
+
file: string;
|
|
240
|
+
format: "json" | "toml";
|
|
241
|
+
/** Which key holds the server map — VS Code and some others use `servers`. */
|
|
242
|
+
serversKey: "mcpServers" | "servers";
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* MCP client configs present under this home directory.
|
|
246
|
+
*
|
|
247
|
+
* Paths come from the shared MCP_CONFIGS so the writer and the scanner cannot
|
|
248
|
+
* drift: a client the scan reports but setup cannot find would look like a bug in
|
|
249
|
+
* whichever of the two the operator happened to trust.
|
|
250
|
+
*/
|
|
251
|
+
export declare function detectMcpClients(home: string): McpClient[];
|
|
252
|
+
/**
|
|
253
|
+
* Find the Python interpreter that actually runs Hermes.
|
|
254
|
+
*
|
|
255
|
+
* This is the difference between installing the plugin and only appearing to.
|
|
256
|
+
* `pip install atbash-hermes-plugin` puts the package wherever the *shell's*
|
|
257
|
+
* `pip` points — commonly a system or conda Python — while Hermes typically runs
|
|
258
|
+
* from its own virtualenv. The install succeeds, prints nothing alarming, and the
|
|
259
|
+
* plugin is invisible to Hermes forever. Nobody can debug that from the output.
|
|
260
|
+
*
|
|
261
|
+
* The launcher knows the answer. A pip-installed console script begins with a
|
|
262
|
+
* shebang naming the interpreter that created it:
|
|
263
|
+
*
|
|
264
|
+
* $ head -1 $(command -v hermes)
|
|
265
|
+
* #!/Users/me/.hermes/hermes-agent/venv/bin/python3
|
|
266
|
+
*
|
|
267
|
+
* So resolve `hermes`, read its first line, and use that interpreter directly via
|
|
268
|
+
* `-m pip`. Falls back to the conventional venv location under ~/.hermes, then to
|
|
269
|
+
* null — and a null becomes a printed command rather than a guess, because a
|
|
270
|
+
* wrong guess here is the silent failure this whole function exists to avoid.
|
|
271
|
+
*/
|
|
272
|
+
export declare function findHermesPython(home: string): {
|
|
273
|
+
python: string;
|
|
274
|
+
how: string;
|
|
275
|
+
} | null;
|
|
276
|
+
/** Same rule as buildPlan's `wanted`: an empty --runtime list means everything. */
|
|
277
|
+
export declare function wantedRuntime(id: string, only: string[]): boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Add the plugin to Hermes' opt-in allow-list at `plugins.enabled` in
|
|
280
|
+
* `~/.hermes/config.yaml`.
|
|
281
|
+
*
|
|
282
|
+
* WHY NOT `hermes plugins enable`: that command cannot accept this plugin, on
|
|
283
|
+
* this version, ever. `_plugin_exists` (hermes_cli/plugins_cmd.py) looks only for
|
|
284
|
+
* a DIRECTORY in the user plugins dir or a bundled dir — it never consults entry
|
|
285
|
+
* points. Meanwhile the runtime loader (`plugins.py:_scan_entry_points`) does
|
|
286
|
+
* discover them. So Hermes will happily LOAD a pip-installed plugin but refuses
|
|
287
|
+
* to put one on the allow-list it requires, and since plugins are opt-in, a
|
|
288
|
+
* package that cannot get onto the list can never load. Running that command
|
|
289
|
+
* exits 1 with "not installed or bundled". Writing the list entry directly is the
|
|
290
|
+
* only route that works.
|
|
291
|
+
*
|
|
292
|
+
* A LINE MERGE, not a parse-and-reserialize. config.yaml is tens of kilobytes of
|
|
293
|
+
* heavily commented configuration; round-tripping it through a YAML emitter would
|
|
294
|
+
* strip every comment and reflow the file. So this inserts the one line needed and
|
|
295
|
+
* leaves every other byte alone — the same discipline as the .env merge.
|
|
296
|
+
*/
|
|
297
|
+
export declare function mergeHermesEnabledPlugins(existing: string | null, plugin?: string): string;
|
|
298
|
+
/**
|
|
299
|
+
* Whether Hermes has actually picked the plugin up.
|
|
300
|
+
*
|
|
301
|
+
* Installing the package is NOT the same as governing the agent. Hermes requires
|
|
302
|
+
* plugins to be enabled explicitly — a real run installed `atbash-hermes-plugin`
|
|
303
|
+
* into the right interpreter, with the right `hermes_agent.plugins` entry point,
|
|
304
|
+
* and `hermes plugins list` still showed only the bundled plugins, all "not
|
|
305
|
+
* enabled". The machine looked wired and enforced nothing, which is the exact
|
|
306
|
+
* failure this command exists to prevent.
|
|
307
|
+
*
|
|
308
|
+
* Parsing a rendered table is inherently fragile, so an unreadable result is
|
|
309
|
+
* reported as "unknown" and treated as "probably needs enabling" rather than as
|
|
310
|
+
* success. Claiming enforcement we have not observed is the one wrong answer.
|
|
311
|
+
*/
|
|
312
|
+
export type HermesPluginState = "configured" | "not-enabled" | "not-installed" | "unknown";
|
|
313
|
+
/**
|
|
314
|
+
* Is a Hermes gateway service running, and therefore restartable?
|
|
315
|
+
*
|
|
316
|
+
* "Restart Hermes" only means something when there is a service to bounce.
|
|
317
|
+
* Hermes has two shapes: `hermes` starts an interactive chat session, and
|
|
318
|
+
* `hermes gateway install` registers a launchd/systemd background service. A
|
|
319
|
+
* setup command must not conflate them —
|
|
320
|
+
*
|
|
321
|
+
* - with a service running, `hermes gateway restart` picks up the new config and
|
|
322
|
+
* is worth doing for the operator;
|
|
323
|
+
* - without one, there is nothing to restart. The plugin loads the next time
|
|
324
|
+
* they run `hermes`, and anything we "restarted" would either be a no-op or,
|
|
325
|
+
* worse, an interactive session someone is in the middle of using.
|
|
326
|
+
*
|
|
327
|
+
* `gateway status` exits 0 either way, so the state comes from its text.
|
|
328
|
+
*/
|
|
329
|
+
export declare function hermesGatewayRunning(): boolean;
|
|
330
|
+
/**
|
|
331
|
+
* Is Hermes CONFIGURED to load the plugin?
|
|
332
|
+
*
|
|
333
|
+
* This used to shell out to `hermes plugins list` and look for an atbash row —
|
|
334
|
+
* a signal that can never be true. `_discover_all_plugins` (plugins_cmd.py) walks
|
|
335
|
+
* plugin DIRECTORIES only: bundled, user, project. It never scans entry points,
|
|
336
|
+
* exactly like the `_plugin_exists` gate behind `plugins enable`. So a
|
|
337
|
+
* pip-installed plugin is invisible to both, and the check reported "not picked
|
|
338
|
+
* up" while everything was in fact correct — then told the operator to run the
|
|
339
|
+
* enable command that cannot work. Advising a known-impossible fix is worse than
|
|
340
|
+
* saying nothing.
|
|
341
|
+
*
|
|
342
|
+
* The two facts that actually decide it are both on disk:
|
|
343
|
+
*
|
|
344
|
+
* 1. the package is importable by the interpreter that runs Hermes, and
|
|
345
|
+
* 2. its name is on the `plugins.enabled` allow-list in config.yaml, which is
|
|
346
|
+
* what the RUNTIME loader (`plugins.py:_scan_entry_points`) honours.
|
|
347
|
+
*
|
|
348
|
+
* "configured" is as far as a setup command can honestly go. Proof of loading is
|
|
349
|
+
* a line in the agent log after Hermes next starts, which is why the caller points
|
|
350
|
+
* at that rather than claiming enforcement.
|
|
351
|
+
*/
|
|
352
|
+
export declare function hermesPluginState(home: string): HermesPluginState;
|
|
353
|
+
/**
|
|
354
|
+
* How to install a Python package into a specific interpreter on THIS machine.
|
|
355
|
+
*
|
|
356
|
+
* `<python> -m pip install` is the obvious answer and it is frequently wrong: a
|
|
357
|
+
* venv created by `uv venv` has no pip at all (that is uv's default), so the
|
|
358
|
+
* command fails with
|
|
359
|
+
*
|
|
360
|
+
* /path/venv/bin/python3: No module named pip
|
|
361
|
+
*
|
|
362
|
+
* after setup has already written every config file — which is exactly what
|
|
363
|
+
* happened on a real Hermes box. So probe, in order of what suits the venv:
|
|
364
|
+
*
|
|
365
|
+
* 1. `uv pip install --python <python>` when uv is present. Correct for a
|
|
366
|
+
* uv-created venv and fast; uv is also what created most pip-less venvs.
|
|
367
|
+
* 2. `<python> -m pip install` when pip actually answers.
|
|
368
|
+
* 3. Neither — hand it over, with `ensurepip` named, rather than planning a
|
|
369
|
+
* command that is known in advance to fail.
|
|
370
|
+
*/
|
|
371
|
+
export declare function pythonInstallStrategy(python: string, pkg: string): {
|
|
372
|
+
kind: "exec";
|
|
373
|
+
command: string;
|
|
374
|
+
args: string[];
|
|
375
|
+
how: string;
|
|
376
|
+
} | {
|
|
377
|
+
kind: "manual";
|
|
378
|
+
why: string;
|
|
379
|
+
snippet: string;
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* The env vars the Hermes plugin documents, merged into an existing `.env`.
|
|
383
|
+
*
|
|
384
|
+
* A `.env` is line-oriented and hand-maintained, so this is a line merge rather
|
|
385
|
+
* than a parse-and-reserialize: keys Atbash owns are replaced in place (keeping
|
|
386
|
+
* their position), keys it does not own are never touched, and anything else in
|
|
387
|
+
* the file — comments, blank lines, unrelated settings, ordering — survives
|
|
388
|
+
* exactly as written. Reformatting someone's .env to add four lines would be a
|
|
389
|
+
* poor trade.
|
|
390
|
+
*
|
|
391
|
+
* Values are from the published plugin README (PyPI atbash-hermes-plugin 0.4.5).
|
|
392
|
+
* `ATBASH_ORG_NAME` is deliberately NOT written: its value is the operator's org,
|
|
393
|
+
* which this command has no reliable way to know, and a wrong org sends the SDK
|
|
394
|
+
* at the wrong chain. It is called out in the manual step instead.
|
|
395
|
+
*/
|
|
396
|
+
export declare function mergeHermesEnv(existing: string | null): string;
|
|
397
|
+
/**
|
|
398
|
+
* Does this config's existing Atbash entry carry a key in its `env` block?
|
|
399
|
+
*
|
|
400
|
+
* True means the operator hand-wired it from the published docs and their private
|
|
401
|
+
* key is sitting in that file today. Setup takes it out, but the backup it writes
|
|
402
|
+
* first still has it — so this exists to make that sayable rather than silently
|
|
403
|
+
* relocating the leak.
|
|
404
|
+
*/
|
|
405
|
+
export declare function hadInlineKey(config: Record<string, unknown>, serversKey?: "mcpServers" | "servers"): boolean;
|
|
406
|
+
export declare function mergeMcpServer(config: Record<string, unknown>, serversKey?: "mcpServers" | "servers"): Record<string, unknown>;
|
|
407
|
+
/**
|
|
408
|
+
* The change we want, expressed as a PATCH rather than a whole file.
|
|
409
|
+
*
|
|
410
|
+
* `openclaw config patch` merges objects recursively and validates in one write,
|
|
411
|
+
* so this is intent — "this entry should exist and look like this" — instead of
|
|
412
|
+
* "here are the complete new bytes of your config". That difference is what makes
|
|
413
|
+
* it survive the things that kept defeating us from outside: OpenClaw's own
|
|
414
|
+
* format changes, key renames, comment preservation, file permissions, its
|
|
415
|
+
* migrations, and the fact that it rewrites `openclaw.json` on its own (a machine
|
|
416
|
+
* in the field carried three `openclaw.json.clobbered.<timestamp>` files).
|
|
417
|
+
*
|
|
418
|
+
* ⚠️ DO NOT call `config patch --dry-run` from buildPlan to preview this. It is
|
|
419
|
+
* NOT side-effect free: running it triggers OpenClaw's state migrations, which
|
|
420
|
+
* wrote `~/.openclaw/update-check.json.migrated` and moved config-health state
|
|
421
|
+
* into SQLite on the machine this was developed on. `atbash setup --dry-run`
|
|
422
|
+
* promises that nothing is written, and that promise has to hold for OpenClaw's
|
|
423
|
+
* housekeeping too. The preview stays a merge we compute ourselves.
|
|
424
|
+
*/
|
|
425
|
+
export declare function openclawPatchPayload(args: {
|
|
426
|
+
entryKey: string;
|
|
427
|
+
orgName?: string;
|
|
428
|
+
keyPath: string;
|
|
429
|
+
/** Switch the legacy entry off in the same patch, when one is live. */
|
|
430
|
+
standDownLegacy: boolean;
|
|
431
|
+
/**
|
|
432
|
+
* Whether this build accepts `hooks` on an entry. Only then is the
|
|
433
|
+
* prompt-mutation denial included — on a build that does not know the key it
|
|
434
|
+
* is fatal, and `config patch` would rightly refuse the whole patch.
|
|
435
|
+
*/
|
|
436
|
+
hooksSupported?: boolean;
|
|
437
|
+
}): string;
|
|
104
438
|
/**
|
|
105
439
|
* Work out everything that needs doing on this machine, without doing any of it.
|
|
106
440
|
*
|
|
@@ -113,10 +447,27 @@ export declare function buildPlan(args: {
|
|
|
113
447
|
home: string;
|
|
114
448
|
privkey: string;
|
|
115
449
|
pubkey: string;
|
|
450
|
+
/** Written into the OpenClaw config; the plugin resolves its chain from it. */
|
|
451
|
+
orgName?: string;
|
|
452
|
+
/** The deployment this agent was onboarded on — selects the plugin's npm tag. */
|
|
453
|
+
endpoint?: string;
|
|
116
454
|
/** Skip package installation; write config and the key file only. */
|
|
117
455
|
noInstall: boolean;
|
|
118
456
|
/** Restrict to these runtime ids; empty means "everything detected". */
|
|
119
457
|
only: string[];
|
|
458
|
+
/**
|
|
459
|
+
* What the OpenClaw on this machine can do, probed by detectOpenclaw.
|
|
460
|
+
*
|
|
461
|
+
* Optional so tests and callers that only want the file-level merge need not
|
|
462
|
+
* shell out. Absent means "assume nothing extra" — the hand-merge path, which
|
|
463
|
+
* works on every version — rather than assuming the newest capabilities.
|
|
464
|
+
*/
|
|
465
|
+
openclaw?: {
|
|
466
|
+
caps: {
|
|
467
|
+
patch: boolean;
|
|
468
|
+
};
|
|
469
|
+
hooksSupported?: boolean;
|
|
470
|
+
};
|
|
120
471
|
}): Plan;
|
|
121
472
|
/**
|
|
122
473
|
* A minimal line diff, so the preview shows what CHANGES rather than dumping a
|
|
@@ -143,9 +494,40 @@ export interface ApplyResult {
|
|
|
143
494
|
backups: string[];
|
|
144
495
|
ran: string[];
|
|
145
496
|
failures: string[];
|
|
497
|
+
/** Writes deliberately NOT made because a step they depend on failed. */
|
|
498
|
+
skipped: string[];
|
|
499
|
+
/** Files restored from backup because the result did not validate. */
|
|
500
|
+
rolledBack: string[];
|
|
146
501
|
}
|
|
147
|
-
/**
|
|
148
|
-
*
|
|
149
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Execute the plan, in the order that cannot leave a half-wired machine.
|
|
504
|
+
*
|
|
505
|
+
* 1. Independent writes — the key file above all. Safe on their own, useful
|
|
506
|
+
* even if everything after fails, and required by the plugin at load.
|
|
507
|
+
* 2. Commands — the plugin install, so the plugin id EXISTS on disk.
|
|
508
|
+
* 3. Dependent writes — the plugin config, now that it refers to something
|
|
509
|
+
* real. Skipped outright if its install failed (see Step.requires).
|
|
510
|
+
* 4. Verify, and roll back anything we broke (see verify).
|
|
511
|
+
*
|
|
512
|
+
* ⚠️ THE ORDER IS THE FIX, and it is the reverse of what this function used to
|
|
513
|
+
* do. "Writes first, then commands, so a failed install still leaves a correct
|
|
514
|
+
* config behind" sounds prudent and produces the single worst outcome available:
|
|
515
|
+
* a config entry for a plugin that is not installed, which OpenClaw reports as a
|
|
516
|
+
* stale reference and offers to delete via `doctor --fix` — silently un-governing
|
|
517
|
+
* the agent — and which on 2026.2.x helped make the config unloadable, killing
|
|
518
|
+
* the install that would have fixed it.
|
|
519
|
+
*/
|
|
520
|
+
export declare function applyPlan(plan: Plan, opts?: {
|
|
521
|
+
/**
|
|
522
|
+
* Ask the runtime to confirm the result after writing, and restore the
|
|
523
|
+
* backup if the runtime says WE broke it. Omitted (or a build without
|
|
524
|
+
* `validate`) means no verification — which must read as "unverified", never
|
|
525
|
+
* as "verified fine".
|
|
526
|
+
*/
|
|
527
|
+
verify?: (file: string) => {
|
|
528
|
+
valid: boolean;
|
|
529
|
+
problems: string;
|
|
530
|
+
} | undefined;
|
|
531
|
+
}): ApplyResult;
|
|
150
532
|
export declare function registerSetupCommand(program: Command): void;
|
|
151
533
|
export {};
|