@atbash/cli 0.5.15-dev.1 → 0.5.15-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ import { Command } from "commander";
2
+ interface KeyMaterial {
3
+ privkey: string;
4
+ /** Present when the source stated one; always re-derived and cross-checked. */
5
+ statedPubkey?: string;
6
+ }
7
+ /**
8
+ * Pull key material out of whatever the owner pointed us at.
9
+ *
10
+ * Accepts every shape Atbash itself produces or documents, so "the file I
11
+ * downloaded from the modal" always works:
12
+ * - `privkey=…` / `pubkey=…` lines (what onboarding downloads, and what the
13
+ * plugin parses)
14
+ * - `{"privKey":…,"pubKey":…}` JSON (the alternate documented key-file form)
15
+ * - a bare 64-hex private key on its own line (someone who copied just the key)
16
+ *
17
+ * Returns null rather than throwing: the caller tries several sources in turn and
18
+ * an unparseable one is a reason to move on, not to abort.
19
+ */
20
+ export declare function parseKeyMaterial(raw: string): KeyMaterial | null;
21
+ /** Normalize to the lowercase 64-hex the SDK validates, or "" if it is not one. */
22
+ export declare function normalizePrivkey(raw: string): string;
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
+ */
44
+ export declare function keyCandidatesInDir(dir: string): string[];
45
+ interface KeySource {
46
+ material: KeyMaterial;
47
+ /** Human description of WHERE it came from. Never contains the key. */
48
+ from: string;
49
+ }
50
+ /**
51
+ * Find the agent key, trying every way an owner could plausibly have it.
52
+ *
53
+ * Order is "most explicit first": a flag the owner typed beats a file we guessed
54
+ * at. The last resort is the interactive prompt, and if there is no TTY the
55
+ * caller gets a clear error listing the flags rather than a hang.
56
+ */
57
+ export declare function resolveKeySource(opts: {
58
+ key?: string;
59
+ keyFile?: string;
60
+ keysDir?: string;
61
+ home: string;
62
+ /** Set for non-interactive runs (CI, an AI assistant with no TTY). */
63
+ allowPrompt: boolean;
64
+ }): Promise<KeySource | {
65
+ error: string;
66
+ }>;
67
+ /**
68
+ * One thing setup will do. Building the whole plan BEFORE touching anything is
69
+ * what makes `--dry-run` truthful: the preview and the run are the same objects,
70
+ * so the preview cannot describe a merge the apply step then performs
71
+ * differently. Every `write` carries its exact final bytes.
72
+ */
73
+ export type Step = {
74
+ kind: "write";
75
+ label: string;
76
+ file: string;
77
+ mode?: number;
78
+ before: string | null;
79
+ after: string;
80
+ secret?: boolean;
81
+ } | {
82
+ kind: "exec";
83
+ label: string;
84
+ command: string;
85
+ args: string[];
86
+ optionalWhy?: string;
87
+ } | {
88
+ kind: "manual";
89
+ label: string;
90
+ detail: string;
91
+ snippet?: string;
92
+ };
93
+ export interface Plan {
94
+ steps: Step[];
95
+ /** Things the owner must know, printed whether or not anything was written. */
96
+ notes: string[];
97
+ /** Runtimes found on this machine, for the summary line. */
98
+ found: string[];
99
+ }
100
+ /** The key file, in the `key=value` form the plugin and the SDK both parse. */
101
+ export declare function keyFileContents(privkey: string, pubkey: string): string;
102
+ /**
103
+ * True when a JSON file uses JSONC features (comments, trailing commas).
104
+ *
105
+ * We must not auto-merge into one: writing it back with JSON.stringify would
106
+ * silently delete the owner's comments. Detected by the disagreement between the
107
+ * strict and tolerant parsers — strict fails, tolerant succeeds.
108
+ */
109
+ export declare function isJsonc(text: string): boolean;
110
+ /**
111
+ * Merge the Atbash plugin block into an OpenClaw config object, in place.
112
+ *
113
+ * A MERGE, not a replacement — that distinction is the whole reason this command
114
+ * exists. Other plugins already in `allow`, `load.paths` and `entries` are
115
+ * preserved, and an entry from the legacy `@atbash/atbash-plugin` install is
116
+ * updated where it stands rather than being shadowed by a duplicate: the
117
+ * dashboard scan recognizes both keys, so two entries would mean two hooks.
118
+ *
119
+ * `load.paths` gets the real absolute extension path. The published docs show a
120
+ * `<your-username>` placeholder that people paste verbatim, producing a path that
121
+ * does not exist and a plugin that never loads.
122
+ */
123
+ export declare function mergeOpenclawConfig(config: Record<string, unknown>, home: string): Record<string, unknown>;
124
+ /**
125
+ * Detect the indentation a JSON file already uses, so a merge does not reformat
126
+ * the parts it did not touch.
127
+ *
128
+ * Without this, `JSON.stringify(obj, null, 2)` re-indents a tab-indented or
129
+ * 4-space config from top to bottom. The RESULT is still correct, but the diff
130
+ * shown for approval becomes every line in the file, which buries the two lines
131
+ * that actually changed — and the operator's own formatting choice is collateral
132
+ * damage in a file we were asked to make one addition to.
133
+ *
134
+ * Falls back to two spaces, which is what the published docs show.
135
+ */
136
+ export declare function detectIndent(text: string | null): string | number;
137
+ /**
138
+ * Serialize a merged config the way the file was already written: same
139
+ * indentation, and a trailing newline only if the original had one.
140
+ */
141
+ export declare function serializeLike(original: string | null, value: unknown): string;
142
+ interface McpClient {
143
+ label: string;
144
+ file: string;
145
+ format: "json" | "toml";
146
+ /** Which key holds the server map — VS Code and some others use `servers`. */
147
+ serversKey: "mcpServers" | "servers";
148
+ }
149
+ /**
150
+ * MCP client configs present under this home directory.
151
+ *
152
+ * Paths come from the shared MCP_CONFIGS so the writer and the scanner cannot
153
+ * drift: a client the scan reports but setup cannot find would look like a bug in
154
+ * whichever of the two the operator happened to trust.
155
+ */
156
+ export declare function detectMcpClients(home: string): McpClient[];
157
+ /**
158
+ * Find the Python interpreter that actually runs Hermes.
159
+ *
160
+ * This is the difference between installing the plugin and only appearing to.
161
+ * `pip install atbash-hermes-plugin` puts the package wherever the *shell's*
162
+ * `pip` points — commonly a system or conda Python — while Hermes typically runs
163
+ * from its own virtualenv. The install succeeds, prints nothing alarming, and the
164
+ * plugin is invisible to Hermes forever. Nobody can debug that from the output.
165
+ *
166
+ * The launcher knows the answer. A pip-installed console script begins with a
167
+ * shebang naming the interpreter that created it:
168
+ *
169
+ * $ head -1 $(command -v hermes)
170
+ * #!/Users/me/.hermes/hermes-agent/venv/bin/python3
171
+ *
172
+ * So resolve `hermes`, read its first line, and use that interpreter directly via
173
+ * `-m pip`. Falls back to the conventional venv location under ~/.hermes, then to
174
+ * null — and a null becomes a printed command rather than a guess, because a
175
+ * wrong guess here is the silent failure this whole function exists to avoid.
176
+ */
177
+ export declare function findHermesPython(home: string): {
178
+ python: string;
179
+ how: string;
180
+ } | null;
181
+ /**
182
+ * The env vars the Hermes plugin documents, merged into an existing `.env`.
183
+ *
184
+ * A `.env` is line-oriented and hand-maintained, so this is a line merge rather
185
+ * than a parse-and-reserialize: keys Atbash owns are replaced in place (keeping
186
+ * their position), keys it does not own are never touched, and anything else in
187
+ * the file — comments, blank lines, unrelated settings, ordering — survives
188
+ * exactly as written. Reformatting someone's .env to add four lines would be a
189
+ * poor trade.
190
+ *
191
+ * Values are from the published plugin README (PyPI atbash-hermes-plugin 0.4.5).
192
+ * `ATBASH_ORG_NAME` is deliberately NOT written: its value is the operator's org,
193
+ * which this command has no reliable way to know, and a wrong org sends the SDK
194
+ * at the wrong chain. It is called out in the manual step instead.
195
+ */
196
+ export declare function mergeHermesEnv(existing: string | null): string;
197
+ /**
198
+ * Does this config's existing Atbash entry carry a key in its `env` block?
199
+ *
200
+ * True means the operator hand-wired it from the published docs and their private
201
+ * key is sitting in that file today. Setup takes it out, but the backup it writes
202
+ * first still has it — so this exists to make that sayable rather than silently
203
+ * relocating the leak.
204
+ */
205
+ export declare function hadInlineKey(config: Record<string, unknown>, serversKey?: "mcpServers" | "servers"): boolean;
206
+ export declare function mergeMcpServer(config: Record<string, unknown>, serversKey?: "mcpServers" | "servers"): Record<string, unknown>;
207
+ /**
208
+ * Work out everything that needs doing on this machine, without doing any of it.
209
+ *
210
+ * Detection drives the plan rather than a flag the owner picks, for the same
211
+ * reason the scan does: what is actually installed here is knowable, and asking
212
+ * someone to identify their own runtime from a list invites a wrong answer that
213
+ * writes a config for a plugin they do not have.
214
+ */
215
+ export declare function buildPlan(args: {
216
+ home: string;
217
+ privkey: string;
218
+ pubkey: string;
219
+ /** Skip package installation; write config and the key file only. */
220
+ noInstall: boolean;
221
+ /** Restrict to these runtime ids; empty means "everything detected". */
222
+ only: string[];
223
+ }): Plan;
224
+ /**
225
+ * A minimal line diff, so the preview shows what CHANGES rather than dumping a
226
+ * whole config and leaving the owner to spot the difference. Standard LCS; these
227
+ * files are small enough that the quadratic table is irrelevant.
228
+ */
229
+ export declare function lineDiff(before: string, after: string): string[];
230
+ /**
231
+ * Print the plan. Used for `--dry-run` and for the confirmation prompt, so what
232
+ * the owner is shown and what they agree to cannot diverge.
233
+ *
234
+ * The key file's CONTENTS are never printed — the whole point of the file is that
235
+ * the private key stays put, and echoing it into a terminal scrollback undoes
236
+ * that. The path, mode and the public key are shown instead.
237
+ */
238
+ export declare function renderPlan(plan: Plan, pubkey: string): void;
239
+ /**
240
+ * Copy a file aside before overwriting it, without ever clobbering an existing
241
+ * backup — a second run must not overwrite the pristine copy from the first.
242
+ */
243
+ export declare function backupFile(file: string): string | null;
244
+ export interface ApplyResult {
245
+ written: string[];
246
+ backups: string[];
247
+ ran: string[];
248
+ failures: string[];
249
+ }
250
+ /** Execute the plan. Writes first, then commands, so a failed install still
251
+ * leaves a correct config and key file behind for a manual retry. */
252
+ export declare function applyPlan(plan: Plan): ApplyResult;
253
+ export declare function registerSetupCommand(program: Command): void;
254
+ export {};